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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user