@hheei/pi-subagents 0.1.2 → 0.2.2

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 (65) hide show
  1. package/README.md +42 -137
  2. package/dist/agent-resolver.d.ts +20 -0
  3. package/dist/agent-resolver.js +155 -21
  4. package/dist/bridge-client.d.ts +55 -0
  5. package/dist/bridge-client.js +453 -0
  6. package/dist/bridge-server.d.ts +72 -0
  7. package/dist/bridge-server.js +490 -0
  8. package/dist/builtin-agents.d.ts +24 -0
  9. package/dist/builtin-agents.js +152 -0
  10. package/dist/child-bridge.d.ts +31 -15
  11. package/dist/child-bridge.js +95 -117
  12. package/dist/child-control.d.ts +58 -0
  13. package/dist/child-control.js +176 -0
  14. package/dist/child-process.d.ts +21 -0
  15. package/dist/child-process.js +93 -0
  16. package/dist/commands.d.ts +2 -2
  17. package/dist/commands.js +52 -52
  18. package/dist/diagnostics.d.ts +0 -2
  19. package/dist/diagnostics.js +0 -8
  20. package/dist/domain.d.ts +57 -27
  21. package/dist/domain.js +60 -11
  22. package/dist/extension.d.ts +26 -1
  23. package/dist/extension.js +243 -46
  24. package/dist/host-adapter.d.ts +37 -12
  25. package/dist/host-adapter.js +214 -63
  26. package/dist/json-lines.js +2 -1
  27. package/dist/launch-spec.d.ts +8 -6
  28. package/dist/launch-spec.js +61 -18
  29. package/dist/manager.d.ts +93 -49
  30. package/dist/manager.js +1175 -528
  31. package/dist/markdown.d.ts +8 -0
  32. package/dist/markdown.js +21 -0
  33. package/dist/protocol.d.ts +33 -52
  34. package/dist/protocol.js +41 -52
  35. package/dist/registry.d.ts +3 -8
  36. package/dist/registry.js +41 -163
  37. package/dist/rpc-events.d.ts +3 -5
  38. package/dist/rpc-events.js +5 -9
  39. package/dist/runtime.d.ts +66 -11
  40. package/dist/runtime.js +182 -208
  41. package/dist/session-bootstrap.d.ts +23 -7
  42. package/dist/session-bootstrap.js +79 -15
  43. package/dist/session-fork.d.ts +16 -0
  44. package/dist/session-fork.js +67 -0
  45. package/dist/state.d.ts +7 -0
  46. package/dist/state.js +60 -25
  47. package/dist/tool-activation.d.ts +18 -0
  48. package/dist/tool-activation.js +227 -0
  49. package/dist/tools.d.ts +6 -3
  50. package/dist/tools.js +213 -72
  51. package/dist/widget.d.ts +4 -8
  52. package/dist/widget.js +55 -64
  53. package/package.json +51 -52
  54. package/dist/child-nudge.d.ts +0 -24
  55. package/dist/child-nudge.js +0 -102
  56. package/dist/connector.d.ts +0 -53
  57. package/dist/connector.js +0 -283
  58. package/dist/pause-gate.d.ts +0 -14
  59. package/dist/pause-gate.js +0 -64
  60. package/dist/rpc-adapter.d.ts +0 -40
  61. package/dist/rpc-adapter.js +0 -194
  62. package/dist/runner-entry.d.ts +0 -24
  63. package/dist/runner-entry.js +0 -166
  64. package/dist/runner.d.ts +0 -52
  65. package/dist/runner.js +0 -930
package/dist/tools.js CHANGED
@@ -1,130 +1,269 @@
1
- import { getToolTui, registerToolTuiTrace } from "@hheei/pi-ext-core";
1
+ import { getToolTui, isRecord, registerToolTuiTrace, textToolResult } from "@hheei/pi-ext-core";
2
2
  import { Type } from "typebox";
3
- import { sendReportToRunner } from "./connector.js";
4
- import { isRecord } from "./domain.js";
3
+ import { discoverAgents } from "./agent-resolver.js";
4
+ import { builtinAgents } from "./builtin-agents.js";
5
+ import { isOperationError } from "./domain.js";
5
6
  const spawnSchema = Type.Object({
7
+ id: Type.Optional(Type.String({
8
+ description: "Optional subagent id (e.g. 'agent-1') to re-awaken an idle or completed subagent with a new task instead of creating a new one.",
9
+ })),
6
10
  task: Type.String({ minLength: 1 }),
7
- agent: Type.String({ minLength: 1 }),
11
+ agent: Type.String({
12
+ minLength: 1,
13
+ description: "Name of an agent defined in .pi/agents/*.md or ~/.pi/agent/agents/*.md",
14
+ }),
8
15
  cwd: Type.Optional(Type.String()),
16
+ title: Type.Optional(Type.String({
17
+ maxLength: 60,
18
+ description: "Short session title for this child, e.g. 'OVITO properties editor'. It becomes the child's Pi session title, prefixed with a robot marker so delegated sessions stay recognizable; omit it — or leave it blank — to derive one from the agent name and child id.",
19
+ })),
20
+ forkFrom: Type.Optional(Type.String({
21
+ description: "Optional session or subagent id to reference context from: 'parent' (parent session), or a subagent id like 'agent-1' (including subagents that ended in 'error'). Does not inject past conversation into the prompt; provides the referenced session JSONL path in instructions so the subagent can inspect prior context on demand via read or grep. Cannot be 'current'.",
22
+ })),
9
23
  });
10
24
  const sendSchema = Type.Object({
11
- id: Type.String({ minLength: 1 }),
25
+ id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
12
26
  message: Type.String({ minLength: 1 }),
13
27
  mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up"), Type.Literal("auto")])),
14
28
  });
15
- const idSchema = Type.Object({ id: Type.String({ minLength: 1 }) });
29
+ const idSchema = Type.Object({
30
+ id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
31
+ });
16
32
  const contactSchema = Type.Object({
17
- reason: Type.Union([
18
- Type.Literal("progress_update"),
19
- Type.Literal("important_finding"),
20
- Type.Literal("need_decision"),
21
- Type.Literal("blocked"),
22
- ]),
23
- message: Type.String({ minLength: 1 }),
33
+ message: Type.String({
34
+ minLength: 1,
35
+ description: "Explanation of what is blocking progress or the error encountered.",
36
+ }),
37
+ reason: Type.Optional(Type.Union([Type.Literal("blocked"), Type.Literal("error")], {
38
+ description: "Reason: 'blocked' if waiting for input/guidance (can be resumed via send_agent), or 'error' if fatal unrecoverable failure (cannot continue). Defaults to 'blocked'.",
39
+ })),
24
40
  });
25
41
  const emptySchema = Type.Object({});
26
- function isOperationError(value) {
27
- return (isRecord(value) &&
28
- typeof value.operation === "string" &&
29
- typeof value.reason === "string" &&
30
- Array.isArray(value.sideEffects));
31
- }
32
42
  function result(value) {
33
43
  if (isOperationError(value))
34
44
  throw new Error(JSON.stringify(value));
35
- return {
36
- content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }],
37
- details: value,
38
- };
45
+ return textToolResult(typeof value === "string" ? value : JSON.stringify(value), value);
39
46
  }
40
- const SPAWN_DESCRIPTION = "Start an independent background RPC subagent. This call waits until the child runtime is ready, then returns the child id and initial state. Do NOT poll get_subagent or list_subagents to wait for the child's work. When the child reports via contact_parent, the harness delivers that report as a pi-subagent-report message and starts your next turn. After this tool returns, either end your turn or work on other independent tasks, including spawning more subagents in parallel. Do not fabricate or assume the child's results.";
41
- const SPAWN_SNIPPET = "Start a background RPC subagent. Returns when the runtime is ready. Results arrive later as pi-subagent-report; do not poll.";
47
+ const SUBAGENTS_NAMESPACE = {
48
+ name: "subagents",
49
+ description: "Subagent orchestration and delegation tools",
50
+ };
51
+ const SPAWN_DESCRIPTION = "Start an independent background RPC agent. PREFER REUSING EXISTING SUBAGENTS: For subtasks with identical or related functionality, prefer reusing an existing subagent via send_agent (even if in 'done' or 'blocked' state) rather than spawning a fresh subagent each time. Reusing subagents avoids redundant session initialization, preserves ongoing context, and maximizes prompt cache and pi-ext-memory efficiency. Only spawn a new agent when a distinct role or isolated fresh context is genuinely needed. Do NOT freeze, pause, or stop other sub-workers before spawning: idle subagents do not consume compute or interfere, so do not waste an agent turn freezing them. Do NOT poll get_agent or list_agents to wait for the child's work. Results are automatically captured when the child settles, and delivered as a subagent-report message to start your next turn; while you are idle, reports from several children may arrive together. After this tool returns, either end your turn or work on other independent tasks, including spawning more agents in parallel. Do not fabricate or assume the child's results.";
52
+ const SPAWN_SNIPPET = "Start a background RPC agent. Prefer reusing existing subagents via send_agent for related work to preserve context and maximize memory/cache efficiency. No need to freeze/stop other workers. Results arrive as subagent-report; do not poll.";
42
53
  const SPAWN_GUIDELINES = [
43
- "Do not poll get_subagent or list_subagents waiting for the child to finish.",
44
- "Do not sleep, wait, or tail session/log files to detect completion. The harness delivers reports.",
45
- "When the child calls contact_parent, a pi-subagent-report message starts your next turn.",
46
- "After spawn returns, end your turn or do other independent work, including more parallel spawns.",
54
+ "Prefer reusing existing subagents: for subtasks that have the same or related functionality, reuse an existing subagent via send_agent (which auto-resumes 'done' or 'blocked' agents) instead of spawning a new one. This preserves conversational continuity and maximizes prompt cache and memory efficiency.",
55
+ "Do not freeze, pause, or stop other subagents: idle subagents consume no compute and do not interfere. Do not waste a turn stopping them.",
56
+ "Do not poll get_agent or list_agents waiting for the child to finish.",
57
+ "Do not sleep, wait, or tail session/log files to detect completion. The harness delivers reports automatically.",
58
+ "After spawn returns, end your turn or do other independent work, including spawning more agents in parallel.",
47
59
  "Do not fabricate, assume, or summarize the child's results before a report arrives.",
48
60
  ];
49
- const SEND_DESCRIPTION = "Send a steer or follow-up message to one owned child. Do NOT poll get_subagent or list_subagents afterwards. Child reports arrive as pi-subagent-report messages that start your next turn.";
50
- const SEND_SNIPPET = "Send a message to one owned child. Reports arrive as pi-subagent-report; do not poll afterwards.";
51
- const GET_DESCRIPTION = "Inspect one owned child: state, mode, summary, usage, and whether the view is live or last-known. Use this when you need current identity or state, not to wait for the child to finish.";
52
- const LIST_DESCRIPTION = "List children owned by this parent session. Use this when you need ids or current state, not to wait for work to finish. Reports still arrive as pi-subagent-report messages.";
61
+ const SEND_DESCRIPTION = "Send a steer or follow-up message to one owned child. PREFERRED FOR SUBSEQUENT/RELATED TASKS: Prefer reusing existing subagents in 'done' or 'blocked' states via send_agent instead of spawning new subagents every time; 'blocked' and 'done' children automatically rebuild and resume with their conversation context intact, maximizing prompt cache and memory efficiency. Subagents in 'error' cannot be continued; use spawn_agent with forkFrom to reference their session context. Do NOT wait for or ask workers to 'freeze': idle subagents are completely dormant and touch nothing until sent to. Dispatch reviewers or follow-up tasks immediately without wasting turns on freeze ceremonies. Do NOT poll get_agent or list_agents afterwards. Child reports arrive as subagent-report messages that start your next turn.";
62
+ const SEND_SNIPPET = "Send a message to one owned child (auto-resumes if finished/blocked). Prefer reusing existing agents for related tasks. Reports arrive as subagent-report; do not poll afterwards.";
63
+ const GET_DESCRIPTION = "Inspect one owned child: state, presentation (panel or background), summary, usage, and whether the view is live or last-known. Use this when you need current identity or state, not to wait for the child to finish.";
64
+ const LIST_DESCRIPTION = "List available interactive agent definitions and running or past subagents owned by this parent session with their state ('running', 'done', 'blocked', 'error'). Use this to inspect existing subagents that can be reused via send_agent before deciding to spawn a new agent, not to wait for work to finish. Reports still arrive as subagent-report messages.";
65
+ const LIST_SNIPPET = "List available interactive agent definitions and running subagents.";
53
66
  const STOP_DESCRIPTION = "Persist a stopped intent, then end that child's runtime.";
54
- const CONTACT_DESCRIPTION = "Report progress, an important finding, a decision you need, or a blocker to the parent. The parent is woken automatically; do not retry the same report. After need_decision or blocked, wait for a parent send_subagent. Do not invent new authority.";
55
- const CONTACT_SNIPPET = "Report to the parent. The parent is woken automatically; do not retry the same report.";
67
+ const CONTACT_DESCRIPTION = "Notify the parent session when you are BLOCKED and cannot continue without parent decision or intervention. Do NOT call this on success or task completion: simply output your final answer directly as normal text in your response, and the harness will automatically capture and report your result to the parent.";
68
+ const CONTACT_SNIPPET = "Report a blocker to the parent. Do NOT call on success: write normal text to finish.";
56
69
  const CONTACT_GUIDELINES = [
57
- "Call contact_parent when the parent needs a progress update, finding, decision, or blocker.",
58
- "Do not send empty status pings or retry the same report.",
59
- "After need_decision or blocked, wait for a parent send_subagent. Do not invent new authority.",
70
+ "Use contact_parent ONLY if you are blocked or urgently require a parent decision midway.",
71
+ "Do NOT use contact_parent to deliver final findings or report success: output normal text instead.",
72
+ "After reporting a blocker, wait for a parent send_agent instruction.",
60
73
  ];
74
+ function spawnFooter(result) {
75
+ const details = result.details;
76
+ const child = details?.child;
77
+ if (child !== undefined && typeof child.id === "string") {
78
+ const agent = child.displayName ?? child.agent;
79
+ return `#${child.id} · ${agent}`;
80
+ }
81
+ return undefined;
82
+ }
83
+ function childStateFooter(result) {
84
+ const details = result.details;
85
+ const child = details?.child;
86
+ if (child !== undefined && typeof child.id === "string") {
87
+ return `#${child.id} · ${child.state}`;
88
+ }
89
+ return undefined;
90
+ }
91
+ function listFooter(result) {
92
+ const details = result.details;
93
+ if (isRecord(details) && Array.isArray(details.runningAgents)) {
94
+ const count = details.runningAgents.length;
95
+ if (count === 0)
96
+ return undefined;
97
+ return count === 1 ? "1 child" : `${count} children`;
98
+ }
99
+ if (Array.isArray(details)) {
100
+ const count = details.length;
101
+ if (count === 0)
102
+ return undefined;
103
+ return count === 1 ? "1 child" : `${count} children`;
104
+ }
105
+ return undefined;
106
+ }
61
107
  export function registerParentTools(pi, manager) {
62
108
  registerToolTuiTrace(pi);
63
109
  const tui = getToolTui(pi);
64
110
  const spawn = {
65
- name: "spawn_subagent",
66
- label: "Spawn subagent",
111
+ name: "spawn_agent",
112
+ namespace: SUBAGENTS_NAMESPACE,
113
+ label: "Spawn agent",
67
114
  description: SPAWN_DESCRIPTION,
68
115
  promptSnippet: SPAWN_SNIPPET,
69
116
  promptGuidelines: [...SPAWN_GUIDELINES],
70
117
  parameters: spawnSchema,
118
+ defaultActive: false,
119
+ annotations: {
120
+ openWorldHint: true,
121
+ },
71
122
  async execute(_id, params) {
72
123
  return result(await manager.spawn(params));
73
124
  },
74
125
  };
75
126
  const send = {
76
- name: "send_subagent",
77
- label: "Send to subagent",
127
+ name: "send_agent",
128
+ namespace: SUBAGENTS_NAMESPACE,
129
+ label: "Send to agent",
78
130
  description: SEND_DESCRIPTION,
79
131
  promptSnippet: SEND_SNIPPET,
80
132
  promptGuidelines: [
81
- "Do not poll get_subagent or list_subagents afterwards.",
82
- "Child reports arrive as pi-subagent-report messages that start your next turn.",
133
+ "Prefer reusing an existing subagent in 'done' or 'blocked' states via send_agent for subsequent or functionally identical/related subtasks instead of spawning a new subagent each time. This preserves context and maximizes memory and prompt cache efficiency.",
134
+ "Do not wait for or ask workers to 'freeze': idle subagents are dormant and touch nothing. Dispatch reviewers or follow-ups immediately.",
135
+ "Do not poll get_agent or list_agents afterwards.",
136
+ "Child reports arrive as subagent-report messages that start your next turn.",
83
137
  ],
84
138
  parameters: sendSchema,
139
+ defaultActive: false,
140
+ annotations: {
141
+ openWorldHint: true,
142
+ },
85
143
  async execute(_id, params, signal) {
86
144
  return result(await manager.send(params.id, params.message, params.mode, signal));
87
145
  },
88
146
  };
89
147
  const get = {
90
- name: "get_subagent",
91
- label: "Get subagent",
148
+ name: "get_agent",
149
+ namespace: SUBAGENTS_NAMESPACE,
150
+ label: "Get agent",
92
151
  description: GET_DESCRIPTION,
93
152
  promptSnippet: GET_DESCRIPTION,
94
153
  parameters: idSchema,
154
+ defaultActive: false,
155
+ annotations: {
156
+ readOnlyHint: true,
157
+ idempotentHint: true,
158
+ },
95
159
  async execute(_id, params) {
96
160
  return result(await manager.get(params.id));
97
161
  },
98
162
  };
99
163
  const list = {
100
- name: "list_subagents",
101
- label: "List subagents",
164
+ name: "list_agents",
165
+ namespace: SUBAGENTS_NAMESPACE,
166
+ label: "List agents",
102
167
  description: LIST_DESCRIPTION,
103
- promptSnippet: LIST_DESCRIPTION,
168
+ promptSnippet: LIST_SNIPPET,
104
169
  parameters: emptySchema,
105
- async execute() {
106
- return result(await manager.list());
170
+ annotations: {
171
+ readOnlyHint: true,
172
+ idempotentHint: true,
173
+ },
174
+ async execute(_id, _params, _signal, _onUpdate, ctx) {
175
+ const children = await manager.list();
176
+ const cwd = ctx !== undefined && typeof ctx === "object" && "cwd" in ctx && typeof ctx.cwd === "string"
177
+ ? ctx.cwd
178
+ : process.cwd();
179
+ let allAgents;
180
+ try {
181
+ allAgents = await discoverAgents(cwd);
182
+ }
183
+ catch {
184
+ allAgents = builtinAgents();
185
+ }
186
+ const visible = allAgents.filter((agent) => agent.enabled !== false && agent.frontmatter.hidden !== true);
187
+ const interactiveAgents = visible
188
+ .filter((agent) => agent.frontmatter.interactive === true)
189
+ .map((agent) => ({
190
+ name: agent.name,
191
+ description: typeof agent.frontmatter.description === "string" &&
192
+ agent.frontmatter.description.trim() !== ""
193
+ ? agent.frontmatter.description.trim()
194
+ : "No description provided.",
195
+ }))
196
+ .sort((a, b) => a.name.localeCompare(b.name));
197
+ const lines = [];
198
+ if (interactiveAgents.length > 0) {
199
+ lines.push("<interactive_agents>");
200
+ for (const agent of interactiveAgents) {
201
+ lines.push(`- ${agent.name}: ${agent.description}`);
202
+ }
203
+ lines.push("</interactive_agents>");
204
+ }
205
+ if (children.length > 0) {
206
+ if (lines.length > 0)
207
+ lines.push("");
208
+ lines.push("<running_agents>");
209
+ for (const child of children) {
210
+ const summaryPart = child.summary ? ` | summary: ${child.summary}` : "";
211
+ lines.push(`- id: ${child.id} | agent: ${child.agent} | state: ${child.state}${summaryPart}`);
212
+ }
213
+ lines.push("</running_agents>");
214
+ }
215
+ if (lines.length === 0) {
216
+ lines.push("No active or available interactive subagents.");
217
+ }
218
+ return textToolResult(lines.join("\n"), {
219
+ ...(interactiveAgents.length > 0 ? { interactiveAgents } : {}),
220
+ runningAgents: children,
221
+ });
107
222
  },
108
223
  };
109
224
  const stop = {
110
- name: "stop_subagent",
111
- label: "Stop subagent",
225
+ name: "stop_agent",
226
+ namespace: SUBAGENTS_NAMESPACE,
227
+ label: "Stop agent",
112
228
  description: STOP_DESCRIPTION,
113
229
  parameters: idSchema,
114
- async execute(_id, params, signal) {
115
- return result(await manager.stop(params.id, signal));
230
+ defaultActive: false,
231
+ annotations: {
232
+ destructiveHint: true,
233
+ },
234
+ async execute(_id, params) {
235
+ return result(await manager.stop(params.id));
116
236
  },
117
237
  };
118
- pi.registerTool(tui.frame(spawn, { summary: (args) => args.agent }));
119
- pi.registerTool(tui.frame(send, { summary: (args) => args.id }));
120
- pi.registerTool(tui.frame(get, { summary: (args) => args.id }));
121
- pi.registerTool(tui.frame(list, { summary: () => "owned children" }));
122
- pi.registerTool(tui.frame(stop, { summary: (args) => args.id }));
238
+ pi.registerTool(tui.frame(spawn, {
239
+ summary: (args) => args.agent,
240
+ headerLine: "truncate",
241
+ footer: spawnFooter,
242
+ }));
243
+ pi.registerTool(tui.frame(send, {
244
+ summary: (args) => args.id,
245
+ headerLine: "truncate",
246
+ footer: childStateFooter,
247
+ }));
248
+ pi.registerTool(tui.frame(get, {
249
+ summary: (args) => args.id,
250
+ headerLine: "truncate",
251
+ footer: childStateFooter,
252
+ }));
253
+ pi.registerTool(tui.frame(list, {
254
+ summary: () => "owned children",
255
+ headerLine: "truncate",
256
+ footer: listFooter,
257
+ }));
258
+ pi.registerTool(tui.frame(stop, {
259
+ summary: (args) => args.id,
260
+ headerLine: "truncate",
261
+ footer: childStateFooter,
262
+ }));
123
263
  }
124
- export function registerChildTools(pi, identity, onReportOrOptions) {
125
- const options = typeof onReportOrOptions === "function"
126
- ? { onReport: onReportOrOptions }
127
- : (onReportOrOptions ?? {});
264
+ export function registerChildTools(pi, identity, options) {
265
+ registerToolTuiTrace(pi);
266
+ const tui = getToolTui(pi);
128
267
  const tool = {
129
268
  name: "contact_parent",
130
269
  label: "Contact parent",
@@ -137,22 +276,24 @@ export function registerChildTools(pi, identity, onReportOrOptions) {
137
276
  if (options.isBound !== undefined && !options.isBound(sessionId)) {
138
277
  throw new Error("This Pi session is not the bound subagent session; contact_parent is disabled.");
139
278
  }
279
+ const reason = params.reason ?? "blocked";
140
280
  const details = {
141
281
  type: "pi_subagent_report",
142
282
  parentSessionId: identity.parentSessionId,
143
283
  childId: identity.subagentId,
144
284
  runtimeIdentity: identity.runtimeIdentity,
145
- reason: params.reason,
285
+ reason,
146
286
  message: params.message,
147
287
  sessionId,
148
288
  };
149
- await sendReportToRunner(identity, details, signal);
150
- options.onReport?.();
151
- return {
152
- content: [{ type: "text", text: "Report queued for the parent." }],
153
- details,
154
- };
289
+ await options.report("contact_parent", details, signal);
290
+ return textToolResult(reason === "error"
291
+ ? "Error report delivered to the parent."
292
+ : "Blocker report queued for the parent.", details);
155
293
  },
156
294
  };
157
- pi.registerTool(tool);
295
+ pi.registerTool(tui.frame(tool, {
296
+ summary: (args) => args.message,
297
+ headerLine: "truncate",
298
+ }));
158
299
  }
package/dist/widget.d.ts CHANGED
@@ -1,16 +1,12 @@
1
1
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
- import type { PublicSubagent } from "./domain.js";
2
+ import { type PublicSubagent, type SubagentState } from "./domain.js";
3
+ export declare const SUBAGENT_TERMINAL_DISPLAY_DURATION_MS = 15000;
3
4
  export interface SubagentWidget {
4
5
  refresh(children: readonly PublicSubagent[]): void;
5
6
  dispose(): void;
6
7
  }
7
- export declare function isWidgetVisibleChild(child: PublicSubagent): boolean;
8
+ export declare function isTerminalState(state: SubagentState): boolean;
9
+ export declare function isWidgetVisibleChild(child: PublicSubagent, nowMs?: number): boolean;
8
10
  export declare function formatElapsed(fromIso: string, nowMs?: number): string;
9
11
  export declare function renderSubagentWidget(children: readonly PublicSubagent[], width: number, theme: Theme, nowMs?: number): string[];
10
12
  export declare function createSubagentWidget(pi: ExtensionAPI, context: ExtensionContext, signal: AbortSignal, initial?: readonly PublicSubagent[]): SubagentWidget | undefined;
11
- export interface ChildIdentityWidgetOptions {
12
- readonly agent: string;
13
- readonly toolCount: number;
14
- }
15
- export declare function renderChildIdentityWidget(options: ChildIdentityWidgetOptions, width: number, theme: Theme): string[];
16
- export declare function createChildIdentityWidget(pi: ExtensionAPI, context: ExtensionContext, signal: AbortSignal, options: ChildIdentityWidgetOptions): SubagentWidget | undefined;
package/dist/widget.js CHANGED
@@ -1,32 +1,26 @@
1
1
  import { truncateToWidth } from "@earendil-works/pi-tui";
2
2
  import { registerWidget } from "@hheei/pi-ext-core";
3
+ import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE, } from "./domain.js";
3
4
  const WIDGET_ID = "@hheei/pi-subagents:status";
4
- const CHILD_WIDGET_ID = "@hheei/pi-subagents:child-identity";
5
5
  const MAX_ROWS = 8;
6
6
  const ELAPSED_TICK_MS = 1_000;
7
- export function isWidgetVisibleChild(child) {
8
- return child.state === "starting" || child.state === "running" || child.state === "idle";
7
+ export const SUBAGENT_TERMINAL_DISPLAY_DURATION_MS = 15_000;
8
+ export function isTerminalState(state) {
9
+ return state === "done" || state === "blocked" || state === "error";
9
10
  }
10
- function visibleChildren(children) {
11
- return children.filter(isWidgetVisibleChild);
12
- }
13
- function stateGlyph(state, theme) {
14
- if (state === "running")
15
- return theme.fg("accent", "◐");
16
- if (state === "starting")
17
- return theme.fg("muted", "○");
18
- return theme.fg("dim", "○");
19
- }
20
- function stateLabel(child, theme) {
21
- const mode = child.mode === "tui" ? ` ${child.state} tui` : ` ${child.state}`;
22
- const freshness = child.freshness === "last_known" ? " last known" : "";
23
- const label = `${mode.trim()}${freshness}`;
24
- if (child.state === "running" && child.freshness === "live")
25
- return theme.fg("accent", label);
26
- if (child.interrupted !== undefined || child.freshness === "last_known") {
27
- return theme.fg("warning", label);
11
+ export function isWidgetVisibleChild(child, nowMs = Date.now()) {
12
+ const visual = toVisualSubagentState(child.state, child.interrupted);
13
+ if (visual === "running") {
14
+ return true;
28
15
  }
29
- return theme.fg("dim", label);
16
+ const updated = Date.parse(child.updatedAt);
17
+ if (!Number.isNaN(updated) && nowMs - updated < SUBAGENT_TERMINAL_DISPLAY_DURATION_MS) {
18
+ return true;
19
+ }
20
+ return false;
21
+ }
22
+ function visibleChildren(children, nowMs = Date.now()) {
23
+ return children.filter((c) => isWidgetVisibleChild(c, nowMs));
30
24
  }
31
25
  export function formatElapsed(fromIso, nowMs = Date.now()) {
32
26
  const from = Date.parse(fromIso);
@@ -43,30 +37,42 @@ export function formatElapsed(fromIso, nowMs = Date.now()) {
43
37
  return `${minutes}m ${seconds}s`;
44
38
  }
45
39
  export function renderSubagentWidget(children, width, theme, nowMs = Date.now()) {
46
- const visible = visibleChildren(children);
40
+ const visible = visibleChildren(children, nowMs);
47
41
  if (visible.length === 0)
48
42
  return [];
49
- const running = visible.filter((child) => child.state === "running").length;
50
- const headingColor = running > 0 ? "accent" : "dim";
51
- const heading = `${theme.fg(headingColor, running > 0 ? "●" : "○")} ${theme.fg("text", `Subagents (${visible.length})`)}`;
52
- const lines = [truncateToWidth(heading, width, "...")];
43
+ const heading = `🤖 ${theme.fg("text", "Agents:")}`;
44
+ const lines = [truncateToWidth(heading, width, theme.fg("dim", "…"))];
53
45
  const rows = visible.slice(0, MAX_ROWS);
46
+ const sep = theme.fg("dim", " · ");
54
47
  for (let index = 0; index < rows.length; index++) {
55
48
  const child = rows[index];
56
49
  if (child === undefined)
57
50
  continue;
58
- const last = index === rows.length - 1 && visible.length <= MAX_ROWS;
59
- const branch = theme.fg("dim", last ? "└─" : "├─");
60
- const name = theme.fg("text", child.displayName ?? child.agent);
51
+ const visual = toVisualSubagentState(child.state, child.interrupted);
52
+ const glyph = VISUAL_SUBAGENT_GLYPH[visual];
53
+ const tone = VISUAL_SUBAGENT_TONE[visual];
54
+ const glyphStyled = theme.fg(tone, glyph);
55
+ const idStyled = theme.fg("dim", `#${child.id}`);
56
+ const nameStyled = theme.fg("text", child.displayName ?? child.agent);
61
57
  const elapsed = formatElapsed(child.createdAt, nowMs);
62
- const age = elapsed === "" ? "" : ` ${theme.fg("dim", `· ${elapsed}`)}`;
63
- const snippet = child.summary === undefined ? "" : child.summary.replace(/\s+/g, " ").trim();
64
- const summary = snippet === "" ? "" : ` ${theme.fg("dim", snippet)}`;
65
- const id = theme.fg("dim", child.id);
66
- lines.push(truncateToWidth(`${branch} ${stateGlyph(child.state, theme)} ${name} ${stateLabel(child, theme)}${age}${summary} ${id}`, width, "..."));
58
+ const timeStyled = theme.fg("dim", elapsed === "" ? "0s" : elapsed);
59
+ const stateStyled = theme.fg(tone, visual);
60
+ const turnsCount = child.usage?.turns ?? 0;
61
+ const turnsLabel = `${turnsCount} turn${turnsCount === 1 ? "" : "s"}`;
62
+ const turnsStyled = theme.fg("dim", turnsLabel);
63
+ let detailText = "";
64
+ if (child.activeTool !== undefined && child.activeTool.trim() !== "") {
65
+ detailText = child.activeTool.trim();
66
+ }
67
+ else if (child.summary !== undefined && child.summary.trim() !== "") {
68
+ detailText = child.summary.replace(/\s+/g, " ").trim();
69
+ }
70
+ const detailStyled = detailText === "" ? "" : `${sep}${theme.fg("dim", detailText)}`;
71
+ const rowContent = `${glyphStyled} ${idStyled} ${nameStyled}${sep}${timeStyled}${sep}${stateStyled}${sep}${turnsStyled}${detailStyled}`;
72
+ lines.push(truncateToWidth(rowContent, width, theme.fg("dim", "…")));
67
73
  }
68
74
  if (visible.length > MAX_ROWS) {
69
- lines.push(truncateToWidth(theme.fg("dim", `└─ +${visible.length - MAX_ROWS} more`), width, "..."));
75
+ lines.push(truncateToWidth(theme.fg("dim", `+${visible.length - MAX_ROWS} more`), width, theme.fg("dim", "…")));
70
76
  }
71
77
  lines.push("");
72
78
  return lines;
@@ -99,10 +105,22 @@ export function createSubagentWidget(pi, context, signal, initial = []) {
99
105
  stopTick();
100
106
  return;
101
107
  }
108
+ // 仅在有进行中任务或待淡出任务时维持 tick,纯 idle 时静默
109
+ const hasActiveOrExpiring = children.some((child) => isWidgetVisibleChild(child));
110
+ if (!hasActiveOrExpiring) {
111
+ stopTick();
112
+ return;
113
+ }
102
114
  if (tick !== undefined)
103
115
  return;
104
116
  tick = setInterval(() => {
105
- widget.requestRender();
117
+ if (!hasRows()) {
118
+ widget.setVisible(false);
119
+ stopTick();
120
+ }
121
+ else {
122
+ widget.requestRender();
123
+ }
106
124
  }, ELAPSED_TICK_MS);
107
125
  };
108
126
  syncTick();
@@ -129,30 +147,3 @@ export function createSubagentWidget(pi, context, signal, initial = []) {
129
147
  },
130
148
  };
131
149
  }
132
- export function renderChildIdentityWidget(options, width, theme) {
133
- const label = options.agent.trim() === "" ? "child" : options.agent.trim();
134
- const line = `${theme.fg("accent", `[${label}]`)} ${theme.fg("dim", "child")} ${theme.fg("muted", "report via contact_parent")} ${theme.fg("dim", `${options.toolCount} tools`)}`;
135
- return [truncateToWidth(line, width, "..."), ""];
136
- }
137
- export function createChildIdentityWidget(pi, context, signal, options) {
138
- if (context.mode !== "tui")
139
- return undefined;
140
- const widget = registerWidget(pi, context, signal, {
141
- id: CHILD_WIDGET_ID,
142
- placement: "aboveEditor",
143
- visible: true,
144
- create: (_tui, theme) => ({
145
- render: (width) => renderChildIdentityWidget(options, width, theme),
146
- invalidate: () => undefined,
147
- dispose: () => undefined,
148
- }),
149
- });
150
- return {
151
- refresh(_next) {
152
- widget.requestRender();
153
- },
154
- dispose() {
155
- widget.dispose();
156
- },
157
- };
158
- }