parallel-codex-tui 0.1.0 → 0.1.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.
- package/.parallel-codex/config.example.toml +90 -3
- package/README.md +269 -12
- package/dist/bootstrap.js +50 -18
- package/dist/cli-args.js +96 -14
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +40 -0
- package/dist/cli.js +291 -35
- package/dist/core/app-root.js +8 -0
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +191 -23
- package/dist/core/file-store.js +130 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +10 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +473 -42
- package/dist/core/session-index.js +225 -30
- package/dist/core/session-manager.js +1182 -44
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +126 -0
- package/dist/doctor.js +384 -30
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1777 -212
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2838 -159
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +15 -0
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +147 -8
- package/dist/workers/native-session-detection.js +17 -0
- package/dist/workers/process-adapter.js +580 -81
- package/dist/workers/registry.js +4 -2
- package/package.json +17 -2
package/dist/tui/AppShell.js
CHANGED
|
@@ -3,10 +3,13 @@ import { basename } from "node:path";
|
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { StatusBar } from "./StatusBar.js";
|
|
5
5
|
import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { TUI_THEME } from "./theme.js";
|
|
7
|
+
const APP_HEADER_ROOMY_SEPARATOR = " · ";
|
|
8
|
+
const APP_HEADER_COMPACT_SEPARATOR = " ";
|
|
9
|
+
export function AppShell({ view, cwd, taskId, statusText, contentHeight = 20, terminalWidth = process.stdout.columns || 120, showStatusBar = true, children, input, error = null }) {
|
|
8
10
|
const header = headerParts({ view, cwd, taskId, terminalWidth });
|
|
9
11
|
const headerSegments = headerDisplaySegments(header);
|
|
12
|
+
const headerSeparatorText = headerSeparator(terminalWidth);
|
|
10
13
|
const headerLeadingWidth = terminalWidth > 1 ? 1 : 0;
|
|
11
14
|
const headerRenderWidth = typeof process.stdout.columns === "number"
|
|
12
15
|
? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
|
|
@@ -14,11 +17,154 @@ export function AppShell({ view, cwd, taskId, statusText, contentHeight = 20, te
|
|
|
14
17
|
const headerBarWidth = headerRenderWidth === null ? null : Math.max(1, headerRenderWidth - 1);
|
|
15
18
|
const headerTrailingWidth = headerBarWidth === null
|
|
16
19
|
? 0
|
|
17
|
-
: Math.max(0, headerBarWidth - headerLeadingWidth - headerSegmentsDisplayWidth(headerSegments));
|
|
18
|
-
|
|
20
|
+
: Math.max(0, headerBarWidth - headerLeadingWidth - headerSegmentsDisplayWidth(headerSegments, headerSeparatorText));
|
|
21
|
+
const errorRow = error ? appShellErrorRow(error, terminalWidth) : null;
|
|
22
|
+
const errorTheme = appShellErrorLineTheme();
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [headerLeadingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " ".repeat(headerLeadingWidth) }) : null, headerSegments.map((segment, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.chrome, color: TUI_THEME.muted, children: headerSeparatorText }) : null, _jsx(Text, { backgroundColor: TUI_THEME.chrome, color: segment.kind === "brand" ? TUI_THEME.accent : segment.kind === "view" ? TUI_THEME.text : TUI_THEME.muted, bold: segment.kind === "brand", children: segment.text })] }, `${segment.kind}-${index}`))), headerTrailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.chrome, children: " ".repeat(headerTrailingWidth) }) : null] }), _jsx(AppShellContentFrame, { contentHeight: contentHeight, terminalWidth: terminalWidth, children: children }), input, showStatusBar ? (_jsx(StatusBar, { text: statusText, terminalWidth: terminalWidth, fillRail: typeof process.stdout.columns === "number" })) : null, errorRow ? (_jsxs(Box, { children: [_jsx(Text, { ...errorTheme, children: " " }), _jsxs(Text, { ...errorTheme, children: [errorRow.text, " ".repeat(errorRow.trailingWidth)] })] })) : null] }));
|
|
19
24
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
25
|
+
function AppShellContentFrame({ contentHeight, terminalWidth, children }) {
|
|
26
|
+
const layout = appShellContentFrameLayout(contentHeight, terminalWidth);
|
|
27
|
+
const gutterTheme = appShellContentGutterTheme();
|
|
28
|
+
return (_jsxs(Box, { height: layout.height, children: [layout.leadingWidth > 0 ? _jsx(Text, { ...gutterTheme, children: appShellContentGutterText(layout.height, layout.leadingWidth) }) : null, _jsx(Box, { flexDirection: "column", height: layout.height, width: layout.contentWidth, children: children })] }));
|
|
29
|
+
}
|
|
30
|
+
export function appShellErrorLineTheme() {
|
|
31
|
+
return {
|
|
32
|
+
backgroundColor: TUI_THEME.dangerSurface,
|
|
33
|
+
color: TUI_THEME.danger
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function appShellContentGutterTheme() {
|
|
37
|
+
return {
|
|
38
|
+
backgroundColor: TUI_THEME.surface
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function appShellContentFrameLayout(contentHeight, terminalWidth) {
|
|
42
|
+
const height = Math.max(1, contentHeight);
|
|
43
|
+
const renderWidth = typeof process.stdout.columns === "number"
|
|
44
|
+
? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
|
|
45
|
+
: Math.max(1, terminalWidth);
|
|
46
|
+
const leadingWidth = renderWidth > 1 ? 1 : 0;
|
|
47
|
+
const reservedWrapColumnWidth = renderWidth > 2 ? 1 : 0;
|
|
48
|
+
return {
|
|
49
|
+
height,
|
|
50
|
+
leadingWidth,
|
|
51
|
+
contentWidth: Math.max(1, renderWidth - leadingWidth - reservedWrapColumnWidth)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export function appShellContentGutterText(contentHeight, gutterWidth) {
|
|
55
|
+
const height = Math.max(1, contentHeight);
|
|
56
|
+
const width = Math.max(0, gutterWidth);
|
|
57
|
+
return Array.from({ length: height }, () => " ".repeat(width)).join("\n");
|
|
58
|
+
}
|
|
59
|
+
export function appShellErrorRow(error, terminalWidth) {
|
|
60
|
+
const renderWidth = typeof process.stdout.columns === "number"
|
|
61
|
+
? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
|
|
62
|
+
: terminalWidth;
|
|
63
|
+
const contentWidth = Math.max(1, renderWidth - 2);
|
|
64
|
+
const text = appShellErrorDisplayText(error, contentWidth);
|
|
65
|
+
return {
|
|
66
|
+
text,
|
|
67
|
+
trailingWidth: Math.max(0, contentWidth - displayWidth(text))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function appShellErrorDisplayText(error, maxWidth) {
|
|
71
|
+
const normalized = normalizeAppShellError(error);
|
|
72
|
+
const message = normalized.replace(/^(?:ERROR|Error|error):\s*/, "").trim();
|
|
73
|
+
const full = `error · ${message || "unknown"}`;
|
|
74
|
+
if (displayWidth(full) <= maxWidth) {
|
|
75
|
+
return full;
|
|
76
|
+
}
|
|
77
|
+
for (const summary of appShellErrorSummaries(message)) {
|
|
78
|
+
const prefixed = `error · ${summary}`;
|
|
79
|
+
if (displayWidth(prefixed) <= maxWidth) {
|
|
80
|
+
return prefixed;
|
|
81
|
+
}
|
|
82
|
+
if (displayWidth(summary) <= maxWidth) {
|
|
83
|
+
return summary;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return ["error", "err", "!"]
|
|
87
|
+
.find((candidate) => displayWidth(candidate) <= maxWidth)
|
|
88
|
+
?? "";
|
|
89
|
+
}
|
|
90
|
+
function normalizeAppShellError(error) {
|
|
91
|
+
return error
|
|
92
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
93
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
94
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ")
|
|
95
|
+
.replace(/\s+/g, " ")
|
|
96
|
+
.trim();
|
|
97
|
+
}
|
|
98
|
+
function appShellErrorSummaries(message) {
|
|
99
|
+
if (/permission denied|\bEACCES\b|\bEPERM\b/i.test(message)) {
|
|
100
|
+
return ["permission denied", "denied"];
|
|
101
|
+
}
|
|
102
|
+
const nativeSession = message.match(/no native session for\s+(.+?)(?:\s+·|$)/i);
|
|
103
|
+
if (nativeSession) {
|
|
104
|
+
const identity = compactErrorIdentity(nativeSession[1] ?? "");
|
|
105
|
+
const role = identity.split("/", 1)[0] ?? "";
|
|
106
|
+
return [
|
|
107
|
+
identity ? `no native session · ${identity}` : "no native session",
|
|
108
|
+
role ? `no session · ${role}` : "no session",
|
|
109
|
+
"no session",
|
|
110
|
+
"no sid"
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
if (/router.*(?:timed out|timeout)|(?:timed out|timeout).*router/i.test(message)) {
|
|
114
|
+
const duration = compactErrorDuration(message);
|
|
115
|
+
const proxy = /\bproxy\b|代理/i.test(message);
|
|
116
|
+
return [
|
|
117
|
+
proxy
|
|
118
|
+
? ["router timeout", duration, "proxy"].filter(Boolean).join(" · ")
|
|
119
|
+
: ["router timeout", duration].filter(Boolean).join(" · "),
|
|
120
|
+
duration ? `router timeout · ${duration}` : "router timeout",
|
|
121
|
+
duration ? `timeout · ${duration}` : "timeout",
|
|
122
|
+
"timeout",
|
|
123
|
+
"time"
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
if (/\bproxy\b|代理/i.test(message)) {
|
|
127
|
+
return ["proxy error", "proxy"];
|
|
128
|
+
}
|
|
129
|
+
if (/\bnetwork\b|\bECONNREFUSED\b|\bECONNRESET\b|\bENETUNREACH\b|\bEHOSTUNREACH\b/i.test(message)) {
|
|
130
|
+
return ["network error", "network", "net"];
|
|
131
|
+
}
|
|
132
|
+
if (/no workers?/i.test(message)) {
|
|
133
|
+
return ["no workers · start task", "no workers", "workers"];
|
|
134
|
+
}
|
|
135
|
+
if (/command not found|\bENOENT\b/i.test(message)) {
|
|
136
|
+
return ["command missing", "command"];
|
|
137
|
+
}
|
|
138
|
+
if (/not a directory|workspace.*(?:missing|not found|does not exist)/i.test(message)) {
|
|
139
|
+
return ["workspace missing", "bad path", "path"];
|
|
140
|
+
}
|
|
141
|
+
const words = message.split(/\s+/).filter(Boolean);
|
|
142
|
+
const summaries = [];
|
|
143
|
+
for (let count = Math.min(4, words.length); count >= 1; count -= 1) {
|
|
144
|
+
summaries.push(words.slice(0, count).join(" "));
|
|
145
|
+
}
|
|
146
|
+
return summaries;
|
|
147
|
+
}
|
|
148
|
+
function compactErrorIdentity(label) {
|
|
149
|
+
const match = label.trim().match(/^([^()]+?)\s*\(([^)]+)\)$/);
|
|
150
|
+
if (!match) {
|
|
151
|
+
return label.trim().toLowerCase();
|
|
152
|
+
}
|
|
153
|
+
return `${(match[1] ?? "").trim().toLowerCase()}/${(match[2] ?? "").trim().toLowerCase()}`;
|
|
154
|
+
}
|
|
155
|
+
function compactErrorDuration(message) {
|
|
156
|
+
const milliseconds = message.match(/\b(\d+(?:\.\d+)?)\s*ms\b/i);
|
|
157
|
+
if (milliseconds) {
|
|
158
|
+
const value = Number(milliseconds[1]);
|
|
159
|
+
if (Number.isFinite(value) && value >= 1000) {
|
|
160
|
+
return `${(value / 1000).toFixed(value % 1000 === 0 ? 0 : 1)}s`;
|
|
161
|
+
}
|
|
162
|
+
return `${milliseconds[1]}ms`;
|
|
163
|
+
}
|
|
164
|
+
return message.match(/\b\d+(?:\.\d+)?s\b/i)?.[0] ?? null;
|
|
165
|
+
}
|
|
166
|
+
function headerSegmentsDisplayWidth(segments, separator) {
|
|
167
|
+
return segments.reduce((sum, segment, index) => sum + displayWidth(segment.text) + (index > 0 ? displayWidth(separator) : 0), 0);
|
|
22
168
|
}
|
|
23
169
|
function headerDisplaySegments(parts) {
|
|
24
170
|
return Object.entries(parts)
|
|
@@ -32,62 +178,66 @@ function headerParts(input) {
|
|
|
32
178
|
const veryNarrow = input.terminalWidth < 56;
|
|
33
179
|
const ultraNarrow = input.terminalWidth < 40;
|
|
34
180
|
const task = compactHeaderTaskId(input.taskId);
|
|
35
|
-
const showTask =
|
|
181
|
+
const showTask = Boolean(task) && input.terminalWidth >= 24;
|
|
36
182
|
const contentWidth = Math.max(1, input.terminalWidth - 2);
|
|
183
|
+
const separator = headerSeparator(input.terminalWidth);
|
|
37
184
|
return fitHeaderParts({
|
|
38
185
|
brand: narrow ? "pct" : "parallel-codex-tui",
|
|
39
186
|
view: nano ? "" : input.terminalWidth <= 24 && input.view === "native" ? "nat" : narrow ? shortViewLabel(input.view) : viewLabel(input.view),
|
|
40
|
-
task: showTask ? ultraNarrow ? ultraCompactTaskId(task) : narrow ? task :
|
|
187
|
+
task: showTask ? ultraNarrow ? ultraCompactTaskId(task) : narrow ? task : `#${task}` : "",
|
|
41
188
|
project: tiny || ultraNarrow ? "" : compactHeaderProject(input.cwd, veryNarrow ? 10 : narrow ? 16 : 40),
|
|
42
189
|
shortcut: narrow ? shortShortcutHint(input.view) : shortcutHint(input.view)
|
|
43
|
-
}, contentWidth);
|
|
190
|
+
}, contentWidth, separator);
|
|
44
191
|
}
|
|
45
|
-
function fitHeaderParts(parts, contentWidth) {
|
|
192
|
+
function fitHeaderParts(parts, contentWidth, separator) {
|
|
46
193
|
const fitted = { ...parts };
|
|
47
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
194
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
48
195
|
return fitted;
|
|
49
196
|
}
|
|
50
197
|
fitted.project = "";
|
|
51
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
198
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
52
199
|
return fitted;
|
|
53
200
|
}
|
|
54
|
-
const taskBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, task: "" }) -
|
|
201
|
+
const taskBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, task: "" }, separator) - displayWidth(separator));
|
|
55
202
|
fitted.task = compactHeaderText(fitted.task, taskBudget);
|
|
56
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
203
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
57
204
|
return fitted;
|
|
58
205
|
}
|
|
59
|
-
const viewBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, view: "" }) -
|
|
206
|
+
const viewBudget = Math.max(3, contentWidth - headerLineLength({ ...fitted, view: "" }, separator) - displayWidth(separator));
|
|
60
207
|
fitted.view = compactHeaderText(fitted.view, viewBudget);
|
|
61
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
208
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
62
209
|
return fitted;
|
|
63
210
|
}
|
|
64
211
|
fitted.shortcut = "";
|
|
65
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
212
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
66
213
|
return fitted;
|
|
67
214
|
}
|
|
68
215
|
fitted.task = "";
|
|
69
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
216
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
70
217
|
return fitted;
|
|
71
218
|
}
|
|
72
219
|
fitted.view = "";
|
|
73
|
-
if (headerLineLength(fitted) <= contentWidth) {
|
|
220
|
+
if (headerLineLength(fitted, separator) <= contentWidth) {
|
|
74
221
|
return fitted;
|
|
75
222
|
}
|
|
76
223
|
fitted.brand = compactHeaderText(fitted.brand, contentWidth);
|
|
77
224
|
return fitted;
|
|
78
225
|
}
|
|
79
|
-
function headerLineLength(parts) {
|
|
80
|
-
return displayWidth(headerPartList(parts).join(
|
|
226
|
+
function headerLineLength(parts, separator) {
|
|
227
|
+
return displayWidth(headerPartList(parts).join(separator));
|
|
81
228
|
}
|
|
82
229
|
function headerPartList(parts) {
|
|
83
230
|
return [parts.brand, parts.view, parts.task, parts.project, parts.shortcut].filter(Boolean);
|
|
84
231
|
}
|
|
232
|
+
function headerSeparator(terminalWidth) {
|
|
233
|
+
return terminalWidth >= 56 ? APP_HEADER_ROOMY_SEPARATOR : APP_HEADER_COMPACT_SEPARATOR;
|
|
234
|
+
}
|
|
85
235
|
function viewLabel(view) {
|
|
86
236
|
return shortViewLabel(view);
|
|
87
237
|
}
|
|
88
238
|
function shortcutHint(view) {
|
|
89
239
|
if (view === "native") {
|
|
90
|
-
return "^]
|
|
240
|
+
return "^] logs";
|
|
91
241
|
}
|
|
92
242
|
return "^C exit";
|
|
93
243
|
}
|
|
@@ -95,9 +245,24 @@ function shortViewLabel(view) {
|
|
|
95
245
|
if (view === "worker") {
|
|
96
246
|
return "logs";
|
|
97
247
|
}
|
|
248
|
+
if (view === "workers") {
|
|
249
|
+
return "workers";
|
|
250
|
+
}
|
|
251
|
+
if (view === "features") {
|
|
252
|
+
return "features";
|
|
253
|
+
}
|
|
254
|
+
if (view === "collaboration") {
|
|
255
|
+
return "timeline";
|
|
256
|
+
}
|
|
257
|
+
if (view === "sessions") {
|
|
258
|
+
return "sessions";
|
|
259
|
+
}
|
|
98
260
|
if (view === "native") {
|
|
99
261
|
return "native";
|
|
100
262
|
}
|
|
263
|
+
if (view === "router") {
|
|
264
|
+
return "routes";
|
|
265
|
+
}
|
|
101
266
|
return "chat";
|
|
102
267
|
}
|
|
103
268
|
function shortShortcutHint(view) {
|
|
@@ -119,7 +284,7 @@ function ultraCompactTaskId(taskId) {
|
|
|
119
284
|
}
|
|
120
285
|
function compactHeaderTaskId(taskId) {
|
|
121
286
|
if (!taskId) {
|
|
122
|
-
return "
|
|
287
|
+
return "";
|
|
123
288
|
}
|
|
124
289
|
const match = taskId.match(/^task-\d{8}-(.+)$/);
|
|
125
290
|
if (match) {
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
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 CollaborationTimelineView({ timeline, featureIndex, loading = false, error = null, selectedEventId = null, detailOpen = false, unresolvedOnly = false, scrollOffset = 0, height = 20, terminalWidth = process.stdout.columns || 120, onViewportChange }) {
|
|
7
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
8
|
+
const width = collaborationTimelineContentWidth(terminalWidth);
|
|
9
|
+
const layout = collaborationTimelineLayout(timeline, featureIndex, terminalWidth, {
|
|
10
|
+
loading,
|
|
11
|
+
error,
|
|
12
|
+
selectedEventId,
|
|
13
|
+
detailOpen,
|
|
14
|
+
unresolvedOnly
|
|
15
|
+
});
|
|
16
|
+
const header = layout.header.slice(0, viewportHeight);
|
|
17
|
+
const eventHeight = Math.max(0, viewportHeight - header.length);
|
|
18
|
+
const maxOffset = Math.max(0, layout.events.length - eventHeight);
|
|
19
|
+
const clampedOffset = Math.min(maxOffset, Math.max(0, Math.trunc(scrollOffset)));
|
|
20
|
+
const start = detailOpen
|
|
21
|
+
? clampedOffset
|
|
22
|
+
: Math.max(0, layout.events.length - eventHeight - clampedOffset);
|
|
23
|
+
const visibleEvents = eventHeight > 0 ? layout.events.slice(start, start + eventHeight) : [];
|
|
24
|
+
const lines = [...header, ...visibleEvents];
|
|
25
|
+
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
onViewportChange?.({ offset: clampedOffset, maxOffset });
|
|
28
|
+
}, [clampedOffset, maxOffset, onViewportChange]);
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(CollaborationTimelineRow, { line: line, width: width }, `${index}-${line.tone}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `collaboration-fill-${index}`)))] }));
|
|
30
|
+
}
|
|
31
|
+
export function collaborationTimelineDisplayLines(timeline, featureIndex, terminalWidth, options = {}) {
|
|
32
|
+
const layout = collaborationTimelineLayout(timeline, featureIndex, terminalWidth, options);
|
|
33
|
+
return [...layout.header, ...layout.events];
|
|
34
|
+
}
|
|
35
|
+
export function nextCollaborationFeatureIndex(current, delta, featureCount) {
|
|
36
|
+
const count = Math.max(0, Math.trunc(featureCount));
|
|
37
|
+
if (count === 0) {
|
|
38
|
+
return -1;
|
|
39
|
+
}
|
|
40
|
+
const slotCount = count + 1;
|
|
41
|
+
const currentSlot = Math.min(count, Math.max(0, Math.trunc(current) + 1));
|
|
42
|
+
const nextSlot = ((currentSlot + Math.trunc(delta)) % slotCount + slotCount) % slotCount;
|
|
43
|
+
return nextSlot - 1;
|
|
44
|
+
}
|
|
45
|
+
export function collaborationTimelineEvents(timeline, featureIndex, unresolvedOnly = false) {
|
|
46
|
+
const feature = selectedCollaborationFeature(timeline, featureIndex);
|
|
47
|
+
const scoped = collaborationEventsForFeature(timeline.events, feature);
|
|
48
|
+
if (!unresolvedOnly) {
|
|
49
|
+
return scoped;
|
|
50
|
+
}
|
|
51
|
+
const unresolvedFeatureIds = new Set(timeline.features.filter(collaborationFeatureIsUnresolved).map((item) => item.id));
|
|
52
|
+
if (feature && !unresolvedFeatureIds.has(feature.id)) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return scoped.filter((event) => (event.featureId
|
|
56
|
+
? unresolvedFeatureIds.has(event.featureId)
|
|
57
|
+
: unresolvedFeatureIds.size > 0 && event.type.startsWith("feature.wave_")));
|
|
58
|
+
}
|
|
59
|
+
export function moveCollaborationEventSelection(events, selectedEventId, delta) {
|
|
60
|
+
if (events.length === 0) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const latestIndex = events.length - 1;
|
|
64
|
+
const selectedIndex = selectedEventId
|
|
65
|
+
? events.findIndex((event) => event.id === selectedEventId)
|
|
66
|
+
: latestIndex;
|
|
67
|
+
const currentIndex = selectedIndex >= 0 ? selectedIndex : latestIndex;
|
|
68
|
+
const nextIndex = Math.min(latestIndex, Math.max(0, currentIndex + Math.trunc(delta)));
|
|
69
|
+
return nextIndex === latestIndex ? null : events[nextIndex]?.id ?? null;
|
|
70
|
+
}
|
|
71
|
+
export function collaborationSelectionScrollOffset(events, selectedEventId, terminalWidth) {
|
|
72
|
+
if (!selectedEventId) {
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
const selectedIndex = events.findIndex((event) => event.id === selectedEventId);
|
|
76
|
+
if (selectedIndex < 0) {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
const lineHeight = terminalWidth < 28 ? 1 : 2;
|
|
80
|
+
return Math.max(0, events.length - 1 - selectedIndex) * lineHeight;
|
|
81
|
+
}
|
|
82
|
+
function collaborationTimelineLayout(timeline, featureIndex, terminalWidth, state = {}) {
|
|
83
|
+
const width = collaborationTimelineContentWidth(terminalWidth);
|
|
84
|
+
const feature = selectedCollaborationFeature(timeline, featureIndex);
|
|
85
|
+
const unresolvedOnly = state.unresolvedOnly ?? false;
|
|
86
|
+
const events = timeline ? collaborationTimelineEvents(timeline, featureIndex, unresolvedOnly) : [];
|
|
87
|
+
const selectedEvent = selectedCollaborationEvent(events, state.selectedEventId ?? null);
|
|
88
|
+
if (state.detailOpen) {
|
|
89
|
+
return collaborationEventDetailLayout(selectedEvent, width);
|
|
90
|
+
}
|
|
91
|
+
const header = [
|
|
92
|
+
{
|
|
93
|
+
text: fitCollaborationCandidates(["Collaboration timeline", "Timeline", "Flow"], width),
|
|
94
|
+
tone: "heading"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
text: collaborationTimelineSummary(timeline, feature, events.length, width, unresolvedOnly),
|
|
98
|
+
tone: "muted"
|
|
99
|
+
}
|
|
100
|
+
];
|
|
101
|
+
if (state.loading && !timeline) {
|
|
102
|
+
return { header, events: [{ text: fitCollaborationText("loading collaboration evidence", width), tone: "muted" }] };
|
|
103
|
+
}
|
|
104
|
+
if (state.error) {
|
|
105
|
+
return { header, events: [{ text: fitCollaborationText(`error · ${safeCollaborationText(state.error)}`, width), tone: "danger" }] };
|
|
106
|
+
}
|
|
107
|
+
if (!timeline) {
|
|
108
|
+
return { header, events: [{ text: fitCollaborationText("no collaboration timeline", width), tone: "muted" }] };
|
|
109
|
+
}
|
|
110
|
+
if (events.length === 0) {
|
|
111
|
+
const emptyMessage = unresolvedOnly
|
|
112
|
+
? "no unresolved collaboration events in this scope"
|
|
113
|
+
: "no collaboration events in this scope";
|
|
114
|
+
return { header, events: [{ text: fitCollaborationText(emptyMessage, width), tone: "muted" }] };
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
header,
|
|
118
|
+
events: events.flatMap((event) => collaborationEventLines(event, width, terminalWidth, event.id === selectedEvent?.id))
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function selectedCollaborationFeature(timeline, featureIndex) {
|
|
122
|
+
if (!timeline || featureIndex < 0 || featureIndex >= timeline.features.length) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return timeline.features[Math.trunc(featureIndex)] ?? null;
|
|
126
|
+
}
|
|
127
|
+
function collaborationEventsForFeature(events, feature) {
|
|
128
|
+
if (!feature) {
|
|
129
|
+
return events;
|
|
130
|
+
}
|
|
131
|
+
return events.filter((event) => (event.featureId === feature.id || event.type.startsWith("feature.wave_")));
|
|
132
|
+
}
|
|
133
|
+
function collaborationFeatureIsUnresolved(feature) {
|
|
134
|
+
return feature.state !== "approved"
|
|
135
|
+
|| (typeof feature.unresolvedFindings === "number"
|
|
136
|
+
? feature.unresolvedFindings > 0
|
|
137
|
+
: feature.findings > feature.replies);
|
|
138
|
+
}
|
|
139
|
+
function selectedCollaborationEvent(events, selectedEventId) {
|
|
140
|
+
if (events.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return selectedEventId
|
|
144
|
+
? events.find((event) => event.id === selectedEventId) ?? events.at(-1) ?? null
|
|
145
|
+
: events.at(-1) ?? null;
|
|
146
|
+
}
|
|
147
|
+
function collaborationTimelineSummary(timeline, feature, eventCount, width, unresolvedOnly = false) {
|
|
148
|
+
if (!timeline) {
|
|
149
|
+
return fitCollaborationCandidates(["waiting for task evidence", "waiting"], width);
|
|
150
|
+
}
|
|
151
|
+
if (feature) {
|
|
152
|
+
const findings = `${feature.findings} ${feature.findings === 1 ? "finding" : "findings"}`;
|
|
153
|
+
const replies = `${feature.replies} ${feature.replies === 1 ? "reply" : "replies"}`;
|
|
154
|
+
const resolution = typeof feature.resolvedFindings === "number"
|
|
155
|
+
&& typeof feature.unresolvedFindings === "number"
|
|
156
|
+
? `${feature.resolvedFindings} fixed · ${feature.unresolvedFindings} open`
|
|
157
|
+
: `${findings} · ${replies}`;
|
|
158
|
+
return fitCollaborationCandidates([
|
|
159
|
+
...(unresolvedOnly ? [
|
|
160
|
+
`${safeCollaborationText(feature.title)} · ${humanizeState(feature.state)} · unresolved · ${eventCount} events`
|
|
161
|
+
] : []),
|
|
162
|
+
`${safeCollaborationText(feature.title)} · ${humanizeState(feature.state)} · ${eventCount} events · ${resolution}`,
|
|
163
|
+
`${safeCollaborationText(feature.title)} · ${humanizeState(feature.state)} · ${eventCount} events`,
|
|
164
|
+
`${safeCollaborationText(feature.title)} · ${humanizeState(feature.state)}`,
|
|
165
|
+
safeCollaborationText(feature.id)
|
|
166
|
+
], width);
|
|
167
|
+
}
|
|
168
|
+
const approved = timeline.features.filter((item) => item.state === "approved").length;
|
|
169
|
+
const revision = timeline.features.filter((item) => item.state === "revision_needed").length;
|
|
170
|
+
return fitCollaborationCandidates([
|
|
171
|
+
...(unresolvedOnly ? [
|
|
172
|
+
`all · ${timeline.features.length} features · unresolved · ${eventCount} events`,
|
|
173
|
+
`all · unresolved · ${eventCount} events`
|
|
174
|
+
] : []),
|
|
175
|
+
`all · ${timeline.features.length} features · approved ${approved} · revision ${revision} · ${eventCount} events`,
|
|
176
|
+
`all · ${timeline.features.length} features · ${eventCount} events`,
|
|
177
|
+
`all · ${timeline.features.length}f · ${eventCount}e`,
|
|
178
|
+
"all"
|
|
179
|
+
], width);
|
|
180
|
+
}
|
|
181
|
+
function collaborationEventLines(event, width, terminalWidth, selected) {
|
|
182
|
+
const role = collaborationRoleLabel(event.role);
|
|
183
|
+
const action = safeCollaborationText(event.action);
|
|
184
|
+
if (terminalWidth < 28) {
|
|
185
|
+
return [{
|
|
186
|
+
text: fitCollaborationCandidates([
|
|
187
|
+
`${selected ? "> " : " "}${event.time.slice(11, 16)} ${role.toLowerCase()} · ${action}`,
|
|
188
|
+
`${selected ? "> " : " "}${role.toLowerCase()} · ${action}`,
|
|
189
|
+
`${selected ? "> " : " "}${action}`
|
|
190
|
+
], width),
|
|
191
|
+
tone: collaborationEventTone(event)
|
|
192
|
+
}];
|
|
193
|
+
}
|
|
194
|
+
const scope = collaborationEventScope(event);
|
|
195
|
+
const turn = event.turnId ? `T${event.turnId}` : "";
|
|
196
|
+
const meta = [event.time.slice(11, 19), turn, role, scope].filter(Boolean).join(" · ");
|
|
197
|
+
const countParts = [
|
|
198
|
+
...(event.findings ? [`${event.findings} ${event.findings === 1 ? "finding" : "findings"}`] : []),
|
|
199
|
+
...(event.replies ? [`${event.replies} ${event.replies === 1 ? "reply" : "replies"}`] : []),
|
|
200
|
+
...(event.artifacts.length > 0 ? [`${event.artifacts.length} ${event.artifacts.length === 1 ? "artifact" : "artifacts"}`] : [])
|
|
201
|
+
];
|
|
202
|
+
const detail = [action, safeCollaborationText(event.message), ...countParts].filter(Boolean).join(" · ");
|
|
203
|
+
return [
|
|
204
|
+
{ text: fitCollaborationText(`${selected ? ">" : " "} ${meta}`, width), tone: collaborationRoleTone(event.role) },
|
|
205
|
+
{ text: fitCollaborationText(` ${detail}`, width), tone: collaborationEventTone(event) }
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
function collaborationEventDetailLayout(event, width) {
|
|
209
|
+
const header = [
|
|
210
|
+
{ text: fitCollaborationCandidates(["Collaboration event", "Event"], width), tone: "heading" },
|
|
211
|
+
{
|
|
212
|
+
text: event
|
|
213
|
+
? fitCollaborationText([
|
|
214
|
+
event.time.slice(11, 19),
|
|
215
|
+
collaborationRoleLabel(event.role),
|
|
216
|
+
collaborationEventScope(event)
|
|
217
|
+
].join(" · "), width)
|
|
218
|
+
: fitCollaborationText("no selected event", width),
|
|
219
|
+
tone: "muted"
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
if (!event) {
|
|
223
|
+
return { header, events: [{ text: "no event in this scope", tone: "muted" }] };
|
|
224
|
+
}
|
|
225
|
+
const lines = [
|
|
226
|
+
...collaborationDetailLines("action", event.action, width, collaborationEventTone(event)),
|
|
227
|
+
...collaborationDetailLines("type", event.type, width, "muted"),
|
|
228
|
+
...(event.featureId
|
|
229
|
+
? collaborationDetailLines("feature", `${event.featureTitle ?? event.featureId} · ${event.featureId}`, width, "muted")
|
|
230
|
+
: []),
|
|
231
|
+
...(event.turnId ? collaborationDetailLines("turn", event.turnId, width, "muted") : []),
|
|
232
|
+
...collaborationDetailLines("message", event.message || "(empty)", width, "text"),
|
|
233
|
+
...(typeof event.findings === "number"
|
|
234
|
+
? collaborationDetailLines("findings", String(event.findings), width, "muted")
|
|
235
|
+
: []),
|
|
236
|
+
...(typeof event.replies === "number"
|
|
237
|
+
? collaborationDetailLines("replies", String(event.replies), width, "muted")
|
|
238
|
+
: []),
|
|
239
|
+
...(typeof event.resolvedFindings === "number"
|
|
240
|
+
? collaborationDetailLines("fixed", String(event.resolvedFindings), width, "success")
|
|
241
|
+
: []),
|
|
242
|
+
...(typeof event.unresolvedFindings === "number"
|
|
243
|
+
? collaborationDetailLines("open", String(event.unresolvedFindings), width, event.unresolvedFindings > 0 ? "critic" : "muted")
|
|
244
|
+
: []),
|
|
245
|
+
...(event.artifactRefs.length > 0
|
|
246
|
+
? event.artifactRefs.flatMap((artifact) => collaborationDetailLines("artifact", `${artifact.label} · ${artifact.path}`, width, "actor"))
|
|
247
|
+
: [{ text: fitCollaborationText("artifacts · none", width), tone: "muted" }])
|
|
248
|
+
];
|
|
249
|
+
return { header, events: lines };
|
|
250
|
+
}
|
|
251
|
+
function collaborationDetailLines(label, value, width, tone) {
|
|
252
|
+
return wrapByDisplayWidth(`${label} · ${safeCollaborationText(value)}`, width)
|
|
253
|
+
.map((text) => ({ text, tone }));
|
|
254
|
+
}
|
|
255
|
+
function collaborationEventScope(event) {
|
|
256
|
+
if (event.featureTitle) {
|
|
257
|
+
return safeCollaborationText(event.featureTitle);
|
|
258
|
+
}
|
|
259
|
+
const wave = event.message.match(/\bWave\s+\d+(?:\/\d+)?/i)?.[0];
|
|
260
|
+
return wave ? wave.replace(/^wave/i, "Wave") : "Task";
|
|
261
|
+
}
|
|
262
|
+
function CollaborationTimelineRow({ line, width }) {
|
|
263
|
+
const fill = Math.max(0, width - displayWidth(line.text));
|
|
264
|
+
return (_jsxs(Text, { children: [_jsx(Text, { ...collaborationTimelineTheme(line.tone), children: line.text }), fill > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(fill) }) : null] }));
|
|
265
|
+
}
|
|
266
|
+
function collaborationTimelineTheme(tone) {
|
|
267
|
+
return {
|
|
268
|
+
backgroundColor: TUI_THEME.surface,
|
|
269
|
+
color: tone === "heading" || tone === "actor"
|
|
270
|
+
? TUI_THEME.accent
|
|
271
|
+
: tone === "critic" || tone === "warning"
|
|
272
|
+
? TUI_THEME.warning
|
|
273
|
+
: tone === "success"
|
|
274
|
+
? TUI_THEME.success
|
|
275
|
+
: tone === "danger"
|
|
276
|
+
? TUI_THEME.danger
|
|
277
|
+
: tone === "text"
|
|
278
|
+
? TUI_THEME.text
|
|
279
|
+
: TUI_THEME.muted,
|
|
280
|
+
...(tone === "heading" || tone === "danger" ? { bold: true } : {})
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function collaborationRoleTone(role) {
|
|
284
|
+
if (role === "actor") {
|
|
285
|
+
return "actor";
|
|
286
|
+
}
|
|
287
|
+
if (role === "critic") {
|
|
288
|
+
return "critic";
|
|
289
|
+
}
|
|
290
|
+
return "muted";
|
|
291
|
+
}
|
|
292
|
+
function collaborationEventTone(event) {
|
|
293
|
+
if (/revision|failed|cancelled/i.test(`${event.action} ${event.message}`)) {
|
|
294
|
+
return /failed|cancelled/i.test(`${event.action} ${event.message}`) ? "danger" : "warning";
|
|
295
|
+
}
|
|
296
|
+
if (/approved|verified|integrated|completed/i.test(event.action)) {
|
|
297
|
+
return "success";
|
|
298
|
+
}
|
|
299
|
+
return collaborationRoleTone(event.role);
|
|
300
|
+
}
|
|
301
|
+
function collaborationRoleLabel(role) {
|
|
302
|
+
return role === "actor" ? "Actor" : role === "critic" ? "Critic" : "Supervisor";
|
|
303
|
+
}
|
|
304
|
+
function humanizeState(state) {
|
|
305
|
+
if (state === "revision_needed") {
|
|
306
|
+
return "revision pending";
|
|
307
|
+
}
|
|
308
|
+
return state.replaceAll("_", " ");
|
|
309
|
+
}
|
|
310
|
+
function fitCollaborationCandidates(candidates, width) {
|
|
311
|
+
const safe = candidates.map(safeCollaborationText);
|
|
312
|
+
return safe.find((candidate) => displayWidth(candidate) <= width)
|
|
313
|
+
?? fitCollaborationText(safe.at(-1) ?? "", width);
|
|
314
|
+
}
|
|
315
|
+
function fitCollaborationText(text, width) {
|
|
316
|
+
return compactEndByDisplayWidth(safeCollaborationText(text), Math.max(1, width));
|
|
317
|
+
}
|
|
318
|
+
function safeCollaborationText(text) {
|
|
319
|
+
return text
|
|
320
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
321
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
322
|
+
.replace(/\s+/g, " ")
|
|
323
|
+
.trim();
|
|
324
|
+
}
|
|
325
|
+
function collaborationTimelineContentWidth(terminalWidth) {
|
|
326
|
+
return Math.max(1, terminalWidth - 2);
|
|
327
|
+
}
|