parallel-codex-tui 0.1.0

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 +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +150 -0
  4. package/dist/bootstrap.js +25 -0
  5. package/dist/cli-args.js +26 -0
  6. package/dist/cli.js +48 -0
  7. package/dist/core/config.js +304 -0
  8. package/dist/core/file-store.js +56 -0
  9. package/dist/core/paths.js +17 -0
  10. package/dist/core/router.js +151 -0
  11. package/dist/core/session-index.js +180 -0
  12. package/dist/core/session-manager.js +264 -0
  13. package/dist/doctor.js +121 -0
  14. package/dist/domain/schemas.js +78 -0
  15. package/dist/orchestrator/collaboration-channel.js +103 -0
  16. package/dist/orchestrator/orchestrator.js +545 -0
  17. package/dist/orchestrator/prompts.js +124 -0
  18. package/dist/orchestrator/supervisor-summary.js +38 -0
  19. package/dist/tui/App.js +740 -0
  20. package/dist/tui/AppShell.js +129 -0
  21. package/dist/tui/InputBar.js +141 -0
  22. package/dist/tui/StatusBar.js +288 -0
  23. package/dist/tui/TerminalOutput.js +22 -0
  24. package/dist/tui/WorkerOutputView.js +4015 -0
  25. package/dist/tui/chat-input.js +37 -0
  26. package/dist/tui/display-width.js +132 -0
  27. package/dist/tui/keyboard.js +38 -0
  28. package/dist/tui/native-input.js +120 -0
  29. package/dist/tui/raw-input-decoder.js +18 -0
  30. package/dist/tui/scrolling.js +25 -0
  31. package/dist/tui/status-line.js +90 -0
  32. package/dist/tui/task-memory.js +26 -0
  33. package/dist/tui/terminal-screen.js +168 -0
  34. package/dist/version.js +1 -0
  35. package/dist/workers/mock-adapter.js +62 -0
  36. package/dist/workers/native-attach.js +189 -0
  37. package/dist/workers/process-adapter.js +265 -0
  38. package/dist/workers/registry.js +32 -0
  39. package/dist/workers/types.js +1 -0
  40. package/package.json +53 -0
@@ -0,0 +1,129 @@
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 { StatusBar } from "./StatusBar.js";
5
+ import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
6
+ const APP_HEADER_BACKGROUND = "ansi256(235)";
7
+ export function AppShell({ view, cwd, taskId, statusText, contentHeight = 20, terminalWidth = process.stdout.columns || 120, children, input, error = null }) {
8
+ const header = headerParts({ view, cwd, taskId, terminalWidth });
9
+ const headerSegments = headerDisplaySegments(header);
10
+ const headerLeadingWidth = terminalWidth > 1 ? 1 : 0;
11
+ const headerRenderWidth = typeof process.stdout.columns === "number"
12
+ ? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
13
+ : null;
14
+ const headerBarWidth = headerRenderWidth === null ? null : Math.max(1, headerRenderWidth - 1);
15
+ const headerTrailingWidth = headerBarWidth === null
16
+ ? 0
17
+ : Math.max(0, headerBarWidth - headerLeadingWidth - headerSegmentsDisplayWidth(headerSegments));
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [headerLeadingWidth > 0 ? _jsx(Text, { backgroundColor: APP_HEADER_BACKGROUND, children: " ".repeat(headerLeadingWidth) }) : null, headerSegments.map((segment, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 ? _jsx(Text, { backgroundColor: APP_HEADER_BACKGROUND, dimColor: true, children: " " }) : null, _jsx(Text, { backgroundColor: APP_HEADER_BACKGROUND, color: segment.kind === "brand" ? "cyan" : segment.kind === "view" ? "white" : undefined, bold: segment.kind === "brand", dimColor: segment.kind !== "brand" && segment.kind !== "view", children: segment.text })] }, `${segment.kind}-${index}`))), headerTrailingWidth > 0 ? _jsx(Text, { backgroundColor: APP_HEADER_BACKGROUND, children: " ".repeat(headerTrailingWidth) }) : null] }), _jsx(Box, { flexDirection: "column", height: contentHeight, paddingX: 1, children: children }), input, _jsx(StatusBar, { text: statusText, terminalWidth: terminalWidth }), error ? (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "red", children: error }) })) : null] }));
19
+ }
20
+ function headerSegmentsDisplayWidth(segments) {
21
+ return segments.reduce((sum, segment, index) => sum + displayWidth(segment.text) + (index > 0 ? 2 : 0), 0);
22
+ }
23
+ function headerDisplaySegments(parts) {
24
+ return Object.entries(parts)
25
+ .filter(([, text]) => Boolean(text))
26
+ .map(([kind, text]) => ({ kind, text }));
27
+ }
28
+ function headerParts(input) {
29
+ const nano = input.terminalWidth < 16;
30
+ const tiny = input.terminalWidth < 24;
31
+ const narrow = input.terminalWidth < 72;
32
+ const veryNarrow = input.terminalWidth < 56;
33
+ const ultraNarrow = input.terminalWidth < 40;
34
+ const task = compactHeaderTaskId(input.taskId);
35
+ const showTask = input.terminalWidth >= 24 && !(task === "none" && input.terminalWidth < 32);
36
+ const contentWidth = Math.max(1, input.terminalWidth - 2);
37
+ return fitHeaderParts({
38
+ brand: narrow ? "pct" : "parallel-codex-tui",
39
+ view: nano ? "" : input.terminalWidth <= 24 && input.view === "native" ? "nat" : narrow ? shortViewLabel(input.view) : viewLabel(input.view),
40
+ task: showTask ? ultraNarrow ? ultraCompactTaskId(task) : narrow ? task : `task ${task}` : "",
41
+ project: tiny || ultraNarrow ? "" : compactHeaderProject(input.cwd, veryNarrow ? 10 : narrow ? 16 : 40),
42
+ shortcut: narrow ? shortShortcutHint(input.view) : shortcutHint(input.view)
43
+ }, contentWidth);
44
+ }
45
+ function fitHeaderParts(parts, contentWidth) {
46
+ const fitted = { ...parts };
47
+ if (headerLineLength(fitted) <= contentWidth) {
48
+ return fitted;
49
+ }
50
+ fitted.project = "";
51
+ if (headerLineLength(fitted) <= contentWidth) {
52
+ return fitted;
53
+ }
54
+ const taskBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, task: "" }) - 2);
55
+ fitted.task = compactHeaderText(fitted.task, taskBudget);
56
+ if (headerLineLength(fitted) <= contentWidth) {
57
+ return fitted;
58
+ }
59
+ const viewBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, view: "" }) - 2);
60
+ fitted.view = compactHeaderText(fitted.view, viewBudget);
61
+ if (headerLineLength(fitted) <= contentWidth) {
62
+ return fitted;
63
+ }
64
+ fitted.shortcut = "";
65
+ if (headerLineLength(fitted) <= contentWidth) {
66
+ return fitted;
67
+ }
68
+ fitted.task = "";
69
+ if (headerLineLength(fitted) <= contentWidth) {
70
+ return fitted;
71
+ }
72
+ fitted.view = "";
73
+ if (headerLineLength(fitted) <= contentWidth) {
74
+ return fitted;
75
+ }
76
+ fitted.brand = compactHeaderText(fitted.brand, contentWidth);
77
+ return fitted;
78
+ }
79
+ function headerLineLength(parts) {
80
+ return displayWidth(headerPartList(parts).join(" "));
81
+ }
82
+ function headerPartList(parts) {
83
+ return [parts.brand, parts.view, parts.task, parts.project, parts.shortcut].filter(Boolean);
84
+ }
85
+ function viewLabel(view) {
86
+ return shortViewLabel(view);
87
+ }
88
+ function shortcutHint(view) {
89
+ if (view === "native") {
90
+ return "^] detach";
91
+ }
92
+ return "^C exit";
93
+ }
94
+ function shortViewLabel(view) {
95
+ if (view === "worker") {
96
+ return "logs";
97
+ }
98
+ if (view === "native") {
99
+ return "native";
100
+ }
101
+ return "chat";
102
+ }
103
+ function shortShortcutHint(view) {
104
+ return view === "native" ? "^]" : "^C";
105
+ }
106
+ function compactHeaderProject(cwd, maxLength) {
107
+ const project = basename(cwd) || cwd;
108
+ return compactHeaderText(project, maxLength);
109
+ }
110
+ function compactHeaderText(text, maxLength) {
111
+ if (maxLength <= 4) {
112
+ return compactEndByDisplayWidth(text, Math.min(maxLength, 3));
113
+ }
114
+ return compactEndByDisplayWidth(text, maxLength);
115
+ }
116
+ function ultraCompactTaskId(taskId) {
117
+ const first = taskId.split("-", 1)[0];
118
+ return first || taskId.slice(0, 6);
119
+ }
120
+ function compactHeaderTaskId(taskId) {
121
+ if (!taskId) {
122
+ return "none";
123
+ }
124
+ const match = taskId.match(/^task-\d{8}-(.+)$/);
125
+ if (match) {
126
+ return match[1] ?? taskId;
127
+ }
128
+ return taskId.startsWith("task-") ? taskId.slice("task-".length) : taskId;
129
+ }
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { compactTailByDisplayWidth, displayWidth } from "./display-width.js";
4
+ const INPUT_BAR_BACKGROUND = "ansi256(234)";
5
+ export function InputBar({ mode, busy = false, hasWorkers = false, nativeClosed = false, value, terminalWidth = process.stdout.columns || 120, onChange, onSubmit }) {
6
+ if (mode === "worker") {
7
+ const hints = workerInputHints(terminalWidth);
8
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "cyan", bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, dimColor: true, children: hints.detail }) : null] }));
9
+ }
10
+ if (mode === "native") {
11
+ const hints = nativeInputHints(terminalWidth, nativeClosed);
12
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "cyan", bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, dimColor: true, children: hints.detail }) : null] }));
13
+ }
14
+ const busyText = chatBusyDisplayValue(terminalWidth);
15
+ const prompt = busy ? "run" : ">";
16
+ if (busy) {
17
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(busyText ? `${prompt} ${busyText}` : prompt), children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "yellow", bold: true, children: prompt }), busyText ? (_jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: " " }), _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "yellow", children: busyText })] })) : null] }));
18
+ }
19
+ if (value) {
20
+ const displayValue = chatInputDisplayValue(value, terminalWidth);
21
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${prompt} ${displayValue}|`), children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "cyan", bold: true, children: prompt }), _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: " " }), _jsxs(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: [displayValue, "|"] })] }));
22
+ }
23
+ const hasLeadingPromptSpace = terminalWidth >= 10;
24
+ const placeholder = chatPlaceholderDisplayText(terminalWidth, { hasWorkers }, { leadingSpace: hasLeadingPromptSpace });
25
+ return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${prompt}${hasLeadingPromptSpace ? " " : ""}|${placeholder}`), children: [_jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, color: "cyan", bold: true, children: prompt }), hasLeadingPromptSpace ? _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: " " }) : null, _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: "|" }), _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, dimColor: true, children: placeholder })] }));
26
+ }
27
+ function InputRail({ terminalWidth, textWidth, children }) {
28
+ const leadingWidth = terminalWidth > 1 ? 1 : 0;
29
+ const renderWidth = typeof process.stdout.columns === "number"
30
+ ? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
31
+ : null;
32
+ const barWidth = renderWidth === null ? null : Math.max(1, renderWidth - 1);
33
+ const trailingWidth = barWidth === null
34
+ ? 0
35
+ : Math.max(0, barWidth - leadingWidth - textWidth);
36
+ return (_jsxs(Box, { children: [leadingWidth > 0 ? _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: " ".repeat(leadingWidth) }) : null, children, trailingWidth > 0 ? _jsx(Text, { backgroundColor: INPUT_BAR_BACKGROUND, children: " ".repeat(trailingWidth) }) : null] }));
37
+ }
38
+ export function chatPlaceholderDisplayText(terminalWidth, options = {}, display = {}) {
39
+ const placeholder = chatPlaceholderDisplayValue(terminalWidth, options);
40
+ if (!placeholder) {
41
+ return "";
42
+ }
43
+ return display.leadingSpace === false ? placeholder : ` ${placeholder}`;
44
+ }
45
+ export function chatInputDisplayValue(value, terminalWidth) {
46
+ const valueWidth = Math.max(1, terminalWidth - 6);
47
+ return compactTailByDisplayWidth(value, valueWidth);
48
+ }
49
+ export function chatPlaceholderDisplayValue(terminalWidth, options = {}) {
50
+ if (options.hasWorkers) {
51
+ return chatTaskPlaceholderDisplayValue(terminalWidth);
52
+ }
53
+ if (terminalWidth < 14) {
54
+ return "msg";
55
+ }
56
+ if (terminalWidth < 22) {
57
+ return "message";
58
+ }
59
+ return chatInputDisplayValue("Type a message", terminalWidth);
60
+ }
61
+ function chatTaskPlaceholderDisplayValue(terminalWidth) {
62
+ if (terminalWidth < 14) {
63
+ return "msg";
64
+ }
65
+ if (terminalWidth < 19) {
66
+ return "msg · ^W";
67
+ }
68
+ if (terminalWidth < 24) {
69
+ return "msg · ^W · ^O";
70
+ }
71
+ if (terminalWidth < 38) {
72
+ return chatInputDisplayValue("Message · ^W · ^O", terminalWidth);
73
+ }
74
+ if (terminalWidth < 64) {
75
+ return chatInputDisplayValue("Message · ^W logs · ^O attach", terminalWidth);
76
+ }
77
+ return chatInputDisplayValue("Type a message · ^W logs · ^O attach", terminalWidth);
78
+ }
79
+ export function chatBusyDisplayValue(terminalWidth) {
80
+ if (terminalWidth < 14) {
81
+ return "";
82
+ }
83
+ if (terminalWidth < 22) {
84
+ return "busy";
85
+ }
86
+ return "Running...";
87
+ }
88
+ function workerInputHints(width) {
89
+ if (width < 12) {
90
+ return { label: "log", detail: "" };
91
+ }
92
+ if (width < 15) {
93
+ return { label: "log", detail: " · ^O" };
94
+ }
95
+ if (width < 18) {
96
+ return { label: "log", detail: " · Pg · ^O" };
97
+ }
98
+ if (width < 28) {
99
+ return { label: "logs", detail: " · Pg · ^O" };
100
+ }
101
+ if (width < 56) {
102
+ return { label: "logs", detail: " · Pg · Tab · ^O · Esc" };
103
+ }
104
+ if (width < 72) {
105
+ return { label: "logs", detail: " · Pg/wheel · Tab · ^O · Esc" };
106
+ }
107
+ return { label: "logs", detail: " · read · wheel/Pg · Tab worker · ^O attach · Esc chat" };
108
+ }
109
+ function nativeInputHints(width, closed = false) {
110
+ if (closed) {
111
+ if (width < 12) {
112
+ return { label: "done", detail: " ^]" };
113
+ }
114
+ if (width < 14) {
115
+ return { label: "closed", detail: " ^]" };
116
+ }
117
+ if (width < 24) {
118
+ return { label: "closed", detail: " · ^]" };
119
+ }
120
+ if (width < 36) {
121
+ return { label: "closed", detail: " · Pg · ^]" };
122
+ }
123
+ if (width < 56) {
124
+ return { label: "closed", detail: " · Pg/wheel · ^]" };
125
+ }
126
+ return { label: "closed", detail: " · wheel/Pg · ^] back" };
127
+ }
128
+ if (width < 12) {
129
+ return { label: "nat", detail: " ^]" };
130
+ }
131
+ if (width < 24) {
132
+ return { label: "native", detail: " · ^]" };
133
+ }
134
+ if (width < 36) {
135
+ return { label: "native", detail: " · Pg · ^]" };
136
+ }
137
+ if (width < 56) {
138
+ return { label: "native", detail: " · Pg/wheel · ^]" };
139
+ }
140
+ return { label: "native", detail: " · wheel/Pg · ^] detach" };
141
+ }
@@ -0,0 +1,288 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
4
+ const STATUS_BAR_BACKGROUND = "ansi256(236)";
5
+ export function StatusBar({ text, terminalWidth = process.stdout.columns || 120, showTask = false }) {
6
+ const parsedSegments = parseStatusText(text, { hideTask: !showTask || terminalWidth < 40 });
7
+ if (isIdleStatus(parsedSegments)) {
8
+ return (_jsx(Box, { children: _jsx(Text, { children: " " }) }));
9
+ }
10
+ const segments = omitTinyCurrentSegment(parsedSegments, terminalWidth);
11
+ const compact = shouldUseCompactStatus(segments, terminalWidth);
12
+ const fittedSegments = fitStatusSegments(segments, terminalWidth, compact);
13
+ if (isIdleStatus(fittedSegments)) {
14
+ return (_jsx(Box, { children: _jsx(Text, { children: " " }) }));
15
+ }
16
+ const leadingWidth = terminalWidth > 1 ? 1 : 0;
17
+ const renderWidth = typeof process.stdout.columns === "number"
18
+ ? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
19
+ : null;
20
+ const barWidth = renderWidth === null ? null : Math.max(1, renderWidth - 1);
21
+ const trailingWidth = barWidth === null
22
+ ? 0
23
+ : Math.max(0, barWidth - leadingWidth - statusSegmentsWidth(fittedSegments, compact));
24
+ return (_jsxs(Box, { children: [leadingWidth > 0 ? _jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, children: " ".repeat(leadingWidth) }) : null, fittedSegments.map((segment, index) => (_jsx(StatusSegment, { segment: segment, compact: compact, isLast: index === fittedSegments.length - 1 }, `${segment.label}-${index}`))), trailingWidth > 0 ? _jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, children: " ".repeat(trailingWidth) }) : null] }));
25
+ }
26
+ function omitTinyCurrentSegment(segments, terminalWidth) {
27
+ if (terminalWidth >= 24 || segments.length <= 1) {
28
+ return segments;
29
+ }
30
+ const compacted = segments.filter((segment) => segment.label.toLowerCase() !== "current");
31
+ return compacted.length > 0 ? compacted : segments;
32
+ }
33
+ function shouldUseCompactStatus(segments, terminalWidth) {
34
+ if (terminalWidth < 56) {
35
+ return true;
36
+ }
37
+ const roomyDisplays = segments.map((segment) => statusSegmentDisplay(segment, false));
38
+ return statusSegmentsDisplayWidth(roomyDisplays, false) > Math.max(1, terminalWidth - 2);
39
+ }
40
+ function isIdleStatus(segments) {
41
+ return segments.length === 1 && segments[0]?.tone === "idle" && segments[0]?.value === "idle";
42
+ }
43
+ function StatusSegment({ segment, compact, isLast }) {
44
+ const display = statusSegmentDisplay(segment, compact);
45
+ return (_jsxs(Box, { flexShrink: 0, children: [display.label ? (_jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, color: colorForTone(segment.tone), dimColor: segment.tone === undefined || segment.tone === "idle", children: display.label }), _jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, dimColor: true, children: display.separator })] })) : null, _jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, color: valueColorForTone(segment.tone), bold: segment.tone !== undefined && segment.tone !== "idle", wrap: "truncate-end", children: display.value }), !isLast ? _jsx(Text, { backgroundColor: STATUS_BAR_BACKGROUND, dimColor: true, children: statusSegmentSeparator(compact) }) : null] }));
46
+ }
47
+ function fitStatusSegments(segments, terminalWidth, compact) {
48
+ const contentWidth = Math.max(1, terminalWidth - 2);
49
+ if (statusSegmentsWidth(segments, compact) <= contentWidth) {
50
+ return segments;
51
+ }
52
+ const displays = segments.map((segment) => statusSegmentDisplay(segment, compact));
53
+ const totalWidth = statusSegmentsDisplayWidth(displays, compact);
54
+ const currentIndex = segments.map((segment) => segment.label.toLowerCase()).lastIndexOf("current");
55
+ if (currentIndex >= 0) {
56
+ const currentDisplay = displays[currentIndex];
57
+ if (currentDisplay) {
58
+ const overflow = totalWidth - contentWidth;
59
+ const nextValueWidth = Math.max(1, displayWidth(currentDisplay.value) - overflow);
60
+ const fitted = segments.map((segment, index) => index === currentIndex
61
+ ? { ...segment, value: compactStatusTextEnd(segment.value, nextValueWidth) }
62
+ : segment);
63
+ if (statusSegmentsWidth(fitted, compact) <= contentWidth) {
64
+ return fitted;
65
+ }
66
+ return selectStatusSegmentsThatFit(fitted, contentWidth, compact);
67
+ }
68
+ }
69
+ return selectStatusSegmentsThatFit(segments, contentWidth, compact);
70
+ }
71
+ function statusSegmentsDisplayWidth(displays, compact) {
72
+ const segmentWidths = displays.map((display) => displayWidth(`${display.label}${display.separator}${display.value}`));
73
+ return segmentWidths.reduce((sum, width) => sum + width, 0) + Math.max(0, displays.length - 1) * displayWidth(statusSegmentSeparator(compact));
74
+ }
75
+ function statusSegmentsWidth(segments, compact) {
76
+ return statusSegmentsDisplayWidth(segments.map((segment) => statusSegmentDisplay(segment, compact)), compact);
77
+ }
78
+ function selectStatusSegmentsThatFit(segments, contentWidth, compact) {
79
+ const selected = [];
80
+ const candidates = segments
81
+ .map((segment, index) => ({ index, segment }))
82
+ .sort((left, right) => statusSegmentKeepPriority(left.segment) - statusSegmentKeepPriority(right.segment));
83
+ for (const candidate of candidates) {
84
+ const next = [...selected, candidate].sort((left, right) => left.index - right.index).map((item) => item.segment);
85
+ if (statusSegmentsWidth(next, compact) <= contentWidth) {
86
+ selected.push(candidate);
87
+ }
88
+ }
89
+ if (selected.length > 0) {
90
+ return selected.sort((left, right) => left.index - right.index).map((item) => item.segment);
91
+ }
92
+ return [compactSingleStatusSegment(segments[0] ?? { label: "STATUS", value: "idle", tone: "idle" }, contentWidth, compact)];
93
+ }
94
+ function statusSegmentKeepPriority(segment) {
95
+ const label = segment.label.toLowerCase();
96
+ if (segment.tone === "fail" || label === "fail") {
97
+ return 0;
98
+ }
99
+ if (segment.tone === "run" || label === "run") {
100
+ return 1;
101
+ }
102
+ if (segment.tone === "wait" || label === "wait") {
103
+ return 2;
104
+ }
105
+ if (label === "workers") {
106
+ return 3;
107
+ }
108
+ if (segment.tone === "done" || label === "done") {
109
+ return 4;
110
+ }
111
+ if (label === "current") {
112
+ return 5;
113
+ }
114
+ return 6;
115
+ }
116
+ function compactSingleStatusSegment(segment, contentWidth, compact) {
117
+ const display = statusSegmentDisplay(segment, compact);
118
+ const labelWidth = displayWidth(`${display.label}${display.separator}`);
119
+ return {
120
+ ...segment,
121
+ value: compactStatusTextEnd(display.value, Math.max(1, contentWidth - labelWidth))
122
+ };
123
+ }
124
+ function statusSegmentSeparator(compact) {
125
+ return compact ? " " : " · ";
126
+ }
127
+ function compactStatusTextEnd(text, maxLength) {
128
+ return compactEndByDisplayWidth(text, maxLength);
129
+ }
130
+ function statusSegmentDisplay(segment, compact) {
131
+ const label = segment.label.toLowerCase();
132
+ if (label === "task") {
133
+ return { label: "", separator: "", value: segment.value };
134
+ }
135
+ if (isRoleStatusLabel(label)) {
136
+ return {
137
+ label: compact ? compactRoleStatusLabel(label) : displayRoleStatusLabel(label),
138
+ separator: compact ? ":" : " ",
139
+ value: segment.value
140
+ };
141
+ }
142
+ if (!compact) {
143
+ if (label === "current") {
144
+ return { label: "@", separator: " ", value: segment.value };
145
+ }
146
+ if (segment.tone && segment.tone !== "idle") {
147
+ return { label: segment.tone, separator: " ", value: segment.value };
148
+ }
149
+ return { label, separator: " ", value: segment.value };
150
+ }
151
+ if (label === "workers") {
152
+ return { label: "w", separator: "", value: segment.value };
153
+ }
154
+ if (label === "current") {
155
+ return { label: "@", separator: " ", value: segment.value };
156
+ }
157
+ if (segment.tone && segment.tone !== "idle") {
158
+ return { label: compactToneLabel(segment.tone), separator: "", value: segment.value };
159
+ }
160
+ return { label, separator: " ", value: segment.value };
161
+ }
162
+ function compactToneLabel(tone) {
163
+ if (tone === "run") {
164
+ return "r";
165
+ }
166
+ if (tone === "done") {
167
+ return "d";
168
+ }
169
+ if (tone === "fail") {
170
+ return "f";
171
+ }
172
+ return "w";
173
+ }
174
+ function isRoleStatusLabel(label) {
175
+ return label === "main" || label === "judge" || label === "actor" || label === "critic";
176
+ }
177
+ function displayRoleStatusLabel(label) {
178
+ return label === "main" ? "chat" : label;
179
+ }
180
+ function compactRoleStatusLabel(label) {
181
+ if (label === "main") {
182
+ return "chat";
183
+ }
184
+ return label.slice(0, 1);
185
+ }
186
+ function parseStatusText(text, options = {}) {
187
+ const parts = text
188
+ .split("|")
189
+ .map((part) => part.trim())
190
+ .filter(Boolean);
191
+ if (parts.length === 0) {
192
+ return [{ label: "STATUS", value: "idle", tone: "idle" }];
193
+ }
194
+ if (parts.length === 1 && parts[0] === "idle") {
195
+ return [{ label: "STATUS", value: "idle", tone: "idle" }];
196
+ }
197
+ const segments = [];
198
+ const [first = "idle", ...rest] = parts;
199
+ const statusParts = isStatusPart(first) ? parts : rest;
200
+ if (!isStatusPart(first) && !options.hideTask) {
201
+ segments.push({ label: "TASK", value: first, tone: first === "idle" ? "idle" : undefined });
202
+ }
203
+ for (const part of statusParts) {
204
+ const workerMatch = part.match(/^workers\s+(\d+)$/i);
205
+ if (workerMatch) {
206
+ segments.push({ label: "WORKERS", value: workerMatch[1] ?? "0" });
207
+ continue;
208
+ }
209
+ const counts = parseStateCounts(part);
210
+ if (counts.length > 0) {
211
+ segments.push(...counts);
212
+ continue;
213
+ }
214
+ segments.push(parseCurrentStatus(part));
215
+ }
216
+ return segments.length > 0 ? segments : [{ label: "STATUS", value: "idle", tone: "idle" }];
217
+ }
218
+ function isStatusPart(part) {
219
+ return /^workers\s+\d+$/i.test(part) || parseStateCounts(part).length > 0;
220
+ }
221
+ function parseStateCounts(part) {
222
+ const matches = Array.from(part.matchAll(/\b(run|done|fail|wait|idle)\s+(\d+)\b/gi));
223
+ if (matches.length === 0) {
224
+ return [];
225
+ }
226
+ return matches.map((match) => {
227
+ const tone = normalizeTone(match[1] ?? "idle");
228
+ return {
229
+ label: tone.toUpperCase(),
230
+ value: match[2] ?? "0",
231
+ tone
232
+ };
233
+ });
234
+ }
235
+ function parseCurrentStatus(part) {
236
+ const roleStatus = parseRoleStatus(part);
237
+ if (roleStatus) {
238
+ return roleStatus;
239
+ }
240
+ const toneMatch = part.match(/\b(run|done|fail|wait|idle)\b/i);
241
+ const tone = toneMatch ? normalizeTone(toneMatch[1] ?? "idle") : undefined;
242
+ const value = toneMatch ? part.replace(toneMatch[0], "").trim() || part : part;
243
+ return {
244
+ label: "CURRENT",
245
+ value,
246
+ tone
247
+ };
248
+ }
249
+ function parseRoleStatus(part) {
250
+ const match = part.match(/^(main|judge|actor|critic)\s+(run|done|fail|wait|idle)\b/i);
251
+ if (!match) {
252
+ return null;
253
+ }
254
+ const tone = normalizeTone(match[2] ?? "idle");
255
+ return {
256
+ label: (match[1] ?? "main").toUpperCase(),
257
+ value: tone,
258
+ tone
259
+ };
260
+ }
261
+ function normalizeTone(value) {
262
+ const normalized = value.toLowerCase();
263
+ if (normalized === "run" || normalized === "done" || normalized === "fail" || normalized === "wait") {
264
+ return normalized;
265
+ }
266
+ return "idle";
267
+ }
268
+ function colorForTone(tone) {
269
+ if (tone === "run") {
270
+ return "cyan";
271
+ }
272
+ if (tone === "done") {
273
+ return "green";
274
+ }
275
+ if (tone === "fail") {
276
+ return "red";
277
+ }
278
+ if (tone === "wait") {
279
+ return "yellow";
280
+ }
281
+ return "gray";
282
+ }
283
+ function valueColorForTone(tone) {
284
+ if (tone === "fail") {
285
+ return "red";
286
+ }
287
+ return undefined;
288
+ }
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ export function TerminalOutput({ lines }) {
4
+ if (lines.length === 0) {
5
+ return _jsx(Text, { children: "(no output yet)" });
6
+ }
7
+ return (_jsx(Box, { flexDirection: "column", children: lines.map((line, index) => (_jsx(Text, { children: line.chunks.length === 0
8
+ ? " "
9
+ : line.chunks.map((chunk, chunkIndex) => (_jsx(Text, { ...textProps(chunk.style), children: chunk.text }, chunkIndex))) }, index))) }));
10
+ }
11
+ function textProps(style) {
12
+ return {
13
+ backgroundColor: style.backgroundColor,
14
+ bold: style.bold,
15
+ color: style.color,
16
+ dimColor: style.dimColor,
17
+ inverse: style.inverse || style.cursor,
18
+ italic: style.italic,
19
+ strikethrough: style.strikethrough,
20
+ underline: style.underline
21
+ };
22
+ }