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.
- package/.parallel-codex/config.example.toml +62 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/bootstrap.js +25 -0
- package/dist/cli-args.js +26 -0
- package/dist/cli.js +48 -0
- package/dist/core/config.js +304 -0
- package/dist/core/file-store.js +56 -0
- package/dist/core/paths.js +17 -0
- package/dist/core/router.js +151 -0
- package/dist/core/session-index.js +180 -0
- package/dist/core/session-manager.js +264 -0
- package/dist/doctor.js +121 -0
- package/dist/domain/schemas.js +78 -0
- package/dist/orchestrator/collaboration-channel.js +103 -0
- package/dist/orchestrator/orchestrator.js +545 -0
- package/dist/orchestrator/prompts.js +124 -0
- package/dist/orchestrator/supervisor-summary.js +38 -0
- package/dist/tui/App.js +740 -0
- package/dist/tui/AppShell.js +129 -0
- package/dist/tui/InputBar.js +141 -0
- package/dist/tui/StatusBar.js +288 -0
- package/dist/tui/TerminalOutput.js +22 -0
- package/dist/tui/WorkerOutputView.js +4015 -0
- package/dist/tui/chat-input.js +37 -0
- package/dist/tui/display-width.js +132 -0
- package/dist/tui/keyboard.js +38 -0
- package/dist/tui/native-input.js +120 -0
- package/dist/tui/raw-input-decoder.js +18 -0
- package/dist/tui/scrolling.js +25 -0
- package/dist/tui/status-line.js +90 -0
- package/dist/tui/task-memory.js +26 -0
- package/dist/tui/terminal-screen.js +168 -0
- package/dist/version.js +1 -0
- package/dist/workers/mock-adapter.js +62 -0
- package/dist/workers/native-attach.js +189 -0
- package/dist/workers/process-adapter.js +265 -0
- package/dist/workers/registry.js +32 -0
- package/dist/workers/types.js +1 -0
- package/package.json +53 -0
package/dist/tui/App.js
ADDED
|
@@ -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
|
+
}
|