parallel-codex-tui 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parallel-codex/config.example.toml +90 -3
- package/README.md +240 -21
- package/dist/bootstrap.js +37 -17
- package/dist/cli-args.js +34 -3
- package/dist/cli-help.js +20 -0
- package/dist/cli-startup-recovery.js +70 -0
- package/dist/cli-workspace-picker.js +330 -0
- package/dist/cli-workspace-transition.js +33 -0
- package/dist/cli-workspace.js +7 -71
- package/dist/cli.js +221 -23
- package/dist/core/collaboration-timeline.js +261 -0
- package/dist/core/config-errors.js +14 -0
- package/dist/core/config.js +154 -24
- package/dist/core/file-store.js +119 -6
- package/dist/core/lease-finalization.js +22 -0
- package/dist/core/paths.js +7 -0
- package/dist/core/process-identity.js +48 -0
- package/dist/core/process-mutation-turn.js +128 -0
- package/dist/core/process-ownership.js +276 -0
- package/dist/core/process-tree.js +90 -0
- package/dist/core/router-audit.js +155 -0
- package/dist/core/router-redaction.js +31 -0
- package/dist/core/router.js +462 -35
- package/dist/core/session-index.js +188 -37
- package/dist/core/session-manager.js +1086 -40
- package/dist/core/task-state-machine.js +17 -0
- package/dist/core/workspace-commit-recovery.js +118 -0
- package/dist/core/workspace.js +19 -11
- package/dist/doctor.js +343 -23
- package/dist/domain/schemas.js +127 -6
- package/dist/orchestrator/collaboration-channel.js +255 -4
- package/dist/orchestrator/feature-plan.js +70 -0
- package/dist/orchestrator/judge-artifacts.js +236 -0
- package/dist/orchestrator/orchestrator.js +1749 -202
- package/dist/orchestrator/prompts.js +126 -2
- package/dist/orchestrator/supervisor-summary.js +56 -2
- package/dist/orchestrator/workspace-sandbox.js +911 -0
- package/dist/tui/App.js +2830 -153
- package/dist/tui/AppShell.js +188 -23
- package/dist/tui/CollaborationTimelineView.js +327 -0
- package/dist/tui/FeatureBoardView.js +227 -0
- package/dist/tui/InputBar.js +514 -57
- package/dist/tui/RouterDiagnosticsView.js +469 -0
- package/dist/tui/StatusBar.js +610 -57
- package/dist/tui/TaskSessionsView.js +207 -0
- package/dist/tui/TerminalOutput.js +53 -9
- package/dist/tui/WorkerOutputView.js +1403 -161
- package/dist/tui/WorkerOverviewView.js +250 -0
- package/dist/tui/chat-history.js +25 -0
- package/dist/tui/chat-input.js +67 -19
- package/dist/tui/chat-paste.js +76 -0
- package/dist/tui/display-width.js +41 -3
- package/dist/tui/incremental-text-file.js +101 -0
- package/dist/tui/keyboard.js +46 -0
- package/dist/tui/markdown-text.js +14 -0
- package/dist/tui/raw-input-decoder.js +3 -0
- package/dist/tui/scrolling.js +2 -1
- package/dist/tui/status-line.js +318 -11
- package/dist/tui/task-memory.js +13 -1
- package/dist/tui/task-result.js +105 -0
- package/dist/tui/terminal-screen.js +13 -1
- package/dist/tui/theme-contrast.js +144 -0
- package/dist/tui/theme-preview.js +109 -0
- package/dist/tui/theme.js +158 -0
- package/dist/version.js +1 -1
- package/dist/workers/capabilities.js +212 -0
- package/dist/workers/live-probe.js +176 -0
- package/dist/workers/mock-adapter.js +39 -6
- package/dist/workers/native-attach.js +78 -3
- package/dist/workers/process-adapter.js +570 -77
- package/dist/workers/registry.js +4 -2
- package/package.json +5 -2
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
|
|
4
|
+
import { effectiveWorkerWatchdog } from "./status-line.js";
|
|
5
|
+
import { TUI_THEME } from "./theme.js";
|
|
6
|
+
export function WorkerOverviewView({ workers, selectedIndex, nowMs = Date.now(), activityPolicies = {}, height = 20, terminalWidth = process.stdout.columns || 120 }) {
|
|
7
|
+
const viewportHeight = Math.max(1, height);
|
|
8
|
+
const width = workerOverviewContentWidth(terminalWidth);
|
|
9
|
+
const lines = workerOverviewDisplayLines(workers, selectedIndex, viewportHeight, terminalWidth, {
|
|
10
|
+
nowMs,
|
|
11
|
+
policies: activityPolicies
|
|
12
|
+
});
|
|
13
|
+
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
14
|
+
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(WorkerOverviewRow, { line: line, width: width }, `${line.workerIndex ?? line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `worker-overview-fill-${index}`)))] }));
|
|
15
|
+
}
|
|
16
|
+
export function workerOverviewDisplayLines(workers, selectedIndex, height, terminalWidth, activity = {
|
|
17
|
+
nowMs: Date.now(),
|
|
18
|
+
policies: {}
|
|
19
|
+
}) {
|
|
20
|
+
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
21
|
+
const width = workerOverviewContentWidth(terminalWidth);
|
|
22
|
+
const lines = [
|
|
23
|
+
{ text: fitWorkerOverviewCandidates(["Workers", "Work", "W"], width), tone: "heading" }
|
|
24
|
+
];
|
|
25
|
+
if (viewportHeight >= 3) {
|
|
26
|
+
lines.push({ text: workerOverviewSummary(workers, width), tone: "muted" });
|
|
27
|
+
}
|
|
28
|
+
const slots = Math.max(0, viewportHeight - lines.length);
|
|
29
|
+
if (workers.length === 0) {
|
|
30
|
+
if (slots > 0) {
|
|
31
|
+
lines.push({ text: fitWorkerOverviewText("No workers yet", width), tone: "muted" });
|
|
32
|
+
}
|
|
33
|
+
return lines;
|
|
34
|
+
}
|
|
35
|
+
if (slots === 0) {
|
|
36
|
+
return lines;
|
|
37
|
+
}
|
|
38
|
+
const selected = clampWorkerIndex(selectedIndex, workers.length);
|
|
39
|
+
const selectedWorker = workers[selected];
|
|
40
|
+
const selectedActivity = selectedWorker
|
|
41
|
+
? workerOverviewActivityLine(selectedWorker, activity.nowMs, activity.policies[selectedWorker.engine] ?? {})
|
|
42
|
+
: null;
|
|
43
|
+
const activityRows = selectedActivity && slots >= 2 ? 1 : 0;
|
|
44
|
+
const visibleCount = Math.min(Math.max(0, slots - activityRows), workers.length);
|
|
45
|
+
const start = workerOverviewWindowStart(selected, workers.length, visibleCount);
|
|
46
|
+
for (let index = start; index < start + visibleCount; index += 1) {
|
|
47
|
+
const worker = workers[index];
|
|
48
|
+
if (!worker) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const state = worker.runtimeStatus?.state ?? "waiting";
|
|
52
|
+
lines.push({
|
|
53
|
+
text: workerOverviewWorkerText(worker, index === selected, width),
|
|
54
|
+
tone: workerOverviewStateTone(state),
|
|
55
|
+
workerIndex: index
|
|
56
|
+
});
|
|
57
|
+
if (index === selected && selectedActivity && activityRows > 0) {
|
|
58
|
+
lines.push({
|
|
59
|
+
text: fitWorkerOverviewText(` ${selectedActivity.text}`, width),
|
|
60
|
+
tone: selectedActivity.tone,
|
|
61
|
+
workerIndex: index
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return lines;
|
|
66
|
+
}
|
|
67
|
+
export function workerOverviewActivityLine(worker, nowMs, policy) {
|
|
68
|
+
const status = worker.runtimeStatus;
|
|
69
|
+
if (!status || (status.state !== "starting" && status.state !== "running")) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const lastEventMs = Date.parse(status.last_event_at);
|
|
73
|
+
if (!Number.isFinite(lastEventMs)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const ageMs = Math.max(0, nowMs - lastEventMs);
|
|
77
|
+
if (status.phase === "process-stopping") {
|
|
78
|
+
return {
|
|
79
|
+
text: `activity · stopping process tree · ${formatWorkerActivityDuration(ageMs)} elapsed`,
|
|
80
|
+
tone: "warning"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const starting = status.state === "starting";
|
|
84
|
+
const limitMs = effectiveWorkerWatchdog(starting ? policy.firstOutputTimeoutMs : policy.idleTimeoutMs, policy.timeoutMs);
|
|
85
|
+
const activity = starting ? "started" : "output";
|
|
86
|
+
const timeout = starting ? "first output timeout" : "idle timeout";
|
|
87
|
+
if (!limitMs || limitMs <= 0) {
|
|
88
|
+
return {
|
|
89
|
+
text: `activity · ${activity} ${formatWorkerActivityDuration(ageMs)} ago · no ${timeout}`,
|
|
90
|
+
tone: "muted"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const remainingMs = limitMs - ageMs;
|
|
94
|
+
if (remainingMs <= 0) {
|
|
95
|
+
return {
|
|
96
|
+
text: [
|
|
97
|
+
"activity",
|
|
98
|
+
starting
|
|
99
|
+
? `waiting for first output ${formatWorkerActivityDuration(ageMs)}`
|
|
100
|
+
: `no output for ${formatWorkerActivityDuration(ageMs)}`,
|
|
101
|
+
`${timeout} overdue ${formatWorkerActivityDuration(Math.abs(remainingMs))}`
|
|
102
|
+
].join(" · "),
|
|
103
|
+
tone: "danger"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
text: `activity · ${activity} ${formatWorkerActivityDuration(ageMs)} ago · ${timeout} in ${formatWorkerActivityDuration(remainingMs)}`,
|
|
108
|
+
tone: remainingMs <= limitMs * 0.2 ? "warning" : "muted"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function moveWorkerSelection(current, delta, workerCount, wrap = false) {
|
|
112
|
+
if (workerCount <= 0) {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
const normalizedCurrent = clampWorkerIndex(current, workerCount);
|
|
116
|
+
const next = normalizedCurrent + Math.trunc(delta);
|
|
117
|
+
if (wrap) {
|
|
118
|
+
return ((next % workerCount) + workerCount) % workerCount;
|
|
119
|
+
}
|
|
120
|
+
return Math.min(workerCount - 1, Math.max(0, next));
|
|
121
|
+
}
|
|
122
|
+
function WorkerOverviewRow({ line, width }) {
|
|
123
|
+
const trailingWidth = Math.max(0, width - displayWidth(line.text));
|
|
124
|
+
const theme = workerOverviewLineTheme(line.tone);
|
|
125
|
+
return (_jsxs(Text, { children: [_jsx(Text, { ...theme, children: line.text }), trailingWidth > 0 ? _jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(trailingWidth) }) : null] }));
|
|
126
|
+
}
|
|
127
|
+
function workerOverviewSummary(workers, width) {
|
|
128
|
+
const counts = new Map();
|
|
129
|
+
let sessions = 0;
|
|
130
|
+
for (const worker of workers) {
|
|
131
|
+
const state = worker.runtimeStatus?.state ?? "waiting";
|
|
132
|
+
counts.set(state, (counts.get(state) ?? 0) + 1);
|
|
133
|
+
if (worker.runtimeStatus?.native_session_id) {
|
|
134
|
+
sessions += 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const stateParts = ["running", "starting", "done", "failed", "waiting", "cancelled", "idle"]
|
|
138
|
+
.flatMap((state) => {
|
|
139
|
+
const count = counts.get(state) ?? 0;
|
|
140
|
+
return count > 0 ? [`${count} ${state}`] : [];
|
|
141
|
+
});
|
|
142
|
+
const full = [
|
|
143
|
+
`${workers.length} ${workers.length === 1 ? "worker" : "workers"}`,
|
|
144
|
+
...stateParts,
|
|
145
|
+
...(sessions > 0 ? [`${sessions} ${sessions === 1 ? "session" : "sessions"}`] : [])
|
|
146
|
+
].join(" · ");
|
|
147
|
+
const active = (counts.get("running") ?? 0) + (counts.get("starting") ?? 0);
|
|
148
|
+
const failed = counts.get("failed") ?? 0;
|
|
149
|
+
const candidates = [
|
|
150
|
+
full,
|
|
151
|
+
`${workers.length} workers · ${active} active · ${failed} failed`,
|
|
152
|
+
`${workers.length} workers`,
|
|
153
|
+
`${workers.length}w`
|
|
154
|
+
];
|
|
155
|
+
return fitWorkerOverviewCandidates(candidates, width);
|
|
156
|
+
}
|
|
157
|
+
function workerOverviewWorkerText(worker, selected, width) {
|
|
158
|
+
const marker = selected ? "> " : " ";
|
|
159
|
+
const label = safeWorkerOverviewText(worker.label);
|
|
160
|
+
const state = worker.runtimeStatus?.state ?? "waiting";
|
|
161
|
+
const phase = humanizeWorkerOverviewPhase(worker.runtimeStatus?.phase ?? "status pending");
|
|
162
|
+
const session = worker.runtimeStatus?.native_session_id ? "session" : "";
|
|
163
|
+
const summary = safeWorkerOverviewText(worker.runtimeStatus?.summary ?? "");
|
|
164
|
+
const identity = `${worker.role}/${worker.engine}`;
|
|
165
|
+
return fitWorkerOverviewCandidates([
|
|
166
|
+
[marker + label, state, phase, session, summary].filter(Boolean).join(" · "),
|
|
167
|
+
[marker + label, state, phase, session].filter(Boolean).join(" · "),
|
|
168
|
+
[marker + label, state, session].filter(Boolean).join(" · "),
|
|
169
|
+
[marker + identity, state].join(" · "),
|
|
170
|
+
marker.trimEnd()
|
|
171
|
+
], width);
|
|
172
|
+
}
|
|
173
|
+
function workerOverviewWindowStart(selected, count, visibleCount) {
|
|
174
|
+
if (visibleCount <= 0 || count <= visibleCount) {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
return Math.min(count - visibleCount, Math.max(0, selected - Math.floor(visibleCount / 2)));
|
|
178
|
+
}
|
|
179
|
+
function clampWorkerIndex(index, count) {
|
|
180
|
+
return Math.min(Math.max(0, count - 1), Math.max(0, Math.trunc(index)));
|
|
181
|
+
}
|
|
182
|
+
function fitWorkerOverviewCandidates(candidates, width) {
|
|
183
|
+
const fitted = candidates.find((candidate) => displayWidth(candidate) <= width);
|
|
184
|
+
return fitted ?? fitWorkerOverviewText(candidates.at(-1) ?? "", width);
|
|
185
|
+
}
|
|
186
|
+
function fitWorkerOverviewText(text, width) {
|
|
187
|
+
return compactEndByDisplayWidth(text, Math.max(1, width));
|
|
188
|
+
}
|
|
189
|
+
function formatWorkerActivityDuration(durationMs) {
|
|
190
|
+
const totalSeconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
191
|
+
if (totalSeconds === 0) {
|
|
192
|
+
return "now";
|
|
193
|
+
}
|
|
194
|
+
if (totalSeconds < 60) {
|
|
195
|
+
return `${totalSeconds}s`;
|
|
196
|
+
}
|
|
197
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
198
|
+
const seconds = totalSeconds % 60;
|
|
199
|
+
if (minutes < 60) {
|
|
200
|
+
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
|
|
201
|
+
}
|
|
202
|
+
const hours = Math.floor(minutes / 60);
|
|
203
|
+
const remainingMinutes = minutes % 60;
|
|
204
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
205
|
+
}
|
|
206
|
+
function safeWorkerOverviewText(text) {
|
|
207
|
+
return text
|
|
208
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
209
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
210
|
+
.replace(/\s+/g, " ")
|
|
211
|
+
.trim();
|
|
212
|
+
}
|
|
213
|
+
function humanizeWorkerOverviewPhase(phase) {
|
|
214
|
+
return safeWorkerOverviewText(phase).replace(/[_-]+/g, " ");
|
|
215
|
+
}
|
|
216
|
+
function workerOverviewStateTone(state) {
|
|
217
|
+
if (state === "done") {
|
|
218
|
+
return "success";
|
|
219
|
+
}
|
|
220
|
+
if (state === "failed") {
|
|
221
|
+
return "danger";
|
|
222
|
+
}
|
|
223
|
+
if (state === "running" || state === "starting") {
|
|
224
|
+
return "active";
|
|
225
|
+
}
|
|
226
|
+
if (state === "waiting") {
|
|
227
|
+
return "warning";
|
|
228
|
+
}
|
|
229
|
+
return "muted";
|
|
230
|
+
}
|
|
231
|
+
function workerOverviewLineTheme(tone) {
|
|
232
|
+
return {
|
|
233
|
+
backgroundColor: TUI_THEME.surface,
|
|
234
|
+
color: tone === "heading" || tone === "active"
|
|
235
|
+
? TUI_THEME.accent
|
|
236
|
+
: tone === "success"
|
|
237
|
+
? TUI_THEME.success
|
|
238
|
+
: tone === "warning"
|
|
239
|
+
? TUI_THEME.warning
|
|
240
|
+
: tone === "danger"
|
|
241
|
+
? TUI_THEME.danger
|
|
242
|
+
: tone === "muted"
|
|
243
|
+
? TUI_THEME.muted
|
|
244
|
+
: TUI_THEME.text,
|
|
245
|
+
...(tone === "heading" || tone === "danger" ? { bold: true } : {})
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function workerOverviewContentWidth(terminalWidth) {
|
|
249
|
+
return Math.max(1, terminalWidth - 2);
|
|
250
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function chatRequestHistory(messages) {
|
|
2
|
+
return messages
|
|
3
|
+
.filter((message) => message.from === "user")
|
|
4
|
+
.map((message) => message.text);
|
|
5
|
+
}
|
|
6
|
+
export function navigateChatDraftHistory(history, current, state, delta) {
|
|
7
|
+
const nextOffset = clampOffset(state.offset + Math.trunc(delta), history.length);
|
|
8
|
+
if (nextOffset === state.offset) {
|
|
9
|
+
return { ...current, state };
|
|
10
|
+
}
|
|
11
|
+
const draft = state.offset === 0 && nextOffset > 0 ? current : state.draft;
|
|
12
|
+
const nextState = { offset: nextOffset, draft };
|
|
13
|
+
if (nextOffset === 0) {
|
|
14
|
+
return { ...draft, state: nextState };
|
|
15
|
+
}
|
|
16
|
+
const value = history[history.length - nextOffset] ?? "";
|
|
17
|
+
return {
|
|
18
|
+
value,
|
|
19
|
+
cursor: Array.from(value).length,
|
|
20
|
+
state: nextState
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function clampOffset(offset, historyLength) {
|
|
24
|
+
return Math.min(Math.max(0, offset), historyLength);
|
|
25
|
+
}
|
package/dist/tui/chat-input.js
CHANGED
|
@@ -1,37 +1,85 @@
|
|
|
1
|
-
export function applyChatInputChunk(currentValue, chunk) {
|
|
1
|
+
export function applyChatInputChunk(currentValue, chunk, currentCursor) {
|
|
2
|
+
let chars = Array.from(currentValue);
|
|
3
|
+
let cursor = clampCursor(currentCursor ?? chars.length, chars.length);
|
|
2
4
|
if (chunk === "\x03") {
|
|
3
5
|
return {
|
|
4
6
|
value: currentValue,
|
|
7
|
+
cursor,
|
|
5
8
|
submit: null,
|
|
6
9
|
exit: true
|
|
7
10
|
};
|
|
8
11
|
}
|
|
9
|
-
let value = currentValue;
|
|
10
12
|
let submit = null;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
for (const match of chunk.matchAll(/\x1b\[M[\s\S]{3}|\x1b\[[0-?]*[ -/]*[@-~]|\x1bO.|\x1b.|./gsu)) {
|
|
14
|
+
const token = match[0];
|
|
15
|
+
if (token === "\x1b[D" || token === "\x1bOD") {
|
|
16
|
+
cursor = Math.max(0, cursor - 1);
|
|
17
|
+
}
|
|
18
|
+
else if (token === "\x1b[C" || token === "\x1bOC") {
|
|
19
|
+
cursor = Math.min(chars.length, cursor + 1);
|
|
20
|
+
}
|
|
21
|
+
else if (token === "\x1b[H" || token === "\x1b[1~" || token === "\x1bOH" || token === "\x01") {
|
|
22
|
+
cursor = 0;
|
|
23
|
+
}
|
|
24
|
+
else if (token === "\x1b[F" || token === "\x1b[4~" || token === "\x1bOF" || token === "\x05") {
|
|
25
|
+
cursor = chars.length;
|
|
26
|
+
}
|
|
27
|
+
else if (token === "\x1b[3~") {
|
|
28
|
+
if (cursor < chars.length) {
|
|
29
|
+
chars.splice(cursor, 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else if (token.startsWith("\x1b")) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
else if (token === "\r" || token === "\n") {
|
|
36
|
+
submit = chars.join("");
|
|
37
|
+
chars = [];
|
|
38
|
+
cursor = 0;
|
|
21
39
|
}
|
|
22
|
-
else if (
|
|
23
|
-
|
|
40
|
+
else if (token === "\x7f" || token === "\b") {
|
|
41
|
+
if (cursor > 0) {
|
|
42
|
+
chars.splice(cursor - 1, 1);
|
|
43
|
+
cursor -= 1;
|
|
44
|
+
}
|
|
24
45
|
}
|
|
25
|
-
else if (
|
|
26
|
-
|
|
46
|
+
else if (token >= " ") {
|
|
47
|
+
chars.splice(cursor, 0, token);
|
|
48
|
+
cursor += 1;
|
|
27
49
|
}
|
|
28
50
|
}
|
|
29
51
|
return {
|
|
30
|
-
value,
|
|
52
|
+
value: chars.join(""),
|
|
53
|
+
cursor,
|
|
31
54
|
submit,
|
|
32
55
|
exit: false
|
|
33
56
|
};
|
|
34
57
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
58
|
+
export function insertChatPaste(currentValue, paste, currentCursor) {
|
|
59
|
+
const chars = Array.from(currentValue);
|
|
60
|
+
const cursor = clampCursor(currentCursor ?? chars.length, chars.length);
|
|
61
|
+
const pastedChars = Array.from(sanitizeChatPaste(paste));
|
|
62
|
+
chars.splice(cursor, 0, ...pastedChars);
|
|
63
|
+
return {
|
|
64
|
+
value: chars.join(""),
|
|
65
|
+
cursor: cursor + pastedChars.length,
|
|
66
|
+
submit: null,
|
|
67
|
+
exit: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function sanitizeChatPaste(value) {
|
|
71
|
+
const withoutTerminalControls = value
|
|
72
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
73
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
74
|
+
.replace(/\x1bO./g, "")
|
|
75
|
+
.replace(/\r\n?/g, "\n");
|
|
76
|
+
return Array.from(withoutTerminalControls)
|
|
77
|
+
.filter((char) => {
|
|
78
|
+
const codePoint = char.codePointAt(0) ?? 0;
|
|
79
|
+
return char === "\n" || char === "\t" || (codePoint >= 32 && codePoint !== 127 && !(codePoint >= 128 && codePoint < 160));
|
|
80
|
+
})
|
|
81
|
+
.join("");
|
|
82
|
+
}
|
|
83
|
+
function clampCursor(cursor, length) {
|
|
84
|
+
return Math.min(length, Math.max(0, Math.trunc(cursor)));
|
|
37
85
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
2
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
3
|
+
const MIN_BUFFERED_START_PREFIX = 4;
|
|
4
|
+
export function createChatPasteDecoder() {
|
|
5
|
+
let startPrefix = "";
|
|
6
|
+
let pasteText = null;
|
|
7
|
+
let endPrefix = "";
|
|
8
|
+
return {
|
|
9
|
+
write(chunk) {
|
|
10
|
+
const hadStartPrefix = startPrefix.length > 0;
|
|
11
|
+
let data = `${startPrefix}${chunk}`;
|
|
12
|
+
startPrefix = "";
|
|
13
|
+
const events = [];
|
|
14
|
+
let intercepted = hadStartPrefix || pasteText !== null;
|
|
15
|
+
while (data) {
|
|
16
|
+
if (pasteText !== null) {
|
|
17
|
+
data = `${endPrefix}${data}`;
|
|
18
|
+
endPrefix = "";
|
|
19
|
+
const endIndex = data.indexOf(BRACKETED_PASTE_END);
|
|
20
|
+
if (endIndex < 0) {
|
|
21
|
+
const keep = suffixPrefixLength(data, BRACKETED_PASTE_END);
|
|
22
|
+
pasteText += data.slice(0, data.length - keep);
|
|
23
|
+
endPrefix = data.slice(data.length - keep);
|
|
24
|
+
return { intercepted: true, events };
|
|
25
|
+
}
|
|
26
|
+
pasteText += data.slice(0, endIndex);
|
|
27
|
+
events.push({ kind: "paste", text: pasteText });
|
|
28
|
+
pasteText = null;
|
|
29
|
+
data = data.slice(endIndex + BRACKETED_PASTE_END.length);
|
|
30
|
+
intercepted = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const startIndex = data.indexOf(BRACKETED_PASTE_START);
|
|
34
|
+
if (startIndex >= 0) {
|
|
35
|
+
intercepted = true;
|
|
36
|
+
if (startIndex > 0) {
|
|
37
|
+
events.push({ kind: "input", text: data.slice(0, startIndex) });
|
|
38
|
+
}
|
|
39
|
+
pasteText = "";
|
|
40
|
+
data = data.slice(startIndex + BRACKETED_PASTE_START.length);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const keep = suffixPrefixLength(data, BRACKETED_PASTE_START);
|
|
44
|
+
if (keep >= MIN_BUFFERED_START_PREFIX) {
|
|
45
|
+
intercepted = true;
|
|
46
|
+
const input = data.slice(0, data.length - keep);
|
|
47
|
+
if (input) {
|
|
48
|
+
events.push({ kind: "input", text: input });
|
|
49
|
+
}
|
|
50
|
+
startPrefix = data.slice(data.length - keep);
|
|
51
|
+
return { intercepted, events };
|
|
52
|
+
}
|
|
53
|
+
if (intercepted) {
|
|
54
|
+
events.push({ kind: "input", text: data });
|
|
55
|
+
return { intercepted, events };
|
|
56
|
+
}
|
|
57
|
+
return { intercepted: false, events: [] };
|
|
58
|
+
}
|
|
59
|
+
return { intercepted, events };
|
|
60
|
+
},
|
|
61
|
+
reset() {
|
|
62
|
+
startPrefix = "";
|
|
63
|
+
pasteText = null;
|
|
64
|
+
endPrefix = "";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function suffixPrefixLength(value, marker) {
|
|
69
|
+
const limit = Math.min(value.length, marker.length - 1);
|
|
70
|
+
for (let length = limit; length > 0; length -= 1) {
|
|
71
|
+
if (value.endsWith(marker.slice(0, length))) {
|
|
72
|
+
return length;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
@@ -50,12 +50,50 @@ export function wrapByDisplayWidth(value, maxWidth) {
|
|
|
50
50
|
: breakWidth > Math.floor(maxWidth * 0.3)
|
|
51
51
|
? breakAt + 1
|
|
52
52
|
: splitAtWidth;
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const balanced = balanceClosingPunctuation(rest.slice(0, splitAt).trimEnd(), rest.slice(splitAt).trimStart(), maxWidth);
|
|
54
|
+
chunks.push(balanced.line);
|
|
55
|
+
rest = balanced.rest;
|
|
56
|
+
}
|
|
57
|
+
if (rest || chunks.length === 0) {
|
|
58
|
+
chunks.push(rest);
|
|
55
59
|
}
|
|
56
|
-
chunks.push(rest);
|
|
57
60
|
return chunks;
|
|
58
61
|
}
|
|
62
|
+
const lineStartForbiddenPunctuation = new Set(Array.from(",.!?;:)]},。!?;:、)》】」』〕〉》〗〙〛)]}’”»"));
|
|
63
|
+
function balanceClosingPunctuation(line, rest, maxWidth) {
|
|
64
|
+
let punctuation = "";
|
|
65
|
+
for (const char of Array.from(rest)) {
|
|
66
|
+
if (!lineStartForbiddenPunctuation.has(char)) {
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
punctuation += char;
|
|
70
|
+
}
|
|
71
|
+
if (!punctuation) {
|
|
72
|
+
return { line, rest };
|
|
73
|
+
}
|
|
74
|
+
const split = splitLastDisplayCharacter(line);
|
|
75
|
+
if (!split || !split.head || displayWidth(`${split.tail}${punctuation}`) > maxWidth) {
|
|
76
|
+
return { line, rest };
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
line: split.head,
|
|
80
|
+
rest: `${split.tail}${rest}`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function splitLastDisplayCharacter(value) {
|
|
84
|
+
const chars = Array.from(value);
|
|
85
|
+
if (chars.length < 2) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
let tailStart = chars.length - 1;
|
|
89
|
+
while (tailStart > 0 && isCombiningCodePoint(chars[tailStart]?.codePointAt(0) ?? 0)) {
|
|
90
|
+
tailStart -= 1;
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
head: chars.slice(0, tailStart).join("").trimEnd(),
|
|
94
|
+
tail: chars.slice(tailStart).join("")
|
|
95
|
+
};
|
|
96
|
+
}
|
|
59
97
|
function takeStartByDisplayWidth(value, maxWidth) {
|
|
60
98
|
let result = "";
|
|
61
99
|
let width = 0;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import { StringDecoder } from "node:string_decoder";
|
|
3
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
4
|
+
const CHECKPOINT_BYTES = 64;
|
|
5
|
+
export function createIncrementalTextFileReader(path) {
|
|
6
|
+
let offset = 0;
|
|
7
|
+
let text = "";
|
|
8
|
+
let checkpoint = Buffer.alloc(0);
|
|
9
|
+
let identity = null;
|
|
10
|
+
let initialized = false;
|
|
11
|
+
let decoder = new StringDecoder("utf8");
|
|
12
|
+
let queue = Promise.resolve();
|
|
13
|
+
const clear = () => {
|
|
14
|
+
offset = 0;
|
|
15
|
+
text = "";
|
|
16
|
+
checkpoint = Buffer.alloc(0);
|
|
17
|
+
identity = null;
|
|
18
|
+
initialized = false;
|
|
19
|
+
decoder = new StringDecoder("utf8");
|
|
20
|
+
};
|
|
21
|
+
const readOnce = async () => {
|
|
22
|
+
let file;
|
|
23
|
+
try {
|
|
24
|
+
file = await open(path, "r");
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code !== "ENOENT") {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const reset = initialized || offset > 0 || text.length > 0;
|
|
31
|
+
clear();
|
|
32
|
+
return { text, changed: reset, reset, bytesRead: 0, size: 0 };
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const stats = await file.stat();
|
|
36
|
+
const currentIdentity = `${stats.dev}:${stats.ino}`;
|
|
37
|
+
const wasInitialized = initialized;
|
|
38
|
+
let reset = initialized && (identity !== currentIdentity || stats.size < offset);
|
|
39
|
+
if (!reset && initialized && checkpoint.length > 0) {
|
|
40
|
+
const existing = await readExact(file, checkpoint.length, offset - checkpoint.length);
|
|
41
|
+
reset = existing.length !== checkpoint.length || !existing.equals(checkpoint);
|
|
42
|
+
}
|
|
43
|
+
if (reset) {
|
|
44
|
+
clear();
|
|
45
|
+
}
|
|
46
|
+
initialized = true;
|
|
47
|
+
identity = currentIdentity;
|
|
48
|
+
const targetSize = stats.size;
|
|
49
|
+
let bytesRead = 0;
|
|
50
|
+
while (offset < targetSize) {
|
|
51
|
+
const length = Math.min(READ_CHUNK_BYTES, targetSize - offset);
|
|
52
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
53
|
+
const result = await file.read(buffer, 0, length, offset);
|
|
54
|
+
if (result.bytesRead === 0) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const chunk = buffer.subarray(0, result.bytesRead);
|
|
58
|
+
text += decoder.write(chunk);
|
|
59
|
+
checkpoint = nextCheckpoint(checkpoint, chunk);
|
|
60
|
+
offset += result.bytesRead;
|
|
61
|
+
bytesRead += result.bytesRead;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
text,
|
|
65
|
+
changed: !wasInitialized || reset || bytesRead > 0,
|
|
66
|
+
reset,
|
|
67
|
+
bytesRead,
|
|
68
|
+
size: offset
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await file.close();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
read() {
|
|
77
|
+
const operation = queue.then(readOnce);
|
|
78
|
+
queue = operation.then(() => undefined, () => undefined);
|
|
79
|
+
return operation;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async function readExact(file, length, position) {
|
|
84
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
85
|
+
let offset = 0;
|
|
86
|
+
while (offset < length) {
|
|
87
|
+
const result = await file.read(buffer, offset, length - offset, position + offset);
|
|
88
|
+
if (result.bytesRead === 0) {
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
offset += result.bytesRead;
|
|
92
|
+
}
|
|
93
|
+
return buffer.subarray(0, offset);
|
|
94
|
+
}
|
|
95
|
+
function nextCheckpoint(previous, chunk) {
|
|
96
|
+
if (chunk.length >= CHECKPOINT_BYTES) {
|
|
97
|
+
return Buffer.from(chunk.subarray(chunk.length - CHECKPOINT_BYTES));
|
|
98
|
+
}
|
|
99
|
+
const combined = Buffer.concat([previous, chunk]);
|
|
100
|
+
return Buffer.from(combined.subarray(Math.max(0, combined.length - CHECKPOINT_BYTES)));
|
|
101
|
+
}
|
package/dist/tui/keyboard.js
CHANGED
|
@@ -7,6 +7,36 @@ export function isAttachShortcut(input, key) {
|
|
|
7
7
|
export function isExitShortcut(input, key) {
|
|
8
8
|
return (key.ctrl === true && input.toLowerCase() === "c") || input === "\u0003";
|
|
9
9
|
}
|
|
10
|
+
export function isNewTaskShortcut(input, key) {
|
|
11
|
+
return (key.ctrl === true && input.toLowerCase() === "n") || input === "\u000e";
|
|
12
|
+
}
|
|
13
|
+
export function isWorkspaceShortcut(input, key) {
|
|
14
|
+
return (key.ctrl === true && input.toLowerCase() === "p") || input === "\u0010";
|
|
15
|
+
}
|
|
16
|
+
export function isRouterDiagnosticsShortcut(input, key) {
|
|
17
|
+
return (key.ctrl === true && input.toLowerCase() === "g") || input === "\u0007";
|
|
18
|
+
}
|
|
19
|
+
export function isWorkerOverviewShortcut(input, key) {
|
|
20
|
+
return (key.ctrl === true && input.toLowerCase() === "b") || input === "\u0002";
|
|
21
|
+
}
|
|
22
|
+
export function isTaskSessionsShortcut(input, key) {
|
|
23
|
+
return (key.ctrl === true && input.toLowerCase() === "t") || input === "\u0014";
|
|
24
|
+
}
|
|
25
|
+
export function isTaskResultShortcut(input, key) {
|
|
26
|
+
return (key.ctrl === true && input.toLowerCase() === "d") || input === "\u0004";
|
|
27
|
+
}
|
|
28
|
+
export function isWorkerSearchShortcut(input, key) {
|
|
29
|
+
return (key.ctrl === true && input.toLowerCase() === "f") || input === "\u0006";
|
|
30
|
+
}
|
|
31
|
+
export function workerLogJumpKind(input) {
|
|
32
|
+
if (input === "e" || input === "E") {
|
|
33
|
+
return "error";
|
|
34
|
+
}
|
|
35
|
+
if (input === "d" || input === "D") {
|
|
36
|
+
return "diff";
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
10
40
|
export function scrollDelta(input, key, pageSize) {
|
|
11
41
|
if (key.pageUp || (key.ctrl === true && input.toLowerCase() === "u")) {
|
|
12
42
|
return pageSize;
|
|
@@ -16,6 +46,22 @@ export function scrollDelta(input, key, pageSize) {
|
|
|
16
46
|
}
|
|
17
47
|
return 0;
|
|
18
48
|
}
|
|
49
|
+
export function rawPageScrollDelta(input, pageSize) {
|
|
50
|
+
const size = Math.max(1, pageSize);
|
|
51
|
+
let delta = 0;
|
|
52
|
+
for (const match of input.matchAll(/\x1b\[(5|6)~/g)) {
|
|
53
|
+
delta += match[1] === "5" ? size : -size;
|
|
54
|
+
}
|
|
55
|
+
return delta;
|
|
56
|
+
}
|
|
57
|
+
export function rawHistoryDelta(input) {
|
|
58
|
+
let delta = 0;
|
|
59
|
+
for (const match of input.matchAll(/\x1b(?:O([AB])|\[[0-9;?]*([AB]))/g)) {
|
|
60
|
+
const direction = match[1] ?? match[2];
|
|
61
|
+
delta += direction === "A" ? 1 : -1;
|
|
62
|
+
}
|
|
63
|
+
return delta;
|
|
64
|
+
}
|
|
19
65
|
export function mouseScrollDelta(input, linesPerWheel = 3) {
|
|
20
66
|
let delta = 0;
|
|
21
67
|
for (const match of input.matchAll(/\x1b\[<(\d+);\d+;\d+[mM]/g)) {
|