@ryan_nookpi/pi-extension-subagent 0.3.2 → 0.3.4

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 CHANGED
@@ -1,55 +1,21 @@
1
1
  # subagent
2
2
 
3
- Asynchronous subagent delegation for [pi](https://github.com/earendil-works/pi). Run specialist agents in dedicated child sessions, optionally pass selected main-session context, and receive results as follow-up messages.
4
-
5
- The primary interface is **CLI-style**: one `subagent` tool accepts a command string with verbs, options, and a `--` task separator, such as `subagent run worker --isolated -- review this change`. This provides one consistent grammar for single runs, continuation, parallel batches, sequential chains, inspection, and cleanup. These strings are tool input, not shell commands.
3
+ Asynchronous subagent delegation for [pi](https://github.com/earendil-works/pi). Run specialist agents in dedicated child sessions, share main-session context when needed, and receive results as follow-up messages.
6
4
 
7
5
  > [!WARNING]
8
- > Subagents run headlessly without approval prompts. Claude-runtime agents use permission bypass, and pi-runtime agents can use every tool listed in their agent definition. This extension is not a sandbox. Use it only in trusted repositories with trusted prompts and agent definitions.
9
-
10
- ## Requirements
11
-
12
- - pi 0.80.6 or later (tested with 0.80.6)
13
- - For `runtime: claude` with the default `claudeRuntime: "sdk"`: supported Anthropic authentication such as `ANTHROPIC_API_KEY`; see the [official Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview)
14
- - For `runtime: claude` with `claudeRuntime: "cli"`: the `claude` executable on `PATH` and an authenticated Claude Code installation
6
+ > Subagents run headlessly without approval prompts. Claude-runtime agents bypass permissions, and pi-runtime agents can use every tool declared by their agent definition. This extension is not a sandbox. Use trusted repositories, prompts, and agent definitions only.
15
7
 
16
8
  ## Install
17
9
 
10
+ Requires pi 0.80.6 or later. Compatibility is tested with pi 0.80.7.
11
+
18
12
  ```bash
19
13
  pi install npm:@ryan_nookpi/pi-extension-subagent
20
14
  ```
21
15
 
22
16
  ## Quick start
23
17
 
24
- ### 1. Discover or seed agents
25
-
26
- Run this from the interactive pi UI:
27
-
28
- ```text
29
- /subagents
30
- ```
31
-
32
- If no agent definitions exist in any discovery location, the extension offers an optional starter pack containing:
33
-
34
- - Nine portable English agents: `browser`, `challenger`, `code-cleaner`, `reviewer`, `searcher`, `security-auditor`, `simplifier`, `verifier`, and `worker`
35
- - The `stress-interview` and `self-healing` skills, written in English and validated against the [Agent Skills specification](https://agentskills.io/specification)
36
- - Missing global `subagent` settings: `defaultAgent: "worker"`, `claudeRuntime: "cli"`, and symbol mappings for searcher, challenger, and browser
37
-
38
- Seeded agents intentionally omit model IDs and inherit the user's Pi model. Existing files and configured setting values are never overwritten. If the offer is declined, nothing is recorded or written, so the extension asks again the next time the list is still empty.
39
-
40
- Agents and subagent settings are available immediately after installation. Run `/reload` or start a new Pi session to activate the two newly copied skills. Headless sessions never install automatically; they return instructions to run `/subagents` interactively.
41
-
42
- The same offer is available from either agent-list tool:
43
-
44
- ```json
45
- { "command": "subagent agents" }
46
- ```
47
-
48
- The separate `list-agents` tool behaves the same way. The `subagent ...` examples in this README are **tool command strings**, not terminal commands. Do not run them in Bash.
49
-
50
- ### 2. Or create an agent manually
51
-
52
- Agents are Markdown files with YAML frontmatter. Create `~/.pi/agent/agents/worker.md` for a global agent, or `.pi/agents/worker.md` inside one project:
18
+ Create a global agent at `~/.pi/agent/agents/worker.md`, or a project agent at `.pi/agents/worker.md`:
53
19
 
54
20
  ```markdown
55
21
  ---
@@ -63,18 +29,7 @@ runtime: pi
63
29
  Implement the requested changes and verify them.
64
30
  ```
65
31
 
66
- `name` and `description` are required. Optional fields are:
67
-
68
- - `runtime`: `pi` (default) or `claude`
69
- - `model`: runtime-compatible model ID
70
- - `thinking`: `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`
71
- - `tools`: comma-separated tool names
72
-
73
- Omitted model, thinking, and tools values use that runtime's defaults.
74
-
75
- ### 3. Launch a run
76
-
77
- Interactive user command:
32
+ Then launch it from pi:
78
33
 
79
34
  ```text
80
35
  /sub:isolate worker implement the requested change and run tests
@@ -86,36 +41,54 @@ Equivalent AI tool call:
86
41
  { "command": "subagent run worker --isolated -- implement the requested change and run tests" }
87
42
  ```
88
43
 
89
- Runs are asynchronous in interactive mode. Wait for the automatic completion or failure follow-up instead of immediately polling `status` or `detail`.
44
+ Tool launches are asynchronous. Wait for the automatic completion or failure follow-up instead of polling immediately.
45
+
46
+ ## Agent definitions
47
+
48
+ Agent files use YAML frontmatter followed by the system prompt. `name` and `description` are required.
90
49
 
91
- ## Agent discovery
50
+ | Field | Description |
51
+ | --- | --- |
52
+ | `runtime` | `pi` (default) or `claude` |
53
+ | `model` | Runtime-compatible model ID |
54
+ | `thinking` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max` |
55
+ | `tools` | Comma-separated tool names |
56
+
57
+ Omitted optional fields use runtime defaults.
92
58
 
93
- Definitions are loaded from the following locations. Later sources override earlier agents with the same name:
59
+ Agents are discovered in this order; later definitions override earlier agents with the same name:
94
60
 
95
61
  1. `$PI_CODING_AGENT_DIR/agents/*.md` (normally `~/.pi/agent/agents/*.md`)
96
62
  2. Nearest `.claude/agents/**/*.md`
97
63
  3. Nearest `.pi/agents/*.md`
98
64
 
99
- Project `.claude/agents` files are discovered recursively. Project `.pi/agents` files are discovered only in the selected directory.
65
+ `.claude/agents` is searched recursively. `.pi/agents` is not.
66
+
67
+ Run `/subagents`, `subagent agents`, or the `list-agents` tool to inspect discovered agents.
68
+
69
+ ### Optional starter pack
100
70
 
101
- ## Context modes and lifecycle
71
+ If no agents are found, `/subagents` can offer an optional, opinionated starter pack. It copies nine agent templates, two example workflow skills, and missing global `subagent` settings. Existing files and configured values are not overwritten.
102
72
 
103
- - `--isolated` starts a dedicated child session without copying the main conversation. It is the default for `subagent` tool launches.
104
- - `--main` adds selected main-session context to the child task.
105
- - `/sub:isolate` selects isolated context; `/sub:main` selects main context.
106
- - `>>` and `>` use main-session context.
107
- - Continuing a run preserves its original context mode and child session. Supplying `--main` or `--isolated` to `continue` does not retroactively change it.
73
+ The starter pack is not required. Decline it if you prefer to define agents manually. It fills a missing `claudeRuntime` setting with `cli`; without that setting, the extension default is `sdk`.
108
74
 
109
- Pi replaces and invalidates extension runtimes during `/new`, `/resume`, `/fork`, and reload. Active child processes are therefore aborted during `session_shutdown`, and the old session records why they stopped. Wait for active runs before replacing the parent session. This follows pi's [official extension lifecycle guidance](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#long-lived-resources-and-shutdown).
75
+ ## Context modes
110
76
 
111
- ## CLI-style tool interface
77
+ - `--isolated` starts without copying the main conversation. It is the default for tool launches.
78
+ - `--main` passes selected main-session context to the child.
79
+ - `/sub:isolate` and `/sub:main` provide the same choice for interactive commands.
80
+ - Continuing a run preserves its original child session and context mode.
112
81
 
113
- Instead of registering a separate tool for every operation, the extension exposes a compact CLI-style grammar through one `subagent` tool. The model passes the full command as the tool's `command` parameter; it must not invoke `subagent` from Bash or another shell.
82
+ Active child processes stop when pi replaces or reloads the parent extension runtime. This follows pi's [official extension lifecycle guidance](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#long-lived-resources-and-shutdown).
114
83
 
115
- The extension registers two main-session tools:
84
+ ## Tool interface
116
85
 
117
- - `list-agents`: return discovered agent definitions and runtime settings
118
- - `subagent`: accept one CLI-style command string
86
+ The extension registers two tools:
87
+
88
+ - `list-agents` — list discovered agents and runtime settings
89
+ - `subagent` — execute a CLI-style command string
90
+
91
+ These strings are tool input, not shell commands. Do not run them in Bash.
119
92
 
120
93
  ```text
121
94
  subagent help
@@ -123,15 +96,15 @@ subagent agents
123
96
  subagent runs
124
97
  subagent run <agent> [--main|--isolated] -- <task>
125
98
  subagent continue <runId> [--agent <agent>] [--main|--isolated] -- <task>
126
- subagent batch [--main|--isolated] --agent <agent> --task <task> [--agent <agent> --task <task> ...]
127
- subagent chain [--main|--isolated] --agent <agent> --task <task> [--agent <agent> --task <task> ...]
99
+ subagent batch [--main|--isolated] --agent <agent> --task <task> ...
100
+ subagent chain [--main|--isolated] --agent <agent> --task <task> ...
128
101
  subagent status <runId>
129
102
  subagent detail <runId>
130
103
  subagent abort <runId|runId,runId|all>
131
104
  subagent remove <runId|runId,runId|all>
132
105
  ```
133
106
 
134
- `batch` runs independent tasks in parallel. Quote tasks containing spaces:
107
+ `batch` runs independent tasks in parallel:
135
108
 
136
109
  ```json
137
110
  {
@@ -147,46 +120,51 @@ subagent remove <runId|runId,runId|all>
147
120
  }
148
121
  ```
149
122
 
150
- Use `status` and `detail` only for explicit, one-off inspection. Repeated polling is unnecessary because completion is delivered automatically.
123
+ Use `status` and `detail` for one-off inspection, not polling loops.
151
124
 
152
- ## Slash commands
125
+ ## Interactive commands
153
126
 
154
- - `/subagents` — list discovered agents and settings
155
- - `/sub:main [agent|alias|runId] <task>` — launch with main-session context or continue a run
156
- - `/sub:isolate [agent|alias|runId] <task>` — launch in isolated context or continue a run
157
- - `/sub:peek [runId]` — show the latest response; defaults to the latest run
158
- - `/sub:open [runId]` — open session replay; defaults to the latest run
159
- - `/sub:history` — show all run history, including removed runs
160
- - `/sub:rm [runId]` — remove a run; defaults to the latest and aborts it if necessary
161
- - `/sub:clear [all]` — clear finished runs, or every run with `all`
162
- - `/sub:abort [runId|all]` — abort the latest running run, one run, or all running runs
127
+ | Command | Description |
128
+ | --- | --- |
129
+ | `/subagents` | List agents and offer the starter pack when none exist |
130
+ | `/sub:main [agent\|runId] <task>` | Launch or continue with main-session context |
131
+ | `/sub:isolate [agent\|runId] <task>` | Launch or continue with isolated context |
132
+ | `/sub:peek [runId]` | Show the latest result |
133
+ | `/sub:open [runId]` | Open the child session replay |
134
+ | `/sub:history` | Show run history, including removed runs |
135
+ | `/sub:abort [runId\|all]` | Abort running work |
136
+ | `/sub:rm [runId]` | Remove a run, aborting it first if necessary |
137
+ | `/sub:clear [all]` | Clear finished runs, or all runs |
163
138
 
164
139
  When an agent is omitted, launch commands use `defaultAgent`.
165
140
 
166
- ## Interactive shortcuts
141
+ ### Shortcuts
167
142
 
168
- | Shortcut | Behavior |
143
+ | Shortcut | Description |
169
144
  | --- | --- |
170
- | `>> [agent\|runId] <task>` | Visible run using main-session context |
171
- | `> [agent\|runId] <task>` | Hidden run using main-session context; interactive UI only |
145
+ | `>> [agent\|runId] <task>` | Visible run with main-session context |
146
+ | `> [agent\|runId] <task>` | Hidden run with main-session context |
172
147
  | `#<runId> <task>` | Continue a run |
173
- | `>><symbol> <task>` | Visible run using the agent mapped in `symbolMap` |
174
- | `><symbol> <task>` | Hidden run using the mapped agent |
175
- | `<>runId` | Compact form of `/sub:peek runId` |
176
- | `<< [runId\|runId,runId]` | Abort selected running runs or clear selected finished runs; without arguments, abort the latest running run |
177
- | `<<< [all]` | Clear finished runs; use `all` to clear every run |
148
+ | `>><symbol> <task>` / `><symbol> <task>` | Run an agent from `symbolMap` |
149
+ | `<>runId` | Peek at a result |
150
+ | `<< [runId\|runId,runId]` | Abort running or clear finished runs |
151
+ | `<<< [all]` | Clear finished runs, or all runs |
178
152
 
179
- Hidden runs do not add start or completion messages to the main transcript. Read their output with `/sub:peek`, `<>runId`, or `/sub:open`. A plain `>` shortcut requires a space before its task; configured symbol shortcuts do not.
153
+ Hidden runs are available only in the interactive UI and do not add start or completion messages to the main transcript.
180
154
 
181
- ## Escalation from pi-runtime agents
155
+ ### Prompt mentions
182
156
 
183
- Pi-runtime subagent sessions receive an `ask_master` tool. It lets a child report a decision that the parent must make, then immediately terminates that child run. The parent receives the escalation as a follow-up.
157
+ Use `>agent-name` inside a prompt to reference a discovered agent. Exact names are highlighted and rewritten to `subagent:agent-name` before the main LLM receives the prompt; they do not launch a run directly. Unknown names remain unchanged.
184
158
 
185
- Use `ask_master` only when the child cannot safely proceed, such as before a destructive operation or an unresolved architecture decision. Claude-runtime agents do not receive this tool; they report blockers in their final text instead.
159
+ ```text
160
+ Delegate implementation to >worker and review to >reviewer.
161
+ ```
162
+
163
+ A mention has no space after `>`. Launch shortcuts remain separate, such as `> worker implement this`.
186
164
 
187
165
  ## Configuration
188
166
 
189
- Global configuration belongs under `subagent` in `$PI_CODING_AGENT_DIR/settings.json` (normally `~/.pi/agent/settings.json`):
167
+ Global settings belong under `subagent` in `$PI_CODING_AGENT_DIR/settings.json`:
190
168
 
191
169
  ```json
192
170
  {
@@ -212,65 +190,44 @@ A nearest project `.pi/subagent.json` overrides global values:
212
190
  }
213
191
  ```
214
192
 
215
- - `claudeRuntime`: `sdk` (default) or `cli`; applies only to agents with `runtime: claude`
216
- - `defaultAgent`: agent used when a launch omits its agent; defaults to `worker` and must match a discovered definition
217
- - `symbolMap`: one-character shortcuts mapped to non-empty agent names; a valid project map replaces the global map as a whole, while a malformed project map falls back to the valid global map
193
+ - `claudeRuntime`: `sdk` (default) or `cli`; applies only to `runtime: claude`
194
+ - `defaultAgent`: used when an interactive launch omits the agent; defaults to `worker`
195
+ - `symbolMap`: one-character shortcuts mapped to agent names; a valid project map replaces the global map
218
196
 
219
- ### Context guard override
220
-
221
- `PI_SUBAGENT_CONTEXT_GUARD_TOKENS` overrides the proactive context limit for pi-runtime children. Set it to a positive integer to apply that ceiling to every pi model. Set it to `0` or an empty value to disable the proactive guard and rely on native compaction or provider overflow handling.
222
-
223
- ## Troubleshooting
197
+ Set `PI_SUBAGENT_CONTEXT_GUARD_TOKENS` to a positive integer to override the proactive context limit for pi-runtime children. Set it to `0` or an empty value to disable the guard.
224
198
 
225
- ### `Configured defaultAgent "worker" was not found`
199
+ ## Claude runtime
226
200
 
227
- Create a `worker` definition from the quick start, choose an existing agent explicitly, or change `defaultAgent`. Run `/subagents` to verify discovery before launching.
201
+ The default Claude runtime uses the Claude Agent SDK and requires supported Anthropic authentication such as `ANTHROPIC_API_KEY`; see the [official Claude Agent SDK documentation](https://platform.claude.com/docs/en/agent-sdk/overview).
228
202
 
229
- ### Claude SDK authentication failure
203
+ For `claudeRuntime: "cli"`, install the `claude` executable, ensure it is on `PATH`, and authenticate Claude Code.
230
204
 
231
- Confirm the environment used to start pi has valid Anthropic authentication, such as `ANTHROPIC_API_KEY`. The SDK runtime does not require the Claude Code CLI.
205
+ ## Escalation
232
206
 
233
- ### `spawn claude ENOENT`
207
+ Pi-runtime children receive an `ask_master` tool. Calling it reports a decision or blocker to the parent and immediately terminates the child run. Use it only when the child cannot proceed safely.
234
208
 
235
- `claudeRuntime` is set to `cli`, but the `claude` executable is not on `PATH`. Install and authenticate Claude Code, or switch back to `claudeRuntime: "sdk"`.
209
+ Claude-runtime children do not receive `ask_master`; they report blockers in their final response.
236
210
 
237
- ### Hidden shortcut produces no transcript message
238
-
239
- That is intentional. Hidden `>` runs are human-only UI jobs. Inspect them with `/sub:peek`, `<>runId`, or `/sub:open`.
240
-
241
- ### Debug runner termination
242
-
243
- The extension records process lifecycle diagnostics as `subagent-runner-diagnostic` custom entries in the parent session JSONL. They include run/batch IDs, parent and child PID/PGID, abort reason, internal kill cause, `exit`/`close` code and signal, settle reason, shutdown reason, and process-error stacks. Custom entries are not sent to the LLM and remain hidden unless an entry renderer is registered, as documented by pi's [`appendEntry` API](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md#piappendentrycustomtype-data).
244
-
245
- Run `/session` to find the current session file, set `SESSION_FILE` to that path, then extract only the diagnostic entries:
246
-
247
- ```bash
248
- jq -c 'select(.type == "custom" and .customType == "subagent-runner-diagnostic") | {timestamp, data}' "$SESSION_FILE"
249
- ```
250
-
251
- For batch failures, compare entries by `batchId` and `runId`, then follow each run from `spawn` through `kill_intent`, `exit`, `close`, and `settled`. Share only the selected diagnostic lines—not the full session file, which may contain prompts or tool output.
252
-
253
- To report a reproducible extension bug, create `subagent-issue.md` with the pi and extension versions, OS and Node version, launch mode/command, reproduction steps, expected and actual behavior, and the sanitized diagnostic lines. Then submit it with:
211
+ ## Troubleshooting
254
212
 
255
- ```bash
256
- gh issue create --repo Jonghakseo/pi-extension \
257
- --title "subagent: unexpected child termination" \
258
- --body-file subagent-issue.md
259
- ```
213
+ - **`Configured defaultAgent "worker" was not found`** — create a matching agent, choose another agent explicitly, or update `defaultAgent`.
214
+ - **Claude SDK authentication failure** — confirm the environment that starts pi contains valid Anthropic authentication.
215
+ - **`spawn claude ENOENT`** — install Claude Code or switch `claudeRuntime` to `sdk`.
216
+ - **Hidden run shows no transcript message** — inspect it with `/sub:peek`, `<>runId`, or `/sub:open`.
260
217
 
261
- Without GitHub CLI, use the repository's [new issue page](https://github.com/Jonghakseo/pi-extension/issues/new). Remove secrets and sensitive paths before attaching diagnostics.
218
+ When reporting a bug, include the pi and extension versions, OS and Node version, launch command, reproduction steps, and sanitized error output. Do not attach full session files because they may contain prompts, tool output, or secrets.
262
219
 
263
- ## Security and trust boundary
220
+ ## Security
264
221
 
265
- - Claude SDK execution uses `permissionMode: "bypassPermissions"` with `allowDangerouslySkipPermissions`; Claude CLI execution uses `--dangerously-skip-permissions`.
266
- - Pi-runtime children are headless and have unrestricted access to the tools declared by their agent.
267
- - Project agent definitions are repository-controlled instructions. Review `.pi/agents` and `.claude/agents` before running this extension in an unfamiliar repository.
268
- - Restrict each agent's `tools` list to what it needs. Avoid broad shell or write access for read-only review agents.
269
- - `--isolated` separates conversation context; it does not provide filesystem, process, credential, or network isolation.
222
+ - Claude SDK uses permission bypass; Claude CLI uses `--dangerously-skip-permissions`.
223
+ - Pi-runtime children can use every tool declared by their agent.
224
+ - Project agent definitions are repository-controlled instructions. Review `.pi/agents` and `.claude/agents` in unfamiliar repositories.
225
+ - Restrict each agent's `tools` list to the minimum required.
226
+ - `--isolated` separates conversation context, not filesystem, process, credential, or network access.
270
227
 
271
228
  ## Stability
272
229
 
273
- This is a `0.1.x` release. The commands, configuration keys, agent frontmatter, and behaviors documented here are the supported surface. Internal TypeScript modules included in the npm tarball are implementation details and may change during the `0.x` series. Compatibility is currently tested against pi 0.80.6.
230
+ This package is pre-1.0. Documented commands, configuration, and agent frontmatter are the supported surface; internal TypeScript modules may change between releases.
274
231
 
275
232
  ## License
276
233
 
package/commands.ts CHANGED
@@ -42,6 +42,11 @@ import {
42
42
  upsertPendingGroupCompletion,
43
43
  } from "./group-pending.js";
44
44
  import { enqueueSubagentInvocation } from "./invocation-queue.js";
45
+ import {
46
+ createAgentMentionAutocompleteProvider,
47
+ registerAgentMentionHighlighting,
48
+ replaceAgentMentions,
49
+ } from "./mentions.js";
45
50
  import { appendDisplayTaskUpdate, getSessionFileSize } from "./persisted-session.js";
46
51
  import { SUBAGENT_COMMANDS, type SubagentCommandName } from "./registration-manifest.js";
47
52
  import { readSessionReplayItems, SubagentSessionReplayOverlay } from "./replay.js";
@@ -904,6 +909,17 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
904
909
  commandDefinitions.set(name, definition);
905
910
  };
906
911
 
912
+ // Prompt mentions are references for the main LLM, not direct launch shortcuts.
913
+ // Register this transform before the legacy `>` handlers so `>worker` cannot
914
+ // be mistaken for a configured one-character symbol shortcut.
915
+ pi.on("input", (event, ctx) => {
916
+ if (event.source === "extension") return { action: "continue" as const };
917
+
918
+ const transformedText = replaceAgentMentions(event.text ?? "", discoverAgents(ctx.cwd).agents);
919
+ if (transformedText === event.text) return { action: "continue" as const };
920
+ return { action: "transform" as const, text: transformedText, images: event.images };
921
+ });
922
+
907
923
  pi.registerTool({
908
924
  name: "list-agents",
909
925
  label: "List Agents",
@@ -2300,6 +2316,19 @@ export async function handleBeforeAgentStart(
2300
2316
  export function handleSessionStart(pi: ExtensionAPI, store: SubagentStore, ctx: ExtensionContext): void {
2301
2317
  restoreRunsFromSession(store, ctx, pi);
2302
2318
  registerTerminalInputRedirect(ctx);
2319
+
2320
+ let cachedAgents = discoverAgents(ctx.cwd).agents;
2321
+ let discoveredAt = Date.now();
2322
+ const getAgents = () => {
2323
+ if (Date.now() - discoveredAt >= 1_000) {
2324
+ cachedAgents = discoverAgents(ctx.cwd).agents;
2325
+ discoveredAt = Date.now();
2326
+ }
2327
+ return cachedAgents;
2328
+ };
2329
+
2330
+ ctx.ui.addAutocompleteProvider((current) => createAgentMentionAutocompleteProvider(current, getAgents));
2331
+ registerAgentMentionHighlighting(ctx, getAgents);
2303
2332
  }
2304
2333
 
2305
2334
  /** session_shutdown handler; index.ts invokes this after shutting down runs. */
package/mentions.ts ADDED
@@ -0,0 +1,177 @@
1
+ import { CustomEditor, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
+ import type {
3
+ AutocompleteProvider,
4
+ AutocompleteSuggestions,
5
+ EditorComponent,
6
+ EditorTheme,
7
+ TUI,
8
+ } from "@earendil-works/pi-tui";
9
+ import type { AgentConfig } from "./agents.js";
10
+ import { COMMAND_COMPLETION_LIMIT } from "./constants.js";
11
+
12
+ const MENTION_PREFIX_PATTERN = /(?:^|[\s([{])>(?!>)([^\s>]*)$/;
13
+ const MENTION_LEADING_BOUNDARY = "[\\s([{]";
14
+ const MENTION_TRAILING_BOUNDARY = "(?=$|[\\s.,!?;:)}\\]])";
15
+ const MENTION_HIGHLIGHT_WRAPPED = Symbol.for("pi-extension-subagent.mentionHighlightWrapped");
16
+ const ANSI_CYAN = "\x1b[36m";
17
+ const ANSI_RESET_FOREGROUND = "\x1b[39m";
18
+
19
+ type HighlightableEditor = EditorComponent & Record<PropertyKey, unknown>;
20
+ type RichEditorTheme = EditorTheme & {
21
+ fg?: (color: string, text: string) => string;
22
+ };
23
+
24
+ function escapeRegExp(value: string): string {
25
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ }
27
+
28
+ function buildAgentMentionPattern(agents: AgentConfig[]): {
29
+ pattern: RegExp;
30
+ canonicalNames: Map<string, string>;
31
+ } | null {
32
+ const canonicalNames = new Map(agents.map((agent) => [agent.name.toLowerCase(), agent.name]));
33
+ const names = Array.from(canonicalNames.values())
34
+ .filter((name) => name.length > 0 && !/\s/.test(name))
35
+ .sort((left, right) => right.length - left.length)
36
+ .map(escapeRegExp);
37
+ if (names.length === 0) return null;
38
+
39
+ return {
40
+ pattern: new RegExp(`(^|${MENTION_LEADING_BOUNDARY})>(${names.join("|")})${MENTION_TRAILING_BOUNDARY}`, "gim"),
41
+ canonicalNames,
42
+ };
43
+ }
44
+
45
+ function styleAgentMention(text: string, theme: EditorTheme): string {
46
+ const richTheme = theme as RichEditorTheme;
47
+ for (const color of ["subagentMention", "syntaxType", "toolTitle"]) {
48
+ try {
49
+ const styled = richTheme.fg?.(color, text);
50
+ if (styled && styled !== text) return styled;
51
+ } catch {
52
+ // Custom theme keys are optional.
53
+ }
54
+ }
55
+ return theme.selectList?.selectedText?.(text) ?? `${ANSI_CYAN}${text}${ANSI_RESET_FOREGROUND}`;
56
+ }
57
+
58
+ /** Return the incomplete agent name after a mention marker at the cursor. */
59
+ export function extractAgentMentionQuery(textBeforeCursor: string): string | undefined {
60
+ return MENTION_PREFIX_PATTERN.exec(textBeforeCursor)?.[1];
61
+ }
62
+
63
+ /** Filter mention candidates by case-insensitive containment, preferring prefix matches. */
64
+ export function filterAgentMentionCandidates(agents: AgentConfig[], query: string): AgentConfig[] {
65
+ const normalizedQuery = query.toLowerCase();
66
+ return agents
67
+ .filter((agent) => agent.name.toLowerCase().includes(normalizedQuery))
68
+ .sort((left, right) => {
69
+ const leftName = left.name.toLowerCase();
70
+ const rightName = right.name.toLowerCase();
71
+ const leftStartsWith = leftName.startsWith(normalizedQuery);
72
+ const rightStartsWith = rightName.startsWith(normalizedQuery);
73
+ if (leftStartsWith !== rightStartsWith) return leftStartsWith ? -1 : 1;
74
+ return leftName.localeCompare(rightName);
75
+ })
76
+ .slice(0, COMMAND_COMPLETION_LIMIT);
77
+ }
78
+
79
+ /** Replace exact, discovered `>agent` mentions with the main-LLM-friendly `subagent:agent` form. */
80
+ export function replaceAgentMentions(text: string, agents: AgentConfig[]): string {
81
+ const mentionPattern = buildAgentMentionPattern(agents);
82
+ if (!mentionPattern) return text;
83
+
84
+ return text.replace(mentionPattern.pattern, (_match, boundary: string, name: string) => {
85
+ const canonicalName = mentionPattern.canonicalNames.get(name.toLowerCase()) ?? name;
86
+ return `${boundary}subagent:${canonicalName}`;
87
+ });
88
+ }
89
+
90
+ /** Highlight only exact mentions for agents that are currently discoverable. */
91
+ export function highlightAgentMentions(
92
+ text: string,
93
+ agents: AgentConfig[],
94
+ style: (mention: string) => string,
95
+ ): string {
96
+ const mentionPattern = buildAgentMentionPattern(agents);
97
+ if (!mentionPattern) return text;
98
+
99
+ return text.replace(mentionPattern.pattern, (_match, boundary: string, name: string) => {
100
+ return `${boundary}${style(`>${name}`)}`;
101
+ });
102
+ }
103
+
104
+ export function createAgentMentionHighlightEditor(
105
+ baseEditor: EditorComponent,
106
+ getAgents: () => AgentConfig[],
107
+ theme: EditorTheme,
108
+ ): EditorComponent {
109
+ const highlightableEditor = baseEditor as HighlightableEditor;
110
+ if (highlightableEditor[MENTION_HIGHLIGHT_WRAPPED]) return baseEditor;
111
+
112
+ return new Proxy(highlightableEditor, {
113
+ get(target, property) {
114
+ if (property === MENTION_HIGHLIGHT_WRAPPED) return true;
115
+ if (property === "render") {
116
+ return (width: number) => {
117
+ const agents = getAgents();
118
+ return target
119
+ .render(width)
120
+ .map((line) => highlightAgentMentions(line, agents, (mention) => styleAgentMention(mention, theme)));
121
+ };
122
+ }
123
+
124
+ const value = Reflect.get(target, property, target);
125
+ return typeof value === "function" ? value.bind(target) : value;
126
+ },
127
+ set(target, property, value) {
128
+ return Reflect.set(target, property, value, target);
129
+ },
130
+ }) as EditorComponent;
131
+ }
132
+
133
+ export function registerAgentMentionHighlighting(ctx: ExtensionContext, getAgents: () => AgentConfig[]): void {
134
+ if (ctx.mode !== "tui") return;
135
+
136
+ const previousEditorFactory = ctx.ui.getEditorComponent();
137
+ ctx.ui.setEditorComponent((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
138
+ const baseEditor = previousEditorFactory?.(tui, theme, keybindings) ?? new CustomEditor(tui, theme, keybindings);
139
+ return createAgentMentionHighlightEditor(baseEditor, getAgents, theme);
140
+ });
141
+ }
142
+
143
+ export function createAgentMentionAutocompleteProvider(
144
+ current: AutocompleteProvider,
145
+ getAgents: () => AgentConfig[],
146
+ ): AutocompleteProvider {
147
+ return {
148
+ triggerCharacters: Array.from(new Set([...(current.triggerCharacters ?? []), ">"])),
149
+ async getSuggestions(lines, cursorLine, cursorCol, options): Promise<AutocompleteSuggestions | null> {
150
+ const currentLine = lines[cursorLine] ?? "";
151
+ const query = extractAgentMentionQuery(currentLine.slice(0, cursorCol));
152
+ if (query === undefined) {
153
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
154
+ }
155
+
156
+ const candidates = filterAgentMentionCandidates(getAgents(), query);
157
+ if (options.signal.aborted || candidates.length === 0) return null;
158
+
159
+ return {
160
+ prefix: `>${query}`,
161
+ items: candidates.map((agent) => ({
162
+ value: `>${agent.name}`,
163
+ label: `>${agent.name}`,
164
+ description: agent.description,
165
+ })),
166
+ };
167
+ },
168
+
169
+ applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
170
+ return current.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
171
+ },
172
+
173
+ shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
174
+ return current.shouldTriggerFileCompletion?.(lines, cursorLine, cursorCol) ?? true;
175
+ },
176
+ };
177
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,6 +40,7 @@
40
40
  "lifecycle.ts",
41
41
  "invocation-queue.ts",
42
42
  "live-preview.ts",
43
+ "mentions.ts",
43
44
  "persisted-session.ts",
44
45
  "replay.ts",
45
46
  "registration-manifest.ts",