@jmfederico/pi-web 1.202609.0 → 1.202609.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 +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-AroYaCLx.js +4130 -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 +144 -0
- package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
- package/dist/pi-packages/captains-log/dist/browser/panel.js +105 -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 +30 -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 +222 -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 +20 -9
- package/dist/server/app.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 +70 -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/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/sessionDaemonShutdown.js +9 -2
- package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
- package/dist/server/sessiond.js +71 -21
- 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/piSessionService.js +269 -86
- 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/sessionNameGenerator.js +3 -2
- package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +18 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +1 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +1 -1
- 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/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 +3 -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 +45 -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/docs/config.md +50 -36
- package/docs/plugins.md +146 -1217
- 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 +17 -11
- 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,24 @@
|
|
|
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
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
7
|
+
import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
|
|
7
8
|
import { BUILTIN_COMMANDS } from "./builtinCommands.js";
|
|
8
9
|
import { SessionCommandService } from "./sessionCommandService.js";
|
|
10
|
+
import { SessionActivityMarker } from "./sessionActivityMarker.js";
|
|
9
11
|
import { projectSessionTree } from "./sessionTreeProjection.js";
|
|
10
12
|
import { SessionArchiveStore } from "./sessionArchiveStore.js";
|
|
11
13
|
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
|
|
14
|
+
import { PiSessionEventConnections } from "./piSessionEventConnections.js";
|
|
12
15
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
13
16
|
import { computeEditPreview } from "./editPreview.js";
|
|
14
17
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
15
18
|
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
16
19
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
17
20
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
21
|
+
import { parseSessionDefaults, parseSessionDefaultsUpdate } from "../../shared/sessionDefaults.js";
|
|
18
22
|
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
|
|
19
23
|
import { readSessionHeaderSummary } from "./sessionFileHeader.js";
|
|
20
24
|
import { createAskUserToolDefinition } from "./askUserTool.js";
|
|
@@ -25,6 +29,7 @@ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS } from "../../config.js";
|
|
|
25
29
|
import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
|
|
26
30
|
import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
|
|
27
31
|
import { buildTranscriptView } from "./subsessionTranscript.js";
|
|
32
|
+
import { annotateAssistantThinkingLevel, historyMessagesFromEntries } from "./transcriptMessages.js";
|
|
28
33
|
import { planSessionCleanup, summarizeSessionCleanupExecution } from "./sessionCleanup.js";
|
|
29
34
|
import { SessionNotificationStore, } from "./sessionNotificationStore.js";
|
|
30
35
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
@@ -422,7 +427,7 @@ export function piWebResourceLoaderOptions(appendSystemPromptSections) {
|
|
|
422
427
|
return undefined;
|
|
423
428
|
return { appendSystemPromptOverride: (base) => [...base, ...appendSystemPromptSections] };
|
|
424
429
|
}
|
|
425
|
-
function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
430
|
+
function createDefaultRuntimeFactory(sessionEvents, modelRuntime, sessionManagers, spawn, subsessions, askUser, appendSystemPromptSections = []) {
|
|
426
431
|
const resourceLoaderOptions = piWebResourceLoaderOptions(appendSystemPromptSections);
|
|
427
432
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled }) => {
|
|
428
433
|
// PI WEB always honors pi's project-trust model. When the workspace ships
|
|
@@ -435,6 +440,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
435
440
|
// browser trust prompt, an untrusted project's resources are skipped
|
|
436
441
|
// (matching `pi` run without a UI). Projects without trust-requiring
|
|
437
442
|
// resources skip resolution entirely and are trusted, as before.
|
|
443
|
+
const eventBus = createEventBus();
|
|
438
444
|
const projectTrustRequiring = hasTrustRequiringProjectResources(cwd);
|
|
439
445
|
const settingsManager = SettingsManager.create(cwd, agentDir, { projectTrusted: !projectTrustRequiring });
|
|
440
446
|
// Pre-session-creation trust failures (`project_trust` handler errors)
|
|
@@ -446,7 +452,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
446
452
|
agentDir,
|
|
447
453
|
modelRuntime,
|
|
448
454
|
settingsManager,
|
|
449
|
-
|
|
455
|
+
resourceLoaderOptions: { ...resourceLoaderOptions, eventBus },
|
|
450
456
|
...(projectTrustRequiring
|
|
451
457
|
? {
|
|
452
458
|
resourceLoaderReloadOptions: {
|
|
@@ -480,6 +486,7 @@ function createDefaultRuntimeFactory(modelRuntime, sessionManagers, spawn, subse
|
|
|
480
486
|
...(modelOptions.thinkingLevel === undefined ? {} : { thinkingLevel: modelOptions.thinkingLevel }),
|
|
481
487
|
...(modelOptions.scopedModels.length === 0 ? {} : { scopedModels: modelOptions.scopedModels }),
|
|
482
488
|
});
|
|
489
|
+
sessionEvents.register(result.session, eventBus);
|
|
483
490
|
return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
|
|
484
491
|
};
|
|
485
492
|
}
|
|
@@ -575,6 +582,10 @@ export class PiSessionService {
|
|
|
575
582
|
this.spawnTargets = deps.spawnTargets;
|
|
576
583
|
this.logger = deps.logger ?? noopLogger;
|
|
577
584
|
this.now = deps.now ?? (() => new Date());
|
|
585
|
+
this.activityMarker = new SessionActivityMarker({
|
|
586
|
+
now: () => this.now().getTime(),
|
|
587
|
+
onError: (error) => { this.logger.info({ err: error }, "Could not update advisory session activity marker"); },
|
|
588
|
+
});
|
|
578
589
|
this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
|
|
579
590
|
this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
|
|
580
591
|
this.onUnreadChanged = deps.onUnreadChanged;
|
|
@@ -588,7 +599,8 @@ export class PiSessionService {
|
|
|
588
599
|
// Subsessions are gated behind their own flag, and they
|
|
589
600
|
// also require the spawn capability (they share its project-scope resolver).
|
|
590
601
|
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
|
|
591
|
-
this.
|
|
602
|
+
this.sessionEvents = deps.sessionEvents ?? new PiSessionEventConnections();
|
|
603
|
+
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.sessionEvents, this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
|
|
592
604
|
spawn: (input) => this.spawnSubsession(input),
|
|
593
605
|
list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
|
|
594
606
|
check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
|
|
@@ -732,7 +744,7 @@ export class PiSessionService {
|
|
|
732
744
|
finally {
|
|
733
745
|
await active.runtime.dispose();
|
|
734
746
|
}
|
|
735
|
-
}));
|
|
747
|
+
})).finally(() => this.activityMarker.dispose());
|
|
736
748
|
await this.publishUnreadMutations([]);
|
|
737
749
|
}
|
|
738
750
|
async list(cwd) {
|
|
@@ -748,14 +760,48 @@ export class PiSessionService {
|
|
|
748
760
|
this.workspaceActivity?.reconcileSessionActivity(cwd, reconcilableSessionIds);
|
|
749
761
|
await this.publishUnreadMutations(this.unreadStore.reconcileCwd(canonicalizeStoredCwd(cwd), reconcilableSessionIds));
|
|
750
762
|
const archivedSessions = archivedForCwd
|
|
751
|
-
.sort(compareArchivedRecords)
|
|
752
763
|
.map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
|
|
753
|
-
.filter(isDefined)
|
|
764
|
+
.filter(isDefined)
|
|
765
|
+
.sort((a, b) => Date.parse(b.modified) - Date.parse(a.modified));
|
|
754
766
|
return [...unarchivedSessions, ...archivedSessions];
|
|
755
767
|
}
|
|
756
768
|
async start(cwd, options = {}) {
|
|
757
769
|
return this.startSession(cwd, options);
|
|
758
770
|
}
|
|
771
|
+
/** Publish a host-owned conversation without submitting an initial prompt. */
|
|
772
|
+
async createHostedSession(cwd, signal) {
|
|
773
|
+
const created = await this.startManagedSession(cwd, signal);
|
|
774
|
+
return Object.freeze({ id: created.id });
|
|
775
|
+
}
|
|
776
|
+
/** Atomically start and prompt one visible host-owned, non-delegating session. */
|
|
777
|
+
async startOneShotRun(cwd, text, signal) {
|
|
778
|
+
const promptText = requirePromptText(text);
|
|
779
|
+
return this.startManagedSession(cwd, signal, promptText);
|
|
780
|
+
}
|
|
781
|
+
async startManagedSession(cwd, signal, promptText) {
|
|
782
|
+
signal.throwIfAborted();
|
|
783
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd, { startupIntent: "create", creationProvenance: "host-one-shot", startupSignal: signal });
|
|
784
|
+
const { session } = active.runtime;
|
|
785
|
+
try {
|
|
786
|
+
signal.throwIfAborted();
|
|
787
|
+
if (promptText !== undefined)
|
|
788
|
+
this.maybeGenerateSessionName(session, promptText);
|
|
789
|
+
const created = this.announceCreatedSession(active, cwd);
|
|
790
|
+
// Publication transfers lifetime ownership to normal hosting. Plugin
|
|
791
|
+
// cancellation after this point must not abort this or later user work.
|
|
792
|
+
const completion = promptText === undefined ? Promise.resolve() : this.submitInitialRunPrompt(session, promptText);
|
|
793
|
+
return Object.freeze({ id: created.id, completion });
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
try {
|
|
797
|
+
await this.closeActive(session.sessionId);
|
|
798
|
+
}
|
|
799
|
+
catch (cleanupError) {
|
|
800
|
+
throw new AggregateError([error, cleanupError], `Failed to clean up managed session ${session.sessionId} after startup failed`, { cause: cleanupError });
|
|
801
|
+
}
|
|
802
|
+
throw error;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
759
805
|
async startSession(cwd, options) {
|
|
760
806
|
const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
|
|
761
807
|
startupIntent: "create",
|
|
@@ -764,6 +810,9 @@ export class PiSessionService {
|
|
|
764
810
|
...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
|
|
765
811
|
...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
|
|
766
812
|
});
|
|
813
|
+
return this.announceCreatedSession(active, cwd, options.parentSession);
|
|
814
|
+
}
|
|
815
|
+
announceCreatedSession(active, cwd, parentSession) {
|
|
767
816
|
const { session } = active.runtime;
|
|
768
817
|
const created = {
|
|
769
818
|
id: session.sessionId,
|
|
@@ -776,7 +825,7 @@ export class PiSessionService {
|
|
|
776
825
|
firstMessage: "",
|
|
777
826
|
// Include the parent so listeners can nest the new session in the tree
|
|
778
827
|
// immediately, instead of showing it flat until the next reload.
|
|
779
|
-
...(
|
|
828
|
+
...(parentSession === undefined ? {} : { parentSessionPath: parentSession }),
|
|
780
829
|
};
|
|
781
830
|
// Broadcast so other clients (and the spawning agent's UI) can add the new
|
|
782
831
|
// session to their list without a manual reload.
|
|
@@ -1114,8 +1163,12 @@ export class PiSessionService {
|
|
|
1114
1163
|
* `async` so a rejected dialog becomes a rejection rather than a synchronous
|
|
1115
1164
|
* throw from a promise-returning method.
|
|
1116
1165
|
*/
|
|
1117
|
-
async openExtensionDialog(session, request, opts) {
|
|
1118
|
-
const signal = opts?.signal
|
|
1166
|
+
async openExtensionDialog(session, request, opts, startupSignal) {
|
|
1167
|
+
const signal = opts?.signal === undefined
|
|
1168
|
+
? startupSignal
|
|
1169
|
+
: startupSignal === undefined
|
|
1170
|
+
? opts.signal
|
|
1171
|
+
: AbortSignal.any([opts.signal, startupSignal]);
|
|
1119
1172
|
// A pre-aborted signal dismisses the dialog before it ever opens.
|
|
1120
1173
|
if (signal?.aborted === true)
|
|
1121
1174
|
return extensionDialogCancelValue(request.kind);
|
|
@@ -1543,6 +1596,7 @@ export class PiSessionService {
|
|
|
1543
1596
|
}
|
|
1544
1597
|
async status(ref) {
|
|
1545
1598
|
const session = await this.sessionForStatusOrDialogClose(ref);
|
|
1599
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
1546
1600
|
if (this.hasActiveWork(session))
|
|
1547
1601
|
return this.statusFromSession(session);
|
|
1548
1602
|
const branch = await this.readableSessionBranch(ref, session);
|
|
@@ -1567,6 +1621,47 @@ export class PiSessionService {
|
|
|
1567
1621
|
: annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
|
|
1568
1622
|
return { seq, partial };
|
|
1569
1623
|
}
|
|
1624
|
+
async getSessionDefaults(ref) {
|
|
1625
|
+
await this.getOrOpen(ref);
|
|
1626
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1627
|
+
await settings.reload();
|
|
1628
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1629
|
+
// Do not use merged getters: workspace overrides are not global pins.
|
|
1630
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1631
|
+
}
|
|
1632
|
+
async setSessionDefaults(ref, defaults) {
|
|
1633
|
+
const update = parseSessionDefaultsUpdate({ ...defaults });
|
|
1634
|
+
await this.assertWritable(ref);
|
|
1635
|
+
const session = await this.getOrOpen(ref);
|
|
1636
|
+
return this.runModelScopeMutation(async () => {
|
|
1637
|
+
// A separate manager avoids mutating the active session's settings cache.
|
|
1638
|
+
const settings = SettingsManager.create(ref.cwd, this.agentDir);
|
|
1639
|
+
await settings.reload();
|
|
1640
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1641
|
+
if (update.provider !== undefined && update.modelId !== undefined) {
|
|
1642
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1643
|
+
const target = `${update.provider}/${update.modelId}`;
|
|
1644
|
+
const enabledIds = await resolveEnabledModelIds({ settingsManager: settings, modelRuntime: session.modelRuntime, scopedModels: [] });
|
|
1645
|
+
if (!session.modelRuntime.getAvailableSnapshot().some((model) => modelScopeId(model) === target)) {
|
|
1646
|
+
throw new Error(`Model not found: ${target}`);
|
|
1647
|
+
}
|
|
1648
|
+
if (enabledIds !== null && !enabledIds.includes(target))
|
|
1649
|
+
throw new Error(`Model is not enabled: ${target}`);
|
|
1650
|
+
settings.setDefaultModelAndProvider(update.provider, update.modelId);
|
|
1651
|
+
}
|
|
1652
|
+
else if (update.thinkingLevel !== undefined) {
|
|
1653
|
+
settings.setDefaultThinkingLevel(update.thinkingLevel);
|
|
1654
|
+
}
|
|
1655
|
+
await settings.flush();
|
|
1656
|
+
this.assertDefaultsSettingsHealthy(settings);
|
|
1657
|
+
return parseSessionDefaults(settings.getGlobalSettings());
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
assertDefaultsSettingsHealthy(settings) {
|
|
1661
|
+
const errors = settings.drainErrors();
|
|
1662
|
+
if (errors.length > 0)
|
|
1663
|
+
throw new Error(`Session defaults settings failed: ${errors.map(({ error }) => error.message).join("; ")}`);
|
|
1664
|
+
}
|
|
1570
1665
|
async availableModels(ref) {
|
|
1571
1666
|
const session = await this.getOrOpen(ref);
|
|
1572
1667
|
const models = await this.sessionModelCandidates(session);
|
|
@@ -1700,7 +1795,12 @@ export class PiSessionService {
|
|
|
1700
1795
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
1701
1796
|
}
|
|
1702
1797
|
for (const template of session.promptTemplates) {
|
|
1703
|
-
commands.push({
|
|
1798
|
+
commands.push({
|
|
1799
|
+
name: template.name,
|
|
1800
|
+
...(template.description === undefined ? {} : { description: template.description }),
|
|
1801
|
+
...(template.argumentHint === undefined ? {} : { argumentHint: template.argumentHint }),
|
|
1802
|
+
source: "prompt",
|
|
1803
|
+
});
|
|
1704
1804
|
}
|
|
1705
1805
|
for (const skill of session.resourceLoader.getSkills().skills) {
|
|
1706
1806
|
commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
|
|
@@ -1740,18 +1840,69 @@ export class PiSessionService {
|
|
|
1740
1840
|
void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
|
|
1741
1841
|
}
|
|
1742
1842
|
submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1843
|
+
return this.beginPromptSubmission(session, text, behavior, images, echoUserMessage).catch(() => undefined);
|
|
1844
|
+
}
|
|
1845
|
+
beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
|
|
1743
1846
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
1744
1847
|
if (behavior === undefined && echoUserMessage)
|
|
1745
1848
|
this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
|
|
1746
1849
|
const promptOptions = buildPromptOptions(behavior, images);
|
|
1747
|
-
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions))
|
|
1850
|
+
const promptPromise = this.runSessionEntryMutation(session, "send a prompt", () => session.prompt(text, promptOptions));
|
|
1851
|
+
void promptPromise.catch((error) => {
|
|
1748
1852
|
const message = error instanceof Error ? error.message : String(error);
|
|
1749
1853
|
this.publishActivity(session, "error", "error", message);
|
|
1750
1854
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
1751
1855
|
});
|
|
1752
|
-
void promptPromise;
|
|
1753
1856
|
return promptPromise;
|
|
1754
1857
|
}
|
|
1858
|
+
async submitInitialRunPrompt(session, text) {
|
|
1859
|
+
let failure;
|
|
1860
|
+
let settled = false;
|
|
1861
|
+
let sawInitialUserMessage = false;
|
|
1862
|
+
const unsubscribe = session.subscribe((event) => {
|
|
1863
|
+
if (settled || !isRecord(event))
|
|
1864
|
+
return;
|
|
1865
|
+
if (event["type"] === "agent_settled") {
|
|
1866
|
+
// Freeze the initial outcome before a later user submission can change it.
|
|
1867
|
+
settled = true;
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (event["type"] === "auto_retry_end" && event["success"] === false && event["finalError"] === "Retry cancelled") {
|
|
1871
|
+
// Native Pi cancels retry sleep without an aborted assistant message.
|
|
1872
|
+
failure = new DOMException("Initial PI session run was aborted", "AbortError");
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
const message = event["message"];
|
|
1876
|
+
if (!isRecord(message))
|
|
1877
|
+
return;
|
|
1878
|
+
if (event["type"] === "message_start" && message["role"] === "user") {
|
|
1879
|
+
// Pi can drain a queued user follow-up inside the same prompt promise.
|
|
1880
|
+
// Its outcome belongs to that later submission, not this launcher.
|
|
1881
|
+
if (sawInitialUserMessage)
|
|
1882
|
+
settled = true;
|
|
1883
|
+
sawInitialUserMessage = true;
|
|
1884
|
+
return;
|
|
1885
|
+
}
|
|
1886
|
+
if (event["type"] !== "message_end" || message["role"] !== "assistant")
|
|
1887
|
+
return;
|
|
1888
|
+
// A successful retry supersedes a transient provider failure. Pi's
|
|
1889
|
+
// prompt promise can fulfill even when the final assistant is an error.
|
|
1890
|
+
const reason = message["stopReason"];
|
|
1891
|
+
failure = reason === "aborted"
|
|
1892
|
+
? new DOMException("Initial PI session run was aborted", "AbortError")
|
|
1893
|
+
: reason === "error"
|
|
1894
|
+
? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : "Initial PI session run failed")
|
|
1895
|
+
: undefined;
|
|
1896
|
+
});
|
|
1897
|
+
try {
|
|
1898
|
+
await this.beginPromptSubmission(session, text, undefined);
|
|
1899
|
+
if (failure !== undefined)
|
|
1900
|
+
throw failure;
|
|
1901
|
+
}
|
|
1902
|
+
finally {
|
|
1903
|
+
unsubscribe();
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1755
1906
|
enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
|
|
1756
1907
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
1757
1908
|
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
|
|
@@ -1882,6 +2033,13 @@ export class PiSessionService {
|
|
|
1882
2033
|
}
|
|
1883
2034
|
return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
|
|
1884
2035
|
}
|
|
2036
|
+
if (session.sessionManager.getLeafId() !== oldLeafId) {
|
|
2037
|
+
// Questions belong to the position we left. Close them for every browser
|
|
2038
|
+
// without appending a cancellation message or waking the rewound session.
|
|
2039
|
+
const outcome = this.pendingAskStore.cancelOpen(session.sessionId);
|
|
2040
|
+
if (outcome !== undefined)
|
|
2041
|
+
this.publishAskClosed(session.sessionId, outcome);
|
|
2042
|
+
}
|
|
1885
2043
|
if (result.summaryEntry !== undefined) {
|
|
1886
2044
|
// A summary entry durably identifies the selected branch as the file's
|
|
1887
2045
|
// newest leaf, superseding any earlier bare selection.
|
|
@@ -2322,10 +2480,12 @@ export class PiSessionService {
|
|
|
2322
2480
|
async archiveStoreArchiveMany(inputs) {
|
|
2323
2481
|
if (inputs.length === 0)
|
|
2324
2482
|
return [];
|
|
2483
|
+
// Work from the bottom of the activity-ordered list, using metadata already loaded.
|
|
2484
|
+
const oldestFirst = [...inputs].sort((a, b) => Date.parse(a.modified) - Date.parse(b.modified));
|
|
2325
2485
|
if (this.archiveStore.archiveMany !== undefined)
|
|
2326
|
-
return this.archiveStore.archiveMany(
|
|
2486
|
+
return this.archiveStore.archiveMany(oldestFirst);
|
|
2327
2487
|
const records = [];
|
|
2328
|
-
for (const input of
|
|
2488
|
+
for (const input of oldestFirst)
|
|
2329
2489
|
records.push(await this.archiveStore.archive(input));
|
|
2330
2490
|
return records;
|
|
2331
2491
|
}
|
|
@@ -2473,6 +2633,7 @@ export class PiSessionService {
|
|
|
2473
2633
|
await this.abortSessionOperations(active.runtime.session);
|
|
2474
2634
|
}
|
|
2475
2635
|
finally {
|
|
2636
|
+
await this.activityMarker.release(active.runtime.session.sessionFile);
|
|
2476
2637
|
await active.runtime.dispose();
|
|
2477
2638
|
}
|
|
2478
2639
|
}
|
|
@@ -2564,6 +2725,9 @@ export class PiSessionService {
|
|
|
2564
2725
|
}
|
|
2565
2726
|
async getActive(ref, options = {}) {
|
|
2566
2727
|
const active = this.activeForRef(ref);
|
|
2728
|
+
const pending = this.pendingSessionOpens.get(JSON.stringify([canonicalizeStoredCwd(ref.cwd), active?.runtime.session.sessionId ?? ref.id]));
|
|
2729
|
+
if (pending !== undefined)
|
|
2730
|
+
return pending.promise;
|
|
2567
2731
|
if (active !== undefined)
|
|
2568
2732
|
return active;
|
|
2569
2733
|
const archived = await this.getArchived(ref);
|
|
@@ -2581,13 +2745,13 @@ export class PiSessionService {
|
|
|
2581
2745
|
return this.openExistingSession(match.id, match.cwd, () => this.sessionManager.open(match.path), options);
|
|
2582
2746
|
}
|
|
2583
2747
|
openExistingSession(sessionId, cwd, openSessionManager, options = {}) {
|
|
2584
|
-
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2585
|
-
if (active !== undefined)
|
|
2586
|
-
return Promise.resolve(active);
|
|
2587
2748
|
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionId]);
|
|
2588
2749
|
const existing = this.pendingSessionOpens.get(key);
|
|
2589
2750
|
if (existing !== undefined)
|
|
2590
2751
|
return existing.promise;
|
|
2752
|
+
const active = this.activeForRef({ id: sessionId, cwd });
|
|
2753
|
+
if (active !== undefined)
|
|
2754
|
+
return Promise.resolve(active);
|
|
2591
2755
|
const pending = {
|
|
2592
2756
|
sessionId,
|
|
2593
2757
|
promise: this.create(openSessionManager(), cwd, options),
|
|
@@ -2667,18 +2831,27 @@ export class PiSessionService {
|
|
|
2667
2831
|
* first is both the slowest phase and one that can fail on its own; a clear
|
|
2668
2832
|
* that only ran for the later phases would leave a stale label behind.
|
|
2669
2833
|
*/
|
|
2670
|
-
|
|
2834
|
+
create(sessionManager, cwd, options = {}) {
|
|
2671
2835
|
const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken);
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2836
|
+
const key = JSON.stringify([canonicalizeStoredCwd(cwd), sessionManager.getSessionId()]);
|
|
2837
|
+
// Early native observation must not admit ordinary operations before
|
|
2838
|
+
// session_start finishes. Track new sessions as well as on-demand opens so
|
|
2839
|
+
// close/dispose wait for startup before tearing down its runtime.
|
|
2840
|
+
const pending = {
|
|
2841
|
+
sessionId: sessionManager.getSessionId(),
|
|
2842
|
+
promise: this.createSessionRuntime(sessionManager, cwd, options, startup),
|
|
2843
|
+
};
|
|
2844
|
+
pending.promise = pending.promise.finally(() => {
|
|
2676
2845
|
startup.end();
|
|
2677
|
-
|
|
2846
|
+
if (this.pendingSessionOpens.get(key) === pending)
|
|
2847
|
+
this.pendingSessionOpens.delete(key);
|
|
2848
|
+
});
|
|
2849
|
+
this.pendingSessionOpens.set(key, pending);
|
|
2850
|
+
return pending.promise;
|
|
2678
2851
|
}
|
|
2679
2852
|
async createSessionRuntime(sessionManager, cwd, options, startup) {
|
|
2680
2853
|
startup.report(STARTUP_PHASE_RUNTIME);
|
|
2681
|
-
const delegationToolsEnabled = options.creationProvenance
|
|
2854
|
+
const delegationToolsEnabled = options.creationProvenance === undefined
|
|
2682
2855
|
&& await sessionAllowsDelegationTools(sessionManager, this.sessionManager);
|
|
2683
2856
|
const runtime = await this.createAgentRuntime(this.createRuntime, {
|
|
2684
2857
|
cwd,
|
|
@@ -2690,6 +2863,11 @@ export class PiSessionService {
|
|
|
2690
2863
|
});
|
|
2691
2864
|
const active = { runtime, unsubscribe: noop };
|
|
2692
2865
|
let boundSession = runtime.session;
|
|
2866
|
+
const startupSignal = options.startupSignal;
|
|
2867
|
+
const cancelStartup = () => { this.endSessionExtensionDialogs(boundSession.sessionId); };
|
|
2868
|
+
if (startupSignal !== undefined && !startupSignal.aborted) {
|
|
2869
|
+
startupSignal.addEventListener("abort", cancelStartup, { once: true });
|
|
2870
|
+
}
|
|
2693
2871
|
let notificationGeneration = options.notificationGeneration;
|
|
2694
2872
|
let notificationOwnership = options.notifications === "disabled"
|
|
2695
2873
|
? "disabled"
|
|
@@ -2712,15 +2890,20 @@ export class PiSessionService {
|
|
|
2712
2890
|
if (notificationGeneration !== undefined)
|
|
2713
2891
|
this.notificationGenerationBySession.set(runtime.session, notificationGeneration);
|
|
2714
2892
|
try {
|
|
2893
|
+
startupSignal?.throwIfAborted();
|
|
2715
2894
|
if (options.creationProvenance === "tracked-subsession") {
|
|
2716
2895
|
await this.publishUnreadMutations(this.unreadStore.excludeSession(runtime.session.sessionId, canonicalizeStoredCwd(runtime.session.sessionManager.getCwd())));
|
|
2717
2896
|
}
|
|
2718
2897
|
else {
|
|
2719
2898
|
await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
|
|
2720
2899
|
}
|
|
2900
|
+
startupSignal?.throwIfAborted();
|
|
2721
2901
|
startup.report(STARTUP_PHASE_EXTENSIONS);
|
|
2722
|
-
|
|
2902
|
+
// session_start hooks use the same native agent as ordinary prompts.
|
|
2903
|
+
// Observe them before binding extensions, including startup messages.
|
|
2723
2904
|
this.bindRuntime(active);
|
|
2905
|
+
await this.bindSessionExtensions(runtime.session, notificationGeneration, startupSignal);
|
|
2906
|
+
startupSignal?.throwIfAborted();
|
|
2724
2907
|
runtime.setRebindSession(async (session) => {
|
|
2725
2908
|
const priorGeneration = notificationGeneration;
|
|
2726
2909
|
let candidateGeneration;
|
|
@@ -2752,6 +2935,7 @@ export class PiSessionService {
|
|
|
2752
2935
|
throw error;
|
|
2753
2936
|
}
|
|
2754
2937
|
});
|
|
2938
|
+
startupSignal?.throwIfAborted();
|
|
2755
2939
|
this.active.set(runtime.session.sessionId, active);
|
|
2756
2940
|
if (notificationOwnership === "replacement" && notificationGeneration !== undefined) {
|
|
2757
2941
|
this.publishNotificationMutations(this.notificationStore.commitReplacement(notificationGeneration));
|
|
@@ -2795,13 +2979,16 @@ export class PiSessionService {
|
|
|
2795
2979
|
}
|
|
2796
2980
|
throw error;
|
|
2797
2981
|
}
|
|
2982
|
+
finally {
|
|
2983
|
+
startupSignal?.removeEventListener("abort", cancelStartup);
|
|
2984
|
+
}
|
|
2798
2985
|
}
|
|
2799
|
-
async bindSessionExtensions(session, generation) {
|
|
2800
|
-
const
|
|
2801
|
-
|
|
2802
|
-
//
|
|
2803
|
-
//
|
|
2804
|
-
//
|
|
2986
|
+
async bindSessionExtensions(session, generation, startupSignal) {
|
|
2987
|
+
const startupDialogs = { signal: startupSignal };
|
|
2988
|
+
const uiContext = this.sessionUiContext(session, generation, startupDialogs);
|
|
2989
|
+
// A session_start hook can park this bind on a browser dialog. Keep
|
|
2990
|
+
// startup lookup and cancellation available for the duration of the bind,
|
|
2991
|
+
// including paths which resolve a dialog before session creation returns.
|
|
2805
2992
|
this.startupSessions.set(session.sessionId, session);
|
|
2806
2993
|
try {
|
|
2807
2994
|
await session.bindExtensions({
|
|
@@ -2815,13 +3002,16 @@ export class PiSessionService {
|
|
|
2815
3002
|
});
|
|
2816
3003
|
}
|
|
2817
3004
|
finally {
|
|
3005
|
+
// Even a captured startup UI context must not carry plugin cancellation
|
|
3006
|
+
// into later user turns after startup has finished.
|
|
3007
|
+
startupDialogs.signal = undefined;
|
|
2818
3008
|
this.startupSessions.delete(session.sessionId);
|
|
2819
3009
|
}
|
|
2820
3010
|
}
|
|
2821
3011
|
replaceSessionNotificationContext(session, generation) {
|
|
2822
3012
|
session.extensionRunner.setUIContext(this.sessionUiContext(session, generation), "rpc");
|
|
2823
3013
|
}
|
|
2824
|
-
sessionUiContext(session, generation) {
|
|
3014
|
+
sessionUiContext(session, generation, startupDialogs) {
|
|
2825
3015
|
const baseUiContext = session.extensionRunner.getUIContext();
|
|
2826
3016
|
const notify = (message, type) => {
|
|
2827
3017
|
if (generation === undefined) {
|
|
@@ -2847,13 +3037,13 @@ export class PiSessionService {
|
|
|
2847
3037
|
if (property === "theme")
|
|
2848
3038
|
return plainTextTheme;
|
|
2849
3039
|
if (property === "confirm") {
|
|
2850
|
-
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
|
|
3040
|
+
return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts, startupDialogs?.signal);
|
|
2851
3041
|
}
|
|
2852
3042
|
if (property === "select") {
|
|
2853
|
-
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
|
|
3043
|
+
return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts, startupDialogs?.signal);
|
|
2854
3044
|
}
|
|
2855
3045
|
if (property === "input") {
|
|
2856
|
-
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
|
|
3046
|
+
return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts, startupDialogs?.signal);
|
|
2857
3047
|
}
|
|
2858
3048
|
const value = Reflect.get(target, property, receiver);
|
|
2859
3049
|
return value;
|
|
@@ -2975,6 +3165,16 @@ export class PiSessionService {
|
|
|
2975
3165
|
this.unreadPublicationRetryTimer = undefined;
|
|
2976
3166
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
2977
3167
|
}
|
|
3168
|
+
connectSessionEvents(ref, lifetime) {
|
|
3169
|
+
const active = this.active.get(ref.id);
|
|
3170
|
+
if (active?.runtime.session.sessionId !== ref.id
|
|
3171
|
+
|| !cwdPathsEqual(active.runtime.session.sessionManager.getCwd(), ref.cwd)) {
|
|
3172
|
+
throw new Error("Selected session is not hosted in this workspace on this machine");
|
|
3173
|
+
}
|
|
3174
|
+
if (this.startupSessions.has(ref.id))
|
|
3175
|
+
throw new Error("Selected session is still initializing its extensions");
|
|
3176
|
+
return this.sessionEvents.connect(active.runtime.session, lifetime);
|
|
3177
|
+
}
|
|
2978
3178
|
bindRuntime(active, session = active.runtime.session) {
|
|
2979
3179
|
active.unsubscribe();
|
|
2980
3180
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
@@ -2984,7 +3184,7 @@ export class PiSessionService {
|
|
|
2984
3184
|
this.clearCompactionPromptQueue(sessionId);
|
|
2985
3185
|
}
|
|
2986
3186
|
}
|
|
2987
|
-
|
|
3187
|
+
const unsubscribe = session.subscribe((event) => {
|
|
2988
3188
|
this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel));
|
|
2989
3189
|
this.publishActivityForEvent(session, event);
|
|
2990
3190
|
const eventType = getString(event, "type");
|
|
@@ -2997,6 +3197,10 @@ export class PiSessionService {
|
|
|
2997
3197
|
this.publishStatus(session);
|
|
2998
3198
|
this.updateSubsessionTracking(session);
|
|
2999
3199
|
});
|
|
3200
|
+
active.unsubscribe = () => {
|
|
3201
|
+
this.sessionEvents.close(session);
|
|
3202
|
+
unsubscribe();
|
|
3203
|
+
};
|
|
3000
3204
|
this.active.set(session.sessionId, active);
|
|
3001
3205
|
}
|
|
3002
3206
|
scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
|
|
@@ -3158,6 +3362,7 @@ export class PiSessionService {
|
|
|
3158
3362
|
// the session still reports active work transiently, so the event-driven
|
|
3159
3363
|
// latch may not fire. The heartbeat re-checks once the session settles.
|
|
3160
3364
|
this.updateSubsessionTracking(session);
|
|
3365
|
+
void this.refreshActivityMarker(session);
|
|
3161
3366
|
const activity = this.activities.get(session.sessionId);
|
|
3162
3367
|
if (!this.hasActiveWork(session)) {
|
|
3163
3368
|
if (activity?.phase === "active")
|
|
@@ -3384,7 +3589,18 @@ export class PiSessionService {
|
|
|
3384
3589
|
this.events.publishGlobal({ type: "activity.update", activity });
|
|
3385
3590
|
this.observeUnreadActivityState(session);
|
|
3386
3591
|
}
|
|
3592
|
+
async refreshActivityMarker(session) {
|
|
3593
|
+
if (this.active.get(session.sessionId)?.runtime.session !== session)
|
|
3594
|
+
return;
|
|
3595
|
+
const previous = this.activityMarker.isActiveElsewhere(session.sessionFile);
|
|
3596
|
+
await this.activityMarker.refresh(session.sessionFile, this.hasActiveWork(session));
|
|
3597
|
+
if (this.active.get(session.sessionId)?.runtime.session === session
|
|
3598
|
+
&& previous !== this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3599
|
+
this.publishStatus(session);
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3387
3602
|
publishStatus(session) {
|
|
3603
|
+
void this.refreshActivityMarker(session);
|
|
3388
3604
|
const status = this.statusFromSession(session);
|
|
3389
3605
|
this.clearStaleActiveActivity(session);
|
|
3390
3606
|
this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
|
|
@@ -3438,6 +3654,13 @@ export class PiSessionService {
|
|
|
3438
3654
|
warningsForSession(session) {
|
|
3439
3655
|
const runtime = this.active.get(session.sessionId)?.runtime;
|
|
3440
3656
|
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
|
|
3657
|
+
if (this.activityMarker.isActiveElsewhere(session.sessionFile)) {
|
|
3658
|
+
warnings.push({
|
|
3659
|
+
severity: "info",
|
|
3660
|
+
message: "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once.",
|
|
3661
|
+
source: "PI-WEB",
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3441
3664
|
const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
|
|
3442
3665
|
if (anthropic !== undefined)
|
|
3443
3666
|
warnings.push(anthropic);
|
|
@@ -3535,7 +3758,7 @@ function clientSessionFromListEntry(session) {
|
|
|
3535
3758
|
created: session.created.toISOString(),
|
|
3536
3759
|
modified: session.modified.toISOString(),
|
|
3537
3760
|
messageCount: session.messageCount,
|
|
3538
|
-
firstMessage: session.firstMessage,
|
|
3761
|
+
firstMessage: clientSessionFirstMessagePreview(session.firstMessage),
|
|
3539
3762
|
...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
|
|
3540
3763
|
};
|
|
3541
3764
|
}
|
|
@@ -3638,7 +3861,7 @@ function clientSessionFromArchivedRecord(record, fallback) {
|
|
|
3638
3861
|
created,
|
|
3639
3862
|
modified,
|
|
3640
3863
|
messageCount,
|
|
3641
|
-
firstMessage,
|
|
3864
|
+
firstMessage: clientSessionFirstMessagePreview(firstMessage),
|
|
3642
3865
|
...(parentSessionPath === undefined ? {} : { parentSessionPath }),
|
|
3643
3866
|
archived: true,
|
|
3644
3867
|
archivedAt: record.archivedAt,
|
|
@@ -3649,13 +3872,6 @@ function addSessionName(names, name) {
|
|
|
3649
3872
|
if (trimmed !== undefined && trimmed !== "")
|
|
3650
3873
|
names.add(trimmed);
|
|
3651
3874
|
}
|
|
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
3875
|
function isDefined(value) {
|
|
3660
3876
|
return value !== undefined;
|
|
3661
3877
|
}
|
|
@@ -3875,49 +4091,9 @@ function buildPromptOptions(behavior, images) {
|
|
|
3875
4091
|
options.images = images;
|
|
3876
4092
|
return Object.keys(options).length > 0 ? options : undefined;
|
|
3877
4093
|
}
|
|
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
4094
|
function historyMessages(session) {
|
|
3895
4095
|
return historyMessagesFromEntries(session.sessionManager.getBranch());
|
|
3896
4096
|
}
|
|
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
4097
|
function transcriptMessageCount(entries) {
|
|
3922
4098
|
let count = 0;
|
|
3923
4099
|
for (const entry of entries) {
|
|
@@ -4007,6 +4183,13 @@ function toClientEvent(event, thinkingLevel) {
|
|
|
4007
4183
|
const result = getProperty(event, "result");
|
|
4008
4184
|
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
4185
|
}
|
|
4186
|
+
if (eventType === "entry_appended") {
|
|
4187
|
+
// Boundary drafts do not emit message_end. Use the same raw-history
|
|
4188
|
+
// projection as reconnects: context edits must not rewrite the transcript.
|
|
4189
|
+
const [message] = historyMessagesFromEntries([getProperty(event, "entry")]);
|
|
4190
|
+
if (message !== undefined)
|
|
4191
|
+
return { type: "message.append", message };
|
|
4192
|
+
}
|
|
4010
4193
|
if (eventType === "agent_start")
|
|
4011
4194
|
return { type: "agent.start" };
|
|
4012
4195
|
if (eventType === "agent_end")
|