surface Claude Code Workflow runs in convo + sidebar (T-416)

A `Workflow` tool-use launches its multi-agent run in the background and
returns immediately; the real fan-out arrives out-of-band on stream-json
`type:"system"` task_* events (task_started / task_progress / task_updated /
task_notification) keyed by the launching tool-use id — which clide was
dropping. (Wire shape captured by two live stream-json probes; recorded on
the ticket.)

- workflow_run.dart: a pure, Flutter-free WorkflowRun/WorkflowAgent model
  that folds those events (phases, per-agent start→progress→done deltas,
  usage) into a snapshot.
- StreamJsonSession recognises the events, accumulates a
  Map<toolUseId, WorkflowRun>, and exposes `workflows` + `workflowsStream`.
- A `Workflow` tool-use with a live run renders a dedicated run card —
  phase groups, per-agent rows with spinner/check status, usage, and the
  script — falling back to the generic tool card pre-progress or on reload.
  The run breaks the activity cluster so it's always first-class (like T-342).
- The sidebar Activity tab adds a WORKFLOWS section: one row per run with its
  done/total agent count, tinted by running/done state.

Closes T-416 and the T-410 epic (all children done).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-12 21:44:41 +02:00
co-authored by Claude Opus 4.8
parent 57978c54c5
commit b399acedbf
17 changed files with 871 additions and 4 deletions
+18
View File
@@ -4367,3 +4367,21 @@ answered headless in stream-json (free, num_turns 0) with parseable text
(session %, week % all-models, week % Sonnet). The Activity tab now renders it
via parseUsageText + a user-initiated refresh control. Remaining scope for this
ticket would be per-member/team budget split, if still wanted.', NULL, '2026-06-12 12:11:28', '2026-06-12 12:11:28', '2026-06-12 12:11:28', NULL, '5e45a2e89f2234f8b642e1b9968d7ef1', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'description', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_<id> runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line.
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_<id> runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line.
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.
--- SPIKE FINDINGS (2026-06-12, claude 2.1.175 stream-json probe) ---
ANCHOR: Workflow run is a normal assistant tool_use {name:''Workflow'', input:{script}}. Its tool_result returns IMMEDIATELY: ''Workflow launched in background. Task ID: <id>''. The run is async — today clide shows only the generic tool card + that result, no fan-out.
PROGRESS: carried on type:''system'' events (clide currently drops these), keyed by tool_use_id + task_id:
- task_started: task_id, tool_use_id, description, task_type:''local_workflow'', workflow_name, prompt(script source)
- task_progress (repeated): usage{total_tokens,tool_uses,duration_ms}, summary, workflow_progress[] = per-agent {index,label,model,state:start->progress->done,agentId} DELTAS (partial; merge by index)
- task_updated: patch{status,end_time}
- task_notification: terminal status:''completed'', output_file, summary, usage
LIMITS: (1) workflow agents are NOT sidechain messages (no separate assistant/user wire events, unlike Task) — they exist only as workflow_progress telemetry; existing parentToolUseId nesting does not apply. (2) system events are ephemeral (not in resumed transcript JSONL) — live progress shows during the session; on reload only the tool card + result summary survive.
PLAN: parse system task_* into a WorkflowRun model keyed by tool_use_id in StreamJsonSession; upgrade the Workflow tool_use card to render agent rows + status; add an aggregate active-workflow indicator to the Activity tab. Raw probe wire: /tmp/wf-probe/wire.jsonl.', NULL, '2026-06-12 14:22:42', '2026-06-12 14:22:42', '2026-06-12 14:22:42', NULL, '0b91f092dbd940481e385ccef6e45e6c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'status', 'ready', 'in_progress', NULL, '2026-06-12 14:22:45', '2026-06-12 14:22:45', '2026-06-12 14:22:45', NULL, 'bff47e0f2b4ebfbf5a4ca6b9c7b45825', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'status', 'in_progress', 'done', NULL, '2026-06-12 14:52:31', '2026-06-12 14:52:31', '2026-06-12 14:52:31', NULL, 'd05d502773832fcfb5c8baf19a606b1c', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3EZC7AJANXZVF3D91QYWM', 'status', 'ready', 'done', NULL, '2026-06-12 19:44:20', '2026-06-12 19:44:20', '2026-06-12 19:44:20', NULL, '3637ac53f5fc94698f604db89fdcb7e0', 2) ON CONFLICT(hash) DO NOTHING;
+49
View File
@@ -4793,3 +4793,52 @@ answered headless in stream-json (free, num_turns 0) with parseable text
(session %, week % all-models, week % Sonnet). The Activity tab now renders it
via parseUsageText + a user-initiated refresh control. Remaining scope for this
ticket would be per-member/team budget split, if still wanted.', 'backlog', 'low', NULL, NULL, NULL, '2026-05-23 20:48:38', '2026-06-12 12:11:28', NULL, '98e11d00c142b8377584e35a45b39cdd', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Surface Claude Code Workflow runs in the convo + status surfaces', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_<id> runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line.
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.
--- SPIKE FINDINGS (2026-06-12, claude 2.1.175 stream-json probe) ---
ANCHOR: Workflow run is a normal assistant tool_use {name:''Workflow'', input:{script}}. Its tool_result returns IMMEDIATELY: ''Workflow launched in background. Task ID: <id>''. The run is async — today clide shows only the generic tool card + that result, no fan-out.
PROGRESS: carried on type:''system'' events (clide currently drops these), keyed by tool_use_id + task_id:
- task_started: task_id, tool_use_id, description, task_type:''local_workflow'', workflow_name, prompt(script source)
- task_progress (repeated): usage{total_tokens,tool_uses,duration_ms}, summary, workflow_progress[] = per-agent {index,label,model,state:start->progress->done,agentId} DELTAS (partial; merge by index)
- task_updated: patch{status,end_time}
- task_notification: terminal status:''completed'', output_file, summary, usage
LIMITS: (1) workflow agents are NOT sidechain messages (no separate assistant/user wire events, unlike Task) — they exist only as workflow_progress telemetry; existing parentToolUseId nesting does not apply. (2) system events are ephemeral (not in resumed transcript JSONL) — live progress shows during the session; on reload only the tool card + result summary survive.
PLAN: parse system task_* into a WorkflowRun model keyed by tool_use_id in StreamJsonSession; upgrade the Workflow tool_use card to render agent rows + status; add an aggregate active-workflow indicator to the Activity tab. Raw probe wire: /tmp/wf-probe/wire.jsonl.', 'ready', 'medium', NULL, NULL, NULL, '2026-06-12 10:25:00', '2026-06-12 14:22:42', NULL, '41ff693db584a23f2e5938f873384c2b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Surface Claude Code Workflow runs in the convo + status surfaces', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_<id> runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line.
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.
--- SPIKE FINDINGS (2026-06-12, claude 2.1.175 stream-json probe) ---
ANCHOR: Workflow run is a normal assistant tool_use {name:''Workflow'', input:{script}}. Its tool_result returns IMMEDIATELY: ''Workflow launched in background. Task ID: <id>''. The run is async — today clide shows only the generic tool card + that result, no fan-out.
PROGRESS: carried on type:''system'' events (clide currently drops these), keyed by tool_use_id + task_id:
- task_started: task_id, tool_use_id, description, task_type:''local_workflow'', workflow_name, prompt(script source)
- task_progress (repeated): usage{total_tokens,tool_uses,duration_ms}, summary, workflow_progress[] = per-agent {index,label,model,state:start->progress->done,agentId} DELTAS (partial; merge by index)
- task_updated: patch{status,end_time}
- task_notification: terminal status:''completed'', output_file, summary, usage
LIMITS: (1) workflow agents are NOT sidechain messages (no separate assistant/user wire events, unlike Task) — they exist only as workflow_progress telemetry; existing parentToolUseId nesting does not apply. (2) system events are ephemeral (not in resumed transcript JSONL) — live progress shows during the session; on reload only the tool card + result summary survive.
PLAN: parse system task_* into a WorkflowRun model keyed by tool_use_id in StreamJsonSession; upgrade the Workflow tool_use card to render agent rows + status; add an aggregate active-workflow indicator to the Activity tab. Raw probe wire: /tmp/wf-probe/wire.jsonl.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-12 10:25:00', '2026-06-12 14:22:45', NULL, 'fad55deb0db50c438acb9efe72c6e76c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'story', '06FBP3EZC7AJANXZVF3D91QYWM', 'Surface Claude Code Workflow runs in the convo + status surfaces', 'Claude Code''s Workflow mechanism (multi-agent orchestration: wf_<id> runs, phases, agent fan-outs, live progress) currently renders in clide as ordinary tool-use cards at best. Integrate it properly: (1) CONVO PANEL — recognise a Workflow tool-use and render a dedicated run card: phase groups, per-agent rows with live status, and the run''s result summary; reuse the collapser/agent-card machinery (T-305/T-342) rather than inventing new chrome. (2) STATUS/SIDEBAR — while a workflow runs, show an aggregate indicator (run id, phase, agents active/done) in the Claude sidebar Activity tab and/or status line.
SPIKE FIRST: capture what the stream-json wire actually emits during a Workflow run (tool_use input shape, progress/notification events, sidechain attribution for workflow-spawned agents) — same probe method as T-410''s. Scope the rendering to what the wire really carries; if progress only exists in the harness UI and not on the wire, document that limit and render what''s available (start/end + result). Filed from user request 2026-06-12.
--- SPIKE FINDINGS (2026-06-12, claude 2.1.175 stream-json probe) ---
ANCHOR: Workflow run is a normal assistant tool_use {name:''Workflow'', input:{script}}. Its tool_result returns IMMEDIATELY: ''Workflow launched in background. Task ID: <id>''. The run is async — today clide shows only the generic tool card + that result, no fan-out.
PROGRESS: carried on type:''system'' events (clide currently drops these), keyed by tool_use_id + task_id:
- task_started: task_id, tool_use_id, description, task_type:''local_workflow'', workflow_name, prompt(script source)
- task_progress (repeated): usage{total_tokens,tool_uses,duration_ms}, summary, workflow_progress[] = per-agent {index,label,model,state:start->progress->done,agentId} DELTAS (partial; merge by index)
- task_updated: patch{status,end_time}
- task_notification: terminal status:''completed'', output_file, summary, usage
LIMITS: (1) workflow agents are NOT sidechain messages (no separate assistant/user wire events, unlike Task) — they exist only as workflow_progress telemetry; existing parentToolUseId nesting does not apply. (2) system events are ephemeral (not in resumed transcript JSONL) — live progress shows during the session; on reload only the tool card + result summary survive.
PLAN: parse system task_* into a WorkflowRun model keyed by tool_use_id in StreamJsonSession; upgrade the Workflow tool_use card to render agent rows + status; add an aggregate active-workflow indicator to the Activity tab. Raw probe wire: /tmp/wf-probe/wire.jsonl.', 'done', 'medium', NULL, NULL, NULL, '2026-06-12 10:25:00', '2026-06-12 14:52:31', NULL, 'b810aebc9678105a6e4e3628810172cb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3EZC7AJANXZVF3D91QYWM', 'epic', NULL, 'Claude sidebar — power control panel + full harness-command capture', 'PROBLEM. clide runs the Claude CLI headless (stream-json). The CLI''s interactive TUI commands don''t exist there: a typed /effort (observed live, 2026-06-12) is forwarded and the CLI answers with a raw system/local_command error — ''/effort isn''t available in this environment.'' Today clide owns only {clear, resume, fork, model} (slash_commands.dart kClideOwnedCommands); every other TUI-only builtin (/permissions /status /cost /help /config /memory /mcp /agents /usage-adjacent…) falls through to that raw error. Meanwhile the sidebar''s Config/Activity tabs (T-182/T-183) are mostly read-only rows — the lone interactions are the permission badge and T-408''s model picker (which lives in the pane''s interaction zone, not the sidebar).
EVIDENCE / GROUNDING. clide already caches the CLI initialize handshake per version (~/.config/clide/claude/init-<version>.json, T-151). The advertised slash_commands list = skills + a small headless-capable builtin set (clear, compact, context, init, review, security-review, usage, …). TUI-only builtins are NOT advertised — that''s the authoritative signal for what forwards safely. Current CLI 2.1.175.
DESIGN — three layers:
1. ROUTING TABLE (one source of truth, replaces kClideOwnedCommands): every command token → forward (advertised by the live probe: skills, compact, context, …) | owned (clide implements natively: clear/resume/fork/model today; effort, permissions, config, memory, mcp, agents, todos, status, help as they land) | unavailable (known TUI-only with no equivalent: doctor, login, logout, terminal-setup, ide, exit → friendly explanation card, never the CLI''s raw error). Probe-driven so new CLI versions stay current without code changes.
2. REACTIVE CATCH-ALL: any system/local_command response whose stdout matches "isn''t available in this environment" renders as a muted hint card (pointing at the matching sidebar control when one exists) instead of raw harness output. This captures FUTURE unknown commands automatically — the guarantee ''nothing surfaces as a raw harness error'', not just a hand-maintained list.
3. SIDEBAR = POWER CONTROL PANEL (D-6 parity): every owned command gets a sidebar interaction and every sidebar control is reachable as a slash command. Config tab gains inline pickers (model T-408, permission T-275, effort new); Activity tab gains session controls (clear/compact/fork/resume) and a usage/cost block (/usage IS advertised in stream-json per the probes — revisit T-158''s upstream blocker).
Child stories carry the implementation slices. Refs: slash_commands.dart, claude_pane._send, claude_meta_sidebar.dart (+T-395 split), claude_config.dart probe, T-408 set_model spike pattern.', 'done', 'high', NULL, NULL, NULL, '2026-06-12 08:58:29', '2026-06-12 19:44:20', NULL, 'ab48816b1f3007884c4b29ac3acd7104', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);