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 +38 -17
- package/dist/index.js +1 -1
- package/dist/planner-query.js +8 -2
- package/dist/planner.js +3 -1
- package/dist/render.d.ts +1 -1
- package/dist/render.js +59 -9
- package/dist/swarm.js +3 -1
- package/dist/ui.d.ts +12 -0
- package/dist/ui.js +164 -21
- package/package.json +1 -1
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
|
-
|
|
162
|
-
|
|
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.
|
|
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 (
|
|
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 (
|
|
209
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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("—
|
|
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")}
|
package/dist/planner-query.js
CHANGED
|
@@ -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
|
-
|
|
157
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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(-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
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 === "\
|
|
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 (
|
|
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
|
-
//
|
|
324
|
-
if (ch === "\x1B"
|
|
421
|
+
// ESC cancels — consume 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
|
-
|
|
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 (
|
|
467
|
+
if (key === "b" || key === "B") {
|
|
348
468
|
this.inputMode = "budget";
|
|
349
469
|
this.inputSegs = [];
|
|
350
470
|
return true;
|
|
351
471
|
}
|
|
352
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 ((
|
|
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 ((
|
|
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 ((
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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": {
|