Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/pr-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Multi-stage build for production deployment
# Trigger rebuild: 2026-04-23
# Builder stage
FROM node:22-slim AS builder

Expand Down
19 changes: 16 additions & 3 deletions src/components/preview/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -92,6 +94,7 @@ export function PreviewPanel({
models = [],
onOpenFile,
onStreamingStateChange,
onFixWithDoce,
}: PreviewPanelProps) {
const isMobile = useIsMobile();
const { baseUrl } = useBaseUrlSetting();
Expand Down Expand Up @@ -673,9 +676,19 @@ export function PreviewPanel({
{message || "Check the terminal for details."}
</p>
</div>
<Button variant="outline" onClick={handleRetry}>
Try Again
</Button>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleRetry}>
Try Again
</Button>
{onFixWithDoce && message && (
<Button
variant="default"
onClick={() => onFixWithDoce(message)}
>
Fix with Doce
</Button>
)}
</div>
</div>
) : null}
</div>
Expand Down
31 changes: 19 additions & 12 deletions src/components/projects/ProjectContentWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -115,10 +131,7 @@ export function ProjectContentWrapper({
isStreaming={isStreaming}
models={models}
onOpenFile={setFileToOpen}
onStreamingStateChange={(count, streaming) => {
setUserMessageCount(count);
setIsStreaming(streaming);
}}
onFixWithDoce={handleFixWithDoce}
/>
</div>
) : (
Expand Down Expand Up @@ -151,10 +164,7 @@ export function ProjectContentWrapper({
projectId={projectId}
models={models}
onOpenFile={setFileToOpen}
onStreamingStateChange={(count, streaming) => {
setUserMessageCount(count);
setIsStreaming(streaming);
}}
hideDetachToggle={false}
/>
</ErrorBoundary>
</motion.div>
Expand Down Expand Up @@ -194,6 +204,7 @@ export function ProjectContentWrapper({
onFileOpened={() => setFileToOpen(null)}
userMessageCount={userMessageCount}
isStreaming={isStreaming}
onFixWithDoce={handleFixWithDoce}
/>
</ErrorBoundary>
</motion.div>
Expand All @@ -203,10 +214,6 @@ export function ProjectContentWrapper({
projectId={projectId}
models={models}
onOpenFile={setFileToOpen}
onStreamingStateChange={(count, streaming) => {
setUserMessageCount(count);
setIsStreaming(streaming);
}}
/>
</>
)}
Expand Down
29 changes: 0 additions & 29 deletions src/server/opencode/apiKeyValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,6 @@ function validateOpenAICompatibleKeyEffect(baseUrl: string) {
apiKey: string,
): Effect.Effect<ValidationResult, ApiKeyValidationError> =>
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`, {
Expand Down Expand Up @@ -197,14 +189,6 @@ function validateAnthropicKeyEffect(
apiKey: string,
): Effect.Effect<ValidationResult, ApiKeyValidationError> {
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", {
Expand Down Expand Up @@ -276,13 +260,6 @@ function validateOpenAIKeyEffect(
apiKey: string,
): Effect.Effect<ValidationResult, ApiKeyValidationError> {
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", {
Expand Down Expand Up @@ -350,11 +327,5 @@ export async function validateOpenAIKey(
function validateOpencodeKeyEffect(
apiKey: string,
): Effect.Effect<ValidationResult, ApiKeyValidationError> {
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 });
}
Loading