@chainingintention/pi-web-cn 1.202608.4 → 1.202608.6

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 (36) hide show
  1. package/dist/client/assets/{CodeViewer-CWHRo84w.js → CodeViewer-CWKEeX0r.js} +1 -1
  2. package/dist/client/assets/{TerminalPanel-CoYwbZrx.js → TerminalPanel-BmA7Ayzk.js} +1 -1
  3. package/dist/client/assets/{UnifiedDiffViewer-DbtJHQ4S.js → UnifiedDiffViewer-2DACwxFY.js} +1 -1
  4. package/dist/client/assets/{index-Bdbjyy7F.js → index-D6j67Izj.js} +493 -301
  5. package/dist/client/index.html +1 -1
  6. package/dist/config.js +17 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/pi-web-plugins/relays/relaysPanelElement.js +112 -46
  9. package/dist/server/piWebStatus.js +7 -1
  10. package/dist/server/piWebStatus.js.map +1 -1
  11. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  12. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  13. package/dist/server/sessiond.js +1 -0
  14. package/dist/server/sessiond.js.map +1 -1
  15. package/dist/server/sessions/extensionDialogWaiters.js +76 -0
  16. package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
  17. package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
  18. package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
  19. package/dist/server/sessions/piSessionService.js +391 -72
  20. package/dist/server/sessions/piSessionService.js.map +1 -1
  21. package/dist/server/sessions/sessionRoutes.js +48 -25
  22. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  23. package/dist/server/sessions/sessionUiEvents.js +9 -2
  24. package/dist/server/sessions/sessionUiEvents.js.map +1 -1
  25. package/dist/server/sessions/spawnSessionTool.js +8 -1
  26. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  27. package/dist/server/sessions/spawnSubsessionTool.js +7 -1
  28. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  29. package/dist/shared/apiTypes.d.ts +116 -1
  30. package/dist/shared/apiTypes.js +8 -0
  31. package/dist/shared/apiTypes.js.map +1 -1
  32. package/dist/shared/federatedRoutes.js +2 -0
  33. package/dist/shared/federatedRoutes.js.map +1 -1
  34. package/docs/config.md +21 -1
  35. package/docs/plugins.md +14 -0
  36. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { statSync } from "node:fs";
1
+ import { realpathSync, statSync } from "node:fs";
2
2
  import { readFile, writeFile } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ModelRuntime, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
@@ -23,10 +23,12 @@ import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
23
23
  import { readSessionHeaderSummary } from "./sessionFileHeader.js";
24
24
  import { countOutOfListingChildren, locateOutOfListingParents } from "./parentSessionLocator.js";
25
25
  import { siblingWorkspaceCwds } from "../workspaces/projectWorkspaceCwds.js";
26
- import { piWebDataDir } from "../../config.js";
26
+ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config.js";
27
27
  import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
28
28
  import { createAskUserToolDefinition } from "./askUserTool.js";
29
29
  import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
30
+ import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
31
+ import { ExtensionDialogWaiters, effectiveExtensionDialogTimeoutMs, extensionDialogCancelValue } from "./extensionDialogWaiters.js";
30
32
  import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
31
33
  import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
32
34
  import { buildTranscriptView } from "./subsessionTranscript.js";
@@ -37,7 +39,7 @@ import { SessionNotificationStore } from "./sessionNotificationStore.js";
37
39
  import { plainTextTheme } from "./plainTextTheme.js";
38
40
  import { SessionUnreadStore } from "./sessionUnreadStore.js";
39
41
  import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
40
- import { getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
42
+ import { annotateAssistantThinkingLevel, getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
41
43
  const noopLogger = { info() { } };
42
44
  const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
43
45
  /**
@@ -64,6 +66,28 @@ function spawnTargetError(decision) {
64
66
  return new Error("派生会话不在已注册项目中");
65
67
  return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
66
68
  }
69
+ function modelSpecOf(model) {
70
+ return `${model.provider}/${model.id}`;
71
+ }
72
+ /**
73
+ * Parse a strict `provider/model-id` spec: split on the first `/` (model ids
74
+ * may themselves contain `/`) and require both parts to be non-empty.
75
+ */
76
+ function parseModelSpec(spec) {
77
+ const slash = spec.indexOf("/");
78
+ if (slash <= 0 || slash === spec.length - 1)
79
+ return undefined;
80
+ return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
81
+ }
82
+ /**
83
+ * Error for a spawn-tool model spec that matched nothing. States the facts —
84
+ * the bad spec and the required format — with deliberately no model list
85
+ * (a list would invite guesses). The agent loop turns the throw into an
86
+ * error tool result; how to recover is the agent's call.
87
+ */
88
+ function unknownSpawnModelError(modelSpec) {
89
+ return new Error(`未找到模型“${modelSpec}”。请传入精确的“provider/model-id”。`);
90
+ }
67
91
  function authLossWarningKey(sessionId, eventScope, provider, modelId) {
68
92
  return `${eventScope}:${sessionId}:${provider}/${modelId}`;
69
93
  }
@@ -76,6 +100,9 @@ function isPiSessionRef(ref) {
76
100
  function lookupMatchesActiveSession(ref, active) {
77
101
  return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
78
102
  }
103
+ function lookupMatchesStartupSession(ref, session) {
104
+ return !isPiSessionRef(ref) || cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
105
+ }
79
106
  function activeSessionKey(sessionId, eventScope) {
80
107
  return `${eventScope}\0${sessionId}`;
81
108
  }
@@ -136,6 +163,20 @@ export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
136
163
  }),
137
164
  };
138
165
  }
166
+ export function filterManagedProjectSkills(cwd, base) {
167
+ const projectRoot = realpathSync(cwd);
168
+ return {
169
+ ...base,
170
+ skills: base.skills.filter((skill) => {
171
+ try {
172
+ return pathInsideOrEqual(projectRoot, realpathSync(skill.filePath));
173
+ }
174
+ catch {
175
+ return false;
176
+ }
177
+ }),
178
+ };
179
+ }
139
180
  function pathInsideOrEqual(parent, child) {
140
181
  const normalizedParent = resolve(parent);
141
182
  const normalizedChild = resolve(child);
@@ -248,33 +289,37 @@ export function dismissSessionWarning(session, dismissId) {
248
289
  function defaultCreateAgentRuntime(createRuntime, options) {
249
290
  if (!(options.sessionManager instanceof SessionManager))
250
291
  throw new Error("Default runtime creation requires an SDK SessionManager");
251
- const runtimeFactory = createRuntimeWithContextAndOneShotInitialModel(createRuntime, options.managementContext, options.initialModel, options.delegationToolsEnabled);
292
+ const runtimeFactory = createRuntimeWithContextAndOneShotSessionOptions(createRuntime, options.managementContext, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
252
293
  return createAgentSessionRuntime(runtimeFactory, {
253
294
  cwd: options.cwd,
254
295
  agentDir: options.agentDir,
255
296
  sessionManager: options.sessionManager,
256
297
  });
257
298
  }
258
- function createRuntimeWithContextAndOneShotInitialModel(createRuntime, managementContext, initialModel, delegationToolsEnabled) {
259
- // The inherited model belongs only to the session being spawned. Do not keep
260
- // reapplying it if that runtime later creates/forks/switches sessions itself.
299
+ function createRuntimeWithContextAndOneShotSessionOptions(createRuntime, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled) {
300
+ // These inherited options belong only to the session being spawned. A later
301
+ // runtime replacement restores its own model and thinking level from disk.
261
302
  let pendingInitialModel = initialModel;
303
+ let pendingInitialThinkingLevel = initialThinkingLevel;
262
304
  let pendingDelegationToolsEnabled = delegationToolsEnabled;
263
305
  return async (options) => {
264
306
  const model = pendingInitialModel;
307
+ const thinkingLevel = pendingInitialThinkingLevel;
265
308
  const toolsEnabled = pendingDelegationToolsEnabled;
266
309
  pendingInitialModel = undefined;
310
+ pendingInitialThinkingLevel = undefined;
267
311
  pendingDelegationToolsEnabled = undefined;
268
312
  return createRuntime({
269
313
  ...options,
270
314
  ...(managementContext === undefined ? {} : { managementContext }),
271
315
  ...(model === undefined ? {} : { initialModel: model }),
316
+ ...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
272
317
  ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
273
318
  });
274
319
  };
275
320
  }
276
321
  function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser) {
277
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, delegationToolsEnabled = true }) => {
322
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
278
323
  const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
279
324
  if (managementContext !== undefined) {
280
325
  return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext)({
@@ -284,6 +329,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
284
329
  delegationToolsEnabled,
285
330
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
286
331
  ...(initialModel === undefined ? {} : { initialModel }),
332
+ ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
287
333
  });
288
334
  }
289
335
  const settingsManager = await createScopedSettingsManager({
@@ -299,12 +345,13 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
299
345
  customTools,
300
346
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
301
347
  ...(initialModel === undefined ? {} : { model: initialModel }),
348
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
302
349
  });
303
350
  return { ...result, services, diagnostics: services.diagnostics };
304
351
  };
305
352
  }
306
353
  function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext) {
307
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled = true }) => {
354
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
308
355
  const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
309
356
  return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
310
357
  const settingsManager = await createScopedSettingsManager({
@@ -319,14 +366,24 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
319
366
  settingsManager,
320
367
  resourceLoaderOptions: {
321
368
  agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
369
+ skillsOverride: (base) => filterManagedProjectSkills(cwd, base),
322
370
  },
323
371
  });
324
372
  const managedToolOptions = createManagedAgentToolOptions(cwd);
373
+ const scopedSpawn = spawn === undefined
374
+ ? undefined
375
+ : (input) => spawn({ ...input, managementContext });
376
+ const scopedSubsessions = subsessions === undefined
377
+ ? undefined
378
+ : {
379
+ ...subsessions,
380
+ spawn: (input) => subsessions.spawn({ ...input, managementContext }),
381
+ };
325
382
  const customTools = [
326
383
  ...managedAgentToolDefinitions(cwd, managedToolOptions),
327
384
  createManagedPythonToolDefinition(cwd, managementContext),
328
- ...(delegationToolsEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
329
- ...(delegationToolsEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool)) : []),
385
+ ...(delegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
386
+ ...(delegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
330
387
  ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
331
388
  ];
332
389
  const options = {
@@ -336,6 +393,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
336
393
  customTools,
337
394
  tools: managementAgentToolNames(managementContext),
338
395
  ...(initialModel === undefined ? {} : { model: initialModel }),
396
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
339
397
  };
340
398
  const result = await createAgentSessionFromServices(options);
341
399
  return { ...result, services, diagnostics: services.diagnostics };
@@ -389,6 +447,8 @@ export class PiSessionService {
389
447
  this.active = new Map();
390
448
  this.runtimeBySession = new WeakMap();
391
449
  this.pendingSessionOpens = new Map();
450
+ /** Sessions whose extension binding is parked on a `session_start` dialog. */
451
+ this.startupSessions = new Map();
392
452
  this.activities = new SessionActivityCoordinator();
393
453
  /** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
394
454
  this.treeNavigations = new WeakSet();
@@ -419,6 +479,8 @@ export class PiSessionService {
419
479
  */
420
480
  this.subsessionNotifyArmed = new Map();
421
481
  this.notificationGenerationBySession = new WeakMap();
482
+ /** The parked extension Promise resolvers behind the store's open dialogs. */
483
+ this.dialogWaiters = new ExtensionDialogWaiters();
422
484
  this.pendingUnreadMutations = [];
423
485
  this.unreadPublicationFlushRequested = false;
424
486
  this.unreadPublicationStopped = false;
@@ -463,6 +525,8 @@ export class PiSessionService {
463
525
  this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
464
526
  this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
465
527
  this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
528
+ this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
529
+ this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
466
530
  this.catalogRefreshStatus = deps.catalogRefreshStatus;
467
531
  this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
468
532
  this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
@@ -563,6 +627,10 @@ export class PiSessionService {
563
627
  this.clearUnreadPublicationRetry();
564
628
  clearInterval(this.heartbeat);
565
629
  this.clearCompactionDrainTimers();
630
+ // Same startup-park hazard as closeActive(): settle `session_start` dialogs
631
+ // of sessions still binding extensions before awaiting their pending opens.
632
+ for (const startup of this.startupSessions.values())
633
+ this.endSessionExtensionDialogs(startup.session);
566
634
  const pendingOpens = this.pendingSessionOpenPromises();
567
635
  if (pendingOpens.length > 0)
568
636
  await Promise.allSettled(pendingOpens);
@@ -570,9 +638,11 @@ export class PiSessionService {
570
638
  for (const active of activeSessions) {
571
639
  this.forgetUnreadActivity(active.runtime.session);
572
640
  this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
641
+ this.endSessionExtensionDialogs(active.runtime.session);
573
642
  }
574
643
  this.active.clear();
575
644
  this.pendingSessionOpens.clear();
645
+ this.startupSessions.clear();
576
646
  this.activities.clear();
577
647
  this.compactionPromptQueues.clear();
578
648
  this.authLossWarnings.clear();
@@ -626,7 +696,7 @@ export class PiSessionService {
626
696
  return { sessionIds };
627
697
  }
628
698
  async setPinned(ref, pinned, managementContext) {
629
- if (typeof ref === "string" || ref.cwd === "")
699
+ if (ref.cwd === "")
630
700
  throw new Error("置顶会话必须提供工作区");
631
701
  const sessions = await this.list(ref.cwd, managementContext);
632
702
  const session = sessions.find((candidate) => candidate.id === ref.id);
@@ -730,6 +800,7 @@ export class PiSessionService {
730
800
  const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
731
801
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
732
802
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
803
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
733
804
  ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
734
805
  ...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
735
806
  startupIntent: "create",
@@ -764,10 +835,27 @@ export class PiSessionService {
764
835
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
765
836
  if (!decision.allowed)
766
837
  throw spawnTargetError(decision);
767
- const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
768
- await this.prompt(created.id, input.prompt);
838
+ // A model spec overrides the inherited model. Only a spec triggers a
839
+ // spawning-session lookup; the default path must not depend on it.
840
+ let model = input.model;
841
+ if (input.modelSpec !== undefined) {
842
+ model = await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec, input.managementContext);
843
+ }
844
+ const created = await this.start(decision.cwd, {
845
+ ...(model === undefined ? {} : { initialModel: model }),
846
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
847
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
848
+ });
849
+ const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
850
+ await this.prompt(created.id, input.prompt, undefined, undefined, {
851
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
852
+ });
769
853
  this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
770
- return { sessionId: created.id, cwd: decision.cwd };
854
+ return {
855
+ sessionId: created.id,
856
+ cwd: decision.cwd,
857
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
858
+ };
771
859
  }
772
860
  /**
773
861
  * Start a *tracked* child session on behalf of a LLM. Identical to
@@ -777,15 +865,23 @@ export class PiSessionService {
777
865
  */
778
866
  async spawnSubsession(input) {
779
867
  if (this.spawnTargets === undefined)
780
- throw new Error("Spawning sessions is disabled");
868
+ throw new Error("派生会话已禁用");
781
869
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
782
870
  if (!decision.allowed)
783
871
  throw spawnTargetError(decision);
872
+ // A model spec overrides the inherited model and is resolved against the
873
+ // parent's model runtime; only a spec triggers that lookup.
874
+ const model = input.modelSpec === undefined
875
+ ? input.model
876
+ : await this.resolveSpawnModel({ id: input.parentSessionId, cwd: input.spawningCwd }, input.modelSpec, input.managementContext);
784
877
  const created = await this.startSession(decision.cwd, {
785
878
  ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
786
- ...(input.model === undefined ? {} : { initialModel: input.model }),
879
+ ...(model === undefined ? {} : { initialModel: model }),
880
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
881
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
787
882
  creationProvenance: "tracked-subsession",
788
883
  });
884
+ const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
789
885
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
790
886
  const link = {
791
887
  parentSessionId: input.parentSessionId,
@@ -797,9 +893,45 @@ export class PiSessionService {
797
893
  await this.registerVerifiedSubsession(link);
798
894
  this.persistSubsessionLink(link);
799
895
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
800
- await this.prompt(created.id, input.prompt);
896
+ await this.prompt(created.id, input.prompt, undefined, undefined, {
897
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
898
+ });
801
899
  this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
802
- return { sessionId: created.id, cwd: decision.cwd };
900
+ return {
901
+ sessionId: created.id,
902
+ cwd: decision.cwd,
903
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
904
+ };
905
+ }
906
+ /**
907
+ * The models a session may pick from: its scoped set when model-scoped,
908
+ * otherwise the runtime's available snapshot. Refreshes the runtime catalog
909
+ * first so callers see newly configured providers and models.
910
+ */
911
+ async sessionModelCandidates(session) {
912
+ await session.modelRuntime.refresh();
913
+ return session.scopedModels.length > 0
914
+ ? session.scopedModels.map((scoped) => scoped.model)
915
+ : await session.modelRuntime.getAvailable();
916
+ }
917
+ /**
918
+ * Resolve a strict `provider/model-id` spec from a spawn tool against the
919
+ * *spawning* session's model runtime, using the same candidates
920
+ * {@link setModel} offers plus a direct runtime lookup as fallback. Unknown
921
+ * or malformed specs throw; the agent loop turns that into an error tool
922
+ * result the spawning agent can retry from.
923
+ */
924
+ async resolveSpawnModel(spawningSession, modelSpec, managementContext) {
925
+ const session = await this.getOrOpen(spawningSession, managementContext);
926
+ const parsed = parseModelSpec(modelSpec);
927
+ const candidates = await this.sessionModelCandidates(session);
928
+ const model = parsed === undefined
929
+ ? undefined
930
+ : candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
931
+ ?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
932
+ if (model === undefined)
933
+ throw unknownSpawnModelError(modelSpec);
934
+ return model;
803
935
  }
804
936
  /**
805
937
  * Register the question set an agent wants the user to answer as the session's
@@ -880,14 +1012,129 @@ export class PiSessionService {
880
1012
  this.publishStatus(session);
881
1013
  }
882
1014
  /**
883
- * Publish status for a session known only by id, as the ask tools are: they
884
- * run inside the session's own runtime, so the active entry is the session.
1015
+ * Record the user's answer to an open extension dialog and resolve the
1016
+ * extension's parked Promise with it. Unlike an ask, nothing is delivered to
1017
+ * the model: the waiter is extension code inside an already in-flight run
1018
+ * (or an idle handler), so no custom message and no turn are triggered.
885
1019
  */
886
- publishStatusForSessionId(sessionId) {
887
- const session = this.active.get(sessionId)?.runtime.session;
888
- if (session !== undefined)
1020
+ async answerDialog(ref, dialogId, value, managementContext) {
1021
+ await this.assertWritable(ref);
1022
+ const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
1023
+ const result = this.pendingExtensionDialogStore.answer(this.dialogSessionKey(session), dialogId, value);
1024
+ if (result.status === "stale")
1025
+ return { result: "stale", sessionStatus: this.statusFromSession(session) };
1026
+ const { outcome } = result;
1027
+ this.publishDialogClosed(session, outcome);
1028
+ // `value` is what the store validated and recorded as the outcome's answer.
1029
+ this.dialogWaiters.settleWithAnswer(dialogId, value);
1030
+ this.publishStatus(session);
1031
+ return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1032
+ }
1033
+ /** Close an open extension dialog without an answer; the extension's wait settles with its kind's cancel value. */
1034
+ async cancelDialog(ref, dialogId, managementContext) {
1035
+ await this.assertWritable(ref);
1036
+ const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
1037
+ const result = this.pendingExtensionDialogStore.cancel(this.dialogSessionKey(session), dialogId, "cancelled");
1038
+ if (result.status === "stale")
1039
+ return { result: "stale", sessionStatus: this.statusFromSession(session) };
1040
+ const { outcome } = result;
1041
+ this.publishDialogClosed(session, outcome);
1042
+ this.dialogWaiters.settleWithCancelValue(dialogId);
1043
+ this.publishStatus(session);
1044
+ return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1045
+ }
1046
+ /**
1047
+ * Implement one `ctx.ui.select()`/`confirm()`/`input()` call from extension
1048
+ * code: open the store record, tell the browsers, and park a Promise that
1049
+ * settles when the browser answers or cancels, the extension's own
1050
+ * `signal`/`timeout` dismisses the dialog, the daemon default timeout
1051
+ * elapses, or the runtime goes away. `store.open` validates the dialog, so a
1052
+ * malformed one rejects the extension's call rather than rendering garbage.
1053
+ * `async` so a rejected dialog becomes a rejection rather than a synchronous
1054
+ * throw from a promise-returning method.
1055
+ */
1056
+ async openExtensionDialog(session, request, opts) {
1057
+ const signal = opts?.signal;
1058
+ // A pre-aborted signal dismisses the dialog before it ever opens.
1059
+ if (signal?.aborted === true)
1060
+ return extensionDialogCancelValue(request.kind);
1061
+ const timeoutMs = effectiveExtensionDialogTimeoutMs(opts?.timeout, this.extensionDialogsTimeoutMs);
1062
+ const dialog = this.pendingExtensionDialogStore.open({
1063
+ sessionId: this.dialogSessionKey(session),
1064
+ kind: request.kind,
1065
+ title: request.title,
1066
+ ...(request.message === undefined ? {} : { message: request.message }),
1067
+ ...(request.options === undefined ? {} : { options: request.options }),
1068
+ ...(request.placeholder === undefined ? {} : { placeholder: request.placeholder }),
1069
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1070
+ runScoped: session.isStreaming,
1071
+ });
1072
+ this.events.publish(session.sessionId, { type: "dialog.opened", dialog }, this.eventScopeForSession(session));
1073
+ this.publishStatus(session);
1074
+ return this.dialogWaiters.park(dialog, {
1075
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1076
+ ...(signal === undefined ? {} : { signal }),
1077
+ onTrigger: (reason) => {
1078
+ if (this.closeExtensionDialogFromTrigger(session, dialog.dialogId, reason))
1079
+ this.publishStatus(session);
1080
+ },
1081
+ });
1082
+ }
1083
+ /**
1084
+ * Close a dialog whose wait ended without the browser (timeout, signal
1085
+ * abort, run end, runtime teardown) and settle its parked Promise. Returns
1086
+ * whether this call closed the dialog; a stale close means a browser answer
1087
+ * or an earlier trigger already settled everything.
1088
+ */
1089
+ closeExtensionDialogFromTrigger(session, dialogId, reason) {
1090
+ const result = this.pendingExtensionDialogStore.cancel(this.dialogSessionKey(session), dialogId, reason);
1091
+ if (result.status !== "closed")
1092
+ return false;
1093
+ this.publishDialogClosed(session, result.outcome);
1094
+ this.dialogWaiters.settleWithCancelValue(dialogId);
1095
+ return true;
1096
+ }
1097
+ /**
1098
+ * Settle the session's run-scoped dialogs as `"aborted"`. Runs at
1099
+ * abort-request time (a user abort parks the agent loop behind the dialog
1100
+ * handler, so `agent_end` would never arrive on its own) and again from
1101
+ * the `agent_end` observer as the run-crash backstop — the store makes the
1102
+ * second settlement a stale no-op. Idle-opened dialogs (a `session_start`
1103
+ * probe, say) are not run-scoped and survive, because their waiter
1104
+ * outlives the run.
1105
+ */
1106
+ abortRunScopedExtensionDialogs(session) {
1107
+ let closedAny = false;
1108
+ for (const dialog of this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session))) {
1109
+ if (dialog.runScoped)
1110
+ closedAny = this.closeExtensionDialogFromTrigger(session, dialog.dialogId, "aborted") || closedAny;
1111
+ }
1112
+ if (closedAny)
1113
+ this.publishStatus(session);
1114
+ }
1115
+ /**
1116
+ * Settle every dialog of the session as `"session-ended"`: the runtime
1117
+ * whose extension code is parked on them is being closed, replaced, or
1118
+ * disposed, so those Promises would otherwise never settle.
1119
+ */
1120
+ endSessionExtensionDialogs(session) {
1121
+ let closedAny = false;
1122
+ for (const dialog of this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session))) {
1123
+ closedAny = this.closeExtensionDialogFromTrigger(session, dialog.dialogId, "session-ended") || closedAny;
1124
+ }
1125
+ // Publishes only while the session is still (or already re-)registered as
1126
+ // active, so teardown paths stay silent and runtime replacement refreshes.
1127
+ if (closedAny)
889
1128
  this.publishStatus(session);
890
1129
  }
1130
+ publishDialogClosed(session, outcome) {
1131
+ this.events.publish(session.sessionId, {
1132
+ type: "dialog.closed",
1133
+ dialogId: outcome.dialogId,
1134
+ reason: outcome.reason,
1135
+ ...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
1136
+ }, this.eventScopeForSession(session));
1137
+ }
891
1138
  /** Summaries of the tracked subsessions spawned by `parentSessionId`. */
892
1139
  async listSubsessions(parentSessionId, parentSessionFile) {
893
1140
  const parentFile = nonEmptyString(parentSessionFile);
@@ -1212,7 +1459,7 @@ export class PiSessionService {
1212
1459
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
1213
1460
  }
1214
1461
  async status(ref, managementContext) {
1215
- return this.statusFromSession(await this.getOrOpen(ref, managementContext));
1462
+ return this.statusFromSession(await this.sessionForStatusOrDialogClose(ref, managementContext));
1216
1463
  }
1217
1464
  async searchCandidates(cwd, normalizedQuery, managementContext) {
1218
1465
  const listing = await this.listWorkspaceSessions(cwd, managementContext);
@@ -1240,24 +1487,20 @@ export class PiSessionService {
1240
1487
  const streamingMessage = session.state.streamingMessage;
1241
1488
  const partial = streamingMessage === undefined || streamingMessage === null
1242
1489
  ? null
1243
- : projectBrowserMessage(streamingMessage);
1490
+ : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1244
1491
  return { seq, partial };
1245
1492
  }
1246
1493
  async availableModels(ref, managementContext) {
1247
1494
  const session = await this.getOrOpen(ref, managementContext);
1248
- await session.modelRuntime.refresh();
1249
- const models = session.scopedModels.length > 0
1250
- ? session.scopedModels.map((scoped) => scoped.model)
1251
- : await session.modelRuntime.getAvailable();
1495
+ const models = await this.sessionModelCandidates(session);
1252
1496
  return models.map(modelToClientModel);
1253
1497
  }
1254
1498
  async setModel(ref, provider, modelId, managementContext) {
1255
1499
  await this.assertWritable(ref);
1256
1500
  const session = await this.getOrOpen(ref, managementContext);
1257
- await session.modelRuntime.refresh();
1258
- const candidates = session.scopedModels.length > 0
1259
- ? session.scopedModels.map((scoped) => scoped.model)
1260
- : await session.modelRuntime.getAvailable();
1501
+ this.assertTreeNavigationInactive(session, "change models");
1502
+ const candidates = await this.sessionModelCandidates(session);
1503
+ this.assertTreeNavigationInactive(session, "change models");
1261
1504
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1262
1505
  ?? session.modelRuntime.getModel(provider, modelId);
1263
1506
  if (model === undefined)
@@ -1733,6 +1976,7 @@ export class PiSessionService {
1733
1976
  const sessionId = active.runtime.session.sessionId;
1734
1977
  this.clearCompactionPromptQueue(sessionId);
1735
1978
  clearSessionQueue(active.runtime.session);
1979
+ this.abortRunScopedExtensionDialogs(active.runtime.session);
1736
1980
  try {
1737
1981
  await this.abortSessionOperations(active.runtime.session);
1738
1982
  this.publishActivity(active.runtime.session, "已停止", "idle");
@@ -1755,7 +1999,17 @@ export class PiSessionService {
1755
1999
  return this.statusFromSession(session);
1756
2000
  }
1757
2001
  async stop(ref, managementContext) {
1758
- const active = this.activeForLookup(ref, managementContext);
2002
+ let active = this.activeForLookup(ref, managementContext);
2003
+ if (active === undefined) {
2004
+ const startup = this.startupSessionForLookup(ref, managementContext);
2005
+ if (startup !== undefined) {
2006
+ this.endSessionExtensionDialogs(startup);
2007
+ const pendingOpens = this.pendingSessionOpenPromises(startup.sessionId);
2008
+ if (pendingOpens.length > 0)
2009
+ await Promise.allSettled(pendingOpens);
2010
+ active = this.activeForLookup(ref, managementContext);
2011
+ }
2012
+ }
1759
2013
  if (active !== undefined) {
1760
2014
  await this.closeActiveSession(active);
1761
2015
  return;
@@ -1765,19 +2019,7 @@ export class PiSessionService {
1765
2019
  }
1766
2020
  }
1767
2021
  async bulkSessionLookupContext(refs) {
1768
- const cwdSet = new Set();
1769
- let needsAllSessions = false;
1770
- for (const ref of refs) {
1771
- if (ref.cwd === undefined)
1772
- needsAllSessions = true;
1773
- else
1774
- cwdSet.add(ref.cwd);
1775
- }
1776
- const [sessionsByCwd, allSessions] = await Promise.all([
1777
- this.listSessionsByCwd([...cwdSet]),
1778
- needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
1779
- ]);
1780
- return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
2022
+ return { sessionsByCwd: await this.listSessionsByCwd(refs.map((ref) => ref.cwd)) };
1781
2023
  }
1782
2024
  async listSessionsByCwd(cwds) {
1783
2025
  const uniqueCwds = uniqueStrings(cwds);
@@ -1951,6 +2193,10 @@ export class PiSessionService {
1951
2193
  return [...names];
1952
2194
  }
1953
2195
  async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2196
+ for (const startup of this.startupSessions.values()) {
2197
+ if (startup.session.sessionId === sessionId)
2198
+ this.endSessionExtensionDialogs(startup.session);
2199
+ }
1954
2200
  const pendingOpens = this.pendingSessionOpenPromises(sessionId);
1955
2201
  if (pendingOpens.length > 0)
1956
2202
  await Promise.allSettled(pendingOpens);
@@ -1981,6 +2227,7 @@ export class PiSessionService {
1981
2227
  }
1982
2228
  this.forgetUnreadActivity(active.runtime.session);
1983
2229
  this.pendingAskStore.forgetSession(sessionId);
2230
+ this.endSessionExtensionDialogs(active.runtime.session);
1984
2231
  this.active.delete(activeSessionKey(sessionId, active.eventScope));
1985
2232
  this.activities.delete(activeSessionKey(sessionId, active.eventScope));
1986
2233
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
@@ -2030,6 +2277,26 @@ export class PiSessionService {
2030
2277
  async getOrOpen(ref, managementContext) {
2031
2278
  return (await this.getActive(ref, managementContext)).runtime.session;
2032
2279
  }
2280
+ startupSessionForLookup(ref, managementContext) {
2281
+ const contextKey = managementContextKey(managementContext);
2282
+ const eventScope = eventScopeFromManagementContext(managementContext);
2283
+ const sessionId = sessionIdFromLookup(ref);
2284
+ const exact = this.startupSessions.get(activeSessionKey(sessionId, eventScope));
2285
+ if (exact !== undefined && exact.managementContextKey === contextKey && lookupMatchesStartupSession(ref, exact.session))
2286
+ return exact.session;
2287
+ for (const startup of this.startupSessions.values()) {
2288
+ if (startup.managementContextKey !== contextKey || !startup.session.sessionId.startsWith(sessionId))
2289
+ continue;
2290
+ if (lookupMatchesStartupSession(ref, startup.session))
2291
+ return startup.session;
2292
+ }
2293
+ return undefined;
2294
+ }
2295
+ /** Resolve status and dialog-close calls even while extension startup is waiting for the browser. */
2296
+ async sessionForStatusOrDialogClose(ref, managementContext) {
2297
+ const reachable = this.activeForLookup(ref, managementContext)?.runtime.session ?? this.startupSessionForLookup(ref, managementContext);
2298
+ return reachable ?? this.getOrOpen(ref, managementContext);
2299
+ }
2033
2300
  async getActive(ref, managementContext) {
2034
2301
  const active = this.activeForLookup(ref, managementContext);
2035
2302
  if (active !== undefined)
@@ -2136,17 +2403,25 @@ export class PiSessionService {
2136
2403
  startup.end();
2137
2404
  }
2138
2405
  }
2139
- async bindSessionExtensions(session, generation) {
2406
+ async bindSessionExtensions(session, generation, owner) {
2140
2407
  const uiContext = this.sessionUiContext(session, generation);
2141
- await session.bindExtensions({
2142
- uiContext,
2143
- mode: "rpc",
2144
- onError: (error) => {
2145
- const message = `${error.extensionPath}: ${error.error}`;
2146
- this.publishActivity(session, "扩展错误", "error", message);
2147
- this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
2148
- },
2149
- });
2408
+ const key = activeSessionKey(session.sessionId, owner.eventScope);
2409
+ this.startupSessions.set(key, { session, ...owner });
2410
+ try {
2411
+ await session.bindExtensions({
2412
+ uiContext,
2413
+ mode: "rpc",
2414
+ onError: (error) => {
2415
+ const message = `${error.extensionPath}: ${error.error}`;
2416
+ this.publishActivity(session, "扩展错误", "error", message);
2417
+ this.events.publish(session.sessionId, { type: "session.error", message }, owner.eventScope);
2418
+ },
2419
+ });
2420
+ }
2421
+ finally {
2422
+ if (this.startupSessions.get(key)?.session === session)
2423
+ this.startupSessions.delete(key);
2424
+ }
2150
2425
  }
2151
2426
  bindRuntime(active, session = active.runtime.session) {
2152
2427
  this.runtimeBySession.set(session, active.runtime);
@@ -2159,9 +2434,11 @@ export class PiSessionService {
2159
2434
  }
2160
2435
  }
2161
2436
  active.unsubscribe = session.subscribe((event) => {
2162
- this.events.publish(session.sessionId, toClientEvent(event), active.eventScope);
2437
+ this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
2163
2438
  this.publishActivityForEvent(session, event);
2164
2439
  const eventType = getString(event, "type");
2440
+ if (eventType === "agent_end")
2441
+ this.abortRunScopedExtensionDialogs(session);
2165
2442
  if (eventType === "compaction_end")
2166
2443
  this.scheduleCompactionQueueDrain(session.sessionId);
2167
2444
  if (eventType === "agent_start" || eventType === "agent_end")
@@ -2574,14 +2851,34 @@ export class PiSessionService {
2574
2851
  if (active.runtime.session === session)
2575
2852
  return active.eventScope;
2576
2853
  }
2854
+ for (const startup of this.startupSessions.values()) {
2855
+ if (startup.session === session)
2856
+ return startup.eventScope;
2857
+ }
2577
2858
  return NORMAL_SESSION_EVENT_SCOPE;
2578
2859
  }
2860
+ dialogSessionKey(session) {
2861
+ const eventScope = this.eventScopeForSession(session);
2862
+ return eventScope === NORMAL_SESSION_EVENT_SCOPE ? session.sessionId : activeSessionKey(session.sessionId, eventScope);
2863
+ }
2864
+ publishStatusForSessionId(sessionId) {
2865
+ const sessions = new Set();
2866
+ for (const active of this.activeSessionsForId(sessionId))
2867
+ sessions.add(active.runtime.session);
2868
+ for (const startup of this.startupSessions.values()) {
2869
+ if (startup.session.sessionId === sessionId)
2870
+ sessions.add(startup.session);
2871
+ }
2872
+ for (const session of sessions)
2873
+ this.publishStatus(session);
2874
+ }
2579
2875
  statusFromSession(session) {
2580
2876
  const stats = session.getSessionStats();
2581
2877
  const model = session.model === undefined ? undefined : modelToClientModel(session.model);
2582
2878
  const contextUsage = session.getContextUsage();
2583
2879
  const warnings = this.warningsForSession(session);
2584
2880
  const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
2881
+ const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
2585
2882
  return {
2586
2883
  sessionId: session.sessionId,
2587
2884
  persisted: sessionFileExists(session.sessionFile),
@@ -2598,6 +2895,7 @@ export class PiSessionService {
2598
2895
  ...(contextUsage === undefined ? {} : { contextUsage }),
2599
2896
  ...(warnings.length === 0 ? {} : { warnings }),
2600
2897
  ...(pendingAsk === undefined ? {} : { pendingAsk }),
2898
+ ...(pendingDialogs.length === 0 ? {} : { pendingDialogs }),
2601
2899
  };
2602
2900
  }
2603
2901
  /**
@@ -2731,6 +3029,7 @@ export class PiSessionService {
2731
3029
  delegationToolsEnabled,
2732
3030
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
2733
3031
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
3032
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
2734
3033
  });
2735
3034
  const active = {
2736
3035
  runtime,
@@ -2768,7 +3067,7 @@ export class PiSessionService {
2768
3067
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
2769
3068
  }
2770
3069
  startup.report(STARTUP_PHASE_EXTENSIONS);
2771
- await this.bindSessionExtensions(runtime.session, notificationGeneration);
3070
+ await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
2772
3071
  this.bindRuntime(active);
2773
3072
  runtime.setRebindSession(async (session) => {
2774
3073
  const priorGeneration = notificationGeneration;
@@ -2781,8 +3080,12 @@ export class PiSessionService {
2781
3080
  this.notificationGenerationBySession.set(session, candidateGeneration);
2782
3081
  }
2783
3082
  this.bindRuntime(active, session);
3083
+ // The runtime being replaced parked every dialog the store still
3084
+ // holds for this session; settle those waits before the new
3085
+ // runtime's extensions can open fresh dialogs under the same id.
3086
+ this.endSessionExtensionDialogs(boundSession);
2784
3087
  boundSession = session;
2785
- await this.bindSessionExtensions(session, candidateGeneration);
3088
+ await this.bindSessionExtensions(session, candidateGeneration, active);
2786
3089
  if (candidateGeneration !== undefined) {
2787
3090
  this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
2788
3091
  notificationGeneration = candidateGeneration;
@@ -2816,6 +3119,7 @@ export class PiSessionService {
2816
3119
  }
2817
3120
  active.unsubscribe();
2818
3121
  this.forgetUnreadActivity(boundSession);
3122
+ this.endSessionExtensionDialogs(boundSession);
2819
3123
  let removedActive = false;
2820
3124
  for (const [sessionKey, candidate] of this.active.entries()) {
2821
3125
  if (candidate !== active)
@@ -2863,15 +3167,26 @@ export class PiSessionService {
2863
3167
  notificationId: added.notification.id,
2864
3168
  });
2865
3169
  };
2866
- // PI WEB owns the browser-facing notification and text-formatting
2867
- // boundaries. Delegate every other UI method to Pi's headless defaults so
2868
- // unsupported dialogs cancel safely instead of hanging.
3170
+ // PI WEB owns the browser-facing dialog, notification, and text-formatting
3171
+ // boundaries: the three dialog primitives park daemon-held Promises that
3172
+ // the browser answers, while every other UI method delegates to Pi's
3173
+ // headless defaults so unsupported surfaces cancel safely instead of
3174
+ // hanging.
2869
3175
  return new Proxy(baseUiContext, {
2870
- get(target, property, receiver) {
3176
+ get: (target, property, receiver) => {
2871
3177
  if (property === "notify")
2872
3178
  return notify;
2873
3179
  if (property === "theme")
2874
3180
  return plainTextTheme;
3181
+ if (property === "confirm") {
3182
+ return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
3183
+ }
3184
+ if (property === "select") {
3185
+ return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
3186
+ }
3187
+ if (property === "input") {
3188
+ return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
3189
+ }
2875
3190
  const value = Reflect.get(target, property, receiver);
2876
3191
  return value;
2877
3192
  },
@@ -3025,7 +3340,7 @@ function uniqueBulkSessionRefs(refs) {
3025
3340
  const seen = new Set();
3026
3341
  const unique = [];
3027
3342
  for (const ref of refs) {
3028
- const key = `${ref.cwd ?? ""}\0${ref.id}`;
3343
+ const key = `${ref.cwd}\0${ref.id}`;
3029
3344
  if (seen.has(key))
3030
3345
  continue;
3031
3346
  seen.add(key);
@@ -3034,15 +3349,13 @@ function uniqueBulkSessionRefs(refs) {
3034
3349
  return unique;
3035
3350
  }
3036
3351
  function bulkRefToLookup(ref) {
3037
- return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
3352
+ return ref;
3038
3353
  }
3039
3354
  function findArchivedRecordForBulkRef(records, ref) {
3040
- return records.find((record) => (ref.cwd === undefined || record.cwd === ref.cwd) && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
3355
+ return records.find((record) => record.cwd === ref.cwd && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
3041
3356
  }
3042
3357
  function findListedSessionForBulkRef(context, ref) {
3043
- if (ref.cwd !== undefined)
3044
- return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
3045
- return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
3358
+ return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
3046
3359
  }
3047
3360
  function findSessionByIdOrPrefix(sessions, sessionId) {
3048
3361
  return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
@@ -3337,11 +3650,17 @@ function historyMessages(session) {
3337
3650
  }
3338
3651
  function historyMessagesFromManager(sessionManager) {
3339
3652
  const messages = [];
3653
+ let thinkingLevel;
3340
3654
  for (const entry of sessionManager.getBranch()) {
3341
3655
  if (!isRecord(entry))
3342
3656
  continue;
3343
3657
  if (entry["type"] === "message")
3344
- messages.push(entry["message"]);
3658
+ messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
3659
+ else if (entry["type"] === "thinking_level_change") {
3660
+ const level = getString(entry, "thinkingLevel");
3661
+ if (level !== undefined)
3662
+ thinkingLevel = level;
3663
+ }
3345
3664
  else if (entry["type"] === "custom_message" && entry["display"] === true)
3346
3665
  messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
3347
3666
  else if (entry["type"] === "compaction")