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,740 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { basename } from "node:path";
4
+ import { Box, Text, useApp, useInput, useStdin } from "ink";
5
+ import { readJson } from "../core/file-store.js";
6
+ import { WorkerStatusSchema } from "../domain/schemas.js";
7
+ import { formatSelectedWorkerStatus, formatStatusLine, formatWorkerRuntimeStatus } from "./status-line.js";
8
+ import { applyChatInputChunk } from "./chat-input.js";
9
+ import { AppShell } from "./AppShell.js";
10
+ import { InputBar } from "./InputBar.js";
11
+ import { applyNativeInputChunk } from "./native-input.js";
12
+ import { nextScrollOffset } from "./scrolling.js";
13
+ import { chooseSubmitTarget, nextSubmitMemoryState } from "./task-memory.js";
14
+ import { TerminalOutput } from "./TerminalOutput.js";
15
+ import { NativeTerminalScreen } from "./terminal-screen.js";
16
+ import { WorkerOutputView } from "./WorkerOutputView.js";
17
+ import { compactEndByDisplayWidth, displayWidth, wrapByDisplayWidth } from "./display-width.js";
18
+ import { isAttachShortcut, isExitShortcut, isLogsShortcut, mouseScrollDelta, scrollDelta } from "./keyboard.js";
19
+ import { createRawInputDecoder } from "./raw-input-decoder.js";
20
+ import { buildNativeAttachLaunch, startNativeAttachProcess } from "../workers/native-attach.js";
21
+ const NATIVE_PANEL_BACKGROUND = "ansi256(235)";
22
+ const NATIVE_PANEL_TITLE_BACKGROUND = "ansi256(24)";
23
+ export function App({ config, orchestrator, cwd, initialTaskId = null, prepareNativeAttach, startNativeAttach }) {
24
+ const [input, setInput] = useState("");
25
+ const [messages, setMessages] = useState([]);
26
+ const [busy, setBusy] = useState(false);
27
+ const [status, setStatus] = useState(initialTaskId ? { taskId: initialTaskId } : null);
28
+ const [view, setView] = useState("chat");
29
+ const [workers, setWorkers] = useState([]);
30
+ const [selectedWorkerIndex, setSelectedWorkerIndex] = useState(0);
31
+ const [activeTaskId, setActiveTaskId] = useState(initialTaskId);
32
+ const [activeMode, setActiveMode] = useState(initialTaskId ? "complex" : null);
33
+ const [attachError, setAttachError] = useState(null);
34
+ const [nativeInput, setNativeInput] = useState("");
35
+ const [workerScrollOffset, setWorkerScrollOffset] = useState(0);
36
+ const [workerMaxScrollOffset, setWorkerMaxScrollOffset] = useState(0);
37
+ const [nativeAttach, setNativeAttach] = useState(null);
38
+ const { exit } = useApp();
39
+ const { setRawMode, internal_eventEmitter: stdinEvents } = useStdin();
40
+ const nativeAttachRef = useRef(nativeAttach);
41
+ const nativeInputRef = useRef(nativeInput);
42
+ const inputRef = useRef(input);
43
+ const viewRef = useRef(view);
44
+ const busyRef = useRef(busy);
45
+ const workersRef = useRef(workers);
46
+ const selectedWorkerIndexRef = useRef(selectedWorkerIndex);
47
+ const workerMaxScrollOffsetRef = useRef(workerMaxScrollOffset);
48
+ const autoSelectedFailedWorkerRef = useRef(false);
49
+ const userSelectedWorkerRef = useRef(false);
50
+ const attachSelectedWorkerRef = useRef(attachSelectedWorker);
51
+ const submitRef = useRef(submit);
52
+ const exitRef = useRef(exit);
53
+ const rawInputDecoderRef = useRef(createRawInputDecoder());
54
+ const contentHeight = appContentHeight(process.stdout.rows || 30, Boolean(attachError));
55
+ const outputHeight = Math.max(1, contentHeight);
56
+ const terminalWidth = process.stdout.columns || 120;
57
+ const selectedWorkerStatus = formatSelectedWorkerStatus(status, selectedWorkerIndex);
58
+ const visibleWorkerStatus = view === "chat" ? "" : selectedWorkerStatus;
59
+ useEffect(() => {
60
+ inputRef.current = input;
61
+ }, [input]);
62
+ useEffect(() => {
63
+ viewRef.current = view;
64
+ }, [view]);
65
+ useEffect(() => {
66
+ busyRef.current = busy;
67
+ }, [busy]);
68
+ useEffect(() => {
69
+ exitRef.current = exit;
70
+ }, [exit]);
71
+ useEffect(() => {
72
+ workersRef.current = workers;
73
+ }, [workers]);
74
+ useEffect(() => {
75
+ selectedWorkerIndexRef.current = selectedWorkerIndex;
76
+ }, [selectedWorkerIndex]);
77
+ useEffect(() => {
78
+ workerMaxScrollOffsetRef.current = workerMaxScrollOffset;
79
+ }, [workerMaxScrollOffset]);
80
+ useEffect(() => {
81
+ attachSelectedWorkerRef.current = attachSelectedWorker;
82
+ submitRef.current = submit;
83
+ });
84
+ useEffect(() => {
85
+ nativeAttachRef.current = nativeAttach;
86
+ }, [nativeAttach]);
87
+ useEffect(() => {
88
+ nativeInputRef.current = nativeInput;
89
+ }, [nativeInput]);
90
+ useEffect(() => {
91
+ if (!initialTaskId) {
92
+ return;
93
+ }
94
+ const taskId = initialTaskId;
95
+ let active = true;
96
+ async function loadInitialWorkers() {
97
+ try {
98
+ const restored = await orchestrator.listTaskWorkers(taskId);
99
+ if (!active || restored.length === 0) {
100
+ return;
101
+ }
102
+ setWorkers(restored);
103
+ selectedWorkerIndexRef.current = 0;
104
+ autoSelectedFailedWorkerRef.current = false;
105
+ userSelectedWorkerRef.current = false;
106
+ setSelectedWorkerIndex(0);
107
+ }
108
+ catch (error) {
109
+ if (active) {
110
+ setAttachError(error instanceof Error ? error.message : String(error));
111
+ }
112
+ }
113
+ }
114
+ void loadInitialWorkers();
115
+ return () => {
116
+ active = false;
117
+ };
118
+ }, [initialTaskId, orchestrator]);
119
+ useEffect(() => {
120
+ if (workers.length === 0 || !status) {
121
+ return;
122
+ }
123
+ let active = true;
124
+ async function refreshWorkerStatuses() {
125
+ const updates = await Promise.all(workers.map(async (worker) => {
126
+ try {
127
+ const workerStatus = await readJson(worker.statusPath, WorkerStatusSchema);
128
+ return {
129
+ label: worker.label,
130
+ role: worker.role,
131
+ state: workerStatus.state,
132
+ status: formatWorkerRuntimeStatus(workerStatus)
133
+ };
134
+ }
135
+ catch {
136
+ return null;
137
+ }
138
+ }));
139
+ if (!active) {
140
+ return;
141
+ }
142
+ setStatus((current) => {
143
+ if (!current) {
144
+ return current;
145
+ }
146
+ const next = { ...current };
147
+ next.workers = updates
148
+ .filter((update) => update !== null)
149
+ .map((update) => ({
150
+ label: update.label,
151
+ status: update.status
152
+ }));
153
+ for (const update of updates) {
154
+ if (update) {
155
+ next[update.role] = update.status;
156
+ }
157
+ }
158
+ return next;
159
+ });
160
+ const failedWorkerIndex = updates.findIndex((update) => update?.state === "failed");
161
+ if (config.ui.autoOpenFailedWorker &&
162
+ failedWorkerIndex >= 0 &&
163
+ !autoSelectedFailedWorkerRef.current &&
164
+ !userSelectedWorkerRef.current) {
165
+ autoSelectedFailedWorkerRef.current = true;
166
+ selectedWorkerIndexRef.current = failedWorkerIndex;
167
+ setSelectedWorkerIndex(failedWorkerIndex);
168
+ setWorkerScrollOffset(0);
169
+ if (viewRef.current !== "native") {
170
+ setView("worker");
171
+ }
172
+ }
173
+ }
174
+ void refreshWorkerStatuses();
175
+ const interval = setInterval(() => {
176
+ void refreshWorkerStatuses();
177
+ }, 1000);
178
+ return () => {
179
+ active = false;
180
+ clearInterval(interval);
181
+ };
182
+ }, [config.ui.autoOpenFailedWorker, status?.taskId, workers]);
183
+ useEffect(() => {
184
+ setRawMode(true);
185
+ process.stdout.write("\x1b[?1000h\x1b[?1002h\x1b[?1006h");
186
+ const handleRawInput = (data) => {
187
+ const chunk = rawInputDecoderRef.current.write(Buffer.isBuffer(data) ? data : String(data ?? ""));
188
+ if (!chunk) {
189
+ return;
190
+ }
191
+ const currentView = viewRef.current;
192
+ if (currentView === "worker") {
193
+ if (isExitShortcut(chunk, {})) {
194
+ exitRef.current();
195
+ return;
196
+ }
197
+ if (chunk === "\x1b") {
198
+ userSelectedWorkerRef.current = true;
199
+ setView("chat");
200
+ return;
201
+ }
202
+ const delta = mouseScrollDelta(chunk, 3);
203
+ if (delta !== 0) {
204
+ setWorkerScrollOffset((current) => nextScrollOffset(current, delta, workerMaxScrollOffsetRef.current));
205
+ }
206
+ return;
207
+ }
208
+ if (currentView === "chat") {
209
+ if (chunk === "\x1b") {
210
+ userSelectedWorkerRef.current = true;
211
+ setView("chat");
212
+ return;
213
+ }
214
+ const wheelDelta = mouseScrollDelta(chunk, 3);
215
+ if (wheelDelta !== 0 && workersRef.current.length > 0) {
216
+ setView("worker");
217
+ setWorkerScrollOffset((current) => nextScrollOffset(current, wheelDelta, workerMaxScrollOffsetRef.current));
218
+ return;
219
+ }
220
+ if (chunk === "\u0017") {
221
+ setView("worker");
222
+ setWorkerScrollOffset(0);
223
+ return;
224
+ }
225
+ if (chunk === "\u000f") {
226
+ const worker = workersRef.current[selectedWorkerIndexRef.current];
227
+ if (!worker) {
228
+ setAttachError("No worker selected. Run a complex task or wait for workers to load before attaching.");
229
+ return;
230
+ }
231
+ void attachSelectedWorkerRef.current(worker);
232
+ return;
233
+ }
234
+ if (chunk === "\t" && workersRef.current.length > 0) {
235
+ const nextIndex = (selectedWorkerIndexRef.current + 1) % workersRef.current.length;
236
+ userSelectedWorkerRef.current = true;
237
+ selectedWorkerIndexRef.current = nextIndex;
238
+ setSelectedWorkerIndex(nextIndex);
239
+ setView("worker");
240
+ setWorkerScrollOffset(0);
241
+ return;
242
+ }
243
+ const update = applyChatInputChunk(inputRef.current, chunk);
244
+ inputRef.current = update.value;
245
+ if (update.exit) {
246
+ exitRef.current();
247
+ return;
248
+ }
249
+ if (!busyRef.current) {
250
+ setInput(update.value);
251
+ }
252
+ if (update.submit !== null) {
253
+ void submitRef.current(update.submit);
254
+ }
255
+ return;
256
+ }
257
+ const nativeWheelDelta = mouseScrollDelta(chunk, Math.max(3, Math.floor(outputHeight / 2)));
258
+ if (nativeWheelDelta !== 0) {
259
+ nativeAttachRef.current?.screen.scrollLines(-nativeWheelDelta);
260
+ setNativeAttach((current) => current
261
+ ? {
262
+ ...current,
263
+ snapshot: current.screen.snapshot()
264
+ }
265
+ : current);
266
+ return;
267
+ }
268
+ const update = applyNativeInputChunk(nativeInputRef.current, chunk, outputHeight - 1);
269
+ if (update.exit) {
270
+ nativeAttachRef.current?.process.kill();
271
+ nativeAttachRef.current = null;
272
+ nativeInputRef.current = "";
273
+ setNativeAttach(null);
274
+ setNativeInput("");
275
+ setView("worker");
276
+ return;
277
+ }
278
+ if (update.scrollDelta !== 0) {
279
+ nativeAttachRef.current?.screen.scrollLines(-update.scrollDelta);
280
+ setNativeAttach((current) => current
281
+ ? {
282
+ ...current,
283
+ snapshot: current.screen.snapshot()
284
+ }
285
+ : current);
286
+ return;
287
+ }
288
+ nativeInputRef.current = update.draft;
289
+ setNativeInput(update.draft);
290
+ if (update.outbound) {
291
+ nativeAttachRef.current?.process.write(update.outbound);
292
+ }
293
+ };
294
+ stdinEvents.on("input", handleRawInput);
295
+ return () => {
296
+ stdinEvents.removeListener("input", handleRawInput);
297
+ rawInputDecoderRef.current.end();
298
+ process.stdout.write("\x1b[?1006l\x1b[?1002l\x1b[?1000l");
299
+ setRawMode(false);
300
+ };
301
+ }, [outputHeight, setRawMode, stdinEvents]);
302
+ useInput((inputKey, key) => {
303
+ if (view === "worker") {
304
+ if (isExitShortcut(inputKey, key)) {
305
+ exitRef.current();
306
+ return;
307
+ }
308
+ const delta = scrollDelta(inputKey, key, outputHeight - 1);
309
+ if (delta !== 0) {
310
+ setWorkerScrollOffset((current) => nextScrollOffset(current, delta, workerMaxScrollOffset));
311
+ return;
312
+ }
313
+ }
314
+ if (key.escape) {
315
+ userSelectedWorkerRef.current = true;
316
+ setView("chat");
317
+ return;
318
+ }
319
+ if (isLogsShortcut(inputKey, key)) {
320
+ setView("worker");
321
+ setWorkerScrollOffset(0);
322
+ }
323
+ if ((key.tab || inputKey === "\t") && workers.length > 0) {
324
+ userSelectedWorkerRef.current = true;
325
+ setSelectedWorkerIndex((index) => (index + 1) % workers.length);
326
+ setView("worker");
327
+ setWorkerScrollOffset(0);
328
+ }
329
+ if (isAttachShortcut(inputKey, key)) {
330
+ const worker = workers[selectedWorkerIndex];
331
+ if (!worker) {
332
+ setAttachError("No worker selected. Run a complex task or wait for workers to load before attaching.");
333
+ return;
334
+ }
335
+ void attachSelectedWorker(worker);
336
+ }
337
+ }, { isActive: view !== "native" && view !== "chat" });
338
+ async function attachSelectedWorker(worker) {
339
+ setAttachError(null);
340
+ try {
341
+ const launch = await (prepareNativeAttach
342
+ ? prepareNativeAttach(worker)
343
+ : buildNativeAttachLaunch({
344
+ config,
345
+ worker
346
+ }));
347
+ const terminalCols = process.stdout.columns || 120;
348
+ const nativeTerminalCols = nativeAttachTerminalColumns(terminalCols);
349
+ const terminalRows = Math.max(1, contentHeight - 2);
350
+ const screen = new NativeTerminalScreen({
351
+ cols: nativeTerminalCols,
352
+ rows: terminalRows
353
+ });
354
+ const sizedLaunch = {
355
+ ...launch,
356
+ cols: nativeTerminalCols,
357
+ rows: terminalRows
358
+ };
359
+ const processRef = (startNativeAttach ?? startNativeAttachProcess)(sizedLaunch, {
360
+ onOutput: (chunk) => {
361
+ void screen.write(chunk).then(() => {
362
+ setNativeAttach((current) => current && current.screen === screen
363
+ ? {
364
+ ...current,
365
+ snapshot: screen.snapshot()
366
+ }
367
+ : current);
368
+ });
369
+ },
370
+ onClose: (code) => {
371
+ void screen.write(`\r\n${nativeAttachExitLine(code, nativeTerminalCols)}\r\n`).then(() => {
372
+ setNativeAttach((current) => current && current.screen === screen
373
+ ? {
374
+ ...current,
375
+ closedCode: code,
376
+ snapshot: screen.snapshot()
377
+ }
378
+ : current);
379
+ });
380
+ },
381
+ onError: (error) => {
382
+ setAttachError(error.message);
383
+ }
384
+ });
385
+ setNativeAttach({
386
+ launch: sizedLaunch,
387
+ process: processRef,
388
+ screen,
389
+ snapshot: screen.snapshot(),
390
+ closedCode: null
391
+ });
392
+ setNativeInput("");
393
+ setView("native");
394
+ }
395
+ catch (error) {
396
+ setAttachError(error instanceof Error ? error.message : String(error));
397
+ }
398
+ }
399
+ async function submit(value) {
400
+ const request = value.trim();
401
+ if (!request || busyRef.current) {
402
+ return;
403
+ }
404
+ inputRef.current = "";
405
+ setInput("");
406
+ busyRef.current = true;
407
+ setBusy(true);
408
+ setWorkers([]);
409
+ selectedWorkerIndexRef.current = 0;
410
+ autoSelectedFailedWorkerRef.current = false;
411
+ userSelectedWorkerRef.current = false;
412
+ setSelectedWorkerIndex(0);
413
+ setWorkerScrollOffset(0);
414
+ setWorkerMaxScrollOffset(0);
415
+ setMessages((current) => [...current, { from: "user", text: request }]);
416
+ try {
417
+ const callbacks = {
418
+ onStatus: setStatus,
419
+ onWorker: (worker) => {
420
+ setWorkers((current) => upsertWorker(current, worker));
421
+ }
422
+ };
423
+ const memory = {
424
+ activeTaskId,
425
+ activeMode
426
+ };
427
+ const followUpRoute = activeTaskId && activeMode === "complex"
428
+ ? await orchestrator.routeTaskFollowUp({
429
+ request,
430
+ cwd,
431
+ taskId: activeTaskId,
432
+ ...callbacks
433
+ })
434
+ : undefined;
435
+ const target = chooseSubmitTarget(memory, followUpRoute);
436
+ const result = target.kind === "task-turn"
437
+ ? await orchestrator.handleTaskTurn({
438
+ request,
439
+ cwd,
440
+ taskId: target.taskId,
441
+ ...callbacks
442
+ })
443
+ : target.kind === "task-question"
444
+ ? await orchestrator.answerTaskQuestion({
445
+ request,
446
+ cwd,
447
+ taskId: target.taskId,
448
+ ...callbacks
449
+ })
450
+ : await orchestrator.handleRequest({
451
+ request,
452
+ cwd,
453
+ ...callbacks
454
+ });
455
+ const nextMemory = nextSubmitMemoryState(memory, target, result);
456
+ setActiveMode(nextMemory.activeMode);
457
+ setActiveTaskId(nextMemory.activeTaskId);
458
+ setMessages((current) => [...current, { from: "system", text: result.summary }]);
459
+ }
460
+ catch (error) {
461
+ setMessages((current) => [
462
+ ...current,
463
+ { from: "system", text: error instanceof Error ? error.message : String(error) }
464
+ ]);
465
+ }
466
+ finally {
467
+ busyRef.current = false;
468
+ setBusy(false);
469
+ }
470
+ }
471
+ return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: `${formatStatusLine(status)}${visibleWorkerStatus ? ` | ${visibleWorkerStatus}` : ""}`, contentHeight: contentHeight, input: _jsx(InputBar, { mode: view, busy: busy, hasWorkers: workers.length > 0, nativeClosed: view === "native" && nativeAttach?.closedCode !== null, value: view === "native" ? "" : input, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach })) : view === "chat" ? (_jsx(ChatView, { messages: messages, cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth, viewportHeight: contentHeight })) : (_jsx(WorkerOutputView, { title: workerTitle(workers, selectedWorkerIndex), role: workers[selectedWorkerIndex]?.role, logPath: workers[selectedWorkerIndex]?.logPath ?? null, scrollOffset: workerScrollOffset, height: Math.max(1, outputHeight - 1), terminalWidth: terminalWidth, onViewportChange: ({ offset, maxOffset }) => {
472
+ setWorkerMaxScrollOffset(maxOffset);
473
+ if (offset !== workerScrollOffset) {
474
+ setWorkerScrollOffset(offset);
475
+ }
476
+ } })) }));
477
+ }
478
+ export function ChatView({ messages, cwd, activeTaskId, terminalWidth = process.stdout.columns || 120, viewportHeight }) {
479
+ const height = viewportHeight ? Math.max(1, viewportHeight) : undefined;
480
+ if (messages.length === 0) {
481
+ return (_jsx(Box, { flexDirection: "column", height: height, justifyContent: height ? "flex-end" : undefined, children: _jsx(ChatEmptyState, { cwd: cwd, activeTaskId: activeTaskId, terminalWidth: terminalWidth }) }));
482
+ }
483
+ const lines = chatMessageDisplayLines(messages, terminalWidth, height ?? 12);
484
+ return (_jsx(Box, { flexDirection: "column", height: height, justifyContent: height ? "flex-end" : undefined, children: lines.map((line, index) => (_jsx(Text, { color: line.from === "user" ? "cyan" : undefined, dimColor: line.from === "system" && !line.text.trim(), children: line.text || " " }, `${line.from}-${index}`))) }));
485
+ }
486
+ export function chatMessageDisplayLines(messages, terminalWidth, maxLines = 12) {
487
+ const contentWidth = Math.max(8, terminalWidth - 2);
488
+ const rendered = messages.flatMap((message) => chatSingleMessageDisplayLines(message, contentWidth));
489
+ return rendered.slice(-maxLines);
490
+ }
491
+ function chatSingleMessageDisplayLines(message, contentWidth) {
492
+ const rawLines = message.text.split(/\r?\n/);
493
+ const rendered = [];
494
+ rawLines.forEach((rawLine, rawIndex) => {
495
+ const isFirstRawLine = rawIndex === 0;
496
+ const firstPrefix = message.from === "user" && isFirstRawLine ? "> " : message.from === "user" ? " " : "";
497
+ const wrapWidth = Math.max(1, contentWidth - displayWidth(firstPrefix));
498
+ const wrapped = wrapByDisplayWidth(rawLine, wrapWidth);
499
+ wrapped.forEach((chunk, chunkIndex) => {
500
+ const continuation = !isFirstRawLine || chunkIndex > 0;
501
+ const prefix = message.from === "user"
502
+ ? continuation
503
+ ? " "
504
+ : "> "
505
+ : "";
506
+ const lineWidth = Math.max(1, contentWidth - displayWidth(prefix));
507
+ const fitted = displayWidth(chunk) > lineWidth
508
+ ? wrapByDisplayWidth(chunk, lineWidth)
509
+ : [chunk];
510
+ fitted.forEach((part, partIndex) => {
511
+ rendered.push({
512
+ from: message.from,
513
+ text: `${prefix}${part}`,
514
+ continuation: continuation || partIndex > 0
515
+ });
516
+ });
517
+ });
518
+ });
519
+ return rendered;
520
+ }
521
+ function ChatEmptyState({ cwd, activeTaskId, terminalWidth }) {
522
+ const contentWidth = Math.max(8, terminalWidth - 2);
523
+ return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: "green", bold: true, children: chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth) }) }));
524
+ }
525
+ function chatEmptyStateDisplayLine(cwd, activeTaskId, contentWidth) {
526
+ const project = compactChatWorkspace(cwd);
527
+ const task = activeTaskId ? compactChatTaskId(activeTaskId) : "";
528
+ if (contentWidth < 14) {
529
+ return compactChatText(project || task || "ready", contentWidth);
530
+ }
531
+ const roomyPrefix = task ? `ready · ${project} · ` : "ready · ";
532
+ const roomyValue = task || project;
533
+ const roomy = `${roomyPrefix}${compactChatText(roomyValue, Math.max(1, contentWidth - displayWidth(roomyPrefix)))}`;
534
+ if (displayWidth(roomy) <= contentWidth && !roomy.endsWith(" · ") && displayWidth(roomyValue) <= Math.max(0, contentWidth - displayWidth(roomyPrefix))) {
535
+ return roomy;
536
+ }
537
+ const readyPrefix = "ready · ";
538
+ const readyProject = `${readyPrefix}${compactChatText(project, Math.max(1, contentWidth - displayWidth(readyPrefix)))}`;
539
+ if (displayWidth(project) <= Math.max(0, contentWidth - displayWidth(readyPrefix)) && displayWidth(readyProject) <= contentWidth) {
540
+ return readyProject;
541
+ }
542
+ const readyValue = task || project;
543
+ const readyCompact = `${readyPrefix}${compactChatText(readyValue, Math.max(1, contentWidth - displayWidth(readyPrefix)))}`;
544
+ if (displayWidth(readyCompact) <= contentWidth && !readyCompact.endsWith(" · ")) {
545
+ return readyCompact;
546
+ }
547
+ const compactPrefix = task ? `${project} · ` : readyPrefix;
548
+ const compactValue = task || project;
549
+ const compact = `${compactPrefix}${compactChatText(compactValue, Math.max(1, contentWidth - displayWidth(compactPrefix)))}`;
550
+ if (displayWidth(compact) <= contentWidth && !compact.endsWith(" · ")) {
551
+ return compact;
552
+ }
553
+ return compactChatText(task || project || "ready", contentWidth);
554
+ }
555
+ function compactChatWorkspace(cwd) {
556
+ return basename(cwd) || cwd;
557
+ }
558
+ function compactChatTaskId(taskId) {
559
+ if (!taskId) {
560
+ return "none";
561
+ }
562
+ const match = taskId.match(/^task-\d{8}-(.+)$/);
563
+ if (match) {
564
+ return match[1] ?? taskId;
565
+ }
566
+ return taskId.startsWith("task-") ? taskId.slice("task-".length) : taskId;
567
+ }
568
+ function compactChatText(text, maxLength) {
569
+ if (maxLength <= 5) {
570
+ return takeChatTextStartByDisplayWidth(text, maxLength);
571
+ }
572
+ return compactEndByDisplayWidth(text, maxLength);
573
+ }
574
+ function takeChatTextStartByDisplayWidth(text, maxLength) {
575
+ if (maxLength <= 0) {
576
+ return "";
577
+ }
578
+ if (displayWidth(text) <= maxLength) {
579
+ return text;
580
+ }
581
+ let result = "";
582
+ let width = 0;
583
+ for (const char of Array.from(text)) {
584
+ const charWidth = displayWidth(char);
585
+ if (width + charWidth > maxLength) {
586
+ break;
587
+ }
588
+ result += char;
589
+ width += charWidth;
590
+ }
591
+ return result;
592
+ }
593
+ function upsertWorker(workers, worker) {
594
+ const existingIndex = workers.findIndex((item) => item.id === worker.id);
595
+ if (existingIndex >= 0) {
596
+ return workers.map((item, index) => (index === existingIndex ? worker : item));
597
+ }
598
+ return [...workers, worker];
599
+ }
600
+ function workerTitle(workers, selectedWorkerIndex) {
601
+ const worker = workers[selectedWorkerIndex];
602
+ if (!worker) {
603
+ return "Worker Output";
604
+ }
605
+ return `${worker.label} output (${selectedWorkerIndex + 1}/${workers.length})`;
606
+ }
607
+ export function appContentHeight(rows, hasError = false) {
608
+ const headerRows = 1;
609
+ const inputRows = 1;
610
+ const statusRows = 1;
611
+ const errorRows = hasError ? 1 : 0;
612
+ return Math.max(2, rows - headerRows - inputRows - statusRows - errorRows);
613
+ }
614
+ function NativeAttachView({ attach }) {
615
+ if (!attach) {
616
+ return _jsx(Text, { children: "Starting native attach..." });
617
+ }
618
+ const terminalWidth = process.stdout.columns || 120;
619
+ const panelWidth = nativeAttachPanelRailWidth(terminalWidth);
620
+ const scroll = attach.screen.scrollState();
621
+ const scrollLabel = nativeTerminalScrollDisplay(scroll.offset, scroll.maxOffset, terminalWidth);
622
+ const title = nativeAttachTitleDisplay(attach.launch.label, attach.launch.sessionId, attach.closedCode, panelWidth, scrollLabel);
623
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NativeAttachTitleRail, { title: title, width: panelWidth }), _jsx(TerminalOutput, { lines: attach.screen.styledSnapshotLines({ showCursor: true }) })] }));
624
+ }
625
+ function NativeAttachTitleRail({ title, width }) {
626
+ const titleText = ` ${title} `;
627
+ const renderWidth = typeof process.stdout.columns === "number"
628
+ ? width
629
+ : null;
630
+ const trailingWidth = renderWidth === null
631
+ ? 0
632
+ : Math.max(0, renderWidth - displayWidth(titleText));
633
+ return (_jsxs(Box, { children: [_jsx(Text, { backgroundColor: NATIVE_PANEL_TITLE_BACKGROUND, color: "white", bold: true, children: titleText }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: NATIVE_PANEL_BACKGROUND, children: " ".repeat(trailingWidth) }) : null] }));
634
+ }
635
+ function nativeAttachPanelRailWidth(terminalWidth) {
636
+ const renderWidth = typeof process.stdout.columns === "number"
637
+ ? Math.max(1, Math.min(terminalWidth, process.stdout.columns))
638
+ : terminalWidth;
639
+ return Math.max(1, renderWidth - 4);
640
+ }
641
+ export function nativeAttachTitleDisplay(label, sessionId, closedCode, terminalWidth = process.stdout.columns || 120, scrollLabel = null) {
642
+ const exit = closedCode === null ? "" : `exit:${closedCode}`;
643
+ const contentWidth = Math.max(1, terminalWidth - 2);
644
+ if (terminalWidth < 24) {
645
+ return tinyNativeAttachTitle(label, exit ? ` ${exit}` : "", contentWidth);
646
+ }
647
+ const compactLabel = compactNativeAttachLabel(label);
648
+ const roleLabel = compactNativeAttachRole(label);
649
+ if (exit) {
650
+ return firstNativeTitleThatFits(withNativeTitleSuffix([
651
+ `native ${compactLabel} · ${exit}`,
652
+ `native ${roleLabel} · ${exit}`,
653
+ `${roleLabel} ${exit}`
654
+ ], scrollLabel), contentWidth);
655
+ }
656
+ const prefix = `native ${compactLabel}`;
657
+ const separator = " · ";
658
+ const sessionBudget = Math.max(0, contentWidth - displayWidth(prefix) - displayWidth(separator));
659
+ const session = sessionBudget > 3 ? compactNativeSessionForTitle(sessionId, sessionBudget) : "";
660
+ return firstNativeTitleThatFits(withNativeTitleSuffix([
661
+ session ? `${prefix}${separator}${session}` : prefix,
662
+ prefix,
663
+ `native ${roleLabel}`,
664
+ roleLabel
665
+ ], scrollLabel), contentWidth);
666
+ }
667
+ export function nativeTerminalScrollDisplay(offset, maxOffset, width) {
668
+ if (maxOffset <= 0) {
669
+ return null;
670
+ }
671
+ if (offset <= 0) {
672
+ return "tail";
673
+ }
674
+ if (offset >= maxOffset) {
675
+ return "top";
676
+ }
677
+ return width < 32 ? `${offset}/${maxOffset}` : `back ${offset}/${maxOffset}`;
678
+ }
679
+ export function nativeAttachTerminalColumns(terminalWidth = process.stdout.columns || 120) {
680
+ return Math.max(1, terminalWidth - 2);
681
+ }
682
+ export function nativeAttachExitLine(code, nativeTerminalCols) {
683
+ const contentWidth = Math.max(1, nativeTerminalCols);
684
+ const candidates = [
685
+ `[process exited with code ${code}]`,
686
+ `[exit ${code}]`,
687
+ `exit:${code}`
688
+ ];
689
+ return firstNativeTitleThatFits(candidates, contentWidth);
690
+ }
691
+ function withNativeTitleSuffix(candidates, suffix) {
692
+ const cleanSuffix = suffix?.trim();
693
+ if (!cleanSuffix) {
694
+ return candidates;
695
+ }
696
+ return [
697
+ ...candidates.map((candidate) => `${candidate} · ${cleanSuffix}`),
698
+ ...candidates
699
+ ];
700
+ }
701
+ function firstNativeTitleThatFits(candidates, contentWidth) {
702
+ for (const candidate of candidates) {
703
+ if (displayWidth(candidate) <= contentWidth) {
704
+ return candidate;
705
+ }
706
+ }
707
+ return compactEndByDisplayWidth(candidates.at(-1) ?? "", contentWidth);
708
+ }
709
+ function tinyNativeAttachTitle(label, exit, contentWidth) {
710
+ const compactLabel = compactNativeAttachLabel(label);
711
+ const roleLabel = compactNativeAttachRole(label);
712
+ if (exit) {
713
+ const roleExit = `${roleLabel}${exit}`;
714
+ if (displayWidth(roleExit) <= contentWidth) {
715
+ return roleExit;
716
+ }
717
+ const exitOnly = exit.trim();
718
+ return displayWidth(exitOnly) <= contentWidth ? exitOnly : compactEndByDisplayWidth(exitOnly, contentWidth);
719
+ }
720
+ for (const candidate of [`native ${compactLabel}`, compactLabel, roleLabel]) {
721
+ if (displayWidth(candidate) <= contentWidth) {
722
+ return candidate;
723
+ }
724
+ }
725
+ return compactEndByDisplayWidth(roleLabel, contentWidth);
726
+ }
727
+ function compactNativeAttachLabel(label) {
728
+ const match = label.match(/^\s*([^(]+?)\s*\(([^)]+)\)\s*$/);
729
+ if (match) {
730
+ return `${(match[1] ?? "").trim().toLowerCase()}/${(match[2] ?? "").trim().toLowerCase()}`;
731
+ }
732
+ return label.trim().toLowerCase().replace(/\s+/g, "/");
733
+ }
734
+ function compactNativeAttachRole(label) {
735
+ const match = label.match(/^\s*([^(]+?)\s*(?:\([^)]+\))?\s*$/);
736
+ return (match?.[1] ?? label).trim().toLowerCase().replace(/\s+/g, "/");
737
+ }
738
+ function compactNativeSessionForTitle(sessionId, maxLength) {
739
+ return compactEndByDisplayWidth(sessionId, Math.min(maxLength, 16));
740
+ }