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.
Files changed (72) hide show
  1. package/.parallel-codex/config.example.toml +90 -3
  2. package/README.md +240 -21
  3. package/dist/bootstrap.js +37 -17
  4. package/dist/cli-args.js +34 -3
  5. package/dist/cli-help.js +20 -0
  6. package/dist/cli-startup-recovery.js +70 -0
  7. package/dist/cli-workspace-picker.js +330 -0
  8. package/dist/cli-workspace-transition.js +33 -0
  9. package/dist/cli-workspace.js +7 -71
  10. package/dist/cli.js +221 -23
  11. package/dist/core/collaboration-timeline.js +261 -0
  12. package/dist/core/config-errors.js +14 -0
  13. package/dist/core/config.js +154 -24
  14. package/dist/core/file-store.js +119 -6
  15. package/dist/core/lease-finalization.js +22 -0
  16. package/dist/core/paths.js +7 -0
  17. package/dist/core/process-identity.js +48 -0
  18. package/dist/core/process-mutation-turn.js +128 -0
  19. package/dist/core/process-ownership.js +276 -0
  20. package/dist/core/process-tree.js +90 -0
  21. package/dist/core/router-audit.js +155 -0
  22. package/dist/core/router-redaction.js +31 -0
  23. package/dist/core/router.js +462 -35
  24. package/dist/core/session-index.js +188 -37
  25. package/dist/core/session-manager.js +1086 -40
  26. package/dist/core/task-state-machine.js +17 -0
  27. package/dist/core/workspace-commit-recovery.js +118 -0
  28. package/dist/core/workspace.js +19 -11
  29. package/dist/doctor.js +343 -23
  30. package/dist/domain/schemas.js +127 -6
  31. package/dist/orchestrator/collaboration-channel.js +255 -4
  32. package/dist/orchestrator/feature-plan.js +70 -0
  33. package/dist/orchestrator/judge-artifacts.js +236 -0
  34. package/dist/orchestrator/orchestrator.js +1749 -202
  35. package/dist/orchestrator/prompts.js +126 -2
  36. package/dist/orchestrator/supervisor-summary.js +56 -2
  37. package/dist/orchestrator/workspace-sandbox.js +911 -0
  38. package/dist/tui/App.js +2830 -153
  39. package/dist/tui/AppShell.js +188 -23
  40. package/dist/tui/CollaborationTimelineView.js +327 -0
  41. package/dist/tui/FeatureBoardView.js +227 -0
  42. package/dist/tui/InputBar.js +514 -57
  43. package/dist/tui/RouterDiagnosticsView.js +469 -0
  44. package/dist/tui/StatusBar.js +610 -57
  45. package/dist/tui/TaskSessionsView.js +207 -0
  46. package/dist/tui/TerminalOutput.js +53 -9
  47. package/dist/tui/WorkerOutputView.js +1403 -161
  48. package/dist/tui/WorkerOverviewView.js +250 -0
  49. package/dist/tui/chat-history.js +25 -0
  50. package/dist/tui/chat-input.js +67 -19
  51. package/dist/tui/chat-paste.js +76 -0
  52. package/dist/tui/display-width.js +41 -3
  53. package/dist/tui/incremental-text-file.js +101 -0
  54. package/dist/tui/keyboard.js +46 -0
  55. package/dist/tui/markdown-text.js +14 -0
  56. package/dist/tui/raw-input-decoder.js +3 -0
  57. package/dist/tui/scrolling.js +2 -1
  58. package/dist/tui/status-line.js +318 -11
  59. package/dist/tui/task-memory.js +13 -1
  60. package/dist/tui/task-result.js +105 -0
  61. package/dist/tui/terminal-screen.js +13 -1
  62. package/dist/tui/theme-contrast.js +144 -0
  63. package/dist/tui/theme-preview.js +109 -0
  64. package/dist/tui/theme.js +158 -0
  65. package/dist/version.js +1 -1
  66. package/dist/workers/capabilities.js +212 -0
  67. package/dist/workers/live-probe.js +176 -0
  68. package/dist/workers/mock-adapter.js +39 -6
  69. package/dist/workers/native-attach.js +78 -3
  70. package/dist/workers/process-adapter.js +570 -77
  71. package/dist/workers/registry.js +4 -2
  72. 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
+ }
@@ -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 visible = chunk
12
- .replaceAll("\x1b[200~", "")
13
- .replaceAll("\x1b[201~", "")
14
- .replace(/\x1b\[M[\s\S]{3}/g, "")
15
- .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
16
- .replace(/\x1bO./g, "");
17
- for (const char of visible) {
18
- if (char === "\r" || char === "\n") {
19
- submit = value;
20
- value = "";
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 (char === "\x7f" || char === "\b") {
23
- value = dropLastCodePoint(value);
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 (char >= " " && char !== "\x1b") {
26
- value += char;
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 dropLastCodePoint(value) {
36
- return [...value].slice(0, -1).join("");
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
- chunks.push(rest.slice(0, splitAt).trimEnd());
54
- rest = rest.slice(splitAt).trimStart();
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
+ }
@@ -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)) {