bosun 0.42.2 → 0.42.4

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 (49) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/bosun-tui.mjs +107 -105
  5. package/cli.mjs +10 -0
  6. package/config/config.mjs +25 -0
  7. package/config/executor-config.mjs +124 -1
  8. package/infra/container-runner.mjs +565 -1
  9. package/infra/monitor.mjs +18 -0
  10. package/infra/tracing.mjs +544 -240
  11. package/infra/tui-bridge.mjs +13 -1
  12. package/kanban/kanban-adapter.mjs +128 -4
  13. package/lib/repo-map.mjs +114 -3
  14. package/package.json +11 -4
  15. package/server/ui-server.mjs +3 -0
  16. package/task/task-archiver.mjs +18 -6
  17. package/task/task-attachments.mjs +14 -10
  18. package/task/task-cli.mjs +24 -4
  19. package/task/task-executor.mjs +19 -0
  20. package/task/task-store.mjs +194 -37
  21. package/telegram/telegram-bot.mjs +4 -1
  22. package/tui/app.mjs +131 -171
  23. package/tui/components/status-header.mjs +178 -75
  24. package/tui/lib/header-config.mjs +68 -0
  25. package/tui/lib/ws-bridge.mjs +61 -9
  26. package/tui/screens/agents.mjs +127 -0
  27. package/tui/screens/tasks.mjs +1 -48
  28. package/ui/app.js +8 -5
  29. package/ui/components/kanban-board.js +65 -3
  30. package/ui/components/session-list.js +18 -32
  31. package/ui/demo-defaults.js +52 -2
  32. package/ui/modules/session-api.js +100 -0
  33. package/ui/modules/state.js +71 -15
  34. package/ui/tabs/workflows.js +25 -1
  35. package/ui/tui/App.js +298 -0
  36. package/ui/tui/TasksScreen.js +564 -0
  37. package/ui/tui/constants.js +55 -0
  38. package/ui/tui/tasks-screen-helpers.js +301 -0
  39. package/ui/tui/useTasks.js +61 -0
  40. package/ui/tui/useWebSocket.js +166 -0
  41. package/ui/tui/useWorkflows.js +30 -0
  42. package/workflow/workflow-engine.mjs +412 -7
  43. package/workflow/workflow-nodes.mjs +616 -75
  44. package/workflow-templates/agents.mjs +3 -0
  45. package/workflow-templates/planning.mjs +7 -0
  46. package/workflow-templates/sub-workflows.mjs +5 -0
  47. package/workflow-templates/task-execution.mjs +3 -0
  48. package/workspace/command-diagnostics.mjs +1 -1
  49. package/workspace/context-cache.mjs +182 -9
@@ -0,0 +1,564 @@
1
+ import React, { useEffect, useMemo, useState } from "react";
2
+ import htm from "htm";
3
+ import { Box, Text, useInput, useStdout } from "ink";
4
+
5
+ import {
6
+ buildBoardColumns,
7
+ buildFormStateFromTask,
8
+ buildListRows,
9
+ createTaskFromForm,
10
+ deleteTaskById,
11
+ EMPTY_TASK_FORM,
12
+ formatColumnSummary,
13
+ LINE_LIST_FIELD_KEYS,
14
+ listTasksFromApi,
15
+ PRIORITY_COLOR_MAP,
16
+ PRIORITY_OPTIONS,
17
+ resolveTaskView,
18
+ STATUS_OPTIONS,
19
+ truncateText,
20
+ updateTaskFromForm,
21
+ validateTaskForm,
22
+ } from "./tasks-screen-helpers.js";
23
+
24
+ const html = htm.bind(React.createElement);
25
+ const FORM_FIELDS = [
26
+ { key: "title", label: "Title", multiline: false },
27
+ { key: "priority", label: "Priority", select: PRIORITY_OPTIONS },
28
+ { key: "status", label: "Status", select: STATUS_OPTIONS },
29
+ { key: "tagsText", label: "Tags", multiline: false },
30
+ { key: "description", label: "Description", multiline: true },
31
+ { key: "stepsText", label: "Steps", multiline: true },
32
+ { key: "acceptanceCriteriaText", label: "AC", multiline: true },
33
+ { key: "verificationText", label: "Verify", multiline: true },
34
+ ];
35
+ const STATUS_MOVE_ORDER = ["todo", "in_progress", "review", "done"];
36
+
37
+ function clamp(value, min, max) {
38
+ if (max < min) return min;
39
+ return Math.max(min, Math.min(max, value));
40
+ }
41
+
42
+ function cycleOption(options, current, direction) {
43
+ const values = Array.isArray(options) ? options : [];
44
+ if (!values.length) return current;
45
+ const index = Math.max(0, values.indexOf(current));
46
+ const nextIndex = (index + direction + values.length) % values.length;
47
+ return values[nextIndex] || values[0] || current;
48
+ }
49
+
50
+ function appendInput(value, input) {
51
+ return `${String(value || "")}${input}`;
52
+ }
53
+
54
+ function removeLastCharacter(value) {
55
+ return String(value || "").slice(0, -1);
56
+ }
57
+
58
+ function getSelectionFromColumns(columns, selection) {
59
+ const column = columns[selection.columnIndex] || columns[0];
60
+ const item = column?.items?.[selection.rowIndex] || null;
61
+ return { column, item };
62
+ }
63
+
64
+ function formatFormValue(field, value, active) {
65
+ const rendered = String(value || "");
66
+ if (field.select) {
67
+ const label = rendered || "(select)";
68
+ return active ? `< ${label} >` : label;
69
+ }
70
+ if (!rendered) return active ? "█" : "(empty)";
71
+ return active ? `${rendered}█` : rendered;
72
+ }
73
+
74
+ function buildFieldHint(field) {
75
+ if (field.select) return "←/→ to cycle";
76
+ if (LINE_LIST_FIELD_KEYS.has(field.key)) return "Enter adds line";
77
+ if (field.multiline) return "Enter adds newline";
78
+ return "Type to edit";
79
+ }
80
+
81
+ function TaskCard({ task, selected, width }) {
82
+ const tagText = (task.tags || []).slice(0, 3).map((tag) => `[${tag}]`).join(" ");
83
+ return html`
84
+ <${Box} flexDirection="column" borderStyle="single" paddingX=${1} marginBottom=${1}>
85
+ <${Box}>
86
+ <${Text} color=${PRIORITY_COLOR_MAP[task.priority] || task.priorityColor}>●<//>
87
+ <${Text} inverse=${selected}> ${task.idShort}<//>
88
+ <${Text} dimColor inverse=${selected}> ${truncateText(task.title || "Untitled", Math.max(8, width - 12))}<//>
89
+ <//>
90
+ ${tagText
91
+ ? html`<${Text} color="cyan" inverse=${selected}>${truncateText(tagText, Math.max(8, width - 4))}<//>`
92
+ : html`<${Text} dimColor inverse=${selected}>No tags<//>`}
93
+ <//>
94
+ `;
95
+ }
96
+
97
+ function FormField({ field, active, value, error }) {
98
+ return html`
99
+ <${Box} flexDirection="column" marginBottom=${1}>
100
+ <${Text} bold=${active}>${field.label}${error ? ` - ${error}` : ""}<//>
101
+ <${Box} borderStyle="single" paddingX=${1}>
102
+ <${Text} color=${error ? "red" : undefined}>${formatFormValue(field, value, active)}<//>
103
+ <//>
104
+ <${Text} dimColor>${buildFieldHint(field)}<//>
105
+ <//>
106
+ `;
107
+ }
108
+
109
+ function TaskForm({ mode, formState, activeFieldIndex, validationErrors, busy }) {
110
+ return html`
111
+ <${Box} marginTop=${1} flexDirection="column" borderStyle="double" paddingX=${1}>
112
+ <${Text} bold>${mode === "create" ? "New Task" : "Edit Task"}<//>
113
+ ${FORM_FIELDS.map((field, index) => html`
114
+ <${FormField}
115
+ key=${field.key}
116
+ field=${field}
117
+ active=${index === activeFieldIndex}
118
+ value=${formState[field.key] || ""}
119
+ error=${validationErrors[field.key]}
120
+ />
121
+ `)}
122
+ <${Text} dimColor>
123
+ [Tab] Next [Shift+Tab] Prev [Left/Right] Select [Ctrl+S] Save [Esc] Cancel
124
+ <//>
125
+ ${busy ? html`<${Text} color="yellow">Saving...<//>` : null}
126
+ <//>
127
+ `;
128
+ }
129
+
130
+ export default function TasksScreen({ tasks = [], onTasksChange, onInputCaptureChange }) {
131
+ const { stdout } = useStdout();
132
+ const terminalWidth = stdout?.columns || process.stdout.columns || 120;
133
+ const [preferredView, setPreferredView] = useState("kanban");
134
+ const [selection, setSelection] = useState({ columnIndex: 0, rowIndex: 0, listIndex: 0 });
135
+ const [filterOpen, setFilterOpen] = useState(false);
136
+ const [filterText, setFilterText] = useState("");
137
+ const [formMode, setFormMode] = useState(null);
138
+ const [formState, setFormState] = useState(EMPTY_TASK_FORM);
139
+ const [activeFieldIndex, setActiveFieldIndex] = useState(0);
140
+ const [validationErrors, setValidationErrors] = useState({});
141
+ const [deletePrompt, setDeletePrompt] = useState(false);
142
+ const [statusLine, setStatusLine] = useState("");
143
+ const [busy, setBusy] = useState(false);
144
+ const [editingTaskId, setEditingTaskId] = useState(null);
145
+
146
+ const viewMode = resolveTaskView(terminalWidth, preferredView);
147
+ const columnWidth = Math.max(24, Math.floor((terminalWidth - 10) / 4));
148
+ const columns = useMemo(
149
+ () => buildBoardColumns(tasks, { filterText, columnWidth }),
150
+ [tasks, filterText, columnWidth],
151
+ );
152
+ const listRows = useMemo(
153
+ () => buildListRows(tasks, { filterText, rowWidth: Math.max(40, terminalWidth - 8) }),
154
+ [tasks, filterText, terminalWidth],
155
+ );
156
+ const columnSummary = useMemo(() => formatColumnSummary(columns), [columns]);
157
+
158
+ useEffect(() => {
159
+ const locked = filterOpen || Boolean(formMode) || deletePrompt || busy;
160
+ if (typeof onInputCaptureChange === "function") {
161
+ onInputCaptureChange(locked);
162
+ }
163
+ return () => {
164
+ if (typeof onInputCaptureChange === "function") {
165
+ onInputCaptureChange(false);
166
+ }
167
+ };
168
+ }, [busy, deletePrompt, filterOpen, formMode, onInputCaptureChange]);
169
+
170
+ useEffect(() => {
171
+ if (viewMode === "kanban") {
172
+ setSelection((current) => {
173
+ const columnIndex = clamp(current.columnIndex, 0, columns.length - 1);
174
+ const rowIndex = clamp(current.rowIndex, 0, Math.max(0, (columns[columnIndex]?.items?.length || 1) - 1));
175
+ return { ...current, columnIndex, rowIndex };
176
+ });
177
+ return;
178
+ }
179
+
180
+ setSelection((current) => ({
181
+ ...current,
182
+ listIndex: clamp(current.listIndex, 0, Math.max(0, listRows.length - 1)),
183
+ }));
184
+ }, [columns, listRows, viewMode]);
185
+
186
+ const selectedTask = useMemo(() => {
187
+ if (viewMode === "kanban") {
188
+ return getSelectionFromColumns(columns, selection).item;
189
+ }
190
+ return listRows[selection.listIndex] || null;
191
+ }, [columns, listRows, selection, viewMode]);
192
+
193
+ async function refreshTasks(message = "") {
194
+ const nextTasks = await listTasksFromApi();
195
+ if (typeof onTasksChange === "function") {
196
+ onTasksChange(nextTasks);
197
+ }
198
+ if (message) setStatusLine(message);
199
+ }
200
+
201
+ function openCreateForm() {
202
+ setFormMode("create");
203
+ setEditingTaskId(null);
204
+ setFormState({ ...EMPTY_TASK_FORM, status: selectedTask?.statusDisplay || "todo" });
205
+ setActiveFieldIndex(0);
206
+ setValidationErrors({});
207
+ setDeletePrompt(false);
208
+ setStatusLine("");
209
+ }
210
+
211
+ function openEditForm(task) {
212
+ if (!task) return;
213
+ setFormMode("edit");
214
+ setEditingTaskId(task.id);
215
+ setFormState(buildFormStateFromTask(task));
216
+ setActiveFieldIndex(0);
217
+ setValidationErrors({});
218
+ setDeletePrompt(false);
219
+ setStatusLine("");
220
+ }
221
+
222
+ function closeForm() {
223
+ setFormMode(null);
224
+ setEditingTaskId(null);
225
+ setValidationErrors({});
226
+ setBusy(false);
227
+ }
228
+
229
+ async function submitForm() {
230
+ const errors = validateTaskForm(formState);
231
+ setValidationErrors(errors);
232
+ if (Object.keys(errors).length) return;
233
+
234
+ setBusy(true);
235
+ try {
236
+ if (formMode === "create") {
237
+ const created = await createTaskFromForm(formState);
238
+ await refreshTasks(`Created ${created.id || "task"}`);
239
+ } else if (editingTaskId) {
240
+ const updated = await updateTaskFromForm(editingTaskId, formState);
241
+ await refreshTasks(`Updated ${updated.id || editingTaskId}`);
242
+ }
243
+ closeForm();
244
+ } catch (error) {
245
+ setValidationErrors(error?.validationErrors || {});
246
+ setStatusLine(String(error?.message || error || "Task save failed"));
247
+ setBusy(false);
248
+ }
249
+ }
250
+
251
+ async function confirmDeleteTask() {
252
+ if (!selectedTask) return;
253
+ setBusy(true);
254
+ try {
255
+ await deleteTaskById(selectedTask.id);
256
+ setDeletePrompt(false);
257
+ await refreshTasks(`Deleted ${selectedTask.idShort}`);
258
+ } catch (error) {
259
+ setStatusLine(String(error?.message || error || "Delete failed"));
260
+ } finally {
261
+ setBusy(false);
262
+ }
263
+ }
264
+
265
+ async function moveSelectedTask(direction) {
266
+ if (!selectedTask || busy) return;
267
+ const currentIndex = STATUS_MOVE_ORDER.indexOf(selectedTask.statusDisplay || "todo");
268
+ const nextIndex = clamp(currentIndex + direction, 0, STATUS_MOVE_ORDER.length - 1);
269
+ if (nextIndex === currentIndex) return;
270
+
271
+ setBusy(true);
272
+ try {
273
+ const nextStatus = STATUS_MOVE_ORDER[nextIndex];
274
+ await updateTaskFromForm(selectedTask.id, {
275
+ ...buildFormStateFromTask(selectedTask),
276
+ status: nextStatus,
277
+ });
278
+ await refreshTasks(`Moved ${selectedTask.idShort} to ${nextStatus.replace("_", " ")}`);
279
+ } catch (error) {
280
+ setStatusLine(String(error?.message || error || "Status change failed"));
281
+ } finally {
282
+ setBusy(false);
283
+ }
284
+ }
285
+
286
+ useInput((input, key) => {
287
+ if (busy) return;
288
+
289
+ if (deletePrompt) {
290
+ if (input === "y" || input === "Y") {
291
+ void confirmDeleteTask();
292
+ return;
293
+ }
294
+ if (key.escape || input === "n" || input === "N") {
295
+ setDeletePrompt(false);
296
+ }
297
+ return;
298
+ }
299
+
300
+ if (formMode) {
301
+ const field = FORM_FIELDS[activeFieldIndex];
302
+ if (!field) return;
303
+
304
+ if (key.escape) {
305
+ closeForm();
306
+ return;
307
+ }
308
+ if (key.ctrl && input === "s") {
309
+ void submitForm();
310
+ return;
311
+ }
312
+ if (key.tab && key.shift) {
313
+ setActiveFieldIndex((current) => clamp(current - 1, 0, FORM_FIELDS.length - 1));
314
+ return;
315
+ }
316
+ if (key.tab || key.downArrow) {
317
+ setActiveFieldIndex((current) => clamp(current + 1, 0, FORM_FIELDS.length - 1));
318
+ return;
319
+ }
320
+ if (key.upArrow) {
321
+ setActiveFieldIndex((current) => clamp(current - 1, 0, FORM_FIELDS.length - 1));
322
+ return;
323
+ }
324
+ if (field.select) {
325
+ if (key.leftArrow) {
326
+ setFormState((current) => ({
327
+ ...current,
328
+ [field.key]: cycleOption(field.select, current[field.key], -1),
329
+ }));
330
+ return;
331
+ }
332
+ if (key.rightArrow || key.return) {
333
+ setFormState((current) => ({
334
+ ...current,
335
+ [field.key]: cycleOption(field.select, current[field.key], 1),
336
+ }));
337
+ return;
338
+ }
339
+ }
340
+ if (key.backspace || key.delete) {
341
+ setFormState((current) => ({
342
+ ...current,
343
+ [field.key]: removeLastCharacter(current[field.key]),
344
+ }));
345
+ return;
346
+ }
347
+ if (key.return) {
348
+ if (field.multiline) {
349
+ setFormState((current) => ({
350
+ ...current,
351
+ [field.key]: appendInput(current[field.key], "\n"),
352
+ }));
353
+ } else {
354
+ setActiveFieldIndex((current) => clamp(current + 1, 0, FORM_FIELDS.length - 1));
355
+ }
356
+ return;
357
+ }
358
+ if (input && !key.ctrl && !key.meta) {
359
+ setFormState((current) => ({
360
+ ...current,
361
+ [field.key]: appendInput(current[field.key], input),
362
+ }));
363
+ if (field.key === "title" && validationErrors.title) {
364
+ setValidationErrors({});
365
+ }
366
+ }
367
+ return;
368
+ }
369
+
370
+ if (filterOpen) {
371
+ if (key.escape) {
372
+ setFilterText("");
373
+ setFilterOpen(false);
374
+ return;
375
+ }
376
+ if (key.backspace || key.delete) {
377
+ setFilterText((current) => current.slice(0, -1));
378
+ return;
379
+ }
380
+ if (key.return) {
381
+ setFilterOpen(false);
382
+ return;
383
+ }
384
+ if (input && !key.ctrl && !key.meta) {
385
+ setFilterText((current) => `${current}${input}`);
386
+ }
387
+ return;
388
+ }
389
+
390
+ if (input === "n" || input === "N") {
391
+ openCreateForm();
392
+ return;
393
+ }
394
+ if (input === "e" || input === "E" || key.return) {
395
+ openEditForm(selectedTask);
396
+ return;
397
+ }
398
+ if (input === "d" || input === "D") {
399
+ if (selectedTask) setDeletePrompt(true);
400
+ return;
401
+ }
402
+ if (input === "f" || input === "F") {
403
+ setFilterOpen(true);
404
+ return;
405
+ }
406
+ if (input === "v" || input === "V") {
407
+ setPreferredView((current) => (current === "kanban" ? "list" : "kanban"));
408
+ return;
409
+ }
410
+ if (input === "[") {
411
+ void moveSelectedTask(-1);
412
+ return;
413
+ }
414
+ if (input === "]") {
415
+ void moveSelectedTask(1);
416
+ return;
417
+ }
418
+
419
+ if (viewMode === "kanban") {
420
+ if (key.leftArrow) {
421
+ setSelection((current) => {
422
+ const columnIndex = clamp(current.columnIndex - 1, 0, columns.length - 1);
423
+ const rowIndex = clamp(current.rowIndex, 0, Math.max(0, (columns[columnIndex]?.items?.length || 1) - 1));
424
+ return { ...current, columnIndex, rowIndex };
425
+ });
426
+ return;
427
+ }
428
+ if (key.rightArrow) {
429
+ setSelection((current) => {
430
+ const columnIndex = clamp(current.columnIndex + 1, 0, columns.length - 1);
431
+ const rowIndex = clamp(current.rowIndex, 0, Math.max(0, (columns[columnIndex]?.items?.length || 1) - 1));
432
+ return { ...current, columnIndex, rowIndex };
433
+ });
434
+ return;
435
+ }
436
+ if (key.upArrow) {
437
+ setSelection((current) => ({ ...current, rowIndex: Math.max(0, current.rowIndex - 1) }));
438
+ return;
439
+ }
440
+ if (key.downArrow) {
441
+ setSelection((current) => {
442
+ const maxRow = Math.max(0, (columns[current.columnIndex]?.items?.length || 1) - 1);
443
+ return { ...current, rowIndex: clamp(current.rowIndex + 1, 0, maxRow) };
444
+ });
445
+ }
446
+ return;
447
+ }
448
+
449
+ if (key.upArrow) {
450
+ setSelection((current) => ({ ...current, listIndex: Math.max(0, current.listIndex - 1) }));
451
+ return;
452
+ }
453
+ if (key.downArrow) {
454
+ setSelection((current) => ({
455
+ ...current,
456
+ listIndex: clamp(current.listIndex + 1, 0, Math.max(0, listRows.length - 1)),
457
+ }));
458
+ }
459
+ });
460
+
461
+ return html`
462
+ <${Box} flexDirection="column" paddingY=${1}>
463
+ <${Box} justifyContent="space-between" paddingX=${1}>
464
+ <${Text} bold>Tasks<//>
465
+ <${Text} dimColor>[V]iew: kanban/list -> ${viewMode}${terminalWidth < 140 ? " (auto)" : ""}<//>
466
+ <//>
467
+
468
+ <${Box} paddingX=${1} marginTop=${1}>
469
+ <${Text} dimColor>${columnSummary}<//>
470
+ <//>
471
+
472
+ <${Box} paddingX=${1} marginTop=${1}>
473
+ <${Text} color=${filterOpen ? "cyan" : undefined}>
474
+ [F]ilter: ${filterText || "(title, tag, id)"}${filterOpen ? "█" : ""}
475
+ <//>
476
+ <//>
477
+
478
+ ${formMode
479
+ ? html`
480
+ <${TaskForm}
481
+ mode=${formMode}
482
+ formState=${formState}
483
+ activeFieldIndex=${activeFieldIndex}
484
+ validationErrors=${validationErrors}
485
+ busy=${busy}
486
+ />
487
+ `
488
+ : null}
489
+
490
+ ${deletePrompt && selectedTask
491
+ ? html`
492
+ <${Box} marginTop=${1} paddingX=${1}>
493
+ <${Text} color="red">Delete ${selectedTask.idShort}? [y/N]<//>
494
+ <//>
495
+ `
496
+ : null}
497
+
498
+ ${viewMode === "kanban"
499
+ ? html`
500
+ <${Box} marginTop=${1}>
501
+ ${columns.map((column, columnIndex) => html`
502
+ <${Box}
503
+ key=${column.key}
504
+ flexDirection="column"
505
+ flexGrow=${1}
506
+ width=${columnWidth}
507
+ marginRight=${columnIndex < columns.length - 1 ? 1 : 0}
508
+ borderStyle="round"
509
+ paddingX=${1}
510
+ >
511
+ <${Text} bold inverse=${selection.columnIndex === columnIndex}>${column.label} (${column.count})<//>
512
+ ${column.items.length
513
+ ? column.items.map((task, rowIndex) => html`
514
+ <${TaskCard}
515
+ key=${task.id}
516
+ task=${task}
517
+ selected=${selection.columnIndex === columnIndex && selection.rowIndex === rowIndex}
518
+ width=${columnWidth - 4}
519
+ />
520
+ `)
521
+ : html`<${Text} dimColor>No tasks<//>`}
522
+ <//>
523
+ `)}
524
+ <//>
525
+ `
526
+ : html`
527
+ <${Box} marginTop=${1} flexDirection="column" borderStyle="round" paddingX=${1}>
528
+ <${Text} bold>Task List<//>
529
+ ${listRows.length
530
+ ? listRows.map((task, index) => html`
531
+ <${Box} key=${task.id} marginTop=${1}>
532
+ <${Text} inverse=${selection.listIndex === index}>${task.statusDisplay.padEnd(11)}<//>
533
+ <${Text} color=${PRIORITY_COLOR_MAP[task.priority] || task.priorityColor}> ● <//>
534
+ <${Text} inverse=${selection.listIndex === index}>${task.idShort} ${task.truncatedTitle}<//>
535
+ <${Text} color="cyan" dimColor=${!(task.tags || []).length}> ${(task.tags || []).map((tag) => `[${tag}]`).join(" ") || "No tags"}<//>
536
+ <//>
537
+ `)
538
+ : html`<${Text} dimColor>No matching tasks<//>`}
539
+ <//>
540
+ `}
541
+
542
+ <${Box} marginTop=${1} paddingX=${1} borderStyle="single">
543
+ <${Text} dimColor>
544
+ [Arrows] Navigate [Enter] Edit [N] New [E] Edit [D] Delete [F] Filter [V] View [[]/[]] Move
545
+ <//>
546
+ <//>
547
+ ${selectedTask
548
+ ? html`
549
+ <${Box} marginTop=${1} paddingX=${1} flexDirection="column" borderStyle="single">
550
+ <${Text} bold>${selectedTask.idShort} - ${selectedTask.title || "Untitled"}<//>
551
+ <${Text} dimColor>${truncateText(selectedTask.description || "No description", Math.max(32, terminalWidth - 8))}<//>
552
+ <//>
553
+ `
554
+ : null}
555
+ ${statusLine
556
+ ? html`
557
+ <${Box} marginTop=${1} paddingX=${1}>
558
+ <${Text} color="yellow">${statusLine}<//>
559
+ <//>
560
+ `
561
+ : null}
562
+ <//>
563
+ `;
564
+ }
@@ -0,0 +1,55 @@
1
+ import figures from "figures";
2
+
3
+ export const ANSI_COLORS = Object.freeze({
4
+ connected: "green",
5
+ reconnecting: "yellow",
6
+ disconnected: "red",
7
+ muted: "gray",
8
+ accent: "cyan",
9
+ warning: "yellow",
10
+ danger: "red",
11
+ });
12
+
13
+ export const MIN_TERMINAL_SIZE = Object.freeze({ columns: 120, rows: 30 });
14
+
15
+ export const TAB_ORDER = Object.freeze([
16
+ { id: "agents", label: "Agents", shortcut: "a" },
17
+ { id: "tasks", label: "Tasks", shortcut: "t" },
18
+ { id: "logs", label: "Logs", shortcut: "l" },
19
+ { id: "workflows", label: "Workflows", shortcut: "w" },
20
+ { id: "telemetry", label: "Telemetry", shortcut: "x" },
21
+ { id: "settings", label: "Settings", shortcut: "s" },
22
+ { id: "help", label: "Help", shortcut: "?" },
23
+ ]);
24
+
25
+ export const KEY_BINDINGS = Object.freeze({
26
+ a: "agents",
27
+ t: "tasks",
28
+ l: "logs",
29
+ w: "workflows",
30
+ x: "telemetry",
31
+ s: "settings",
32
+ "?": "help",
33
+ tab: "next",
34
+ shiftTab: "previous",
35
+ q: "quit",
36
+ });
37
+
38
+ export const COLUMN_WIDTHS = Object.freeze({
39
+ id: 10,
40
+ status: 12,
41
+ priority: 10,
42
+ title: 40,
43
+ turns: 7,
44
+ updated: 18,
45
+ workflow: 28,
46
+ });
47
+
48
+ export const GLYPHS = Object.freeze({
49
+ connected: figures.circleFilled || "●",
50
+ reconnectingOn: figures.circleFilled || "●",
51
+ reconnectingOff: figures.circleDotted || "◌",
52
+ disconnected: figures.cross || "×",
53
+ warning: figures.warning || "⚠",
54
+ pointer: figures.pointer || ">",
55
+ });