feat: in-app Orchestrator (Phase 2) — multi-agent conductor

Brings the deterministic Han-flow conductor into BooCode: launch any read-only
flow from BooChat or BooCoder, watch each agent stream live in a Paseo-style
run pane, get an evidence-disciplined report — on local Qwen, persisted and
resumable. Read-only enforced hard via qwen --approval-mode plan (orchestrator
tasks fail closed if qwen is unavailable; never fall to write-capable native).

Backend (apps/coder): re-homed conductor defs, flow_runs/flow_steps schema,
flow-runner + dispatcher onTaskTerminal hook, restart-resume, runs routes
(launch/list/get/cancel), user-channel WS. Contracts: two flow_run_* frames.
Web: orchestrator pane kind + OrchestratorPane, Workflow button + slash flows
(BooChat/BooCoder parity), FlowLauncherDialog, "New Orchestrator" in the + and
split menus, runs history + export. Plan: openspec/changes/orchestrator.
This commit is contained in:
2026-06-03 14:59:07 +00:00
parent 7ff99238c9
commit fa8d707923
118 changed files with 15723 additions and 27 deletions

View File

@@ -6,6 +6,7 @@ import type {
ClosedPaneEntry,
HtmlArtifactState,
MarkdownArtifactState,
OrchestratorState,
WorkspacePane,
WorkspaceState,
WorkspaceTabKind,
@@ -176,6 +177,16 @@ function htmlArtifactPane(state: HtmlArtifactState): WorkspacePane {
};
}
function orchestratorPane(state: OrchestratorState): WorkspacePane {
return {
id: generateId(),
kind: 'orchestrator',
chatIds: [],
activeChatIdx: -1,
orchestrator_state: state,
};
}
// v1.9: settings panes are ephemeral. Filter them out before persisting so a
// page reload always returns to a clean workspace; the user re-opens via the
// sidebar Settings button when needed.
@@ -277,6 +288,8 @@ export interface UseWorkspacePanesResult {
addSplitPane: (kind: 'chat' | 'terminal' | 'coder') => string | null;
/** Mixed tabs: add a tab of any kind to a pane (the "+" menu). */
createTab: (paneIdx: number, kind: WorkspaceTabKind) => Promise<void>;
/** Open an orchestrator run pane (or focus an existing one for the same run_id). */
addOrchestratorPane: (state: OrchestratorState) => string | null;
/** Back-compat alias for createTab(paneIdx, 'coder'). */
createCoderTab: (paneIdx: number) => Promise<void>;
// Open-on-first-click, close-on-second-click. Singleton — settings panes
@@ -831,6 +844,39 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
return success ? newPaneId : null;
}, [seedPaneChat]);
const addOrchestratorPane = useCallback((state: OrchestratorState): string | null => {
let openedId: string | null = null;
setPanes((prev) => {
// Dedup: focus an existing pane for the same run.
const existingIdx = prev.findIndex(
(p) => p.kind === 'orchestrator' && p.orchestrator_state?.run_id === state.run_id,
);
if (existingIdx >= 0) {
setActivePaneIdx(existingIdx);
openedId = prev[existingIdx]!.id;
return prev;
}
if (nonSettingsCount(prev) >= MAX_PANES) {
toast.error(`Maximum ${MAX_PANES} panes`);
return prev;
}
const newPane = orchestratorPane(state);
openedId = newPane.id;
const next = [...prev, newPane];
setActivePaneIdx(next.length - 1);
return next;
});
return openedId;
}, []);
// Orchestrator pane: open via sessionEvents (fired by ChatInput slash/button).
useEffect(() => {
return sessionEvents.subscribe((ev) => {
if (ev.type !== 'open_orchestrator_pane') return;
addOrchestratorPane(ev.state);
});
}, [addOrchestratorPane]);
// Returns the new settings pane id when one is OPENED (so mobile callers can
// push ?pane= atomically — see addPaneAndSwitch), or null when it was closed.
// Id generated outside the updater so a strict-mode double-invoke agrees.
@@ -1074,6 +1120,7 @@ export function useWorkspacePanes(sessionId: string): UseWorkspacePanesResult {
closeSessionHistory,
addSplitPane,
createTab,
addOrchestratorPane,
createCoderTab,
toggleSettingsPane,
removePane,