@synergenius/flow-weaver-pack-weaver 0.9.199 → 0.9.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/dist/ai-chat-provider.js +5 -5
  2. package/dist/ai-chat-provider.js.map +1 -1
  3. package/dist/bot/acceptance-merge.d.ts +21 -0
  4. package/dist/bot/acceptance-merge.d.ts.map +1 -0
  5. package/dist/bot/acceptance-merge.js +46 -0
  6. package/dist/bot/acceptance-merge.js.map +1 -0
  7. package/dist/bot/ai-client.d.ts +14 -2
  8. package/dist/bot/ai-client.d.ts.map +1 -1
  9. package/dist/bot/ai-client.js +71 -24
  10. package/dist/bot/ai-client.js.map +1 -1
  11. package/dist/bot/assistant-tools.js +3 -3
  12. package/dist/bot/assistant-tools.js.map +1 -1
  13. package/dist/bot/audit-logger.d.ts.map +1 -1
  14. package/dist/bot/audit-logger.js +34 -14
  15. package/dist/bot/audit-logger.js.map +1 -1
  16. package/dist/bot/audit-trail.d.ts +67 -0
  17. package/dist/bot/audit-trail.d.ts.map +1 -0
  18. package/dist/bot/audit-trail.js +153 -0
  19. package/dist/bot/audit-trail.js.map +1 -0
  20. package/dist/bot/behavior-defaults.d.ts +1 -1
  21. package/dist/bot/behavior-defaults.d.ts.map +1 -1
  22. package/dist/bot/behavior-defaults.js +7 -3
  23. package/dist/bot/behavior-defaults.js.map +1 -1
  24. package/dist/bot/capability-registry.d.ts +9 -0
  25. package/dist/bot/capability-registry.d.ts.map +1 -1
  26. package/dist/bot/capability-registry.js +81 -27
  27. package/dist/bot/capability-registry.js.map +1 -1
  28. package/dist/bot/capability-types.d.ts +10 -0
  29. package/dist/bot/capability-types.d.ts.map +1 -1
  30. package/dist/bot/cli-provider.d.ts.map +1 -1
  31. package/dist/bot/cli-provider.js +8 -7
  32. package/dist/bot/cli-provider.js.map +1 -1
  33. package/dist/bot/preflight.d.ts +48 -0
  34. package/dist/bot/preflight.d.ts.map +1 -0
  35. package/dist/bot/preflight.js +247 -0
  36. package/dist/bot/preflight.js.map +1 -0
  37. package/dist/bot/provider-shim.d.ts +74 -0
  38. package/dist/bot/provider-shim.d.ts.map +1 -0
  39. package/dist/bot/provider-shim.js +176 -0
  40. package/dist/bot/provider-shim.js.map +1 -0
  41. package/dist/bot/runner.d.ts +2 -0
  42. package/dist/bot/runner.d.ts.map +1 -1
  43. package/dist/bot/runner.js +60 -17
  44. package/dist/bot/runner.js.map +1 -1
  45. package/dist/bot/step-executor.d.ts.map +1 -1
  46. package/dist/bot/step-executor.js +72 -115
  47. package/dist/bot/step-executor.js.map +1 -1
  48. package/dist/bot/swarm-controller.d.ts +2 -0
  49. package/dist/bot/swarm-controller.d.ts.map +1 -1
  50. package/dist/bot/swarm-controller.js +92 -20
  51. package/dist/bot/swarm-controller.js.map +1 -1
  52. package/dist/bot/task-create-handler.d.ts +37 -0
  53. package/dist/bot/task-create-handler.d.ts.map +1 -0
  54. package/dist/bot/task-create-handler.js +124 -0
  55. package/dist/bot/task-create-handler.js.map +1 -0
  56. package/dist/bot/task-store.d.ts +1 -0
  57. package/dist/bot/task-store.d.ts.map +1 -1
  58. package/dist/bot/task-store.js +67 -0
  59. package/dist/bot/task-store.js.map +1 -1
  60. package/dist/bot/types.d.ts +1 -1
  61. package/dist/bot/types.d.ts.map +1 -1
  62. package/dist/bot/weaver-tools.d.ts.map +1 -1
  63. package/dist/bot/weaver-tools.js +7 -39
  64. package/dist/bot/weaver-tools.js.map +1 -1
  65. package/dist/node-types/agent-execute.d.ts +25 -8
  66. package/dist/node-types/agent-execute.d.ts.map +1 -1
  67. package/dist/node-types/agent-execute.js +89 -23
  68. package/dist/node-types/agent-execute.js.map +1 -1
  69. package/dist/node-types/bot-report.d.ts.map +1 -1
  70. package/dist/node-types/bot-report.js +24 -3
  71. package/dist/node-types/bot-report.js.map +1 -1
  72. package/dist/node-types/plan-task.d.ts +8 -17
  73. package/dist/node-types/plan-task.d.ts.map +1 -1
  74. package/dist/node-types/plan-task.js +217 -256
  75. package/dist/node-types/plan-task.js.map +1 -1
  76. package/dist/node-types/review-result.js +8 -6
  77. package/dist/node-types/review-result.js.map +1 -1
  78. package/dist/palindrome.d.ts +9 -0
  79. package/dist/palindrome.d.ts.map +1 -0
  80. package/dist/palindrome.js +14 -0
  81. package/dist/palindrome.js.map +1 -0
  82. package/dist/ui/approval-card.js +91 -82
  83. package/dist/ui/bot-activity.js +73 -56
  84. package/dist/ui/bot-config.js +48 -31
  85. package/dist/ui/bot-dashboard.js +52 -36
  86. package/dist/ui/bot-panel.js +230 -228
  87. package/dist/ui/bot-slot-card.js +100 -90
  88. package/dist/ui/bot-status.js +37 -15
  89. package/dist/ui/budget-bar.js +57 -31
  90. package/dist/ui/capability-editor.js +447 -378
  91. package/dist/ui/chat-task-result.js +78 -71
  92. package/dist/ui/decision-log.js +68 -81
  93. package/dist/ui/genesis-block.js +86 -95
  94. package/dist/ui/instance-stream-view.js +722 -0
  95. package/dist/ui/profile-card.js +96 -221
  96. package/dist/ui/profile-editor.js +532 -575
  97. package/dist/ui/settings-section.js +41 -45
  98. package/dist/ui/swarm-controls.js +212 -135
  99. package/dist/ui/swarm-dashboard.js +3992 -2715
  100. package/dist/ui/task-detail-view.js +415 -521
  101. package/dist/ui/task-editor.js +339 -390
  102. package/dist/ui/task-pool-list.js +60 -55
  103. package/dist/workflows/src/palindrome.d.ts +11 -0
  104. package/dist/workflows/src/palindrome.d.ts.map +1 -0
  105. package/dist/workflows/src/palindrome.js +16 -0
  106. package/dist/workflows/src/palindrome.js.map +1 -0
  107. package/dist/workflows/tests/palindrome.test.d.ts +2 -0
  108. package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
  109. package/dist/workflows/tests/palindrome.test.js +41 -0
  110. package/dist/workflows/tests/palindrome.test.js.map +1 -0
  111. package/dist/workflows/weaver-bot-batch.js +1 -1
  112. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  113. package/dist/workflows/weaver-bot.js +1 -1
  114. package/dist/workflows/weaver-bot.js.map +1 -1
  115. package/flowweaver.manifest.json +1 -1
  116. package/package.json +8 -2
  117. package/src/ai-chat-provider.ts +5 -5
  118. package/src/bot/acceptance-merge.ts +62 -0
  119. package/src/bot/ai-client.ts +77 -21
  120. package/src/bot/assistant-tools.ts +3 -3
  121. package/src/bot/audit-logger.ts +42 -14
  122. package/src/bot/audit-trail.ts +211 -0
  123. package/src/bot/behavior-defaults.ts +7 -2
  124. package/src/bot/capability-registry.ts +84 -28
  125. package/src/bot/capability-types.ts +11 -0
  126. package/src/bot/cli-provider.ts +8 -7
  127. package/src/bot/preflight.ts +285 -0
  128. package/src/bot/provider-shim.ts +218 -0
  129. package/src/bot/runner.ts +68 -20
  130. package/src/bot/step-executor.ts +69 -127
  131. package/src/bot/swarm-controller.ts +94 -20
  132. package/src/bot/task-create-handler.ts +164 -0
  133. package/src/bot/task-store.ts +83 -0
  134. package/src/bot/types.ts +4 -1
  135. package/src/bot/weaver-tools.ts +7 -45
  136. package/src/node-types/agent-execute.ts +102 -16
  137. package/src/node-types/bot-report.ts +24 -3
  138. package/src/node-types/plan-task.ts +238 -280
  139. package/src/node-types/review-result.ts +8 -6
  140. package/src/palindrome.ts +14 -0
  141. package/src/ui/approval-card.tsx +78 -62
  142. package/src/ui/bot-activity.tsx +12 -10
  143. package/src/ui/bot-config.tsx +12 -10
  144. package/src/ui/bot-dashboard.tsx +13 -11
  145. package/src/ui/bot-panel.tsx +189 -171
  146. package/src/ui/bot-slot-card.tsx +125 -70
  147. package/src/ui/bot-status.tsx +4 -4
  148. package/src/ui/budget-bar.tsx +86 -25
  149. package/src/ui/capability-editor.tsx +392 -257
  150. package/src/ui/chat-task-result.tsx +81 -78
  151. package/src/ui/decision-log.tsx +76 -73
  152. package/src/ui/genesis-block.tsx +91 -61
  153. package/src/ui/instance-stream-view.tsx +861 -0
  154. package/src/ui/profile-card.tsx +195 -168
  155. package/src/ui/profile-editor.tsx +453 -370
  156. package/src/ui/settings-section.tsx +46 -39
  157. package/src/ui/swarm-controls.tsx +252 -123
  158. package/src/ui/swarm-dashboard.tsx +999 -466
  159. package/src/ui/task-detail-view.tsx +485 -428
  160. package/src/ui/task-editor.tsx +329 -271
  161. package/src/ui/task-pool-list.tsx +68 -62
  162. package/src/workflows/src/palindrome.ts +16 -0
  163. package/src/workflows/tests/palindrome.test.ts +49 -0
  164. package/src/workflows/weaver-bot-batch.ts +1 -1
  165. package/src/workflows/weaver-bot.ts +1 -1
  166. package/dist/ui/bot-constants.d.ts +0 -14
  167. package/dist/ui/bot-constants.d.ts.map +0 -1
  168. package/dist/ui/bot-constants.js +0 -189
  169. package/dist/ui/bot-constants.js.map +0 -1
  170. package/dist/ui/steer-api.d.ts +0 -7
  171. package/dist/ui/steer-api.d.ts.map +0 -1
  172. package/dist/ui/steer-api.js +0 -11
  173. package/dist/ui/steer-api.js.map +0 -1
  174. package/dist/ui/trace-to-timeline.d.ts +0 -91
  175. package/dist/ui/trace-to-timeline.d.ts.map +0 -1
  176. package/dist/ui/trace-to-timeline.js +0 -116
  177. package/dist/ui/trace-to-timeline.js.map +0 -1
  178. package/dist/ui/use-stream-timeline.d.ts +0 -50
  179. package/dist/ui/use-stream-timeline.d.ts.map +0 -1
  180. package/dist/ui/use-stream-timeline.js +0 -245
  181. package/dist/ui/use-stream-timeline.js.map +0 -1
@@ -0,0 +1,861 @@
1
+ /**
2
+ * InstanceStreamView — real-time execution viewer for a single bot instance.
3
+ *
4
+ * Reached by clicking an executing bot card in the Swarm Dashboard Bots tab.
5
+ * Shows a conversation-style AI stream: thinking → text → tool calls → text,
6
+ * with content growing progressively as events arrive.
7
+ */
8
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
9
+ import {
10
+ Flex, Typography, ScrollArea, StatusIcon, Icon, IconButton, Chip, TaskBlock, Button,
11
+ CollapsibleBlock, EmptyState, MarkdownContent, FadeIn, Divider, Section, SectionTitle,
12
+ Table, toast, usePackWorkspace,
13
+ } from '@fw/plugin-ui-kit';
14
+
15
+ import { traceToTimeline } from './trace-to-timeline';
16
+ import type { HistoricalRun } from './trace-to-timeline';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Types
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface InstanceStreamViewProps {
23
+ instanceId: string;
24
+ profileId: string;
25
+ profileName?: string;
26
+ profileIcon?: string;
27
+ profileColor?: string;
28
+ currentRunId?: string;
29
+ currentTaskId?: string;
30
+ onBack: () => void;
31
+ }
32
+
33
+ interface TaskInfo {
34
+ id: string;
35
+ title: string;
36
+ description?: string;
37
+ status: string;
38
+ priority: number;
39
+ assignedProfile?: string;
40
+ }
41
+
42
+ interface StreamEvent {
43
+ id: number;
44
+ type: string;
45
+ timestamp: number;
46
+ taskId?: string;
47
+ data?: Record<string, unknown>;
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Conversation block types — accumulated from stream events
52
+ // ---------------------------------------------------------------------------
53
+
54
+ interface ThinkingBlock {
55
+ kind: 'thinking';
56
+ content: string;
57
+ streaming: boolean;
58
+ }
59
+
60
+ interface TextBlock {
61
+ kind: 'text';
62
+ content: string;
63
+ streaming: boolean;
64
+ }
65
+
66
+ interface ToolCallBlock {
67
+ kind: 'tool_call';
68
+ id: string;
69
+ name: string;
70
+ status: 'running' | 'completed' | 'error';
71
+ result?: string;
72
+ isError?: boolean;
73
+ }
74
+
75
+ interface StatusBlock {
76
+ kind: 'status';
77
+ label: string;
78
+ variant: 'started' | 'completed' | 'failed';
79
+ detail?: string;
80
+ }
81
+
82
+ type ConversationBlock = ThinkingBlock | TextBlock | ToolCallBlock | StatusBlock;
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Build conversation blocks from events
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function buildConversation(events: StreamEvent[]): ConversationBlock[] {
89
+ const blocks: ConversationBlock[] = [];
90
+ let thinkingBuf = '';
91
+ let textBuf = '';
92
+ let lastType = '';
93
+ const toolMap = new Map<string, number>(); // toolId → index in blocks
94
+
95
+ const flushThinking = (streaming: boolean) => {
96
+ if (thinkingBuf) {
97
+ blocks.push({ kind: 'thinking', content: thinkingBuf, streaming });
98
+ thinkingBuf = '';
99
+ }
100
+ };
101
+
102
+ const flushText = (streaming: boolean) => {
103
+ if (textBuf) {
104
+ blocks.push({ kind: 'text', content: textBuf, streaming });
105
+ textBuf = '';
106
+ }
107
+ };
108
+
109
+ for (let i = 0; i < events.length; i++) {
110
+ const e = events[i]!;
111
+ const d = e.data ?? {};
112
+ const isLast = i === events.length - 1;
113
+
114
+ switch (e.type) {
115
+ case 'thinking_delta':
116
+ if (lastType !== 'thinking_delta') flushText(false);
117
+ thinkingBuf += (d.text as string) ?? '';
118
+ lastType = 'thinking_delta';
119
+ if (isLast) flushThinking(true); // still streaming
120
+ break;
121
+
122
+ case 'thinking_done':
123
+ flushThinking(false);
124
+ lastType = 'thinking_done';
125
+ break;
126
+
127
+ case 'text_delta':
128
+ if (lastType === 'thinking_delta') flushThinking(false);
129
+ textBuf += (d.text as string) ?? '';
130
+ lastType = 'text_delta';
131
+ if (isLast) flushText(true); // still streaming
132
+ break;
133
+
134
+ case 'tool_use_start': {
135
+ flushThinking(false);
136
+ flushText(false);
137
+ const toolId = (d.id as string) ?? `tool-${i}`;
138
+ const idx = blocks.length;
139
+ toolMap.set(toolId, idx);
140
+ blocks.push({
141
+ kind: 'tool_call',
142
+ id: toolId,
143
+ name: (d.name as string) ?? 'unknown',
144
+ status: 'running',
145
+ });
146
+ lastType = 'tool_use_start';
147
+ break;
148
+ }
149
+
150
+ case 'tool_result': {
151
+ const toolId = (d.id as string) ?? '';
152
+ const idx = toolMap.get(toolId);
153
+ if (idx != null && blocks[idx]?.kind === 'tool_call') {
154
+ const block = blocks[idx] as ToolCallBlock;
155
+ block.status = (d.isError as boolean) ? 'error' : 'completed';
156
+ block.result = (d.result as string) ?? '';
157
+ block.isError = (d.isError as boolean) ?? false;
158
+ }
159
+ lastType = 'tool_result';
160
+ break;
161
+ }
162
+
163
+ case 'bot-started':
164
+ blocks.push({
165
+ kind: 'status',
166
+ label: 'Task started',
167
+ variant: 'started',
168
+ detail: (d.instruction as string) ?? undefined,
169
+ });
170
+ lastType = 'bot-started';
171
+ break;
172
+
173
+ case 'bot-completed':
174
+ flushThinking(false);
175
+ flushText(false);
176
+ blocks.push({
177
+ kind: 'status',
178
+ label: (d.success as boolean) ? 'Task completed' : 'Task finished',
179
+ variant: 'completed',
180
+ detail: (d.report as string) ?? (d.summary as string) ?? undefined,
181
+ });
182
+ lastType = 'bot-completed';
183
+ break;
184
+
185
+ case 'bot-failed':
186
+ flushThinking(false);
187
+ flushText(false);
188
+ blocks.push({
189
+ kind: 'status',
190
+ label: 'Task failed',
191
+ variant: 'failed',
192
+ detail: (d.error as string) ?? undefined,
193
+ });
194
+ lastType = 'bot-failed';
195
+ break;
196
+
197
+ // ── Swarm/workflow-level events (emitted by runner + swarm-controller) ──
198
+
199
+ case 'task-claimed':
200
+ blocks.push({
201
+ kind: 'status',
202
+ label: `Task claimed by ${(d.profileId as string) ?? 'bot'}`,
203
+ variant: 'started',
204
+ });
205
+ lastType = 'task-claimed';
206
+ break;
207
+
208
+ case 'task-done':
209
+ flushThinking(false);
210
+ flushText(false);
211
+ blocks.push({
212
+ kind: 'status',
213
+ label: (d.outcome as string) === 'completed' ? 'Task completed' : `Task ${(d.outcome as string) ?? 'done'}`,
214
+ variant: (d.outcome as string) === 'completed' ? 'completed' : 'failed',
215
+ });
216
+ lastType = 'task-done';
217
+ break;
218
+
219
+ case 'task-run-error':
220
+ flushThinking(false);
221
+ flushText(false);
222
+ blocks.push({
223
+ kind: 'status',
224
+ label: 'Run error',
225
+ variant: 'failed',
226
+ detail: (d.error as string) ?? undefined,
227
+ });
228
+ lastType = 'task-run-error';
229
+ break;
230
+
231
+ // Workflow node events — shown as tool-call-like blocks when no
232
+ // conversation-level events are available (non-CLI providers)
233
+ case 'node-start': {
234
+ flushThinking(false);
235
+ flushText(false);
236
+ const nodeId = (d.nodeId as string) ?? `node-${i}`;
237
+ const idx = blocks.length;
238
+ toolMap.set(`node:${nodeId}`, idx);
239
+ blocks.push({
240
+ kind: 'tool_call',
241
+ id: `node:${nodeId}`,
242
+ name: (d.label as string) ?? (d.nodeType as string) ?? nodeId,
243
+ status: 'running',
244
+ });
245
+ lastType = 'node-start';
246
+ break;
247
+ }
248
+
249
+ case 'node-complete': {
250
+ const nodeId = (d.nodeId as string) ?? '';
251
+ const idx = toolMap.get(`node:${nodeId}`);
252
+ if (idx != null && blocks[idx]?.kind === 'tool_call') {
253
+ const block = blocks[idx] as ToolCallBlock;
254
+ block.status = 'completed';
255
+ const dur = d.durationMs as number | undefined;
256
+ block.result = dur ? `Completed in ${dur}ms` : 'Completed';
257
+ }
258
+ lastType = 'node-complete';
259
+ break;
260
+ }
261
+
262
+ case 'node-error': {
263
+ const nodeId = (d.nodeId as string) ?? '';
264
+ const idx = toolMap.get(`node:${nodeId}`);
265
+ if (idx != null && blocks[idx]?.kind === 'tool_call') {
266
+ const block = blocks[idx] as ToolCallBlock;
267
+ block.status = 'error';
268
+ block.result = (d.error as string) ?? 'Failed';
269
+ block.isError = true;
270
+ }
271
+ lastType = 'node-error';
272
+ break;
273
+ }
274
+
275
+ default:
276
+ break;
277
+ }
278
+ }
279
+
280
+ // Flush any remaining buffer
281
+ if (thinkingBuf) flushThinking(true);
282
+ if (textBuf) flushText(true);
283
+
284
+ return blocks;
285
+ }
286
+
287
+ // ---------------------------------------------------------------------------
288
+ // StreamIndicator — subtle conversational state indicator
289
+ // Uses Divider (with label) for ended state, StatusIcon for others.
290
+ // ---------------------------------------------------------------------------
291
+
292
+ type StreamState = 'waiting' | 'connected' | 'ended' | 'error';
293
+
294
+ const STREAM_STATUS_MAP: Record<StreamState, 'pending' | 'running' | 'completed' | 'failed'> = {
295
+ waiting: 'pending',
296
+ connected: 'running',
297
+ ended: 'completed',
298
+ error: 'failed',
299
+ };
300
+
301
+ const STREAM_DEFAULT_MSG: Record<StreamState, string> = {
302
+ waiting: 'Waiting for bot',
303
+ connected: 'Connected',
304
+ ended: 'End of stream',
305
+ error: 'Connection lost',
306
+ };
307
+
308
+ function StreamIndicator({ state, message }: { state: StreamState; message?: string }) {
309
+ const label = message ?? STREAM_DEFAULT_MSG[state];
310
+
311
+ if (state === 'ended') {
312
+ return (
313
+ <Section padding="compact">
314
+ <Divider label={label} />
315
+ </Section>
316
+ );
317
+ }
318
+
319
+ return (
320
+ <Section padding="default">
321
+ <Flex variant="row-center-center-nowrap-6">
322
+ <StatusIcon
323
+ status={STREAM_STATUS_MAP[state]}
324
+ size="xs"
325
+ pulsing={state === 'waiting' || state === 'connected'}
326
+ />
327
+ <Typography
328
+ variant="smallCaption-regular"
329
+ color={state === 'error' ? 'color-status-negative' : 'color-text-subtle'}
330
+ >
331
+ {label}
332
+ </Typography>
333
+ </Flex>
334
+ </Section>
335
+ );
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Typewriter config
340
+ // ---------------------------------------------------------------------------
341
+
342
+ const TYPEWRITER = {
343
+ charsPerTick: 2,
344
+ intervalMs: 20,
345
+ cursor: '\u258F',
346
+ maxToolResultLen: 500,
347
+ };
348
+
349
+ // ---------------------------------------------------------------------------
350
+ // useConversationTypewriter — single sequential typewriter for all blocks.
351
+ // Returns { getBlockText, isBlockActive, isBlockDone } keyed by block index.
352
+ // Only one block types at a time; previous blocks show full content.
353
+ // ---------------------------------------------------------------------------
354
+
355
+ function useConversationTypewriter(blocks: ConversationBlock[]) {
356
+ const [activeIdx, setActiveIdx] = useState(0);
357
+ const [charPos, setCharPos] = useState(0);
358
+ const activeIdxRef = useRef(0);
359
+ const charPosRef = useRef(0);
360
+ activeIdxRef.current = activeIdx;
361
+ charPosRef.current = charPos;
362
+
363
+ // Get the text content length of a block
364
+ const getBlockLen = useCallback((block: ConversationBlock): number => {
365
+ switch (block.kind) {
366
+ case 'thinking': return block.content.length;
367
+ case 'text': return block.content.length;
368
+ case 'tool_call': {
369
+ if (block.status === 'running') return 0; // no content to type yet
370
+ const display = block.result ?? '';
371
+ return Math.min(display.length, TYPEWRITER.maxToolResultLen);
372
+ }
373
+ case 'status': return (block.detail ?? '').length;
374
+ default: return 0;
375
+ }
376
+ }, []);
377
+
378
+ // Tick: advance charPos, or move to next block when done
379
+ useEffect(() => {
380
+ const timer = setInterval(() => {
381
+ const idx = activeIdxRef.current;
382
+ if (idx >= blocks.length) return;
383
+
384
+ const block = blocks[idx]!;
385
+ const targetLen = getBlockLen(block);
386
+
387
+ // Tool call running — wait here until result arrives (don't skip)
388
+ if (block.kind === 'tool_call' && (block as ToolCallBlock).status === 'running') return;
389
+
390
+ // Skip blocks with no typeable content (status with no detail)
391
+ if (targetLen === 0 && block.kind !== 'tool_call') {
392
+ setActiveIdx(idx + 1);
393
+ setCharPos(0);
394
+ return;
395
+ }
396
+
397
+ const pos = charPosRef.current;
398
+ if (pos >= targetLen) {
399
+ // Block done — still streaming content? Stay. Otherwise advance.
400
+ if (block.kind === 'thinking' && (block as ThinkingBlock).streaming) return;
401
+ if (block.kind === 'text' && (block as TextBlock).streaming) return;
402
+ // Advance to next
403
+ setActiveIdx(idx + 1);
404
+ setCharPos(0);
405
+ } else {
406
+ const speed = 1;
407
+ const chars = Math.max(1, Math.round(TYPEWRITER.charsPerTick * speed));
408
+ setCharPos(Math.min(pos + chars, targetLen));
409
+ }
410
+ }, TYPEWRITER.intervalMs);
411
+ return () => clearInterval(timer);
412
+ }, [blocks, getBlockLen]);
413
+
414
+ // When new blocks appear beyond activeIdx, ensure we catch up if current is done
415
+ useEffect(() => {
416
+ const idx = activeIdx;
417
+ if (idx < blocks.length) {
418
+ const block = blocks[idx]!;
419
+ const targetLen = getBlockLen(block);
420
+ if (targetLen === 0 || charPos >= targetLen) {
421
+ const isStreaming = (block.kind === 'thinking' || block.kind === 'text')
422
+ && (block as ThinkingBlock | TextBlock).streaming;
423
+ if (!isStreaming && idx < blocks.length - 1) {
424
+ setActiveIdx(idx + 1);
425
+ setCharPos(0);
426
+ }
427
+ }
428
+ }
429
+ }, [blocks.length]);
430
+
431
+ return { activeIdx, charPos, getBlockLen };
432
+ }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Block renderers that accept revealed length
436
+ // ---------------------------------------------------------------------------
437
+
438
+ function RevealedMarkdown({ content, revealedLen, showCursor }: {
439
+ content: string; revealedLen: number; showCursor: boolean;
440
+ }) {
441
+ const visibleText = content.slice(0, revealedLen);
442
+ return (
443
+ <Flex inline>
444
+ <MarkdownContent content={visibleText + (showCursor ? TYPEWRITER.cursor : '')} />
445
+ </Flex>
446
+ );
447
+ }
448
+
449
+ function RevealedToolResult({ result, revealedLen, isError }: {
450
+ result: string; revealedLen: number; isError?: boolean;
451
+ }) {
452
+ const display = result.length > TYPEWRITER.maxToolResultLen
453
+ ? result.slice(0, TYPEWRITER.maxToolResultLen) + '...'
454
+ : result;
455
+ const visibleText = display.slice(0, revealedLen);
456
+ return (
457
+ <Section
458
+ padding="compact"
459
+ background="surface"
460
+ border={isError ? 'none' : 'top'}
461
+ >
462
+ <Typography
463
+ variant="smallCaption-regular"
464
+ color={isError ? 'color-status-negative' : 'color-text-medium'}
465
+ mono
466
+ >
467
+ {visibleText || '\u200B'}
468
+ </Typography>
469
+ </Section>
470
+ );
471
+ }
472
+
473
+ // ---------------------------------------------------------------------------
474
+ // Tool name formatting
475
+ // ---------------------------------------------------------------------------
476
+
477
+ function formatToolName(name: string): string {
478
+ return name.replace(/^fw_/, '').replace(/_/g, ' ');
479
+ }
480
+
481
+ // ---------------------------------------------------------------------------
482
+ // Component
483
+ // ---------------------------------------------------------------------------
484
+
485
+ function InstanceStreamView({
486
+ instanceId,
487
+ profileId,
488
+ profileName,
489
+ profileIcon,
490
+ profileColor,
491
+ currentRunId,
492
+ currentTaskId,
493
+ onBack,
494
+ }: InstanceStreamViewProps) {
495
+ const ctx = usePackWorkspace();
496
+ const { callTool } = ctx;
497
+
498
+ // ── Redirect panel state ──
499
+ const [showRedirect, setShowRedirect] = useState(false);
500
+ const [openTasks, setOpenTasks] = useState<Array<{ id: string; title: string; priority: number }>>([]);
501
+ const [redirecting, setRedirecting] = useState(false);
502
+
503
+ // ── Task context ──
504
+ const [task, setTask] = useState<TaskInfo | null>(null);
505
+
506
+ useEffect(() => {
507
+ if (!currentTaskId) return;
508
+ callTool('fw_weaver_task_get', { id: currentTaskId }).then((raw: unknown) => {
509
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
510
+ const t = (data as Record<string, unknown>)?.task ?? data;
511
+ if (t && typeof t === 'object') setTask(t as TaskInfo);
512
+ }).catch(() => {});
513
+ }, [callTool, currentTaskId]);
514
+
515
+ // ── Run history for this instance ──
516
+ const [history, setHistory] = useState<HistoricalRun[]>([]);
517
+
518
+ useEffect(() => {
519
+ callTool('fw_weaver_history', { instanceId, limit: 10 }).then((raw: unknown) => {
520
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
521
+ if (Array.isArray(data)) {
522
+ setHistory(data.map((r: Record<string, unknown>) => {
523
+ const costObj = r.cost && typeof r.cost === 'object' ? (r.cost as Record<string, unknown>) : undefined;
524
+ return { ...r, costDetail: costObj, cost: costObj?.totalCost ?? r.cost } as unknown as HistoricalRun;
525
+ }));
526
+ }
527
+ }).catch(() => {});
528
+ }, [callTool, instanceId]);
529
+
530
+ // ── Live streaming ──
531
+ const stream = ctx.createEventStream();
532
+ const isLive = !!currentRunId;
533
+
534
+ useEffect(() => {
535
+ if (!isLive || !currentRunId) return;
536
+ stream.start(ctx.packId, 'fw_weaver_events', currentRunId);
537
+ return () => stream.stop();
538
+ }, [isLive, currentRunId, ctx.packId]);
539
+
540
+ // ── Build conversation from stream events ──
541
+ const conversation = useMemo<ConversationBlock[]>(
542
+ () => buildConversation(stream.events as StreamEvent[]),
543
+ [stream.events],
544
+ );
545
+
546
+ // ── Sequential typewriter ──
547
+ const { activeIdx, charPos, getBlockLen } = useConversationTypewriter(conversation);
548
+
549
+ // ── Auto-scroll ──
550
+ const scrollRef = useRef<HTMLDivElement>(null);
551
+ useEffect(() => {
552
+ const el = scrollRef.current;
553
+ if (el) el.scrollTop = el.scrollHeight;
554
+ }, [conversation.length, stream.events.length]);
555
+
556
+ // ── Thinking expand state ──
557
+ const [thinkingExpanded, setThinkingExpanded] = useState<Record<number, boolean>>({});
558
+
559
+ // ── Run expansion state (for history) ──
560
+ const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
561
+
562
+ const runItems = useMemo(() => {
563
+ return history.map((run: HistoricalRun) => ({
564
+ run,
565
+ runTimeline: traceToTimeline(run),
566
+ }));
567
+ }, [history]);
568
+
569
+ function extractInstruction(run: HistoricalRun): string {
570
+ if (run.instruction) {
571
+ const titleMatch = run.instruction.match(/^##\s*Task:\s*(.+)/m);
572
+ if (titleMatch) return titleMatch[1].trim().slice(0, 120);
573
+ return run.instruction.split('\n')[0].trim().slice(0, 120);
574
+ }
575
+ return run.summary?.slice(0, 120) ?? task?.title ?? 'Bot run';
576
+ }
577
+
578
+ // ── Status display ──
579
+ const statusIcon = isLive ? 'running' : 'pending';
580
+ const statusLabel = isLive ? 'Executing' : 'Idle';
581
+
582
+ return (
583
+ <Flex variant="column-stretch-start-nowrap-0" fill>
584
+ {/* ── Header ── */}
585
+ <Section padding="default" border="bottom" shrink>
586
+ <Flex variant="column-stretch-start-nowrap-6">
587
+ <Flex variant="row-center-space-between-nowrap-8">
588
+ <Flex variant="row-center-start-nowrap-8">
589
+ <IconButton icon="back" size="xs" variant="clear" onClick={onBack} />
590
+ <Icon
591
+ name={profileIcon || 'smartToy'}
592
+ size={16}
593
+ color={profileColor || 'color-text-medium'}
594
+ />
595
+ <Typography variant="caption-thick" color="color-text-high">
596
+ {profileName || profileId}
597
+ </Typography>
598
+ <StatusIcon status={statusIcon} size="sm" />
599
+ <Typography
600
+ variant="smallCaption-regular"
601
+ color={isLive ? 'color-brand-main' : 'color-text-subtle'}
602
+ >
603
+ {statusLabel}
604
+ </Typography>
605
+
606
+ {/* Redirect button (only when executing) */}
607
+ {isLive && (
608
+ <IconButton
609
+ icon="swapHoriz"
610
+ size="xs"
611
+ variant="clear"
612
+ title="Redirect to another task"
613
+ onClick={() => {
614
+ if (showRedirect) { setShowRedirect(false); return; }
615
+ callTool('fw_weaver_task_list', { status: 'open' }).then((raw: unknown) => {
616
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
617
+ if (Array.isArray(data)) setOpenTasks(data.filter((t: Record<string, unknown>) => !t.isParent && t.id !== currentTaskId));
618
+ }).catch(() => {});
619
+ setShowRedirect(true);
620
+ }}
621
+ />
622
+ )}
623
+ </Flex>
624
+ </Flex>
625
+ {task && (
626
+ <Flex variant="column-stretch-start-nowrap-2">
627
+ <Typography variant="caption-regular" color="color-text-medium">
628
+ {task.title}
629
+ </Typography>
630
+ {task.description && (
631
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
632
+ {task.description.length > 200 ? task.description.slice(0, 197) + '...' : task.description}
633
+ </Typography>
634
+ )}
635
+ </Flex>
636
+ )}
637
+ {!isLive && <StreamIndicator state="ended" message="No active run — instance is idle" />}
638
+ </Flex>
639
+ </Section>
640
+
641
+ {/* ── Redirect panel (inline, slides in below header) ── */}
642
+ {showRedirect && (
643
+ <Section padding="compact" border="bottom" shrink>
644
+ <Flex variant="column-stretch-start-nowrap-8">
645
+ <SectionTitle size="xs">Redirect to Task</SectionTitle>
646
+ {openTasks.length > 0 ? (
647
+ <Table
648
+ size="compact"
649
+ getRowKey={(row: Record<string, unknown>) => row.id as string}
650
+ onRowClick={async (row: Record<string, unknown>) => {
651
+ setRedirecting(true);
652
+ try {
653
+ await callTool('fw_weaver_steer', { botId: instanceId, command: 'redirect', payload: row.id });
654
+ toast(`Redirected to "${row.title}"`, { type: 'success' });
655
+ setShowRedirect(false);
656
+ } catch (err: unknown) {
657
+ toast(err instanceof Error ? err.message : 'Failed to redirect', { type: 'error' });
658
+ }
659
+ setRedirecting(false);
660
+ }}
661
+ columns={[
662
+ { key: 'title', header: 'Task' },
663
+ { key: 'priority', header: 'P', width: '30px', align: 'center' as const },
664
+ ]}
665
+ data={openTasks as unknown as Array<Record<string, unknown>>}
666
+ />
667
+ ) : (
668
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
669
+ No open tasks
670
+ </Typography>
671
+ )}
672
+ <Button size="xs" variant="clear" color="secondary" onClick={() => setShowRedirect(false)}>
673
+ Cancel
674
+ </Button>
675
+ </Flex>
676
+ </Section>
677
+ )}
678
+
679
+ {/* ── Scrollable conversation ── */}
680
+ <ScrollArea ref={scrollRef}>
681
+ <Section padding="default">
682
+
683
+ {/* ── Live conversation stream ── */}
684
+ {isLive && conversation.length > 0 && (
685
+ <Flex variant="column-stretch-start-nowrap-8" marginBottom="default">
686
+ {conversation.map((block: ConversationBlock, i: number) => {
687
+ // Sequencer: blocks before activeIdx show full content, activeIdx types, future blocks hidden
688
+ const isDone = i < activeIdx;
689
+ const isActive = i === activeIdx;
690
+ const isFuture = i > activeIdx;
691
+ if (isFuture) return null;
692
+
693
+ const revealedLen = isDone ? getBlockLen(block) : isActive ? charPos : 0;
694
+ const showCursor = isActive && revealedLen < getBlockLen(block);
695
+
696
+ const inner = (() => {
697
+ switch (block.kind) {
698
+ case 'thinking': {
699
+ const isExpanded = thinkingExpanded[i] ?? (isActive || block.streaming);
700
+ const fullContent = block.content;
701
+ return (
702
+ <CollapsibleBlock
703
+ status="thinking"
704
+ label={(isActive || block.streaming) ? 'Thinking...' : 'Thought'}
705
+ expanded={isExpanded}
706
+ onToggle={() => setThinkingExpanded((prev: Record<number, boolean>) => ({ ...prev, [i]: !isExpanded }))}
707
+ >
708
+ {fullContent ? (
709
+ <Section padding="compact" background="surface">
710
+ <RevealedMarkdown
711
+ content={fullContent}
712
+ revealedLen={revealedLen}
713
+ showCursor={showCursor}
714
+ />
715
+ </Section>
716
+ ) : undefined}
717
+ </CollapsibleBlock>
718
+ );
719
+ }
720
+ case 'text':
721
+ return (
722
+ <Section padding="minimal">
723
+ <RevealedMarkdown
724
+ content={block.content}
725
+ revealedLen={revealedLen}
726
+ showCursor={showCursor}
727
+ />
728
+ </Section>
729
+ );
730
+ case 'tool_call': {
731
+ const hasResult = !!block.result;
732
+ const isRunning = block.status === 'running';
733
+ return (
734
+ <CollapsibleBlock
735
+ status={block.status}
736
+ label={
737
+ <Typography variant="caption-regular" color="color-text-high" mono>
738
+ {formatToolName(block.name)}
739
+ </Typography>
740
+ }
741
+ headerSuffix={
742
+ isRunning
743
+ ? <Typography variant="smallCaption-regular" color="color-text-subtle">running...</Typography>
744
+ : block.isError
745
+ ? <Typography variant="smallCaption-regular" color="color-status-negative">failed</Typography>
746
+ : null
747
+ }
748
+ expanded
749
+ canExpand={hasResult}
750
+ onToggle={() => {}}
751
+ >
752
+ {isRunning && (
753
+ <Section padding="compact" background="surface" border="top">
754
+ <Typography variant="smallCaption-regular" color="color-text-subtle" mono>...</Typography>
755
+ </Section>
756
+ )}
757
+ {hasResult && (
758
+ <RevealedToolResult
759
+ result={block.result!}
760
+ revealedLen={revealedLen}
761
+ isError={block.isError}
762
+ />
763
+ )}
764
+ </CollapsibleBlock>
765
+ );
766
+ }
767
+ case 'status': {
768
+ const statusMap: Record<string, string> = { started: 'running', completed: 'completed', failed: 'error' };
769
+ return (
770
+ <CollapsibleBlock
771
+ status={statusMap[block.variant] ?? 'running'}
772
+ label={block.label}
773
+ expanded={!!(block.detail && (isDone || revealedLen > 0))}
774
+ canExpand={!!block.detail}
775
+ onToggle={() => {}}
776
+ >
777
+ {block.detail && (
778
+ <Section padding="compact">
779
+ <RevealedMarkdown
780
+ content={block.detail}
781
+ revealedLen={revealedLen}
782
+ showCursor={showCursor}
783
+ />
784
+ </Section>
785
+ )}
786
+ </CollapsibleBlock>
787
+ );
788
+ }
789
+ default:
790
+ return null;
791
+ }
792
+ })();
793
+
794
+ if (!inner) return null;
795
+ return (
796
+ <FadeIn key={`block-${i}`} visible duration={0.3} slide={8}>
797
+ {inner}
798
+ </FadeIn>
799
+ );
800
+ })}
801
+ </Flex>
802
+ )}
803
+
804
+ {/* ── Waiting for events ── */}
805
+ {isLive && conversation.length === 0 && stream.isStreaming && (
806
+ <StreamIndicator state="waiting" />
807
+ )}
808
+
809
+ {/* ── Stream error ── */}
810
+ {isLive && stream.error && (
811
+ <StreamIndicator state="error" message={stream.error} />
812
+ )}
813
+
814
+ {/* ── Stream ended indicator ── */}
815
+ {isLive && stream.isDone && conversation.length > 0 && (
816
+ <StreamIndicator state="ended" />
817
+ )}
818
+
819
+ {/* ── Run history section ── */}
820
+ {runItems.length > 0 && (
821
+ <Flex variant="column-stretch-start-nowrap-4">
822
+ <SectionTitle size="xs">{`History (${runItems.length})`}</SectionTitle>
823
+ {runItems.map(({ run, runTimeline }: { run: HistoricalRun; runTimeline: Array<Record<string, unknown>> }) => {
824
+ const runId = run.id;
825
+ const isExpanded = expandedRunId === runId;
826
+ const isSuccess = run.outcome === 'completed' || run.success === true;
827
+ return (
828
+ <TaskBlock
829
+ key={`run-${runId}`}
830
+ state={isSuccess ? 'completed' : 'failed'}
831
+ instruction={extractInstruction(run)}
832
+ timeline={runTimeline}
833
+ report={run.report ?? null}
834
+ cost={typeof run.cost === 'number' ? run.cost : ((run.costDetail?.totalCost as number) ?? null)}
835
+ plan={run.plan}
836
+ startedAt={run.startedAt}
837
+ durationMs={run.durationMs ?? run.duration}
838
+ expanded={isExpanded}
839
+ onToggleExpand={() => setExpandedRunId((prev: string | null) => (prev === runId ? null : runId))}
840
+ />
841
+ );
842
+ })}
843
+ </Flex>
844
+ )}
845
+
846
+ {/* ── Empty state ── */}
847
+ {!isLive && runItems.length === 0 && (
848
+ <EmptyState
849
+ icon="smartToy"
850
+ message="No execution history"
851
+ description="This instance has no runs yet."
852
+ />
853
+ )}
854
+ </Section>
855
+ </ScrollArea>
856
+ </Flex>
857
+ );
858
+ }
859
+
860
+ export { InstanceStreamView };
861
+ export default InstanceStreamView;