@hheei/pi-subagents 0.1.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 (53) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -0
  3. package/dist/agent-resolver.d.ts +37 -0
  4. package/dist/agent-resolver.js +232 -0
  5. package/dist/child-bridge.d.ts +41 -0
  6. package/dist/child-bridge.js +214 -0
  7. package/dist/child-nudge.d.ts +24 -0
  8. package/dist/child-nudge.js +102 -0
  9. package/dist/commands.d.ts +6 -0
  10. package/dist/commands.js +155 -0
  11. package/dist/connector.d.ts +53 -0
  12. package/dist/connector.js +283 -0
  13. package/dist/diagnostics.d.ts +10 -0
  14. package/dist/diagnostics.js +20 -0
  15. package/dist/domain.d.ts +192 -0
  16. package/dist/domain.js +63 -0
  17. package/dist/extension.d.ts +2 -0
  18. package/dist/extension.js +145 -0
  19. package/dist/host-adapter.d.ts +76 -0
  20. package/dist/host-adapter.js +323 -0
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +1 -0
  23. package/dist/json-lines.d.ts +14 -0
  24. package/dist/json-lines.js +94 -0
  25. package/dist/launch-spec.d.ts +47 -0
  26. package/dist/launch-spec.js +149 -0
  27. package/dist/manager.d.ts +101 -0
  28. package/dist/manager.js +802 -0
  29. package/dist/pause-gate.d.ts +14 -0
  30. package/dist/pause-gate.js +64 -0
  31. package/dist/protocol.d.ts +141 -0
  32. package/dist/protocol.js +168 -0
  33. package/dist/registry.d.ts +41 -0
  34. package/dist/registry.js +575 -0
  35. package/dist/rpc-adapter.d.ts +40 -0
  36. package/dist/rpc-adapter.js +194 -0
  37. package/dist/rpc-events.d.ts +8 -0
  38. package/dist/rpc-events.js +29 -0
  39. package/dist/runner-entry.d.ts +24 -0
  40. package/dist/runner-entry.js +166 -0
  41. package/dist/runner.d.ts +52 -0
  42. package/dist/runner.js +930 -0
  43. package/dist/runtime.d.ts +22 -0
  44. package/dist/runtime.js +253 -0
  45. package/dist/session-bootstrap.d.ts +69 -0
  46. package/dist/session-bootstrap.js +195 -0
  47. package/dist/state.d.ts +15 -0
  48. package/dist/state.js +168 -0
  49. package/dist/tools.d.ts +9 -0
  50. package/dist/tools.js +158 -0
  51. package/dist/widget.d.ts +16 -0
  52. package/dist/widget.js +158 -0
  53. package/package.json +53 -0
package/dist/state.js ADDED
@@ -0,0 +1,168 @@
1
+ const EMPTY_USAGE = {
2
+ inputTokens: 0,
3
+ outputTokens: 0,
4
+ cacheReadTokens: 0,
5
+ cacheWriteTokens: 0,
6
+ costUsd: 0,
7
+ turns: 0,
8
+ };
9
+ function record(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value)
11
+ ? Object.fromEntries(Object.entries(value))
12
+ : undefined;
13
+ }
14
+ function finite(value) {
15
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
16
+ }
17
+ function assistant(entry) {
18
+ const item = record(entry);
19
+ const message = record(item?.message);
20
+ return item?.type === "message" && typeof item.id === "string" && message?.role === "assistant"
21
+ ? { id: item.id, message }
22
+ : undefined;
23
+ }
24
+ function text(message) {
25
+ if (!Array.isArray(message.content))
26
+ return undefined;
27
+ const value = message.content
28
+ .map((part) => {
29
+ const item = record(part);
30
+ return item?.type === "text" && typeof item.text === "string" ? item.text : "";
31
+ })
32
+ .join("")
33
+ .trim();
34
+ return value === "" ? undefined : value;
35
+ }
36
+ function completed(message) {
37
+ return message.stopReason !== "pending" && message.stopReason !== "deferred";
38
+ }
39
+ function diagnostic(message) {
40
+ if (message.stopReason === "aborted")
41
+ return "Assistant turn was interrupted";
42
+ if (message.stopReason !== "error")
43
+ return undefined;
44
+ return typeof message.errorMessage === "string" && message.errorMessage !== ""
45
+ ? message.errorMessage
46
+ : "Assistant turn failed";
47
+ }
48
+ function messageUsage(message) {
49
+ const usage = record(message.usage);
50
+ const inputTokens = finite(usage?.input);
51
+ const outputTokens = finite(usage?.output);
52
+ const cacheReadTokens = finite(usage?.cacheRead);
53
+ const cacheWriteTokens = finite(usage?.cacheWrite);
54
+ if (inputTokens === undefined ||
55
+ outputTokens === undefined ||
56
+ cacheReadTokens === undefined ||
57
+ cacheWriteTokens === undefined)
58
+ return undefined;
59
+ return {
60
+ inputTokens,
61
+ outputTokens,
62
+ cacheReadTokens,
63
+ cacheWriteTokens,
64
+ costUsd: finite(record(usage?.cost)?.total) ?? null,
65
+ };
66
+ }
67
+ function add(total, next) {
68
+ return {
69
+ inputTokens: total.inputTokens + next.inputTokens,
70
+ outputTokens: total.outputTokens + next.outputTokens,
71
+ cacheReadTokens: total.cacheReadTokens + next.cacheReadTokens,
72
+ cacheWriteTokens: total.cacheWriteTokens + next.cacheWriteTokens,
73
+ costUsd: total.costUsd === null || next.costUsd === null ? null : total.costUsd + next.costUsd,
74
+ turns: total.turns + 1,
75
+ };
76
+ }
77
+ export function summarizeCurrentBranch(entries) {
78
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
79
+ const entry = entries[index];
80
+ if (entry === undefined)
81
+ continue;
82
+ const item = assistant(entry);
83
+ if (item === undefined || !completed(item.message))
84
+ continue;
85
+ const value = text(item.message);
86
+ if (value !== undefined)
87
+ return value;
88
+ }
89
+ return undefined;
90
+ }
91
+ export function aggregateUsage(entries) {
92
+ let total = EMPTY_USAGE;
93
+ const seen = new Set();
94
+ for (const entry of entries) {
95
+ const item = assistant(entry);
96
+ if (item === undefined || seen.has(item.id) || !completed(item.message))
97
+ continue;
98
+ seen.add(item.id);
99
+ const usage = messageUsage(item.message);
100
+ if (usage !== undefined)
101
+ total = add(total, usage);
102
+ }
103
+ return total;
104
+ }
105
+ export function createStateProjector(initialState = "starting") {
106
+ let state = initialState;
107
+ let summary;
108
+ let interrupted;
109
+ let usage = EMPTY_USAGE;
110
+ const snapshot = () => ({
111
+ state,
112
+ ...(summary === undefined ? {} : { summary }),
113
+ ...(interrupted === undefined ? {} : { interrupted }),
114
+ usage,
115
+ });
116
+ return {
117
+ applyEvent(event) {
118
+ const value = record(event);
119
+ if (value === undefined)
120
+ return snapshot();
121
+ if (value.type === "agent_start")
122
+ state = "running";
123
+ else if (value.type === "agent_end" || value.type === "agent_settled") {
124
+ state = "idle";
125
+ const messages = Array.isArray(value.messages) ? value.messages : [];
126
+ const message = record(messages.length > 0 ? messages[messages.length - 1] : value.message);
127
+ if (message?.role === "assistant") {
128
+ summary = text(message) ?? summary;
129
+ interrupted = diagnostic(message) ?? interrupted;
130
+ if (message.stopReason === "error")
131
+ state = "failed";
132
+ }
133
+ }
134
+ else if (value.type === "error") {
135
+ state = "failed";
136
+ interrupted =
137
+ typeof value.message === "string" ? value.message : "Runner reported an error";
138
+ }
139
+ else if (value.type === "runner_events_dropped") {
140
+ const count = typeof value.count === "number" ? value.count : undefined;
141
+ interrupted =
142
+ count === undefined
143
+ ? "Runner dropped events; live state may be stale"
144
+ : `Runner dropped ${count} events; live state may be stale`;
145
+ }
146
+ else if (value.type === "runner_exit" || value.type === "exit") {
147
+ state = value.code === 0 ? "done" : "failed";
148
+ if (value.code !== 0)
149
+ interrupted = `Runner exited${typeof value.code === "number" ? ` with code ${value.code}` : " unexpectedly"}`;
150
+ }
151
+ return snapshot();
152
+ },
153
+ rebuild(entries) {
154
+ summary = summarizeCurrentBranch(entries);
155
+ usage = aggregateUsage(entries);
156
+ for (const entry of entries) {
157
+ const item = assistant(entry);
158
+ if (item === undefined || !completed(item.message))
159
+ continue;
160
+ interrupted = diagnostic(item.message) ?? interrupted;
161
+ if (item.message.stopReason === "error")
162
+ state = "failed";
163
+ }
164
+ return snapshot();
165
+ },
166
+ snapshot,
167
+ };
168
+ }
@@ -0,0 +1,9 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { ChildIdentity } from "./domain.js";
3
+ import type { SubagentManager } from "./manager.js";
4
+ export declare function registerParentTools(pi: ExtensionAPI, manager: SubagentManager): void;
5
+ export interface RegisterChildToolsOptions {
6
+ readonly onReport?: () => void;
7
+ readonly isBound?: (sessionId: string) => boolean;
8
+ }
9
+ export declare function registerChildTools(pi: ExtensionAPI, identity: ChildIdentity, onReportOrOptions?: (() => void) | RegisterChildToolsOptions): void;
package/dist/tools.js ADDED
@@ -0,0 +1,158 @@
1
+ import { getToolTui, registerToolTuiTrace } from "@hheei/pi-ext-core";
2
+ import { Type } from "typebox";
3
+ import { sendReportToRunner } from "./connector.js";
4
+ import { isRecord } from "./domain.js";
5
+ const spawnSchema = Type.Object({
6
+ task: Type.String({ minLength: 1 }),
7
+ agent: Type.String({ minLength: 1 }),
8
+ cwd: Type.Optional(Type.String()),
9
+ });
10
+ const sendSchema = Type.Object({
11
+ id: Type.String({ minLength: 1 }),
12
+ message: Type.String({ minLength: 1 }),
13
+ mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up"), Type.Literal("auto")])),
14
+ });
15
+ const idSchema = Type.Object({ id: Type.String({ minLength: 1 }) });
16
+ 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 }),
24
+ });
25
+ 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
+ function result(value) {
33
+ if (isOperationError(value))
34
+ throw new Error(JSON.stringify(value));
35
+ return {
36
+ content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }],
37
+ details: value,
38
+ };
39
+ }
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.";
42
+ 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.",
47
+ "Do not fabricate, assume, or summarize the child's results before a report arrives.",
48
+ ];
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.";
53
+ 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.";
56
+ 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.",
60
+ ];
61
+ export function registerParentTools(pi, manager) {
62
+ registerToolTuiTrace(pi);
63
+ const tui = getToolTui(pi);
64
+ const spawn = {
65
+ name: "spawn_subagent",
66
+ label: "Spawn subagent",
67
+ description: SPAWN_DESCRIPTION,
68
+ promptSnippet: SPAWN_SNIPPET,
69
+ promptGuidelines: [...SPAWN_GUIDELINES],
70
+ parameters: spawnSchema,
71
+ async execute(_id, params) {
72
+ return result(await manager.spawn(params));
73
+ },
74
+ };
75
+ const send = {
76
+ name: "send_subagent",
77
+ label: "Send to subagent",
78
+ description: SEND_DESCRIPTION,
79
+ promptSnippet: SEND_SNIPPET,
80
+ 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.",
83
+ ],
84
+ parameters: sendSchema,
85
+ async execute(_id, params, signal) {
86
+ return result(await manager.send(params.id, params.message, params.mode, signal));
87
+ },
88
+ };
89
+ const get = {
90
+ name: "get_subagent",
91
+ label: "Get subagent",
92
+ description: GET_DESCRIPTION,
93
+ promptSnippet: GET_DESCRIPTION,
94
+ parameters: idSchema,
95
+ async execute(_id, params) {
96
+ return result(await manager.get(params.id));
97
+ },
98
+ };
99
+ const list = {
100
+ name: "list_subagents",
101
+ label: "List subagents",
102
+ description: LIST_DESCRIPTION,
103
+ promptSnippet: LIST_DESCRIPTION,
104
+ parameters: emptySchema,
105
+ async execute() {
106
+ return result(await manager.list());
107
+ },
108
+ };
109
+ const stop = {
110
+ name: "stop_subagent",
111
+ label: "Stop subagent",
112
+ description: STOP_DESCRIPTION,
113
+ parameters: idSchema,
114
+ async execute(_id, params, signal) {
115
+ return result(await manager.stop(params.id, signal));
116
+ },
117
+ };
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 }));
123
+ }
124
+ export function registerChildTools(pi, identity, onReportOrOptions) {
125
+ const options = typeof onReportOrOptions === "function"
126
+ ? { onReport: onReportOrOptions }
127
+ : (onReportOrOptions ?? {});
128
+ const tool = {
129
+ name: "contact_parent",
130
+ label: "Contact parent",
131
+ description: CONTACT_DESCRIPTION,
132
+ promptSnippet: CONTACT_SNIPPET,
133
+ promptGuidelines: [...CONTACT_GUIDELINES],
134
+ parameters: contactSchema,
135
+ async execute(_id, params, signal, _onUpdate, ctx) {
136
+ const sessionId = ctx.sessionManager.getSessionId();
137
+ if (options.isBound !== undefined && !options.isBound(sessionId)) {
138
+ throw new Error("This Pi session is not the bound subagent session; contact_parent is disabled.");
139
+ }
140
+ const details = {
141
+ type: "pi_subagent_report",
142
+ parentSessionId: identity.parentSessionId,
143
+ childId: identity.subagentId,
144
+ runtimeIdentity: identity.runtimeIdentity,
145
+ reason: params.reason,
146
+ message: params.message,
147
+ sessionId,
148
+ };
149
+ await sendReportToRunner(identity, details, signal);
150
+ options.onReport?.();
151
+ return {
152
+ content: [{ type: "text", text: "Report queued for the parent." }],
153
+ details,
154
+ };
155
+ },
156
+ };
157
+ pi.registerTool(tool);
158
+ }
@@ -0,0 +1,16 @@
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import type { PublicSubagent } from "./domain.js";
3
+ export interface SubagentWidget {
4
+ refresh(children: readonly PublicSubagent[]): void;
5
+ dispose(): void;
6
+ }
7
+ export declare function isWidgetVisibleChild(child: PublicSubagent): boolean;
8
+ export declare function formatElapsed(fromIso: string, nowMs?: number): string;
9
+ export declare function renderSubagentWidget(children: readonly PublicSubagent[], width: number, theme: Theme, nowMs?: number): string[];
10
+ 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 ADDED
@@ -0,0 +1,158 @@
1
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { registerWidget } from "@hheei/pi-ext-core";
3
+ const WIDGET_ID = "@hheei/pi-subagents:status";
4
+ const CHILD_WIDGET_ID = "@hheei/pi-subagents:child-identity";
5
+ const MAX_ROWS = 8;
6
+ const ELAPSED_TICK_MS = 1_000;
7
+ export function isWidgetVisibleChild(child) {
8
+ return child.state === "starting" || child.state === "running" || child.state === "idle";
9
+ }
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);
28
+ }
29
+ return theme.fg("dim", label);
30
+ }
31
+ export function formatElapsed(fromIso, nowMs = Date.now()) {
32
+ const from = Date.parse(fromIso);
33
+ if (Number.isNaN(from))
34
+ return "";
35
+ const totalSeconds = Math.max(0, Math.floor((nowMs - from) / 1000));
36
+ if (totalSeconds < 60)
37
+ return `${totalSeconds}s`;
38
+ const hours = Math.floor(totalSeconds / 3600);
39
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
40
+ const seconds = totalSeconds % 60;
41
+ if (hours > 0)
42
+ return `${hours}h ${minutes}m`;
43
+ return `${minutes}m ${seconds}s`;
44
+ }
45
+ export function renderSubagentWidget(children, width, theme, nowMs = Date.now()) {
46
+ const visible = visibleChildren(children);
47
+ if (visible.length === 0)
48
+ 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, "...")];
53
+ const rows = visible.slice(0, MAX_ROWS);
54
+ for (let index = 0; index < rows.length; index++) {
55
+ const child = rows[index];
56
+ if (child === undefined)
57
+ 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);
61
+ 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, "..."));
67
+ }
68
+ if (visible.length > MAX_ROWS) {
69
+ lines.push(truncateToWidth(theme.fg("dim", `└─ +${visible.length - MAX_ROWS} more`), width, "..."));
70
+ }
71
+ lines.push("");
72
+ return lines;
73
+ }
74
+ export function createSubagentWidget(pi, context, signal, initial = []) {
75
+ if (context.mode !== "tui")
76
+ return undefined;
77
+ let children = initial;
78
+ let disposed = false;
79
+ let tick;
80
+ const hasRows = () => visibleChildren(children).length > 0;
81
+ const widget = registerWidget(pi, context, signal, {
82
+ id: WIDGET_ID,
83
+ placement: "aboveEditor",
84
+ visible: hasRows(),
85
+ create: (_tui, theme) => ({
86
+ render: (width) => renderSubagentWidget(children, width, theme),
87
+ invalidate: () => undefined,
88
+ dispose: () => undefined,
89
+ }),
90
+ });
91
+ const stopTick = () => {
92
+ if (tick === undefined)
93
+ return;
94
+ clearInterval(tick);
95
+ tick = undefined;
96
+ };
97
+ const syncTick = () => {
98
+ if (disposed || !hasRows()) {
99
+ stopTick();
100
+ return;
101
+ }
102
+ if (tick !== undefined)
103
+ return;
104
+ tick = setInterval(() => {
105
+ widget.requestRender();
106
+ }, ELAPSED_TICK_MS);
107
+ };
108
+ syncTick();
109
+ signal.addEventListener("abort", stopTick, { once: true });
110
+ return {
111
+ refresh(next) {
112
+ if (disposed)
113
+ return;
114
+ children = next;
115
+ if (!hasRows())
116
+ widget.setVisible(false);
117
+ else {
118
+ widget.setVisible(true);
119
+ widget.requestRender();
120
+ }
121
+ syncTick();
122
+ },
123
+ dispose() {
124
+ if (disposed)
125
+ return;
126
+ disposed = true;
127
+ stopTick();
128
+ widget.dispose();
129
+ },
130
+ };
131
+ }
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
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@hheei/pi-subagents",
3
+ "version": "0.1.2",
4
+ "description": "Independent background Pi subagent sessions with durable identity and RPC control.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "pi": {
17
+ "extensions": [
18
+ "dist/extension.js"
19
+ ]
20
+ },
21
+ "scripts": {
22
+ "prepack": "pnpm run build",
23
+ "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
24
+ },
25
+ "keywords": [
26
+ "pi",
27
+ "pi-package",
28
+ "pi-extension",
29
+ "hepi",
30
+ "subagents"
31
+ ],
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/hheei/hepi-mono.git",
36
+ "directory": "packages/pi-subagents"
37
+ },
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "dependencies": {
42
+ "@hheei/pi-ext-core": "workspace:^0.1.1",
43
+ "typebox": "1.3.27"
44
+ },
45
+ "peerDependencies": {
46
+ "@earendil-works/pi-coding-agent": ">=0.87.0",
47
+ "@earendil-works/pi-tui": ">=0.87.0"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "0.87.0",
51
+ "@earendil-works/pi-tui": "0.87.0"
52
+ }
53
+ }