claude-overnight 1.25.47 → 1.25.48
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/dist/cli/cli.d.ts +3 -24
- package/dist/cli/cli.js +59 -92
- package/dist/core/_version.d.ts +1 -1
- package/dist/core/_version.js +1 -1
- package/dist/ui/input.d.ts +4 -4
- package/dist/ui/input.js +154 -166
- package/dist/ui/raw-input.d.ts +38 -0
- package/dist/ui/raw-input.js +241 -0
- package/package.json +1 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
package/dist/cli/cli.d.ts
CHANGED
|
@@ -9,32 +9,11 @@ import { isJWTAuthError } from "../core/auth.js";
|
|
|
9
9
|
export declare const isAuthError: typeof isJWTAuthError;
|
|
10
10
|
export { isJWTAuthError };
|
|
11
11
|
export declare function fetchModels(timeoutMs?: number): Promise<ModelInfo[]>;
|
|
12
|
-
export declare const PASTE_START = "\u001B[200~";
|
|
13
|
-
export declare const PASTE_END = "\u001B[201~";
|
|
14
12
|
export declare const PASTE_PLACEHOLDER_MAX = 80;
|
|
15
|
-
export type InputSegment = {
|
|
16
|
-
type: "text";
|
|
17
|
-
content: string;
|
|
18
|
-
} | {
|
|
19
|
-
type: "paste";
|
|
20
|
-
content: string;
|
|
21
|
-
};
|
|
22
|
-
/** Split a raw stdin chunk into typed and pasted segments. */
|
|
23
|
-
export declare function splitPaste(chunk: string): Array<{
|
|
24
|
-
type: "typed" | "paste";
|
|
25
|
-
text: string;
|
|
26
|
-
}>;
|
|
27
|
-
export declare function segmentsToString(segs: InputSegment[]): string;
|
|
28
|
-
export declare function renderSegments(segs: InputSegment[]): string;
|
|
29
|
-
export declare function appendCharToSegments(segs: InputSegment[], ch: string): void;
|
|
30
|
-
/** Appends a pasted block. Short single-line pastes inline as text; the rest become placeholders. */
|
|
31
|
-
export declare function appendPasteToSegments(segs: InputSegment[], text: string): void;
|
|
32
|
-
/** Backspace removes one char, or an entire paste block atomically. */
|
|
33
|
-
export declare function backspaceSegments(segs: InputSegment[]): void;
|
|
34
13
|
/**
|
|
35
|
-
* Read a line from the user with bracketed-paste awareness.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
14
|
+
* Read a line from the user with bracketed-paste awareness. Pasted multi-line
|
|
15
|
+
* text stays in the buffer as a single block -- only a typed Enter submits.
|
|
16
|
+
* Falls back to cooked readline when stdin isn't a TTY.
|
|
38
17
|
*/
|
|
39
18
|
export declare function ask(question: string): Promise<string>;
|
|
40
19
|
export declare function select<T>(label: string, items: {
|
package/dist/cli/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { resolve } from "path";
|
|
|
4
4
|
import { createInterface } from "readline";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
7
|
+
import { parseChunk, setBracketedPaste, deleteWordBackward } from "../ui/raw-input.js";
|
|
7
8
|
// ── CLI flag parsing ──
|
|
8
9
|
export function parseCliFlags(argv) {
|
|
9
10
|
const known = new Set(["concurrency", "model", "timeout", "budget", "usage-cap", "extra-usage-budget", "merge"]);
|
|
@@ -66,69 +67,34 @@ export async function fetchModels(timeoutMs = 10_000) {
|
|
|
66
67
|
return [];
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
|
-
// ──
|
|
70
|
+
// ── Interactive primitives ──
|
|
70
71
|
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
// Text entry goes through the shared raw-input parser in `../ui/raw-input.ts`,
|
|
73
|
+
// which enforces the single invariant that used to be duplicated (and buggy)
|
|
74
|
+
// here and in the Ink overlay:
|
|
75
|
+
// - Typed Enter = a stdin chunk that is exactly "\r", "\n", or "\r\n".
|
|
76
|
+
// - Anything else with embedded newlines is a paste, not a submit.
|
|
77
|
+
// Multi-line pastes render as a compact `[Pasted +N lines]` placeholder while
|
|
78
|
+
// editing — the full content is substituted on submit.
|
|
77
79
|
export const PASTE_PLACEHOLDER_MAX = 80;
|
|
78
|
-
|
|
79
|
-
export function splitPaste(chunk) {
|
|
80
|
-
const out = [];
|
|
81
|
-
let i = 0;
|
|
82
|
-
while (i < chunk.length) {
|
|
83
|
-
const start = chunk.indexOf(PASTE_START, i);
|
|
84
|
-
if (start === -1) {
|
|
85
|
-
out.push({ type: "typed", text: chunk.slice(i) });
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
if (start > i)
|
|
89
|
-
out.push({ type: "typed", text: chunk.slice(i, start) });
|
|
90
|
-
const bodyStart = start + PASTE_START.length;
|
|
91
|
-
const end = chunk.indexOf(PASTE_END, bodyStart);
|
|
92
|
-
if (end === -1) {
|
|
93
|
-
out.push({ type: "paste", text: chunk.slice(bodyStart) });
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
out.push({ type: "paste", text: chunk.slice(bodyStart, end) });
|
|
97
|
-
i = end + PASTE_END.length;
|
|
98
|
-
}
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
export function segmentsToString(segs) {
|
|
102
|
-
return segs.map((s) => s.content).join("");
|
|
103
|
-
}
|
|
104
|
-
export function renderSegments(segs) {
|
|
105
|
-
return segs.map((s) => {
|
|
106
|
-
if (s.type === "text")
|
|
107
|
-
return s.content;
|
|
108
|
-
const lines = s.content.split("\n").length;
|
|
109
|
-
return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
|
|
110
|
-
}).join("");
|
|
111
|
-
}
|
|
112
|
-
export function appendCharToSegments(segs, ch) {
|
|
80
|
+
function appendTypedChar(segs, ch) {
|
|
113
81
|
const last = segs[segs.length - 1];
|
|
114
82
|
if (last && last.type === "text")
|
|
115
83
|
last.content += ch;
|
|
116
84
|
else
|
|
117
85
|
segs.push({ type: "text", content: ch });
|
|
118
86
|
}
|
|
119
|
-
|
|
120
|
-
export function appendPasteToSegments(segs, text) {
|
|
87
|
+
function appendPaste(segs, text) {
|
|
121
88
|
if (!text)
|
|
122
89
|
return;
|
|
123
90
|
const norm = text.replace(/\r\n?/g, "\n");
|
|
124
91
|
if (!norm.includes("\n") && norm.length <= PASTE_PLACEHOLDER_MAX) {
|
|
125
|
-
|
|
92
|
+
appendTypedChar(segs, norm);
|
|
126
93
|
return;
|
|
127
94
|
}
|
|
128
95
|
segs.push({ type: "paste", content: norm });
|
|
129
96
|
}
|
|
130
|
-
|
|
131
|
-
export function backspaceSegments(segs) {
|
|
97
|
+
function backspaceSegs(segs) {
|
|
132
98
|
while (segs.length > 0) {
|
|
133
99
|
const last = segs[segs.length - 1];
|
|
134
100
|
if (last.type === "paste") {
|
|
@@ -143,14 +109,22 @@ export function backspaceSegments(segs) {
|
|
|
143
109
|
return;
|
|
144
110
|
}
|
|
145
111
|
}
|
|
112
|
+
function segsToString(segs) { return segs.map((s) => s.content).join(""); }
|
|
113
|
+
function renderSegs(segs) {
|
|
114
|
+
return segs.map((s) => {
|
|
115
|
+
if (s.type === "text")
|
|
116
|
+
return s.content;
|
|
117
|
+
const lines = s.content.split("\n").length;
|
|
118
|
+
return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
|
|
119
|
+
}).join("");
|
|
120
|
+
}
|
|
146
121
|
function stripAnsi(s) {
|
|
147
122
|
return s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
|
|
148
123
|
}
|
|
149
|
-
// ── Interactive primitives ──
|
|
150
124
|
/**
|
|
151
|
-
* Read a line from the user with bracketed-paste awareness.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
125
|
+
* Read a line from the user with bracketed-paste awareness. Pasted multi-line
|
|
126
|
+
* text stays in the buffer as a single block -- only a typed Enter submits.
|
|
127
|
+
* Falls back to cooked readline when stdin isn't a TTY.
|
|
154
128
|
*/
|
|
155
129
|
export function ask(question) {
|
|
156
130
|
const { stdin, stdout } = process;
|
|
@@ -163,28 +137,25 @@ export function ask(question) {
|
|
|
163
137
|
const tail = question.split("\n").pop() ?? "";
|
|
164
138
|
const tailVisibleLen = stripAnsi(tail).length;
|
|
165
139
|
let prevWrapRows = 0;
|
|
166
|
-
// Only rewrite the input line (and any wrapped continuation rows). The
|
|
167
|
-
// question header above is never touched, so redraws can't stack copies
|
|
168
|
-
// even if the initial write scrolled the viewport.
|
|
169
140
|
const redraw = () => {
|
|
170
141
|
const cols = stdout.columns || 80;
|
|
171
142
|
if (prevWrapRows > 0)
|
|
172
143
|
stdout.write(`\x1B[${prevWrapRows}A`);
|
|
173
144
|
stdout.write("\r\x1B[J");
|
|
174
|
-
const rendered =
|
|
145
|
+
const rendered = renderSegs(segs);
|
|
175
146
|
stdout.write(tail + rendered);
|
|
176
147
|
const visible = tailVisibleLen + stripAnsi(rendered).length;
|
|
177
148
|
prevWrapRows = visible > 0 ? Math.floor((visible - 1) / cols) : 0;
|
|
178
149
|
};
|
|
179
150
|
stdout.write(question);
|
|
180
|
-
stdout
|
|
151
|
+
setBracketedPaste(stdout, true);
|
|
181
152
|
try {
|
|
182
153
|
stdin.setRawMode(true);
|
|
183
154
|
}
|
|
184
155
|
catch { }
|
|
185
156
|
stdin.resume();
|
|
186
157
|
const cleanup = () => {
|
|
187
|
-
stdout
|
|
158
|
+
setBracketedPaste(stdout, false);
|
|
188
159
|
try {
|
|
189
160
|
stdin.setRawMode(false);
|
|
190
161
|
}
|
|
@@ -192,48 +163,44 @@ export function ask(question) {
|
|
|
192
163
|
stdin.removeListener("data", onData);
|
|
193
164
|
stdin.pause();
|
|
194
165
|
};
|
|
166
|
+
const submit = () => { stdout.write("\n"); cleanup(); resolve(segsToString(segs).trim()); };
|
|
195
167
|
const onData = (buf) => {
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
168
|
+
for (const ev of parseChunk(buf.toString())) {
|
|
169
|
+
switch (ev.type) {
|
|
170
|
+
case "char":
|
|
171
|
+
appendTypedChar(segs, ev.text);
|
|
172
|
+
break;
|
|
173
|
+
case "paste":
|
|
174
|
+
appendPaste(segs, ev.text);
|
|
175
|
+
break;
|
|
176
|
+
case "backspace":
|
|
177
|
+
backspaceSegs(segs);
|
|
178
|
+
break;
|
|
179
|
+
case "word-delete": {
|
|
180
|
+
const s = segsToString(segs);
|
|
181
|
+
const next = deleteWordBackward(s);
|
|
182
|
+
segs.length = 0;
|
|
183
|
+
if (next)
|
|
184
|
+
segs.push({ type: "text", content: next });
|
|
185
|
+
break;
|
|
210
186
|
}
|
|
211
|
-
|
|
187
|
+
case "clear-line":
|
|
188
|
+
segs.length = 0;
|
|
189
|
+
break;
|
|
190
|
+
case "submit":
|
|
191
|
+
submit();
|
|
192
|
+
return;
|
|
193
|
+
case "cancel":
|
|
194
|
+
submit();
|
|
195
|
+
return; // lone ESC = submit, preserves old behavior
|
|
196
|
+
case "interrupt":
|
|
212
197
|
cleanup();
|
|
213
198
|
stdout.write("\n");
|
|
214
199
|
process.exit(130);
|
|
215
|
-
|
|
216
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
217
|
-
backspaceSegments(segs);
|
|
218
|
-
redraw();
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
// ESC submits the current input (same as Enter)
|
|
222
|
-
if (ch === "\x1B") {
|
|
223
|
-
stdout.write("\n");
|
|
224
|
-
cleanup();
|
|
225
|
-
resolve(segmentsToString(segs).trim());
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const code = ch.charCodeAt(0);
|
|
229
|
-
if (code < 0x20)
|
|
230
|
-
continue; // control chars
|
|
231
|
-
if (code >= 0x7F && code < 0xA0)
|
|
232
|
-
continue; // DEL + C1 controls
|
|
233
|
-
appendCharToSegments(segs, ch);
|
|
200
|
+
// tab + nav: ignore during single-line prompts
|
|
234
201
|
}
|
|
235
|
-
redraw();
|
|
236
202
|
}
|
|
203
|
+
redraw();
|
|
237
204
|
};
|
|
238
205
|
stdin.on("data", onData);
|
|
239
206
|
});
|
package/dist/core/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.48";
|
package/dist/core/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.48";
|
package/dist/ui/input.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { UiStore, HostCallbacks } from "./store.js";
|
|
3
|
+
import { deleteWordBackward as rawDeleteWordBackward } from "./raw-input.js";
|
|
3
4
|
export declare const MAX_INPUT_LEN = 600;
|
|
4
5
|
export declare const CONTROL_CHAR_RE: RegExp;
|
|
5
|
-
/** Strip control characters from typed raw input
|
|
6
|
-
* and C1 bytes never end up in the user's buffer. Exported for tests. */
|
|
6
|
+
/** Strip control characters from typed raw input. Exported for tests. */
|
|
7
7
|
export declare function sanitizeTyped(raw: string): string;
|
|
8
8
|
/** Delete the previous word including any trailing whitespace, readline-style.
|
|
9
|
-
*
|
|
10
|
-
export declare
|
|
9
|
+
* Exported for tests. */
|
|
10
|
+
export declare const deleteWordBackward: typeof rawDeleteWordBackward;
|
|
11
11
|
interface Props {
|
|
12
12
|
store: UiStore;
|
|
13
13
|
callbacks: HostCallbacks;
|
package/dist/ui/input.js
CHANGED
|
@@ -1,171 +1,172 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Keyboard + input-overlay layer.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// here is the executable mirror of the canonical footer contract:
|
|
6
|
-
// ? ask · i steer · d debrief · p pause · s settings · f fallback ·
|
|
7
|
-
// r skip-rl · q quit · arrows+0-9 for agent detail nav
|
|
4
|
+
// Two input paths, chosen by `state.input.mode`:
|
|
8
5
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// - mode === "none" → Ink's `useInput` dispatches hotkeys (?, i, d, p, s, …)
|
|
7
|
+
// - mode !== "none" → a raw stdin tap using the shared parser in
|
|
8
|
+
// `./raw-input.ts`. Ink's useInput is disabled while
|
|
9
|
+
// the overlay is open so paste never gets fragmented
|
|
10
|
+
// into per-char keypress events (which used to fire
|
|
11
|
+
// `key.return` on any `\n` in the paste and submit).
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import { Text, Box, useInput } from "ink";
|
|
13
|
+
// The shared parser is the same one `cli.ts` `ask()` uses, so preflight
|
|
14
|
+
// prompts and in-run overlays behave identically: typed Enter = a stdin chunk
|
|
15
|
+
// that's exactly "\r"/"\n"/"\r\n"; anything else with embedded newlines is a
|
|
16
|
+
// paste, not a submit.
|
|
17
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
18
|
+
import { Text, Box, useInput, useStdin } from "ink";
|
|
20
19
|
import chalk from "chalk";
|
|
21
20
|
import { visibleLen, wrap } from "./primitives.js";
|
|
22
21
|
import { SETTINGS_FIELDS, SETTINGS_LABELS, NUMERIC_SETTINGS_FIELDS, applySettingEdit, readSettingValue, } from "./settings.js";
|
|
22
|
+
import { parseChunk, setBracketedPaste, deleteWordBackward as rawDeleteWordBackward } from "./raw-input.js";
|
|
23
23
|
export const MAX_INPUT_LEN = 600;
|
|
24
|
-
//
|
|
25
|
-
// touching the buffer. Matches: ASCII C0 controls (0x00-0x1F), DEL (0x7F), C1
|
|
26
|
-
// controls (0x80-0x9F), and lone ESC (already handled by `key.escape`).
|
|
24
|
+
// Kept for backwards compatibility with existing tests. Matches C0, DEL, C1.
|
|
27
25
|
export const CONTROL_CHAR_RE = /[\x00-\x1f\x7f-\x9f]/g;
|
|
28
|
-
/** Strip control characters from typed raw input
|
|
29
|
-
* and C1 bytes never end up in the user's buffer. Exported for tests. */
|
|
26
|
+
/** Strip control characters from typed raw input. Exported for tests. */
|
|
30
27
|
export function sanitizeTyped(raw) {
|
|
31
28
|
return raw.replace(CONTROL_CHAR_RE, "");
|
|
32
29
|
}
|
|
33
30
|
/** Delete the previous word including any trailing whitespace, readline-style.
|
|
34
|
-
*
|
|
35
|
-
export
|
|
36
|
-
const trimmed = s.replace(/\s+$/, "");
|
|
37
|
-
const idx = trimmed.search(/\S+$/);
|
|
38
|
-
return idx < 0 ? "" : trimmed.slice(0, idx);
|
|
39
|
-
}
|
|
31
|
+
* Exported for tests. */
|
|
32
|
+
export const deleteWordBackward = rawDeleteWordBackward;
|
|
40
33
|
export function InputLayer({ store, callbacks, onToast }) {
|
|
41
34
|
const [buffer, setBuffer] = useState("");
|
|
42
35
|
const [settingsField, setSettingsField] = useState(0);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
const state = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
37
|
+
const mode = state.input.mode;
|
|
38
|
+
const textEntry = mode !== "none";
|
|
39
|
+
// ── Text-entry path: raw stdin tap via the shared parser ──
|
|
40
|
+
const { stdin, setRawMode, isRawModeSupported } = useStdin();
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!textEntry || !stdin || !isRawModeSupported)
|
|
43
|
+
return;
|
|
44
|
+
setRawMode(true);
|
|
45
|
+
setBracketedPaste(process.stdout, true);
|
|
46
|
+
// Snapshot the overlay-relevant state locally; callbacks always pull the
|
|
47
|
+
// latest live state via `store.get()` on each event.
|
|
48
|
+
const onData = (buf) => {
|
|
49
|
+
const cur = store.get();
|
|
50
|
+
const m = cur.input.mode;
|
|
51
|
+
if (m === "none")
|
|
56
52
|
return;
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
const swarm = cur.swarm;
|
|
54
|
+
const lc = cur.liveConfig;
|
|
55
|
+
const closeOverlay = () => {
|
|
59
56
|
setBuffer("");
|
|
60
57
|
setSettingsField(0);
|
|
61
58
|
store.patch({ input: { mode: "none", buffer: "", settingsField: 0 } });
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const text = buffer.trim();
|
|
66
|
-
if (mode === "steer" && text)
|
|
67
|
-
callbacks.onSteer(text);
|
|
68
|
-
else if (mode === "ask" && text)
|
|
69
|
-
callbacks.onAsk(text);
|
|
70
|
-
else if (mode === "settings") {
|
|
71
|
-
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
72
|
-
if (lc)
|
|
73
|
-
applySettingEdit(field, text, lc, swarm);
|
|
74
|
-
callbacks.settingsTick();
|
|
75
|
-
const next = settingsField + 1;
|
|
76
|
-
setBuffer("");
|
|
77
|
-
if (next >= SETTINGS_FIELDS.length) {
|
|
78
|
-
setSettingsField(0);
|
|
79
|
-
store.patch({ input: { mode: "none", buffer: "", settingsField: 0 } });
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
setSettingsField(next);
|
|
83
|
-
store.patch({ input: { mode: "settings", buffer: "", settingsField: next } });
|
|
84
|
-
}
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
59
|
+
};
|
|
60
|
+
const advanceSettings = () => {
|
|
61
|
+
const next = settingsField + 1;
|
|
87
62
|
setBuffer("");
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
// Word-delete: option/alt + backspace — expected on macOS.
|
|
93
|
-
if ((key.meta || key.ctrl) && (key.backspace || key.delete)) {
|
|
94
|
-
const next = deleteWordBackward(buffer);
|
|
95
|
-
setBuffer(next);
|
|
96
|
-
store.patch({ input: { ...state.input, buffer: next } });
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
// Swallow modifier combos so they can't leak as stray letters.
|
|
100
|
-
// (cmd+→ on macOS Terminal = \x05 = ctrl+e → input handler sees raw='e';
|
|
101
|
-
// without this guard we used to append 'e'.)
|
|
102
|
-
if (key.ctrl || key.meta) {
|
|
103
|
-
if (mode !== "settings" && key.ctrl && raw === "u") {
|
|
104
|
-
// ctrl+U: clear the whole line — standard readline behavior.
|
|
105
|
-
setBuffer("");
|
|
106
|
-
store.patch({ input: { ...state.input, buffer: "" } });
|
|
107
|
-
return;
|
|
63
|
+
if (next >= SETTINGS_FIELDS.length) {
|
|
64
|
+
setSettingsField(0);
|
|
65
|
+
store.patch({ input: { mode: "none", buffer: "", settingsField: 0 } });
|
|
108
66
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
store.patch({ input: { ...state.input, buffer: next } });
|
|
113
|
-
return;
|
|
67
|
+
else {
|
|
68
|
+
setSettingsField(next);
|
|
69
|
+
store.patch({ input: { mode: "settings", buffer: "", settingsField: next } });
|
|
114
70
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
71
|
+
};
|
|
72
|
+
for (const ev of parseChunk(buf.toString())) {
|
|
73
|
+
switch (ev.type) {
|
|
74
|
+
case "cancel":
|
|
75
|
+
closeOverlay();
|
|
76
|
+
return;
|
|
77
|
+
case "interrupt":
|
|
78
|
+
// Treat ^C inside overlay as cancel, not process exit.
|
|
79
|
+
closeOverlay();
|
|
80
|
+
return;
|
|
81
|
+
case "submit": {
|
|
82
|
+
const text = buffer.trim();
|
|
83
|
+
if (m === "steer" && text)
|
|
84
|
+
callbacks.onSteer(text);
|
|
85
|
+
else if (m === "ask" && text)
|
|
86
|
+
callbacks.onAsk(text);
|
|
87
|
+
else if (m === "settings") {
|
|
88
|
+
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
89
|
+
if (lc)
|
|
90
|
+
applySettingEdit(field, text, lc, swarm);
|
|
91
|
+
callbacks.settingsTick();
|
|
92
|
+
advanceSettings();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
closeOverlay();
|
|
96
|
+
return;
|
|
132
97
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
98
|
+
case "tab":
|
|
99
|
+
if (m === "settings") {
|
|
100
|
+
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
101
|
+
if (field === "pause" && swarm && lc) {
|
|
102
|
+
const next = !swarm.paused;
|
|
103
|
+
swarm.setPaused(next);
|
|
104
|
+
lc.paused = next;
|
|
105
|
+
lc.dirty = true;
|
|
106
|
+
callbacks.settingsTick();
|
|
107
|
+
}
|
|
108
|
+
advanceSettings();
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
111
|
+
case "backspace":
|
|
112
|
+
setBuffer((prev) => {
|
|
113
|
+
const next = prev.slice(0, -1);
|
|
114
|
+
store.patch({ input: { ...store.get().input, buffer: next } });
|
|
115
|
+
return next;
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
case "word-delete":
|
|
119
|
+
setBuffer((prev) => {
|
|
120
|
+
const next = rawDeleteWordBackward(prev);
|
|
121
|
+
store.patch({ input: { ...store.get().input, buffer: next } });
|
|
122
|
+
return next;
|
|
123
|
+
});
|
|
124
|
+
break;
|
|
125
|
+
case "clear-line":
|
|
126
|
+
if (m !== "settings") {
|
|
127
|
+
setBuffer("");
|
|
128
|
+
store.patch({ input: { ...store.get().input, buffer: "" } });
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
case "nav":
|
|
132
|
+
// Navigation keys are a no-op inside the overlay. Used to leak
|
|
133
|
+
// as stray letters because cmd+→ on macOS sends ctrl+e.
|
|
134
|
+
break;
|
|
135
|
+
case "char":
|
|
136
|
+
case "paste": {
|
|
137
|
+
let text = ev.type === "paste" ? ev.text.replace(/\r\n?/g, "\n") : ev.text;
|
|
138
|
+
// Settings mode is single-line and numeric fields are digits-only.
|
|
139
|
+
if (m === "settings") {
|
|
140
|
+
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
141
|
+
if (field === "pause")
|
|
142
|
+
break;
|
|
143
|
+
if (NUMERIC_SETTINGS_FIELDS.has(field))
|
|
144
|
+
text = text.replace(/[^0-9.]/g, "");
|
|
145
|
+
text = text.replace(/\n/g, " ");
|
|
146
|
+
}
|
|
147
|
+
if (!text)
|
|
148
|
+
break;
|
|
149
|
+
setBuffer((prev) => {
|
|
150
|
+
const next = (prev + text).slice(0, MAX_INPUT_LEN);
|
|
151
|
+
store.patch({ input: { ...store.get().input, buffer: next } });
|
|
152
|
+
return next;
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
136
155
|
}
|
|
137
156
|
}
|
|
138
|
-
// Tab in steer/ask modes is a no-op, not a submit or a "tab character".
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (key.backspace || key.delete) {
|
|
142
|
-
const next = buffer.slice(0, -1);
|
|
143
|
-
setBuffer(next);
|
|
144
|
-
store.patch({ input: { ...state.input, buffer: next } });
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
// Typed printable char(s) — raw is the string for this event. Strip any
|
|
148
|
-
// control chars (lone ESC flushes, \n linefeeds parseKeypress labels as
|
|
149
|
-
// 'enter', partial ESC [ sequences) before touching the buffer.
|
|
150
|
-
if (raw && raw.length > 0) {
|
|
151
|
-
let text = sanitizeTyped(raw);
|
|
152
|
-
if (mode === "settings") {
|
|
153
|
-
const field = SETTINGS_FIELDS[settingsField % SETTINGS_FIELDS.length];
|
|
154
|
-
if (NUMERIC_SETTINGS_FIELDS.has(field))
|
|
155
|
-
text = text.replace(/[^0-9.]/g, "");
|
|
156
|
-
if (field === "pause")
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (!text)
|
|
160
|
-
return;
|
|
161
|
-
const next = (buffer + text).slice(0, MAX_INPUT_LEN);
|
|
162
|
-
setBuffer(next);
|
|
163
|
-
store.patch({ input: { ...state.input, buffer: next } });
|
|
164
157
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
};
|
|
159
|
+
stdin.on("data", onData);
|
|
160
|
+
return () => {
|
|
161
|
+
stdin.off("data", onData);
|
|
162
|
+
setBracketedPaste(process.stdout, false);
|
|
163
|
+
};
|
|
164
|
+
}, [textEntry, stdin, setRawMode, isRawModeSupported, buffer, settingsField, store, callbacks]);
|
|
165
|
+
// ── Hotkey path: Ink's useInput, only active when no overlay is open ──
|
|
166
|
+
useInput((raw, key) => {
|
|
167
|
+
const s = store.get();
|
|
168
|
+
const swarm = s.swarm;
|
|
169
|
+
const lc = s.liveConfig;
|
|
169
170
|
if (key.rightArrow || key.downArrow) {
|
|
170
171
|
callbacks.cycleAgent(1);
|
|
171
172
|
return;
|
|
@@ -178,19 +179,17 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
178
179
|
callbacks.clearSelectedAgent();
|
|
179
180
|
return;
|
|
180
181
|
}
|
|
181
|
-
// Escape in hotkey mode — clear agent selection or dismiss answered ask
|
|
182
182
|
if (key.escape) {
|
|
183
|
-
if (
|
|
183
|
+
if (s.selectedAgentId != null) {
|
|
184
184
|
callbacks.clearSelectedAgent();
|
|
185
185
|
return;
|
|
186
186
|
}
|
|
187
|
-
if (
|
|
187
|
+
if (s.ask && !s.ask.streaming) {
|
|
188
188
|
callbacks.clearAsk();
|
|
189
189
|
return;
|
|
190
190
|
}
|
|
191
191
|
return;
|
|
192
192
|
}
|
|
193
|
-
// Ctrl-C: abort swarm or exit
|
|
194
193
|
if (key.ctrl && raw === "c") {
|
|
195
194
|
if (swarm && !swarm.aborted) {
|
|
196
195
|
swarm.abort();
|
|
@@ -198,9 +197,8 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
198
197
|
}
|
|
199
198
|
process.exit(0);
|
|
200
199
|
}
|
|
201
|
-
// Enter in hotkey mode — reveal ask answer file in Finder if we have one
|
|
202
200
|
if (key.return) {
|
|
203
|
-
if (
|
|
201
|
+
if (s.askTempFileAvailable)
|
|
204
202
|
callbacks.openAskTempFile();
|
|
205
203
|
return;
|
|
206
204
|
}
|
|
@@ -209,18 +207,16 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
209
207
|
const code = raw.charCodeAt(0);
|
|
210
208
|
if (code < 0x20 || code > 0x7E)
|
|
211
209
|
return;
|
|
212
|
-
// Any ctrl/meta combo (that isn't one of the specific hotkeys above) is
|
|
213
|
-
// nav-adjacent on most terminals; ignore instead of matching "c"/"i" etc.
|
|
214
210
|
if (key.ctrl || key.meta)
|
|
215
211
|
return;
|
|
216
212
|
const toast = (msg) => onToast(msg);
|
|
217
213
|
switch (raw.toLowerCase()) {
|
|
218
214
|
case "?":
|
|
219
|
-
if (!
|
|
215
|
+
if (!s.hasOnAsk)
|
|
220
216
|
return toast("Ask not wired for this run");
|
|
221
|
-
if (
|
|
217
|
+
if (s.askBusy || s.ask?.streaming)
|
|
222
218
|
return toast("Ask already in flight");
|
|
223
|
-
if (
|
|
219
|
+
if (s.ask && !s.ask.streaming) {
|
|
224
220
|
callbacks.clearAsk();
|
|
225
221
|
return;
|
|
226
222
|
}
|
|
@@ -228,17 +224,16 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
228
224
|
setBuffer("");
|
|
229
225
|
return;
|
|
230
226
|
case "i":
|
|
231
|
-
if (!
|
|
227
|
+
if (!s.hasOnSteer)
|
|
232
228
|
return toast("Steering not wired for this run");
|
|
233
229
|
store.patch({ input: { mode: "steer", buffer: "", settingsField: 0 } });
|
|
234
230
|
setBuffer("");
|
|
235
231
|
return;
|
|
236
232
|
case "d":
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const last = state.debriefHistory[state.debriefHistory.length - 1];
|
|
233
|
+
if (s.debrief)
|
|
234
|
+
return;
|
|
235
|
+
if (s.debriefHistory.length > 0) {
|
|
236
|
+
const last = s.debriefHistory[s.debriefHistory.length - 1];
|
|
242
237
|
store.patch({ debrief: { text: last.text, label: last.label } });
|
|
243
238
|
return;
|
|
244
239
|
}
|
|
@@ -270,11 +265,8 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
270
265
|
swarm.retryRateLimitNow();
|
|
271
266
|
return;
|
|
272
267
|
case "q":
|
|
273
|
-
// Second press with the current swarm already aborted = hard exit.
|
|
274
268
|
if (swarm?.aborted)
|
|
275
269
|
process.exit(0);
|
|
276
|
-
// Always request quit: flips the runner's `stopping` flag so the wave
|
|
277
|
-
// loop breaks instead of advancing to steering / post-run review.
|
|
278
270
|
callbacks.requestQuit();
|
|
279
271
|
return;
|
|
280
272
|
}
|
|
@@ -284,9 +276,7 @@ export function InputLayer({ store, callbacks, onToast }) {
|
|
|
284
276
|
if (n < running.length)
|
|
285
277
|
callbacks.selectAgent(running[n].id);
|
|
286
278
|
}
|
|
287
|
-
});
|
|
288
|
-
// Render the active text-entry prompt under the footer hint.
|
|
289
|
-
const state = useSyncExternalStore(store.subscribe, store.get, store.get);
|
|
279
|
+
}, { isActive: !textEntry });
|
|
290
280
|
if (state.input.mode === "none")
|
|
291
281
|
return null;
|
|
292
282
|
const caretOn = state.tick % 2 === 0;
|
|
@@ -305,7 +295,7 @@ function InputPrompt({ mode, buffer, settingsField, state, caretOn }) {
|
|
|
305
295
|
let subtitle;
|
|
306
296
|
let hint;
|
|
307
297
|
let currentLine = null;
|
|
308
|
-
|
|
298
|
+
const filteredBuffer = buffer;
|
|
309
299
|
if (mode === "settings") {
|
|
310
300
|
const total = SETTINGS_FIELDS.length;
|
|
311
301
|
const field = SETTINGS_FIELDS[settingsField % total];
|
|
@@ -323,13 +313,11 @@ function InputPrompt({ mode, buffer, settingsField, state, caretOn }) {
|
|
|
323
313
|
const action = mode === "steer" ? "queue" : "send";
|
|
324
314
|
hint = chalk.dim(`Enter ${action} \u00b7 Esc cancel \u00b7 Ctrl+U clear \u00b7 Ctrl+W del word`);
|
|
325
315
|
}
|
|
326
|
-
// Word-wrap the buffer so long entries don't blow past the box edge.
|
|
327
316
|
const bufferLines = filteredBuffer.length === 0
|
|
328
317
|
? [""]
|
|
329
318
|
: wrap(filteredBuffer, Math.max(20, innerW));
|
|
330
319
|
const caret = caretOn ? accent("\u2588") : " ";
|
|
331
320
|
const lastIdx = bufferLines.length - 1;
|
|
332
|
-
// Char counter — dims normally, warns at 80%, red at 95%.
|
|
333
321
|
const pct = buffer.length / MAX_INPUT_LEN;
|
|
334
322
|
const counter = buffer.length === 0 ? "" :
|
|
335
323
|
pct >= 0.95 ? chalk.red(`${buffer.length}/${MAX_INPUT_LEN}`)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type InputEvent = {
|
|
2
|
+
type: "char";
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: "paste";
|
|
6
|
+
text: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: "backspace";
|
|
9
|
+
} | {
|
|
10
|
+
type: "word-delete";
|
|
11
|
+
} | {
|
|
12
|
+
type: "clear-line";
|
|
13
|
+
} | {
|
|
14
|
+
type: "submit";
|
|
15
|
+
} | {
|
|
16
|
+
type: "cancel";
|
|
17
|
+
} | {
|
|
18
|
+
type: "interrupt";
|
|
19
|
+
} | {
|
|
20
|
+
type: "tab";
|
|
21
|
+
} | {
|
|
22
|
+
type: "nav";
|
|
23
|
+
name: "up" | "down" | "left" | "right" | "home" | "end" | "pgup" | "pgdn";
|
|
24
|
+
};
|
|
25
|
+
export declare const PASTE_START = "\u001B[200~";
|
|
26
|
+
export declare const PASTE_END = "\u001B[201~";
|
|
27
|
+
export declare function sanitize(raw: string): string;
|
|
28
|
+
/** Split a chunk into events. Honors bracketed paste, detects paste-by-shape
|
|
29
|
+
* (multi-byte chunk containing newlines), and cleanly consumes escape
|
|
30
|
+
* sequences without leaking their terminator as a typed char. */
|
|
31
|
+
export declare function parseChunk(chunk: string): InputEvent[];
|
|
32
|
+
/** Enable/disable bracketed paste on the given stdout. Best-effort — terminals
|
|
33
|
+
* that don't support it simply ignore the sequence, and `parseChunk`'s
|
|
34
|
+
* shape-based paste detection covers them. */
|
|
35
|
+
export declare function setBracketedPaste(stdout: NodeJS.WriteStream, enabled: boolean): void;
|
|
36
|
+
/** Readline-style word delete: strip trailing whitespace, then strip the last
|
|
37
|
+
* non-whitespace run. */
|
|
38
|
+
export declare function deleteWordBackward(s: string): string;
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Shared raw-stdin parser for text entry.
|
|
2
|
+
//
|
|
3
|
+
// Single source of truth for what a chunk from stdin means. Used by both the
|
|
4
|
+
// Ink overlay (src/ui/input.tsx) during a run and the preflight `ask()` prompt
|
|
5
|
+
// (src/cli/cli.ts) before a run. Fixes two classes of bugs that existed in
|
|
6
|
+
// both copies:
|
|
7
|
+
//
|
|
8
|
+
// 1. "@ triggered send" — CSI/SS3 terminator check was `< 0x7E` (missed `~`)
|
|
9
|
+
// and the ESC+printable branch silently dropped the next char.
|
|
10
|
+
// 2. "paste with newline sent early" — Ink's useInput fragments multi-byte
|
|
11
|
+
// chunks into per-char keypress events, firing key.return on any `\n` in
|
|
12
|
+
// a paste. Here we keep the whole chunk and decide paste-vs-typed-enter
|
|
13
|
+
// by whether the chunk is exactly a newline (typed) or contains
|
|
14
|
+
// newlines alongside other bytes (pasted).
|
|
15
|
+
//
|
|
16
|
+
// The parser is pure: takes a string chunk, returns an ordered event list.
|
|
17
|
+
// Bracketed-paste markers are honored when present but we don't rely on them.
|
|
18
|
+
export const PASTE_START = "\x1B[200~";
|
|
19
|
+
export const PASTE_END = "\x1B[201~";
|
|
20
|
+
// Control chars to strip from any text we append to a buffer. Matches C0
|
|
21
|
+
// (0x00-0x1F), DEL (0x7F), and C1 (0x80-0x9F). Newlines are kept — the caller
|
|
22
|
+
// decides per segment whether to strip or preserve them.
|
|
23
|
+
const CONTROL_STRIP_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g;
|
|
24
|
+
export function sanitize(raw) {
|
|
25
|
+
return raw.replace(CONTROL_STRIP_RE, "");
|
|
26
|
+
}
|
|
27
|
+
// CSI/SS3 final byte is in the range 0x40..0x7E inclusive. (The old code used
|
|
28
|
+
// `< 0x7E` which dropped past the `~` terminator used by function keys and
|
|
29
|
+
// the bracketed-paste close marker.)
|
|
30
|
+
function isCsiFinal(code) {
|
|
31
|
+
return code >= 0x40 && code <= 0x7E;
|
|
32
|
+
}
|
|
33
|
+
// Recognized CSI/SS3 sequences for navigation. Anything else with a valid
|
|
34
|
+
// CSI/SS3 shape is silently consumed.
|
|
35
|
+
function csiToNav(body) {
|
|
36
|
+
// body = everything between ESC[ (or ESCO) and the terminator, plus terminator
|
|
37
|
+
// Common sequences: A=up, B=down, C=right, D=left, H=home, F=end,
|
|
38
|
+
// 5~=pgup, 6~=pgdn, 1~/7~=home, 4~/8~=end
|
|
39
|
+
if (body === "A")
|
|
40
|
+
return { type: "nav", name: "up" };
|
|
41
|
+
if (body === "B")
|
|
42
|
+
return { type: "nav", name: "down" };
|
|
43
|
+
if (body === "C")
|
|
44
|
+
return { type: "nav", name: "right" };
|
|
45
|
+
if (body === "D")
|
|
46
|
+
return { type: "nav", name: "left" };
|
|
47
|
+
if (body === "H" || body === "1~" || body === "7~")
|
|
48
|
+
return { type: "nav", name: "home" };
|
|
49
|
+
if (body === "F" || body === "4~" || body === "8~")
|
|
50
|
+
return { type: "nav", name: "end" };
|
|
51
|
+
if (body === "5~")
|
|
52
|
+
return { type: "nav", name: "pgup" };
|
|
53
|
+
if (body === "6~")
|
|
54
|
+
return { type: "nav", name: "pgdn" };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/** Split a chunk into events. Honors bracketed paste, detects paste-by-shape
|
|
58
|
+
* (multi-byte chunk containing newlines), and cleanly consumes escape
|
|
59
|
+
* sequences without leaking their terminator as a typed char. */
|
|
60
|
+
export function parseChunk(chunk) {
|
|
61
|
+
const out = [];
|
|
62
|
+
if (!chunk)
|
|
63
|
+
return out;
|
|
64
|
+
// First pass: carve out bracketed-paste blocks. Everything outside those
|
|
65
|
+
// markers is "free text" — we still apply shape-based paste detection to it.
|
|
66
|
+
const parts = [];
|
|
67
|
+
let i = 0;
|
|
68
|
+
while (i < chunk.length) {
|
|
69
|
+
const ps = chunk.indexOf(PASTE_START, i);
|
|
70
|
+
if (ps === -1) {
|
|
71
|
+
parts.push({ kind: "free", text: chunk.slice(i) });
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
if (ps > i)
|
|
75
|
+
parts.push({ kind: "free", text: chunk.slice(i, ps) });
|
|
76
|
+
const bodyStart = ps + PASTE_START.length;
|
|
77
|
+
const pe = chunk.indexOf(PASTE_END, bodyStart);
|
|
78
|
+
if (pe === -1) {
|
|
79
|
+
parts.push({ kind: "paste", text: chunk.slice(bodyStart) });
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
parts.push({ kind: "paste", text: chunk.slice(bodyStart, pe) });
|
|
83
|
+
i = pe + PASTE_END.length;
|
|
84
|
+
}
|
|
85
|
+
for (const part of parts) {
|
|
86
|
+
if (part.kind === "paste") {
|
|
87
|
+
if (part.text)
|
|
88
|
+
out.push({ type: "paste", text: part.text });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Free text: walk byte by byte, consuming escape sequences and control
|
|
92
|
+
// bytes. Typed enter = a chunk that's EXACTLY "\r", "\n", or "\r\n".
|
|
93
|
+
const s = part.text;
|
|
94
|
+
if (!s)
|
|
95
|
+
continue;
|
|
96
|
+
// Fast path: a chunk that is just enter, tab, backspace, or ^C.
|
|
97
|
+
if (s === "\r" || s === "\n" || s === "\r\n") {
|
|
98
|
+
out.push({ type: "submit" });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (s === "\x03") {
|
|
102
|
+
out.push({ type: "interrupt" });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (s === "\x7F" || s === "\b") {
|
|
106
|
+
out.push({ type: "backspace" });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (s === "\t") {
|
|
110
|
+
out.push({ type: "tab" });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (s === "\x1B") {
|
|
114
|
+
out.push({ type: "cancel" });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (s === "\x15") {
|
|
118
|
+
out.push({ type: "clear-line" });
|
|
119
|
+
continue;
|
|
120
|
+
} // ^U
|
|
121
|
+
if (s === "\x17") {
|
|
122
|
+
out.push({ type: "word-delete" });
|
|
123
|
+
continue;
|
|
124
|
+
} // ^W
|
|
125
|
+
// Shape-based paste: multi-char chunk containing a newline that is NOT
|
|
126
|
+
// just "\r\n". Terminals buffer keypresses at ~1 byte; paste comes in as
|
|
127
|
+
// a single large chunk. Anything multi-char with embedded newlines is
|
|
128
|
+
// paste, not a sequence of typed Enters.
|
|
129
|
+
if ((s.includes("\n") || s.includes("\r")) && s.length > 2) {
|
|
130
|
+
const stripped = s.replace(/\r/g, "");
|
|
131
|
+
if (stripped)
|
|
132
|
+
out.push({ type: "paste", text: stripped });
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Otherwise walk the chunk one logical token at a time.
|
|
136
|
+
let j = 0;
|
|
137
|
+
let buf = "";
|
|
138
|
+
const flushBuf = () => { if (buf) {
|
|
139
|
+
out.push({ type: "char", text: buf });
|
|
140
|
+
buf = "";
|
|
141
|
+
} };
|
|
142
|
+
while (j < s.length) {
|
|
143
|
+
const ch = s[j];
|
|
144
|
+
if (ch === "\r" || ch === "\n") {
|
|
145
|
+
// Bare newline embedded inside a short non-paste chunk: treat as
|
|
146
|
+
// submit if it's the tail, otherwise drop (shouldn't happen in well-
|
|
147
|
+
// formed terminal input — paste goes through the shape path above).
|
|
148
|
+
flushBuf();
|
|
149
|
+
out.push({ type: "submit" });
|
|
150
|
+
j++;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (ch === "\x03") {
|
|
154
|
+
flushBuf();
|
|
155
|
+
out.push({ type: "interrupt" });
|
|
156
|
+
j++;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
160
|
+
flushBuf();
|
|
161
|
+
out.push({ type: "backspace" });
|
|
162
|
+
j++;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (ch === "\t") {
|
|
166
|
+
flushBuf();
|
|
167
|
+
out.push({ type: "tab" });
|
|
168
|
+
j++;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (ch === "\x15") {
|
|
172
|
+
flushBuf();
|
|
173
|
+
out.push({ type: "clear-line" });
|
|
174
|
+
j++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (ch === "\x17") {
|
|
178
|
+
flushBuf();
|
|
179
|
+
out.push({ type: "word-delete" });
|
|
180
|
+
j++;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (ch === "\x1B") {
|
|
184
|
+
flushBuf();
|
|
185
|
+
const next = j + 1 < s.length ? s[j + 1] : null;
|
|
186
|
+
if (next === null) {
|
|
187
|
+
out.push({ type: "cancel" });
|
|
188
|
+
j++;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (next === "[" || next === "O") {
|
|
192
|
+
// CSI or SS3: consume up to and including the final byte (0x40..0x7E).
|
|
193
|
+
let k = j + 2;
|
|
194
|
+
let body = "";
|
|
195
|
+
while (k < s.length) {
|
|
196
|
+
const code = s.charCodeAt(k);
|
|
197
|
+
body += s[k];
|
|
198
|
+
k++;
|
|
199
|
+
if (isCsiFinal(code))
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
const nav = csiToNav(body);
|
|
203
|
+
if (nav)
|
|
204
|
+
out.push(nav);
|
|
205
|
+
j = k;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// ESC + printable = Meta/Alt + key. Drop both bytes; upstream can
|
|
209
|
+
// surface specific combos later if needed.
|
|
210
|
+
j += 2;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const code = ch.charCodeAt(0);
|
|
214
|
+
// Skip remaining control bytes silently.
|
|
215
|
+
if (code < 0x20 || (code >= 0x7F && code < 0xA0)) {
|
|
216
|
+
j++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
buf += ch;
|
|
220
|
+
j++;
|
|
221
|
+
}
|
|
222
|
+
flushBuf();
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
/** Enable/disable bracketed paste on the given stdout. Best-effort — terminals
|
|
227
|
+
* that don't support it simply ignore the sequence, and `parseChunk`'s
|
|
228
|
+
* shape-based paste detection covers them. */
|
|
229
|
+
export function setBracketedPaste(stdout, enabled) {
|
|
230
|
+
try {
|
|
231
|
+
stdout.write(enabled ? "\x1B[?2004h" : "\x1B[?2004l");
|
|
232
|
+
}
|
|
233
|
+
catch { }
|
|
234
|
+
}
|
|
235
|
+
/** Readline-style word delete: strip trailing whitespace, then strip the last
|
|
236
|
+
* non-whitespace run. */
|
|
237
|
+
export function deleteWordBackward(s) {
|
|
238
|
+
const trimmed = s.replace(/\s+$/, "");
|
|
239
|
+
const idx = trimmed.search(/\S+$/);
|
|
240
|
+
return idx < 0 ? "" : trimmed.slice(0, idx);
|
|
241
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.48",
|
|
4
4
|
"description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.48",
|
|
4
4
|
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Francesco Fornace"
|