pi-subagents-lite 1.4.1 → 1.4.3
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 +6 -5
- package/package.json +5 -4
- package/src/agents/agent-discovery.ts +1 -2
- package/src/agents/agent-manager.ts +2 -1
- package/src/agents/agent-runner.ts +2 -9
- package/src/agents/agent-status.ts +2 -2
- package/src/agents/agent-types.ts +22 -20
- package/src/agents/output-file.ts +92 -12
- package/src/agents/tool-execution.ts +11 -13
- package/src/agents/usage.ts +3 -10
- package/src/config/config-io.ts +40 -29
- package/src/config/config-store.ts +17 -19
- package/src/config/types.ts +1 -0
- package/src/events.ts +3 -1
- package/src/models/model-precedence.ts +2 -0
- package/src/spawn/spawn-coordinator.ts +41 -23
- package/src/spawn/worktree-validator.ts +10 -7
- package/src/ui/agent-widget.ts +21 -19
- package/src/ui/format.ts +3 -3
- package/src/ui/menu/helpers.ts +38 -8
- package/src/ui/menu/menu-concurrency.ts +31 -18
- package/src/ui/menu/menu-model-settings.ts +28 -23
- package/src/ui/menu/menu-running-agents.ts +2 -1
- package/src/ui/menu/menu-spawn-options.ts +5 -0
- package/src/ui/menu/menu-spawn-wizard.ts +34 -19
- package/src/ui/menu/menu-system-prompt.ts +6 -2
- package/src/ui/menu/menu-widget-settings.ts +28 -0
- package/src/ui/menu/submenus/confirm.ts +2 -1
- package/src/ui/menu/submenus/model-select.ts +5 -4
- package/src/ui/menu/wrappers/settings-list.ts +73 -105
- package/src/ui/result-viewer.ts +43 -11
- package/src/{models/model-selector.ts → ui/searchable-select.ts} +29 -29
- package/src/utils.ts +2 -0
|
@@ -20,10 +20,8 @@ import type { AgentManager } from "../agents/agent-manager.js";
|
|
|
20
20
|
import { CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
|
|
21
21
|
import type { SystemPromptMode } from "../agents/types.js";
|
|
22
22
|
import type { ThinkingLevel } from "../types.js";
|
|
23
|
-
import {
|
|
23
|
+
import { VALID_SYSTEM_PROMPT_MODES, DEFAULT_CONCURRENCY, loadConfig, saveConfigAtomic } from "./config-io.js";
|
|
24
24
|
|
|
25
|
-
/** Valid values for systemPromptMode — checked once at module load. */
|
|
26
|
-
const VALID_SYSTEM_PROMPT_MODES = new Set<string>(["replace", "inherit", "custom"]);
|
|
27
25
|
|
|
28
26
|
/** Injected persistence adapter. Swap for an in-memory adapter in tests. */
|
|
29
27
|
export interface ConfigIO {
|
|
@@ -76,6 +74,8 @@ export interface ResolvedAgentSettings {
|
|
|
76
74
|
readonly showContext: boolean;
|
|
77
75
|
/** Whether to show elapsed time in widget stats line. */
|
|
78
76
|
readonly showTime: boolean;
|
|
77
|
+
/** Buffer size for streaming thinking blocks to output file. 0 = disabled. */
|
|
78
|
+
readonly outputThinkingBufferSize: number;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/** Side-effect targets, injected after construction. */
|
|
@@ -106,29 +106,22 @@ export class ConfigStore {
|
|
|
106
106
|
|
|
107
107
|
get agent(): ResolvedAgentSettings {
|
|
108
108
|
const a = this.config.agent;
|
|
109
|
-
const widgetMaxLines = a.widgetMaxLines
|
|
109
|
+
const widgetMaxLines = a.widgetMaxLines!; // guaranteed by loadConfig default merge
|
|
110
110
|
const widgetMaxLinesCompact = a.widgetMaxLinesCompact ?? Math.floor(widgetMaxLines / 2);
|
|
111
|
-
const widgetCompact = a.widgetCompact === true;
|
|
112
|
-
const widgetShortcut = a.widgetShortcut === true;
|
|
113
|
-
const widgetDescLengthFull = a.widgetDescLengthFull ?? DEFAULT_CONFIG.agent.widgetDescLengthFull ?? 50;
|
|
114
|
-
const widgetDescLengthCompact = a.widgetDescLengthCompact ?? DEFAULT_CONFIG.agent.widgetDescLengthCompact ?? 30;
|
|
115
|
-
const rawMode = a.systemPromptMode;
|
|
116
|
-
const systemPromptMode = VALID_SYSTEM_PROMPT_MODES.has(rawMode as string) ? rawMode as SystemPromptMode : "replace";
|
|
117
|
-
const includeContextFiles = a.includeContextFiles ?? DEFAULT_CONFIG.agent.includeContextFiles ?? true;
|
|
118
111
|
|
|
119
112
|
return {
|
|
120
113
|
defaultModel: a.default ?? null,
|
|
121
114
|
forceBackground: a.forceBackground === true,
|
|
122
115
|
showCost: this.sessionShowCost ?? (a.showCost === true),
|
|
123
|
-
graceTurns: a.graceTurns ??
|
|
116
|
+
graceTurns: a.graceTurns ?? 6,
|
|
124
117
|
widgetMaxLines,
|
|
125
118
|
widgetMaxLinesCompact,
|
|
126
|
-
widgetCompact,
|
|
127
|
-
widgetShortcut,
|
|
128
|
-
widgetDescLengthFull,
|
|
129
|
-
widgetDescLengthCompact,
|
|
130
|
-
systemPromptMode,
|
|
131
|
-
includeContextFiles,
|
|
119
|
+
widgetCompact: a.widgetCompact === true,
|
|
120
|
+
widgetShortcut: a.widgetShortcut === true,
|
|
121
|
+
widgetDescLengthFull: a.widgetDescLengthFull ?? 50,
|
|
122
|
+
widgetDescLengthCompact: a.widgetDescLengthCompact ?? 30,
|
|
123
|
+
systemPromptMode: VALID_SYSTEM_PROMPT_MODES.has(a.systemPromptMode as string) ? (a.systemPromptMode as SystemPromptMode) : "replace",
|
|
124
|
+
includeContextFiles: a.includeContextFiles ?? true,
|
|
132
125
|
defaultThinking: a.defaultThinking as ThinkingLevel | undefined,
|
|
133
126
|
defaultMaxTurns: a.defaultMaxTurns,
|
|
134
127
|
loadSkillsImplicitly: a.loadSkillsImplicitly !== false,
|
|
@@ -140,6 +133,7 @@ export class ConfigStore {
|
|
|
140
133
|
showOutput: a.showOutput !== false,
|
|
141
134
|
showContext: a.showContext !== false,
|
|
142
135
|
showTime: a.showTime !== false,
|
|
136
|
+
outputThinkingBufferSize: a.outputThinkingBufferSize ?? 0,
|
|
143
137
|
};
|
|
144
138
|
}
|
|
145
139
|
|
|
@@ -271,6 +265,10 @@ export class ConfigStore {
|
|
|
271
265
|
setShowOutput: (enabled: boolean) => this.setAgentVisibility("showOutput", enabled),
|
|
272
266
|
setShowContext: (enabled: boolean) => this.setAgentVisibility("showContext", enabled),
|
|
273
267
|
setShowTime: (enabled: boolean) => this.setAgentVisibility("showTime", enabled),
|
|
268
|
+
setOutputThinkingBufferSize: (size: number): void => {
|
|
269
|
+
this.config.agent.outputThinkingBufferSize = size;
|
|
270
|
+
this.persist();
|
|
271
|
+
},
|
|
274
272
|
},
|
|
275
273
|
widget: {
|
|
276
274
|
setCompact: (enabled: boolean): void => {
|
|
@@ -337,7 +335,7 @@ export class ConfigStore {
|
|
|
337
335
|
this.applyConcurrency();
|
|
338
336
|
},
|
|
339
337
|
reset: (): void => {
|
|
340
|
-
this.config.concurrency = { ...
|
|
338
|
+
this.config.concurrency = { ...DEFAULT_CONCURRENCY };
|
|
341
339
|
this.persist();
|
|
342
340
|
this.applyConcurrency();
|
|
343
341
|
},
|
package/src/config/types.ts
CHANGED
package/src/events.ts
CHANGED
|
@@ -39,13 +39,15 @@ export function ensureManagerAndWidget(): void {
|
|
|
39
39
|
const newManager = new AgentManager(
|
|
40
40
|
undefined, // onComplete wired below
|
|
41
41
|
getStore().concurrency as unknown as ConstructorParameters<typeof AgentManager>[1],
|
|
42
|
+
undefined,
|
|
43
|
+
getStore().agent.outputThinkingBufferSize,
|
|
42
44
|
);
|
|
43
45
|
setManager(newManager);
|
|
44
46
|
// Sync the manager as a config side-effect target (concurrency setters call setConcurrency).
|
|
45
47
|
getStore().setDeps({ manager: newManager });
|
|
46
48
|
|
|
47
49
|
// Now create coordinator with the real manager
|
|
48
|
-
const coordinator = new SpawnCoordinator(newManager
|
|
50
|
+
const coordinator = new SpawnCoordinator(newManager);
|
|
49
51
|
setCoordinator(coordinator);
|
|
50
52
|
|
|
51
53
|
// Wire the manager's onComplete to the coordinator
|
|
@@ -56,6 +56,8 @@ export interface SubagentsConfig {
|
|
|
56
56
|
widgetDescLengthFull?: number;
|
|
57
57
|
/** Max description length in widget compact mode. Default: 30. */
|
|
58
58
|
widgetDescLengthCompact?: number;
|
|
59
|
+
/** When > 0, thinking deltas stream to output file during message_update events. Default: 0 (disabled). */
|
|
60
|
+
outputThinkingBufferSize?: number;
|
|
59
61
|
[agentType: string]: string | null | undefined | boolean | number;
|
|
60
62
|
};
|
|
61
63
|
concurrency: {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getStatusNote } from "../status-note.js";
|
|
2
|
+
import { getPiInstance, getSessionCtx, getWidget } from "../shell.js";
|
|
2
3
|
/**
|
|
3
4
|
* spawn-coordinator.ts — Spawn-and-track coordination for subagents.
|
|
4
5
|
*
|
|
@@ -14,7 +15,6 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
14
15
|
import type { AgentRecord, SpawnConfig, ToolActivity } from "../types.js";
|
|
15
16
|
import type { AgentManager, SpawnOptions } from "../agents/agent-manager.js";
|
|
16
17
|
import { buildAgentDetails } from "../agents/tool-execution.js";
|
|
17
|
-
import { getWidget } from "../shell.js";
|
|
18
18
|
|
|
19
19
|
// ============================================================================
|
|
20
20
|
// Types
|
|
@@ -64,12 +64,10 @@ export class SpawnCoordinator {
|
|
|
64
64
|
/** Active nudge timer. */
|
|
65
65
|
private nudgeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
private
|
|
67
|
+
/** Set during dispose to prevent nudge emission after session replacement. */
|
|
68
|
+
private disposed = false;
|
|
69
69
|
|
|
70
|
-
constructor(private manager: AgentManager
|
|
71
|
-
if (pi) this.pi = pi;
|
|
72
|
-
}
|
|
70
|
+
constructor(private manager: AgentManager) {}
|
|
73
71
|
|
|
74
72
|
/**
|
|
75
73
|
* Spawn + wire tracking + (foreground) await.
|
|
@@ -80,9 +78,6 @@ export class SpawnCoordinator {
|
|
|
80
78
|
ctx: ExtensionContext,
|
|
81
79
|
intent: SpawnIntent,
|
|
82
80
|
): Promise<SpawnResult> {
|
|
83
|
-
// Store pi for nudge emission
|
|
84
|
-
this.pi = pi;
|
|
85
|
-
|
|
86
81
|
// Create live view BEFORE spawn so callbacks can close over it
|
|
87
82
|
const liveView: LiveView = {
|
|
88
83
|
activeTools: new Map(),
|
|
@@ -116,6 +111,9 @@ export class SpawnCoordinator {
|
|
|
116
111
|
this.backgroundAgentIds.add(agentId);
|
|
117
112
|
}
|
|
118
113
|
|
|
114
|
+
// pi is read from shell at nudge time, not stored here
|
|
115
|
+
// (avoids stale ctx after session replacement or reload)
|
|
116
|
+
|
|
119
117
|
const record = this.manager.getRecord(agentId)!;
|
|
120
118
|
|
|
121
119
|
if (!intent.runInBackground) {
|
|
@@ -183,7 +181,7 @@ export class SpawnCoordinator {
|
|
|
183
181
|
this.pendingNudges.clear();
|
|
184
182
|
this.liveViews.clear();
|
|
185
183
|
this.backgroundAgentIds.clear();
|
|
186
|
-
this.
|
|
184
|
+
this.disposed = true;
|
|
187
185
|
}
|
|
188
186
|
|
|
189
187
|
// ── Private ──
|
|
@@ -208,25 +206,45 @@ export class SpawnCoordinator {
|
|
|
208
206
|
|
|
209
207
|
/** Emit an individual nudge for a completed background agent. */
|
|
210
208
|
private emitIndividualNudge(agentId: string): void {
|
|
209
|
+
// Skip if disposed — prevents stale pi usage after session replacement
|
|
210
|
+
if (this.disposed) return;
|
|
211
|
+
|
|
212
|
+
// Read pi from shell at call time so we get a fresh reference after reload.
|
|
213
|
+
const pi = getPiInstance();
|
|
214
|
+
if (!pi) return;
|
|
215
|
+
|
|
211
216
|
const record = this.manager.getRecord(agentId);
|
|
212
|
-
if (!record
|
|
217
|
+
if (!record) return;
|
|
213
218
|
|
|
214
219
|
const details = buildAgentDetails(record, {
|
|
215
220
|
includeStats: true,
|
|
216
221
|
includeStatus: true,
|
|
217
222
|
});
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
try {
|
|
225
|
+
// Pick delivery mode based on parent session state:
|
|
226
|
+
// - steer: queues while running, delivers before next LLM call
|
|
227
|
+
// - followUp: waits for agent to finish, then delivers
|
|
228
|
+
const ctx = getSessionCtx();
|
|
229
|
+
const parentIdle = ctx?.isIdle?.() ?? true;
|
|
230
|
+
const deliverAs = parentIdle ? "followUp" : "steer";
|
|
231
|
+
|
|
232
|
+
pi.sendMessage(
|
|
233
|
+
{
|
|
234
|
+
customType: "subagent-result",
|
|
235
|
+
content: `[Subagent "${record.display.type}" ${record.lifecycle.status}]\n\n${record.result ?? ""} ${getStatusNote(record.lifecycle.status)}`,
|
|
236
|
+
details,
|
|
237
|
+
display: true,
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
deliverAs,
|
|
241
|
+
triggerTurn: true,
|
|
242
|
+
},
|
|
243
|
+
);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
// pi may be stale after session replacement (newSession/fork/switchSession).
|
|
246
|
+
// PI invalidates the extension runner on session.dispose() and never
|
|
247
|
+
// clears the stale flag for non-reload replacements. Nothing we can do.
|
|
248
|
+
}
|
|
231
249
|
}
|
|
232
250
|
}
|
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import * as path from "node:path";
|
|
12
12
|
import { existsSync, statSync, realpathSync } from "node:fs";
|
|
13
|
-
|
|
14
|
-
/** Timeout for git commands (ms). */
|
|
15
|
-
const GIT_EXEC_TIMEOUT_MS = 5000;
|
|
16
|
-
|
|
13
|
+
import { GIT_EXEC_TIMEOUT_MS } from "../utils.js";
|
|
17
14
|
/** Specific error messages returned to the LLM for self-correction. */
|
|
18
15
|
export const WORKTREE_VALIDATION_ERRORS = {
|
|
19
16
|
PATH_DOES_NOT_EXIST: "worktree_path does not exist: the specified path was not found on disk",
|
|
@@ -60,6 +57,7 @@ async function getGitCommonDir(
|
|
|
60
57
|
pi: PiExec,
|
|
61
58
|
cwd: string,
|
|
62
59
|
notInRepoError: string,
|
|
60
|
+
onWarning?: (msg: string) => void,
|
|
63
61
|
): Promise<{ ok: true; commonDir: string } | { ok: false; error: string }> {
|
|
64
62
|
try {
|
|
65
63
|
const result = await pi.exec("git", ["rev-parse", "--git-common-dir"], { cwd, timeout: GIT_EXEC_TIMEOUT_MS });
|
|
@@ -72,7 +70,11 @@ async function getGitCommonDir(
|
|
|
72
70
|
if (msg.includes("ENOENT") || msg.includes("not found")) {
|
|
73
71
|
return { ok: false, error: WORKTREE_VALIDATION_ERRORS.GIT_NOT_FOUND };
|
|
74
72
|
}
|
|
75
|
-
|
|
73
|
+
if (msg.includes("timed out") || msg.includes("timeout")) {
|
|
74
|
+
return { ok: false, error: WORKTREE_VALIDATION_ERRORS.GIT_TIMEOUT };
|
|
75
|
+
}
|
|
76
|
+
onWarning?.(`git rev-parse --git-common-dir failed in ${cwd}: ${msg}`);
|
|
77
|
+
return { ok: false, error: `worktree_path validation failed: git rev-parse failed: ${msg}` };
|
|
76
78
|
}
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -97,6 +99,7 @@ export async function validateWorktreePath(
|
|
|
97
99
|
pi: PiExec,
|
|
98
100
|
worktreePath: string,
|
|
99
101
|
parentCwd: string,
|
|
102
|
+
onWarning?: (msg: string) => void,
|
|
100
103
|
): Promise<WorktreeValidationResult> {
|
|
101
104
|
// Step 1: Empty / whitespace → treat as omitted
|
|
102
105
|
if (!worktreePath || worktreePath.trim() === "") {
|
|
@@ -128,10 +131,10 @@ export async function validateWorktreePath(
|
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
// Step 5: Get and compare git-common-dir for parent and target
|
|
131
|
-
const parentResult = await getGitCommonDir(pi, parentCwd, WORKTREE_VALIDATION_ERRORS.PARENT_NOT_IN_GIT_REPO);
|
|
134
|
+
const parentResult = await getGitCommonDir(pi, parentCwd, WORKTREE_VALIDATION_ERRORS.PARENT_NOT_IN_GIT_REPO, onWarning);
|
|
132
135
|
if (!parentResult.ok) return parentResult;
|
|
133
136
|
|
|
134
|
-
const targetResult = await getGitCommonDir(pi, realPath, WORKTREE_VALIDATION_ERRORS.NOT_IN_GIT_REPO);
|
|
137
|
+
const targetResult = await getGitCommonDir(pi, realPath, WORKTREE_VALIDATION_ERRORS.NOT_IN_GIT_REPO, onWarning);
|
|
135
138
|
if (!targetResult.ok) return targetResult;
|
|
136
139
|
|
|
137
140
|
// Compare common dirs — must share the same repo
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
import { formatMs, buildStatsParts, getDisplayName, truncateDesc, type StatsVisibility } from "./format.js";
|
|
15
15
|
import type { LiveView } from "../spawn/spawn-coordinator.js";
|
|
16
16
|
|
|
17
|
-
// Re-export Theme so existing consumers (
|
|
17
|
+
// Re-export Theme so existing consumers (searchable-select, result-viewer) don't break
|
|
18
18
|
export type { Theme } from "./types.js";
|
|
19
19
|
|
|
20
20
|
// ---- Constants ----
|
|
@@ -56,7 +56,6 @@ const TOOL_DISPLAY: Record<string, string> = {
|
|
|
56
56
|
write: "writing",
|
|
57
57
|
grep: "searching",
|
|
58
58
|
find: "finding files",
|
|
59
|
-
ls: "listing",
|
|
60
59
|
};
|
|
61
60
|
|
|
62
61
|
// ---- Types ----
|
|
@@ -136,6 +135,14 @@ function describeActivity(activeTools: Map<string, string>, responseText?: strin
|
|
|
136
135
|
return THINKING_TEXT;
|
|
137
136
|
}
|
|
138
137
|
|
|
138
|
+
/** Build the worktree/output continuation line parts for an agent record. */
|
|
139
|
+
function buildWorktreeOutputParts(a: AgentRecord): string[] {
|
|
140
|
+
const parts: string[] = [];
|
|
141
|
+
if (a.display.worktreeLabel) parts.push(`@${a.display.worktreeLabel}`);
|
|
142
|
+
if (a.display.outputFile) parts.push(`tail -f ${a.display.outputFile}`);
|
|
143
|
+
return parts;
|
|
144
|
+
}
|
|
145
|
+
|
|
139
146
|
// ---- Widget manager ----
|
|
140
147
|
|
|
141
148
|
export class AgentWidget {
|
|
@@ -375,10 +382,8 @@ export class AgentWidget {
|
|
|
375
382
|
for (const a of finished) {
|
|
376
383
|
const continuations: string[] = [];
|
|
377
384
|
if (!this.isCompact()) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (a.display.worktreeLabel) parts.push(`@${a.display.worktreeLabel}`);
|
|
381
|
-
if (a.display.outputFile) parts.push(`tail -f ${a.display.outputFile}`);
|
|
385
|
+
const parts = buildWorktreeOutputParts(a);
|
|
386
|
+
if (parts.length > 0) {
|
|
382
387
|
continuations.push(truncate(theme.fg("dim", `${VLINE} ${parts.join(" ")}`)));
|
|
383
388
|
}
|
|
384
389
|
}
|
|
@@ -418,10 +423,8 @@ export class AgentWidget {
|
|
|
418
423
|
const fullDesc = truncateDesc(a.display.description, this.descLengthFull);
|
|
419
424
|
const headerLine = `${BRANCH} ${theme.fg("accent", frame)} ${theme.bold(name)} ${fullDesc} ${statsLine}`;
|
|
420
425
|
const continuations: string[] = [];
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (a.display.worktreeLabel) parts.push(`@${a.display.worktreeLabel}`);
|
|
424
|
-
if (a.display.outputFile) parts.push(`tail -f ${a.display.outputFile}`);
|
|
426
|
+
const parts = buildWorktreeOutputParts(a);
|
|
427
|
+
if (parts.length > 0) {
|
|
425
428
|
continuations.push(truncate(`${VLINE} ` + theme.fg("dim", `${VLINE} ${parts.join(" ")}`)));
|
|
426
429
|
}
|
|
427
430
|
continuations.push(truncate(`${VLINE} ` + theme.fg("dim", `└ ${activity}`)));
|
|
@@ -571,15 +574,14 @@ export class AgentWidget {
|
|
|
571
574
|
}
|
|
572
575
|
|
|
573
576
|
// Overflow summary line
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
: undefined;
|
|
577
|
+
let overflowLine: string | undefined;
|
|
578
|
+
if (hiddenRunning + hiddenFinished > 0) {
|
|
579
|
+
const parts: string[] = [];
|
|
580
|
+
if (hiddenRunning > 0) parts.push(`${hiddenRunning} running`);
|
|
581
|
+
if (hiddenFinished > 0) parts.push(`${hiddenFinished} finished`);
|
|
582
|
+
const summary = `+${hiddenRunning + hiddenFinished} more (${parts.join(", ")})`;
|
|
583
|
+
overflowLine = `${theme.fg("dim", CORNER)} ${theme.fg("dim", summary)}`;
|
|
584
|
+
}
|
|
583
585
|
|
|
584
586
|
return { visible, overflowLine };
|
|
585
587
|
}
|
package/src/ui/format.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { getConfig } from "../agents/agent-types.js";
|
|
12
12
|
import type { SubagentType } from "../agents/types.js";
|
|
13
13
|
import type { Theme } from "./types.js";
|
|
14
|
-
import { formatTokens,
|
|
14
|
+
import { formatTokens, formatCost } from "../agents/usage.js";
|
|
15
15
|
|
|
16
16
|
/** Truncate a description string to `maxLen` characters, appending "..." if truncated. */
|
|
17
17
|
export function truncateDesc(text: string, maxLen: number): string {
|
|
@@ -44,8 +44,8 @@ function formatSessionTokens(
|
|
|
44
44
|
compactions = 0,
|
|
45
45
|
): string {
|
|
46
46
|
const tokenParts: string[] = [];
|
|
47
|
-
if (inputTokens > 0) tokenParts.push(`↑${
|
|
48
|
-
if (outputTokens > 0) tokenParts.push(`↓${
|
|
47
|
+
if (inputTokens > 0) tokenParts.push(`↑${formatTokens(inputTokens, true)}`);
|
|
48
|
+
if (outputTokens > 0) tokenParts.push(`↓${formatTokens(outputTokens, true)}`);
|
|
49
49
|
const tokenStr = tokenParts.join("");
|
|
50
50
|
const annot: string[] = [];
|
|
51
51
|
if (percent !== null) {
|
package/src/ui/menu/helpers.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* helpers.ts — Shared helpers for menu modules:
|
|
3
3
|
* theme builders for SettingsList/SelectList, numeric validation,
|
|
4
|
-
* model-option building,
|
|
4
|
+
* model-option building, a swappable delegating component, and a
|
|
5
|
+
* searchable pick-list submenu factory.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
7
|
import type { Component, SettingsListTheme } from "@earendil-works/pi-tui";
|
|
8
|
-
import type {
|
|
8
|
+
import type { Theme } from "../types.js";
|
|
9
|
+
import { SearchableSelectDialog, type SelectOption } from "../searchable-select.js";
|
|
9
10
|
import { parseModelKey } from "../../utils.js";
|
|
10
|
-
|
|
11
11
|
/**
|
|
12
|
-
* Build
|
|
12
|
+
* Build SelectOption[] from raw "provider/model-id" strings.
|
|
13
13
|
* Includes "(inherits parent)" as the first option.
|
|
14
14
|
*/
|
|
15
|
-
export function buildModelOptions(rawOptions: string[]):
|
|
16
|
-
const items:
|
|
15
|
+
export function buildModelOptions(rawOptions: string[]): SelectOption[] {
|
|
16
|
+
const items: SelectOption[] = [
|
|
17
17
|
{ value: "(inherits parent)", label: "(inherits parent)", provider: "" },
|
|
18
18
|
];
|
|
19
19
|
|
|
@@ -33,7 +33,7 @@ export function buildSettingsListTheme(theme: { fg(color: string, text: string):
|
|
|
33
33
|
return {
|
|
34
34
|
label: (text, selected) => selected ? theme.fg("accent", text) : text,
|
|
35
35
|
value: (text, selected) => selected ? theme.fg("accent", text) : theme.fg("muted", text),
|
|
36
|
-
description: (text) => theme.fg("
|
|
36
|
+
description: (text) => theme.fg("dim", text),
|
|
37
37
|
// Use "→ " (2 chars) to match non-selected prefix " " (2 spaces)
|
|
38
38
|
// This prevents menu items from shifting left/right when cursor moves
|
|
39
39
|
cursor: theme.fg("accent", "→ "),
|
|
@@ -91,3 +91,33 @@ export function buildSelectListTheme(theme: { fg(color: string, text: string): s
|
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Build a searchable pick-list submenu backed by SearchableSelectDialog.
|
|
96
|
+
*
|
|
97
|
+
* Hides the delegator-forward-declaration dance shared by every menu that
|
|
98
|
+
* needs "type to filter, Enter to pick" over a flat option list
|
|
99
|
+
* (provider/model/type/worktree selection). onSelect may return a Component
|
|
100
|
+
* to chain into next (e.g. a numeric-input submenu); returning void leaves
|
|
101
|
+
* the submenu as-is so the caller can close it via done().
|
|
102
|
+
*/
|
|
103
|
+
export function createSearchableSelect(
|
|
104
|
+
items: SelectOption[],
|
|
105
|
+
callbacks: { onSelect: (value: string) => Component | void; onCancel: () => void },
|
|
106
|
+
theme: Theme,
|
|
107
|
+
): Component {
|
|
108
|
+
let delegator: ReturnType<typeof createDelegatingComponent>;
|
|
109
|
+
const selector = new SearchableSelectDialog(
|
|
110
|
+
items,
|
|
111
|
+
null,
|
|
112
|
+
{
|
|
113
|
+
onSelect: (value) => {
|
|
114
|
+
const next = callbacks.onSelect(value);
|
|
115
|
+
if (next) delegator.setActive(next);
|
|
116
|
+
},
|
|
117
|
+
onCancel: callbacks.onCancel,
|
|
118
|
+
},
|
|
119
|
+
theme,
|
|
120
|
+
);
|
|
121
|
+
delegator = createDelegatingComponent(selector);
|
|
122
|
+
return delegator;
|
|
123
|
+
}
|
|
@@ -11,18 +11,26 @@
|
|
|
11
11
|
|
|
12
12
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { SettingsList, SelectList, type SettingItem } from "@earendil-works/pi-tui";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildSettingsListTheme,
|
|
16
|
+
buildSelectListTheme,
|
|
17
|
+
buildModelOptions,
|
|
18
|
+
createDelegatingComponent,
|
|
19
|
+
createSearchableSelect,
|
|
20
|
+
} from "./helpers.js";
|
|
15
21
|
import { createNumericSubmenu } from "./submenus/numeric-input.js";
|
|
16
22
|
import { createConfirmSubmenu } from "./submenus/confirm.js";
|
|
17
23
|
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
18
24
|
import { getStore } from "../../shell.js";
|
|
25
|
+
import type { SelectOption } from "../searchable-select.js";
|
|
26
|
+
import type { Theme } from "../types.js";
|
|
19
27
|
|
|
20
28
|
export async function showConcurrencySettingsMenu(
|
|
21
29
|
ctx: ExtensionCommandContext,
|
|
22
30
|
modelOptions: string[],
|
|
23
31
|
): Promise<void> {
|
|
24
32
|
// Build menu items from current store state.
|
|
25
|
-
const buildItems = (store: ReturnType<typeof getStore>, theme:
|
|
33
|
+
const buildItems = (store: ReturnType<typeof getStore>, theme: Theme, modelOptions: string[], onRebuild?: () => void): SettingItem[] => {
|
|
26
34
|
const providers = [...new Set(modelOptions.map((m) => m.split("/")[0]))].sort();
|
|
27
35
|
const items: SettingItem[] = [];
|
|
28
36
|
|
|
@@ -52,28 +60,28 @@ export async function showConcurrencySettingsMenu(
|
|
|
52
60
|
return delegator;
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
// Submenu factory: pick
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
// Submenu factory: searchable-pick an option, then enter a numeric value.
|
|
64
|
+
// Used for both per-provider and per-model limits; items differ by caller.
|
|
65
|
+
const addPickThenValueSubmenu = (
|
|
66
|
+
items: SelectOption[],
|
|
58
67
|
onPick: (key: string, parsed: number) => void,
|
|
59
|
-
): SettingItem["submenu"] => (_currentValue, subDone) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
): SettingItem["submenu"] => (_currentValue, subDone) =>
|
|
69
|
+
createSearchableSelect(
|
|
70
|
+
items,
|
|
71
|
+
{
|
|
72
|
+
onSelect: (key) =>
|
|
73
|
+
createNumericSubmenu(ctx, { min: 1 }, (parsed) => onPick(key, parsed))("1", subDone),
|
|
74
|
+
onCancel: () => subDone(),
|
|
75
|
+
},
|
|
76
|
+
theme,
|
|
63
77
|
);
|
|
64
|
-
const delegator = createDelegatingComponent(list);
|
|
65
|
-
list.onSelect = (item) => {
|
|
66
|
-
delegator.setActive(createNumericSubmenu(ctx, { min: 1 }, (parsed) => onPick(item.value, parsed))("1", subDone));
|
|
67
|
-
};
|
|
68
|
-
list.onCancel = () => subDone();
|
|
69
|
-
return delegator;
|
|
70
|
-
};
|
|
71
78
|
|
|
72
79
|
// Global default
|
|
73
80
|
items.push({
|
|
74
81
|
id: "defaultConcurrency",
|
|
75
82
|
label: "Default concurrency limit",
|
|
76
83
|
currentValue: String(store.concurrency.default),
|
|
84
|
+
description: "Concurrent agent slots when no per-provider or per-model limit applies.",
|
|
77
85
|
submenu: createNumericSubmenu(ctx, (parsed) => {
|
|
78
86
|
store.mutate.concurrency.setDefault(parsed);
|
|
79
87
|
ctx.ui.notify(`Default concurrency set to ${parsed}`, "info");
|
|
@@ -90,6 +98,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
90
98
|
id: `provider:${provider}`,
|
|
91
99
|
label: provider,
|
|
92
100
|
currentValue: `${limit} slots`,
|
|
101
|
+
description: `Concurrent slots reserved for agents using the ${provider} provider.`,
|
|
93
102
|
submenu: editOrRemoveSubmenu(
|
|
94
103
|
limit,
|
|
95
104
|
(parsed) => {
|
|
@@ -111,7 +120,8 @@ export async function showConcurrencySettingsMenu(
|
|
|
111
120
|
id: "addProviderLimit",
|
|
112
121
|
label: "Add per-provider limit...",
|
|
113
122
|
currentValue: "",
|
|
114
|
-
|
|
123
|
+
description: "Cap how many agents run at once for a single provider.",
|
|
124
|
+
submenu: addPickThenValueSubmenu(providers.map((o) => ({ value: o, label: o })), (provider, parsed) => {
|
|
115
125
|
store.mutate.concurrency.setProvider(provider, parsed);
|
|
116
126
|
ctx.ui.notify(`${provider} concurrency set to ${parsed}`, "info");
|
|
117
127
|
}),
|
|
@@ -128,6 +138,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
128
138
|
id: `model:${modelKey}`,
|
|
129
139
|
label: modelKey,
|
|
130
140
|
currentValue: `${limit} slots`,
|
|
141
|
+
description: `Concurrent slots reserved for agents using the ${modelKey} model.`,
|
|
131
142
|
submenu: editOrRemoveSubmenu(
|
|
132
143
|
limit,
|
|
133
144
|
(parsed) => {
|
|
@@ -149,7 +160,8 @@ export async function showConcurrencySettingsMenu(
|
|
|
149
160
|
id: "addModelLimit",
|
|
150
161
|
label: "Add per-model limit...",
|
|
151
162
|
currentValue: "",
|
|
152
|
-
|
|
163
|
+
description: "Cap how many agents run at once for a single model.",
|
|
164
|
+
submenu: addPickThenValueSubmenu(buildModelOptions(modelOptions), (modelKey, parsed) => {
|
|
153
165
|
store.mutate.concurrency.setModel(modelKey, parsed);
|
|
154
166
|
ctx.ui.notify(`${modelKey} concurrency set to ${parsed}`, "info");
|
|
155
167
|
}),
|
|
@@ -162,6 +174,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
162
174
|
id: "resetAll",
|
|
163
175
|
label: "Reset all to defaults",
|
|
164
176
|
currentValue: "",
|
|
177
|
+
description: "Restore the default limit and remove all per-provider and per-model limits.",
|
|
165
178
|
submenu: createConfirmSubmenu({
|
|
166
179
|
message: "Reset all concurrency limits to defaults?",
|
|
167
180
|
theme,
|