pi-subagents-lite 0.4.1 → 1.0.1
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 +128 -28
- package/package.json +6 -3
- package/src/agent-discovery.ts +11 -17
- package/src/agent-manager.ts +6 -17
- package/src/agent-runner.ts +254 -63
- package/src/agent-types.ts +65 -19
- package/src/config-io.ts +2 -4
- package/src/context.ts +9 -6
- package/src/default-agents.ts +3 -3
- package/src/index.ts +7 -4
- package/src/menus.ts +36 -9
- package/src/model-precedence.ts +4 -2
- package/src/model-selector.ts +1 -7
- package/src/output-file.ts +3 -6
- package/src/prompts.ts +2 -3
- package/src/result-viewer.ts +1 -4
- package/src/skill-loader.ts +0 -2
- package/src/tool-execution.ts +10 -14
- package/src/types.ts +20 -22
- package/src/ui/agent-widget.ts +2 -5
- package/src/utils.ts +4 -5
package/src/index.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* - Config mutations update cache + atomic write to disk
|
|
16
16
|
*
|
|
17
17
|
* Commands:
|
|
18
|
-
* - /agents: Management menu
|
|
18
|
+
* - /agents: Management menu (model settings, concurrency, running agents, debug)
|
|
19
19
|
*
|
|
20
20
|
* Events:
|
|
21
21
|
* - tool_call: Inject model into Agent tool calls
|
|
@@ -33,10 +33,10 @@ import type {
|
|
|
33
33
|
} from "@earendil-works/pi-coding-agent";
|
|
34
34
|
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
35
35
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
36
|
-
import { registerAgents, getAvailableTypes } from "./agent-types.js";
|
|
36
|
+
import { registerAgents, getAvailableTypes, setAgentScanDirs } from "./agent-types.js";
|
|
37
37
|
import { scanAgentFilesInDir, mergeAgents } from "./agent-discovery.js";
|
|
38
38
|
import { AgentManager } from "./agent-manager.js";
|
|
39
|
-
import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type UICtx } from "./ui/agent-widget.js";
|
|
39
|
+
import { AgentWidget, buildStatsParts, formatMs, getDisplayName, type AgentActivity, type Theme, type UICtx } from "./ui/agent-widget.js";
|
|
40
40
|
import { showAgentsMainMenu } from "./menus.js";
|
|
41
41
|
import { loadConfig, DEFAULT_CONFIG } from "./config-io.js";
|
|
42
42
|
import { executeAgentTool, toolCallListener, backgroundAgentIds, scheduleNudge } from "./tool-execution.js";
|
|
@@ -111,6 +111,9 @@ async function scanAndRegisterAgents(ctx: ExtensionContext): Promise<void> {
|
|
|
111
111
|
const userAgentDir = path.join(homeDir, ".pi", "agent", "agents");
|
|
112
112
|
const projectAgentDir = path.join(ctx.cwd, ".pi", "agents");
|
|
113
113
|
|
|
114
|
+
// Store scan dirs for on-demand discovery (agents added during the session)
|
|
115
|
+
setAgentScanDirs(userAgentDir, projectAgentDir);
|
|
116
|
+
|
|
114
117
|
const [userAgents, projectAgents] = await Promise.all([
|
|
115
118
|
scanAgentFilesInDir(userAgentDir, "user"),
|
|
116
119
|
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
@@ -134,7 +137,7 @@ async function loadConfigAndRegisterAgents(ctx: ExtensionContext): Promise<void>
|
|
|
134
137
|
// ============================================================================
|
|
135
138
|
|
|
136
139
|
/** Build the stats line for an agent result card. Used by both renderers. */
|
|
137
|
-
function buildStatsLine(d: Record<string, unknown>, theme:
|
|
140
|
+
function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
|
|
138
141
|
const parts = buildStatsParts({
|
|
139
142
|
toolUses: (d.toolUses as number) ?? 0,
|
|
140
143
|
turnCount: d.turnCount as number | undefined,
|
package/src/menus.ts
CHANGED
|
@@ -301,6 +301,26 @@ export async function showModelSettingsMenu(
|
|
|
301
301
|
);
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
// Grace turns setting
|
|
305
|
+
const graceTurns = __config.agent.graceTurns ?? 6;
|
|
306
|
+
items.push(`Grace turns · ${graceTurns}`);
|
|
307
|
+
actions.push(async () => {
|
|
308
|
+
const input = await ctx.ui.input("Grace turns (≥ 0)", String(graceTurns));
|
|
309
|
+
if (input === undefined) return;
|
|
310
|
+
const parsed = parseInt(input.trim(), 10);
|
|
311
|
+
if (isNaN(parsed)) {
|
|
312
|
+
ctx.ui.notify("Invalid value — must be a number", "error");
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (parsed < 0) {
|
|
316
|
+
ctx.ui.notify("Invalid value — must be ≥ 0", "error");
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
__config.agent.graceTurns = parsed;
|
|
320
|
+
saveConfigAtomic(__config);
|
|
321
|
+
ctx.ui.notify(`Grace turns set to ${parsed}`, "info");
|
|
322
|
+
});
|
|
323
|
+
|
|
304
324
|
items.push("");
|
|
305
325
|
actions.push(async () => {});
|
|
306
326
|
items.push("─── per-type overrides ───");
|
|
@@ -375,13 +395,20 @@ export async function showModelSettingsMenu(
|
|
|
375
395
|
items.push("Clear all overrides");
|
|
376
396
|
actions.push(async () => {
|
|
377
397
|
const hasOverrides = Object.entries(__config.agent).some(
|
|
378
|
-
([k, v]) => k !== "default" && k !== "forceBackground" && v != null,
|
|
398
|
+
([k, v]) => k !== "default" && k !== "forceBackground" && k !== "graceTurns" && v != null,
|
|
379
399
|
);
|
|
380
400
|
if (!hasOverrides && __config.agent.default === null) {
|
|
381
401
|
ctx.ui.notify("No overrides to clear", "info");
|
|
382
402
|
return;
|
|
383
403
|
}
|
|
384
|
-
|
|
404
|
+
const preserved: Record<string, unknown> = {
|
|
405
|
+
default: __config.agent.default,
|
|
406
|
+
forceBackground: __config.agent.forceBackground,
|
|
407
|
+
};
|
|
408
|
+
if (__config.agent.graceTurns != null) {
|
|
409
|
+
preserved.graceTurns = __config.agent.graceTurns;
|
|
410
|
+
}
|
|
411
|
+
__config.agent = preserved as typeof __config.agent;
|
|
385
412
|
saveConfigAtomic(__config);
|
|
386
413
|
ctx.ui.notify("All model overrides cleared", "info");
|
|
387
414
|
});
|
|
@@ -453,8 +480,8 @@ async function handleAgentBriefing(ctx: ExtensionCommandContext): Promise<void>
|
|
|
453
480
|
lines.push(config.description);
|
|
454
481
|
lines.push("");
|
|
455
482
|
|
|
456
|
-
if (config.
|
|
457
|
-
lines.push(`**Tools:** ${config.
|
|
483
|
+
if (config.registeredTools) {
|
|
484
|
+
lines.push(`**Tools:** ${config.registeredTools.join(", ")}`);
|
|
458
485
|
}
|
|
459
486
|
if (config.model) {
|
|
460
487
|
lines.push(`**Default model:** ${config.model}`);
|
|
@@ -533,7 +560,7 @@ export async function showConcurrencySettingsMenu(
|
|
|
533
560
|
items.push(`Default concurrency limit · ${__config.concurrency.default}`);
|
|
534
561
|
actions.push(async () => {
|
|
535
562
|
await promptConcurrencyInput(
|
|
536
|
-
ctx, "Default
|
|
563
|
+
ctx, "Default limit", __config.concurrency.default,
|
|
537
564
|
(value) => { __config.concurrency.default = value; },
|
|
538
565
|
);
|
|
539
566
|
});
|
|
@@ -839,13 +866,13 @@ async function showAgentTypes(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
839
866
|
for (const name of types) {
|
|
840
867
|
const cfg = getAgentConfig(name);
|
|
841
868
|
if (!cfg) continue;
|
|
842
|
-
const
|
|
869
|
+
const hidden = cfg.hidden === true ? " [HIDDEN]" : "";
|
|
843
870
|
const model = cfg.model ? ` Model: ${cfg.model}` : "";
|
|
844
|
-
const tools = cfg.
|
|
845
|
-
? ` Tools: ${cfg.
|
|
871
|
+
const tools = cfg.registeredTools
|
|
872
|
+
? ` Tools: ${cfg.registeredTools.join(", ")}`
|
|
846
873
|
: " Tools: all built-in tools";
|
|
847
874
|
const source = cfg.source ? ` Source: ${cfg.source}` : "";
|
|
848
|
-
lines.push(` ${name}${
|
|
875
|
+
lines.push(` ${name}${hidden}`);
|
|
849
876
|
lines.push(` ${cfg.description}`);
|
|
850
877
|
if (model) lines.push(model);
|
|
851
878
|
lines.push(tools);
|
package/src/model-precedence.ts
CHANGED
|
@@ -17,7 +17,8 @@ export interface SubagentsConfig {
|
|
|
17
17
|
agent: {
|
|
18
18
|
default: string | null;
|
|
19
19
|
forceBackground: boolean;
|
|
20
|
-
|
|
20
|
+
graceTurns?: number;
|
|
21
|
+
[agentType: string]: string | null | undefined | boolean | number;
|
|
21
22
|
};
|
|
22
23
|
concurrency: {
|
|
23
24
|
default: number;
|
|
@@ -60,10 +61,11 @@ export function resolveModel(options: ResolveModelOptions): string {
|
|
|
60
61
|
const { subagentType, agentConfig, config, parentModelId, sessionOverrides } = options;
|
|
61
62
|
|
|
62
63
|
// Precedence chain: session > config > frontmatter > parent
|
|
64
|
+
// Cast agent values: index signature includes number (graceTurns), but models are always strings
|
|
63
65
|
const candidates: Array<string | boolean | null | undefined> = [
|
|
64
66
|
sessionOverrides?.[subagentType],
|
|
65
67
|
sessionOverrides?.["default"],
|
|
66
|
-
config.agent[subagentType],
|
|
68
|
+
config.agent[subagentType] as string | null | undefined,
|
|
67
69
|
config.agent["default"],
|
|
68
70
|
agentConfig?.model,
|
|
69
71
|
parentModelId, // final fallback (always a valid string)
|
package/src/model-selector.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* model-selector.ts — TUI model selection dialog.
|
|
3
3
|
*
|
|
4
|
-
* Ported from subagent-lazy/model-selector.ts verbatim.
|
|
5
|
-
* Used by the /agents menu for model selection.
|
|
6
|
-
*
|
|
7
4
|
* Reuses the same building blocks as pi's ModelSelectorComponent but without
|
|
8
5
|
* the SettingsManager dependency — no side effects, just callbacks.
|
|
9
6
|
*/
|
|
@@ -18,10 +15,7 @@ import {
|
|
|
18
15
|
Text,
|
|
19
16
|
} from "@earendil-works/pi-tui";
|
|
20
17
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
21
|
-
|
|
22
|
-
// Theme type from ctx.ui.custom() callback — avoid deep import that may not resolve
|
|
23
|
-
// in jiti extension loader. The constructor receives the theme instance directly.
|
|
24
|
-
type Theme = any;
|
|
18
|
+
import type { Theme } from "./ui/agent-widget.js";
|
|
25
19
|
|
|
26
20
|
/* ------------------------------------------------------------------ */
|
|
27
21
|
/* Types */
|
package/src/output-file.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* output-file.ts — Human-readable output logging for agent transcripts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Directory created with 0o700 permissions
|
|
8
|
-
* - Append-only, human-readable, supports `tail -f`
|
|
9
|
-
* - Lines: [USER], [TOOL], [ASSISTANT], [DONE] with ISO timestamps
|
|
4
|
+
* Path: /tmp/pi-agent-outputs/<agentId>.log
|
|
5
|
+
* Append-only, human-readable, supports `tail -f`.
|
|
6
|
+
* Lines: [USER], [TOOL], [ASSISTANT], [DONE] with ISO timestamps.
|
|
10
7
|
*/
|
|
11
8
|
|
|
12
9
|
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
package/src/prompts.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import type { AgentConfig, EnvInfo } from "./types.js";
|
|
9
9
|
import type { SkillMeta } from "./skill-loader.js";
|
|
10
|
-
export type { SkillMeta };
|
|
11
10
|
|
|
12
|
-
/** Extra sections to inject into the system prompt (skills
|
|
11
|
+
/** Extra sections to inject into the system prompt (skills). */
|
|
13
12
|
export interface PromptExtras {
|
|
14
13
|
/** Preloaded skill contents to inject (full content). */
|
|
15
14
|
skillBlocks?: { name: string; content: string }[];
|
|
@@ -45,7 +44,7 @@ export function buildAgentPrompt(
|
|
|
45
44
|
envLines.push(`Platform: ${env.platform}`);
|
|
46
45
|
const envBlock = envLines.join("\n");
|
|
47
46
|
|
|
48
|
-
// Build optional extras suffix (skills
|
|
47
|
+
// Build optional extras suffix (skills)
|
|
49
48
|
const extraSections: string[] = [];
|
|
50
49
|
|
|
51
50
|
// Skill metadata whitelist (like Pi's available_skills format)
|
package/src/result-viewer.ts
CHANGED
|
@@ -17,10 +17,7 @@ import {
|
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
18
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { type LifetimeUsage, formatTokens } from "./usage.js";
|
|
20
|
-
import { formatMs } from "./ui/agent-widget.js";
|
|
21
|
-
|
|
22
|
-
// Theme type from ctx.ui.custom() callback
|
|
23
|
-
type Theme = any;
|
|
20
|
+
import { formatMs, type Theme } from "./ui/agent-widget.js";
|
|
24
21
|
|
|
25
22
|
/* ------------------------------------------------------------------ */
|
|
26
23
|
/* Types */
|
package/src/skill-loader.ts
CHANGED
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
* SKILL.md is a skill — we don't descend into it (Pi: skills don't nest).
|
|
17
17
|
*
|
|
18
18
|
* Symlinks are rejected for security (deviation from Pi, which follows them).
|
|
19
|
-
*
|
|
20
|
-
* Changed from upstream: imports from ./utils.js instead of ./memory.js.
|
|
21
19
|
*/
|
|
22
20
|
|
|
23
21
|
import type { Dirent } from "node:fs";
|
package/src/tool-execution.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-
|
|
|
10
10
|
import type { AgentRecord } from "./types.js";
|
|
11
11
|
import type { SpawnOptions as AgentManagerSpawnOptions } from "./agent-manager.js";
|
|
12
12
|
import type { AgentActivity } from "./ui/agent-widget.js";
|
|
13
|
-
import { resolveType, getAgentConfig } from "./agent-types.js";
|
|
13
|
+
import { resolveType, getAgentConfig, discoverNewAgents } from "./agent-types.js";
|
|
14
14
|
import { resolveModel } from "./model-precedence.js";
|
|
15
15
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
16
16
|
|
|
@@ -173,7 +173,12 @@ export async function executeAgentTool(
|
|
|
173
173
|
ctx: ExtensionContext,
|
|
174
174
|
): Promise<any> {
|
|
175
175
|
const type = (params.agent as string) || "general-purpose";
|
|
176
|
-
|
|
176
|
+
let resolvedType = resolveType(type);
|
|
177
|
+
if (!resolvedType) {
|
|
178
|
+
// Not found in registry — try scanning filesystem for agents added during the session
|
|
179
|
+
await discoverNewAgents();
|
|
180
|
+
resolvedType = resolveType(type);
|
|
181
|
+
}
|
|
177
182
|
if (!resolvedType) {
|
|
178
183
|
return errorResult(`Unknown agent type: ${type}`);
|
|
179
184
|
}
|
|
@@ -181,7 +186,6 @@ export async function executeAgentTool(
|
|
|
181
186
|
const prompt = params.prompt as string;
|
|
182
187
|
const description = params.description as string;
|
|
183
188
|
const runInBackground = params.run_in_background as boolean | undefined;
|
|
184
|
-
const isolated = params.isolated as boolean | undefined;
|
|
185
189
|
const maxTurns = params.max_turns as number | undefined ?? getAgentConfig(resolvedType)?.maxTurns;
|
|
186
190
|
const modelStr = params.model as string | undefined;
|
|
187
191
|
const model = findModelInRegistry(modelStr, ctx.modelRegistry, ctx.model);
|
|
@@ -201,10 +205,10 @@ export async function executeAgentTool(
|
|
|
201
205
|
description,
|
|
202
206
|
model,
|
|
203
207
|
maxTurns,
|
|
204
|
-
isolated,
|
|
205
208
|
thinkingLevel,
|
|
206
209
|
modelKey,
|
|
207
210
|
invocation: modelName ? { modelName } : undefined,
|
|
211
|
+
graceTurns: __config.agent.graceTurns,
|
|
208
212
|
};
|
|
209
213
|
|
|
210
214
|
if (runInBackground || __config.agent.forceBackground) {
|
|
@@ -234,12 +238,9 @@ async function executeSpawnBackground(
|
|
|
234
238
|
widget?.ensureTimer();
|
|
235
239
|
widget?.update();
|
|
236
240
|
|
|
237
|
-
const record = manager.getRecord(agentId)
|
|
238
|
-
if (!record) {
|
|
239
|
-
return errorResult("Failed to create agent");
|
|
240
|
-
}
|
|
241
|
+
const record = manager.getRecord(agentId)!;
|
|
241
242
|
const details: Record<string, unknown> = { type: resolvedType, description: spawnOptions.description };
|
|
242
|
-
const suffix = `A notification will arrive when done - User asks you not to poll or duplicate the delegated work.\n\nAgent ID: ${agentId}`;
|
|
243
|
+
const suffix = `A notification will arrive when done - User asks you not to poll, check status or duplicate the delegated work.\n\nAgent ID: ${agentId}`;
|
|
243
244
|
const label = record.status === "queued" ? "Agent queued" : "Agent running";
|
|
244
245
|
|
|
245
246
|
return successResult(`[${label}] ${suffix}`, details);
|
|
@@ -327,11 +328,6 @@ export async function toolCallListener(
|
|
|
327
328
|
}
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
// Inject isolated from agent config if not explicitly passed
|
|
331
|
-
if (input.isolated === undefined && agentConfig?.isolated !== undefined) {
|
|
332
|
-
input.isolated = agentConfig.isolated;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
331
|
// Inject thinking from agent config if not explicitly passed
|
|
336
332
|
if (input.thinking === undefined && agentConfig?.thinking !== undefined) {
|
|
337
333
|
input.thinking = agentConfig.thinking;
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Trimmed from upstream: removed ScheduledSubagent, ScheduleStoreData,
|
|
5
|
-
* IsolationMode, MemoryScope, JoinMode.
|
|
6
|
-
* From AgentConfig: removed memory, isolation, inheritContext, runInBackground.
|
|
7
|
-
* From AgentRecord: removed groupId, joinMode, worktree, worktreeResult.
|
|
8
|
-
* From AgentInvocation: removed inheritContext, isolation.
|
|
2
|
+
* Type definitions for the subagent system.
|
|
9
3
|
*/
|
|
10
4
|
|
|
11
5
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
@@ -22,11 +16,21 @@ export interface AgentConfig {
|
|
|
22
16
|
name: string;
|
|
23
17
|
displayName?: string;
|
|
24
18
|
description: string;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
/** Tools to register with the session (controls availability, not LLM visibility). */
|
|
20
|
+
registeredTools?: string[];
|
|
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. */
|
|
29
31
|
extensions: true | string[] | false;
|
|
32
|
+
/** Extension blacklist — all extensions except these load. Mutually exclusive with extensions (when extensions is string[]). */
|
|
33
|
+
excludeExtensions?: string[];
|
|
30
34
|
/** Whitelist of allowed skills (metadata only in system prompt). true = all, string[] = listed, false = none */
|
|
31
35
|
skills: true | string[] | false;
|
|
32
36
|
/** Skills to preload with full content into system prompt. string[] = listed, false/undefined = none */
|
|
@@ -35,14 +39,13 @@ export interface AgentConfig {
|
|
|
35
39
|
thinking?: ThinkingLevel;
|
|
36
40
|
maxTurns?: number;
|
|
37
41
|
systemPrompt: string;
|
|
38
|
-
|
|
39
|
-
isolated?: boolean;
|
|
42
|
+
|
|
40
43
|
/** true = this is an embedded default agent (informational) */
|
|
41
44
|
isDefault?: boolean;
|
|
42
|
-
/**
|
|
43
|
-
|
|
45
|
+
/** true = agent is hidden from the schema enum but can still be called by name. */
|
|
46
|
+
hidden?: boolean;
|
|
44
47
|
/** Where this agent was loaded from */
|
|
45
|
-
source?: "
|
|
48
|
+
source?: "project" | "global";
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
export interface AgentRecord {
|
|
@@ -87,7 +90,6 @@ export interface AgentInvocation {
|
|
|
87
90
|
modelName?: string;
|
|
88
91
|
thinking?: ThinkingLevel;
|
|
89
92
|
maxTurns?: number;
|
|
90
|
-
isolated?: boolean;
|
|
91
93
|
runInBackground?: boolean;
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -109,8 +111,4 @@ export interface CompactionInfo {
|
|
|
109
111
|
tokensBefore: number;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
export interface ModelKey {
|
|
114
|
-
provider: string;
|
|
115
|
-
modelId: string;
|
|
116
|
-
}
|
|
114
|
+
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent-widget.ts — Persistent widget showing running/completed agents above the editor.
|
|
3
|
-
*
|
|
4
|
-
* Ported from upstream pi-subagents.
|
|
5
|
-
* Import paths use relative imports within our extension.
|
|
6
|
-
* addUsage/getLifetimeTotal/getSessionContextPercent imported from ../usage.js.
|
|
7
3
|
*/
|
|
8
4
|
|
|
9
5
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
@@ -62,9 +58,10 @@ const TOOL_DISPLAY: Record<string, string> = {
|
|
|
62
58
|
|
|
63
59
|
// ---- Types ----
|
|
64
60
|
|
|
65
|
-
type Theme = {
|
|
61
|
+
export type Theme = {
|
|
66
62
|
fg(color: string, text: string): string;
|
|
67
63
|
bold(text: string): string;
|
|
64
|
+
italic?: (text: string) => string;
|
|
68
65
|
};
|
|
69
66
|
|
|
70
67
|
export type UICtx = {
|
package/src/utils.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* utils.ts — Security helpers
|
|
2
|
+
* utils.ts — Security helpers and general utilities.
|
|
3
3
|
*
|
|
4
|
-
* Security helpers
|
|
5
|
-
*
|
|
6
|
-
* and tool-execution can use them without circular dependencies.
|
|
4
|
+
* Security helpers (isUnsafeName, isSymlink, safeReadFile) protect against
|
|
5
|
+
* path traversal and symlink attacks in agent/skill name resolution.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { lstatSync, readFileSync } from "node:fs";
|
|
@@ -35,7 +34,7 @@ export function isSymlink(filePath: string): boolean {
|
|
|
35
34
|
*/
|
|
36
35
|
export function safeReadFile(filePath: string): string | undefined {
|
|
37
36
|
try {
|
|
38
|
-
if (
|
|
37
|
+
if (isSymlink(filePath)) return undefined;
|
|
39
38
|
return readFileSync(filePath, "utf-8");
|
|
40
39
|
} catch {
|
|
41
40
|
return undefined;
|