pi-dynamic-help 0.1.0 → 0.2.0
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/CHANGELOG.md +11 -1
- package/README.md +23 -9
- package/extensions/help.ts +4 -843
- package/package.json +4 -3
- package/src/constants.ts +26 -0
- package/src/extension.ts +154 -0
- package/src/identity.ts +92 -0
- package/src/render/markdown.ts +114 -0
- package/src/resources/index.ts +201 -0
- package/src/resources/mcp.ts +31 -0
- package/src/resources/settings.ts +45 -0
- package/src/state/migrate.ts +80 -0
- package/src/state/store.ts +70 -0
- package/src/state/usage.ts +60 -0
- package/src/types.ts +56 -0
package/extensions/help.ts
CHANGED
|
@@ -1,845 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { basename, isAbsolute, join, relative } from "node:path";
|
|
4
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { DynamicBorder, getAgentDir, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { Container, Markdown, matchesKey, Text } from "@earendil-works/pi-tui";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { registerHelpExtension } from "../src/extension.js";
|
|
7
3
|
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
export type ResourceScope = "project" | "user" | "temporary" | "builtin";
|
|
11
|
-
|
|
12
|
-
export interface IndexedItem {
|
|
13
|
-
key: string;
|
|
14
|
-
kind: ResourceKind;
|
|
15
|
-
name: string;
|
|
16
|
-
description?: string;
|
|
17
|
-
sourceLabel: string;
|
|
18
|
-
sourceRef?: string;
|
|
19
|
-
scope?: ResourceScope;
|
|
20
|
-
packageName?: string;
|
|
21
|
-
useCount: number;
|
|
22
|
-
lastUsedAt?: number;
|
|
23
|
-
lastSeenAt?: number;
|
|
24
|
-
pinned?: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface HelpState {
|
|
28
|
-
version: 2;
|
|
29
|
-
items: Record<string, IndexedItem>;
|
|
30
|
-
lastRefresh?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface LegacyHelpState {
|
|
34
|
-
version?: number;
|
|
35
|
-
items?: Record<string, Partial<IndexedItem> & { key?: string; kind?: ResourceKind; name?: string; source?: string; origin?: string }>;
|
|
36
|
-
lastRefresh?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface McpServerEntry {
|
|
40
|
-
name: string;
|
|
41
|
-
mode: string;
|
|
42
|
-
lifecycle?: string;
|
|
43
|
-
command?: string;
|
|
44
|
-
url?: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface PackageEntry {
|
|
48
|
-
name: string;
|
|
49
|
-
scope: ResourceScope;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const PACKAGE_STATE_DIR = join(getAgentDir(), "state", "pi-dynamic-help");
|
|
53
|
-
const STATE_FILE = join(PACKAGE_STATE_DIR, "state.json");
|
|
54
|
-
const LEGACY_STATE_FILE = join(getAgentDir(), "state", "dynamic-help.json");
|
|
55
|
-
const MAX_ITEMS = 250;
|
|
56
|
-
const MAX_COMMON = 8;
|
|
57
|
-
const MAX_SECTION = 16;
|
|
58
|
-
const COMPACT_PREFIXES = ["local:", "project:", "pkg:", "mcp:", "npm:", "git:", "http://", "https://"];
|
|
59
|
-
|
|
60
|
-
const BUILTIN_COMMANDS: Array<{ name: string; description: string }> = [
|
|
61
|
-
{ name: "model", description: "Switch models" },
|
|
62
|
-
{ name: "scoped-models", description: "Enable or disable models for cycling" },
|
|
63
|
-
{ name: "settings", description: "Open interactive settings" },
|
|
64
|
-
{ name: "resume", description: "Pick from previous sessions" },
|
|
65
|
-
{ name: "new", description: "Start a new session" },
|
|
66
|
-
{ name: "name", description: "Set session display name" },
|
|
67
|
-
{ name: "session", description: "Show session file, messages, tokens, and cost" },
|
|
68
|
-
{ name: "tree", description: "Navigate the current session tree" },
|
|
69
|
-
{ name: "fork", description: "Create a new session from an earlier user message" },
|
|
70
|
-
{ name: "clone", description: "Duplicate the current active branch" },
|
|
71
|
-
{ name: "compact", description: "Summarize older context" },
|
|
72
|
-
{ name: "copy", description: "Copy the last assistant message" },
|
|
73
|
-
{ name: "export", description: "Export the session to HTML" },
|
|
74
|
-
{ name: "share", description: "Upload the session as a private gist" },
|
|
75
|
-
{ name: "reload", description: "Reload extensions, skills, prompts, and context files" },
|
|
76
|
-
{ name: "hotkeys", description: "Show keyboard shortcuts" },
|
|
77
|
-
{ name: "changelog", description: "Display version history" },
|
|
78
|
-
{ name: "quit", description: "Quit Pi" },
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
const MCP_CONFIG_FILES = [
|
|
82
|
-
join(homedir(), ".config", "mcp", "mcp.json"),
|
|
83
|
-
join(getAgentDir(), "mcp.json"),
|
|
84
|
-
];
|
|
85
|
-
|
|
86
|
-
function ensurePackageStateDir(): void {
|
|
87
|
-
if (!existsSync(PACKAGE_STATE_DIR)) {
|
|
88
|
-
mkdirSync(PACKAGE_STATE_DIR, { recursive: true });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function loadJson<T>(path: string, fallback: T): T {
|
|
93
|
-
if (!existsSync(path)) {
|
|
94
|
-
return fallback;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
return JSON.parse(readFileSync(path, "utf-8")) as T;
|
|
99
|
-
} catch {
|
|
100
|
-
return fallback;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function saveJson(path: string, data: unknown): void {
|
|
105
|
-
ensurePackageStateDir();
|
|
106
|
-
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function emptyState(): HelpState {
|
|
110
|
-
return { version: 2, items: {} };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function loadState(): HelpState {
|
|
114
|
-
const current = loadJson<LegacyHelpState | HelpState | undefined>(STATE_FILE, undefined);
|
|
115
|
-
if (current?.items) {
|
|
116
|
-
return migrateState(current);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const legacy = loadJson<LegacyHelpState | undefined>(LEGACY_STATE_FILE, undefined);
|
|
120
|
-
const migrated = migrateState(legacy ?? emptyState());
|
|
121
|
-
if (legacy?.items) {
|
|
122
|
-
saveJson(STATE_FILE, migrated);
|
|
123
|
-
}
|
|
124
|
-
return migrated;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function legacySourceFromKey(key: string | undefined): string | undefined {
|
|
128
|
-
if (!key) return undefined;
|
|
129
|
-
const first = key.indexOf(":");
|
|
130
|
-
const last = key.lastIndexOf(":");
|
|
131
|
-
if (first < 0 || last <= first) return undefined;
|
|
132
|
-
return key.slice(first + 1, last);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function sourceLabelForLegacy(kind: ResourceKind, source?: string, scope?: ResourceScope): string {
|
|
136
|
-
if (kind === "package") return sourceLabelForPackage(source ?? "package");
|
|
137
|
-
if (kind === "mcp") return source === "mcp" ? "内置网关" : "MCP 服务器";
|
|
138
|
-
if (kind === "tool") return sourceLabelForTool({ source, scope });
|
|
139
|
-
return sourceLabelForCommand({ source, scope });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function mergeMigratedItem(existing: IndexedItem | undefined, next: IndexedItem): IndexedItem {
|
|
143
|
-
if (!existing) return next;
|
|
144
|
-
|
|
145
|
-
return {
|
|
146
|
-
...existing,
|
|
147
|
-
...next,
|
|
148
|
-
useCount: Math.max(existing.useCount ?? 0, next.useCount ?? 0),
|
|
149
|
-
lastUsedAt: Math.max(existing.lastUsedAt ?? 0, next.lastUsedAt ?? 0) || undefined,
|
|
150
|
-
lastSeenAt: Math.max(existing.lastSeenAt ?? 0, next.lastSeenAt ?? 0) || undefined,
|
|
151
|
-
pinned: Boolean(existing.pinned || next.pinned),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function migrateState(state: LegacyHelpState | HelpState): HelpState {
|
|
156
|
-
const items: Record<string, IndexedItem> = {};
|
|
157
|
-
|
|
158
|
-
for (const raw of Object.values(state.items ?? {})) {
|
|
159
|
-
if (!raw.kind || !raw.name) continue;
|
|
160
|
-
const rawSource = raw.sourceRef ?? raw.source ?? legacySourceFromKey(raw.key) ?? raw.packageName ?? raw.sourceLabel;
|
|
161
|
-
const sourceRef = compactSourceRef(rawSource);
|
|
162
|
-
const sourceLabel = raw.sourceLabel && !isAbsolute(raw.sourceLabel) ? raw.sourceLabel : sourceLabelForLegacy(raw.kind, rawSource, raw.scope);
|
|
163
|
-
const next: IndexedItem = {
|
|
164
|
-
key: makeKey(raw.kind, raw.name, sourceRef),
|
|
165
|
-
kind: raw.kind,
|
|
166
|
-
name: raw.name,
|
|
167
|
-
description: raw.description,
|
|
168
|
-
sourceLabel,
|
|
169
|
-
sourceRef,
|
|
170
|
-
scope: raw.scope,
|
|
171
|
-
packageName: raw.packageName,
|
|
172
|
-
useCount: raw.useCount ?? 0,
|
|
173
|
-
lastUsedAt: raw.lastUsedAt,
|
|
174
|
-
lastSeenAt: raw.lastSeenAt,
|
|
175
|
-
pinned: raw.pinned,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
items[next.key] = mergeMigratedItem(items[next.key], next);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return {
|
|
182
|
-
version: 2,
|
|
183
|
-
items,
|
|
184
|
-
lastRefresh: state.lastRefresh,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function normalize(value: string | undefined): string {
|
|
189
|
-
return (value ?? "")
|
|
190
|
-
.toLowerCase()
|
|
191
|
-
.replace(/[^a-z0-9\u4e00-\u9fff]+/g, " ")
|
|
192
|
-
.trim();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function extractPackageNameFromPath(source: string): string | undefined {
|
|
196
|
-
const match = source.replace(/\\/g, "/").match(/\/node_modules\/(.*)$/);
|
|
197
|
-
if (!match) return undefined;
|
|
198
|
-
|
|
199
|
-
const parts = match[1].split("/").filter(Boolean);
|
|
200
|
-
if (parts.length === 0) return undefined;
|
|
201
|
-
if (parts[0].startsWith("@")) {
|
|
202
|
-
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return parts[0];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function stripRepeatedLocalPrefixes(value: string): string {
|
|
209
|
-
let next = value;
|
|
210
|
-
while (next.startsWith("local:local:")) {
|
|
211
|
-
next = next.slice("local:".length);
|
|
212
|
-
}
|
|
213
|
-
return next;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function unwrapLocalWrappedCompactRef(value: string): string {
|
|
217
|
-
if (!value.startsWith("local:")) return value;
|
|
218
|
-
|
|
219
|
-
const inner = value.slice("local:".length);
|
|
220
|
-
if (inner === "builtin" || inner === "sdk" || inner === "unknown") return inner;
|
|
221
|
-
if (COMPACT_PREFIXES.filter((prefix) => prefix !== "local:").some((prefix) => inner.startsWith(prefix))) {
|
|
222
|
-
return inner;
|
|
223
|
-
}
|
|
224
|
-
return value;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function compactSourceRef(source?: string): string {
|
|
228
|
-
if (!source) return "unknown";
|
|
229
|
-
|
|
230
|
-
const normalized = unwrapLocalWrappedCompactRef(stripRepeatedLocalPrefixes(source.replace(/\\/g, "/")));
|
|
231
|
-
if (normalized === "builtin" || normalized === "sdk" || normalized === "unknown") return normalized;
|
|
232
|
-
if (COMPACT_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return normalized;
|
|
233
|
-
|
|
234
|
-
const packageName = extractPackageNameFromPath(normalized);
|
|
235
|
-
if (packageName) return `pkg:${packageName}`;
|
|
236
|
-
|
|
237
|
-
if (isAbsolute(normalized)) {
|
|
238
|
-
const homeRelative = relative(homedir(), normalized).replace(/\\/g, "/");
|
|
239
|
-
if (homeRelative && !homeRelative.startsWith("..")) {
|
|
240
|
-
return `local:${homeRelative}`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return `local:${basename(normalized)}`;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return `local:${normalized}`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
export function makeKey(kind: ResourceKind, name: string, sourceRef: string): string {
|
|
250
|
-
return `${kind}:${sourceRef}:${name}`;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function getProjectSettingsPath(cwd: string): string {
|
|
254
|
-
return join(cwd, ".pi", "settings.json");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function getProjectMcpPath(cwd: string): string {
|
|
258
|
-
return join(cwd, ".mcp.json");
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function getProjectPiMcpPath(cwd: string): string {
|
|
262
|
-
return join(cwd, ".pi", "mcp.json");
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function readPackagesFromSettings(path: string, scope: ResourceScope): PackageEntry[] {
|
|
266
|
-
const settings = loadJson<Record<string, unknown>>(path, {});
|
|
267
|
-
const raw = Array.isArray(settings.packages) ? settings.packages : [];
|
|
268
|
-
const values: PackageEntry[] = [];
|
|
269
|
-
|
|
270
|
-
for (const entry of raw) {
|
|
271
|
-
if (typeof entry === "string") {
|
|
272
|
-
values.push({ name: entry, scope });
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (!entry || typeof entry !== "object") {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const obj = entry as Record<string, unknown>;
|
|
281
|
-
if (typeof obj.source === "string") {
|
|
282
|
-
values.push({ name: obj.source, scope });
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (typeof obj.package === "string") {
|
|
287
|
-
values.push({ name: obj.package, scope });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return values;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function mergePackageEntries(entries: PackageEntry[]): PackageEntry[] {
|
|
295
|
-
const byName = new Map<string, PackageEntry>();
|
|
296
|
-
for (const entry of entries) {
|
|
297
|
-
const existing = byName.get(entry.name);
|
|
298
|
-
if (!existing || existing.scope !== "project") {
|
|
299
|
-
byName.set(entry.name, entry);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return [...byName.values()];
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function readMcpServersFromConfig(path: string): McpServerEntry[] {
|
|
306
|
-
const config = loadJson<Record<string, unknown>>(path, {});
|
|
307
|
-
const servers = config.mcpServers;
|
|
308
|
-
if (!servers || typeof servers !== "object") {
|
|
309
|
-
return [];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const entries: McpServerEntry[] = [];
|
|
313
|
-
for (const [name, value] of Object.entries(servers as Record<string, unknown>)) {
|
|
314
|
-
if (!value || typeof value !== "object") {
|
|
315
|
-
continue;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
const server = value as Record<string, unknown>;
|
|
319
|
-
const command = typeof server.command === "string" ? server.command : undefined;
|
|
320
|
-
const url = typeof server.url === "string" ? server.url : undefined;
|
|
321
|
-
const lifecycle = typeof server.lifecycle === "string" ? server.lifecycle : undefined;
|
|
322
|
-
const mode = url ? "http" : "stdio";
|
|
323
|
-
|
|
324
|
-
entries.push({ name, mode, lifecycle, command, url });
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return entries;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function sourceLabelForCommand(sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }): string {
|
|
331
|
-
const source = sourceInfo.source?.replace(/\\/g, "/");
|
|
332
|
-
if (!source) return "未知";
|
|
333
|
-
if (source === "builtin") return "内置";
|
|
334
|
-
if (source === "sdk") return "SDK";
|
|
335
|
-
if (source === "local") return sourceInfo.scope === "project" ? "项目本地" : "全局本地";
|
|
336
|
-
if (sourceInfo.origin === "package") {
|
|
337
|
-
const pkg = extractPackageNameFromPath(source) ?? (source.startsWith("npm:") ? source.slice("npm:".length) : undefined);
|
|
338
|
-
return pkg ? `npm:${pkg}` : sourceLabelForPackage(source);
|
|
339
|
-
}
|
|
340
|
-
if (source === "extension") return "扩展";
|
|
341
|
-
if (source === "prompt") return "模板";
|
|
342
|
-
if (source === "skill") return "技能";
|
|
343
|
-
if (source.startsWith("npm:") || source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://")) return source;
|
|
344
|
-
if (isAbsolute(source)) {
|
|
345
|
-
const pkg = extractPackageNameFromPath(source);
|
|
346
|
-
if (pkg) return `npm:${pkg}`;
|
|
347
|
-
return sourceInfo.scope === "project" ? "项目本地" : "全局本地";
|
|
348
|
-
}
|
|
349
|
-
return source;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function sourceLabelForTool(sourceInfo: { source?: string; scope?: ResourceScope; origin?: string }): string {
|
|
353
|
-
const source = sourceInfo.source?.replace(/\\/g, "/");
|
|
354
|
-
if (!source) return "未知工具";
|
|
355
|
-
if (source === "builtin") return "内置工具";
|
|
356
|
-
if (source === "sdk") return "SDK 工具";
|
|
357
|
-
if (source === "local") return sourceInfo.scope === "project" ? "项目扩展" : "全局扩展";
|
|
358
|
-
if (sourceInfo.origin === "package") {
|
|
359
|
-
const pkg = extractPackageNameFromPath(source) ?? (source.startsWith("npm:") ? source.slice("npm:".length) : undefined);
|
|
360
|
-
return pkg ? `npm:${pkg}` : sourceLabelForPackage(source);
|
|
361
|
-
}
|
|
362
|
-
if (source.startsWith("npm:") || source.startsWith("git:") || source.startsWith("http://") || source.startsWith("https://")) return source;
|
|
363
|
-
if (isAbsolute(source)) {
|
|
364
|
-
const pkg = extractPackageNameFromPath(source);
|
|
365
|
-
if (pkg) return `npm:${pkg}`;
|
|
366
|
-
return sourceInfo.scope === "project" ? "项目扩展" : "全局扩展";
|
|
367
|
-
}
|
|
368
|
-
return source;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function sourceLabelForPackage(pkg: string): string {
|
|
372
|
-
if (pkg.startsWith("npm:")) return pkg;
|
|
373
|
-
if (pkg.startsWith("git:")) return "git";
|
|
374
|
-
if (pkg.startsWith("http://") || pkg.startsWith("https://")) return "git";
|
|
375
|
-
if (pkg.startsWith("./") || pkg.startsWith("../") || isAbsolute(pkg)) return "本地包";
|
|
376
|
-
return `npm:${pkg}`;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function sourceLabelForMcp(server: McpServerEntry): string {
|
|
380
|
-
const bits = [server.mode === "http" ? "HTTP" : "stdio"];
|
|
381
|
-
if (server.lifecycle) bits.push(server.lifecycle);
|
|
382
|
-
return bits.join(" · ");
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function upsertItem(state: HelpState, item: IndexedItem): IndexedItem {
|
|
386
|
-
const existing = state.items[item.key];
|
|
387
|
-
const merged: IndexedItem = {
|
|
388
|
-
...existing,
|
|
389
|
-
...item,
|
|
390
|
-
useCount: existing?.useCount ?? item.useCount ?? 0,
|
|
391
|
-
lastUsedAt: existing?.lastUsedAt ?? item.lastUsedAt,
|
|
392
|
-
lastSeenAt: existing?.lastSeenAt ?? item.lastSeenAt,
|
|
393
|
-
pinned: existing?.pinned ?? item.pinned ?? false,
|
|
394
|
-
};
|
|
395
|
-
state.items[item.key] = merged;
|
|
396
|
-
return merged;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function recordUsage(state: HelpState, key: string): void {
|
|
400
|
-
const item = state.items[key];
|
|
401
|
-
if (!item) return;
|
|
402
|
-
item.useCount = (item.useCount ?? 0) + 1;
|
|
403
|
-
item.lastUsedAt = Date.now();
|
|
404
|
-
item.lastSeenAt = Date.now();
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function recordUsageByName(state: HelpState, kind: ResourceKind, name: string): void {
|
|
408
|
-
const matches = Object.values(state.items).filter((item) => item.kind === kind && item.name === name);
|
|
409
|
-
if (matches.length === 0) return;
|
|
410
|
-
const target = [...matches].sort(sortItems)[0];
|
|
411
|
-
if (!target) return;
|
|
412
|
-
recordUsage(state, target.key);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function syncSessionUsage(ctx: ExtensionContext, state: HelpState): void {
|
|
416
|
-
for (const entry of ctx.sessionManager.getBranch()) {
|
|
417
|
-
const entryType = (entry as { type?: string }).type;
|
|
418
|
-
if (entryType === "bashExecution") {
|
|
419
|
-
recordUsageByName(state, "tool", "bash");
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
if (entryType !== "message") continue;
|
|
424
|
-
const message = (entry as { message?: unknown }).message as {
|
|
425
|
-
role?: string;
|
|
426
|
-
toolName?: string;
|
|
427
|
-
content?: unknown;
|
|
428
|
-
} | undefined;
|
|
429
|
-
if (!message) continue;
|
|
430
|
-
|
|
431
|
-
if (message.role === "toolResult" && typeof message.toolName === "string") {
|
|
432
|
-
recordUsageByName(state, "tool", message.toolName);
|
|
433
|
-
if (message.toolName === "mcp") {
|
|
434
|
-
recordUsageByName(state, "mcp", "mcp");
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
439
|
-
for (const block of message.content) {
|
|
440
|
-
if (!block || typeof block !== "object") continue;
|
|
441
|
-
const toolCall = block as { type?: string; name?: string };
|
|
442
|
-
if (toolCall.type === "toolCall" && typeof toolCall.name === "string") {
|
|
443
|
-
recordUsageByName(state, "tool", toolCall.name);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function syncMcpUsageFromResult(state: HelpState, event: { toolName: string; input?: { server?: string } | undefined }): void {
|
|
451
|
-
if (event.toolName !== "mcp") return;
|
|
452
|
-
recordUsageByName(state, "mcp", "mcp");
|
|
453
|
-
if (event.input?.server) {
|
|
454
|
-
recordUsageByName(state, "mcp", event.input.server);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function pruneState(state: HelpState): void {
|
|
459
|
-
const items = Object.values(state.items);
|
|
460
|
-
if (items.length <= MAX_ITEMS) return;
|
|
461
|
-
|
|
462
|
-
const removable = items
|
|
463
|
-
.filter((item) => !item.pinned)
|
|
464
|
-
.sort((a, b) => {
|
|
465
|
-
if ((a.useCount ?? 0) !== (b.useCount ?? 0)) {
|
|
466
|
-
return (a.useCount ?? 0) - (b.useCount ?? 0);
|
|
467
|
-
}
|
|
468
|
-
return (a.lastUsedAt ?? 0) - (b.lastUsedAt ?? 0);
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
while (Object.keys(state.items).length > MAX_ITEMS && removable.length > 0) {
|
|
472
|
-
const victim = removable.shift();
|
|
473
|
-
if (victim) {
|
|
474
|
-
delete state.items[victim.key];
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function sortItems(a: IndexedItem, b: IndexedItem): number {
|
|
480
|
-
if ((a.pinned ? 1 : 0) !== (b.pinned ? 1 : 0)) return (b.pinned ? 1 : 0) - (a.pinned ? 1 : 0);
|
|
481
|
-
if ((a.useCount ?? 0) !== (b.useCount ?? 0)) return (b.useCount ?? 0) - (a.useCount ?? 0);
|
|
482
|
-
if ((a.lastUsedAt ?? 0) !== (b.lastUsedAt ?? 0)) return (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0);
|
|
483
|
-
return a.name.localeCompare(b.name);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
function formatItem(item: IndexedItem): string {
|
|
487
|
-
const meta = item.sourceLabel && !isAbsolute(item.sourceLabel) ? [item.sourceLabel] : [];
|
|
488
|
-
if (item.scope && item.scope !== "builtin") {
|
|
489
|
-
meta.push(item.scope === "project" ? "项目" : item.scope === "user" ? "全局" : item.scope);
|
|
490
|
-
}
|
|
491
|
-
if ((item.useCount ?? 0) > 0) {
|
|
492
|
-
meta.push(`使用 ${item.useCount} 次`);
|
|
493
|
-
}
|
|
494
|
-
if (item.pinned) {
|
|
495
|
-
meta.push("已固定");
|
|
496
|
-
}
|
|
497
|
-
const suffix = meta.length > 0 ? ` · ${meta.join(" · ")}` : "";
|
|
498
|
-
const desc = item.description ? ` — ${item.description}` : "";
|
|
499
|
-
return `- **${item.name}**${desc}${suffix}`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function normalizeResourceIndex(pi: ExtensionAPI, ctx: ExtensionContext, state: HelpState): { commands: IndexedItem[]; tools: IndexedItem[]; mcp: IndexedItem[]; packages: IndexedItem[] } {
|
|
503
|
-
const commands = pi.getCommands();
|
|
504
|
-
const tools = pi.getAllTools();
|
|
505
|
-
const packages = mergePackageEntries([
|
|
506
|
-
...readPackagesFromSettings(join(getAgentDir(), "settings.json"), "user"),
|
|
507
|
-
...readPackagesFromSettings(getProjectSettingsPath(ctx.cwd), "project"),
|
|
508
|
-
]);
|
|
509
|
-
const mcpServers = [
|
|
510
|
-
...readMcpServersFromConfig(MCP_CONFIG_FILES[0]),
|
|
511
|
-
...readMcpServersFromConfig(MCP_CONFIG_FILES[1]),
|
|
512
|
-
...readMcpServersFromConfig(getProjectMcpPath(ctx.cwd)),
|
|
513
|
-
...readMcpServersFromConfig(getProjectPiMcpPath(ctx.cwd)),
|
|
514
|
-
];
|
|
515
|
-
|
|
516
|
-
const mergedMcp = new Map<string, McpServerEntry>();
|
|
517
|
-
for (const server of mcpServers) {
|
|
518
|
-
mergedMcp.set(server.name, server);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const commandItems: IndexedItem[] = [];
|
|
522
|
-
for (const command of commands) {
|
|
523
|
-
const sourceRef = compactSourceRef(command.sourceInfo.source ?? command.sourceInfo.origin ?? command.sourceInfo.scope);
|
|
524
|
-
const sourceLabel = sourceLabelForCommand(command.sourceInfo);
|
|
525
|
-
const key = makeKey("command", command.name, sourceRef);
|
|
526
|
-
commandItems.push(upsertItem(state, {
|
|
527
|
-
key,
|
|
528
|
-
kind: "command",
|
|
529
|
-
name: `/${command.name}`,
|
|
530
|
-
description: command.description,
|
|
531
|
-
sourceLabel,
|
|
532
|
-
sourceRef,
|
|
533
|
-
scope: command.sourceInfo.scope,
|
|
534
|
-
packageName: command.sourceInfo.source,
|
|
535
|
-
useCount: state.items[key]?.useCount ?? 0,
|
|
536
|
-
lastUsedAt: state.items[key]?.lastUsedAt,
|
|
537
|
-
lastSeenAt: state.items[key]?.lastSeenAt,
|
|
538
|
-
pinned: state.items[key]?.pinned,
|
|
539
|
-
}));
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
for (const builtin of BUILTIN_COMMANDS) {
|
|
543
|
-
const sourceRef = "builtin";
|
|
544
|
-
const key = makeKey("command", builtin.name, sourceRef);
|
|
545
|
-
commandItems.push(upsertItem(state, {
|
|
546
|
-
key,
|
|
547
|
-
kind: "command",
|
|
548
|
-
name: `/${builtin.name}`,
|
|
549
|
-
description: builtin.description,
|
|
550
|
-
sourceLabel: "内置",
|
|
551
|
-
sourceRef,
|
|
552
|
-
scope: "builtin",
|
|
553
|
-
useCount: state.items[key]?.useCount ?? 0,
|
|
554
|
-
lastUsedAt: state.items[key]?.lastUsedAt,
|
|
555
|
-
lastSeenAt: state.items[key]?.lastSeenAt,
|
|
556
|
-
pinned: state.items[key]?.pinned,
|
|
557
|
-
}));
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
const toolItems: IndexedItem[] = [];
|
|
561
|
-
for (const tool of tools) {
|
|
562
|
-
if (tool.name === "mcp") continue;
|
|
563
|
-
const sourceRef = compactSourceRef(tool.sourceInfo.source ?? tool.sourceInfo.origin ?? tool.sourceInfo.scope);
|
|
564
|
-
const sourceLabel = sourceLabelForTool(tool.sourceInfo);
|
|
565
|
-
const key = makeKey("tool", tool.name, sourceRef);
|
|
566
|
-
toolItems.push(upsertItem(state, {
|
|
567
|
-
key,
|
|
568
|
-
kind: "tool",
|
|
569
|
-
name: tool.name,
|
|
570
|
-
description: tool.description,
|
|
571
|
-
sourceLabel,
|
|
572
|
-
sourceRef,
|
|
573
|
-
scope: tool.sourceInfo.scope,
|
|
574
|
-
packageName: tool.sourceInfo.source,
|
|
575
|
-
useCount: state.items[key]?.useCount ?? 0,
|
|
576
|
-
lastUsedAt: state.items[key]?.lastUsedAt,
|
|
577
|
-
lastSeenAt: state.items[key]?.lastSeenAt,
|
|
578
|
-
pinned: state.items[key]?.pinned,
|
|
579
|
-
}));
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
const packageItems: IndexedItem[] = [];
|
|
583
|
-
for (const pkg of packages) {
|
|
584
|
-
const sourceRef = compactSourceRef(pkg.name);
|
|
585
|
-
const sourceLabel = sourceLabelForPackage(pkg.name);
|
|
586
|
-
const key = makeKey("package", pkg.name, sourceRef);
|
|
587
|
-
packageItems.push(upsertItem(state, {
|
|
588
|
-
key,
|
|
589
|
-
kind: "package",
|
|
590
|
-
name: pkg.name,
|
|
591
|
-
description: "已安装包",
|
|
592
|
-
sourceLabel,
|
|
593
|
-
sourceRef,
|
|
594
|
-
scope: pkg.scope,
|
|
595
|
-
packageName: pkg.name,
|
|
596
|
-
useCount: state.items[key]?.useCount ?? 0,
|
|
597
|
-
lastUsedAt: state.items[key]?.lastUsedAt,
|
|
598
|
-
lastSeenAt: state.items[key]?.lastSeenAt,
|
|
599
|
-
pinned: state.items[key]?.pinned,
|
|
600
|
-
}));
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const mcpItems: IndexedItem[] = [];
|
|
604
|
-
const gatewaySourceRef = "mcp:gateway";
|
|
605
|
-
const gatewayKey = makeKey("mcp", "mcp", gatewaySourceRef);
|
|
606
|
-
mcpItems.push(upsertItem(state, {
|
|
607
|
-
key: gatewayKey,
|
|
608
|
-
kind: "mcp",
|
|
609
|
-
name: "mcp",
|
|
610
|
-
description: "显示 MCP 服务器状态",
|
|
611
|
-
sourceLabel: "内置网关",
|
|
612
|
-
sourceRef: gatewaySourceRef,
|
|
613
|
-
scope: "builtin",
|
|
614
|
-
useCount: state.items[gatewayKey]?.useCount ?? 0,
|
|
615
|
-
lastUsedAt: state.items[gatewayKey]?.lastUsedAt,
|
|
616
|
-
lastSeenAt: state.items[gatewayKey]?.lastSeenAt,
|
|
617
|
-
pinned: state.items[gatewayKey]?.pinned,
|
|
618
|
-
}));
|
|
619
|
-
for (const server of mergedMcp.values()) {
|
|
620
|
-
const sourceRef = `mcp:${server.name}`;
|
|
621
|
-
const sourceLabel = sourceLabelForMcp(server);
|
|
622
|
-
const key = makeKey("mcp", server.name, sourceRef);
|
|
623
|
-
mcpItems.push(upsertItem(state, {
|
|
624
|
-
key,
|
|
625
|
-
kind: "mcp",
|
|
626
|
-
name: server.name,
|
|
627
|
-
description: server.url ? server.url : server.command ? server.command : "MCP 服务器",
|
|
628
|
-
sourceLabel,
|
|
629
|
-
sourceRef,
|
|
630
|
-
scope: "user",
|
|
631
|
-
packageName: server.command ?? server.url,
|
|
632
|
-
useCount: state.items[key]?.useCount ?? 0,
|
|
633
|
-
lastUsedAt: state.items[key]?.lastUsedAt,
|
|
634
|
-
lastSeenAt: state.items[key]?.lastSeenAt,
|
|
635
|
-
pinned: state.items[key]?.pinned,
|
|
636
|
-
}));
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
pruneState(state);
|
|
640
|
-
state.lastRefresh = Date.now();
|
|
641
|
-
saveJson(STATE_FILE, state);
|
|
642
|
-
|
|
643
|
-
return { commands: commandItems, tools: toolItems, mcp: mcpItems, packages: packageItems };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function selectTop(items: IndexedItem[], count: number): IndexedItem[] {
|
|
647
|
-
return [...items].sort(sortItems).slice(0, count);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function renderSection(title: string, items: IndexedItem[], emptyText: string): string {
|
|
651
|
-
if (items.length === 0) {
|
|
652
|
-
return [`## ${title}`, `- ${emptyText}`].join("\n");
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return [`## ${title}`, items.map(formatItem).join("\n")].join("\n");
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
export function buildMarkdown(pi: ExtensionAPI, ctx: ExtensionContext, state: HelpState, query?: string): string {
|
|
659
|
-
const index = normalizeResourceIndex(pi, ctx, state);
|
|
660
|
-
const normalizedQuery = normalize(query);
|
|
661
|
-
|
|
662
|
-
const allVisible = [
|
|
663
|
-
...index.commands,
|
|
664
|
-
...index.tools,
|
|
665
|
-
...index.mcp,
|
|
666
|
-
...index.packages,
|
|
667
|
-
].filter((item) => {
|
|
668
|
-
if (!normalizedQuery) return true;
|
|
669
|
-
return normalize([item.name, item.description, item.sourceLabel, item.packageName, item.sourceRef, item.kind].join(" ")).includes(normalizedQuery);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
const commonCommands = selectTop(allVisible.filter((item) => item.kind === "command"), MAX_COMMON);
|
|
673
|
-
const commonTools = selectTop(allVisible.filter((item) => item.kind === "tool"), MAX_COMMON);
|
|
674
|
-
const commonMcp = selectTop(allVisible.filter((item) => item.kind === "mcp"), MAX_COMMON);
|
|
675
|
-
const commonPackages = selectTop(allVisible.filter((item) => item.kind === "package"), MAX_COMMON);
|
|
676
|
-
|
|
677
|
-
const commandItems = selectTop(allVisible.filter((item) => item.kind === "command"), MAX_SECTION);
|
|
678
|
-
const toolItems = selectTop(allVisible.filter((item) => item.kind === "tool"), MAX_SECTION);
|
|
679
|
-
const mcpItems = selectTop(allVisible.filter((item) => item.kind === "mcp"), MAX_SECTION);
|
|
680
|
-
const packageItems = selectTop(allVisible.filter((item) => item.kind === "package"), MAX_SECTION);
|
|
681
|
-
|
|
682
|
-
const gatewayItems = mcpItems.filter((item) => item.name === "mcp");
|
|
683
|
-
const serverItems = mcpItems.filter((item) => item.name !== "mcp");
|
|
684
|
-
|
|
685
|
-
const updatedAt = state.lastRefresh ? new Date(state.lastRefresh).toLocaleString() : new Date().toLocaleString();
|
|
686
|
-
const title = normalizedQuery ? `动态帮助 · 搜索:${query?.trim()}` : "动态帮助";
|
|
687
|
-
|
|
688
|
-
const mcpUsageLines = [
|
|
689
|
-
"### 用法",
|
|
690
|
-
"- `mcp({})` 查看状态",
|
|
691
|
-
"- `mcp({ server: \"chrome-devtools\" })` 列出服务器工具",
|
|
692
|
-
"- `mcp({ search: \"screenshot\" })` 搜索工具",
|
|
693
|
-
"- `mcp({ tool: \"...\", args: '{\"...\"}' })` 调用工具",
|
|
694
|
-
].join("\n");
|
|
695
|
-
|
|
696
|
-
return [
|
|
697
|
-
`# ${title}`,
|
|
698
|
-
`_更新时间:${updatedAt}_`,
|
|
699
|
-
"",
|
|
700
|
-
`> 已加载:${index.commands.length} 条命令 · ${index.tools.length} 个工具 · ${index.mcp.length} 个 MCP 服务器 · ${index.packages.length} 个包`,
|
|
701
|
-
"",
|
|
702
|
-
renderSection("常用命令", commonCommands, "暂无"),
|
|
703
|
-
"",
|
|
704
|
-
renderSection("常用工具", commonTools, "暂无"),
|
|
705
|
-
"",
|
|
706
|
-
renderSection("常用 MCP 服务器", commonMcp, "暂无"),
|
|
707
|
-
"",
|
|
708
|
-
renderSection("常用包", commonPackages, "暂无"),
|
|
709
|
-
"",
|
|
710
|
-
"## 命令",
|
|
711
|
-
commandItems.length > 0 ? commandItems.map(formatItem).join("\n") : "- 暂无",
|
|
712
|
-
"",
|
|
713
|
-
"## 工具",
|
|
714
|
-
toolItems.length > 0 ? toolItems.map(formatItem).join("\n") : "- 暂无",
|
|
715
|
-
"",
|
|
716
|
-
"## MCP 服务器",
|
|
717
|
-
"### 网关",
|
|
718
|
-
gatewayItems.length > 0 ? gatewayItems.map(formatItem).join("\n") : "- 暂无",
|
|
719
|
-
"",
|
|
720
|
-
mcpUsageLines,
|
|
721
|
-
"",
|
|
722
|
-
"### 服务器",
|
|
723
|
-
serverItems.length > 0 ? serverItems.map(formatItem).join("\n") : "- 暂无",
|
|
724
|
-
"",
|
|
725
|
-
"## 包",
|
|
726
|
-
packageItems.length > 0 ? packageItems.map(formatItem).join("\n") : "- 暂无",
|
|
727
|
-
"",
|
|
728
|
-
"## 操作",
|
|
729
|
-
"- `/help search <词>` 搜索当前索引",
|
|
730
|
-
"- `/help pin <词>` 固定匹配项",
|
|
731
|
-
"- `/help unpin <词>` 取消固定",
|
|
732
|
-
"- `/help refresh` 重新扫描并刷新使用统计",
|
|
733
|
-
"",
|
|
734
|
-
"## 示例",
|
|
735
|
-
"- `需求分析:...`",
|
|
736
|
-
"- `系统设计:...`",
|
|
737
|
-
"- `实现:...`",
|
|
738
|
-
"- `审查:...`",
|
|
739
|
-
"- `/goal ...`",
|
|
740
|
-
"- `/brainstorm ...`",
|
|
741
|
-
].join("\n");
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
async function showMarkdownPanel(ctx: ExtensionContext, markdown: string): Promise<void> {
|
|
745
|
-
if (!ctx.hasUI) {
|
|
746
|
-
console.log(markdown);
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
await ctx.ui.custom((_tui, theme, _kb, done) => {
|
|
751
|
-
const container = new Container();
|
|
752
|
-
const border = new DynamicBorder((s: string) => theme.fg("accent", s));
|
|
753
|
-
const mdTheme = getMarkdownTheme();
|
|
754
|
-
|
|
755
|
-
container.addChild(border);
|
|
756
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("Pi 帮助")), 1, 0));
|
|
757
|
-
container.addChild(new Markdown(markdown, 1, 1, mdTheme));
|
|
758
|
-
container.addChild(new Text(theme.fg("dim", "按 Enter 或 Esc 关闭"), 1, 0));
|
|
759
|
-
container.addChild(border);
|
|
760
|
-
|
|
761
|
-
return {
|
|
762
|
-
render: (width: number) => container.render(width),
|
|
763
|
-
invalidate: () => container.invalidate(),
|
|
764
|
-
handleInput: (data: string) => {
|
|
765
|
-
if (matchesKey(data, "enter") || matchesKey(data, "escape")) {
|
|
766
|
-
done(undefined);
|
|
767
|
-
}
|
|
768
|
-
},
|
|
769
|
-
};
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function findMatches(state: HelpState, query: string): IndexedItem[] {
|
|
774
|
-
const normalizedQuery = normalize(query);
|
|
775
|
-
if (!normalizedQuery) return [];
|
|
776
|
-
return Object.values(state.items)
|
|
777
|
-
.filter((item) => normalize([item.name, item.description, item.sourceLabel, item.packageName, item.sourceRef, item.kind].join(" ")).includes(normalizedQuery))
|
|
778
|
-
.sort(sortItems);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function refreshState(pi: ExtensionAPI, ctx: ExtensionContext, state: HelpState): void {
|
|
782
|
-
syncSessionUsage(ctx, state);
|
|
783
|
-
// Make sure MCP/package caches are always rebuilt on refresh.
|
|
784
|
-
normalizeResourceIndex(pi, ctx, state);
|
|
785
|
-
saveJson(STATE_FILE, state);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
export default function (pi: ExtensionAPI) {
|
|
789
|
-
const state = loadState();
|
|
790
|
-
|
|
791
|
-
pi.registerCommand("help", {
|
|
792
|
-
description: "Show a dynamic help dashboard for installed Pi resources",
|
|
793
|
-
handler: async (args, ctx) => {
|
|
794
|
-
const trimmed = args.trim();
|
|
795
|
-
const [subcommand, ...rest] = trimmed.length > 0 ? trimmed.split(/\s+/) : [""];
|
|
796
|
-
const query = rest.join(" ").trim();
|
|
797
|
-
|
|
798
|
-
if (subcommand === "refresh") {
|
|
799
|
-
refreshState(pi, ctx, state);
|
|
800
|
-
ctx.ui.notify("帮助索引已刷新", "info");
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
if (subcommand === "pin" || subcommand === "unpin") {
|
|
805
|
-
refreshState(pi, ctx, state);
|
|
806
|
-
const matches = findMatches(state, query);
|
|
807
|
-
if (matches.length === 0) {
|
|
808
|
-
ctx.ui.notify(`未找到:${query || "<空>"}`, "warning");
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const target = matches[0];
|
|
813
|
-
target.pinned = subcommand === "pin";
|
|
814
|
-
target.lastSeenAt = Date.now();
|
|
815
|
-
saveJson(STATE_FILE, state);
|
|
816
|
-
ctx.ui.notify(`${subcommand === "pin" ? "已固定" : "已取消固定"}:${target.name} (${target.kind} · ${target.sourceLabel})`, "info");
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
refreshState(pi, ctx, state);
|
|
821
|
-
const markdown = buildMarkdown(pi, ctx, state, subcommand === "search" ? query : trimmed);
|
|
822
|
-
await showMarkdownPanel(ctx, markdown);
|
|
823
|
-
},
|
|
824
|
-
});
|
|
825
|
-
|
|
826
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
827
|
-
refreshState(pi, ctx, state);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
pi.on("tool_result", async (event) => {
|
|
831
|
-
if (event.isError) {
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
recordUsageByName(state, "tool", event.toolName);
|
|
836
|
-
syncMcpUsageFromResult(state, event as { toolName: string; input?: { server?: string } });
|
|
837
|
-
pruneState(state);
|
|
838
|
-
saveJson(STATE_FILE, state);
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
pi.on("session_shutdown", async () => {
|
|
842
|
-
pruneState(state);
|
|
843
|
-
saveJson(STATE_FILE, state);
|
|
844
|
-
});
|
|
4
|
+
export default function (pi: ExtensionAPI): void {
|
|
5
|
+
registerHelpExtension(pi);
|
|
845
6
|
}
|