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:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:clide/builtin/claude/src/stream_json_session.dart';
|
||||
import 'package:clide/builtin/claude/src/transcript_reader.dart';
|
||||
import 'package:clide/builtin/claude/src/workflow_run.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
class _FakeProc extends StreamJsonProcess {
|
||||
@@ -945,4 +946,57 @@ void main() {
|
||||
expect(b.lines, ['line 2', 'line 3', 'line 4']);
|
||||
});
|
||||
});
|
||||
|
||||
group('workflow runs (T-416)', () {
|
||||
test('accumulates a run from system task_* events keyed by tool_use_id', () async {
|
||||
final p = _FakeProc();
|
||||
final session = StreamJsonSession(p)..start();
|
||||
final snapshots = <Map<String, WorkflowRun>>[];
|
||||
session.workflowsStream.listen(snapshots.add);
|
||||
|
||||
p.emit(
|
||||
jsonEncode({
|
||||
'type': 'system',
|
||||
'subtype': 'task_started',
|
||||
'task_id': 'wy01fihjt',
|
||||
'tool_use_id': 'toolu_wf',
|
||||
'description': 'Two agents',
|
||||
'workflow_name': 'parallel-words',
|
||||
}),
|
||||
);
|
||||
p.emit(
|
||||
jsonEncode({
|
||||
'type': 'system',
|
||||
'subtype': 'task_progress',
|
||||
'tool_use_id': 'toolu_wf',
|
||||
'workflow_progress': [
|
||||
{'type': 'workflow_agent', 'index': 1, 'label': 'alpha', 'state': 'start'},
|
||||
{'type': 'workflow_agent', 'index': 2, 'label': 'beta', 'state': 'done'},
|
||||
],
|
||||
}),
|
||||
);
|
||||
p.emit(jsonEncode({'type': 'system', 'subtype': 'task_notification', 'tool_use_id': 'toolu_wf', 'status': 'completed', 'summary': 'done'}));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
final run = session.workflows['toolu_wf'];
|
||||
expect(run, isNotNull);
|
||||
expect(run!.name, 'parallel-words');
|
||||
expect(run.agentCount, 2);
|
||||
expect(run.doneCount, 1);
|
||||
expect(run.done, isTrue);
|
||||
expect(run.summary, 'done');
|
||||
expect(snapshots, isNotEmpty);
|
||||
});
|
||||
|
||||
test('a workflow system event produces no conversation item', () async {
|
||||
final p = _FakeProc();
|
||||
final session = StreamJsonSession(p)..start();
|
||||
final items = <ConversationItem>[];
|
||||
session.items.listen(items.add);
|
||||
p.emit(jsonEncode({'type': 'system', 'subtype': 'task_progress', 'tool_use_id': 'toolu_wf', 'workflow_progress': const []}));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(items, isEmpty);
|
||||
expect(session.workflows.containsKey('toolu_wf'), isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user