pi-cc-extensions 0.9.4 → 0.9.6

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.en.md CHANGED
@@ -29,7 +29,7 @@ Run `/reload` after installation.
29
29
  | --------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------- |
30
30
  | Claude Code UI | Tool summaries, expand/collapse, rich edit/write diffs, and `on` / `compact` / `off` modes | `/ccstyle` |
31
31
  | Markdown enhancements | Mermaid diagrams, admonitions, URL linking, and more | Automatic |
32
- | Fullscreen mode | Tool card/group expand and collapse, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
32
+ | Fullscreen mode | Tool card/group expand and collapse on click, previews, hover highlight, and a back-to-bottom button | `TUIMODE=fullscreen` or `--tui-mode fullscreen` |
33
33
  | Settings panel | `Style / Features / UI / Diff / Thinking / Footer` tabs | `/ccstyle` |
34
34
  | Context inspection | Usage breakdown and previews for the system prompt, memory, skills, tools definition, and messages | `/context` |
35
35
  | Session/Subagent references | Search and inject effective context from previous Sessions or existing SubAgents | `@` |
@@ -50,7 +50,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
50
50
 
51
51
  // features
52
52
  "enableSessionReference": true, // @ session references
53
- "enableSubagentAutocomplete": true, // @ subagent completion and delegation hints
53
+ "enableSubagentAutocomplete": true, // @subagent:[name] completion and delegation hints
54
54
  "enableContextCommand": true, // /context usage check
55
55
  "enableAgentSummary": true, // per-turn tool summary
56
56
  "enableWorkingMessage": true, // Working... bottom token/elapsed
@@ -59,7 +59,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
59
59
  // ui
60
60
  "expandedInputMaxLines": 5, // expanded tool Input lines; overflow shows a footer hint
61
61
  "expandedOutputMaxLines": 10, // expanded tool Output lines; overflow shows a footer hint
62
- "expandedPreviewMaxLines": 40, // max lines for expanded diff/TaskList bodies
62
+ "expandedPreviewMaxLines": 40, // max lines for expanded TaskList bodies (expanded diffs always show all)
63
63
  "inputClip": 100, // tool summary path/command clip length
64
64
  "showStartupHeader": true, // startup header (logo + tips) toggle
65
65
  "scrollStepLines": 3, // fullscreen wheel scroll step
@@ -89,7 +89,7 @@ Renderer snapshots for `on` / `compact` modes: [default](./docs/tool-render-exam
89
89
  ```
90
90
 
91
91
  > [!TIP]
92
- > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. When expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview. Double-click an expanded panel to collapse it.
92
+ > **Fullscreen**: click `click to show more` to expand tool cards, thinking, Skill, and compact summaries. An expanded diff always shows every line; when expanded Input/Output exceeds the line cap, the footer `… +N more lines • click to show more` opens a full preview. Click to collapse (dragging inside the card selects text).
93
93
 
94
94
  > [!NOTE]
95
95
  > **Mermaid rendering**: set `markdown.mermaid` to `final` via `~/.pi/agent/settings.json` or the Mermaid diagrams option in `/settings`. Default `streaming` redraws per frame; `final` renders once at completion.
package/README.md CHANGED
@@ -29,7 +29,7 @@ pi install git:github.com/minuque/pi-cc-extensions
29
29
  | --------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
30
30
  | Claude Code UI | 工具摘要、折叠展开、rich edit/write diff,以及`on` / `compact` / `off` 三种模式 | `/ccstyle` |
31
31
  | Markdown 增强 | Mermaid 图、提示框、URL 链接化等 | 自动生效 |
32
- | Fullscreen mode | 工具卡/group 单击展开、双击收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
32
+ | Fullscreen mode | 工具卡/group 单击展开与收起、预览、hover 高亮、回到底部按钮 | `TUIMODE=fullscreen` 或 `--tui-mode fullscreen` |
33
33
  | 配置面板 | `Style / Features / UI / Diff / Thinking / Footer` 六页签 | `/ccstyle` |
34
34
  | 上下文检查 | 查看上下文占用,并预览 System prompt、Memory、Skills、Tools definition 和消息内容 | `/context` |
35
35
  | Session/Subagent 引用 | 搜索并注入历史 Session 或现有 SubAgent 的有效上下文 | `@` |
@@ -50,7 +50,7 @@ pi install git:github.com/minuque/pi-cc-extensions
50
50
 
51
51
  // features
52
52
  "enableSessionReference": true, // @ session 引用
53
- "enableSubagentAutocomplete": true, // @ subagent 补全与委派提示
53
+ "enableSubagentAutocomplete": true, // @subagent:[name] 补全与委派提示
54
54
  "enableContextCommand": true, // /context 上下文检查
55
55
  "enableAgentSummary": true, // 每回合工具摘要
56
56
  "enableWorkingMessage": true, // Working... 底部 token/耗时
@@ -59,7 +59,7 @@ pi install git:github.com/minuque/pi-cc-extensions
59
59
  // ui
60
60
  "expandedInputMaxLines": 5, // 展开工具卡 Input 可见行数,超出在末行显示展开提示
61
61
  "expandedOutputMaxLines": 10, // 展开工具卡 Output 可见行数,超出在末行显示展开提示
62
- "expandedPreviewMaxLines": 40, // 展开 diff/TaskList 正文最大行数
62
+ "expandedPreviewMaxLines": 40, // 展开 TaskList 正文最大行数(展开 diff 始终全量)
63
63
  "inputClip": 100, // 工具摘要 path/command 折叠字符数
64
64
  "showStartupHeader": true, // 启动头(logo + tips)开关
65
65
  "scrollStepLines": 3, // fullscreen 滚轮步进
@@ -89,7 +89,7 @@ pi install git:github.com/minuque/pi-cc-extensions
89
89
  ```
90
90
 
91
91
  > [!TIP]
92
- > **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;展开后 Input/Output 超行时,末行 `… +N more lines • click to show more` 打开全量预览,双击展开面板收起。
92
+ > **全屏模式**:单击 `click to show more` 展开工具卡、思考、Skill 和 compact 摘要;工具卡展开态 diff 全量显示,Input/Output 超行时末行 `… +N more lines • click to show more` 打开全量预览,单击收起(卡内拖动为选中文本)。
93
93
 
94
94
  > [!NOTE]
95
95
  > **Mermaid 渲染**:建议把 `markdown.mermaid` 设为 `final`(`~/.pi/agent/settings.json` 或 `/settings` 面板的 Mermaid diagrams 选项)。默认 `streaming` 逐帧重绘,`final` 渲染最终版更稳定。
@@ -37,9 +37,8 @@ export const DEFAULT_TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
37
37
  writeDiffCollapsedLines: 0,
38
38
  diffWordWrap: true,
39
39
  /**
40
- * Expanded tool/diff body cap. 40 ≈ one screen of content after title,
41
- * Input section, editor, and status — keeps the TUI compact.
42
- * Raise via /ccstyle → Diff → Expanded max lines when reviewing large dumps.
40
+ * Expanded TaskList body cap. Tool Input/Output and diffs use their own settings;
41
+ * an expanded diff always renders every line.
43
42
  */
44
43
  expandedPreviewMaxLines: 40,
45
44
  };
@@ -485,8 +485,7 @@ export async function showCcstylePanel(
485
485
  const expandedMaxSetting = {
486
486
  id: "expandedPreviewMaxLines",
487
487
  label: "Expanded max lines",
488
- description:
489
- "Max diff/TaskList body lines when expanded. Tool Input/Output use the two settings above.",
488
+ description: "Max TaskList body lines when expanded. Expanded diffs show every line.",
490
489
  currentValue: String(config.expandedPreviewMaxLines),
491
490
  values: [...EXPANDED_PREVIEW_MAX_LINES_VALUES],
492
491
  submenu: (_current: string, closeSubmenu: (selected?: string) => void) =>
@@ -556,7 +555,7 @@ export async function showCcstylePanel(
556
555
  const subagentAutocompleteToggle = featureToggleSetting(
557
556
  "enableSubagentAutocomplete",
558
557
  "Subagent autocomplete",
559
- "@ subagent mentions suggest agents and inject delegation instructions. Next restart applies.",
558
+ "@subagent:[name] mentions suggest agents and inject delegation instructions. Next restart applies.",
560
559
  "Subagent autocomplete disabled.",
561
560
  config.enableSubagentAutocomplete,
562
561
  );
@@ -323,7 +323,7 @@ function hiddenPreviewHint(
323
323
 
324
324
  const expandedThinking = new Set<number>();
325
325
 
326
- /** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、双击整块收起,对齐工具卡。 */
326
+ /** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、展开卡单击整块收起(拖动选择文本时不收起)。 */
327
327
  export class ThinkingPreviewBlock implements Component {
328
328
  private heading: string;
329
329
  private text: string;
@@ -14,6 +14,7 @@ import {
14
14
  import {
15
15
  SESSION_REFERENCE_CUSTOM_TYPE,
16
16
  SESSION_REFERENCE_PREFIX,
17
+ assignReferenceTokens,
17
18
  buildReferenceContentFromSections,
18
19
  extractSessionReferenceIds,
19
20
  formatReferenceSession,
@@ -123,7 +124,7 @@ function formatDate(date: Date): string {
123
124
  const sessionSearchTextCache = new WeakMap<SessionReference, string>();
124
125
  const sessionItemCache = new WeakMap<
125
126
  SessionReference,
126
- { cwd: string; named?: AutocompleteItem; stable?: AutocompleteItem }
127
+ { cwd: string; token: string; item: AutocompleteItem }
127
128
  >();
128
129
 
129
130
  function sessionSearchText(reference: SessionReference): string {
@@ -138,30 +139,24 @@ function sessionSearchText(reference: SessionReference): string {
138
139
  function sessionItem(
139
140
  reference: SessionReference,
140
141
  currentCwd: string,
141
- useStableId: boolean,
142
+ token: string,
142
143
  ): AutocompleteItem {
143
- let cached = sessionItemCache.get(reference);
144
- const variant = useStableId ? "stable" : "named";
145
- if (cached?.cwd === currentCwd && cached[variant]) return cached[variant];
144
+ const cached = sessionItemCache.get(reference);
145
+ if (cached?.cwd === currentCwd && cached.token === token) return cached.item;
146
146
 
147
147
  const session = reference.info;
148
148
  const workspace = samePath(session.cwd, currentCwd)
149
149
  ? "current workspace"
150
150
  : session.cwd || "unknown workspace";
151
- const label = reference.kind === "subagent" ? "[SubAgent]" : "[Session]";
152
- // 唯一名称保持短格式;同名或无名称时使用稳定 ID,避免引用歧义。
153
- const sessionName = session.name?.trim();
154
- const referenceId = useStableId
155
- ? reference.referenceIds[0]
156
- : (sessionName ?? reference.referenceIds[0]);
151
+ const kindLabel = reference.kind === "subagent" ? "[SubAgent]" : "[Session]";
152
+ // 消歧后的 token 会带上日期;回落到 ID 时下拉仍显示可读标题。
153
+ const title = token === (reference.referenceIds[0] ?? session.id) ? sessionTitle(session) : token;
157
154
  const item: AutocompleteItem = {
158
- value: `${SESSION_REFERENCE_PREFIX}[${referenceId}]`,
159
- label: `${label} ${sessionTitle(session)}`,
155
+ value: `${SESSION_REFERENCE_PREFIX}[${token}]`,
156
+ label: `${kindLabel} ${title}`,
160
157
  description: `${workspace} · ${session.messageCount} messages · ${formatDate(session.modified)}`,
161
158
  };
162
- if (cached?.cwd !== currentCwd) cached = { cwd: currentCwd };
163
- cached[variant] = item;
164
- sessionItemCache.set(reference, cached);
159
+ sessionItemCache.set(reference, { cwd: currentCwd, token, item });
165
160
  return item;
166
161
  }
167
162
 
@@ -197,11 +192,7 @@ function filterSessions(
197
192
  if (query.startsWith("session:")) return [];
198
193
 
199
194
  const ordered = orderSessionReferences(references, currentCwd);
200
- const nameCounts = new Map<string, number>();
201
- for (const reference of ordered) {
202
- const name = reference.info.name?.trim();
203
- if (name) nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
204
- }
195
+ const tokens = assignReferenceTokens(ordered);
205
196
  const trimmed = query.trim();
206
197
  // 模糊匹配只接受有 session name 的会话;subagent 按 agent name 匹配。
207
198
  const searchable = ordered.filter(
@@ -211,10 +202,11 @@ function filterSessions(
211
202
  const matches = trimmed
212
203
  ? fuzzyFilter(searchable, trimmed, sessionSearchText)
213
204
  : ordered.slice(0, MAX_SESSION_SUGGESTIONS);
214
- return matches.slice(0, MAX_SESSION_SUGGESTIONS).map((reference) => {
215
- const name = reference.info.name?.trim();
216
- return sessionItem(reference, currentCwd, Boolean(name && (nameCounts.get(name) ?? 0) > 1));
217
- });
205
+ return matches
206
+ .slice(0, MAX_SESSION_SUGGESTIONS)
207
+ .map((reference) =>
208
+ sessionItem(reference, currentCwd, tokens.get(reference) ?? reference.info.id),
209
+ );
218
210
  }
219
211
 
220
212
  function isPathLikeQuery(query: string): boolean {
@@ -452,22 +444,17 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
452
444
  ),
453
445
  ));
454
446
  const referencesById = new Map<string, SessionReference>();
455
- const referencesByName = new Map<string, SessionReference>();
456
- const ambiguousNames = new Set<string>();
447
+ const referencesByToken = new Map<string, SessionReference>();
457
448
  for (const reference of references) {
458
449
  for (const id of reference.referenceIds) referencesById.set(id, reference);
459
- const name = reference.info.name?.trim();
460
- if (!name || ambiguousNames.has(name)) continue;
461
- if (referencesByName.has(name)) {
462
- referencesByName.delete(name);
463
- ambiguousNames.add(name);
464
- } else {
465
- referencesByName.set(name, reference);
466
- }
450
+ }
451
+ // 同位重名的会话用带日期的 token 区分,解析端与补全端用同一套分配。
452
+ for (const [reference, token] of assignReferenceTokens(references)) {
453
+ referencesByToken.set(token, reference);
467
454
  }
468
455
  const seenReferences = new Set<SessionReference>();
469
456
  const matchingReferences = referenceIds
470
- .map((id) => referencesById.get(id) ?? referencesByName.get(id))
457
+ .map((id) => referencesById.get(id) ?? referencesByToken.get(id))
471
458
  .filter((reference): reference is SessionReference => {
472
459
  if (!reference || reference.info.id === currentSessionId || seenReferences.has(reference))
473
460
  return false;
@@ -1,3 +1,6 @@
1
+ import { truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { stripTerminalSequences } from "../../utils/ansi-text.ts";
3
+
1
4
  export const SESSION_REFERENCE_CUSTOM_TYPE = "session-reference";
2
5
  export const SESSION_REFERENCE_PREFIX = "@session:";
3
6
 
@@ -45,13 +48,104 @@ export function extractSessionReferenceIds(text: string): string[] {
45
48
  return ids;
46
49
  }
47
50
 
51
+ const EMPTY_SESSION_TITLE = "(no messages)";
52
+
48
53
  export function sessionTitle(info: ReferenceSessionInfo, maxLength = 80): string {
49
- const source = info.name?.trim() || info.firstMessage || "(no messages)";
54
+ const source = info.name?.trim() || info.firstMessage || EMPTY_SESSION_TITLE;
50
55
  const normalized = source.replace(CONTROL_CHARACTERS, " ").replace(/\s+/g, " ").trim();
51
56
  if (normalized.length <= maxLength) return normalized;
52
57
  return `${normalized.slice(0, Math.max(1, maxLength - 1)).trimEnd()}…`;
53
58
  }
54
59
 
60
+ /**
61
+ * 编辑器里的 `@session:[…]` 是行内文本,token 按显示宽度收口,
62
+ * 不让首条消息整段挤进输入行。同名会话靠后缀消歧,不受它影响。
63
+ */
64
+ export const REFERENCE_TOKEN_MAX_WIDTH = 32;
65
+
66
+ /** Editor `@session:[…]` text: the readable title, never a placeholder or brackets. */
67
+ export function sessionReferenceLabel(
68
+ info: ReferenceSessionInfo,
69
+ maxWidth = REFERENCE_TOKEN_MAX_WIDTH,
70
+ ): string {
71
+ const title = sessionTitle(info).replace(/[[\]]/g, "").trim();
72
+ if (!title || title === EMPTY_SESSION_TITLE) return "";
73
+ // truncateToWidth 会在省略号前后补 ANSI reset,token 是明文,去掉。
74
+ return stripTerminalSequences(truncateToWidth(title, maxWidth, "…")).trim();
75
+ }
76
+
77
+ export interface ReferenceTokenSource {
78
+ referenceIds: string[];
79
+ info: ReferenceSessionInfo;
80
+ }
81
+
82
+ /** UTC keeps the disambiguator identical across timezones (same-day drift is harmless). */
83
+ function referenceTokenDate(date: Date): string {
84
+ const month = String(date.getUTCMonth() + 1).padStart(2, "0");
85
+ const day = String(date.getUTCDate()).padStart(2, "0");
86
+ return `${month}-${day}`;
87
+ }
88
+
89
+ function referenceTokenTime(date: Date): string {
90
+ const hours = String(date.getUTCHours()).padStart(2, "0");
91
+ const minutes = String(date.getUTCMinutes()).padStart(2, "0");
92
+ return `${hours}:${minutes}`;
93
+ }
94
+
95
+ /** Title first; date then time widen the disambiguation before the ID fallback. */
96
+ function referenceTokenCandidates(reference: ReferenceTokenSource): string[] {
97
+ const title = sessionReferenceLabel(reference.info);
98
+ const stableId = reference.referenceIds[0] ?? reference.info.id;
99
+ if (!title) return [stableId];
100
+ const date = referenceTokenDate(reference.info.modified);
101
+ return [
102
+ title,
103
+ `${title} ${date}`,
104
+ `${title} ${date} ${referenceTokenTime(reference.info.modified)}`,
105
+ stableId,
106
+ ];
107
+ }
108
+
109
+ /**
110
+ * Assign each reference a unique editor token. Same-named sessions keep a readable
111
+ * title plus a date/time suffix; the stable ID is only the last resort.
112
+ */
113
+ export function assignReferenceTokens<T extends ReferenceTokenSource>(
114
+ references: T[],
115
+ ): Map<T, string> {
116
+ const candidates = new Map<T, string[]>();
117
+ let levels = 0;
118
+ for (const reference of references) {
119
+ const options = referenceTokenCandidates(reference);
120
+ levels = Math.max(levels, options.length);
121
+ candidates.set(reference, options);
122
+ }
123
+
124
+ const tokens = new Map<T, string>();
125
+ const taken = new Set<string>();
126
+ for (let level = 0; level < levels && tokens.size < references.length; level++) {
127
+ const counts = new Map<string, number>();
128
+ for (const [reference, options] of candidates) {
129
+ const candidate = tokens.has(reference) ? undefined : options[level];
130
+ if (candidate) counts.set(candidate, (counts.get(candidate) ?? 0) + 1);
131
+ }
132
+ for (const [reference, options] of candidates) {
133
+ if (tokens.has(reference)) continue;
134
+ const candidate = options[level];
135
+ if (candidate && counts.get(candidate) === 1 && !taken.has(candidate)) {
136
+ tokens.set(reference, candidate);
137
+ taken.add(candidate);
138
+ }
139
+ }
140
+ }
141
+ for (const reference of references) {
142
+ if (!tokens.has(reference)) {
143
+ tokens.set(reference, reference.referenceIds[0] ?? reference.info.id);
144
+ }
145
+ }
146
+ return tokens;
147
+ }
148
+
55
149
  function byteLength(text: string): number {
56
150
  return Buffer.byteLength(text, "utf8");
57
151
  }
@@ -18,9 +18,12 @@ type AgentInfo = {
18
18
  filePath: string;
19
19
  };
20
20
 
21
+ export const SUBAGENT_DELEGATION_CUSTOM_TYPE = "subagent-delegation";
22
+ export const SUBAGENT_REFERENCE_PREFIX = "@subagent:";
23
+
21
24
  const MAX_SUGGESTIONS = 2;
22
- // Match `@name` but NOT `@session:` (reserved by session-reference extension).
23
- const AGENT_NAME_PATTERN = /(?:^|[\t ])@((?:[^\s:@][^\s:]*)?)$/;
25
+ const MENTION_PATTERN = /(?:^|[\t ])@([^\s@]*)$/;
26
+ const SUBAGENT_REFERENCE_PATTERN = /(?:^|\s)@subagent:\[([^\]]+)](?![\w-])/g;
24
27
 
25
28
  function getAgentDir(): string {
26
29
  const override = process.env.PI_CODING_AGENT_DIR;
@@ -60,6 +63,36 @@ function loadAgents(): AgentInfo[] {
60
63
  });
61
64
  }
62
65
 
66
+ function extractMentionQuery(textBeforeCursor: string): string | undefined {
67
+ return textBeforeCursor.match(MENTION_PATTERN)?.[1];
68
+ }
69
+
70
+ /** Filter text for agent names, or null when this provider should not claim the @ query. */
71
+ function agentSearchQuery(raw: string): string | null {
72
+ if (raw.startsWith("session:")) return null;
73
+ if (raw === "subagent") return "";
74
+ const prefixed = raw.match(/^subagent:\[?([^\]]*)$/);
75
+ if (prefixed) return prefixed[1] ?? "";
76
+ return raw;
77
+ }
78
+
79
+ export function extractSubagentReferenceNames(text: string): string[] {
80
+ const names: string[] = [];
81
+ const seen = new Set<string>();
82
+ for (const match of text.matchAll(SUBAGENT_REFERENCE_PATTERN)) {
83
+ const name = match[1]?.trim();
84
+ if (name && !seen.has(name)) {
85
+ seen.add(name);
86
+ names.push(name);
87
+ }
88
+ }
89
+ return names;
90
+ }
91
+
92
+ export function subagentReferenceValue(name: string): string {
93
+ return `${SUBAGENT_REFERENCE_PREFIX}[${name}]`;
94
+ }
95
+
63
96
  export function createAgentAutocompleteProvider(
64
97
  current: AutocompleteProvider,
65
98
  getAgents: () => AgentInfo[],
@@ -74,13 +107,16 @@ export function createAgentAutocompleteProvider(
74
107
  ): Promise<AutocompleteSuggestions | null> {
75
108
  const currentLine = lines[cursorLine] ?? "";
76
109
  const textBeforeCursor = currentLine.slice(0, cursorCol);
77
- const match = textBeforeCursor.match(AGENT_NAME_PATTERN);
110
+ const rawQuery = extractMentionQuery(textBeforeCursor);
111
+ if (rawQuery === undefined) {
112
+ return current.getSuggestions(lines, cursorLine, cursorCol, options);
113
+ }
78
114
 
79
- if (!match) {
115
+ const search = agentSearchQuery(rawQuery);
116
+ if (search === null) {
80
117
  return current.getSuggestions(lines, cursorLine, cursorCol, options);
81
118
  }
82
119
 
83
- const query = match[1]!;
84
120
  const agents = getAgents();
85
121
 
86
122
  if (options.signal.aborted || agents.length === 0) {
@@ -89,17 +125,20 @@ export function createAgentAutocompleteProvider(
89
125
 
90
126
  const [baseSuggestions, matches] = await Promise.all([
91
127
  current.getSuggestions(lines, cursorLine, cursorCol, options),
92
- Promise.resolve(query.trim() ? fuzzyFilter(agents, query, (a) => a.name) : agents),
128
+ Promise.resolve(search.trim() ? fuzzyFilter(agents, search, (a) => a.name) : agents),
93
129
  ]);
94
130
  if (options.signal.aborted) return null;
95
131
 
96
132
  const agentItems: AutocompleteItem[] = matches.slice(0, MAX_SUGGESTIONS).map((agent) => ({
97
- value: `@${agent.name}`,
133
+ value: subagentReferenceValue(agent.name),
98
134
  label: `[SubAgent] ${agent.displayName}`,
99
135
  description: `${agent.model ?? "?"} · ${agent.thinking ?? "?"}`,
100
136
  }));
101
- const hasCompatibleBaseSuggestions = baseSuggestions?.prefix === `@${query}`;
102
- const agentValues = new Set(agents.map((agent) => `@${agent.name}`));
137
+ const prefix = `@${rawQuery}`;
138
+ const hasCompatibleBaseSuggestions = baseSuggestions?.prefix === prefix;
139
+ const agentValues = new Set(
140
+ agents.flatMap((agent) => [`@${agent.name}`, subagentReferenceValue(agent.name)]),
141
+ );
103
142
  const baseItems = hasCompatibleBaseSuggestions
104
143
  ? baseSuggestions.items.filter((item) => !agentValues.has(item.value))
105
144
  : [];
@@ -112,7 +151,7 @@ export function createAgentAutocompleteProvider(
112
151
  });
113
152
 
114
153
  if (items.length === 0 && !hasCompatibleBaseSuggestions) return baseSuggestions;
115
- return { items, prefix: `@${query}` };
154
+ return { items, prefix };
116
155
  },
117
156
 
118
157
  applyCompletion(
@@ -166,16 +205,13 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
166
205
  sessionGeneration++;
167
206
  });
168
207
 
169
- // Inject instruction when user types @agent-name (not @session:) in prompt.
170
- // Supports multiple different subagents in a single prompt.
171
- const AGENT_PROMPT_PATTERN = /(?:^|[\s])@([^\s:@][^\s:]*)/g;
208
+ // Inject as a request message, not systemPrompt, so the cached prefix stays stable.
172
209
  pi.on("before_agent_start", async (event, _ctx) => {
173
210
  const agents = getAgents();
174
211
  if (agents.length === 0) return;
175
212
 
176
213
  const mentions: string[] = [];
177
- for (const m of event.prompt.matchAll(AGENT_PROMPT_PATTERN)) {
178
- const name = m[1]!;
214
+ for (const name of extractSubagentReferenceNames(event.prompt)) {
179
215
  if (agents.some((a) => a.name === name) && !mentions.includes(name)) {
180
216
  mentions.push(name);
181
217
  }
@@ -186,12 +222,15 @@ export default function agentAutocompleteExtension(pi: ExtensionAPI): void {
186
222
  const agentList = mentions.map((n) => `"${n}" (${agentMap.get(n)!.displayName})`).join(", ");
187
223
 
188
224
  return {
189
- systemPrompt:
190
- event.systemPrompt +
191
- `\n\nThe user's prompt references these subagent types: ${agentList}. ` +
192
- `You MUST use the Agent tool for EACH mentioned subagent to delegate the relevant parts of the request. ` +
193
- `Handle different subagents separately — do NOT merge their tasks into a single Agent call. ` +
194
- `For example, if the user mentions @coder and @explore, make two separate Agent tool calls, one with subagent_type="coder" and another with subagent_type="explore".`,
225
+ message: {
226
+ customType: SUBAGENT_DELEGATION_CUSTOM_TYPE,
227
+ content:
228
+ `The user's prompt references these subagent types: ${agentList}. ` +
229
+ `You MUST use the Agent tool for EACH mentioned subagent to delegate the relevant parts of the request. ` +
230
+ `Handle different subagents separately — do NOT merge their tasks into a single Agent call. ` +
231
+ `For example, if the user mentions @subagent:[coder] and @subagent:[explore], make two separate Agent tool calls, one with subagent_type="coder" and another with subagent_type="explore".`,
232
+ display: false,
233
+ },
195
234
  };
196
235
  });
197
236
  }