@trim21/personal-pi-extensions 0.1.586 → 0.1.588
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/bwrap/core.ts +9 -0
- package/src/bwrap/runtime.ts +57 -19
- package/src/spawn-agent-agents.ts +20 -0
- package/src/spawn-agent.ts +39 -76
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -134,6 +134,15 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* 把配置文件形状(可缺省、容忍未知字段)补全成完整 BwrapConfig:合并默认值后走
|
|
139
|
+
* 同一套 schema 校验。与 loadBwrapConfig 的文件路径共用同一条补全语义,供
|
|
140
|
+
* frontmatter 等非文件来源的配置使用(配置的加载/构造与沙箱创建解耦)。
|
|
141
|
+
*/
|
|
142
|
+
export function completeBwrapConfig(file: BwrapConfigFile): BwrapConfig {
|
|
143
|
+
return Value.Parse(bwrapConfigSchema, deepMerge(DEFAULT_CONFIG, file));
|
|
144
|
+
}
|
|
145
|
+
|
|
137
146
|
function deepMerge(base: BwrapConfig, overrides: Partial<BwrapConfig>): BwrapConfig {
|
|
138
147
|
return {
|
|
139
148
|
mode: overrides.mode ?? base.mode,
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { type SelectAction, selectMultiple, selectWithOptionalInput } from "../l
|
|
|
23
23
|
import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
|
|
24
24
|
import { commandPatternsFor } from "./approval-suggest.js";
|
|
25
25
|
import {
|
|
26
|
+
type BwrapConfig,
|
|
26
27
|
type BwrapMode,
|
|
27
28
|
findBwrap,
|
|
28
29
|
findMihomo,
|
|
@@ -308,12 +309,17 @@ const SANDBOX_ESCAPE_HATCH =
|
|
|
308
309
|
* 命令没经沙箱(allow-all、审批通过的全权限、Windows)时返回 undefined:
|
|
309
310
|
* 没有沙箱就没什么可提示的。
|
|
310
311
|
*/
|
|
311
|
-
export function describeSandbox(
|
|
312
|
+
export function describeSandbox(
|
|
313
|
+
resolved: ResolvedBwrap,
|
|
314
|
+
unsandboxed: boolean,
|
|
315
|
+
fixed = false,
|
|
316
|
+
): string | undefined {
|
|
312
317
|
if (unsandboxed) return undefined;
|
|
313
318
|
return [
|
|
314
319
|
"<system-reminder>",
|
|
315
320
|
`This command ran in a sandbox: ${describeLimits(resolved)}.`,
|
|
316
|
-
|
|
321
|
+
// 固定沙箱的提权请求会被直接拒绝,再提示 escape hatch 只会诱导无效请求
|
|
322
|
+
...(fixed ? [] : [SANDBOX_ESCAPE_HATCH]),
|
|
317
323
|
"</system-reminder>",
|
|
318
324
|
].join("\n");
|
|
319
325
|
}
|
|
@@ -347,9 +353,16 @@ export class BwrapRuntime {
|
|
|
347
353
|
* 独立入口各持一份、经 pi.events 同步。
|
|
348
354
|
*/
|
|
349
355
|
private readonly policy: RequestPolicy;
|
|
356
|
+
/**
|
|
357
|
+
* 创建时传入的完整配置:加载配置与创建解耦——有它就不再读盘,且沙箱视为固定
|
|
358
|
+
* (不注册 /bwrap-* 命令、非沙盒请求一律拒绝)。undefined 表示从全局/项目
|
|
359
|
+
* bwrap.json 加载(主会话入口的现状行为)。
|
|
360
|
+
*/
|
|
361
|
+
private readonly config: BwrapConfig | undefined;
|
|
350
362
|
|
|
351
|
-
constructor(policy: RequestPolicy) {
|
|
363
|
+
constructor(policy: RequestPolicy, config?: BwrapConfig) {
|
|
352
364
|
this.policy = policy;
|
|
365
|
+
this.config = config;
|
|
353
366
|
}
|
|
354
367
|
|
|
355
368
|
setup(pi: ExtensionAPI): void {
|
|
@@ -394,11 +407,18 @@ export class BwrapRuntime {
|
|
|
394
407
|
pi.on("before_agent_start", (event, ctx) => {
|
|
395
408
|
const runtime = this.resolve(ctx);
|
|
396
409
|
const isWindows = process.platform === "win32";
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
410
|
+
let modeText: string;
|
|
411
|
+
if (ctx.hasUI) {
|
|
412
|
+
if (isWindows) {
|
|
413
|
+
modeText = "Every bash command requires user approval before it runs.";
|
|
414
|
+
} else if (this.config === undefined) {
|
|
415
|
+
modeText = `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`;
|
|
416
|
+
} else {
|
|
417
|
+
modeText = `Current bwrap mode: **${runtime.mode}**, fixed by the agent's declared sandbox config. Unsandboxed execution requests are refused without approval.`;
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
modeText = `Current bwrap mode: **${runtime.mode}**. This headless session cannot ask for approval: commands that require user approval are denied.`;
|
|
421
|
+
}
|
|
402
422
|
const unavailableText =
|
|
403
423
|
!isWindows && this.bwrapUnavailable
|
|
404
424
|
? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
|
|
@@ -414,11 +434,16 @@ export class BwrapRuntime {
|
|
|
414
434
|
};
|
|
415
435
|
});
|
|
416
436
|
|
|
417
|
-
|
|
437
|
+
// 固定沙箱(创建时显式传入配置)不注册 /bwrap-* 命令:切模式与恢复提权审批
|
|
438
|
+
// 都可能放宽声明的沙箱
|
|
439
|
+
if (this.config === undefined) this.registerCommands(pi);
|
|
418
440
|
}
|
|
419
441
|
|
|
420
442
|
setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
|
|
421
|
-
this.resolved =
|
|
443
|
+
this.resolved =
|
|
444
|
+
this.config === undefined
|
|
445
|
+
? loadSandboxConfig({ workspace: cwd, mode })
|
|
446
|
+
: resolveBwrap({ ...this.config, mode });
|
|
422
447
|
return this.resolved;
|
|
423
448
|
}
|
|
424
449
|
|
|
@@ -453,9 +478,13 @@ export class BwrapRuntime {
|
|
|
453
478
|
// 用户在审批框选「Run this in sandbox」拒绝提权:命令降级为沙盒内执行
|
|
454
479
|
let userChoseSandbox = false;
|
|
455
480
|
if (needsApproval && runtime.bwrapEnabled) {
|
|
456
|
-
// /bwrap-deny-request
|
|
457
|
-
//
|
|
458
|
-
|
|
481
|
+
// /bwrap-deny-request 与固定沙箱(创建时显式传入配置):非沙盒请求直接
|
|
482
|
+
// 拒绝——审批规则与审批框都不再参与,拒绝文案与用户点 Deny 相同
|
|
483
|
+
// (deny-request 可用 /bwrap-allow-request 恢复审批,固定沙箱不可恢复)。
|
|
484
|
+
if (
|
|
485
|
+
request.requestFullAccess === true &&
|
|
486
|
+
(this.config !== undefined || this.policy.deniesRequests())
|
|
487
|
+
) {
|
|
459
488
|
throw new Error(UNSANDBOXED_DENIED);
|
|
460
489
|
}
|
|
461
490
|
// 先按 approvalRules 自动判定:allow 直接放行,deny 直接拒绝,未命中才弹框
|
|
@@ -484,7 +513,9 @@ export class BwrapRuntime {
|
|
|
484
513
|
}
|
|
485
514
|
// 命令失败时附带的沙箱状态(写边界 + 网络层级),由上层拼进错误文本。
|
|
486
515
|
// 用户选「Run this in sandbox」时不附:沙箱作用域改由 sandboxReminder 说明
|
|
487
|
-
const sandboxHint = userChoseSandbox
|
|
516
|
+
const sandboxHint = userChoseSandbox
|
|
517
|
+
? undefined
|
|
518
|
+
: describeSandbox(runtime, local, this.config !== undefined);
|
|
488
519
|
// 提权被拒、命令在沙盒内执行的系统提醒:成功与失败都由上层附上
|
|
489
520
|
const sandboxReminder = userChoseSandbox ? describeSandboxChoice(runtime) : undefined;
|
|
490
521
|
// 计时起点放在审批之后:审批弹窗的等待时长属于用户 UI 操作,不是命令运行时间
|
|
@@ -592,8 +623,7 @@ export class BwrapRuntime {
|
|
|
592
623
|
}
|
|
593
624
|
|
|
594
625
|
private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
|
|
595
|
-
|
|
596
|
-
if (!this.resolved) this.resolved = resolveBwrap(config);
|
|
626
|
+
if (!this.resolved) this.resolved = resolveBwrap(this.config ?? loadBwrapConfig(ctx.cwd));
|
|
597
627
|
return this.resolved;
|
|
598
628
|
}
|
|
599
629
|
|
|
@@ -960,7 +990,15 @@ export class BwrapRuntime {
|
|
|
960
990
|
}
|
|
961
991
|
}
|
|
962
992
|
|
|
963
|
-
/**
|
|
964
|
-
|
|
965
|
-
|
|
993
|
+
/**
|
|
994
|
+
* 缺省自建一份不跨入口同步的策略;入口通常显式传入 `createRequestPolicy(pi.events)`。
|
|
995
|
+
* config 为完整配置(completeBwrapConfig 的产物):加载配置与创建解耦,传入后不再
|
|
996
|
+
* 读盘,且沙箱固定(不注册 /bwrap-* 命令、非沙盒请求直接拒绝)。缺省从全局/项目
|
|
997
|
+
* bwrap.json 加载。
|
|
998
|
+
*/
|
|
999
|
+
export function createBwrapRuntime(
|
|
1000
|
+
policy: RequestPolicy = createRequestPolicy(),
|
|
1001
|
+
config?: BwrapConfig,
|
|
1002
|
+
): BwrapRuntime {
|
|
1003
|
+
return new BwrapRuntime(policy, config);
|
|
966
1004
|
}
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
* provider: openai # optional; overrides the global default
|
|
17
17
|
* model: claude-haiku-4-5 # optional; overrides the global default
|
|
18
18
|
* thinkingLevel: high # optional; overrides the global default
|
|
19
|
+
* sandbox: # optional; fixed bwrap sandbox for the bash tool
|
|
20
|
+
* mode: readonly
|
|
21
|
+
* extraWritablePaths:
|
|
22
|
+
* - /tmp
|
|
19
23
|
* ---
|
|
20
24
|
* System prompt for the agent goes here.
|
|
21
25
|
*
|
|
@@ -24,6 +28,16 @@
|
|
|
24
28
|
* omitted, the subagent runs with the read-only default toolset from the
|
|
25
29
|
* spawn-agent config (read/grep/find/ls) unless overridden there.
|
|
26
30
|
*
|
|
31
|
+
* `sandbox` is a complete bwrap config (same shape as bwrap.json) that becomes
|
|
32
|
+
* the bash tool's fixed sandbox: it is parsed here and passed to the subagent's
|
|
33
|
+
* bwrap runtime directly (config loading is decoupled from sandbox creation),
|
|
34
|
+
* so the user's bwrap.json is not consulted for that agent. Unsandboxed
|
|
35
|
+
* execution requests are refused and /bwrap-* mode commands are not registered.
|
|
36
|
+
* Fields are optional and fall back to the standard bwrap defaults (e.g.
|
|
37
|
+
* `sandbox: {mode: readonly}` alone is enough for a read-only shell);
|
|
38
|
+
* `extraWritablePaths` is the writable escape hatch under a read-only mode.
|
|
39
|
+
* Without `sandbox`, the subagent's bash follows the user bwrap config.
|
|
40
|
+
*
|
|
27
41
|
* Global defaults for provider/model/thinkingLevel come from
|
|
28
42
|
* `~/.pi/agent/spawn-agent.json` (see loadSpawnAgentConfig); frontmatter
|
|
29
43
|
* fields take precedence over them, which in turn take precedence over the
|
|
@@ -38,6 +52,8 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
|
38
52
|
import { type Static, Type } from "typebox";
|
|
39
53
|
import { Value } from "typebox/value";
|
|
40
54
|
|
|
55
|
+
import { type BwrapConfig, bwrapConfigFileSchema, completeBwrapConfig } from "./bwrap/core.js";
|
|
56
|
+
|
|
41
57
|
/**
|
|
42
58
|
* Valid thinking levels, mirroring pi's CLI --thinking validation
|
|
43
59
|
* (VALID_THINKING_LEVELS). "off" disables thinking; "max" is deliberately
|
|
@@ -52,6 +68,7 @@ const agentFrontmatterSchema = Type.Object({
|
|
|
52
68
|
provider: Type.Optional(Type.String()),
|
|
53
69
|
model: Type.Optional(Type.String()),
|
|
54
70
|
thinkingLevel: Type.Optional(Type.Union(THINKING_LEVELS.map((level) => Type.Literal(level)))),
|
|
71
|
+
sandbox: Type.Optional(bwrapConfigFileSchema),
|
|
55
72
|
});
|
|
56
73
|
|
|
57
74
|
function parseAgentFrontmatter(frontmatter: unknown) {
|
|
@@ -71,6 +88,8 @@ export interface AgentConfig {
|
|
|
71
88
|
model?: string;
|
|
72
89
|
/** Thinking level, applied via --thinking. */
|
|
73
90
|
thinkingLevel?: (typeof THINKING_LEVELS)[number];
|
|
91
|
+
/** bash 的完整沙箱配置(bwrap.json 同构);undefined 表示跟随用户 bwrap 配置。 */
|
|
92
|
+
sandbox?: BwrapConfig;
|
|
74
93
|
systemPrompt: string;
|
|
75
94
|
filePath: string;
|
|
76
95
|
}
|
|
@@ -118,6 +137,7 @@ export function discoverAgents(dir = join(getAgentDir(), "agents")): AgentConfig
|
|
|
118
137
|
provider: fm.provider,
|
|
119
138
|
model: fm.model,
|
|
120
139
|
thinkingLevel: fm.thinkingLevel,
|
|
140
|
+
sandbox: fm.sandbox ? completeBwrapConfig(fm.sandbox) : undefined,
|
|
121
141
|
systemPrompt: body,
|
|
122
142
|
filePath,
|
|
123
143
|
});
|
package/src/spawn-agent.ts
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Security default: without an explicit `tools:` in the frontmatter, the
|
|
28
28
|
* subagent only gets read-only tools (read/grep/find/ls) — no bash/write/edit.
|
|
29
|
+
* A frontmatter `sandbox:` gives the bash tool a fixed bwrap sandbox config
|
|
30
|
+
* (bwrap.json shape, e.g. mode readonly): it is passed to the subagent's bwrap
|
|
31
|
+
* runtime as the complete config, unsandboxed execution requests are refused,
|
|
32
|
+
* and /bwrap-* mode commands are not registered (see spawn-agent-agents.ts).
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
import { existsSync } from "node:fs";
|
|
@@ -42,16 +46,20 @@ import {
|
|
|
42
46
|
type ExtensionAPI,
|
|
43
47
|
type ExtensionUIContext,
|
|
44
48
|
getAgentDir,
|
|
49
|
+
type InlineExtension,
|
|
45
50
|
ModelRuntime,
|
|
46
51
|
type PromptOptions,
|
|
47
52
|
SessionManager,
|
|
48
53
|
SettingsManager,
|
|
49
54
|
truncateTail,
|
|
50
|
-
truncateToVisualLines,
|
|
51
55
|
} from "@earendil-works/pi-coding-agent";
|
|
52
56
|
import { Type } from "typebox";
|
|
53
57
|
|
|
58
|
+
import { type BwrapRuntime, createBwrapRuntime } from "./bwrap/runtime.js";
|
|
59
|
+
import { registerShellTools } from "./claude-code/shell.js";
|
|
54
60
|
import { type ToolPendant } from "./lib/pendant.js";
|
|
61
|
+
import { createRequestPolicy } from "./lib/request-policy.js";
|
|
62
|
+
import opencodeBash from "./opencode/bash.js";
|
|
55
63
|
import {
|
|
56
64
|
type AgentConfig,
|
|
57
65
|
applyAgentDefaults,
|
|
@@ -87,10 +95,11 @@ const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
|
87
95
|
* `additionalExtensionPaths` so the subagent uses the enhanced implementation
|
|
88
96
|
* instead of the built-in one.
|
|
89
97
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
98
|
+
* `bash` (opencode) and `Bash` (claude-code) are deliberately not in this map:
|
|
99
|
+
* they are injected via an inline extension factory (subagentShellExtension)
|
|
100
|
+
* instead, because their bwrap runtime carries the agent's declared sandbox
|
|
101
|
+
* config — a path-based override has no channel to pass per-agent config
|
|
102
|
+
* (see createSubagentSession).
|
|
94
103
|
* (Workspace write protection is embedded in the opencode write/edit tools.)
|
|
95
104
|
*
|
|
96
105
|
* Claude Code style tools (capitalized names) map to their claude-code
|
|
@@ -111,7 +120,6 @@ const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
|
|
|
111
120
|
read: "opencode/files.ts",
|
|
112
121
|
edit: "opencode/files.ts",
|
|
113
122
|
write: "opencode/files.ts",
|
|
114
|
-
bash: "opencode/bash.ts",
|
|
115
123
|
grep: "opencode/grep.ts",
|
|
116
124
|
glob: "opencode/glob.ts",
|
|
117
125
|
Grep: "claude-code/grep.ts",
|
|
@@ -139,7 +147,7 @@ interface UsageStats {
|
|
|
139
147
|
turns: number;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
|
-
/** 子 agent 的一次运行结果:runAgent
|
|
150
|
+
/** 子 agent 的一次运行结果:runAgent 的返回值。 */
|
|
143
151
|
interface SubagentResult {
|
|
144
152
|
agent: string;
|
|
145
153
|
task: string;
|
|
@@ -153,8 +161,6 @@ interface SubagentResult {
|
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
interface SubagentDetails {
|
|
156
|
-
/** 子 agent 运行结果(除 pendant 外的字段都收在这里)。 */
|
|
157
|
-
result: SubagentResult;
|
|
158
164
|
/** 折叠 markdown 面板:父 agent 的 prompt 与父 agent 看到的子 agent 结果。 */
|
|
159
165
|
pendant?: ToolPendant;
|
|
160
166
|
}
|
|
@@ -271,6 +277,20 @@ export function overrideExtensionPaths(tools: string[]): string[] {
|
|
|
271
277
|
return paths;
|
|
272
278
|
}
|
|
273
279
|
|
|
280
|
+
/**
|
|
281
|
+
* bash 类工具(opencode `bash` / cc `Bash`)的内联扩展工厂:bwrap runtime 随闭包
|
|
282
|
+
* 携带该 agent 的完整沙箱配置(frontmatter `sandbox`,已在 discoverAgents 补全成
|
|
283
|
+
* BwrapConfig)。路径式 override(additionalExtensionPaths)没有 per-agent 配置
|
|
284
|
+
* 通道,extensionFactories 的工厂闭包是 SDK 提供的唯一注入点。agent.sandbox 为
|
|
285
|
+
* undefined 时与各注册函数的默认构造完全一致(跟随用户 bwrap 配置)。
|
|
286
|
+
*/
|
|
287
|
+
export function subagentShellExtension(
|
|
288
|
+
agent: AgentConfig,
|
|
289
|
+
register: (pi: ExtensionAPI, runtime: BwrapRuntime) => void,
|
|
290
|
+
): InlineExtension {
|
|
291
|
+
return (pi) => register(pi, createBwrapRuntime(createRequestPolicy(pi.events), agent.sandbox));
|
|
292
|
+
}
|
|
293
|
+
|
|
274
294
|
/**
|
|
275
295
|
* Resolve the frontmatter model to a runtime Model. A "provider/model" string
|
|
276
296
|
* carries its own provider; a bare model id uses the declared provider, then
|
|
@@ -328,13 +348,18 @@ export async function createSubagentSession(
|
|
|
328
348
|
cwd: string,
|
|
329
349
|
parentUI: ExtensionUIContext | undefined,
|
|
330
350
|
): Promise<AgentSession> {
|
|
351
|
+
const tools = agent.tools ?? DEFAULT_TOOLS;
|
|
331
352
|
const settingsManager = SettingsManager.create(cwd, getAgentDir());
|
|
332
353
|
const loader = new DefaultResourceLoader({
|
|
333
354
|
cwd,
|
|
334
355
|
agentDir: getAgentDir(),
|
|
335
356
|
settingsManager,
|
|
336
357
|
noExtensions: true,
|
|
337
|
-
additionalExtensionPaths: overrideExtensionPaths(
|
|
358
|
+
additionalExtensionPaths: overrideExtensionPaths(tools),
|
|
359
|
+
extensionFactories: [
|
|
360
|
+
...(tools.includes("bash") ? [subagentShellExtension(agent, opencodeBash)] : []),
|
|
361
|
+
...(tools.includes("Bash") ? [subagentShellExtension(agent, registerShellTools)] : []),
|
|
362
|
+
],
|
|
338
363
|
appendSystemPrompt: agent.systemPrompt ? [agent.systemPrompt] : undefined,
|
|
339
364
|
});
|
|
340
365
|
await loader.reload();
|
|
@@ -344,7 +369,7 @@ export async function createSubagentSession(
|
|
|
344
369
|
cwd,
|
|
345
370
|
model: resolveModel(modelRuntime, agent, settingsManager),
|
|
346
371
|
thinkingLevel: agent.thinkingLevel,
|
|
347
|
-
tools
|
|
372
|
+
tools,
|
|
348
373
|
sessionManager: SessionManager.inMemory(cwd),
|
|
349
374
|
settingsManager,
|
|
350
375
|
resourceLoader: loader,
|
|
@@ -437,7 +462,7 @@ export async function runAgent(
|
|
|
437
462
|
const footer = usageLine ? `\`${name}\` ${usageLine}` : `\`${name}\``;
|
|
438
463
|
onUpdate?.({
|
|
439
464
|
content: [{ type: "text", text: [...logLines, footer].join("\n") }],
|
|
440
|
-
details: {
|
|
465
|
+
details: {},
|
|
441
466
|
});
|
|
442
467
|
};
|
|
443
468
|
|
|
@@ -563,20 +588,7 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
563
588
|
text: `Unknown agent "${params.agent}". Available agents: ${formatAgentList(agents)}`,
|
|
564
589
|
},
|
|
565
590
|
],
|
|
566
|
-
details: {
|
|
567
|
-
result: {
|
|
568
|
-
agent: params.agent,
|
|
569
|
-
task: params.task,
|
|
570
|
-
exitCode: 1,
|
|
571
|
-
messages: [],
|
|
572
|
-
stderr: "",
|
|
573
|
-
usage: {
|
|
574
|
-
cost: 0,
|
|
575
|
-
contextTokens: 0,
|
|
576
|
-
turns: 0,
|
|
577
|
-
},
|
|
578
|
-
},
|
|
579
|
-
},
|
|
591
|
+
details: {},
|
|
580
592
|
isError: true,
|
|
581
593
|
};
|
|
582
594
|
}
|
|
@@ -599,7 +611,6 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
599
611
|
{ type: "text", text: `Subagent "${result.agent}" failed (${reason}):\n${message}` },
|
|
600
612
|
],
|
|
601
613
|
details: {
|
|
602
|
-
result,
|
|
603
614
|
pendant: {
|
|
604
615
|
subtitle: result.agent,
|
|
605
616
|
markdown: formatPendantMarkdown(params.task, message),
|
|
@@ -612,12 +623,11 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
612
623
|
const output = getFinalOutput(result.messages) || "(no output)";
|
|
613
624
|
const truncation = truncateTail(output, { maxBytes: MAX_OUTPUT_BYTES });
|
|
614
625
|
const text = truncation.truncated
|
|
615
|
-
? `${truncation.content}\n\n[Output truncated to ${formatTokens(truncation.content.length)} bytes.
|
|
626
|
+
? `${truncation.content}\n\n[Output truncated to ${formatTokens(truncation.content.length)} bytes.]`
|
|
616
627
|
: output;
|
|
617
628
|
return {
|
|
618
629
|
content: [{ type: "text", text }],
|
|
619
630
|
details: {
|
|
620
|
-
result,
|
|
621
631
|
pendant: {
|
|
622
632
|
subtitle: result.agent,
|
|
623
633
|
markdown: formatPendantMarkdown(params.task, text),
|
|
@@ -625,52 +635,5 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
625
635
|
},
|
|
626
636
|
};
|
|
627
637
|
},
|
|
628
|
-
|
|
629
|
-
renderCall(args, theme) {
|
|
630
|
-
const preview = args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task;
|
|
631
|
-
const line =
|
|
632
|
-
theme.fg("toolTitle", theme.bold("spawn_agent ")) + theme.fg("accent", args.agent);
|
|
633
|
-
const detail = ` ${theme.fg("dim", preview)}`;
|
|
634
|
-
return {
|
|
635
|
-
render: (width: number) =>
|
|
636
|
-
truncateToVisualLines(`${line}\n${detail}`, 2, width).visualLines,
|
|
637
|
-
invalidate: (): void => undefined,
|
|
638
|
-
};
|
|
639
|
-
},
|
|
640
|
-
|
|
641
|
-
renderResult(result, { expanded }, theme) {
|
|
642
|
-
const details = result.details.result;
|
|
643
|
-
const isError =
|
|
644
|
-
details.exitCode !== 0 ||
|
|
645
|
-
details.stopReason === "error" ||
|
|
646
|
-
details.stopReason === "aborted";
|
|
647
|
-
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
648
|
-
const finalOutput = getFinalOutput(details.messages);
|
|
649
|
-
const usageStr = formatUsageStats(details.usage, details.model);
|
|
650
|
-
|
|
651
|
-
const lines: string[] = [];
|
|
652
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(details.agent))}`;
|
|
653
|
-
if (details.stopReason) header += ` ${theme.fg("error", `[${details.stopReason}]`)}`;
|
|
654
|
-
lines.push(header);
|
|
655
|
-
if (isError && details.errorMessage) {
|
|
656
|
-
lines.push(theme.fg("error", `Error: ${details.errorMessage}`));
|
|
657
|
-
}
|
|
658
|
-
if (expanded) {
|
|
659
|
-
lines.push("", theme.fg("muted", "─── Task ───"), theme.fg("dim", details.task));
|
|
660
|
-
if (finalOutput) {
|
|
661
|
-
lines.push("", theme.fg("muted", "─── Output ───"), finalOutput.trim());
|
|
662
|
-
}
|
|
663
|
-
} else if (finalOutput) {
|
|
664
|
-
lines.push(theme.fg("toolOutput", finalOutput.split("\n").slice(0, 5).join("\n")));
|
|
665
|
-
} else {
|
|
666
|
-
lines.push(theme.fg("muted", "(no output)"));
|
|
667
|
-
}
|
|
668
|
-
if (usageStr) lines.push(theme.fg("dim", usageStr));
|
|
669
|
-
return {
|
|
670
|
-
render: (width: number) =>
|
|
671
|
-
truncateToVisualLines(lines.join("\n"), Infinity, width).visualLines,
|
|
672
|
-
invalidate: (): void => undefined,
|
|
673
|
-
};
|
|
674
|
-
},
|
|
675
638
|
});
|
|
676
639
|
}
|