@buyi1net/pi-toolkit 0.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/assembler.ts +127 -0
  4. package/config.ts +201 -0
  5. package/i18n.ts +693 -0
  6. package/index.ts +99 -0
  7. package/menu/items.ts +264 -0
  8. package/menu/panels.ts +91 -0
  9. package/menu/settings-list.ts +84 -0
  10. package/menu/theme.ts +31 -0
  11. package/menu/toolkit-menu.ts +139 -0
  12. package/module.ts +156 -0
  13. package/modules/eyes/chain.ts +173 -0
  14. package/modules/eyes/config.ts +319 -0
  15. package/modules/eyes/index.ts +161 -0
  16. package/modules/eyes/menu.ts +828 -0
  17. package/modules/eyes/pi-model-backend.ts +245 -0
  18. package/modules/eyes/resilience.ts +161 -0
  19. package/modules/eyes/vision-bridge.ts +232 -0
  20. package/modules/eyes/vision-cache.ts +86 -0
  21. package/modules/eyes/vision-json.ts +19 -0
  22. package/modules/eyes/vision-preprocess.ts +192 -0
  23. package/modules/eyes/vision-probe.ts +21 -0
  24. package/modules/eyes/vision-prompt.ts +90 -0
  25. package/modules/eyes/vision-tool.ts +59 -0
  26. package/modules/eyes/vision-types.ts +33 -0
  27. package/modules/index.ts +10 -0
  28. package/modules/subagents/agents/researcher.md +51 -0
  29. package/modules/subagents/agents/scout.md +40 -0
  30. package/modules/subagents/agents/worker.md +79 -0
  31. package/modules/subagents/config.json.example +6 -0
  32. package/modules/subagents/config.ts +144 -0
  33. package/modules/subagents/index.ts +91 -0
  34. package/modules/subagents/menu.ts +388 -0
  35. package/modules/subagents/src/activity.ts +511 -0
  36. package/modules/subagents/src/agents.ts +126 -0
  37. package/modules/subagents/src/command.ts +37 -0
  38. package/modules/subagents/src/dependencies.ts +246 -0
  39. package/modules/subagents/src/diagnostics.ts +13 -0
  40. package/modules/subagents/src/display.ts +94 -0
  41. package/modules/subagents/src/headless.ts +342 -0
  42. package/modules/subagents/src/herdr.ts +203 -0
  43. package/modules/subagents/src/index.ts +2798 -0
  44. package/modules/subagents/src/inspect-tool.ts +839 -0
  45. package/modules/subagents/src/launch-config.ts +196 -0
  46. package/modules/subagents/src/layout-budget.ts +26 -0
  47. package/modules/subagents/src/list-tool.ts +292 -0
  48. package/modules/subagents/src/message-tool.ts +808 -0
  49. package/modules/subagents/src/names.ts +17 -0
  50. package/modules/subagents/src/pane-layout.ts +49 -0
  51. package/modules/subagents/src/params.ts +145 -0
  52. package/modules/subagents/src/renderers.ts +181 -0
  53. package/modules/subagents/src/result.ts +43 -0
  54. package/modules/subagents/src/retention.ts +114 -0
  55. package/modules/subagents/src/route-error.ts +212 -0
  56. package/modules/subagents/src/routing.ts +235 -0
  57. package/modules/subagents/src/runtime-registry.ts +159 -0
  58. package/modules/subagents/src/session.ts +801 -0
  59. package/modules/subagents/src/status.ts +513 -0
  60. package/modules/subagents/src/stop-tool.ts +235 -0
  61. package/modules/subagents/src/subagent-done.ts +590 -0
  62. package/modules/subagents/src/subagent-tool.ts +1155 -0
  63. package/modules/subagents/src/surface.ts +355 -0
  64. package/modules/subagents/src/team-dispatch-tool.ts +219 -0
  65. package/modules/subagents/src/team.ts +232 -0
  66. package/modules/subagents/src/tmux.ts +210 -0
  67. package/modules/subagents/src/tools/safe-bash.ts +72 -0
  68. package/modules/subagents/src/types.ts +131 -0
  69. package/modules/tui/adapter/provider-usage.ts +143 -0
  70. package/modules/tui/config.ts +50 -0
  71. package/modules/tui/index.ts +120 -0
  72. package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
  73. package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
  74. package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
  75. package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
  76. package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
  77. package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
  78. package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
  79. package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
  80. package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
  81. package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
  82. package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
  83. package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
  84. package/modules/tui/kernel/usage-core.ts +2 -0
  85. package/modules/tui/kernel/usage-node.ts +2 -0
  86. package/modules/tui/menu.ts +418 -0
  87. package/modules/tui/plugin/editor.ts +396 -0
  88. package/modules/tui/plugin/footer.ts +161 -0
  89. package/modules/tui/plugin/index.ts +30 -0
  90. package/modules/tui/plugin/lifecycle.ts +637 -0
  91. package/modules/tui/plugin/package-order.ts +169 -0
  92. package/modules/tui/plugin/screen-transition.ts +203 -0
  93. package/modules/tui/plugin/settings-config.ts +297 -0
  94. package/modules/tui/plugin/status-sources.ts +49 -0
  95. package/modules/tui/plugin/transition-gate.ts +261 -0
  96. package/modules/tui/renderer/custom-header.ts +157 -0
  97. package/modules/tui/renderer/editor.ts +70 -0
  98. package/modules/tui/renderer/header.ts +72 -0
  99. package/modules/tui/renderer/icons.ts +149 -0
  100. package/modules/tui/renderer/pi-installer-logo.ts +194 -0
  101. package/modules/tui/status/auto-compaction.ts +67 -0
  102. package/modules/tui/status/project-status.ts +655 -0
  103. package/modules/tui/status/provider-status.ts +120 -0
  104. package/modules/tui/status/runtime-status.ts +307 -0
  105. package/modules/tui/status/session-status.ts +325 -0
  106. package/modules/tui/status/status-config.ts +95 -0
  107. package/modules/tui/status/status-segments.ts +263 -0
  108. package/modules/tui/status/turn-telemetry.ts +466 -0
  109. package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
  110. package/modules/tui/themes/UPSTREAM.md +7 -0
  111. package/modules/tui/themes/catppuccin-latte.json +80 -0
  112. package/modules/tui/themes/catppuccin-mocha.json +79 -0
  113. package/modules/tui/themes/crimson-noir.json +85 -0
  114. package/modules/tui/themes/dracula.json +79 -0
  115. package/modules/tui/themes/everforest-dark.json +85 -0
  116. package/modules/tui/themes/gruvbox-dark.json +85 -0
  117. package/modules/tui/themes/gruvbox-light.json +85 -0
  118. package/modules/tui/themes/matrix.json +85 -0
  119. package/modules/tui/themes/nord.json +85 -0
  120. package/modules/tui/themes/one-dark.json +85 -0
  121. package/modules/tui/themes/rose-pine-dawn.json +85 -0
  122. package/modules/tui/themes/rose-pine.json +85 -0
  123. package/modules/tui/themes/solarized-dark.json +85 -0
  124. package/modules/tui/themes/solarized-light.json +85 -0
  125. package/modules/tui/themes/tokyo-night-storm.json +79 -0
  126. package/modules/tui/themes/tokyo-night.json +79 -0
  127. package/package.json +28 -0
  128. package/services.ts +38 -0
  129. package/toolkit.ts +147 -0
@@ -0,0 +1,196 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import type { AgentDefaults, AgentSessionMode } from "./agents.ts";
4
+ import type { SubagentParams } from "./params.ts";
5
+
6
+ export const SPAWNING_TOOLS = [
7
+ "subagent",
8
+ "subagent_message",
9
+ "subagents_list",
10
+ "subagent_inspect",
11
+ ] as const;
12
+
13
+ /** 成员间直信工具:仅 member spawn 注入白名单(注册在 subagent-done.ts)。 */
14
+ export const TEAM_TOOLS = ["team_send"] as const;
15
+
16
+ export type SubagentSessionMode = AgentSessionMode;
17
+
18
+ export function resolveSubagentPaths(
19
+ params: SubagentParams,
20
+ agentDefs: AgentDefaults | null,
21
+ getAgentConfigDir: () => string,
22
+ ): { effectiveCwd: string | null; localAgentDir: string | null; effectiveAgentDir: string } {
23
+ const rawCwd = params.cwd ?? agentDefs?.cwd ?? null;
24
+ const cwdIsFromAgent = !params.cwd && agentDefs?.cwd != null;
25
+ const cwdBase = cwdIsFromAgent ? getAgentConfigDir() : process.cwd();
26
+ const effectiveCwd = rawCwd
27
+ ? isAbsolute(rawCwd)
28
+ ? rawCwd
29
+ : join(cwdBase, rawCwd)
30
+ : null;
31
+ const localAgentDir = effectiveCwd ? join(effectiveCwd, ".pi", "agent") : null;
32
+ const effectiveAgentDir =
33
+ localAgentDir && existsSync(localAgentDir) ? localAgentDir : getAgentConfigDir();
34
+ return { effectiveCwd, localAgentDir, effectiveAgentDir };
35
+ }
36
+
37
+ export function getDefaultSessionDirFor(cwd: string, agentDir: string): string {
38
+ const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
39
+ const sessionDir = join(agentDir, "sessions", safePath);
40
+ if (!existsSync(sessionDir)) mkdirSync(sessionDir, { recursive: true });
41
+ return sessionDir;
42
+ }
43
+
44
+ export function resolveEffectiveSessionMode(
45
+ agentDefs: AgentDefaults | null,
46
+ ): SubagentSessionMode {
47
+ return agentDefs?.sessionMode ?? "standalone";
48
+ }
49
+
50
+ export function resolveLaunchBehavior(
51
+ params: SubagentParams,
52
+ agentDefs: AgentDefaults | null,
53
+ ): {
54
+ sessionMode: SubagentSessionMode;
55
+ seededSessionMode: "lineage-only" | "fork" | null;
56
+ inheritsConversationContext: boolean;
57
+ taskDelivery: "direct" | "artifact";
58
+ } {
59
+ const sessionMode = resolveEffectiveSessionMode(agentDefs);
60
+ const inheritsConversationContext = sessionMode === "fork";
61
+ return {
62
+ sessionMode,
63
+ seededSessionMode: sessionMode === "standalone" ? null : sessionMode,
64
+ inheritsConversationContext,
65
+ taskDelivery: inheritsConversationContext ? "direct" : "artifact",
66
+ };
67
+ }
68
+
69
+ export function resolveEffectiveInteractive(
70
+ _params: SubagentParams,
71
+ agentDefs: AgentDefaults | null,
72
+ ): boolean {
73
+ if (agentDefs?.interactive != null) return agentDefs.interactive;
74
+ return !(agentDefs?.autoExit ?? false);
75
+ }
76
+
77
+ /**
78
+ * Resolve the lifecycle flag that is actually passed to the child process.
79
+ * `interactive` and `auto-exit` describe the same boundary from opposite
80
+ * directions, so an explicit value on either side must not leave the other
81
+ * side undefined. In particular, `interactive: false` without
82
+ * `auto-exit: true` must still terminate a headless child after its turn.
83
+ */
84
+ export function resolveEffectiveAutoExit(agentDefs: AgentDefaults | null): boolean {
85
+ if (agentDefs?.interactive != null) return !agentDefs.interactive;
86
+ return agentDefs?.autoExit ?? false;
87
+ }
88
+
89
+ /** Reject contradictory frontmatter instead of silently creating a hanging or
90
+ * prematurely self-closing subagent. A valid pair is interactive=true,
91
+ * auto-exit=false, or interactive=false, auto-exit=true. */
92
+ export function validateAgentLifecycleConfig(agentDefs: AgentDefaults | null): string | null {
93
+ if (
94
+ agentDefs?.interactive != null &&
95
+ agentDefs.autoExit != null &&
96
+ agentDefs.interactive === agentDefs.autoExit
97
+ ) {
98
+ return (
99
+ `Agent lifecycle settings conflict: interactive=${agentDefs.interactive} and ` +
100
+ `auto-exit=${agentDefs.autoExit}. Set interactive=false with auto-exit=true ` +
101
+ `for an autonomous agent, or interactive=true with auto-exit=false for a demo pane.`
102
+ );
103
+ }
104
+ return null;
105
+ }
106
+
107
+ export type SubagentSurfaceChoice = "headless" | "pane";
108
+
109
+ /**
110
+ * 表面选择(纯函数):
111
+ * - "pane":强制可见 pane(现有 herdr/tmux 路径,行为不变)。
112
+ * - "background":强制 headless——独立后台 pi 进程,不创建 pane。
113
+ * interactive(演示型)子代理必须拒绝:它们依赖用户在 pane 中操作,
114
+ * 静默降级成 headless 或反过来静默建 pane 都是错的,直接报错返回。
115
+ * - "auto"(默认,兼容旧调用不传 surface):interactive → pane,自动任务 → headless。
116
+ *
117
+ * 返回 { choice } 或 { error }(error 为面向 tool result 的完整错误文本)。
118
+ */
119
+ export function resolveSurfaceChoice(
120
+ surface: string | undefined,
121
+ effectiveInteractive: boolean,
122
+ ): { choice: SubagentSurfaceChoice } | { error: string } {
123
+ const requested = surface ?? "auto";
124
+ if (requested === "pane") return { choice: "pane" };
125
+ if (requested === "background") {
126
+ if (effectiveInteractive) {
127
+ const reason =
128
+ 'interactive (demo) sub-agents run in a visible pane the user operates; ' +
129
+ 'surface "background" (headless) was refused instead of silently creating a pane. ' +
130
+ "Spawn an autonomous agent, or omit `surface` to place this sub-agent in a pane.";
131
+ return { error: reason };
132
+ }
133
+ return { choice: "headless" };
134
+ }
135
+ if (requested === "auto") {
136
+ return { choice: effectiveInteractive ? "pane" : "headless" };
137
+ }
138
+ return {
139
+ error: `Invalid surface "${requested}". Use "auto", "background", or "pane".`,
140
+ };
141
+ }
142
+
143
+ export function buildSubagentToolAllowlist(
144
+ effectiveTools: string | undefined,
145
+ opts?: { grantSpawning?: boolean; grantTeamSend?: boolean },
146
+ ): string | null {
147
+ const requested = (effectiveTools ?? "")
148
+ .split(",")
149
+ .map((tool) => tool.trim())
150
+ .filter(Boolean);
151
+ const grantSpawning = opts?.grantSpawning ?? false;
152
+ const grantTeamSend = opts?.grantTeamSend ?? false;
153
+ const hasExplicitTools = effectiveTools !== undefined;
154
+ const spawningNames = new Set<string>(SPAWNING_TOOLS);
155
+ const teamNames = new Set<string>(TEAM_TOOLS);
156
+
157
+ // A profile cannot opt itself into delegation merely by putting a spawning
158
+ // tool in `tools`; the explicit subagent_agents grant is the authority.
159
+ // team_send 同理:profile 不能靠把工具名写进 tools 自授;member spawn 的
160
+ // 显式注入才是授权来源。
161
+ const allow = new Set(
162
+ requested.filter((tool) =>
163
+ (grantSpawning || !spawningNames.has(tool)) && (grantTeamSend || !teamNames.has(tool)),
164
+ ),
165
+ );
166
+ if (grantSpawning) {
167
+ for (const tool of SPAWNING_TOOLS) allow.add(tool);
168
+ }
169
+ if (grantTeamSend) {
170
+ for (const tool of TEAM_TOOLS) allow.add(tool);
171
+ }
172
+ if (requested.length > 0 || grantSpawning || grantTeamSend) allow.add("ask_question");
173
+
174
+ // null means "use Pi's normal defaults". An explicitly empty/forbidden
175
+ // list must remain an empty allowlist rather than widening back to defaults.
176
+ if (allow.size === 0 && !hasExplicitTools && !grantSpawning && !grantTeamSend) return null;
177
+ return [...allow].join(",");
178
+ }
179
+
180
+ export function buildPiPromptArgs(params: {
181
+ effectiveSkills?: string;
182
+ taskDelivery: "direct" | "artifact";
183
+ taskArg: string;
184
+ }): string[] {
185
+ const skillPrompts = (params.effectiveSkills ?? "")
186
+ .split(",")
187
+ .map((skill) => skill.trim())
188
+ .filter(Boolean)
189
+ .map((skill) => `/skill:${skill}`);
190
+ const needsSeparator = params.taskDelivery === "artifact" && skillPrompts.length > 0;
191
+ return [
192
+ ...(needsSeparator ? [""] : []),
193
+ ...skillPrompts,
194
+ params.taskArg,
195
+ ];
196
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 布局预算 —— 子代理 pane 的最小可用尺寸。
3
+ *
4
+ * 背景(2026-08-30 事故):同一 tab 内连环分屏会把每个 pane 挤到极窄,
5
+ * 63 列时宿主 TUI 状态栏渲染超宽直接崩溃 pi(uncaughtException)。即使
6
+ * 不崩,过窄的 pane 也无法正常使用。防线必须在编排层:低于预算的 pane
7
+ * 不能作为子代理载体;单 Tab 无法继续安全分屏时直接拒绝创建。
8
+ *
9
+ * 两表面(herdr/tmux)共用;调整阈值时两边行为自动一致。
10
+ */
11
+
12
+ /** 子代理 pane 的最小列数。63 列已实证崩溃,70 留出余量。 */
13
+ export const MIN_PANE_COLS = 70;
14
+
15
+ /** 子代理 pane 的最小行数。 */
16
+ export const MIN_PANE_ROWS = 15;
17
+
18
+ export interface PaneRect {
19
+ width: number;
20
+ height: number;
21
+ }
22
+
23
+ /** 尺寸是否满足子代理最低可用预算。 */
24
+ export function rectMeetsBudget(rect: PaneRect | null | undefined): boolean {
25
+ return !!rect && rect.width >= MIN_PANE_COLS && rect.height >= MIN_PANE_ROWS;
26
+ }
@@ -0,0 +1,292 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { Text } from "@earendil-works/pi-tui";
4
+ import { existsSync } from "node:fs";
5
+ import type { ListedAgentDefinition } from "./agents.ts";
6
+ import type { NameRegistry, NameRegistryEntry, SubagentLoadout } from "./session.ts";
7
+ import type { RuntimeRecord } from "./runtime-registry.ts";
8
+ import type { RunningSubagent } from "./types.ts";
9
+
10
+ export interface SubagentsListDeps {
11
+ discoverAgents: () => ListedAgentDefinition[];
12
+ /** scope=session/all 需要:父会话 artifact 目录解析。缺省时 session 视图明确报不可用。 */
13
+ getArtifactDir?: (sessionDir: string, sessionId: string) => string;
14
+ /** 本进程正在运行的子代理(状态判定用)。 */
15
+ runningSubagents?: Map<string, RunningSubagent>;
16
+ readNameRegistry?: (artifactDir: string) => NameRegistry;
17
+ readSubagentLoadout?: (sessionFile: string) => SubagentLoadout | null;
18
+ readRuntimeRecords?: (path: string) => RuntimeRecord[];
19
+ runtimeRegistryPath?: (artifactDir: string) => string;
20
+ }
21
+
22
+ export interface SessionSubagentView {
23
+ name: string;
24
+ agent?: string | null;
25
+ /** running:本进程仍在运行;finished:已到终态;discarded:会话工件已被 retention 清理。 */
26
+ status: "running" | "finished" | "discarded";
27
+ /** 持久团队成员:finished 实为 offline(running 时不区分轮次内细态)。 */
28
+ member?: boolean;
29
+ /** running 成员的轮次状态(idle = 等待派单)。 */
30
+ memberRound?: "idle" | "dispatched";
31
+ /** 会话文件存在且带 loadout 快照,可被 subagent_message resume。 */
32
+ resumable: boolean;
33
+ model?: string | null;
34
+ tier?: string | null;
35
+ cohortId?: string;
36
+ }
37
+
38
+ /**
39
+ * 从会话名字注册表(补充运行态登记)构建当前父会话的子代理只读视图:
40
+ * 只读展示名称、运行/完成/可恢复状态与 model/tier,不泄漏凭据。
41
+ */
42
+ export function buildSessionSubagentViews(
43
+ registry: NameRegistry,
44
+ runningSubagents: Map<string, RunningSubagent> | undefined,
45
+ readSubagentLoadout: (sessionFile: string) => SubagentLoadout | null,
46
+ ): SessionSubagentView[] {
47
+ const views: SessionSubagentView[] = [];
48
+ const consider = (name: string, entry: NameRegistryEntry) => {
49
+ const running = runningSubagents
50
+ ? Array.from(runningSubagents.values()).find((candidate) => candidate.name === name)
51
+ : undefined;
52
+ if (running) {
53
+ views.push({
54
+ name,
55
+ ...(running.agent ? { agent: running.agent } : {}),
56
+ status: "running",
57
+ ...(running.member ? { member: true, memberRound: running.dispatchedRound ? "dispatched" : "idle" } : {}),
58
+ resumable: false,
59
+ ...(running.model ? { model: running.model } : {}),
60
+ ...(running.cohortId ? { cohortId: running.cohortId } : {}),
61
+ });
62
+ return;
63
+ }
64
+ const sessionExists = Boolean(entry.sessionFile) && existsSync(entry.sessionFile);
65
+ if (!sessionExists) {
66
+ // retention 清理或外部删除:保留登记仅供追溯,resume 已不可能。
67
+ views.push({
68
+ name,
69
+ status: "discarded",
70
+ resumable: false,
71
+ ...(entry.cohortId ? { cohortId: entry.cohortId } : {}),
72
+ });
73
+ return;
74
+ }
75
+ const loadout = readSubagentLoadout(entry.sessionFile);
76
+ const cohortId = loadout?.cohortId ?? entry.cohortId;
77
+ views.push({
78
+ name,
79
+ ...(loadout?.agent ? { agent: loadout.agent } : {}),
80
+ status: "finished",
81
+ ...(loadout?.member ? { member: true } : {}),
82
+ resumable: Boolean(loadout),
83
+ ...(loadout?.model ? { model: loadout.model } : {}),
84
+ ...(loadout?.tier ? { tier: loadout.tier } : {}),
85
+ ...(cohortId ? { cohortId } : {}),
86
+ });
87
+ };
88
+ for (const [name, entry] of Object.entries(registry)) {
89
+ if (entry && typeof entry.sessionFile === "string") consider(name, entry);
90
+ }
91
+ // 运行态登记里可能还有尚未写入名字注册表的名字(启动竞态/reload 边界)。
92
+ if (runningSubagents) {
93
+ const known = new Set(views.map((view) => view.name));
94
+ for (const running of runningSubagents.values()) {
95
+ if (!known.has(running.name)) consider(running.name, {
96
+ sessionFile: running.sessionFile,
97
+ sessionId: null,
98
+ ...(running.cohortId ? { cohortId: running.cohortId } : {}),
99
+ });
100
+ }
101
+ }
102
+ views.sort((a, b) => a.name.localeCompare(b.name));
103
+ return views;
104
+ }
105
+
106
+ function formatSessionViewLine(view: SessionSubagentView): string {
107
+ const memberBadge = view.member
108
+ ? view.status === "running"
109
+ ? ` (member · ${view.memberRound ?? "idle"})`
110
+ : " (member · offline)"
111
+ : "";
112
+ const badge =
113
+ view.status === "running" && !view.member ? " (running)"
114
+ : view.status === "discarded" ? " (discarded)" : "";
115
+ const resumable = view.status === "finished" ? (view.resumable ? " [resumable]" : " [not resumable]") : "";
116
+ const agent = view.agent ? ` — ${view.agent}` : "";
117
+ const model = view.model ? ` [${view.model}]` : "";
118
+ const tier = view.tier ? ` [tier: ${view.tier}]` : "";
119
+ const cohort = view.cohortId ? ` [cohort: ${view.cohortId}]` : "";
120
+ return `• ${view.name}${memberBadge}${badge}${resumable}${agent}${model}${tier}${cohort}`;
121
+ }
122
+
123
+ export function registerSubagentsListTool(
124
+ pi: ExtensionAPI,
125
+ deps: SubagentsListDeps,
126
+ ): void {
127
+ pi.registerTool({
128
+ name: "subagents_list",
129
+ label: "List Subagents",
130
+ description:
131
+ "List all available subagent definitions. " +
132
+ "Scans project-local .pi/agents/ and global ~/.pi/agent/agents/. " +
133
+ "Project-local agents override global ones with the same name. " +
134
+ "scope=session|all additionally shows a READ-ONLY view of the subagents registered by the CURRENT parent session: " +
135
+ "their names, whether their session files still exist, running/finished/resumable state, model/tier, and optional cohortId. " +
136
+ "No credentials are included.",
137
+ promptSnippet:
138
+ "List available subagent definitions; scope=session|all also shows a read-only view of this session's registered subagents (name, session present, running/finished/resumable, model/tier, cohortId).",
139
+ parameters: Type.Object({
140
+ scope: Type.Optional(
141
+ Type.Union(
142
+ [Type.Literal("definitions"), Type.Literal("session"), Type.Literal("all")],
143
+ {
144
+ description:
145
+ "'definitions' (default): available agent definitions only. 'session': subagents registered by the current parent session only. " +
146
+ "'all': both views.",
147
+ },
148
+ ),
149
+ ),
150
+ }),
151
+
152
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
153
+ const scope = params?.scope ?? "definitions";
154
+ const definitionList = deps.discoverAgents().filter((agent) => !agent.disableModelInvocation);
155
+
156
+ const definitionsResult = () => {
157
+ if (definitionList.length === 0) {
158
+ return {
159
+ text: "No subagent definitions found.",
160
+ details: { scope, agents: [] as ListedAgentDefinition[] },
161
+ };
162
+ }
163
+ const lines = definitionList.map((agent) => {
164
+ const badge = agent.source === "project" ? " (project)" : "";
165
+ const description = agent.description ? ` — ${agent.description}` : "";
166
+ const model = agent.model ? ` [${agent.model}]` : "";
167
+ return `• ${agent.name}${badge}${model}${description}`;
168
+ });
169
+ return {
170
+ text: lines.join("\n"),
171
+ details: { scope, agents: definitionList },
172
+ };
173
+ };
174
+
175
+ if (scope === "definitions") {
176
+ const result = definitionsResult();
177
+ return { content: [{ type: "text", text: result.text }], details: result.details };
178
+ }
179
+
180
+ // ── session 视图 ──
181
+ const sessionFile = ctx?.sessionManager?.getSessionFile?.();
182
+ if (!sessionFile || !deps.getArtifactDir || !deps.readNameRegistry) {
183
+ return {
184
+ content: [{
185
+ type: "text",
186
+ text:
187
+ "The session view is unavailable in this context: no parent session file or no registry access. " +
188
+ "Start pi with a persistent session to see registered subagents.",
189
+ }],
190
+ details: { scope, error: "session-unavailable" },
191
+ };
192
+ }
193
+ const artifactDir = deps.getArtifactDir(
194
+ ctx.sessionManager.getSessionDir(),
195
+ ctx.sessionManager.getSessionId(),
196
+ );
197
+ const registry = deps.readNameRegistry(artifactDir);
198
+ const views = buildSessionSubagentViews(registry, deps.runningSubagents, (file) =>
199
+ deps.readSubagentLoadout
200
+ ? deps.readSubagentLoadout(file)
201
+ : null,
202
+ );
203
+
204
+ const sessionLines = () =>
205
+ views.length === 0
206
+ ? "No subagents have been registered in this session yet."
207
+ : views.map(formatSessionViewLine).join("\n");
208
+
209
+ if (scope === "session") {
210
+ return {
211
+ content: [{ type: "text", text: sessionLines() }],
212
+ details: { scope, subagents: views },
213
+ };
214
+ }
215
+
216
+ // scope === "all":两个视图一起给。
217
+ const definitions = definitionsResult();
218
+ const agents = definitions.details.agents;
219
+ const text = agents.length > 0 ? definitions.text : "No subagent definitions found.";
220
+ return {
221
+ content: [{
222
+ type: "text",
223
+ text: `${text}\n\n— Session subagents —\n${sessionLines()}`,
224
+ }],
225
+ details: { scope: "all", agents: definitions.details.agents, subagents: views },
226
+ };
227
+ },
228
+
229
+ renderResult(result, _opts, theme) {
230
+ const details = result.details as any;
231
+ const scope = details?.scope ?? "definitions";
232
+ const renderDefinitions = (): string | null => {
233
+ const agents = details?.agents ?? [];
234
+ if (agents.length === 0) return null;
235
+ return agents
236
+ .map((agent: any) => {
237
+ const badge = agent.source === "project" ? theme.fg("accent", " (project)") : "";
238
+ const description = agent.description ? theme.fg("dim", ` — ${agent.description}`) : "";
239
+ const model = agent.model ? theme.fg("dim", ` [${agent.model}]`) : "";
240
+ return ` ${theme.fg("toolTitle", theme.bold(agent.name))}${badge}${model}${description}`;
241
+ })
242
+ .join("\n");
243
+ };
244
+ const renderSession = (): string => {
245
+ const subagents: SessionSubagentView[] = details?.subagents ?? [];
246
+ if (subagents.length === 0) {
247
+ return theme.fg("dim", "No subagents have been registered in this session yet.");
248
+ }
249
+ return subagents
250
+ .map((view) => {
251
+ const memberBadge = view.member
252
+ ? view.status === "running"
253
+ ? theme.fg("accent", ` (member · ${view.memberRound ?? "idle"})`)
254
+ : theme.fg("muted", " (member · offline)")
255
+ : "";
256
+ const statusColor =
257
+ view.status === "running" && !view.member ? "accent" : view.status === "discarded" ? "muted" : view.member ? "muted" : "success";
258
+ const badge = memberBadge
259
+ || (view.status === "running"
260
+ ? theme.fg("accent", " (running)")
261
+ : view.status === "discarded"
262
+ ? theme.fg("dim", " (discarded)")
263
+ : "");
264
+ const resumable =
265
+ view.status === "finished"
266
+ ? view.resumable
267
+ ? theme.fg("success", " [resumable]")
268
+ : theme.fg("dim", " [not resumable]")
269
+ : "";
270
+ const agent = view.agent ? theme.fg("dim", ` — ${view.agent}`) : "";
271
+ const model = view.model ? theme.fg("dim", ` [${view.model}]`) : "";
272
+ const tier = view.tier ? theme.fg("dim", ` [tier: ${view.tier}]`) : "";
273
+ const cohort = view.cohortId ? theme.fg("dim", ` [cohort: ${view.cohortId}]`) : "";
274
+ return ` ${theme.fg("toolTitle", theme.bold(view.name))}${theme.fg(statusColor, badge)}${resumable}${agent}${model}${tier}${cohort}`;
275
+ })
276
+ .join("\n");
277
+ };
278
+
279
+ if (scope === "session") return new Text(renderSession(), 0, 0);
280
+ if (scope === "all") {
281
+ const defs = renderDefinitions();
282
+ return new Text(
283
+ `${defs ?? theme.fg("dim", "No subagent definitions found.")}\n\n${theme.fg("toolTitle", "— Session subagents —")}\n${renderSession()}`,
284
+ 0,
285
+ 0,
286
+ );
287
+ }
288
+ const defs = renderDefinitions();
289
+ return new Text(defs ?? theme.fg("dim", "No subagent definitions found."), 0, 0);
290
+ },
291
+ });
292
+ }