claude-overnight 1.25.23 → 1.25.24
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/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/interactive-panel.d.ts +40 -0
- package/dist/interactive-panel.js +111 -0
- package/dist/models.js +1 -4
- package/dist/planner-query.d.ts +8 -0
- package/dist/planner-query.js +22 -18
- package/dist/providers.js +34 -32
- package/dist/render.d.ts +10 -10
- package/dist/render.js +135 -26
- package/dist/run.js +26 -1
- package/dist/state.js +18 -0
- package/dist/swarm.d.ts +1 -0
- package/dist/swarm.js +21 -8
- package/dist/types.d.ts +20 -1
- package/dist/types.js +16 -0
- package/dist/ui.d.ts +5 -13
- package/dist/ui.js +66 -41
- package/package.json +1 -1
- package/plugins/claude-overnight/.claude-plugin/plugin.json +1 -1
package/dist/_version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.25.
|
|
1
|
+
export declare const VERSION = "1.25.24";
|
package/dist/_version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by build — do not edit manually.
|
|
2
|
-
export const VERSION = "1.25.
|
|
2
|
+
export const VERSION = "1.25.24";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type PanelMode = "debrief" | "ask" | "custom" | "none";
|
|
2
|
+
/** Mutable state of the interactive panel. */
|
|
3
|
+
export interface PanelState {
|
|
4
|
+
mode: PanelMode;
|
|
5
|
+
expanded: boolean;
|
|
6
|
+
scrollOffset: number;
|
|
7
|
+
/** Short title shown in the header bar. */
|
|
8
|
+
header: string;
|
|
9
|
+
/** One-line summary shown when collapsed. */
|
|
10
|
+
preview: string;
|
|
11
|
+
/** Multi-line body shown when expanded. */
|
|
12
|
+
body: string;
|
|
13
|
+
/** Whether to show a text input at the bottom (ask/steer). */
|
|
14
|
+
inputActive: boolean;
|
|
15
|
+
inputPlaceholder?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class InteractivePanel {
|
|
18
|
+
state: PanelState;
|
|
19
|
+
/** Cached non-empty body lines — rebuilt when body changes. */
|
|
20
|
+
private _bodyLines;
|
|
21
|
+
/** Set or clear the panel content. Mode "none" hides it. */
|
|
22
|
+
set(params: {
|
|
23
|
+
mode: PanelMode;
|
|
24
|
+
header: string;
|
|
25
|
+
preview: string;
|
|
26
|
+
body: string;
|
|
27
|
+
}): void;
|
|
28
|
+
/** Collapse the panel back to the compact bar. */
|
|
29
|
+
collapse(): void;
|
|
30
|
+
/** Toggle expanded/collapsed state. */
|
|
31
|
+
toggle(): void;
|
|
32
|
+
/** Scroll up/down within the expanded body. */
|
|
33
|
+
scroll(direction: "up" | "down", visibleRows: number): void;
|
|
34
|
+
/** Whether the panel is currently visible (any mode other than none). */
|
|
35
|
+
get visible(): boolean;
|
|
36
|
+
/** Render the collapsed compact bar. Returns empty string if no content. */
|
|
37
|
+
renderCollapsed(width: number): string;
|
|
38
|
+
/** Render the expanded panel as an array of lines for the content area. */
|
|
39
|
+
renderExpanded(width: number, maxRows: number): string[];
|
|
40
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
const DARK_GREEN_BG = "\x1B[48;5;22m";
|
|
3
|
+
const LIGHT_GREEN_FG = "\x1B[38;5;156m";
|
|
4
|
+
const RESET = "\x1B[0m";
|
|
5
|
+
function greenBg(text) {
|
|
6
|
+
return `${DARK_GREEN_BG}${LIGHT_GREEN_FG} ${text} ${RESET}`;
|
|
7
|
+
}
|
|
8
|
+
function greenBgLine(text, width) {
|
|
9
|
+
const padded = text.padEnd(Math.max(0, width));
|
|
10
|
+
return `${DARK_GREEN_BG}${LIGHT_GREEN_FG}${padded}${RESET}`;
|
|
11
|
+
}
|
|
12
|
+
function truncate(s, max) {
|
|
13
|
+
return s.length <= max ? s : s.slice(0, max - 1) + "\u2026";
|
|
14
|
+
}
|
|
15
|
+
export class InteractivePanel {
|
|
16
|
+
state = {
|
|
17
|
+
mode: "none",
|
|
18
|
+
expanded: false,
|
|
19
|
+
scrollOffset: 0,
|
|
20
|
+
header: "",
|
|
21
|
+
preview: "",
|
|
22
|
+
body: "",
|
|
23
|
+
inputActive: false,
|
|
24
|
+
};
|
|
25
|
+
/** Cached non-empty body lines — rebuilt when body changes. */
|
|
26
|
+
_bodyLines = [];
|
|
27
|
+
/** Set or clear the panel content. Mode "none" hides it. */
|
|
28
|
+
set(params) {
|
|
29
|
+
this.state.mode = params.mode;
|
|
30
|
+
this.state.header = params.header;
|
|
31
|
+
this.state.preview = params.preview;
|
|
32
|
+
this.state.body = params.body;
|
|
33
|
+
// Rebuild cached lines and reset scroll only when content changes
|
|
34
|
+
this._bodyLines = params.body.split("\n").filter(l => l.length > 0);
|
|
35
|
+
this.state.scrollOffset = 0;
|
|
36
|
+
}
|
|
37
|
+
/** Collapse the panel back to the compact bar. */
|
|
38
|
+
collapse() {
|
|
39
|
+
this.state.expanded = false;
|
|
40
|
+
this.state.scrollOffset = 0;
|
|
41
|
+
}
|
|
42
|
+
/** Toggle expanded/collapsed state. */
|
|
43
|
+
toggle() {
|
|
44
|
+
if (this.state.mode === "none")
|
|
45
|
+
return;
|
|
46
|
+
this.state.expanded = !this.state.expanded;
|
|
47
|
+
if (!this.state.expanded)
|
|
48
|
+
this.state.scrollOffset = 0;
|
|
49
|
+
}
|
|
50
|
+
/** Scroll up/down within the expanded body. */
|
|
51
|
+
scroll(direction, visibleRows) {
|
|
52
|
+
if (!this.state.expanded)
|
|
53
|
+
return;
|
|
54
|
+
const maxScroll = Math.max(0, this._bodyLines.length - visibleRows);
|
|
55
|
+
if (direction === "up") {
|
|
56
|
+
this.state.scrollOffset = Math.max(0, this.state.scrollOffset - 1);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.state.scrollOffset = Math.min(maxScroll, this.state.scrollOffset + 1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Whether the panel is currently visible (any mode other than none). */
|
|
63
|
+
get visible() {
|
|
64
|
+
return this.state.mode !== "none";
|
|
65
|
+
}
|
|
66
|
+
/** Render the collapsed compact bar. Returns empty string if no content. */
|
|
67
|
+
renderCollapsed(width) {
|
|
68
|
+
if (this.state.mode === "none" || !this.state.preview)
|
|
69
|
+
return "";
|
|
70
|
+
const icon = this.state.expanded ? "\u25BC" : "\u25B6";
|
|
71
|
+
const modeLabel = this.state.header;
|
|
72
|
+
const hint = chalk.dim(`[Ctrl-O expand]`);
|
|
73
|
+
const content = truncate(this.state.preview, width - modeLabel.length - hint.length - 8);
|
|
74
|
+
return ` ${greenBg(`${icon} ${modeLabel}`)} ${content} ${hint}`;
|
|
75
|
+
}
|
|
76
|
+
/** Render the expanded panel as an array of lines for the content area. */
|
|
77
|
+
renderExpanded(width, maxRows) {
|
|
78
|
+
if (this.state.mode === "none")
|
|
79
|
+
return [];
|
|
80
|
+
const innerW = Math.max(20, width - 6);
|
|
81
|
+
const lines = [];
|
|
82
|
+
// Header bar — full-width dark green bg
|
|
83
|
+
const headerText = ` ${this.state.header} ${chalk.dim("[Ctrl-O] collapse")}${this.state.inputActive ? chalk.dim(" [Esc] cancel") : ""}`;
|
|
84
|
+
lines.push(greenBgLine(headerText, Math.min(width - 4, innerW + 2)));
|
|
85
|
+
// Body content — scrolled
|
|
86
|
+
const headerSpace = this.state.inputActive ? 3 : 2; // header + footer + optional input
|
|
87
|
+
const visibleRows = Math.max(2, maxRows - headerSpace);
|
|
88
|
+
const start = this.state.scrollOffset;
|
|
89
|
+
const end = Math.min(start + visibleRows, this._bodyLines.length);
|
|
90
|
+
for (let i = start; i < end; i++) {
|
|
91
|
+
const ln = truncate(this._bodyLines[i], innerW);
|
|
92
|
+
lines.push(` ${chalk.greenBright(ln)}`);
|
|
93
|
+
}
|
|
94
|
+
if (end < this._bodyLines.length) {
|
|
95
|
+
lines.push(chalk.dim(` \u2026 +${this._bodyLines.length - end} more`));
|
|
96
|
+
}
|
|
97
|
+
if (this._bodyLines.length === 0) {
|
|
98
|
+
lines.push(chalk.dim(" (empty)"));
|
|
99
|
+
}
|
|
100
|
+
// Footer hint
|
|
101
|
+
if (this.state.inputActive && this.state.inputPlaceholder) {
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(` ${chalk.cyan(">")} ${this.state.inputPlaceholder}`);
|
|
104
|
+
}
|
|
105
|
+
else if (!this.state.inputActive) {
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push(chalk.dim(" \u2191\u2193 scroll [Ctrl-O] collapse"));
|
|
108
|
+
}
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/models.js
CHANGED
|
@@ -22,7 +22,6 @@ export const MODEL_CAPABILITIES = {
|
|
|
22
22
|
// Opus 4.7: only model that earns "relaxed". 100% on 38-task routing, 95%+ IFEval.
|
|
23
23
|
// Step-change agentic coding over Opus 4.6. 1M tokens, 128K output.
|
|
24
24
|
"claude-opus-4-7": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "relaxed", displayName: "Opus 4.7" },
|
|
25
|
-
"claude-opus-4.7": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "relaxed", displayName: "Opus 4.7" },
|
|
26
25
|
"claude-opus-4-7-low": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "moderate", displayName: "Opus 4.7 Low" },
|
|
27
26
|
"claude-opus-4-7-medium": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "relaxed", displayName: "Opus 4.7 Medium" },
|
|
28
27
|
"claude-opus-4-7-high": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "relaxed", displayName: "Opus 4.7 High" },
|
|
@@ -36,13 +35,11 @@ export const MODEL_CAPABILITIES = {
|
|
|
36
35
|
"claude-opus-4-7-thinking-max": { contextWindow: 1_000_000, safeContext: 400_000, contextConstraint: "relaxed", displayName: "Opus 4.7 Max Thinking" },
|
|
37
36
|
// Sonnet 4.6: 200K context, tight constraint.
|
|
38
37
|
"claude-sonnet-4-6": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "tight", displayName: "Sonnet 4.6" },
|
|
39
|
-
"claude-sonnet-4.6": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "tight", displayName: "Sonnet 4.6" },
|
|
40
38
|
"claude-4.6-sonnet-medium": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "tight", displayName: "Sonnet 4.6 Medium" },
|
|
41
39
|
"claude-sonnet-4-6-thinking": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "tight", displayName: "Sonnet 4.6 Thinking" },
|
|
42
40
|
"claude-4.6-sonnet-medium-thinking": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "tight", displayName: "Sonnet 4.6 Medium Thinking" },
|
|
43
41
|
// Sonnet 4.5: 200K context.
|
|
44
42
|
"claude-sonnet-4-5": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "moderate", displayName: "Sonnet 4.5" },
|
|
45
|
-
"claude-sonnet-4.5": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "moderate", displayName: "Sonnet 4.5" },
|
|
46
43
|
"claude-4.5-sonnet": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "moderate", displayName: "Sonnet 4.5" },
|
|
47
44
|
"claude-sonnet-4-5-thinking": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "moderate", displayName: "Sonnet 4.5 Thinking" },
|
|
48
45
|
"claude-4.5-sonnet-thinking": { contextWindow: 200_000, safeContext: 60_000, contextConstraint: "moderate", displayName: "Sonnet 4.5 Thinking" },
|
|
@@ -71,7 +68,7 @@ export const MODEL_CAPABILITIES = {
|
|
|
71
68
|
"gpt-5.4-xhigh-fast": { contextWindow: 1_050_000, safeContext: 300_000, contextConstraint: "moderate", displayName: "GPT-5.4 Extra High Fast" },
|
|
72
69
|
"gpt-5.4-mini": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini" },
|
|
73
70
|
"gpt-5.4-mini-low": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini Low" },
|
|
74
|
-
"gpt-5.4-mini-medium": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini" },
|
|
71
|
+
"gpt-5.4-mini-medium": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini Medium" },
|
|
75
72
|
"gpt-5.4-mini-high": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini High" },
|
|
76
73
|
"gpt-5.4-mini-xhigh": { contextWindow: 1_050_000, safeContext: 200_000, contextConstraint: "tight", displayName: "GPT-5.4 Mini Extra High" },
|
|
77
74
|
// Codex 5.3: best agentic coder from OpenAI. 400K context, 128K output.
|
package/dist/planner-query.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface PlannerRateLimitInfo {
|
|
|
13
13
|
windows: Map<string, RateLimitWindow>;
|
|
14
14
|
resetsAt?: number;
|
|
15
15
|
costUsd: number;
|
|
16
|
+
/** Peak total input tokens (input + cache) in any single planner turn — proxy for context-window occupancy. */
|
|
17
|
+
contextTokens?: number;
|
|
18
|
+
/** Model used by the current planner query (for safeContext lookup). */
|
|
19
|
+
model?: string;
|
|
16
20
|
}
|
|
17
21
|
export interface PlannerOpts {
|
|
18
22
|
cwd: string;
|
|
@@ -36,6 +40,10 @@ export interface PlannerOpts {
|
|
|
36
40
|
}
|
|
37
41
|
export declare function setPlannerEnvResolver(fn: ((model?: string) => Record<string, string> | undefined) | undefined): void;
|
|
38
42
|
export declare function getTotalPlannerCost(): number;
|
|
43
|
+
export declare function getPeakPlannerContext(): {
|
|
44
|
+
tokens: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
};
|
|
39
47
|
export declare function getPlannerRateLimitInfo(): PlannerRateLimitInfo;
|
|
40
48
|
export declare function runPlannerQuery(prompt: string, opts: PlannerOpts, onLog: PlannerLog): Promise<string>;
|
|
41
49
|
export declare function postProcess(raw: Task[], budget: number | undefined, onLog: (text: string) => void): Task[];
|
package/dist/planner-query.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
|
-
import { NudgeError } from "./types.js";
|
|
3
|
+
import { NudgeError, extractToolTarget, sumUsageTokens } from "./types.js";
|
|
4
4
|
import { writeTranscriptEvent } from "./transcripts.js";
|
|
5
|
+
/** Log a tool invocation with a short target for planner queries. */
|
|
6
|
+
const logTool = (label, input) => {
|
|
7
|
+
const target = extractToolTarget(input);
|
|
8
|
+
return target ? `${label} \u2192 ${target}` : label;
|
|
9
|
+
};
|
|
5
10
|
const DEFAULT_TOOLS = ["Read", "Glob", "Grep", "Write", "Bash", "WebFetch", "WebSearch", "TodoWrite", "Agent"];
|
|
6
11
|
const DEFAULT_MAX_TURNS = 20;
|
|
7
12
|
// ── Shared env resolver (set once at run start, used by every planner query) ──
|
|
@@ -21,6 +26,11 @@ function isRateLimitError(err) {
|
|
|
21
26
|
}
|
|
22
27
|
let _totalPlannerCostUsd = 0;
|
|
23
28
|
export function getTotalPlannerCost() { return _totalPlannerCostUsd; }
|
|
29
|
+
let _peakPlannerContextTokens = 0;
|
|
30
|
+
let _peakPlannerContextModel;
|
|
31
|
+
export function getPeakPlannerContext() {
|
|
32
|
+
return { tokens: _peakPlannerContextTokens, model: _peakPlannerContextModel };
|
|
33
|
+
}
|
|
24
34
|
let _plannerRateLimitInfo = {
|
|
25
35
|
utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0,
|
|
26
36
|
};
|
|
@@ -66,22 +76,6 @@ async function throttlePlanner(onLog, aborted) {
|
|
|
66
76
|
}
|
|
67
77
|
// Exhausted backoffs — proceed anyway, the retry loop will catch a rejection.
|
|
68
78
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Pick a short, human-readable target for a tool invocation (Read/Grep/Bash/…).
|
|
71
|
-
* Prefers explicit file paths; falls back to the first few tokens of a shell
|
|
72
|
-
* command. Returns `""` when the input has no useful identifier.
|
|
73
|
-
*/
|
|
74
|
-
function extractToolTarget(input) {
|
|
75
|
-
if (!input)
|
|
76
|
-
return "";
|
|
77
|
-
const p = input.path ?? input.file_path ?? input.pattern;
|
|
78
|
-
if (typeof p === "string" && p)
|
|
79
|
-
return p;
|
|
80
|
-
if (typeof input.command === "string" && input.command) {
|
|
81
|
-
return input.command.split(" ").slice(0, 3).join(" ");
|
|
82
|
-
}
|
|
83
|
-
return "";
|
|
84
|
-
}
|
|
85
79
|
// ── Query execution ──
|
|
86
80
|
const NUDGE_MS = 15 * 60 * 1000;
|
|
87
81
|
const HARD_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -123,7 +117,7 @@ export async function runPlannerQuery(prompt, opts, onLog) {
|
|
|
123
117
|
throw new Error("Planner query failed after retries");
|
|
124
118
|
}
|
|
125
119
|
async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
126
|
-
_plannerRateLimitInfo = { utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0 };
|
|
120
|
+
_plannerRateLimitInfo = { utilization: 0, status: "", isUsingOverage: false, windows: new Map(), costUsd: 0, contextTokens: 0, model: opts.model };
|
|
127
121
|
let resultText = "";
|
|
128
122
|
let structuredOutput;
|
|
129
123
|
const startedAt = Date.now();
|
|
@@ -310,6 +304,16 @@ async function runPlannerQueryOnce(prompt, opts, onLog) {
|
|
|
310
304
|
// turn message for tool_use / thinking / text so the ticker still moves
|
|
311
305
|
// every ~6-15s instead of sitting silent for minutes.
|
|
312
306
|
if (msg.type === "assistant") {
|
|
307
|
+
const u = msg.message?.usage;
|
|
308
|
+
if (u) {
|
|
309
|
+
const turnTotal = sumUsageTokens(u);
|
|
310
|
+
if (turnTotal > (_plannerRateLimitInfo.contextTokens ?? 0))
|
|
311
|
+
_plannerRateLimitInfo.contextTokens = turnTotal;
|
|
312
|
+
if (turnTotal > _peakPlannerContextTokens) {
|
|
313
|
+
_peakPlannerContextTokens = turnTotal;
|
|
314
|
+
_peakPlannerContextModel = opts.model;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
313
317
|
const content = msg.message?.content;
|
|
314
318
|
if (Array.isArray(content)) {
|
|
315
319
|
for (const part of content) {
|
package/dist/providers.js
CHANGED
|
@@ -11,6 +11,28 @@ import { getBearerToken, clearTokenCache } from "./auth.js";
|
|
|
11
11
|
import { DEFAULT_MODEL } from "./models.js";
|
|
12
12
|
import { CURSOR_PRIORITY_MODELS, CURSOR_KNOWN_MODELS, KNOWN_CURSOR_MODEL_IDS, cursorModelHint, } from "./cursor-models.js";
|
|
13
13
|
import { VERSION } from "./_version.js";
|
|
14
|
+
/** Cached system Node.js and agent script paths — resolved once, reused across envFor calls. */
|
|
15
|
+
let _cachedAgentNode = null;
|
|
16
|
+
let _cachedAgentScript = null;
|
|
17
|
+
/** Resolve system Node.js and agent index.js paths. Returns [nodePath, scriptPath] or [null, null]. */
|
|
18
|
+
function resolveAgentPaths(timeoutMs = 2_000) {
|
|
19
|
+
let nodePath = null;
|
|
20
|
+
let agentJs = null;
|
|
21
|
+
try {
|
|
22
|
+
nodePath = execSync("which node 2>/dev/null", { timeout: timeoutMs, encoding: "utf-8", shell: "bash" }).trim() || null;
|
|
23
|
+
const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
|
|
24
|
+
timeout: timeoutMs, encoding: "utf-8", shell: "bash",
|
|
25
|
+
}).trim();
|
|
26
|
+
if (agentPath) {
|
|
27
|
+
const agentDir = dirname(realpathSync(agentPath));
|
|
28
|
+
const indexPath = `${agentDir}/index.js`;
|
|
29
|
+
if (existsSync(indexPath))
|
|
30
|
+
agentJs = indexPath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch { }
|
|
34
|
+
return [nodePath, agentJs];
|
|
35
|
+
}
|
|
14
36
|
/** Run the installed package CLI with `node` (avoids npx/npm invoking extra tooling on macOS). */
|
|
15
37
|
function resolveCursorComposerCli() {
|
|
16
38
|
try {
|
|
@@ -129,23 +151,17 @@ export function envFor(p) {
|
|
|
129
151
|
// agents and workers must use "plan" so they actually interact with the codebase.
|
|
130
152
|
base.CURSOR_BRIDGE_MODE = "plan";
|
|
131
153
|
// Use system Node.js for agent subprocess to avoid macOS segfaults with
|
|
132
|
-
// bundled Node.js. Resolve lazily
|
|
133
|
-
if (!
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
base.CURSOR_AGENT_NODE = sysNode;
|
|
144
|
-
base.CURSOR_AGENT_SCRIPT = indexPath;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
catch { }
|
|
154
|
+
// bundled Node.js. Resolve lazily.
|
|
155
|
+
if (!_cachedAgentNode || !_cachedAgentScript) {
|
|
156
|
+
const [node, script] = resolveAgentPaths(2_000);
|
|
157
|
+
_cachedAgentNode = node;
|
|
158
|
+
_cachedAgentScript = script;
|
|
159
|
+
}
|
|
160
|
+
if (_cachedAgentNode) {
|
|
161
|
+
base.CURSOR_AGENT_NODE = _cachedAgentNode;
|
|
162
|
+
}
|
|
163
|
+
if (_cachedAgentScript) {
|
|
164
|
+
base.CURSOR_AGENT_SCRIPT = _cachedAgentScript;
|
|
149
165
|
}
|
|
150
166
|
return base;
|
|
151
167
|
}
|
|
@@ -837,21 +853,7 @@ async function startProxyProcess(baseUrl, url, port) {
|
|
|
837
853
|
console.log(chalk.yellow(`\n Proxy not running at ${baseUrl} — starting it for you…`));
|
|
838
854
|
// Resolve system node and agent index.js so the proxy uses system Node.js
|
|
839
855
|
// for the agent subprocess (avoids segfaults with --list-models on macOS).
|
|
840
|
-
|
|
841
|
-
let agentJs = null;
|
|
842
|
-
try {
|
|
843
|
-
sysNode = execSync("which node 2>/dev/null", { timeout: 3_000, encoding: "utf-8", shell: "bash" }).trim() || null;
|
|
844
|
-
const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
|
|
845
|
-
timeout: 3_000, encoding: "utf-8", shell: "bash",
|
|
846
|
-
}).trim();
|
|
847
|
-
if (agentPath) {
|
|
848
|
-
const agentDir = dirname(realpathSync(agentPath));
|
|
849
|
-
const indexPath = `${agentDir}/index.js`;
|
|
850
|
-
if (existsSync(indexPath))
|
|
851
|
-
agentJs = indexPath;
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
catch { }
|
|
856
|
+
const [sysNode, agentJs] = resolveAgentPaths(3_000);
|
|
855
857
|
const apiKeyStored = loadProviders().find(p => p.cursorProxy)?.cursorApiKey;
|
|
856
858
|
const agentToken = resolveCursorAgentToken();
|
|
857
859
|
if (!agentToken) {
|
package/dist/render.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
1
2
|
import type { Swarm } from "./swarm.js";
|
|
2
|
-
import type {
|
|
3
|
+
import type { RLGetter } from "./types.js";
|
|
3
4
|
import type { RunInfo, SteeringContext, SteeringEvent } from "./ui.js";
|
|
5
|
+
import { InteractivePanel } from "./interactive-panel.js";
|
|
4
6
|
export interface Section {
|
|
5
7
|
title: string;
|
|
6
8
|
rows: string[];
|
|
@@ -14,6 +16,11 @@ export interface ContentRenderer {
|
|
|
14
16
|
export declare function truncate(s: string, max: number): string;
|
|
15
17
|
export declare function fmtTokens(n: number): string;
|
|
16
18
|
export declare function fmtDur(ms: number): string;
|
|
19
|
+
/** Context-fill percentage and color function for a token count vs safe limit. */
|
|
20
|
+
export declare function contextFillInfo(tokens: number, safe: number): {
|
|
21
|
+
pct: number;
|
|
22
|
+
color: typeof chalk;
|
|
23
|
+
};
|
|
17
24
|
export declare function renderUnifiedFrame(params: {
|
|
18
25
|
model?: string;
|
|
19
26
|
phase: string;
|
|
@@ -36,13 +43,7 @@ export declare function renderUnifiedFrame(params: {
|
|
|
36
43
|
extraFooterRows?: string[];
|
|
37
44
|
maxRows?: number;
|
|
38
45
|
}): string;
|
|
39
|
-
|
|
40
|
-
utilization: number;
|
|
41
|
-
isUsingOverage: boolean;
|
|
42
|
-
windows: Map<string, RateLimitWindow>;
|
|
43
|
-
resetsAt?: number;
|
|
44
|
-
};
|
|
45
|
-
export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo, selectedAgentId?: number, maxRows?: number, debrief?: string): string;
|
|
46
|
+
export declare function renderFrame(swarm: Swarm, showHotkeys: boolean, runInfo?: RunInfo, selectedAgentId?: number, maxRows?: number, panel?: InteractivePanel): string;
|
|
46
47
|
export interface SteeringViewData {
|
|
47
48
|
/** The ephemeral ticker heartbeat -- elapsed, tool count, cost, current reasoning snippet. */
|
|
48
49
|
statusLine: string;
|
|
@@ -51,6 +52,5 @@ export interface SteeringViewData {
|
|
|
51
52
|
/** Optional context read from disk at setSteering() time. */
|
|
52
53
|
context?: SteeringContext;
|
|
53
54
|
}
|
|
54
|
-
export declare function renderSteeringFrame(runInfo: RunInfo, data: SteeringViewData, showHotkeys: boolean, rlGetter?: RLGetter, maxRows?: number,
|
|
55
|
+
export declare function renderSteeringFrame(runInfo: RunInfo, data: SteeringViewData, showHotkeys: boolean, rlGetter?: RLGetter, maxRows?: number, panel?: InteractivePanel): string;
|
|
55
56
|
export declare function renderSummary(swarm: Swarm): string;
|
|
56
|
-
export {};
|
package/dist/render.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { RATE_LIMIT_WINDOW_SHORT } from "./types.js";
|
|
3
|
+
import { getModelCapability, modelDisplayName } from "./models.js";
|
|
3
4
|
const SPINNER = ["|", "/", "-", "\\"];
|
|
4
5
|
// ── Shared helpers ──
|
|
5
6
|
export function truncate(s, max) {
|
|
@@ -62,10 +63,53 @@ function renderHeader(out, w, p) {
|
|
|
62
63
|
(costStr ? ` ${costStr}` : "") + sessionStr);
|
|
63
64
|
}
|
|
64
65
|
// ── Usage bars ──
|
|
65
|
-
function
|
|
66
|
+
/** Context-fill percentage and color function for a token count vs safe limit. */
|
|
67
|
+
export function contextFillInfo(tokens, safe) {
|
|
68
|
+
const pct = safe > 0 ? Math.round((tokens / safe) * 100) : 0;
|
|
69
|
+
const color = pct > 80 ? chalk.red : pct > 50 ? chalk.yellow : chalk.green;
|
|
70
|
+
return { pct, color };
|
|
71
|
+
}
|
|
72
|
+
function drawContextBar(out, w, tokens, safe, label) {
|
|
73
|
+
const barW = Math.min(30, w - 40);
|
|
74
|
+
const { pct, color } = contextFillInfo(tokens, safe);
|
|
75
|
+
const filled = Math.round((pct / 100) * barW);
|
|
76
|
+
const bar = color("\u2588".repeat(filled)) + chalk.gray("\u2591".repeat(barW - filled));
|
|
77
|
+
const prefix = chalk.dim("Ctx ");
|
|
78
|
+
out.push(` ${prefix}${bar} ${label}`);
|
|
79
|
+
}
|
|
80
|
+
/** Pick the running agent with the highest context-fill ratio; returns {tokens, safe, agentId} or null. */
|
|
81
|
+
function peakAgentContext(swarm) {
|
|
82
|
+
let best = null;
|
|
83
|
+
for (const a of swarm.agents) {
|
|
84
|
+
if (a.status !== "running")
|
|
85
|
+
continue;
|
|
86
|
+
const tokens = a.contextTokens ?? 0;
|
|
87
|
+
if (tokens <= 0)
|
|
88
|
+
continue;
|
|
89
|
+
const model = a.task.model || swarm.model || "unknown";
|
|
90
|
+
const safe = getModelCapability(model).safeContext;
|
|
91
|
+
const ratio = safe > 0 ? tokens / safe : 0;
|
|
92
|
+
if (!best || ratio > best.ratio)
|
|
93
|
+
best = { tokens, safe, agentId: a.id, model, ratio };
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
function renderUsageBars(out, w, swarm, selectedAgentId) {
|
|
66
98
|
const windows = Array.from(swarm.rateLimitWindows.values());
|
|
67
99
|
const rlPct = swarm.rateLimitUtilization;
|
|
68
|
-
|
|
100
|
+
const hasRL = !(rlPct <= 0 && !swarm.rateLimitResetsAt && !swarm.cappedOut && swarm.rateLimitPaused <= 0 && windows.length === 0);
|
|
101
|
+
// Context bar — prefer the selected agent when detail view is open, else the peak running agent.
|
|
102
|
+
let ctx = null;
|
|
103
|
+
if (selectedAgentId != null) {
|
|
104
|
+
const a = swarm.agents.find(x => x.id === selectedAgentId);
|
|
105
|
+
if (a && (a.contextTokens ?? 0) > 0) {
|
|
106
|
+
const model = a.task.model || swarm.model || "unknown";
|
|
107
|
+
ctx = { tokens: a.contextTokens ?? 0, safe: getModelCapability(model).safeContext, agentId: a.id, model };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!ctx)
|
|
111
|
+
ctx = peakAgentContext(swarm);
|
|
112
|
+
if (!hasRL && !ctx)
|
|
69
113
|
return;
|
|
70
114
|
const barW = Math.min(30, w - 40);
|
|
71
115
|
const capFrac = swarm.usageCap;
|
|
@@ -107,10 +151,13 @@ function renderUsageBars(out, w, swarm) {
|
|
|
107
151
|
}
|
|
108
152
|
if (swarm.isUsingOverage && !swarm.cappedOut)
|
|
109
153
|
label += chalk.red(" [OVERAGE]");
|
|
110
|
-
const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("
|
|
154
|
+
const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("RL ");
|
|
111
155
|
out.push(` ${prefix}${barStr} ${label}`);
|
|
112
156
|
};
|
|
113
|
-
if (
|
|
157
|
+
if (!hasRL) {
|
|
158
|
+
// Skip the Anthropic RL bar entirely when there's no signal — just show the context bar below.
|
|
159
|
+
}
|
|
160
|
+
else if (windows.length > 1) {
|
|
114
161
|
const cycleIdx = Math.floor(Date.now() / 3000) % windows.length;
|
|
115
162
|
const win = windows[cycleIdx];
|
|
116
163
|
const shortName = RATE_LIMIT_WINDOW_SHORT[win.type] ?? win.type.replace(/_/g, " ");
|
|
@@ -137,6 +184,12 @@ function renderUsageBars(out, w, swarm) {
|
|
|
137
184
|
: `$${swarm.overageCostUsd.toFixed(2)}/$${swarm.extraUsageBudget}`;
|
|
138
185
|
out.push(` ${chalk.dim("Extra ")}${barStr} ${label}`);
|
|
139
186
|
}
|
|
187
|
+
// Context fullness bar (per peak-running-agent or selected agent).
|
|
188
|
+
if (ctx) {
|
|
189
|
+
const who = selectedAgentId != null && ctx.agentId === selectedAgentId ? `agent ${ctx.agentId}` : `peak a${ctx.agentId}`;
|
|
190
|
+
const label = `${fmtTokens(ctx.tokens)}/${fmtTokens(ctx.safe)} safe ${chalk.dim(`${who} · ${modelDisplayName(ctx.model)}`)}`;
|
|
191
|
+
drawContextBar(out, w, ctx.tokens, ctx.safe, label);
|
|
192
|
+
}
|
|
140
193
|
}
|
|
141
194
|
// ── Unified frame renderer ──
|
|
142
195
|
export function renderUnifiedFrame(params) {
|
|
@@ -188,9 +241,14 @@ export function renderUnifiedFrame(params) {
|
|
|
188
241
|
content.push(row);
|
|
189
242
|
}
|
|
190
243
|
}
|
|
191
|
-
|
|
244
|
+
const full = [...header, ...content, ...footer];
|
|
245
|
+
if (params.maxRows != null && full.length > params.maxRows) {
|
|
246
|
+
return full.slice(0, Math.max(0, params.maxRows)).join("\n");
|
|
247
|
+
}
|
|
248
|
+
return full.join("\n");
|
|
192
249
|
}
|
|
193
|
-
|
|
250
|
+
// ── Frame renderers ──
|
|
251
|
+
export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRows, panel) {
|
|
194
252
|
const allDone = swarm.agents.length > 0 && swarm.agents.every(a => a.status !== "running");
|
|
195
253
|
const doneTag = allDone && !swarm.aborted ? chalk.green("COMPLETE") : "";
|
|
196
254
|
const stoppingTag = swarm.aborted ? chalk.yellow("STOPPING") : "";
|
|
@@ -211,6 +269,14 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
|
|
|
211
269
|
const content = {
|
|
212
270
|
sections() {
|
|
213
271
|
const secs = [];
|
|
272
|
+
// Expanded panel (debrief, ask, custom) — rendered as first section
|
|
273
|
+
if (panel?.visible && panel.state.expanded) {
|
|
274
|
+
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
275
|
+
const panelRows = maxRows != null ? Math.max(4, maxRows - 6) : 12;
|
|
276
|
+
const lines = panel.renderExpanded(ww, panelRows);
|
|
277
|
+
if (lines.length > 0)
|
|
278
|
+
secs.push({ title: "", rows: lines });
|
|
279
|
+
}
|
|
214
280
|
// Agent table (undecorated -- raw header + rows)
|
|
215
281
|
if (show.length > 0) {
|
|
216
282
|
const rows = [
|
|
@@ -245,6 +311,13 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
|
|
|
245
311
|
meta.push(chalk.yellow(`$${detailAgent.costUsd.toFixed(3)}`));
|
|
246
312
|
if (detailAgent.toolCalls > 0)
|
|
247
313
|
meta.push(chalk.dim(`${detailAgent.toolCalls} tools`));
|
|
314
|
+
if ((detailAgent.contextTokens ?? 0) > 0) {
|
|
315
|
+
const mdl = detailAgent.task.model || swarm.model || "unknown";
|
|
316
|
+
const safe = getModelCapability(mdl).safeContext;
|
|
317
|
+
const tok = detailAgent.contextTokens ?? 0;
|
|
318
|
+
const { pct, color } = contextFillInfo(tok, safe);
|
|
319
|
+
meta.push(color(`ctx ${fmtTokens(tok)}/${fmtTokens(safe)} (${pct}%)`));
|
|
320
|
+
}
|
|
248
321
|
if (meta.length > 0)
|
|
249
322
|
rows.push(` ${meta.join(chalk.dim(" \u00b7 "))}`);
|
|
250
323
|
secs.push({ title: `Agent ${detailAgent.id} detail \u00b7 [d] next \u00b7 [Esc] close`, rows });
|
|
@@ -290,8 +363,12 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
|
|
|
290
363
|
// Build footer
|
|
291
364
|
let hotkeyRow;
|
|
292
365
|
const extraFooterRows = [];
|
|
293
|
-
|
|
294
|
-
|
|
366
|
+
// Collapsed panel bar shown in footer area
|
|
367
|
+
if (panel?.visible && !panel.state.expanded) {
|
|
368
|
+
const bar = panel.renderCollapsed(Math.max((process.stdout.columns ?? 80) || 80, 60));
|
|
369
|
+
if (bar)
|
|
370
|
+
extraFooterRows.push(bar);
|
|
371
|
+
}
|
|
295
372
|
if (showHotkeys) {
|
|
296
373
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
297
374
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
@@ -300,7 +377,8 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
|
|
|
300
377
|
const pauseLabel = swarm.paused ? "[p] resume" : "[p] pause";
|
|
301
378
|
const detailChip = swarm.active > 0 ? chalk.dim(" [d] detail") : "";
|
|
302
379
|
const selectChip = swarm.active > 0 && running.length <= 10 ? chalk.dim(" [0-9] select") : "";
|
|
303
|
-
|
|
380
|
+
const panelChip = panel?.visible ? chalk.green(` [Ctrl-O] ${panel.state.expanded ? "collapse" : "expand"}`) : "";
|
|
381
|
+
hotkeyRow = chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip + panelChip;
|
|
304
382
|
if (swarm.blocked > 0 && swarm.blocked === swarm.active) {
|
|
305
383
|
extraFooterRows.push(chalk.yellow(` all workers rate-limited -- [r] retry-now, [c] reduce concurrency, [p] pause, [q] quit`));
|
|
306
384
|
}
|
|
@@ -321,7 +399,7 @@ export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRow
|
|
|
321
399
|
sessionsUsed: (runInfo ? runInfo.accCompleted + runInfo.accFailed : 0) + waveUsed,
|
|
322
400
|
sessionsBudget: runInfo?.sessionsBudget ?? swarm.total,
|
|
323
401
|
remaining: Math.max(0, (runInfo?.remaining ?? swarm.total) - waveUsed),
|
|
324
|
-
usageBarRender: (out, w) => renderUsageBars(out, w, swarm),
|
|
402
|
+
usageBarRender: (out, w) => renderUsageBars(out, w, swarm, selectedAgentId),
|
|
325
403
|
content,
|
|
326
404
|
hotkeyRow,
|
|
327
405
|
extraFooterRows,
|
|
@@ -352,16 +430,25 @@ function renderSteeringUsageBar(out, w, rl) {
|
|
|
352
430
|
const mm = Math.floor(waitSec / 60), ss = waitSec % 60;
|
|
353
431
|
lbl = chalk.red(`Waiting for reset ${mm > 0 ? `${mm}m ${ss}s` : `${ss}s`}`);
|
|
354
432
|
}
|
|
355
|
-
const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("
|
|
433
|
+
const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("RL ");
|
|
356
434
|
out.push(` ${prefix}${barStr} ${lbl}`);
|
|
357
435
|
};
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
436
|
+
const hasRL = rl.utilization > 0 || rl.windows.size > 0 || (rl.resetsAt && rl.resetsAt > Date.now());
|
|
437
|
+
if (hasRL) {
|
|
438
|
+
if (rl.windows.size > 1) {
|
|
439
|
+
const wins = Array.from(rl.windows.values());
|
|
440
|
+
const idx = Math.floor(Date.now() / 3000) % wins.length;
|
|
441
|
+
draw(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
draw(rl.utilization);
|
|
445
|
+
}
|
|
362
446
|
}
|
|
363
|
-
|
|
364
|
-
|
|
447
|
+
if ((rl.contextTokens ?? 0) > 0 && rl.model) {
|
|
448
|
+
const safe = getModelCapability(rl.model).safeContext;
|
|
449
|
+
const tok = rl.contextTokens ?? 0;
|
|
450
|
+
const label = `${fmtTokens(tok)}/${fmtTokens(safe)} safe ${chalk.dim(`planner · ${modelDisplayName(rl.model)}`)}`;
|
|
451
|
+
drawContextBar(out, w, tok, safe, label);
|
|
365
452
|
}
|
|
366
453
|
}
|
|
367
454
|
function renderLastWave(out, w, lw) {
|
|
@@ -399,13 +486,20 @@ function renderStatusBlock(out, w, status) {
|
|
|
399
486
|
for (const ln of lines)
|
|
400
487
|
out.push(` ${chalk.dim(truncate(ln.trim(), w - 4))}`);
|
|
401
488
|
}
|
|
402
|
-
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRows,
|
|
489
|
+
export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRows, panel) {
|
|
403
490
|
const totalUsed = runInfo.accCompleted + runInfo.accFailed;
|
|
404
491
|
const ctx = data.context;
|
|
405
492
|
const content = {
|
|
406
493
|
sections() {
|
|
407
494
|
const secs = [];
|
|
408
495
|
const ww = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
496
|
+
// Expanded panel (debrief, ask, custom) — rendered as first section
|
|
497
|
+
if (panel?.visible && panel.state.expanded) {
|
|
498
|
+
const panelRows = maxRows != null ? Math.max(4, maxRows - 6) : 12;
|
|
499
|
+
const lines = panel.renderExpanded(ww, panelRows);
|
|
500
|
+
if (lines.length > 0)
|
|
501
|
+
secs.push({ title: "", rows: lines });
|
|
502
|
+
}
|
|
409
503
|
// Objective (undecorated -- raw line)
|
|
410
504
|
if (ctx?.objective) {
|
|
411
505
|
const obj = ctx.objective.replace(/\s+/g, " ").trim();
|
|
@@ -462,7 +556,7 @@ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRow
|
|
|
462
556
|
const usageBarRender = rlGetter
|
|
463
557
|
? (out, w) => {
|
|
464
558
|
const rl = rlGetter();
|
|
465
|
-
if (rl && (rl.utilization > 0 || rl.windows.size > 0)) {
|
|
559
|
+
if (rl && (rl.utilization > 0 || rl.windows.size > 0 || (rl.contextTokens ?? 0) > 0)) {
|
|
466
560
|
renderSteeringUsageBar(out, w, rl);
|
|
467
561
|
}
|
|
468
562
|
}
|
|
@@ -470,12 +564,17 @@ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRow
|
|
|
470
564
|
// Footer
|
|
471
565
|
let hotkeyRow;
|
|
472
566
|
const extraFooterRows = [];
|
|
473
|
-
|
|
474
|
-
|
|
567
|
+
// Collapsed panel bar shown in footer area
|
|
568
|
+
if (panel?.visible && !panel.state.expanded) {
|
|
569
|
+
const bar = panel.renderCollapsed(Math.max((process.stdout.columns ?? 80) || 80, 60));
|
|
570
|
+
if (bar)
|
|
571
|
+
extraFooterRows.push(bar);
|
|
572
|
+
}
|
|
475
573
|
if (showHotkeys) {
|
|
476
574
|
const pending = runInfo?.pendingSteer ?? 0;
|
|
477
575
|
const chip = pending > 0 ? chalk.cyan(` \u270E ${pending} steer queued`) : "";
|
|
478
|
-
|
|
576
|
+
const panelChip = panel?.visible ? chalk.green(` [Ctrl-O] ${panel.state.expanded ? "collapse" : "expand"}`) : "";
|
|
577
|
+
hotkeyRow = chalk.dim(" [b] budget [s] steer [q] stop") + chip + panelChip;
|
|
479
578
|
}
|
|
480
579
|
return renderUnifiedFrame({
|
|
481
580
|
model: runInfo.model,
|
|
@@ -502,11 +601,12 @@ export function renderSteeringFrame(runInfo, data, showHotkeys, rlGetter, maxRow
|
|
|
502
601
|
export function renderSummary(swarm) {
|
|
503
602
|
const w = Math.max((process.stdout.columns ?? 80) || 80, 60);
|
|
504
603
|
const out = [];
|
|
505
|
-
const
|
|
604
|
+
const ctxW = 5;
|
|
605
|
+
const fixedW = 3 + 6 + 8 + 5 + 5 + 8 + ctxW + 14;
|
|
506
606
|
const taskW = Math.max(10, w - fixedW);
|
|
507
607
|
out.push("");
|
|
508
608
|
out.push(chalk.gray(" " + "#".padStart(3) + " " + "Status".padEnd(6) + " " + "Task".padEnd(taskW) +
|
|
509
|
-
" " + "Duration".padStart(8) + " " + "Files".padStart(5) + " " + "Tools".padStart(5) + " " + "Cost".padStart(8)));
|
|
609
|
+
" " + "Duration".padStart(8) + " " + "Files".padStart(5) + " " + "Tools".padStart(5) + " " + "Ctx%".padStart(ctxW) + " " + "Cost".padStart(8)));
|
|
510
610
|
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, fixedW + taskW))));
|
|
511
611
|
const groups = [
|
|
512
612
|
swarm.agents.filter(a => a.status === "running"),
|
|
@@ -516,6 +616,7 @@ export function renderSummary(swarm) {
|
|
|
516
616
|
].filter(g => g.length > 0);
|
|
517
617
|
const thinSep = chalk.gray(" " + "\u254C".repeat(Math.min(w - 4, fixedW + taskW)));
|
|
518
618
|
let totalDurMs = 0, totalFiles = 0, totalTools = 0, totalCost = 0;
|
|
619
|
+
let peakCtxPct = 0;
|
|
519
620
|
for (let gi = 0; gi < groups.length; gi++) {
|
|
520
621
|
if (gi > 0)
|
|
521
622
|
out.push(thinSep);
|
|
@@ -532,17 +633,25 @@ export function renderSummary(swarm) {
|
|
|
532
633
|
const files = String(a.filesChanged ?? 0).padStart(5);
|
|
533
634
|
const tools = String(a.toolCalls).padStart(5);
|
|
534
635
|
const cost = a.costUsd != null ? `$${a.costUsd.toFixed(3)}`.padStart(8) : "".padStart(8);
|
|
636
|
+
const mdl = a.task.model || swarm.model || "unknown";
|
|
637
|
+
const safe = getModelCapability(mdl).safeContext;
|
|
638
|
+
const ctxTok = a.contextTokens ?? 0;
|
|
639
|
+
const { pct: ctxPct, color: ctxColor } = ctxTok > 0 ? contextFillInfo(ctxTok, safe) : { pct: 0, color: chalk.gray };
|
|
640
|
+
if (ctxPct > peakCtxPct)
|
|
641
|
+
peakCtxPct = ctxPct;
|
|
642
|
+
const ctxCell = ctxTok > 0 ? `${ctxPct}%`.padStart(ctxW) : "".padStart(ctxW);
|
|
535
643
|
totalDurMs += durMs;
|
|
536
644
|
totalFiles += a.filesChanged ?? 0;
|
|
537
645
|
totalTools += a.toolCalls;
|
|
538
646
|
totalCost += a.costUsd ?? 0;
|
|
539
647
|
const color = ok ? chalk.white : a.status === "running" ? chalk.blue : a.status === "paused" ? chalk.yellow : chalk.red;
|
|
540
|
-
out.push(color(` ${id} ${status} ${task} ${dur} ${files} ${tools} ${cost}`));
|
|
648
|
+
out.push(color(` ${id} ${status} ${task} ${dur} ${files} ${tools} `) + (ctxTok > 0 ? ctxColor(ctxCell) : chalk.gray(ctxCell)) + color(` ${cost}`));
|
|
541
649
|
}
|
|
542
650
|
}
|
|
543
651
|
out.push(chalk.gray(" " + "\u2500".repeat(Math.min(w - 4, fixedW + taskW))));
|
|
544
652
|
const label = `${swarm.agents.length} tasks`.padEnd(taskW);
|
|
545
|
-
|
|
653
|
+
const peakCell = peakCtxPct > 0 ? `${peakCtxPct}%`.padStart(ctxW) : "".padStart(ctxW);
|
|
654
|
+
out.push(chalk.bold(` ${"".padStart(3)} ${"Total ".padEnd(6)} ${label} ${fmtDur(totalDurMs).padStart(8)} ${String(totalFiles).padStart(5)} ${String(totalTools).padStart(5)} ${peakCell} ${`$${totalCost.toFixed(3)}`.padStart(8)}`));
|
|
546
655
|
out.push("");
|
|
547
656
|
return out.join("\n");
|
|
548
657
|
}
|
package/dist/run.js
CHANGED
|
@@ -4,7 +4,9 @@ import { execSync } from "child_process";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { Swarm } from "./swarm.js";
|
|
6
6
|
import { steerWave } from "./steering.js";
|
|
7
|
-
import { getTotalPlannerCost, getPlannerRateLimitInfo, runPlannerQuery, setPlannerEnvResolver } from "./planner-query.js";
|
|
7
|
+
import { getTotalPlannerCost, getPlannerRateLimitInfo, getPeakPlannerContext, runPlannerQuery, setPlannerEnvResolver } from "./planner-query.js";
|
|
8
|
+
import { contextFillInfo } from "./render.js";
|
|
9
|
+
import { getModelCapability } from "./models.js";
|
|
8
10
|
import { buildEnvResolver, isCursorProxyProvider } from "./providers.js";
|
|
9
11
|
import { RunDisplay } from "./ui.js";
|
|
10
12
|
import { renderSummary } from "./render.js";
|
|
@@ -47,6 +49,7 @@ export async function executeRun(cfg) {
|
|
|
47
49
|
const waveHistory = [];
|
|
48
50
|
let accCost, accCompleted, accFailed, accTools;
|
|
49
51
|
let accIn = 0, accOut = 0;
|
|
52
|
+
let peakWorkerCtxPct = 0, peakWorkerCtxTokens = 0;
|
|
50
53
|
let lastCapped = false, lastAborted = false, objectiveComplete = false, lastHealed = false;
|
|
51
54
|
let lastEstimate;
|
|
52
55
|
const branches = [];
|
|
@@ -429,6 +432,18 @@ export async function executeRun(cfg) {
|
|
|
429
432
|
accCompleted += swarm.completed;
|
|
430
433
|
accFailed += swarm.failed;
|
|
431
434
|
accTools += swarm.agents.reduce((sum, a) => sum + a.toolCalls, 0);
|
|
435
|
+
for (const a of swarm.agents) {
|
|
436
|
+
const tok = a.contextTokens ?? 0;
|
|
437
|
+
if (tok <= 0)
|
|
438
|
+
continue;
|
|
439
|
+
const mdl = a.task.model || swarm.model || "unknown";
|
|
440
|
+
const safe = getModelCapability(mdl).safeContext;
|
|
441
|
+
const { pct } = contextFillInfo(tok, safe);
|
|
442
|
+
if (pct > peakWorkerCtxPct) {
|
|
443
|
+
peakWorkerCtxPct = pct;
|
|
444
|
+
peakWorkerCtxTokens = tok;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
432
447
|
remaining = Math.max(0, remaining - swarm.completed - swarm.failed);
|
|
433
448
|
const totalConsumed = accCompleted + accFailed + cfg.thinkingUsed;
|
|
434
449
|
const expectedFloor = Math.max(0, cfg.budget - totalConsumed);
|
|
@@ -619,11 +634,21 @@ export async function executeRun(cfg) {
|
|
|
619
634
|
console.log(chalk.bold.yellow(` CLAUDE OVERNIGHT -- STOPPED`));
|
|
620
635
|
console.log(chalk.green(` ${bannerChar.repeat(Math.min(termW - 4, 60))}`));
|
|
621
636
|
console.log("");
|
|
637
|
+
const peakPlanner = getPeakPlannerContext();
|
|
638
|
+
const plannerSafe = peakPlanner.model ? getModelCapability(peakPlanner.model).safeContext : 0;
|
|
639
|
+
const plannerPct = plannerSafe > 0 && peakPlanner.tokens > 0 ? Math.round((peakPlanner.tokens / plannerSafe) * 100) : 0;
|
|
640
|
+
const colorByPct = (pct) => pct > 80 ? chalk.red : pct > 50 ? chalk.yellow : chalk.green;
|
|
641
|
+
const fmtCtx = (tok, pct) => {
|
|
642
|
+
if (tok <= 0)
|
|
643
|
+
return chalk.dim("—");
|
|
644
|
+
return colorByPct(pct)(`${fmtTokens(tok)} (${pct}%)`);
|
|
645
|
+
};
|
|
622
646
|
const statRows = [
|
|
623
647
|
[chalk.bold("Waves"), String(waves), chalk.bold("Sessions"), `${accCompleted} done${accFailed > 0 ? ` / ${accFailed} failed` : ""}${remaining > 0 ? ` (${remaining} remaining)` : ""}`],
|
|
624
648
|
[chalk.bold("Cost"), chalk.green(`$${accCost.toFixed(2)}`), chalk.bold("Elapsed"), elapsedStr],
|
|
625
649
|
[chalk.bold("Merged"), `${totalMerged} branches`, chalk.bold("Conflicts"), totalConflicts > 0 ? chalk.red(String(totalConflicts)) : chalk.green("0")],
|
|
626
650
|
[chalk.bold("Tokens"), `${fmtTokens(accIn)} in / ${fmtTokens(accOut)} out`, chalk.bold("Tool calls"), String(accTools)],
|
|
651
|
+
[chalk.bold("Peak ctx"), `worker ${fmtCtx(peakWorkerCtxTokens, peakWorkerCtxPct)}`, chalk.bold(""), `planner ${fmtCtx(peakPlanner.tokens, plannerPct)}`],
|
|
627
652
|
];
|
|
628
653
|
for (const [k1, v1, k2, v2] of statRows)
|
|
629
654
|
console.log(` ${k1} ${v1.padEnd(20)} ${k2} ${v2}`);
|
package/dist/state.js
CHANGED
|
@@ -18,6 +18,14 @@ export function readMdDir(dir) {
|
|
|
18
18
|
return "";
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
function hasMdFiles(dir) {
|
|
22
|
+
try {
|
|
23
|
+
return readdirSync(dir).some(f => f.endsWith(".md"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
21
29
|
export function readRunMemory(runDir, previousRuns) {
|
|
22
30
|
let goal = "", status = "";
|
|
23
31
|
try {
|
|
@@ -194,6 +202,16 @@ export function findIncompleteRuns(rootDir, filterCwd) {
|
|
|
194
202
|
const state = loadRunState(runDir);
|
|
195
203
|
if (!state || state.phase === "done" || state.cwd !== filterCwd)
|
|
196
204
|
continue;
|
|
205
|
+
// Filter empty planning shells: no tasks.json, no designs/, no spent
|
|
206
|
+
// cost or completed sessions — nothing to resume.
|
|
207
|
+
if (state.phase === "planning"
|
|
208
|
+
&& !existsSync(join(runDir, "tasks.json"))
|
|
209
|
+
&& !hasMdFiles(join(runDir, "designs"))
|
|
210
|
+
&& (state.accCost ?? 0) === 0
|
|
211
|
+
&& (state.accCompleted ?? 0) === 0
|
|
212
|
+
&& (state.accFailed ?? 0) === 0) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
197
215
|
results.push({ dir: runDir, state });
|
|
198
216
|
}
|
|
199
217
|
return results;
|
package/dist/swarm.d.ts
CHANGED
package/dist/swarm.js
CHANGED
|
@@ -3,9 +3,10 @@ import { join } from "path";
|
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
-
import { NudgeError, RATE_LIMIT_WINDOW_SHORT } from "./types.js";
|
|
6
|
+
import { NudgeError, RATE_LIMIT_WINDOW_SHORT, extractToolTarget, sumUsageTokens } from "./types.js";
|
|
7
7
|
import { gitExec, autoCommit, mergeAllBranches, warnDirtyTree, cleanStaleWorktrees, writeSwarmLog } from "./merge.js";
|
|
8
8
|
import { ensureCursorProxyRunning } from "./providers.js";
|
|
9
|
+
import { getModelCapability } from "./models.js";
|
|
9
10
|
const SIMPLIFY_PROMPT = `You just finished your task. Now review and simplify your changes.
|
|
10
11
|
|
|
11
12
|
Run \`git diff\` to see what you changed, then fix any issues:
|
|
@@ -76,6 +77,7 @@ export class Swarm {
|
|
|
76
77
|
// with empty `input` and streams the real payload via `input_json_delta`, so we
|
|
77
78
|
// need to wait for content_block_stop before we can log the file/path target.
|
|
78
79
|
pendingTools = new WeakMap();
|
|
80
|
+
ctxWarned = new WeakSet();
|
|
79
81
|
logFile;
|
|
80
82
|
model;
|
|
81
83
|
usageCap;
|
|
@@ -449,7 +451,7 @@ export class Swarm {
|
|
|
449
451
|
return;
|
|
450
452
|
}
|
|
451
453
|
const id = this.nextId++;
|
|
452
|
-
const agent = { id, task, status: "running", startedAt: Date.now(), toolCalls: 0 };
|
|
454
|
+
const agent = { id, task, status: "running", startedAt: Date.now(), toolCalls: 0, contextTokens: 0 };
|
|
453
455
|
this.agents.push(agent);
|
|
454
456
|
let agentCwd = task.agentCwd || task.cwd || this.config.cwd;
|
|
455
457
|
if (this.config.useWorktrees && this.worktreeBase && !task.noWorktree && !task.agentCwd) {
|
|
@@ -706,12 +708,7 @@ export class Swarm {
|
|
|
706
708
|
// ── Message handler ──
|
|
707
709
|
/** Log a tool invocation with a short target extracted from its input. */
|
|
708
710
|
logToolUse(agent, name, input) {
|
|
709
|
-
const
|
|
710
|
-
const target = typeof p === "string" && p
|
|
711
|
-
? p
|
|
712
|
-
: typeof input.command === "string" && input.command
|
|
713
|
-
? input.command.split(" ").slice(0, 3).join(" ")
|
|
714
|
-
: "";
|
|
711
|
+
const target = extractToolTarget(input);
|
|
715
712
|
this.log(agent.id, target ? `${name} \u2192 ${target}` : name);
|
|
716
713
|
}
|
|
717
714
|
handleMsg(agent, msg) {
|
|
@@ -725,6 +722,22 @@ export class Swarm {
|
|
|
725
722
|
switch (msg.type) {
|
|
726
723
|
case "assistant": {
|
|
727
724
|
const m = msg;
|
|
725
|
+
const u = m.message?.usage;
|
|
726
|
+
if (u) {
|
|
727
|
+
const turnTotal = sumUsageTokens(u);
|
|
728
|
+
if (turnTotal > (agent.contextTokens ?? 0)) {
|
|
729
|
+
agent.contextTokens = turnTotal;
|
|
730
|
+
if (!this.ctxWarned.has(agent)) {
|
|
731
|
+
const mdl = agent.task.model || this.config.model || "unknown";
|
|
732
|
+
const safe = getModelCapability(mdl).safeContext;
|
|
733
|
+
if (safe > 0 && turnTotal > safe * 0.8) {
|
|
734
|
+
this.ctxWarned.add(agent);
|
|
735
|
+
const pct = Math.round((turnTotal / safe) * 100);
|
|
736
|
+
this.log(agent.id, `\u26A0 context ${pct}% of safe window — task may degrade`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
728
741
|
if (!m.message?.content)
|
|
729
742
|
break;
|
|
730
743
|
for (const block of m.message.content) {
|
package/dist/types.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface AgentState {
|
|
|
80
80
|
filesChanged?: number;
|
|
81
81
|
/** Unix timestamp (ms) when this agent entered a rate-limit wait inside its retry loop. Cleared when work resumes. */
|
|
82
82
|
blockedAt?: number;
|
|
83
|
+
/** Peak total input tokens (input + cache_read + cache_creation) seen in any single turn — a proxy for current context-window occupancy. */
|
|
84
|
+
contextTokens?: number;
|
|
83
85
|
}
|
|
84
86
|
/** A timestamped log line from an agent's execution. */
|
|
85
87
|
export interface LogEntry {
|
|
@@ -151,7 +153,7 @@ export interface SteerResult {
|
|
|
151
153
|
statusUpdate?: string;
|
|
152
154
|
estimatedSessionsRemaining?: number;
|
|
153
155
|
}
|
|
154
|
-
/**
|
|
156
|
+
/** RunMemory accumulates run designs, reflections, verifications, milestones, status, goal, and previous runs. */
|
|
155
157
|
export interface RunMemory {
|
|
156
158
|
designs: string;
|
|
157
159
|
reflections: string;
|
|
@@ -235,3 +237,20 @@ export interface RunState extends RunConfigBase {
|
|
|
235
237
|
/** Working directory for the run. */
|
|
236
238
|
cwd: string;
|
|
237
239
|
}
|
|
240
|
+
/** Function that returns a rate-limit snapshot with optional context token info. */
|
|
241
|
+
export type RLGetter = () => {
|
|
242
|
+
utilization: number;
|
|
243
|
+
isUsingOverage: boolean;
|
|
244
|
+
windows: Map<string, RateLimitWindow>;
|
|
245
|
+
resetsAt?: number;
|
|
246
|
+
contextTokens?: number;
|
|
247
|
+
model?: string;
|
|
248
|
+
};
|
|
249
|
+
/** Pick a short, human-readable target for a tool invocation (Read/Grep/Bash/...). */
|
|
250
|
+
export declare function extractToolTarget(input: Record<string, unknown> | undefined): string;
|
|
251
|
+
/** Sum input + cache read + cache creation tokens from a usage object. */
|
|
252
|
+
export declare function sumUsageTokens(u: {
|
|
253
|
+
input_tokens?: number;
|
|
254
|
+
cache_read_input_tokens?: number;
|
|
255
|
+
cache_creation_input_tokens?: number;
|
|
256
|
+
}): number;
|
package/dist/types.js
CHANGED
|
@@ -11,3 +11,19 @@ export class NudgeError extends Error {
|
|
|
11
11
|
this.name = "NudgeError";
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/** Pick a short, human-readable target for a tool invocation (Read/Grep/Bash/...). */
|
|
15
|
+
export function extractToolTarget(input) {
|
|
16
|
+
if (!input)
|
|
17
|
+
return "";
|
|
18
|
+
const p = input.path ?? input.file_path ?? input.pattern;
|
|
19
|
+
if (typeof p === "string" && p)
|
|
20
|
+
return p;
|
|
21
|
+
if (typeof input.command === "string" && input.command) {
|
|
22
|
+
return input.command.split(" ").slice(0, 3).join(" ");
|
|
23
|
+
}
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
/** Sum input + cache read + cache creation tokens from a usage object. */
|
|
27
|
+
export function sumUsageTokens(u) {
|
|
28
|
+
return (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0);
|
|
29
|
+
}
|
package/dist/ui.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Swarm } from "./swarm.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { RLGetter, WaveSummary } from "./types.js";
|
|
3
|
+
import { InteractivePanel } from "./interactive-panel.js";
|
|
3
4
|
/** Short-lived context the steering view renders around its live log. */
|
|
4
5
|
export interface SteeringContext {
|
|
5
6
|
objective?: string;
|
|
@@ -43,12 +44,6 @@ export interface AskState {
|
|
|
43
44
|
streaming: boolean;
|
|
44
45
|
error?: string;
|
|
45
46
|
}
|
|
46
|
-
type RLGetter = () => {
|
|
47
|
-
utilization: number;
|
|
48
|
-
isUsingOverage: boolean;
|
|
49
|
-
windows: Map<string, RateLimitWindow>;
|
|
50
|
-
resetsAt?: number;
|
|
51
|
-
};
|
|
52
47
|
export declare class RunDisplay {
|
|
53
48
|
readonly runInfo: RunInfo;
|
|
54
49
|
private liveConfig?;
|
|
@@ -72,12 +67,11 @@ export declare class RunDisplay {
|
|
|
72
67
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
73
68
|
private selectedAgentId?;
|
|
74
69
|
private navState;
|
|
70
|
+
/** Interactive panel for debrief, Q&A, and other user-facing content. */
|
|
71
|
+
readonly panel: InteractivePanel;
|
|
75
72
|
private onSteer?;
|
|
76
73
|
private onAsk?;
|
|
77
|
-
|
|
78
|
-
/** Get the latest debrief line for footer rendering. */
|
|
79
|
-
getDebrief(): string | undefined;
|
|
80
|
-
/** Set or clear the debrief text shown in the footer. */
|
|
74
|
+
/** Set or clear the debrief text shown in the interactive panel. */
|
|
81
75
|
setDebrief(text: string | undefined): void;
|
|
82
76
|
constructor(runInfo: RunInfo, liveConfig?: LiveConfig, callbacks?: {
|
|
83
77
|
onSteer?: (text: string) => void;
|
|
@@ -124,7 +118,6 @@ export declare class RunDisplay {
|
|
|
124
118
|
private flush;
|
|
125
119
|
private render;
|
|
126
120
|
private renderInputPrompt;
|
|
127
|
-
private renderAskPanel;
|
|
128
121
|
private hasHotkeys;
|
|
129
122
|
private setupHotkeys;
|
|
130
123
|
/** Handle a pasted block. Returns true if the frame needs a redraw. */
|
|
@@ -145,4 +138,3 @@ export declare class RunDisplay {
|
|
|
145
138
|
private handleTyped;
|
|
146
139
|
private plainTick;
|
|
147
140
|
}
|
|
148
|
-
export {};
|
package/dist/ui.js
CHANGED
|
@@ -5,9 +5,14 @@ import { mkdtempSync, writeFileSync, rmSync } from "fs";
|
|
|
5
5
|
import { tmpdir } from "os";
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
import { execSync } from "child_process";
|
|
8
|
+
import { InteractivePanel } from "./interactive-panel.js";
|
|
8
9
|
const MAX_STEERING_EVENTS = 60;
|
|
9
10
|
const MAX_INPUT_LEN = 600;
|
|
10
11
|
const MAX_ASK_LINES = 40;
|
|
12
|
+
/** Visible lines for the ask panel, clamped to leave room for header/content/footer/input. */
|
|
13
|
+
function askDisplayCap() {
|
|
14
|
+
return Math.max(3, Math.min(MAX_ASK_LINES, (process.stdout.rows || 40) - 20));
|
|
15
|
+
}
|
|
11
16
|
let askTempDir;
|
|
12
17
|
export class RunDisplay {
|
|
13
18
|
runInfo;
|
|
@@ -32,13 +37,19 @@ export class RunDisplay {
|
|
|
32
37
|
/** ID of the agent whose detail panel is open; undefined = no detail shown. */
|
|
33
38
|
selectedAgentId;
|
|
34
39
|
navState = { focusSection: 0, focusRow: 0, scrollOffset: 0 };
|
|
40
|
+
/** Interactive panel for debrief, Q&A, and other user-facing content. */
|
|
41
|
+
panel = new InteractivePanel();
|
|
35
42
|
onSteer;
|
|
36
43
|
onAsk;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
/** Set or clear the debrief text shown in the interactive panel. */
|
|
45
|
+
setDebrief(text) {
|
|
46
|
+
if (text) {
|
|
47
|
+
this.panel.set({ mode: "debrief", header: "Debrief", preview: text, body: text });
|
|
48
|
+
}
|
|
49
|
+
else if (this.panel.state.mode === "debrief") {
|
|
50
|
+
this.panel.set({ mode: "none", header: "", preview: "", body: "" });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
42
53
|
constructor(runInfo, liveConfig, callbacks) {
|
|
43
54
|
this.runInfo = runInfo;
|
|
44
55
|
this.liveConfig = liveConfig;
|
|
@@ -54,7 +65,7 @@ export class RunDisplay {
|
|
|
54
65
|
// Write full answer to temp file when streaming is done and answer is long
|
|
55
66
|
if (state && !state.streaming && !state.error && state.answer) {
|
|
56
67
|
const lines = state.answer.split("\n");
|
|
57
|
-
if (lines.length >
|
|
68
|
+
if (lines.length > askDisplayCap()) {
|
|
58
69
|
try {
|
|
59
70
|
askTempDir = mkdtempSync(join(tmpdir(), "overnight-ask-"));
|
|
60
71
|
this.askTempFile = join(askTempDir, "answer.txt");
|
|
@@ -62,6 +73,20 @@ export class RunDisplay {
|
|
|
62
73
|
}
|
|
63
74
|
catch { }
|
|
64
75
|
}
|
|
76
|
+
// Also populate the panel with the Q&A content
|
|
77
|
+
const preview = state.answer.split("\n")[0]?.slice(0, 120) || "(answered)";
|
|
78
|
+
this.panel.set({
|
|
79
|
+
mode: "ask",
|
|
80
|
+
header: `Ask`,
|
|
81
|
+
preview,
|
|
82
|
+
body: `Q: ${state.question}\n\nA: ${state.answer}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (state && state.error) {
|
|
86
|
+
this.panel.set({ mode: "ask", header: "Ask", preview: state.error, body: `Q: ${state.question}\n\nError: ${state.error}` });
|
|
87
|
+
}
|
|
88
|
+
else if (!state && this.panel.state.mode === "ask") {
|
|
89
|
+
this.panel.set({ mode: "none", header: "", preview: "", body: "" });
|
|
65
90
|
}
|
|
66
91
|
}
|
|
67
92
|
/** Signal to the UI whether an ask is in progress (prevents duplicate firings). */
|
|
@@ -364,23 +389,25 @@ export class RunDisplay {
|
|
|
364
389
|
}
|
|
365
390
|
}
|
|
366
391
|
render(maxRows) {
|
|
367
|
-
// Compute how many rows the input prompt +
|
|
368
|
-
// main frame can shrink its content area to leave room.
|
|
392
|
+
// Compute how many rows the bottom area (input prompt + collapsed panel) need.
|
|
369
393
|
const inputPrompt = this.renderInputPrompt();
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
394
|
+
const panelCollapsed = this.panel.visible && !this.panel.state.expanded
|
|
395
|
+
? this.panel.renderCollapsed(Math.max((process.stdout.columns ?? 80) || 80, 60))
|
|
396
|
+
: "";
|
|
397
|
+
const panelExpanded = this.panel.visible && this.panel.state.expanded ? 1 : 0; // 1 = header row, content handled by frame
|
|
398
|
+
const bottom = inputPrompt + (panelCollapsed ? "\n" + panelCollapsed : "");
|
|
399
|
+
const bottomRows = bottom ? (bottom.match(/\n/g) || []).length + 1 : 0;
|
|
373
400
|
const frameBudget = maxRows != null ? maxRows - bottomRows : undefined;
|
|
374
401
|
let frame = "";
|
|
375
402
|
if (this.swarm) {
|
|
376
|
-
frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo, this.selectedAgentId, frameBudget, this.
|
|
403
|
+
frame = renderFrame(this.swarm, this.hasHotkeys(), this.runInfo, this.selectedAgentId, frameBudget, this.panel);
|
|
377
404
|
}
|
|
378
405
|
else if (this.steeringActive) {
|
|
379
406
|
frame = renderSteeringFrame(this.runInfo, {
|
|
380
407
|
statusLine: this.steeringStatusLine,
|
|
381
408
|
events: this.steeringEvents,
|
|
382
409
|
context: this.steeringContext,
|
|
383
|
-
}, this.hasHotkeys(), this.rlGetter, frameBudget, this.
|
|
410
|
+
}, this.hasHotkeys(), this.rlGetter, frameBudget, this.panel);
|
|
384
411
|
}
|
|
385
412
|
else {
|
|
386
413
|
return "";
|
|
@@ -411,34 +438,6 @@ export class RunDisplay {
|
|
|
411
438
|
}
|
|
412
439
|
return "";
|
|
413
440
|
}
|
|
414
|
-
renderAskPanel() {
|
|
415
|
-
const a = this.askState;
|
|
416
|
-
if (!a)
|
|
417
|
-
return "";
|
|
418
|
-
const out = ["", chalk.gray(" \u2500\u2500\u2500 Ask " + "\u2500".repeat(40))];
|
|
419
|
-
out.push(` ${chalk.bold.cyan("Q:")} ${a.question}`);
|
|
420
|
-
if (a.error) {
|
|
421
|
-
out.push(` ${chalk.red("A:")} ${chalk.red(a.error)}`);
|
|
422
|
-
}
|
|
423
|
-
else if (a.streaming) {
|
|
424
|
-
out.push(` ${chalk.dim("A: " + (a.answer || "thinking..."))}`);
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
const allLines = a.answer.split("\n");
|
|
428
|
-
const showLines = allLines.slice(0, MAX_ASK_LINES);
|
|
429
|
-
out.push(` ${chalk.bold.green("A:")} ${showLines[0] || ""}`);
|
|
430
|
-
for (const ln of showLines.slice(1))
|
|
431
|
-
out.push(` ${ln}`);
|
|
432
|
-
if (allLines.length > MAX_ASK_LINES) {
|
|
433
|
-
const overflow = allLines.length - MAX_ASK_LINES;
|
|
434
|
-
out.push(chalk.dim(` \u2026 + ${overflow} more lines`));
|
|
435
|
-
if (this.askTempFile) {
|
|
436
|
-
out.push(chalk.dim(" \u23CE Enter to reveal full answer in Finder"));
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
return "\n" + out.join("\n");
|
|
441
|
-
}
|
|
442
441
|
hasHotkeys() {
|
|
443
442
|
return !!this.liveConfig && !!process.stdin.isTTY;
|
|
444
443
|
}
|
|
@@ -508,6 +507,24 @@ export class RunDisplay {
|
|
|
508
507
|
// ── 1. Arrow keys: \x1B[A = up, \x1B[B = down, \x1B[C = right, \x1B[D = left ──
|
|
509
508
|
if (s.startsWith("\x1B[")) {
|
|
510
509
|
const dir = s[2];
|
|
510
|
+
// When panel is expanded, arrows scroll the panel content
|
|
511
|
+
if (this.panel.state.expanded) {
|
|
512
|
+
const rows = Math.max(4, (process.stdout.rows || 40) - 10);
|
|
513
|
+
if (dir === "A") {
|
|
514
|
+
this.panel.scroll("up", rows);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
if (dir === "B") {
|
|
518
|
+
this.panel.scroll("down", rows);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
// Left/right: collapse or pass through
|
|
522
|
+
if (dir === "D") {
|
|
523
|
+
this.panel.collapse();
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
511
528
|
if (dir === "A") {
|
|
512
529
|
this.navigate("up");
|
|
513
530
|
return true;
|
|
@@ -672,6 +689,14 @@ export class RunDisplay {
|
|
|
672
689
|
}
|
|
673
690
|
return true;
|
|
674
691
|
}
|
|
692
|
+
// Ctrl+O: toggle interactive panel expand/collapse
|
|
693
|
+
if (s === "\x0F") {
|
|
694
|
+
if (this.panel.visible) {
|
|
695
|
+
this.panel.toggle();
|
|
696
|
+
return true;
|
|
697
|
+
}
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
675
700
|
// Only single printable ASCII characters reach hotkey matching
|
|
676
701
|
if (s.length !== 1)
|
|
677
702
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.24",
|
|
4
4
|
"description": "Parallel Claude agents in git worktrees with a usage cap that reserves headroom for your interactive Claude Code. Crash-safe resume. Provider-agnostic model catalog (Anthropic, Cursor, OpenAI, Gemini, DeepSeek, Llama, Qwen) with capability-based task scoping.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-overnight",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.24",
|
|
4
4
|
"description": "Claude Code skill for understanding, installing, and inspecting claude-overnight runs -- parallel Claude agents in git worktrees with thinking waves, multi-wave steering, and crash-safe resume. Supports Cursor API Proxy, Qwen, OpenRouter.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Francesco Fornace"
|