@chainingintention/pi-web-cn 1.202608.5 → 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-BUT2Idcq.js → CodeViewer-CWKEeX0r.js} +1 -1
  2. package/dist/client/assets/{TerminalPanel-b8CZY7en.js → TerminalPanel-BmA7Ayzk.js} +1 -1
  3. package/dist/client/assets/{UnifiedDiffViewer-CXY7AXrD.js → UnifiedDiffViewer-2DACwxFY.js} +1 -1
  4. package/dist/client/assets/{index-80SI7dCz.js → index-D6j67Izj.js} +491 -300
  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 +375 -71
  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
@@ -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
  }
@@ -262,33 +289,37 @@ export function dismissSessionWarning(session, dismissId) {
262
289
  function defaultCreateAgentRuntime(createRuntime, options) {
263
290
  if (!(options.sessionManager instanceof SessionManager))
264
291
  throw new Error("Default runtime creation requires an SDK SessionManager");
265
- const runtimeFactory = createRuntimeWithContextAndOneShotInitialModel(createRuntime, options.managementContext, options.initialModel, options.delegationToolsEnabled);
292
+ const runtimeFactory = createRuntimeWithContextAndOneShotSessionOptions(createRuntime, options.managementContext, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
266
293
  return createAgentSessionRuntime(runtimeFactory, {
267
294
  cwd: options.cwd,
268
295
  agentDir: options.agentDir,
269
296
  sessionManager: options.sessionManager,
270
297
  });
271
298
  }
272
- function createRuntimeWithContextAndOneShotInitialModel(createRuntime, managementContext, initialModel, delegationToolsEnabled) {
273
- // The inherited model belongs only to the session being spawned. Do not keep
274
- // 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.
275
302
  let pendingInitialModel = initialModel;
303
+ let pendingInitialThinkingLevel = initialThinkingLevel;
276
304
  let pendingDelegationToolsEnabled = delegationToolsEnabled;
277
305
  return async (options) => {
278
306
  const model = pendingInitialModel;
307
+ const thinkingLevel = pendingInitialThinkingLevel;
279
308
  const toolsEnabled = pendingDelegationToolsEnabled;
280
309
  pendingInitialModel = undefined;
310
+ pendingInitialThinkingLevel = undefined;
281
311
  pendingDelegationToolsEnabled = undefined;
282
312
  return createRuntime({
283
313
  ...options,
284
314
  ...(managementContext === undefined ? {} : { managementContext }),
285
315
  ...(model === undefined ? {} : { initialModel: model }),
316
+ ...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
286
317
  ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
287
318
  });
288
319
  };
289
320
  }
290
321
  function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser) {
291
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, delegationToolsEnabled = true }) => {
322
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
292
323
  const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
293
324
  if (managementContext !== undefined) {
294
325
  return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext)({
@@ -298,6 +329,7 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
298
329
  delegationToolsEnabled,
299
330
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
300
331
  ...(initialModel === undefined ? {} : { initialModel }),
332
+ ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
301
333
  });
302
334
  }
303
335
  const settingsManager = await createScopedSettingsManager({
@@ -313,12 +345,13 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
313
345
  customTools,
314
346
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
315
347
  ...(initialModel === undefined ? {} : { model: initialModel }),
348
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
316
349
  });
317
350
  return { ...result, services, diagnostics: services.diagnostics };
318
351
  };
319
352
  }
320
353
  function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext) {
321
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled = true }) => {
354
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
322
355
  const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
323
356
  return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
324
357
  const settingsManager = await createScopedSettingsManager({
@@ -337,11 +370,20 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
337
370
  },
338
371
  });
339
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
+ };
340
382
  const customTools = [
341
383
  ...managedAgentToolDefinitions(cwd, managedToolOptions),
342
384
  createManagedPythonToolDefinition(cwd, managementContext),
343
- ...(delegationToolsEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
344
- ...(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)) : []),
345
387
  ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
346
388
  ];
347
389
  const options = {
@@ -351,6 +393,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
351
393
  customTools,
352
394
  tools: managementAgentToolNames(managementContext),
353
395
  ...(initialModel === undefined ? {} : { model: initialModel }),
396
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
354
397
  };
355
398
  const result = await createAgentSessionFromServices(options);
356
399
  return { ...result, services, diagnostics: services.diagnostics };
@@ -404,6 +447,8 @@ export class PiSessionService {
404
447
  this.active = new Map();
405
448
  this.runtimeBySession = new WeakMap();
406
449
  this.pendingSessionOpens = new Map();
450
+ /** Sessions whose extension binding is parked on a `session_start` dialog. */
451
+ this.startupSessions = new Map();
407
452
  this.activities = new SessionActivityCoordinator();
408
453
  /** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
409
454
  this.treeNavigations = new WeakSet();
@@ -434,6 +479,8 @@ export class PiSessionService {
434
479
  */
435
480
  this.subsessionNotifyArmed = new Map();
436
481
  this.notificationGenerationBySession = new WeakMap();
482
+ /** The parked extension Promise resolvers behind the store's open dialogs. */
483
+ this.dialogWaiters = new ExtensionDialogWaiters();
437
484
  this.pendingUnreadMutations = [];
438
485
  this.unreadPublicationFlushRequested = false;
439
486
  this.unreadPublicationStopped = false;
@@ -478,6 +525,8 @@ export class PiSessionService {
478
525
  this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
479
526
  this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
480
527
  this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
528
+ this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
529
+ this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
481
530
  this.catalogRefreshStatus = deps.catalogRefreshStatus;
482
531
  this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
483
532
  this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
@@ -578,6 +627,10 @@ export class PiSessionService {
578
627
  this.clearUnreadPublicationRetry();
579
628
  clearInterval(this.heartbeat);
580
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);
581
634
  const pendingOpens = this.pendingSessionOpenPromises();
582
635
  if (pendingOpens.length > 0)
583
636
  await Promise.allSettled(pendingOpens);
@@ -585,9 +638,11 @@ export class PiSessionService {
585
638
  for (const active of activeSessions) {
586
639
  this.forgetUnreadActivity(active.runtime.session);
587
640
  this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
641
+ this.endSessionExtensionDialogs(active.runtime.session);
588
642
  }
589
643
  this.active.clear();
590
644
  this.pendingSessionOpens.clear();
645
+ this.startupSessions.clear();
591
646
  this.activities.clear();
592
647
  this.compactionPromptQueues.clear();
593
648
  this.authLossWarnings.clear();
@@ -641,7 +696,7 @@ export class PiSessionService {
641
696
  return { sessionIds };
642
697
  }
643
698
  async setPinned(ref, pinned, managementContext) {
644
- if (typeof ref === "string" || ref.cwd === "")
699
+ if (ref.cwd === "")
645
700
  throw new Error("置顶会话必须提供工作区");
646
701
  const sessions = await this.list(ref.cwd, managementContext);
647
702
  const session = sessions.find((candidate) => candidate.id === ref.id);
@@ -745,6 +800,7 @@ export class PiSessionService {
745
800
  const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
746
801
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
747
802
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
803
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
748
804
  ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
749
805
  ...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
750
806
  startupIntent: "create",
@@ -779,10 +835,27 @@ export class PiSessionService {
779
835
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
780
836
  if (!decision.allowed)
781
837
  throw spawnTargetError(decision);
782
- const created = await this.start(decision.cwd, input.model === undefined ? {} : { initialModel: input.model });
783
- 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
+ });
784
853
  this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
785
- return { sessionId: created.id, cwd: decision.cwd };
854
+ return {
855
+ sessionId: created.id,
856
+ cwd: decision.cwd,
857
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
858
+ };
786
859
  }
787
860
  /**
788
861
  * Start a *tracked* child session on behalf of a LLM. Identical to
@@ -792,15 +865,23 @@ export class PiSessionService {
792
865
  */
793
866
  async spawnSubsession(input) {
794
867
  if (this.spawnTargets === undefined)
795
- throw new Error("Spawning sessions is disabled");
868
+ throw new Error("派生会话已禁用");
796
869
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
797
870
  if (!decision.allowed)
798
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);
799
877
  const created = await this.startSession(decision.cwd, {
800
878
  ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
801
- ...(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 }),
802
882
  creationProvenance: "tracked-subsession",
803
883
  });
884
+ const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
804
885
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
805
886
  const link = {
806
887
  parentSessionId: input.parentSessionId,
@@ -812,9 +893,45 @@ export class PiSessionService {
812
893
  await this.registerVerifiedSubsession(link);
813
894
  this.persistSubsessionLink(link);
814
895
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
815
- 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
+ });
816
899
  this.logger.info({ parentSessionId: input.parentSessionId, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_subsession started a tracked child session");
817
- 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;
818
935
  }
819
936
  /**
820
937
  * Register the question set an agent wants the user to answer as the session's
@@ -895,14 +1012,129 @@ export class PiSessionService {
895
1012
  this.publishStatus(session);
896
1013
  }
897
1014
  /**
898
- * Publish status for a session known only by id, as the ask tools are: they
899
- * 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.
900
1019
  */
901
- publishStatusForSessionId(sessionId) {
902
- const session = this.active.get(sessionId)?.runtime.session;
903
- 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)
904
1113
  this.publishStatus(session);
905
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)
1128
+ this.publishStatus(session);
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
+ }
906
1138
  /** Summaries of the tracked subsessions spawned by `parentSessionId`. */
907
1139
  async listSubsessions(parentSessionId, parentSessionFile) {
908
1140
  const parentFile = nonEmptyString(parentSessionFile);
@@ -1227,7 +1459,7 @@ export class PiSessionService {
1227
1459
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
1228
1460
  }
1229
1461
  async status(ref, managementContext) {
1230
- return this.statusFromSession(await this.getOrOpen(ref, managementContext));
1462
+ return this.statusFromSession(await this.sessionForStatusOrDialogClose(ref, managementContext));
1231
1463
  }
1232
1464
  async searchCandidates(cwd, normalizedQuery, managementContext) {
1233
1465
  const listing = await this.listWorkspaceSessions(cwd, managementContext);
@@ -1255,24 +1487,20 @@ export class PiSessionService {
1255
1487
  const streamingMessage = session.state.streamingMessage;
1256
1488
  const partial = streamingMessage === undefined || streamingMessage === null
1257
1489
  ? null
1258
- : projectBrowserMessage(streamingMessage);
1490
+ : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1259
1491
  return { seq, partial };
1260
1492
  }
1261
1493
  async availableModels(ref, managementContext) {
1262
1494
  const session = await this.getOrOpen(ref, managementContext);
1263
- await session.modelRuntime.refresh();
1264
- const models = session.scopedModels.length > 0
1265
- ? session.scopedModels.map((scoped) => scoped.model)
1266
- : await session.modelRuntime.getAvailable();
1495
+ const models = await this.sessionModelCandidates(session);
1267
1496
  return models.map(modelToClientModel);
1268
1497
  }
1269
1498
  async setModel(ref, provider, modelId, managementContext) {
1270
1499
  await this.assertWritable(ref);
1271
1500
  const session = await this.getOrOpen(ref, managementContext);
1272
- await session.modelRuntime.refresh();
1273
- const candidates = session.scopedModels.length > 0
1274
- ? session.scopedModels.map((scoped) => scoped.model)
1275
- : await session.modelRuntime.getAvailable();
1501
+ this.assertTreeNavigationInactive(session, "change models");
1502
+ const candidates = await this.sessionModelCandidates(session);
1503
+ this.assertTreeNavigationInactive(session, "change models");
1276
1504
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1277
1505
  ?? session.modelRuntime.getModel(provider, modelId);
1278
1506
  if (model === undefined)
@@ -1748,6 +1976,7 @@ export class PiSessionService {
1748
1976
  const sessionId = active.runtime.session.sessionId;
1749
1977
  this.clearCompactionPromptQueue(sessionId);
1750
1978
  clearSessionQueue(active.runtime.session);
1979
+ this.abortRunScopedExtensionDialogs(active.runtime.session);
1751
1980
  try {
1752
1981
  await this.abortSessionOperations(active.runtime.session);
1753
1982
  this.publishActivity(active.runtime.session, "已停止", "idle");
@@ -1770,7 +1999,17 @@ export class PiSessionService {
1770
1999
  return this.statusFromSession(session);
1771
2000
  }
1772
2001
  async stop(ref, managementContext) {
1773
- 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
+ }
1774
2013
  if (active !== undefined) {
1775
2014
  await this.closeActiveSession(active);
1776
2015
  return;
@@ -1780,19 +2019,7 @@ export class PiSessionService {
1780
2019
  }
1781
2020
  }
1782
2021
  async bulkSessionLookupContext(refs) {
1783
- const cwdSet = new Set();
1784
- let needsAllSessions = false;
1785
- for (const ref of refs) {
1786
- if (ref.cwd === undefined)
1787
- needsAllSessions = true;
1788
- else
1789
- cwdSet.add(ref.cwd);
1790
- }
1791
- const [sessionsByCwd, allSessions] = await Promise.all([
1792
- this.listSessionsByCwd([...cwdSet]),
1793
- needsAllSessions ? this.sessionManager.listAll?.() ?? Promise.resolve([]) : Promise.resolve(undefined),
1794
- ]);
1795
- return allSessions === undefined ? { sessionsByCwd } : { sessionsByCwd, allSessions };
2022
+ return { sessionsByCwd: await this.listSessionsByCwd(refs.map((ref) => ref.cwd)) };
1796
2023
  }
1797
2024
  async listSessionsByCwd(cwds) {
1798
2025
  const uniqueCwds = uniqueStrings(cwds);
@@ -1966,6 +2193,10 @@ export class PiSessionService {
1966
2193
  return [...names];
1967
2194
  }
1968
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
+ }
1969
2200
  const pendingOpens = this.pendingSessionOpenPromises(sessionId);
1970
2201
  if (pendingOpens.length > 0)
1971
2202
  await Promise.allSettled(pendingOpens);
@@ -1996,6 +2227,7 @@ export class PiSessionService {
1996
2227
  }
1997
2228
  this.forgetUnreadActivity(active.runtime.session);
1998
2229
  this.pendingAskStore.forgetSession(sessionId);
2230
+ this.endSessionExtensionDialogs(active.runtime.session);
1999
2231
  this.active.delete(activeSessionKey(sessionId, active.eventScope));
2000
2232
  this.activities.delete(activeSessionKey(sessionId, active.eventScope));
2001
2233
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
@@ -2045,6 +2277,26 @@ export class PiSessionService {
2045
2277
  async getOrOpen(ref, managementContext) {
2046
2278
  return (await this.getActive(ref, managementContext)).runtime.session;
2047
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
+ }
2048
2300
  async getActive(ref, managementContext) {
2049
2301
  const active = this.activeForLookup(ref, managementContext);
2050
2302
  if (active !== undefined)
@@ -2151,17 +2403,25 @@ export class PiSessionService {
2151
2403
  startup.end();
2152
2404
  }
2153
2405
  }
2154
- async bindSessionExtensions(session, generation) {
2406
+ async bindSessionExtensions(session, generation, owner) {
2155
2407
  const uiContext = this.sessionUiContext(session, generation);
2156
- await session.bindExtensions({
2157
- uiContext,
2158
- mode: "rpc",
2159
- onError: (error) => {
2160
- const message = `${error.extensionPath}: ${error.error}`;
2161
- this.publishActivity(session, "扩展错误", "error", message);
2162
- this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
2163
- },
2164
- });
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
+ }
2165
2425
  }
2166
2426
  bindRuntime(active, session = active.runtime.session) {
2167
2427
  this.runtimeBySession.set(session, active.runtime);
@@ -2174,9 +2434,11 @@ export class PiSessionService {
2174
2434
  }
2175
2435
  }
2176
2436
  active.unsubscribe = session.subscribe((event) => {
2177
- this.events.publish(session.sessionId, toClientEvent(event), active.eventScope);
2437
+ this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
2178
2438
  this.publishActivityForEvent(session, event);
2179
2439
  const eventType = getString(event, "type");
2440
+ if (eventType === "agent_end")
2441
+ this.abortRunScopedExtensionDialogs(session);
2180
2442
  if (eventType === "compaction_end")
2181
2443
  this.scheduleCompactionQueueDrain(session.sessionId);
2182
2444
  if (eventType === "agent_start" || eventType === "agent_end")
@@ -2589,14 +2851,34 @@ export class PiSessionService {
2589
2851
  if (active.runtime.session === session)
2590
2852
  return active.eventScope;
2591
2853
  }
2854
+ for (const startup of this.startupSessions.values()) {
2855
+ if (startup.session === session)
2856
+ return startup.eventScope;
2857
+ }
2592
2858
  return NORMAL_SESSION_EVENT_SCOPE;
2593
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
+ }
2594
2875
  statusFromSession(session) {
2595
2876
  const stats = session.getSessionStats();
2596
2877
  const model = session.model === undefined ? undefined : modelToClientModel(session.model);
2597
2878
  const contextUsage = session.getContextUsage();
2598
2879
  const warnings = this.warningsForSession(session);
2599
2880
  const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
2881
+ const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
2600
2882
  return {
2601
2883
  sessionId: session.sessionId,
2602
2884
  persisted: sessionFileExists(session.sessionFile),
@@ -2613,6 +2895,7 @@ export class PiSessionService {
2613
2895
  ...(contextUsage === undefined ? {} : { contextUsage }),
2614
2896
  ...(warnings.length === 0 ? {} : { warnings }),
2615
2897
  ...(pendingAsk === undefined ? {} : { pendingAsk }),
2898
+ ...(pendingDialogs.length === 0 ? {} : { pendingDialogs }),
2616
2899
  };
2617
2900
  }
2618
2901
  /**
@@ -2746,6 +3029,7 @@ export class PiSessionService {
2746
3029
  delegationToolsEnabled,
2747
3030
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
2748
3031
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
3032
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
2749
3033
  });
2750
3034
  const active = {
2751
3035
  runtime,
@@ -2783,7 +3067,7 @@ export class PiSessionService {
2783
3067
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
2784
3068
  }
2785
3069
  startup.report(STARTUP_PHASE_EXTENSIONS);
2786
- await this.bindSessionExtensions(runtime.session, notificationGeneration);
3070
+ await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
2787
3071
  this.bindRuntime(active);
2788
3072
  runtime.setRebindSession(async (session) => {
2789
3073
  const priorGeneration = notificationGeneration;
@@ -2796,8 +3080,12 @@ export class PiSessionService {
2796
3080
  this.notificationGenerationBySession.set(session, candidateGeneration);
2797
3081
  }
2798
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);
2799
3087
  boundSession = session;
2800
- await this.bindSessionExtensions(session, candidateGeneration);
3088
+ await this.bindSessionExtensions(session, candidateGeneration, active);
2801
3089
  if (candidateGeneration !== undefined) {
2802
3090
  this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
2803
3091
  notificationGeneration = candidateGeneration;
@@ -2831,6 +3119,7 @@ export class PiSessionService {
2831
3119
  }
2832
3120
  active.unsubscribe();
2833
3121
  this.forgetUnreadActivity(boundSession);
3122
+ this.endSessionExtensionDialogs(boundSession);
2834
3123
  let removedActive = false;
2835
3124
  for (const [sessionKey, candidate] of this.active.entries()) {
2836
3125
  if (candidate !== active)
@@ -2878,15 +3167,26 @@ export class PiSessionService {
2878
3167
  notificationId: added.notification.id,
2879
3168
  });
2880
3169
  };
2881
- // PI WEB owns the browser-facing notification and text-formatting
2882
- // boundaries. Delegate every other UI method to Pi's headless defaults so
2883
- // 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.
2884
3175
  return new Proxy(baseUiContext, {
2885
- get(target, property, receiver) {
3176
+ get: (target, property, receiver) => {
2886
3177
  if (property === "notify")
2887
3178
  return notify;
2888
3179
  if (property === "theme")
2889
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
+ }
2890
3190
  const value = Reflect.get(target, property, receiver);
2891
3191
  return value;
2892
3192
  },
@@ -3040,7 +3340,7 @@ function uniqueBulkSessionRefs(refs) {
3040
3340
  const seen = new Set();
3041
3341
  const unique = [];
3042
3342
  for (const ref of refs) {
3043
- const key = `${ref.cwd ?? ""}\0${ref.id}`;
3343
+ const key = `${ref.cwd}\0${ref.id}`;
3044
3344
  if (seen.has(key))
3045
3345
  continue;
3046
3346
  seen.add(key);
@@ -3049,15 +3349,13 @@ function uniqueBulkSessionRefs(refs) {
3049
3349
  return unique;
3050
3350
  }
3051
3351
  function bulkRefToLookup(ref) {
3052
- return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
3352
+ return ref;
3053
3353
  }
3054
3354
  function findArchivedRecordForBulkRef(records, ref) {
3055
- 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)));
3056
3356
  }
3057
3357
  function findListedSessionForBulkRef(context, ref) {
3058
- if (ref.cwd !== undefined)
3059
- return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
3060
- return context.allSessions === undefined ? undefined : findSessionByIdOrPrefix(context.allSessions, ref.id);
3358
+ return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
3061
3359
  }
3062
3360
  function findSessionByIdOrPrefix(sessions, sessionId) {
3063
3361
  return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
@@ -3352,11 +3650,17 @@ function historyMessages(session) {
3352
3650
  }
3353
3651
  function historyMessagesFromManager(sessionManager) {
3354
3652
  const messages = [];
3653
+ let thinkingLevel;
3355
3654
  for (const entry of sessionManager.getBranch()) {
3356
3655
  if (!isRecord(entry))
3357
3656
  continue;
3358
3657
  if (entry["type"] === "message")
3359
- 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
+ }
3360
3664
  else if (entry["type"] === "custom_message" && entry["display"] === true)
3361
3665
  messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
3362
3666
  else if (entry["type"] === "compaction")