diff --git a/README.md b/README.md index 615d591..98267a6 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,15 @@ oneshot orchestrate "Build a full-stack Trello clone with auth, Postgres, and re If you use the launch scripts below, OneShot will auto-check/install most local requirements for you (Bun/Rust/Tauri deps/Temporal where possible). -Manual setup requirements: +Manual setup requirements (source/dev builds): - [Bun](https://bun.sh) ≥ 1.0 - Rust + Cargo (for desktop/Tauri) - [Temporal Server](https://github.com/temporalio/temporal) reachable at `127.0.0.1:7233` (or set `TEMPORAL_ADDRESS`) - [Temporal CLI](https://github.com/temporalio/cli) (`temporal`) installed on your PATH > OneShot orchestration uses Temporal for durable workflow execution. The runtime starts a worker in-process and uses the `temporal` CLI to start/signal/query workflows. +> +> Desktop release bundles include both `oneshot-cli` and `temporal` sidecars and will auto-start local Temporal by default, so end users do not need a separate Temporal install. --- diff --git a/packages/app/src/context/orchestrate.tsx b/packages/app/src/context/orchestrate.tsx index a0e156c..b2bf381 100644 --- a/packages/app/src/context/orchestrate.tsx +++ b/packages/app/src/context/orchestrate.tsx @@ -2339,7 +2339,7 @@ export const { use: useOrchestrate, provider: OrchestrateProvider } = createSimp ) { return "retrying" } - if (status === "completed") return "done" + if (status === "completed" || status === "skipped") return "done" if (status === "failed") return "failed" if (status === "in_progress") return "busy" if (status === "blocked") return "blocked" @@ -2461,7 +2461,7 @@ export const { use: useOrchestrate, provider: OrchestrateProvider } = createSimp const runPaused = state.runStatus === "paused" const hasAnyInProgress = state.tasks.some((task) => task.status === "in_progress") const hasAnyBlocked = state.tasks.some((task) => task.status === "blocked") - const hasAnyCompleted = state.tasks.some((task) => task.status === "completed") + const hasAnyCompleted = state.tasks.some((task) => task.status === "completed" || task.status === "skipped") const hasAnyOutstanding = state.tasks.some((task) => task.status === "pending" || task.status === "in_progress" || task.status === "blocked", ) @@ -2482,6 +2482,15 @@ export const { use: useOrchestrate, provider: OrchestrateProvider } = createSimp state.phase === "researching" || !!state.researchContext || !!state.researchSessionId + const shouldShowPlannerNode = + !!state.planningSessionId + && ( + state.phase === "researching" + || state.phase === "planning" + || state.tasks.length === 0 + || runPaused + || runFailed + ) const researchStatus: AgentNode["status"] = state.phase === "researching" @@ -2491,6 +2500,14 @@ export const { use: useOrchestrate, provider: OrchestrateProvider } = createSimp : state.phase === "failed" && !state.researchContext ? "failed" : "done" + const plannerStatus: AgentNode["status"] = + runFailed + ? "failed" + : runPaused + ? "paused" + : state.phase === "planning" || (state.runStatus === "active" && state.tasks.length === 0) + ? "busy" + : "done" // During planning phase, show the planner session; during execution, show the supervisor session const orchestratorSessionId = resolveMasterSessionId({ @@ -2532,6 +2549,15 @@ export const { use: useOrchestrate, provider: OrchestrateProvider } = createSimp status: researchStatus, children: [], }] : []), + ...(shouldShowPlannerNode ? [{ + id: "root-planner", + role: "task" as const, + label: "Root Planner", + sessionId: state.planningSessionId, + status: plannerStatus, + kind: "planning", + children: [], + }] : []), ...rootNodes, // Merge agent — uses distinct role for special UI rendering ...(shouldShowIngestNode ? [{ diff --git a/packages/app/src/pages/deploy.tsx b/packages/app/src/pages/deploy.tsx index 6a184c6..a4f071d 100644 --- a/packages/app/src/pages/deploy.tsx +++ b/packages/app/src/pages/deploy.tsx @@ -2,6 +2,8 @@ import { For, Show, createEffect, createMemo, createSignal, onCleanup, type JSX import { Button } from "@oneshot-ai/ui/button" import { Icon } from "@oneshot-ai/ui/icon" import { showToast } from "@oneshot-ai/ui/toast" +import { Terminal } from "@/components/terminal" +import type { LocalPTY } from "@/context/terminal" import { useOrchestrate } from "@/context/orchestrate" import { usePlatform } from "@/context/platform" import { useSDK } from "@/context/sdk" @@ -117,8 +119,10 @@ const DEFAULT_TOOL_DESCRIPTIONS: Record = { artifact_capture: "Capture screenshots, traces, and videos.", } -const DEFAULT_TOOLS: DeployToolDescriptor[] = Object.entries(DEFAULT_TOOL_DESCRIPTIONS) - .map(([id, description]) => ({ id: id as DeployToolName, description })) +const DEFAULT_TOOLS: DeployToolDescriptor[] = Object.entries(DEFAULT_TOOL_DESCRIPTIONS).map(([id, description]) => ({ + id: id as DeployToolName, + description, +})) const SESSION_STATUS_LABEL: Record = { idle: "Idle", @@ -255,10 +259,28 @@ function invocationDisplayTitle(invocation: DeployToolInvocation): string { function parseSession(payload: unknown): DeploySession | undefined { if (!payload || typeof payload !== "object") return undefined const maybe = payload as DeploySession - if (typeof maybe.id !== "string" || !Array.isArray(maybe.messages) || !Array.isArray(maybe.invocations)) return undefined + if (typeof maybe.id !== "string" || !Array.isArray(maybe.messages) || !Array.isArray(maybe.invocations)) + return undefined return maybe } +function invocationLivePty(invocation: DeployToolInvocation): LocalPTY | undefined { + const meta = invocation.output?.meta + if (!meta || typeof meta !== "object") return undefined + const record = meta as Record + const id = typeof record.livePtyId === "string" ? record.livePtyId.trim() : "" + if (!id) return undefined + const title = + typeof record.livePtyTitle === "string" && record.livePtyTitle.trim() + ? record.livePtyTitle.trim() + : "Deploy App Terminal" + return { + id, + title, + titleNumber: 0, + } +} + type LaunchGuide = { status: "RUNNING" | "READY_FOR_MANUAL_START" | "UNKNOWN" summary?: string @@ -317,44 +339,55 @@ function parseLaunchGuideFromAssistantMessage(message: DeployMessage): LaunchGui function parseTools(payload: unknown): DeployToolDescriptor[] { if (!Array.isArray(payload)) return [] - return payload - .flatMap((item) => { - if (!item || typeof item !== "object") return [] - const record = item as Record - if (typeof record.id !== "string" || typeof record.description !== "string") return [] - return [{ id: record.id as DeployToolName, description: record.description }] - }) + return payload.flatMap((item) => { + if (!item || typeof item !== "object") return [] + const record = item as Record + if (typeof record.id !== "string" || typeof record.description !== "string") return [] + return [{ id: record.id as DeployToolName, description: record.description }] + }) } function InvocationWidget(props: { invocation: DeployToolInvocation onOpenUrl?: (url: string) => void + compact?: boolean }): JSX.Element { + const compact = !!props.compact const theme = TOOL_WIDGET_THEME[props.invocation.tool] - const logs = clampLogs(props.invocation.output?.logs) + const logs = clampLogs(props.invocation.output?.logs, compact ? 5 : 14) const table = props.invocation.output?.table + const livePty = createMemo(() => invocationLivePty(props.invocation)) + const maxArtifacts = compact ? 2 : 6 return (
-
+
-
{theme.label}
-
{invocationDisplayTitle(props.invocation)}
+
+ {theme.label} +
+
+ {invocationDisplayTitle(props.invocation)} +
- + {props.invocation.status.toUpperCase()}
- {(summary) =>

{summary()}

} + {(summary) =>

{summary()}

}
{(command) => ( -
- {command()} +
+ {compact && command().length > 220 ? `${command().slice(0, 220)}...` : command()}
)} @@ -363,68 +396,104 @@ function InvocationWidget(props: { {(url) => ( )} + + {(pty) => ( +
+
+ Live Terminal + {pty().id.slice(-6)} +
+
+ +
+
+ )} +
+ 0 || table.rows.length > 0)}> -
- - 0}> - - - {(column) => } - - - - - - {(row) => ( - - {(cell) => } + + Table output captured ({table?.rows.length ?? 0} rows). Open Flight Deck for full view. + + } + > +
+
{column}
{cell}
+ 0}> + + + + {(column) => } + - )} - - -
{column}
-
+ +
+ + + {(row) => ( + + {(cell) => {cell}} + + )} + + + +
+
- 0}> -
+         0}>
+          
             {logs.join("\n")}
           
- + {(text) => ( -
+            
               {text()}
             
)} - 0}> + 0 && !compact}>
- + {(artifact) => (
{artifact.label}
{artifact.relativePath}
- )} + } >
- {artifact.label} -
{artifact.label}
+ {artifact.label} +
+ {artifact.label} +
@@ -435,19 +504,26 @@ function InvocationWidget(props: { {(route) => ( -
+
{route().accepted ? "Remediation queued for linked run" : "Remediation queueing was not accepted"}
{route().message}
-
Run: {route().runId} | {formatDate(route().at)}
+
+ Run: {route().runId} | {formatDate(route().at)} +
)} -
+
Started {formatDate(props.invocation.startedAt)} - | Duration {formatDuration(props.invocation.startedAt, props.invocation.completedAt)} + + {" "} + | Duration {formatDuration(props.invocation.startedAt, props.invocation.completedAt)}{" "} +
@@ -545,7 +621,10 @@ export default function DeployPage() { if (current.status === "passed") { const launchInvocation = [...current.invocations] .reverse() - .find((invocation) => invocation.tool === "terminal_exec" && invocation.status === "passed" && !!invocation.output?.command) + .find( + (invocation) => + invocation.tool === "terminal_exec" && invocation.status === "passed" && !!invocation.output?.command, + ) if (launchInvocation?.output?.command) { return { @@ -596,13 +675,13 @@ export default function DeployPage() { const toolPaneOpen = createMemo(() => !paneDismissed() && !!activeInvocation()) - const routedFailureCount = createMemo(() => - (session()?.invocations ?? []).filter((invocation) => invocation.status === "failed" && invocation.routedToRun).length, + const routedFailureCount = createMemo( + () => + (session()?.invocations ?? []).filter((invocation) => invocation.status === "failed" && invocation.routedToRun) + .length, ) - const sortedSessions = createMemo(() => - [...sessions()].sort((left, right) => right.updatedAt - left.updatedAt), - ) + const sortedSessions = createMemo(() => [...sessions()].sort((left, right) => right.updatedAt - left.updatedAt)) const workspaceKey = createMemo(() => `${sdk.url ?? ""}::${sdk.directory}`) @@ -646,9 +725,7 @@ export default function DeployPage() { setSessions([]) return } - const parsed = payload - .map((item) => parseSession(item)) - .filter((item): item is DeploySession => !!item) + const parsed = payload.map((item) => parseSession(item)).filter((item): item is DeploySession => !!item) setSessions(parsed) } @@ -816,9 +893,7 @@ export default function DeployPage() { const messageInvocationResolver = (message: DeployMessage): DeployToolInvocation[] => { const map = invocationById() - return message.invocationIds - .map((id) => map.get(id)) - .filter((item): item is DeployToolInvocation => !!item) + return message.invocationIds.map((id) => map.get(id)).filter((item): item is DeployToolInvocation => !!item) } const latestMessageByInvocation = createMemo(() => { @@ -864,7 +939,7 @@ export default function DeployPage() { method: "POST", body: JSON.stringify({}), }) - const record = payload && typeof payload === "object" ? payload as Record : undefined + const record = payload && typeof payload === "object" ? (payload as Record) : undefined if (!record) throw new Error("Invalid open response") const nextSession = parseSession(record.session) @@ -875,15 +950,18 @@ export default function DeployPage() { } const invocationRecord = record.invocation - const invocationId = invocationRecord && typeof invocationRecord === "object" && typeof (invocationRecord as { id?: unknown }).id === "string" - ? (invocationRecord as { id: string }).id - : undefined + const invocationId = + invocationRecord && + typeof invocationRecord === "object" && + typeof (invocationRecord as { id?: unknown }).id === "string" + ? (invocationRecord as { id: string }).id + : undefined if (invocationId) { setActiveInvocationId(invocationId) setPaneDismissed(false) } - const target = typeof record.target === "string" ? record.target as DeployOpenTarget : "terminal_view" + const target = typeof record.target === "string" ? (record.target as DeployOpenTarget) : "terminal_view" const openedUrl = typeof record.url === "string" && record.url.trim() ? record.url.trim() : undefined if (openedUrl && target === "web_view") { setBrowserReloadToken((token) => token + 1) @@ -949,16 +1027,23 @@ export default function DeployPage() {
Deploy Agent
- Chat-first local deploy validation. Tool widgets render inline in chat and can expand into the right-side flight deck. + 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.
- + {SESSION_STATUS_LABEL[session()?.status ?? "idle"]} - {(id) => {id()}} + {(id) => ( + + {id()} + + )} 0}> @@ -1025,7 +1110,11 @@ export default function DeployPage() { }} > - {(item) => } + {(item) => ( + + )} @@ -1039,29 +1128,16 @@ export default function DeployPage() { )} -
{(guide) => ( -
+
Launch Guide
- + {guide().status} Source: {guide().source} @@ -1073,12 +1149,15 @@ export default function DeployPage() { 0}>
    - - {(step) =>
  1. {step}
  2. } -
    + {(step) =>
  3. {step}
  4. }
+
+ Manual review is still required. If a button or flow fails, post exact steps and expected behavior + in chat so Deploy can rerun and patch. +
+
@@ -1115,17 +1195,21 @@ export default function DeployPage() { > Initializing deploy session...
} + fallback={ +
+ Initializing deploy session... +
+ } > {session() ? "No messages yet. Ask the deploy agent to validate this repo." : "No deploy session selected. Click New Session to start manually."}
- )} + } > {(message) => { @@ -1136,22 +1220,35 @@ export default function DeployPage() {
- {ROLE_LABEL[message.role]} - {formatDate(message.createdAt)} + + {ROLE_LABEL[message.role]} + + + {formatDate(message.createdAt)} +
{message.text}
0}>
- latestMessageByInvocation().get(invocation.id) === message.id)}> + + latestMessageByInvocation().get(invocation.id) === message.id, + )} + > {(invocation) => (
- + {TOOL_WIDGET_THEME[invocation.tool].label} - {invocation.id.slice(-6)} + + {invocation.id.slice(-6)} +
@@ -1183,11 +1281,17 @@ export default function DeployPage() { {(invocation) => ( - +
- Running Now + + Running Now +
@@ -1223,9 +1328,7 @@ export default function DeployPage() { void sendMessage() }} > -
- Deploy Agent -
+
Deploy Agent