pi-subagents-lite 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -225
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +8 -5
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +115 -173
- package/src/agents/agent-status.ts +50 -0
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +61 -223
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -271
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
package/src/types.ts
CHANGED
|
@@ -2,50 +2,32 @@
|
|
|
2
2
|
* Type definitions for the subagent system.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { Model } from "@earendil-works/pi-ai";
|
|
5
6
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import type {
|
|
7
|
+
import type { AgentOutputLog } from "./agents/output-file.js";
|
|
8
|
+
import type { LifetimeUsage } from "./agents/usage.js";
|
|
9
|
+
import type { SubagentType, AgentInvocation } from "./agents/types.js";
|
|
7
10
|
|
|
8
11
|
/** Thinking level for agent models. */
|
|
9
12
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
export
|
|
14
|
+
/** Tool activity event: start/end of a tool invocation. */
|
|
15
|
+
export interface ToolActivity {
|
|
16
|
+
type: "start" | "end";
|
|
17
|
+
toolName: string;
|
|
18
|
+
}
|
|
13
19
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Controls which tool schemas the LLM sees. Can reference built-in tools
|
|
23
|
-
* and extension tools. true = all, string[] = listed, false = none.
|
|
24
|
-
* Supports ext/* syntax to include all tools from an extension.
|
|
25
|
-
* Mutually exclusive with excludeTools.
|
|
26
|
-
*/
|
|
27
|
-
tools?: true | string[] | false;
|
|
28
|
-
/** Tool blacklist — all tools except these are visible. Mutually exclusive with tools (when tools is string[]). */
|
|
29
|
-
excludeTools?: string[];
|
|
30
|
-
/** true = inherit all, string[] = only listed, false = none. Mutually exclusive with excludeExtensions. */
|
|
31
|
-
extensions: true | string[] | false;
|
|
32
|
-
/** Extension blacklist — all extensions except these load. Mutually exclusive with extensions (when extensions is string[]). */
|
|
33
|
-
excludeExtensions?: string[];
|
|
34
|
-
/** Whitelist of allowed skills (metadata only in system prompt). true = all, string[] = listed, false = none */
|
|
35
|
-
skills: true | string[] | false;
|
|
36
|
-
/** Skills to preload with full content into system prompt. string[] = listed, false/undefined = none */
|
|
37
|
-
preloadSkills?: string[] | false;
|
|
38
|
-
model?: string;
|
|
39
|
-
thinking?: ThinkingLevel;
|
|
20
|
+
/**
|
|
21
|
+
* Resolved model + run-limit tunables shared by every spawn/run shape
|
|
22
|
+
* (RunOptions, SpawnOptions, SpawnIntent). Add a tunable here once and it
|
|
23
|
+
* flows through the whole chain.
|
|
24
|
+
*/
|
|
25
|
+
export interface RunTunables {
|
|
26
|
+
model?: Model<any>;
|
|
40
27
|
maxTurns?: number;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
isDefault?: boolean;
|
|
45
|
-
/** true = agent is hidden from the schema enum but can still be called by name. */
|
|
46
|
-
hidden?: boolean;
|
|
47
|
-
/** Where this agent was loaded from */
|
|
48
|
-
source?: "project" | "global";
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
thinkingLevel?: ThinkingLevel;
|
|
30
|
+
graceTurns?: number;
|
|
49
31
|
}
|
|
50
32
|
|
|
51
33
|
export interface AgentRecord {
|
|
@@ -62,46 +44,40 @@ export interface AgentRecord {
|
|
|
62
44
|
stats: AgentAccumulatedStats;
|
|
63
45
|
}
|
|
64
46
|
|
|
65
|
-
export interface AgentInvocation {
|
|
66
|
-
/** Short display name, e.g. "haiku" — only set when different from parent. */
|
|
67
|
-
modelName?: string;
|
|
68
|
-
thinking?: ThinkingLevel;
|
|
69
|
-
maxTurns?: number;
|
|
70
|
-
runInBackground?: boolean;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
47
|
export interface EnvInfo {
|
|
74
48
|
isGitRepo: boolean;
|
|
75
49
|
branch: string | null;
|
|
76
50
|
platform: string;
|
|
77
51
|
}
|
|
78
52
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Streaming/callback surface shared by RunOptions and SpawnOptions.
|
|
55
|
+
* Bridges agent-runner events to record tracking and live-view updates.
|
|
56
|
+
*/
|
|
57
|
+
export interface RunCallbacks {
|
|
58
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
59
|
+
onTextDelta?: (delta: string, fullText: string) => void;
|
|
60
|
+
onSessionCreated?: (session: AgentSession) => void;
|
|
61
|
+
onTurnEnd?: (turnCount: number) => void;
|
|
62
|
+
onAssistantUsage?: (usage: LifetimeUsage) => void;
|
|
63
|
+
onCompaction?: (info: CompactionInfo) => void;
|
|
64
|
+
}
|
|
81
65
|
|
|
82
66
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
67
|
+
* Coordinator-side spawn config shared by SpawnOptions and SpawnIntent.
|
|
68
|
+
* The resolved run params that both the manager and coordinator agree on;
|
|
69
|
+
* extends RunTunables with display/identity fields.
|
|
86
70
|
*/
|
|
87
|
-
export
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"forceBackground",
|
|
98
|
-
"graceTurns",
|
|
99
|
-
"showCost",
|
|
100
|
-
"widgetMaxLines",
|
|
101
|
-
"widgetMaxLinesCompact",
|
|
102
|
-
"widgetCompact",
|
|
103
|
-
"widgetShortcut",
|
|
104
|
-
];
|
|
71
|
+
export interface SpawnConfig extends RunTunables {
|
|
72
|
+
description: string;
|
|
73
|
+
modelKey?: string;
|
|
74
|
+
worktreePath?: string;
|
|
75
|
+
worktreeLabel?: string;
|
|
76
|
+
invocation?: AgentInvocation;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** How many characters of agent ID to show in display. */
|
|
80
|
+
export const SHORT_ID_LENGTH = 8;
|
|
105
81
|
|
|
106
82
|
/** Reason for a context compaction event. */
|
|
107
83
|
export type CompactionReason = "manual" | "threshold" | "overflow";
|
|
@@ -117,7 +93,7 @@ export interface CompactionInfo {
|
|
|
117
93
|
// ---------------------------------------------------------------------------
|
|
118
94
|
|
|
119
95
|
/** Possible agent lifecycle statuses. */
|
|
120
|
-
export type AgentStatus = "queued" | "running" | "completed" | "
|
|
96
|
+
export type AgentStatus = "queued" | "running" | "completed" | "turn_limited" | "aborted" | "stopped" | "error";
|
|
121
97
|
|
|
122
98
|
/**
|
|
123
99
|
* Lifecycle state: when the agent started, completed, and its current status.
|
|
@@ -158,8 +134,8 @@ export interface AgentExecutionState {
|
|
|
158
134
|
promise?: Promise<string>;
|
|
159
135
|
/** Steering messages queued before the session was ready. */
|
|
160
136
|
pendingSteers?: string[];
|
|
161
|
-
/**
|
|
162
|
-
|
|
137
|
+
/** Lifecycle wrapper for the output file stream. */
|
|
138
|
+
outputLog?: AgentOutputLog;
|
|
163
139
|
}
|
|
164
140
|
|
|
165
141
|
/**
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
6
|
-
import type { AgentManager } from "../agent-manager.js";
|
|
7
|
-
import type { AgentRecord
|
|
6
|
+
import type { AgentManager } from "../agents/agent-manager.js";
|
|
7
|
+
import type { AgentRecord } from "../types.js";
|
|
8
|
+
import type { Theme } from "./types.js";
|
|
8
9
|
import {
|
|
9
10
|
formatCost,
|
|
10
11
|
getLifetimeTotal,
|
|
11
12
|
getSessionContextPercent,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "../
|
|
15
|
-
import { formatMs, buildStatsParts, getDisplayName } from "../format.js";
|
|
13
|
+
} from "../agents/usage.js";
|
|
14
|
+
import { formatMs, buildStatsParts, getDisplayName, truncateDesc, type StatsVisibility } from "./format.js";
|
|
15
|
+
import type { LiveView } from "../spawn/spawn-coordinator.js";
|
|
16
16
|
|
|
17
17
|
// Re-export Theme so existing consumers (model-selector, result-viewer) don't break
|
|
18
|
-
export type { Theme } from "
|
|
18
|
+
export type { Theme } from "./types.js";
|
|
19
19
|
|
|
20
20
|
// ---- Constants ----
|
|
21
21
|
|
|
@@ -26,7 +26,7 @@ const DEFAULT_MAX_WIDGET_LINES = 12;
|
|
|
26
26
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
27
27
|
|
|
28
28
|
/** Non-success statuses — used for linger behavior and icon rendering. */
|
|
29
|
-
const ERROR_STATUSES = new Set(["error", "aborted", "
|
|
29
|
+
const ERROR_STATUSES = new Set(["error", "aborted", "turn_limited", "stopped"]);
|
|
30
30
|
|
|
31
31
|
/** Tree-drawing connectors used in the widget header/continuation lines. */
|
|
32
32
|
const BRANCH = "├─";
|
|
@@ -83,22 +83,9 @@ interface RenderBlock {
|
|
|
83
83
|
continuations: string[];
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/** Per-agent live activity state. */
|
|
87
|
-
export interface AgentActivity {
|
|
88
|
-
activeTools: Map<string, string>;
|
|
89
|
-
toolUses: number;
|
|
90
|
-
responseText: string;
|
|
91
|
-
session?: SessionLike;
|
|
92
|
-
/** Current turn count. */
|
|
93
|
-
turnCount: number;
|
|
94
|
-
/** Effective max turns for this agent (undefined = unlimited). */
|
|
95
|
-
maxTurns?: number;
|
|
96
|
-
/** Lifetime usage breakdown — see LifetimeUsage docs. */
|
|
97
|
-
lifetimeUsage: LifetimeUsage;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
86
|
// ---- Re-exports from format.ts (backward compatibility) ----
|
|
101
|
-
export { formatMs, buildStatsParts, getDisplayName } from "
|
|
87
|
+
export { formatMs, buildStatsParts, getDisplayName, type StatsVisibility } from "./format.js";
|
|
88
|
+
export type { LiveView as AgentActivity } from "../spawn/spawn-coordinator.js";
|
|
102
89
|
|
|
103
90
|
// ---- Widget-internal helpers ----
|
|
104
91
|
|
|
@@ -161,6 +148,9 @@ export class AgentWidget {
|
|
|
161
148
|
/** Whether to show cost in stats and status bar. */
|
|
162
149
|
private showCost = false;
|
|
163
150
|
|
|
151
|
+
/** Stats visibility flags. Controls which stats appear in the stats line. */
|
|
152
|
+
private statsVisibility: StatsVisibility = {};
|
|
153
|
+
|
|
164
154
|
/** Whether the widget callback is currently registered with the TUI. */
|
|
165
155
|
private widgetRegistered = false;
|
|
166
156
|
/** Cached TUI reference from widget factory callback, used for requestRender(). */
|
|
@@ -185,9 +175,15 @@ export class AgentWidget {
|
|
|
185
175
|
/** Maximum lines for compact mode. */
|
|
186
176
|
private maxLinesCompact = Math.floor(DEFAULT_MAX_WIDGET_LINES / 2);
|
|
187
177
|
|
|
178
|
+
/** Max description length in full mode. */
|
|
179
|
+
private descLengthFull = 50;
|
|
180
|
+
|
|
181
|
+
/** Max description length in compact mode. */
|
|
182
|
+
private descLengthCompact = 30;
|
|
183
|
+
|
|
188
184
|
constructor(
|
|
189
185
|
private manager: AgentManager,
|
|
190
|
-
private
|
|
186
|
+
private getLiveView: (id: string) => LiveView | undefined,
|
|
191
187
|
) {}
|
|
192
188
|
|
|
193
189
|
/** Set whether to show cost in stats and status bar. */
|
|
@@ -195,6 +191,11 @@ export class AgentWidget {
|
|
|
195
191
|
this.showCost = enabled;
|
|
196
192
|
}
|
|
197
193
|
|
|
194
|
+
/** Set stats visibility flags. */
|
|
195
|
+
setStatsVisibility(visible: StatsVisibility) {
|
|
196
|
+
this.statsVisibility = visible;
|
|
197
|
+
}
|
|
198
|
+
|
|
198
199
|
/** Set compact mode (internal, for sync from ctrl+o). */
|
|
199
200
|
setCompactMode(enabled: boolean) {
|
|
200
201
|
if (this.compactMode === enabled) return;
|
|
@@ -228,6 +229,16 @@ export class AgentWidget {
|
|
|
228
229
|
this.maxLinesCompact = lines;
|
|
229
230
|
}
|
|
230
231
|
|
|
232
|
+
/** Set max description length for full mode. */
|
|
233
|
+
setDescLengthFull(len: number) {
|
|
234
|
+
this.descLengthFull = len;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Set max description length for compact mode. */
|
|
238
|
+
setDescLengthCompact(len: number) {
|
|
239
|
+
this.descLengthCompact = len;
|
|
240
|
+
}
|
|
241
|
+
|
|
231
242
|
/** Set the UI context (grabbed from first tool execution). */
|
|
232
243
|
setUICtx(ctx: UICtx) {
|
|
233
244
|
if (ctx !== this.uiCtx) {
|
|
@@ -297,7 +308,7 @@ export class AgentWidget {
|
|
|
297
308
|
switch (status) {
|
|
298
309
|
case "completed":
|
|
299
310
|
return { icon: theme.fg("success", "✓"), statusText: "" };
|
|
300
|
-
case "
|
|
311
|
+
case "turn_limited":
|
|
301
312
|
return { icon: theme.fg("warning", "✓"), statusText: theme.fg("warning", " (turn limit)") };
|
|
302
313
|
case "stopped":
|
|
303
314
|
return { icon: theme.fg("dim", "■"), statusText: theme.fg("dim", " stopped") };
|
|
@@ -314,42 +325,42 @@ export class AgentWidget {
|
|
|
314
325
|
/** Render a finished agent line. */
|
|
315
326
|
private renderFinishedLine(a: AgentRecord, theme: Theme): string {
|
|
316
327
|
const name = getDisplayName(a.display.type);
|
|
317
|
-
const
|
|
328
|
+
const fullDesc = truncateDesc(a.display.description, this.descLengthFull);
|
|
318
329
|
const { icon, statusText } = this.finishedIconAndStatus(a.lifecycle.status, a.error, theme);
|
|
319
330
|
|
|
320
|
-
const
|
|
321
|
-
const usage = activity?.lifetimeUsage ?? a.stats.lifetimeUsage;
|
|
331
|
+
const durationMs = (a.lifecycle.completedAt ?? Date.now()) - a.lifecycle.startedAt;
|
|
322
332
|
const statsParts = buildStatsParts({
|
|
323
333
|
toolUses: a.stats.toolUses,
|
|
324
|
-
turnCount:
|
|
325
|
-
maxTurns:
|
|
326
|
-
|
|
327
|
-
|
|
334
|
+
turnCount: a.stats.turnCount,
|
|
335
|
+
maxTurns: a.stats.maxTurns,
|
|
336
|
+
input: a.stats.lifetimeUsage.input,
|
|
337
|
+
output: a.stats.lifetimeUsage.output,
|
|
338
|
+
contextPercent: a.stats.contextPercent ?? null,
|
|
328
339
|
compactions: a.stats.compactionCount,
|
|
329
|
-
cost:
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
cost: a.stats.lifetimeUsage.cost,
|
|
341
|
+
durationMs,
|
|
342
|
+
}, theme, this.statsVisibility);
|
|
332
343
|
|
|
333
344
|
const statsLine = statsParts.join("·");
|
|
334
|
-
return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim",
|
|
345
|
+
return `${icon} ${theme.fg("dim", name)} ${theme.fg("dim", fullDesc)} ${wrapInDim(theme, statsLine)}${statusText}`;
|
|
335
346
|
}
|
|
336
347
|
|
|
337
348
|
/** Build the stats line (toolUses · turns · tokens · cost · elapsed) for a running agent. */
|
|
338
349
|
private buildStatsLine(
|
|
339
350
|
agent: AgentRecord,
|
|
340
|
-
activity: AgentActivity | undefined,
|
|
341
351
|
theme: Theme,
|
|
342
352
|
): string {
|
|
343
353
|
const parts = buildStatsParts({
|
|
344
|
-
toolUses:
|
|
345
|
-
turnCount:
|
|
346
|
-
maxTurns:
|
|
347
|
-
|
|
348
|
-
|
|
354
|
+
toolUses: agent.stats.toolUses,
|
|
355
|
+
turnCount: agent.stats.turnCount,
|
|
356
|
+
maxTurns: agent.stats.maxTurns,
|
|
357
|
+
input: agent.stats.lifetimeUsage.input,
|
|
358
|
+
output: agent.stats.lifetimeUsage.output,
|
|
359
|
+
contextPercent: agent.execution.session ? getSessionContextPercent(agent.execution.session) : agent.stats.contextPercent ?? null,
|
|
349
360
|
compactions: agent.stats.compactionCount,
|
|
350
|
-
cost:
|
|
351
|
-
|
|
352
|
-
|
|
361
|
+
cost: agent.stats.lifetimeUsage.cost,
|
|
362
|
+
durationMs: Date.now() - agent.lifecycle.startedAt,
|
|
363
|
+
}, theme, this.statsVisibility);
|
|
353
364
|
return parts.join("·");
|
|
354
365
|
}
|
|
355
366
|
|
|
@@ -390,13 +401,13 @@ export class AgentWidget {
|
|
|
390
401
|
const blocks: RenderBlock[] = [];
|
|
391
402
|
for (const a of running) {
|
|
392
403
|
const name = getDisplayName(a.display.type);
|
|
393
|
-
const bg = this.
|
|
394
|
-
const statsLine = this.buildStatsLine(a,
|
|
404
|
+
const bg = this.getLiveView(a.id);
|
|
405
|
+
const statsLine = this.buildStatsLine(a, theme);
|
|
395
406
|
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : THINKING_TEXT;
|
|
396
407
|
|
|
397
408
|
if (this.isCompact()) {
|
|
398
409
|
// Compact: single line with activity inline, truncated description
|
|
399
|
-
const desc = a.display.description
|
|
410
|
+
const desc = truncateDesc(a.display.description, this.descLengthCompact);
|
|
400
411
|
const headerLine = `${BRANCH} ${theme.fg("accent", frame)} ${theme.bold(name)} ${desc} ${statsLine} ${theme.fg("dim", activity)}`;
|
|
401
412
|
blocks.push({
|
|
402
413
|
header: truncate(headerLine),
|
|
@@ -404,7 +415,8 @@ export class AgentWidget {
|
|
|
404
415
|
});
|
|
405
416
|
} else {
|
|
406
417
|
// Full: header + continuation lines
|
|
407
|
-
const
|
|
418
|
+
const fullDesc = truncateDesc(a.display.description, this.descLengthFull);
|
|
419
|
+
const headerLine = `${BRANCH} ${theme.fg("accent", frame)} ${theme.bold(name)} ${fullDesc} ${statsLine}`;
|
|
408
420
|
const continuations: string[] = [];
|
|
409
421
|
if (a.display.outputFile || a.display.worktreeLabel) {
|
|
410
422
|
const parts: string[] = [];
|
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
* Pure functions — no module-level state, no side effects.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { getConfig } from "
|
|
12
|
-
import type { SubagentType
|
|
13
|
-
import {
|
|
11
|
+
import { getConfig } from "../agents/agent-types.js";
|
|
12
|
+
import type { SubagentType } from "../agents/types.js";
|
|
13
|
+
import type { Theme } from "./types.js";
|
|
14
|
+
import { formatTokens, formatTokensCompact, formatCost } from "../agents/usage.js";
|
|
15
|
+
|
|
16
|
+
/** Truncate a description string to `maxLen` characters, appending "..." if truncated. */
|
|
17
|
+
export function truncateDesc(text: string, maxLen: number): string {
|
|
18
|
+
return text.length > maxLen ? text.slice(0, maxLen - 3) + "..." : text;
|
|
19
|
+
}
|
|
14
20
|
|
|
15
21
|
/** Max length for a truncated command in tool arg summaries. */
|
|
16
22
|
const MAX_COMMAND_DISPLAY_LENGTH = 100;
|
|
@@ -25,18 +31,22 @@ const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
|
|
|
25
31
|
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
26
32
|
* Compaction count rendered as `↻ N` in dim.
|
|
27
33
|
*
|
|
28
|
-
* "
|
|
29
|
-
* "
|
|
30
|
-
* "
|
|
31
|
-
* "
|
|
34
|
+
* "↑12k↓8k" — no annotations
|
|
35
|
+
* "↑12k↓8k 45%" — percent only
|
|
36
|
+
* "↑12k↓8k ↻ 2" — compactions only (e.g. right after compact)
|
|
37
|
+
* "↑12k↓8k 45% ↻ 2" — both
|
|
32
38
|
*/
|
|
33
39
|
function formatSessionTokens(
|
|
34
|
-
|
|
40
|
+
inputTokens: number,
|
|
41
|
+
outputTokens: number,
|
|
35
42
|
percent: number | null,
|
|
36
43
|
theme: Theme,
|
|
37
44
|
compactions = 0,
|
|
38
45
|
): string {
|
|
39
|
-
const
|
|
46
|
+
const tokenParts: string[] = [];
|
|
47
|
+
if (inputTokens > 0) tokenParts.push(`↑${formatTokensCompact(inputTokens)}`);
|
|
48
|
+
if (outputTokens > 0) tokenParts.push(`↓${formatTokensCompact(outputTokens)}`);
|
|
49
|
+
const tokenStr = tokenParts.join("");
|
|
40
50
|
const annot: string[] = [];
|
|
41
51
|
if (percent !== null) {
|
|
42
52
|
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
@@ -46,16 +56,17 @@ function formatSessionTokens(
|
|
|
46
56
|
annot.push(theme.fg("dim", `↻ ${compactions}`));
|
|
47
57
|
}
|
|
48
58
|
if (annot.length === 0) return tokenStr;
|
|
49
|
-
|
|
50
|
-
// ANSI reset from leaving `)` in default color when wrapped in outer dim.
|
|
51
|
-
const lastIdx = annot.length - 1;
|
|
52
|
-
annot[lastIdx] += ")";
|
|
53
|
-
return `${tokenStr}(${annot.join("·")}`;
|
|
59
|
+
return `${tokenStr} ${annot.join(" ")}`;
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
/** Format turn count with optional max limit
|
|
57
|
-
function formatTurns(turnCount: number, maxTurns
|
|
58
|
-
|
|
62
|
+
/** Format turn count with optional max limit. Shows max when >= 80% of limit. */
|
|
63
|
+
function formatTurns(turnCount: number, maxTurns: number | null | undefined, theme: Theme): string {
|
|
64
|
+
if (maxTurns == null) return `${turnCount}⟳ `;
|
|
65
|
+
const ratio = turnCount / maxTurns;
|
|
66
|
+
const text = ratio >= 0.8 ? `${turnCount}≤${maxTurns}⟳ ` : `${turnCount}⟳ `;
|
|
67
|
+
if (ratio >= 1) return theme.fg("error", text);
|
|
68
|
+
if (ratio >= 0.8) return theme.fg("warning", text);
|
|
69
|
+
return text;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
// ---- Exported formatting functions ----
|
|
@@ -77,31 +88,58 @@ export function formatMs(ms: number): string {
|
|
|
77
88
|
return parts.join(" ");
|
|
78
89
|
}
|
|
79
90
|
|
|
91
|
+
/** Visibility flags for stats parts. All default to true. */
|
|
92
|
+
export interface StatsVisibility {
|
|
93
|
+
showTools?: boolean;
|
|
94
|
+
showTurns?: boolean;
|
|
95
|
+
showInput?: boolean;
|
|
96
|
+
showOutput?: boolean;
|
|
97
|
+
showContext?: boolean;
|
|
98
|
+
showCost?: boolean;
|
|
99
|
+
showTime?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
80
102
|
/**
|
|
81
|
-
* Build common stats parts: toolUses · turns ·
|
|
103
|
+
* Build common stats parts: toolUses · turns · input↓ output with context % · cost · time.
|
|
82
104
|
* Shared by AgentWidget and index.ts for consistent stats display.
|
|
105
|
+
*
|
|
106
|
+
* @param visible - Optional visibility flags. All default to true for backward compatibility.
|
|
107
|
+
* @param durationMs - Optional duration in ms. When provided and showTime is not false, appends formatted time.
|
|
83
108
|
*/
|
|
84
109
|
export function buildStatsParts(
|
|
85
110
|
args: {
|
|
86
111
|
toolUses: number;
|
|
87
112
|
turnCount?: number;
|
|
88
113
|
maxTurns?: number;
|
|
89
|
-
|
|
114
|
+
input: number;
|
|
115
|
+
output: number;
|
|
90
116
|
contextPercent: number | null;
|
|
91
117
|
compactions: number;
|
|
92
118
|
cost?: number;
|
|
119
|
+
durationMs?: number;
|
|
93
120
|
},
|
|
94
121
|
theme: Theme,
|
|
122
|
+
visible?: StatsVisibility,
|
|
95
123
|
): string[] {
|
|
96
124
|
const parts: string[] = [];
|
|
97
|
-
if (args.toolUses > 0) parts.push(`${args.toolUses}🛠 `);
|
|
98
|
-
if (args.turnCount != null) parts.push(formatTurns(args.turnCount, args.maxTurns));
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
125
|
+
if (visible?.showTools !== false && args.toolUses > 0) parts.push(`${args.toolUses}🛠 `);
|
|
126
|
+
if (visible?.showTurns !== false && args.turnCount != null) parts.push(formatTurns(args.turnCount, args.maxTurns, theme));
|
|
127
|
+
if (visible?.showInput !== false || visible?.showOutput !== false) {
|
|
128
|
+
const showIn = visible?.showInput !== false;
|
|
129
|
+
const showOut = visible?.showOutput !== false;
|
|
130
|
+
const inputTokens = showIn ? args.input : 0;
|
|
131
|
+
const outputTokens = showOut ? args.output : 0;
|
|
132
|
+
if (inputTokens > 0 || outputTokens > 0) {
|
|
133
|
+
parts.push(formatSessionTokens(
|
|
134
|
+
inputTokens, outputTokens,
|
|
135
|
+
visible?.showContext !== false ? args.contextPercent : null,
|
|
136
|
+
theme,
|
|
137
|
+
visible?.showContext !== false ? args.compactions : 0,
|
|
138
|
+
));
|
|
139
|
+
}
|
|
103
140
|
}
|
|
104
|
-
if (args.cost != null && args.cost > 0) parts.push(formatCost(args.cost));
|
|
141
|
+
if (visible?.showCost !== false && args.cost != null && args.cost > 0) parts.push(formatCost(args.cost));
|
|
142
|
+
if (visible?.showTime !== false && args.durationMs != null) parts.push(formatMs(args.durationMs));
|
|
105
143
|
return parts;
|
|
106
144
|
}
|
|
107
145
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* helpers.ts — Shared helpers for menu modules:
|
|
3
|
+
* theme builders for SettingsList/SelectList, numeric validation,
|
|
4
|
+
* model-option building, and a swappable delegating component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Component, SettingsListTheme } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { ModelOption } from "../../models/model-selector.js";
|
|
9
|
+
import { parseModelKey } from "../../utils.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build ModelOption[] from raw "provider/model-id" strings.
|
|
13
|
+
* Includes "(inherits parent)" as the first option.
|
|
14
|
+
*/
|
|
15
|
+
export function buildModelOptions(rawOptions: string[]): ModelOption[] {
|
|
16
|
+
const items: ModelOption[] = [
|
|
17
|
+
{ value: "(inherits parent)", label: "(inherits parent)", provider: "" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
for (const opt of rawOptions) {
|
|
21
|
+
const parsed = parseModelKey(opt);
|
|
22
|
+
if (!parsed) continue;
|
|
23
|
+
items.push({ value: opt, label: parsed.modelId, provider: parsed.provider });
|
|
24
|
+
}
|
|
25
|
+
return items;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build a SettingsListTheme from a pi-coding-agent Theme.
|
|
30
|
+
* Shared by widget settings and future SettingsList-based menus.
|
|
31
|
+
*/
|
|
32
|
+
export function buildSettingsListTheme(theme: { fg(color: string, text: string): string; bold(text: string): string }): SettingsListTheme {
|
|
33
|
+
return {
|
|
34
|
+
label: (text, selected) => selected ? theme.fg("accent", text) : text,
|
|
35
|
+
value: (text, selected) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
|
|
36
|
+
description: (text) => theme.fg("muted", text),
|
|
37
|
+
// Use "→ " (2 chars) to match non-selected prefix " " (2 spaces)
|
|
38
|
+
// This prevents menu items from shifting left/right when cursor moves
|
|
39
|
+
cursor: theme.fg("accent", "→ "),
|
|
40
|
+
hint: (text) => theme.fg("dim", text),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pure numeric validation. Returns parsed integer ≥ min, or undefined.
|
|
46
|
+
* Extracted from parseNumericInput for use in submenu Components.
|
|
47
|
+
*/
|
|
48
|
+
export function validateNumeric(value: string, min: number): number | undefined {
|
|
49
|
+
const parsed = parseInt(value.trim(), 10);
|
|
50
|
+
if (isNaN(parsed) || parsed < min) return undefined;
|
|
51
|
+
return parsed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a Component that delegates to a swappable inner component.
|
|
56
|
+
* Use in submenus that switch between SelectList → Input (or similar).
|
|
57
|
+
*/
|
|
58
|
+
export function createDelegatingComponent(initial: Component): Component & { setActive(c: Component): void; focused?: boolean; items?: any; onSelect?: any; onCancel?: any } {
|
|
59
|
+
let active = initial;
|
|
60
|
+
return {
|
|
61
|
+
invalidate() { active.invalidate?.(); },
|
|
62
|
+
render(width: number) { return active.render(width); },
|
|
63
|
+
handleInput(data: string) { active.handleInput?.(data); },
|
|
64
|
+
setActive(c: Component) { active = c; },
|
|
65
|
+
// Propagate focused to the active child so isFocusable() returns true,
|
|
66
|
+
// which tells SettingsListWrapper to passthrough keys instead of converting them.
|
|
67
|
+
get focused() { return (active as any)?.focused ?? false; },
|
|
68
|
+
set focused(value: boolean) { if ((active as any)?.focused != null) (active as any).focused = value; },
|
|
69
|
+
// Proxy SelectList properties so SettingsListWrapper can add "Back" button.
|
|
70
|
+
get items() { return (active as any)?.items; },
|
|
71
|
+
set items(v: any) { (active as any).items = v; },
|
|
72
|
+
get onSelect() { return (active as any)?.onSelect; },
|
|
73
|
+
set onSelect(v: any) { (active as any).onSelect = v; },
|
|
74
|
+
get onCancel() { return (active as any)?.onCancel; },
|
|
75
|
+
set onCancel(v: any) { (active as any).onCancel = v; },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build a SelectListTheme from a pi-coding-agent Theme.
|
|
81
|
+
* Produces identical visual style to buildSettingsListTheme:
|
|
82
|
+
* → cursor, accent colors, muted descriptions.
|
|
83
|
+
*/
|
|
84
|
+
export function buildSelectListTheme(theme: { fg(color: string, text: string): string; bold(text: string): string }): import("@earendil-works/pi-tui").SelectListTheme {
|
|
85
|
+
return {
|
|
86
|
+
selectedPrefix: () => theme.fg("accent", "→ "),
|
|
87
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
88
|
+
description: (text) => theme.fg("muted", text),
|
|
89
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
90
|
+
noMatch: (text) => theme.fg("dim", text),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|