@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.
Files changed (196) hide show
  1. package/README.md +3 -3
  2. package/dist/.plugins-ready +1 -0
  3. package/dist/cli.js +5 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client/assets/index-AroYaCLx.js +4130 -0
  6. package/dist/client/assets/vendor-editor-core-YWog6mAR.js +10 -0
  7. package/dist/client/assets/vendor-editor-languages-DOCw3vc9.js +28 -0
  8. package/dist/client/index.html +4 -4
  9. package/dist/docker/piWebDockerCommandPlan.js +9 -1
  10. package/dist/docker/piWebDockerCommandPlan.js.map +1 -1
  11. package/dist/nativeServices/installedServiceDefinitions.js +24 -17
  12. package/dist/nativeServices/installedServiceDefinitions.js.map +1 -1
  13. package/dist/nativeServices/serviceAction.js +38 -26
  14. package/dist/nativeServices/serviceAction.js.map +1 -1
  15. package/dist/nativeServices/servicePlan.js +3 -3
  16. package/dist/nativeServices/servicePlan.js.map +1 -1
  17. package/dist/pi-packages/captains-log/README.md +11 -0
  18. package/dist/pi-packages/captains-log/dist/browser/channelProtocol.js +29 -0
  19. package/dist/pi-packages/captains-log/dist/browser/index.js +144 -0
  20. package/dist/pi-packages/captains-log/dist/browser/markdown.js +67 -0
  21. package/dist/pi-packages/captains-log/dist/browser/panel.js +105 -0
  22. package/dist/pi-packages/captains-log/dist/browser/protocol.js +26 -0
  23. package/dist/pi-packages/captains-log/dist/companion.js +126 -0
  24. package/dist/pi-packages/captains-log/dist/roundtrip.js +62 -0
  25. package/dist/pi-packages/captains-log/dist/server.js +210 -0
  26. package/dist/pi-packages/captains-log/dist/store.js +75 -0
  27. package/dist/pi-packages/captains-log/docs/usage.md +30 -0
  28. package/dist/pi-packages/captains-log/package.json +35 -0
  29. package/dist/pi-packages/captains-log/tsconfig.json +21 -0
  30. package/dist/pi-packages/captains-log/vite.config.mjs +28 -0
  31. package/dist/pi-packages/relays/pi-web-plugin.js +1 -1
  32. package/dist/pi-packages/relays/prompts/relay.md +1 -1
  33. package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +21 -13
  34. package/dist/pi-web-plugins/files/browser/assets/files-icon-DZObYhxb.svg +3 -0
  35. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +439 -0
  36. package/dist/pi-web-plugins/files/package.json +15 -0
  37. package/dist/pi-web-plugins/git/browser/git-panel.js +54 -24
  38. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +1 -1
  39. package/dist/pi-web-plugins/git/git-backend.js +5 -5
  40. package/dist/pi-web-plugins/git/server-plugin.js +5 -3
  41. package/dist/pi-web-plugins/info/pi-web-plugin.js +1 -1
  42. package/dist/pi-web-plugins/mermaid/browser/mermaid-engine.js +3590 -0
  43. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +6 -0
  44. package/dist/pi-web-plugins/mermaid/package.json +13 -0
  45. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +210 -0
  46. package/dist/pi-web-plugins/terminal/package.json +16 -0
  47. package/dist/pi-web-plugins/terminal/server-plugin.js +365 -0
  48. package/dist/{server/terminals → pi-web-plugins/terminal}/terminalService.js +159 -79
  49. package/dist/pi-web-plugins/updates/pi-web-plugin.js +1 -1
  50. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +1 -1
  51. package/dist/piWebVersionReport.js +12 -4
  52. package/dist/piWebVersionReport.js.map +1 -1
  53. package/dist/plugin-api.d.ts +222 -23
  54. package/dist/plugin-api.js +1 -0
  55. package/dist/pluginRecoveryCli.js +3 -2
  56. package/dist/pluginRecoveryCli.js.map +1 -1
  57. package/dist/server/activity/workspaceActivityService.js.map +1 -1
  58. package/dist/server/app.js +20 -9
  59. package/dist/server/app.js.map +1 -1
  60. package/dist/server/knownPiPackages.js +12 -0
  61. package/dist/server/knownPiPackages.js.map +1 -0
  62. package/dist/server/machines/machineClient.js +2 -2
  63. package/dist/server/machines/machineClient.js.map +1 -1
  64. package/dist/server/machines/machinePluginProxyRoutes.js +53 -11
  65. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  66. package/dist/server/machines/machineProxyRoutes.js +70 -7
  67. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  68. package/dist/server/notices/serverNoticeStore.js +45 -4
  69. package/dist/server/notices/serverNoticeStore.js.map +1 -1
  70. package/dist/server/piPackageService.js +5 -5
  71. package/dist/server/piPackageService.js.map +1 -1
  72. package/dist/server/piWebPluginCatalog.js +46 -12
  73. package/dist/server/piWebPluginCatalog.js.map +1 -1
  74. package/dist/server/piWebPluginLifecycle.js +26 -3
  75. package/dist/server/piWebPluginLifecycle.js.map +1 -1
  76. package/dist/server/piWebPluginService.js +37 -2
  77. package/dist/server/piWebPluginService.js.map +1 -1
  78. package/dist/server/pluginCallbackDrain.js +35 -0
  79. package/dist/server/pluginCallbackDrain.js.map +1 -0
  80. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js +84 -0
  81. package/dist/server/plugins/pluginBackendChannelProxyAdmission.js.map +1 -0
  82. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js +234 -0
  83. package/dist/server/plugins/pluginBackendChannelProxyCoordinator.js.map +1 -0
  84. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js +52 -0
  85. package/dist/server/plugins/pluginBackendChannelProxyRoutes.js.map +1 -0
  86. package/dist/server/plugins/pluginBackendProxyRoutes.js +38 -31
  87. package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
  88. package/dist/server/plugins/pluginBackendRegistry.js +740 -0
  89. package/dist/server/plugins/pluginBackendRegistry.js.map +1 -0
  90. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js +34 -0
  91. package/dist/server/plugins/serverPluginPiSessionEventsCapability.js.map +1 -0
  92. package/dist/server/plugins/serverPluginPiSessionsCapability.js +185 -0
  93. package/dist/server/plugins/serverPluginPiSessionsCapability.js.map +1 -0
  94. package/dist/server/plugins/serverPluginRuntime.js +1028 -107
  95. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  96. package/dist/server/plugins/serverPluginWorkspacesCapability.js +114 -0
  97. package/dist/server/plugins/serverPluginWorkspacesCapability.js.map +1 -0
  98. package/dist/server/sessiond/pluginBackendChannelRoutes.js +393 -0
  99. package/dist/server/sessiond/pluginBackendChannelRoutes.js.map +1 -0
  100. package/dist/server/sessiond/pluginBackendRoutes.js +10 -7
  101. package/dist/server/sessiond/pluginBackendRoutes.js.map +1 -1
  102. package/dist/server/sessiond/sessionDaemonShutdown.js +9 -2
  103. package/dist/server/sessiond/sessionDaemonShutdown.js.map +1 -1
  104. package/dist/server/sessiond.js +71 -21
  105. package/dist/server/sessiond.js.map +1 -1
  106. package/dist/server/sessions/builtinCommands.js +2 -2
  107. package/dist/server/sessions/builtinCommands.js.map +1 -1
  108. package/dist/server/sessions/clientSessionPreview.js +14 -0
  109. package/dist/server/sessions/clientSessionPreview.js.map +1 -0
  110. package/dist/server/sessions/piSessionEventConnections.js +65 -0
  111. package/dist/server/sessions/piSessionEventConnections.js.map +1 -0
  112. package/dist/server/sessions/piSessionService.js +269 -86
  113. package/dist/server/sessions/piSessionService.js.map +1 -1
  114. package/dist/server/sessions/sessionActivityMarker.js +105 -0
  115. package/dist/server/sessions/sessionActivityMarker.js.map +1 -0
  116. package/dist/server/sessions/sessionEnvironmentFacts.js +5 -4
  117. package/dist/server/sessions/sessionEnvironmentFacts.js.map +1 -1
  118. package/dist/server/sessions/sessionNameGenerator.js +3 -2
  119. package/dist/server/sessions/sessionNameGenerator.js.map +1 -1
  120. package/dist/server/sessions/sessionRoutes.js +18 -0
  121. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  122. package/dist/server/sessions/spawnSessionTool.js +1 -1
  123. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  124. package/dist/server/sessions/spawnSubsessionTool.js +1 -1
  125. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  126. package/dist/server/sessions/transcriptMessages.js +54 -0
  127. package/dist/server/sessions/transcriptMessages.js.map +1 -0
  128. package/dist/server/terminals/requiredTerminalService.js +103 -0
  129. package/dist/server/terminals/requiredTerminalService.js.map +1 -0
  130. package/dist/server/webSocketBridge.js +339 -0
  131. package/dist/server/webSocketBridge.js.map +1 -1
  132. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +23 -3
  133. package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
  134. package/dist/server/workspaces/workspaceCatalog.js +3 -1
  135. package/dist/server/workspaces/workspaceCatalog.js.map +1 -1
  136. package/dist/server/workspaces/workspaceProviderRegistry.js +64 -149
  137. package/dist/server/workspaces/workspaceProviderRegistry.js.map +1 -1
  138. package/dist/server/workspaces/workspaceRemovalService.js +23 -1
  139. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
  140. package/dist/server-plugin-api.d.ts +216 -22
  141. package/dist/server-plugin-api.js +319 -2
  142. package/dist/server-plugin-api.js.map +1 -1
  143. package/dist/serverPluginRecovery.js +4 -0
  144. package/dist/serverPluginRecovery.js.map +1 -1
  145. package/dist/sessiond/sessionDaemonClient.js +3 -3
  146. package/dist/sessiond/sessionDaemonClient.js.map +1 -1
  147. package/dist/shared/apiTypes.js +1 -1
  148. package/dist/shared/apiTypes.js.map +1 -1
  149. package/dist/shared/federatedRoutes.js +45 -17
  150. package/dist/shared/federatedRoutes.js.map +1 -1
  151. package/dist/shared/machinePluginIds.js +28 -7
  152. package/dist/shared/machinePluginIds.js.map +1 -1
  153. package/dist/shared/pluginApiTypes.d.ts +21 -1
  154. package/dist/shared/pluginApiTypes.js +0 -1
  155. package/dist/shared/pluginBackendProtocol.js +163 -2
  156. package/dist/shared/pluginBackendProtocol.js.map +1 -1
  157. package/dist/shared/pluginIds.js +8 -1
  158. package/dist/shared/pluginIds.js.map +1 -1
  159. package/dist/shared/requiredTerminalPlugin.js +6 -0
  160. package/dist/shared/requiredTerminalPlugin.js.map +1 -0
  161. package/dist/shared/serverNoticeContract.js +88 -0
  162. package/dist/shared/serverNoticeContract.js.map +1 -0
  163. package/dist/shared/sessionDefaults.js +41 -0
  164. package/dist/shared/sessionDefaults.js.map +1 -0
  165. package/docs/config.md +50 -36
  166. package/docs/plugins.md +146 -1217
  167. package/examples/session-bridge-plugin/README.md +20 -0
  168. package/examples/session-bridge-plugin/docs/usage.md +54 -0
  169. package/examples/session-bridge-plugin/package.json +26 -0
  170. package/examples/session-bridge-plugin/src/browser/index.ts +84 -0
  171. package/examples/session-bridge-plugin/src/browser/protocol.ts +24 -0
  172. package/examples/session-bridge-plugin/src/companion.ts +65 -0
  173. package/examples/session-bridge-plugin/src/reviewRun.ts +39 -0
  174. package/examples/session-bridge-plugin/src/server.ts +85 -0
  175. package/examples/session-bridge-plugin/src/store.ts +57 -0
  176. package/examples/session-bridge-plugin/tsconfig.json +21 -0
  177. package/examples/workspace-provider-plugin/README.md +4 -4
  178. package/examples/workspace-provider-plugin/package.json +1 -1
  179. package/examples/workspace-provider-plugin/src/browser/index.ts +9 -9
  180. package/examples/workspace-provider-plugin/src/server.ts +15 -11
  181. package/package.json +17 -11
  182. package/dist/client/assets/CodeViewer-CGAlg9S8.js +0 -4
  183. package/dist/client/assets/TerminalPanel-xhJRhOas.js +0 -187
  184. package/dist/client/assets/index-DXQKhn1P.js +0 -4326
  185. package/dist/client/assets/vendor-editor-core-CXO8gGab.js +0 -12
  186. package/dist/client/assets/vendor-editor-languages-CpW4sJsX.js +0 -46
  187. package/dist/client/assets/vendor-editor-legacy-CYBnW6ZU.js +0 -1
  188. package/dist/client/assets/vendor-terminal-BrP-ENHg.css +0 -1
  189. package/dist/client/assets/vendor-terminal-D8k4UKM2.js +0 -35
  190. package/dist/server/terminalProxyRoutes.js +0 -141
  191. package/dist/server/terminalProxyRoutes.js.map +0 -1
  192. package/dist/server/terminals/terminalRoutes.js +0 -155
  193. package/dist/server/terminals/terminalRoutes.js.map +0 -1
  194. package/dist/server/terminals/terminalService.js.map +0 -1
  195. package/dist/server/terminals/terminalSize.js +0 -17
  196. 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
- ...(resourceLoaderOptions === undefined ? {} : { resourceLoaderOptions }),
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.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRuntime, this.sessionManager, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), !subsessionsActive ? undefined : {
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
- ...(options.parentSession === undefined ? {} : { parentSessionPath: options.parentSession }),
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({ name: template.name, ...(template.description === undefined ? {} : { description: template.description }), source: "prompt" });
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)).catch((error) => {
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(inputs);
2486
+ return this.archiveStore.archiveMany(oldestFirst);
2327
2487
  const records = [];
2328
- for (const input of inputs)
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
- async create(sessionManager, cwd, options = {}) {
2834
+ create(sessionManager, cwd, options = {}) {
2671
2835
  const startup = this.startupProgress(sessionManager, options.startupIntent ?? "open", options.startupToken);
2672
- try {
2673
- return await this.createSessionRuntime(sessionManager, cwd, options, startup);
2674
- }
2675
- finally {
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 !== "tracked-subsession"
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
- await this.bindSessionExtensions(runtime.session, notificationGeneration);
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 uiContext = this.sessionUiContext(session, generation);
2801
- // A `session_start` hook can park this bind on a dialog the browser has
2802
- // not answered yet. On the initial create/open path the session becomes
2803
- // active only after this returns, so register it for the duration: the
2804
- // answer that unblocks startup has to be reachable while it waits.
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
- active.unsubscribe = session.subscribe((event) => {
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")