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
97 changes: 81 additions & 16 deletions packages/app/src/pages/orchestrate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const PHASE_LABELS: Record<OrchestratePhase, string> = {
failed: "Failed",
}
const NO_WORKERS_WARNING_GRACE_MS = 5000
const PLAN_QUALITY_GATE_FAILURE_LINE_RE = /^plan quality gate failed(?: before registration)?[:.]?/i

type RunModelRole = keyof OrchestrateRoleModels
type RunModelOption = {
Expand Down Expand Up @@ -239,6 +240,28 @@ function parseConflictFiles(text?: string): string[] {
return [...discovered]
}

type PlanRetryPresentation = {
summary: string
details: string
}

function planRetryPresentation(text: string): PlanRetryPresentation | undefined {
const trimmed = text.trim()
if (!trimmed) return undefined
const lines = trimmed
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
if (lines.length === 0) return undefined
if (!lines.some((line) => PLAN_QUALITY_GATE_FAILURE_LINE_RE.test(line))) return undefined

const detailLines = lines.filter((line) => !PLAN_QUALITY_GATE_FAILURE_LINE_RE.test(line))
return {
summary: "Retrying plan after quality gate failure.",
details: detailLines.length > 0 ? detailLines.join("\n") : trimmed,
}
}

function taskStatusLabel(status: OrchestrateTask["status"]): string {
switch (status) {
case "completed":
Expand Down Expand Up @@ -4267,16 +4290,36 @@ function SessionTurnTimeline(props: {
<div class="flex flex-col gap-3 pb-3">
<For each={assistantFallbackMessages()}>
{(message) => (
<div class="text-13-regular leading-relaxed whitespace-pre-wrap break-words">
<div class="flex items-center gap-1.5 mb-1">
<span class="size-1.5 rounded-full shrink-0 bg-icon-info-base" />
<span class="text-10-medium text-icon-info-base uppercase tracking-wider">Assistant</span>
<span class="text-10-regular text-text-weak ml-auto">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
<div class="text-text-base">{message.text}</div>
</div>
(() => {
const compact = planRetryPresentation(message.text)
return (
<div class="text-13-regular leading-relaxed whitespace-pre-wrap break-words">
<div class="flex items-center gap-1.5 mb-1">
<span class="size-1.5 rounded-full shrink-0 bg-icon-info-base" />
<span class="text-10-medium text-icon-info-base uppercase tracking-wider">Assistant</span>
<span class="text-10-regular text-text-weak ml-auto">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
<Show
when={compact}
fallback={<div class="text-text-base">{message.text}</div>}
>
{(presented) => (
<div class="space-y-1.5">
<div class="text-text-base">{presented().summary}</div>
<details class="rounded border border-border-weak-base/70 bg-surface-base/45 px-2 py-1.5">
<summary class="cursor-pointer text-11-medium text-text-weak">Show details</summary>
<div class="mt-1 text-12-regular text-text-weak whitespace-pre-wrap break-words">
{presented().details}
</div>
</details>
</div>
)}
</Show>
</div>
)
})()
)}
</For>
<Show when={isSessionStreaming()}>
Expand Down Expand Up @@ -4330,6 +4373,7 @@ function SessionTurnTimeline(props: {
<Match when={item.kind === "thought"}>
{(() => {
const thoughtItem = item as Extract<TimelineItem, { kind: "thought" }>
const compact = planRetryPresentation(thoughtItem.thought.reasoning)
return (
<div class="text-13-regular leading-relaxed whitespace-pre-wrap break-words">
<div class="flex items-center gap-1.5 mb-1">
Expand All @@ -4350,12 +4394,33 @@ function SessionTurnTimeline(props: {
{new Date(thoughtItem.thought.timestamp).toLocaleTimeString()}
</span>
</div>
<div classList={{
"text-text-base": thoughtItem.thought.action !== "intervene",
"text-icon-warning-base": thoughtItem.thought.action === "intervene",
}}>
{thoughtItem.thought.reasoning}
</div>
<Show
when={compact}
fallback={
<div classList={{
"text-text-base": thoughtItem.thought.action !== "intervene",
"text-icon-warning-base": thoughtItem.thought.action === "intervene",
}}>
{thoughtItem.thought.reasoning}
</div>
}
>
{(presented) => (
<div classList={{
"space-y-1.5": true,
"text-text-base": thoughtItem.thought.action !== "intervene",
"text-icon-warning-base": thoughtItem.thought.action === "intervene",
}}>
<div>{presented().summary}</div>
<details class="rounded border border-border-weak-base/70 bg-surface-base/45 px-2 py-1.5 text-text-base">
<summary class="cursor-pointer text-11-medium text-text-weak">Show details</summary>
<div class="mt-1 text-12-regular text-text-weak whitespace-pre-wrap break-words">
{presented().details}
</div>
</details>
</div>
)}
</Show>
</div>
)
})()}
Expand Down
7 changes: 6 additions & 1 deletion packages/desktop/src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -540,11 +540,16 @@ pub fn serve(
) -> (CommandChild, oneshot::Receiver<TerminatedPayload>) {
let (exit_tx, exit_rx) = oneshot::channel::<TerminatedPayload>();

tracing::info!(port, "Spawning sidecar");
// Isolate each desktop sidecar worker so stale/orphaned processes cannot
// consume Temporal activities for newly-started runs.
let temporal_task_queue = format!("oneshot-orchestrator-v2-desktop-{port}");

tracing::info!(port, temporal_task_queue, "Spawning sidecar");

let envs = [
("OPENCODE_SERVER_USERNAME", "oneshot".to_string()),
("OPENCODE_SERVER_PASSWORD", password.to_string()),
("TEMPORAL_TASK_QUEUE", temporal_task_queue),
];

let (events, child) = spawn_command(
Expand Down