From f05b8f44bcdb293736c4084d9f92adce2295cf7a Mon Sep 17 00:00:00 2001 From: Braedon Saunders Date: Thu, 26 Feb 2026 00:34:55 -0500 Subject: [PATCH] chore: commit local workspace changes --- packages/app/src/context/orchestrate.tsx | 28 + packages/app/src/pages/deploy.tsx | 770 +++++++++++------- packages/oneshot/src/deploy/session.ts | 123 ++- packages/oneshot/src/orchestrator/index.ts | 101 ++- .../orchestrator/runtime/langgraph-engine.ts | 377 ++++++++- .../src/orchestrator/runtime/recovery.ts | 105 +++ .../src/orchestrator/runtime/verification.ts | 116 ++- .../src/orchestrator/runtime/workflow.ts | 35 +- .../orchestrator/runtime/worktree-ingest.ts | 47 +- .../orchestrator/checklist-expansion.test.ts | 38 + .../test/orchestrator/recovery-errors.test.ts | 37 + .../replan-decision-sanitization.test.ts | 107 +++ .../test/orchestrator/verification.test.ts | 45 + 13 files changed, 1572 insertions(+), 357 deletions(-) create mode 100644 packages/oneshot/src/orchestrator/runtime/recovery.ts create mode 100644 packages/oneshot/test/orchestrator/checklist-expansion.test.ts create mode 100644 packages/oneshot/test/orchestrator/recovery-errors.test.ts diff --git a/packages/app/src/context/orchestrate.tsx b/packages/app/src/context/orchestrate.tsx index b78ab44..9a1ebbb 100644 --- a/packages/app/src/context/orchestrate.tsx +++ b/packages/app/src/context/orchestrate.tsx @@ -210,10 +210,20 @@ export interface LocalDeployRequest { error?: string } +export interface LocalDeployOpenTarget { + type: "url" | "path" + target: string + label?: string + app?: string + autoOpenedAt?: number + autoOpenError?: string +} + export interface LocalDeployState { status: "idle" | "running" | "passed" | "failed" trigger?: "manual" | "finalize" url?: string + openTarget?: LocalDeployOpenTarget requestedAt?: number startedAt?: number completedAt?: number @@ -722,6 +732,23 @@ function parseLocalDeployRequest(value: unknown): LocalDeployRequest | undefined } } +function parseLocalDeployOpenTarget(value: unknown): LocalDeployOpenTarget | undefined { + if (!isRecord(value)) return undefined + const type = value.type === "url" || value.type === "path" + ? value.type + : undefined + const target = asString(value.target) + if (!type || !target) return undefined + return { + type, + target, + label: asString(value.label), + app: asString(value.app), + autoOpenedAt: asNumber(value.autoOpenedAt), + autoOpenError: asString(value.autoOpenError), + } +} + function parseLocalDeploy(value: unknown): LocalDeployState | undefined { if (!isRecord(value)) return undefined const status = asLocalDeployStatus(value.status) @@ -811,6 +838,7 @@ function parseLocalDeploy(value: unknown): LocalDeployState | undefined { status, trigger, url: asString(value.url), + openTarget: parseLocalDeployOpenTarget(value.openTarget), requestedAt: asNumber(value.requestedAt), startedAt: asNumber(value.startedAt), completedAt: asNumber(value.completedAt), diff --git a/packages/app/src/pages/deploy.tsx b/packages/app/src/pages/deploy.tsx index b30711c..d915883 100644 --- a/packages/app/src/pages/deploy.tsx +++ b/packages/app/src/pages/deploy.tsx @@ -6,6 +6,12 @@ import type { LocalPTY } from "@/context/terminal" import { useOrchestrate } from "@/context/orchestrate" import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" +import { + openDeployOpenTarget, + resolveDeployInvocationOpenTarget, + resolveDeployOpenTarget, + type DeployOpenTarget as DeployResolvedOpenTarget, +} from "@/lib/deploy-open-target" type DeployToolName = | "terminal_exec" @@ -218,8 +224,6 @@ const ROLE_STREAM_STYLE: Record).openAction === true +} + +function invocationOpenMode(invocation: DeployToolInvocation | undefined): DeployOpenMode | undefined { + if (!invocation) return undefined + const inputTarget = parseDeployOpenMode(invocation.input?.target) + if (inputTarget) return inputTarget + const meta = invocation.output?.meta + if (!meta || typeof meta !== "object") return undefined + const record = meta as Record + return parseDeployOpenMode(record.target) +} function extractFirstUrl(value: string): string | undefined { const match = value.match(/https?:\/\/[^\s)]+/i) @@ -455,7 +486,7 @@ function InvocationWidget(props: { when={!compact} fallback={
- Table output captured ({table?.rows.length ?? 0} rows). Open Flight Deck for full view. + Table output captured ({table?.rows.length ?? 0} rows). Open Runtime view for full table details.
} > @@ -575,9 +606,10 @@ export default function DeployPage() { const [openingApp, setOpeningApp] = createSignal(false) const [chatInput, setChatInput] = createSignal("") - const [runLinkInput, setRunLinkInput] = createSignal("") - const [activeInvocationId, setActiveInvocationId] = createSignal(undefined) - const [paneDismissed, setPaneDismissed] = createSignal(true) + const [viewMode, setViewMode] = createSignal("chat") + const [lastExternalTarget, setLastExternalTarget] = createSignal(undefined) + const [externalOpenError, setExternalOpenError] = createSignal(undefined) + const [autoLinkedKey, setAutoLinkedKey] = createSignal(undefined) const [launchGuideExpanded, setLaunchGuideExpanded] = createSignal(true) let chatScrollRef: HTMLDivElement | undefined @@ -689,27 +721,77 @@ export default function DeployPage() { return undefined }) - const latestInvocation = createMemo(() => { + const latestRunningInvocation = createMemo(() => { const list = session()?.invocations ?? [] - if (list.length === 0) return undefined - return list[list.length - 1] + for (let index = list.length - 1; index >= 0; index--) { + if (list[index]?.status === "running") return list[index] + } + return undefined }) - const latestRunningInvocation = createMemo(() => { + const latestOpenInvocation = createMemo(() => { const list = session()?.invocations ?? [] for (let index = list.length - 1; index >= 0; index--) { - if (list[index]?.status === "running") return list[index] + const invocation = list[index] + if (invocation && invocationIsOpenAction(invocation)) return invocation } return undefined }) - const activeInvocation = createMemo(() => { - const focused = activeInvocationId() ? invocationById().get(activeInvocationId()!) : undefined - if (focused) return focused - return latestRunningInvocation() ?? latestInvocation() + const latestLiveTerminal = createMemo(() => { + const list = session()?.invocations ?? [] + for (let index = list.length - 1; index >= 0; index--) { + const invocation = list[index] + if (!invocation) continue + const pty = invocationLivePty(invocation) + if (pty) return pty + } + return undefined + }) + + const openMode = createMemo(() => invocationOpenMode(latestOpenInvocation())) + + const deployOpenTarget = createMemo(() => { + const fromInvocation = resolveDeployInvocationOpenTarget(latestOpenInvocation()?.output, { + includePreviewFallback: false, + }) + if (fromInvocation) return fromInvocation + const fromRun = resolveDeployOpenTarget(linkedRunLocalDeploy()) + if (fromRun) return fromRun + return lastExternalTarget() + }) + + const surface = createMemo(() => { + const mode = openMode() + const url = latestPreviewUrl() + const target = deployOpenTarget() + const terminal = latestLiveTerminal() + + if (mode === "external") return { kind: "external", target, source: "open_target" } + if (mode === "web_view") { + if (url) return { kind: "web", url, source: "preview" } + if (target?.type === "url") return { kind: "web", url: target.target, source: "open_target" } + return terminal ? { kind: "terminal", pty: terminal, source: "live_pty" } : undefined + } + if (mode === "terminal_view") { + if (terminal) return { kind: "terminal", pty: terminal, source: "live_pty" } + if (url) return { kind: "web", url, source: "preview" } + } + + if (url) return { kind: "web", url, source: "preview" } + if (terminal) return { kind: "terminal", pty: terminal, source: "live_pty" } + if (target?.type === "url") return { kind: "web", url: target.target, source: "open_target" } + if (target) return { kind: "external", target, source: "open_target" } + return undefined }) - const toolPaneOpen = createMemo(() => !paneDismissed() && !!activeInvocation()) + const surfaceLabel = createMemo(() => { + const value = surface() + if (!value) return "No runtime surface detected yet" + if (value.kind === "web") return "Surface: Webview" + if (value.kind === "terminal") return "Surface: Interactive terminal" + return "Surface: External app" + }) const routedFailureCount = createMemo( () => @@ -784,8 +866,6 @@ export default function DeployPage() { throw new Error("Invalid deploy session response") } setSession(parsed) - setActiveInvocationId(undefined) - setPaneDismissed(true) return parsed } finally { setCreatingSession(false) @@ -796,8 +876,10 @@ export default function DeployPage() { const token = ++initializeToken setInitializing(true) setSession(undefined) - setActiveInvocationId(undefined) - setPaneDismissed(true) + setViewMode("chat") + setLastExternalTarget(undefined) + setExternalOpenError(undefined) + setAutoLinkedKey(undefined) try { await loadToolsState() @@ -810,10 +892,8 @@ export default function DeployPage() { if (latest) { setSession(latest) - setRunLinkInput(latest.linkedRunId ?? activeRunId()) } else { setSession(undefined) - setRunLinkInput(activeRunId()) } } catch (error) { if (token !== initializeToken) return @@ -864,7 +944,6 @@ export default function DeployPage() { const next = parseSession(payload) if (!next) throw new Error("Invalid deploy session response") setSession(next) - setRunLinkInput(next.linkedRunId ?? "") } catch (error) { showToast({ title: "Run link update failed", @@ -882,17 +961,13 @@ export default function DeployPage() { createEffect(() => { const current = session() - if (!current) return - if (runLinkInput().trim()) return - setRunLinkInput(current.linkedRunId ?? activeRunId()) - }) - - createEffect(() => { - const running = latestRunningInvocation() - if (!running) return - if (activeInvocationId() !== running.id) { - setActiveInvocationId(running.id) - } + const runId = activeRunId() + if (!current || !runId || linkingRun()) return + if (current.linkedRunId?.trim() === runId) return + const key = `${current.id}:${runId}` + if (autoLinkedKey() === key) return + setAutoLinkedKey(key) + void linkRun(runId) }) createEffect(() => { @@ -916,6 +991,20 @@ export default function DeployPage() { scrollChatToBottom() }) + createEffect((previousStatus?: DeploySessionStatus) => { + const status = session()?.status + if (status === "passed" && previousStatus !== "passed") { + void openApp({ silent: true }) + } + return status + }) + + createEffect(() => { + if (!surface()) return + if (session()?.status !== "passed") return + setViewMode("surface") + }) + onCleanup(() => { if (chatScrollFrame !== undefined) cancelAnimationFrame(chatScrollFrame) }) @@ -935,7 +1024,7 @@ export default function DeployPage() { return map }) - async function openApp(): Promise { + async function openApp(options: { silent?: boolean } = {}): Promise { if (openingApp()) return const current = session() if (!current) { @@ -951,6 +1040,36 @@ export default function DeployPage() { return } + const fallbackTarget = deployOpenTarget() + const hasServerOpenTarget = Boolean( + current.launchUrl?.trim() || + (current.launchCommand?.length ?? 0) > 0 || + latestPreviewUrl() || + launchGuide()?.openUrl, + ) + if (!hasServerOpenTarget && fallbackTarget) { + if (fallbackTarget.type === "path") { + setExternalOpenError(undefined) + try { + await openDeployOpenTarget({ platform, target: fallbackTarget }) + setLastExternalTarget(fallbackTarget) + setViewMode("surface") + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + setExternalOpenError(message) + if (!options.silent) { + showToast({ + title: "Open failed", + description: message, + }) + } + } + return + } + setViewMode("surface") + return + } + setOpeningApp(true) try { const payload = await apiJson(`/deploy/session/${current.id}/open`, { @@ -961,10 +1080,11 @@ export default function DeployPage() { if (!record) throw new Error("Invalid open response") const nextSession = parseSession(record.session) + let resolvedSession = nextSession if (nextSession) { setSession(nextSession) } else { - await refreshSessionById(current.id) + resolvedSession = await refreshSessionById(current.id) } const invocationRecord = record.invocation @@ -974,26 +1094,48 @@ export default function DeployPage() { typeof (invocationRecord as { id?: unknown }).id === "string" ? (invocationRecord as { id: string }).id : undefined - if (invocationId) { - setActiveInvocationId(invocationId) - setPaneDismissed(false) - } + const invocation = invocationId + ? resolvedSession?.invocations.find((entry) => entry.id === invocationId) + : undefined - const target = typeof record.target === "string" ? (record.target as DeployOpenTarget) : "terminal_view" + const target = parseDeployOpenMode(record.target) ?? invocationOpenMode(invocation) ?? "terminal_view" const openedUrl = typeof record.url === "string" && record.url.trim() ? record.url.trim() : undefined - if (openedUrl && (target === "web_view" || target === "external")) { - platform.openLink(openedUrl) + const openTarget = resolveDeployOpenTarget({ + openTarget: record.openTarget, + url: openedUrl, + }) + ?? resolveDeployInvocationOpenTarget(invocation?.output, { includePreviewFallback: false }) + ?? resolveDeployOpenTarget(linkedRunLocalDeploy()) + + setExternalOpenError(undefined) + if (target === "external") { + setLastExternalTarget(openTarget) + if (openTarget) { + await openDeployOpenTarget({ + platform, + target: openTarget, + }).catch((error) => { + const message = error instanceof Error ? error.message : String(error) + setExternalOpenError(message) + throw error + }) + } } + setViewMode("surface") - showToast({ - title: "Open complete", - description: typeof record.message === "string" ? record.message : undefined, - }) + if (!options.silent) { + showToast({ + title: "Open complete", + description: typeof record.message === "string" ? record.message : undefined, + }) + } } catch (error) { - showToast({ - title: "Open failed", - description: error instanceof Error ? error.message : String(error), - }) + if (!options.silent) { + showToast({ + title: "Open failed", + description: error instanceof Error ? error.message : String(error), + }) + } } finally { setOpeningApp(false) } @@ -1040,8 +1182,7 @@ export default function DeployPage() {
Deploy Agent
- Deploy checks can pass while UX is still broken. Report exact repro steps in chat; widgets stay - compact inline and expand in the flight deck. + Deploy stays linked to the master orchestrator run so failures can trigger intervention and reruns.
@@ -1073,7 +1214,8 @@ export default function DeployPage() { (!session()?.launchUrl?.trim() && !(session()?.launchCommand?.length ?? 0) && !latestPreviewUrl() && - !launchGuide()?.openUrl) + !launchGuide()?.openUrl && + !deployOpenTarget()) } onClick={() => { void openApp() @@ -1086,6 +1228,23 @@ export default function DeployPage() {
+ + Master run: {activeRunId() || "none"} + + + Linked run: {session()?.linkedRunId ?? "none"} + + + + Syncing link... + + {(detected) => ( @@ -1155,287 +1314,304 @@ export default function DeployPage() {
-
-
-
-
-
Deploy Chat
-
- Describe the failure and repro steps. Deploy reruns local checks, and inline widgets can be - expanded into the flight deck. +
+
+
+ + + + + + {surfaceLabel()} + + 0}> + Tools: {tools().length} + +
+
+ + +
+
+
Deploy Chat
+
+ Describe failures with repro steps. Deploy reruns checks and routes failures back to master. +
-
-
-
{ - chatScrollRef = el - }} - class="relative min-h-0 h-full min-w-0 w-full overflow-y-auto session-scroller" - > +
{ + chatScrollRef = el + }} + class="relative min-h-0 h-full min-w-0 w-full overflow-y-auto session-scroller" > - - Initializing deploy session... -
- } +
- {session() - ? "No messages yet. Ask the deploy agent to validate this repo." - : "Deploy thread is initializing. It should appear automatically."} +
+ Initializing deploy session...
} > - - {(message) => { - const invocations = messageInvocationResolver(message) - const style = ROLE_STREAM_STYLE[message.role] - return ( -
-
-
- - - {ROLE_LABEL[message.role]} - - - {formatDate(message.createdAt)} - -
-
{message.text}
- - 0}> -
- - latestMessageByInvocation().get(invocation.id) === message.id, - )} - > - {(invocation) => ( -
-
- - {TOOL_WIDGET_THEME[invocation.tool].label} - - {invocation.id.slice(-6)} + + {session() + ? "No messages yet. Ask the deploy agent to validate this repo." + : "Deploy thread is initializing. It should appear automatically."} +
+ } + > + + {(message) => { + const invocations = messageInvocationResolver(message) + const style = ROLE_STREAM_STYLE[message.role] + return ( +
+
+
+ + + {ROLE_LABEL[message.role]} + + + {formatDate(message.createdAt)} + +
+
{message.text}
+ + 0}> +
+ + latestMessageByInvocation().get(invocation.id) === message.id, + )} + > + {(invocation) => ( +
+
+ + {TOOL_WIDGET_THEME[invocation.tool].label} + + {invocation.id.slice(-6)} + - - +
+ window.open(url, "_blank", "noopener,noreferrer")} + />
- window.open(url, "_blank", "noopener,noreferrer")} - /> -
- )} - -
- + )} + +
+ +
-
- ) - }} - - - - - {(invocation) => ( - -
-
-
- - Running Now - - + ) + }} + + + + + {(invocation) => ( + +
+
+
+ + Running Now + +
+ window.open(url, "_blank", "noopener,noreferrer")} + />
- window.open(url, "_blank", "noopener,noreferrer")} - />
-
- - )} + + )} + - -
-
-
- -
-
{ - event.preventDefault() - if (!session() || !chatInput().trim() || sendingMessage()) return - void sendMessage() - }} - > -
Deploy Agent
-
-