@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.
- package/dist/ai-chat-provider.js +5 -5
- package/dist/ai-chat-provider.js.map +1 -1
- package/dist/bot/acceptance-merge.d.ts +21 -0
- package/dist/bot/acceptance-merge.d.ts.map +1 -0
- package/dist/bot/acceptance-merge.js +46 -0
- package/dist/bot/acceptance-merge.js.map +1 -0
- package/dist/bot/ai-client.d.ts +14 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +71 -24
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/assistant-tools.js +3 -3
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +34 -14
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/audit-trail.d.ts +67 -0
- package/dist/bot/audit-trail.d.ts.map +1 -0
- package/dist/bot/audit-trail.js +153 -0
- package/dist/bot/audit-trail.js.map +1 -0
- package/dist/bot/behavior-defaults.d.ts +1 -1
- package/dist/bot/behavior-defaults.d.ts.map +1 -1
- package/dist/bot/behavior-defaults.js +7 -3
- package/dist/bot/behavior-defaults.js.map +1 -1
- package/dist/bot/capability-registry.d.ts +9 -0
- package/dist/bot/capability-registry.d.ts.map +1 -1
- package/dist/bot/capability-registry.js +81 -27
- package/dist/bot/capability-registry.js.map +1 -1
- package/dist/bot/capability-types.d.ts +10 -0
- package/dist/bot/capability-types.d.ts.map +1 -1
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +8 -7
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/preflight.d.ts +48 -0
- package/dist/bot/preflight.d.ts.map +1 -0
- package/dist/bot/preflight.js +247 -0
- package/dist/bot/preflight.js.map +1 -0
- package/dist/bot/provider-shim.d.ts +74 -0
- package/dist/bot/provider-shim.d.ts.map +1 -0
- package/dist/bot/provider-shim.js +176 -0
- package/dist/bot/provider-shim.js.map +1 -0
- package/dist/bot/runner.d.ts +2 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +60 -17
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +72 -115
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/swarm-controller.d.ts +2 -0
- package/dist/bot/swarm-controller.d.ts.map +1 -1
- package/dist/bot/swarm-controller.js +92 -20
- package/dist/bot/swarm-controller.js.map +1 -1
- package/dist/bot/task-create-handler.d.ts +37 -0
- package/dist/bot/task-create-handler.d.ts.map +1 -0
- package/dist/bot/task-create-handler.js +124 -0
- package/dist/bot/task-create-handler.js.map +1 -0
- package/dist/bot/task-store.d.ts +1 -0
- package/dist/bot/task-store.d.ts.map +1 -1
- package/dist/bot/task-store.js +67 -0
- package/dist/bot/task-store.js.map +1 -1
- package/dist/bot/types.d.ts +1 -1
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +7 -39
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +25 -8
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +89 -23
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +24 -3
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/plan-task.d.ts +8 -17
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +217 -256
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/review-result.js +8 -6
- package/dist/node-types/review-result.js.map +1 -1
- package/dist/palindrome.d.ts +9 -0
- package/dist/palindrome.d.ts.map +1 -0
- package/dist/palindrome.js +14 -0
- package/dist/palindrome.js.map +1 -0
- package/dist/ui/approval-card.js +91 -82
- package/dist/ui/bot-activity.js +73 -56
- package/dist/ui/bot-config.js +48 -31
- package/dist/ui/bot-dashboard.js +52 -36
- package/dist/ui/bot-panel.js +230 -228
- package/dist/ui/bot-slot-card.js +100 -90
- package/dist/ui/bot-status.js +37 -15
- package/dist/ui/budget-bar.js +57 -31
- package/dist/ui/capability-editor.js +447 -378
- package/dist/ui/chat-task-result.js +78 -71
- package/dist/ui/decision-log.js +68 -81
- package/dist/ui/genesis-block.js +86 -95
- package/dist/ui/instance-stream-view.js +722 -0
- package/dist/ui/profile-card.js +96 -221
- package/dist/ui/profile-editor.js +532 -575
- package/dist/ui/settings-section.js +41 -45
- package/dist/ui/swarm-controls.js +212 -135
- package/dist/ui/swarm-dashboard.js +3992 -2715
- package/dist/ui/task-detail-view.js +415 -521
- package/dist/ui/task-editor.js +339 -390
- package/dist/ui/task-pool-list.js +60 -55
- package/dist/workflows/src/palindrome.d.ts +11 -0
- package/dist/workflows/src/palindrome.d.ts.map +1 -0
- package/dist/workflows/src/palindrome.js +16 -0
- package/dist/workflows/src/palindrome.js.map +1 -0
- package/dist/workflows/tests/palindrome.test.d.ts +2 -0
- package/dist/workflows/tests/palindrome.test.d.ts.map +1 -0
- package/dist/workflows/tests/palindrome.test.js +41 -0
- package/dist/workflows/tests/palindrome.test.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.js +1 -1
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.js +1 -1
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +1 -1
- package/package.json +8 -2
- package/src/ai-chat-provider.ts +5 -5
- package/src/bot/acceptance-merge.ts +62 -0
- package/src/bot/ai-client.ts +77 -21
- package/src/bot/assistant-tools.ts +3 -3
- package/src/bot/audit-logger.ts +42 -14
- package/src/bot/audit-trail.ts +211 -0
- package/src/bot/behavior-defaults.ts +7 -2
- package/src/bot/capability-registry.ts +84 -28
- package/src/bot/capability-types.ts +11 -0
- package/src/bot/cli-provider.ts +8 -7
- package/src/bot/preflight.ts +285 -0
- package/src/bot/provider-shim.ts +218 -0
- package/src/bot/runner.ts +68 -20
- package/src/bot/step-executor.ts +69 -127
- package/src/bot/swarm-controller.ts +94 -20
- package/src/bot/task-create-handler.ts +164 -0
- package/src/bot/task-store.ts +83 -0
- package/src/bot/types.ts +4 -1
- package/src/bot/weaver-tools.ts +7 -45
- package/src/node-types/agent-execute.ts +102 -16
- package/src/node-types/bot-report.ts +24 -3
- package/src/node-types/plan-task.ts +238 -280
- package/src/node-types/review-result.ts +8 -6
- package/src/palindrome.ts +14 -0
- package/src/ui/approval-card.tsx +78 -62
- package/src/ui/bot-activity.tsx +12 -10
- package/src/ui/bot-config.tsx +12 -10
- package/src/ui/bot-dashboard.tsx +13 -11
- package/src/ui/bot-panel.tsx +189 -171
- package/src/ui/bot-slot-card.tsx +125 -70
- package/src/ui/bot-status.tsx +4 -4
- package/src/ui/budget-bar.tsx +86 -25
- package/src/ui/capability-editor.tsx +392 -257
- package/src/ui/chat-task-result.tsx +81 -78
- package/src/ui/decision-log.tsx +76 -73
- package/src/ui/genesis-block.tsx +91 -61
- package/src/ui/instance-stream-view.tsx +861 -0
- package/src/ui/profile-card.tsx +195 -168
- package/src/ui/profile-editor.tsx +453 -370
- package/src/ui/settings-section.tsx +46 -39
- package/src/ui/swarm-controls.tsx +252 -123
- package/src/ui/swarm-dashboard.tsx +999 -466
- package/src/ui/task-detail-view.tsx +485 -428
- package/src/ui/task-editor.tsx +329 -271
- package/src/ui/task-pool-list.tsx +68 -62
- package/src/workflows/src/palindrome.ts +16 -0
- package/src/workflows/tests/palindrome.test.ts +49 -0
- package/src/workflows/weaver-bot-batch.ts +1 -1
- package/src/workflows/weaver-bot.ts +1 -1
- package/dist/ui/bot-constants.d.ts +0 -14
- package/dist/ui/bot-constants.d.ts.map +0 -1
- package/dist/ui/bot-constants.js +0 -189
- package/dist/ui/bot-constants.js.map +0 -1
- package/dist/ui/steer-api.d.ts +0 -7
- package/dist/ui/steer-api.d.ts.map +0 -1
- package/dist/ui/steer-api.js +0 -11
- package/dist/ui/steer-api.js.map +0 -1
- package/dist/ui/trace-to-timeline.d.ts +0 -91
- package/dist/ui/trace-to-timeline.d.ts.map +0 -1
- package/dist/ui/trace-to-timeline.js +0 -116
- package/dist/ui/trace-to-timeline.js.map +0 -1
- package/dist/ui/use-stream-timeline.d.ts +0 -50
- package/dist/ui/use-stream-timeline.d.ts.map +0 -1
- package/dist/ui/use-stream-timeline.js +0 -245
- 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;
|