@maplezzk/pi-interactive-subagents 3.13.1 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +3 -1
- package/SKILL.md +2 -0
- package/locales/index.json +64 -0
- package/package.json +7 -7
- package/pi-extension/subagents/index.ts +423 -16
- package/pi-extension/subagents/mux-config.ts +10 -0
package/README.md
CHANGED
|
@@ -173,6 +173,8 @@ The persisted Herdr mode, child-spawning policy, and explicit child-extension li
|
|
|
173
173
|
|
|
174
174
|
`herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field. `allowSubagentSpawning` is a global switch for whether child subagents may create or manage other subagents; it defaults to `false`. Set it to `true` to enable the lifecycle tools in child sessions when the corresponding extension is selected. `subagentExtensions` is an optional list of extension paths to load explicitly in child sessions; automatic project/global extension discovery is disabled. Paths may be absolute, start with `~/`, or be relative to `PI_CODING_AGENT_DIR`. `subagent-done.ts` is always loaded. Explicit `deny-tools` restrictions still apply.
|
|
175
175
|
|
|
176
|
+
You can configure `subagentExtensions` from the input slash-command menu instead of editing JSON: `/config:subagent extensions` discovers project and global Pi extension entrypoints and opens a toggle-style chooser. The built-in `subagent-done.ts` entry is always shown as required and cannot be disabled. Uninstalled extensions are shown as missing and are skipped; saving the chooser removes their stale paths. Choose `Enter extension paths manually` for custom paths. Direct forms remain available: `/config:subagent extensions path-a.ts,path-b.ts` saves paths, and `/config:subagent extensions clear` removes all optional entries. The dedicated `/config:subagent-extensions` command (also `/subagent-extensions`) supports the same behavior. Changes apply to the next child session.
|
|
177
|
+
|
|
176
178
|
`config.json` is gitignored so local overrides don't get committed.
|
|
177
179
|
|
|
178
180
|
---
|
package/README.zh-CN.md
CHANGED
|
@@ -62,7 +62,9 @@ zellij --session pi # 然后运行 pi
|
|
|
62
62
|
}
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
`herdrMode` 支持 `split
|
|
65
|
+
`herdrMode` 支持 `split`(默认,兼容原有分屏布局)和 `tab`(每个 subagent 独立后台 Tab)。`allowSubagentSpawning` 是全局开关,控制子 agent 是否可以创建或管理其他 subagent,默认值为 `false`;设置为 `true` 后,子 agent 才会获得这些生命周期工具。`subagentExtensions` 是可选扩展路径列表;子 agent 不再自动发现项目级和全局扩展,只加载列表中的扩展,`subagent-done.ts` 始终加载。路径可以是绝对路径、`~/` 路径,或相对于 `PI_CODING_AGENT_DIR` 的路径。`/config:subagent herdr split|tab` 会更新 Herdr 字段。
|
|
66
|
+
|
|
67
|
+
现在也可以直接在输入框使用斜杠命令配置扩展:`/config:subagent extensions` 会自动发现项目级和全局 Pi 扩展,并打开可切换的候选列表。内置的 `subagent-done.ts` 会显示为必选项,始终加载且不能取消。已卸载的插件会显示为缺失并被跳过;保存候选列表时会清理这些残留路径。也可以选择“手动输入扩展路径”配置自定义入口。直接配置仍然支持 `/config:subagent extensions path-a.ts,path-b.ts`,清空可使用 `/config:subagent extensions clear`。另提供 `/config:subagent-extensions`(别名 `/subagent-extensions`),用法相同。修改会在下一个子会话启动时生效。
|
|
66
68
|
|
|
67
69
|
## 致谢
|
|
68
70
|
|
package/SKILL.md
CHANGED
|
@@ -11,6 +11,8 @@ description: "配置与排查 interactive subagents 的终端复用器、Herdr s
|
|
|
11
11
|
|
|
12
12
|
环境变量优先级:`PI_TERMINAL_MUX` > `PI_SUBAGENT_MUX` > 持久化 `mux` > `auto`;`PI_SUBAGENT_HERDR_MODE` > 持久化 `herdrMode` > `split`。
|
|
13
13
|
|
|
14
|
+
`/config:subagent extensions` 或 `/config:subagent-extensions` 可直接配置 `subagentExtensions`:命令会发现项目级和全局扩展并提供候选列表,`subagent-done.ts` 始终加载且不能移除;已卸载的扩展会被检测并跳过。也可以手动输入路径,命令参数用逗号分隔,输入 `clear` 清空可选扩展。
|
|
15
|
+
|
|
14
16
|
## 修改
|
|
15
17
|
|
|
16
18
|
优先运行:
|
package/locales/index.json
CHANGED
|
@@ -46,5 +46,69 @@
|
|
|
46
46
|
"muxInteractiveOnly": {
|
|
47
47
|
"zh-CN": "请在支持 UI 的 Pi 会话中运行 /config:subagent。",
|
|
48
48
|
"en-US": "Run /config:subagent in a Pi session with UI support."
|
|
49
|
+
},
|
|
50
|
+
"extensionCommandDescription": {
|
|
51
|
+
"zh-CN": "配置子 agent 显式加载的扩展",
|
|
52
|
+
"en-US": "Configure extensions explicitly loaded by child subagents"
|
|
53
|
+
},
|
|
54
|
+
"extensionConfigTitle": {
|
|
55
|
+
"zh-CN": "配置子 agent 扩展(每行一个路径;命令参数用逗号分隔)",
|
|
56
|
+
"en-US": "Configure subagent extensions (one path per line; comma-separated in commands)"
|
|
57
|
+
},
|
|
58
|
+
"extensionSaved": {
|
|
59
|
+
"zh-CN": "子 agent 扩展已设置为:{value}",
|
|
60
|
+
"en-US": "Subagent extensions set to: {value}"
|
|
61
|
+
},
|
|
62
|
+
"extensionInvalid": {
|
|
63
|
+
"zh-CN": "扩展配置无效:{value}。编辑器中每行输入一个路径;命令参数请使用逗号分隔,或输入 clear 清空。",
|
|
64
|
+
"en-US": "Invalid extension configuration: {value}. Use one path per line in the editor, comma-separated paths in commands, or clear to remove them."
|
|
65
|
+
},
|
|
66
|
+
"extensionInteractiveOnly": {
|
|
67
|
+
"zh-CN": "请在支持 UI 的 Pi 会话中配置子 agent 扩展。",
|
|
68
|
+
"en-US": "Configure subagent extensions in a Pi session with UI support."
|
|
69
|
+
},
|
|
70
|
+
"extensionNone": {
|
|
71
|
+
"zh-CN": "未配置",
|
|
72
|
+
"en-US": "none"
|
|
73
|
+
},
|
|
74
|
+
"extensionClearDescription": {
|
|
75
|
+
"zh-CN": "清空子 agent 扩展列表",
|
|
76
|
+
"en-US": "Clear the child-session extension list"
|
|
77
|
+
},
|
|
78
|
+
"extensionConfiguredDescription": {
|
|
79
|
+
"zh-CN": "当前配置的扩展路径",
|
|
80
|
+
"en-US": "Currently configured extension path"
|
|
81
|
+
},
|
|
82
|
+
"extensionChooserTitle": {
|
|
83
|
+
"zh-CN": "选择要加载到 subagent 的扩展",
|
|
84
|
+
"en-US": "Choose extensions to load in subagents"
|
|
85
|
+
},
|
|
86
|
+
"extensionChooserDone": {
|
|
87
|
+
"zh-CN": "完成并保存",
|
|
88
|
+
"en-US": "Done and save"
|
|
89
|
+
},
|
|
90
|
+
"extensionChooserManual": {
|
|
91
|
+
"zh-CN": "手动输入扩展路径",
|
|
92
|
+
"en-US": "Enter extension paths manually"
|
|
93
|
+
},
|
|
94
|
+
"extensionCandidateProject": {
|
|
95
|
+
"zh-CN": "项目扩展",
|
|
96
|
+
"en-US": "project extension"
|
|
97
|
+
},
|
|
98
|
+
"extensionCandidateGlobal": {
|
|
99
|
+
"zh-CN": "全局扩展",
|
|
100
|
+
"en-US": "global extension"
|
|
101
|
+
},
|
|
102
|
+
"extensionCandidateConfigured": {
|
|
103
|
+
"zh-CN": "已配置扩展",
|
|
104
|
+
"en-US": "configured extension"
|
|
105
|
+
},
|
|
106
|
+
"extensionCandidateRequired": {
|
|
107
|
+
"zh-CN": "始终加载,不可移除",
|
|
108
|
+
"en-US": "always loaded and cannot be removed"
|
|
109
|
+
},
|
|
110
|
+
"extensionCandidateMissing": {
|
|
111
|
+
"zh-CN": "插件未安装,已跳过(完成保存后会移除)",
|
|
112
|
+
"en-US": "plugin is not installed and will be skipped (removed when saved)"
|
|
49
113
|
}
|
|
50
114
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maplezzk/pi-interactive-subagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.0",
|
|
4
4
|
"description": "Interactive async subagents for pi — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. Fork of HazAT/pi-interactive-subagents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.ts",
|
|
@@ -62,17 +62,17 @@
|
|
|
62
62
|
],
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"ajv": "^8.20.0",
|
|
65
|
-
"pi-extensions-i18n": "^0.
|
|
66
|
-
"pi-terminal-mux": "^0.
|
|
65
|
+
"pi-extensions-i18n": "^0.5.0",
|
|
66
|
+
"pi-terminal-mux": "^0.6.2"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
|
-
"@earendil-works/pi-coding-agent": ">=0.80.0
|
|
70
|
-
"@earendil-works/pi-tui": ">=0.80.0
|
|
69
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0",
|
|
70
|
+
"@earendil-works/pi-tui": ">=0.80.0",
|
|
71
71
|
"@sinclair/typebox": "*"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
75
|
-
"@earendil-works/pi-tui": "0.
|
|
74
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
75
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
76
76
|
"@sinclair/typebox": "^0.34.49",
|
|
77
77
|
"@types/node": "24.12.4",
|
|
78
78
|
"tsx": "4.23.1",
|
|
@@ -2,12 +2,13 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type, type Static } from "@sinclair/typebox";
|
|
4
4
|
import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { dirname, isAbsolute, join } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { createTranslator, loadCatalog } from "pi-extensions-i18n";
|
|
7
|
+
import { createTranslator, loadCatalog, notifyWithSource, type NoticeColor, type NoticeSource } from "pi-extensions-i18n";
|
|
8
8
|
import {
|
|
9
9
|
readdirSync,
|
|
10
10
|
readFileSync,
|
|
11
|
+
type Dirent,
|
|
11
12
|
writeFileSync,
|
|
12
13
|
existsSync,
|
|
13
14
|
mkdirSync,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
loadSubagentSpawningConfig,
|
|
52
53
|
saveHerdrMode,
|
|
53
54
|
saveMuxPreference,
|
|
55
|
+
saveSubagentExtensions,
|
|
54
56
|
SUBAGENT_MUX_BACKENDS,
|
|
55
57
|
type HerdrSurfaceMode,
|
|
56
58
|
type SubagentMuxPreference,
|
|
@@ -85,6 +87,21 @@ import {
|
|
|
85
87
|
|
|
86
88
|
const i18n = createTranslator(loadCatalog(new URL("../../locales/index.json", import.meta.url)));
|
|
87
89
|
|
|
90
|
+
/** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
|
|
91
|
+
const NOTICE_TAG = "subagents";
|
|
92
|
+
/** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
|
|
93
|
+
const NOTICE_COLOR: NoticeColor = "toolTitle";
|
|
94
|
+
/** 本扩展的提示来源。 */
|
|
95
|
+
const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 统一的用户可见提示出口:加来源标签后交给 Pi 的 notify。
|
|
99
|
+
* 包内所有 notify 都走这里,避免遗漏标签或颜色。
|
|
100
|
+
*/
|
|
101
|
+
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error"): void {
|
|
102
|
+
notifyWithSource({ ctx, source: NOTICE_SOURCE, level, message });
|
|
103
|
+
}
|
|
104
|
+
|
|
88
105
|
/** Absolute path to `pi-extension/subagents`. https://github.com/nodejs/node/issues/37845 */
|
|
89
106
|
const SUBAGENTS_DIR = dirname(fileURLToPath(import.meta.url));
|
|
90
107
|
|
|
@@ -275,17 +292,167 @@ function getBundledAgentsDir(): string {
|
|
|
275
292
|
}
|
|
276
293
|
|
|
277
294
|
const HOME_PREFIX = "~/";
|
|
295
|
+
const PROJECT_EXTENSIONS_DIR = [".pi", "extensions"] as const;
|
|
296
|
+
const GLOBAL_EXTENSIONS_DIR = "extensions";
|
|
297
|
+
const EXTENSION_ENTRY_SUFFIXES = [".ts", ".js", ".mjs", ".cjs"] as const;
|
|
298
|
+
const EXTENSION_DECLARATION_SUFFIX = ".d.ts";
|
|
299
|
+
const EXTENSION_INDEX_FILE_NAMES = ["index.ts", "index.js", "index.mjs", "index.cjs"] as const;
|
|
300
|
+
|
|
301
|
+
const EXTENSION_SOURCE_PROJECT = "project" as const;
|
|
302
|
+
const EXTENSION_SOURCE_GLOBAL = "global" as const;
|
|
303
|
+
const EXTENSION_SOURCE_CONFIGURED = "configured" as const;
|
|
304
|
+
|
|
305
|
+
type SubagentExtensionCandidateSource =
|
|
306
|
+
| typeof EXTENSION_SOURCE_PROJECT
|
|
307
|
+
| typeof EXTENSION_SOURCE_GLOBAL
|
|
308
|
+
| typeof EXTENSION_SOURCE_CONFIGURED;
|
|
309
|
+
|
|
310
|
+
export interface SubagentExtensionCandidate {
|
|
311
|
+
path: string;
|
|
312
|
+
label: string;
|
|
313
|
+
source: SubagentExtensionCandidateSource;
|
|
314
|
+
missing?: boolean;
|
|
315
|
+
required?: boolean;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Narrow unknown manifest values to object records before reading fields. */
|
|
319
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
320
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Check whether a file can be loaded as an extension entrypoint, excluding declarations. */
|
|
324
|
+
function isExtensionEntry(fileName: string): boolean {
|
|
325
|
+
return EXTENSION_ENTRY_SUFFIXES.some((suffix) => fileName.endsWith(suffix)) && !fileName.endsWith(EXTENSION_DECLARATION_SUFFIX);
|
|
326
|
+
}
|
|
278
327
|
|
|
279
328
|
/** Resolve an extension path from the user config against the Pi agent directory. */
|
|
280
|
-
function resolveSubagentExtensionPath(extensionPath: string): string {
|
|
329
|
+
function resolveSubagentExtensionPath(extensionPath: string, agentDir = getAgentConfigDir()): string {
|
|
281
330
|
if (extensionPath === "~") return homedir();
|
|
282
331
|
if (extensionPath.startsWith(HOME_PREFIX)) return join(homedir(), extensionPath.slice(HOME_PREFIX.length));
|
|
283
|
-
return isAbsolute(extensionPath) ? extensionPath : join(
|
|
332
|
+
return isAbsolute(extensionPath) ? extensionPath : join(agentDir, extensionPath);
|
|
284
333
|
}
|
|
285
334
|
|
|
286
|
-
/**
|
|
335
|
+
/** Read the Pi extension manifest fields needed to discover an installed entrypoint. */
|
|
336
|
+
function readExtensionManifest(extensionDir: string): { name?: string; extensions: string[] } | null {
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(readFileSync(join(extensionDir, "package.json"), "utf8")) as unknown;
|
|
339
|
+
if (!isRecord(parsed)) return null;
|
|
340
|
+
|
|
341
|
+
const packageName = typeof parsed.name === "string" ? parsed.name : undefined;
|
|
342
|
+
const pi = isRecord(parsed.pi) ? parsed.pi : undefined;
|
|
343
|
+
const configured = pi?.extensions;
|
|
344
|
+
const extensions = Array.isArray(configured)
|
|
345
|
+
? configured.filter((entry): entry is string => typeof entry === "string" && entry.trim() !== "")
|
|
346
|
+
: typeof configured === "string" && configured.trim() !== ""
|
|
347
|
+
? [configured]
|
|
348
|
+
: [];
|
|
349
|
+
return { name: packageName, extensions };
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Discover extension entrypoints in one Pi extension root without throwing on stale installs. */
|
|
356
|
+
function discoverExtensionRoot(
|
|
357
|
+
root: string,
|
|
358
|
+
source: Exclude<SubagentExtensionCandidateSource, "configured">,
|
|
359
|
+
): SubagentExtensionCandidate[] {
|
|
360
|
+
if (!existsSync(root)) return [];
|
|
361
|
+
|
|
362
|
+
let entries: Dirent[];
|
|
363
|
+
try {
|
|
364
|
+
entries = readdirSync(root, { withFileTypes: true });
|
|
365
|
+
} catch {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const candidates: SubagentExtensionCandidate[] = [];
|
|
370
|
+
const seen = new Set<string>();
|
|
371
|
+
|
|
372
|
+
/** Add an existing extension candidate once, using its normalized path as the key. */
|
|
373
|
+
const add = (path: string, label: string) => {
|
|
374
|
+
const normalizedPath = resolve(path);
|
|
375
|
+
if (!existsSync(path) || seen.has(normalizedPath)) return;
|
|
376
|
+
seen.add(normalizedPath);
|
|
377
|
+
candidates.push({ path: normalizedPath, label, source });
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const entryPath = join(root, entry.name);
|
|
382
|
+
if (entry.isFile()) {
|
|
383
|
+
if (isExtensionEntry(entry.name)) add(entryPath, entry.name);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!entry.isDirectory()) continue;
|
|
387
|
+
|
|
388
|
+
const manifest = readExtensionManifest(entryPath);
|
|
389
|
+
const manifestEntries = manifest?.extensions ?? [];
|
|
390
|
+
if (manifestEntries.length > 0) {
|
|
391
|
+
for (const extension of manifestEntries) {
|
|
392
|
+
const extensionPath = isAbsolute(extension) ? extension : resolve(entryPath, extension);
|
|
393
|
+
add(extensionPath, manifest?.name ?? entry.name);
|
|
394
|
+
}
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const indexPath = EXTENSION_INDEX_FILE_NAMES
|
|
399
|
+
.map((fileName) => join(entryPath, fileName))
|
|
400
|
+
.find((path) => existsSync(path));
|
|
401
|
+
if (indexPath) {
|
|
402
|
+
add(indexPath, manifest?.name ?? entry.name);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
for (const child of readdirSync(entryPath, { withFileTypes: true })) {
|
|
408
|
+
if (child.isFile() && isExtensionEntry(child.name)) {
|
|
409
|
+
add(join(entryPath, child.name), `${manifest?.name ?? entry.name}/${child.name}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
} catch {
|
|
413
|
+
// An extension can disappear while the chooser is open; skip that entry.
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return candidates;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Discover project/global extension candidates and retain stale configured paths for cleanup. */
|
|
421
|
+
export function discoverSubagentExtensionCandidates(params: {
|
|
422
|
+
cwd?: string;
|
|
423
|
+
agentDir?: string;
|
|
424
|
+
configured?: readonly string[];
|
|
425
|
+
} = {}): SubagentExtensionCandidate[] {
|
|
426
|
+
const cwd = params.cwd ?? process.cwd();
|
|
427
|
+
const agentDir = params.agentDir ?? getAgentConfigDir();
|
|
428
|
+
const candidates = [
|
|
429
|
+
...discoverExtensionRoot(join(cwd, ...PROJECT_EXTENSIONS_DIR), EXTENSION_SOURCE_PROJECT),
|
|
430
|
+
...discoverExtensionRoot(join(agentDir, GLOBAL_EXTENSIONS_DIR), EXTENSION_SOURCE_GLOBAL),
|
|
431
|
+
];
|
|
432
|
+
const known = new Set(candidates.map((candidate) => resolve(candidate.path)));
|
|
433
|
+
const configured = params.configured ?? loadSubagentExtensionsConfig().extensions;
|
|
434
|
+
|
|
435
|
+
for (const configuredPath of configured) {
|
|
436
|
+
const resolvedPath = resolveSubagentExtensionPath(configuredPath, agentDir);
|
|
437
|
+
const key = resolve(resolvedPath);
|
|
438
|
+
if (known.has(key)) continue;
|
|
439
|
+
candidates.push({
|
|
440
|
+
path: key,
|
|
441
|
+
label: basename(key),
|
|
442
|
+
source: EXTENSION_SOURCE_CONFIGURED,
|
|
443
|
+
missing: !existsSync(key),
|
|
444
|
+
});
|
|
445
|
+
known.add(key);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return candidates;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Load and resolve explicitly configured child-session extension paths that still exist. */
|
|
287
452
|
function getConfiguredSubagentExtensions(): string[] {
|
|
288
|
-
return loadSubagentExtensionsConfig().extensions
|
|
453
|
+
return loadSubagentExtensionsConfig().extensions
|
|
454
|
+
.map((extensionPath) => resolveSubagentExtensionPath(extensionPath))
|
|
455
|
+
.filter((extensionPath) => existsSync(extensionPath));
|
|
289
456
|
}
|
|
290
457
|
|
|
291
458
|
/** Build the no-discovery flag and explicit extensions for a child Pi command. */
|
|
@@ -1037,7 +1204,227 @@ function parseMuxConfigRequest(requested: string): MuxConfigSelection | null {
|
|
|
1037
1204
|
return { preference, herdrMode: requestedMode as HerdrSurfaceMode };
|
|
1038
1205
|
}
|
|
1039
1206
|
|
|
1040
|
-
|
|
1207
|
+
const SUBAGENT_EXTENSIONS_COMMAND = "extensions";
|
|
1208
|
+
const SUBAGENT_EXTENSIONS_CLEAR = "clear";
|
|
1209
|
+
const SUBAGENT_EXTENSIONS_COMMAND_NAMES = [
|
|
1210
|
+
"config:subagent-extensions",
|
|
1211
|
+
"subagent-extensions",
|
|
1212
|
+
] as const;
|
|
1213
|
+
|
|
1214
|
+
/** Parse comma-separated extension paths from a slash-command argument. */
|
|
1215
|
+
function parseSubagentExtensionPaths(value: string): string[] | null {
|
|
1216
|
+
const parts = value.split(",").map((part) => part.trim());
|
|
1217
|
+
if (parts.length === 0 || parts.some((part) => !part)) return null;
|
|
1218
|
+
return [...new Set(parts)];
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/** Format the configured child-session extension paths for user-facing messages. */
|
|
1222
|
+
function formatSubagentExtensions(extensions: readonly string[]): string {
|
|
1223
|
+
return extensions.length > 0 ? extensions.join(", ") : i18n.t("extensionNone");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
/** Complete the shared subagent configuration command's first argument. */
|
|
1227
|
+
function getSubagentConfigArgumentCompletions(argumentPrefix: string) {
|
|
1228
|
+
const trimmedPrefix = argumentPrefix.trimStart();
|
|
1229
|
+
const [firstToken] = trimmedPrefix.split(/\s+/, 1);
|
|
1230
|
+
if (firstToken?.toLowerCase() === SUBAGENT_EXTENSIONS_COMMAND && trimmedPrefix !== firstToken) {
|
|
1231
|
+
return getSubagentExtensionsArgumentCompletions(trimmedPrefix.slice(firstToken.length).trimStart());
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
1235
|
+
const options = [
|
|
1236
|
+
{
|
|
1237
|
+
value: SUBAGENT_EXTENSIONS_COMMAND,
|
|
1238
|
+
label: SUBAGENT_EXTENSIONS_COMMAND,
|
|
1239
|
+
description: i18n.t("extensionCommandDescription"),
|
|
1240
|
+
},
|
|
1241
|
+
{ value: AUTO_MUX_PREFERENCE, label: AUTO_MUX_PREFERENCE, description: i18n.t("muxAuto") },
|
|
1242
|
+
...SUBAGENT_MUX_BACKENDS.map((backend) => ({
|
|
1243
|
+
value: backend,
|
|
1244
|
+
label: backend,
|
|
1245
|
+
description: i18n.t("muxCommandDescription"),
|
|
1246
|
+
})),
|
|
1247
|
+
];
|
|
1248
|
+
return options.filter((option) => !prefix || option.value.startsWith(prefix));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
/** Complete the dedicated extension command with clear and current paths. */
|
|
1252
|
+
function getSubagentExtensionsArgumentCompletions(argumentPrefix: string) {
|
|
1253
|
+
const prefix = argumentPrefix.trim().toLowerCase();
|
|
1254
|
+
const configured = loadSubagentExtensionsConfig().extensions;
|
|
1255
|
+
const options = [
|
|
1256
|
+
{
|
|
1257
|
+
value: SUBAGENT_EXTENSIONS_CLEAR,
|
|
1258
|
+
label: SUBAGENT_EXTENSIONS_CLEAR,
|
|
1259
|
+
description: i18n.t("extensionClearDescription"),
|
|
1260
|
+
},
|
|
1261
|
+
...configured.map((extension) => ({
|
|
1262
|
+
value: extension,
|
|
1263
|
+
label: extension,
|
|
1264
|
+
description: i18n.t("extensionConfiguredDescription"),
|
|
1265
|
+
})),
|
|
1266
|
+
];
|
|
1267
|
+
return options.filter((option) => !prefix || option.value.toLowerCase().startsWith(prefix));
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/** Return a localized source label for an extension candidate. */
|
|
1271
|
+
function extensionCandidateSourceLabel(source: SubagentExtensionCandidateSource): string {
|
|
1272
|
+
if (source === EXTENSION_SOURCE_PROJECT) return i18n.t("extensionCandidateProject");
|
|
1273
|
+
if (source === EXTENSION_SOURCE_GLOBAL) return i18n.t("extensionCandidateGlobal");
|
|
1274
|
+
return i18n.t("extensionCandidateConfigured");
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/** Format one candidate for the toggle-style slash-command chooser. */
|
|
1278
|
+
function formatExtensionCandidateOption(
|
|
1279
|
+
candidate: SubagentExtensionCandidate,
|
|
1280
|
+
selected: ReadonlySet<string>,
|
|
1281
|
+
): string {
|
|
1282
|
+
if (candidate.required) {
|
|
1283
|
+
return `● ${candidate.label} — ${i18n.t("extensionCandidateRequired")}`;
|
|
1284
|
+
}
|
|
1285
|
+
if (candidate.missing) {
|
|
1286
|
+
return `⚠ ${candidate.label} — ${i18n.t("extensionCandidateMissing")}`;
|
|
1287
|
+
}
|
|
1288
|
+
const marker = selected.has(resolve(candidate.path)) ? "●" : "○";
|
|
1289
|
+
return `${marker} ${candidate.label} — ${candidate.path} (${extensionCandidateSourceLabel(candidate.source)})`;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/** Edit explicit paths manually and return the entered values without persisting them. */
|
|
1293
|
+
async function editSubagentExtensionsManually(
|
|
1294
|
+
ctx: ExtensionContext,
|
|
1295
|
+
initial: readonly string[],
|
|
1296
|
+
): Promise<string[] | undefined> {
|
|
1297
|
+
if (!ctx.hasUI) {
|
|
1298
|
+
notify(ctx, i18n.t("extensionInteractiveOnly"), "warning");
|
|
1299
|
+
return undefined;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const value = await ctx.ui.editor(i18n.t("extensionConfigTitle"), initial.join("\n"));
|
|
1303
|
+
if (value === null || value === undefined) return undefined;
|
|
1304
|
+
return value
|
|
1305
|
+
.split(/\r?\n/)
|
|
1306
|
+
.map((extension) => extension.trim())
|
|
1307
|
+
.filter(Boolean);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Save a selected extension list and report the effective configuration. */
|
|
1311
|
+
function saveSelectedSubagentExtensions(ctx: ExtensionContext, extensions: readonly string[]): void {
|
|
1312
|
+
const saved = saveSubagentExtensions(extensions);
|
|
1313
|
+
notify(ctx,
|
|
1314
|
+
i18n.t("extensionSaved", { value: formatSubagentExtensions(saved.extensions) }),
|
|
1315
|
+
"info",
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** Let the user toggle discovered extensions while keeping the built-in done hook fixed. */
|
|
1320
|
+
async function chooseSubagentExtensions(ctx: ExtensionContext): Promise<void> {
|
|
1321
|
+
if (!ctx.hasUI) {
|
|
1322
|
+
notify(ctx, i18n.t("extensionInteractiveOnly"), "warning");
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
const configured = loadSubagentExtensionsConfig().extensions;
|
|
1327
|
+
const configuredResolved = configured.map((extension) => resolveSubagentExtensionPath(extension));
|
|
1328
|
+
const requiredPath = resolve(join(SUBAGENTS_DIR, SUBAGENT_DONE_EXTENSION_FILE));
|
|
1329
|
+
const candidateMap = new Map<string, SubagentExtensionCandidate>();
|
|
1330
|
+
for (const candidate of [
|
|
1331
|
+
{
|
|
1332
|
+
path: requiredPath,
|
|
1333
|
+
label: SUBAGENT_DONE_EXTENSION_FILE,
|
|
1334
|
+
source: EXTENSION_SOURCE_CONFIGURED,
|
|
1335
|
+
required: true,
|
|
1336
|
+
},
|
|
1337
|
+
...discoverSubagentExtensionCandidates({ configured }),
|
|
1338
|
+
]) {
|
|
1339
|
+
const key = resolve(candidate.path);
|
|
1340
|
+
if (!candidateMap.has(key)) candidateMap.set(key, candidate);
|
|
1341
|
+
}
|
|
1342
|
+
const candidates = [...candidateMap.values()];
|
|
1343
|
+
const selected = new Set(
|
|
1344
|
+
configuredResolved
|
|
1345
|
+
.filter((extensionPath) => existsSync(extensionPath))
|
|
1346
|
+
.map((extensionPath) => resolve(extensionPath)),
|
|
1347
|
+
);
|
|
1348
|
+
|
|
1349
|
+
const candidateOptions = new Map<string, SubagentExtensionCandidate>();
|
|
1350
|
+
while (true) {
|
|
1351
|
+
candidateOptions.clear();
|
|
1352
|
+
const options = candidates.map((candidate) => {
|
|
1353
|
+
const option = formatExtensionCandidateOption(candidate, selected);
|
|
1354
|
+
candidateOptions.set(option, candidate);
|
|
1355
|
+
return option;
|
|
1356
|
+
});
|
|
1357
|
+
const doneOption = i18n.t("extensionChooserDone");
|
|
1358
|
+
const manualOption = i18n.t("extensionChooserManual");
|
|
1359
|
+
options.push(manualOption, doneOption);
|
|
1360
|
+
|
|
1361
|
+
const choice = await ctx.ui.select(i18n.t("extensionChooserTitle"), options);
|
|
1362
|
+
if (choice === undefined) return;
|
|
1363
|
+
if (choice === doneOption) {
|
|
1364
|
+
saveSelectedSubagentExtensions(ctx, [...selected]);
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (choice === manualOption) {
|
|
1368
|
+
const manual = await editSubagentExtensionsManually(ctx, [...selected]);
|
|
1369
|
+
if (manual !== undefined) {
|
|
1370
|
+
selected.clear();
|
|
1371
|
+
for (const extension of manual) selected.add(resolveSubagentExtensionPath(extension));
|
|
1372
|
+
}
|
|
1373
|
+
continue;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const candidate = candidateOptions.get(choice);
|
|
1377
|
+
if (!candidate || candidate.required || candidate.missing) continue;
|
|
1378
|
+
const normalizedPath = resolve(candidate.path);
|
|
1379
|
+
if (selected.has(normalizedPath)) selected.delete(normalizedPath);
|
|
1380
|
+
else selected.add(normalizedPath);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/** Handle `/config:subagent extensions ...` or the dedicated extension command. */
|
|
1385
|
+
async function handleSubagentExtensionsCommand(
|
|
1386
|
+
args: string,
|
|
1387
|
+
ctx: ExtensionContext,
|
|
1388
|
+
hasKeyword: boolean,
|
|
1389
|
+
): Promise<boolean> {
|
|
1390
|
+
const requested = args.trim();
|
|
1391
|
+
let extensionArgs = requested;
|
|
1392
|
+
|
|
1393
|
+
if (hasKeyword) {
|
|
1394
|
+
const [keyword] = requested.split(/\s+/, 1);
|
|
1395
|
+
if (keyword?.toLowerCase() !== SUBAGENT_EXTENSIONS_COMMAND) return false;
|
|
1396
|
+
extensionArgs = requested.slice(keyword.length).trim();
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
if (!extensionArgs) {
|
|
1400
|
+
await chooseSubagentExtensions(ctx);
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
if (extensionArgs.toLowerCase() === SUBAGENT_EXTENSIONS_CLEAR) {
|
|
1405
|
+
const saved = saveSubagentExtensions([]);
|
|
1406
|
+
notify(ctx,
|
|
1407
|
+
i18n.t("extensionSaved", { value: formatSubagentExtensions(saved.extensions) }),
|
|
1408
|
+
"info",
|
|
1409
|
+
);
|
|
1410
|
+
return true;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const extensions = parseSubagentExtensionPaths(extensionArgs);
|
|
1414
|
+
if (!extensions) {
|
|
1415
|
+
notify(ctx, i18n.t("extensionInvalid", { value: requested }), "warning");
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const saved = saveSubagentExtensions(extensions);
|
|
1420
|
+
notify(ctx,
|
|
1421
|
+
i18n.t("extensionSaved", { value: formatSubagentExtensions(saved.extensions) }),
|
|
1422
|
+
"info",
|
|
1423
|
+
);
|
|
1424
|
+
return true;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/** Save mux selection, and optionally its Herdr surface mode. */
|
|
1041
1428
|
function saveMuxConfigSelection(selection: MuxConfigSelection): MuxConfigSelection {
|
|
1042
1429
|
const savedMux = saveMuxPreference(selection.preference);
|
|
1043
1430
|
const savedMode = selection.herdrMode ? saveHerdrMode(selection.herdrMode).herdrMode : undefined;
|
|
@@ -1048,12 +1435,14 @@ function saveMuxConfigSelection(selection: MuxConfigSelection): MuxConfigSelecti
|
|
|
1048
1435
|
function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
1049
1436
|
const command = {
|
|
1050
1437
|
description: i18n.t("muxCommandDescription"),
|
|
1438
|
+
getArgumentCompletions: getSubagentConfigArgumentCompletions,
|
|
1051
1439
|
handler: async (args, ctx) => {
|
|
1052
1440
|
const requested = args.trim();
|
|
1441
|
+
if (requested && await handleSubagentExtensionsCommand(requested, ctx, true)) return;
|
|
1053
1442
|
if (requested) {
|
|
1054
1443
|
const selection = parseMuxConfigRequest(requested);
|
|
1055
1444
|
if (!selection) {
|
|
1056
|
-
|
|
1445
|
+
notify(ctx, i18n.t("muxInvalid", { value: requested }), "warning");
|
|
1057
1446
|
return;
|
|
1058
1447
|
}
|
|
1059
1448
|
try {
|
|
@@ -1061,18 +1450,18 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1061
1450
|
const herdrMode = saved.preference === HERDR_MUX_BACKEND
|
|
1062
1451
|
? saved.herdrMode ?? loadHerdrModeConfig().herdrMode
|
|
1063
1452
|
: undefined;
|
|
1064
|
-
|
|
1453
|
+
notify(ctx,
|
|
1065
1454
|
i18n.t("muxSaved", { value: muxPreferenceLabel(saved.preference, herdrMode) }),
|
|
1066
1455
|
"info",
|
|
1067
1456
|
);
|
|
1068
1457
|
} catch (error) {
|
|
1069
|
-
|
|
1458
|
+
notify(ctx, String(error), "error");
|
|
1070
1459
|
}
|
|
1071
1460
|
return;
|
|
1072
1461
|
}
|
|
1073
1462
|
|
|
1074
1463
|
if (!ctx.hasUI) {
|
|
1075
|
-
|
|
1464
|
+
notify(ctx, i18n.t("muxInteractiveOnly"), "warning");
|
|
1076
1465
|
return;
|
|
1077
1466
|
}
|
|
1078
1467
|
|
|
@@ -1117,20 +1506,31 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
|
|
|
1117
1506
|
if (!selected) return;
|
|
1118
1507
|
try {
|
|
1119
1508
|
const saved = saveMuxConfigSelection(selected);
|
|
1120
|
-
|
|
1509
|
+
notify(ctx,
|
|
1121
1510
|
i18n.t("muxSaved", {
|
|
1122
1511
|
value: muxPreferenceLabel(saved.preference, saved.herdrMode),
|
|
1123
1512
|
}),
|
|
1124
1513
|
"info",
|
|
1125
1514
|
);
|
|
1126
1515
|
} catch (error) {
|
|
1127
|
-
|
|
1516
|
+
notify(ctx, String(error), "error");
|
|
1128
1517
|
}
|
|
1129
1518
|
},
|
|
1130
1519
|
};
|
|
1131
1520
|
for (const name of ["config:subagent", "subagent-config", "pi-subagent-config"] as const) {
|
|
1132
1521
|
pi.registerCommand(name, command);
|
|
1133
1522
|
}
|
|
1523
|
+
|
|
1524
|
+
const extensionsCommand = {
|
|
1525
|
+
description: i18n.t("extensionCommandDescription"),
|
|
1526
|
+
getArgumentCompletions: getSubagentExtensionsArgumentCompletions,
|
|
1527
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
1528
|
+
await handleSubagentExtensionsCommand(args, ctx, false);
|
|
1529
|
+
},
|
|
1530
|
+
};
|
|
1531
|
+
for (const name of SUBAGENT_EXTENSIONS_COMMAND_NAMES) {
|
|
1532
|
+
pi.registerCommand(name, extensionsCommand);
|
|
1533
|
+
}
|
|
1134
1534
|
}
|
|
1135
1535
|
|
|
1136
1536
|
/** 每次启动或恢复都用新 surface 重建归属,不能继承父进程的改名范围。 */
|
|
@@ -1140,13 +1540,20 @@ function buildTerminalRenameEnvironment(surface: string, backend = getMuxBackend
|
|
|
1140
1540
|
}
|
|
1141
1541
|
|
|
1142
1542
|
export const __test__ = {
|
|
1543
|
+
notify,
|
|
1544
|
+
NOTICE_SOURCE,
|
|
1143
1545
|
buildTerminalRenameEnvironment,
|
|
1144
1546
|
borderLine,
|
|
1145
1547
|
parseMuxConfigRequest,
|
|
1548
|
+
parseSubagentExtensionPaths,
|
|
1549
|
+
getSubagentConfigArgumentCompletions,
|
|
1550
|
+
getSubagentExtensionsArgumentCompletions,
|
|
1146
1551
|
getShellReadyDelayMs,
|
|
1147
1552
|
renderSubagentWidgetLines,
|
|
1148
1553
|
loadAgentDefaults,
|
|
1149
1554
|
discoverAgentDefinitions,
|
|
1555
|
+
discoverSubagentExtensionCandidates,
|
|
1556
|
+
getConfiguredSubagentExtensions,
|
|
1150
1557
|
resolveEffectiveSessionMode,
|
|
1151
1558
|
resolveLaunchBehavior,
|
|
1152
1559
|
resolveEffectiveInteractive,
|
|
@@ -2224,7 +2631,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2224
2631
|
handler: async (args, ctx) => {
|
|
2225
2632
|
const trimmed = args.trim();
|
|
2226
2633
|
if (!trimmed) {
|
|
2227
|
-
|
|
2634
|
+
notify(ctx, "Usage: /subagent <agent> [task]", "warning");
|
|
2228
2635
|
return;
|
|
2229
2636
|
}
|
|
2230
2637
|
|
|
@@ -2234,7 +2641,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2234
2641
|
|
|
2235
2642
|
const defs = loadAgentDefaults(agentName);
|
|
2236
2643
|
if (!defs) {
|
|
2237
|
-
|
|
2644
|
+
notify(ctx,
|
|
2238
2645
|
`Agent "${agentName}" not found in ~/.pi/agent/agents/ or .pi/agents/`,
|
|
2239
2646
|
"error",
|
|
2240
2647
|
);
|
|
@@ -2392,7 +2799,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2392
2799
|
handler: async (args, ctx) => {
|
|
2393
2800
|
const task = args.trim();
|
|
2394
2801
|
if (!task) {
|
|
2395
|
-
|
|
2802
|
+
notify(ctx, "Usage: /plan <what to build>", "warning");
|
|
2396
2803
|
return;
|
|
2397
2804
|
}
|
|
2398
2805
|
|
|
@@ -146,6 +146,16 @@ export function loadSubagentExtensionsConfig(path = muxConfigPath()): SubagentEx
|
|
|
146
146
|
return { extensions: [...DEFAULT_SUBAGENT_EXTENSIONS], source: "default" };
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/** Persist the explicit extension list for child sessions. */
|
|
150
|
+
export function saveSubagentExtensions(
|
|
151
|
+
extensions: readonly string[],
|
|
152
|
+
path = muxConfigPath(),
|
|
153
|
+
): SubagentExtensionsConfig {
|
|
154
|
+
const normalized = [...new Set(extensions.map((extension) => extension.trim()).filter(Boolean))];
|
|
155
|
+
writeConfigObject(path, { ...readConfigObject(path), subagentExtensions: normalized });
|
|
156
|
+
return { extensions: normalized, source: "file" };
|
|
157
|
+
}
|
|
158
|
+
|
|
149
159
|
/** Apply persisted mux and Herdr mode settings when no explicit environment override exists. */
|
|
150
160
|
export function applyPersistedMuxPreference(path = muxConfigPath()): void {
|
|
151
161
|
if (!environmentPreference()) {
|