pi-subagents-lite 1.4.2 → 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/package.json +1 -1
- package/src/agents/agent-discovery.ts +1 -2
- package/src/agents/agent-runner.ts +2 -9
- package/src/agents/agent-types.ts +22 -20
- package/src/agents/output-file.ts +20 -22
- package/src/agents/tool-execution.ts +3 -10
- package/src/agents/usage.ts +3 -10
- package/src/config/config-io.ts +40 -29
- package/src/config/config-store.ts +10 -19
- package/src/spawn/spawn-coordinator.ts +18 -15
- package/src/spawn/worktree-validator.ts +1 -4
- package/src/ui/agent-widget.ts +20 -18
- package/src/ui/format.ts +3 -3
- package/src/ui/menu/helpers.ts +2 -2
- package/src/ui/menu/menu-concurrency.ts +2 -1
- package/src/ui/menu/menu-model-settings.ts +2 -1
- package/src/ui/menu/menu-running-agents.ts +2 -1
- package/src/ui/menu/menu-spawn-wizard.ts +4 -4
- package/src/ui/menu/menu-system-prompt.ts +1 -1
- package/src/ui/menu/submenus/confirm.ts +2 -1
- package/src/ui/menu/submenus/model-select.ts +2 -1
- package/src/ui/result-viewer.ts +1 -1
- package/src/ui/searchable-select.ts +1 -1
- package/src/utils.ts +2 -0
package/package.json
CHANGED
|
@@ -257,7 +257,6 @@ function compactDefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
|
257
257
|
*/
|
|
258
258
|
export function parseAgentFile(
|
|
259
259
|
content: string,
|
|
260
|
-
_filename: string,
|
|
261
260
|
source: "user" | "project",
|
|
262
261
|
): AgentConfigFromMd {
|
|
263
262
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -310,7 +309,7 @@ export async function scanAgentFilesInDir(
|
|
|
310
309
|
const filePath = path.join(dirPath, entry.name);
|
|
311
310
|
try {
|
|
312
311
|
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
313
|
-
const info = parseAgentFile(content,
|
|
312
|
+
const info = parseAgentFile(content, source);
|
|
314
313
|
if (info.name) {
|
|
315
314
|
agents.push(info);
|
|
316
315
|
}
|
|
@@ -21,21 +21,14 @@ import {
|
|
|
21
21
|
import { getAgentConfig, getConfig, getToolNamesForType, resolveVisibleTools } from "./agent-types.js";
|
|
22
22
|
import { extractText } from "../prompt/context.js";
|
|
23
23
|
import type { LifetimeUsage } from "./usage.js";
|
|
24
|
-
import { findModelInRegistry } from "../utils.js";
|
|
24
|
+
import { findModelInRegistry, GIT_EXEC_TIMEOUT_MS } from "../utils.js";
|
|
25
25
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
26
26
|
import { buildAgentPrompt, type PromptExtras } from "../prompt/prompts.js";
|
|
27
27
|
import { preloadSkills, loadSkillMeta, type SkillMeta } from "../prompt/skill-loader.js";
|
|
28
28
|
import { type EnvInfo, type RunCallbacks, type RunTunables, SHORT_ID_LENGTH } from "../types.js";
|
|
29
29
|
import type { SubagentType, SystemPromptMode } from "./types.js";
|
|
30
30
|
import { getStore } from "../shell.js";
|
|
31
|
-
import { DEFAULT_GRACE_TURNS } from "../config/config-io.js";
|
|
32
|
-
|
|
33
|
-
/** Path to custom prompt file. Exported for use in menus.ts. */
|
|
34
|
-
export const CUSTOM_PROMPT_PATH = path.join(process.env.HOME || "", ".pi", "agent", "subagents-lite-prompt.md");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/** Timeout for quick git commands (branch detection, repo check). */
|
|
38
|
-
const GIT_EXEC_TIMEOUT_MS = 5000;
|
|
31
|
+
import { DEFAULT_GRACE_TURNS, CUSTOM_PROMPT_PATH } from "../config/config-io.js";
|
|
39
32
|
|
|
40
33
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
41
34
|
function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
@@ -66,6 +66,15 @@ export function setAgentScanDirs(userDir: string, projectDir: string): void {
|
|
|
66
66
|
projectAgentDir = projectDir;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Scan user and project agent directories, merge with defaults. Returns the merged Map. */
|
|
70
|
+
async function scanAndMerge(options?: { disableDefaultAgents?: boolean }): Promise<Map<string, AgentConfig>> {
|
|
71
|
+
const [userAgents, projectAgents] = await Promise.all([
|
|
72
|
+
scanAgentFilesInDir(userAgentDir, "user"),
|
|
73
|
+
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
74
|
+
]);
|
|
75
|
+
const defaults = options?.disableDefaultAgents ? new Map<string, AgentConfig>() : DEFAULT_AGENTS;
|
|
76
|
+
return mergeAgents(defaults, userAgents, projectAgents);
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* Scan the known agent directories and register any newly discovered agents
|
|
71
80
|
* that aren't already in the registry. Returns the number of new agents added.
|
|
@@ -77,13 +86,7 @@ export function setAgentScanDirs(userDir: string, projectDir: string): void {
|
|
|
77
86
|
* @param options - Optional settings. disableDefaultAgents skips DEFAULT_AGENTS in the merge.
|
|
78
87
|
*/
|
|
79
88
|
export async function discoverNewAgents(worktreeDir?: string, options?: { disableDefaultAgents?: boolean }): Promise<number> {
|
|
80
|
-
const
|
|
81
|
-
scanAgentFilesInDir(userAgentDir, "user"),
|
|
82
|
-
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
const defaults = options?.disableDefaultAgents ? new Map<string, AgentConfig>() : DEFAULT_AGENTS;
|
|
86
|
-
const merged = mergeAgents(defaults, userAgents, projectAgents);
|
|
89
|
+
const merged = await scanAndMerge(options);
|
|
87
90
|
|
|
88
91
|
let count = 0;
|
|
89
92
|
for (const [name, config] of merged) {
|
|
@@ -96,8 +99,6 @@ export async function discoverNewAgents(worktreeDir?: string, options?: { disabl
|
|
|
96
99
|
// Scan worktree-local agents (only when worktreeDir is provided)
|
|
97
100
|
if (worktreeDir) {
|
|
98
101
|
const worktreeAgents = await scanAgentFilesInDir(worktreeDir, "project");
|
|
99
|
-
// Use mergeAgents to convert AgentConfigFromMd to AgentConfig (applies fromMd
|
|
100
|
-
// and BASE_DEFAULTS), then add only names not already in the registry.
|
|
101
102
|
const wtMerged = mergeAgents(new Map(), [], worktreeAgents);
|
|
102
103
|
for (const [name, config] of wtMerged) {
|
|
103
104
|
if (!agents.has(name)) {
|
|
@@ -302,22 +303,23 @@ function applyGlobalDefaults(
|
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
/** Find the first non-hidden config: resolved type, then general-purpose, then undefined. */
|
|
307
|
+
function findActiveConfig(type: string): AgentConfig | undefined {
|
|
308
|
+
const key = resolveType(type);
|
|
309
|
+
const config = key ? agents.get(key) : undefined;
|
|
310
|
+
if (config?.hidden !== true) return config;
|
|
311
|
+
return agents.get("general-purpose");
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
/** Get config for a type (case-insensitive). Falls back to general-purpose. */
|
|
306
315
|
export function getConfig(
|
|
307
316
|
type: string,
|
|
308
317
|
loadSkillsImplicitly: boolean = true,
|
|
309
318
|
loadExtensionsImplicitly: boolean = true,
|
|
310
319
|
): ResolvedAgentConfig {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// If config exists and is not hidden, use it; otherwise fall back to general-purpose
|
|
315
|
-
const activeConfig = config?.hidden !== true
|
|
316
|
-
? config
|
|
317
|
-
: agents.get("general-purpose");
|
|
318
|
-
|
|
319
|
-
if (activeConfig && activeConfig.hidden !== true) {
|
|
320
|
-
const { skills, extensions, ...rest } = activeConfig;
|
|
320
|
+
const config = findActiveConfig(type);
|
|
321
|
+
if (config) {
|
|
322
|
+
const { skills, extensions, ...rest } = config;
|
|
321
323
|
const defaults = applyGlobalDefaults(skills, extensions, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
322
324
|
return {
|
|
323
325
|
displayName: rest.displayName ?? rest.name,
|
|
@@ -328,7 +330,7 @@ export function getConfig(
|
|
|
328
330
|
};
|
|
329
331
|
}
|
|
330
332
|
|
|
331
|
-
// Absolute fallback —
|
|
333
|
+
// Absolute fallback — no config found at all
|
|
332
334
|
const defaults = applyGlobalDefaults(undefined, undefined, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
333
335
|
return {
|
|
334
336
|
displayName: "Agent",
|
|
@@ -25,6 +25,13 @@ function findLastSentenceBoundary(text: string): number {
|
|
|
25
25
|
}
|
|
26
26
|
return -1;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/** Format the [DONE] summary line with final stats. */
|
|
30
|
+
function formatDoneLine(stats: { turnCount: number; toolUseCount: number; totalTokens: number; cost: number }): string {
|
|
31
|
+
const tokensStr = `${formatTokens(stats.totalTokens)} tokens`;
|
|
32
|
+
const costStr = `$${stats.cost.toFixed(3)}`;
|
|
33
|
+
return `${timestamp()} [DONE] ${stats.turnCount} turns, ${stats.toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`;
|
|
34
|
+
}
|
|
28
35
|
/** Max content length for full tool result display — longer results get a summary line. */
|
|
29
36
|
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
30
37
|
|
|
@@ -176,7 +183,7 @@ export function streamToOutputFile(
|
|
|
176
183
|
|
|
177
184
|
const flushThinkingBuffer = () => {
|
|
178
185
|
if (thinkingBuffer.length > 0) {
|
|
179
|
-
safeAppend(path,
|
|
186
|
+
safeAppend(path, `${timestamp()} [THINKING] ${thinkingBuffer}\n`);
|
|
180
187
|
streamedThinkingChars += thinkingBuffer.length;
|
|
181
188
|
thinkingBuffer = "";
|
|
182
189
|
}
|
|
@@ -225,21 +232,16 @@ export function streamToOutputFile(
|
|
|
225
232
|
thinkingBlockInProgress = true;
|
|
226
233
|
} else if (assistantEvent.type === "thinking_delta") {
|
|
227
234
|
thinkingBuffer += assistantEvent.delta;
|
|
228
|
-
if (thinkingBuffer.length >= bufferSize
|
|
235
|
+
if (thinkingBuffer.length >= bufferSize) {
|
|
229
236
|
// Round down to nearest sentence boundary when possible
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
streamedThinkingChars += flushText.length;
|
|
237
|
-
} else {
|
|
238
|
-
// No sentence boundary found, flush at buffer limit
|
|
239
|
-
flushThinkingBuffer();
|
|
240
|
-
}
|
|
237
|
+
const boundary = findLastSentenceBoundary(thinkingBuffer);
|
|
238
|
+
if (boundary >= 0) {
|
|
239
|
+
const flushText = thinkingBuffer.slice(0, boundary + 1);
|
|
240
|
+
thinkingBuffer = thinkingBuffer.slice(boundary + 1);
|
|
241
|
+
safeAppend(path, `${timestamp()} [THINKING] ${flushText}\n`);
|
|
242
|
+
streamedThinkingChars += flushText.length;
|
|
241
243
|
} else {
|
|
242
|
-
//
|
|
244
|
+
// No sentence boundary found, flush at buffer limit
|
|
243
245
|
flushThinkingBuffer();
|
|
244
246
|
}
|
|
245
247
|
}
|
|
@@ -249,7 +251,7 @@ export function streamToOutputFile(
|
|
|
249
251
|
flushThinkingBuffer();
|
|
250
252
|
if (assistantEvent.content && assistantEvent.content.length > streamedThinkingChars) {
|
|
251
253
|
const remaining = assistantEvent.content.slice(streamedThinkingChars);
|
|
252
|
-
safeAppend(path,
|
|
254
|
+
safeAppend(path, `${timestamp()} [THINKING] ${remaining}\n`);
|
|
253
255
|
streamedThinkingChars = assistantEvent.content.length;
|
|
254
256
|
}
|
|
255
257
|
streamedThinkingBlocks++;
|
|
@@ -264,10 +266,8 @@ export function streamToOutputFile(
|
|
|
264
266
|
flush();
|
|
265
267
|
|
|
266
268
|
// Write DONE line
|
|
267
|
-
const { turnCount
|
|
268
|
-
|
|
269
|
-
const costStr = `$${cost.toFixed(3)}`;
|
|
270
|
-
safeAppend(path, `${timestamp()} [DONE] ${turnCount} turns, ${toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
269
|
+
const doneStats = stats ?? { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
|
|
270
|
+
safeAppend(path, formatDoneLine(doneStats));
|
|
271
271
|
|
|
272
272
|
// Unsubscribe from session events
|
|
273
273
|
unsubscribe();
|
|
@@ -336,9 +336,7 @@ export class AgentOutputLog {
|
|
|
336
336
|
this.statsRef = undefined;
|
|
337
337
|
} else {
|
|
338
338
|
// No attach was called — write DONE directly
|
|
339
|
-
|
|
340
|
-
const costStr = `$${stats.cost.toFixed(3)}`;
|
|
341
|
-
safeAppend(this.path, `${timestamp()} [DONE] ${stats.turnCount} turns, ${stats.toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
339
|
+
safeAppend(this.path, formatDoneLine(stats));
|
|
342
340
|
}
|
|
343
341
|
}
|
|
344
342
|
}
|
|
@@ -52,16 +52,9 @@ function errorResult(text: string, details?: Record<string, unknown>) {
|
|
|
52
52
|
* Consolidates the identical field-selection logic previously duplicated
|
|
53
53
|
* across emitIndividualNudge, executeSpawnForeground, and executeSpawnBackground.
|
|
54
54
|
*/
|
|
55
|
-
interface AgentDetailsOptions {
|
|
56
|
-
/** Include full stats (turns, tokens, context%, compactions, cost). Default: false. */
|
|
57
|
-
includeStats?: boolean;
|
|
58
|
-
/** Include status and outputFile. Default: false. */
|
|
59
|
-
includeStatus?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
55
|
export function buildAgentDetails(
|
|
63
56
|
record: AgentRecord,
|
|
64
|
-
|
|
57
|
+
opts?: { includeStats?: boolean; includeStatus?: boolean },
|
|
65
58
|
): Record<string, unknown> {
|
|
66
59
|
const details: Record<string, unknown> = {
|
|
67
60
|
type: record.display.type,
|
|
@@ -72,12 +65,12 @@ export function buildAgentDetails(
|
|
|
72
65
|
details.worktreePath = record.display.worktreePath;
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
if (
|
|
68
|
+
if (opts?.includeStatus) {
|
|
76
69
|
details.status = record.lifecycle.status;
|
|
77
70
|
details.outputFile = record.display.outputFile;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
if (
|
|
73
|
+
if (opts?.includeStats) {
|
|
81
74
|
const elapsedMs = record.lifecycle.completedAt ? record.lifecycle.completedAt - record.lifecycle.startedAt : 0;
|
|
82
75
|
|
|
83
76
|
details.turnCount = record.stats.turnCount;
|
package/src/agents/usage.ts
CHANGED
|
@@ -29,17 +29,10 @@ type SessionStatsLike = {
|
|
|
29
29
|
};
|
|
30
30
|
export type SessionLike = { getSessionStats(): SessionStatsLike };
|
|
31
31
|
|
|
32
|
-
/** Format a token count compactly: "12.3k", "1.2M", or raw number. */
|
|
33
|
-
export function formatTokens(count: number): string {
|
|
32
|
+
/** Format a token count compactly: "12.3k", "1.2M", or raw number. When compact is true, thousands round to whole numbers. */
|
|
33
|
+
export function formatTokens(count: number, compact = false): string {
|
|
34
34
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
35
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
36
|
-
return `${count}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Format token count for widget display: rounded to whole number for k. */
|
|
40
|
-
export function formatTokensCompact(count: number): string {
|
|
41
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
42
|
-
if (count >= 1_000) return `${Math.round(count / 1_000)}k`;
|
|
35
|
+
if (count >= 1_000) return compact ? `${Math.round(count / 1_000)}k` : `${(count / 1_000).toFixed(1)}k`;
|
|
43
36
|
return `${count}`;
|
|
44
37
|
}
|
|
45
38
|
|
package/src/config/config-io.ts
CHANGED
|
@@ -11,46 +11,57 @@ import type { SubagentsConfig } from "../models/model-precedence.js";
|
|
|
11
11
|
|
|
12
12
|
const CONFIG_DIR = path.join(process.env.HOME || "", ".pi", "agent");
|
|
13
13
|
const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
|
|
14
|
-
|
|
14
|
+
/** Path to custom prompt file for subagent system prompts. */
|
|
15
|
+
export const CUSTOM_PROMPT_PATH = path.join(CONFIG_DIR, "subagents-lite-prompt.md");
|
|
15
16
|
/** Default number of grace turns before an agent is force-stopped. */
|
|
16
17
|
export const DEFAULT_GRACE_TURNS = 6;
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
/** Valid system prompt modes. */
|
|
20
|
+
export const VALID_SYSTEM_PROMPT_MODES = new Set<string>(["replace", "inherit", "custom"]);
|
|
21
|
+
|
|
22
|
+
/** Default concurrency config — used for resets. */
|
|
23
|
+
export const DEFAULT_CONCURRENCY: SubagentsConfig["concurrency"] = { default: 4 };
|
|
24
|
+
|
|
25
|
+
/** Default agent settings — merged into loaded config so callers get a complete shape. */
|
|
26
|
+
const DEFAULT_AGENT: SubagentsConfig["agent"] = {
|
|
27
|
+
default: null,
|
|
28
|
+
forceBackground: false,
|
|
29
|
+
graceTurns: DEFAULT_GRACE_TURNS,
|
|
30
|
+
widgetMaxLines: 12,
|
|
31
|
+
widgetDescLengthFull: 50,
|
|
32
|
+
widgetDescLengthCompact: 30,
|
|
33
|
+
widgetCompact: false,
|
|
34
|
+
widgetShortcut: false,
|
|
35
|
+
systemPromptMode: "replace",
|
|
36
|
+
includeContextFiles: true,
|
|
37
|
+
disableDefaultAgents: false,
|
|
38
|
+
showTools: true,
|
|
39
|
+
showTurns: true,
|
|
40
|
+
showInput: true,
|
|
41
|
+
showOutput: true,
|
|
42
|
+
showContext: true,
|
|
43
|
+
showCost: false,
|
|
44
|
+
showTime: true,
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* Read config from disk.
|
|
48
|
+
* Read config from disk. Merges loaded values over defaults so the result
|
|
49
|
+
* is always a complete SubagentsConfig — no partial shapes for callers to handle.
|
|
46
50
|
*/
|
|
47
51
|
export function loadConfig(): SubagentsConfig {
|
|
52
|
+
let raw: SubagentsConfig;
|
|
48
53
|
try {
|
|
49
|
-
|
|
50
|
-
return JSON.parse(raw) as SubagentsConfig;
|
|
54
|
+
raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as SubagentsConfig;
|
|
51
55
|
} catch {
|
|
52
|
-
|
|
56
|
+
raw = {} as SubagentsConfig;
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error TS2783: spread may override 'default', which is intentional (loaded value wins)
|
|
60
|
+
const concurrency = { default: 4, ...(raw.concurrency ?? {}) } as SubagentsConfig["concurrency"];
|
|
61
|
+
return {
|
|
62
|
+
agent: { ...DEFAULT_AGENT, ...raw.agent },
|
|
63
|
+
concurrency,
|
|
64
|
+
};
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/** Write config to disk with atomic rename. */
|
|
@@ -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 {
|
|
@@ -108,29 +106,22 @@ export class ConfigStore {
|
|
|
108
106
|
|
|
109
107
|
get agent(): ResolvedAgentSettings {
|
|
110
108
|
const a = this.config.agent;
|
|
111
|
-
const widgetMaxLines = a.widgetMaxLines
|
|
109
|
+
const widgetMaxLines = a.widgetMaxLines!; // guaranteed by loadConfig default merge
|
|
112
110
|
const widgetMaxLinesCompact = a.widgetMaxLinesCompact ?? Math.floor(widgetMaxLines / 2);
|
|
113
|
-
const widgetCompact = a.widgetCompact === true;
|
|
114
|
-
const widgetShortcut = a.widgetShortcut === true;
|
|
115
|
-
const widgetDescLengthFull = a.widgetDescLengthFull ?? DEFAULT_CONFIG.agent.widgetDescLengthFull ?? 50;
|
|
116
|
-
const widgetDescLengthCompact = a.widgetDescLengthCompact ?? DEFAULT_CONFIG.agent.widgetDescLengthCompact ?? 30;
|
|
117
|
-
const rawMode = a.systemPromptMode;
|
|
118
|
-
const systemPromptMode = VALID_SYSTEM_PROMPT_MODES.has(rawMode as string) ? rawMode as SystemPromptMode : "replace";
|
|
119
|
-
const includeContextFiles = a.includeContextFiles ?? DEFAULT_CONFIG.agent.includeContextFiles ?? true;
|
|
120
111
|
|
|
121
112
|
return {
|
|
122
113
|
defaultModel: a.default ?? null,
|
|
123
114
|
forceBackground: a.forceBackground === true,
|
|
124
115
|
showCost: this.sessionShowCost ?? (a.showCost === true),
|
|
125
|
-
graceTurns: a.graceTurns ??
|
|
116
|
+
graceTurns: a.graceTurns ?? 6,
|
|
126
117
|
widgetMaxLines,
|
|
127
118
|
widgetMaxLinesCompact,
|
|
128
|
-
widgetCompact,
|
|
129
|
-
widgetShortcut,
|
|
130
|
-
widgetDescLengthFull,
|
|
131
|
-
widgetDescLengthCompact,
|
|
132
|
-
systemPromptMode,
|
|
133
|
-
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,
|
|
134
125
|
defaultThinking: a.defaultThinking as ThinkingLevel | undefined,
|
|
135
126
|
defaultMaxTurns: a.defaultMaxTurns,
|
|
136
127
|
loadSkillsImplicitly: a.loadSkillsImplicitly !== false,
|
|
@@ -344,7 +335,7 @@ export class ConfigStore {
|
|
|
344
335
|
this.applyConcurrency();
|
|
345
336
|
},
|
|
346
337
|
reset: (): void => {
|
|
347
|
-
this.config.concurrency = { ...
|
|
338
|
+
this.config.concurrency = { ...DEFAULT_CONCURRENCY };
|
|
348
339
|
this.persist();
|
|
349
340
|
this.applyConcurrency();
|
|
350
341
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getStatusNote } from "../status-note.js";
|
|
2
|
-
import { getWidget } from "../shell.js";
|
|
2
|
+
import { getPiInstance, getSessionCtx, getWidget } from "../shell.js";
|
|
3
3
|
/**
|
|
4
4
|
* spawn-coordinator.ts — Spawn-and-track coordination for subagents.
|
|
5
5
|
*
|
|
@@ -64,9 +64,6 @@ export class SpawnCoordinator {
|
|
|
64
64
|
/** Active nudge timer. */
|
|
65
65
|
private nudgeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
66
|
|
|
67
|
-
/** Latest ExtensionAPI reference, refreshed on each spawn() call. */
|
|
68
|
-
private pi: ExtensionAPI | null = null;
|
|
69
|
-
|
|
70
67
|
/** Set during dispose to prevent nudge emission after session replacement. */
|
|
71
68
|
private disposed = false;
|
|
72
69
|
|
|
@@ -114,8 +111,8 @@ export class SpawnCoordinator {
|
|
|
114
111
|
this.backgroundAgentIds.add(agentId);
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
//
|
|
118
|
-
|
|
114
|
+
// pi is read from shell at nudge time, not stored here
|
|
115
|
+
// (avoids stale ctx after session replacement or reload)
|
|
119
116
|
|
|
120
117
|
const record = this.manager.getRecord(agentId)!;
|
|
121
118
|
|
|
@@ -212,11 +209,9 @@ export class SpawnCoordinator {
|
|
|
212
209
|
// Skip if disposed — prevents stale pi usage after session replacement
|
|
213
210
|
if (this.disposed) return;
|
|
214
211
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return;
|
|
219
|
-
}
|
|
212
|
+
// Read pi from shell at call time so we get a fresh reference after reload.
|
|
213
|
+
const pi = getPiInstance();
|
|
214
|
+
if (!pi) return;
|
|
220
215
|
|
|
221
216
|
const record = this.manager.getRecord(agentId);
|
|
222
217
|
if (!record) return;
|
|
@@ -227,7 +222,14 @@ export class SpawnCoordinator {
|
|
|
227
222
|
});
|
|
228
223
|
|
|
229
224
|
try {
|
|
230
|
-
|
|
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(
|
|
231
233
|
{
|
|
232
234
|
customType: "subagent-result",
|
|
233
235
|
content: `[Subagent "${record.display.type}" ${record.lifecycle.status}]\n\n${record.result ?? ""} ${getStatusNote(record.lifecycle.status)}`,
|
|
@@ -235,13 +237,14 @@ export class SpawnCoordinator {
|
|
|
235
237
|
display: true,
|
|
236
238
|
},
|
|
237
239
|
{
|
|
238
|
-
deliverAs
|
|
240
|
+
deliverAs,
|
|
239
241
|
triggerTurn: true,
|
|
240
242
|
},
|
|
241
243
|
);
|
|
242
244
|
} catch (error) {
|
|
243
|
-
//
|
|
244
|
-
|
|
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.
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
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",
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -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
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* searchable pick-list submenu factory.
|
|
6
6
|
*/
|
|
7
7
|
import type { Component, SettingsListTheme } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { Theme } from "../types.js";
|
|
8
9
|
import { SearchableSelectDialog, type SelectOption } from "../searchable-select.js";
|
|
9
10
|
import { parseModelKey } from "../../utils.js";
|
|
10
|
-
|
|
11
11
|
/**
|
|
12
12
|
* Build SelectOption[] from raw "provider/model-id" strings.
|
|
13
13
|
* Includes "(inherits parent)" as the first option.
|
|
@@ -103,7 +103,7 @@ export function buildSelectListTheme(theme: { fg(color: string, text: string): s
|
|
|
103
103
|
export function createSearchableSelect(
|
|
104
104
|
items: SelectOption[],
|
|
105
105
|
callbacks: { onSelect: (value: string) => Component | void; onCancel: () => void },
|
|
106
|
-
theme:
|
|
106
|
+
theme: Theme,
|
|
107
107
|
): Component {
|
|
108
108
|
let delegator: ReturnType<typeof createDelegatingComponent>;
|
|
109
109
|
const selector = new SearchableSelectDialog(
|
|
@@ -23,13 +23,14 @@ import { createConfirmSubmenu } from "./submenus/confirm.js";
|
|
|
23
23
|
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
24
24
|
import { getStore } from "../../shell.js";
|
|
25
25
|
import type { SelectOption } from "../searchable-select.js";
|
|
26
|
+
import type { Theme } from "../types.js";
|
|
26
27
|
|
|
27
28
|
export async function showConcurrencySettingsMenu(
|
|
28
29
|
ctx: ExtensionCommandContext,
|
|
29
30
|
modelOptions: string[],
|
|
30
31
|
): Promise<void> {
|
|
31
32
|
// Build menu items from current store state.
|
|
32
|
-
const buildItems = (store: ReturnType<typeof getStore>, theme:
|
|
33
|
+
const buildItems = (store: ReturnType<typeof getStore>, theme: Theme, modelOptions: string[], onRebuild?: () => void): SettingItem[] => {
|
|
33
34
|
const providers = [...new Set(modelOptions.map((m) => m.split("/")[0]))].sort();
|
|
34
35
|
const items: SettingItem[] = [];
|
|
35
36
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { SettingsList, type SettingItem } from "@earendil-works/pi-tui";
|
|
14
14
|
import { getAgentConfig, getAllTypes } from "../../agents/agent-types.js";
|
|
15
|
+
import type { Theme } from "../types.js";
|
|
15
16
|
import { CONFIG_AGENT_NON_MODEL_KEYS } from "../../config/types.js";
|
|
16
17
|
import { buildSettingsListTheme, createSearchableSelect } from "./helpers.js";
|
|
17
18
|
import { createModelSelectSubmenu } from "./submenus/model-select.js";
|
|
@@ -24,7 +25,7 @@ export async function showModelSettingsMenu(
|
|
|
24
25
|
modelOptions: string[],
|
|
25
26
|
): Promise<void> {
|
|
26
27
|
// Build menu items from current store state.
|
|
27
|
-
const buildItems = (store: ReturnType<typeof getStore>, theme:
|
|
28
|
+
const buildItems = (store: ReturnType<typeof getStore>, theme: Theme): SettingItem[] => {
|
|
28
29
|
const items: SettingItem[] = [];
|
|
29
30
|
|
|
30
31
|
// Shared onSelect for model override submenus: applies session/permanent/clear
|
|
@@ -22,6 +22,7 @@ import { getDisplayName, truncateDesc } from "../format.js";
|
|
|
22
22
|
import { buildSnapshotMarkdown } from "../../prompt/context.js";
|
|
23
23
|
import { buildSelectListTheme, createDelegatingComponent } from "./helpers.js";
|
|
24
24
|
import { getManager, getStore } from "../../shell.js";
|
|
25
|
+
import type { Theme } from "../types.js";
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Show a ResultViewer for an agent's result, error, or snapshot.
|
|
@@ -73,7 +74,7 @@ async function showResultViewer(
|
|
|
73
74
|
export function buildAgentActionsList(
|
|
74
75
|
ctx: ExtensionCommandContext,
|
|
75
76
|
record: AgentRecord,
|
|
76
|
-
theme:
|
|
77
|
+
theme: Theme,
|
|
77
78
|
done: () => void,
|
|
78
79
|
setActive: (c: import("@earendil-works/pi-tui").Component) => void,
|
|
79
80
|
onClose: () => void,
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { SettingsList, SelectList, type SettingItem } from "@earendil-works/pi-tui";
|
|
13
13
|
import type { ThinkingLevel } from "../../types.js";
|
|
14
|
+
import type { Theme } from "../types.js";
|
|
14
15
|
import { getAgentConfig, getAvailableTypes, resolveType, discoverNewAgents } from "../../agents/agent-types.js";
|
|
15
|
-
import { findModelInRegistry } from "../../utils.js";
|
|
16
|
+
import { findModelInRegistry, VALID_THINKING_LEVELS } from "../../utils.js";
|
|
16
17
|
import { buildSettingsListTheme, buildSelectListTheme, createSearchableSelect } from "./helpers.js";
|
|
17
18
|
import { DEFAULT_GRACE_TURNS } from "../../config/config-io.js";
|
|
18
19
|
import { createModelSelectSubmenu } from "./submenus/model-select.js";
|
|
@@ -120,7 +121,6 @@ async function isInGitRepo(cwd: string): Promise<boolean> {
|
|
|
120
121
|
// Spawn agent wizard
|
|
121
122
|
// ============================================================================
|
|
122
123
|
|
|
123
|
-
const THINKING_LEVELS: ThinkingLevel[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Show the spawn agent flow as a multi-step wizard:
|
|
@@ -363,7 +363,7 @@ export async function showSpawnAgentMenu(
|
|
|
363
363
|
label: "Thinking level",
|
|
364
364
|
currentValue: currentThinking ?? "inherit",
|
|
365
365
|
description: "Set the reasoning effort level",
|
|
366
|
-
values: [...
|
|
366
|
+
values: [...VALID_THINKING_LEVELS, "inherit"],
|
|
367
367
|
},
|
|
368
368
|
{
|
|
369
369
|
id: "maxTokens",
|
|
@@ -406,7 +406,7 @@ export async function showSpawnAgentMenu(
|
|
|
406
406
|
return items;
|
|
407
407
|
};
|
|
408
408
|
|
|
409
|
-
let theme:
|
|
409
|
+
let theme: Theme;
|
|
410
410
|
let doneRef: () => void;
|
|
411
411
|
|
|
412
412
|
await ctx.ui.custom((_tui, t, _kb, done) => {
|
|
@@ -17,7 +17,7 @@ import { buildSettingsListTheme } from "./helpers.js";
|
|
|
17
17
|
import { SettingsListWrapper } from "./wrappers/settings-list.js";
|
|
18
18
|
import type { SystemPromptMode } from "../../agents/types.js";
|
|
19
19
|
import { getStore } from "../../shell.js";
|
|
20
|
-
import { CUSTOM_PROMPT_PATH } from "../../
|
|
20
|
+
import { CUSTOM_PROMPT_PATH } from "../../config/config-io.js";
|
|
21
21
|
|
|
22
22
|
export async function showSystemPromptMenu(ctx: ExtensionCommandContext): Promise<void> {
|
|
23
23
|
const store = getStore();
|
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { SelectList, type Component } from "@earendil-works/pi-tui";
|
|
9
|
+
import type { Theme } from "../../types.js";
|
|
9
10
|
import { buildSelectListTheme } from "../helpers.js";
|
|
10
11
|
|
|
11
12
|
export interface ConfirmSubmenuOptions {
|
|
12
13
|
/** Message shown to the user */
|
|
13
14
|
message: string;
|
|
14
15
|
/** Theme from pi-coding-agent (fg, bold, italic) */
|
|
15
|
-
theme:
|
|
16
|
+
theme: Theme;
|
|
16
17
|
/** Called when user confirms (selects Yes) */
|
|
17
18
|
onConfirm: () => void;
|
|
18
19
|
}
|
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { SelectList, type Component } from "@earendil-works/pi-tui";
|
|
11
|
+
import type { Theme } from "../../types.js";
|
|
11
12
|
import { SearchableSelectDialog, type SelectOption } from "../../../ui/searchable-select.js";
|
|
12
13
|
import { buildModelOptions, buildSelectListTheme, createDelegatingComponent } from "../helpers.js";
|
|
13
14
|
|
|
14
15
|
export interface ModelSelectSubmenuOptions {
|
|
15
16
|
modelOptions: string[];
|
|
16
17
|
showClear: boolean;
|
|
17
|
-
theme:
|
|
18
|
+
theme: Theme;
|
|
18
19
|
onSelect: (mode: "session" | "permanent" | "clear", model: string | null) => void;
|
|
19
20
|
}
|
|
20
21
|
|
package/src/ui/result-viewer.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
visibleWidth,
|
|
18
18
|
} from "@earendil-works/pi-tui";
|
|
19
19
|
import { type LifetimeUsage, formatTokens } from "../agents/usage.js";
|
|
20
|
-
import type { Theme } from "./
|
|
20
|
+
import type { Theme } from "./types.js";
|
|
21
21
|
import { formatMs } from "./format.js";
|
|
22
22
|
|
|
23
23
|
/* ------------------------------------------------------------------ */
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Text,
|
|
16
16
|
} from "@earendil-works/pi-tui";
|
|
17
17
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import type { Theme } from "./
|
|
18
|
+
import type { Theme } from "./types.js";
|
|
19
19
|
|
|
20
20
|
/* ------------------------------------------------------------------ */
|
|
21
21
|
/* Types */
|
package/src/utils.ts
CHANGED
|
@@ -86,3 +86,5 @@ export function findModelInRegistry(
|
|
|
86
86
|
if (!parsed) return fallback;
|
|
87
87
|
return registry.find(parsed.provider, parsed.modelId) ?? fallback;
|
|
88
88
|
}
|
|
89
|
+
/** Timeout for git commands (ms). Shared by agent-runner and worktree-validator. */
|
|
90
|
+
export const GIT_EXEC_TIMEOUT_MS = 5000;
|