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.
@@ -1 +1 @@
1
- export declare const VERSION = "1.25.23";
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.23";
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.
@@ -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[];
@@ -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 — same logic as startProxyProcess().
133
- if (!base.CURSOR_AGENT_NODE || !base.CURSOR_AGENT_SCRIPT) {
134
- try {
135
- const sysNode = execSync("which node 2>/dev/null", { timeout: 2_000, encoding: "utf-8", shell: "bash" }).trim();
136
- const agentPath = execSync("command -v agent 2>/dev/null || command -v cursor-agent 2>/dev/null", {
137
- timeout: 2_000, encoding: "utf-8", shell: "bash",
138
- }).trim();
139
- if (sysNode && agentPath) {
140
- const agentDir = dirname(realpathSync(agentPath));
141
- const indexPath = `${agentDir}/index.js`;
142
- if (existsSync(indexPath)) {
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
- let sysNode = null;
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 { RateLimitWindow } from "./types.js";
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
- type RLGetter = () => {
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, debrief?: string): string;
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 renderUsageBars(out, w, swarm) {
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
- if (rlPct <= 0 && !swarm.rateLimitResetsAt && !swarm.cappedOut && swarm.rateLimitPaused <= 0 && windows.length === 0)
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("Usage ");
154
+ const prefix = windowLabel ? chalk.dim(windowLabel.padEnd(6)) : chalk.dim("RL ");
111
155
  out.push(` ${prefix}${barStr} ${label}`);
112
156
  };
113
- if (windows.length > 1) {
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
- return [...header, ...content, ...footer].join("\n");
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
- export function renderFrame(swarm, showHotkeys, runInfo, selectedAgentId, maxRows, debrief) {
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
- if (debrief)
294
- extraFooterRows.push(chalk.dim(` ${debrief}`));
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
- hotkeyRow = chalk.dim(` [b] budget [t] cap [c] conc [e] extra ${pauseLabel} [s] steer [?] ask [q] stop`) + fixChip + retryChip + chip + detailChip + selectChip;
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("Usage ");
433
+ const prefix = label ? chalk.dim(label.padEnd(6)) : chalk.dim("RL ");
356
434
  out.push(` ${prefix}${barStr} ${lbl}`);
357
435
  };
358
- if (rl.windows.size > 1) {
359
- const wins = Array.from(rl.windows.values());
360
- const idx = Math.floor(Date.now() / 3000) % wins.length;
361
- draw(wins[idx].utilization, wins[idx].type.replace(/_/g, " ").slice(0, 5));
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
- else {
364
- draw(rl.utilization);
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, debrief) {
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
- if (debrief)
474
- extraFooterRows.push(chalk.dim(` ${debrief}`));
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
- hotkeyRow = chalk.dim(" [b] budget [s] steer [q] stop") + chip;
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 fixedW = 3 + 6 + 8 + 5 + 5 + 8 + 12 + 2;
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
- out.push(chalk.bold(` ${"".padStart(3)} ${"Total ".padEnd(6)} ${label} ${fmtDur(totalDurMs).padStart(8)} ${String(totalFiles).padStart(5)} ${String(totalTools).padStart(5)} ${`$${totalCost.toFixed(3)}`.padStart(8)}`));
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
@@ -68,6 +68,7 @@ export declare class Swarm {
68
68
  private activeQueries;
69
69
  private cleanedUp;
70
70
  private pendingTools;
71
+ private ctxWarned;
71
72
  logFile?: string;
72
73
  readonly model: string | undefined;
73
74
  usageCap: number | undefined;
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 p = input.path ?? input.file_path ?? input.pattern;
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
- /** Accumulated run memory -- designs, verifications, etc. -- fed to the steerer. */
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 { RateLimitWindow, WaveSummary } from "./types.js";
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
- private debriefText?;
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
- debriefText;
38
- /** Get the latest debrief line for footer rendering. */
39
- getDebrief() { return this.debriefText; }
40
- /** Set or clear the debrief text shown in the footer. */
41
- setDebrief(text) { this.debriefText = text; }
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 > MAX_ASK_LINES) {
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 + ask panel need so the
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 askPanel = this.renderAskPanel();
371
- const bottom = inputPrompt + askPanel;
372
- const bottomRows = bottom ? (bottom.match(/\n/g) || []).length : 0;
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.debriefText);
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.debriefText);
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.23",
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.23",
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"