@jmfederico/pi-web 1.202607.3 → 1.202608.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/cli.js +69 -7
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-CDVbMiN9.js → CodeViewer-mEiBZFA5.js} +2 -2
- package/dist/client/assets/{TerminalPanel-CTS1CgqF.js → TerminalPanel-O9381Eme.js} +6 -6
- package/dist/client/assets/index-uI2AGFUy.js +4091 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +109 -73
- package/dist/config.js.map +1 -1
- package/dist/docker/piWebDockerCommandPlan.js +4 -3
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +1 -1
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-web-plugins/git/browser/git-contract.js +108 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +609 -0
- package/dist/pi-web-plugins/git/browser/gitFileList.js +43 -0
- package/dist/pi-web-plugins/git/browser/gitFileShared.js +16 -0
- package/dist/pi-web-plugins/git/browser/gitFileTree.js +81 -0
- package/dist/pi-web-plugins/git/browser/gitFileViewPreference.js +35 -0
- package/dist/pi-web-plugins/git/browser/gitRoute.js +42 -0
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +10 -0
- package/dist/pi-web-plugins/git/browser/unifiedDiff.js +301 -0
- package/dist/{server/git/gitService.js → pi-web-plugins/git/git-backend.js} +209 -60
- package/dist/pi-web-plugins/git/package.json +16 -0
- package/dist/pi-web-plugins/git/server-plugin.js +176 -0
- package/dist/pi-web-plugins/info/infoInternals.js +1 -2
- package/dist/pi-web-plugins/info/package.json +1 -1
- package/dist/pi-web-plugins/info/pi-web-plugin.js +2 -2
- package/dist/pi-web-plugins/relays/package.json +1 -1
- package/dist/pi-web-plugins/relays/pi-web-plugin.js +3 -3
- package/dist/pi-web-plugins/relays/relayDiscovery.js +159 -27
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +173 -16
- package/dist/pi-web-plugins/updates/package.json +1 -1
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/package.json +1 -1
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +3 -3
- package/dist/plugin-api.d.ts +33 -16
- package/dist/pluginRecoveryCli.js +172 -0
- package/dist/pluginRecoveryCli.js.map +1 -0
- package/dist/server/activity/workspaceActivityService.js +27 -19
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +35 -37
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +0 -2
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/configRoutes.js +4 -31
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/machines/machineClient.js +23 -4
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +74 -17
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +167 -21
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +22 -0
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +584 -0
- package/dist/server/piWebPluginCatalog.js.map +1 -0
- package/dist/server/piWebPluginLifecycle.js +162 -0
- package/dist/server/piWebPluginLifecycle.js.map +1 -0
- package/dist/server/piWebPluginService.js +159 -261
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +40 -45
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/plugins/pluginBackendProxyRoutes.js +69 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -0
- package/dist/server/plugins/serverPluginExec.js +184 -0
- package/dist/server/plugins/serverPluginExec.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +480 -0
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +26 -11
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/requestCancellation.js +32 -0
- package/dist/server/requestCancellation.js.map +1 -0
- package/dist/server/sessiond/agentHttpDispatcher.js +108 -0
- package/dist/server/sessiond/agentHttpDispatcher.js.map +1 -0
- package/dist/server/sessiond/agentProcessEnvironment.js +61 -0
- package/dist/server/sessiond/agentProcessEnvironment.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +64 -0
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +24 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +2 -2
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +3 -1
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond/sessiondStateOwnership.js +235 -0
- package/dist/server/sessiond/sessiondStateOwnership.js.map +1 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js +31 -0
- package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js +41 -0
- package/dist/server/sessiond/workspaceRemovalRoutes.js.map +1 -0
- package/dist/server/sessiond.js +283 -96
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/askUserTool.js +0 -3
- package/dist/server/sessions/askUserTool.js.map +1 -1
- package/dist/server/sessions/authRoutes.js +0 -10
- package/dist/server/sessions/authRoutes.js.map +1 -1
- package/dist/server/sessions/authService.js +15 -78
- package/dist/server/sessions/authService.js.map +1 -1
- package/dist/server/sessions/dockerEnvironmentFacts.js +254 -0
- package/dist/server/sessions/dockerEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/messagePaging.js +2 -2
- package/dist/server/sessions/messagePaging.js.map +1 -1
- package/dist/server/sessions/modelCatalogRefresher.js +4 -3
- package/dist/server/sessions/modelCatalogRefresher.js.map +1 -1
- package/dist/server/sessions/oauthLoginFlowService.js +2 -5
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/pendingAskStore.js +0 -3
- package/dist/server/sessions/pendingAskStore.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +134 -3
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +318 -253
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionCommandService.js +24 -1
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionEnvironmentFacts.js +48 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -0
- package/dist/server/sessions/sessionFileFormat.js +27 -0
- package/dist/server/sessions/sessionFileFormat.js.map +1 -0
- package/dist/server/sessions/sessionFileHeader.js +82 -23
- package/dist/server/sessions/sessionFileHeader.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +88 -56
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionSummaryScanner.js +491 -0
- package/dist/server/sessions/sessionSummaryScanner.js.map +1 -0
- package/dist/server/sessions/spawnSessionTool.js +8 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +11 -9
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/spawnTargetResolver.js +1 -1
- package/dist/server/status/machineStatusRoutes.js +8 -0
- package/dist/server/status/machineStatusRoutes.js.map +1 -0
- package/dist/server/status/machineStatusService.js +181 -0
- package/dist/server/status/machineStatusService.js.map +1 -0
- package/dist/server/status/workspaceAttribution.js +87 -0
- package/dist/server/status/workspaceAttribution.js.map +1 -0
- package/dist/server/terminalProxyRoutes.js +2 -1
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +20 -13
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/effectivePathAccess.js +0 -19
- package/dist/server/workspaces/effectivePathAccess.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +18 -9
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/filePreviewResponseHeaders.js +18 -0
- package/dist/server/workspaces/filePreviewResponseHeaders.js.map +1 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js +55 -0
- package/dist/server/workspaces/filePreviewResponsePolicy.js.map +1 -0
- package/dist/server/workspaces/filePreviewService.js +81 -0
- package/dist/server/workspaces/filePreviewService.js.map +1 -0
- package/dist/server/workspaces/projectWorkspaceCwds.js +0 -5
- package/dist/server/workspaces/projectWorkspaceCwds.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +402 -0
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceCatalog.js +47 -0
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -0
- package/dist/server/workspaces/workspaceContext.js +1 -3
- package/dist/server/workspaces/workspaceContext.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +39 -103
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +651 -0
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -0
- package/dist/server/workspaces/workspaceRemovalService.js +256 -0
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -0
- package/dist/server/workspaces/workspaceRouteErrors.js +7 -0
- package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js +39 -0
- package/dist/server/workspaces/worktreePreRemoveHook.js.map +1 -0
- package/dist/server-plugin-api.d.ts +140 -0
- package/dist/server-plugin-api.js +2 -0
- package/dist/server-plugin-api.js.map +1 -0
- package/dist/serverPluginRecovery.js +138 -0
- package/dist/serverPluginRecovery.js.map +1 -0
- package/dist/sessiond/activeAgentProfile.js +3 -22
- package/dist/sessiond/activeAgentProfile.js.map +1 -1
- package/dist/sessiond/config.js +13 -2
- package/dist/sessiond/config.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +11 -9
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/activeAgentProfile.js +2 -41
- package/dist/shared/activeAgentProfile.js.map +1 -1
- package/dist/shared/activity.js +0 -3
- package/dist/shared/activity.js.map +1 -1
- package/dist/shared/apiTypes.js +7 -14
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +8 -40
- package/dist/shared/capabilities.js.map +1 -1
- package/dist/shared/federatedRoutes.js +28 -8
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +4 -2
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/machineStatus.js +94 -0
- package/dist/shared/machineStatus.js.map +1 -0
- package/dist/shared/piWebStatusParsing.js +29 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +148 -0
- package/dist/shared/pluginApiTypes.js +3 -0
- package/dist/shared/pluginApiTypes.js.map +1 -0
- package/dist/shared/pluginBackendProtocol.js +110 -0
- package/dist/shared/pluginBackendProtocol.js.map +1 -0
- package/dist/shared/pluginIds.js +5 -0
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/pluginRecoveryCommands.js +14 -0
- package/dist/shared/pluginRecoveryCommands.js.map +1 -0
- package/dist/shared/workspaceFiles.js +41 -2
- package/dist/shared/workspaceFiles.js.map +1 -1
- package/dist/shared/workspaceRemovalProtocol.js +24 -0
- package/dist/shared/workspaceRemovalProtocol.js.map +1 -0
- package/docs/config.md +140 -47
- package/docs/plugins.md +386 -137
- package/examples/workspace-provider-plugin/README.md +52 -0
- package/examples/workspace-provider-plugin/package.json +25 -0
- package/examples/workspace-provider-plugin/src/browser/index.ts +69 -0
- package/examples/workspace-provider-plugin/src/server.ts +64 -0
- package/examples/workspace-provider-plugin/tsconfig.json +21 -0
- package/package.json +32 -12
- package/server-plugin-api.d.ts +1 -0
- package/dist/client/assets/UnifiedDiffViewer-Cqke12ZX.js +0 -39
- package/dist/client/assets/index-LME0LfPb.js +0 -4068
- package/dist/plugin-api/unstable.d.ts +0 -22
- package/dist/server/activity/workspaceActivityRoutes.js +0 -4
- package/dist/server/activity/workspaceActivityRoutes.js.map +0 -1
- package/dist/server/git/gitService.js.map +0 -1
- package/dist/server/gitRoutes.js +0 -23
- package/dist/server/gitRoutes.js.map +0 -1
- package/dist/server/sessions/parentSessionLocator.js +0 -75
- package/dist/server/sessions/parentSessionLocator.js.map +0 -1
- package/dist/server/workspaces/gitWorktreeDiscovery.js +0 -43
- package/dist/server/workspaces/gitWorktreeDiscovery.js.map +0 -1
- package/dist/server/workspaces/imagePreviewService.js +0 -40
- package/dist/server/workspaces/imagePreviewService.js.map +0 -1
- package/dist/server/workspaces/workspaceService.js +0 -52
- package/dist/server/workspaces/workspaceService.js.map +0 -1
- package/dist/shared/apiTypes.d.ts +0 -1234
- package/dist/shared/thinkingLevels.d.ts +0 -27
- package/plugin-api/unstable.d.ts +0 -1
|
@@ -16,8 +16,6 @@ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
|
16
16
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
17
17
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
18
18
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
19
|
-
import { countOutOfListingChildren, locateOutOfListingParents } from "./parentSessionLocator.js";
|
|
20
|
-
import { siblingWorkspaceCwds } from "../workspaces/projectWorkspaceCwds.js";
|
|
21
19
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
22
20
|
import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
|
|
23
21
|
import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
|
|
@@ -56,20 +54,44 @@ function spawnTargetError(decision) {
|
|
|
56
54
|
return new Error("Spawning session is not in a registered project");
|
|
57
55
|
return new Error(`cwd must be a workspace of this project. Allowed: ${decision.allowedCwds.join(", ")}`);
|
|
58
56
|
}
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Tracked subsessions are worktree-scoped, so a requested target other than the
|
|
59
|
+
* parent's own cwd fails closed instead of being silently retargeted. The
|
|
60
|
+
* message names the rule and both supported ways to get work done elsewhere.
|
|
61
|
+
*/
|
|
62
|
+
function subsessionCwdError(spawningCwd, requestedCwd) {
|
|
63
|
+
return new Error(`A tracked subsession runs in this session's working directory (${spawningCwd}); ${requestedCwd} was requested. Instruct the child to work elsewhere from this workspace, or use spawn_session for an independent session in another workspace.`);
|
|
61
64
|
}
|
|
62
|
-
function
|
|
63
|
-
return
|
|
65
|
+
function modelSpecOf(model) {
|
|
66
|
+
return `${model.provider}/${model.id}`;
|
|
64
67
|
}
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Parse a strict `provider/model-id` spec: split on the first `/` (model ids
|
|
70
|
+
* may themselves contain `/`) and require both parts to be non-empty.
|
|
71
|
+
*/
|
|
72
|
+
function parseModelSpec(spec) {
|
|
73
|
+
const slash = spec.indexOf("/");
|
|
74
|
+
if (slash <= 0 || slash === spec.length - 1)
|
|
75
|
+
return undefined;
|
|
76
|
+
return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
|
|
67
77
|
}
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Error for a spawn-tool model spec that matched nothing. States the facts —
|
|
80
|
+
* the bad spec and the required format — with deliberately no model list
|
|
81
|
+
* (a list would invite guesses). The agent loop turns the throw into an
|
|
82
|
+
* error tool result; how to recover is the agent's call.
|
|
83
|
+
*/
|
|
84
|
+
function unknownSpawnModelError(modelSpec) {
|
|
85
|
+
return new Error(`Unknown model "${modelSpec}". Pass an exact "provider/model-id".`);
|
|
70
86
|
}
|
|
71
|
-
function
|
|
72
|
-
return
|
|
87
|
+
function authLossWarningKey(sessionId, provider, modelId) {
|
|
88
|
+
return `${sessionId}:${provider}/${modelId}`;
|
|
89
|
+
}
|
|
90
|
+
function refMatchesActiveSession(ref, active) {
|
|
91
|
+
return cwdPathsEqual(active.runtime.cwd, ref.cwd);
|
|
92
|
+
}
|
|
93
|
+
function refMatchesStartupSession(ref, session) {
|
|
94
|
+
return cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
|
|
73
95
|
}
|
|
74
96
|
function sessionTreeNavigationOptions(request) {
|
|
75
97
|
switch (request.summary.mode) {
|
|
@@ -220,26 +242,31 @@ export function dismissSessionWarning(session, dismissId) {
|
|
|
220
242
|
function defaultCreateAgentRuntime(createRuntime, options) {
|
|
221
243
|
if (!(options.sessionManager instanceof SessionManager))
|
|
222
244
|
throw new Error("Default runtime creation requires an SDK SessionManager");
|
|
223
|
-
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.delegationToolsEnabled);
|
|
245
|
+
const runtimeFactory = createRuntimeWithOneShotSessionOptions(createRuntime, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
|
|
224
246
|
return createAgentSessionRuntime(runtimeFactory, {
|
|
225
247
|
cwd: options.cwd,
|
|
226
248
|
agentDir: options.agentDir,
|
|
227
249
|
sessionManager: options.sessionManager,
|
|
228
250
|
});
|
|
229
251
|
}
|
|
230
|
-
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, delegationToolsEnabled) {
|
|
252
|
+
function createRuntimeWithOneShotSessionOptions(createRuntime, initialModel, initialThinkingLevel, delegationToolsEnabled) {
|
|
231
253
|
// These inputs belong only to the session being opened. A later runtime
|
|
232
|
-
// replacement resolves its own model and delegation capability
|
|
254
|
+
// replacement resolves its own model and delegation capability, and restores
|
|
255
|
+
// the thinking level from the existing session file.
|
|
233
256
|
let pendingInitialModel = initialModel;
|
|
257
|
+
let pendingInitialThinkingLevel = initialThinkingLevel;
|
|
234
258
|
let pendingDelegationToolsEnabled = delegationToolsEnabled;
|
|
235
259
|
return async (options) => {
|
|
236
260
|
const model = pendingInitialModel;
|
|
261
|
+
const thinkingLevel = pendingInitialThinkingLevel;
|
|
237
262
|
const toolsEnabled = pendingDelegationToolsEnabled;
|
|
238
263
|
pendingInitialModel = undefined;
|
|
264
|
+
pendingInitialThinkingLevel = undefined;
|
|
239
265
|
pendingDelegationToolsEnabled = undefined;
|
|
240
266
|
return createRuntime({
|
|
241
267
|
...options,
|
|
242
268
|
...(model === undefined ? {} : { initialModel: model }),
|
|
269
|
+
...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
|
|
243
270
|
...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
|
|
244
271
|
});
|
|
245
272
|
};
|
|
@@ -254,9 +281,28 @@ export function createPiWebCustomToolDefinitions(cwd, delegationEnabled, spawn,
|
|
|
254
281
|
...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
|
|
255
282
|
];
|
|
256
283
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Resource-loader options that append PI WEB's own system-prompt sections.
|
|
286
|
+
*
|
|
287
|
+
* `appendSystemPromptOverride` composes with what the loader already resolved,
|
|
288
|
+
* so the operator's `SYSTEM.md` / `APPEND_SYSTEM.md` files keep their content
|
|
289
|
+
* and PI WEB's sections land after them. Returns `undefined` when there is
|
|
290
|
+
* nothing to add, leaving the loader exactly as pi configures it.
|
|
291
|
+
*/
|
|
292
|
+
export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
293
|
+
if (appendSystemPromptSections.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
296
|
+
}
|
|
297
|
+
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
298
|
+
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
299
|
+
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
300
|
+
const services = await createAgentSessionServices({
|
|
301
|
+
cwd,
|
|
302
|
+
agentDir,
|
|
303
|
+
modelRuntime,
|
|
304
|
+
...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
|
|
305
|
+
});
|
|
260
306
|
const resolvedDelegationToolsEnabled = delegationToolsEnabled
|
|
261
307
|
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
|
|
262
308
|
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions, askUser);
|
|
@@ -266,6 +312,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
266
312
|
customTools,
|
|
267
313
|
...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
|
|
268
314
|
...(initialModel === undefined ? {} : { model: initialModel }),
|
|
315
|
+
...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
|
|
269
316
|
});
|
|
270
317
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
271
318
|
};
|
|
@@ -325,8 +372,6 @@ export class PiSessionService {
|
|
|
325
372
|
this.subsessionLinks = new Map();
|
|
326
373
|
/** Parent id/file identities whose persisted links have already been loaded. */
|
|
327
374
|
this.subsessionHydratedParents = new Set();
|
|
328
|
-
/** Session file path -> its parsed header. Headers are written once, so successful reads are cached. */
|
|
329
|
-
this.sessionHeaderCache = new Map();
|
|
330
375
|
/**
|
|
331
376
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
332
377
|
* Armed when the child starts working; firing on completion disarms it so a
|
|
@@ -339,37 +384,23 @@ export class PiSessionService {
|
|
|
339
384
|
this.pendingUnreadMutations = [];
|
|
340
385
|
this.unreadPublicationFlushRequested = false;
|
|
341
386
|
this.unreadPublicationStopped = false;
|
|
342
|
-
/**
|
|
343
|
-
* Read a session file header, memoized per path. Pi writes the header once at
|
|
344
|
-
* session creation, so a successful read stays valid for the process lifetime;
|
|
345
|
-
* failures are not cached so a session file that appears later is picked up.
|
|
346
|
-
*/
|
|
347
|
-
this.readCachedSessionHeader = async (sessionFile) => {
|
|
348
|
-
const cached = this.sessionHeaderCache.get(sessionFile);
|
|
349
|
-
if (cached !== undefined)
|
|
350
|
-
return cached;
|
|
351
|
-
const header = await readSessionHeaderSummary(sessionFile);
|
|
352
|
-
if (header !== undefined)
|
|
353
|
-
this.sessionHeaderCache.set(sessionFile, header);
|
|
354
|
-
return header;
|
|
355
|
-
};
|
|
356
387
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
357
388
|
this.agentDir = deps.agentDir;
|
|
358
389
|
this.sessionManager = deps.sessionManager;
|
|
359
390
|
this.modelRuntime = deps.modelRuntime;
|
|
360
391
|
this.spawnTargets = deps.spawnTargets;
|
|
361
|
-
this.projectWorkspaces = deps.projectWorkspaces;
|
|
362
392
|
this.logger = deps.logger ?? noopLogger;
|
|
363
393
|
this.now = deps.now ?? (() => new Date());
|
|
364
394
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
365
395
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
396
|
+
this.onUnreadChanged = deps.onUnreadChanged;
|
|
366
397
|
this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
|
|
367
398
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
368
399
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
369
400
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
370
401
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
371
402
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
372
|
-
// Subsessions are
|
|
403
|
+
// Subsessions are gated behind their own flag, and they
|
|
373
404
|
// also require the spawn capability (they share its project-scope resolver).
|
|
374
405
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
375
406
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
@@ -377,11 +408,11 @@ export class PiSessionService {
|
|
|
377
408
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
378
409
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
379
410
|
read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
|
|
380
|
-
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
|
|
411
|
+
}, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.appendSystemPromptSections ?? []);
|
|
381
412
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
382
413
|
this.workspaceActivity = deps.workspaceActivity;
|
|
383
414
|
this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
|
|
384
|
-
this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
415
|
+
this.commandService = new SessionCommandService((sessionId) => this.getActive(this.activeSessionRef(sessionId)), (sessionId, text) => this.prompt(this.activeSessionRef(sessionId), text, undefined, undefined, { echoUserMessage: false }), events, {
|
|
385
416
|
onCompactionStart: (session) => {
|
|
386
417
|
this.beginSessionEntryMutation(session, "compact the session");
|
|
387
418
|
this.publishActivity(session, "compacting", "active");
|
|
@@ -465,7 +496,6 @@ export class PiSessionService {
|
|
|
465
496
|
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
466
497
|
readyDeleteRecords.push(record);
|
|
467
498
|
}
|
|
468
|
-
await this.ensureArchivedRecordsMoved(readyDeleteRecords);
|
|
469
499
|
const deletedSessionIds = new Set(await this.archiveStoreDeleteArchivedMany(readyDeleteRecords.map((record) => record.sessionId)));
|
|
470
500
|
deleteRecords.push(...readyDeleteRecords.filter((record) => deletedSessionIds.has(record.sessionId)));
|
|
471
501
|
await this.forgetUnreadSessions(deleteRecords);
|
|
@@ -506,7 +536,6 @@ export class PiSessionService {
|
|
|
506
536
|
this.subsessionLinks.clear();
|
|
507
537
|
this.subsessionHydratedParents.clear();
|
|
508
538
|
this.subsessionNotifyArmed.clear();
|
|
509
|
-
this.sessionHeaderCache.clear();
|
|
510
539
|
this.notificationStore.clearAll("service-dispose");
|
|
511
540
|
await Promise.all(activeSessions.map(async (active) => {
|
|
512
541
|
active.unsubscribe();
|
|
@@ -524,9 +553,7 @@ export class PiSessionService {
|
|
|
524
553
|
async list(cwd) {
|
|
525
554
|
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
|
|
526
555
|
const sessionsById = new Map(sessions.map((session) => [session.id, session]));
|
|
527
|
-
const archivedForCwd =
|
|
528
|
-
.filter((record) => record.cwd === cwd)
|
|
529
|
-
.map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
|
|
556
|
+
const archivedForCwd = archivedRecords.filter((record) => record.cwd === cwd);
|
|
530
557
|
const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
|
|
531
558
|
for (const record of archivedForCwd) {
|
|
532
559
|
this.publishNotificationMutations(this.notificationStore.clearSession(record.sessionId, "archive-reconcile"));
|
|
@@ -539,63 +566,7 @@ export class PiSessionService {
|
|
|
539
566
|
.sort(compareArchivedRecords)
|
|
540
567
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
541
568
|
.filter(isDefined);
|
|
542
|
-
return
|
|
543
|
-
}
|
|
544
|
-
/**
|
|
545
|
-
* Annotate a listing with the session relationships that cross workspace
|
|
546
|
-
* boundaries: where an out-of-listing parent lives, and how many children a
|
|
547
|
-
* listed session has in sibling workspaces.
|
|
548
|
-
*
|
|
549
|
-
* Both directions are best-effort. An unreadable parent header or a sibling
|
|
550
|
-
* workspace that cannot be listed leaves the session unannotated rather than
|
|
551
|
-
* failing the listing, and the browser falls back to its generic states.
|
|
552
|
-
*/
|
|
553
|
-
async withRelatedSessionsElsewhere(sessions, cwd) {
|
|
554
|
-
const [parentLocations, childCounts] = await Promise.all([
|
|
555
|
-
locateOutOfListingParents(sessions, cwd, this.readCachedSessionHeader),
|
|
556
|
-
this.countChildrenInSiblingWorkspaces(sessions, cwd),
|
|
557
|
-
]);
|
|
558
|
-
return sessions.map((session) => {
|
|
559
|
-
const parent = session.parentSessionPath === undefined ? undefined : parentLocations.get(session.parentSessionPath);
|
|
560
|
-
const childrenElsewhere = childCounts.get(session.path);
|
|
561
|
-
if (parent === undefined && childrenElsewhere === undefined)
|
|
562
|
-
return session;
|
|
563
|
-
const annotated = { ...session };
|
|
564
|
-
if (parent !== undefined) {
|
|
565
|
-
annotated.parentSessionId = parent.parentSessionId;
|
|
566
|
-
annotated.parentSessionCwd = parent.parentSessionCwd;
|
|
567
|
-
}
|
|
568
|
-
if (childrenElsewhere !== undefined)
|
|
569
|
-
annotated.childSessionsElsewhere = childrenElsewhere;
|
|
570
|
-
return annotated;
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
/**
|
|
574
|
-
* Count children of the listed sessions that live in other workspaces of the
|
|
575
|
-
* same project.
|
|
576
|
-
*
|
|
577
|
-
* Only sibling workspaces are scanned: agents may only spawn into workspaces
|
|
578
|
-
* of the spawning session's own project, so that bounds where a child can be.
|
|
579
|
-
* Listing is skipped entirely when no project-workspace locator is configured
|
|
580
|
-
* or the cwd belongs to no registered project.
|
|
581
|
-
*/
|
|
582
|
-
async countChildrenInSiblingWorkspaces(sessions, cwd) {
|
|
583
|
-
if (this.projectWorkspaces === undefined || sessions.length === 0)
|
|
584
|
-
return new Map();
|
|
585
|
-
try {
|
|
586
|
-
const siblingCwds = await siblingWorkspaceCwds(this.projectWorkspaces, cwd);
|
|
587
|
-
if (siblingCwds.length === 0)
|
|
588
|
-
return new Map();
|
|
589
|
-
const listings = await Promise.all(siblingCwds.map(async (siblingCwd) => {
|
|
590
|
-
const entries = await this.sessionManager.list(siblingCwd);
|
|
591
|
-
return entries.flatMap((entry) => entry.parentSessionPath === undefined ? [] : [entry.parentSessionPath]);
|
|
592
|
-
}));
|
|
593
|
-
return countOutOfListingChildren(sessions, listings.flat());
|
|
594
|
-
}
|
|
595
|
-
catch (error) {
|
|
596
|
-
this.logger.info({ cwd, error: error instanceof Error ? error.message : String(error) }, "failed to count child sessions in sibling workspaces");
|
|
597
|
-
return new Map();
|
|
598
|
-
}
|
|
569
|
+
return [...unarchivedSessions, ...archivedSessions];
|
|
599
570
|
}
|
|
600
571
|
async start(cwd, options = {}) {
|
|
601
572
|
return this.startSession(cwd, options);
|
|
@@ -605,6 +576,7 @@ export class PiSessionService {
|
|
|
605
576
|
startupIntent: "create",
|
|
606
577
|
...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
|
|
607
578
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
579
|
+
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
608
580
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
609
581
|
});
|
|
610
582
|
const { session } = active.runtime;
|
|
@@ -637,28 +609,55 @@ export class PiSessionService {
|
|
|
637
609
|
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
|
|
638
610
|
if (!decision.allowed)
|
|
639
611
|
throw spawnTargetError(decision);
|
|
640
|
-
|
|
641
|
-
|
|
612
|
+
// A model spec overrides the inherited model. Only a spec resolves
|
|
613
|
+
// against the spawning session; the default path must not depend on it.
|
|
614
|
+
const model = input.modelSpec === undefined
|
|
615
|
+
? input.model
|
|
616
|
+
: await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec);
|
|
617
|
+
const created = await this.start(decision.cwd, {
|
|
618
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
619
|
+
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
620
|
+
});
|
|
621
|
+
const modelUsed = this.active.get(created.id)?.runtime.session.model;
|
|
622
|
+
await this.prompt(created, input.prompt);
|
|
642
623
|
this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
|
|
643
|
-
return {
|
|
624
|
+
return {
|
|
625
|
+
sessionId: created.id,
|
|
626
|
+
cwd: decision.cwd,
|
|
627
|
+
...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
|
|
628
|
+
};
|
|
644
629
|
}
|
|
645
630
|
/**
|
|
646
|
-
* Start a *tracked* child session on behalf of a LLM.
|
|
647
|
-
* {@link spawnSession}
|
|
648
|
-
*
|
|
649
|
-
*
|
|
631
|
+
* Start a *tracked* child session on behalf of a LLM. Unlike
|
|
632
|
+
* {@link spawnSession}, a tracked child always runs in the parent's own
|
|
633
|
+
* workspace: parent/child trees are worktree-scoped, so a child elsewhere
|
|
634
|
+
* would be invisible to the parent's listing. The child records its parent
|
|
635
|
+
* (so it shows in the session tree) and is registered so the parent is
|
|
636
|
+
* notified when it stops working and can inspect it later.
|
|
650
637
|
*/
|
|
651
638
|
async spawnSubsession(input) {
|
|
652
639
|
if (this.spawnTargets === undefined)
|
|
653
640
|
throw new Error("Spawning sessions is disabled");
|
|
654
|
-
|
|
641
|
+
if (input.cwd !== undefined && input.cwd !== "" && !cwdPathsEqual(input.cwd, input.spawningCwd)) {
|
|
642
|
+
throw subsessionCwdError(input.spawningCwd, input.cwd);
|
|
643
|
+
}
|
|
644
|
+
// Resolved against the parent's own cwd only: this still refuses spawning
|
|
645
|
+
// from an unregistered directory, which keeps the child visible in the UI.
|
|
646
|
+
const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, undefined);
|
|
655
647
|
if (!decision.allowed)
|
|
656
648
|
throw spawnTargetError(decision);
|
|
649
|
+
// A model spec overrides the inherited model and is resolved against the
|
|
650
|
+
// parent's model runtime; only a spec resolves against the parent.
|
|
651
|
+
const model = input.modelSpec === undefined
|
|
652
|
+
? input.model
|
|
653
|
+
: await this.resolveSpawnModel({ id: input.parentSessionId, cwd: input.spawningCwd }, input.modelSpec);
|
|
657
654
|
const created = await this.startSession(decision.cwd, {
|
|
658
655
|
...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
|
|
659
|
-
...(
|
|
656
|
+
...(model === undefined ? {} : { initialModel: model }),
|
|
657
|
+
...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
|
|
660
658
|
creationProvenance: "tracked-subsession",
|
|
661
659
|
});
|
|
660
|
+
const modelUsed = this.active.get(created.id)?.runtime.session.model;
|
|
662
661
|
const parentSessionFile = nonEmptyString(input.parentSessionFile);
|
|
663
662
|
const link = {
|
|
664
663
|
parentSessionId: input.parentSessionId,
|
|
@@ -670,9 +669,45 @@ export class PiSessionService {
|
|
|
670
669
|
await this.registerVerifiedSubsession(link);
|
|
671
670
|
this.persistSubsessionLink(link);
|
|
672
671
|
this.persistSubsessionChildMarker(input.parentSessionId, created.id);
|
|
673
|
-
await this.prompt(created
|
|
672
|
+
await this.prompt(created, input.prompt);
|
|
674
673
|
this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
|
|
675
|
-
return {
|
|
674
|
+
return {
|
|
675
|
+
sessionId: created.id,
|
|
676
|
+
cwd: decision.cwd,
|
|
677
|
+
...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* The models a session may pick from: its scoped set when model-scoped,
|
|
682
|
+
* otherwise the runtime's available snapshot. Refreshes the runtime catalog
|
|
683
|
+
* first so callers see newly configured providers and models. The refresh
|
|
684
|
+
* stays local (`allowNetwork: false`); network refreshes belong to the
|
|
685
|
+
* bounded background catalog refresher, not this request path.
|
|
686
|
+
*/
|
|
687
|
+
async sessionModelCandidates(session) {
|
|
688
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
689
|
+
return session.scopedModels.length > 0
|
|
690
|
+
? session.scopedModels.map((scoped) => scoped.model)
|
|
691
|
+
: session.modelRuntime.getAvailableSnapshot();
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Resolve a strict `provider/model-id` spec from a spawn tool against the
|
|
695
|
+
* *spawning* session's model runtime, using the same candidates
|
|
696
|
+
* {@link setModel} offers plus a direct runtime lookup as fallback. Unknown
|
|
697
|
+
* or malformed specs throw; the agent loop turns that into an error tool
|
|
698
|
+
* result the spawning agent can retry from.
|
|
699
|
+
*/
|
|
700
|
+
async resolveSpawnModel(spawningRef, modelSpec) {
|
|
701
|
+
const session = await this.getOrOpen(spawningRef);
|
|
702
|
+
const parsed = parseModelSpec(modelSpec);
|
|
703
|
+
const candidates = await this.sessionModelCandidates(session);
|
|
704
|
+
const model = parsed === undefined
|
|
705
|
+
? undefined
|
|
706
|
+
: candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
|
|
707
|
+
?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
|
|
708
|
+
if (model === undefined)
|
|
709
|
+
throw unknownSpawnModelError(modelSpec);
|
|
710
|
+
return model;
|
|
676
711
|
}
|
|
677
712
|
/**
|
|
678
713
|
* Register the question set an agent wants the user to answer as the session's
|
|
@@ -1240,26 +1275,20 @@ export class PiSessionService {
|
|
|
1240
1275
|
const streamingMessage = session.state.streamingMessage;
|
|
1241
1276
|
const partial = streamingMessage === undefined || streamingMessage === null
|
|
1242
1277
|
? null
|
|
1243
|
-
: projectBrowserMessage(streamingMessage);
|
|
1278
|
+
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1244
1279
|
return { seq, partial };
|
|
1245
1280
|
}
|
|
1246
1281
|
async availableModels(ref) {
|
|
1247
1282
|
const session = await this.getOrOpen(ref);
|
|
1248
|
-
await
|
|
1249
|
-
const models = session.scopedModels.length > 0
|
|
1250
|
-
? session.scopedModels.map((scoped) => scoped.model)
|
|
1251
|
-
: session.modelRuntime.getAvailableSnapshot();
|
|
1283
|
+
const models = await this.sessionModelCandidates(session);
|
|
1252
1284
|
return models.map(modelToClientModel);
|
|
1253
1285
|
}
|
|
1254
1286
|
async setModel(ref, provider, modelId) {
|
|
1255
1287
|
await this.assertWritable(ref);
|
|
1256
1288
|
const session = await this.getOrOpen(ref);
|
|
1257
1289
|
this.assertTreeNavigationInactive(session, "change models");
|
|
1258
|
-
await
|
|
1290
|
+
const candidates = await this.sessionModelCandidates(session);
|
|
1259
1291
|
this.assertTreeNavigationInactive(session, "change models");
|
|
1260
|
-
const candidates = session.scopedModels.length > 0
|
|
1261
|
-
? session.scopedModels.map((scoped) => scoped.model)
|
|
1262
|
-
: session.modelRuntime.getAvailableSnapshot();
|
|
1263
1292
|
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
|
|
1264
1293
|
?? session.modelRuntime.getModel(provider, modelId);
|
|
1265
1294
|
if (model === undefined)
|
|
@@ -1432,7 +1461,7 @@ export class PiSessionService {
|
|
|
1432
1461
|
async navigateTree(ref, request) {
|
|
1433
1462
|
if (request.targetId.trim() === "")
|
|
1434
1463
|
throw new Error("Session tree target is required");
|
|
1435
|
-
if (this.isTreeExclusiveSessionIdentityActive(
|
|
1464
|
+
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
1436
1465
|
throw new Error("Stop current session activity before navigating the session tree");
|
|
1437
1466
|
}
|
|
1438
1467
|
await this.assertWritable(ref);
|
|
@@ -1482,6 +1511,83 @@ export class PiSessionService {
|
|
|
1482
1511
|
}
|
|
1483
1512
|
}
|
|
1484
1513
|
}
|
|
1514
|
+
/**
|
|
1515
|
+
* Fork the session from one entry of its tree into a new session file,
|
|
1516
|
+
* leaving the original session untouched. The forked runtime replaces the
|
|
1517
|
+
* current one, so the outcome is reported for the session the client is
|
|
1518
|
+
* about to join rather than the forked-from record.
|
|
1519
|
+
*/
|
|
1520
|
+
async forkFromTree(ref, request) {
|
|
1521
|
+
if (request.entryId.trim() === "")
|
|
1522
|
+
throw new Error("Session tree entry is required");
|
|
1523
|
+
if (this.isTreeExclusiveSessionIdentityActive(ref.id)) {
|
|
1524
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1525
|
+
}
|
|
1526
|
+
await this.assertWritable(ref);
|
|
1527
|
+
const session = await this.getOrOpen(ref);
|
|
1528
|
+
if (this.hasActiveWork(session))
|
|
1529
|
+
throw new Error("Stop current session activity before forking the session tree");
|
|
1530
|
+
if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
|
|
1531
|
+
throw new Error("The session changed since /tree was opened. Reopen /tree and try again.");
|
|
1532
|
+
}
|
|
1533
|
+
this.publishActivity(session, "forking session from entry", "active");
|
|
1534
|
+
this.publishStatus(session);
|
|
1535
|
+
try {
|
|
1536
|
+
const result = await this.commandService.forkEntry(session.sessionId, request.entryId, {
|
|
1537
|
+
expectedLeafId: request.expectedLeafId,
|
|
1538
|
+
});
|
|
1539
|
+
if (result.type === "unsupported")
|
|
1540
|
+
throw new Error(result.message);
|
|
1541
|
+
if (result.type !== "done")
|
|
1542
|
+
throw new Error("Session fork is unavailable");
|
|
1543
|
+
if (result.session === undefined) {
|
|
1544
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1545
|
+
this.publishActivity(session, "fork cancelled", "idle");
|
|
1546
|
+
this.publishStatus(session);
|
|
1547
|
+
}
|
|
1548
|
+
return { cancelled: true };
|
|
1549
|
+
}
|
|
1550
|
+
const forkedSession = this.active.get(result.session.id)?.runtime.session;
|
|
1551
|
+
if (forkedSession !== undefined && this.isCurrentActiveSession(forkedSession)) {
|
|
1552
|
+
this.publishActivity(forkedSession, "session forked", "idle");
|
|
1553
|
+
this.publishStatus(forkedSession);
|
|
1554
|
+
}
|
|
1555
|
+
if (result.session.id !== session.sessionId)
|
|
1556
|
+
this.clearSupersededSessionActivity(session);
|
|
1557
|
+
return {
|
|
1558
|
+
cancelled: false,
|
|
1559
|
+
session: result.session,
|
|
1560
|
+
...(result.promptDraft === undefined ? {} : { promptDraft: result.promptDraft }),
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
catch (error) {
|
|
1564
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1565
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1566
|
+
this.publishActivity(session, "fork failed", "error", message);
|
|
1567
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1568
|
+
this.publishStatus(session);
|
|
1569
|
+
}
|
|
1570
|
+
throw error;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* A changed-id fork can rebind its runtime after a heartbeat published the
|
|
1575
|
+
* prior identity as active. Clear every observable owner before forgetting
|
|
1576
|
+
* that identity's local activity record.
|
|
1577
|
+
*/
|
|
1578
|
+
clearSupersededSessionActivity(session) {
|
|
1579
|
+
const sessionId = session.sessionId;
|
|
1580
|
+
if (this.activities.get(sessionId)?.phase === "active") {
|
|
1581
|
+
const at = new Date().toISOString();
|
|
1582
|
+
const stored = { phase: "idle", label: "idle", at };
|
|
1583
|
+
this.activities.set(sessionId, stored);
|
|
1584
|
+
const activity = { sessionId, ...stored };
|
|
1585
|
+
this.events.publish(sessionId, { type: "activity.update", activity });
|
|
1586
|
+
this.events.publishGlobal({ type: "activity.update", activity });
|
|
1587
|
+
}
|
|
1588
|
+
this.workspaceActivity?.removeSession(sessionId, session.sessionManager.getCwd());
|
|
1589
|
+
this.activities.delete(sessionId);
|
|
1590
|
+
}
|
|
1485
1591
|
async reloadSessionRuntime(session) {
|
|
1486
1592
|
if (this.hasActiveWork(session))
|
|
1487
1593
|
throw new Error("Stop current session activity before reloading");
|
|
@@ -1531,7 +1637,7 @@ export class PiSessionService {
|
|
|
1531
1637
|
const uniqueRefs = uniqueBulkSessionRefs(refs);
|
|
1532
1638
|
const [archivedRecords, sessionContext] = await Promise.all([
|
|
1533
1639
|
this.archiveStore.list(),
|
|
1534
|
-
this.
|
|
1640
|
+
this.bulkSessionRefContext(uniqueRefs),
|
|
1535
1641
|
]);
|
|
1536
1642
|
const failures = [];
|
|
1537
1643
|
const alreadyArchivedSessionIds = [];
|
|
@@ -1545,7 +1651,7 @@ export class PiSessionService {
|
|
|
1545
1651
|
unreadArchivedIdentities.push(archived);
|
|
1546
1652
|
continue;
|
|
1547
1653
|
}
|
|
1548
|
-
const active = this.
|
|
1654
|
+
const active = this.activeForRef(bulkRefToSessionRef(ref));
|
|
1549
1655
|
const listed = findListedSessionForBulkRef(sessionContext, ref);
|
|
1550
1656
|
const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
|
|
1551
1657
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
@@ -1569,7 +1675,7 @@ export class PiSessionService {
|
|
|
1569
1675
|
}
|
|
1570
1676
|
const readyPlanItems = [];
|
|
1571
1677
|
for (const item of planItems) {
|
|
1572
|
-
const active = this.
|
|
1678
|
+
const active = this.activeForRef({ id: item.input.sessionId, cwd: item.input.cwd });
|
|
1573
1679
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1574
1680
|
failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
|
|
1575
1681
|
continue;
|
|
@@ -1646,18 +1752,6 @@ export class PiSessionService {
|
|
|
1646
1752
|
await this.archiveStore.restore(archived.sessionId);
|
|
1647
1753
|
await this.forgetUnreadSessions([archived]);
|
|
1648
1754
|
}
|
|
1649
|
-
async deleteArchived(ref) {
|
|
1650
|
-
const record = await this.getArchived(ref);
|
|
1651
|
-
if (record === undefined)
|
|
1652
|
-
throw new Error("Archived session not found");
|
|
1653
|
-
if (this.archiveStore.deleteArchived === undefined)
|
|
1654
|
-
throw new Error("Archive store does not support deletion");
|
|
1655
|
-
await this.closeActive(record.sessionId, { kind: "clear", reason: "delete" });
|
|
1656
|
-
if (record.archivePath === undefined)
|
|
1657
|
-
await this.ensureArchivedRecordMoved(record);
|
|
1658
|
-
await this.archiveStore.deleteArchived(record.sessionId);
|
|
1659
|
-
await this.forgetUnreadSessions([record]);
|
|
1660
|
-
}
|
|
1661
1755
|
async deleteArchivedMany(refs) {
|
|
1662
1756
|
if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
|
|
1663
1757
|
throw new Error("Archive store does not support deletion");
|
|
@@ -1671,7 +1765,7 @@ export class PiSessionService {
|
|
|
1671
1765
|
failures.push({ sessionId: ref.id, error: "Archived session not found" });
|
|
1672
1766
|
continue;
|
|
1673
1767
|
}
|
|
1674
|
-
const active = this.
|
|
1768
|
+
const active = this.activeForRef({ id: record.sessionId, cwd: record.cwd });
|
|
1675
1769
|
if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
|
|
1676
1770
|
failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
|
|
1677
1771
|
continue;
|
|
@@ -1688,12 +1782,7 @@ export class PiSessionService {
|
|
|
1688
1782
|
failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
|
|
1689
1783
|
}
|
|
1690
1784
|
}
|
|
1691
|
-
const
|
|
1692
|
-
failures.push(...moveFailures);
|
|
1693
|
-
const moveFailureIds = new Set(moveFailures.map((failure) => failure.sessionId));
|
|
1694
|
-
const deleteIds = readyRecords
|
|
1695
|
-
.map((record) => record.sessionId)
|
|
1696
|
-
.filter((sessionId) => !moveFailureIds.has(sessionId));
|
|
1785
|
+
const deleteIds = readyRecords.map((record) => record.sessionId);
|
|
1697
1786
|
let deletedSessionIds = [];
|
|
1698
1787
|
try {
|
|
1699
1788
|
deletedSessionIds = await this.archiveStoreDeleteArchivedMany(deleteIds);
|
|
@@ -1746,6 +1835,11 @@ export class PiSessionService {
|
|
|
1746
1835
|
if (sessionFile === undefined || sessionFile === "")
|
|
1747
1836
|
throw new Error("Session is not persisted");
|
|
1748
1837
|
await clearParentSession(sessionFile);
|
|
1838
|
+
// The header rewrite keeps the inode, and whenever it leaves the file's
|
|
1839
|
+
// size unchanged it is invisible to the gateway's summary memo, which
|
|
1840
|
+
// cannot detect such rewrites from identity + size alone and would keep
|
|
1841
|
+
// listing the old parent link until restart.
|
|
1842
|
+
this.sessionManager.invalidateSessionFile(sessionFile);
|
|
1749
1843
|
clearParentSessionHeader(session.sessionManager);
|
|
1750
1844
|
this.unregisterSubsession(session.sessionId);
|
|
1751
1845
|
await this.forgetUnreadSessions([{ sessionId: session.sessionId, cwd: session.sessionManager.getCwd() }]);
|
|
@@ -1765,7 +1859,7 @@ export class PiSessionService {
|
|
|
1765
1859
|
return this.statusFromSession(session);
|
|
1766
1860
|
}
|
|
1767
1861
|
async abort(ref) {
|
|
1768
|
-
const active = this.
|
|
1862
|
+
const active = this.activeForRef(ref);
|
|
1769
1863
|
if (active === undefined)
|
|
1770
1864
|
return;
|
|
1771
1865
|
const sessionId = active.runtime.session.sessionId;
|
|
@@ -1791,31 +1885,26 @@ export class PiSessionService {
|
|
|
1791
1885
|
}
|
|
1792
1886
|
}
|
|
1793
1887
|
async stop(ref) {
|
|
1794
|
-
const active = this.
|
|
1888
|
+
const active = this.activeForRef(ref);
|
|
1795
1889
|
if (active !== undefined) {
|
|
1796
1890
|
await this.closeActive(active.runtime.session.sessionId);
|
|
1797
1891
|
return;
|
|
1798
1892
|
}
|
|
1799
|
-
|
|
1800
|
-
|
|
1893
|
+
// A session whose open is parked (e.g. on a session_start dialog) is not
|
|
1894
|
+
// active yet; close it through the same path so stopping cannot block
|
|
1895
|
+
// behind the dialog timeout.
|
|
1896
|
+
const startup = this.startupSessionForRef(ref);
|
|
1897
|
+
if (startup !== undefined) {
|
|
1898
|
+
await this.closeActive(startup.sessionId);
|
|
1801
1899
|
return;
|
|
1802
1900
|
}
|
|
1803
|
-
|
|
1901
|
+
this.publishNotificationMutations(this.notificationStore.clearSessionIdentity(ref.id, canonicalizeStoredCwd(ref.cwd), "runtime-close"));
|
|
1804
1902
|
}
|
|
1805
|
-
async
|
|
1903
|
+
async bulkSessionRefContext(refs) {
|
|
1806
1904
|
const cwdSet = new Set();
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
needsAllSessions = true;
|
|
1811
|
-
else
|
|
1812
|
-
cwdSet.add(ref.cwd);
|
|
1813
|
-
}
|
|
1814
|
-
const [sessionsByCwd, allSessions] = await Promise.all([
|
|
1815
|
-
this.listSessionsByCwd([...cwdSet]),
|
|
1816
|
-
needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
|
|
1817
|
-
]);
|
|
1818
|
-
return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
|
|
1905
|
+
for (const ref of refs)
|
|
1906
|
+
cwdSet.add(ref.cwd);
|
|
1907
|
+
return { sessionsByCwd: await this.listSessionsByCwd([...cwdSet]) };
|
|
1819
1908
|
}
|
|
1820
1909
|
async listSessionsByCwd(cwds) {
|
|
1821
1910
|
const uniqueCwds = uniqueStrings(cwds);
|
|
@@ -1843,36 +1932,8 @@ export class PiSessionService {
|
|
|
1843
1932
|
await this.archiveStore.deleteArchived(sessionId);
|
|
1844
1933
|
return [...sessionIds];
|
|
1845
1934
|
}
|
|
1846
|
-
async moveLegacyArchivedRecordsForDelete(records) {
|
|
1847
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1848
|
-
if (legacyRecords.length === 0)
|
|
1849
|
-
return [];
|
|
1850
|
-
let sessionsByCwd;
|
|
1851
|
-
try {
|
|
1852
|
-
sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1853
|
-
}
|
|
1854
|
-
catch (error) {
|
|
1855
|
-
return legacyRecords.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1856
|
-
}
|
|
1857
|
-
const moveInputs = legacyRecords
|
|
1858
|
-
.map((record) => findSessionByIdOrPrefix(sessionsByCwd.get(record.cwd) ?? [], record.sessionId))
|
|
1859
|
-
.filter(isDefined)
|
|
1860
|
-
.map(archiveInputFromListEntry);
|
|
1861
|
-
if (moveInputs.length === 0)
|
|
1862
|
-
return [];
|
|
1863
|
-
try {
|
|
1864
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
1865
|
-
return [];
|
|
1866
|
-
}
|
|
1867
|
-
catch (error) {
|
|
1868
|
-
const failedIds = new Set(moveInputs.map((input) => input.sessionId));
|
|
1869
|
-
return legacyRecords
|
|
1870
|
-
.filter((record) => failedIds.has(record.sessionId))
|
|
1871
|
-
.map((record) => ({ sessionId: record.sessionId, error: errorMessage(error) }));
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
1935
|
async cleanupPlan(request) {
|
|
1875
|
-
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll
|
|
1936
|
+
const [sessions, archivedRecords] = await Promise.all([this.sessionManager.listAll(), this.archiveStore.list()]);
|
|
1876
1937
|
return planSessionCleanup({
|
|
1877
1938
|
sessions,
|
|
1878
1939
|
archivedRecords,
|
|
@@ -1901,34 +1962,6 @@ export class PiSessionService {
|
|
|
1901
1962
|
}
|
|
1902
1963
|
return [...sessionIds];
|
|
1903
1964
|
}
|
|
1904
|
-
async ensureArchivedSessionMoved(record, session) {
|
|
1905
|
-
if (session === undefined || this.active.has(record.sessionId))
|
|
1906
|
-
return record;
|
|
1907
|
-
try {
|
|
1908
|
-
return await this.archiveStore.archive(archiveInputFromListEntry(session));
|
|
1909
|
-
}
|
|
1910
|
-
catch {
|
|
1911
|
-
return record;
|
|
1912
|
-
}
|
|
1913
|
-
}
|
|
1914
|
-
async ensureArchivedRecordMoved(record) {
|
|
1915
|
-
const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
|
|
1916
|
-
if (session === undefined)
|
|
1917
|
-
return record;
|
|
1918
|
-
const [moved] = await this.archiveStoreArchiveMany([archiveInputFromListEntry(session)]);
|
|
1919
|
-
return moved ?? record;
|
|
1920
|
-
}
|
|
1921
|
-
async ensureArchivedRecordsMoved(records) {
|
|
1922
|
-
const legacyRecords = records.filter((record) => record.archivePath === undefined);
|
|
1923
|
-
if (legacyRecords.length === 0)
|
|
1924
|
-
return;
|
|
1925
|
-
const sessionsByCwd = await this.listSessionsByCwd(legacyRecords.map((record) => record.cwd));
|
|
1926
|
-
const moveInputs = legacyRecords
|
|
1927
|
-
.map((record) => sessionsByCwd.get(record.cwd)?.find((candidate) => candidate.id === record.sessionId))
|
|
1928
|
-
.filter(isDefined)
|
|
1929
|
-
.map(archiveInputFromListEntry);
|
|
1930
|
-
await this.archiveStoreArchiveMany(moveInputs);
|
|
1931
|
-
}
|
|
1932
1965
|
async archiveInputForSession(session) {
|
|
1933
1966
|
const cwd = session.sessionManager.getCwd();
|
|
1934
1967
|
const sessionFile = session.sessionFile;
|
|
@@ -2065,7 +2098,7 @@ export class PiSessionService {
|
|
|
2065
2098
|
return (await this.getActive(ref)).runtime.session;
|
|
2066
2099
|
}
|
|
2067
2100
|
async getActive(ref, options = {}) {
|
|
2068
|
-
const active = this.
|
|
2101
|
+
const active = this.activeForRef(ref);
|
|
2069
2102
|
if (active !== undefined)
|
|
2070
2103
|
return active;
|
|
2071
2104
|
const archived = await this.getArchived(ref);
|
|
@@ -2073,15 +2106,17 @@ export class PiSessionService {
|
|
|
2073
2106
|
const { archivePath } = archived;
|
|
2074
2107
|
return this.openExistingSession(archived.sessionId, archived.cwd, () => this.sessionManager.open(archivePath), { notifications: "disabled" });
|
|
2075
2108
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2109
|
+
// Resolve the session file directly by id: opening one known session must
|
|
2110
|
+
// not depend on — or wait behind — a full transcript listing of its whole
|
|
2111
|
+
// workspace. `getActive` routes prompt/shell/runCommand, so coupling it to
|
|
2112
|
+
// the listing would let an in-flight listing serialize unrelated sends.
|
|
2113
|
+
const match = await this.sessionManager.resolveSessionFile(ref.cwd, ref.id);
|
|
2079
2114
|
if (!match)
|
|
2080
2115
|
throw new Error("Session not found");
|
|
2081
2116
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
2082
2117
|
}
|
|
2083
2118
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2084
|
-
const active = this.
|
|
2119
|
+
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2085
2120
|
if (active !== undefined)
|
|
2086
2121
|
return Promise.resolve(active);
|
|
2087
2122
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
@@ -2105,34 +2140,45 @@ export class PiSessionService {
|
|
|
2105
2140
|
.map((pending) => pending.promise);
|
|
2106
2141
|
}
|
|
2107
2142
|
async getArchived(ref) {
|
|
2108
|
-
const archived = await this.archiveStore.get(
|
|
2143
|
+
const archived = await this.archiveStore.get(ref.id);
|
|
2109
2144
|
if (archived === undefined)
|
|
2110
2145
|
return undefined;
|
|
2111
|
-
if (
|
|
2146
|
+
if (archived.cwd !== ref.cwd)
|
|
2112
2147
|
return undefined;
|
|
2113
2148
|
return archived;
|
|
2114
2149
|
}
|
|
2115
2150
|
isCurrentActiveSession(session) {
|
|
2116
2151
|
return this.active.get(session.sessionId)?.runtime.session === session;
|
|
2117
2152
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2153
|
+
/**
|
|
2154
|
+
* The command service tracks sessions by id alone; its callbacks only ever
|
|
2155
|
+
* run against a session the caller just resolved as active, so the cwd
|
|
2156
|
+
* needed for a full ref comes from that active runtime.
|
|
2157
|
+
*/
|
|
2158
|
+
activeSessionRef(sessionId) {
|
|
2159
|
+
const active = this.active.get(sessionId);
|
|
2160
|
+
if (active === undefined)
|
|
2161
|
+
throw new Error("Session not found");
|
|
2162
|
+
return { id: sessionId, cwd: active.runtime.cwd };
|
|
2163
|
+
}
|
|
2164
|
+
activeForRef(ref) {
|
|
2165
|
+
const sessionId = ref.id;
|
|
2120
2166
|
const exact = this.active.get(sessionId);
|
|
2121
|
-
if (exact !== undefined &&
|
|
2167
|
+
if (exact !== undefined && refMatchesActiveSession(ref, exact))
|
|
2122
2168
|
return exact;
|
|
2123
2169
|
for (const [candidateId, active] of this.active.entries()) {
|
|
2124
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2170
|
+
if (candidateId.startsWith(sessionId) && refMatchesActiveSession(ref, active))
|
|
2125
2171
|
return active;
|
|
2126
2172
|
}
|
|
2127
2173
|
return undefined;
|
|
2128
2174
|
}
|
|
2129
|
-
|
|
2130
|
-
const sessionId =
|
|
2175
|
+
startupSessionForRef(ref) {
|
|
2176
|
+
const sessionId = ref.id;
|
|
2131
2177
|
const exact = this.startupSessions.get(sessionId);
|
|
2132
|
-
if (exact !== undefined &&
|
|
2178
|
+
if (exact !== undefined && refMatchesStartupSession(ref, exact))
|
|
2133
2179
|
return exact;
|
|
2134
2180
|
for (const [candidateId, session] of this.startupSessions.entries()) {
|
|
2135
|
-
if (candidateId.startsWith(sessionId) &&
|
|
2181
|
+
if (candidateId.startsWith(sessionId) && refMatchesStartupSession(ref, session))
|
|
2136
2182
|
return session;
|
|
2137
2183
|
}
|
|
2138
2184
|
return undefined;
|
|
@@ -2144,7 +2190,7 @@ export class PiSessionService {
|
|
|
2144
2190
|
* its status projection).
|
|
2145
2191
|
*/
|
|
2146
2192
|
async sessionForStatusOrDialogClose(ref) {
|
|
2147
|
-
const reachable = this.
|
|
2193
|
+
const reachable = this.activeForRef(ref)?.runtime.session ?? this.startupSessionForRef(ref);
|
|
2148
2194
|
if (reachable !== undefined)
|
|
2149
2195
|
return reachable;
|
|
2150
2196
|
return this.getOrOpen(ref);
|
|
@@ -2175,6 +2221,7 @@ export class PiSessionService {
|
|
|
2175
2221
|
sessionManager,
|
|
2176
2222
|
delegationToolsEnabled,
|
|
2177
2223
|
...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
|
|
2224
|
+
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
2178
2225
|
});
|
|
2179
2226
|
const active = { runtime, unsubscribe: noop };
|
|
2180
2227
|
let boundSession = runtime.session;
|
|
@@ -2322,14 +2369,6 @@ export class PiSessionService {
|
|
|
2322
2369
|
}
|
|
2323
2370
|
const added = this.notificationStore.addNotification(generation, message, type);
|
|
2324
2371
|
this.publishNotificationMutations(added.mutations);
|
|
2325
|
-
if (added.notification === undefined)
|
|
2326
|
-
return;
|
|
2327
|
-
this.events.publish(session.sessionId, {
|
|
2328
|
-
type: "command.output",
|
|
2329
|
-
level: type === "error" ? "error" : "info",
|
|
2330
|
-
message,
|
|
2331
|
-
notificationId: added.notification.id,
|
|
2332
|
-
});
|
|
2333
2372
|
};
|
|
2334
2373
|
// PI WEB owns the browser-facing dialog, notification, and text-formatting
|
|
2335
2374
|
// boundaries: the three dialog primitives park daemon-held Promises that
|
|
@@ -2387,6 +2426,11 @@ export class PiSessionService {
|
|
|
2387
2426
|
void this.publishUnreadMutations(mutations).catch(() => undefined);
|
|
2388
2427
|
}
|
|
2389
2428
|
publishUnreadMutations(mutations) {
|
|
2429
|
+
// The store applied the mutations already, so the status projection is told
|
|
2430
|
+
// now rather than after the durable flush: it reads in-memory unread state
|
|
2431
|
+
// and must not lag behind the rows the browser is about to see.
|
|
2432
|
+
if (mutations.length > 0)
|
|
2433
|
+
this.onUnreadChanged?.();
|
|
2390
2434
|
this.enqueueUnreadMutations(mutations);
|
|
2391
2435
|
this.unreadPublicationFlushRequested = true;
|
|
2392
2436
|
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
@@ -2476,7 +2520,7 @@ export class PiSessionService {
|
|
|
2476
2520
|
}
|
|
2477
2521
|
}
|
|
2478
2522
|
active.unsubscribe = session.subscribe((event) => {
|
|
2479
|
-
this.events.publish(session.sessionId, toClientEvent(event));
|
|
2523
|
+
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
2480
2524
|
this.publishActivityForEvent(session, event);
|
|
2481
2525
|
const eventType = getString(event, "type");
|
|
2482
2526
|
if (eventType === "agent_end")
|
|
@@ -2957,7 +3001,7 @@ function uniqueBulkSessionRefs(refs) {
|
|
|
2957
3001
|
const seen = new Set();
|
|
2958
3002
|
const unique = [];
|
|
2959
3003
|
for (const ref of refs) {
|
|
2960
|
-
const key = `${ref.cwd
|
|
3004
|
+
const key = `${ref.cwd}\0${ref.id}`;
|
|
2961
3005
|
if (seen.has(key))
|
|
2962
3006
|
continue;
|
|
2963
3007
|
seen.add(key);
|
|
@@ -2965,16 +3009,14 @@ function uniqueBulkSessionRefs(refs) {
|
|
|
2965
3009
|
}
|
|
2966
3010
|
return unique;
|
|
2967
3011
|
}
|
|
2968
|
-
function
|
|
2969
|
-
return
|
|
3012
|
+
function bulkRefToSessionRef(ref) {
|
|
3013
|
+
return { id: ref.id, cwd: ref.cwd };
|
|
2970
3014
|
}
|
|
2971
3015
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
2972
|
-
return records.find((record) =>
|
|
3016
|
+
return records.find((record) => record.cwd === ref.cwd && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
|
|
2973
3017
|
}
|
|
2974
3018
|
function findListedSessionForBulkRef(context, ref) {
|
|
2975
|
-
|
|
2976
|
-
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
2977
|
-
return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
|
|
3019
|
+
return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
|
|
2978
3020
|
}
|
|
2979
3021
|
function findSessionByIdOrPrefix(sessions, sessionId) {
|
|
2980
3022
|
return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
|
|
@@ -3357,13 +3399,34 @@ function buildPromptOptions(behavior, images) {
|
|
|
3357
3399
|
function stringValue(value) {
|
|
3358
3400
|
return typeof value === "string" ? value : "";
|
|
3359
3401
|
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Attach the thinking level in effect when an assistant message was generated,
|
|
3404
|
+
* so chat bubbles can show it next to the model. Non-assistant messages pass
|
|
3405
|
+
* through by reference; assistant messages are copied only when a level is set.
|
|
3406
|
+
* "off" is the absence of thinking, not a level worth labeling on every bubble.
|
|
3407
|
+
*/
|
|
3408
|
+
function annotateAssistantThinkingLevel(message, thinkingLevel) {
|
|
3409
|
+
if (thinkingLevel === undefined || thinkingLevel === "" || thinkingLevel === "off")
|
|
3410
|
+
return message;
|
|
3411
|
+
if (!isRecord(message) || message["role"] !== "assistant")
|
|
3412
|
+
return message;
|
|
3413
|
+
return { ...message, thinkingLevel };
|
|
3414
|
+
}
|
|
3360
3415
|
function historyMessages(session) {
|
|
3361
3416
|
const messages = [];
|
|
3417
|
+
// Pi records the initial level at session creation and every later change, so
|
|
3418
|
+
// walking the branch yields the level in effect for each assistant message.
|
|
3419
|
+
let thinkingLevel;
|
|
3362
3420
|
for (const entry of session.sessionManager.getBranch()) {
|
|
3363
3421
|
if (!isRecord(entry))
|
|
3364
3422
|
continue;
|
|
3365
3423
|
if (entry["type"] === "message")
|
|
3366
|
-
messages.push(entry["message"]);
|
|
3424
|
+
messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
|
|
3425
|
+
else if (entry["type"] === "thinking_level_change") {
|
|
3426
|
+
const level = getString(entry, "thinkingLevel");
|
|
3427
|
+
if (level !== undefined)
|
|
3428
|
+
thinkingLevel = level;
|
|
3429
|
+
}
|
|
3367
3430
|
else if (entry["type"] === "custom_message" && entry["display"] === true)
|
|
3368
3431
|
messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
|
|
3369
3432
|
else if (entry["type"] === "compaction")
|
|
@@ -3408,7 +3471,7 @@ function finalAssistantText(messages) {
|
|
|
3408
3471
|
}
|
|
3409
3472
|
return "";
|
|
3410
3473
|
}
|
|
3411
|
-
function toClientEvent(event) {
|
|
3474
|
+
function toClientEvent(event, thinkingLevel) {
|
|
3412
3475
|
const eventType = getString(event, "type");
|
|
3413
3476
|
const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
|
|
3414
3477
|
if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
|
|
@@ -3435,7 +3498,9 @@ function toClientEvent(event) {
|
|
|
3435
3498
|
return { type: "agent.end" };
|
|
3436
3499
|
if (eventType === "message_end") {
|
|
3437
3500
|
const message = getProperty(event, "message");
|
|
3438
|
-
|
|
3501
|
+
if (message === undefined)
|
|
3502
|
+
return { type: "message.end" };
|
|
3503
|
+
return { type: "message.end", message: annotateAssistantThinkingLevel(message, thinkingLevel) };
|
|
3439
3504
|
}
|
|
3440
3505
|
return { type: "pi.event", eventType: eventType ?? "unknown" };
|
|
3441
3506
|
}
|