diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml
index f93eb0f4..c70d1f60 100644
--- a/.github/workflows/pr-preview.yml
+++ b/.github/workflows/pr-preview.yml
@@ -109,10 +109,20 @@ jobs:
echo "Dockerfile unchanged, will reuse main branch image"
fi
+ - name: Clean Tailscale cache
+ run: rm -f ~/.cache/tailscale.tgz
+
+ - name: Stop any running Tailscale
+ run: |
+ sudo pkill -f tailscaled 2>/dev/null || true
+ sudo rm -f /usr/local/bin/tailscale /usr/local/bin/tailscaled 2>/dev/null || true
+ sleep 2
+
- name: Connect to Tailscale
uses: tailscale/github-action@v4
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
+ use-cache: false
- name: Extract PR number
id: pr
@@ -298,10 +308,20 @@ jobs:
contents: read
steps:
+ - name: Clean Tailscale cache
+ run: rm -f ~/.cache/tailscale.tgz
+
+ - name: Stop any running Tailscale
+ run: |
+ sudo pkill -f tailscaled 2>/dev/null || true
+ sudo rm -f /usr/local/bin/tailscale /usr/local/bin/tailscaled 2>/dev/null || true
+ sleep 2
+
- name: Connect to Tailscale
uses: tailscale/github-action@v4
with:
authkey: ${{ secrets.TAILSCALE_AUTHKEY }}
+ use-cache: false
- name: Extract PR number
id: pr
diff --git a/Dockerfile b/Dockerfile
index 3a52ecfb..a922c824 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,5 @@
# Multi-stage build for production deployment
+# Trigger rebuild: 2026-04-23
# Builder stage
FROM node:22-slim AS builder
diff --git a/src/components/preview/PreviewPanel.tsx b/src/components/preview/PreviewPanel.tsx
index 3d48cf24..5dc7fda6 100644
--- a/src/components/preview/PreviewPanel.tsx
+++ b/src/components/preview/PreviewPanel.tsx
@@ -70,6 +70,8 @@ interface PreviewPanelProps {
userMessageCount: number,
isStreaming: boolean,
) => void;
+ /** Callback to send a "Fix with Doce" message to the chat */
+ onFixWithDoce?: (errorMessage: string) => void;
}
type PreviewState =
@@ -92,6 +94,7 @@ export function PreviewPanel({
models = [],
onOpenFile,
onStreamingStateChange,
+ onFixWithDoce,
}: PreviewPanelProps) {
const isMobile = useIsMobile();
const { baseUrl } = useBaseUrlSetting();
@@ -673,9 +676,19 @@ export function PreviewPanel({
{message || "Check the terminal for details."}
-
+
+
+ {onFixWithDoce && message && (
+
+ )}
+
) : null}
diff --git a/src/components/projects/ProjectContentWrapper.tsx b/src/components/projects/ProjectContentWrapper.tsx
index 4dade1a9..25dfbb61 100644
--- a/src/components/projects/ProjectContentWrapper.tsx
+++ b/src/components/projects/ProjectContentWrapper.tsx
@@ -6,6 +6,7 @@ import { ErrorBoundary } from "@/components/error/ErrorBoundary";
import { PreviewPanel } from "@/components/preview/PreviewPanel";
import { ResizableSeparator } from "@/components/preview/ResizableSeparator";
import { ContainerStartupDisplay } from "@/components/setup/ContainerStartupDisplay";
+import { useChatPanel } from "@/hooks/useChatPanel";
import { useLiveState } from "@/hooks/useLiveState";
import { useResizablePanel } from "@/hooks/useResizablePanel";
import { useChatLayout } from "@/stores/useChatLayout";
@@ -57,6 +58,21 @@ export function ProjectContentWrapper({
(s) => s.pendingByProjectId.get(projectId) ?? null,
);
+ // Get chat send function for "Fix with Doce" feature
+ const { handleSend } = useChatPanel({
+ projectId,
+ models,
+ onStreamingStateChange: (count, streaming) => {
+ setUserMessageCount(count);
+ setIsStreaming(streaming);
+ },
+ });
+
+ const handleFixWithDoce = (errorMessage: string) => {
+ const prompt = `The preview server failed to start with this error:\n\n${errorMessage}\n\nPlease fix this issue.`;
+ void handleSend(prompt);
+ };
+
useEffect(() => {
if (!showStartupDisplay) return;
if (
@@ -115,10 +131,7 @@ export function ProjectContentWrapper({
isStreaming={isStreaming}
models={models}
onOpenFile={setFileToOpen}
- onStreamingStateChange={(count, streaming) => {
- setUserMessageCount(count);
- setIsStreaming(streaming);
- }}
+ onFixWithDoce={handleFixWithDoce}
/>
) : (
@@ -151,10 +164,7 @@ export function ProjectContentWrapper({
projectId={projectId}
models={models}
onOpenFile={setFileToOpen}
- onStreamingStateChange={(count, streaming) => {
- setUserMessageCount(count);
- setIsStreaming(streaming);
- }}
+ hideDetachToggle={false}
/>
@@ -194,6 +204,7 @@ export function ProjectContentWrapper({
onFileOpened={() => setFileToOpen(null)}
userMessageCount={userMessageCount}
isStreaming={isStreaming}
+ onFixWithDoce={handleFixWithDoce}
/>
@@ -203,10 +214,6 @@ export function ProjectContentWrapper({
projectId={projectId}
models={models}
onOpenFile={setFileToOpen}
- onStreamingStateChange={(count, streaming) => {
- setUserMessageCount(count);
- setIsStreaming(streaming);
- }}
/>
>
)}
diff --git a/src/server/opencode/apiKeyValidation.ts b/src/server/opencode/apiKeyValidation.ts
index dfe53181..9358046a 100644
--- a/src/server/opencode/apiKeyValidation.ts
+++ b/src/server/opencode/apiKeyValidation.ts
@@ -67,14 +67,6 @@ function validateOpenAICompatibleKeyEffect(baseUrl: string) {
apiKey: string,
): Effect.Effect =>
Effect.gen(function* () {
- if (!apiKey.startsWith("sk-") && !apiKey.startsWith("api-")) {
- return {
- valid: false,
- error:
- "Invalid API key format. Keys typically start with 'sk-' or 'api-'.",
- };
- }
-
const response = yield* Effect.tryPromise({
try: () =>
fetch(`${baseUrl}/models`, {
@@ -197,14 +189,6 @@ function validateAnthropicKeyEffect(
apiKey: string,
): Effect.Effect {
return Effect.gen(function* () {
- if (!apiKey.startsWith("sk-ant-")) {
- return {
- valid: false,
- error:
- "Invalid Anthropic API key format. Keys should start with 'sk-ant-'.",
- };
- }
-
const response = yield* Effect.tryPromise({
try: () =>
fetch("https://api.anthropic.com/v1/models", {
@@ -276,13 +260,6 @@ function validateOpenAIKeyEffect(
apiKey: string,
): Effect.Effect {
return Effect.gen(function* () {
- if (!apiKey.startsWith("sk-")) {
- return {
- valid: false,
- error: "Invalid OpenAI API key format. Keys should start with 'sk-'.",
- };
- }
-
const response = yield* Effect.tryPromise({
try: () =>
fetch("https://api.openai.com/v1/models", {
@@ -350,11 +327,5 @@ export async function validateOpenAIKey(
function validateOpencodeKeyEffect(
apiKey: string,
): Effect.Effect {
- if (!apiKey.startsWith("sk-")) {
- return Effect.succeed({
- valid: false,
- error: "Invalid OpenCode API key format. Keys should start with 'sk-'.",
- });
- }
return Effect.succeed({ valid: true });
}