claude-overnight 1.11.9 → 1.11.12

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.d.ts CHANGED
@@ -6,6 +6,33 @@ export declare function parseCliFlags(argv: string[]): {
6
6
  };
7
7
  export declare function isAuthError(err: unknown): boolean;
8
8
  export declare function fetchModels(timeoutMs?: number): Promise<ModelInfo[]>;
9
+ export declare const PASTE_START = "\u001B[200~";
10
+ export declare const PASTE_END = "\u001B[201~";
11
+ export declare const PASTE_PLACEHOLDER_MAX = 80;
12
+ export type InputSegment = {
13
+ type: "text";
14
+ content: string;
15
+ } | {
16
+ type: "paste";
17
+ content: string;
18
+ };
19
+ /** Split a raw stdin chunk into typed and pasted segments. */
20
+ export declare function splitPaste(chunk: string): Array<{
21
+ type: "typed" | "paste";
22
+ text: string;
23
+ }>;
24
+ export declare function segmentsToString(segs: InputSegment[]): string;
25
+ export declare function renderSegments(segs: InputSegment[]): string;
26
+ export declare function appendCharToSegments(segs: InputSegment[], ch: string): void;
27
+ /** Appends a pasted block. Short single-line pastes inline as text; the rest become placeholders. */
28
+ export declare function appendPasteToSegments(segs: InputSegment[], text: string): void;
29
+ /** Backspace removes one char, or an entire paste block atomically. */
30
+ export declare function backspaceSegments(segs: InputSegment[]): void;
31
+ /**
32
+ * Read a line from the user with bracketed-paste awareness.
33
+ * Pasted multi-line text stays in the buffer as a single block — only a typed
34
+ * Enter submits. Falls back to cooked readline when stdin isn't a TTY.
35
+ */
9
36
  export declare function ask(question: string): Promise<string>;
10
37
  export declare function select<T>(label: string, items: {
11
38
  name: string;
package/dist/cli.js CHANGED
@@ -67,11 +67,149 @@ export async function fetchModels(timeoutMs = 10_000) {
67
67
  return [];
68
68
  }
69
69
  }
70
+ // ── Bracketed paste + segment-based input ──
71
+ //
72
+ // When the terminal is in bracketed paste mode, pasted content is wrapped with
73
+ // \x1B[200~ ... \x1B[201~ so we can distinguish typed Enter from pasted newlines.
74
+ // Multi-line or long pastes are stored as opaque segments and shown as a compact
75
+ // [Pasted +N lines] placeholder while editing — the full text is substituted on submit.
76
+ export const PASTE_START = "\x1B[200~";
77
+ export const PASTE_END = "\x1B[201~";
78
+ export const PASTE_PLACEHOLDER_MAX = 80;
79
+ /** Split a raw stdin chunk into typed and pasted segments. */
80
+ export function splitPaste(chunk) {
81
+ const out = [];
82
+ let i = 0;
83
+ while (i < chunk.length) {
84
+ const start = chunk.indexOf(PASTE_START, i);
85
+ if (start === -1) {
86
+ out.push({ type: "typed", text: chunk.slice(i) });
87
+ break;
88
+ }
89
+ if (start > i)
90
+ out.push({ type: "typed", text: chunk.slice(i, start) });
91
+ const bodyStart = start + PASTE_START.length;
92
+ const end = chunk.indexOf(PASTE_END, bodyStart);
93
+ if (end === -1) {
94
+ out.push({ type: "paste", text: chunk.slice(bodyStart) });
95
+ break;
96
+ }
97
+ out.push({ type: "paste", text: chunk.slice(bodyStart, end) });
98
+ i = end + PASTE_END.length;
99
+ }
100
+ return out;
101
+ }
102
+ export function segmentsToString(segs) {
103
+ return segs.map((s) => s.content).join("");
104
+ }
105
+ export function renderSegments(segs) {
106
+ return segs.map((s) => {
107
+ if (s.type === "text")
108
+ return s.content;
109
+ const lines = s.content.split("\n").length;
110
+ return chalk.dim(`[Pasted +${lines} line${lines === 1 ? "" : "s"}]`);
111
+ }).join("");
112
+ }
113
+ export function appendCharToSegments(segs, ch) {
114
+ const last = segs[segs.length - 1];
115
+ if (last && last.type === "text")
116
+ last.content += ch;
117
+ else
118
+ segs.push({ type: "text", content: ch });
119
+ }
120
+ /** Appends a pasted block. Short single-line pastes inline as text; the rest become placeholders. */
121
+ export function appendPasteToSegments(segs, text) {
122
+ if (!text)
123
+ return;
124
+ const norm = text.replace(/\r\n?/g, "\n");
125
+ if (!norm.includes("\n") && norm.length <= PASTE_PLACEHOLDER_MAX) {
126
+ appendCharToSegments(segs, norm);
127
+ return;
128
+ }
129
+ segs.push({ type: "paste", content: norm });
130
+ }
131
+ /** Backspace removes one char, or an entire paste block atomically. */
132
+ export function backspaceSegments(segs) {
133
+ while (segs.length > 0) {
134
+ const last = segs[segs.length - 1];
135
+ if (last.type === "paste") {
136
+ segs.pop();
137
+ return;
138
+ }
139
+ if (last.content.length > 1) {
140
+ last.content = last.content.slice(0, -1);
141
+ return;
142
+ }
143
+ segs.pop();
144
+ return;
145
+ }
146
+ }
70
147
  // ── Interactive primitives ──
148
+ /**
149
+ * Read a line from the user with bracketed-paste awareness.
150
+ * Pasted multi-line text stays in the buffer as a single block — only a typed
151
+ * Enter submits. Falls back to cooked readline when stdin isn't a TTY.
152
+ */
71
153
  export function ask(question) {
72
- const rl = createInterface({ input: process.stdin, output: process.stdout });
73
- return new Promise((res) => {
74
- rl.question(question, (answer) => { rl.close(); res(answer.trim()); });
154
+ const { stdin, stdout } = process;
155
+ if (!stdin.isTTY) {
156
+ const rl = createInterface({ input: stdin, output: stdout });
157
+ return new Promise((res) => rl.question(question, (a) => { rl.close(); res(a.trim()); }));
158
+ }
159
+ return new Promise((resolve) => {
160
+ const segs = [];
161
+ const lastLine = question.split("\n").pop() ?? "";
162
+ const redraw = () => {
163
+ stdout.write("\r\x1B[K" + lastLine + renderSegments(segs));
164
+ };
165
+ stdout.write(question);
166
+ stdout.write("\x1B[?2004h");
167
+ try {
168
+ stdin.setRawMode(true);
169
+ }
170
+ catch { }
171
+ stdin.resume();
172
+ const cleanup = () => {
173
+ stdout.write("\x1B[?2004l");
174
+ try {
175
+ stdin.setRawMode(false);
176
+ }
177
+ catch { }
178
+ stdin.removeListener("data", onData);
179
+ stdin.pause();
180
+ };
181
+ const onData = (buf) => {
182
+ const chunk = buf.toString();
183
+ for (const seg of splitPaste(chunk)) {
184
+ if (seg.type === "paste") {
185
+ appendPasteToSegments(segs, seg.text);
186
+ redraw();
187
+ continue;
188
+ }
189
+ for (const ch of seg.text) {
190
+ if (ch === "\r" || ch === "\n") {
191
+ stdout.write("\n");
192
+ cleanup();
193
+ resolve(segmentsToString(segs).trim());
194
+ return;
195
+ }
196
+ if (ch === "\x03") {
197
+ cleanup();
198
+ stdout.write("\n");
199
+ process.exit(130);
200
+ }
201
+ if (ch === "\x7F" || ch === "\b") {
202
+ backspaceSegments(segs);
203
+ continue;
204
+ }
205
+ const code = ch.charCodeAt(0);
206
+ if (ch !== "\x1B" && code >= 0x20)
207
+ appendCharToSegments(segs, ch);
208
+ }
209
+ redraw();
210
+ }
211
+ };
212
+ stdin.on("data", onData);
75
213
  });
76
214
  }
77
215
  export async function select(label, items, defaultIdx = 0) {
package/dist/merge.d.ts CHANGED
@@ -7,7 +7,7 @@ export interface MergeResult {
7
7
  error?: string;
8
8
  filesChanged: number;
9
9
  }
10
- export declare function autoCommit(agentId: number, taskPrompt: string, worktreeCwd: string, log: (id: number, msg: string) => void): number;
10
+ export declare function autoCommit(agentId: number, taskPrompt: string, worktreeCwd: string, baseRef: string | undefined, log: (id: number, msg: string) => void): number;
11
11
  export interface MergeAllResult {
12
12
  mergeResults: MergeResult[];
13
13
  mergeBranch?: string;
package/dist/merge.js CHANGED
@@ -5,11 +5,13 @@ import { tmpdir } from "os";
5
5
  export function gitExec(cmd, cwd) {
6
6
  return execSync(cmd, { cwd, encoding: "utf-8", stdio: "pipe" });
7
7
  }
8
- export function autoCommit(agentId, taskPrompt, worktreeCwd, log) {
8
+ export function autoCommit(agentId, taskPrompt, worktreeCwd, baseRef, log) {
9
9
  if (!existsSync(worktreeCwd)) {
10
10
  log(agentId, "Worktree directory gone, skipping commit");
11
11
  return 0;
12
12
  }
13
+ // Step 1: commit any uncommitted changes. Agents *may* commit their own work;
14
+ // we pick up whatever is still dirty.
13
15
  let status;
14
16
  try {
15
17
  status = gitExec("git status --porcelain", worktreeCwd);
@@ -18,27 +20,40 @@ export function autoCommit(agentId, taskPrompt, worktreeCwd, log) {
18
20
  log(agentId, `git status failed: ${String(err.message || err).slice(0, 120)}`);
19
21
  return 0;
20
22
  }
21
- if (!status.trim())
22
- return 0;
23
- const lines = status.trim().split("\n").length;
24
- try {
25
- gitExec("git add -A", worktreeCwd);
26
- }
27
- catch (err) {
28
- log(agentId, `git add failed: ${String(err.message || err).slice(0, 120)}`);
29
- return lines;
23
+ if (status.trim()) {
24
+ try {
25
+ gitExec("git add -A", worktreeCwd);
26
+ }
27
+ catch (err) {
28
+ log(agentId, `git add failed: ${String(err.message || err).slice(0, 120)}`);
29
+ }
30
+ try {
31
+ const msg = taskPrompt.slice(0, 72).replace(/'/g, "'\\''");
32
+ gitExec(`git commit -m 'swarm: ${msg}'`, worktreeCwd);
33
+ }
34
+ catch (err) {
35
+ const m = String(err.message || err);
36
+ if (!m.includes("nothing to commit"))
37
+ log(agentId, `git commit failed: ${m.slice(0, 120)}`);
38
+ }
30
39
  }
40
+ // Step 2: measure against the worktree's origin commit — true regardless of
41
+ // who made the commits (agent or autoCommit). Pre-1.11.10 this counted
42
+ // `git status --porcelain` lines, which was zero whenever the agent committed
43
+ // its own work → filesChanged=0 → branch skipped by the merge gate → orphaned.
44
+ if (!baseRef)
45
+ return 0;
31
46
  try {
32
- const msg = taskPrompt.slice(0, 72).replace(/'/g, "'\\''");
33
- gitExec(`git commit -m 'swarm: ${msg}'`, worktreeCwd);
34
- log(agentId, `Committed ${lines} file(s)`);
47
+ const diff = gitExec(`git diff --name-only ${baseRef}..HEAD`, worktreeCwd);
48
+ const count = diff.trim().split("\n").filter(Boolean).length;
49
+ if (count > 0)
50
+ log(agentId, `${count} file(s) changed`);
51
+ return count;
35
52
  }
36
53
  catch (err) {
37
- const msg = String(err.message || err);
38
- if (!msg.includes("nothing to commit"))
39
- log(agentId, `git commit failed: ${msg.slice(0, 120)}`);
54
+ log(agentId, `diff vs base failed: ${String(err.message || err).slice(0, 120)}`);
55
+ return 0;
40
56
  }
41
- return lines;
42
57
  }
43
58
  export function mergeAllBranches(agents, cwd, strategy, log) {
44
59
  const mergeResults = [];
@@ -194,14 +209,23 @@ export function cleanStaleWorktrees(cwd, log) {
194
209
  .split("\n")
195
210
  .map(b => b.trim().replace(/^\* /, ""))
196
211
  .filter(b => b.startsWith("swarm/task-") && !worktreeBranches.has(b));
212
+ // Use -d (safe delete) rather than -D: branches whose commits aren't in
213
+ // HEAD are orphaned work from a prior failed/unmerged run and must survive
214
+ // so the user can recover them. Only already-merged (ff) branches are
215
+ // actually deleted here.
216
+ let cleaned = 0;
197
217
  for (const b of branches) {
198
218
  try {
199
- gitExec(`git branch -D "${b}"`, cwd);
219
+ gitExec(`git branch -d "${b}"`, cwd);
220
+ cleaned++;
200
221
  }
201
222
  catch { }
202
223
  }
203
- if (branches.length > 0)
204
- log(-1, `Cleaned ${branches.length} stale swarm branch(es)`);
224
+ if (cleaned > 0)
225
+ log(-1, `Cleaned ${cleaned} stale swarm branch(es)`);
226
+ const orphaned = branches.length - cleaned;
227
+ if (orphaned > 0)
228
+ log(-1, `Kept ${orphaned} orphaned swarm branch(es) with unmerged commits — resume to recover`);
205
229
  }
206
230
  catch { }
207
231
  }
package/dist/run.js CHANGED
@@ -342,11 +342,16 @@ export async function executeRun(cfg) {
342
342
  lastAborted = swarm.aborted;
343
343
  recordBranches(swarm.agents, swarm.mergeResults, branches);
344
344
  saveWaveSession(runDir, waveNum, swarm.agents, swarm.totalCostUsd);
345
+ // Tasks that never made it into the swarm (queue cleared on abort/cap)
346
+ // are preserved as currentTasks so resume picks them up. Budget for these
347
+ // wasn't decremented (only attempted agents were), so no refund needed.
348
+ const attemptedPrompts = new Set(swarm.agents.map(a => a.task.prompt));
349
+ const neverStarted = currentTasks.filter(t => !attemptedPrompts.has(t.prompt));
345
350
  saveRunState(runDir, {
346
351
  id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
347
352
  remaining, workerModel, plannerModel, concurrency, permissionMode,
348
353
  usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
349
- flex, useWorktrees, mergeStrategy, waveNum, currentTasks: [],
354
+ flex, useWorktrees, mergeStrategy, waveNum, currentTasks: neverStarted,
350
355
  accCost, accCompleted, accFailed, accIn, accOut, accTools,
351
356
  branches, phase: "steering", startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
352
357
  });
package/dist/state.js CHANGED
@@ -441,7 +441,10 @@ export function recordBranches(agents, mergeResults, branches) {
441
441
  }
442
442
  }
443
443
  export function autoMergeBranches(cwd, branches, onLog) {
444
- const unmerged = branches.filter(b => b.status === "unmerged" && b.filesChanged > 0);
444
+ // Do NOT gate on filesChanged — pre-1.11.10 runs can record filesChanged=0
445
+ // for branches that actually contain real commits (agent self-committed).
446
+ // Feed every unmerged branch to git; it will no-op harmlessly if truly empty.
447
+ const unmerged = branches.filter(b => b.status === "unmerged");
445
448
  if (unmerged.length === 0)
446
449
  return;
447
450
  onLog(`Merging ${unmerged.length} unmerged branches...`);
package/dist/swarm.js CHANGED
@@ -227,6 +227,11 @@ export class Swarm {
227
227
  if (this.config.useWorktrees && this.worktreeBase && !task.noWorktree) {
228
228
  const branch = `swarm/task-${id}`;
229
229
  const dir = join(this.worktreeBase, `agent-${id}`);
230
+ let baseRef;
231
+ try {
232
+ baseRef = gitExec("git rev-parse HEAD", this.config.cwd).trim();
233
+ }
234
+ catch { }
230
235
  let worktreeOk = false;
231
236
  for (let wt = 0; wt < 2 && !worktreeOk; wt++) {
232
237
  try {
@@ -254,6 +259,7 @@ export class Swarm {
254
259
  if (worktreeOk) {
255
260
  agentCwd = dir;
256
261
  agent.branch = branch;
262
+ agent.baseRef = baseRef;
257
263
  this.log(id, `Worktree: ${branch}`);
258
264
  }
259
265
  else {
@@ -327,6 +333,10 @@ export class Swarm {
327
333
  this.activeQueries.delete(agentQuery);
328
334
  if (sessionId)
329
335
  resumeSessionId = sessionId;
336
+ try {
337
+ agentQuery.close();
338
+ }
339
+ catch { }
330
340
  }
331
341
  };
332
342
  try {
@@ -355,7 +365,7 @@ export class Swarm {
355
365
  const duration = agent.finishedAt - (agent.startedAt || agent.finishedAt);
356
366
  if (agent.toolCalls === 0 && (agent.costUsd ?? 0) < 0.001 && duration < 15_000) {
357
367
  agent.status = "error";
358
- agent.error = "Agent did no work (likely rate-limited before starting)";
368
+ agent.error = "Agent did no work exited without tool use";
359
369
  this.failed++;
360
370
  }
361
371
  else {
@@ -397,14 +407,15 @@ export class Swarm {
397
407
  }
398
408
  }
399
409
  if (this.config.useWorktrees && agent.branch) {
400
- agent.filesChanged = autoCommit(agent.id, agent.task.prompt, agentCwd, (id, text) => this.log(id, text));
410
+ agent.filesChanged = autoCommit(agent.id, agent.task.prompt, agentCwd, agent.baseRef, (id, text) => this.log(id, text));
401
411
  }
402
412
  }
403
413
  agentSummary(agent) {
404
414
  const dur = (agent.finishedAt ?? Date.now()) - (agent.startedAt ?? Date.now());
405
415
  const m = Math.floor(dur / 60000);
406
416
  const s = Math.round((dur % 60000) / 1000);
407
- return `Agent ${agent.id} done: ${m}m ${s}s, ${agent.toolCalls} tools, ${agent.filesChanged ?? 0} files changed`;
417
+ const verb = agent.status === "error" ? "errored" : "done";
418
+ return `Agent ${agent.id} ${verb}: ${m}m ${s}s, ${agent.toolCalls} tools, ${agent.filesChanged ?? 0} files changed`;
408
419
  }
409
420
  // ── Message handler ──
410
421
  handleMsg(agent, msg) {
@@ -489,6 +500,12 @@ export class Swarm {
489
500
  const pct = info.utilization != null ? `${Math.round(info.utilization * 100)}%` : "";
490
501
  const overageTag = this.isUsingOverage ? " [EXTRA]" : "";
491
502
  this.log(agent.id, `Rate: ${info.status} ${pct}${overageTag}${windowType ? ` (${windowType})` : ""}`);
503
+ if (info.status === "rejected") {
504
+ if (!this.rateLimitResetsAt || this.rateLimitResetsAt <= Date.now()) {
505
+ this.rateLimitResetsAt = Date.now() + 60_000;
506
+ }
507
+ throw new Error("rate limit rejected — retrying");
508
+ }
492
509
  break;
493
510
  }
494
511
  }
package/dist/types.d.ts CHANGED
@@ -64,6 +64,8 @@ export interface AgentState {
64
64
  costUsd?: number;
65
65
  /** Git branch name when using worktree isolation. */
66
66
  branch?: string;
67
+ /** Commit the worktree branch was created from — the baseline for measuring filesChanged. */
68
+ baseRef?: string;
67
69
  /** Number of files changed by the agent (from git diff). */
68
70
  filesChanged?: number;
69
71
  }
package/dist/ui.d.ts CHANGED
@@ -57,7 +57,7 @@ export declare class RunDisplay {
57
57
  private interval?;
58
58
  private keyHandler?;
59
59
  private inputMode;
60
- private inputBuf;
60
+ private inputSegs;
61
61
  private started;
62
62
  private readonly isTTY;
63
63
  private lastSeq;
@@ -94,6 +94,10 @@ export declare class RunDisplay {
94
94
  private renderAskPanel;
95
95
  private hasHotkeys;
96
96
  private setupHotkeys;
97
+ /** Handle a pasted block. Returns true if the frame needs a redraw. */
98
+ private handlePaste;
99
+ /** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw. */
100
+ private handleTyped;
97
101
  private plainTick;
98
102
  }
99
103
  export {};
package/dist/ui.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { renderFrame, renderSteeringFrame } from "./render.js";
3
+ import { splitPaste, segmentsToString, renderSegments, appendCharToSegments, appendPasteToSegments, backspaceSegments, } from "./cli.js";
3
4
  const MAX_STEERING_EVENTS = 60;
4
5
  const MAX_INPUT_LEN = 600;
5
6
  export class RunDisplay {
@@ -14,7 +15,7 @@ export class RunDisplay {
14
15
  interval;
15
16
  keyHandler;
16
17
  inputMode = "none";
17
- inputBuf = "";
18
+ inputSegs = [];
18
19
  started = false;
19
20
  isTTY;
20
21
  lastSeq = 0;
@@ -87,6 +88,10 @@ export class RunDisplay {
87
88
  if (this.keyHandler) {
88
89
  process.stdin.removeListener("data", this.keyHandler);
89
90
  this.keyHandler = undefined;
91
+ try {
92
+ process.stdout.write("\x1B[?2004l");
93
+ }
94
+ catch { }
90
95
  try {
91
96
  process.stdin.setRawMode(false);
92
97
  process.stdin.pause();
@@ -149,17 +154,18 @@ export class RunDisplay {
149
154
  renderInputPrompt() {
150
155
  if (this.inputMode === "none")
151
156
  return "";
157
+ const rendered = renderSegments(this.inputSegs);
152
158
  if (this.inputMode === "budget") {
153
- return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${this.inputBuf}\u2588`;
159
+ return `\n ${chalk.cyan(">")} New budget (remaining sessions): ${rendered}\u2588`;
154
160
  }
155
161
  if (this.inputMode === "threshold") {
156
- return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${this.inputBuf}\u2588`;
162
+ return `\n ${chalk.cyan(">")} New usage cap (0-100%): ${rendered}\u2588`;
157
163
  }
158
164
  if (this.inputMode === "steer") {
159
- return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
165
+ return `\n ${chalk.cyan(">")} ${chalk.bold("Steer next wave")} ${chalk.dim("(Enter to queue, Esc to cancel)")}\n ${rendered}\u2588`;
160
166
  }
161
167
  if (this.inputMode === "ask") {
162
- return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${this.inputBuf}\u2588`;
168
+ return `\n ${chalk.cyan(">")} ${chalk.bold("Ask the planner")} ${chalk.dim("(Enter to send, Esc to cancel)")}\n ${rendered}\u2588`;
163
169
  }
164
170
  return "";
165
171
  }
@@ -196,12 +202,52 @@ export class RunDisplay {
196
202
  catch {
197
203
  return;
198
204
  }
199
- const lc = this.liveConfig;
205
+ try {
206
+ process.stdout.write("\x1B[?2004h");
207
+ }
208
+ catch { }
200
209
  this.keyHandler = (buf) => {
201
- const s = buf.toString();
202
- if (this.inputMode === "budget" || this.inputMode === "threshold") {
203
- if (s === "\r" || s === "\n") {
204
- const val = parseFloat(this.inputBuf);
210
+ const chunk = buf.toString();
211
+ let dirty = false;
212
+ for (const seg of splitPaste(chunk)) {
213
+ if (seg.type === "paste") {
214
+ if (this.handlePaste(seg.text))
215
+ dirty = true;
216
+ }
217
+ else {
218
+ if (this.handleTyped(seg.text))
219
+ dirty = true;
220
+ }
221
+ }
222
+ if (dirty)
223
+ this.flush();
224
+ };
225
+ process.stdin.on("data", this.keyHandler);
226
+ }
227
+ /** Handle a pasted block. Returns true if the frame needs a redraw. */
228
+ handlePaste(text) {
229
+ if (this.inputMode === "budget" || this.inputMode === "threshold") {
230
+ const clean = text.replace(/[^0-9.]/g, "");
231
+ if (clean)
232
+ appendCharToSegments(this.inputSegs, clean);
233
+ return !!clean;
234
+ }
235
+ if (this.inputMode === "steer" || this.inputMode === "ask") {
236
+ if (segmentsToString(this.inputSegs).length + text.length > MAX_INPUT_LEN)
237
+ return false;
238
+ appendPasteToSegments(this.inputSegs, text);
239
+ return true;
240
+ }
241
+ return false;
242
+ }
243
+ /** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw. */
244
+ handleTyped(s) {
245
+ const lc = this.liveConfig;
246
+ if (this.inputMode === "budget" || this.inputMode === "threshold") {
247
+ let dirty = false;
248
+ for (const ch of s) {
249
+ if (ch === "\r" || ch === "\n") {
250
+ const val = parseFloat(segmentsToString(this.inputSegs));
205
251
  if (this.inputMode === "budget" && !isNaN(val) && val > 0) {
206
252
  lc.remaining = Math.round(val);
207
253
  lc.dirty = true;
@@ -216,105 +262,113 @@ export class RunDisplay {
216
262
  this.swarm?.log(-1, `Usage cap changed to ${val > 0 ? val + "%" : "unlimited"}`);
217
263
  }
218
264
  this.inputMode = "none";
219
- this.inputBuf = "";
265
+ this.inputSegs = [];
266
+ return true;
220
267
  }
221
- else if (s === "\x1B" || s === "\x03") {
268
+ if (ch === "\x1B" || ch === "\x03") {
222
269
  this.inputMode = "none";
223
- this.inputBuf = "";
270
+ this.inputSegs = [];
271
+ return true;
224
272
  }
225
- else if (s === "\x7F") {
226
- this.inputBuf = this.inputBuf.slice(0, -1);
273
+ if (ch === "\x7F") {
274
+ backspaceSegments(this.inputSegs);
275
+ dirty = true;
276
+ continue;
227
277
  }
228
- else if (/^[0-9.]$/.test(s)) {
229
- this.inputBuf += s;
278
+ if (/^[0-9.]$/.test(ch)) {
279
+ appendCharToSegments(this.inputSegs, ch);
280
+ dirty = true;
230
281
  }
231
- this.flush();
232
- return;
233
282
  }
234
- if (this.inputMode === "steer" || this.inputMode === "ask") {
235
- for (const ch of s) {
236
- if (ch === "\r" || ch === "\n") {
237
- const text = this.inputBuf.trim();
238
- const wasAsk = this.inputMode === "ask";
239
- this.inputMode = "none";
240
- this.inputBuf = "";
241
- if (text) {
242
- if (wasAsk)
243
- this.onAsk?.(text);
244
- else
245
- this.onSteer?.(text);
246
- }
247
- this.flush();
248
- return;
249
- }
250
- if (ch === "\x03") {
251
- this.inputMode = "none";
252
- this.inputBuf = "";
253
- this.flush();
254
- return;
255
- }
256
- // Ignore raw ESC only — let ANSI sequences (arrows etc.) fall through
257
- if (ch === "\x1B" && s.length === 1) {
258
- this.inputMode = "none";
259
- this.inputBuf = "";
260
- this.flush();
261
- return;
262
- }
263
- if (ch === "\x7F" || ch === "\b") {
264
- this.inputBuf = this.inputBuf.slice(0, -1);
265
- continue;
266
- }
267
- const code = ch.charCodeAt(0);
268
- if (code >= 0x20 && code <= 0x7E && this.inputBuf.length < MAX_INPUT_LEN) {
269
- this.inputBuf += ch;
283
+ return dirty;
284
+ }
285
+ if (this.inputMode === "steer" || this.inputMode === "ask") {
286
+ let dirty = false;
287
+ for (const ch of s) {
288
+ if (ch === "\r" || ch === "\n") {
289
+ const text = segmentsToString(this.inputSegs).trim();
290
+ const wasAsk = this.inputMode === "ask";
291
+ this.inputMode = "none";
292
+ this.inputSegs = [];
293
+ if (text) {
294
+ if (wasAsk)
295
+ this.onAsk?.(text);
296
+ else
297
+ this.onSteer?.(text);
270
298
  }
299
+ return true;
271
300
  }
272
- this.flush();
273
- return;
274
- }
275
- // Dismiss completed ask panel on Escape
276
- if (s === "\x1B" && this.askState && !this.askState.streaming) {
277
- this.askState = undefined;
278
- return;
279
- }
280
- if (s === "b" || s === "B") {
281
- this.inputMode = "budget";
282
- this.inputBuf = "";
283
- }
284
- else if (s === "t" || s === "T") {
285
- if (this.swarm) {
286
- this.inputMode = "threshold";
287
- this.inputBuf = "";
301
+ if (ch === "\x03") {
302
+ this.inputMode = "none";
303
+ this.inputSegs = [];
304
+ return true;
305
+ }
306
+ // Ignore raw ESC only — let ANSI sequences (arrows etc.) fall through
307
+ if (ch === "\x1B" && s.length === 1) {
308
+ this.inputMode = "none";
309
+ this.inputSegs = [];
310
+ return true;
311
+ }
312
+ if (ch === "\x7F" || ch === "\b") {
313
+ backspaceSegments(this.inputSegs);
314
+ dirty = true;
315
+ continue;
316
+ }
317
+ const code = ch.charCodeAt(0);
318
+ if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
319
+ appendCharToSegments(this.inputSegs, ch);
320
+ dirty = true;
288
321
  }
289
322
  }
290
- else if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
291
- this.swarm.requeueFailed();
292
- }
293
- else if ((s === "s" || s === "S") && this.onSteer) {
294
- this.inputMode = "steer";
295
- this.inputBuf = "";
323
+ return dirty;
324
+ }
325
+ // Hotkey mode
326
+ if (s === "\x1B" && this.askState && !this.askState.streaming) {
327
+ this.askState = undefined;
328
+ return false;
329
+ }
330
+ if (s === "b" || s === "B") {
331
+ this.inputMode = "budget";
332
+ this.inputSegs = [];
333
+ return true;
334
+ }
335
+ if (s === "t" || s === "T") {
336
+ if (this.swarm) {
337
+ this.inputMode = "threshold";
338
+ this.inputSegs = [];
339
+ return true;
296
340
  }
297
- else if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
298
- // If ask panel is showing a completed answer, dismiss it instead of opening new
299
- if (this.askState && !this.askState.streaming) {
300
- this.askState = undefined;
301
- return;
302
- }
303
- this.inputMode = "ask";
304
- this.inputBuf = "";
341
+ return false;
342
+ }
343
+ if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
344
+ this.swarm.requeueFailed();
345
+ return false;
346
+ }
347
+ if ((s === "s" || s === "S") && this.onSteer) {
348
+ this.inputMode = "steer";
349
+ this.inputSegs = [];
350
+ return true;
351
+ }
352
+ if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
353
+ if (this.askState && !this.askState.streaming) {
354
+ this.askState = undefined;
355
+ return false;
305
356
  }
306
- else if (s === "q" || s === "Q" || s === "\x03") {
307
- if (this.swarm) {
308
- if (this.swarm.aborted)
309
- process.exit(0);
310
- this.swarm.abort();
311
- }
312
- else {
357
+ this.inputMode = "ask";
358
+ this.inputSegs = [];
359
+ return true;
360
+ }
361
+ if (s === "q" || s === "Q" || s === "\x03") {
362
+ if (this.swarm) {
363
+ if (this.swarm.aborted)
313
364
  process.exit(0);
314
- }
365
+ this.swarm.abort();
315
366
  }
316
- };
317
- process.stdin.on("data", this.keyHandler);
367
+ else {
368
+ process.exit(0);
369
+ }
370
+ }
371
+ return false;
318
372
  }
319
373
  plainTick() {
320
374
  if (!this.swarm)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.11.9",
3
+ "version": "1.11.12",
4
4
  "description": "Run 10, 100, or 1000 Claude agents overnight. Parallel autonomous AI coding with thinking waves, iterative quality steering, crash recovery, and rate limit handling. Built on the Claude Agent SDK.",
5
5
  "type": "module",
6
6
  "bin": {