@synergenius/flow-weaver-pack-weaver 0.9.201 → 0.9.203
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/bot/preflight.d.ts.map +1 -1
- package/dist/bot/preflight.js +26 -0
- package/dist/bot/preflight.js.map +1 -1
- package/dist/bot/task-create-handler.d.ts +9 -0
- package/dist/bot/task-create-handler.d.ts.map +1 -1
- package/dist/bot/task-create-handler.js +26 -0
- package/dist/bot/task-create-handler.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts.map +1 -1
- package/dist/node-types/agent-execute.js +26 -9
- package/dist/node-types/agent-execute.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +28 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/ui/bot-slot-card.js +10 -0
- package/dist/ui/budget-bar.js +5 -3
- package/dist/ui/budget-strip.js +156 -0
- package/dist/ui/chat-task-result.js +22 -27
- package/dist/ui/instance-stream-view.js +36 -0
- package/dist/ui/swarm-dashboard.js +1596 -1654
- package/dist/ui/task-detail-view.js +973 -485
- package/dist/ui/task-editor.js +32 -34
- package/dist/ui/task-pool-list.js +11 -3
- package/flowweaver.manifest.json +1 -1
- package/package.json +3 -2
- package/src/bot/preflight.ts +26 -0
- package/src/bot/task-create-handler.ts +39 -0
- package/src/node-types/agent-execute.ts +27 -10
- package/src/node-types/plan-task.ts +25 -2
- package/src/ui/bot-slot-card.tsx +23 -0
- package/src/ui/budget-bar.tsx +13 -5
- package/src/ui/budget-strip.tsx +199 -0
- package/src/ui/chat-task-result.tsx +5 -25
- package/src/ui/instance-stream-view.tsx +50 -1
- package/src/ui/swarm-dashboard.tsx +89 -84
- package/src/ui/task-detail-view.tsx +376 -442
- package/src/ui/task-editor.tsx +65 -96
- package/src/ui/task-pool-list.tsx +3 -12
- package/src/ui/task-status.ts +60 -0
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Task Detail View — full task detail with
|
|
2
|
+
* Task Detail View — full task detail with rich header, acceptance checks,
|
|
3
|
+
* live status, and run history.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* - Header: title
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
5
|
+
* Redesigned layout:
|
|
6
|
+
* - Header: title + inline status/profile/attempts/cost
|
|
7
|
+
* - Tab 1 "Actions": What's happening, Acceptance, Quick actions
|
|
8
|
+
* - Tab 2 "Details": Edit form (description, profile, priority, context)
|
|
9
|
+
* - Tab 3 "Runs": Run history + live run
|
|
10
|
+
* - Tab 4 "Subtasks": (conditional)
|
|
11
|
+
* - Tab 5 "Context": Files, notes, accumulated context
|
|
11
12
|
*/
|
|
12
13
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
13
14
|
import {
|
|
14
|
-
Flex, Typography, ScrollArea, StatusIcon,
|
|
15
|
-
Card, Chip,
|
|
15
|
+
Flex, Typography, ScrollArea, StatusIcon, Icon, IconButton, TaskBlock, Button,
|
|
16
|
+
Card, Chip, Tabs, EmptyState, CollapsibleBlock, StatusChip,
|
|
17
|
+
toast, usePackWorkspace, useEventStream, formatCost, formatDuration,
|
|
16
18
|
} from '@fw/plugin-ui-kit';
|
|
17
19
|
|
|
18
20
|
import { useStreamTimeline } from './use-stream-timeline';
|
|
19
21
|
import { traceToTimeline } from './trace-to-timeline';
|
|
20
22
|
import type { HistoricalRun } from './trace-to-timeline';
|
|
23
|
+
import TaskEditor from './task-editor';
|
|
21
24
|
|
|
22
25
|
// ---------------------------------------------------------------------------
|
|
23
26
|
// Types
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
25
28
|
|
|
26
|
-
type TaskStatus = 'open' | 'in-progress' | 'done' | 'cancelled';
|
|
27
29
|
|
|
28
30
|
interface TaskContext {
|
|
29
31
|
files: string[];
|
|
@@ -46,11 +48,22 @@ interface TaskContext {
|
|
|
46
48
|
stagnationCount: number;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
interface AcceptanceCheck {
|
|
52
|
+
name: string;
|
|
53
|
+
command: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface AcceptanceResult {
|
|
57
|
+
met: boolean;
|
|
58
|
+
results: Array<{ name: string; pass: boolean; detail?: string }>;
|
|
59
|
+
checkedAt: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
interface Task {
|
|
50
63
|
id: string;
|
|
51
64
|
title: string;
|
|
52
65
|
description: string;
|
|
53
|
-
status:
|
|
66
|
+
status: TTaskStatus;
|
|
54
67
|
priority: number;
|
|
55
68
|
isParent: boolean;
|
|
56
69
|
parentId?: string;
|
|
@@ -65,12 +78,14 @@ interface Task {
|
|
|
65
78
|
completedAt?: string;
|
|
66
79
|
assignedProfile?: string;
|
|
67
80
|
routingReason?: string;
|
|
81
|
+
acceptance?: { checks: AcceptanceCheck[] };
|
|
82
|
+
lastAcceptanceCheck?: AcceptanceResult;
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
interface Subtask {
|
|
71
86
|
id: string;
|
|
72
87
|
title: string;
|
|
73
|
-
status:
|
|
88
|
+
status: TTaskStatus;
|
|
74
89
|
priority: number;
|
|
75
90
|
assignedProfile?: string;
|
|
76
91
|
}
|
|
@@ -81,115 +96,90 @@ interface TaskDetailViewProps {
|
|
|
81
96
|
onEdit?: (taskId: string) => void;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const statusToIcon: Record<TaskStatus, string> = {
|
|
89
|
-
'open': 'pending',
|
|
90
|
-
'in-progress': 'running',
|
|
91
|
-
'done': 'completed',
|
|
92
|
-
'cancelled': 'failed',
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const statusToLabel: Record<TaskStatus, string> = {
|
|
96
|
-
'open': 'Open',
|
|
97
|
-
'in-progress': 'Running',
|
|
98
|
-
'done': 'Done',
|
|
99
|
-
'cancelled': 'Cancelled',
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
const priorityLabel = (p: number) => p >= 3 ? 'High' : p === 2 ? 'Medium' : p === 1 ? 'Low' : 'None';
|
|
99
|
+
import {
|
|
100
|
+
TASK_STATUS_ICON, TASK_STATUS_ICON_OVERRIDE, TASK_STATUS_CHIP, TASK_STATUS_LABEL,
|
|
101
|
+
} from './task-status';
|
|
102
|
+
import type { TaskStatus as TTaskStatus } from './task-status';
|
|
103
103
|
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
|
-
// Inline styles
|
|
105
|
+
// Inline styles
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
107
|
|
|
108
108
|
const headerStyle: React.CSSProperties = {
|
|
109
109
|
padding: '12px 16px',
|
|
110
|
-
borderBottom: '1px solid var(--color-border-
|
|
110
|
+
borderBottom: '1px solid var(--color-border-faint)',
|
|
111
111
|
flexShrink: 0,
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const fileItemStyle: React.CSSProperties = {
|
|
120
|
-
fontFamily: 'monospace',
|
|
121
|
-
fontSize: '12px',
|
|
122
|
-
color: 'var(--color-text-mid)',
|
|
123
|
-
padding: '2px 4px',
|
|
124
|
-
borderRadius: '4px',
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const fileItemHoverStyle: React.CSSProperties = {
|
|
128
|
-
...fileItemStyle,
|
|
129
|
-
backgroundColor: 'var(--color-surface-elevated)',
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const notesBlockStyle: React.CSSProperties = {
|
|
133
|
-
whiteSpace: 'pre-wrap',
|
|
134
|
-
fontSize: '13px',
|
|
135
|
-
color: 'var(--color-text-mid)',
|
|
136
|
-
lineHeight: 1.5,
|
|
137
|
-
padding: '4px 0',
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
const subtaskRowStyle: React.CSSProperties = {
|
|
141
|
-
padding: '6px 8px',
|
|
142
|
-
cursor: 'pointer',
|
|
143
|
-
borderRadius: '6px',
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
const subtaskRowHoverStyle: React.CSSProperties = {
|
|
147
|
-
...subtaskRowStyle,
|
|
148
|
-
backgroundColor: 'var(--color-surface-elevated)',
|
|
114
|
+
const sectionDivider: React.CSSProperties = {
|
|
115
|
+
borderBottom: '1px solid var(--color-border-faint)',
|
|
116
|
+
paddingBottom: '12px',
|
|
117
|
+
marginBottom: '4px',
|
|
149
118
|
};
|
|
150
119
|
|
|
151
120
|
// ---------------------------------------------------------------------------
|
|
152
|
-
//
|
|
121
|
+
// Sub-components
|
|
153
122
|
// ---------------------------------------------------------------------------
|
|
154
123
|
|
|
155
|
-
function
|
|
124
|
+
function SubtaskRowItem(props: { key?: string; sub: Subtask; onClick: () => void }) {
|
|
125
|
+
const { sub, onClick } = props;
|
|
156
126
|
const [hovered, setHovered] = useState(false);
|
|
157
127
|
return (
|
|
158
128
|
<Flex
|
|
159
|
-
variant="row-center-start-nowrap-
|
|
160
|
-
style={
|
|
129
|
+
variant="row-center-start-nowrap-8"
|
|
130
|
+
style={{
|
|
131
|
+
padding: '6px 8px',
|
|
132
|
+
cursor: 'pointer',
|
|
133
|
+
borderRadius: '6px',
|
|
134
|
+
backgroundColor: hovered ? 'var(--color-surface-hover)' : 'transparent',
|
|
135
|
+
}}
|
|
136
|
+
onClick={onClick}
|
|
161
137
|
onMouseEnter={() => setHovered(true)}
|
|
162
138
|
onMouseLeave={() => setHovered(false)}
|
|
163
139
|
>
|
|
164
|
-
{
|
|
140
|
+
<StatusIcon status={TASK_STATUS_ICON[sub.status] || 'pending'} icon={TASK_STATUS_ICON_OVERRIDE[sub.status]} size="sm" />
|
|
141
|
+
<Typography variant="caption-regular" truncate style={{ flex: 1, minWidth: 0 }}>
|
|
142
|
+
{sub.title}
|
|
143
|
+
</Typography>
|
|
144
|
+
{sub.assignedProfile && (
|
|
145
|
+
<Chip label={sub.assignedProfile} size="small" color="color-status-info" />
|
|
146
|
+
)}
|
|
165
147
|
</Flex>
|
|
166
148
|
);
|
|
167
149
|
}
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
|
|
151
|
+
/** Single acceptance check result row */
|
|
152
|
+
function AcceptanceCheckRow(props: { key?: string; name: string; pass: boolean; detail?: string }) {
|
|
153
|
+
const { name, pass, detail } = props;
|
|
154
|
+
const [expanded, setExpanded] = useState(!pass && !!detail);
|
|
171
155
|
return (
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
156
|
+
<CollapsibleBlock
|
|
157
|
+
status={pass ? 'completed' : 'error'}
|
|
158
|
+
label={
|
|
159
|
+
<Flex variant="row-center-start-nowrap-6" style={{ flex: 1 }}>
|
|
160
|
+
<Typography variant="smallCaption-thick" color={pass ? 'color-status-positive' : 'color-status-negative'}>
|
|
161
|
+
{name}
|
|
162
|
+
</Typography>
|
|
163
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
164
|
+
{pass ? 'passed' : 'failed'}
|
|
165
|
+
</Typography>
|
|
166
|
+
</Flex>
|
|
167
|
+
}
|
|
168
|
+
expanded={expanded}
|
|
169
|
+
onToggle={() => setExpanded((v: boolean) => !v)}
|
|
170
|
+
canExpand={!!detail}
|
|
178
171
|
>
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
</Flex>
|
|
189
|
-
{sub.assignedProfile && (
|
|
190
|
-
<Chip label={sub.assignedProfile} size="small" color="color-status-info" />
|
|
172
|
+
{detail && (
|
|
173
|
+
<Typography
|
|
174
|
+
variant="smallCaption-regular"
|
|
175
|
+
color="color-text-medium"
|
|
176
|
+
mono
|
|
177
|
+
style={{ whiteSpace: 'pre-wrap', padding: '6px 0' }}
|
|
178
|
+
>
|
|
179
|
+
{detail}
|
|
180
|
+
</Typography>
|
|
191
181
|
)}
|
|
192
|
-
</
|
|
182
|
+
</CollapsibleBlock>
|
|
193
183
|
);
|
|
194
184
|
}
|
|
195
185
|
|
|
@@ -223,7 +213,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
223
213
|
const summaries = rs?.runHistory as Array<Record<string, unknown>> | undefined;
|
|
224
214
|
if (summaries?.length) {
|
|
225
215
|
setHistory(prev => {
|
|
226
|
-
if (prev.length > 0) return prev;
|
|
216
|
+
if (prev.length > 0) return prev;
|
|
227
217
|
return summaries.map((s) => ({
|
|
228
218
|
id: s.runId as string,
|
|
229
219
|
runId: s.runId as string,
|
|
@@ -239,18 +229,15 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
239
229
|
});
|
|
240
230
|
}
|
|
241
231
|
|
|
242
|
-
// Subtasks from response or fetch separately
|
|
243
232
|
if (data.subtasks && Array.isArray(data.subtasks)) {
|
|
244
233
|
setSubtasks(data.subtasks as Subtask[]);
|
|
245
234
|
} else if (t.isParent) {
|
|
246
235
|
const listRaw = await callTool('fw_weaver_task_list', { parentId: taskId });
|
|
247
236
|
const listData = typeof listRaw === 'string' ? JSON.parse(listRaw) : listRaw;
|
|
248
|
-
if (Array.isArray(listData))
|
|
249
|
-
setSubtasks(listData as Subtask[]);
|
|
250
|
-
}
|
|
237
|
+
if (Array.isArray(listData)) setSubtasks(listData as Subtask[]);
|
|
251
238
|
}
|
|
252
|
-
} catch
|
|
253
|
-
// Non-fatal
|
|
239
|
+
} catch {
|
|
240
|
+
// Non-fatal
|
|
254
241
|
}
|
|
255
242
|
}, [callTool, taskId]);
|
|
256
243
|
|
|
@@ -265,7 +252,6 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
265
252
|
return { ...r, costDetail: costObj, cost: costObj?.totalCost } as unknown as HistoricalRun;
|
|
266
253
|
}),
|
|
267
254
|
);
|
|
268
|
-
return;
|
|
269
255
|
}
|
|
270
256
|
} catch {
|
|
271
257
|
// Non-fatal
|
|
@@ -278,8 +264,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
278
264
|
Promise.all([fetchTask(), fetchHistory()]).finally(() => setLoading(false));
|
|
279
265
|
}, [fetchTask, fetchHistory]);
|
|
280
266
|
|
|
281
|
-
|
|
282
|
-
// Poll while in-progress (every 5s)
|
|
267
|
+
// Poll while in-progress
|
|
283
268
|
useEffect(() => {
|
|
284
269
|
if (!task || task.status !== 'in-progress') return;
|
|
285
270
|
const interval = setInterval(() => {
|
|
@@ -311,21 +296,50 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
311
296
|
return () => stream.stop();
|
|
312
297
|
}, [isLive, activeRunId, packId]);
|
|
313
298
|
|
|
314
|
-
// --
|
|
315
|
-
const [
|
|
299
|
+
// -- Tab state --
|
|
300
|
+
const [activeTab, setActiveTab] = useState('runs');
|
|
316
301
|
|
|
317
302
|
// -- Action state --
|
|
318
303
|
const [actionLoading, setActionLoading] = useState<string | null>(null);
|
|
319
|
-
const [
|
|
304
|
+
const [runningChecks, setRunningChecks] = useState(false);
|
|
305
|
+
|
|
306
|
+
// -- Approval --
|
|
307
|
+
const [approvalStatus, setApprovalStatus] = useState<'pending' | 'approved' | 'rejected' | null>(null);
|
|
308
|
+
const [approvalLoading, setApprovalLoading] = useState(false);
|
|
320
309
|
|
|
321
|
-
// Fetch profiles for assign dropdown
|
|
322
310
|
useEffect(() => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
311
|
+
if (awaitingApproval) setApprovalStatus('pending');
|
|
312
|
+
}, [awaitingApproval]);
|
|
313
|
+
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
if (stream.events.length === 0) setApprovalStatus(null);
|
|
316
|
+
}, [stream.events.length]);
|
|
317
|
+
|
|
318
|
+
const handleApprove = useCallback(async () => {
|
|
319
|
+
setApprovalLoading(true);
|
|
320
|
+
try {
|
|
321
|
+
await callTool('fw_weaver_approve', { approved: true });
|
|
322
|
+
setApprovalStatus('approved');
|
|
323
|
+
toast('Plan approved', { type: 'success' });
|
|
324
|
+
} catch (err: unknown) {
|
|
325
|
+
toast(err instanceof Error ? err.message : 'Failed to approve', { type: 'error' });
|
|
326
|
+
}
|
|
327
|
+
setApprovalLoading(false);
|
|
327
328
|
}, [callTool]);
|
|
328
329
|
|
|
330
|
+
const handleReject = useCallback(async () => {
|
|
331
|
+
setApprovalLoading(true);
|
|
332
|
+
try {
|
|
333
|
+
await callTool('fw_weaver_approve', { approved: false });
|
|
334
|
+
setApprovalStatus('rejected');
|
|
335
|
+
toast('Plan rejected', { type: 'info' });
|
|
336
|
+
} catch (err: unknown) {
|
|
337
|
+
toast(err instanceof Error ? err.message : 'Failed to reject', { type: 'error' });
|
|
338
|
+
}
|
|
339
|
+
setApprovalLoading(false);
|
|
340
|
+
}, [callTool]);
|
|
341
|
+
|
|
342
|
+
// -- Actions --
|
|
329
343
|
const handleRetry = useCallback(async () => {
|
|
330
344
|
setActionLoading('retry');
|
|
331
345
|
try {
|
|
@@ -356,31 +370,23 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
356
370
|
setActionLoading(null);
|
|
357
371
|
}, [callTool, taskId, fetchTask, ctx]);
|
|
358
372
|
|
|
359
|
-
const
|
|
360
|
-
|
|
373
|
+
const handleRunChecks = useCallback(async () => {
|
|
374
|
+
setRunningChecks(true);
|
|
361
375
|
try {
|
|
362
|
-
|
|
363
|
-
await callTool('fw_weaver_task_update', { id: taskId,
|
|
364
|
-
toast(
|
|
365
|
-
|
|
376
|
+
// Trigger acceptance check via task retry with check-only flag
|
|
377
|
+
await callTool('fw_weaver_task_update', { id: taskId, runAcceptanceChecks: true });
|
|
378
|
+
toast('Acceptance checks triggered', { type: 'info' });
|
|
379
|
+
// Re-fetch to get updated results
|
|
380
|
+
setTimeout(() => fetchTask(), 2000);
|
|
366
381
|
} catch (err: unknown) {
|
|
367
|
-
toast(err instanceof Error ? err.message : 'Failed to
|
|
382
|
+
toast(err instanceof Error ? err.message : 'Failed to run checks', { type: 'error' });
|
|
368
383
|
}
|
|
369
|
-
|
|
370
|
-
}, [callTool, taskId,
|
|
371
|
-
|
|
372
|
-
const handlePriorityChange = useCallback(async (delta: number) => {
|
|
373
|
-
const newPriority = Math.max(0, (task?.priority ?? 0) + delta);
|
|
374
|
-
try {
|
|
375
|
-
await callTool('fw_weaver_task_update', { id: taskId, priority: newPriority });
|
|
376
|
-
fetchTask();
|
|
377
|
-
} catch { /* non-fatal */ }
|
|
378
|
-
}, [callTool, taskId, task, fetchTask]);
|
|
384
|
+
setRunningChecks(false);
|
|
385
|
+
}, [callTool, taskId, fetchTask]);
|
|
379
386
|
|
|
380
|
-
// -- Run expansion
|
|
387
|
+
// -- Run expansion --
|
|
381
388
|
const [expandedRunId, setExpandedRunId] = useState<string | null>(null);
|
|
382
389
|
const [liveExpanded, setLiveExpanded] = useState(true);
|
|
383
|
-
|
|
384
390
|
const toggleExpand = useCallback((id: string) => {
|
|
385
391
|
setExpandedRunId((prev: string | null) => (prev === id ? null : id));
|
|
386
392
|
}, []);
|
|
@@ -392,43 +398,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
392
398
|
if (el) el.scrollTop = el.scrollHeight;
|
|
393
399
|
}, [liveTimeline.length, stream.events.length]);
|
|
394
400
|
|
|
395
|
-
// --
|
|
396
|
-
const [approvalStatus, setApprovalStatus] = useState<'pending' | 'approved' | 'rejected' | null>(null);
|
|
397
|
-
const [approvalLoading, setApprovalLoading] = useState(false);
|
|
398
|
-
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
if (awaitingApproval) setApprovalStatus('pending');
|
|
401
|
-
}, [awaitingApproval]);
|
|
402
|
-
|
|
403
|
-
useEffect(() => {
|
|
404
|
-
if (stream.events.length === 0) setApprovalStatus(null);
|
|
405
|
-
}, [stream.events.length]);
|
|
406
|
-
|
|
407
|
-
const handleApprove = useCallback(async () => {
|
|
408
|
-
setApprovalLoading(true);
|
|
409
|
-
try {
|
|
410
|
-
await callTool('fw_weaver_approve', { approved: true });
|
|
411
|
-
setApprovalStatus('approved');
|
|
412
|
-
toast('Plan approved', { type: 'success' });
|
|
413
|
-
} catch (err: unknown) {
|
|
414
|
-
toast(err instanceof Error ? err.message : 'Failed to approve', { type: 'error' });
|
|
415
|
-
}
|
|
416
|
-
setApprovalLoading(false);
|
|
417
|
-
}, [callTool]);
|
|
418
|
-
|
|
419
|
-
const handleReject = useCallback(async () => {
|
|
420
|
-
setApprovalLoading(true);
|
|
421
|
-
try {
|
|
422
|
-
await callTool('fw_weaver_approve', { approved: false });
|
|
423
|
-
setApprovalStatus('rejected');
|
|
424
|
-
toast('Plan rejected', { type: 'info' });
|
|
425
|
-
} catch (err: unknown) {
|
|
426
|
-
toast(err instanceof Error ? err.message : 'Failed to reject', { type: 'error' });
|
|
427
|
-
}
|
|
428
|
-
setApprovalLoading(false);
|
|
429
|
-
}, [callTool]);
|
|
430
|
-
|
|
431
|
-
// -- Build run timeline entries from history --
|
|
401
|
+
// -- Build run timeline --
|
|
432
402
|
const runItems = useMemo(() => {
|
|
433
403
|
return history.map((run: HistoricalRun) => ({
|
|
434
404
|
run,
|
|
@@ -436,7 +406,6 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
436
406
|
}));
|
|
437
407
|
}, [history]);
|
|
438
408
|
|
|
439
|
-
// -- Extract instruction from run --
|
|
440
409
|
function extractInstruction(run: HistoricalRun): string {
|
|
441
410
|
if (run.instruction) {
|
|
442
411
|
const text = run.instruction;
|
|
@@ -466,23 +435,42 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
466
435
|
return task?.title ?? 'Bot run';
|
|
467
436
|
}
|
|
468
437
|
|
|
469
|
-
// --
|
|
438
|
+
// -- Derived data --
|
|
439
|
+
const acceptanceChecks = task?.acceptance?.checks ?? [];
|
|
440
|
+
const lastAcceptance = task?.lastAcceptanceCheck;
|
|
441
|
+
const hasAcceptance = acceptanceChecks.length > 0;
|
|
442
|
+
const hasContext = (task?.context?.files?.length ?? 0) > 0 || task?.context?.notes;
|
|
443
|
+
const hasSubtasks = task?.isParent && subtasks.length > 0;
|
|
444
|
+
const hasRuns = runItems.length > 0 || isLive;
|
|
445
|
+
const attemptCount = task?.context?.runHistory?.length ?? 0;
|
|
446
|
+
const isTerminal = task?.status === 'done' || task?.status === 'cancelled';
|
|
447
|
+
const blockedByIds = task?.dependsOn?.length ? task.dependsOn : [];
|
|
448
|
+
|
|
449
|
+
// Files changed since last acceptance pass
|
|
450
|
+
const filesChangedSincePass = useMemo(() => {
|
|
451
|
+
if (!lastAcceptance?.met || !lastAcceptance.checkedAt) return [];
|
|
452
|
+
const passTime = new Date(lastAcceptance.checkedAt).getTime();
|
|
453
|
+
const changed: string[] = [];
|
|
454
|
+
for (const run of (task?.context?.runHistory ?? [])) {
|
|
455
|
+
const runTime = new Date(run.endedAt).getTime();
|
|
456
|
+
if (runTime > passTime) {
|
|
457
|
+
changed.push(...(run.filesModified ?? []), ...(run.filesCreated ?? []));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return [...new Set(changed)];
|
|
461
|
+
}, [lastAcceptance, task?.context?.runHistory]);
|
|
462
|
+
|
|
463
|
+
// -- Loading / not found --
|
|
470
464
|
if (loading && !task) {
|
|
471
465
|
return (
|
|
472
|
-
<Flex
|
|
473
|
-
variant="column-stretch-start-nowrap-0"
|
|
474
|
-
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
|
475
|
-
>
|
|
466
|
+
<Flex variant="column-stretch-start-nowrap-0" style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
|
476
467
|
<Flex variant="column-stretch-start-nowrap-8" style={headerStyle}>
|
|
477
468
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
|
478
|
-
<Icon name="arrowBack" size={16} />
|
|
479
|
-
{' Back'}
|
|
469
|
+
<Icon name="arrowBack" size={16} /> Back
|
|
480
470
|
</Button>
|
|
481
471
|
</Flex>
|
|
482
472
|
<Flex variant="column-center-center-nowrap-0" style={{ flex: 1 }}>
|
|
483
|
-
<Typography variant="caption-regular" color="color-text-subtle">
|
|
484
|
-
Loading task...
|
|
485
|
-
</Typography>
|
|
473
|
+
<Typography variant="caption-regular" color="color-text-subtle">Loading task...</Typography>
|
|
486
474
|
</Flex>
|
|
487
475
|
</Flex>
|
|
488
476
|
);
|
|
@@ -490,50 +478,41 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
490
478
|
|
|
491
479
|
if (!task) {
|
|
492
480
|
return (
|
|
493
|
-
<Flex
|
|
494
|
-
variant="column-stretch-start-nowrap-0"
|
|
495
|
-
style={{ width: '100%', height: '100%', overflow: 'hidden' }}
|
|
496
|
-
>
|
|
481
|
+
<Flex variant="column-stretch-start-nowrap-0" style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
|
497
482
|
<Flex variant="column-stretch-start-nowrap-8" style={headerStyle}>
|
|
498
483
|
<Button variant="ghost" size="sm" onClick={onBack}>
|
|
499
|
-
<Icon name="arrowBack" size={16} />
|
|
500
|
-
{' Back'}
|
|
484
|
+
<Icon name="arrowBack" size={16} /> Back
|
|
501
485
|
</Button>
|
|
502
486
|
</Flex>
|
|
503
487
|
<Flex variant="column-center-center-nowrap-0" style={{ flex: 1 }}>
|
|
504
|
-
<Typography variant="caption-regular" color="color-text-subtle">
|
|
505
|
-
Task not found.
|
|
506
|
-
</Typography>
|
|
488
|
+
<Typography variant="caption-regular" color="color-text-subtle">Task not found.</Typography>
|
|
507
489
|
</Flex>
|
|
508
490
|
</Flex>
|
|
509
491
|
);
|
|
510
492
|
}
|
|
511
493
|
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
494
|
+
// -- Build tabs --
|
|
495
|
+
const tabs = [
|
|
496
|
+
{ id: 'runs', title: `Runs (${runItems.length}${isLive ? '+1' : ''})` },
|
|
497
|
+
...(hasAcceptance ? [{ id: 'checks', title: 'Checks' }] : []),
|
|
498
|
+
{ id: 'details', title: 'Details' },
|
|
499
|
+
...(hasSubtasks ? [{ id: 'subtasks', title: `Subtasks (${subtasks.filter((s: Subtask) => s.status === 'done').length}/${subtasks.length})` }] : []),
|
|
500
|
+
...(hasContext ? [{ id: 'context', title: 'Context' }] : []),
|
|
501
|
+
];
|
|
515
502
|
|
|
516
503
|
return (
|
|
517
|
-
<Flex
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
variant="
|
|
524
|
-
style={{ ...headerStyle, padding: '12px 16px', borderBottom: '1px solid var(--color-border-default)' }}
|
|
525
|
-
>
|
|
526
|
-
{/* Top row: back + title + status + edit */}
|
|
527
|
-
<Flex variant="row-center-space-between-nowrap-8">
|
|
504
|
+
<Flex variant="column-stretch-start-nowrap-0" style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
|
|
505
|
+
{/* ----------------------------------------------------------------- */}
|
|
506
|
+
{/* HEADER: back + title + inline meta */}
|
|
507
|
+
{/* ----------------------------------------------------------------- */}
|
|
508
|
+
<Flex variant="column-stretch-start-nowrap-6" style={headerStyle}>
|
|
509
|
+
{/* Row 1: back, status icon, title, actions */}
|
|
510
|
+
<Flex variant="row-center-space-between-nowrap-8" style={{ minWidth: 0 }}>
|
|
528
511
|
<Flex variant="row-center-start-nowrap-8" style={{ flex: 1, minWidth: 0 }}>
|
|
529
|
-
<IconButton
|
|
530
|
-
icon="back"
|
|
531
|
-
size="xs"
|
|
532
|
-
variant="clear"
|
|
533
|
-
onClick={onBack}
|
|
534
|
-
/>
|
|
512
|
+
<IconButton icon="back" size="xs" variant="clear" onClick={onBack} />
|
|
535
513
|
<StatusIcon
|
|
536
|
-
status={
|
|
514
|
+
status={TASK_STATUS_ICON[task.status as TTaskStatus] || 'pending'}
|
|
515
|
+
icon={TASK_STATUS_ICON_OVERRIDE[task.status as TTaskStatus]}
|
|
537
516
|
size="sm"
|
|
538
517
|
/>
|
|
539
518
|
<Typography
|
|
@@ -544,91 +523,202 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
544
523
|
{task.title || 'Untitled Task'}
|
|
545
524
|
</Typography>
|
|
546
525
|
</Flex>
|
|
547
|
-
{
|
|
548
|
-
<
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
526
|
+
{!isTerminal && (
|
|
527
|
+
<Flex variant="row-center-end-nowrap-4" style={{ flexShrink: 0 }}>
|
|
528
|
+
{task.status === 'open' && attemptCount > 0 && (
|
|
529
|
+
<IconButton
|
|
530
|
+
icon="replay"
|
|
531
|
+
size="xs"
|
|
532
|
+
variant="outlined"
|
|
533
|
+
onClick={handleRetry}
|
|
534
|
+
title="Retry"
|
|
535
|
+
/>
|
|
536
|
+
)}
|
|
537
|
+
<IconButton
|
|
538
|
+
icon="close"
|
|
539
|
+
size="xs"
|
|
540
|
+
variant="outlined"
|
|
541
|
+
color="danger"
|
|
542
|
+
onClick={handleCancel}
|
|
543
|
+
title="Cancel"
|
|
544
|
+
/>
|
|
545
|
+
</Flex>
|
|
555
546
|
)}
|
|
556
547
|
</Flex>
|
|
557
548
|
|
|
558
|
-
{/*
|
|
549
|
+
{/* Row 2: inline chips — status, profile, priority, attempts, cost */}
|
|
559
550
|
<Flex variant="row-center-start-wrap-6">
|
|
560
|
-
<
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
color={task.status === 'done' ? 'color-status-positive'
|
|
564
|
-
: task.status === 'cancelled' ? 'color-status-negative'
|
|
565
|
-
: task.status === 'in-progress' ? 'color-status-info'
|
|
566
|
-
: 'color-brand-alt'}
|
|
567
|
-
/>
|
|
551
|
+
<StatusChip status={TASK_STATUS_CHIP[task.status as TTaskStatus] ?? 'info'} icon={TASK_STATUS_ICON[task.status as TTaskStatus]}>
|
|
552
|
+
{TASK_STATUS_LABEL[task.status as TTaskStatus]}
|
|
553
|
+
</StatusChip>
|
|
568
554
|
|
|
569
555
|
{task.assignedProfile && (
|
|
570
|
-
<
|
|
556
|
+
<StatusChip status="info" icon="person">{task.assignedProfile}</StatusChip>
|
|
571
557
|
)}
|
|
572
558
|
|
|
573
559
|
{task.priority > 0 && (
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
560
|
+
<StatusChip
|
|
561
|
+
status={task.priority >= 3 ? 'error' : task.priority === 2 ? 'warning' : 'info'}
|
|
562
|
+
icon="flag"
|
|
563
|
+
>
|
|
564
|
+
P{task.priority}
|
|
565
|
+
</StatusChip>
|
|
579
566
|
)}
|
|
580
567
|
|
|
581
|
-
{
|
|
582
|
-
<
|
|
583
|
-
{
|
|
584
|
-
</
|
|
568
|
+
{attemptCount > 0 && (
|
|
569
|
+
<StatusChip status="info" icon="replay">
|
|
570
|
+
{attemptCount} {attemptCount === 1 ? 'attempt' : 'attempts'}
|
|
571
|
+
</StatusChip>
|
|
572
|
+
)}
|
|
573
|
+
|
|
574
|
+
{task.costUsed > 0 && (
|
|
575
|
+
<StatusChip status="info" icon="payments">
|
|
576
|
+
{formatCost(task.costUsed)}
|
|
577
|
+
{task.budgetCost ? ` / ${formatCost(task.budgetCost)}` : ''}
|
|
578
|
+
</StatusChip>
|
|
579
|
+
)}
|
|
580
|
+
|
|
581
|
+
{hasAcceptance && lastAcceptance && (
|
|
582
|
+
<StatusChip
|
|
583
|
+
status={lastAcceptance.met ? 'success' : 'error'}
|
|
584
|
+
icon={lastAcceptance.met ? 'checkCircle' : 'cancel'}
|
|
585
|
+
>
|
|
586
|
+
{lastAcceptance.met ? 'Checks pass' : 'Checks fail'}
|
|
587
|
+
</StatusChip>
|
|
585
588
|
)}
|
|
586
589
|
</Flex>
|
|
587
590
|
|
|
588
|
-
{/* Description */}
|
|
591
|
+
{/* Description (one line) */}
|
|
589
592
|
{task.description && (
|
|
590
|
-
<Typography variant="smallCaption-regular" color="color-text-medium">
|
|
593
|
+
<Typography variant="smallCaption-regular" color="color-text-medium" truncate>
|
|
591
594
|
{task.description}
|
|
592
595
|
</Typography>
|
|
593
596
|
)}
|
|
594
|
-
|
|
595
|
-
{/* Profile routing info */}
|
|
596
|
-
{task.assignedProfile && (
|
|
597
|
-
<Flex variant="column-stretch-start-nowrap-2">
|
|
598
|
-
<Typography variant="smallCaption-regular" color="color-text-medium">
|
|
599
|
-
{`Profile: ${task.assignedProfile}`}
|
|
600
|
-
</Typography>
|
|
601
|
-
{task.routingReason && (
|
|
602
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
603
|
-
{task.routingReason}
|
|
604
|
-
</Typography>
|
|
605
|
-
)}
|
|
606
|
-
</Flex>
|
|
607
|
-
)}
|
|
608
597
|
</Flex>
|
|
609
598
|
|
|
610
|
-
{/*
|
|
599
|
+
{/* ----------------------------------------------------------------- */}
|
|
600
|
+
{/* TABS */}
|
|
601
|
+
{/* ----------------------------------------------------------------- */}
|
|
611
602
|
<Tabs
|
|
612
|
-
tabs={
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
...(task.status !== 'done' && task.status !== 'cancelled' ? [{ id: 'actions', title: 'Actions' }] : []),
|
|
616
|
-
...(hasContext ? [{ id: 'context', title: 'Context' }] : []),
|
|
617
|
-
]}
|
|
618
|
-
activeTabId={detailTab}
|
|
619
|
-
onSelectTab={(id: string) => setDetailTab(id)}
|
|
603
|
+
tabs={tabs}
|
|
604
|
+
activeTabId={activeTab}
|
|
605
|
+
onSelectTab={(id: string) => setActiveTab(id)}
|
|
620
606
|
size="sm"
|
|
621
607
|
/>
|
|
622
608
|
|
|
623
|
-
{/*
|
|
609
|
+
{/* ----------------------------------------------------------------- */}
|
|
610
|
+
{/* TAB CONTENT */}
|
|
611
|
+
{/* ----------------------------------------------------------------- */}
|
|
624
612
|
<ScrollArea ref={scrollRef} style={{ flex: 1 }}>
|
|
625
|
-
<Flex variant="column-stretch-start-nowrap-
|
|
613
|
+
<Flex variant="column-stretch-start-nowrap-12" style={{ padding: '12px 16px' }}>
|
|
614
|
+
|
|
615
|
+
{/* ============================================================= */}
|
|
616
|
+
{/* ACTIONS TAB */}
|
|
617
|
+
{/* ============================================================= */}
|
|
618
|
+
{activeTab === 'checks' && hasAcceptance && (
|
|
619
|
+
<Flex variant="column-stretch-start-nowrap-8">
|
|
620
|
+
<Flex variant="row-center-space-between-nowrap-8">
|
|
621
|
+
<Typography variant="caption-thick" color="color-text-medium">
|
|
622
|
+
Acceptance Checks
|
|
623
|
+
</Typography>
|
|
624
|
+
<Button
|
|
625
|
+
size="xs"
|
|
626
|
+
variant="outlined"
|
|
627
|
+
onClick={handleRunChecks}
|
|
628
|
+
loading={runningChecks}
|
|
629
|
+
disabled={runningChecks || isLive}
|
|
630
|
+
>
|
|
631
|
+
<Icon name="playArrow" size={12} /> Run
|
|
632
|
+
</Button>
|
|
633
|
+
</Flex>
|
|
634
|
+
|
|
635
|
+
{lastAcceptance ? (
|
|
636
|
+
<Flex variant="column-stretch-start-nowrap-4">
|
|
637
|
+
<Flex variant="row-center-start-nowrap-8">
|
|
638
|
+
<StatusChip
|
|
639
|
+
status={lastAcceptance.met ? 'success' : 'error'}
|
|
640
|
+
icon={lastAcceptance.met ? 'check' : 'close'}
|
|
641
|
+
>
|
|
642
|
+
{lastAcceptance.results.filter((r: { pass: boolean }) => r.pass).length}/{lastAcceptance.results.length} passed
|
|
643
|
+
</StatusChip>
|
|
644
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
645
|
+
{new Date(lastAcceptance.checkedAt).toLocaleTimeString()}
|
|
646
|
+
</Typography>
|
|
647
|
+
</Flex>
|
|
648
|
+
|
|
649
|
+
{lastAcceptance.results.map((r: { name: string; pass: boolean; detail?: string }) => (
|
|
650
|
+
<AcceptanceCheckRow
|
|
651
|
+
key={r.name}
|
|
652
|
+
name={r.name}
|
|
653
|
+
pass={r.pass}
|
|
654
|
+
detail={r.detail}
|
|
655
|
+
/>
|
|
656
|
+
))}
|
|
657
|
+
|
|
658
|
+
{lastAcceptance.met && filesChangedSincePass.length > 0 && (
|
|
659
|
+
<Flex variant="column-stretch-start-nowrap-4" style={{ marginTop: '4px' }}>
|
|
660
|
+
<Flex variant="row-center-start-nowrap-6">
|
|
661
|
+
<Icon name="warning" size={12} color="color-status-caution" />
|
|
662
|
+
<Typography variant="smallCaption-regular" color="color-status-caution">
|
|
663
|
+
{filesChangedSincePass.length} file{filesChangedSincePass.length !== 1 ? 's' : ''} changed since last pass
|
|
664
|
+
</Typography>
|
|
665
|
+
</Flex>
|
|
666
|
+
{filesChangedSincePass.slice(0, 5).map((f: string) => (
|
|
667
|
+
<Typography key={f} variant="smallCaption-regular" color="color-text-subtle" mono>
|
|
668
|
+
{f}
|
|
669
|
+
</Typography>
|
|
670
|
+
))}
|
|
671
|
+
{filesChangedSincePass.length > 5 && (
|
|
672
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
673
|
+
+{filesChangedSincePass.length - 5} more
|
|
674
|
+
</Typography>
|
|
675
|
+
)}
|
|
676
|
+
</Flex>
|
|
677
|
+
)}
|
|
678
|
+
</Flex>
|
|
679
|
+
) : (
|
|
680
|
+
<Flex variant="column-stretch-start-nowrap-4">
|
|
681
|
+
{acceptanceChecks.map((check: AcceptanceCheck) => (
|
|
682
|
+
<Flex key={check.name} variant="row-center-start-nowrap-6">
|
|
683
|
+
<Icon name="schedule" size={12} color="color-text-subtle" />
|
|
684
|
+
<Typography variant="smallCaption-regular" color="color-text-medium">
|
|
685
|
+
{check.name}
|
|
686
|
+
</Typography>
|
|
687
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle" mono>
|
|
688
|
+
{check.command}
|
|
689
|
+
</Typography>
|
|
690
|
+
</Flex>
|
|
691
|
+
))}
|
|
692
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
693
|
+
Not yet run
|
|
694
|
+
</Typography>
|
|
695
|
+
</Flex>
|
|
696
|
+
)}
|
|
697
|
+
</Flex>
|
|
698
|
+
)}
|
|
699
|
+
|
|
700
|
+
{/* ============================================================= */}
|
|
701
|
+
{/* DETAILS TAB (embedded edit form) */}
|
|
702
|
+
{/* ============================================================= */}
|
|
703
|
+
{activeTab === 'details' && (
|
|
704
|
+
<div style={{ margin: '-12px -16px', flex: 1, minHeight: 0 }}>
|
|
705
|
+
<TaskEditor
|
|
706
|
+
mode="edit"
|
|
707
|
+
taskId={taskId}
|
|
708
|
+
embedded
|
|
709
|
+
onSave={() => { fetchTask(); setActiveTab('actions'); }}
|
|
710
|
+
onCancel={() => setActiveTab('actions')}
|
|
711
|
+
onDelete={() => { fetchTask(); setActiveTab('actions'); }}
|
|
712
|
+
/>
|
|
713
|
+
</div>
|
|
714
|
+
)}
|
|
626
715
|
|
|
627
|
-
{/*
|
|
628
|
-
{
|
|
716
|
+
{/* ============================================================= */}
|
|
717
|
+
{/* RUNS TAB */}
|
|
718
|
+
{/* ============================================================= */}
|
|
719
|
+
{activeTab === 'runs' && (hasRuns
|
|
629
720
|
? (
|
|
630
721
|
<Flex variant="column-stretch-start-nowrap-8">
|
|
631
|
-
{/* Historical runs */}
|
|
632
722
|
{runItems.map(({ run, runTimeline }: { run: HistoricalRun; runTimeline: Array<Record<string, unknown>> }) => {
|
|
633
723
|
const runId = run.id;
|
|
634
724
|
const isExpanded = expandedRunId === runId;
|
|
@@ -649,7 +739,6 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
649
739
|
/>
|
|
650
740
|
);
|
|
651
741
|
})}
|
|
652
|
-
{/* Live run */}
|
|
653
742
|
{isLive && (
|
|
654
743
|
<TaskBlock
|
|
655
744
|
key="live-run"
|
|
@@ -672,160 +761,31 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
672
761
|
)}
|
|
673
762
|
</Flex>
|
|
674
763
|
)
|
|
675
|
-
:
|
|
764
|
+
: (
|
|
676
765
|
<EmptyState
|
|
677
766
|
icon="smartToy"
|
|
678
767
|
message="No runs yet"
|
|
679
768
|
description="This task has not been executed yet."
|
|
680
769
|
/>
|
|
681
|
-
)
|
|
770
|
+
)
|
|
771
|
+
)}
|
|
682
772
|
|
|
683
|
-
{/*
|
|
684
|
-
{
|
|
773
|
+
{/* ============================================================= */}
|
|
774
|
+
{/* SUBTASKS TAB */}
|
|
775
|
+
{/* ============================================================= */}
|
|
776
|
+
{activeTab === 'subtasks' && hasSubtasks && (
|
|
685
777
|
<Flex variant="column-stretch-start-nowrap-0">
|
|
686
778
|
{(subtasks ?? []).map((sub: Subtask) => (
|
|
687
|
-
<SubtaskRowItem
|
|
688
|
-
key={sub.id}
|
|
689
|
-
sub={sub}
|
|
690
|
-
onBack={onBack}
|
|
691
|
-
/>
|
|
779
|
+
<SubtaskRowItem key={sub.id} sub={sub} onClick={onBack} />
|
|
692
780
|
))}
|
|
693
781
|
</Flex>
|
|
694
782
|
)}
|
|
695
783
|
|
|
696
|
-
{/*
|
|
697
|
-
{
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
<Flex variant="column-stretch-start-nowrap-8">
|
|
701
|
-
<Typography variant="caption-thick" color="color-text-medium">Status</Typography>
|
|
702
|
-
<Flex variant="row-center-start-nowrap-6">
|
|
703
|
-
{(task.status === 'open' && (task.context?.runHistory?.length ?? 0) > 0) && (
|
|
704
|
-
<Button
|
|
705
|
-
size="xs"
|
|
706
|
-
variant="fill"
|
|
707
|
-
color="primary"
|
|
708
|
-
onClick={handleRetry}
|
|
709
|
-
loading={actionLoading === 'retry'}
|
|
710
|
-
disabled={!!actionLoading}
|
|
711
|
-
>
|
|
712
|
-
Retry Task
|
|
713
|
-
</Button>
|
|
714
|
-
)}
|
|
715
|
-
|
|
716
|
-
{(task.status === 'open' || task.status === 'in-progress') && (
|
|
717
|
-
<Button
|
|
718
|
-
size="xs"
|
|
719
|
-
variant="outlined"
|
|
720
|
-
color="danger"
|
|
721
|
-
onClick={handleCancel}
|
|
722
|
-
loading={actionLoading === 'cancel'}
|
|
723
|
-
disabled={!!actionLoading}
|
|
724
|
-
>
|
|
725
|
-
Cancel Task
|
|
726
|
-
</Button>
|
|
727
|
-
)}
|
|
728
|
-
</Flex>
|
|
729
|
-
</Flex>
|
|
730
|
-
|
|
731
|
-
{/* Priority */}
|
|
732
|
-
<Flex variant="column-stretch-start-nowrap-8">
|
|
733
|
-
<Typography variant="caption-thick" color="color-text-medium">Priority</Typography>
|
|
734
|
-
<Flex variant="row-center-start-nowrap-8">
|
|
735
|
-
<IconButton
|
|
736
|
-
icon="expandLess"
|
|
737
|
-
size="xs"
|
|
738
|
-
variant="outlined"
|
|
739
|
-
onClick={() => handlePriorityChange(1)}
|
|
740
|
-
title="Increase priority"
|
|
741
|
-
/>
|
|
742
|
-
<Typography variant="smallCaption-regular" color="color-text-high">
|
|
743
|
-
{`P${task.priority ?? 0}`}
|
|
744
|
-
</Typography>
|
|
745
|
-
<IconButton
|
|
746
|
-
icon="expandMore"
|
|
747
|
-
size="xs"
|
|
748
|
-
variant="outlined"
|
|
749
|
-
onClick={() => handlePriorityChange(-1)}
|
|
750
|
-
title="Decrease priority"
|
|
751
|
-
/>
|
|
752
|
-
</Flex>
|
|
753
|
-
</Flex>
|
|
754
|
-
|
|
755
|
-
{/* Assign Profile */}
|
|
756
|
-
{availableProfiles.length > 0 && (
|
|
757
|
-
<Flex variant="column-stretch-start-nowrap-8">
|
|
758
|
-
<Typography variant="caption-thick" color="color-text-medium">Assign Profile</Typography>
|
|
759
|
-
<Table
|
|
760
|
-
size="compact"
|
|
761
|
-
getRowKey={(row: Record<string, unknown>) => row.id as string}
|
|
762
|
-
columns={[
|
|
763
|
-
{
|
|
764
|
-
key: 'icon',
|
|
765
|
-
header: '',
|
|
766
|
-
width: '30px',
|
|
767
|
-
render: (_: unknown, row: Record<string, unknown>) => (
|
|
768
|
-
<Icon
|
|
769
|
-
name={(row.icon as string) || 'smartToy'}
|
|
770
|
-
size={14}
|
|
771
|
-
color={(row.color as string) || 'color-text-medium'}
|
|
772
|
-
/>
|
|
773
|
-
),
|
|
774
|
-
},
|
|
775
|
-
{
|
|
776
|
-
key: 'name',
|
|
777
|
-
header: 'Profile',
|
|
778
|
-
},
|
|
779
|
-
{
|
|
780
|
-
key: 'capabilities',
|
|
781
|
-
header: 'Capabilities',
|
|
782
|
-
render: (_: unknown, row: Record<string, unknown>) => {
|
|
783
|
-
const caps = (row.capabilities as Array<{ name: string; description: string }>) || [];
|
|
784
|
-
if (caps.length === 0) return null;
|
|
785
|
-
return (
|
|
786
|
-
<Flex variant="row-center-start-wrap-4">
|
|
787
|
-
{caps.slice(0, 4).map((cap) => (
|
|
788
|
-
<span key={cap.name} title={cap.description}>
|
|
789
|
-
<Chip label={cap.name} size="small" color="color-brand-main" />
|
|
790
|
-
</span>
|
|
791
|
-
))}
|
|
792
|
-
{caps.length > 4 && (
|
|
793
|
-
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
794
|
-
{`+${caps.length - 4}`}
|
|
795
|
-
</Typography>
|
|
796
|
-
)}
|
|
797
|
-
</Flex>
|
|
798
|
-
);
|
|
799
|
-
},
|
|
800
|
-
},
|
|
801
|
-
{
|
|
802
|
-
key: 'assigned',
|
|
803
|
-
header: 'Assign',
|
|
804
|
-
width: '50px',
|
|
805
|
-
align: 'right' as const,
|
|
806
|
-
render: (_: unknown, row: Record<string, unknown>) => {
|
|
807
|
-
const isAssigned = task.assignedProfile === (row.id as string);
|
|
808
|
-
return (
|
|
809
|
-
<Checkbox
|
|
810
|
-
checked={isAssigned}
|
|
811
|
-
onChange={() => handleAssignProfile(row.id as string)}
|
|
812
|
-
size="sm"
|
|
813
|
-
/>
|
|
814
|
-
);
|
|
815
|
-
},
|
|
816
|
-
},
|
|
817
|
-
]}
|
|
818
|
-
data={availableProfiles}
|
|
819
|
-
/>
|
|
820
|
-
</Flex>
|
|
821
|
-
)}
|
|
822
|
-
</Flex>
|
|
823
|
-
)}
|
|
824
|
-
|
|
825
|
-
{/* -- Context tab -- */}
|
|
826
|
-
{detailTab === 'context' && hasContext && (
|
|
784
|
+
{/* ============================================================= */}
|
|
785
|
+
{/* CONTEXT TAB */}
|
|
786
|
+
{/* ============================================================= */}
|
|
787
|
+
{activeTab === 'context' && hasContext && (
|
|
827
788
|
<Flex variant="column-stretch-start-nowrap-12">
|
|
828
|
-
{/* Files */}
|
|
829
789
|
{(task.context?.files?.length ?? 0) > 0 && (
|
|
830
790
|
<Flex variant="column-stretch-start-nowrap-4">
|
|
831
791
|
<Typography variant="caption-thick" color="color-text-medium">Files</Typography>
|
|
@@ -834,7 +794,7 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
834
794
|
key={file}
|
|
835
795
|
variant="smallCaption-regular"
|
|
836
796
|
color="color-text-high"
|
|
837
|
-
|
|
797
|
+
mono
|
|
838
798
|
>
|
|
839
799
|
{file}
|
|
840
800
|
</Typography>
|
|
@@ -842,7 +802,6 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
842
802
|
</Flex>
|
|
843
803
|
)}
|
|
844
804
|
|
|
845
|
-
{/* Notes */}
|
|
846
805
|
{task.context?.notes && (
|
|
847
806
|
<Flex variant="column-stretch-start-nowrap-4">
|
|
848
807
|
<Typography variant="caption-thick" color="color-text-medium">Notes</Typography>
|
|
@@ -856,64 +815,39 @@ function TaskDetailView({ taskId, onBack, onEdit }: TaskDetailViewProps) {
|
|
|
856
815
|
</Flex>
|
|
857
816
|
)}
|
|
858
817
|
|
|
859
|
-
{/* Run history (accumulated context) */}
|
|
860
818
|
{(task.context?.runHistory?.length ?? 0) > 0 && (
|
|
861
819
|
<Flex variant="column-stretch-start-nowrap-6">
|
|
862
820
|
<Typography variant="caption-thick" color="color-text-medium">Run History</Typography>
|
|
863
821
|
{(task.context?.runHistory ?? []).map((rs: Record<string, unknown>, i: number) => (
|
|
864
|
-
<Card
|
|
865
|
-
key={`rs-${i}`}
|
|
866
|
-
variant="bordered"
|
|
867
|
-
padding="compact"
|
|
868
|
-
style={{ gap: '4px' }}
|
|
869
|
-
>
|
|
822
|
+
<Card key={`rs-${i}`} variant="bordered" padding="compact" style={{ gap: '4px' }}>
|
|
870
823
|
<Flex variant="row-center-space-between-nowrap-8">
|
|
871
824
|
<Typography
|
|
872
825
|
variant="smallCaption-thick"
|
|
873
826
|
color={rs.outcome === 'success' ? 'color-status-positive' : 'color-status-negative'}
|
|
874
827
|
>
|
|
875
|
-
{
|
|
828
|
+
{rs.outcome === 'success' ? 'Success' : 'Failed'} ({(rs as Record<string, unknown>).botId ?? 'unknown bot'})
|
|
876
829
|
</Typography>
|
|
877
830
|
<Typography variant="smallCaption-regular" color="color-text-subtle">
|
|
878
|
-
{
|
|
831
|
+
{formatDuration((rs.durationMs as number) ?? 0)} · {rs.tokensUsed ?? 0} tok · {formatCost((rs.cost as number) ?? 0)}
|
|
879
832
|
</Typography>
|
|
880
833
|
</Flex>
|
|
881
834
|
<Typography variant="smallCaption-regular" color="color-text-medium">
|
|
882
835
|
{rs.summary as string ?? ''}
|
|
883
836
|
</Typography>
|
|
884
837
|
{(rs.filesModified as string[] ?? []).length > 0 && (
|
|
885
|
-
<Typography
|
|
886
|
-
|
|
887
|
-
color="color-text-subtle"
|
|
888
|
-
style={{ fontFamily: 'var(--font-mono, monospace)' }}
|
|
889
|
-
>
|
|
890
|
-
{`Files: ${(rs.filesModified as string[]).join(', ')}`}
|
|
838
|
+
<Typography variant="smallCaption-regular" color="color-text-subtle" mono>
|
|
839
|
+
Files: {(rs.filesModified as string[]).join(', ')}
|
|
891
840
|
</Typography>
|
|
892
841
|
)}
|
|
893
842
|
{rs.remainingWork && (
|
|
894
843
|
<Typography variant="smallCaption-regular" color="color-status-caution">
|
|
895
|
-
|
|
844
|
+
Remaining: {rs.remainingWork}
|
|
896
845
|
</Typography>
|
|
897
846
|
)}
|
|
898
847
|
</Card>
|
|
899
848
|
))}
|
|
900
849
|
</Flex>
|
|
901
850
|
)}
|
|
902
|
-
|
|
903
|
-
{/* Budget */}
|
|
904
|
-
{(task.tokensUsed > 0 || task.costUsed > 0) && (
|
|
905
|
-
<Flex variant="column-stretch-start-nowrap-4">
|
|
906
|
-
<Typography variant="caption-thick" color="color-text-medium">Budget</Typography>
|
|
907
|
-
<Flex variant="row-center-start-nowrap-16">
|
|
908
|
-
<Typography variant="smallCaption-regular" color="color-text-high">
|
|
909
|
-
{`Tokens: ${task.tokensUsed?.toLocaleString() ?? 0}`}
|
|
910
|
-
</Typography>
|
|
911
|
-
<Typography variant="smallCaption-regular" color="color-text-high">
|
|
912
|
-
{`Cost: $${(task.costUsed ?? 0).toFixed(3)}${task.budgetCost ? ` / $${task.budgetCost.toFixed(2)}` : ''}`}
|
|
913
|
-
</Typography>
|
|
914
|
-
</Flex>
|
|
915
|
-
</Flex>
|
|
916
|
-
)}
|
|
917
851
|
</Flex>
|
|
918
852
|
)}
|
|
919
853
|
</Flex>
|