claude-overnight 1.16.11 → 1.16.15
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/README.md +32 -7
- package/dist/cli.js +5 -3
- package/dist/index.js +1 -1
- package/dist/providers.js +3 -3
- package/dist/render.d.ts +31 -0
- package/dist/render.js +232 -139
- package/dist/run.js +57 -7
- package/dist/ui.d.ts +23 -1
- package/dist/ui.js +233 -43
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -8,13 +8,38 @@ Isolated by default. Every agent runs in its own git worktree on its own branch,
|
|
|
8
8
|
|
|
9
9
|
Different shape from hosted agent harnesses like [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview): instead of one agent in one cloud container billed separately, you get many parallel sessions on your own machine, in your real repo, against your own Max plan (or API key). Works with Claude Opus, Sonnet, and Haiku — or pair an Anthropic planner with a cheaper executor on Qwen, OpenRouter, or any Anthropic-compatible endpoint.
|
|
10
10
|
|
|
11
|
+
## Run on Qwen 3.6 Plus
|
|
12
|
+
|
|
13
|
+
Hit your Claude Max plan limits? Running on a tight budget? Qwen 3.6 Plus via Alibaba Cloud's DashScope gateway is a drop-in executor that speaks the Anthropic Messages API — same client, same flow, pennies per run.
|
|
14
|
+
|
|
15
|
+
1. **Get an API key.** Sign up at [Alibaba Cloud](https://account.alibabacloud.com/login/login.htm?oauth_callback=https%3A%2F%2Fmodelstudio.console.alibabacloud.com%2Fap-southeast-1%3Ftab%3Ddashboard%23%2Fapi-key&clearRedirectCookie=1) — the link takes you straight to the API key dashboard.
|
|
16
|
+
2. **Configure the provider.** Run `claude-overnight`, choose `Other…` on the executor step, and fill in:
|
|
17
|
+
|
|
18
|
+
| Field | Value |
|
|
19
|
+
|---|---|
|
|
20
|
+
| Name | `Qwen 3.6 Plus` |
|
|
21
|
+
| Base URL | `https://dashscope-intl.aliyuncs.com/apps/anthropic` |
|
|
22
|
+
| Model id | `qwen3.6-plus` |
|
|
23
|
+
| API key | your DashScope key |
|
|
24
|
+
|
|
25
|
+
3. That's it. Planner runs on Sonnet (or Opus), executor runs on Qwen.
|
|
26
|
+
|
|
27
|
+
Or set it via env directly:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export ANTHROPIC_BASE_URL="https://dashscope-intl.aliyuncs.com/apps/anthropic"
|
|
31
|
+
export ANTHROPIC_API_KEY="sk-..."
|
|
32
|
+
export ANTHROPIC_MODEL="qwen3.6-plus"
|
|
33
|
+
claude-overnight
|
|
34
|
+
```
|
|
35
|
+
|
|
11
36
|
## Install
|
|
12
37
|
|
|
13
38
|
```bash
|
|
14
39
|
npm install -g claude-overnight
|
|
15
40
|
```
|
|
16
41
|
|
|
17
|
-
Requires Node.js ≥ 20 and Claude authentication (`claude auth login` or `ANTHROPIC_API_KEY`).
|
|
42
|
+
Requires Node.js ≥ 20 and Claude authentication (`claude auth login` or `ANTHROPIC_API_KEY`). No Anthropic plan or key? See **Run on Qwen 3.6 Plus** above — a cheap, drop-in alternative.
|
|
18
43
|
|
|
19
44
|
## Quick start
|
|
20
45
|
|
|
@@ -35,7 +60,7 @@ claude-overnight
|
|
|
35
60
|
● Opus — Opus 4.6 · Most capable
|
|
36
61
|
○ Sonnet — Sonnet 4.6 · Best for everyday tasks
|
|
37
62
|
|
|
38
|
-
⑤ Executor model (what runs the tasks — Qwen/OpenRouter/etc via Other…):
|
|
63
|
+
⑤ Executor model (what runs the tasks — Qwen 3.6 Plus / OpenRouter / etc via Other…):
|
|
39
64
|
● Sonnet — Sonnet 4.6 · Best for everyday tasks
|
|
40
65
|
○ Opus — Opus 4.6 · Most capable
|
|
41
66
|
○ Other… · custom OpenAI/Anthropic-compatible endpoint
|
|
@@ -232,14 +257,14 @@ Planner and executor are picked separately — pair Opus-on-Anthropic for the pl
|
|
|
232
257
|
From the interactive picker, choose `Other…` on the planner or executor step:
|
|
233
258
|
|
|
234
259
|
```
|
|
235
|
-
⑤ Executor model (what runs the tasks — Qwen/OpenRouter/etc via Other…):
|
|
260
|
+
⑤ Executor model (what runs the tasks — Qwen 3.6 Plus / OpenRouter / etc via Other…):
|
|
236
261
|
○ Sonnet
|
|
237
262
|
○ Opus
|
|
238
263
|
● Other…
|
|
239
264
|
|
|
240
|
-
Name: Qwen
|
|
241
|
-
Base URL: https://dashscope-intl.aliyuncs.com/
|
|
242
|
-
Model id: qwen3-
|
|
265
|
+
Name: Qwen 3.6 Plus
|
|
266
|
+
Base URL: https://dashscope-intl.aliyuncs.com/apps/anthropic
|
|
267
|
+
Model id: qwen3.6-plus
|
|
243
268
|
API key source:
|
|
244
269
|
● Paste key now · stored plaintext in ~/.claude/claude-overnight/providers.json (0600)
|
|
245
270
|
○ Read from env var · nothing written to disk
|
|
@@ -253,7 +278,7 @@ Saved providers live user-level at `~/.claude/claude-overnight/providers.json` (
|
|
|
253
278
|
|
|
254
279
|
**Resume.** Provider ids are persisted in `run.json` and rehydrated on resume. If you deleted a provider between runs, resume refuses to start and tells you exactly which id is missing.
|
|
255
280
|
|
|
256
|
-
**Non-interactive / CI.** `claude-overnight --model=qwen3-
|
|
281
|
+
**Non-interactive / CI.** `claude-overnight --model=qwen3.6-plus` auto-resolves the model id to a saved provider — no separate `--provider` flag.
|
|
257
282
|
|
|
258
283
|
## Spend caps and usage controls
|
|
259
284
|
|
package/dist/cli.js
CHANGED
|
@@ -219,10 +219,12 @@ export function ask(question) {
|
|
|
219
219
|
redraw();
|
|
220
220
|
continue;
|
|
221
221
|
}
|
|
222
|
-
//
|
|
223
|
-
// (arrow keys, function keys, etc. arrive as \x1B [ ... letter)
|
|
222
|
+
// ESC submits the current input (same as Enter)
|
|
224
223
|
if (ch === "\x1B") {
|
|
225
|
-
|
|
224
|
+
stdout.write("\n");
|
|
225
|
+
cleanup();
|
|
226
|
+
resolve(segmentsToString(segs).trim());
|
|
227
|
+
return;
|
|
226
228
|
}
|
|
227
229
|
const code = ch.charCodeAt(0);
|
|
228
230
|
if (code < 0x20)
|
package/dist/index.js
CHANGED
|
@@ -541,7 +541,7 @@ async function main() {
|
|
|
541
541
|
const plannerPick = await pickModel(`${chalk.cyan("④")} Planner model ${chalk.dim("(thinking, steering — use your strongest)")}:`, models);
|
|
542
542
|
plannerModel = plannerPick.model;
|
|
543
543
|
plannerProvider = plannerPick.provider;
|
|
544
|
-
const workerPick = await pickModel(`${chalk.cyan("⑤")} Executor model ${chalk.dim("(what runs the tasks — Qwen/OpenRouter/etc via Other…)")}:`, models);
|
|
544
|
+
const workerPick = await pickModel(`${chalk.cyan("⑤")} Executor model ${chalk.dim("(what runs the tasks — Qwen 3.6 Plus / OpenRouter / etc via Other…)")}:`, models);
|
|
545
545
|
workerModel = workerPick.model;
|
|
546
546
|
workerProvider = workerPick.provider;
|
|
547
547
|
usageCap = await select(`${chalk.cyan("⑥")} Usage cap:`, [
|
package/dist/providers.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function pickModel(label, anthropicModels, currentModelId) {
|
|
|
92
92
|
const keySrc = p.keyEnv ? `env ${p.keyEnv}` : "stored key";
|
|
93
93
|
items.push({ name: `${p.displayName}`, value: { kind: "provider", provider: p }, hint: `${p.model} · ${keySrc}` });
|
|
94
94
|
}
|
|
95
|
-
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "
|
|
95
|
+
items.push({ name: chalk.cyan("Other…"), value: { kind: "other" }, hint: "Qwen 3.6 Plus, OpenRouter, or any Anthropic-compatible endpoint" });
|
|
96
96
|
let defaultIdx = 0;
|
|
97
97
|
if (currentModelId) {
|
|
98
98
|
const i = items.findIndex(it => {
|
|
@@ -126,11 +126,11 @@ async function promptNewProvider() {
|
|
|
126
126
|
if (!displayName)
|
|
127
127
|
return null;
|
|
128
128
|
const id = slugify(displayName);
|
|
129
|
-
const baseURLRaw = await ask(`\n ${chalk.cyan("Base URL")} ${chalk.dim("(e.g. https://dashscope-intl.aliyuncs.com/
|
|
129
|
+
const baseURLRaw = await ask(`\n ${chalk.cyan("Base URL")} ${chalk.dim("(e.g. https://dashscope-intl.aliyuncs.com/apps/anthropic for Qwen 3.6 Plus):")} `);
|
|
130
130
|
if (!baseURLRaw)
|
|
131
131
|
return null;
|
|
132
132
|
const baseURL = normalizeBaseURL(baseURLRaw);
|
|
133
|
-
const model = await ask(`\n ${chalk.cyan("Model id")} ${chalk.dim("(e.g. qwen3-
|
|
133
|
+
const model = await ask(`\n ${chalk.cyan("Model id")} ${chalk.dim("(e.g. qwen3.6-plus):")} `);
|
|
134
134
|
if (!model)
|
|
135
135
|
return null;
|
|
136
136
|
const keyMode = await select(` ${chalk.cyan("API key source")}:`, [
|
package/dist/render.d.ts
CHANGED
|
@@ -1,9 +1,40 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
2
|
import type { RateLimitWindow } from "./types.js";
|
|
3
3
|
import type { RunInfo, SteeringContext, SteeringEvent } from "./ui.js";
|
|
4
|
+
export interface Section {
|
|
5
|
+
title: string;
|
|
6
|
+
rows: string[];
|
|
7
|
+
scrollable?: boolean;
|
|
8
|
+
highlightKey?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ContentRenderer {
|
|
11
|
+
/** Returns an array of sections to render in the content area */
|
|
12
|
+
sections(): Section[];
|
|
13
|
+
}
|
|
4
14
|
export declare function truncate(s: string, max: number): string;
|
|
5
15
|
export declare function fmtTokens(n: number): string;
|
|
6
16
|
export declare function fmtDur(ms: number): string;
|
|
17
|
+
export declare function renderUnifiedFrame(params: {
|
|
18
|
+
model?: string;
|
|
19
|
+
phase: string;
|
|
20
|
+
barPct: number;
|
|
21
|
+
barLabel: string;
|
|
22
|
+
active?: number;
|
|
23
|
+
blocked?: number;
|
|
24
|
+
queued?: number;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
totalIn: number;
|
|
27
|
+
totalOut: number;
|
|
28
|
+
totalCost: number;
|
|
29
|
+
waveNum: number;
|
|
30
|
+
sessionsUsed: number;
|
|
31
|
+
sessionsBudget: number;
|
|
32
|
+
remaining: number;
|
|
33
|
+
usageBarRender?: (out: string[], w: number) => void;
|
|
34
|
+
content: ContentRenderer;
|
|
35
|
+
hotkeyRow?: string;
|
|
36
|
+
extraFooterRows?: string[];
|
|
37
|
+
}): string;
|
|
7
38
|
type RLGetter = () => {
|
|
8
39
|
utilization: number;
|
|
9
40
|
isUsingOverage: boolean;
|
package/dist/render.js
CHANGED
|
@@ -138,9 +138,57 @@ function renderUsageBars(out, w, swarm) {
|
|
|
138
138
|
out.push(` ${chalk.dim("Extra ")}${barStr} ${label}`);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
// ── Unified frame renderer ──
|
|
142
|
+
export function renderUnifiedFrame(params) {
|
|
142
143
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
143
144
|
const out = [];
|
|
145
|
+
// Header
|
|
146
|
+
renderHeader(out, w, {
|
|
147
|
+
model: params.model,
|
|
148
|
+
phase: params.phase,
|
|
149
|
+
barPct: params.barPct,
|
|
150
|
+
barLabel: params.barLabel,
|
|
151
|
+
active: params.active ?? 0,
|
|
152
|
+
blocked: params.blocked,
|
|
153
|
+
queued: params.queued ?? 0,
|
|
154
|
+
startedAt: params.startedAt,
|
|
155
|
+
totalIn: params.totalIn,
|
|
156
|
+
totalOut: params.totalOut,
|
|
157
|
+
totalCost: params.totalCost,
|
|
158
|
+
waveNum: params.waveNum,
|
|
159
|
+
sessionsUsed: params.sessionsUsed,
|
|
160
|
+
sessionsBudget: params.sessionsBudget,
|
|
161
|
+
remaining: params.remaining,
|
|
162
|
+
});
|
|
163
|
+
// Usage bar
|
|
164
|
+
if (params.usageBarRender) {
|
|
165
|
+
params.usageBarRender(out, w);
|
|
166
|
+
}
|
|
167
|
+
out.push("");
|
|
168
|
+
// Content sections
|
|
169
|
+
const sections = params.content.sections();
|
|
170
|
+
for (const sec of sections) {
|
|
171
|
+
if (sec.title) {
|
|
172
|
+
section(out, w, sec.title);
|
|
173
|
+
}
|
|
174
|
+
for (const row of sec.rows) {
|
|
175
|
+
out.push(row);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Footer
|
|
179
|
+
out.push("");
|
|
180
|
+
if (params.hotkeyRow) {
|
|
181
|
+
out.push(params.hotkeyRow);
|
|
182
|
+
}
|
|
183
|
+
if (params.extraFooterRows) {
|
|
184
|
+
for (const row of params.extraFooterRows) {
|
|
185
|
+
out.push(row);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
out.push("");
|
|
189
|
+
return out.join("\n");
|
|
190
|
+
}
|
|
191
|
+
export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
144
192
|
const stoppingTag = swarm.aborted ? chalk.yellow("STOPPING") : "";
|
|
145
193
|
const pausedTag = swarm.paused ? chalk.yellow("PAUSED") : "";
|
|
146
194
|
const stallTag = swarm.stallLevel >= 3 ? chalk.red("STALL") : swarm.stallLevel > 0 ? chalk.yellow(`STALL L${swarm.stallLevel}`) : "";
|
|
@@ -149,93 +197,90 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
|
149
197
|
: swarm.rateLimitPaused > 0 ? chalk.yellow("COOLING") : "";
|
|
150
198
|
const phase = [phaseLabel, pausedTag, stallTag, stoppingTag].filter(Boolean).join(" ");
|
|
151
199
|
const waveUsed = swarm.completed + swarm.failed;
|
|
152
|
-
renderHeader(out, w, {
|
|
153
|
-
model: runInfo?.model ?? swarm.model,
|
|
154
|
-
phase,
|
|
155
|
-
barPct: swarm.total > 0 ? swarm.completed / swarm.total : 0,
|
|
156
|
-
barLabel: `${swarm.completed}/${swarm.total}`,
|
|
157
|
-
active: swarm.active, blocked: swarm.blocked, queued: swarm.pending,
|
|
158
|
-
startedAt: runInfo?.startedAt ?? swarm.startedAt,
|
|
159
|
-
totalIn: (runInfo?.accIn ?? 0) + swarm.totalInputTokens,
|
|
160
|
-
totalOut: (runInfo?.accOut ?? 0) + swarm.totalOutputTokens,
|
|
161
|
-
totalCost: (runInfo?.accCost ?? swarm.baseCostUsd) + swarm.totalCostUsd,
|
|
162
|
-
waveNum: runInfo?.waveNum ?? -1,
|
|
163
|
-
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
164
|
-
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
165
|
-
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
166
|
-
});
|
|
167
|
-
renderUsageBars(out, w, swarm);
|
|
168
|
-
out.push("");
|
|
169
|
-
// Agent table
|
|
170
200
|
const running = swarm.agents.filter(a => a.status === "running");
|
|
171
201
|
const finished = swarm.agents.filter(a => a.status !== "running");
|
|
172
202
|
const showFinished = finished.slice(-Math.max(2, 12 - running.length));
|
|
173
203
|
const show = [...running, ...showFinished];
|
|
174
|
-
if (show.length > 0) {
|
|
175
|
-
out.push(chalk.gray(" # Status Task" + " ".repeat(Math.max(1, w - 56)) + "Action"));
|
|
176
|
-
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, 100))));
|
|
177
|
-
for (const a of show)
|
|
178
|
-
out.push(fmtRow(a, w, a.id === (selectedAgentId ?? -1)));
|
|
179
|
-
if (swarm.pending > 0)
|
|
180
|
-
out.push(chalk.gray(` ... + ${swarm.pending} queued`));
|
|
181
|
-
}
|
|
182
|
-
// ── Agent detail (progressive discovery) ──
|
|
183
204
|
const detailAgent = selectedAgentId != null
|
|
184
205
|
? swarm.agents.find(a => a.id === selectedAgentId)
|
|
185
206
|
: undefined;
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
const content = {
|
|
208
|
+
sections() {
|
|
209
|
+
const secs = [];
|
|
210
|
+
// Agent table (undecorated — raw header + rows)
|
|
211
|
+
if (show.length > 0) {
|
|
212
|
+
const rows = [
|
|
213
|
+
chalk.gray(" # Status Task" + " ".repeat(Math.max(1, (process.stdout.columns ?? 80) || 80, 60) - 56)) + "Action",
|
|
214
|
+
chalk.gray(" " + "\u2500".repeat(Math.min(Math.max((process.stdout.columns ?? 80) || 80, 60) - 4, 100))),
|
|
215
|
+
];
|
|
216
|
+
for (const a of show)
|
|
217
|
+
rows.push(fmtRow(a, (process.stdout.columns ?? 80) || 80, a.id === (selectedAgentId ?? -1)));
|
|
218
|
+
if (swarm.pending > 0)
|
|
219
|
+
rows.push(chalk.gray(` ... + ${swarm.pending} queued`));
|
|
220
|
+
secs.push({ title: "", rows });
|
|
221
|
+
}
|
|
222
|
+
// Agent detail (decorated)
|
|
223
|
+
if (detailAgent) {
|
|
224
|
+
const rows = [];
|
|
225
|
+
const taskLines = detailAgent.task.prompt.split("\n");
|
|
226
|
+
const maxTaskLines = Math.min(6, taskLines.length);
|
|
227
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
228
|
+
for (let i = 0; i < maxTaskLines; i++) {
|
|
229
|
+
rows.push(` ${chalk.dim(truncate(taskLines[i].trim(), ww - 6))}`);
|
|
230
|
+
}
|
|
231
|
+
if (taskLines.length > maxTaskLines)
|
|
232
|
+
rows.push(chalk.dim(` \u2026 + ${taskLines.length - maxTaskLines} more lines`));
|
|
233
|
+
const meta = [];
|
|
234
|
+
if (detailAgent.currentTool)
|
|
235
|
+
meta.push(chalk.yellow(`tool: ${detailAgent.currentTool}`));
|
|
236
|
+
if (detailAgent.lastText)
|
|
237
|
+
meta.push(chalk.dim(truncate(detailAgent.lastText, 60)));
|
|
238
|
+
if (detailAgent.filesChanged != null)
|
|
239
|
+
meta.push(chalk.dim(`${detailAgent.filesChanged} files`));
|
|
240
|
+
if (detailAgent.costUsd != null)
|
|
241
|
+
meta.push(chalk.yellow(`$${detailAgent.costUsd.toFixed(3)}`));
|
|
242
|
+
if (detailAgent.toolCalls > 0)
|
|
243
|
+
meta.push(chalk.dim(`${detailAgent.toolCalls} tools`));
|
|
244
|
+
if (meta.length > 0)
|
|
245
|
+
rows.push(` ${meta.join(chalk.dim(" \u00b7 "))}`);
|
|
246
|
+
secs.push({ title: `Agent ${detailAgent.id} detail \u00b7 [d] next \u00b7 [Esc] close`, rows });
|
|
247
|
+
}
|
|
248
|
+
// Merge results (undecorated)
|
|
249
|
+
if (swarm.mergeResults.length > 0) {
|
|
250
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
251
|
+
const rows = [chalk.gray(" \u2500\u2500\u2500 Merges " + "\u2500".repeat(Math.min(ww - 16, 90)))];
|
|
252
|
+
for (const mr of swarm.mergeResults) {
|
|
253
|
+
const icon = mr.ok ? chalk.green("\u2713") : chalk.red("\u2717");
|
|
254
|
+
const info = mr.ok ? chalk.dim(`${mr.filesChanged} file(s)`) : chalk.red(truncate(mr.error || "conflict", 40));
|
|
255
|
+
rows.push(` ${icon} ${mr.branch} ${info}`);
|
|
256
|
+
}
|
|
257
|
+
secs.push({ title: "", rows });
|
|
258
|
+
}
|
|
259
|
+
// Event log (undecorated)
|
|
260
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
261
|
+
const eventRows = [chalk.gray(" \u2500\u2500\u2500 Events " + "\u2500".repeat(Math.min(ww - 16, 90)))];
|
|
262
|
+
const logN = Math.min(12, swarm.logs.length);
|
|
263
|
+
for (const entry of swarm.logs.slice(-logN)) {
|
|
264
|
+
const t = new Date(entry.time).toLocaleTimeString("en", { hour12: false });
|
|
265
|
+
const tag = entry.agentId < 0 ? chalk.magenta("[sys]") : chalk.cyan(`[${entry.agentId}]`);
|
|
266
|
+
const arrowIdx = entry.text.indexOf(" \u2192 ");
|
|
267
|
+
if (arrowIdx > 0 && arrowIdx < 20) {
|
|
268
|
+
const toolName = entry.text.slice(0, arrowIdx);
|
|
269
|
+
const target = entry.text.slice(arrowIdx + 3);
|
|
270
|
+
eventRows.push(chalk.gray(` ${t} `) + tag + ` ${chalk.yellow(toolName)}`);
|
|
271
|
+
eventRows.push(chalk.dim(` ${truncate(target, ww - 10)}`));
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
eventRows.push(chalk.gray(` ${t} `) + tag + ` ${colorEvent(truncate(entry.text, ww - 22))}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
secs.push({ title: "", rows: eventRows });
|
|
278
|
+
return secs;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
// Build footer
|
|
282
|
+
let hotkeyRow;
|
|
283
|
+
const extraFooterRows = [];
|
|
239
284
|
if (showHotkeys) {
|
|
240
285
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
241
286
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
@@ -244,13 +289,32 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId) {
|
|
|
244
289
|
const pauseLabel = swarm.paused ? "[p] resume" : "[p] pause";
|
|
245
290
|
const detailChip = swarm.active > 0 ? chalk.dim(" [d] detail") : "";
|
|
246
291
|
const selectChip = swarm.active > 0 && running.length <= 10 ? chalk.dim(" [0-9] select") : "";
|
|
247
|
-
|
|
292
|
+
hotkeyRow = chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip;
|
|
248
293
|
if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
|
|
249
|
-
|
|
294
|
+
extraFooterRows.push(chalk.yellow(` all workers rate-limited — [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
|
|
250
295
|
}
|
|
251
296
|
}
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
return renderUnifiedFrame({
|
|
298
|
+
model: runInfo?.model ?? swarm.model,
|
|
299
|
+
phase,
|
|
300
|
+
barPct: swarm.total > 0 ? swarm.completed / swarm.total : 0,
|
|
301
|
+
barLabel: `${swarm.completed}/${swarm.total}`,
|
|
302
|
+
active: swarm.active,
|
|
303
|
+
blocked: swarm.blocked,
|
|
304
|
+
queued: swarm.pending,
|
|
305
|
+
startedAt: runInfo?.startedAt ?? swarm.startedAt,
|
|
306
|
+
totalIn: (runInfo?.accIn ?? 0) + swarm.totalInputTokens,
|
|
307
|
+
totalOut: (runInfo?.accOut ?? 0) + swarm.totalOutputTokens,
|
|
308
|
+
totalCost: (runInfo?.accCost ?? swarm.baseCostUsd) + swarm.totalCostUsd,
|
|
309
|
+
waveNum: runInfo?.waveNum ?? -1,
|
|
310
|
+
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
311
|
+
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
312
|
+
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
313
|
+
usageBarRender: (out, w) => renderUsageBars(out, w, swarm),
|
|
314
|
+
content,
|
|
315
|
+
hotkeyRow,
|
|
316
|
+
extraFooterRows,
|
|
317
|
+
});
|
|
254
318
|
}
|
|
255
319
|
function section(out, w, title) {
|
|
256
320
|
const inner = ` ${title} `;
|
|
@@ -324,70 +388,99 @@ function renderStatusBlock(out, w, status) {
|
|
|
324
388
|
out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
|
|
325
389
|
}
|
|
326
390
|
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter) {
|
|
327
|
-
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
328
|
-
const out = [];
|
|
329
391
|
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
330
|
-
renderHeader(out, w, {
|
|
331
|
-
model: runInfo.model,
|
|
332
|
-
phase: chalk.magenta("STEERING"),
|
|
333
|
-
barPct: runInfo.sessionsBudget > 0 ? totalUsed / runInfo.sessionsBudget : 0,
|
|
334
|
-
barLabel: `${totalUsed}/${runInfo.sessionsBudget}`,
|
|
335
|
-
active: 0, queued: 0,
|
|
336
|
-
startedAt: runInfo.startedAt,
|
|
337
|
-
totalIn: runInfo.accIn, totalOut: runInfo.accOut, totalCost: runInfo.accCost,
|
|
338
|
-
waveNum: runInfo.waveNum,
|
|
339
|
-
sessionsUsed: totalUsed, sessionsBudget: runInfo.sessionsBudget, remaining: runInfo.remaining,
|
|
340
|
-
});
|
|
341
|
-
const rl = rlGetter?.();
|
|
342
|
-
if (rl && (rl.utilization > 0 || rl.windows.size > 0))
|
|
343
|
-
renderSteeringUsageBar(out, w, rl);
|
|
344
|
-
out.push("");
|
|
345
392
|
const ctx = data.context;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
//
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
393
|
+
const content = {
|
|
394
|
+
sections() {
|
|
395
|
+
const secs = [];
|
|
396
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
397
|
+
// Objective (undecorated — raw line)
|
|
398
|
+
if (ctx?.objective) {
|
|
399
|
+
const obj = ctx.objective.replace(/\s+/g, " ").trim();
|
|
400
|
+
secs.push({ title: "", rows: [
|
|
401
|
+
` ${chalk.bold.white("Objective")} ${chalk.dim(truncate(obj, ww - 15))}`,
|
|
402
|
+
"",
|
|
403
|
+
] });
|
|
404
|
+
}
|
|
405
|
+
// Status (decorated via renderStatusBlock)
|
|
406
|
+
if (ctx?.status) {
|
|
407
|
+
const statusRows = [];
|
|
408
|
+
renderStatusBlock(statusRows, ww, ctx.status);
|
|
409
|
+
if (statusRows.length > 0) {
|
|
410
|
+
statusRows.push("");
|
|
411
|
+
secs.push({ title: "", rows: statusRows });
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
// Last wave (decorated via renderLastWave)
|
|
415
|
+
if (ctx?.lastWave && ctx.lastWave.tasks.length > 0) {
|
|
416
|
+
const lwRows = [];
|
|
417
|
+
renderLastWave(lwRows, ww, ctx.lastWave);
|
|
418
|
+
lwRows.push("");
|
|
419
|
+
secs.push({ title: "", rows: lwRows });
|
|
420
|
+
}
|
|
421
|
+
// Planner activity (decorated)
|
|
422
|
+
const plannerRows = [];
|
|
423
|
+
const events = data.events.slice(-15);
|
|
424
|
+
if (events.length === 0) {
|
|
425
|
+
plannerRows.push(chalk.dim(" (waiting for planner\u2026)"));
|
|
374
426
|
}
|
|
375
427
|
else {
|
|
376
|
-
|
|
428
|
+
for (const e of events) {
|
|
429
|
+
const t = new Date(e.time).toLocaleTimeString("en", { hour12: false });
|
|
430
|
+
const arrowIdx = e.text.indexOf(" \u2192 ");
|
|
431
|
+
if (arrowIdx > 0 && arrowIdx < 30) {
|
|
432
|
+
const toolName = e.text.slice(0, arrowIdx);
|
|
433
|
+
const target = e.text.slice(arrowIdx + 3);
|
|
434
|
+
plannerRows.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + chalk.yellow(toolName));
|
|
435
|
+
plannerRows.push(chalk.dim(` ${truncate(target, ww - 10)}`));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
plannerRows.push(chalk.gray(` ${t} `) + chalk.magenta("[plan] ") + colorEvent(truncate(e.text, ww - 22)));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
secs.push({ title: "Planner activity", rows: plannerRows });
|
|
443
|
+
// Status line (undecorated)
|
|
444
|
+
const liveClean = data.statusLine.replace(/\n/g, " ");
|
|
445
|
+
secs.push({ title: "", rows: [` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, ww - 6))}`] });
|
|
446
|
+
return secs;
|
|
447
|
+
},
|
|
448
|
+
};
|
|
449
|
+
// Usage bar
|
|
450
|
+
const usageBarRender = rlGetter
|
|
451
|
+
? (out, w) => {
|
|
452
|
+
const rl = rlGetter();
|
|
453
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0)) {
|
|
454
|
+
renderSteeringUsageBar(out, w, rl);
|
|
377
455
|
}
|
|
378
456
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
out.push(` ${chalk.cyan("\u25B6")} ${chalk.dim(truncate(liveClean, w - 6))}`);
|
|
383
|
-
out.push("");
|
|
457
|
+
: undefined;
|
|
458
|
+
// Footer
|
|
459
|
+
let hotkeyRow;
|
|
384
460
|
if (showHotkeys) {
|
|
385
461
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
386
462
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
387
|
-
|
|
463
|
+
hotkeyRow = chalk.dim(" [b] budget [s] steer [q] stop") + chip;
|
|
388
464
|
}
|
|
389
|
-
|
|
390
|
-
|
|
465
|
+
return renderUnifiedFrame({
|
|
466
|
+
model: runInfo.model,
|
|
467
|
+
phase: chalk.magenta(`STEERING \u2192 wave ${runInfo.waveNum + 2}`),
|
|
468
|
+
barPct: runInfo.sessionsBudget > 0 ? totalUsed / runInfo.sessionsBudget : 0,
|
|
469
|
+
barLabel: `${totalUsed}/${runInfo.sessionsBudget}`,
|
|
470
|
+
active: 0,
|
|
471
|
+
queued: 0,
|
|
472
|
+
startedAt: runInfo.startedAt,
|
|
473
|
+
totalIn: runInfo.accIn,
|
|
474
|
+
totalOut: runInfo.accOut,
|
|
475
|
+
totalCost: runInfo.accCost,
|
|
476
|
+
waveNum: runInfo.waveNum,
|
|
477
|
+
sessionsUsed: totalUsed,
|
|
478
|
+
sessionsBudget: runInfo.sessionsBudget,
|
|
479
|
+
remaining: runInfo.remaining,
|
|
480
|
+
usageBarRender,
|
|
481
|
+
content,
|
|
482
|
+
hotkeyRow,
|
|
483
|
+
});
|
|
391
484
|
}
|
|
392
485
|
export function renderSummary(swarm) {
|
|
393
486
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
package/dist/run.js
CHANGED
|
@@ -205,8 +205,35 @@ export async function executeRun(cfg) {
|
|
|
205
205
|
};
|
|
206
206
|
process.on("SIGINT", gracefulStop);
|
|
207
207
|
process.on("SIGTERM", gracefulStop);
|
|
208
|
-
|
|
209
|
-
|
|
208
|
+
const crashHandler = (label, detail) => {
|
|
209
|
+
// Save run state with currentTasks so resume can pick up where we left off.
|
|
210
|
+
try {
|
|
211
|
+
saveRunState(runDir, {
|
|
212
|
+
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
213
|
+
remaining, workerModel, plannerModel,
|
|
214
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
215
|
+
concurrency, permissionMode,
|
|
216
|
+
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
217
|
+
flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
218
|
+
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
219
|
+
branches, phase: "stopped", startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
// Save partial wave session if swarm was running.
|
|
224
|
+
if (currentSwarm?.agents.length) {
|
|
225
|
+
try {
|
|
226
|
+
saveWaveSession(runDir, waveNum, currentSwarm.agents, currentSwarm.totalCostUsd);
|
|
227
|
+
}
|
|
228
|
+
catch { }
|
|
229
|
+
}
|
|
230
|
+
display.stop();
|
|
231
|
+
restore();
|
|
232
|
+
console.error(chalk.red(`\n ${label}: ${detail}`));
|
|
233
|
+
process.exit(1);
|
|
234
|
+
};
|
|
235
|
+
process.on("uncaughtException", (err) => { currentSwarm?.abort(); currentSwarm?.cleanup(); crashHandler("Uncaught", err instanceof Error ? err.message : String(err)); });
|
|
236
|
+
process.on("unhandledRejection", (reason) => { currentSwarm?.abort(); currentSwarm?.cleanup(); crashHandler("Unhandled", reason instanceof Error ? reason.message : String(reason)); });
|
|
210
237
|
// Shared steering logic used by both resume-steering and in-loop steering
|
|
211
238
|
const runSteering = async () => {
|
|
212
239
|
let steered = false;
|
|
@@ -300,19 +327,33 @@ export async function executeRun(cfg) {
|
|
|
300
327
|
while (runAnotherRound) {
|
|
301
328
|
runAnotherRound = false;
|
|
302
329
|
while (remaining > 0 && currentTasks.length > 0 && !stopping) {
|
|
330
|
+
// Health check: runs once per process start to fix a broken build before
|
|
331
|
+
// any real work begins. Only triggers when there's nothing else queued —
|
|
332
|
+
// it must NEVER override tasks that steering has already planned.
|
|
303
333
|
if (!lastHealed) {
|
|
334
|
+
lastHealed = true;
|
|
335
|
+
// Only replace tasks with health fix if the queue was essentially empty.
|
|
336
|
+
// Steering-planned tasks always take priority.
|
|
304
337
|
const healTask = checkProjectHealth(cwd);
|
|
305
|
-
if (healTask && remaining > 0) {
|
|
306
|
-
lastHealed = true;
|
|
338
|
+
if (healTask && remaining > 0 && currentTasks.length <= 1) {
|
|
307
339
|
currentTasks = [healTask];
|
|
308
340
|
}
|
|
309
341
|
}
|
|
310
|
-
else {
|
|
311
|
-
lastHealed = false;
|
|
312
|
-
}
|
|
313
342
|
if (currentTasks.length > remaining)
|
|
314
343
|
currentTasks = currentTasks.slice(0, remaining);
|
|
315
344
|
syncRunInfo();
|
|
345
|
+
// Save run state BEFORE the swarm — if the process crashes mid-swarm,
|
|
346
|
+
// resume picks up currentTasks. This is the critical resilience checkpoint.
|
|
347
|
+
saveRunState(runDir, {
|
|
348
|
+
id: `run-${new Date().toISOString().slice(0, 19)}`, objective: objective ?? "", budget: cfg.budget,
|
|
349
|
+
remaining, workerModel, plannerModel,
|
|
350
|
+
workerProviderId: cfg.workerProvider?.id, plannerProviderId: cfg.plannerProvider?.id,
|
|
351
|
+
concurrency, permissionMode,
|
|
352
|
+
usageCap, allowExtraUsage: cfg.allowExtraUsage, extraUsageBudget: cfg.extraUsageBudget,
|
|
353
|
+
flex, useWorktrees, mergeStrategy, waveNum, currentTasks,
|
|
354
|
+
accCost, accCompleted, accFailed, accIn, accOut, accTools,
|
|
355
|
+
branches, phase: "steering", startedAt: new Date(cfg.runStartedAt).toISOString(), cwd,
|
|
356
|
+
});
|
|
316
357
|
const swarm = new Swarm({
|
|
317
358
|
tasks: currentTasks, concurrency, cwd, model: workerModel, permissionMode, allowedTools,
|
|
318
359
|
useWorktrees, mergeStrategy: waveMerge, agentTimeoutMs: cfg.agentTimeoutMs,
|
|
@@ -332,6 +373,15 @@ export async function executeRun(cfg) {
|
|
|
332
373
|
console.error(chalk.red(`\n Authentication failed — check your API key or run: claude auth\n`));
|
|
333
374
|
process.exit(1);
|
|
334
375
|
}
|
|
376
|
+
// Swarm crashed mid-execution — save partial results before propagating.
|
|
377
|
+
// The pre-swarm saveRunState already preserved currentTasks for resume.
|
|
378
|
+
// Also save the wave session with whatever agents completed.
|
|
379
|
+
if (swarm.agents.length > 0) {
|
|
380
|
+
try {
|
|
381
|
+
saveWaveSession(runDir, waveNum, swarm.agents, swarm.totalCostUsd);
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
}
|
|
335
385
|
throw err;
|
|
336
386
|
}
|
|
337
387
|
display.pause();
|
package/dist/ui.d.ts
CHANGED
|
@@ -71,6 +71,7 @@ export declare class RunDisplay {
|
|
|
71
71
|
private askTempFile?;
|
|
72
72
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
73
73
|
private selectedAgentId?;
|
|
74
|
+
private navState;
|
|
74
75
|
private onSteer?;
|
|
75
76
|
private onAsk?;
|
|
76
77
|
constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
|
|
@@ -87,6 +88,15 @@ export declare class RunDisplay {
|
|
|
87
88
|
selectAgent(id: number): void;
|
|
88
89
|
/** Clear the agent detail panel. */
|
|
89
90
|
clearSelectedAgent(): void;
|
|
91
|
+
/** Arrow-key navigation dispatched by the demux in handleTyped(). */
|
|
92
|
+
navigate(direction: "up" | "down" | "left" | "right" | "enter"): boolean;
|
|
93
|
+
/** Get the agents visible in the table (running + last N finished). */
|
|
94
|
+
private getVisibleAgents;
|
|
95
|
+
/** Discover sections from the current render state for navigation boundaries. */
|
|
96
|
+
private getSections;
|
|
97
|
+
private clampNavState;
|
|
98
|
+
/** Returns the unique highlight key for the currently focused row, used by renderer. */
|
|
99
|
+
getHighlightKey(): string | undefined;
|
|
90
100
|
private clearAskTempFile;
|
|
91
101
|
/** Get the currently selected agent's ID for rendering. */
|
|
92
102
|
getSelectedAgentId(): number | undefined;
|
|
@@ -112,7 +122,19 @@ export declare class RunDisplay {
|
|
|
112
122
|
private setupHotkeys;
|
|
113
123
|
/** Handle a pasted block. Returns true if the frame needs a redraw. */
|
|
114
124
|
private handlePaste;
|
|
115
|
-
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
125
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
126
|
+
*
|
|
127
|
+
* Demux pipeline — routes arrow keys and ESC BEFORE hotkey matching:
|
|
128
|
+
* Raw stdin chunk → splitPaste
|
|
129
|
+
* ├─ paste → handlePaste (existing, fine)
|
|
130
|
+
* └─ typed → demux
|
|
131
|
+
* ├─ ESC + [A/B/C/D → this.navigate("up"/"down"/"right"/"left")
|
|
132
|
+
* ├─ ESC → cancel input / close detail / dismiss panel
|
|
133
|
+
* ├─ Enter → submit / reveal / select
|
|
134
|
+
* ├─ Ctrl+C → abort
|
|
135
|
+
* ├─ Backspace → delete
|
|
136
|
+
* └─ printable → hotkey matching (b, t, c, e, p, s, q, ?, d, 0-9)
|
|
137
|
+
*/
|
|
116
138
|
private handleTyped;
|
|
117
139
|
private plainTick;
|
|
118
140
|
}
|
package/dist/ui.js
CHANGED
|
@@ -31,6 +31,7 @@ export class RunDisplay {
|
|
|
31
31
|
askTempFile;
|
|
32
32
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
33
33
|
selectedAgentId;
|
|
34
|
+
navState = { focusSection: 0, focusRow: 0, scrollOffset: 0 };
|
|
34
35
|
onSteer;
|
|
35
36
|
onAsk;
|
|
36
37
|
constructor(runInfo, liveConfig, callbacks) {
|
|
@@ -86,6 +87,160 @@ export class RunDisplay {
|
|
|
86
87
|
}
|
|
87
88
|
/** Clear the agent detail panel. */
|
|
88
89
|
clearSelectedAgent() { this.selectedAgentId = undefined; }
|
|
90
|
+
/** Arrow-key navigation dispatched by the demux in handleTyped(). */
|
|
91
|
+
navigate(direction) {
|
|
92
|
+
const sections = this.getSections();
|
|
93
|
+
const nav = this.navState;
|
|
94
|
+
const section = sections[Math.min(nav.focusSection, sections.length - 1)];
|
|
95
|
+
let changed = false;
|
|
96
|
+
switch (direction) {
|
|
97
|
+
case "up":
|
|
98
|
+
if (nav.focusRow > 0) {
|
|
99
|
+
nav.focusRow--;
|
|
100
|
+
nav.scrollOffset = Math.max(0, nav.scrollOffset - 1);
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
else if (nav.focusSection > 0) {
|
|
104
|
+
nav.focusSection--;
|
|
105
|
+
const prevSection = sections[nav.focusSection];
|
|
106
|
+
nav.focusRow = Math.max(0, prevSection.rowCount - 1);
|
|
107
|
+
changed = true;
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "down":
|
|
111
|
+
if (nav.focusRow < section.rowCount - 1) {
|
|
112
|
+
nav.focusRow++;
|
|
113
|
+
changed = true;
|
|
114
|
+
}
|
|
115
|
+
else if (nav.focusSection < sections.length - 1) {
|
|
116
|
+
nav.focusSection++;
|
|
117
|
+
nav.focusRow = 0;
|
|
118
|
+
changed = true;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
case "left":
|
|
122
|
+
if (this.selectedAgentId != null) {
|
|
123
|
+
this.clearSelectedAgent();
|
|
124
|
+
changed = true;
|
|
125
|
+
}
|
|
126
|
+
else if (nav.focusSection > 0) {
|
|
127
|
+
nav.focusSection--;
|
|
128
|
+
nav.focusRow = 0;
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
case "right":
|
|
133
|
+
if (this.swarm && this.selectedAgentId == null) {
|
|
134
|
+
const agents = this.getVisibleAgents();
|
|
135
|
+
const agent = agents[nav.focusRow];
|
|
136
|
+
if (agent && agent.status === "running") {
|
|
137
|
+
this.selectAgent(agent.id);
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (nav.focusSection < sections.length - 1) {
|
|
142
|
+
nav.focusSection++;
|
|
143
|
+
nav.focusRow = 0;
|
|
144
|
+
changed = true;
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
147
|
+
case "enter":
|
|
148
|
+
if (this.swarm) {
|
|
149
|
+
const agents = this.getVisibleAgents();
|
|
150
|
+
const agent = agents[nav.focusRow];
|
|
151
|
+
if (agent) {
|
|
152
|
+
if (this.selectedAgentId === agent.id) {
|
|
153
|
+
this.clearSelectedAgent();
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
this.selectAgent(agent.id);
|
|
157
|
+
}
|
|
158
|
+
changed = true;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
this.clampNavState(sections);
|
|
164
|
+
return changed;
|
|
165
|
+
}
|
|
166
|
+
/** Get the agents visible in the table (running + last N finished). */
|
|
167
|
+
getVisibleAgents() {
|
|
168
|
+
if (!this.swarm)
|
|
169
|
+
return [];
|
|
170
|
+
const running = this.swarm.agents.filter(a => a.status === "running");
|
|
171
|
+
const finished = this.swarm.agents.filter(a => a.status !== "running");
|
|
172
|
+
const showFinished = finished.slice(-Math.max(2, 12 - running.length));
|
|
173
|
+
return [...running, ...showFinished];
|
|
174
|
+
}
|
|
175
|
+
/** Discover sections from the current render state for navigation boundaries. */
|
|
176
|
+
getSections() {
|
|
177
|
+
const sections = [];
|
|
178
|
+
if (this.swarm) {
|
|
179
|
+
// Agent table section
|
|
180
|
+
const show = this.getVisibleAgents();
|
|
181
|
+
sections.push({
|
|
182
|
+
title: "Agents",
|
|
183
|
+
rowCount: show.length,
|
|
184
|
+
highlightKeyForRow: (row) => show[row]?.id != null ? `agent-${show[row].id}` : undefined,
|
|
185
|
+
});
|
|
186
|
+
// Agent detail section
|
|
187
|
+
if (this.selectedAgentId != null) {
|
|
188
|
+
sections.push({
|
|
189
|
+
title: "Detail",
|
|
190
|
+
rowCount: 1,
|
|
191
|
+
highlightKeyForRow: () => "detail",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// Merge results section
|
|
195
|
+
if (this.swarm.mergeResults.length > 0) {
|
|
196
|
+
sections.push({
|
|
197
|
+
title: "Merges",
|
|
198
|
+
rowCount: this.swarm.mergeResults.length,
|
|
199
|
+
highlightKeyForRow: (row) => `merge-${row}`,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// Event log section
|
|
203
|
+
sections.push({
|
|
204
|
+
title: "Events",
|
|
205
|
+
rowCount: Math.min(12, this.swarm.logs.length),
|
|
206
|
+
highlightKeyForRow: (row) => `event-${row}`,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
else if (this.steeringActive) {
|
|
210
|
+
// Steering mode sections
|
|
211
|
+
if (this.steeringContext?.objective) {
|
|
212
|
+
sections.push({ title: "Objective", rowCount: 1, highlightKeyForRow: () => "objective" });
|
|
213
|
+
}
|
|
214
|
+
if (this.steeringContext?.status) {
|
|
215
|
+
sections.push({ title: "Status", rowCount: 1, highlightKeyForRow: () => "status" });
|
|
216
|
+
}
|
|
217
|
+
if (this.steeringContext?.lastWave) {
|
|
218
|
+
sections.push({ title: "LastWave", rowCount: Math.min(6, this.steeringContext.lastWave.tasks.length + 1), highlightKeyForRow: (row) => `wave-task-${row}` });
|
|
219
|
+
}
|
|
220
|
+
sections.push({ title: "PlannerActivity", rowCount: Math.min(15, this.steeringEvents.length), highlightKeyForRow: (row) => `steer-event-${row}` });
|
|
221
|
+
sections.push({ title: "StatusLine", rowCount: 1, highlightKeyForRow: () => "status-line" });
|
|
222
|
+
}
|
|
223
|
+
// Ensure at least one section
|
|
224
|
+
if (sections.length === 0) {
|
|
225
|
+
sections.push({ title: "Content", rowCount: 1, highlightKeyForRow: () => "content" });
|
|
226
|
+
}
|
|
227
|
+
return sections;
|
|
228
|
+
}
|
|
229
|
+
clampNavState(sections) {
|
|
230
|
+
const nav = this.navState;
|
|
231
|
+
nav.focusSection = Math.min(Math.max(0, nav.focusSection), sections.length - 1);
|
|
232
|
+
const s = sections[nav.focusSection];
|
|
233
|
+
if (s) {
|
|
234
|
+
nav.focusRow = Math.min(Math.max(0, nav.focusRow), Math.max(0, s.rowCount - 1));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** Returns the unique highlight key for the currently focused row, used by renderer. */
|
|
238
|
+
getHighlightKey() {
|
|
239
|
+
const sections = this.getSections();
|
|
240
|
+
const nav = this.navState;
|
|
241
|
+
const section = sections[Math.min(nav.focusSection, sections.length - 1)];
|
|
242
|
+
return section?.highlightKeyForRow?.(nav.focusRow);
|
|
243
|
+
}
|
|
89
244
|
clearAskTempFile() {
|
|
90
245
|
if (this.askTempFile) {
|
|
91
246
|
try {
|
|
@@ -325,9 +480,62 @@ export class RunDisplay {
|
|
|
325
480
|
}
|
|
326
481
|
return false;
|
|
327
482
|
}
|
|
328
|
-
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
483
|
+
/** Handle a typed (non-pasted) chunk. Returns true if the frame needs a redraw.
|
|
484
|
+
*
|
|
485
|
+
* Demux pipeline — routes arrow keys and ESC BEFORE hotkey matching:
|
|
486
|
+
* Raw stdin chunk → splitPaste
|
|
487
|
+
* ├─ paste → handlePaste (existing, fine)
|
|
488
|
+
* └─ typed → demux
|
|
489
|
+
* ├─ ESC + [A/B/C/D → this.navigate("up"/"down"/"right"/"left")
|
|
490
|
+
* ├─ ESC → cancel input / close detail / dismiss panel
|
|
491
|
+
* ├─ Enter → submit / reveal / select
|
|
492
|
+
* ├─ Ctrl+C → abort
|
|
493
|
+
* ├─ Backspace → delete
|
|
494
|
+
* └─ printable → hotkey matching (b, t, c, e, p, s, q, ?, d, 0-9)
|
|
495
|
+
*/
|
|
329
496
|
handleTyped(s) {
|
|
330
497
|
const lc = this.liveConfig;
|
|
498
|
+
// ── 1. Arrow keys: \x1B[A = up, \x1B[B = down, \x1B[C = right, \x1B[D = left ──
|
|
499
|
+
if (s.startsWith("\x1B[")) {
|
|
500
|
+
const dir = s[2];
|
|
501
|
+
if (dir === "A") {
|
|
502
|
+
this.navigate("up");
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
if (dir === "B") {
|
|
506
|
+
this.navigate("down");
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
if (dir === "C") {
|
|
510
|
+
this.navigate("right");
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
if (dir === "D") {
|
|
514
|
+
this.navigate("left");
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
// Other ANSI sequences — swallow silently
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
// ── 2. Standalone ESC ──
|
|
521
|
+
if (s === "\x1B") {
|
|
522
|
+
if (this.inputMode !== "none") {
|
|
523
|
+
this.inputMode = "none";
|
|
524
|
+
this.inputSegs = [];
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (this.selectedAgentId != null) {
|
|
528
|
+
this.clearSelectedAgent();
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
if (this.askState && !this.askState.streaming) {
|
|
532
|
+
this.askState = undefined;
|
|
533
|
+
this.clearAskTempFile();
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
// ── 3. Input mode: budget / threshold / concurrency / extra ──
|
|
331
539
|
if (this.inputMode === "budget" || this.inputMode === "threshold" || this.inputMode === "concurrency" || this.inputMode === "extra") {
|
|
332
540
|
let dirty = false;
|
|
333
541
|
for (const ch of s) {
|
|
@@ -366,12 +574,6 @@ export class RunDisplay {
|
|
|
366
574
|
this.inputSegs = [];
|
|
367
575
|
return true;
|
|
368
576
|
}
|
|
369
|
-
// ESC cancels input mode
|
|
370
|
-
if (ch === "\x1B") {
|
|
371
|
-
this.inputMode = "none";
|
|
372
|
-
this.inputSegs = [];
|
|
373
|
-
return true;
|
|
374
|
-
}
|
|
375
577
|
if (ch === "\x7F") {
|
|
376
578
|
backspaceSegments(this.inputSegs);
|
|
377
579
|
dirty = true;
|
|
@@ -384,6 +586,7 @@ export class RunDisplay {
|
|
|
384
586
|
}
|
|
385
587
|
return dirty;
|
|
386
588
|
}
|
|
589
|
+
// ── 4. Input mode: steer / ask ──
|
|
387
590
|
if (this.inputMode === "steer" || this.inputMode === "ask") {
|
|
388
591
|
let dirty = false;
|
|
389
592
|
for (let ci = 0; ci < s.length; ci++) {
|
|
@@ -406,18 +609,11 @@ export class RunDisplay {
|
|
|
406
609
|
this.inputSegs = [];
|
|
407
610
|
return true;
|
|
408
611
|
}
|
|
409
|
-
// ESC cancels
|
|
612
|
+
// ESC cancels input mode (no ANSI-byte consumption loop — arrows arrive
|
|
613
|
+
// as "\x1B[A" in a single call and are caught by step 1 above)
|
|
410
614
|
if (ch === "\x1B") {
|
|
411
615
|
this.inputMode = "none";
|
|
412
616
|
this.inputSegs = [];
|
|
413
|
-
// Skip any remaining ANSI sequence bytes (e.g. [A for arrow keys)
|
|
414
|
-
while (ci + 1 < s.length) {
|
|
415
|
-
const next = s[ci + 1];
|
|
416
|
-
const nc = next.charCodeAt(0);
|
|
417
|
-
ci++;
|
|
418
|
-
if ((nc >= 0x40 && nc <= 0x7E) || nc === 0x7F)
|
|
419
|
-
break; // final byte
|
|
420
|
-
}
|
|
421
617
|
return true;
|
|
422
618
|
}
|
|
423
619
|
if (ch === "\x7F" || ch === "\b") {
|
|
@@ -427,9 +623,9 @@ export class RunDisplay {
|
|
|
427
623
|
}
|
|
428
624
|
const code = ch.charCodeAt(0);
|
|
429
625
|
if (code < 0x20)
|
|
430
|
-
continue;
|
|
626
|
+
continue;
|
|
431
627
|
if (code >= 0x7F && code < 0xA0)
|
|
432
|
-
continue;
|
|
628
|
+
continue;
|
|
433
629
|
if (code >= 0x20 && code <= 0x7E && segmentsToString(this.inputSegs).length < MAX_INPUT_LEN) {
|
|
434
630
|
appendCharToSegments(this.inputSegs, ch);
|
|
435
631
|
dirty = true;
|
|
@@ -437,16 +633,9 @@ export class RunDisplay {
|
|
|
437
633
|
}
|
|
438
634
|
return dirty;
|
|
439
635
|
}
|
|
440
|
-
// Hotkey mode
|
|
441
|
-
//
|
|
442
|
-
if (s
|
|
443
|
-
return false;
|
|
444
|
-
if (s.length !== 1)
|
|
445
|
-
return false;
|
|
446
|
-
const key = s[0];
|
|
447
|
-
const code = key.charCodeAt(0);
|
|
448
|
-
// Allow \r / \n through for Enter-to-reveal
|
|
449
|
-
if (code === 0x0D || code === 0x0A) {
|
|
636
|
+
// ── 5. Hotkey mode ──
|
|
637
|
+
// Enter
|
|
638
|
+
if (s === "\r" || s === "\n") {
|
|
450
639
|
if (this.askTempFile) {
|
|
451
640
|
try {
|
|
452
641
|
execSync(`open -R ${JSON.stringify(this.askTempFile)}`);
|
|
@@ -455,12 +644,21 @@ export class RunDisplay {
|
|
|
455
644
|
}
|
|
456
645
|
return true;
|
|
457
646
|
}
|
|
458
|
-
//
|
|
459
|
-
if (
|
|
460
|
-
this.
|
|
461
|
-
|
|
462
|
-
|
|
647
|
+
// Ctrl+C
|
|
648
|
+
if (s === "\x03") {
|
|
649
|
+
if (this.swarm && !this.swarm.aborted) {
|
|
650
|
+
this.swarm.abort();
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
process.exit(0);
|
|
654
|
+
}
|
|
655
|
+
return true;
|
|
463
656
|
}
|
|
657
|
+
// Only single printable ASCII characters reach hotkey matching
|
|
658
|
+
if (s.length !== 1)
|
|
659
|
+
return false;
|
|
660
|
+
const key = s[0];
|
|
661
|
+
const code = key.charCodeAt(0);
|
|
464
662
|
if (code < 0x20 || code > 0x7E)
|
|
465
663
|
return false;
|
|
466
664
|
if (key === "b" || key === "B") {
|
|
@@ -527,15 +725,7 @@ export class RunDisplay {
|
|
|
527
725
|
}
|
|
528
726
|
// [d] cycle agent detail panel
|
|
529
727
|
if ((key === "d" || key === "D") && this.swarm && this.swarm.active > 0) {
|
|
530
|
-
|
|
531
|
-
this.cycleSelectedAgent();
|
|
532
|
-
else
|
|
533
|
-
this.cycleSelectedAgent();
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
// ESC closes detail panel
|
|
537
|
-
if (key === "\x1B" && this.selectedAgentId != null) {
|
|
538
|
-
this.clearSelectedAgent();
|
|
728
|
+
this.cycleSelectedAgent();
|
|
539
729
|
return true;
|
|
540
730
|
}
|
|
541
731
|
// Number keys 0-9 select a specific agent by row index in the visible table
|
|
@@ -547,7 +737,7 @@ export class RunDisplay {
|
|
|
547
737
|
return true;
|
|
548
738
|
}
|
|
549
739
|
}
|
|
550
|
-
if (key === "q" || key === "Q"
|
|
740
|
+
if (key === "q" || key === "Q") {
|
|
551
741
|
if (this.swarm) {
|
|
552
742
|
if (this.swarm.aborted)
|
|
553
743
|
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.15",
|
|
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": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"build": "tsc",
|
|
11
11
|
"dev": "tsc --watch",
|
|
12
12
|
"start": "node dist/bin.js",
|
|
13
|
+
"test": "node --test dist/__tests__/*.test.js",
|
|
13
14
|
"prepublishOnly": "node scripts/sync-plugin-version.js"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
@@ -18,6 +19,8 @@
|
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/node": "^22.0.0",
|
|
22
|
+
"node-pty": "^1.1.0",
|
|
23
|
+
"strip-ansi": "^7.1.0",
|
|
21
24
|
"typescript": "^5.7.0"
|
|
22
25
|
},
|
|
23
26
|
"license": "MIT",
|