@jmfederico/pi-web 1.202610.3 → 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.
- package/dist/cli.js +1 -1
- package/dist/client/assets/classic-CyM97Akm.css +54 -0
- package/dist/client/assets/graphite-DquaqFXU.css +54 -0
- package/dist/client/assets/index-DPS1kTH7.js +4389 -0
- package/dist/client/assets/paper-CB1oLuRr.css +54 -0
- package/dist/client/assets/pi-web-dark-D_KtbM95.css +54 -0
- package/dist/client/assets/pi-web-light-DSNLMIBI.css +54 -0
- package/dist/client/index.html +2 -2
- package/dist/config.js +56 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-packages/captains-log/dist/browser/index.js +13 -17
- package/dist/pi-packages/captains-log/dist/browser/panel.js +7 -11
- package/dist/pi-packages/relays/extensions/index.js +1 -0
- package/dist/pi-packages/relays/pi-web-plugin.js +21 -1
- package/dist/pi-packages/relays/relaysPanelElement.js +36 -32
- package/dist/pi-packages/workflows/browser/pi-web-plugin.js +35 -27
- package/dist/pi-packages/workflows/browser/worktreeDialog.js +156 -0
- package/dist/pi-packages/workflows/package.json +1 -0
- package/dist/pi-packages/workflows/prompts/worktree.md +11 -2
- package/dist/pi-packages/workflows/server-plugin.js +579 -0
- package/dist/pi-packages/workflows/vite.config.mjs +17 -0
- package/dist/pi-web-plugins/code-highlight/browser/pi-web-plugin.js +22 -0
- package/dist/pi-web-plugins/code-highlight/package.json +13 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +119 -162
- package/dist/pi-web-plugins/git/browser/git-panel.js +61 -46
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +25 -3
- package/dist/pi-web-plugins/git/git-backend.js +13 -1
- package/dist/pi-web-plugins/git/server-plugin.js +8 -1
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +8 -3
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +50 -29
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +11 -11
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +21 -1
- package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +35 -19
- package/dist/piWebUpdate.js +32 -9
- package/dist/piWebUpdate.js.map +1 -1
- package/dist/plugin-api.d.ts +174 -45
- package/dist/server/browserMessageProjection.js +1 -1
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +7 -1
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +3 -2
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/piWebPluginService.js +7 -1
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -1
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +31 -38
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -45
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +58 -34
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -1
- package/dist/server/plugins/serverPluginCatalogCapability.js +106 -0
- package/dist/server/plugins/serverPluginCatalogCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +21 -8
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +24 -14
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -1
- package/dist/server/sessiond/pluginBackendRoutes.js +51 -47
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/pluginCatalogRoutes.js +37 -0
- package/dist/server/sessiond/pluginCatalogRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +17 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond.js +24 -4
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +1 -0
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +13 -0
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/builtinExtensionFactories.js +15 -1
- package/dist/server/sessions/builtinExtensionFactories.js.map +1 -1
- package/dist/server/sessions/companionBackend.js +286 -0
- package/dist/server/sessions/companionBackend.js.map +1 -0
- package/dist/server/sessions/mcpAuthFlowService.js +237 -0
- package/dist/server/sessions/mcpAuthFlowService.js.map +1 -0
- package/dist/server/sessions/mcpAuthRoutes.js +76 -0
- package/dist/server/sessions/mcpAuthRoutes.js.map +1 -0
- package/dist/server/sessions/messageHistoryTarget.js +18 -1
- package/dist/server/sessions/messageHistoryTarget.js.map +1 -1
- package/dist/server/sessions/piSessionEventConnections.js +5 -2
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +279 -22
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +3 -17
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionNameGenerator.js +4 -4
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +1 -0
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +1 -0
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server-plugin-api.d.ts +65 -15
- package/dist/server-plugin-api.js +25 -0
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +17 -5
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +53 -0
- package/dist/shared/pluginBackendProtocol.js +56 -1
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginCatalogTypes.d.ts +49 -0
- package/dist/shared/pluginCatalogTypes.js +156 -0
- package/dist/shared/pluginCatalogTypes.js.map +1 -0
- package/docs/config.md +68 -2
- package/docs/plugins.md +382 -20
- package/examples/session-bridge-plugin/src/browser/index.ts +1 -1
- package/package.json +7 -12
- 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
|
-
|
|
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,
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 === "
|
|
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
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
}
|