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 }); }