@jmfederico/pi-web 1.202609.0 → 1.202610.0
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 +3 -3
- package/dist/.plugins-ready +1 -0
- package/dist/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-_1GwJfSs.js +4176 -0
- package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
- package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
- package/dist/client/index.html +4 -4
- package/dist/docker/piWebDockerCommandPlan.js +9 -1
- package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
- package/dist/nativeServices/installedServiceDefinitions.js +24 -17
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
- package/dist/nativeServices/serviceAction.js +38 -26
- package/dist/nativeServices/serviceAction.js.map +1 -1
- package/dist/nativeServices/servicePlan.js +3 -3
- package/dist/nativeServices/servicePlan.js.map +1 -1
- package/dist/pi-packages/captains-log/README.md +11 -0
- package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
- package/dist/pi-packages/captains-log/dist/browser/index.js +145 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +107 -0
- package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
- package/dist/pi-packages/captains-log/dist/companion.js +126 -0
- package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
- package/dist/pi-packages/captains-log/dist/server.js +210 -0
- package/dist/pi-packages/captains-log/dist/store.js +75 -0
- package/dist/pi-packages/captains-log/docs/usage.md +32 -0
- package/dist/pi-packages/captains-log/package.json +35 -0
- package/dist/pi-packages/captains-log/tsconfig.json +21 -0
- package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
- package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
- package/dist/pi-packages/relays/prompts/relay.md +1 -1
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +21 -13
- package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
- package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
- package/dist/pi-web-plugins/files/package.json +15 -0
- package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
- package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/git/git-backend.js +5 -5
- package/dist/pi-web-plugins/git/server-plugin.js +5 -3
- package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
- package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
- package/dist/pi-web-plugins/mermaid/package.json +13 -0
- package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
- package/dist/pi-web-plugins/terminal/package.json +16 -0
- package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
- package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +159 -79
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
- package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
- package/dist/piWebVersionReport.js +12 -4
- package/dist/piWebVersionReport.js.map +1 -1
- package/dist/plugin-api.d.ts +235 -23
- package/dist/plugin-api.js +1 -0
- package/dist/pluginRecoveryCli.js +3 -2
- package/dist/pluginRecoveryCli.js.map +1 -1
- package/dist/server/activity/workspaceActivityService.js.map +1 -1
- package/dist/server/app.js +32 -28
- package/dist/server/app.js.map +1 -1
- package/dist/server/browserMessageProjection.js +39 -20
- package/dist/server/browserMessageProjection.js.map +1 -1
- package/dist/server/knownPiPackages.js +12 -0
- package/dist/server/knownPiPackages.js.map +1 -0
- package/dist/server/machines/machineClient.js +2 -2
- package/dist/server/machines/machineClient.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineProxyRoutes.js +101 -7
- package/dist/server/machines/machineProxyRoutes.js.map +1 -1
- package/dist/server/notices/serverNoticeStore.js +45 -4
- package/dist/server/notices/serverNoticeStore.js.map +1 -1
- package/dist/server/piPackageService.js +5 -5
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebPluginCatalog.js +46 -12
- package/dist/server/piWebPluginCatalog.js.map +1 -1
- package/dist/server/piWebPluginLifecycle.js +26 -3
- package/dist/server/piWebPluginLifecycle.js.map +1 -1
- package/dist/server/piWebPluginService.js +37 -2
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/pluginCallbackDrain.js +35 -0
- package/dist/server/pluginCallbackDrain.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
- package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
- package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
- package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
- package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/plugins/pluginBackendRegistry.js +740 -0
- package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
- package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
- package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
- package/dist/server/plugins/serverPluginRuntime.js +1028 -107
- package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
- package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
- package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
- package/dist/server/projects/projectLifecycleService.js +91 -0
- package/dist/server/projects/projectLifecycleService.js.map +1 -0
- package/dist/server/realtime/sessionEventHub.js +22 -9
- package/dist/server/realtime/sessionEventHub.js.map +1 -1
- package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
- package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
- package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
- package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
- package/dist/server/sessiond/projectMutationRoutes.js +38 -0
- package/dist/server/sessiond/projectMutationRoutes.js.map +1 -0
- package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
- package/dist/server/sessiond/sessionProxyRoutes.js +24 -0
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +99 -24
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/builtinCommands.js +2 -2
- package/dist/server/sessions/builtinCommands.js.map +1 -1
- package/dist/server/sessions/clientSessionPreview.js +14 -0
- package/dist/server/sessions/clientSessionPreview.js.map +1 -0
- package/dist/server/sessions/piSessionEventConnections.js +65 -0
- package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
- package/dist/server/sessions/piSessionManagerGateway.js +8 -2
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +411 -102
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionActivityMarker.js +105 -0
- package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
- package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
- package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
- package/dist/server/sessions/sessionMediaIndex.js +184 -0
- package/dist/server/sessions/sessionMediaIndex.js.map +1 -0
- package/dist/server/sessions/sessionNameGenerator.js +3 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +64 -4
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionUnreadStore.js +26 -1
- package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +7 -2
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +7 -2
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/server/sessions/transcriptMessages.js +54 -0
- package/dist/server/sessions/transcriptMessages.js.map +1 -0
- package/dist/server/terminals/requiredTerminalService.js +103 -0
- package/dist/server/terminals/requiredTerminalService.js.map +1 -0
- package/dist/server/webSocketBridge.js +339 -0
- package/dist/server/webSocketBridge.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +1 -1
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/filePreviewService.js +27 -3
- package/dist/server/workspaces/filePreviewService.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceCatalog.js +3 -1
- package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
- package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +23 -1
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/server-plugin-api.d.ts +216 -22
- package/dist/server-plugin-api.js +319 -2
- package/dist/server-plugin-api.js.map +1 -1
- package/dist/serverPluginRecovery.js +4 -0
- package/dist/serverPluginRecovery.js.map +1 -1
- package/dist/sessiond/sessionDaemonClient.js +27 -3
- package/dist/sessiond/sessionDaemonClient.js.map +1 -1
- package/dist/shared/apiTypes.js +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +47 -17
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/machinePluginIds.js +28 -7
- package/dist/shared/machinePluginIds.js.map +1 -1
- package/dist/shared/pluginApiTypes.d.ts +21 -1
- package/dist/shared/pluginApiTypes.js +0 -1
- package/dist/shared/pluginBackendProtocol.js +163 -2
- package/dist/shared/pluginBackendProtocol.js.map +1 -1
- package/dist/shared/pluginIds.js +8 -1
- package/dist/shared/pluginIds.js.map +1 -1
- package/dist/shared/requiredTerminalPlugin.js +6 -0
- package/dist/shared/requiredTerminalPlugin.js.map +1 -0
- package/dist/shared/serverNoticeContract.js +88 -0
- package/dist/shared/serverNoticeContract.js.map +1 -0
- package/dist/shared/sessionDefaults.js +41 -0
- package/dist/shared/sessionDefaults.js.map +1 -0
- package/dist/shared/sessionMedia.js +6 -0
- package/dist/shared/sessionMedia.js.map +1 -0
- package/docs/config.md +50 -36
- package/docs/plugins.md +157 -1212
- package/examples/session-bridge-plugin/README.md +20 -0
- package/examples/session-bridge-plugin/docs/usage.md +54 -0
- package/examples/session-bridge-plugin/package.json +26 -0
- package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
- package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
- package/examples/session-bridge-plugin/src/companion.ts +65 -0
- package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
- package/examples/session-bridge-plugin/src/server.ts +85 -0
- package/examples/session-bridge-plugin/src/store.ts +57 -0
- package/examples/session-bridge-plugin/tsconfig.json +21 -0
- package/examples/workspace-provider-plugin/README.md +4 -4
- package/examples/workspace-provider-plugin/package.json +1 -1
- package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
- package/examples/workspace-provider-plugin/src/server.ts +15 -11
- package/package.json +19 -12
- package/dist/client/assets/CodeViewer-CGAlg9S8.js +0 -4
- package/dist/client/assets/TerminalPanel-xhJRhOas.js +0 -187
- package/dist/client/assets/index-DXQKhn1P.js +0 -4326
- package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
- package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
- package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
- package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
- package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
- package/dist/server/terminalProxyRoutes.js +0 -141
- package/dist/server/terminalProxyRoutes.js.map +0 -1
- package/dist/server/terminals/terminalRoutes.js +0 -155
- package/dist/server/terminals/terminalRoutes.js.map +0 -1
- package/dist/server/terminals/terminalService.js.map +0 -1
- package/dist/server/terminals/terminalSize.js +0 -17
- package/dist/server/terminals/terminalSize.js.map +0 -1
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, hasTrustRequiringProjectResources, ProjectTrustStore, readStoredCredential, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, defineTool, hasTrustRequiringProjectResources, ProjectTrustStore, readStoredCredential, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
6
|
+
import { isSessionMediaId } from "../../shared/sessionMedia.js";
|
|
6
7
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
8
|
+
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
7
9
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
8
10
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
11
|
+
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
9
12
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
10
13
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
11
14
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
15
|
+
import { PiSessionEventConnections } from "./piSessionEventConnections.js";
|
|
12
16
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
13
17
|
import { computeEditPreview } from "./editPreview.js";
|
|
14
18
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
15
19
|
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
16
20
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
17
21
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
22
|
+
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
18
23
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
19
24
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
20
25
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
@@ -25,6 +30,7 @@ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS } from "../../config.js";
|
|
|
25
30
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
26
31
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
27
32
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
33
|
+
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
28
34
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
29
35
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
30
36
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
@@ -422,7 +428,7 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
|
422
428
|
return undefined;
|
|
423
429
|
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
424
430
|
}
|
|
425
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
431
|
+
function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
426
432
|
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
427
433
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
428
434
|
// PI WEB always honors pi's project-trust model. When the workspace ships
|
|
@@ -435,6 +441,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
435
441
|
// browser trust prompt, an untrusted project's resources are skipped
|
|
436
442
|
// (matching `pi` run without a UI). Projects without trust-requiring
|
|
437
443
|
// resources skip resolution entirely and are trusted, as before.
|
|
444
|
+
const eventBus = createEventBus();
|
|
438
445
|
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
439
446
|
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !projectTrustRequiring });
|
|
440
447
|
// Pre-session-creation trust failures (`project_trust` handler errors)
|
|
@@ -446,7 +453,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
446
453
|
agentDir,
|
|
447
454
|
modelRuntime,
|
|
448
455
|
settingsManager,
|
|
449
|
-
|
|
456
|
+
resourceLoaderOptions: { ...resourceLoaderOptions, eventBus },
|
|
450
457
|
...(projectTrustRequiring
|
|
451
458
|
? {
|
|
452
459
|
resourceLoaderReloadOptions: {
|
|
@@ -480,6 +487,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
480
487
|
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
481
488
|
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
482
489
|
});
|
|
490
|
+
sessionEvents.register(result.session, eventBus);
|
|
483
491
|
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
484
492
|
};
|
|
485
493
|
}
|
|
@@ -528,6 +536,8 @@ export class PiSessionService {
|
|
|
528
536
|
this.unpersistedTreeBranchLeaves = new WeakMap();
|
|
529
537
|
/** Counts async operations that may append an entry before they settle. */
|
|
530
538
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
539
|
+
this.pendingPromptEchoes = new WeakMap();
|
|
540
|
+
this.publishedAssistantPartials = new WeakMap();
|
|
531
541
|
/** Settings-wide queue preventing enabled-model read/modify/write races across sessions. */
|
|
532
542
|
this.modelScopeMutationQueue = Promise.resolve();
|
|
533
543
|
/** Global and workspace-owned enabled-model states; live session scopes project one entry lazily. */
|
|
@@ -556,8 +566,8 @@ export class PiSessionService {
|
|
|
556
566
|
this.subsessionHydratedParents = new Set();
|
|
557
567
|
/**
|
|
558
568
|
* Tracked subsession id -> whether a completion notification is armed.
|
|
559
|
-
* Armed when the child starts
|
|
560
|
-
*
|
|
569
|
+
* Armed when the child's agent starts a run or a prompt fails before starting
|
|
570
|
+
* one (not for standalone commands or compaction); completion disarms it.
|
|
561
571
|
*/
|
|
562
572
|
this.subsessionNotifyArmed = new Map();
|
|
563
573
|
this.notificationGenerationBySession = new WeakMap();
|
|
@@ -575,6 +585,10 @@ export class PiSessionService {
|
|
|
575
585
|
this.spawnTargets = deps.spawnTargets;
|
|
576
586
|
this.logger = deps.logger ?? noopLogger;
|
|
577
587
|
this.now = deps.now ?? (() => new Date());
|
|
588
|
+
this.activityMarker = new SessionActivityMarker({
|
|
589
|
+
now: () => this.now().getTime(),
|
|
590
|
+
onError: (error) => { this.logger.info({ err: error }, "Could not update advisory session activity marker"); },
|
|
591
|
+
});
|
|
578
592
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
579
593
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
580
594
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
@@ -588,7 +602,8 @@ export class PiSessionService {
|
|
|
588
602
|
// Subsessions are gated behind their own flag, and they
|
|
589
603
|
// also require the spawn capability (they share its project-scope resolver).
|
|
590
604
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
591
|
-
this.
|
|
605
|
+
this.sessionEvents = deps.sessionEvents ?? new PiSessionEventConnections();
|
|
606
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.sessionEvents, this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
592
607
|
spawn: (input) => this.spawnSubsession(input),
|
|
593
608
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
594
609
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -625,6 +640,9 @@ export class PiSessionService {
|
|
|
625
640
|
notificationCatalog() {
|
|
626
641
|
return this.notificationStore.catalogSnapshot();
|
|
627
642
|
}
|
|
643
|
+
async reconcileUnreadWorkspaces(cwds) {
|
|
644
|
+
await this.publishUnreadMutations(this.unreadStore.reconcileWorkspaces(cwds));
|
|
645
|
+
}
|
|
628
646
|
async unreadCatalog() {
|
|
629
647
|
await this.publishUnreadMutations([]);
|
|
630
648
|
return this.unreadStore.durableCatalogSnapshot();
|
|
@@ -732,7 +750,10 @@ export class PiSessionService {
|
|
|
732
750
|
finally {
|
|
733
751
|
await active.runtime.dispose();
|
|
734
752
|
}
|
|
735
|
-
}))
|
|
753
|
+
})).finally(async () => {
|
|
754
|
+
this.events.mediaIndex.clear();
|
|
755
|
+
await this.activityMarker.dispose();
|
|
756
|
+
});
|
|
736
757
|
await this.publishUnreadMutations([]);
|
|
737
758
|
}
|
|
738
759
|
async list(cwd) {
|
|
@@ -748,14 +769,48 @@ export class PiSessionService {
|
|
|
748
769
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
749
770
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
750
771
|
const archivedSessions = archivedForCwd
|
|
751
|
-
.sort(compareArchivedRecords)
|
|
752
772
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
753
|
-
.filter(isDefined)
|
|
773
|
+
.filter(isDefined)
|
|
774
|
+
.sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
754
775
|
return [...unarchivedSessions, ...archivedSessions];
|
|
755
776
|
}
|
|
756
777
|
async start(cwd, options = {}) {
|
|
757
778
|
return this.startSession(cwd, options);
|
|
758
779
|
}
|
|
780
|
+
/** Publish a host-owned conversation without submitting an initial prompt. */
|
|
781
|
+
async createHostedSession(cwd, signal) {
|
|
782
|
+
const created = await this.startManagedSession(cwd, signal);
|
|
783
|
+
return Object.freeze({ id: created.id });
|
|
784
|
+
}
|
|
785
|
+
/** Atomically start and prompt one visible host-owned, non-delegating session. */
|
|
786
|
+
async startOneShotRun(cwd, text, signal) {
|
|
787
|
+
const promptText = requirePromptText(text);
|
|
788
|
+
return this.startManagedSession(cwd, signal, promptText);
|
|
789
|
+
}
|
|
790
|
+
async startManagedSession(cwd, signal, promptText) {
|
|
791
|
+
signal.throwIfAborted();
|
|
792
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, { startupIntent: "create", creationProvenance: "host-one-shot", startupSignal: signal });
|
|
793
|
+
const { session } = active.runtime;
|
|
794
|
+
try {
|
|
795
|
+
signal.throwIfAborted();
|
|
796
|
+
if (promptText !== undefined)
|
|
797
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
798
|
+
const created = this.announceCreatedSession(active, cwd);
|
|
799
|
+
// Publication transfers lifetime ownership to normal hosting. Plugin
|
|
800
|
+
// cancellation after this point must not abort this or later user work.
|
|
801
|
+
const completion = promptText === undefined ? Promise.resolve() : this.submitInitialRunPrompt(session, promptText);
|
|
802
|
+
return Object.freeze({ id: created.id, completion });
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
try {
|
|
806
|
+
await this.closeActive(session.sessionId);
|
|
807
|
+
}
|
|
808
|
+
catch (cleanupError) {
|
|
809
|
+
throw new AggregateError([error, cleanupError], `Failed to clean up managed session ${session.sessionId} after startup failed`, { cause: cleanupError });
|
|
810
|
+
}
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
759
814
|
async startSession(cwd, options) {
|
|
760
815
|
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
761
816
|
startupIntent: "create",
|
|
@@ -764,6 +819,9 @@ export class PiSessionService {
|
|
|
764
819
|
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
765
820
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
766
821
|
});
|
|
822
|
+
return this.announceCreatedSession(active, cwd, options.parentSession);
|
|
823
|
+
}
|
|
824
|
+
announceCreatedSession(active, cwd, parentSession) {
|
|
767
825
|
const { session } = active.runtime;
|
|
768
826
|
const created = {
|
|
769
827
|
id: session.sessionId,
|
|
@@ -776,7 +834,7 @@ export class PiSessionService {
|
|
|
776
834
|
firstMessage: "",
|
|
777
835
|
// Include the parent so listeners can nest the new session in the tree
|
|
778
836
|
// immediately, instead of showing it flat until the next reload.
|
|
779
|
-
...(
|
|
837
|
+
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
780
838
|
};
|
|
781
839
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
782
840
|
// session to their list without a manual reload.
|
|
@@ -1114,8 +1172,12 @@ export class PiSessionService {
|
|
|
1114
1172
|
* `async` so a rejected dialog becomes a rejection rather than a synchronous
|
|
1115
1173
|
* throw from a promise-returning method.
|
|
1116
1174
|
*/
|
|
1117
|
-
async openExtensionDialog(session, request, opts) {
|
|
1118
|
-
const signal = opts?.signal
|
|
1175
|
+
async openExtensionDialog(session, request, opts, startupSignal) {
|
|
1176
|
+
const signal = opts?.signal === undefined
|
|
1177
|
+
? startupSignal
|
|
1178
|
+
: startupSignal === undefined
|
|
1179
|
+
? opts.signal
|
|
1180
|
+
: AbortSignal.any([opts.signal, startupSignal]);
|
|
1119
1181
|
// A pre-aborted signal dismisses the dialog before it ever opens.
|
|
1120
1182
|
if (signal?.aborted === true)
|
|
1121
1183
|
return extensionDialogCancelValue(request.kind);
|
|
@@ -1465,31 +1527,34 @@ export class PiSessionService {
|
|
|
1465
1527
|
});
|
|
1466
1528
|
}
|
|
1467
1529
|
/**
|
|
1468
|
-
*
|
|
1469
|
-
*
|
|
1470
|
-
*
|
|
1471
|
-
* parent is busy and delivers immediately when it is idle).
|
|
1530
|
+
* Arm on a tracked child's agent_start, then notify the parent once it stops
|
|
1531
|
+
* working. Prompt failures before agent_start explicitly arm the same latch.
|
|
1532
|
+
* Standalone commands and compactions must not arm a completion notice.
|
|
1472
1533
|
*/
|
|
1473
|
-
updateSubsessionTracking(session) {
|
|
1534
|
+
updateSubsessionTracking(session, agentStarted = false) {
|
|
1474
1535
|
const link = this.subsessionLinkForActiveChild(session);
|
|
1475
1536
|
if (link === undefined)
|
|
1476
1537
|
return;
|
|
1477
1538
|
const childId = link.childSessionId;
|
|
1478
|
-
if (
|
|
1539
|
+
if (agentStarted) {
|
|
1479
1540
|
this.subsessionNotifyArmed.set(childId, true);
|
|
1480
1541
|
return;
|
|
1481
1542
|
}
|
|
1543
|
+
if (this.hasActiveWork(session))
|
|
1544
|
+
return;
|
|
1482
1545
|
if (this.subsessionNotifyArmed.get(childId) !== true)
|
|
1483
1546
|
return;
|
|
1484
1547
|
this.subsessionNotifyArmed.set(childId, false);
|
|
1485
|
-
const
|
|
1548
|
+
const activity = this.activities.get(childId);
|
|
1549
|
+
const status = activity?.phase === "error" ? "error" : "idle";
|
|
1550
|
+
const errorSection = status === "error" && activity?.detail !== undefined ? `Error: ${activity.detail}\n\n` : "";
|
|
1486
1551
|
const finalText = finalAssistantText(historyMessages(session));
|
|
1487
1552
|
const outputSection = formatSubsessionNotificationOutput(childId, finalText);
|
|
1488
1553
|
const workingIds = this.workingSubsessionIds(link.parentSessionId);
|
|
1489
1554
|
const next = workingIds.length === 0
|
|
1490
1555
|
? "No other tracked subsessions are working."
|
|
1491
1556
|
: `Still working: ${workingIds.join(", ")}. Continue working, or call yield_to_subsessions alone and last at the next join point. Further completion notices arrive automatically; do not poll.`;
|
|
1492
|
-
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${outputSection}`;
|
|
1557
|
+
const text = `Subsession ${childId} stopped working (${status}).\n${next}\n\n${errorSection}${outputSection}`;
|
|
1493
1558
|
void this.notifyParentOfSubsession(link.parentSessionId, childId, text);
|
|
1494
1559
|
}
|
|
1495
1560
|
async getOrOpenParentForSubsession(parentSessionId, childSessionId) {
|
|
@@ -1537,12 +1602,72 @@ export class PiSessionService {
|
|
|
1537
1602
|
logSubsessionNotificationFailure(parentId, childId, error) {
|
|
1538
1603
|
this.logger.info({ parentSessionId: parentId, sessionId: childId, error: error instanceof Error ? error.message : String(error) }, "failed to notify parent of subsession completion");
|
|
1539
1604
|
}
|
|
1605
|
+
async transcriptSnapshot(ref, page) {
|
|
1606
|
+
const session = await this.getOrOpen(ref);
|
|
1607
|
+
const seqBeforeRead = this.events.currentSeq(session.sessionId);
|
|
1608
|
+
const activeAtCapture = this.hasActiveWork(session);
|
|
1609
|
+
const diskBranch = activeAtCapture ? undefined : await this.readableSessionBranch(ref, session);
|
|
1610
|
+
// Disk reads yield, even if a whole turn starts AND ends while they run.
|
|
1611
|
+
// From the final runtime read through the watermark capture nothing may yield.
|
|
1612
|
+
const branch = diskBranch === undefined || this.hasActiveWork(session) || this.events.currentSeq(session.sessionId) !== seqBeforeRead
|
|
1613
|
+
? session.sessionManager.getBranch()
|
|
1614
|
+
: diskBranch;
|
|
1615
|
+
const messages = historyMessagesFromEntries(branch);
|
|
1616
|
+
const userCount = messages.filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
1617
|
+
const echoes = (this.pendingPromptEchoes.get(session) ?? []).filter((echo) => echo.userIndex >= userCount);
|
|
1618
|
+
messages.push(...echoes.map((echo) => echo.message));
|
|
1619
|
+
return {
|
|
1620
|
+
page: pageMessagesAtSafeBoundary(messages, page),
|
|
1621
|
+
status: this.statusFromSession(session, transcriptMessageCount(branch) + echoes.length),
|
|
1622
|
+
seq: this.events.currentSeq(session.sessionId),
|
|
1623
|
+
partial: this.publishedAssistantPartials.get(session) ?? null,
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1540
1626
|
async messages(ref, page) {
|
|
1541
1627
|
const session = await this.getOrOpen(ref);
|
|
1542
1628
|
return pageMessagesAtSafeBoundary(historyMessagesFromEntries(await this.readableSessionBranch(ref, session)), page);
|
|
1543
1629
|
}
|
|
1630
|
+
async media(ref, mediaId) {
|
|
1631
|
+
if (!isSessionMediaId(mediaId))
|
|
1632
|
+
throw new Error("Invalid media id");
|
|
1633
|
+
const index = this.events.mediaIndex;
|
|
1634
|
+
const session = this.activeForRef(ref)?.runtime.session ?? this.startupSessionForRef(ref);
|
|
1635
|
+
// Verify session/cwd ownership before consulting the session-scoped cache.
|
|
1636
|
+
// Cold binary reads do not construct an SDK runtime (which can migrate files).
|
|
1637
|
+
const archived = session === undefined ? await this.getArchived(ref) : undefined;
|
|
1638
|
+
const resolved = session === undefined && archived?.archivePath === undefined
|
|
1639
|
+
? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
|
|
1640
|
+
: undefined;
|
|
1641
|
+
const sessionId = session?.sessionId ?? archived?.sessionId ?? resolved?.id;
|
|
1642
|
+
if (sessionId === undefined || (resolved !== undefined && !cwdPathsEqual(resolved.cwd, ref.cwd)))
|
|
1643
|
+
throw new Error("Session not found");
|
|
1644
|
+
const scope = { id: sessionId, cwd: canonicalizeStoredCwd(session?.sessionManager.getCwd() ?? archived?.cwd ?? resolved?.cwd ?? ref.cwd) };
|
|
1645
|
+
const cached = index.get(scope, mediaId);
|
|
1646
|
+
if (cached !== undefined)
|
|
1647
|
+
return cached;
|
|
1648
|
+
if (session !== undefined) {
|
|
1649
|
+
const live = index.find(scope, mediaId, [
|
|
1650
|
+
session.sessionManager.getEntries?.() ?? session.sessionManager.getBranch(),
|
|
1651
|
+
session.messages,
|
|
1652
|
+
session.state.streamingMessage,
|
|
1653
|
+
this.publishedAssistantPartials.get(session),
|
|
1654
|
+
...(this.pendingPromptEchoes.get(session) ?? []).map((echo) => echo.message),
|
|
1655
|
+
]);
|
|
1656
|
+
if (live !== undefined)
|
|
1657
|
+
return live;
|
|
1658
|
+
}
|
|
1659
|
+
// Read disk on misses even for a cached idle runtime: other processes can
|
|
1660
|
+
// append images on any branch without emitting events through this daemon.
|
|
1661
|
+
const path = session?.sessionFile ?? session?.sessionManager.getSessionFile() ?? archived?.archivePath ?? resolved?.path
|
|
1662
|
+
?? (session === undefined ? undefined : (await this.sessionManager.resolveSessionFile(ref.cwd, sessionId))?.path);
|
|
1663
|
+
if (path === undefined)
|
|
1664
|
+
return undefined;
|
|
1665
|
+
const entries = await this.sessionManager.readEntries?.(path);
|
|
1666
|
+
return entries === undefined ? undefined : index.find(scope, mediaId, entries);
|
|
1667
|
+
}
|
|
1544
1668
|
async status(ref) {
|
|
1545
1669
|
const session = await this.sessionForStatusOrDialogClose(ref);
|
|
1670
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
1546
1671
|
if (this.hasActiveWork(session))
|
|
1547
1672
|
return this.statusFromSession(session);
|
|
1548
1673
|
const branch = await this.readableSessionBranch(ref, session);
|
|
@@ -1567,6 +1692,47 @@ export class PiSessionService {
|
|
|
1567
1692
|
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1568
1693
|
return { seq, partial };
|
|
1569
1694
|
}
|
|
1695
|
+
async getSessionDefaults(ref) {
|
|
1696
|
+
await this.getOrOpen(ref);
|
|
1697
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1698
|
+
await settings.reload();
|
|
1699
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1700
|
+
// Do not use merged getters: workspace overrides are not global pins.
|
|
1701
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1702
|
+
}
|
|
1703
|
+
async setSessionDefaults(ref, defaults) {
|
|
1704
|
+
const update = parseSessionDefaultsUpdate({ ...defaults });
|
|
1705
|
+
await this.assertWritable(ref);
|
|
1706
|
+
const session = await this.getOrOpen(ref);
|
|
1707
|
+
return this.runModelScopeMutation(async () => {
|
|
1708
|
+
// A separate manager avoids mutating the active session's settings cache.
|
|
1709
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1710
|
+
await settings.reload();
|
|
1711
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1712
|
+
if (update.provider !== undefined && update.modelId !== undefined) {
|
|
1713
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1714
|
+
const target = `${update.provider}/${update.modelId}`;
|
|
1715
|
+
const enabledIds = await resolveEnabledModelIds({ settingsManager: settings, modelRuntime: session.modelRuntime, scopedModels: [] });
|
|
1716
|
+
if (!session.modelRuntime.getAvailableSnapshot().some((model) => modelScopeId(model) === target)) {
|
|
1717
|
+
throw new Error(`Model not found: ${target}`);
|
|
1718
|
+
}
|
|
1719
|
+
if (enabledIds !== null && !enabledIds.includes(target))
|
|
1720
|
+
throw new Error(`Model is not enabled: ${target}`);
|
|
1721
|
+
settings.setDefaultModelAndProvider(update.provider, update.modelId);
|
|
1722
|
+
}
|
|
1723
|
+
else if (update.thinkingLevel !== undefined) {
|
|
1724
|
+
settings.setDefaultThinkingLevel(update.thinkingLevel);
|
|
1725
|
+
}
|
|
1726
|
+
await settings.flush();
|
|
1727
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1728
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
assertDefaultsSettingsHealthy(settings) {
|
|
1732
|
+
const errors = settings.drainErrors();
|
|
1733
|
+
if (errors.length > 0)
|
|
1734
|
+
throw new Error(`Session defaults settings failed: ${errors.map(({ error }) => error.message).join("; ")}`);
|
|
1735
|
+
}
|
|
1570
1736
|
async availableModels(ref) {
|
|
1571
1737
|
const session = await this.getOrOpen(ref);
|
|
1572
1738
|
const models = await this.sessionModelCandidates(session);
|
|
@@ -1700,7 +1866,12 @@ export class PiSessionService {
|
|
|
1700
1866
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
1701
1867
|
}
|
|
1702
1868
|
for (const template of session.promptTemplates) {
|
|
1703
|
-
commands.push({
|
|
1869
|
+
commands.push({
|
|
1870
|
+
name: template.name,
|
|
1871
|
+
...(template.description === undefined ? {} : { description: template.description }),
|
|
1872
|
+
...(template.argumentHint === undefined ? {} : { argumentHint: template.argumentHint }),
|
|
1873
|
+
source: "prompt",
|
|
1874
|
+
});
|
|
1704
1875
|
}
|
|
1705
1876
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1706
1877
|
commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
|
|
@@ -1740,18 +1911,104 @@ export class PiSessionService {
|
|
|
1740
1911
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1741
1912
|
}
|
|
1742
1913
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1914
|
+
return this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
|
|
1915
|
+
}
|
|
1916
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1743
1917
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1744
|
-
|
|
1745
|
-
|
|
1918
|
+
const echoes = this.pendingPromptEchoes.get(session) ?? [];
|
|
1919
|
+
const userCount = historyMessagesFromEntries(session.sessionManager.getBranch()).filter((message) => isRecord(message) && message["role"] === "user").length;
|
|
1920
|
+
const echo = behavior === undefined && echoUserMessage
|
|
1921
|
+
? { userIndex: Math.max(userCount, ...echoes.map((pending) => pending.userIndex + 1)), message: userMessage(text, images) }
|
|
1922
|
+
: undefined;
|
|
1923
|
+
if (echo !== undefined) {
|
|
1924
|
+
// SDK input hooks may await before appending the user message. Keep the
|
|
1925
|
+
// already-published echo visible in snapshots until that append occurs.
|
|
1926
|
+
this.pendingPromptEchoes.set(session, [...echoes, echo]);
|
|
1927
|
+
this.events.publish(session.sessionId, { type: "message.append", message: echo.message }, { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
1928
|
+
}
|
|
1746
1929
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1747
|
-
const
|
|
1930
|
+
const commandName = text.startsWith("/") ? text.slice(1).split(" ")[0] : undefined;
|
|
1931
|
+
const trackEarlyFailure = behavior === undefined
|
|
1932
|
+
&& this.subsessionLinkForActiveChild(session) !== undefined
|
|
1933
|
+
&& !session.extensionRunner.getRegisteredCommands().some((command) => command.invocationName === commandName);
|
|
1934
|
+
let agentStarted = false;
|
|
1935
|
+
const unsubscribe = !trackEarlyFailure ? undefined : session.subscribe((event) => {
|
|
1936
|
+
if (getString(event, "type") === "agent_start")
|
|
1937
|
+
agentStarted = true;
|
|
1938
|
+
});
|
|
1939
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions)).finally(() => {
|
|
1940
|
+
unsubscribe?.();
|
|
1941
|
+
if (echo !== undefined) {
|
|
1942
|
+
const remaining = (this.pendingPromptEchoes.get(session) ?? []).filter((pending) => pending !== echo);
|
|
1943
|
+
if (remaining.length === 0)
|
|
1944
|
+
this.pendingPromptEchoes.delete(session);
|
|
1945
|
+
else
|
|
1946
|
+
this.pendingPromptEchoes.set(session, remaining);
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
void promptPromise.catch((error) => {
|
|
1748
1950
|
const message = error instanceof Error ? error.message : String(error);
|
|
1749
1951
|
this.publishActivity(session, "error", "error", message);
|
|
1750
1952
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1953
|
+
if (this.isCurrentActiveSession(session)) {
|
|
1954
|
+
const link = this.subsessionLinkForActiveChild(session);
|
|
1955
|
+
// Model/auth validation can reject without any agent event. Wake the
|
|
1956
|
+
// parent, but do not re-arm a run that already emitted agent_start.
|
|
1957
|
+
if (trackEarlyFailure && !agentStarted && link !== undefined)
|
|
1958
|
+
this.subsessionNotifyArmed.set(link.childSessionId, true);
|
|
1959
|
+
this.updateSubsessionTracking(session);
|
|
1960
|
+
}
|
|
1751
1961
|
});
|
|
1752
|
-
void promptPromise;
|
|
1753
1962
|
return promptPromise;
|
|
1754
1963
|
}
|
|
1964
|
+
async submitInitialRunPrompt(session, text) {
|
|
1965
|
+
let failure;
|
|
1966
|
+
let settled = false;
|
|
1967
|
+
let sawInitialUserMessage = false;
|
|
1968
|
+
const unsubscribe = session.subscribe((event) => {
|
|
1969
|
+
if (settled || !isRecord(event))
|
|
1970
|
+
return;
|
|
1971
|
+
if (event["type"] === "agent_settled") {
|
|
1972
|
+
// Freeze the initial outcome before a later user submission can change it.
|
|
1973
|
+
settled = true;
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
if (event["type"] === "auto_retry_end" && event["success"] === false && event["finalError"] === "Retry cancelled") {
|
|
1977
|
+
// Native Pi cancels retry sleep without an aborted assistant message.
|
|
1978
|
+
failure = new DOMException("Initial PI session run was aborted", "AbortError");
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const message = event["message"];
|
|
1982
|
+
if (!isRecord(message))
|
|
1983
|
+
return;
|
|
1984
|
+
if (event["type"] === "message_start" && message["role"] === "user") {
|
|
1985
|
+
// Pi can drain a queued user follow-up inside the same prompt promise.
|
|
1986
|
+
// Its outcome belongs to that later submission, not this launcher.
|
|
1987
|
+
if (sawInitialUserMessage)
|
|
1988
|
+
settled = true;
|
|
1989
|
+
sawInitialUserMessage = true;
|
|
1990
|
+
return;
|
|
1991
|
+
}
|
|
1992
|
+
if (event["type"] !== "message_end" || message["role"] !== "assistant")
|
|
1993
|
+
return;
|
|
1994
|
+
// A successful retry supersedes a transient provider failure. Pi's
|
|
1995
|
+
// prompt promise can fulfill even when the final assistant is an error.
|
|
1996
|
+
const reason = message["stopReason"];
|
|
1997
|
+
failure = reason === "aborted"
|
|
1998
|
+
? new DOMException("Initial PI session run was aborted", "AbortError")
|
|
1999
|
+
: reason === "error"
|
|
2000
|
+
? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : "Initial PI session run failed")
|
|
2001
|
+
: undefined;
|
|
2002
|
+
});
|
|
2003
|
+
try {
|
|
2004
|
+
await this.beginPromptSubmission(session, text, undefined);
|
|
2005
|
+
if (failure !== undefined)
|
|
2006
|
+
throw failure;
|
|
2007
|
+
}
|
|
2008
|
+
finally {
|
|
2009
|
+
unsubscribe();
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
1755
2012
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1756
2013
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1757
2014
|
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
@@ -1882,6 +2139,13 @@ export class PiSessionService {
|
|
|
1882
2139
|
}
|
|
1883
2140
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1884
2141
|
}
|
|
2142
|
+
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2143
|
+
// Questions belong to the position we left. Close them for every browser
|
|
2144
|
+
// without appending a cancellation message or waking the rewound session.
|
|
2145
|
+
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
2146
|
+
if (outcome !== undefined)
|
|
2147
|
+
this.publishAskClosed(session.sessionId, outcome);
|
|
2148
|
+
}
|
|
1885
2149
|
if (result.summaryEntry !== undefined) {
|
|
1886
2150
|
// A summary entry durably identifies the selected branch as the file's
|
|
1887
2151
|
// newest leaf, superseding any earlier bare selection.
|
|
@@ -2322,10 +2586,12 @@ export class PiSessionService {
|
|
|
2322
2586
|
async archiveStoreArchiveMany(inputs) {
|
|
2323
2587
|
if (inputs.length === 0)
|
|
2324
2588
|
return [];
|
|
2589
|
+
// Work from the bottom of the activity-ordered list, using metadata already loaded.
|
|
2590
|
+
const oldestFirst = [...inputs].sort((a, b) => Date.parse(a.modified) - Date.parse(b.modified));
|
|
2325
2591
|
if (this.archiveStore.archiveMany !== undefined)
|
|
2326
|
-
return this.archiveStore.archiveMany(
|
|
2592
|
+
return this.archiveStore.archiveMany(oldestFirst);
|
|
2327
2593
|
const records = [];
|
|
2328
|
-
for (const input of
|
|
2594
|
+
for (const input of oldestFirst)
|
|
2329
2595
|
records.push(await this.archiveStore.archive(input));
|
|
2330
2596
|
return records;
|
|
2331
2597
|
}
|
|
@@ -2457,6 +2723,7 @@ export class PiSessionService {
|
|
|
2457
2723
|
// Promises inside the dying runtime: settle them rather than dropping them.
|
|
2458
2724
|
this.endSessionExtensionDialogs(sessionId);
|
|
2459
2725
|
this.active.delete(sessionId);
|
|
2726
|
+
this.events.mediaIndex.forgetSession({ id: sessionId, cwd: active.runtime.session.sessionManager.getCwd() });
|
|
2460
2727
|
this.activities.delete(sessionId);
|
|
2461
2728
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
2462
2729
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -2473,6 +2740,7 @@ export class PiSessionService {
|
|
|
2473
2740
|
await this.abortSessionOperations(active.runtime.session);
|
|
2474
2741
|
}
|
|
2475
2742
|
finally {
|
|
2743
|
+
await this.activityMarker.release(active.runtime.session.sessionFile);
|
|
2476
2744
|
await active.runtime.dispose();
|
|
2477
2745
|
}
|
|
2478
2746
|
}
|
|
@@ -2564,6 +2832,9 @@ export class PiSessionService {
|
|
|
2564
2832
|
}
|
|
2565
2833
|
async getActive(ref, options = {}) {
|
|
2566
2834
|
const active = this.activeForRef(ref);
|
|
2835
|
+
const pending = this.pendingSessionOpens.get(JSON.stringify([canonicalizeStoredCwd(ref.cwd), active?.runtime.session.sessionId ?? ref.id]));
|
|
2836
|
+
if (pending !== undefined)
|
|
2837
|
+
return pending.promise;
|
|
2567
2838
|
if (active !== undefined)
|
|
2568
2839
|
return active;
|
|
2569
2840
|
const archived = await this.getArchived(ref);
|
|
@@ -2581,13 +2852,13 @@ export class PiSessionService {
|
|
|
2581
2852
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
2582
2853
|
}
|
|
2583
2854
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2584
|
-
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2585
|
-
if (active !== undefined)
|
|
2586
|
-
return Promise.resolve(active);
|
|
2587
2855
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
2588
2856
|
const existing = this.pendingSessionOpens.get(key);
|
|
2589
2857
|
if (existing !== undefined)
|
|
2590
2858
|
return existing.promise;
|
|
2859
|
+
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2860
|
+
if (active !== undefined)
|
|
2861
|
+
return Promise.resolve(active);
|
|
2591
2862
|
const pending = {
|
|
2592
2863
|
sessionId,
|
|
2593
2864
|
promise: this.create(openSessionManager(), cwd, options),
|
|
@@ -2667,18 +2938,27 @@ export class PiSessionService {
|
|
|
2667
2938
|
* first is both the slowest phase and one that can fail on its own; a clear
|
|
2668
2939
|
* that only ran for the later phases would leave a stale label behind.
|
|
2669
2940
|
*/
|
|
2670
|
-
|
|
2941
|
+
create(sessionManager, cwd, options = {}) {
|
|
2671
2942
|
const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken);
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2943
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionManager.getSessionId()]);
|
|
2944
|
+
// Early native observation must not admit ordinary operations before
|
|
2945
|
+
// session_start finishes. Track new sessions as well as on-demand opens so
|
|
2946
|
+
// close/dispose wait for startup before tearing down its runtime.
|
|
2947
|
+
const pending = {
|
|
2948
|
+
sessionId: sessionManager.getSessionId(),
|
|
2949
|
+
promise: this.createSessionRuntime(sessionManager, cwd, options, startup),
|
|
2950
|
+
};
|
|
2951
|
+
pending.promise = pending.promise.finally(() => {
|
|
2676
2952
|
startup.end();
|
|
2677
|
-
|
|
2953
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
2954
|
+
this.pendingSessionOpens.delete(key);
|
|
2955
|
+
});
|
|
2956
|
+
this.pendingSessionOpens.set(key, pending);
|
|
2957
|
+
return pending.promise;
|
|
2678
2958
|
}
|
|
2679
2959
|
async createSessionRuntime(sessionManager, cwd, options, startup) {
|
|
2680
2960
|
startup.report(STARTUP_PHASE_RUNTIME);
|
|
2681
|
-
const delegationToolsEnabled = options.creationProvenance
|
|
2961
|
+
const delegationToolsEnabled = options.creationProvenance === undefined
|
|
2682
2962
|
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
2683
2963
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
2684
2964
|
cwd,
|
|
@@ -2690,6 +2970,11 @@ export class PiSessionService {
|
|
|
2690
2970
|
});
|
|
2691
2971
|
const active = { runtime, unsubscribe: noop };
|
|
2692
2972
|
let boundSession = runtime.session;
|
|
2973
|
+
const startupSignal = options.startupSignal;
|
|
2974
|
+
const cancelStartup = () => { this.endSessionExtensionDialogs(boundSession.sessionId); };
|
|
2975
|
+
if (startupSignal !== undefined && !startupSignal.aborted) {
|
|
2976
|
+
startupSignal.addEventListener("abort", cancelStartup, { once: true });
|
|
2977
|
+
}
|
|
2693
2978
|
let notificationGeneration = options.notificationGeneration;
|
|
2694
2979
|
let notificationOwnership = options.notifications === "disabled"
|
|
2695
2980
|
? "disabled"
|
|
@@ -2712,15 +2997,20 @@ export class PiSessionService {
|
|
|
2712
2997
|
if (notificationGeneration !== undefined)
|
|
2713
2998
|
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
2714
2999
|
try {
|
|
3000
|
+
startupSignal?.throwIfAborted();
|
|
2715
3001
|
if (options.creationProvenance === "tracked-subsession") {
|
|
2716
3002
|
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
2717
3003
|
}
|
|
2718
3004
|
else {
|
|
2719
3005
|
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
2720
3006
|
}
|
|
3007
|
+
startupSignal?.throwIfAborted();
|
|
2721
3008
|
startup.report(STARTUP_PHASE_EXTENSIONS);
|
|
2722
|
-
|
|
3009
|
+
// session_start hooks use the same native agent as ordinary prompts.
|
|
3010
|
+
// Observe them before binding extensions, including startup messages.
|
|
2723
3011
|
this.bindRuntime(active);
|
|
3012
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration, startupSignal);
|
|
3013
|
+
startupSignal?.throwIfAborted();
|
|
2724
3014
|
runtime.setRebindSession(async (session) => {
|
|
2725
3015
|
const priorGeneration = notificationGeneration;
|
|
2726
3016
|
let candidateGeneration;
|
|
@@ -2752,6 +3042,7 @@ export class PiSessionService {
|
|
|
2752
3042
|
throw error;
|
|
2753
3043
|
}
|
|
2754
3044
|
});
|
|
3045
|
+
startupSignal?.throwIfAborted();
|
|
2755
3046
|
this.active.set(runtime.session.sessionId, active);
|
|
2756
3047
|
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
2757
3048
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
@@ -2795,13 +3086,16 @@ export class PiSessionService {
|
|
|
2795
3086
|
}
|
|
2796
3087
|
throw error;
|
|
2797
3088
|
}
|
|
3089
|
+
finally {
|
|
3090
|
+
startupSignal?.removeEventListener("abort", cancelStartup);
|
|
3091
|
+
}
|
|
2798
3092
|
}
|
|
2799
|
-
async bindSessionExtensions(session, generation) {
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
//
|
|
2803
|
-
//
|
|
2804
|
-
//
|
|
3093
|
+
async bindSessionExtensions(session, generation, startupSignal) {
|
|
3094
|
+
const startupDialogs = { signal: startupSignal };
|
|
3095
|
+
const uiContext = this.sessionUiContext(session, generation, startupDialogs);
|
|
3096
|
+
// A session_start hook can park this bind on a browser dialog. Keep
|
|
3097
|
+
// startup lookup and cancellation available for the duration of the bind,
|
|
3098
|
+
// including paths which resolve a dialog before session creation returns.
|
|
2805
3099
|
this.startupSessions.set(session.sessionId, session);
|
|
2806
3100
|
try {
|
|
2807
3101
|
await session.bindExtensions({
|
|
@@ -2815,13 +3109,16 @@ export class PiSessionService {
|
|
|
2815
3109
|
});
|
|
2816
3110
|
}
|
|
2817
3111
|
finally {
|
|
3112
|
+
// Even a captured startup UI context must not carry plugin cancellation
|
|
3113
|
+
// into later user turns after startup has finished.
|
|
3114
|
+
startupDialogs.signal = undefined;
|
|
2818
3115
|
this.startupSessions.delete(session.sessionId);
|
|
2819
3116
|
}
|
|
2820
3117
|
}
|
|
2821
3118
|
replaceSessionNotificationContext(session, generation) {
|
|
2822
3119
|
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
2823
3120
|
}
|
|
2824
|
-
sessionUiContext(session, generation) {
|
|
3121
|
+
sessionUiContext(session, generation, startupDialogs) {
|
|
2825
3122
|
const baseUiContext = session.extensionRunner.getUIContext();
|
|
2826
3123
|
const notify = (message, type) => {
|
|
2827
3124
|
if (generation === undefined) {
|
|
@@ -2847,13 +3144,13 @@ export class PiSessionService {
|
|
|
2847
3144
|
if (property === "theme")
|
|
2848
3145
|
return plainTextTheme;
|
|
2849
3146
|
if (property === "confirm") {
|
|
2850
|
-
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
|
|
3147
|
+
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs?.signal);
|
|
2851
3148
|
}
|
|
2852
3149
|
if (property === "select") {
|
|
2853
|
-
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
|
|
3150
|
+
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs?.signal);
|
|
2854
3151
|
}
|
|
2855
3152
|
if (property === "input") {
|
|
2856
|
-
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
|
|
3153
|
+
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs?.signal);
|
|
2857
3154
|
}
|
|
2858
3155
|
const value = Reflect.get(target, property, receiver);
|
|
2859
3156
|
return value;
|
|
@@ -2895,7 +3192,7 @@ export class PiSessionService {
|
|
|
2895
3192
|
// now rather than after the durable flush: it reads in-memory unread state
|
|
2896
3193
|
// and must not lag behind the rows the browser is about to see.
|
|
2897
3194
|
if (mutations.length > 0)
|
|
2898
|
-
this.onUnreadChanged?.();
|
|
3195
|
+
this.onUnreadChanged?.(mutations.some(({ event }) => event.unread !== null));
|
|
2899
3196
|
this.enqueueUnreadMutations(mutations);
|
|
2900
3197
|
this.unreadPublicationFlushRequested = true;
|
|
2901
3198
|
if (this.unreadPublication === undefined && this.unreadPublicationRetryTimer !== undefined) {
|
|
@@ -2975,6 +3272,16 @@ export class PiSessionService {
|
|
|
2975
3272
|
this.unreadPublicationRetryTimer = undefined;
|
|
2976
3273
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2977
3274
|
}
|
|
3275
|
+
connectSessionEvents(ref, lifetime) {
|
|
3276
|
+
const active = this.active.get(ref.id);
|
|
3277
|
+
if (active?.runtime.session.sessionId !== ref.id
|
|
3278
|
+
|| !cwdPathsEqual(active.runtime.session.sessionManager.getCwd(), ref.cwd)) {
|
|
3279
|
+
throw new Error("Selected session is not hosted in this workspace on this machine");
|
|
3280
|
+
}
|
|
3281
|
+
if (this.startupSessions.has(ref.id))
|
|
3282
|
+
throw new Error("Selected session is still initializing its extensions");
|
|
3283
|
+
return this.sessionEvents.connect(active.runtime.session, lifetime);
|
|
3284
|
+
}
|
|
2978
3285
|
bindRuntime(active, session = active.runtime.session) {
|
|
2979
3286
|
active.unsubscribe();
|
|
2980
3287
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
@@ -2984,10 +3291,28 @@ export class PiSessionService {
|
|
|
2984
3291
|
this.clearCompactionPromptQueue(sessionId);
|
|
2985
3292
|
}
|
|
2986
3293
|
}
|
|
2987
|
-
|
|
2988
|
-
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
2989
|
-
this.publishActivityForEvent(session, event);
|
|
3294
|
+
const unsubscribe = session.subscribe((event) => {
|
|
2990
3295
|
const eventType = getString(event, "type");
|
|
3296
|
+
const message = getProperty(event, "message");
|
|
3297
|
+
if ((eventType === "message_start" || eventType === "message_update") && getString(message, "role") === "assistant") {
|
|
3298
|
+
// SDK state advances before async extension hooks finish. Capture only
|
|
3299
|
+
// published messages, detaching even blocks left intact by projection.
|
|
3300
|
+
this.publishedAssistantPartials.set(session, structuredClone(annotateAssistantThinkingLevel(projectBrowserMessage(message), session.thinkingLevel)));
|
|
3301
|
+
}
|
|
3302
|
+
else if (eventType === "message_start" || eventType === "message_end" || eventType === "agent_end") {
|
|
3303
|
+
this.publishedAssistantPartials.delete(session);
|
|
3304
|
+
}
|
|
3305
|
+
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), { id: session.sessionId, cwd: session.sessionManager.getCwd() });
|
|
3306
|
+
this.publishActivityForEvent(session, event);
|
|
3307
|
+
// Queued messages can reach the model after an ask opened, even though
|
|
3308
|
+
// there was no ask to dismiss when the user originally submitted them.
|
|
3309
|
+
if (eventType === "message_start" && isRecord(event) && getString(event["message"], "role") === "user") {
|
|
3310
|
+
void this.voidOpenAskForUserMessage(session).catch((error) => {
|
|
3311
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3312
|
+
this.publishActivity(session, "error", "error", message);
|
|
3313
|
+
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
2991
3316
|
if (eventType === "agent_end")
|
|
2992
3317
|
this.abortRunScopedExtensionDialogs(session.sessionId);
|
|
2993
3318
|
if (eventType === "compaction_end")
|
|
@@ -2995,8 +3320,13 @@ export class PiSessionService {
|
|
|
2995
3320
|
if (eventType === "agent_start" || eventType === "agent_end")
|
|
2996
3321
|
this.scheduleCompactionQueueDrain(session.sessionId);
|
|
2997
3322
|
this.publishStatus(session);
|
|
2998
|
-
this.updateSubsessionTracking(session);
|
|
3323
|
+
this.updateSubsessionTracking(session, eventType === "agent_start");
|
|
2999
3324
|
});
|
|
3325
|
+
active.unsubscribe = () => {
|
|
3326
|
+
this.sessionEvents.close(session);
|
|
3327
|
+
unsubscribe();
|
|
3328
|
+
this.publishedAssistantPartials.delete(session);
|
|
3329
|
+
};
|
|
3000
3330
|
this.active.set(session.sessionId, active);
|
|
3001
3331
|
}
|
|
3002
3332
|
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
@@ -3158,6 +3488,7 @@ export class PiSessionService {
|
|
|
3158
3488
|
// the session still reports active work transiently, so the event-driven
|
|
3159
3489
|
// latch may not fire. The heartbeat re-checks once the session settles.
|
|
3160
3490
|
this.updateSubsessionTracking(session);
|
|
3491
|
+
void this.refreshActivityMarker(session);
|
|
3161
3492
|
const activity = this.activities.get(session.sessionId);
|
|
3162
3493
|
if (!this.hasActiveWork(session)) {
|
|
3163
3494
|
if (activity?.phase === "active")
|
|
@@ -3384,7 +3715,18 @@ export class PiSessionService {
|
|
|
3384
3715
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
3385
3716
|
this.observeUnreadActivityState(session);
|
|
3386
3717
|
}
|
|
3718
|
+
async refreshActivityMarker(session) {
|
|
3719
|
+
if (this.active.get(session.sessionId)?.runtime.session !== session)
|
|
3720
|
+
return;
|
|
3721
|
+
const previous = this.activityMarker.isActiveElsewhere(session.sessionFile);
|
|
3722
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
3723
|
+
if (this.active.get(session.sessionId)?.runtime.session === session
|
|
3724
|
+
&& previous !== this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3725
|
+
this.publishStatus(session);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3387
3728
|
publishStatus(session) {
|
|
3729
|
+
void this.refreshActivityMarker(session);
|
|
3388
3730
|
const status = this.statusFromSession(session);
|
|
3389
3731
|
this.clearStaleActiveActivity(session);
|
|
3390
3732
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
@@ -3438,6 +3780,13 @@ export class PiSessionService {
|
|
|
3438
3780
|
warningsForSession(session) {
|
|
3439
3781
|
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
3440
3782
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3783
|
+
if (this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3784
|
+
warnings.push({
|
|
3785
|
+
severity: "info",
|
|
3786
|
+
message: "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once.",
|
|
3787
|
+
source: "PI-WEB",
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3441
3790
|
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
3442
3791
|
if (anthropic !== undefined)
|
|
3443
3792
|
warnings.push(anthropic);
|
|
@@ -3535,7 +3884,7 @@ function clientSessionFromListEntry(session) {
|
|
|
3535
3884
|
created: session.created.toISOString(),
|
|
3536
3885
|
modified: session.modified.toISOString(),
|
|
3537
3886
|
messageCount: session.messageCount,
|
|
3538
|
-
firstMessage: session.firstMessage,
|
|
3887
|
+
firstMessage: clientSessionFirstMessagePreview(session.firstMessage),
|
|
3539
3888
|
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
3540
3889
|
};
|
|
3541
3890
|
}
|
|
@@ -3638,7 +3987,7 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3638
3987
|
created,
|
|
3639
3988
|
modified,
|
|
3640
3989
|
messageCount,
|
|
3641
|
-
firstMessage,
|
|
3990
|
+
firstMessage: clientSessionFirstMessagePreview(firstMessage),
|
|
3642
3991
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
3643
3992
|
archived: true,
|
|
3644
3993
|
archivedAt: record.archivedAt,
|
|
@@ -3649,13 +3998,6 @@ function addSessionName(names, name) {
|
|
|
3649
3998
|
if (trimmed !== undefined && trimmed !== "")
|
|
3650
3999
|
names.add(trimmed);
|
|
3651
4000
|
}
|
|
3652
|
-
function compareArchivedRecords(a, b) {
|
|
3653
|
-
return archivedTimestamp(b) - archivedTimestamp(a);
|
|
3654
|
-
}
|
|
3655
|
-
function archivedTimestamp(record) {
|
|
3656
|
-
const time = Date.parse(record.archivedAt);
|
|
3657
|
-
return Number.isNaN(time) ? 0 : time;
|
|
3658
|
-
}
|
|
3659
4001
|
function isDefined(value) {
|
|
3660
4002
|
return value !== undefined;
|
|
3661
4003
|
}
|
|
@@ -3875,49 +4217,9 @@ function buildPromptOptions(behavior, images) {
|
|
|
3875
4217
|
options.images = images;
|
|
3876
4218
|
return Object.keys(options).length > 0 ? options : undefined;
|
|
3877
4219
|
}
|
|
3878
|
-
function stringValue(value) {
|
|
3879
|
-
return typeof value === "string" ? value : "";
|
|
3880
|
-
}
|
|
3881
|
-
/**
|
|
3882
|
-
* Attach the thinking level in effect when an assistant message was generated,
|
|
3883
|
-
* so chat bubbles can show it next to the model. Non-assistant messages pass
|
|
3884
|
-
* through by reference; assistant messages are copied only when a level is set.
|
|
3885
|
-
* "off" is the absence of thinking, not a level worth labeling on every bubble.
|
|
3886
|
-
*/
|
|
3887
|
-
function annotateAssistantThinkingLevel(message, thinkingLevel) {
|
|
3888
|
-
if (thinkingLevel === undefined || thinkingLevel === "" || thinkingLevel === "off")
|
|
3889
|
-
return message;
|
|
3890
|
-
if (!isRecord(message) || message["role"] !== "assistant")
|
|
3891
|
-
return message;
|
|
3892
|
-
return { ...message, thinkingLevel };
|
|
3893
|
-
}
|
|
3894
4220
|
function historyMessages(session) {
|
|
3895
4221
|
return historyMessagesFromEntries(session.sessionManager.getBranch());
|
|
3896
4222
|
}
|
|
3897
|
-
function historyMessagesFromEntries(entries) {
|
|
3898
|
-
const messages = [];
|
|
3899
|
-
// Pi records the initial level at session creation and every later change, so
|
|
3900
|
-
// walking the branch yields the level in effect for each assistant message.
|
|
3901
|
-
let thinkingLevel;
|
|
3902
|
-
for (const entry of entries) {
|
|
3903
|
-
if (!isRecord(entry))
|
|
3904
|
-
continue;
|
|
3905
|
-
if (entry["type"] === "message")
|
|
3906
|
-
messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
|
|
3907
|
-
else if (entry["type"] === "thinking_level_change") {
|
|
3908
|
-
const level = getString(entry, "thinkingLevel");
|
|
3909
|
-
if (level !== undefined)
|
|
3910
|
-
thinkingLevel = level;
|
|
3911
|
-
}
|
|
3912
|
-
else if (entry["type"] === "custom_message" && entry["display"] === true)
|
|
3913
|
-
messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
|
|
3914
|
-
else if (entry["type"] === "compaction")
|
|
3915
|
-
messages.push({ role: "system", source: "compaction", content: `Compacted history:\n\n${stringValue(entry["summary"])}` });
|
|
3916
|
-
else if (entry["type"] === "branch_summary")
|
|
3917
|
-
messages.push({ role: "system", source: "branch_summary", content: `Branch summary:\n\n${stringValue(entry["summary"])}` });
|
|
3918
|
-
}
|
|
3919
|
-
return messages;
|
|
3920
|
-
}
|
|
3921
4223
|
function transcriptMessageCount(entries) {
|
|
3922
4224
|
let count = 0;
|
|
3923
4225
|
for (const entry of entries) {
|
|
@@ -4007,6 +4309,13 @@ function toClientEvent(event, thinkingLevel) {
|
|
|
4007
4309
|
const result = getProperty(event, "result");
|
|
4008
4310
|
return { type: "tool.end", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(result), content: toolResultContent(result), details: toolResultDetails(result), isError: getBoolean(event, "isError") === true };
|
|
4009
4311
|
}
|
|
4312
|
+
if (eventType === "entry_appended") {
|
|
4313
|
+
// Boundary drafts do not emit message_end. Use the same raw-history
|
|
4314
|
+
// projection as reconnects: context edits must not rewrite the transcript.
|
|
4315
|
+
const [message] = historyMessagesFromEntries([getProperty(event, "entry")]);
|
|
4316
|
+
if (message !== undefined)
|
|
4317
|
+
return { type: "message.append", message };
|
|
4318
|
+
}
|
|
4010
4319
|
if (eventType === "agent_start")
|
|
4011
4320
|
return { type: "agent.start" };
|
|
4012
4321
|
if (eventType === "agent_end")
|