claude-overnight 1.16.5 → 1.16.9

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.js CHANGED
@@ -144,6 +144,9 @@ export function backspaceSegments(segs) {
144
144
  return;
145
145
  }
146
146
  }
147
+ function stripAnsi(s) {
148
+ return s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "");
149
+ }
147
150
  // ── Interactive primitives ──
148
151
  /**
149
152
  * Read a line from the user with bracketed-paste awareness.
@@ -158,12 +161,22 @@ export function ask(question) {
158
161
  }
159
162
  return new Promise((resolve) => {
160
163
  const segs = [];
161
- // DEC save/restore cursor + clear-to-end-of-screen so redraws don't pile
162
- // up when the input wraps past the terminal width onto additional rows.
164
+ const tail = question.split("\n").pop() ?? "";
165
+ const tailVisibleLen = stripAnsi(tail).length;
166
+ let prevWrapRows = 0;
167
+ // Only rewrite the input line (and any wrapped continuation rows). The
168
+ // question header above is never touched, so redraws can't stack copies
169
+ // even if the initial write scrolled the viewport.
163
170
  const redraw = () => {
164
- stdout.write("\x1B8\x1B[J" + question + renderSegments(segs));
171
+ const cols = stdout.columns || 80;
172
+ if (prevWrapRows > 0)
173
+ stdout.write(`\x1B[${prevWrapRows}A`);
174
+ stdout.write("\r\x1B[J");
175
+ const rendered = renderSegments(segs);
176
+ stdout.write(tail + rendered);
177
+ const visible = tailVisibleLen + stripAnsi(rendered).length;
178
+ prevWrapRows = visible > 0 ? Math.floor((visible - 1) / cols) : 0;
165
179
  };
166
- stdout.write("\x1B7");
167
180
  stdout.write(question);
168
181
  stdout.write("\x1B[?2004h");
169
182
  try {
@@ -188,7 +201,8 @@ export function ask(question) {
188
201
  redraw();
189
202
  continue;
190
203
  }
191
- for (const ch of seg.text) {
204
+ for (let ci = 0; ci < seg.text.length; ci++) {
205
+ const ch = seg.text[ci];
192
206
  if (ch === "\r" || ch === "\n") {
193
207
  stdout.write("\n");
194
208
  cleanup();
@@ -202,11 +216,20 @@ export function ask(question) {
202
216
  }
203
217
  if (ch === "\x7F" || ch === "\b") {
204
218
  backspaceSegments(segs);
219
+ redraw();
220
+ continue;
221
+ }
222
+ // Skip ESC and any bytes that are part of an ANSI escape sequence
223
+ // (arrow keys, function keys, etc. arrive as \x1B [ ... letter)
224
+ if (ch === "\x1B") {
205
225
  continue;
206
226
  }
207
227
  const code = ch.charCodeAt(0);
208
- if (ch !== "\x1B" && code >= 0x20)
209
- appendCharToSegments(segs, ch);
228
+ if (code < 0x20)
229
+ continue; // control chars
230
+ if (code >= 0x7F && code < 0xA0)
231
+ continue; // DEL + C1 controls
232
+ appendCharToSegments(segs, ch);
210
233
  }
211
234
  redraw();
212
235
  }
@@ -241,15 +264,10 @@ export async function select(label, items, defaultIdx = 0) {
241
264
  };
242
265
  const handler = (buf) => {
243
266
  const s = buf.toString();
244
- if (s === "\x1B[A") {
245
- idx = (idx - 1 + items.length) % items.length;
246
- draw();
247
- }
248
- else if (s === "\x1B[B") {
249
- idx = (idx + 1) % items.length;
250
- draw();
251
- }
252
- else if (s === "\r")
267
+ // Ignore ANSI escape sequences (arrow keys etc.)
268
+ if (s[0] === "\x1B")
269
+ return;
270
+ if (s === "\r")
253
271
  done(items[idx].value);
254
272
  else if (s === "\x03") {
255
273
  stdin.setRawMode(false);
@@ -277,6 +295,9 @@ export async function selectKey(label, options) {
277
295
  stdin.resume();
278
296
  const handler = (buf) => {
279
297
  const s = buf.toString().toLowerCase();
298
+ // Ignore ANSI escape sequences
299
+ if (s[0] === "\x1B")
300
+ return;
280
301
  if (s === "\x03") {
281
302
  stdin.setRawMode(false);
282
303
  process.exit(0);
@@ -288,7 +309,7 @@ export async function selectKey(label, options) {
288
309
  resolve(keys[0]);
289
310
  return;
290
311
  }
291
- if (keys.includes(s)) {
312
+ if (s.length === 1 && keys.includes(s)) {
292
313
  stdin.setRawMode(false);
293
314
  stdin.removeListener("data", handler);
294
315
  stdin.pause();
package/dist/index.js CHANGED
@@ -166,7 +166,7 @@ async function main() {
166
166
  }
167
167
  if (argv.includes("-h") || argv.includes("--help")) {
168
168
  console.log(`
169
- ${chalk.bold("🌙 claude-overnight")} ${chalk.dim("— fire off Claude agents, come back to shipped work")}
169
+ ${chalk.bold("🌙 claude-overnight")} ${chalk.dim("— background lane for your Claude Max plan")}
170
170
  ${chalk.dim("─".repeat(60))}
171
171
 
172
172
  ${chalk.cyan("Usage")}
@@ -153,8 +153,14 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
153
153
  const ev = msg.event;
154
154
  if (ev?.type === "content_block_start" && ev.content_block?.type === "tool_use") {
155
155
  toolCount++;
156
- lastLogText = ev.content_block.name;
157
- onLog(ev.content_block.name, "event");
156
+ const toolName = ev.content_block.name;
157
+ const input = ev.content_block.input;
158
+ // Enrich event with target file/path for readability
159
+ const target = input?.path ?? input?.file_path ?? input?.command
160
+ ? (typeof input?.command === "string" ? input.command.split(" ").slice(0, 3).join(" ") : "")
161
+ : "";
162
+ lastLogText = target ? `${toolName} ${target}` : toolName;
163
+ onLog(target ? `${toolName} → ${target}` : toolName, "event");
158
164
  }
159
165
  if (ev?.type === "content_block_delta") {
160
166
  const delta = ev.delta;
package/dist/planner.js CHANGED
@@ -212,7 +212,9 @@ export function buildThinkingTasks(objective, themes, designDir, plannerModel, p
212
212
  const prevBlock = previousKnowledge ? `\nKNOWLEDGE FROM PREVIOUS RUNS:\n${previousKnowledge}\n\nBuild on this — don't re-discover what's already known.\n` : "";
213
213
  return themes.map((theme, i) => ({
214
214
  id: `think-${i}`,
215
- prompt: `You are a senior architect exploring a codebase to design a solution.
215
+ prompt: `## Research: ${theme}
216
+
217
+ You are a senior architect exploring a codebase to design a solution.
216
218
 
217
219
  OVERALL OBJECTIVE: ${objective}
218
220
  ${prevBlock}
package/dist/render.d.ts CHANGED
@@ -10,7 +10,7 @@ type RLGetter = () => {
10
10
  windows: Map<string, RateLimitWindow>;
11
11
  resetsAt?: number;
12
12
  };
13
- export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo): string;
13
+ export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo, selectedAgentId?: number): string;
14
14
  export interface SteeringViewData {
15
15
  /** The ephemeral ticker heartbeat — elapsed, tool count, cost, current reasoning snippet. */
16
16
  statusLine: string;
package/dist/render.js CHANGED
@@ -138,7 +138,7 @@ function renderUsageBars(out, w, swarm) {
138
138
  out.push(` ${chalk.dim("Extra ")}${barStr} ${label}`);
139
139
  }
140
140
  }
141
- export function renderFrame(swarm, showHotkeys, runInfo) {
141
+ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
142
142
  const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
143
143
  const out = [];
144
144
  const stoppingTag = swarm.aborted ? chalk.yellow("STOPPING") : "";
@@ -175,10 +175,38 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
175
175
  out.push(chalk.gray(" # Status Task" + " ".repeat(Math.max(1, w - 56)) + "Action"));
176
176
  out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 100))));
177
177
  for (const a of show)
178
- out.push(fmtRow(a, w));
178
+ out.push(fmtRow(a, w, a.id === (selectedAgentId ?? -1)));
179
179
  if (swarm.pending > 0)
180
180
  out.push(chalk.gray(` ... + ${swarm.pending} queued`));
181
181
  }
182
+ // ── Agent detail (progressive discovery) ──
183
+ const detailAgent = selectedAgentId != null
184
+ ? swarm.agents.find(a => a.id === selectedAgentId)
185
+ : undefined;
186
+ if (detailAgent) {
187
+ out.push("");
188
+ section(out, w, `Agent ${detailAgent.id} detail \u00b7 [d] next \u00b7 [Esc] close`);
189
+ const taskLines = detailAgent.task.prompt.split("\n");
190
+ const maxTaskLines = Math.min(6, taskLines.length);
191
+ for (let i = 0; i < maxTaskLines; i++) {
192
+ out.push(` ${chalk.dim(truncate(taskLines[i].trim(), w - 6))}`);
193
+ }
194
+ if (taskLines.length > maxTaskLines)
195
+ out.push(chalk.dim(` \u2026 + ${taskLines.length - maxTaskLines} more lines`));
196
+ const meta = [];
197
+ if (detailAgent.currentTool)
198
+ meta.push(chalk.yellow(`tool: ${detailAgent.currentTool}`));
199
+ if (detailAgent.lastText)
200
+ meta.push(chalk.dim(truncate(detailAgent.lastText, 60)));
201
+ if (detailAgent.filesChanged != null)
202
+ meta.push(chalk.dim(`${detailAgent.filesChanged} files`));
203
+ if (detailAgent.costUsd != null)
204
+ meta.push(chalk.yellow(`$${detailAgent.costUsd.toFixed(3)}`));
205
+ if (detailAgent.toolCalls > 0)
206
+ meta.push(chalk.dim(`${detailAgent.toolCalls} tools`));
207
+ if (meta.length > 0)
208
+ out.push(` ${meta.join(chalk.dim(" \u00b7 "))}`);
209
+ }
182
210
  // Merge results
183
211
  if (swarm.mergeResults.length > 0) {
184
212
  out.push("");
@@ -192,11 +220,21 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
192
220
  // Event log
193
221
  out.push("");
194
222
  out.push(chalk.gray(" \u2500\u2500\u2500 Events " + "\u2500".repeat(Math.min(w - 16, 90))));
195
- const logN = Math.min(10, swarm.logs.length);
223
+ const logN = Math.min(12, swarm.logs.length);
196
224
  for (const entry of swarm.logs.slice(-logN)) {
197
225
  const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
198
226
  const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
199
- out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
227
+ // Tool-use events with target get a secondary detail line
228
+ const arrowIdx = entry.text.indexOf(" \u2192 ");
229
+ if (arrowIdx > 0 && arrowIdx < 20) {
230
+ const toolName = entry.text.slice(0, arrowIdx);
231
+ const target = entry.text.slice(arrowIdx + 3);
232
+ out.push(chalk.gray(` ${t} `) + tag + ` ${chalk.yellow(toolName)}`);
233
+ out.push(chalk.dim(` ${truncate(target, w - 10)}`));
234
+ }
235
+ else {
236
+ out.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, w - 22))}`);
237
+ }
200
238
  }
201
239
  if (showHotkeys) {
202
240
  const pending = runInfo?.pendingSteer ?? 0;
@@ -204,7 +242,9 @@ export function renderFrame(swarm, showHotkeys, runInfo) {
204
242
  const fixChip = swarm.failed > 0 && swarm.active > 0 ? chalk.yellow(" [f] fix") : "";
205
243
  const retryChip = swarm.rateLimitPaused > 0 ? chalk.yellow(" [r] retry-now") : "";
206
244
  const pauseLabel = swarm.paused ? "[p] resume" : "[p] pause";
207
- out.push(chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip);
245
+ const detailChip = swarm.active > 0 ? chalk.dim(" [d] detail") : "";
246
+ const selectChip = swarm.active > 0 && running.length <= 10 ? chalk.dim(" [0-9] select") : "";
247
+ out.push(chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip);
208
248
  if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
209
249
  out.push(chalk.yellow(` all workers rate-limited — [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
210
250
  }
@@ -317,14 +357,24 @@ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
317
357
  out.push("");
318
358
  }
319
359
  section(out, w, "Planner activity");
320
- const events = data.events.slice(-10);
360
+ const events = data.events.slice(-15);
321
361
  if (events.length === 0) {
322
362
  out.push(chalk.dim(" (waiting for planner\u2026)"));
323
363
  }
324
364
  else {
325
365
  for (const e of events) {
326
366
  const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
327
- out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, w - 22)));
367
+ // Tool-use events with target get a secondary detail line
368
+ const arrowIdx = e.text.indexOf(" \u2192 ");
369
+ if (arrowIdx > 0 && arrowIdx < 30) {
370
+ const toolName = e.text.slice(0, arrowIdx);
371
+ const target = e.text.slice(arrowIdx + 3);
372
+ out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + chalk.yellow(toolName));
373
+ out.push(chalk.dim(` ${truncate(target, w - 10)}`));
374
+ }
375
+ else {
376
+ out.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, w - 22)));
377
+ }
328
378
  }
329
379
  }
330
380
  out.push("");
@@ -383,8 +433,8 @@ export function renderSummary(swarm) {
383
433
  return out.join("\n");
384
434
  }
385
435
  // ── Row formatting ──
386
- function fmtRow(a, w) {
387
- const id = String(a.id).padStart(3);
436
+ function fmtRow(a, w, selected = false) {
437
+ const id = selected ? chalk.cyan.bold(String(a.id).padStart(3)) : String(a.id).padStart(3);
388
438
  const elapsed = a.status === "running" && a.startedAt ? " " + chalk.dim(fmtDur(Date.now() - a.startedAt)) : "";
389
439
  const spin = SPINNER[Math.floor(Date.now() / 250) % SPINNER.length];
390
440
  const icon = a.status === "running"
package/dist/swarm.js CHANGED
@@ -629,7 +629,9 @@ export class Swarm {
629
629
  if (cb?.type === "tool_use") {
630
630
  agent.currentTool = cb.name;
631
631
  agent.toolCalls++;
632
- this.log(agent.id, cb.name);
632
+ const input = cb.input;
633
+ const target = input?.path ?? input?.file_path ?? (typeof input?.command === "string" ? input.command.split(" ").slice(0, 3).join(" ") : "");
634
+ this.log(agent.id, target ? `${cb.name} \u2192 ${target}` : cb.name);
633
635
  }
634
636
  }
635
637
  else if (ev.type === "content_block_delta") {
package/dist/ui.d.ts CHANGED
@@ -68,6 +68,9 @@ export declare class RunDisplay {
68
68
  private lastCompleted;
69
69
  private askState?;
70
70
  private askBusy;
71
+ private askTempFile?;
72
+ /** ID of the agent whose detail panel is open; undefined = no detail shown. */
73
+ private selectedAgentId?;
71
74
  private onSteer?;
72
75
  private onAsk?;
73
76
  constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
@@ -78,6 +81,15 @@ export declare class RunDisplay {
78
81
  setAsk(state: AskState | undefined): void;
79
82
  /** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
80
83
  setAskBusy(busy: boolean): void;
84
+ /** Cycle the selected agent detail to the next running agent (or first running if none selected). */
85
+ cycleSelectedAgent(): void;
86
+ /** Select a specific agent by ID for the detail panel. */
87
+ selectAgent(id: number): void;
88
+ /** Clear the agent detail panel. */
89
+ clearSelectedAgent(): void;
90
+ private clearAskTempFile;
91
+ /** Get the currently selected agent's ID for rendering. */
92
+ getSelectedAgentId(): number | undefined;
81
93
  start(): void;
82
94
  setWave(swarm: Swarm): void;
83
95
  setSteering(rlGetter?: RLGetter, ctx?: SteeringContext): void;
package/dist/ui.js CHANGED
@@ -1,8 +1,14 @@
1
1
  import chalk from "chalk";
2
2
  import { renderFrame, renderSteeringFrame } from "./render.js";
3
3
  import { splitPaste, segmentsToString, renderSegments, appendCharToSegments, appendPasteToSegments, backspaceSegments, } from "./cli.js";
4
+ import { mkdtempSync, writeFileSync, rmSync } from "fs";
5
+ import { tmpdir } from "os";
6
+ import { join } from "path";
7
+ import { execSync } from "child_process";
4
8
  const MAX_STEERING_EVENTS = 60;
5
9
  const MAX_INPUT_LEN = 600;
10
+ const MAX_ASK_LINES = 40;
11
+ let askTempDir;
6
12
  export class RunDisplay {
7
13
  runInfo;
8
14
  liveConfig;
@@ -22,6 +28,9 @@ export class RunDisplay {
22
28
  lastCompleted = -1;
23
29
  askState;
24
30
  askBusy = false;
31
+ askTempFile;
32
+ /** ID of the agent whose detail panel is open; undefined = no detail shown. */
33
+ selectedAgentId;
25
34
  onSteer;
26
35
  onAsk;
27
36
  constructor(runInfo, liveConfig, callbacks) {
@@ -32,9 +41,69 @@ export class RunDisplay {
32
41
  this.isTTY = !!process.stdout.isTTY;
33
42
  }
34
43
  /** Replace the ask state. Called by run.ts as the side query streams and completes. */
35
- setAsk(state) { this.askState = state; }
44
+ setAsk(state) {
45
+ this.askState = state;
46
+ // Clean up previous temp file
47
+ this.clearAskTempFile();
48
+ // Write full answer to temp file when streaming is done and answer is long
49
+ if (state && !state.streaming && !state.error && state.answer) {
50
+ const lines = state.answer.split("\n");
51
+ if (lines.length > MAX_ASK_LINES) {
52
+ try {
53
+ askTempDir = mkdtempSync(join(tmpdir(), "overnight-ask-"));
54
+ this.askTempFile = join(askTempDir, "answer.txt");
55
+ writeFileSync(this.askTempFile, state.answer, "utf8");
56
+ }
57
+ catch { }
58
+ }
59
+ }
60
+ }
36
61
  /** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
37
62
  setAskBusy(busy) { this.askBusy = busy; }
63
+ /** Cycle the selected agent detail to the next running agent (or first running if none selected). */
64
+ cycleSelectedAgent() {
65
+ if (!this.swarm)
66
+ return;
67
+ const running = this.swarm.agents.filter(a => a.status === "running");
68
+ if (running.length === 0) {
69
+ this.selectedAgentId = undefined;
70
+ return;
71
+ }
72
+ if (this.selectedAgentId == null) {
73
+ this.selectedAgentId = running[0].id;
74
+ return;
75
+ }
76
+ const idx = running.findIndex(a => a.id === this.selectedAgentId);
77
+ this.selectedAgentId = running[(idx + 1) % running.length].id;
78
+ }
79
+ /** Select a specific agent by ID for the detail panel. */
80
+ selectAgent(id) {
81
+ if (!this.swarm)
82
+ return;
83
+ const agent = this.swarm.agents.find(a => a.id === id);
84
+ if (agent && agent.status === "running")
85
+ this.selectedAgentId = id;
86
+ }
87
+ /** Clear the agent detail panel. */
88
+ clearSelectedAgent() { this.selectedAgentId = undefined; }
89
+ clearAskTempFile() {
90
+ if (this.askTempFile) {
91
+ try {
92
+ rmSync(this.askTempFile, { force: true });
93
+ }
94
+ catch { }
95
+ if (askTempDir) {
96
+ try {
97
+ rmSync(askTempDir, { recursive: true, force: true });
98
+ }
99
+ catch { }
100
+ }
101
+ this.askTempFile = undefined;
102
+ askTempDir = undefined;
103
+ }
104
+ }
105
+ /** Get the currently selected agent's ID for rendering. */
106
+ getSelectedAgentId() { return this.selectedAgentId; }
38
107
  start() {
39
108
  if (this.started)
40
109
  return;
@@ -102,6 +171,8 @@ export class RunDisplay {
102
171
  process.stdout.write("\x1B[?25h");
103
172
  }
104
173
  catch { }
174
+ // Clean up ask temp file
175
+ this.clearAskTempFile();
105
176
  this.started = false;
106
177
  }
107
178
  resumeInterval() {
@@ -135,7 +206,7 @@ export class RunDisplay {
135
206
  render() {
136
207
  let frame = "";
137
208
  if (this.swarm) {
138
- frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo);
209
+ frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo, this.selectedAgentId);
139
210
  }
140
211
  else if (this.steeringActive) {
141
212
  frame = renderSteeringFrame(this.runInfo, {
@@ -188,10 +259,18 @@ export class RunDisplay {
188
259
  out.push(` ${chalk.dim("A: " + (a.answer || "thinking..."))}`);
189
260
  }
190
261
  else {
191
- const lines = a.answer.split("\n").slice(0, 20);
192
- out.push(` ${chalk.bold.green("A:")} ${lines[0] || ""}`);
193
- for (const ln of lines.slice(1))
262
+ const allLines = a.answer.split("\n");
263
+ const showLines = allLines.slice(0, MAX_ASK_LINES);
264
+ out.push(` ${chalk.bold.green("A:")} ${showLines[0] || ""}`);
265
+ for (const ln of showLines.slice(1))
194
266
  out.push(` ${ln}`);
267
+ if (allLines.length > MAX_ASK_LINES) {
268
+ const overflow = allLines.length - MAX_ASK_LINES;
269
+ out.push(chalk.dim(` \u2026 + ${overflow} more lines`));
270
+ if (this.askTempFile) {
271
+ out.push(chalk.dim(" \u23CE Enter to reveal full answer in Finder"));
272
+ }
273
+ }
195
274
  }
196
275
  return "\n" + out.join("\n");
197
276
  }
@@ -249,6 +328,18 @@ export class RunDisplay {
249
328
  /** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw. */
250
329
  handleTyped(s) {
251
330
  const lc = this.liveConfig;
331
+ // Enter in hotkey mode reveals truncated ask answer in Finder
332
+ if (this.inputMode === "none" && this.askTempFile) {
333
+ for (const ch of s) {
334
+ if (ch === "\r" || ch === "\n") {
335
+ try {
336
+ execSync(`open -R ${JSON.stringify(this.askTempFile)}`);
337
+ }
338
+ catch { }
339
+ return true;
340
+ }
341
+ }
342
+ }
252
343
  if (this.inputMode === "budget" || this.inputMode === "threshold" || this.inputMode === "concurrency" || this.inputMode === "extra") {
253
344
  let dirty = false;
254
345
  for (const ch of s) {
@@ -282,7 +373,13 @@ export class RunDisplay {
282
373
  this.inputSegs = [];
283
374
  return true;
284
375
  }
285
- if (ch === "\x1B" || ch === "\x03") {
376
+ if (ch === "\x03") {
377
+ this.inputMode = "none";
378
+ this.inputSegs = [];
379
+ return true;
380
+ }
381
+ // ESC cancels input mode
382
+ if (ch === "\x1B") {
286
383
  this.inputMode = "none";
287
384
  this.inputSegs = [];
288
385
  return true;
@@ -301,7 +398,8 @@ export class RunDisplay {
301
398
  }
302
399
  if (this.inputMode === "steer" || this.inputMode === "ask") {
303
400
  let dirty = false;
304
- for (const ch of s) {
401
+ for (let ci = 0; ci < s.length; ci++) {
402
+ const ch = s[ci];
305
403
  if (ch === "\r" || ch === "\n") {
306
404
  const text = segmentsToString(this.inputSegs).trim();
307
405
  const wasAsk = this.inputMode === "ask";
@@ -320,10 +418,18 @@ export class RunDisplay {
320
418
  this.inputSegs = [];
321
419
  return true;
322
420
  }
323
- // Ignore raw ESC onlylet ANSI sequences (arrows etc.) fall through
324
- if (ch === "\x1B" && s.length === 1) {
421
+ // ESC cancelsconsume this byte and any following ANSI sequence bytes
422
+ if (ch === "\x1B") {
325
423
  this.inputMode = "none";
326
424
  this.inputSegs = [];
425
+ // Skip any remaining ANSI sequence bytes (e.g. [A for arrow keys)
426
+ while (ci + 1 < s.length) {
427
+ const next = s[ci + 1];
428
+ const nc = next.charCodeAt(0);
429
+ ci++;
430
+ if ((nc >= 0x40 && nc <= 0x7E) || nc === 0x7F)
431
+ break; // final byte
432
+ }
327
433
  return true;
328
434
  }
329
435
  if (ch === "\x7F" || ch === "\b") {
@@ -332,6 +438,10 @@ export class RunDisplay {
332
438
  continue;
333
439
  }
334
440
  const code = ch.charCodeAt(0);
441
+ if (code < 0x20)
442
+ continue; // control chars
443
+ if (code >= 0x7F && code < 0xA0)
444
+ continue; // DEL + C1 controls
335
445
  if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
336
446
  appendCharToSegments(this.inputSegs, ch);
337
447
  dirty = true;
@@ -339,17 +449,27 @@ export class RunDisplay {
339
449
  }
340
450
  return dirty;
341
451
  }
342
- // Hotkey mode
343
- if (s === "\x1B" && this.askState && !this.askState.streaming) {
452
+ // Hotkey mode — only accept single printable ASCII characters
453
+ // Skip ESC and ANSI sequences entirely
454
+ if (s.length > 1 && (s[0] === "\x1B" || s.charCodeAt(0) < 0x20))
455
+ return false;
456
+ if (s.length !== 1)
457
+ return false;
458
+ const key = s[0];
459
+ const code = key.charCodeAt(0);
460
+ if (code < 0x20 || code > 0x7E)
461
+ return false;
462
+ if (key === "\x1B" && this.askState && !this.askState.streaming) {
344
463
  this.askState = undefined;
464
+ this.clearAskTempFile();
345
465
  return false;
346
466
  }
347
- if (s === "b" || s === "B") {
467
+ if (key === "b" || key === "B") {
348
468
  this.inputMode = "budget";
349
469
  this.inputSegs = [];
350
470
  return true;
351
471
  }
352
- if (s === "t" || s === "T") {
472
+ if (key === "t" || key === "T") {
353
473
  if (this.swarm) {
354
474
  this.inputMode = "threshold";
355
475
  this.inputSegs = [];
@@ -357,7 +477,7 @@ export class RunDisplay {
357
477
  }
358
478
  return false;
359
479
  }
360
- if (s === "c" || s === "C") {
480
+ if (key === "c" || key === "C") {
361
481
  if (this.swarm) {
362
482
  this.inputMode = "concurrency";
363
483
  this.inputSegs = [];
@@ -365,7 +485,7 @@ export class RunDisplay {
365
485
  }
366
486
  return false;
367
487
  }
368
- if (s === "e" || s === "E") {
488
+ if (key === "e" || key === "E") {
369
489
  if (this.swarm) {
370
490
  this.inputMode = "extra";
371
491
  this.inputSegs = [];
@@ -373,7 +493,7 @@ export class RunDisplay {
373
493
  }
374
494
  return false;
375
495
  }
376
- if (s === "p" || s === "P") {
496
+ if (key === "p" || key === "P") {
377
497
  if (this.swarm) {
378
498
  const next = !this.swarm.paused;
379
499
  this.swarm.setPaused(next);
@@ -383,29 +503,52 @@ export class RunDisplay {
383
503
  }
384
504
  return false;
385
505
  }
386
- if ((s === "f" || s === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
506
+ if ((key === "f" || key === "F") && this.swarm && this.swarm.failed > 0 && this.swarm.active > 0) {
387
507
  this.swarm.requeueFailed();
388
508
  return false;
389
509
  }
390
- if ((s === "r" || s === "R") && this.swarm && this.swarm.rateLimitPaused > 0) {
510
+ if ((key === "r" || key === "R") && this.swarm && this.swarm.rateLimitPaused > 0) {
391
511
  this.swarm.retryRateLimitNow();
392
512
  return true;
393
513
  }
394
- if ((s === "s" || s === "S") && this.onSteer) {
514
+ if ((key === "s" || key === "S") && this.onSteer) {
395
515
  this.inputMode = "steer";
396
516
  this.inputSegs = [];
397
517
  return true;
398
518
  }
399
- if (s === "?" && this.onAsk && this.swarm && !this.askBusy) {
519
+ if (key === "?" && this.onAsk && this.swarm && !this.askBusy) {
400
520
  if (this.askState && !this.askState.streaming) {
401
521
  this.askState = undefined;
522
+ this.clearAskTempFile();
402
523
  return false;
403
524
  }
404
525
  this.inputMode = "ask";
405
526
  this.inputSegs = [];
406
527
  return true;
407
528
  }
408
- if (s === "q" || s === "Q" || s === "\x03") {
529
+ // [d] cycle agent detail panel
530
+ if ((key === "d" || key === "D") && this.swarm && this.swarm.active > 0) {
531
+ if (this.selectedAgentId != null)
532
+ this.cycleSelectedAgent();
533
+ else
534
+ this.cycleSelectedAgent();
535
+ return true;
536
+ }
537
+ // ESC closes detail panel
538
+ if (key === "\x1B" && this.selectedAgentId != null) {
539
+ this.clearSelectedAgent();
540
+ return true;
541
+ }
542
+ // Number keys 0-9 select a specific agent by row index in the visible table
543
+ if (/^[0-9]$/.test(key) && this.swarm) {
544
+ const n = parseInt(key);
545
+ const running = this.swarm.agents.filter(a => a.status === "running");
546
+ if (n < running.length) {
547
+ this.selectAgent(running[n].id);
548
+ return true;
549
+ }
550
+ }
551
+ if (key === "q" || key === "Q" || key === "\x03") {
409
552
  if (this.swarm) {
410
553
  if (this.swarm.aborted)
411
554
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-overnight",
3
- "version": "1.16.5",
3
+ "version": "1.16.9",
4
4
  "description": "Background lane for your Claude Max plan. Parallel Claude Agent SDK sessions in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Opus/Sonnet/Haiku + Qwen/OpenRouter.",
5
5
  "type": "module",
6
6
  "bin": {