pi-profile-switch 0.3.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -36
- package/README.zh-CN.md +31 -36
- package/bin/pi-profile.js +11 -0
- package/bin/pi-profile.ts +65 -0
- package/bin/postinstall.d.ts +13 -0
- package/bin/postinstall.js +88 -0
- package/defaults/profiles.json +18 -0
- package/examples/profiles.json +31 -5
- package/extensions/pi-profile/index.ts +451 -0
- package/package.json +10 -13
- package/schemas/profiles.schema.json +22 -24
- package/src/extension-discovery.ts +347 -0
- package/src/json-file.ts +1 -21
- package/src/launcher/args.ts +57 -0
- package/src/launcher/discovery.ts +64 -0
- package/src/launcher/initial-profile.ts +179 -0
- package/src/launcher/model-check.ts +52 -0
- package/src/launcher/runtime-cleanup.ts +85 -0
- package/src/launcher/spawn.ts +82 -0
- package/src/mcp-config.ts +37 -153
- package/src/mcp-coordination.ts +29 -10
- package/src/profile-catalog-store.ts +31 -12
- package/src/profile-catalog.ts +45 -93
- package/src/profile-resolver.ts +239 -245
- package/src/project-trust.ts +82 -0
- package/src/runtime-state-store.ts +25 -41
- package/src/settings-generator.ts +541 -0
- package/src/skill-registry.ts +94 -0
- package/src/switching/apply-plan.ts +197 -0
- package/src/switching/customize.ts +62 -33
- package/src/switching/list-profiles.ts +9 -6
- package/src/switching/mcp-toggle.ts +14 -26
- package/src/switching/profile-crud.ts +31 -24
- package/src/switching/profile-wizard.ts +21 -49
- package/src/switching/status.ts +142 -72
- package/src/switching/switch-profile.ts +219 -0
- package/src/switching/tool-references.ts +40 -0
- package/src/workspace.ts +57 -0
- package/LICENSE +0 -21
- package/examples/profiles.example.json +0 -74
- package/extensions/pi-profile-switch/index.ts +0 -778
- package/src/adapter-presence.ts +0 -75
- package/src/default-profiles.ts +0 -59
- package/src/mcp-overlay-file.ts +0 -35
- package/src/mcp-overlay.ts +0 -122
- package/src/model-selection.ts +0 -64
- package/src/name-matching.ts +0 -50
- package/src/profile-badge.ts +0 -142
- package/src/profile-presets.ts +0 -61
- package/src/skill-selection.ts +0 -81
- package/src/startup-mcp-scope.ts +0 -271
- package/src/startup-selection.ts +0 -201
- package/src/switching/activate-profile.ts +0 -144
- package/src/switching/apply-profile.ts +0 -131
|
@@ -1,778 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getAgentDir,
|
|
3
|
-
type BuildSystemPromptOptions,
|
|
4
|
-
type ExtensionAPI,
|
|
5
|
-
type ExtensionCommandContext,
|
|
6
|
-
type ExtensionContext,
|
|
7
|
-
} from "@earendil-works/pi-coding-agent";
|
|
8
|
-
|
|
9
|
-
import { adapterPresent } from "../../src/adapter-presence.ts";
|
|
10
|
-
import { discoverAdapterServerNames } from "../../src/mcp-config.ts";
|
|
11
|
-
import { probeAdapterPresence } from "../../src/mcp-coordination.ts";
|
|
12
|
-
import { readSessionChoices } from "../../src/model-selection.ts";
|
|
13
|
-
import { buildProfileBadge, PROFILE_STATUS_KEY, renderProfileBadge } from "../../src/profile-badge.ts";
|
|
14
|
-
import type { ProfileDefinition } from "../../src/profile-catalog.ts";
|
|
15
|
-
import { seedDefaultProfilesSync } from "../../src/default-profiles.ts";
|
|
16
|
-
import {
|
|
17
|
-
formatSelectionWarnings,
|
|
18
|
-
formatSkillWarnings,
|
|
19
|
-
skillWarnings,
|
|
20
|
-
type LiveResources,
|
|
21
|
-
type ResolvedSelection,
|
|
22
|
-
} from "../../src/profile-resolver.ts";
|
|
23
|
-
import { RuntimeStateStore, overlayNarrows, stateDirFor, type RuntimeOverlay } from "../../src/runtime-state-store.ts";
|
|
24
|
-
import {
|
|
25
|
-
applySkillsFilter,
|
|
26
|
-
formatInstructionsBlock,
|
|
27
|
-
type SkillsFilterOutcome,
|
|
28
|
-
} from "../../src/skill-selection.ts";
|
|
29
|
-
import {
|
|
30
|
-
appliedProfile,
|
|
31
|
-
detectExplicitDeclarations,
|
|
32
|
-
readProfileFlag,
|
|
33
|
-
recordAppliedProfile,
|
|
34
|
-
registerProfileFlag,
|
|
35
|
-
resolveStartupProfile,
|
|
36
|
-
} from "../../src/startup-selection.ts";
|
|
37
|
-
import {
|
|
38
|
-
readFlagFromArgv,
|
|
39
|
-
syncMcpOverlayForSelection,
|
|
40
|
-
syncStartupMcpOverlay,
|
|
41
|
-
} from "../../src/startup-mcp-scope.ts";
|
|
42
|
-
import { retryPendingTools, type ApplySurface } from "../../src/switching/apply-profile.ts";
|
|
43
|
-
import {
|
|
44
|
-
activateProfile,
|
|
45
|
-
type ActivationDeps,
|
|
46
|
-
type ActivationResult,
|
|
47
|
-
} from "../../src/switching/activate-profile.ts";
|
|
48
|
-
import { CUSTOMIZE_USAGE, customizeOverlay, parseCustomizeArgs, resetOverlay } from "../../src/switching/customize.ts";
|
|
49
|
-
import { formatProfileList, listProfiles, type ProfileListEntry } from "../../src/switching/list-profiles.ts";
|
|
50
|
-
import { setMcpServerEnabled } from "../../src/switching/mcp-toggle.ts";
|
|
51
|
-
import {
|
|
52
|
-
createProfile,
|
|
53
|
-
deleteProfile,
|
|
54
|
-
duplicateProfile,
|
|
55
|
-
editProfile,
|
|
56
|
-
readCatalogScope,
|
|
57
|
-
type CatalogInput,
|
|
58
|
-
} from "../../src/switching/profile-crud.ts";
|
|
59
|
-
import {
|
|
60
|
-
runProfileCreateWizard,
|
|
61
|
-
runProfileDuplicateWizard,
|
|
62
|
-
runProfileEditWizard,
|
|
63
|
-
} from "../../src/switching/profile-wizard.ts";
|
|
64
|
-
import { buildStatusReport, formatStatusMarkdown } from "../../src/switching/status.ts";
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* pi-profile-switch extension entry (ADR-0007).
|
|
68
|
-
*
|
|
69
|
-
* Installed like any other Pi package: no launcher, no environment override,
|
|
70
|
-
* no generated settings. The agent dir stays Pi's own, so sessions,
|
|
71
|
-
* extension configuration, packages, project trust, and context files are
|
|
72
|
-
* all native.
|
|
73
|
-
*
|
|
74
|
-
* Responsibilities:
|
|
75
|
-
* - load: seed `<agentDir>/profiles.json` from the shipped default catalog
|
|
76
|
-
* when the file does not exist yet (Pi packages have no install hook), and
|
|
77
|
-
* report a failure once at `session_start`.
|
|
78
|
-
* - `session_start`: resolve the startup profile (`--profile <flag>`, else
|
|
79
|
-
* the saved selection, else `default`; every start after the first
|
|
80
|
-
* continues the selection this process already applied), then apply the
|
|
81
|
-
* runtime parts of the selection — model preset, active tools, MCP
|
|
82
|
-
* allowlist. A failed activation applies nothing and reports loudly.
|
|
83
|
-
* - `before_agent_start`: rebuild the system prompt each turn — replace the
|
|
84
|
-
* skills section with the profile's visible set and append the profile's
|
|
85
|
-
* instructions. Unselected skills stay loaded and `/skill:`-invocable.
|
|
86
|
-
* - `/profile …` command family and `/mcp enable|disable`.
|
|
87
|
-
* - Retry pending tool literals each turn until MCP/extension tools register.
|
|
88
|
-
* - Footer badge: `profile: <name>` (plus `*` for a runtime overlay) in Pi's
|
|
89
|
-
* footer status line while a non-`default` profile is active.
|
|
90
|
-
*/
|
|
91
|
-
|
|
92
|
-
type ContextWithOptions = ExtensionContext & { getSystemPromptOptions?: () => BuildSystemPromptOptions };
|
|
93
|
-
|
|
94
|
-
/** The active profile in this runtime and the last prompt-filter result. */
|
|
95
|
-
interface Activation {
|
|
96
|
-
selection: ResolvedSelection;
|
|
97
|
-
skillsOutcome?: SkillsFilterOutcome;
|
|
98
|
-
/** Whether the skill references were checked against Pi's loaded set.
|
|
99
|
-
* False after a startup activation: `session_start`'s event context
|
|
100
|
-
* cannot read that list, so the check moves to the first turn. */
|
|
101
|
-
skillsChecked: boolean;
|
|
102
|
-
/** The overlay this runtime was activated with, when one is in effect. */
|
|
103
|
-
overlay?: RuntimeOverlay;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** Maps an activation result onto the runtime state `current` mirrors. */
|
|
107
|
-
function activationOf(result: ActivationResult, skillsChecked: boolean): Activation {
|
|
108
|
-
return {
|
|
109
|
-
selection: result.selection,
|
|
110
|
-
skillsChecked,
|
|
111
|
-
...(result.overlay === undefined ? {} : { overlay: result.overlay }),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Subcommands that mutate a catalog; they need dialog-capable UI. */
|
|
116
|
-
const CRUD_SUBCOMMANDS = ["create", "duplicate", "edit", "delete"] as const;
|
|
117
|
-
const SUBCOMMANDS = ["use", "list", "status", "customize", "reset", ...CRUD_SUBCOMMANDS] as const;
|
|
118
|
-
const REQUIRED_ARGUMENT: Readonly<Record<string, string>> = { use: "<name>", edit: "<name>", delete: "<name>" };
|
|
119
|
-
const PROFILE_USAGE = [
|
|
120
|
-
"/profile [picker]",
|
|
121
|
-
"/profile use <name>",
|
|
122
|
-
"/profile list | /profile status",
|
|
123
|
-
CUSTOMIZE_USAGE,
|
|
124
|
-
"/profile reset",
|
|
125
|
-
"/profile create | /profile duplicate",
|
|
126
|
-
"/profile edit <name> | /profile delete <name>",
|
|
127
|
-
].join(" · ");
|
|
128
|
-
|
|
129
|
-
export default function piProfileExtension(pi: ExtensionAPI): void {
|
|
130
|
-
registerProfileFlag(pi);
|
|
131
|
-
const argv = process.argv.slice(2);
|
|
132
|
-
const explicit = detectExplicitDeclarations(argv);
|
|
133
|
-
const loadAgentDir = getAgentDir();
|
|
134
|
-
// Install-time default: Pi packages have no install hook, so the first
|
|
135
|
-
// load seeds <agentDir>/profiles.json from the shipped catalog. The write
|
|
136
|
-
// is idempotent, never overwrites a user catalog, and a failure is
|
|
137
|
-
// reported at session_start instead of blocking the load.
|
|
138
|
-
let seedWarning: string | undefined;
|
|
139
|
-
try {
|
|
140
|
-
seedDefaultProfilesSync(loadAgentDir);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
seedWarning = `pi-profile-switch: could not write the default profiles.json — ${error instanceof Error ? error.message : String(error)}`;
|
|
143
|
-
}
|
|
144
|
-
// pi-mcp-adapter reads its config before any session event fires (and, for
|
|
145
|
-
// eager servers, at its own load time), so the startup profile's overlay
|
|
146
|
-
// is generated here, synchronously. Pi applies CLI flag values only after
|
|
147
|
-
// extension loading, hence argv.
|
|
148
|
-
const adapterInstalled = adapterPresent({
|
|
149
|
-
agentDir: loadAgentDir,
|
|
150
|
-
argv,
|
|
151
|
-
probeAnswered: probeAdapterPresence(pi.events),
|
|
152
|
-
});
|
|
153
|
-
if (adapterInstalled) {
|
|
154
|
-
const requestedConfigPath = readFlagFromArgv(argv, "mcp-config");
|
|
155
|
-
// A reload's overlay belongs to the run's current selection: the same
|
|
156
|
-
// continuation `session_start` applies right after this pass.
|
|
157
|
-
const runProfile = appliedProfile();
|
|
158
|
-
syncStartupMcpOverlay({
|
|
159
|
-
agentDir: loadAgentDir,
|
|
160
|
-
cwd: process.cwd(),
|
|
161
|
-
argv,
|
|
162
|
-
...(requestedConfigPath === undefined ? {} : { overridePath: requestedConfigPath }),
|
|
163
|
-
...(runProfile === undefined ? {} : { continuation: runProfile }),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
let current: Activation | undefined;
|
|
167
|
-
let filterWarningShown = false;
|
|
168
|
-
/** The last badge written to the footer, so a refresh only talks to Pi
|
|
169
|
-
* when the rendering actually changed. */
|
|
170
|
-
let badgeText: string | undefined;
|
|
171
|
-
|
|
172
|
-
/** The only writer of `current`'s profile identity (`selection.name` and
|
|
173
|
-
* `overlay`) and of the footer badge. Both mirror the selection this
|
|
174
|
-
* runtime applied, so a failed activation (which throws before reaching
|
|
175
|
-
* here) never claims to be active. The per-turn updates (`pendingTools`,
|
|
176
|
-
* `skillsOutcome`) leave the identity and the badge untouched. */
|
|
177
|
-
function setCurrent(ctx: ExtensionContext, next: Activation | undefined): void {
|
|
178
|
-
current = next;
|
|
179
|
-
refreshBadge(ctx);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/** Re-renders the badge from `current`. `default` and an unapplied profile
|
|
183
|
-
* render no badge, which removes Pi's footer status line entirely. */
|
|
184
|
-
function refreshBadge(ctx: ExtensionContext): void {
|
|
185
|
-
if (!ctx.hasUI) return;
|
|
186
|
-
const badge =
|
|
187
|
-
current === undefined
|
|
188
|
-
? undefined
|
|
189
|
-
: buildProfileBadge(current.selection.name, { overlay: overlayNarrows(current.overlay) });
|
|
190
|
-
const text = badge === undefined ? undefined : renderProfileBadge(badge, ctx.ui.theme);
|
|
191
|
-
if (text === badgeText) return;
|
|
192
|
-
badgeText = text;
|
|
193
|
-
ctx.ui.setStatus(PROFILE_STATUS_KEY, text);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const surface = (ctx: ExtensionContext): ApplySurface => ({
|
|
197
|
-
getAllTools: () => pi.getAllTools(),
|
|
198
|
-
setActiveTools: (names) => pi.setActiveTools(names),
|
|
199
|
-
modelRegistry: ctx.modelRegistry,
|
|
200
|
-
// The package does not export Pi's ThinkingLevel type, so the surface
|
|
201
|
-
// takes the API's own parameter types at the wiring boundary.
|
|
202
|
-
setModel: (model) => pi.setModel(model as Parameters<ExtensionAPI["setModel"]>[0]),
|
|
203
|
-
setThinkingLevel: (level) =>
|
|
204
|
-
pi.setThinkingLevel(level as Parameters<ExtensionAPI["setThinkingLevel"]>[0]),
|
|
205
|
-
events: pi.events,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
function notify(ctx: ExtensionContext, message: string, level: "info" | "warning" | "error"): void {
|
|
209
|
-
ctx.ui.notify(message, level);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function reportWarnings(ctx: ExtensionContext, warnings: string[]): void {
|
|
213
|
-
for (const warning of warnings) notify(ctx, warning, "warning");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function loadLive(ctx: ExtensionContext, projectTrusted: boolean): Promise<LiveResources> {
|
|
217
|
-
// Only command contexts expose the system-prompt options; the
|
|
218
|
-
// `session_start` event context has no accessor, so the loaded skills
|
|
219
|
-
// stay unknown there and their existence check moves to the first turn.
|
|
220
|
-
const options = (ctx as ContextWithOptions).getSystemPromptOptions?.();
|
|
221
|
-
const adapterPresent = probeAdapterPresence(pi.events);
|
|
222
|
-
let servers: string[] = [];
|
|
223
|
-
if (adapterPresent) {
|
|
224
|
-
try {
|
|
225
|
-
servers = await discoverAdapterServerNames(getAgentDir(), projectTrusted ? ctx.cwd : undefined);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "warning");
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return {
|
|
231
|
-
...(options?.skills === undefined
|
|
232
|
-
? {}
|
|
233
|
-
: { skills: options.skills.map((skill) => ({ name: skill.name, filePath: skill.filePath })) }),
|
|
234
|
-
toolNames: pi.getAllTools().map((tool) => tool.name),
|
|
235
|
-
mcp: { adapterPresent, servers },
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Builds the dependencies for one activation. `force` marks an explicit
|
|
240
|
-
* user choice (`/profile use`, picker, CRUD reactivation): the profile's
|
|
241
|
-
* model and tool declarations then outrank the session state and the
|
|
242
|
-
* CLI flags. */
|
|
243
|
-
async function activationDeps(ctx: ExtensionContext, force: boolean): Promise<ActivationDeps> {
|
|
244
|
-
const projectTrusted = ctx.isProjectTrusted();
|
|
245
|
-
return {
|
|
246
|
-
agentDir: getAgentDir(),
|
|
247
|
-
cwd: ctx.cwd,
|
|
248
|
-
projectTrusted,
|
|
249
|
-
live: await loadLive(ctx, projectTrusted),
|
|
250
|
-
surface: surface(ctx),
|
|
251
|
-
presetInputs: { explicit, session: readSessionChoices(ctx.sessionManager.getEntries()), force },
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Resolves, validates, persists and applies one profile, then records it
|
|
256
|
-
* as the active selection and reports its warnings. */
|
|
257
|
-
async function activate(
|
|
258
|
-
ctx: ExtensionContext,
|
|
259
|
-
name: string,
|
|
260
|
-
options?: { force?: boolean; overlay?: RuntimeOverlay | null; persist?: boolean },
|
|
261
|
-
): Promise<ActivationResult> {
|
|
262
|
-
const deps = await activationDeps(ctx, options?.force ?? false);
|
|
263
|
-
const result = await activateProfile(name, deps, {
|
|
264
|
-
overlay: options?.overlay ?? null,
|
|
265
|
-
persist: options?.persist ?? true,
|
|
266
|
-
});
|
|
267
|
-
// The run's current selection, for the reload continuation.
|
|
268
|
-
recordAppliedProfile(result.selection.name);
|
|
269
|
-
setCurrent(ctx, activationOf(result, deps.live.skills !== undefined));
|
|
270
|
-
reportWarnings(ctx, formatSelectionWarnings(result.selection));
|
|
271
|
-
return result;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async function profileEntries(ctx: ExtensionContext): Promise<ProfileListEntry[]> {
|
|
275
|
-
return listProfiles({
|
|
276
|
-
realAgentDir: getAgentDir(),
|
|
277
|
-
cwd: ctx.cwd,
|
|
278
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function sendListMessage(entries: ProfileListEntry[]): void {
|
|
283
|
-
pi.sendMessage({
|
|
284
|
-
customType: "pi-profile-switch",
|
|
285
|
-
content: formatProfileList(entries, current?.selection.name),
|
|
286
|
-
display: true,
|
|
287
|
-
details: { kind: "list", profiles: entries },
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** `session_start` re-check. The load-time pass cannot know an interactive
|
|
292
|
-
* trust answer, and the adapter has already read its config by now — so a
|
|
293
|
-
* difference is written for the next reload and reported instead. */
|
|
294
|
-
function reportMcpOverlayState(
|
|
295
|
-
ctx: ExtensionContext,
|
|
296
|
-
profileName: string,
|
|
297
|
-
selection: ResolvedSelection,
|
|
298
|
-
input: { agentDir: string; cwd: string; projectTrusted: boolean },
|
|
299
|
-
): void {
|
|
300
|
-
const configOverride = readFlagFromArgv(argv, "mcp-config");
|
|
301
|
-
const sync = syncStartupMcpOverlay({
|
|
302
|
-
agentDir: input.agentDir,
|
|
303
|
-
cwd: input.cwd,
|
|
304
|
-
projectTrusted: input.projectTrusted,
|
|
305
|
-
profileName,
|
|
306
|
-
argv,
|
|
307
|
-
...(configOverride === undefined ? {} : { overridePath: configOverride }),
|
|
308
|
-
});
|
|
309
|
-
if (sync.error !== undefined) {
|
|
310
|
-
notify(ctx, `pi-profile-switch: MCP overlay unavailable — ${sync.error}`, "warning");
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (!sync.managed) {
|
|
314
|
-
if (selection.mcp !== undefined) {
|
|
315
|
-
notify(
|
|
316
|
-
ctx,
|
|
317
|
-
`pi-profile-switch: --mcp-config points at another file — profile "${profileName}" cannot filter MCP servers`,
|
|
318
|
-
"warning",
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
if (!sync.changed) return;
|
|
324
|
-
notify(ctx, `pi-profile-switch: MCP config updated for profile "${profileName}" — run /reload to apply`, "warning");
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Repoints the adapter at the overlay for a just-activated selection and
|
|
328
|
-
* rebuilds the runtime when the MCP surface actually moved. Must be the
|
|
329
|
-
* caller's LAST use of `ctx`: `reload()` invalidates the old context. */
|
|
330
|
-
async function reloadForMcpOverlay(ctx: ExtensionCommandContext, selection: ResolvedSelection): Promise<void> {
|
|
331
|
-
if (!adapterInstalled) return;
|
|
332
|
-
const configOverride = readFlagFromArgv(argv, "mcp-config");
|
|
333
|
-
const sync = syncMcpOverlayForSelection({
|
|
334
|
-
agentDir: getAgentDir(),
|
|
335
|
-
cwd: ctx.cwd,
|
|
336
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
337
|
-
allowed: selection.mcp === undefined ? "all" : selection.mcp,
|
|
338
|
-
...(configOverride === undefined ? {} : { overridePath: configOverride }),
|
|
339
|
-
});
|
|
340
|
-
if (sync.error !== undefined) {
|
|
341
|
-
notify(ctx, `pi-profile-switch: MCP overlay not updated — ${sync.error}`, "warning");
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
if (!sync.managed) {
|
|
345
|
-
if (selection.mcp !== undefined) {
|
|
346
|
-
notify(
|
|
347
|
-
ctx,
|
|
348
|
-
`pi-profile-switch: --mcp-config points at another file — profile "${selection.name}" cannot filter MCP servers`,
|
|
349
|
-
"warning",
|
|
350
|
-
);
|
|
351
|
-
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
if (!sync.changed) return;
|
|
355
|
-
notify(ctx, `profile "${selection.name}": MCP servers updated — reloading runtime`, "info");
|
|
356
|
-
await ctx.waitForIdle();
|
|
357
|
-
await ctx.reload();
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/** Bare `/profile`: the interactive picker, with a list fallback for
|
|
361
|
-
* modes without dialogs. */
|
|
362
|
-
async function runPicker(ctx: ExtensionCommandContext, entries: ProfileListEntry[]): Promise<void> {
|
|
363
|
-
if (!ctx.hasUI) {
|
|
364
|
-
sendListMessage(entries);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
const choice = await ctx.ui.select(
|
|
368
|
-
"select a profile",
|
|
369
|
-
entries.map((entry) => {
|
|
370
|
-
const label = entry.label ?? entry.description;
|
|
371
|
-
return `${entry.name} [${entry.source}]${label !== undefined ? ` — ${label}` : ""}`;
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
if (choice === undefined) return; // cancelled
|
|
375
|
-
const chosen = entries.find((entry) => choice.startsWith(`${entry.name} [`));
|
|
376
|
-
if (chosen === undefined || chosen.name === current?.selection.name) return;
|
|
377
|
-
const result = await activate(ctx, chosen.name, { force: true, overlay: null, persist: true });
|
|
378
|
-
notify(ctx, `profile active: ${result.selection.name}`, "info");
|
|
379
|
-
await reloadForMcpOverlay(ctx, result.selection);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/** `/profile create|duplicate|edit|delete`: TUI-only catalog CRUD. */
|
|
383
|
-
async function runCatalogCrud(
|
|
384
|
-
ctx: ExtensionCommandContext,
|
|
385
|
-
subcommand: string,
|
|
386
|
-
rest: string[],
|
|
387
|
-
scopeInput: CatalogInput,
|
|
388
|
-
): Promise<void> {
|
|
389
|
-
if (subcommand === "create") {
|
|
390
|
-
const wizard = await runProfileCreateWizard(ctx.ui, { projectTrusted: scopeInput.projectTrusted });
|
|
391
|
-
if (wizard === undefined) return;
|
|
392
|
-
await createProfile(scopeInput, wizard.scope, wizard.name, wizard.definition);
|
|
393
|
-
const origin = wizard.preset !== undefined ? ` from preset "${wizard.preset}"` : "";
|
|
394
|
-
notify(
|
|
395
|
-
ctx,
|
|
396
|
-
`created profile "${wizard.name}" (${wizard.scope})${origin} — activate with /profile use ${wizard.name}`,
|
|
397
|
-
"info",
|
|
398
|
-
);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
if (subcommand === "duplicate") {
|
|
402
|
-
const entries = await profileEntries(ctx);
|
|
403
|
-
const byScope = {
|
|
404
|
-
global: await readCatalogScope(scopeInput, "global"),
|
|
405
|
-
project: await readCatalogScope(scopeInput, "project"),
|
|
406
|
-
};
|
|
407
|
-
const candidates: Array<{ name: string; source: "global" | "project"; definition: ProfileDefinition }> = [];
|
|
408
|
-
for (const entry of entries) {
|
|
409
|
-
if (entry.source === "builtin") continue;
|
|
410
|
-
const definition = byScope[entry.source].get(entry.name);
|
|
411
|
-
if (definition !== undefined) {
|
|
412
|
-
candidates.push({ name: entry.name, source: entry.source, definition });
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
const wizard = await runProfileDuplicateWizard(ctx.ui, { candidates });
|
|
416
|
-
if (wizard === undefined) return;
|
|
417
|
-
await duplicateProfile(scopeInput, wizard.scope, wizard.sourceName, wizard.newName);
|
|
418
|
-
notify(ctx, `duplicated "${wizard.sourceName}" → "${wizard.newName}" (${wizard.scope})`, "info");
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
const name = rest[0] as string;
|
|
423
|
-
const entries = await profileEntries(ctx);
|
|
424
|
-
const existing = entries.find((entry) => entry.name === name);
|
|
425
|
-
if (existing === undefined || existing.source === "builtin") {
|
|
426
|
-
notify(ctx, `profile "${name}" not found in a writable catalog`, "error");
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
if (subcommand === "edit") {
|
|
430
|
-
const definition = (await readCatalogScope(scopeInput, existing.source)).get(name);
|
|
431
|
-
if (definition === undefined) {
|
|
432
|
-
notify(ctx, `profile "${name}" not found in the ${existing.source} catalog`, "error");
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
const wizard = await runProfileEditWizard(ctx.ui, {
|
|
436
|
-
existing: { name, source: existing.source, definition },
|
|
437
|
-
});
|
|
438
|
-
if (wizard === undefined) return;
|
|
439
|
-
await editProfile(scopeInput, wizard.scope, wizard.name, wizard.definition);
|
|
440
|
-
if (current !== undefined && name === current.selection.name) {
|
|
441
|
-
const reactivated = await activate(ctx, name, { persist: true });
|
|
442
|
-
notify(ctx, `saved and reactivated profile "${name}"`, "info");
|
|
443
|
-
await reloadForMcpOverlay(ctx, reactivated.selection);
|
|
444
|
-
} else {
|
|
445
|
-
notify(ctx, `saved profile "${name}" (inactive — runtime untouched)`, "info");
|
|
446
|
-
}
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
// delete: choose the scope when both catalogs hold the name, and the
|
|
451
|
-
// replacement when the active profile disappears for good.
|
|
452
|
-
let scope = existing.source as "global" | "project";
|
|
453
|
-
const projectCatalog = await readCatalogScope(scopeInput, "project");
|
|
454
|
-
const globalCatalog = await readCatalogScope(scopeInput, "global");
|
|
455
|
-
if (projectCatalog.has(name) && globalCatalog.has(name)) {
|
|
456
|
-
const chosen = await ctx.ui.select(`delete "${name}" from which catalog?`, ["global", "project"]);
|
|
457
|
-
if (chosen === undefined) return;
|
|
458
|
-
scope = chosen as "global" | "project";
|
|
459
|
-
}
|
|
460
|
-
const survivesElsewhere =
|
|
461
|
-
(scope === "project" && globalCatalog.has(name)) || (scope === "global" && projectCatalog.has(name));
|
|
462
|
-
const isActive = current !== undefined && name === current.selection.name;
|
|
463
|
-
let replacement: string | undefined;
|
|
464
|
-
if (isActive && !survivesElsewhere) {
|
|
465
|
-
const survivors = entries.filter((entry) => entry.name !== name);
|
|
466
|
-
const chosen = await ctx.ui.select(
|
|
467
|
-
`"${name}" is active — switch to which profile?`,
|
|
468
|
-
survivors.map((entry) => `${entry.name} [${entry.source}]`),
|
|
469
|
-
);
|
|
470
|
-
if (chosen === undefined) return;
|
|
471
|
-
replacement = survivors.find((entry) => chosen.startsWith(`${entry.name} [`))?.name;
|
|
472
|
-
if (replacement === undefined) return;
|
|
473
|
-
}
|
|
474
|
-
await deleteProfile(scopeInput, scope, name, {
|
|
475
|
-
...(isActive ? { activeProfile: name } : {}),
|
|
476
|
-
...(replacement !== undefined ? { replacement } : {}),
|
|
477
|
-
});
|
|
478
|
-
if (isActive) {
|
|
479
|
-
const result = await activate(ctx, replacement ?? name, { force: replacement !== undefined, persist: true });
|
|
480
|
-
notify(ctx, `deleted "${name}" (${scope}); profile active: ${result.selection.name}`, "info");
|
|
481
|
-
await reloadForMcpOverlay(ctx, result.selection);
|
|
482
|
-
} else {
|
|
483
|
-
notify(ctx, `deleted profile "${name}" (${scope})`, "info");
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
pi.on("session_start", async (event, ctx) => {
|
|
488
|
-
setCurrent(ctx, undefined);
|
|
489
|
-
filterWarningShown = false;
|
|
490
|
-
if (seedWarning !== undefined) {
|
|
491
|
-
notify(ctx, seedWarning, "warning");
|
|
492
|
-
seedWarning = undefined;
|
|
493
|
-
}
|
|
494
|
-
const agentDir = getAgentDir();
|
|
495
|
-
const projectTrusted = ctx.isProjectTrusted();
|
|
496
|
-
// Every session start after the first (reload, new, resume, fork)
|
|
497
|
-
// continues the selection this process already applied. The `--profile`
|
|
498
|
-
// flag is a startup directive: re-reading it here would silently
|
|
499
|
-
// resurrect the profile the user just left.
|
|
500
|
-
const continuation = event.reason === "startup" ? undefined : appliedProfile();
|
|
501
|
-
const requested = readProfileFlag(pi);
|
|
502
|
-
let startup;
|
|
503
|
-
try {
|
|
504
|
-
startup = await resolveStartupProfile({
|
|
505
|
-
agentDir,
|
|
506
|
-
cwd: ctx.cwd,
|
|
507
|
-
projectTrusted,
|
|
508
|
-
...(requested !== undefined ? { requested } : {}),
|
|
509
|
-
...(continuation !== undefined ? { continuation } : {}),
|
|
510
|
-
});
|
|
511
|
-
} catch (error) {
|
|
512
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
try {
|
|
516
|
-
// Startup activation never persists (a `--profile` selection is for
|
|
517
|
-
// this run only) and never applies a stored overlay.
|
|
518
|
-
const activated = await activate(ctx, startup.name, { persist: false, overlay: null });
|
|
519
|
-
reportWarnings(ctx, startup.warnings);
|
|
520
|
-
if (adapterInstalled) {
|
|
521
|
-
reportMcpOverlayState(ctx, startup.name, activated.selection, {
|
|
522
|
-
agentDir,
|
|
523
|
-
cwd: ctx.cwd,
|
|
524
|
-
projectTrusted,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
} catch (error) {
|
|
528
|
-
recordAppliedProfile(undefined);
|
|
529
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
530
|
-
reportWarnings(ctx, startup.warnings);
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
535
|
-
if (current === undefined) return;
|
|
536
|
-
// Retry tool literals that were not registered at activation time;
|
|
537
|
-
// once all of them resolve, retrying stops so a native manual toggle
|
|
538
|
-
// is never clobbered.
|
|
539
|
-
const retry = retryPendingTools({ selection: current.selection, surface: surface(ctx) });
|
|
540
|
-
if (retry.applied) {
|
|
541
|
-
current = {
|
|
542
|
-
...current,
|
|
543
|
-
selection: {
|
|
544
|
-
...current.selection,
|
|
545
|
-
pendingTools: retry.pendingTools,
|
|
546
|
-
...(retry.active !== undefined ? { tools: retry.active } : {}),
|
|
547
|
-
},
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
// A startup activation cannot see Pi's skill list, so its references are
|
|
551
|
-
// checked here, on the first turn that carries the complete set — by then
|
|
552
|
-
// `resources_discover` has contributed every extension's skills. The
|
|
553
|
-
// corrected warnings replace the (empty) startup ones, so `/profile
|
|
554
|
-
// status` reports the same thing the user was told.
|
|
555
|
-
if (!current.skillsChecked) {
|
|
556
|
-
const refs = current.selection.skills?.refs;
|
|
557
|
-
const corrected =
|
|
558
|
-
refs === undefined || refs === "all"
|
|
559
|
-
? undefined
|
|
560
|
-
: skillWarnings(
|
|
561
|
-
refs,
|
|
562
|
-
(event.systemPromptOptions.skills ?? []).map((skill) => ({
|
|
563
|
-
name: skill.name,
|
|
564
|
-
filePath: skill.filePath,
|
|
565
|
-
})),
|
|
566
|
-
);
|
|
567
|
-
current = {
|
|
568
|
-
...current,
|
|
569
|
-
skillsChecked: true,
|
|
570
|
-
...(corrected === undefined
|
|
571
|
-
? {}
|
|
572
|
-
: { selection: { ...current.selection, warnings: { ...current.selection.warnings, ...corrected } } }),
|
|
573
|
-
};
|
|
574
|
-
if (corrected !== undefined) {
|
|
575
|
-
reportWarnings(ctx, formatSkillWarnings(current.selection.name, corrected));
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
// Pi has no extension-visible theme-change event and footer statuses are
|
|
579
|
-
// stored as finished strings, so re-render once per turn: a `/theme`
|
|
580
|
-
// switch is picked up without waiting for the next profile change.
|
|
581
|
-
// `refreshBadge` dedupes, so an unchanged badge sends nothing.
|
|
582
|
-
refreshBadge(ctx);
|
|
583
|
-
const filtered = applySkillsFilter({
|
|
584
|
-
systemPrompt: event.systemPrompt,
|
|
585
|
-
options: event.systemPromptOptions,
|
|
586
|
-
filter: current.selection.skills,
|
|
587
|
-
});
|
|
588
|
-
if (
|
|
589
|
-
current.selection.skills !== undefined &&
|
|
590
|
-
(filtered.outcome === "section-missing" || filtered.outcome === "no-read-tool") &&
|
|
591
|
-
!filterWarningShown
|
|
592
|
-
) {
|
|
593
|
-
filterWarningShown = true;
|
|
594
|
-
const cause =
|
|
595
|
-
filtered.outcome === "no-read-tool"
|
|
596
|
-
? "neither the read nor the bash tool is active"
|
|
597
|
-
: "the system prompt carries no skills section";
|
|
598
|
-
notify(
|
|
599
|
-
ctx,
|
|
600
|
-
`pi-profile-switch: ${cause} — profile "${current.selection.name}" skills are not narrowed this session`,
|
|
601
|
-
"warning",
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
current = { ...current, skillsOutcome: filtered.outcome };
|
|
605
|
-
let systemPrompt = filtered.systemPrompt;
|
|
606
|
-
if (current.selection.instructions !== undefined) {
|
|
607
|
-
systemPrompt += formatInstructionsBlock(current.selection.name, current.selection.instructions);
|
|
608
|
-
}
|
|
609
|
-
if (systemPrompt === event.systemPrompt) return;
|
|
610
|
-
return { systemPrompt };
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
pi.registerCommand("profile", {
|
|
614
|
-
description: `pi-profile-switch: ${PROFILE_USAGE}`,
|
|
615
|
-
handler: async (args, ctx) => {
|
|
616
|
-
const [subcommandRaw, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
617
|
-
const subcommand = subcommandRaw ?? "";
|
|
618
|
-
if (subcommand !== "" && !(SUBCOMMANDS as readonly string[]).includes(subcommand)) {
|
|
619
|
-
notify(ctx, PROFILE_USAGE, "error");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
const required = REQUIRED_ARGUMENT[subcommand];
|
|
623
|
-
if (required !== undefined && rest[0] === undefined) {
|
|
624
|
-
notify(ctx, `usage: /profile ${subcommand} ${required}`, "error");
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if ((CRUD_SUBCOMMANDS as readonly string[]).includes(subcommand) && ctx.mode !== "tui") {
|
|
628
|
-
notify(ctx, `/profile ${subcommand} requires TUI mode (current mode: ${ctx.mode})`, "error");
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
try {
|
|
632
|
-
switch (subcommand) {
|
|
633
|
-
case "": {
|
|
634
|
-
await runPicker(ctx, await profileEntries(ctx));
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
case "use": {
|
|
638
|
-
const result = await activate(ctx, rest[0] as string, { force: true, overlay: null, persist: true });
|
|
639
|
-
notify(ctx, `profile active: ${result.selection.name}`, "info");
|
|
640
|
-
await reloadForMcpOverlay(ctx, result.selection);
|
|
641
|
-
return;
|
|
642
|
-
}
|
|
643
|
-
case "customize": {
|
|
644
|
-
if (current === undefined) {
|
|
645
|
-
notify(ctx, "no active profile — nothing to customize", "error");
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
const deps = await activationDeps(ctx, false);
|
|
649
|
-
const target = { profile: { name: current.selection.name, source: current.selection.source } };
|
|
650
|
-
const result = await customizeOverlay({ ...deps, ...target }, parseCustomizeArgs(rest.join(" ")));
|
|
651
|
-
setCurrent(ctx, activationOf(result, deps.live.skills !== undefined));
|
|
652
|
-
notify(ctx, `overlay updated: ${result.selection.name}`, "info");
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
case "reset": {
|
|
656
|
-
if (current === undefined) {
|
|
657
|
-
notify(ctx, "no active profile — nothing to reset", "error");
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
const deps = await activationDeps(ctx, false);
|
|
661
|
-
const target = { profile: { name: current.selection.name, source: current.selection.source } };
|
|
662
|
-
const result = await resetOverlay({ ...deps, ...target });
|
|
663
|
-
setCurrent(ctx, activationOf(result, deps.live.skills !== undefined));
|
|
664
|
-
notify(ctx, `overlay cleared: ${result.selection.name}`, "info");
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
case "list": {
|
|
668
|
-
sendListMessage(await profileEntries(ctx));
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
case "status": {
|
|
672
|
-
await sendStatus(pi, ctx, current);
|
|
673
|
-
return;
|
|
674
|
-
}
|
|
675
|
-
default: {
|
|
676
|
-
await runCatalogCrud(ctx, subcommand, rest, {
|
|
677
|
-
realAgentDir: getAgentDir(),
|
|
678
|
-
cwd: ctx.cwd,
|
|
679
|
-
projectTrusted: ctx.isProjectTrusted(),
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
} catch (error) {
|
|
684
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
685
|
-
}
|
|
686
|
-
},
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
pi.registerCommand("mcp", {
|
|
690
|
-
description: "pi-profile-switch: /mcp enable <server> | /mcp disable <server>",
|
|
691
|
-
handler: async (args, ctx) => {
|
|
692
|
-
const [action, server] = args.trim().split(/\s+/).filter(Boolean);
|
|
693
|
-
if (!["enable", "disable"].includes(action ?? "") || server === undefined) {
|
|
694
|
-
notify(ctx, "usage: /mcp enable <server> | /mcp disable <server>", "error");
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (current === undefined) {
|
|
698
|
-
notify(ctx, "no active profile — /mcp enable|disable edits the active profile's catalog entry", "error");
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
try {
|
|
702
|
-
if (!probeAdapterPresence(pi.events)) {
|
|
703
|
-
throw new Error("pi-mcp-adapter is not active in this session — /mcp enable|disable requires it");
|
|
704
|
-
}
|
|
705
|
-
const agentDir = getAgentDir();
|
|
706
|
-
const projectTrusted = ctx.isProjectTrusted();
|
|
707
|
-
const profile = { name: current.selection.name, source: current.selection.source };
|
|
708
|
-
const result = await setMcpServerEnabled(
|
|
709
|
-
{ realAgentDir: agentDir, cwd: ctx.cwd, projectTrusted, profile },
|
|
710
|
-
server,
|
|
711
|
-
action === "enable",
|
|
712
|
-
);
|
|
713
|
-
if (!result.changed) {
|
|
714
|
-
notify(ctx, `MCP server "${server}" is already ${action}d in profile "${profile.name}"`, "info");
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
// Re-activate so the runtime allowlist matches the edited
|
|
718
|
-
// catalog; the stored overlay is preserved.
|
|
719
|
-
const overlay = (await new RuntimeStateStore(stateDirFor(profile.source, { agentDir, cwd: ctx.cwd })).read())
|
|
720
|
-
.overlay;
|
|
721
|
-
const reactivated = await activate(ctx, profile.name, { overlay: overlay ?? null, persist: true });
|
|
722
|
-
notify(
|
|
723
|
-
ctx,
|
|
724
|
-
`${action}d MCP server "${server}" in profile "${profile.name}" (mcps: [${result.mcps.join(", ")}])`,
|
|
725
|
-
"info",
|
|
726
|
-
);
|
|
727
|
-
await reloadForMcpOverlay(ctx, reactivated.selection);
|
|
728
|
-
} catch (error) {
|
|
729
|
-
notify(ctx, error instanceof Error ? error.message : String(error), "error");
|
|
730
|
-
}
|
|
731
|
-
},
|
|
732
|
-
});
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/** `/profile status`: the active selection against the live view. */
|
|
736
|
-
async function sendStatus(
|
|
737
|
-
pi: ExtensionAPI,
|
|
738
|
-
ctx: ExtensionCommandContext,
|
|
739
|
-
current: Activation | undefined,
|
|
740
|
-
): Promise<void> {
|
|
741
|
-
if (current === undefined) {
|
|
742
|
-
pi.sendMessage({
|
|
743
|
-
customType: "pi-profile-switch",
|
|
744
|
-
content: "no active profile (activation failed or nothing was resolved — a plain Pi session)",
|
|
745
|
-
display: true,
|
|
746
|
-
details: { kind: "status", report: undefined },
|
|
747
|
-
});
|
|
748
|
-
return;
|
|
749
|
-
}
|
|
750
|
-
const options = ctx.getSystemPromptOptions();
|
|
751
|
-
const allSkills = (options.skills ?? []).map((skill) => ({ name: skill.name, filePath: skill.filePath }));
|
|
752
|
-
let discovered: string[] = [];
|
|
753
|
-
try {
|
|
754
|
-
discovered = await discoverAdapterServerNames(getAgentDir(), ctx.isProjectTrusted() ? ctx.cwd : undefined);
|
|
755
|
-
} catch (error) {
|
|
756
|
-
ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
|
|
757
|
-
}
|
|
758
|
-
const overlay = (
|
|
759
|
-
await new RuntimeStateStore(
|
|
760
|
-
stateDirFor(current.selection.source, { agentDir: getAgentDir(), cwd: ctx.cwd }),
|
|
761
|
-
).read()
|
|
762
|
-
).overlay;
|
|
763
|
-
const report = {
|
|
764
|
-
...buildStatusReport({
|
|
765
|
-
selection: current.selection,
|
|
766
|
-
allSkills,
|
|
767
|
-
discoveredMcpServers: discovered,
|
|
768
|
-
...(current.skillsOutcome !== undefined ? { filterOutcome: current.skillsOutcome } : {}),
|
|
769
|
-
}),
|
|
770
|
-
...(overlay !== undefined ? { overlay } : {}),
|
|
771
|
-
};
|
|
772
|
-
pi.sendMessage({
|
|
773
|
-
customType: "pi-profile-switch",
|
|
774
|
-
content: formatStatusMarkdown(report),
|
|
775
|
-
display: true,
|
|
776
|
-
details: { kind: "status", report },
|
|
777
|
-
});
|
|
778
|
-
}
|