parallel-codex-tui 0.1.4 → 0.1.5

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 (40) hide show
  1. package/.parallel-codex/config.example.toml +46 -0
  2. package/README.md +76 -17
  3. package/dist/cli-startup-preflight.js +18 -0
  4. package/dist/cli-startup-recovery.js +13 -1
  5. package/dist/cli.js +18 -6
  6. package/dist/core/collaboration-timeline.js +9 -2
  7. package/dist/core/config.js +161 -103
  8. package/dist/core/router.js +1 -1
  9. package/dist/core/session-index.js +234 -61
  10. package/dist/core/session-manager.js +25 -1
  11. package/dist/core/task-session-details.js +175 -0
  12. package/dist/core/task-state-machine.js +10 -9
  13. package/dist/doctor.js +58 -39
  14. package/dist/domain/schemas.js +15 -1
  15. package/dist/orchestrator/collaboration-channel.js +35 -3
  16. package/dist/orchestrator/final-acceptance.js +86 -0
  17. package/dist/orchestrator/orchestrator.js +405 -69
  18. package/dist/orchestrator/prompts.js +42 -3
  19. package/dist/orchestrator/workspace-sandbox.js +16 -0
  20. package/dist/tui/App.js +401 -52
  21. package/dist/tui/AppShell.js +9 -3
  22. package/dist/tui/FeatureBoardView.js +7 -2
  23. package/dist/tui/InputBar.js +82 -15
  24. package/dist/tui/StatusBar.js +1 -1
  25. package/dist/tui/StatusDetailView.js +164 -0
  26. package/dist/tui/TaskSessionDetailView.js +222 -0
  27. package/dist/tui/TaskSessionsView.js +5 -2
  28. package/dist/tui/WorkerOutputView.js +1 -0
  29. package/dist/tui/WorkerOverviewView.js +23 -4
  30. package/dist/tui/keyboard.js +3 -0
  31. package/dist/tui/status-line.js +42 -0
  32. package/dist/version.js +1 -1
  33. package/dist/workers/capabilities.js +4 -3
  34. package/dist/workers/live-probe.js +4 -3
  35. package/dist/workers/mock-adapter.js +37 -5
  36. package/dist/workers/native-attach.js +32 -17
  37. package/dist/workers/process-adapter.js +2 -0
  38. package/dist/workers/provider.js +26 -0
  39. package/dist/workers/registry.js +12 -22
  40. package/package.json +7 -1
@@ -236,12 +236,18 @@ function viewLabel(view) {
236
236
  return shortViewLabel(view);
237
237
  }
238
238
  function shortcutHint(view) {
239
+ if (view === "status") {
240
+ return "^S back";
241
+ }
239
242
  if (view === "native") {
240
- return "^] logs";
243
+ return "^S status · ^] logs";
241
244
  }
242
- return "^C exit";
245
+ return "^S status · ^C exit";
243
246
  }
244
247
  function shortViewLabel(view) {
248
+ if (view === "status") {
249
+ return "status";
250
+ }
245
251
  if (view === "worker") {
246
252
  return "logs";
247
253
  }
@@ -266,7 +272,7 @@ function shortViewLabel(view) {
266
272
  return "chat";
267
273
  }
268
274
  function shortShortcutHint(view) {
269
- return view === "native" ? "^]" : "^C";
275
+ return view === "native" ? "^]" : view === "status" ? "^S" : "^C";
270
276
  }
271
277
  function compactHeaderProject(cwd, maxLength) {
272
278
  const project = basename(cwd) || cwd;
@@ -99,6 +99,7 @@ function featureBoardSummary(timeline, width) {
99
99
  const approved = timeline.features.filter((feature) => feature.state === "approved").length;
100
100
  const active = timeline.features.filter((feature) => featureBoardStateIsActive(feature.state)).length;
101
101
  const revision = timeline.features.filter((feature) => feature.state === "revision_needed").length;
102
+ const paused = timeline.features.filter((feature) => feature.state === "paused").length;
102
103
  const failed = timeline.features.filter((feature) => feature.state === "failed" || feature.state === "cancelled").length;
103
104
  const blocked = timeline.features.filter((feature) => featureBoardBlockedDependencies(timeline, feature).length > 0).length;
104
105
  const full = [
@@ -106,6 +107,7 @@ function featureBoardSummary(timeline, width) {
106
107
  ...(approved > 0 ? [`${approved} approved`] : []),
107
108
  ...(active > 0 ? [`${active} active`] : []),
108
109
  ...(revision > 0 ? [`${revision} ${revision === 1 ? "revision" : "revisions"}`] : []),
110
+ ...(paused > 0 ? [`${paused} paused`] : []),
109
111
  ...(blocked > 0 ? [`${blocked} blocked`] : []),
110
112
  ...(failed > 0 ? [`${failed} failed`] : [])
111
113
  ].join(" · ");
@@ -139,12 +141,15 @@ function featureBoardEvidenceText(timeline, feature, width) {
139
141
  const dependencyText = dependencies.length > 0
140
142
  ? `deps ${dependencies.map((item) => safeFeatureBoardText(item.title)).join(", ")}`
141
143
  : "independent";
144
+ const assignmentText = feature.actorEngine && feature.criticEngine
145
+ ? `actor ${feature.actorEngine} · critic ${feature.criticEngine}`
146
+ : "";
142
147
  const evidence = feature.latestFinding
143
148
  ? `finding · ${safeFeatureBoardText(feature.latestFinding)}`
144
149
  : feature.latestReply
145
150
  ? `reply · ${safeFeatureBoardText(feature.latestReply)}`
146
151
  : safeFeatureBoardText(feature.description);
147
- return fitFeatureBoardText(` ${[dependencyText, evidence].filter(Boolean).join(" · ")}`, width);
152
+ return fitFeatureBoardText(` ${[assignmentText, dependencyText, evidence].filter(Boolean).join(" · ")}`, width);
148
153
  }
149
154
  function featureBoardDependencies(timeline, feature) {
150
155
  return feature.dependsOn.flatMap((dependency) => {
@@ -168,7 +173,7 @@ function featureBoardStateTone(state) {
168
173
  if (state === "failed" || state === "cancelled") {
169
174
  return "danger";
170
175
  }
171
- if (state === "revision_needed") {
176
+ if (state === "revision_needed" || state === "paused") {
172
177
  return "warning";
173
178
  }
174
179
  if (featureBoardStateIsActive(state)) {
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { Box, Text } from "ink";
3
3
  import { compactEndByDisplayWidth, compactTailByDisplayWidth, displayWidth } from "./display-width.js";
4
4
  import { TUI_THEME } from "./theme.js";
5
- export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCancelConfirm = false, taskSessionAction = null, taskSessionsIncludeArchived = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
5
+ export function InputBar({ mode, ready = true, busy = false, routeFallback = false, collaborationDetail = false, collaborationUnresolved = false, collaborationBack = "workers", featureCanCancel = false, featureCanPause = false, featureCanReassign = false, featureCancelConfirm = false, featurePauseConfirm = false, featureAssignment = false, taskSessionAction = null, taskSessionsIncludeArchived = false, taskSessionDetail = false, taskSessionDetailHasNative = false, taskSessionDetailCanFork = false, canRetry = false, hasWorkers = false, hasActiveTask = false, hasTaskResult = false, taskResultExpanded = false, chatScrollOffset = 0, chatMaxScrollOffset = 0, nativeClosed = false, searchMatchIndex = 0, searchMatchCount = 0, value, cursor, terminalWidth: providedTerminalWidth, onChange, onSubmit }) {
6
6
  const terminalWidth = providedTerminalWidth ?? process.stdout.columns ?? 120;
7
7
  const fillRail = providedTerminalWidth !== undefined || typeof process.stdout.columns === "number";
8
8
  if (mode === "worker-search") {
@@ -14,6 +14,10 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
14
14
  const textWidth = displayWidth(`${prefix}${display.before}|${display.after}${suffix}`);
15
15
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: textWidth, fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: prefix }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.before }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: "|" }), _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.text, children: display.after }), suffix ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: suffix }) : null] }));
16
16
  }
17
+ if (mode === "status") {
18
+ const hints = statusDetailInputHints(terminalWidth);
19
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
20
+ }
17
21
  if (mode === "sessions") {
18
22
  if (taskSessionAction?.type === "rename") {
19
23
  const prefix = terminalWidth < 12 ? "> " : "rename > ";
@@ -26,6 +30,10 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
26
30
  const hints = taskSessionDeleteInputHints(terminalWidth, taskSessionAction.title);
27
31
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.danger, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
28
32
  }
33
+ if (taskSessionDetail) {
34
+ const hints = taskSessionDetailInputHints(terminalWidth, taskSessionDetailHasNative, taskSessionDetailCanFork);
35
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
36
+ }
29
37
  const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
30
38
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
31
39
  }
@@ -38,7 +46,11 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
38
46
  if (mode === "features") {
39
47
  const hints = featureBoardInputHints(terminalWidth, {
40
48
  canCancel: featureCanCancel,
49
+ canPause: featureCanPause,
50
+ canReassign: featureCanReassign,
41
51
  confirmCancel: featureCancelConfirm,
52
+ confirmPause: featurePauseConfirm,
53
+ assignment: featureAssignment,
42
54
  canRetry
43
55
  });
44
56
  return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
@@ -396,6 +408,15 @@ function workerInputHints(width) {
396
408
  }
397
409
  return { label: "logs", detail: " · scroll · ^F find · E err · D diff · Tab · ^B workers · ^O attach · Esc chat" };
398
410
  }
411
+ function statusDetailInputHints(width) {
412
+ return selectInputHints(width, [
413
+ { label: "status", detail: " · ^S/Esc back · ^C exit" },
414
+ { label: "status", detail: " · ^S back · ^C exit" },
415
+ { label: "status", detail: " · ^S back" },
416
+ { label: "status", detail: "" },
417
+ { label: "st", detail: "" }
418
+ ]);
419
+ }
399
420
  function workerOverviewInputHints(width) {
400
421
  if (width < 16) {
401
422
  return { label: "wrk", detail: "" };
@@ -421,6 +442,26 @@ function workerOverviewInputHints(width) {
421
442
  return { label: "workers", detail: " · Up/Dn select · Enter logs · F features · C timeline · ^O attach · Esc back" };
422
443
  }
423
444
  function featureBoardInputHints(width, options) {
445
+ if (options.assignment) {
446
+ return selectInputHints(width, [
447
+ { label: "assign provider", detail: " · A Actor · C Critic · M/Esc done" },
448
+ { label: "assign", detail: " · A Actor · C Critic · Esc done" },
449
+ { label: "assign", detail: " · A actor · C critic" },
450
+ { label: "provider", detail: " · A · C · Esc" },
451
+ { label: "provider", detail: "" },
452
+ { label: "M", detail: "" }
453
+ ]);
454
+ }
455
+ if (options.confirmPause) {
456
+ return selectInputHints(width, [
457
+ { label: "pause feature?", detail: " · P confirm · Esc keep" },
458
+ { label: "pause?", detail: " · P confirm · Esc keep" },
459
+ { label: "pause?", detail: " · Esc keep" },
460
+ { label: "Esc keep", detail: "" },
461
+ { label: "hold?", detail: "" },
462
+ { label: "?", detail: "" }
463
+ ]);
464
+ }
424
465
  if (options.confirmCancel) {
425
466
  return selectInputHints(width, [
426
467
  { label: "cancel feature?", detail: " · X confirm · Esc keep" },
@@ -431,13 +472,21 @@ function featureBoardInputHints(width, options) {
431
472
  { label: "?", detail: "" }
432
473
  ]);
433
474
  }
434
- if (options.canCancel) {
475
+ if (options.canCancel || options.canPause) {
476
+ const controls = [
477
+ ...(options.canPause ? ["P pause"] : []),
478
+ ...(options.canCancel ? ["X cancel"] : [])
479
+ ].join(" · ");
435
480
  return selectInputHints(width, [
436
- { label: "features", detail: " · Up/Dn select · Enter timeline · X cancel · R refresh · Esc workers" },
437
- { label: "features", detail: " · Up/Dn select · X cancel · R refresh · Esc workers" },
438
- { label: "features", detail: " · Up/Dn select · X cancel · Esc workers" },
439
- { label: "features", detail: " · X cancel · Esc workers" },
440
- { label: "features", detail: " · Esc workers" },
481
+ { label: "features", detail: ` · Up/Dn select · Enter timeline · ${controls} · R refresh · Esc workers` },
482
+ { label: "features", detail: ` · Up/Dn select · ${controls} · R refresh · Esc workers` },
483
+ { label: "features", detail: ` · Up/Dn select · ${controls} · Esc workers` },
484
+ { label: "features", detail: ` · ${controls} · Esc workers` },
485
+ { label: "ft", detail: ` · ${controls} · Esc workers` },
486
+ { label: "f", detail: ` · ${controls} · Esc workers` },
487
+ { label: "features", detail: ` · ${controls}` },
488
+ { label: "ft", detail: ` · ${controls}` },
489
+ { label: "f", detail: ` · ${controls}` },
441
490
  { label: "features", detail: "" },
442
491
  { label: "ft", detail: "" },
443
492
  { label: "f", detail: "" }
@@ -445,10 +494,9 @@ function featureBoardInputHints(width, options) {
445
494
  }
446
495
  if (options.canRetry) {
447
496
  return selectInputHints(width, [
448
- { label: "features", detail: " · Up/Dn select · Enter timeline · ^R retry task · R refresh · Esc workers" },
449
- { label: "features", detail: " · Up/Dn select · ^R retry task · R refresh · Esc workers" },
450
- { label: "features", detail: " · Up/Dn select · ^R retry · R refresh · Esc workers" },
451
- { label: "features", detail: " · Up/Dn select · ^R retry · Esc workers" },
497
+ { label: "features", detail: ` · Up/Dn select · Enter timeline · ${options.canReassign ? "M provider · " : ""}^R retry task · R refresh · Esc workers` },
498
+ { label: "features", detail: ` · Up/Dn select · ${options.canReassign ? "M provider · " : ""}^R retry task · R refresh · Esc workers` },
499
+ { label: "features", detail: ` · Up/Dn select · ${options.canReassign ? "M provider · " : ""}^R retry · Esc workers` },
452
500
  { label: "features", detail: " · ^R retry · Esc workers" },
453
501
  { label: "features", detail: " · Esc workers" },
454
502
  { label: "features", detail: "" },
@@ -507,11 +555,13 @@ function collaborationDetailInputHints(width) {
507
555
  function taskSessionsInputHints(width, includeArchived) {
508
556
  const archivedAction = includeArchived ? "H hide archived" : "H archived";
509
557
  return selectInputHints(width, [
510
- { label: "sessions", detail: ` · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · ${archivedAction} · Esc back` },
511
- { label: "sessions", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · Esc back" },
512
- { label: "sessions", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · Esc back" },
513
- { label: "sessions", detail: " · Up/Dn select · Enter restore · R rename · A archive · Esc back" },
558
+ { label: "sessions", detail: ` · Up/Dn select · Enter restore · I inspect · R rename · A archive · D delete · E export · ${archivedAction} · Esc back` },
559
+ { label: "sessions", detail: " · Up/Dn select · Enter restore · I inspect · R rename · A archive · D delete · E export · Esc back" },
560
+ { label: "sessions", detail: " · Up/Dn select · Enter restore · I inspect · R rename · A archive · D delete · Esc back" },
561
+ { label: "sessions", detail: " · Up/Dn select · Enter restore · I inspect · R rename · A archive · Esc back" },
562
+ { label: "sessions", detail: " · Up/Dn select · Enter restore · I inspect · R rename · Esc back" },
514
563
  { label: "sessions", detail: " · Up/Dn select · Enter restore · R rename · Esc back" },
564
+ { label: "sessions", detail: " · Up/Dn select · Enter restore · I inspect · Esc back" },
515
565
  { label: "sessions", detail: " · Up/Dn select · Enter restore · Esc back" },
516
566
  { label: "sessions", detail: " · Up/Dn select · Esc back" },
517
567
  { label: "sessions", detail: " · Esc back" },
@@ -521,6 +571,23 @@ function taskSessionsInputHints(width, includeArchived) {
521
571
  { label: "s", detail: "" }
522
572
  ]);
523
573
  }
574
+ function taskSessionDetailInputHints(width, hasNative, canFork) {
575
+ const nativeActions = hasNative
576
+ ? `${canFork ? "C continue · B branch" : "C continue"} · `
577
+ : "";
578
+ return selectInputHints(width, [
579
+ { label: "session", detail: ` · Up/Dn worker · Enter logs · ${nativeActions}R refresh · Esc tasks` },
580
+ { label: "session", detail: ` · Up/Dn worker · Enter logs · ${nativeActions}Esc tasks` },
581
+ { label: "session", detail: ` · Up/Dn · Enter logs · ${nativeActions}Esc tasks` },
582
+ { label: "session", detail: ` · Enter logs · ${nativeActions}Esc tasks` },
583
+ { label: "session", detail: ` · ${hasNative ? "C continue · " : ""}Esc tasks` },
584
+ { label: "session", detail: " · Esc tasks" },
585
+ { label: "Esc tasks", detail: "" },
586
+ { label: "session", detail: "" },
587
+ { label: "ses", detail: "" },
588
+ { label: "s", detail: "" }
589
+ ]);
590
+ }
524
591
  function taskSessionDeleteInputHints(width, title) {
525
592
  const safeTitle = title.replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim();
526
593
  return selectInputHints(width, [
@@ -772,7 +772,7 @@ function runtimeStatusTone(status) {
772
772
  if (status === "fail" || status === "failed" || status === "error") {
773
773
  return "fail";
774
774
  }
775
- if (status === "wait" || status === "waiting" || status === "queued" || status === "stopping") {
775
+ if (status === "wait" || status === "waiting" || status === "queued" || status === "stopping" || status === "paused") {
776
776
  return "wait";
777
777
  }
778
778
  if (status === "stop" || status === "cancelled" || status === "canceled") {
@@ -0,0 +1,164 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { basename } from "node:path";
3
+ import { Box, Text } from "ink";
4
+ import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
5
+ import { TUI_THEME } from "./theme.js";
6
+ export function StatusDetailView({ height = 20, terminalWidth = process.stdout.columns || 120, ...input }) {
7
+ const viewportHeight = Math.max(1, Math.trunc(height));
8
+ const width = Math.max(1, terminalWidth - 2);
9
+ const lines = statusDetailDisplayLines(input, width, viewportHeight);
10
+ const blankRows = Math.max(0, viewportHeight - lines.length);
11
+ return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(StatusDetailRow, { line: line, width: width }, `${line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `status-detail-fill-${index}`)))] }));
12
+ }
13
+ export function statusDetailDisplayLines(input, width, height) {
14
+ const safeWidth = Math.max(1, Math.trunc(width));
15
+ const maxLines = Math.max(1, Math.trunc(height));
16
+ const selected = input.workers[clampWorkerIndex(input.selectedWorkerIndex, input.workers.length)];
17
+ const lines = [
18
+ { text: "Status", tone: "heading" },
19
+ {
20
+ text: fitStatusDetailText(taskDetail(input), safeWidth),
21
+ tone: input.busy ? "active" : input.canRetry ? "warning" : "text"
22
+ },
23
+ {
24
+ text: fitStatusDetailText(`workspace · ${basename(input.cwd) || input.cwd} · ${input.cwd}`, safeWidth),
25
+ tone: "muted"
26
+ }
27
+ ];
28
+ if (input.routeStatus.trim()) {
29
+ lines.push({
30
+ text: fitStatusDetailText(input.routeStatus.trim(), safeWidth),
31
+ tone: /fallback|failed|error|timeout/i.test(input.routeStatus) ? "danger" : "text"
32
+ });
33
+ }
34
+ lines.push({
35
+ text: fitStatusDetailText(workerSummary(input.workers, input.taskStatus), safeWidth),
36
+ tone: input.workers.some((worker) => worker.runtimeStatus?.state === "failed") ? "danger" : "text"
37
+ });
38
+ if (selected) {
39
+ lines.push(...selectedWorkerLines(selected, safeWidth));
40
+ }
41
+ const reason = sanitizeStatusDetailText(input.routeReason ?? "");
42
+ if (reason) {
43
+ const prefix = "reason · ";
44
+ const wrapped = wrapByDisplayWidth(reason, Math.max(1, safeWidth - displayWidth(prefix))).slice(0, 2);
45
+ wrapped.forEach((part, index) => lines.push({
46
+ text: fitStatusDetailText(`${index === 0 ? prefix : " "}${part}`, safeWidth),
47
+ tone: "muted"
48
+ }));
49
+ }
50
+ return lines.slice(0, maxLines);
51
+ }
52
+ function taskDetail(input) {
53
+ if (!input.taskId) {
54
+ return "task · none";
55
+ }
56
+ const state = input.busy ? "running" : input.canRetry ? "retryable" : "ready";
57
+ return ["task", compactTaskId(input.taskId), input.mode, state].filter(Boolean).join(" · ");
58
+ }
59
+ function workerSummary(workers, taskStatus) {
60
+ if (workers.length === 0) {
61
+ return "workers · none";
62
+ }
63
+ const summary = taskStatus
64
+ .split("|")
65
+ .map((part) => part.trim())
66
+ .filter((part) => /^workers\s+\d+$|^(?:fail|stop|run|wait|done|idle)\s+\d+/i.test(part))
67
+ .join(" · ");
68
+ return summary || `workers · ${workers.length}`;
69
+ }
70
+ function selectedWorkerLines(worker, width) {
71
+ const status = worker.runtimeStatus;
72
+ const state = status?.state ?? "waiting";
73
+ const identity = [
74
+ "selected",
75
+ `${worker.role}/${worker.engine}`,
76
+ state,
77
+ status?.feature_title ?? worker.featureId ?? ""
78
+ ].filter(Boolean).join(" · ");
79
+ const model = [status?.model_provider, status?.model_name].filter(Boolean).join("/");
80
+ const phase = status
81
+ ? ["phase", humanizeStatusDetail(status.phase), `updated ${formatStatusDetailTime(status.last_event_at)}`].join(" · ")
82
+ : "phase · status pending";
83
+ const lines = [
84
+ { text: fitStatusDetailText(identity, width), tone: workerStateTone(state) },
85
+ ...(model ? [{ text: fitStatusDetailText(`model · ${model}`, width), tone: "muted" }] : []),
86
+ { text: fitStatusDetailText(phase, width), tone: "muted" }
87
+ ];
88
+ if (status?.native_session_id) {
89
+ lines.push({
90
+ text: fitStatusDetailText(`session · ${status.native_session_id}`, width),
91
+ tone: "muted"
92
+ });
93
+ }
94
+ const summary = sanitizeStatusDetailText(status?.summary ?? "");
95
+ if (summary) {
96
+ lines.push({ text: fitStatusDetailText(`summary · ${summary}`, width), tone: "text" });
97
+ }
98
+ return lines;
99
+ }
100
+ function StatusDetailRow({ line, width }) {
101
+ const text = fitStatusDetailText(line.text, width);
102
+ const trailingWidth = Math.max(0, width - displayWidth(text));
103
+ const theme = statusDetailLineTheme(line.tone);
104
+ return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: text }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(trailingWidth) }) : null] }));
105
+ }
106
+ export function statusDetailLineTheme(tone) {
107
+ return {
108
+ backgroundColor: TUI_THEME.surface,
109
+ color: tone === "heading" || tone === "active"
110
+ ? TUI_THEME.accent
111
+ : tone === "success"
112
+ ? TUI_THEME.success
113
+ : tone === "warning"
114
+ ? TUI_THEME.warning
115
+ : tone === "danger"
116
+ ? TUI_THEME.danger
117
+ : tone === "muted"
118
+ ? TUI_THEME.muted
119
+ : TUI_THEME.text,
120
+ ...(tone === "heading" || tone === "danger" ? { bold: true } : {})
121
+ };
122
+ }
123
+ function workerStateTone(state) {
124
+ if (state === "done") {
125
+ return "success";
126
+ }
127
+ if (state === "failed") {
128
+ return "danger";
129
+ }
130
+ if (state === "running" || state === "starting") {
131
+ return "active";
132
+ }
133
+ if (state === "waiting" || state === "cancelled") {
134
+ return "warning";
135
+ }
136
+ return "muted";
137
+ }
138
+ function fitStatusDetailText(text, width) {
139
+ return compactEndByDisplayWidth(sanitizeStatusDetailText(text), Math.max(1, width));
140
+ }
141
+ function sanitizeStatusDetailText(text) {
142
+ return text
143
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
144
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
145
+ .replace(/\s+/g, " ")
146
+ .trim();
147
+ }
148
+ function humanizeStatusDetail(value) {
149
+ return sanitizeStatusDetailText(value).replace(/[-_]+/g, " ");
150
+ }
151
+ function formatStatusDetailTime(value) {
152
+ const parsed = new Date(value);
153
+ if (!Number.isFinite(parsed.getTime())) {
154
+ return "unknown";
155
+ }
156
+ return parsed.toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
157
+ }
158
+ function compactTaskId(taskId) {
159
+ const withoutPrefix = taskId.startsWith("task-") ? taskId.slice("task-".length) : taskId;
160
+ return withoutPrefix.replace(/^\d{8}-/, "");
161
+ }
162
+ function clampWorkerIndex(index, count) {
163
+ return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
164
+ }
@@ -0,0 +1,222 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
4
+ import { TUI_THEME } from "./theme.js";
5
+ export function TaskSessionDetailView({ details, selectedWorkerIndex, loading = false, error = null, notice = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
6
+ const viewportHeight = Math.max(1, Math.trunc(height));
7
+ const width = taskSessionDetailContentWidth(terminalWidth);
8
+ const lines = taskSessionDetailDisplayLines(details, selectedWorkerIndex, viewportHeight, terminalWidth, {
9
+ loading,
10
+ error,
11
+ notice
12
+ });
13
+ const blankRows = Math.max(0, viewportHeight - lines.length);
14
+ return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(TaskSessionDetailRow, { line: line, width: width }, `${line.kind}-${line.workerIndex ?? index}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `task-session-detail-fill-${index}`)))] }));
15
+ }
16
+ export function taskSessionDetailDisplayLines(details, selectedWorkerIndex, height, terminalWidth, state = {}) {
17
+ const viewportHeight = Math.max(1, Math.trunc(height));
18
+ const width = taskSessionDetailContentWidth(terminalWidth);
19
+ const header = [
20
+ { text: fitDetailCandidates(["Session hierarchy", "Session detail", "Session", "S"], width), tone: "heading", kind: "heading" }
21
+ ];
22
+ if (details && viewportHeight >= 3) {
23
+ header.push({
24
+ text: fitDetailCandidates([
25
+ `project · ${safeDetailText(details.projectName)} · ${safeDetailText(details.projectPath)}`,
26
+ `project · ${safeDetailText(details.projectName)}`,
27
+ safeDetailText(details.projectName)
28
+ ], width),
29
+ tone: "muted",
30
+ kind: "project"
31
+ });
32
+ }
33
+ if (details && viewportHeight >= 4) {
34
+ header.push({
35
+ text: fitDetailCandidates([
36
+ `task · ${safeDetailText(details.task.title)} · ${humanizeDetailState(details.task.status)} · ${details.turns.length} turns · ${details.workers.length} workers`,
37
+ `task · ${safeDetailText(details.task.title)} · ${humanizeDetailState(details.task.status)}`,
38
+ safeDetailText(details.task.title)
39
+ ], width),
40
+ tone: detailTaskTone(details.task.status),
41
+ kind: "task"
42
+ });
43
+ }
44
+ if (state.notice && header.length < viewportHeight) {
45
+ header.push({ text: fitDetailText(safeDetailText(state.notice), width), tone: "active", kind: "heading" });
46
+ }
47
+ const bodySlots = Math.max(0, viewportHeight - header.length);
48
+ if (bodySlots === 0) {
49
+ return header;
50
+ }
51
+ if (state.loading) {
52
+ return [...header, { text: fitDetailText("loading session hierarchy", width), tone: "muted", kind: "empty" }];
53
+ }
54
+ if (state.error) {
55
+ return [...header, {
56
+ text: fitDetailText(`error · ${safeDetailText(state.error)}`, width),
57
+ tone: "danger",
58
+ kind: "error"
59
+ }];
60
+ }
61
+ if (!details) {
62
+ return [...header, { text: fitDetailText("No task selected", width), tone: "muted", kind: "empty" }];
63
+ }
64
+ if (details.turns.length === 0) {
65
+ return [...header, { text: fitDetailText("No persisted turns", width), tone: "muted", kind: "empty" }];
66
+ }
67
+ const selected = clampDetailWorkerIndex(selectedWorkerIndex, details.workers.length);
68
+ const body = taskSessionDetailBodyLines(details, selected, width);
69
+ const selectedLine = Math.max(0, body.findIndex((line) => line.workerIndex === selected && line.kind === "worker"));
70
+ let start = Math.min(Math.max(0, body.length - bodySlots), Math.max(0, selectedLine - Math.floor(bodySlots / 2)));
71
+ if (start > 0) {
72
+ for (let index = selectedLine; index >= start; index -= 1) {
73
+ if (body[index]?.kind === "turn" && selectedLine - index < bodySlots) {
74
+ start = index;
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ return [...header, ...body.slice(start, start + bodySlots)];
80
+ }
81
+ export function moveTaskSessionDetailSelection(current, delta, workerCount, wrap = false) {
82
+ if (workerCount <= 0) {
83
+ return 0;
84
+ }
85
+ const normalized = clampDetailWorkerIndex(current, workerCount);
86
+ const next = normalized + Math.trunc(delta);
87
+ if (wrap) {
88
+ return ((next % workerCount) + workerCount) % workerCount;
89
+ }
90
+ return Math.min(workerCount - 1, Math.max(0, next));
91
+ }
92
+ function taskSessionDetailBodyLines(details, selectedWorkerIndex, width) {
93
+ const workerIndexById = new Map(details.workers.map((worker, index) => [worker.id, index]));
94
+ return details.turns.flatMap((turn) => {
95
+ const turnNumber = Number(turn.turnId);
96
+ const turnLabel = Number.isInteger(turnNumber) ? `Turn ${turnNumber}` : `Turn ${turn.turnId}`;
97
+ const lines = [{
98
+ text: fitDetailCandidates([
99
+ `${turnLabel} · ${formatDetailTime(turn.createdAt)} · ${safeDetailText(turn.request) || "request unavailable"}`,
100
+ `${turnLabel} · ${safeDetailText(turn.request) || "request unavailable"}`,
101
+ turnLabel
102
+ ], width),
103
+ tone: "active",
104
+ kind: "turn"
105
+ }];
106
+ if (turn.workers.length === 0) {
107
+ lines.push({ text: fitDetailText(" no workers", width), tone: "muted", kind: "empty" });
108
+ return lines;
109
+ }
110
+ for (const worker of turn.workers) {
111
+ const workerIndex = workerIndexById.get(worker.id) ?? 0;
112
+ lines.push({
113
+ text: detailWorkerText(worker, workerIndex === selectedWorkerIndex, width),
114
+ tone: detailWorkerTone(worker),
115
+ workerIndex,
116
+ kind: "worker"
117
+ });
118
+ lines.push({
119
+ text: detailNativeText(worker, width),
120
+ tone: "muted",
121
+ workerIndex,
122
+ kind: "native"
123
+ });
124
+ }
125
+ return lines;
126
+ });
127
+ }
128
+ function detailWorkerText(worker, selected, width) {
129
+ const marker = selected ? "> " : " ";
130
+ const role = `${worker.role.slice(0, 1).toUpperCase()}${worker.role.slice(1)}`;
131
+ const engine = [worker.engine, worker.model, worker.modelProvider].filter(Boolean).join("/");
132
+ const feature = worker.featureTitle ?? worker.featureId ?? "";
133
+ return fitDetailCandidates([
134
+ `${marker}${role} · ${engine} · ${feature ? `${safeDetailText(feature)} · ` : ""}${worker.state} · ${formatDetailTime(worker.lastActivityAt)}`,
135
+ `${marker}${role} · ${engine} · ${worker.state}`,
136
+ `${marker}${role} · ${worker.engine}`,
137
+ `${marker}${safeDetailText(worker.id)}`,
138
+ marker.trimEnd()
139
+ ], width);
140
+ }
141
+ function detailNativeText(worker, width) {
142
+ const session = worker.nativeSession;
143
+ const cwd = safeDetailText(session?.cwd ?? worker.dir);
144
+ if (!session) {
145
+ return fitDetailCandidates([
146
+ ` native · none · cwd ${cwd}`,
147
+ ` native · none`,
148
+ " no native session"
149
+ ], width);
150
+ }
151
+ return fitDetailCandidates([
152
+ ` native · ${safeDetailText(session.sessionId)} · cwd ${cwd} · used ${formatDetailTime(session.lastUsedAt)}`,
153
+ ` native · ${safeDetailText(session.sessionId)} · used ${formatDetailTime(session.lastUsedAt)}`,
154
+ ` native · ${safeDetailText(session.sessionId)}`,
155
+ ` session ${safeDetailText(session.sessionId)}`
156
+ ], width);
157
+ }
158
+ function TaskSessionDetailRow({ line, width }) {
159
+ const fill = Math.max(0, width - displayWidth(line.text));
160
+ return (_jsxs(Text, { children: [_jsx(Text, { ...detailLineTheme(line.tone), children: line.text }), fill > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(fill) }) : null] }));
161
+ }
162
+ function detailTaskTone(status) {
163
+ if (status === "done")
164
+ return "success";
165
+ if (status === "failed")
166
+ return "danger";
167
+ if (status === "paused" || status === "cancelled")
168
+ return "warning";
169
+ return "active";
170
+ }
171
+ function detailWorkerTone(worker) {
172
+ if (worker.state === "done")
173
+ return "success";
174
+ if (worker.state === "failed")
175
+ return "danger";
176
+ if (worker.state === "cancelled")
177
+ return "warning";
178
+ if (worker.state === "running" || worker.state === "starting")
179
+ return "active";
180
+ return "muted";
181
+ }
182
+ function detailLineTheme(tone) {
183
+ return {
184
+ backgroundColor: TUI_THEME.surface,
185
+ color: tone === "heading" || tone === "active"
186
+ ? TUI_THEME.accent
187
+ : tone === "success"
188
+ ? TUI_THEME.success
189
+ : tone === "warning"
190
+ ? TUI_THEME.warning
191
+ : tone === "danger"
192
+ ? TUI_THEME.danger
193
+ : TUI_THEME.muted,
194
+ ...(tone === "heading" || tone === "danger" ? { bold: true } : {})
195
+ };
196
+ }
197
+ function clampDetailWorkerIndex(index, count) {
198
+ return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
199
+ }
200
+ function humanizeDetailState(value) {
201
+ return value.replaceAll("_", " ");
202
+ }
203
+ function formatDetailTime(value) {
204
+ return value.slice(5, 16).replace("T", " ");
205
+ }
206
+ function fitDetailCandidates(candidates, width) {
207
+ return candidates.find((candidate) => displayWidth(candidate) <= width)
208
+ ?? fitDetailText(candidates.at(-1) ?? "", width);
209
+ }
210
+ function fitDetailText(text, width) {
211
+ return compactEndByDisplayWidth(text, Math.max(1, width));
212
+ }
213
+ function safeDetailText(text) {
214
+ return text
215
+ .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
216
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
217
+ .replace(/\s+/g, " ")
218
+ .trim();
219
+ }
220
+ function taskSessionDetailContentWidth(terminalWidth) {
221
+ return Math.max(1, terminalWidth - 2);
222
+ }