parallel-codex-tui 0.1.3 → 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 (78) hide show
  1. package/.parallel-codex/config.example.toml +136 -3
  2. package/README.md +299 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-preflight.js +18 -0
  7. package/dist/cli-startup-recovery.js +82 -0
  8. package/dist/cli-workspace-picker.js +330 -0
  9. package/dist/cli-workspace-transition.js +33 -0
  10. package/dist/cli-workspace.js +7 -71
  11. package/dist/cli.js +234 -24
  12. package/dist/core/collaboration-timeline.js +268 -0
  13. package/dist/core/config-errors.js +14 -0
  14. package/dist/core/config.js +297 -109
  15. package/dist/core/file-store.js +119 -6
  16. package/dist/core/lease-finalization.js +22 -0
  17. package/dist/core/paths.js +7 -0
  18. package/dist/core/process-identity.js +48 -0
  19. package/dist/core/process-mutation-turn.js +128 -0
  20. package/dist/core/process-ownership.js +276 -0
  21. package/dist/core/process-tree.js +90 -0
  22. package/dist/core/router-audit.js +155 -0
  23. package/dist/core/router-redaction.js +31 -0
  24. package/dist/core/router.js +462 -35
  25. package/dist/core/session-index.js +412 -88
  26. package/dist/core/session-manager.js +1110 -40
  27. package/dist/core/task-session-details.js +175 -0
  28. package/dist/core/task-state-machine.js +18 -0
  29. package/dist/core/workspace-commit-recovery.js +118 -0
  30. package/dist/core/workspace.js +19 -11
  31. package/dist/doctor.js +373 -34
  32. package/dist/domain/schemas.js +142 -7
  33. package/dist/orchestrator/collaboration-channel.js +289 -6
  34. package/dist/orchestrator/feature-plan.js +70 -0
  35. package/dist/orchestrator/final-acceptance.js +86 -0
  36. package/dist/orchestrator/judge-artifacts.js +236 -0
  37. package/dist/orchestrator/orchestrator.js +2086 -203
  38. package/dist/orchestrator/prompts.js +168 -5
  39. package/dist/orchestrator/supervisor-summary.js +56 -2
  40. package/dist/orchestrator/workspace-sandbox.js +927 -0
  41. package/dist/tui/App.js +3187 -161
  42. package/dist/tui/AppShell.js +196 -25
  43. package/dist/tui/CollaborationTimelineView.js +327 -0
  44. package/dist/tui/FeatureBoardView.js +232 -0
  45. package/dist/tui/InputBar.js +581 -57
  46. package/dist/tui/RouterDiagnosticsView.js +469 -0
  47. package/dist/tui/StatusBar.js +610 -57
  48. package/dist/tui/StatusDetailView.js +164 -0
  49. package/dist/tui/TaskSessionDetailView.js +222 -0
  50. package/dist/tui/TaskSessionsView.js +210 -0
  51. package/dist/tui/TerminalOutput.js +53 -9
  52. package/dist/tui/WorkerOutputView.js +1404 -161
  53. package/dist/tui/WorkerOverviewView.js +269 -0
  54. package/dist/tui/chat-history.js +25 -0
  55. package/dist/tui/chat-input.js +67 -19
  56. package/dist/tui/chat-paste.js +76 -0
  57. package/dist/tui/display-width.js +41 -3
  58. package/dist/tui/incremental-text-file.js +101 -0
  59. package/dist/tui/keyboard.js +49 -0
  60. package/dist/tui/markdown-text.js +14 -0
  61. package/dist/tui/raw-input-decoder.js +3 -0
  62. package/dist/tui/scrolling.js +2 -1
  63. package/dist/tui/status-line.js +360 -11
  64. package/dist/tui/task-memory.js +13 -1
  65. package/dist/tui/task-result.js +105 -0
  66. package/dist/tui/terminal-screen.js +13 -1
  67. package/dist/tui/theme-contrast.js +144 -0
  68. package/dist/tui/theme-preview.js +109 -0
  69. package/dist/tui/theme.js +158 -0
  70. package/dist/version.js +1 -1
  71. package/dist/workers/capabilities.js +213 -0
  72. package/dist/workers/live-probe.js +177 -0
  73. package/dist/workers/mock-adapter.js +73 -8
  74. package/dist/workers/native-attach.js +106 -16
  75. package/dist/workers/process-adapter.js +572 -77
  76. package/dist/workers/provider.js +26 -0
  77. package/dist/workers/registry.js +12 -20
  78. package/package.json +11 -2
@@ -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
- 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 }) {
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
- 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] }));
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 headerSegmentsDisplayWidth(segments) {
21
- return segments.reduce((sum, segment, index) => sum + displayWidth(segment.text) + (index > 0 ? 2 : 0), 0);
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,76 +178,101 @@ 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 = input.terminalWidth >= 24 && !(task === "none" && input.terminalWidth < 32);
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 : `task ${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: "" }) - 2);
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: "" }) - 2);
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) {
239
+ if (view === "status") {
240
+ return "^S back";
241
+ }
89
242
  if (view === "native") {
90
- return "^] detach";
243
+ return "^S status · ^] logs";
91
244
  }
92
- return "^C exit";
245
+ return "^S status · ^C exit";
93
246
  }
94
247
  function shortViewLabel(view) {
248
+ if (view === "status") {
249
+ return "status";
250
+ }
95
251
  if (view === "worker") {
96
252
  return "logs";
97
253
  }
254
+ if (view === "workers") {
255
+ return "workers";
256
+ }
257
+ if (view === "features") {
258
+ return "features";
259
+ }
260
+ if (view === "collaboration") {
261
+ return "timeline";
262
+ }
263
+ if (view === "sessions") {
264
+ return "sessions";
265
+ }
98
266
  if (view === "native") {
99
267
  return "native";
100
268
  }
269
+ if (view === "router") {
270
+ return "routes";
271
+ }
101
272
  return "chat";
102
273
  }
103
274
  function shortShortcutHint(view) {
104
- return view === "native" ? "^]" : "^C";
275
+ return view === "native" ? "^]" : view === "status" ? "^S" : "^C";
105
276
  }
106
277
  function compactHeaderProject(cwd, maxLength) {
107
278
  const project = basename(cwd) || cwd;
@@ -119,7 +290,7 @@ function ultraCompactTaskId(taskId) {
119
290
  }
120
291
  function compactHeaderTaskId(taskId) {
121
292
  if (!taskId) {
122
- return "none";
293
+ return "";
123
294
  }
124
295
  const match = taskId.match(/^task-\d{8}-(.+)$/);
125
296
  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
+ }