@evandu/super-pi 0.1.104 → 0.1.106

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 (42) hide show
  1. package/dist/.super-pi-check-inputs.json +2 -2
  2. package/dist/client/.super-pi-client-sources.json +8 -7
  3. package/dist/client/assets/index-DBR_t9lF.js +79 -0
  4. package/dist/client/assets/index-mo5Dvh_v.css +1 -0
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/application/catalog-service.d.ts +9 -0
  7. package/dist/server/application/catalog-service.js +11 -0
  8. package/dist/server/application/provider-service.d.ts +77 -0
  9. package/dist/server/application/provider-service.js +165 -0
  10. package/dist/server/application/run-service.d.ts +19 -0
  11. package/dist/server/application/run-service.js +78 -0
  12. package/dist/server/command-handler.d.ts +16 -1
  13. package/dist/server/command-handler.js +40 -1
  14. package/dist/server/content-search.d.ts +32 -0
  15. package/dist/server/content-search.js +155 -0
  16. package/dist/server/file-changes.d.ts +119 -0
  17. package/dist/server/file-changes.js +278 -0
  18. package/dist/server/file-diff.d.ts +38 -0
  19. package/dist/server/file-diff.js +97 -0
  20. package/dist/server/file-index.d.ts +56 -0
  21. package/dist/server/file-index.js +150 -0
  22. package/dist/server/files.d.ts +8 -0
  23. package/dist/server/files.js +3 -1
  24. package/dist/server/git.d.ts +17 -0
  25. package/dist/server/git.js +39 -0
  26. package/dist/server/http-server.js +121 -1
  27. package/dist/server/index.js +28 -1
  28. package/dist/server/pi-session-events.d.ts +9 -0
  29. package/dist/server/pi-session-events.js +12 -0
  30. package/dist/server/pi-session-pool.d.ts +19 -1
  31. package/dist/server/pi-session-pool.js +13 -1
  32. package/dist/server/pi-session.d.ts +10 -0
  33. package/dist/server/pi-session.js +11 -0
  34. package/dist/server/providers.d.ts +160 -0
  35. package/dist/server/providers.js +396 -0
  36. package/dist/server/runtime.d.ts +67 -0
  37. package/dist/server/runtime.js +54 -2
  38. package/dist/server/server-context.d.ts +15 -0
  39. package/dist/server/types.d.ts +143 -0
  40. package/package.json +14 -1
  41. package/dist/client/assets/index-DDkbPYLz.css +0 -1
  42. package/dist/client/assets/index-mAu39Bra.js +0 -74
@@ -22,6 +22,7 @@ import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_END
22
22
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
23
23
  import { loadConversationImages } from "./upload.js";
24
24
  import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
25
+ import { toolTargetPath } from "./file-changes.js";
25
26
  const traceLatency = (conversationId, phase, startedAt, extra = "") => {
26
27
  if (process.env.SUPER_PI_TRACE_LATENCY !== "1")
27
28
  return;
@@ -342,6 +343,17 @@ export class PiRpcRuntime {
342
343
  * otherwise be re-imported on every picker open.
343
344
  */
344
345
  sdkCatalogPromise;
346
+ /**
347
+ * Bumped whenever the set of models Pi knows about changes.
348
+ *
349
+ * A live session holds its own `ModelRuntime`, built when the session was built, and that runtime
350
+ * does not know about a provider added afterwards. Pi's refusal to switch to a model its runtime
351
+ * does not have is swallowed by the session pool into a warning, so the turn would go on running
352
+ * the OLD model while the picker said the new one - a control that lies. The sessions are made
353
+ * STALE by this counter instead: each one is rebuilt at its next turn, which is a moment when
354
+ * throwing a session away costs nothing, and the new one is built with a runtime that knows.
355
+ */
356
+ modelEpoch = 0;
345
357
  /** Latches the "no extension on disk" warning, so it is said once rather than once per turn. */
346
358
  warnedMissingExtension = false;
347
359
  /** Latches the "Pi's SDK is not installed" warning, for the same reason. */
@@ -429,7 +441,7 @@ export class PiRpcRuntime {
429
441
  // ABOUT that session and stop being true with it.
430
442
  this.sessionHosts.delete(conversationId);
431
443
  this.reportedSessionFiles.delete(conversationId);
432
- }, options.sessionFactory ?? createPiSession, (conversationId) => mintCapability(conversationId));
444
+ }, options.sessionFactory ?? createPiSession, (conversationId) => mintCapability(conversationId), () => this.modelEpoch);
433
445
  }
434
446
  /**
435
447
  * The configuration of the session that was built for a conversation, for tests to inspect.
@@ -719,6 +731,7 @@ export class PiRpcRuntime {
719
731
  const queue = new TurnQueue();
720
732
  const handlers = {
721
733
  onToolCall: input.onToolCall,
734
+ onFileWrite: input.onFileWrite,
722
735
  onToolUpdate: input.onToolUpdate,
723
736
  onToolPreparing: input.onToolPreparing,
724
737
  onToolEnd: input.onToolEnd,
@@ -1052,7 +1065,7 @@ export class PiRpcRuntime {
1052
1065
  * Still used for the memory review, the knowledge reranker, and as the fallback when Pi's SDK is not
1053
1066
  * installed. The main conversation path does NOT come here - see `answer`.
1054
1067
  */
1055
- async *answerOneShot({ conversation, prompt, signal, cwd, host, onPiQueue, onSteerDelivered, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, onThinkingEnd, images, memoryReview, memorySourceConversationId, knowledgeRerank }) {
1068
+ async *answerOneShot({ conversation, prompt, signal, cwd, host, onPiQueue, onSteerDelivered, onUserEntry, onSessionFile, onToolCall, onFileWrite, onToolUpdate, onToolPreparing, onToolEnd, onThinking, onThinkingEnd, images, memoryReview, memorySourceConversationId, knowledgeRerank }) {
1056
1069
  const latencyStartedAt = performance.now();
1057
1070
  traceLatency(conversation.id, "answer.begin", latencyStartedAt);
1058
1071
  await mkdir(this.sessionDir, { recursive: true });
@@ -1379,6 +1392,19 @@ export class PiRpcRuntime {
1379
1392
  catch (error) {
1380
1393
  console.warn(`PiRpcRuntime: the tool call handler threw: ${error instanceof Error ? error.message : "unknown error"}`);
1381
1394
  }
1395
+ // THE FILE-WRITING CALL, announced before it runs. This is the ONLY moment the old content
1396
+ // is still on the disk, so this is where a baseline has to be taken - and it is taken by a
1397
+ // listener that does its own I/O, off the drain loop's path, because a disk read between
1398
+ // yields would stall the stream.
1399
+ const writePath = toolTargetPath(name, record.args);
1400
+ if (writePath) {
1401
+ try {
1402
+ onFileWrite?.(writePath, name, record.toolCallId ?? undefined);
1403
+ }
1404
+ catch (error) {
1405
+ console.warn(`PiRpcRuntime: the file-write handler threw: ${error instanceof Error ? error.message : "unknown error"}`);
1406
+ }
1407
+ }
1382
1408
  continue;
1383
1409
  }
1384
1410
  if (record.type === "tool_execution_end") {
@@ -2242,6 +2268,32 @@ export class PiRpcRuntime {
2242
2268
  }
2243
2269
  return answer;
2244
2270
  }
2271
+ /**
2272
+ * Throws away every cached answer about the models Pi offers.
2273
+ *
2274
+ * Called when the model list itself changes - which today means a person added a provider
2275
+ * through the settings window. The caches are three deep and all three have to go: the TTL map
2276
+ * (ten minutes), the in-flight map (a read that is already running would answer with the old
2277
+ * list), and the SDK catalog, which is remembered for the life of the process because it holds
2278
+ * objects that read the auth store and the model store from disk - and those files are exactly
2279
+ * what just changed.
2280
+ */
2281
+ invalidateCatalog() {
2282
+ this.catalogs.clear();
2283
+ this.pendingCatalogs.clear();
2284
+ this.sdkCatalogPromise = undefined;
2285
+ }
2286
+ /**
2287
+ * Makes every live session stale, so the next turn builds one that knows the current models.
2288
+ *
2289
+ * WHY A COUNTER AND NOT A REBUILD. Rebuilding a session means throwing it away and reading its
2290
+ * transcript back, and a turn may be RUNNING right now. The counter is read by the session pool
2291
+ * as part of the fingerprint, so each session notices at its next acquire - between turns, where
2292
+ * a rebuild is free.
2293
+ */
2294
+ bumpModelEpoch() {
2295
+ this.modelEpoch += 1;
2296
+ }
2245
2297
  /**
2246
2298
  * The model/thinking catalog, from Pi, cached.
2247
2299
  *
@@ -7,6 +7,7 @@ import type { ConversationService } from "./application/conversation-service.js"
7
7
  import type { RunService } from "./application/run-service.js";
8
8
  import type { ExtensionUiService } from "./application/extension-ui-service.js";
9
9
  import type { CatalogService } from "./application/catalog-service.js";
10
+ import type { ProviderService } from "./application/provider-service.js";
10
11
  import type { ExecService } from "./exec-service.js";
11
12
  import type { ToolRegistry } from "./tool-registry.js";
12
13
  import type { ToolRunner } from "./tool-runner.js";
@@ -49,9 +50,23 @@ export interface ServerContext {
49
50
  runService: RunService;
50
51
  extensionUiService: ExtensionUiService;
51
52
  catalogService: CatalogService;
53
+ /** The providers a person added through the settings window. */
54
+ providerService: ProviderService;
52
55
  execService: ExecService;
53
56
  toolRegistry: ToolRegistry;
54
57
  toolRunner: ToolRunner;
58
+ /**
59
+ * WHAT THE AGENT CHANGED, and what each file looked like before.
60
+ *
61
+ * The tracker is here because TWO layers need it: the run service writes to it (a write tool is
62
+ * announced, so the "before" is captured) and the HTTP layer reads from it (a diff request asks for
63
+ * the "before" of one file). Putting it in the context rather than inside either service is what
64
+ * keeps it ONE tracker - two trackers would be two answers to "what did the agent change", and only
65
+ * one of them would have the baselines.
66
+ */
67
+ fileChanges?: import("./file-changes.js").FileChangeTracker;
68
+ /** Shared file indexes: the HTTP layer starts them lazily and write paths refresh known entries. */
69
+ fileIndexes?: Map<string, import("./file-index.js").FileIndex>;
55
70
  /** Absent only if construction failed; a runtime with no Pi reports itself through `available`. */
56
71
  toolLlm?: import("./tool-llm.js").ToolLlmService;
57
72
  /**
@@ -377,6 +377,29 @@ export type ModelOption = ModelSelection & {
377
377
  * honest answer is "the list could not be read". `fetchedAt` travels with the value so a stale
378
378
  * cache is visible rather than silent.
379
379
  */
380
+ /**
381
+ * A provider a person added through the settings window, as the BROWSER sees it.
382
+ *
383
+ * The API key is a FACT here and not a value: `hasKey` says whether one is stored, and the key
384
+ * itself never leaves the server. It is in Super Pi's own registry and in Pi's `auth.json`, both of
385
+ * them files with restrictive permissions, and in neither the state snapshot nor any event.
386
+ */
387
+ export type PublicProviderModel = {
388
+ id: string;
389
+ name: string;
390
+ reasoning: boolean;
391
+ imageInput: boolean;
392
+ contextWindow: number;
393
+ maxTokens: number;
394
+ };
395
+ export type PublicProvider = {
396
+ id: string;
397
+ name: string;
398
+ baseUrl: string;
399
+ api: string;
400
+ hasKey: boolean;
401
+ models: PublicProviderModel[];
402
+ };
380
403
  export type ModelCatalog = {
381
404
  source: "pi" | "unavailable";
382
405
  models: ModelOption[];
@@ -874,6 +897,54 @@ export type ClientCommandBody =
874
897
  type: "model.rules.set";
875
898
  rules: ModelFilterRule[];
876
899
  }
900
+ /**
901
+ * Adds a provider, or adds a model to one, or updates one.
902
+ *
903
+ * The key is OPTIONAL and its absence means "keep the stored one": the key is never shown again
904
+ * once it is saved, so an empty field is the normal state of the form for a provider that already
905
+ * has one. `clearKey` is how a person asks for it to be removed.
906
+ */
907
+ | {
908
+ type: "providers.set";
909
+ provider: {
910
+ id: string;
911
+ name?: string;
912
+ baseUrl: string;
913
+ api: string;
914
+ apiKey?: string;
915
+ clearKey?: boolean;
916
+ models: {
917
+ id: string;
918
+ name?: string;
919
+ reasoning?: boolean;
920
+ imageInput?: boolean;
921
+ contextWindow?: number;
922
+ maxTokens?: number;
923
+ supportsDeveloperRole?: boolean;
924
+ supportsReasoningEffort?: boolean;
925
+ }[];
926
+ };
927
+ }
928
+ /** Removes a provider Super Pi manages. One it does not manage is refused. */
929
+ | {
930
+ type: "providers.remove";
931
+ providerId: string;
932
+ }
933
+ /**
934
+ * Removes ONE model from a provider, leaving the provider and its other models alone.
935
+ *
936
+ * Separate from `providers.remove` because a provider is a GROUP of models sharing one address
937
+ * and one key: taking one model out must not cost the others their credential.
938
+ */
939
+ | {
940
+ type: "providers.removeModel";
941
+ providerId: string;
942
+ modelId: string;
943
+ }
944
+ /** Asks for the providers Super Pi manages. The answer carries no key. */
945
+ | {
946
+ type: "providers.fetch";
947
+ }
877
948
  /**
878
949
  * Sets the app-wide default model. `null` clears it back to "Pi's own default" - the same
879
950
  * "absence, not a frozen copy" convention the workspace defaults use, so removing the default
@@ -1210,9 +1281,69 @@ export type MountedToolSummary = {
1210
1281
  description?: string;
1211
1282
  enabled: boolean;
1212
1283
  };
1284
+ /**
1285
+ * A file the agent changed, as the Files panel learns about it.
1286
+ *
1287
+ * `path` is workspace-relative and `/`-separated, the same spelling every other file path uses on the
1288
+ * wire. `baseline` is the content BEFORE the agent's write, or `null` when there was none - which is
1289
+ * how a NEW file is told from an EDITED one, and the two are different things to a reader.
1290
+ *
1291
+ * `baselineUnknown` says the "before" could not be read, as opposed to not existing. A diff view that
1292
+ * shows an empty "before" for a file that existed is a lie, and this flag is what keeps it from being
1293
+ * one.
1294
+ */
1295
+ export type ChangedFile = {
1296
+ path: string;
1297
+ /** The content before the agent wrote it, or `null` when the file did not exist then. */
1298
+ baseline: string | null;
1299
+ /** True when the baseline is missing because it could not be read, not because the file was new. */
1300
+ baselineUnknown?: boolean;
1301
+ /** The conversation the change belongs to, so a workspace's changes can be attributed to a session. */
1302
+ conversationId?: string;
1303
+ /** When it happened, so the panel can order a turn's changes the way they happened. */
1304
+ at: number;
1305
+ };
1213
1306
  export type ServerEvent = {
1214
1307
  type: "tools.updated";
1215
1308
  tools: MountedToolSummary[];
1309
+ }
1310
+ /**
1311
+ * THE AGENT CHANGED THESE FILES.
1312
+ *
1313
+ * WHY THIS EXISTS. The Files panel could browse the workspace but could not show a CHANGE, and a panel
1314
+ * that cannot show a change is a browser rather than a workbench: the user asked for the opposite - being
1315
+ * able to see the files is what makes the agent's edits reviewable. The server already knew which files
1316
+ * the agent wrote (the tool call names the path) and the knowledge went no further than one line on a
1317
+ * card, so a file the agent had rewritten looked exactly like a file it had never touched.
1318
+ *
1319
+ * A SNAPSHOT rather than a delta, like every other state-bearing event here: the client replaces its
1320
+ * list with this one, so a browser that missed an event rebuilds the list from the next one rather than
1321
+ * accumulating a partial truth.
1322
+ *
1323
+ * `dropped` says how many baselines were evicted because the per-workspace cap was reached. It is
1324
+ * reported rather than hidden, because a list that is shorter than the agent's actual work - with no
1325
+ * explanation - reads as "the agent only changed these" and is wrong.
1326
+ */
1327
+ | {
1328
+ type: "files.changed";
1329
+ workspaceId: string;
1330
+ changes: ChangedFile[];
1331
+ dropped: number;
1332
+ } | {
1333
+ type: "files.index";
1334
+ workspaceId: string;
1335
+ version: number;
1336
+ changed: {
1337
+ path: string;
1338
+ kind: "file" | "directory";
1339
+ size: number | null;
1340
+ mtimeMs: number | null;
1341
+ etag: string | null;
1342
+ text: boolean | null;
1343
+ }[];
1344
+ removed: string[];
1345
+ complete: boolean;
1346
+ skipped: string[];
1216
1347
  } | {
1217
1348
  type: "operation.result";
1218
1349
  requestId: string;
@@ -1330,6 +1461,18 @@ export type ServerEvent = {
1330
1461
  type: "model.catalog.all";
1331
1462
  catalog: ModelCatalog;
1332
1463
  }
1464
+ /**
1465
+ * The providers Super Pi manages. Sent when they change, and when a client connects.
1466
+ *
1467
+ * `apis` travels with them so the form's API-type list has ONE source of truth: the list of
1468
+ * adapters Pi ships is a fact about the Pi that is installed, and a copy of it in the browser
1469
+ * would be a copy that goes stale.
1470
+ */
1471
+ | {
1472
+ type: "providers.list";
1473
+ providers: PublicProvider[];
1474
+ apis: string[];
1475
+ }
1333
1476
  /**
1334
1477
  * The answer to conversation.autoRename. Carries a title OR an error and never both, because the client
1335
1478
  * has exactly two things to say about it: here is the new name, or here is why there isnt one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.104",
3
+ "version": "0.1.106",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -87,6 +87,10 @@
87
87
  "scripts/tool-calls.test.mjs",
88
88
  "scripts/files-api.test.mjs",
89
89
  "scripts/files-missing-path.test.mjs",
90
+ "scripts/file-changes.test.mjs",
91
+ "scripts/file-index.test.mjs",
92
+ "scripts/file-content-search.test.mjs",
93
+ "scripts/file-diff-client.test.mjs",
90
94
  "scripts/task-tool.test.mjs",
91
95
  "scripts/fork-server.test.mjs",
92
96
  "scripts/styles-no-dangling.test.mjs",
@@ -94,6 +98,8 @@
94
98
  "scripts/agent-config-regression.test.mjs",
95
99
  "scripts/model-filter.test.mjs",
96
100
  "scripts/settings-default-model.test.mjs",
101
+ "scripts/provider-registry.test.mjs",
102
+ "scripts/provider-pi-reads.test.mjs",
97
103
  "scripts/active-memory.test.mjs",
98
104
  "scripts/knowledge-base.test.mjs",
99
105
  "scripts/tool-runner.test.mjs",
@@ -128,6 +134,9 @@
128
134
  "scripts/session-context-menu-browser.test.mjs",
129
135
  "scripts/tool-call-blocks-browser.test.mjs",
130
136
  "scripts/files-tab-browser.test.mjs",
137
+ "scripts/file-quickopen-browser.test.mjs",
138
+ "scripts/file-content-search-browser.test.mjs",
139
+ "scripts/file-changes-browser.test.mjs",
131
140
  "scripts/msg-tabs-browser.test.mjs",
132
141
  "scripts/tabs-on-top-browser.test.mjs",
133
142
  "scripts/subagent-source.test.mjs",
@@ -140,6 +149,7 @@
140
149
  "scripts/theme-switch-state.test.mjs",
141
150
  "scripts/settings-window-structure.test.mjs",
142
151
  "scripts/settings-model-browser.test.mjs",
152
+ "scripts/provider-browser.test.mjs",
143
153
  "scripts/file-tab-selected-contrast.test.mjs",
144
154
  "scripts/help-window.test.mjs",
145
155
  "scripts/composer-regression-browser.test.mjs",
@@ -151,8 +161,11 @@
151
161
  ],
152
162
  "dependencies": {
153
163
  "@earendil-works/pi-coding-agent": "^0.86.1",
164
+ "@types/diff": "^5.2.3",
154
165
  "@xterm/addon-fit": "^0.11.0",
155
166
  "@xterm/xterm": "^6.0.0",
167
+ "diff": "^5.2.2",
168
+ "fuse.js": "^7.5.0",
156
169
  "node-pty": "^1.2.0-beta.15",
157
170
  "ws": "latest"
158
171
  },