@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.
Files changed (38) hide show
  1. package/dist/bot/preflight.d.ts.map +1 -1
  2. package/dist/bot/preflight.js +26 -0
  3. package/dist/bot/preflight.js.map +1 -1
  4. package/dist/bot/task-create-handler.d.ts +9 -0
  5. package/dist/bot/task-create-handler.d.ts.map +1 -1
  6. package/dist/bot/task-create-handler.js +26 -0
  7. package/dist/bot/task-create-handler.js.map +1 -1
  8. package/dist/node-types/agent-execute.d.ts.map +1 -1
  9. package/dist/node-types/agent-execute.js +26 -9
  10. package/dist/node-types/agent-execute.js.map +1 -1
  11. package/dist/node-types/plan-task.d.ts.map +1 -1
  12. package/dist/node-types/plan-task.js +28 -2
  13. package/dist/node-types/plan-task.js.map +1 -1
  14. package/dist/ui/bot-slot-card.js +10 -0
  15. package/dist/ui/budget-bar.js +5 -3
  16. package/dist/ui/budget-strip.js +156 -0
  17. package/dist/ui/chat-task-result.js +22 -27
  18. package/dist/ui/instance-stream-view.js +36 -0
  19. package/dist/ui/swarm-dashboard.js +1596 -1654
  20. package/dist/ui/task-detail-view.js +973 -485
  21. package/dist/ui/task-editor.js +32 -34
  22. package/dist/ui/task-pool-list.js +11 -3
  23. package/flowweaver.manifest.json +1 -1
  24. package/package.json +3 -2
  25. package/src/bot/preflight.ts +26 -0
  26. package/src/bot/task-create-handler.ts +39 -0
  27. package/src/node-types/agent-execute.ts +27 -10
  28. package/src/node-types/plan-task.ts +25 -2
  29. package/src/ui/bot-slot-card.tsx +23 -0
  30. package/src/ui/budget-bar.tsx +13 -5
  31. package/src/ui/budget-strip.tsx +199 -0
  32. package/src/ui/chat-task-result.tsx +5 -25
  33. package/src/ui/instance-stream-view.tsx +50 -1
  34. package/src/ui/swarm-dashboard.tsx +89 -84
  35. package/src/ui/task-detail-view.tsx +376 -442
  36. package/src/ui/task-editor.tsx +65 -96
  37. package/src/ui/task-pool-list.tsx +3 -12
  38. package/src/ui/task-status.ts +60 -0
@@ -1,29 +1,31 @@
1
1
  /**
2
- * Task Detail View — full task detail with context, subtasks, and run history.
2
+ * Task Detail View — full task detail with rich header, acceptance checks,
3
+ * live status, and run history.
3
4
  *
4
- * Shows a single task's complete information:
5
- * - Header: title, status, assigned profile, priority, attempt count
6
- * - Context: files list, notes
7
- * - Subtasks: list with status icons (clickable)
8
- * - Run history: each run as a TaskBlock
9
- * - Active run: live streaming via useEventStream
10
- * - Back button to return to dashboard
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, Badge, Icon, IconButton, TaskBlock, Button,
15
- Card, Chip, Checkbox, Table, Tabs, EmptyState, toast, usePackWorkspace, useEventStream,
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: TaskStatus;
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: TaskStatus;
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
- // Status mapping
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 for replaced styled components
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-default)',
110
+ borderBottom: '1px solid var(--color-border-faint)',
111
111
  flexShrink: 0,
112
112
  };
113
113
 
114
- const sectionStyle: React.CSSProperties = {
115
- padding: '12px 16px',
116
- borderBottom: '1px solid var(--color-border-default)',
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
- // Small hover-wrapper components
121
+ // Sub-components
153
122
  // ---------------------------------------------------------------------------
154
123
 
155
- function FileItemRow({ children }: { children: React.ReactNode }) {
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-0"
160
- style={hovered ? fileItemHoverStyle : fileItemStyle}
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
- {children}
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
- function SubtaskRowItem({ sub, onBack }: { sub: Subtask; onBack: () => void }) {
170
- const [hovered, setHovered] = useState(false);
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
- <Flex
173
- variant="row-center-start-nowrap-8"
174
- style={hovered ? subtaskRowHoverStyle : subtaskRowStyle}
175
- onClick={() => onBack()}
176
- onMouseEnter={() => setHovered(true)}
177
- onMouseLeave={() => setHovered(false)}
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
- <StatusIcon
180
- status={statusToIcon[sub.status] || 'pending'}
181
- size="sm"
182
- />
183
- <Flex
184
- variant="row-center-start-nowrap-0"
185
- style={{ flex: 1, minWidth: 0 }}
186
- >
187
- <Typography variant="caption-regular" truncate={true}>{sub.title}</Typography>
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
- </Flex>
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; // Don't overwrite if RunStore data exists
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 (err) {
253
- // Non-fatal — keep existing data
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
- // -- Detail tab state --
315
- const [detailTab, setDetailTab] = useState('runs');
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 [availableProfiles, setAvailableProfiles] = useState<Array<{ id: string; name: string; icon?: string; color?: string }>>([]);
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
- callTool('fw_weaver_profile_list', {}).then((raw: unknown) => {
324
- const data = typeof raw === 'string' ? JSON.parse(raw as string) : raw;
325
- if (Array.isArray(data)) setAvailableProfiles(data);
326
- }).catch(() => {});
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 handleAssignProfile = useCallback(async (profileId: string) => {
360
- setActionLoading('assign-profile');
373
+ const handleRunChecks = useCallback(async () => {
374
+ setRunningChecks(true);
361
375
  try {
362
- const newProfile = task?.assignedProfile === profileId ? undefined : profileId;
363
- await callTool('fw_weaver_task_update', { id: taskId, assignedProfile: newProfile ?? null });
364
- toast(newProfile ? `Assigned profile ${profileId}` : `Unassigned profile`, { type: 'success' });
365
- fetchTask();
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 assign profile', { type: 'error' });
382
+ toast(err instanceof Error ? err.message : 'Failed to run checks', { type: 'error' });
368
383
  }
369
- setActionLoading(null);
370
- }, [callTool, taskId, task, fetchTask]);
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 state --
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
- // -- Approval handling --
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
- // -- Loading state --
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
- const hasContext = (task.context?.files?.length > 0) || task.context?.notes;
513
- const hasSubtasks = task.isParent && subtasks.length > 0;
514
- const hasRuns = runItems.length > 0 || isLive;
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
- variant="column-stretch-start-nowrap-0"
519
- style={{ width: '100%', height: '100%', overflow: 'hidden' }}
520
- >
521
- {/* -- Header -- */}
522
- <Flex
523
- variant="column-stretch-start-nowrap-6"
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={statusToIcon[task.status] || 'pending'}
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
- {onEdit && (
548
- <IconButton
549
- icon="edit"
550
- size="xs"
551
- variant="clear"
552
- onClick={() => onEdit(taskId)}
553
- title="Edit task"
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
- {/* Meta row: status tag, assigned profile, priority, attempts */}
549
+ {/* Row 2: inline chips — status, profile, priority, attempts, cost */}
559
550
  <Flex variant="row-center-start-wrap-6">
560
- <Chip
561
- label={statusToLabel[task.status as TaskStatus] || task.status || 'open'}
562
- size="small"
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
- <Chip key={`profile-${task.assignedProfile}`} label={task.assignedProfile} size="small" color="color-status-info" />
556
+ <StatusChip status="info" icon="person">{task.assignedProfile}</StatusChip>
571
557
  )}
572
558
 
573
559
  {task.priority > 0 && (
574
- <Chip
575
- label={`P${task.priority}`}
576
- size="small"
577
- color={task.priority >= 3 ? 'color-status-caution' : 'color-status-info'}
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
- {(task.context?.runHistory?.length ?? 0) > 0 && (
582
- <Typography variant="smallCaption-regular" color="color-text-subtle">
583
- {`${task.context.runHistory.length} run${task.context.runHistory.length !== 1 ? 's' : ''}`}
584
- </Typography>
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
- {/* -- Tabs -- */}
599
+ {/* ----------------------------------------------------------------- */}
600
+ {/* TABS */}
601
+ {/* ----------------------------------------------------------------- */}
611
602
  <Tabs
612
- tabs={[
613
- { id: 'runs', title: `Runs (${runItems.length}${isLive ? '+1' : ''})` },
614
- ...(hasSubtasks ? [{ id: 'subtasks', title: `Subtasks (${subtasks.filter((s: Subtask) => s.status === 'done').length}/${subtasks.length})` }] : []),
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
- {/* -- Tab content -- */}
609
+ {/* ----------------------------------------------------------------- */}
610
+ {/* TAB CONTENT */}
611
+ {/* ----------------------------------------------------------------- */}
624
612
  <ScrollArea ref={scrollRef} style={{ flex: 1 }}>
625
- <Flex variant="column-stretch-start-nowrap-8" style={{ padding: '12px 16px' }}>
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
- {/* -- Runs tab -- */}
628
- {detailTab === 'runs' && (hasRuns || (task.context?.runHistory?.length ?? 0) > 0)
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
- : detailTab === 'runs' && (
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
- {/* -- Subtasks tab -- */}
684
- {detailTab === 'subtasks' && hasSubtasks && (
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
- {/* -- Actions tab -- */}
697
- {detailTab === 'actions' && (
698
- <Flex variant="column-stretch-start-nowrap-16">
699
- {/* Status actions */}
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
- style={{ fontFamily: 'var(--font-mono, monospace)' }}
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
- {`${rs.outcome === 'success' ? 'Success' : 'Failed'} (${(rs as Record<string, unknown>).botId ?? 'unknown bot'})`}
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
- {`${Math.round((rs.durationMs as number ?? 0) / 1000)}s · ${rs.tokensUsed ?? 0} tok · $${((rs.cost as number) ?? 0).toFixed(3)}`}
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
- variant="smallCaption-regular"
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
- {`Remaining: ${rs.remainingWork}`}
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>