@evandu/super-pi 0.1.125 → 0.1.127

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.
Files changed (40) hide show
  1. package/dist/.super-pi-check-inputs.json +2 -2
  2. package/dist/client/.super-pi-client-sources.json +8 -8
  3. package/dist/client/assets/{CodeMirrorEditor-CK5tZxIo.js → CodeMirrorEditor-DpbDVB1W.js} +2 -2
  4. package/dist/client/assets/{dist-BDULvJIt.js → dist-3JbohZwT.js} +1 -1
  5. package/dist/client/assets/{dist-DsY1TPps.js → dist-BTxdkV1I.js} +1 -1
  6. package/dist/client/assets/{dist-D9IZ5pHN.js → dist-C30Mvvko.js} +1 -1
  7. package/dist/client/assets/{dist-DsObObmI.js → dist-CBqTq6Rd.js} +1 -1
  8. package/dist/client/assets/{dist-nSf0tcKa.js → dist-CKY9oRoB.js} +1 -1
  9. package/dist/client/assets/{dist-DR8q7BI-.js → dist-CZ_MzICk.js} +1 -1
  10. package/dist/client/assets/{dist-D90mwiSm.js → dist-CbkhMDE7.js} +1 -1
  11. package/dist/client/assets/{dist-5O-klFrP.js → dist-Cv9yhljh.js} +1 -1
  12. package/dist/client/assets/{dist-CrnIVt5K.js → dist-DL2rVK6z.js} +1 -1
  13. package/dist/client/assets/{dist-B8X48E9G.js → dist-DhVx85Hi.js} +1 -1
  14. package/dist/client/assets/{dist-Did2mkbm.js → dist-DkWOTrCe.js} +1 -1
  15. package/dist/client/assets/{dist-Om17wCc8.js → dist-Dx5uAXkp.js} +1 -1
  16. package/dist/client/assets/{dist-Ck1wVFJ1.js → dist-H1xOj1sR.js} +1 -1
  17. package/dist/client/assets/{dist-BR5dccfj.js → dist-NOyudbxD.js} +1 -1
  18. package/dist/client/assets/{dist-D87R6Wol.js → dist-NukDmwGc.js} +1 -1
  19. package/dist/client/assets/{dist-BC3_yB3K.js → dist-Uvt7BHub.js} +1 -1
  20. package/dist/client/assets/{dist-BbK-QOz1.js → dist-XSLGDk_-.js} +1 -1
  21. package/dist/client/assets/{dist-fEUHAgwX.js → dist-ik9WKwIy.js} +1 -1
  22. package/dist/client/assets/{dist-r_bnlg1k.js → dist-j9nf5NbD.js} +1 -1
  23. package/dist/client/assets/{dist-DIa1uLNH.js → dist-kPkvm0f9.js} +1 -1
  24. package/dist/client/assets/{dist-BDAJ6COK.js → dist-qTKaDyIl.js} +1 -1
  25. package/dist/client/assets/{dist-MYmiV3U3.js → dist-zedNmYPO.js} +1 -1
  26. package/dist/client/assets/{index-CBPWeTvl.css → index-BAaymrG2.css} +1 -1
  27. package/dist/client/assets/{index-qTPZ6eno.js → index-DvxaTOG8.js} +12 -11
  28. package/dist/client/index.html +2 -2
  29. package/dist/server/application/catalog-service.d.ts +4 -1
  30. package/dist/server/application/catalog-service.js +21 -0
  31. package/dist/server/application/provider-service.d.ts +14 -0
  32. package/dist/server/application/provider-service.js +16 -0
  33. package/dist/server/command-handler.js +12 -0
  34. package/dist/server/pi-session.d.ts +31 -0
  35. package/dist/server/pi-session.js +21 -10
  36. package/dist/server/providers.d.ts +27 -0
  37. package/dist/server/providers.js +83 -1
  38. package/dist/server/runtime.js +5 -0
  39. package/dist/server/types.d.ts +43 -0
  40. package/package.json +5 -3
@@ -8,8 +8,8 @@
8
8
  <link rel="alternate icon" href="/favicon.ico" />
9
9
  <link rel="apple-touch-icon" href="/favicon.svg" />
10
10
  <title>Super Pi</title>
11
- <script type="module" crossorigin src="/assets/index-qTPZ6eno.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-CBPWeTvl.css">
11
+ <script type="module" crossorigin src="/assets/index-DvxaTOG8.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-BAaymrG2.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -1,5 +1,5 @@
1
1
  import type { AgentRuntime } from "../runtime.js";
2
- import type { AgentCatalog, ModelCatalog, ModelSelection } from "../types.js";
2
+ import type { AgentCatalog, ModelCatalog, ModelSelection, PromptCatalog } from "../types.js";
3
3
  import type { EventBroadcaster } from "../server-context.js";
4
4
  import type { Store } from "../store.js";
5
5
  /**
@@ -12,6 +12,7 @@ export declare class CatalogService {
12
12
  private readonly rootDir?;
13
13
  private readonly onDirsChanged?;
14
14
  private agentDirs;
15
+ private readonly promptCatalogs;
15
16
  constructor(runtime: AgentRuntime, broadcaster: EventBroadcaster, agentsDir: string | readonly string[], store?: Store | undefined, rootDir?: string | undefined, onDirsChanged?: ((dirs: string[]) => void) | undefined);
16
17
  /**
17
18
  * Returns current active agent discovery directories.
@@ -67,6 +68,8 @@ export declare class CatalogService {
67
68
  * Broadcasts subagents to connected clients.
68
69
  */
69
70
  broadcastSubagents(): Promise<void>;
71
+ promptCatalog(workspaceId: string): Promise<PromptCatalog>;
72
+ broadcastPromptCatalog(workspaceId: string): Promise<void>;
70
73
  /**
71
74
  * Sets the app-wide default model, or clears it back to "Pi's own default".
72
75
  *
@@ -1,5 +1,6 @@
1
1
  import { assertModelAllowed, conversationWorkspace } from "../model-filter.js";
2
2
  import { readAgents, resolveAgentDirectories } from "../agents.js";
3
+ import { loadPromptTemplateSummaries } from "../pi-session.js";
3
4
  function summarize(agent) {
4
5
  const { prompt, toolFlags, ...summary } = agent;
5
6
  const toolSummary = toolFlags.includes("--no-tools")
@@ -21,6 +22,7 @@ export class CatalogService {
21
22
  rootDir;
22
23
  onDirsChanged;
23
24
  agentDirs;
25
+ promptCatalogs = new Map();
24
26
  constructor(runtime, broadcaster, agentsDir, store, rootDir, onDirsChanged) {
25
27
  this.runtime = runtime;
26
28
  this.broadcaster = broadcaster;
@@ -176,6 +178,25 @@ export class CatalogService {
176
178
  async broadcastSubagents() {
177
179
  this.broadcaster.broadcast({ type: "subagent.catalog", catalog: await this.subagentCatalog() });
178
180
  }
181
+ async promptCatalog(workspaceId) {
182
+ const cached = this.promptCatalogs.get(workspaceId);
183
+ if (cached && cached.expiresAt > Date.now())
184
+ return cached.catalog;
185
+ const workspace = this.store?.snapshot().workspaces.find((item) => item.id === workspaceId);
186
+ if (!workspace)
187
+ return { source: "unavailable", prompts: [], workspaceId, fetchedAt: new Date().toISOString(), error: "workspace not found" };
188
+ try {
189
+ const catalog = { source: "sdk", prompts: await loadPromptTemplateSummaries(workspace.path), workspaceId, fetchedAt: new Date().toISOString() };
190
+ this.promptCatalogs.set(workspaceId, { expiresAt: Date.now() + 1_000, catalog });
191
+ return catalog;
192
+ }
193
+ catch (error) {
194
+ return { source: "unavailable", prompts: [], workspaceId, fetchedAt: new Date().toISOString(), error: error instanceof Error ? error.message : String(error) };
195
+ }
196
+ }
197
+ async broadcastPromptCatalog(workspaceId) {
198
+ this.broadcaster.broadcast({ type: "prompt.catalog", catalog: await this.promptCatalog(workspaceId) });
199
+ }
179
200
  /**
180
201
  * Sets the app-wide default model, or clears it back to "Pi's own default".
181
202
  *
@@ -56,6 +56,20 @@ export declare class ProviderService {
56
56
  */
57
57
  disable(providerId: string): Promise<PublicProvider[]>;
58
58
  enable(providerId: string): Promise<PublicProvider[]>;
59
+ /**
60
+ * Repairs Pi's model configuration: the empty fields Pi's schema refuses are dropped, and nothing
61
+ * else in the file is touched.
62
+ *
63
+ * WHY IT IS WORTH A BUTTON. Pi validates the WHOLE `models.json`, so one entry with an empty
64
+ * `baseUrl` takes every custom provider out of every picker - and nothing on screen says so,
65
+ * because the settings panel reads the file leniently and lists them as if they worked. The repair
66
+ * restores the file Pi will accept, which is the difference between "my provider is gone" and
67
+ * "your file had an empty field".
68
+ */
69
+ repair(): Promise<{
70
+ fixed: string[];
71
+ providers: PublicProvider[];
72
+ }>;
59
73
  /**
60
74
  * Adds a provider, or adds a model to one, or updates one.
61
75
  *
@@ -135,6 +135,22 @@ export class ProviderService {
135
135
  await this.afterChange();
136
136
  return this.list();
137
137
  }
138
+ /**
139
+ * Repairs Pi's model configuration: the empty fields Pi's schema refuses are dropped, and nothing
140
+ * else in the file is touched.
141
+ *
142
+ * WHY IT IS WORTH A BUTTON. Pi validates the WHOLE `models.json`, so one entry with an empty
143
+ * `baseUrl` takes every custom provider out of every picker - and nothing on screen says so,
144
+ * because the settings panel reads the file leniently and lists them as if they worked. The repair
145
+ * restores the file Pi will accept, which is the difference between "my provider is gone" and
146
+ * "your file had an empty field".
147
+ */
148
+ async repair() {
149
+ const result = await this.registry.repairModelsConfig();
150
+ if (result.changed)
151
+ await this.afterChange();
152
+ return { fixed: result.fixed, providers: await this.list() };
153
+ }
138
154
  /**
139
155
  * Adds a provider, or adds a model to one, or updates one.
140
156
  *
@@ -122,6 +122,12 @@ export class CommandHandler {
122
122
  await this.providerService.enable(command.providerId);
123
123
  return;
124
124
  }
125
+ if (command.type === "providers.repair") {
126
+ if (!this.providerService)
127
+ throw new Error("供应商功能不可用");
128
+ await this.providerService.repair();
129
+ return;
130
+ }
125
131
  if (command.type === "providers.fetch") {
126
132
  if (!this.providerService)
127
133
  throw new Error("供应商功能不可用");
@@ -326,6 +332,12 @@ export class CommandHandler {
326
332
  sendToSocket({ type: "model.catalog.all", catalog: await this.catalogService.getAllCatalog() });
327
333
  return;
328
334
  }
335
+ if (command.type === "prompt.catalog.fetch") {
336
+ const catalog = await this.catalogService.promptCatalog(command.workspaceId);
337
+ if (sendToSocket)
338
+ sendToSocket({ type: "prompt.catalog", catalog });
339
+ return;
340
+ }
329
341
  if (command.type === "agent.catalog.fetch") {
330
342
  await this.catalogService.broadcastAgents();
331
343
  return;
@@ -77,6 +77,15 @@ export interface SdkCatalog {
77
77
  provider: string;
78
78
  modelId: string;
79
79
  }): Promise<string[] | undefined>;
80
+ /**
81
+ * WHAT PI SAYS IS WRONG WITH ITS OWN MODEL CONFIGURATION, verbatim.
82
+ *
83
+ * Pi validates the WHOLE `models.json` against a schema, and one entry it refuses takes every
84
+ * custom provider out of every picker - while a lenient reader of the same file still lists them
85
+ * all. Pi's own words are the only ones that name the offending field, so they are what the
86
+ * settings panel shows instead of a guess.
87
+ */
88
+ configError(): string | undefined;
80
89
  }
81
90
  /**
82
91
  * Builds a zero-process catalog reader.
@@ -361,6 +370,21 @@ export interface SessionCreation {
361
370
  */
362
371
  export type PiSessionFactory = (creation: SessionCreation) => Promise<PiSessionLike>;
363
372
  export declare function createPiSession(creation: SessionCreation): Promise<PiSessionLike>;
373
+ /**
374
+ * Pi's own global configuration directory, when it can be worked out without importing the SDK.
375
+ *
376
+ * The SDK's `getAgentDir()` is the authority, but asking it would mean importing the SDK - which this
377
+ * file goes out of its way not to do for a path it can derive. The derivation is Pi's own default:
378
+ * `~/.pi/agent`, with the home directory taken from the environment rather than from `os.homedir()`,
379
+ * because on Windows the two disagree often enough to matter.
380
+ *
381
+ * `PI_CODING_AGENT_DIR` WINS, exactly as it wins in Pi (`core/config.ts:getAgentDir`). A person who
382
+ * set it expects Pi to read its whole configuration from there, and this function used to disagree
383
+ * with Pi about it: Pi's model runtime read `models.json` from that directory while the session's
384
+ * resource loader read the skills from `~/.pi/agent`. Two halves of one Pi pointed at two
385
+ * directories, and only one of them was the one the person had configured.
386
+ */
387
+ export declare function defaultAgentDir(): string;
364
388
  /**
365
389
  * The Super Pi extension files, as absolute paths.
366
390
  *
@@ -369,4 +393,11 @@ export declare function createPiSession(creation: SessionCreation): Promise<PiSe
369
393
  * rather than against the server's directory, so a relative path would find a different file for every
370
394
  * workspace. The SDK takes an absolute path, and this keeps the two paths of the same file.
371
395
  */
396
+ export interface PromptTemplateSummary {
397
+ name: string;
398
+ description: string;
399
+ argumentHint?: string;
400
+ scope: "user" | "project";
401
+ }
402
+ export declare function loadPromptTemplateSummaries(cwd: string): Promise<PromptTemplateSummary[]>;
372
403
  export declare function superPiExtensionPaths(repositoryRoot: string): string[];
@@ -24,7 +24,8 @@
24
24
  * would notice the difference.
25
25
  */
26
26
  import { existsSync, statSync } from "node:fs";
27
- import { join } from "node:path";
27
+ import { dirname, join } from "node:path";
28
+ import { fileURLToPath, pathToFileURL } from "node:url";
28
29
  const traceSession = (conversationId, phase, startedAt, extra = "") => {
29
30
  if (process.env.SUPER_PI_TRACE_LATENCY !== "1")
30
31
  return;
@@ -89,6 +90,7 @@ export async function createSdkCatalog(cwd) {
89
90
  const levelsByModel = new Map();
90
91
  return {
91
92
  models,
93
+ configError: () => modelRuntime.getError?.(),
92
94
  ...(settingsManager.getDefaultProvider?.() && settingsManager.getDefaultModel?.()
93
95
  ? {
94
96
  defaultModel: {
@@ -352,21 +354,30 @@ async function resolveSessionManager(sdk, creation) {
352
354
  * resource loader read the skills from `~/.pi/agent`. Two halves of one Pi pointed at two
353
355
  * directories, and only one of them was the one the person had configured.
354
356
  */
355
- function defaultAgentDir() {
357
+ export function defaultAgentDir() {
356
358
  const configured = process.env.PI_CODING_AGENT_DIR?.trim();
357
359
  if (configured)
358
360
  return configured;
359
361
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
360
362
  return home ? join(home, ".pi", "agent") : join(".pi", "agent");
361
363
  }
362
- /**
363
- * The Super Pi extension files, as absolute paths.
364
- *
365
- * Resolved from the repository root, exactly as `runtime.ts` resolves the `--extension` argument it
366
- * passes to a child process: Pi resolves a relative `--extension` against the CONVERSATION's workspace
367
- * rather than against the server's directory, so a relative path would find a different file for every
368
- * workspace. The SDK takes an absolute path, and this keeps the two paths of the same file.
369
- */
364
+ let promptTemplateLoader;
365
+ /** Reads Pi's own prompt-template loader rather than duplicating its discovery rules. */
366
+ async function loadPromptTemplateLoader() {
367
+ if (!promptTemplateLoader) {
368
+ const packageRoot = dirname(fileURLToPath(await import.meta.resolve("@earendil-works/pi-coding-agent")));
369
+ promptTemplateLoader = import(pathToFileURL(join(packageRoot, "core", "prompt-templates.js")).href)
370
+ .then((module) => module.loadPromptTemplates);
371
+ }
372
+ return promptTemplateLoader;
373
+ }
374
+ export async function loadPromptTemplateSummaries(cwd) {
375
+ const loadPromptTemplates = await loadPromptTemplateLoader();
376
+ const templates = loadPromptTemplates({ cwd, agentDir: defaultAgentDir(), promptPaths: [], includeDefaults: true });
377
+ return templates
378
+ .filter((template) => template.sourceInfo.scope === "user" || template.sourceInfo.scope === "project")
379
+ .map((template) => ({ name: template.name, description: template.description, ...(template.argumentHint ? { argumentHint: template.argumentHint } : {}), scope: template.sourceInfo.scope }));
380
+ }
370
381
  export function superPiExtensionPaths(repositoryRoot) {
371
382
  const paths = [
372
383
  join(repositoryRoot, "agents", "super-pi-task", "index.ts"),
@@ -64,6 +64,16 @@ export type ProviderDefinition = {
64
64
  apiKey?: string;
65
65
  hasAuthKey?: boolean;
66
66
  models: ProviderModelDefinition[];
67
+ /**
68
+ * The fields in this provider's `models.json` entry that Pi's own schema refuses - today, an
69
+ * empty string where Pi asks for at least one character.
70
+ *
71
+ * WHY THE PANEL CARES. Pi validates the WHOLE file, so one bad entry takes every custom
72
+ * provider out of every picker, while this panel - which reads the file leniently - still lists
73
+ * them all. Naming the field is what turns "my provider is gone" into "your file has an empty
74
+ * baseUrl".
75
+ */
76
+ invalidFields?: string[];
67
77
  /**
68
78
  * The key is written into `models.json` itself rather than into `auth.json`.
69
79
  *
@@ -227,6 +237,23 @@ export declare class ProviderRegistry {
227
237
  removeCredential(providerId: string): Promise<void>;
228
238
  /** Forgets a disabled provider without restoring anything: the permanent form of "off". */
229
239
  forgetDisabled(providerId: string): Promise<void>;
240
+ /**
241
+ * Removes the empty fields Pi's schema refuses, so the file is accepted again.
242
+ *
243
+ * WHY THIS IS A REPAIR AND NOT A DELETE. An empty `baseUrl` is not a provider with no address -
244
+ * it is a value Pi refuses, and Pi refuses the WHOLE FILE when it sees one. The fix is to drop
245
+ * the empty field (leaving the provider exactly as it was otherwise), NOT to remove the provider:
246
+ * the person wrote it, and an override-only entry - one that only adds models to a built-in - has
247
+ * no address at all, which is legal as long as the field is ABSENT rather than empty.
248
+ *
249
+ * NOTHING ELSE IN THE FILE IS TOUCHED. The entries are read as raw JSON and written back with
250
+ * only the offending fields removed, so a comment-free copy of a hand-written file stays a
251
+ * hand-written file.
252
+ */
253
+ repairModelsConfig(): Promise<{
254
+ fixed: string[];
255
+ changed: boolean;
256
+ }>;
230
257
  /** Writes the registry. The key lives here, so the file is 0600. */
231
258
  save(providers: ProviderDefinition[]): Promise<void>;
232
259
  /**
@@ -167,6 +167,21 @@ async function writeJsonAtomic(path, value) {
167
167
  throw error;
168
168
  }
169
169
  }
170
+ /**
171
+ * The fields of a `models.json` provider entry that Pi's schema refuses.
172
+ *
173
+ * PI'S RULE, NOT OURS: `baseUrl`, `name` and `api` are optional, but when they are present they
174
+ * need at least one character. An empty string is not "absent" - it is a value Pi rejects, and Pi
175
+ * rejects the WHOLE FILE when it finds one, which is why one bad entry can empty every picker.
176
+ */
177
+ function invalidModelFields(entry) {
178
+ const fields = [];
179
+ for (const field of ["name", "baseUrl", "api"]) {
180
+ if (entry[field] === "")
181
+ fields.push(field);
182
+ }
183
+ return fields;
184
+ }
170
185
  /**
171
186
  * The provider entry Super Pi writes into Pi's `models.json`, MERGED into what was already there.
172
187
  *
@@ -200,7 +215,10 @@ function toPiProvider(provider, existing) {
200
215
  return {
201
216
  ...priorModel,
202
217
  id: model.id,
203
- name: model.name ?? model.id,
218
+ // NEVER AN EMPTY STRING. Pi's schema asks for at least one character, and one empty name
219
+ // invalidates the WHOLE models.json - which takes every custom provider out of every picker
220
+ // with nothing but "must not have fewer than 1 characters" to explain it.
221
+ name: model.name || model.id,
204
222
  reasoning: model.reasoning ?? false,
205
223
  input: model.imageInput ? ["text", "image"] : ["text"],
206
224
  contextWindow: model.contextWindow ?? DEFAULT_CONTEXT_WINDOW,
@@ -325,6 +343,12 @@ export class ProviderRegistry {
325
343
  // credential - and the panel then says "Pi will not offer it", which is wrong exactly when
326
344
  // the environment variable is set and Pi is offering it right now.
327
345
  ...(typeof rec.apiKey === "string" && rec.apiKey ? { keyInModelsJson: true } : {}),
346
+ // THE FIELDS PI WILL REFUSE. Pi validates the WHOLE models.json against a schema where an
347
+ // empty string is not a value ("must not have fewer than 1 characters"), and one bad entry
348
+ // invalidates the file - which takes EVERY custom provider out of every picker while this
349
+ // panel, which reads the file leniently, still lists them all. Naming the offending fields
350
+ // is what turns "my provider vanished" into "your file has an empty baseUrl".
351
+ ...(invalidModelFields(rec).length ? { invalidFields: invalidModelFields(rec) } : {}),
328
352
  models,
329
353
  });
330
354
  }
@@ -485,6 +509,63 @@ export class ProviderRegistry {
485
509
  throw new Error(`供应商「${providerId}」没有被停用`);
486
510
  await this.writeRegistry({ disabled: next });
487
511
  }
512
+ /**
513
+ * Removes the empty fields Pi's schema refuses, so the file is accepted again.
514
+ *
515
+ * WHY THIS IS A REPAIR AND NOT A DELETE. An empty `baseUrl` is not a provider with no address -
516
+ * it is a value Pi refuses, and Pi refuses the WHOLE FILE when it sees one. The fix is to drop
517
+ * the empty field (leaving the provider exactly as it was otherwise), NOT to remove the provider:
518
+ * the person wrote it, and an override-only entry - one that only adds models to a built-in - has
519
+ * no address at all, which is legal as long as the field is ABSENT rather than empty.
520
+ *
521
+ * NOTHING ELSE IN THE FILE IS TOUCHED. The entries are read as raw JSON and written back with
522
+ * only the offending fields removed, so a comment-free copy of a hand-written file stays a
523
+ * hand-written file.
524
+ */
525
+ async repairModelsConfig() {
526
+ const modelsPath = piModelsPath(this.piDir);
527
+ const raw = (await readJsonFile(modelsPath));
528
+ if (!raw || !raw.providers || typeof raw.providers !== "object")
529
+ return { fixed: [], changed: false };
530
+ const fixed = [];
531
+ let changed = false;
532
+ const providers = { ...raw.providers };
533
+ for (const [providerId, value] of Object.entries(providers)) {
534
+ if (!value || typeof value !== "object" || Array.isArray(value))
535
+ continue;
536
+ const entry = { ...value };
537
+ let touched = false;
538
+ for (const field of invalidModelFields(entry)) {
539
+ delete entry[field];
540
+ fixed.push(`${providerId}.${field}`);
541
+ touched = true;
542
+ }
543
+ if (Array.isArray(entry.models)) {
544
+ let modelsTouched = false;
545
+ const models = entry.models.map((model) => {
546
+ if (!model || typeof model !== "object" || Array.isArray(model))
547
+ return model;
548
+ const record = { ...model };
549
+ if (record.name === "") {
550
+ delete record.name;
551
+ fixed.push(`${providerId}.models.name`);
552
+ modelsTouched = true;
553
+ }
554
+ return record;
555
+ });
556
+ if (modelsTouched)
557
+ entry.models = models;
558
+ touched = touched || modelsTouched;
559
+ }
560
+ if (touched) {
561
+ providers[providerId] = entry;
562
+ changed = true;
563
+ }
564
+ }
565
+ if (changed)
566
+ await writeJsonAtomic(modelsPath, { ...raw, providers });
567
+ return { fixed, changed };
568
+ }
488
569
  /** Writes the registry. The key lives here, so the file is 0600. */
489
570
  async save(providers) {
490
571
  await this.writeRegistry({ providers });
@@ -541,6 +622,7 @@ export function publicProviders(providers) {
541
622
  api: provider.api,
542
623
  hasKey: Boolean(provider.apiKey || provider.hasAuthKey),
543
624
  ...(provider.keyInModelsJson ? { keyInModelsJson: true } : {}),
625
+ ...(provider.invalidFields?.length ? { invalidFields: [...provider.invalidFields] } : {}),
544
626
  ...(provider.builtin ? { builtin: true } : {}),
545
627
  ...(provider.disabled ? { disabled: true } : {}),
546
628
  models: provider.models.map(model => ({
@@ -2445,6 +2445,11 @@ export class PiRpcRuntime {
2445
2445
  source: "pi",
2446
2446
  models,
2447
2447
  levels: levelNames,
2448
+ // PI'S OWN VERDICT ON ITS OWN CONFIGURATION. It belongs on the catalog because the catalog is
2449
+ // what the picker shows, and a picker that is missing a provider without saying why is the
2450
+ // hardest kind of missing: Pi rejects a whole models.json over one bad field, so EVERY
2451
+ // custom provider disappears at once and the only explanation is somewhere else entirely.
2452
+ ...(reader.configError?.() ? { configError: reader.configError() } : {}),
2448
2453
  defaultModel: pick(reader.defaultModel ? describeModel({ provider: reader.defaultModel.provider, id: reader.defaultModel.modelId, name: reader.defaultModel.modelId }) : undefined),
2449
2454
  defaultThinkingLevel: reader.defaultThinkingLevel ?? levelNames[0],
2450
2455
  fetchedAt: new Date().toISOString(),
@@ -410,6 +410,12 @@ export type PublicProvider = {
410
410
  * in `auth.json`. It is a credential all the same, so the panel must not report it as missing.
411
411
  */
412
412
  keyInModelsJson?: boolean;
413
+ /**
414
+ * The fields of this provider's `models.json` entry that Pi's schema refuses (an empty string
415
+ * where Pi asks for at least one character). Pi rejects the WHOLE file over one of them, so every
416
+ * custom provider leaves the picker at once.
417
+ */
418
+ invalidFields?: string[];
413
419
  /**
414
420
  * TRUE when the provider is one of Pi's own, with no entry in `models.json`.
415
421
  *
@@ -441,6 +447,13 @@ export type ModelCatalog = {
441
447
  defaultThinkingLevel?: string;
442
448
  fetchedAt: string;
443
449
  error?: string;
450
+ /**
451
+ * What Pi says is wrong with its own model configuration, in Pi's own words.
452
+ *
453
+ * Pi validates the whole `models.json`, so one entry it refuses empties every picker of custom
454
+ * providers. This is the only place that says which field is at fault.
455
+ */
456
+ configError?: string;
444
457
  };
445
458
  export type Conversation = {
446
459
  id: string;
@@ -535,6 +548,19 @@ export type AgentSummary = Omit<AgentDefinition, "prompt" | "toolFlags" | "tools
535
548
  toolSummary: string;
536
549
  sourceDir?: string;
537
550
  };
551
+ export type PromptTemplateSummary = {
552
+ name: string;
553
+ description: string;
554
+ argumentHint?: string;
555
+ scope: "user" | "project";
556
+ };
557
+ export type PromptCatalog = {
558
+ source: "sdk" | "unavailable";
559
+ prompts: PromptTemplateSummary[];
560
+ workspaceId: string;
561
+ fetchedAt: string;
562
+ error?: string;
563
+ };
538
564
  export type AgentCatalog = {
539
565
  source: "directory" | "unavailable";
540
566
  agents: AgentSummary[];
@@ -1000,7 +1026,16 @@ export type ClientCommandBody =
1000
1026
  modelId: string;
1001
1027
  }
1002
1028
  /** Asks for the providers Super Pi manages. The answer carries no key. */
1029
+ /**
1030
+ * Repairs Pi's model configuration: the empty fields Pi's schema refuses are dropped from
1031
+ * `models.json`, and nothing else in the file is touched.
1032
+ *
1033
+ * Pi validates the whole file, so one refused entry takes every custom provider out of every
1034
+ * picker. This is the way back.
1035
+ */
1003
1036
  | {
1037
+ type: "providers.repair";
1038
+ } | {
1004
1039
  type: "providers.fetch";
1005
1040
  }
1006
1041
  /**
@@ -1154,6 +1189,11 @@ export type ClientCommandBody =
1154
1189
  | {
1155
1190
  type: "agent.catalog.fetch";
1156
1191
  }
1192
+ /** Asks for prompt templates loaded by Pi for a workspace. */
1193
+ | {
1194
+ type: "prompt.catalog.fetch";
1195
+ workspaceId: string;
1196
+ }
1157
1197
  /**
1158
1198
  * Sends a message to a conversation, and THAT IS ALL IT CLAIMS.
1159
1199
  *
@@ -1559,6 +1599,9 @@ export type ServerEvent = {
1559
1599
  } | {
1560
1600
  type: "agent.catalog";
1561
1601
  catalog: AgentCatalog;
1602
+ } | {
1603
+ type: "prompt.catalog";
1604
+ catalog: PromptCatalog;
1562
1605
  }
1563
1606
  /**
1564
1607
  * The `mode: subagent` half of the same directory, as its OWN event rather than an extra key on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.125",
3
+ "version": "0.1.127",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -123,7 +123,8 @@
123
123
  "scripts/skills-discovery.test.mjs",
124
124
  "scripts/session-state-icon.test.mjs",
125
125
  "scripts/provider-fetch-models.test.mjs",
126
- "scripts/shared-chrome.test.mjs"
126
+ "scripts/shared-chrome.test.mjs",
127
+ "scripts/prompt-templates.test.mjs"
127
128
  ],
128
129
  "check:browser": [
129
130
  "scripts/interrupt-browser.test.mjs",
@@ -172,7 +173,8 @@
172
173
  "scripts/active-memory-browser.test.mjs",
173
174
  "scripts/knowledge-base-browser.test.mjs",
174
175
  "scripts/terminal-browser.test.mjs",
175
- "scripts/ux-polish-browser.test.mjs"
176
+ "scripts/ux-polish-browser.test.mjs",
177
+ "scripts/prompt-templates-browser.test.mjs"
176
178
  ],
177
179
  "dependencies": {
178
180
  "@codemirror/commands": "^6.11.1",