@synergenius/flow-weaver-pack-weaver 0.9.81 → 0.9.83

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.
@@ -0,0 +1,628 @@
1
+ /**
2
+ * TaskEditor — unified component for creating and editing tasks.
3
+ *
4
+ * Handles both modes:
5
+ * - Create: all fields start empty/default, saves via fw_weaver_task_create.
6
+ * - Edit: loads task by ID on mount, saves via fw_weaver_task_update.
7
+ *
8
+ * Calls tool functions internally via usePackWorkspace.
9
+ *
10
+ * Pattern: CommonJS require for platform deps, ESM import for local files,
11
+ * React.createElement throughout, module.exports at end.
12
+ */
13
+ const React = require('react');
14
+ const { useState, useEffect, useCallback } = React;
15
+ const {
16
+ Flex, Typography, Input, Button, IconButton, Tag, Field,
17
+ toast, usePackWorkspace,
18
+ } = require('@fw/plugin-ui-kit');
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Types
22
+ // ---------------------------------------------------------------------------
23
+
24
+ interface TaskEditorProps {
25
+ mode: 'create' | 'edit';
26
+ taskId?: string;
27
+ onSave: () => void;
28
+ onCancel: () => void;
29
+ onDelete?: () => void;
30
+ }
31
+
32
+ type TaskStatus = 'pending' | 'in-progress' | 'blocked' | 'done' | 'failed' | 'cancelled';
33
+
34
+ interface TaskData {
35
+ id: string;
36
+ title: string;
37
+ description: string;
38
+ status: TaskStatus;
39
+ priority: number;
40
+ complexity?: 'trivial' | 'simple' | 'moderate' | 'complex';
41
+ assignedProfile?: string;
42
+ maxAttempts: number;
43
+ budgetTokens?: number;
44
+ budgetCost?: number;
45
+ dependsOn: string[];
46
+ context: {
47
+ files: string[];
48
+ notes: string;
49
+ lastError?: string;
50
+ runSummaries: unknown[];
51
+ };
52
+ tokensUsed: number;
53
+ costUsed: number;
54
+ createdBy: string;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Constants
61
+ // ---------------------------------------------------------------------------
62
+
63
+ const PRIORITY_OPTIONS = [
64
+ { id: '0', label: '0 - None' },
65
+ { id: '1', label: '1 - Low' },
66
+ { id: '2', label: '2 - Medium' },
67
+ { id: '3', label: '3 - High' },
68
+ { id: '4', label: '4 - Critical' },
69
+ ];
70
+
71
+ const COMPLEXITY_OPTIONS = [
72
+ { id: '', label: 'Not set' },
73
+ { id: 'trivial', label: 'Trivial' },
74
+ { id: 'simple', label: 'Simple' },
75
+ { id: 'moderate', label: 'Moderate' },
76
+ { id: 'complex', label: 'Complex' },
77
+ ];
78
+
79
+ const STATUS_COLOR: Record<string, string> = {
80
+ 'pending': 'secondary',
81
+ 'in-progress': 'info',
82
+ 'blocked': 'caution',
83
+ 'done': 'positive',
84
+ 'failed': 'negative',
85
+ 'cancelled': 'negative',
86
+ };
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Component
90
+ // ---------------------------------------------------------------------------
91
+
92
+ function TaskEditor({ mode, taskId, onSave, onCancel, onDelete }: TaskEditorProps) {
93
+ const ctx = usePackWorkspace();
94
+ const { callTool } = ctx;
95
+
96
+ // --- Field state ---
97
+ const [title, setTitle] = useState('');
98
+ const [description, setDescription] = useState('');
99
+ const [priority, setPriority] = useState('0');
100
+ const [complexity, setComplexity] = useState('');
101
+ const [assignedProfile, setAssignedProfile] = useState('');
102
+ const [maxAttempts, setMaxAttempts] = useState('3');
103
+ const [budgetTokens, setBudgetTokens] = useState('');
104
+ const [budgetCost, setBudgetCost] = useState('');
105
+ const [notes, setNotes] = useState('');
106
+ const [files, setFiles] = useState<string[]>([]);
107
+ const [newFile, setNewFile] = useState('');
108
+ const [dependsOn, setDependsOn] = useState<string[]>([]);
109
+ const [newDep, setNewDep] = useState('');
110
+
111
+ // --- Edit-only read-only state ---
112
+ const [taskData, setTaskData] = useState<TaskData | null>(null);
113
+
114
+ // --- Profiles for select ---
115
+ const [profiles, setProfiles] = useState<Array<{ id: string; name: string }>>([]);
116
+
117
+ const [loading, setLoading] = useState(mode === 'edit');
118
+
119
+ // --- Load profiles ---
120
+ useEffect(() => {
121
+ (async () => {
122
+ try {
123
+ const raw = await callTool('fw_weaver_profile_list', {});
124
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
125
+ if (Array.isArray(data)) {
126
+ setProfiles(data.map((p: Record<string, unknown>) => ({
127
+ id: p.id as string,
128
+ name: (p.name as string) || (p.id as string),
129
+ })));
130
+ }
131
+ } catch { /* non-fatal */ }
132
+ })();
133
+ }, [callTool]);
134
+
135
+ // --- Load task for edit mode ---
136
+ useEffect(() => {
137
+ if (mode !== 'edit' || !taskId) return;
138
+ let cancelled = false;
139
+
140
+ (async () => {
141
+ try {
142
+ const raw = await callTool('fw_weaver_task_get', { id: taskId });
143
+ if (cancelled) return;
144
+ const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
145
+ const t = (data?.task ?? data) as TaskData;
146
+ if (!t || !t.id) {
147
+ toast('Task not found', { type: 'error' });
148
+ onCancel();
149
+ return;
150
+ }
151
+ setTaskData(t);
152
+ setTitle(t.title || '');
153
+ setDescription(t.description || '');
154
+ setPriority(String(t.priority ?? 0));
155
+ setComplexity(t.complexity || '');
156
+ setAssignedProfile(t.assignedProfile || '');
157
+ setMaxAttempts(String(t.maxAttempts ?? 3));
158
+ setBudgetTokens(t.budgetTokens != null ? String(t.budgetTokens) : '');
159
+ setBudgetCost(t.budgetCost != null ? String(t.budgetCost) : '');
160
+ setNotes(t.context?.notes || '');
161
+ setFiles(t.context?.files || []);
162
+ setDependsOn(t.dependsOn || []);
163
+ } catch {
164
+ toast('Failed to load task', { type: 'error' });
165
+ onCancel();
166
+ } finally {
167
+ if (!cancelled) setLoading(false);
168
+ }
169
+ })();
170
+
171
+ return () => { cancelled = true; };
172
+ }, [mode, taskId, callTool]);
173
+
174
+ // --- Dirty-form check on back/cancel ---
175
+ const isDirty = useCallback(() => {
176
+ if (mode === 'create') {
177
+ return !!(title.trim() || description.trim() || notes.trim() || files.length > 0 || dependsOn.length > 0
178
+ || priority !== '0' || complexity || assignedProfile || maxAttempts !== '3' || budgetTokens || budgetCost);
179
+ }
180
+ // Edit mode: compare against loaded data
181
+ if (!taskData) return false;
182
+ return title.trim() !== (taskData.title || '')
183
+ || description.trim() !== (taskData.description || '')
184
+ || notes.trim() !== (taskData.context?.notes || '')
185
+ || JSON.stringify(files) !== JSON.stringify(taskData.context?.files || [])
186
+ || priority !== String(taskData.priority ?? 0)
187
+ || complexity !== (taskData.complexity || '')
188
+ || assignedProfile !== (taskData.assignedProfile || '')
189
+ || maxAttempts !== String(taskData.maxAttempts ?? 3)
190
+ || budgetTokens !== (taskData.budgetTokens != null ? String(taskData.budgetTokens) : '')
191
+ || budgetCost !== (taskData.budgetCost != null ? String(taskData.budgetCost) : '');
192
+ }, [mode, title, description, notes, files, dependsOn, priority, complexity, assignedProfile, maxAttempts, budgetTokens, budgetCost, taskData]);
193
+
194
+ const handleBack = useCallback(async () => {
195
+ if (isDirty()) {
196
+ const ok = await ctx.confirm('Discard unsaved changes?', {
197
+ title: 'Unsaved Changes',
198
+ state: 'warning',
199
+ });
200
+ if (!ok) return;
201
+ }
202
+ onCancel();
203
+ }, [isDirty, ctx, onCancel]);
204
+
205
+ // --- Files list ---
206
+ const handleAddFile = useCallback(() => {
207
+ const trimmed = newFile.trim();
208
+ if (!trimmed) return;
209
+ setFiles((prev: string[]) => [...prev, trimmed]);
210
+ setNewFile('');
211
+ }, [newFile]);
212
+
213
+ const handleRemoveFile = useCallback((index: number) => {
214
+ setFiles((prev: string[]) => prev.filter((_: string, i: number) => i !== index));
215
+ }, []);
216
+
217
+ // --- Dependencies list ---
218
+ const handleAddDep = useCallback(() => {
219
+ const trimmed = newDep.trim();
220
+ if (!trimmed) return;
221
+ setDependsOn((prev: string[]) => [...prev, trimmed]);
222
+ setNewDep('');
223
+ }, [newDep]);
224
+
225
+ const handleRemoveDep = useCallback((index: number) => {
226
+ setDependsOn((prev: string[]) => prev.filter((_: string, i: number) => i !== index));
227
+ }, []);
228
+
229
+ // --- Save ---
230
+ const handleSave = useCallback(async () => {
231
+ if (!title.trim()) {
232
+ toast('Title is required', { type: 'error' });
233
+ return;
234
+ }
235
+
236
+ try {
237
+ if (mode === 'create') {
238
+ const args: Record<string, unknown> = {
239
+ title: title.trim(),
240
+ description: description.trim(),
241
+ priority: parseInt(priority, 10) || 0,
242
+ maxAttempts: parseInt(maxAttempts, 10) || 3,
243
+ };
244
+ if (complexity) args.complexity = complexity;
245
+ if (assignedProfile) args.assignedProfile = assignedProfile;
246
+ if (budgetTokens) args.budgetTokens = parseInt(budgetTokens, 10);
247
+ if (budgetCost) args.budgetCost = parseFloat(budgetCost);
248
+ if (dependsOn.length > 0) args.dependsOn = dependsOn;
249
+
250
+ const result = await callTool('fw_weaver_task_create', args);
251
+ const createData = typeof result === 'string' ? JSON.parse(result) : result;
252
+ const createdId = createData?.task?.id;
253
+
254
+ // task_create doesn't support context fields — follow up with an update
255
+ if (createdId && (notes.trim() || files.length > 0)) {
256
+ await callTool('fw_weaver_task_update', {
257
+ id: createdId,
258
+ context: {
259
+ notes: notes.trim(),
260
+ files,
261
+ runSummaries: [],
262
+ },
263
+ });
264
+ }
265
+
266
+ toast('Task created', { type: 'success' });
267
+ } else {
268
+ // Edit mode — build patch
269
+ const patch: Record<string, unknown> = {
270
+ id: taskId,
271
+ title: title.trim(),
272
+ description: description.trim(),
273
+ priority: parseInt(priority, 10) || 0,
274
+ maxAttempts: parseInt(maxAttempts, 10) || 3,
275
+ complexity: complexity || undefined,
276
+ assignedProfile: assignedProfile || null,
277
+ };
278
+ if (budgetTokens) {
279
+ patch.budgetTokens = parseInt(budgetTokens, 10);
280
+ } else {
281
+ patch.budgetTokens = undefined;
282
+ }
283
+ if (budgetCost) {
284
+ patch.budgetCost = parseFloat(budgetCost);
285
+ } else {
286
+ patch.budgetCost = undefined;
287
+ }
288
+
289
+ // Update context (preserve runSummaries and lastError from existing data)
290
+ patch.context = {
291
+ files,
292
+ notes: notes.trim(),
293
+ runSummaries: taskData?.context?.runSummaries || [],
294
+ lastError: taskData?.context?.lastError,
295
+ };
296
+
297
+ await callTool('fw_weaver_task_update', patch);
298
+ toast('Task updated', { type: 'success' });
299
+ }
300
+ onSave();
301
+ } catch (err: unknown) {
302
+ toast(err instanceof Error ? err.message : `Failed to ${mode} task`, { type: 'error' });
303
+ }
304
+ }, [mode, taskId, title, description, priority, complexity, assignedProfile, maxAttempts, budgetTokens, budgetCost, notes, files, dependsOn, taskData, callTool, onSave]);
305
+
306
+ // --- Delete (cancel task) ---
307
+ const handleDelete = useCallback(async () => {
308
+ if (!taskId) return;
309
+ const ok = await ctx.confirm('Are you sure you want to cancel this task?', {
310
+ title: 'Cancel Task',
311
+ confirmLabel: 'Cancel Task',
312
+ state: 'danger',
313
+ });
314
+ if (!ok) return;
315
+ try {
316
+ await callTool('fw_weaver_task_cancel', { id: taskId });
317
+ toast('Task cancelled', { type: 'success' });
318
+ if (onDelete) onDelete();
319
+ } catch (err: unknown) {
320
+ toast(err instanceof Error ? err.message : 'Failed to cancel task', { type: 'error' });
321
+ }
322
+ }, [taskId, callTool, onDelete, ctx]);
323
+
324
+ // --- Loading state ---
325
+ if (loading) {
326
+ return React.createElement(Flex, {
327
+ variant: 'column-center-center-nowrap-12',
328
+ style: { padding: '24px 16px' },
329
+ },
330
+ React.createElement(Typography, { variant: 'caption-regular', color: 'color-text-subtle' }, 'Loading task...'),
331
+ );
332
+ }
333
+
334
+ // --- Profile select options ---
335
+ const profileOptions = [
336
+ { id: '', label: 'Not assigned' },
337
+ ...profiles.map((p: { id: string; name: string }) => ({ id: p.id, label: p.name })),
338
+ ];
339
+
340
+ // --- Render ---
341
+ return React.createElement(Flex, {
342
+ variant: 'column-stretch-start-nowrap-0',
343
+ style: { width: '100%', height: '100%', overflow: 'hidden' },
344
+ },
345
+ // -- Header bar --
346
+ React.createElement(Flex, {
347
+ variant: 'row-center-space-between-nowrap-8',
348
+ style: { padding: '8px 16px', flexShrink: 0, borderBottom: '1px solid var(--color-border-default)' },
349
+ },
350
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-8' },
351
+ React.createElement(IconButton, {
352
+ icon: 'back', size: 'xs', variant: 'clear',
353
+ onClick: handleBack,
354
+ title: 'Back',
355
+ }),
356
+ React.createElement(Typography, { variant: 'caption-thick', color: 'color-text-high' },
357
+ mode === 'create' ? 'Create Task' : 'Edit Task',
358
+ ),
359
+ ),
360
+ mode === 'edit' && onDelete && React.createElement(IconButton, {
361
+ icon: 'outlinedDelete', size: 'sm', variant: 'clear', color: 'danger',
362
+ onClick: handleDelete,
363
+ title: 'Cancel task',
364
+ }),
365
+ ),
366
+
367
+ // -- Scrollable form body --
368
+ React.createElement(Flex, {
369
+ variant: 'column-stretch-start-nowrap-10',
370
+ style: { flex: 1, minHeight: 0, overflow: 'auto', padding: '12px 16px' },
371
+ },
372
+
373
+ // -- Edit-only: Status tag --
374
+ mode === 'edit' && taskData && React.createElement(Field, { label: 'Status' },
375
+ React.createElement(Tag, {
376
+ size: 'small',
377
+ color: STATUS_COLOR[taskData.status] || 'secondary',
378
+ }, taskData.status),
379
+ ),
380
+
381
+ // -- Title --
382
+ React.createElement(Field, { label: 'Title' },
383
+ React.createElement(Input, {
384
+ type: 'text', size: 'small',
385
+ placeholder: 'Task title (required)',
386
+ value: title,
387
+ onChange: (v: string) => setTitle(v),
388
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
389
+ inputBoxStyle: { maxWidth: 'none' },
390
+ }),
391
+ ),
392
+
393
+ // -- Description --
394
+ React.createElement(Field, { label: 'Description' },
395
+ React.createElement(Input, {
396
+ type: 'text', size: 'small',
397
+ placeholder: 'Detailed task description',
398
+ value: description,
399
+ onChange: (v: string) => setDescription(v),
400
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
401
+ inputBoxStyle: { maxWidth: 'none' },
402
+ }),
403
+ ),
404
+
405
+ // -- Priority --
406
+ React.createElement(Field, { label: 'Priority' },
407
+ React.createElement(Input, {
408
+ type: 'select', size: 'small',
409
+ options: PRIORITY_OPTIONS,
410
+ optionId: priority,
411
+ onChange: (id: string) => setPriority(id),
412
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
413
+ }),
414
+ ),
415
+
416
+ // -- Complexity --
417
+ React.createElement(Field, { label: 'Complexity' },
418
+ React.createElement(Input, {
419
+ type: 'select', size: 'small',
420
+ options: COMPLEXITY_OPTIONS,
421
+ optionId: complexity,
422
+ onChange: (id: string) => setComplexity(id),
423
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
424
+ }),
425
+ ),
426
+
427
+ // -- Assigned Profile --
428
+ React.createElement(Field, { label: 'Profile' },
429
+ React.createElement(Input, {
430
+ type: 'select', size: 'small',
431
+ options: profileOptions,
432
+ optionId: assignedProfile,
433
+ onChange: (id: string) => setAssignedProfile(id),
434
+ placeholder: 'Select profile',
435
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
436
+ }),
437
+ ),
438
+
439
+ // -- Max Attempts --
440
+ React.createElement(Field, { label: 'Max Attempts' },
441
+ React.createElement(Input, {
442
+ type: 'number', size: 'small',
443
+ placeholder: '3',
444
+ value: maxAttempts,
445
+ onChange: (v: string) => setMaxAttempts(v),
446
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
447
+ inputBoxStyle: { maxWidth: 'none' },
448
+ }),
449
+ ),
450
+
451
+ // -- Budget Tokens --
452
+ React.createElement(Field, { label: 'Budget Tokens' },
453
+ React.createElement(Input, {
454
+ type: 'number', size: 'small',
455
+ placeholder: 'Optional token limit',
456
+ value: budgetTokens,
457
+ onChange: (v: string) => setBudgetTokens(v),
458
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
459
+ inputBoxStyle: { maxWidth: 'none' },
460
+ }),
461
+ ),
462
+
463
+ // -- Budget Cost --
464
+ React.createElement(Field, { label: 'Budget Cost' },
465
+ React.createElement(Input, {
466
+ type: 'number', size: 'small',
467
+ placeholder: 'Optional cost limit (USD)',
468
+ value: budgetCost,
469
+ onChange: (v: string) => setBudgetCost(v),
470
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
471
+ inputBoxStyle: { maxWidth: 'none' },
472
+ }),
473
+ ),
474
+
475
+ // -- Notes --
476
+ React.createElement(Field, { label: 'Notes' },
477
+ React.createElement(Input, {
478
+ type: 'text', size: 'small',
479
+ placeholder: 'Optional notes for context',
480
+ value: notes,
481
+ onChange: (v: string) => setNotes(v),
482
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
483
+ inputBoxStyle: { maxWidth: 'none' },
484
+ }),
485
+ ),
486
+
487
+ // -- Files --
488
+ React.createElement(Field, { label: 'Files', align: 'start' },
489
+ React.createElement(Flex, { variant: 'column-stretch-start-nowrap-6' },
490
+ // Add file row
491
+ React.createElement(Flex, { variant: 'row-center-start-nowrap-4', style: { overflow: 'hidden' } },
492
+ React.createElement(Input, {
493
+ type: 'text', size: 'small',
494
+ placeholder: 'File path',
495
+ value: newFile,
496
+ onChange: (v: string) => setNewFile(v),
497
+ onEnter: handleAddFile,
498
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
499
+ inputBoxStyle: { maxWidth: 'none' },
500
+ }),
501
+ React.createElement(IconButton, {
502
+ icon: 'add', size: 'sm', variant: 'outlined', color: 'primary',
503
+ onClick: handleAddFile,
504
+ disabled: !newFile.trim(),
505
+ }),
506
+ ),
507
+ // File list
508
+ ...files.map((file: string, idx: number) =>
509
+ React.createElement(Flex, {
510
+ key: `file-${idx}`,
511
+ variant: 'row-center-start-nowrap-6',
512
+ style: { paddingLeft: '4px' },
513
+ },
514
+ React.createElement(Typography, {
515
+ variant: 'smallCaption-regular', color: 'color-text-high',
516
+ style: { flex: 1, minWidth: 0, fontFamily: 'var(--font-mono, monospace)' },
517
+ }, file),
518
+ React.createElement(IconButton, {
519
+ icon: 'close', size: 'xs', variant: 'clear', color: 'danger',
520
+ onClick: () => handleRemoveFile(idx),
521
+ }),
522
+ ),
523
+ ),
524
+ ),
525
+ ),
526
+
527
+ // -- Dependencies --
528
+ React.createElement(Field, { label: 'Dependencies', align: 'start' },
529
+ React.createElement(Flex, { variant: 'column-stretch-start-nowrap-6' },
530
+ // Add dependency row (create mode only)
531
+ mode === 'create' && React.createElement(Flex, { variant: 'row-center-start-nowrap-4', style: { overflow: 'hidden' } },
532
+ React.createElement(Input, {
533
+ type: 'text', size: 'small',
534
+ placeholder: 'Task ID',
535
+ value: newDep,
536
+ onChange: (v: string) => setNewDep(v),
537
+ onEnter: handleAddDep,
538
+ defaultBoxStyle: { flex: 1, minWidth: 0 },
539
+ inputBoxStyle: { maxWidth: 'none' },
540
+ }),
541
+ React.createElement(IconButton, {
542
+ icon: 'add', size: 'sm', variant: 'outlined', color: 'primary',
543
+ onClick: handleAddDep,
544
+ disabled: !newDep.trim(),
545
+ }),
546
+ ),
547
+ // Dependency list
548
+ dependsOn.length > 0
549
+ ? React.createElement(Flex, { variant: 'column-stretch-start-nowrap-4' },
550
+ ...dependsOn.map((dep: string, idx: number) =>
551
+ React.createElement(Flex, {
552
+ key: `dep-${idx}`,
553
+ variant: 'row-center-start-nowrap-6',
554
+ style: { paddingLeft: '4px' },
555
+ },
556
+ React.createElement(Typography, {
557
+ variant: 'smallCaption-regular', color: 'color-text-high',
558
+ style: { flex: 1, minWidth: 0, fontFamily: 'var(--font-mono, monospace)' },
559
+ }, dep),
560
+ mode === 'create' && React.createElement(IconButton, {
561
+ icon: 'close', size: 'xs', variant: 'clear', color: 'danger',
562
+ onClick: () => handleRemoveDep(idx),
563
+ }),
564
+ ),
565
+ ),
566
+ )
567
+ : React.createElement(Typography, {
568
+ variant: 'smallCaption-regular', color: 'color-text-subtle',
569
+ style: { paddingLeft: '4px' },
570
+ }, 'None'),
571
+ mode === 'edit' && React.createElement(Typography, {
572
+ variant: 'smallCaption-regular', color: 'color-text-subtle',
573
+ style: { paddingLeft: '4px', fontStyle: 'italic' },
574
+ }, 'Dependencies cannot be changed after creation'),
575
+ ),
576
+ ),
577
+
578
+ // -- Edit-only: read-only metadata --
579
+ mode === 'edit' && taskData && React.createElement(Flex, {
580
+ variant: 'column-stretch-start-nowrap-10',
581
+ style: { marginTop: 8, paddingTop: 12, borderTop: '1px solid var(--color-border-default)' },
582
+ },
583
+ React.createElement(Field, { label: 'Created by' },
584
+ React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
585
+ taskData.createdBy || 'unknown'),
586
+ ),
587
+ React.createElement(Field, { label: 'Created at' },
588
+ React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
589
+ taskData.createdAt ? new Date(taskData.createdAt).toLocaleString() : '-'),
590
+ ),
591
+ React.createElement(Field, { label: 'Updated at' },
592
+ React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
593
+ taskData.updatedAt ? new Date(taskData.updatedAt).toLocaleString() : '-'),
594
+ ),
595
+ React.createElement(Field, { label: 'Tokens used' },
596
+ React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
597
+ (taskData.tokensUsed ?? 0).toLocaleString()),
598
+ ),
599
+ React.createElement(Field, { label: 'Cost used' },
600
+ React.createElement(Typography, { variant: 'smallCaption-regular', color: 'color-text-medium' },
601
+ `$${(taskData.costUsed ?? 0).toFixed(3)}`),
602
+ ),
603
+ taskData.context?.lastError && React.createElement(Field, { label: 'Last error', align: 'start' },
604
+ React.createElement(Typography, {
605
+ variant: 'smallCaption-regular', color: 'color-status-negative',
606
+ style: { fontFamily: 'var(--font-mono, monospace)', whiteSpace: 'pre-wrap' },
607
+ }, taskData.context.lastError),
608
+ ),
609
+ ),
610
+
611
+ ),
612
+
613
+ // -- Footer bar with save button --
614
+ React.createElement(Flex, {
615
+ variant: 'row-center-end-nowrap-8',
616
+ style: { padding: '8px 16px', flexShrink: 0, borderTop: '1px solid var(--color-border-default)' },
617
+ },
618
+ React.createElement(Button, {
619
+ size: 'xs', variant: 'fill', color: 'primary',
620
+ onClick: handleSave,
621
+ disabled: !title.trim(),
622
+ }, mode === 'create' ? 'Create' : 'Save'),
623
+ ),
624
+ );
625
+ }
626
+
627
+ export default TaskEditor;
628
+ module.exports = TaskEditor;