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 +4 -4
- package/README.md +4 -4
- package/extensions/config/config.ts +2 -3
- package/extensions/config/panel.ts +2 -3
- package/extensions/feature/compact-thinking.ts +1 -1
- package/extensions/feature/reference/index.ts +23 -36
- package/extensions/feature/reference/session.ts +95 -1
- package/extensions/feature/reference/subagent.ts +60 -21
- package/extensions/renderer/compact-mode.ts +174 -62
- package/extensions/renderer/default-mode.ts +36 -39
- package/extensions/renderer/index.ts +5 -3
- package/extensions/renderer/mouse/interaction.ts +251 -80
- package/extensions/renderer/mouse/layout.ts +14 -5
- package/extensions/renderer/tool/diff/diff-component.ts +19 -2
- package/extensions/renderer/tool/diff/diff-edit-render.ts +18 -9
- package/extensions/renderer/tool/diff/diff-limits.ts +22 -21
- package/extensions/renderer/tool/diff/diff-write-render.ts +28 -18
- package/extensions/renderer/tool/grouping.ts +21 -5
- package/extensions/renderer/tool/mcp-title.ts +39 -0
- package/extensions/renderer/tool/names.ts +103 -1
- package/extensions/renderer/tool/result.ts +6 -1
- package/extensions/renderer/tool/show-more-hint.ts +10 -0
- package/package.json +2 -1
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, // @
|
|
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
|
|
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.
|
|
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
|
|
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, // @
|
|
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, // 展开
|
|
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
|
|
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
|
|
41
|
-
*
|
|
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
|
-
"@
|
|
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;
|
|
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
|
-
|
|
142
|
+
token: string,
|
|
142
143
|
): AutocompleteItem {
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
152
|
-
//
|
|
153
|
-
const
|
|
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}[${
|
|
159
|
-
label: `${
|
|
155
|
+
value: `${SESSION_REFERENCE_PREFIX}[${token}]`,
|
|
156
|
+
label: `${kindLabel} ${title}`,
|
|
160
157
|
description: `${workspace} · ${session.messageCount} messages · ${formatDate(session.modified)}`,
|
|
161
158
|
};
|
|
162
|
-
|
|
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
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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) ??
|
|
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 ||
|
|
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
|
-
|
|
23
|
-
const
|
|
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
|
|
110
|
+
const rawQuery = extractMentionQuery(textBeforeCursor);
|
|
111
|
+
if (rawQuery === undefined) {
|
|
112
|
+
return current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
113
|
+
}
|
|
78
114
|
|
|
79
|
-
|
|
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(
|
|
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:
|
|
133
|
+
value: subagentReferenceValue(agent.name),
|
|
98
134
|
label: `[SubAgent] ${agent.displayName}`,
|
|
99
135
|
description: `${agent.model ?? "?"} · ${agent.thinking ?? "?"}`,
|
|
100
136
|
}));
|
|
101
|
-
const
|
|
102
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
}
|