@jmfederico/pi-web 1.202610.4 → 1.202610.5

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 (106) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/client/assets/classic-CyM97Akm.css +54 -0
  3. package/dist/client/assets/graphite-DquaqFXU.css +54 -0
  4. package/dist/client/assets/index-DPS1kTH7.js +4389 -0
  5. package/dist/client/assets/paper-CB1oLuRr.css +54 -0
  6. package/dist/client/assets/pi-web-dark-D_KtbM95.css +54 -0
  7. package/dist/client/assets/pi-web-light-DSNLMIBI.css +54 -0
  8. package/dist/client/index.html +2 -2
  9. package/dist/config.js +56 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/pi-packages/captains-log/dist/browser/index.js +13 -17
  12. package/dist/pi-packages/captains-log/dist/browser/panel.js +7 -11
  13. package/dist/pi-packages/relays/pi-web-plugin.js +21 -1
  14. package/dist/pi-packages/relays/relaysPanelElement.js +36 -32
  15. package/dist/pi-packages/workflows/browser/pi-web-plugin.js +35 -27
  16. package/dist/pi-packages/workflows/browser/worktreeDialog.js +156 -0
  17. package/dist/pi-packages/workflows/package.json +1 -0
  18. package/dist/pi-packages/workflows/prompts/worktree.md +11 -2
  19. package/dist/pi-packages/workflows/server-plugin.js +579 -0
  20. package/dist/pi-packages/workflows/vite.config.mjs +17 -0
  21. package/dist/pi-web-plugins/code-highlight/browser/pi-web-plugin.js +22 -0
  22. package/dist/pi-web-plugins/code-highlight/package.json +13 -0
  23. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +119 -162
  24. package/dist/pi-web-plugins/git/browser/git-panel.js +61 -46
  25. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +25 -3
  26. package/dist/pi-web-plugins/git/git-backend.js +13 -1
  27. package/dist/pi-web-plugins/git/server-plugin.js +8 -1
  28. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +8 -3
  29. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +50 -29
  30. package/dist/pi-web-plugins/updates/pi-web-plugin.js +11 -11
  31. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +21 -1
  32. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +35 -19
  33. package/dist/piWebUpdate.js +32 -9
  34. package/dist/piWebUpdate.js.map +1 -1
  35. package/dist/plugin-api.d.ts +174 -45
  36. package/dist/server/browserMessageProjection.js +1 -1
  37. package/dist/server/browserMessageProjection.js.map +1 -1
  38. package/dist/server/configRoutes.js +7 -1
  39. package/dist/server/configRoutes.js.map +1 -1
  40. package/dist/server/machines/machineProxyRoutes.js +3 -2
  41. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  42. package/dist/server/piWebPluginService.js +7 -1
  43. package/dist/server/piWebPluginService.js.map +1 -1
  44. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +1 -1
  45. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -1
  46. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +31 -38
  47. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -1
  48. package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -45
  49. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  50. package/dist/server/plugins/pluginBackendRegistry.js +58 -34
  51. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -1
  52. package/dist/server/plugins/serverPluginCatalogCapability.js +106 -0
  53. package/dist/server/plugins/serverPluginCatalogCapability.js.map +1 -0
  54. package/dist/server/plugins/serverPluginRuntime.js +21 -8
  55. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  56. package/dist/server/sessiond/pluginBackendChannelRoutes.js +24 -14
  57. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -1
  58. package/dist/server/sessiond/pluginBackendRoutes.js +51 -47
  59. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  60. package/dist/server/sessiond/pluginCatalogRoutes.js +37 -0
  61. package/dist/server/sessiond/pluginCatalogRoutes.js.map +1 -0
  62. package/dist/server/sessiond/sessionProxyRoutes.js +17 -0
  63. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  64. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  65. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  66. package/dist/server/sessiond.js +24 -4
  67. package/dist/server/sessiond.js.map +1 -1
  68. package/dist/server/sessions/authRoutes.js +1 -0
  69. package/dist/server/sessions/authRoutes.js.map +1 -1
  70. package/dist/server/sessions/authService.js +13 -0
  71. package/dist/server/sessions/authService.js.map +1 -1
  72. package/dist/server/sessions/builtinExtensionFactories.js +15 -1
  73. package/dist/server/sessions/builtinExtensionFactories.js.map +1 -1
  74. package/dist/server/sessions/companionBackend.js +286 -0
  75. package/dist/server/sessions/companionBackend.js.map +1 -0
  76. package/dist/server/sessions/mcpAuthFlowService.js +237 -0
  77. package/dist/server/sessions/mcpAuthFlowService.js.map +1 -0
  78. package/dist/server/sessions/mcpAuthRoutes.js +76 -0
  79. package/dist/server/sessions/mcpAuthRoutes.js.map +1 -0
  80. package/dist/server/sessions/messageHistoryTarget.js +18 -1
  81. package/dist/server/sessions/messageHistoryTarget.js.map +1 -1
  82. package/dist/server/sessions/piSessionEventConnections.js +5 -2
  83. package/dist/server/sessions/piSessionEventConnections.js.map +1 -1
  84. package/dist/server/sessions/piSessionService.js +279 -22
  85. package/dist/server/sessions/piSessionService.js.map +1 -1
  86. package/dist/server/sessions/sessionCommandService.js +3 -17
  87. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  88. package/dist/server/sessions/sessionNameGenerator.js +4 -4
  89. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  90. package/dist/server-plugin-api.d.ts +65 -15
  91. package/dist/server-plugin-api.js +25 -0
  92. package/dist/server-plugin-api.js.map +1 -1
  93. package/dist/shared/apiTypes.js.map +1 -1
  94. package/dist/shared/federatedRoutes.js +17 -5
  95. package/dist/shared/federatedRoutes.js.map +1 -1
  96. package/dist/shared/pluginApiTypes.d.ts +53 -0
  97. package/dist/shared/pluginBackendProtocol.js +56 -1
  98. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  99. package/dist/shared/pluginCatalogTypes.d.ts +49 -0
  100. package/dist/shared/pluginCatalogTypes.js +156 -0
  101. package/dist/shared/pluginCatalogTypes.js.map +1 -0
  102. package/docs/config.md +68 -2
  103. package/docs/plugins.md +382 -20
  104. package/examples/session-bridge-plugin/src/browser/index.ts +1 -1
  105. package/package.json +7 -12
  106. package/dist/client/assets/index-BAl5IbYx.js +0 -4429
@@ -8,6 +8,7 @@ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentS
8
8
  import { projectBrowserMessage } from "../browserMessageProjection.js";
9
9
  import { isSessionMediaId } from "../../shared/sessionMedia.js";
10
10
  import { getBuiltinExtensionFactories } from "./builtinExtensionFactories.js";
11
+ import { McpAuthFlowService } from "./mcpAuthFlowService.js";
11
12
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
12
13
  import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
13
14
  import { BUILTIN_COMMANDS } from "./builtinCommands.js";
@@ -561,10 +562,15 @@ export class PiSessionService {
561
562
  this.suggestedInputs = new Map();
562
563
  /** Counts async operations that may append an entry before they settle. */
563
564
  this.sessionEntryMutationCounts = new WeakMap();
565
+ this.manualCompactionEnded = new WeakSet();
564
566
  /** Exempt only a command's own prompt receipt while it invokes guarded session control. */
565
567
  this.extensionCommandExecution = new AsyncLocalStorage();
568
+ this.mcpAuthFlows = new McpAuthFlowService();
569
+ this.runtimeMessageCounts = new WeakMap();
570
+ this.snapshotMessageCounts = new WeakMap();
566
571
  this.pendingPromptEchoes = new WeakMap();
567
572
  this.publishedAssistantPartials = new WeakMap();
573
+ this.assistantStreamIds = new WeakMap();
568
574
  /** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
569
575
  this.modelScopeMutationQueue = Promise.resolve();
570
576
  /** Global and workspace-owned enabled-model states; live session scopes project one entry lazily. */
@@ -650,12 +656,17 @@ export class PiSessionService {
650
656
  this.commandService = new SessionCommandService((sessionId) => this.getActive(this.activeSessionRef(sessionId)), (sessionId, text) => this.prompt(this.activeSessionRef(sessionId), text, undefined, undefined, { echoUserMessage: false }), events, {
651
657
  onCompactionStart: (session) => {
652
658
  this.beginSessionEntryMutation(session, "compact the session");
659
+ this.manualCompactionEnded.delete(session);
653
660
  this.publishActivity(session, "compacting", "active");
654
661
  this.publishStatus(session);
655
662
  },
656
663
  onCompactionEnd: (session, result, detail) => {
657
664
  this.endSessionEntryMutation(session);
658
- this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
665
+ // The SDK terminal event carries the authoritative aborted flag, even
666
+ // when an extension cancels by throwing an otherwise ordinary error.
667
+ if (!this.manualCompactionEnded.delete(session) && result === "error") {
668
+ this.publishCompactionOutcome(session, { reason: "manual", errorMessage: `Compaction failed: ${detail ?? "Unknown error"}` });
669
+ }
659
670
  this.publishStatus(session);
660
671
  },
661
672
  reloadSession: (session) => this.reloadSessionRuntime(session),
@@ -746,6 +757,7 @@ export class PiSessionService {
746
757
  });
747
758
  }
748
759
  async dispose() {
760
+ this.mcpAuthFlows.dispose();
749
761
  this.unreadPublicationStopped = true;
750
762
  this.clearUnreadPublicationRetry();
751
763
  clearInterval(this.heartbeat);
@@ -833,6 +845,44 @@ export class PiSessionService {
833
845
  .sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
834
846
  return [...unarchivedSessions, ...archivedSessions];
835
847
  }
848
+ /** Read-only plugin catalog: do not open runtimes or reconcile unread/activity state. */
849
+ async listCatalogSessions(cwd, signal) {
850
+ signal?.throwIfAborted();
851
+ const [entries, archives] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
852
+ signal?.throwIfAborted();
853
+ const archived = archives.filter((record) => cwdPathsEqual(record.cwd, cwd));
854
+ const archivedIds = new Set(archived.map((record) => record.sessionId));
855
+ const rows = new Map(entries.filter((entry) => !archivedIds.has(entry.id)).map((entry) => [entry.id, clientSessionFromListEntry(entry)]));
856
+ const runtimes = new Map();
857
+ for (const active of new Set(this.active.values())) {
858
+ const session = active.runtime.session;
859
+ if (!cwdPathsEqual(session.sessionManager.getCwd(), cwd) || archivedIds.has(session.sessionId))
860
+ continue;
861
+ const row = rows.get(session.sessionId);
862
+ // A disk clone with the same id must not inherit another file's runtime.
863
+ if (row !== undefined && (session.sessionFile === undefined || !sessionPathsEqual(row.path, session.sessionFile)))
864
+ continue;
865
+ runtimes.set(session.sessionId, session);
866
+ rows.set(session.sessionId, clientSessionFromActiveSession(session));
867
+ }
868
+ const entriesById = new Map(entries.map((entry) => [entry.id, entry]));
869
+ for (const record of archived) {
870
+ const row = clientSessionFromArchivedRecord(record, entriesById.get(record.sessionId));
871
+ if (row !== undefined)
872
+ rows.set(row.id, row);
873
+ }
874
+ return [...rows.values()].sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified)).map((row) => {
875
+ const runtime = runtimes.get(row.id);
876
+ return Object.freeze({
877
+ id: row.id,
878
+ cwd: row.cwd,
879
+ ...(row.name === undefined ? {} : { name: row.name }),
880
+ ...(row.metadata === undefined ? {} : { metadata: structuredClone(row.metadata) }),
881
+ archived: row.archived === true,
882
+ runtime: runtime === undefined ? "not-hosted" : this.hasActiveWork(runtime) ? "busy" : "idle",
883
+ });
884
+ });
885
+ }
836
886
  async start(cwd, options = {}) {
837
887
  return this.startSession(cwd, options);
838
888
  }
@@ -1843,7 +1893,7 @@ export class PiSessionService {
1843
1893
  if (this.hasActiveWork(session))
1844
1894
  return this.statusFromSession(session);
1845
1895
  const branch = await this.readableSessionBranch(ref, session);
1846
- return this.statusFromSession(session, transcriptMessageCount(branch));
1896
+ return this.statusFromSession(session, this.snapshotMessageCount(branch));
1847
1897
  }
1848
1898
  /**
1849
1899
  * Join-time snapshot of the in-flight assistant stream. The `seq` watermark and
@@ -1861,7 +1911,7 @@ export class PiSessionService {
1861
1911
  const streamingMessage = session.state.streamingMessage;
1862
1912
  const partial = streamingMessage === undefined || streamingMessage === null
1863
1913
  ? null
1864
- : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1914
+ : annotateAssistantStreamId(annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel), this.assistantStreamIds.get(session));
1865
1915
  return { seq, partial };
1866
1916
  }
1867
1917
  async getSessionDefaults(ref) {
@@ -2050,6 +2100,108 @@ export class PiSessionService {
2050
2100
  }
2051
2101
  return commands.sort((a, b) => a.name.localeCompare(b.name));
2052
2102
  }
2103
+ async mcpAuthSession(ref) {
2104
+ await this.assertWritable(ref);
2105
+ const session = await this.getOrOpen(ref);
2106
+ await this.assertWritable(ref);
2107
+ this.assertCurrentExtensionSession(session);
2108
+ if (session.sessionId !== ref.id || !cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()))
2109
+ throw new Error("MCP authentication session workspace changed");
2110
+ if (session.settingsManager.isProjectTrusted?.() !== true)
2111
+ throw new Error("MCP authentication requires a trusted workspace");
2112
+ if (!session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === "mcp" && command.sourceInfo?.path === "builtin:mcp")) {
2113
+ throw new Error("Pi's built-in MCP command is not available for this session");
2114
+ }
2115
+ if (this.hasActiveWork(session))
2116
+ throw new Error("Wait for current session activity before authenticating MCP");
2117
+ return session;
2118
+ }
2119
+ assertMcpAuthIdentity(ref, session) {
2120
+ this.assertCurrentExtensionSession(session);
2121
+ if (session.sessionId !== ref.id || !cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()))
2122
+ throw new Error("MCP authentication session workspace changed");
2123
+ if (session.settingsManager.isProjectTrusted?.() !== true)
2124
+ throw new Error("MCP authentication requires a trusted workspace");
2125
+ }
2126
+ async discoverMcpAuthServers(ref, session) {
2127
+ return this.mcpAuthFlows.discover({
2128
+ sessionId: ref.id, identity: session,
2129
+ isCurrent: () => this.isCurrentActiveSession(session),
2130
+ run: async () => {
2131
+ // The SDK has no public MCP readiness promise. In RPC mode this supported
2132
+ // status command awaits startup without reconnecting or touching tokens.
2133
+ await session.prompt("/mcp", { expandPromptTemplates: true });
2134
+ await this.assertWritable(ref);
2135
+ this.assertMcpAuthIdentity(ref, session);
2136
+ const command = session.extensionRunner.getRegisteredCommands().find((candidate) => candidate.invocationName === "mcp" && candidate.sourceInfo?.path === "builtin:mcp");
2137
+ if (!command)
2138
+ throw new Error("Pi's built-in MCP command is not available for this session");
2139
+ const completions = await command.getArgumentCompletions?.("login ") ?? [];
2140
+ await this.assertWritable(ref);
2141
+ this.assertMcpAuthIdentity(ref, session);
2142
+ if (completions.length > 200)
2143
+ throw new Error("Too many MCP authentication servers");
2144
+ const servers = [];
2145
+ for (const item of completions) {
2146
+ const name = item.value.startsWith("login ") ? item.value.slice(6) : "";
2147
+ // Commands split on whitespace. Never forward an arbitrary suffix or
2148
+ // a name that could change the builtin command's argument structure.
2149
+ if (name.length === 0 || name.length > 256 || /\s/u.test(name) || Array.from(name).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
2150
+ continue;
2151
+ if ((item.description?.length ?? 0) > 32 * 1024)
2152
+ throw new Error("MCP server description is too large");
2153
+ if (!servers.some((server) => server.name === name))
2154
+ servers.push({ name, ...(item.description === undefined ? {} : { description: item.description }) });
2155
+ }
2156
+ return servers;
2157
+ },
2158
+ });
2159
+ }
2160
+ async mcpAuthServers(ref) {
2161
+ const session = await this.mcpAuthSession(ref);
2162
+ if (this.hasActiveWork(session))
2163
+ throw new Error("Wait for current session activity before listing MCP servers");
2164
+ return this.runSessionEntryMutation(session, "list MCP authentication servers", async () => ({ servers: await this.discoverMcpAuthServers(ref, session) }));
2165
+ }
2166
+ async startMcpAuth(ref, mode, serverName) {
2167
+ const session = await this.mcpAuthSession(ref);
2168
+ if (this.hasActiveWork(session))
2169
+ throw new Error("Wait for current session activity before authenticating MCP");
2170
+ const servers = await this.runSessionEntryMutation(session, "discover MCP authentication servers", () => this.discoverMcpAuthServers(ref, session));
2171
+ if (serverName !== undefined && !servers.some((server) => server.name === serverName))
2172
+ throw new Error("Unknown or ineligible MCP authentication server");
2173
+ this.assertMcpAuthIdentity(ref, session);
2174
+ if (this.hasActiveWork(session))
2175
+ throw new Error("Wait for current session activity before authenticating MCP");
2176
+ return this.mcpAuthFlows.start({
2177
+ sessionId: ref.id, mode,
2178
+ identity: session,
2179
+ isCurrent: () => this.isCurrentActiveSession(session) && session.sessionId === ref.id && cwdPathsEqual(ref.cwd, session.sessionManager.getCwd()),
2180
+ // This is an awaited extension command, not a model prompt submission.
2181
+ // Avoid prompt echoes/activity and the fire-and-forget command receipt.
2182
+ run: () => this.runSessionEntryMutation(session, "authenticate MCP", async () => {
2183
+ const operation = McpAuthFlowService.current(session);
2184
+ if (!operation)
2185
+ throw new Error("MCP authentication session is no longer current");
2186
+ if (servers.length === 0) {
2187
+ operation.notify("No eligible MCP servers are available for this action.");
2188
+ return;
2189
+ }
2190
+ const selected = serverName ?? await operation.selectServer(servers);
2191
+ if (selected === undefined || operation.isCancelled())
2192
+ return;
2193
+ await this.assertWritable(ref);
2194
+ this.assertMcpAuthIdentity(ref, session);
2195
+ const fresh = await this.discoverMcpAuthServers(ref, session);
2196
+ if (!fresh.some((server) => server.name === selected))
2197
+ throw new Error("MCP authentication server is no longer eligible");
2198
+ if (operation.isCancelled())
2199
+ return;
2200
+ operation.setServer(selected);
2201
+ await session.prompt(`/mcp ${mode} ${selected}`, { expandPromptTemplates: true });
2202
+ }),
2203
+ });
2204
+ }
2053
2205
  async prompt(ref, text, streamingBehavior, attachments, options) {
2054
2206
  const promptText = requirePromptText(text);
2055
2207
  // Command-forwarded prompts (e.g. /skill:*) are expanded by the agent, which
@@ -2357,6 +2509,7 @@ export class PiSessionService {
2357
2509
  this.treeNavigations.delete(session);
2358
2510
  if (this.isCurrentActiveSession(session)) {
2359
2511
  this.flushDeferredTreeNavigationWork(session);
2512
+ this.reconcileRuntimeMessageCount(session);
2360
2513
  this.publishStatus(session);
2361
2514
  }
2362
2515
  else {
@@ -3045,6 +3198,7 @@ export class PiSessionService {
3045
3198
  // Extension dialog waiters are parked Promises inside the dying runtime:
3046
3199
  // settle them rather than dropping them.
3047
3200
  this.endSessionExtensionDialogs(sessionId);
3201
+ this.mcpAuthFlows.cancelSession(sessionId);
3048
3202
  this.active.delete(sessionId);
3049
3203
  this.events.mediaIndex.forgetSession({ id: sessionId, cwd: active.runtime.session.sessionManager.getCwd() });
3050
3204
  this.activities.delete(sessionId);
@@ -3092,6 +3246,7 @@ export class PiSessionService {
3092
3246
  async assertWritable(ref) {
3093
3247
  if (await this.getArchived(ref) !== undefined)
3094
3248
  throw new Error("Archived sessions are read-only. Restore the session to continue.");
3249
+ this.mcpAuthFlows.assertSessionAvailable(ref.id);
3095
3250
  }
3096
3251
  async getOrOpen(ref) {
3097
3252
  return (await this.getActive(ref)).runtime.session;
@@ -3419,6 +3574,11 @@ export class PiSessionService {
3419
3574
  commandContextActions: this.extensionCommandActions(session),
3420
3575
  onError: (error) => {
3421
3576
  const message = `${error.extensionPath}: ${error.error}`;
3577
+ const operation = McpAuthFlowService.current(session);
3578
+ if (operation) {
3579
+ operation.error(message);
3580
+ return;
3581
+ }
3422
3582
  this.publishActivity(session, "extension error", "error", message);
3423
3583
  this.events.publish(session.sessionId, { type: "session.error", message });
3424
3584
  },
@@ -3514,6 +3674,11 @@ export class PiSessionService {
3514
3674
  sessionUiContext(session, generation, startupDialogs) {
3515
3675
  const baseUiContext = session.extensionRunner.getUIContext();
3516
3676
  const notify = (message, type) => {
3677
+ const operation = McpAuthFlowService.current(session);
3678
+ if (operation) {
3679
+ operation.notify(message);
3680
+ return;
3681
+ }
3517
3682
  if (generation === undefined) {
3518
3683
  this.events.publish(session.sessionId, {
3519
3684
  type: "command.output",
@@ -3537,13 +3702,16 @@ export class PiSessionService {
3537
3702
  if (property === "theme")
3538
3703
  return plainTextTheme;
3539
3704
  if (property === "confirm") {
3540
- return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs?.signal);
3705
+ return (title, message, opts) => McpAuthFlowService.current(session)?.confirm(title, message, opts)
3706
+ ?? this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs?.signal);
3541
3707
  }
3542
3708
  if (property === "select") {
3543
- return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs?.signal);
3709
+ return (title, options, opts) => McpAuthFlowService.current(session)?.select(title, options, opts)
3710
+ ?? this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs?.signal);
3544
3711
  }
3545
3712
  if (property === "input") {
3546
- return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs?.signal);
3713
+ return (title, placeholder, opts) => McpAuthFlowService.current(session)?.input(title, placeholder, opts)
3714
+ ?? this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs?.signal);
3547
3715
  }
3548
3716
  const value = Reflect.get(target, property, receiver);
3549
3717
  return value;
@@ -3685,6 +3853,7 @@ export class PiSessionService {
3685
3853
  }
3686
3854
  }
3687
3855
  this.restoreSubsessionReceipts(session);
3856
+ this.reconcileRuntimeMessageCount(session);
3688
3857
  const unsubscribe = session.subscribe((event) => {
3689
3858
  const eventType = getString(event, "type");
3690
3859
  const message = getProperty(event, "message");
@@ -3698,6 +3867,17 @@ export class PiSessionService {
3698
3867
  this.subsessionStoppedParents.add(session);
3699
3868
  }
3700
3869
  const entry = getProperty(event, "entry");
3870
+ // Regular message_end precedes persistence; boundary drafts instead emit
3871
+ // entry_appended. Only message entries contribute to the transcript count.
3872
+ const role = getString(message, "role");
3873
+ if ((eventType === "message_end" && (role === "user" || role === "assistant" || role === "toolResult" || role === "system"))
3874
+ || (eventType === "entry_appended" && getString(entry, "type") === "message")) {
3875
+ this.runtimeMessageCounts.set(session, this.runtimeMessageCount(session) + 1);
3876
+ }
3877
+ // Reconcile once after the run has actually settled, not per streamed
3878
+ // update or per message. Tree changes can replace the branch entirely.
3879
+ if (eventType === "agent_settled" || eventType === "session_tree")
3880
+ this.reconcileRuntimeMessageCount(session);
3701
3881
  if (eventType === "entry_appended" && getString(entry, "type") === "custom_message"
3702
3882
  && getString(entry, "customType") === SUBSESSION_NOTIFICATION_CUSTOM_TYPE) {
3703
3883
  inbox.receive(completionReceiptIds(getProperty(entry, "details")));
@@ -3708,26 +3888,37 @@ export class PiSessionService {
3708
3888
  this.restoreSubsessionReceipts(session);
3709
3889
  if (eventType === "agent_settled")
3710
3890
  this.scheduleSubsessionDelivery(session);
3891
+ if (eventType === "message_start" && getString(message, "role") === "assistant") {
3892
+ this.assistantStreamIds.set(session, randomUUID());
3893
+ }
3894
+ const streamId = this.assistantStreamIds.get(session);
3711
3895
  if ((eventType === "message_start" || eventType === "message_update") && getString(message, "role") === "assistant") {
3712
3896
  // SDK state advances before async extension hooks finish. Capture only
3713
3897
  // published messages, detaching even blocks left intact by projection.
3714
- this.publishedAssistantPartials.set(session, structuredClone(annotateAssistantThinkingLevel(projectBrowserMessage(message), session.thinkingLevel)));
3898
+ this.publishedAssistantPartials.set(session, structuredClone(annotateAssistantStreamId(annotateAssistantThinkingLevel(projectBrowserMessage(message), session.thinkingLevel), streamId)));
3715
3899
  }
3716
- else if (eventType === "message_start" || eventType === "message_end" || eventType === "agent_end") {
3900
+ else if ((eventType === "message_end" && getString(message, "role") === "assistant") || eventType === "agent_end") {
3717
3901
  this.publishedAssistantPartials.delete(session);
3718
3902
  }
3719
- const clientEvent = toClientEvent(event, session.thinkingLevel);
3903
+ const clientEvent = toClientEvent(event, session.thinkingLevel, streamId);
3720
3904
  if ((clientEvent.type === "message.end" || clientEvent.type === "message.append") && clientEvent.message !== undefined) {
3721
3905
  clientEvent.message = this.browserTranscriptMessage(session, clientEvent.message);
3722
3906
  }
3723
3907
  this.events.publish(session.sessionId, clientEvent, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
3908
+ if ((eventType === "message_end" && getString(message, "role") === "assistant") || eventType === "agent_end") {
3909
+ this.assistantStreamIds.delete(session);
3910
+ }
3724
3911
  this.publishActivityForEvent(session, event);
3725
3912
  if (eventType === "message_start" && getString(message, "role") === "user")
3726
3913
  this.suggestedInputs.delete(session.sessionId);
3727
3914
  if (eventType === "agent_end")
3728
3915
  this.abortRunScopedExtensionDialogs(session.sessionId);
3729
- if (eventType === "compaction_end")
3916
+ if (eventType === "compaction_end") {
3917
+ if (getString(event, "reason") === "manual")
3918
+ this.manualCompactionEnded.add(session);
3919
+ this.publishCompactionOutcome(session, event);
3730
3920
  this.scheduleCompactionQueueDrain(session.sessionId);
3921
+ }
3731
3922
  if (eventType === "compaction_end" || eventType === "bash_execution_end")
3732
3923
  this.scheduleSubsessionDelivery(session);
3733
3924
  if (eventType === "agent_start" || eventType === "agent_end")
@@ -3739,9 +3930,28 @@ export class PiSessionService {
3739
3930
  this.sessionEvents.close(session);
3740
3931
  unsubscribe();
3741
3932
  this.publishedAssistantPartials.delete(session);
3933
+ this.assistantStreamIds.delete(session);
3742
3934
  };
3743
3935
  this.active.set(session.sessionId, active);
3744
3936
  }
3937
+ publishCompactionOutcome(session, event) {
3938
+ const aborted = getBoolean(event, "aborted") === true;
3939
+ const errorMessage = getString(event, "errorMessage");
3940
+ const automatic = getString(event, "reason") !== "manual";
3941
+ const message = aborted
3942
+ ? (automatic ? "Automatic compaction cancelled." : "Compaction cancelled.")
3943
+ : errorMessage === undefined ? undefined : "Couldn’t compact history. Try again.";
3944
+ if (!aborted && errorMessage !== undefined) {
3945
+ this.events.publish(session.sessionId, { type: "session.diagnostic", message: errorMessage });
3946
+ }
3947
+ this.publishActivity(session, aborted ? "compaction cancelled" : errorMessage === undefined ? "compaction complete" : "compaction failed", !aborted && errorMessage !== undefined ? "error" : "idle", message);
3948
+ // Successful compaction already has a canonical summary in the transcript.
3949
+ // Keep cancellation/failure feedback in the refresh-safe session inbox only.
3950
+ const generation = this.notificationGenerationBySession.get(session);
3951
+ if (message !== undefined && generation !== undefined) {
3952
+ this.publishNotificationMutations(this.notificationStore.addNotification(generation, message, aborted ? "info" : "error").mutations);
3953
+ }
3954
+ }
3745
3955
  scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
3746
3956
  if (!this.compactionPromptQueues.has(sessionId) || this.compactionDrainTimers.has(sessionId))
3747
3957
  return;
@@ -3814,15 +4024,30 @@ export class PiSessionService {
3814
4024
  maybeGenerateSessionName(session, firstMessage) {
3815
4025
  if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
3816
4026
  return;
3817
- const model = session.model;
3818
- if (model === undefined)
3819
- return;
3820
- void generateShortSessionName(session.agent.streamFunction, model, firstMessage).then((name) => {
4027
+ void this.generateSessionName(session, firstMessage).then((name) => {
4028
+ if (name === undefined)
4029
+ this.logger.info({ sessionId: session.sessionId }, "Session naming returned no title; using the first-message fallback");
3821
4030
  this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
3822
- }).catch(() => {
4031
+ }).catch((err) => {
4032
+ this.logger.info({ sessionId: session.sessionId, err }, "Session naming failed; using the first-message fallback");
3823
4033
  this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
3824
4034
  });
3825
4035
  }
4036
+ async generateSessionName(session, firstMessage) {
4037
+ // Capture one config snapshot per request; later edits must not reroute an
4038
+ // in-progress naming call or rename sessions that already have a title.
4039
+ const naming = this.config === undefined ? {} : (await this.config.read()).effectiveConfig.sessionNaming ?? {};
4040
+ let model = session.model;
4041
+ if (naming.model !== undefined) {
4042
+ const parsed = parseModelSpec(naming.model);
4043
+ model = parsed === undefined ? undefined : session.modelRuntime.getModel(parsed.provider, parsed.modelId);
4044
+ if (model === undefined)
4045
+ throw new Error(`Session naming model is unavailable: ${naming.model}`);
4046
+ }
4047
+ if (model === undefined)
4048
+ return undefined;
4049
+ return generateShortSessionName(session.agent.streamFunction, model, firstMessage, naming);
4050
+ }
3826
4051
  applyGeneratedSessionName(session, name) {
3827
4052
  if (name === undefined || session.sessionName !== undefined)
3828
4053
  return;
@@ -4006,6 +4231,7 @@ export class PiSessionService {
4006
4231
  }
4007
4232
  }
4008
4233
  beginSessionEntryMutation(session, action) {
4234
+ this.mcpAuthFlows.assertSessionAvailable(session.sessionId);
4009
4235
  this.assertTreeNavigationInactive(session, action);
4010
4236
  this.sessionEntryMutationCounts.set(session, (this.sessionEntryMutationCounts.get(session) ?? 0) + 1);
4011
4237
  this.observeUnreadActivityState(session);
@@ -4163,7 +4389,26 @@ export class PiSessionService {
4163
4389
  this.events.publish(session.sessionId, { type: "activity.update", activity });
4164
4390
  this.events.publishGlobal({ type: "activity.update", activity });
4165
4391
  }
4166
- statusFromSession(session, messageCount = session.messages.length) {
4392
+ reconcileRuntimeMessageCount(session) {
4393
+ const count = transcriptMessageCount(session.sessionManager.getBranch());
4394
+ this.runtimeMessageCounts.set(session, count);
4395
+ return count;
4396
+ }
4397
+ runtimeMessageCount(session) {
4398
+ return this.runtimeMessageCounts.get(session) ?? this.reconcileRuntimeMessageCount(session);
4399
+ }
4400
+ snapshotMessageCount(branch) {
4401
+ // The gateway reuses immutable disk snapshots until the file changes.
4402
+ // Keep disk counts separate: an idle runtime may lag external appends.
4403
+ const cached = this.snapshotMessageCounts.get(branch);
4404
+ if (cached !== undefined)
4405
+ return cached;
4406
+ const count = transcriptMessageCount(branch);
4407
+ this.snapshotMessageCounts.set(branch, count);
4408
+ return count;
4409
+ }
4410
+ // session.messages is the compacted model context, not the browser transcript.
4411
+ statusFromSession(session, messageCount = this.runtimeMessageCount(session)) {
4167
4412
  const stats = session.getSessionStats();
4168
4413
  const model = session.model === undefined ? undefined : modelToClientModel(session.model);
4169
4414
  const contextUsage = session.getContextUsage();
@@ -4307,7 +4552,7 @@ function clientSessionFromActiveSession(session) {
4307
4552
  persisted: sessionFileExists(session.sessionFile),
4308
4553
  created,
4309
4554
  modified: created,
4310
- messageCount: session.messages.length,
4555
+ messageCount: transcriptMessageCount(session.sessionManager.getBranch()),
4311
4556
  firstMessage: "",
4312
4557
  ...(metadata === undefined ? {} : { metadata }),
4313
4558
  ...(session.sessionName === undefined ? {} : { name: session.sessionName }),
@@ -4356,7 +4601,7 @@ function archiveInputFromActiveSession(session) {
4356
4601
  path: sessionFile,
4357
4602
  created: new Date().toISOString(),
4358
4603
  modified: new Date().toISOString(),
4359
- messageCount: session.messages.length,
4604
+ messageCount: transcriptMessageCount(session.sessionManager.getBranch()),
4360
4605
  firstMessage: "",
4361
4606
  ...(metadata === undefined ? {} : { metadata }),
4362
4607
  ...(session.sessionName === undefined ? {} : { name: session.sessionName }),
@@ -4785,19 +5030,31 @@ function finalAssistantText(messages) {
4785
5030
  }
4786
5031
  return "";
4787
5032
  }
4788
- function toClientEvent(event, thinkingLevel) {
5033
+ function annotateAssistantStreamId(message, streamId) {
5034
+ return streamId !== undefined && getString(message, "role") === "assistant" && isRecord(message)
5035
+ ? { ...message, streamId }
5036
+ : message;
5037
+ }
5038
+ function toClientEvent(event, thinkingLevel, streamId) {
4789
5039
  const eventType = getString(event, "type");
4790
5040
  // Nested execution belongs to the parent tool result, not a durable message.
4791
5041
  // Do not create standalone transcript rows that disappear on reconnect.
4792
5042
  if (getString(event, "parentToolCallId") !== undefined && eventType?.startsWith("tool_execution_") === true) {
4793
5043
  return { type: "pi.event", eventType };
4794
5044
  }
5045
+ if (eventType === "message_start" && getString(getProperty(event, "message"), "role") === "assistant") {
5046
+ return {
5047
+ type: "assistant.start",
5048
+ message: annotateAssistantStreamId(annotateAssistantThinkingLevel(projectBrowserMessage(getProperty(event, "message")), thinkingLevel), streamId),
5049
+ };
5050
+ }
5051
+ const streamIdentity = streamId === undefined ? {} : { streamId };
4795
5052
  const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
4796
5053
  if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
4797
- return { type: "assistant.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
5054
+ return { type: "assistant.delta", text: getString(assistantMessageEvent, "delta") ?? "", ...streamIdentity };
4798
5055
  }
4799
5056
  if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "thinking_delta") {
4800
- return { type: "assistant.thinking.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
5057
+ return { type: "assistant.thinking.delta", text: getString(assistantMessageEvent, "delta") ?? "", ...streamIdentity };
4801
5058
  }
4802
5059
  if (eventType === "tool_execution_start") {
4803
5060
  const args = getProperty(event, "args");
@@ -4826,7 +5083,7 @@ function toClientEvent(event, thinkingLevel) {
4826
5083
  const message = getProperty(event, "message");
4827
5084
  if (message === undefined)
4828
5085
  return { type: "message.end" };
4829
- return { type: "message.end", message: annotateAssistantThinkingLevel(message, thinkingLevel) };
5086
+ return { type: "message.end", message: annotateAssistantStreamId(annotateAssistantThinkingLevel(message, thinkingLevel), streamId) };
4830
5087
  }
4831
5088
  return { type: "pi.event", eventType: eventType ?? "unknown" };
4832
5089
  }