@chainingintention/pi-web-cn 1.202608.5 → 1.202608.7

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 (74) hide show
  1. package/dist/cli.js +8 -0
  2. package/dist/cli.js.map +1 -1
  3. package/dist/client/assets/{CodeViewer-BUT2Idcq.js → CodeViewer-CWKEeX0r.js} +1 -1
  4. package/dist/client/assets/{TerminalPanel-b8CZY7en.js → TerminalPanel-BmA7Ayzk.js} +1 -1
  5. package/dist/client/assets/{UnifiedDiffViewer-CXY7AXrD.js → UnifiedDiffViewer-2DACwxFY.js} +1 -1
  6. package/dist/client/assets/{index-80SI7dCz.js → index-D6j67Izj.js} +491 -300
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +86 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-web-plugins/relays/relaysPanelElement.js +112 -46
  11. package/dist/server/app.js +4 -1
  12. package/dist/server/app.js.map +1 -1
  13. package/dist/server/audit/managementAuditStore.js +211 -0
  14. package/dist/server/audit/managementAuditStore.js.map +1 -0
  15. package/dist/server/audit/normalToolAuditCli.js +143 -0
  16. package/dist/server/audit/normalToolAuditCli.js.map +1 -0
  17. package/dist/server/audit/normalToolAuditStore.js +264 -0
  18. package/dist/server/audit/normalToolAuditStore.js.map +1 -0
  19. package/dist/server/managementEmbed.js +19 -1
  20. package/dist/server/managementEmbed.js.map +1 -1
  21. package/dist/server/piWebStatus.js +7 -1
  22. package/dist/server/piWebStatus.js.map +1 -1
  23. package/dist/server/requestLogging.js +10 -7
  24. package/dist/server/requestLogging.js.map +1 -1
  25. package/dist/server/sessiond/sessionProxyRoutes.js +13 -7
  26. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  27. package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
  28. package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
  29. package/dist/server/sessiond.js +91 -4
  30. package/dist/server/sessiond.js.map +1 -1
  31. package/dist/server/sessions/extensionDialogWaiters.js +76 -0
  32. package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
  33. package/dist/server/sessions/managementPermissionSystem.js +8 -8
  34. package/dist/server/sessions/managementPermissionSystem.js.map +1 -1
  35. package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
  36. package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
  37. package/dist/server/sessions/piSessionService.js +493 -78
  38. package/dist/server/sessions/piSessionService.js.map +1 -1
  39. package/dist/server/sessions/sessionRoutes.js +54 -27
  40. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  41. package/dist/server/sessions/sessionUiEvents.js +9 -2
  42. package/dist/server/sessions/sessionUiEvents.js.map +1 -1
  43. package/dist/server/sessions/spawnSessionTool.js +8 -1
  44. package/dist/server/sessions/spawnSessionTool.js.map +1 -1
  45. package/dist/server/sessions/spawnSubsessionTool.js +7 -1
  46. package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
  47. package/dist/server/workbench/accessStateRoutes.js +30 -0
  48. package/dist/server/workbench/accessStateRoutes.js.map +1 -0
  49. package/dist/server/workbench/accessStateStore.js +29 -0
  50. package/dist/server/workbench/accessStateStore.js.map +1 -0
  51. package/dist/server/workbench/capabilitySearch.js +62 -0
  52. package/dist/server/workbench/capabilitySearch.js.map +1 -0
  53. package/dist/server/workbench/gatewayIntegration.js +56 -0
  54. package/dist/server/workbench/gatewayIntegration.js.map +1 -0
  55. package/dist/server/workbench/mcpClient.js +148 -0
  56. package/dist/server/workbench/mcpClient.js.map +1 -0
  57. package/dist/server/workbench/skillSync.js +375 -0
  58. package/dist/server/workbench/skillSync.js.map +1 -0
  59. package/dist/server/workbench/types.js +2 -0
  60. package/dist/server/workbench/types.js.map +1 -0
  61. package/dist/server/workbench/workbenchClient.js +136 -0
  62. package/dist/server/workbench/workbenchClient.js.map +1 -0
  63. package/dist/server/workbench/workbenchTools.js +145 -0
  64. package/dist/server/workbench/workbenchTools.js.map +1 -0
  65. package/dist/shared/apiTypes.d.ts +141 -1
  66. package/dist/shared/apiTypes.js +8 -0
  67. package/dist/shared/apiTypes.js.map +1 -1
  68. package/dist/shared/federatedRoutes.js +2 -0
  69. package/dist/shared/federatedRoutes.js.map +1 -1
  70. package/dist/shared/piWebConfigParsing.js +94 -0
  71. package/dist/shared/piWebConfigParsing.js.map +1 -1
  72. package/docs/config.md +86 -1
  73. package/docs/plugins.md +14 -0
  74. package/package.json +7 -3
@@ -23,10 +23,13 @@ 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 { workbenchAccessHandle } from "../managementEmbed.js";
27
+ import { DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS, piWebDataDir } from "../../config.js";
27
28
  import { createScopedSettingsManager, resolveSettingsScopeDirectory } from "./projectSettingsScope.js";
28
29
  import { createAskUserToolDefinition } from "./askUserTool.js";
29
30
  import { PendingAskStore, renderAskUserAnswersText } from "./pendingAskStore.js";
31
+ import { PendingExtensionDialogStore } from "./pendingExtensionDialogStore.js";
32
+ import { ExtensionDialogWaiters, effectiveExtensionDialogTimeoutMs, extensionDialogCancelValue } from "./extensionDialogWaiters.js";
30
33
  import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
31
34
  import { createSubsessionToolDefinitions } from "./spawnSubsessionTool.js";
32
35
  import { buildTranscriptView } from "./subsessionTranscript.js";
@@ -37,7 +40,9 @@ import { SessionNotificationStore } from "./sessionNotificationStore.js";
37
40
  import { plainTextTheme } from "./plainTextTheme.js";
38
41
  import { SessionUnreadStore } from "./sessionUnreadStore.js";
39
42
  import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
40
- import { getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
43
+ import { annotateAssistantThinkingLevel, getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
44
+ import { WorkbenchHttpError } from "../workbench/workbenchClient.js";
45
+ import { createWorkbenchToolDefinitions } from "../workbench/workbenchTools.js";
41
46
  const noopLogger = { info() { } };
42
47
  const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
43
48
  /**
@@ -64,6 +69,28 @@ function spawnTargetError(decision) {
64
69
  return new Error("派生会话不在已注册项目中");
65
70
  return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
66
71
  }
72
+ function modelSpecOf(model) {
73
+ return `${model.provider}/${model.id}`;
74
+ }
75
+ /**
76
+ * Parse a strict `provider/model-id` spec: split on the first `/` (model ids
77
+ * may themselves contain `/`) and require both parts to be non-empty.
78
+ */
79
+ function parseModelSpec(spec) {
80
+ const slash = spec.indexOf("/");
81
+ if (slash <= 0 || slash === spec.length - 1)
82
+ return undefined;
83
+ return { provider: spec.slice(0, slash), modelId: spec.slice(slash + 1) };
84
+ }
85
+ /**
86
+ * Error for a spawn-tool model spec that matched nothing. States the facts —
87
+ * the bad spec and the required format — with deliberately no model list
88
+ * (a list would invite guesses). The agent loop turns the throw into an
89
+ * error tool result; how to recover is the agent's call.
90
+ */
91
+ function unknownSpawnModelError(modelSpec) {
92
+ return new Error(`未找到模型“${modelSpec}”。请传入精确的“provider/model-id”。`);
93
+ }
67
94
  function authLossWarningKey(sessionId, eventScope, provider, modelId) {
68
95
  return `${eventScope}:${sessionId}:${provider}/${modelId}`;
69
96
  }
@@ -76,6 +103,9 @@ function isPiSessionRef(ref) {
76
103
  function lookupMatchesActiveSession(ref, active) {
77
104
  return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
78
105
  }
106
+ function lookupMatchesStartupSession(ref, session) {
107
+ return !isPiSessionRef(ref) || cwdPathsEqual(session.sessionManager.getCwd(), ref.cwd);
108
+ }
79
109
  function activeSessionKey(sessionId, eventScope) {
80
110
  return `${eventScope}\0${sessionId}`;
81
111
  }
@@ -150,6 +180,22 @@ export function filterManagedProjectSkills(cwd, base) {
150
180
  }),
151
181
  };
152
182
  }
183
+ export function filterManagedWorkbenchSkills(cwd, base, receipt) {
184
+ const projectRoot = realpathSync(cwd);
185
+ const allowedDirectories = new Set(receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)));
186
+ return {
187
+ ...base,
188
+ skills: base.skills.filter((skill) => {
189
+ try {
190
+ const path = realpathSync(skill.filePath);
191
+ return [...allowedDirectories].some((directory) => pathInsideOrEqual(directory, path));
192
+ }
193
+ catch {
194
+ return false;
195
+ }
196
+ }),
197
+ };
198
+ }
153
199
  function pathInsideOrEqual(parent, child) {
154
200
  const normalizedParent = resolve(parent);
155
201
  const normalizedChild = resolve(child);
@@ -262,42 +308,47 @@ export function dismissSessionWarning(session, dismissId) {
262
308
  function defaultCreateAgentRuntime(createRuntime, options) {
263
309
  if (!(options.sessionManager instanceof SessionManager))
264
310
  throw new Error("Default runtime creation requires an SDK SessionManager");
265
- const runtimeFactory = createRuntimeWithContextAndOneShotInitialModel(createRuntime, options.managementContext, options.initialModel, options.delegationToolsEnabled);
311
+ const runtimeFactory = createRuntimeWithContextAndOneShotSessionOptions(createRuntime, options.managementContext, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
266
312
  return createAgentSessionRuntime(runtimeFactory, {
267
313
  cwd: options.cwd,
268
314
  agentDir: options.agentDir,
269
315
  sessionManager: options.sessionManager,
270
316
  });
271
317
  }
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.
318
+ function createRuntimeWithContextAndOneShotSessionOptions(createRuntime, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled) {
319
+ // These inherited options belong only to the session being spawned. A later
320
+ // runtime replacement restores its own model and thinking level from disk.
275
321
  let pendingInitialModel = initialModel;
322
+ let pendingInitialThinkingLevel = initialThinkingLevel;
276
323
  let pendingDelegationToolsEnabled = delegationToolsEnabled;
277
324
  return async (options) => {
278
325
  const model = pendingInitialModel;
326
+ const thinkingLevel = pendingInitialThinkingLevel;
279
327
  const toolsEnabled = pendingDelegationToolsEnabled;
280
328
  pendingInitialModel = undefined;
329
+ pendingInitialThinkingLevel = undefined;
281
330
  pendingDelegationToolsEnabled = undefined;
282
331
  return createRuntime({
283
332
  ...options,
284
333
  ...(managementContext === undefined ? {} : { managementContext }),
285
334
  ...(model === undefined ? {} : { initialModel: model }),
335
+ ...(thinkingLevel === undefined ? {} : { initialThinkingLevel: thinkingLevel }),
286
336
  ...(toolsEnabled === undefined ? {} : { delegationToolsEnabled: toolsEnabled }),
287
337
  });
288
338
  };
289
339
  }
290
- function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser) {
291
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, delegationToolsEnabled = true }) => {
340
+ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, workbench, logger = noopLogger, managementAudit, managementProjectIdForCwd) {
341
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, managementContext, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
292
342
  const scopedModelRuntime = managementContext === undefined ? await normalModelRuntimeForCwd(cwd) : await managementModelRuntime();
293
343
  if (managementContext !== undefined) {
294
- return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext)({
344
+ return createManagementRuntimeFactory(scopedModelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd)({
295
345
  cwd,
296
346
  agentDir,
297
347
  sessionManager,
298
348
  delegationToolsEnabled,
299
349
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
300
350
  ...(initialModel === undefined ? {} : { initialModel }),
351
+ ...(initialThinkingLevel === undefined ? {} : { initialThinkingLevel }),
301
352
  });
302
353
  }
303
354
  const settingsManager = await createScopedSettingsManager({
@@ -313,13 +364,46 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
313
364
  customTools,
314
365
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
315
366
  ...(initialModel === undefined ? {} : { model: initialModel }),
367
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
316
368
  });
317
369
  return { ...result, services, diagnostics: services.diagnostics };
318
370
  };
319
371
  }
320
- function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext) {
321
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled = true }) => {
322
- const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
372
+ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spawn, subsessions, askUser, managementContext, workbench, logger, managementAudit, managementProjectIdForCwd) {
373
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, initialThinkingLevel, delegationToolsEnabled = true }) => {
374
+ const auditIdentity = await resolveManagementAuditIdentity(cwd, managementContext, managementProjectIdForCwd, logger);
375
+ const accessHandle = workbenchAccessHandle(managementContext);
376
+ const state = workbench === undefined ? undefined : workbench.accessStates.require(accessHandle);
377
+ let receipt;
378
+ try {
379
+ receipt = state === undefined ? undefined : await workbench?.skills.synchronize(cwd, state, {
380
+ ...(managementAudit === undefined ? {} : { recorder: managementAudit }),
381
+ ...auditIdentity,
382
+ sessionId: sessionManager.getSessionId(),
383
+ cwd,
384
+ });
385
+ }
386
+ catch (error) {
387
+ if (error instanceof WorkbenchHttpError && error.status === 401 && accessHandle !== undefined)
388
+ workbench?.accessStates.delete(accessHandle);
389
+ throw error;
390
+ }
391
+ const workbenchTools = state === undefined || workbench === undefined ? [] : createWorkbenchToolDefinitions({
392
+ getState: () => workbench.accessStates.require(accessHandle),
393
+ workbench: workbench.client,
394
+ mcp: workbench.mcp,
395
+ invalidate: () => { if (accessHandle !== undefined)
396
+ workbench.accessStates.delete(accessHandle); },
397
+ logger,
398
+ ...(managementAudit === undefined ? {} : { audit: managementAudit }),
399
+ auditContext: {
400
+ ...auditIdentity,
401
+ sessionId: sessionManager.getSessionId(),
402
+ cwd,
403
+ },
404
+ });
405
+ const workbenchToolNames = workbenchTools.map((tool) => tool.name);
406
+ const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext, workbenchToolNames);
323
407
  return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
324
408
  const settingsManager = await createScopedSettingsManager({
325
409
  cwd,
@@ -333,24 +417,35 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, spaw
333
417
  settingsManager,
334
418
  resourceLoaderOptions: {
335
419
  agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
336
- skillsOverride: (base) => filterManagedProjectSkills(cwd, base),
420
+ skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base) : filterManagedWorkbenchSkills(cwd, base, receipt),
337
421
  },
338
422
  });
339
423
  const managedToolOptions = createManagedAgentToolOptions(cwd);
424
+ const scopedSpawn = spawn === undefined
425
+ ? undefined
426
+ : (input) => spawn({ ...input, managementContext });
427
+ const scopedSubsessions = subsessions === undefined
428
+ ? undefined
429
+ : {
430
+ ...subsessions,
431
+ spawn: (input) => subsessions.spawn({ ...input, managementContext }),
432
+ };
340
433
  const customTools = [
341
434
  ...managedAgentToolDefinitions(cwd, managedToolOptions),
342
435
  createManagedPythonToolDefinition(cwd, managementContext),
343
- ...(delegationToolsEnabled && spawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn }))] : []),
344
- ...(delegationToolsEnabled && subsessions !== undefined ? createSubsessionToolDefinitions(cwd, subsessions).map((tool) => defineTool(tool)) : []),
436
+ ...(delegationToolsEnabled && scopedSpawn !== undefined ? [defineTool(createSpawnSessionToolDefinition(cwd, { spawn: scopedSpawn }))] : []),
437
+ ...(delegationToolsEnabled && scopedSubsessions !== undefined ? createSubsessionToolDefinitions(cwd, scopedSubsessions).map((tool) => defineTool(tool)) : []),
345
438
  ...(askUser === undefined ? [] : [createAskUserToolDefinition(askUser)]),
439
+ ...workbenchTools,
346
440
  ];
347
441
  const options = {
348
442
  services,
349
443
  sessionManager,
350
444
  ...(sessionStartEvent === undefined ? {} : { sessionStartEvent }),
351
445
  customTools,
352
- tools: managementAgentToolNames(managementContext),
446
+ tools: managementAgentToolNames(managementContext, workbenchToolNames),
353
447
  ...(initialModel === undefined ? {} : { model: initialModel }),
448
+ ...(initialThinkingLevel === undefined ? {} : { thinkingLevel: initialThinkingLevel }),
354
449
  };
355
450
  const result = await createAgentSessionFromServices(options);
356
451
  return { ...result, services, diagnostics: services.diagnostics };
@@ -404,6 +499,8 @@ export class PiSessionService {
404
499
  this.active = new Map();
405
500
  this.runtimeBySession = new WeakMap();
406
501
  this.pendingSessionOpens = new Map();
502
+ /** Sessions whose extension binding is parked on a `session_start` dialog. */
503
+ this.startupSessions = new Map();
407
504
  this.activities = new SessionActivityCoordinator();
408
505
  /** Runtime-identity gate held while Pi may await abandoned-branch summarization. */
409
506
  this.treeNavigations = new WeakSet();
@@ -434,6 +531,8 @@ export class PiSessionService {
434
531
  */
435
532
  this.subsessionNotifyArmed = new Map();
436
533
  this.notificationGenerationBySession = new WeakMap();
534
+ /** The parked extension Promise resolvers behind the store's open dialogs. */
535
+ this.dialogWaiters = new ExtensionDialogWaiters();
437
536
  this.pendingUnreadMutations = [];
438
537
  this.unreadPublicationFlushRequested = false;
439
538
  this.unreadPublicationStopped = false;
@@ -474,10 +573,15 @@ export class PiSessionService {
474
573
  this.spawnTargets = deps.spawnTargets;
475
574
  this.projectWorkspaces = deps.projectWorkspaces;
476
575
  this.logger = deps.logger ?? noopLogger;
576
+ this.normalToolAudit = deps.normalToolAudit;
577
+ this.managementAudit = deps.managementAudit;
578
+ this.managementProjectIdForCwd = deps.managementProjectIdForCwd;
477
579
  this.now = deps.now ?? (() => new Date());
478
580
  this.notificationStore = deps.notificationStore ?? new SessionNotificationStore();
479
581
  this.unreadStore = deps.unreadStore ?? new SessionUnreadStore();
480
582
  this.pendingAskStore = deps.pendingAskStore ?? new PendingAskStore();
583
+ this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
584
+ this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
481
585
  this.catalogRefreshStatus = deps.catalogRefreshStatus;
482
586
  this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
483
587
  this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
@@ -489,7 +593,7 @@ export class PiSessionService {
489
593
  list: (parentSessionId, parentSessionFile) => this.listSubsessions(parentSessionId, parentSessionFile),
490
594
  check: (parentSessionId, sessionId, parentSessionFile) => this.checkSubsession(parentSessionId, sessionId, parentSessionFile),
491
595
  read: (parentSessionId, sessionId, query, parentSessionFile) => this.readSubsession(parentSessionId, sessionId, query, parentSessionFile),
492
- }, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined);
596
+ }, deps.askUserEnabled === true ? { open: (input) => this.openAsk(input) } : undefined, deps.workbench, this.logger, this.managementAudit, this.managementProjectIdForCwd);
493
597
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
494
598
  this.workspaceActivity = deps.workspaceActivity;
495
599
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
@@ -578,6 +682,10 @@ export class PiSessionService {
578
682
  this.clearUnreadPublicationRetry();
579
683
  clearInterval(this.heartbeat);
580
684
  this.clearCompactionDrainTimers();
685
+ // Same startup-park hazard as closeActive(): settle `session_start` dialogs
686
+ // of sessions still binding extensions before awaiting their pending opens.
687
+ for (const startup of this.startupSessions.values())
688
+ this.endSessionExtensionDialogs(startup.session);
581
689
  const pendingOpens = this.pendingSessionOpenPromises();
582
690
  if (pendingOpens.length > 0)
583
691
  await Promise.allSettled(pendingOpens);
@@ -585,9 +693,11 @@ export class PiSessionService {
585
693
  for (const active of activeSessions) {
586
694
  this.forgetUnreadActivity(active.runtime.session);
587
695
  this.pendingAskStore.forgetSession(active.runtime.session.sessionId);
696
+ this.endSessionExtensionDialogs(active.runtime.session);
588
697
  }
589
698
  this.active.clear();
590
699
  this.pendingSessionOpens.clear();
700
+ this.startupSessions.clear();
591
701
  this.activities.clear();
592
702
  this.compactionPromptQueues.clear();
593
703
  this.authLossWarnings.clear();
@@ -641,7 +751,7 @@ export class PiSessionService {
641
751
  return { sessionIds };
642
752
  }
643
753
  async setPinned(ref, pinned, managementContext) {
644
- if (typeof ref === "string" || ref.cwd === "")
754
+ if (ref.cwd === "")
645
755
  throw new Error("置顶会话必须提供工作区");
646
756
  const sessions = await this.list(ref.cwd, managementContext);
647
757
  const session = sessions.find((candidate) => candidate.id === ref.id);
@@ -745,6 +855,7 @@ export class PiSessionService {
745
855
  const active = await this.create(this.sessionManager.create(cwd, options.parentSession === undefined ? undefined : { parentSession: options.parentSession }), cwd, {
746
856
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
747
857
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
858
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
748
859
  ...(options.creationProvenance === undefined ? {} : { creationProvenance: options.creationProvenance }),
749
860
  ...(options.startupToken === undefined ? {} : { startupToken: options.startupToken }),
750
861
  startupIntent: "create",
@@ -779,10 +890,27 @@ export class PiSessionService {
779
890
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
780
891
  if (!decision.allowed)
781
892
  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);
893
+ // A model spec overrides the inherited model. Only a spec triggers a
894
+ // spawning-session lookup; the default path must not depend on it.
895
+ let model = input.model;
896
+ if (input.modelSpec !== undefined) {
897
+ model = await this.resolveSpawnModel({ id: input.spawningSessionId, cwd: input.spawningCwd }, input.modelSpec, input.managementContext);
898
+ }
899
+ const created = await this.start(decision.cwd, {
900
+ ...(model === undefined ? {} : { initialModel: model }),
901
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
902
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
903
+ });
904
+ const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
905
+ await this.prompt(created.id, input.prompt, undefined, undefined, {
906
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
907
+ });
784
908
  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 };
909
+ return {
910
+ sessionId: created.id,
911
+ cwd: decision.cwd,
912
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
913
+ };
786
914
  }
787
915
  /**
788
916
  * Start a *tracked* child session on behalf of a LLM. Identical to
@@ -792,15 +920,23 @@ export class PiSessionService {
792
920
  */
793
921
  async spawnSubsession(input) {
794
922
  if (this.spawnTargets === undefined)
795
- throw new Error("Spawning sessions is disabled");
923
+ throw new Error("派生会话已禁用");
796
924
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
797
925
  if (!decision.allowed)
798
926
  throw spawnTargetError(decision);
927
+ // A model spec overrides the inherited model and is resolved against the
928
+ // parent's model runtime; only a spec triggers that lookup.
929
+ const model = input.modelSpec === undefined
930
+ ? input.model
931
+ : await this.resolveSpawnModel({ id: input.parentSessionId, cwd: input.spawningCwd }, input.modelSpec, input.managementContext);
799
932
  const created = await this.startSession(decision.cwd, {
800
933
  ...(input.parentSessionFile === undefined ? {} : { parentSession: input.parentSessionFile }),
801
- ...(input.model === undefined ? {} : { initialModel: input.model }),
934
+ ...(model === undefined ? {} : { initialModel: model }),
935
+ ...(input.thinkingLevel === undefined ? {} : { initialThinkingLevel: input.thinkingLevel }),
936
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
802
937
  creationProvenance: "tracked-subsession",
803
938
  });
939
+ const modelUsed = this.activeForLookup(created.id, input.managementContext)?.runtime.session.model;
804
940
  const parentSessionFile = nonEmptyString(input.parentSessionFile);
805
941
  const link = {
806
942
  parentSessionId: input.parentSessionId,
@@ -812,9 +948,45 @@ export class PiSessionService {
812
948
  await this.registerVerifiedSubsession(link);
813
949
  this.persistSubsessionLink(link);
814
950
  this.persistSubsessionChildMarker(input.parentSessionId, created.id);
815
- await this.prompt(created.id, input.prompt);
951
+ await this.prompt(created.id, input.prompt, undefined, undefined, {
952
+ ...(input.managementContext === undefined ? {} : { managementContext: input.managementContext }),
953
+ });
816
954
  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 };
955
+ return {
956
+ sessionId: created.id,
957
+ cwd: decision.cwd,
958
+ ...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
959
+ };
960
+ }
961
+ /**
962
+ * The models a session may pick from: its scoped set when model-scoped,
963
+ * otherwise the runtime's available snapshot. Refreshes the runtime catalog
964
+ * first so callers see newly configured providers and models.
965
+ */
966
+ async sessionModelCandidates(session) {
967
+ await session.modelRuntime.refresh();
968
+ return session.scopedModels.length > 0
969
+ ? session.scopedModels.map((scoped) => scoped.model)
970
+ : await session.modelRuntime.getAvailable();
971
+ }
972
+ /**
973
+ * Resolve a strict `provider/model-id` spec from a spawn tool against the
974
+ * *spawning* session's model runtime, using the same candidates
975
+ * {@link setModel} offers plus a direct runtime lookup as fallback. Unknown
976
+ * or malformed specs throw; the agent loop turns that into an error tool
977
+ * result the spawning agent can retry from.
978
+ */
979
+ async resolveSpawnModel(spawningSession, modelSpec, managementContext) {
980
+ const session = await this.getOrOpen(spawningSession, managementContext);
981
+ const parsed = parseModelSpec(modelSpec);
982
+ const candidates = await this.sessionModelCandidates(session);
983
+ const model = parsed === undefined
984
+ ? undefined
985
+ : candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
986
+ ?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
987
+ if (model === undefined)
988
+ throw unknownSpawnModelError(modelSpec);
989
+ return model;
818
990
  }
819
991
  /**
820
992
  * Register the question set an agent wants the user to answer as the session's
@@ -895,14 +1067,129 @@ export class PiSessionService {
895
1067
  this.publishStatus(session);
896
1068
  }
897
1069
  /**
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.
1070
+ * Record the user's answer to an open extension dialog and resolve the
1071
+ * extension's parked Promise with it. Unlike an ask, nothing is delivered to
1072
+ * the model: the waiter is extension code inside an already in-flight run
1073
+ * (or an idle handler), so no custom message and no turn are triggered.
900
1074
  */
901
- publishStatusForSessionId(sessionId) {
902
- const session = this.active.get(sessionId)?.runtime.session;
903
- if (session !== undefined)
1075
+ async answerDialog(ref, dialogId, value, managementContext) {
1076
+ await this.assertWritable(ref);
1077
+ const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
1078
+ const result = this.pendingExtensionDialogStore.answer(this.dialogSessionKey(session), dialogId, value);
1079
+ if (result.status === "stale")
1080
+ return { result: "stale", sessionStatus: this.statusFromSession(session) };
1081
+ const { outcome } = result;
1082
+ this.publishDialogClosed(session, outcome);
1083
+ // `value` is what the store validated and recorded as the outcome's answer.
1084
+ this.dialogWaiters.settleWithAnswer(dialogId, value);
1085
+ this.publishStatus(session);
1086
+ return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1087
+ }
1088
+ /** Close an open extension dialog without an answer; the extension's wait settles with its kind's cancel value. */
1089
+ async cancelDialog(ref, dialogId, managementContext) {
1090
+ await this.assertWritable(ref);
1091
+ const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
1092
+ const result = this.pendingExtensionDialogStore.cancel(this.dialogSessionKey(session), dialogId, "cancelled");
1093
+ if (result.status === "stale")
1094
+ return { result: "stale", sessionStatus: this.statusFromSession(session) };
1095
+ const { outcome } = result;
1096
+ this.publishDialogClosed(session, outcome);
1097
+ this.dialogWaiters.settleWithCancelValue(dialogId);
1098
+ this.publishStatus(session);
1099
+ return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1100
+ }
1101
+ /**
1102
+ * Implement one `ctx.ui.select()`/`confirm()`/`input()` call from extension
1103
+ * code: open the store record, tell the browsers, and park a Promise that
1104
+ * settles when the browser answers or cancels, the extension's own
1105
+ * `signal`/`timeout` dismisses the dialog, the daemon default timeout
1106
+ * elapses, or the runtime goes away. `store.open` validates the dialog, so a
1107
+ * malformed one rejects the extension's call rather than rendering garbage.
1108
+ * `async` so a rejected dialog becomes a rejection rather than a synchronous
1109
+ * throw from a promise-returning method.
1110
+ */
1111
+ async openExtensionDialog(session, request, opts) {
1112
+ const signal = opts?.signal;
1113
+ // A pre-aborted signal dismisses the dialog before it ever opens.
1114
+ if (signal?.aborted === true)
1115
+ return extensionDialogCancelValue(request.kind);
1116
+ const timeoutMs = effectiveExtensionDialogTimeoutMs(opts?.timeout, this.extensionDialogsTimeoutMs);
1117
+ const dialog = this.pendingExtensionDialogStore.open({
1118
+ sessionId: this.dialogSessionKey(session),
1119
+ kind: request.kind,
1120
+ title: request.title,
1121
+ ...(request.message === undefined ? {} : { message: request.message }),
1122
+ ...(request.options === undefined ? {} : { options: request.options }),
1123
+ ...(request.placeholder === undefined ? {} : { placeholder: request.placeholder }),
1124
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1125
+ runScoped: session.isStreaming,
1126
+ });
1127
+ this.events.publish(session.sessionId, { type: "dialog.opened", dialog }, this.eventScopeForSession(session));
1128
+ this.publishStatus(session);
1129
+ return this.dialogWaiters.park(dialog, {
1130
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
1131
+ ...(signal === undefined ? {} : { signal }),
1132
+ onTrigger: (reason) => {
1133
+ if (this.closeExtensionDialogFromTrigger(session, dialog.dialogId, reason))
1134
+ this.publishStatus(session);
1135
+ },
1136
+ });
1137
+ }
1138
+ /**
1139
+ * Close a dialog whose wait ended without the browser (timeout, signal
1140
+ * abort, run end, runtime teardown) and settle its parked Promise. Returns
1141
+ * whether this call closed the dialog; a stale close means a browser answer
1142
+ * or an earlier trigger already settled everything.
1143
+ */
1144
+ closeExtensionDialogFromTrigger(session, dialogId, reason) {
1145
+ const result = this.pendingExtensionDialogStore.cancel(this.dialogSessionKey(session), dialogId, reason);
1146
+ if (result.status !== "closed")
1147
+ return false;
1148
+ this.publishDialogClosed(session, result.outcome);
1149
+ this.dialogWaiters.settleWithCancelValue(dialogId);
1150
+ return true;
1151
+ }
1152
+ /**
1153
+ * Settle the session's run-scoped dialogs as `"aborted"`. Runs at
1154
+ * abort-request time (a user abort parks the agent loop behind the dialog
1155
+ * handler, so `agent_end` would never arrive on its own) and again from
1156
+ * the `agent_end` observer as the run-crash backstop — the store makes the
1157
+ * second settlement a stale no-op. Idle-opened dialogs (a `session_start`
1158
+ * probe, say) are not run-scoped and survive, because their waiter
1159
+ * outlives the run.
1160
+ */
1161
+ abortRunScopedExtensionDialogs(session) {
1162
+ let closedAny = false;
1163
+ for (const dialog of this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session))) {
1164
+ if (dialog.runScoped)
1165
+ closedAny = this.closeExtensionDialogFromTrigger(session, dialog.dialogId, "aborted") || closedAny;
1166
+ }
1167
+ if (closedAny)
1168
+ this.publishStatus(session);
1169
+ }
1170
+ /**
1171
+ * Settle every dialog of the session as `"session-ended"`: the runtime
1172
+ * whose extension code is parked on them is being closed, replaced, or
1173
+ * disposed, so those Promises would otherwise never settle.
1174
+ */
1175
+ endSessionExtensionDialogs(session) {
1176
+ let closedAny = false;
1177
+ for (const dialog of this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session))) {
1178
+ closedAny = this.closeExtensionDialogFromTrigger(session, dialog.dialogId, "session-ended") || closedAny;
1179
+ }
1180
+ // Publishes only while the session is still (or already re-)registered as
1181
+ // active, so teardown paths stay silent and runtime replacement refreshes.
1182
+ if (closedAny)
904
1183
  this.publishStatus(session);
905
1184
  }
1185
+ publishDialogClosed(session, outcome) {
1186
+ this.events.publish(session.sessionId, {
1187
+ type: "dialog.closed",
1188
+ dialogId: outcome.dialogId,
1189
+ reason: outcome.reason,
1190
+ ...(outcome.answer === undefined ? {} : { answer: outcome.answer }),
1191
+ }, this.eventScopeForSession(session));
1192
+ }
906
1193
  /** Summaries of the tracked subsessions spawned by `parentSessionId`. */
907
1194
  async listSubsessions(parentSessionId, parentSessionFile) {
908
1195
  const parentFile = nonEmptyString(parentSessionFile);
@@ -1227,7 +1514,7 @@ export class PiSessionService {
1227
1514
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
1228
1515
  }
1229
1516
  async status(ref, managementContext) {
1230
- return this.statusFromSession(await this.getOrOpen(ref, managementContext));
1517
+ return this.statusFromSession(await this.sessionForStatusOrDialogClose(ref, managementContext));
1231
1518
  }
1232
1519
  async searchCandidates(cwd, normalizedQuery, managementContext) {
1233
1520
  const listing = await this.listWorkspaceSessions(cwd, managementContext);
@@ -1255,24 +1542,20 @@ export class PiSessionService {
1255
1542
  const streamingMessage = session.state.streamingMessage;
1256
1543
  const partial = streamingMessage === undefined || streamingMessage === null
1257
1544
  ? null
1258
- : projectBrowserMessage(streamingMessage);
1545
+ : annotateAssistantThinkingLevel(projectBrowserMessage(streamingMessage), session.thinkingLevel);
1259
1546
  return { seq, partial };
1260
1547
  }
1261
1548
  async availableModels(ref, managementContext) {
1262
1549
  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();
1550
+ const models = await this.sessionModelCandidates(session);
1267
1551
  return models.map(modelToClientModel);
1268
1552
  }
1269
1553
  async setModel(ref, provider, modelId, managementContext) {
1270
1554
  await this.assertWritable(ref);
1271
1555
  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();
1556
+ this.assertTreeNavigationInactive(session, "change models");
1557
+ const candidates = await this.sessionModelCandidates(session);
1558
+ this.assertTreeNavigationInactive(session, "change models");
1276
1559
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1277
1560
  ?? session.modelRuntime.getModel(provider, modelId);
1278
1561
  if (model === undefined)
@@ -1748,6 +2031,7 @@ export class PiSessionService {
1748
2031
  const sessionId = active.runtime.session.sessionId;
1749
2032
  this.clearCompactionPromptQueue(sessionId);
1750
2033
  clearSessionQueue(active.runtime.session);
2034
+ this.abortRunScopedExtensionDialogs(active.runtime.session);
1751
2035
  try {
1752
2036
  await this.abortSessionOperations(active.runtime.session);
1753
2037
  this.publishActivity(active.runtime.session, "已停止", "idle");
@@ -1770,7 +2054,17 @@ export class PiSessionService {
1770
2054
  return this.statusFromSession(session);
1771
2055
  }
1772
2056
  async stop(ref, managementContext) {
1773
- const active = this.activeForLookup(ref, managementContext);
2057
+ let active = this.activeForLookup(ref, managementContext);
2058
+ if (active === undefined) {
2059
+ const startup = this.startupSessionForLookup(ref, managementContext);
2060
+ if (startup !== undefined) {
2061
+ this.endSessionExtensionDialogs(startup);
2062
+ const pendingOpens = this.pendingSessionOpenPromises(startup.sessionId);
2063
+ if (pendingOpens.length > 0)
2064
+ await Promise.allSettled(pendingOpens);
2065
+ active = this.activeForLookup(ref, managementContext);
2066
+ }
2067
+ }
1774
2068
  if (active !== undefined) {
1775
2069
  await this.closeActiveSession(active);
1776
2070
  return;
@@ -1780,19 +2074,7 @@ export class PiSessionService {
1780
2074
  }
1781
2075
  }
1782
2076
  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 };
2077
+ return { sessionsByCwd: await this.listSessionsByCwd(refs.map((ref) => ref.cwd)) };
1796
2078
  }
1797
2079
  async listSessionsByCwd(cwds) {
1798
2080
  const uniqueCwds = uniqueStrings(cwds);
@@ -1966,6 +2248,10 @@ export class PiSessionService {
1966
2248
  return [...names];
1967
2249
  }
1968
2250
  async closeActive(sessionId, notificationPolicy = CLEAR_RUNTIME_NOTIFICATIONS) {
2251
+ for (const startup of this.startupSessions.values()) {
2252
+ if (startup.session.sessionId === sessionId)
2253
+ this.endSessionExtensionDialogs(startup.session);
2254
+ }
1969
2255
  const pendingOpens = this.pendingSessionOpenPromises(sessionId);
1970
2256
  if (pendingOpens.length > 0)
1971
2257
  await Promise.allSettled(pendingOpens);
@@ -1996,6 +2282,7 @@ export class PiSessionService {
1996
2282
  }
1997
2283
  this.forgetUnreadActivity(active.runtime.session);
1998
2284
  this.pendingAskStore.forgetSession(sessionId);
2285
+ this.endSessionExtensionDialogs(active.runtime.session);
1999
2286
  this.active.delete(activeSessionKey(sessionId, active.eventScope));
2000
2287
  this.activities.delete(activeSessionKey(sessionId, active.eventScope));
2001
2288
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd(), active.eventScope);
@@ -2045,6 +2332,26 @@ export class PiSessionService {
2045
2332
  async getOrOpen(ref, managementContext) {
2046
2333
  return (await this.getActive(ref, managementContext)).runtime.session;
2047
2334
  }
2335
+ startupSessionForLookup(ref, managementContext) {
2336
+ const contextKey = managementContextKey(managementContext);
2337
+ const eventScope = eventScopeFromManagementContext(managementContext);
2338
+ const sessionId = sessionIdFromLookup(ref);
2339
+ const exact = this.startupSessions.get(activeSessionKey(sessionId, eventScope));
2340
+ if (exact !== undefined && exact.managementContextKey === contextKey && lookupMatchesStartupSession(ref, exact.session))
2341
+ return exact.session;
2342
+ for (const startup of this.startupSessions.values()) {
2343
+ if (startup.managementContextKey !== contextKey || !startup.session.sessionId.startsWith(sessionId))
2344
+ continue;
2345
+ if (lookupMatchesStartupSession(ref, startup.session))
2346
+ return startup.session;
2347
+ }
2348
+ return undefined;
2349
+ }
2350
+ /** Resolve status and dialog-close calls even while extension startup is waiting for the browser. */
2351
+ async sessionForStatusOrDialogClose(ref, managementContext) {
2352
+ const reachable = this.activeForLookup(ref, managementContext)?.runtime.session ?? this.startupSessionForLookup(ref, managementContext);
2353
+ return reachable ?? this.getOrOpen(ref, managementContext);
2354
+ }
2048
2355
  async getActive(ref, managementContext) {
2049
2356
  const active = this.activeForLookup(ref, managementContext);
2050
2357
  if (active !== undefined)
@@ -2151,17 +2458,25 @@ export class PiSessionService {
2151
2458
  startup.end();
2152
2459
  }
2153
2460
  }
2154
- async bindSessionExtensions(session, generation) {
2461
+ async bindSessionExtensions(session, generation, owner) {
2155
2462
  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
- });
2463
+ const key = activeSessionKey(session.sessionId, owner.eventScope);
2464
+ this.startupSessions.set(key, { session, ...owner });
2465
+ try {
2466
+ await session.bindExtensions({
2467
+ uiContext,
2468
+ mode: "rpc",
2469
+ onError: (error) => {
2470
+ const message = `${error.extensionPath}: ${error.error}`;
2471
+ this.publishActivity(session, "扩展错误", "error", message);
2472
+ this.events.publish(session.sessionId, { type: "session.error", message }, owner.eventScope);
2473
+ },
2474
+ });
2475
+ }
2476
+ finally {
2477
+ if (this.startupSessions.get(key)?.session === session)
2478
+ this.startupSessions.delete(key);
2479
+ }
2165
2480
  }
2166
2481
  bindRuntime(active, session = active.runtime.session) {
2167
2482
  this.runtimeBySession.set(session, active.runtime);
@@ -2174,9 +2489,12 @@ export class PiSessionService {
2174
2489
  }
2175
2490
  }
2176
2491
  active.unsubscribe = session.subscribe((event) => {
2177
- this.events.publish(session.sessionId, toClientEvent(event), active.eventScope);
2492
+ this.events.publish(session.sessionId, toClientEvent(event, session.thinkingLevel), active.eventScope);
2493
+ this.auditToolExecutionEvent(active, event);
2178
2494
  this.publishActivityForEvent(session, event);
2179
2495
  const eventType = getString(event, "type");
2496
+ if (eventType === "agent_end")
2497
+ this.abortRunScopedExtensionDialogs(session);
2180
2498
  if (eventType === "compaction_end")
2181
2499
  this.scheduleCompactionQueueDrain(session.sessionId);
2182
2500
  if (eventType === "agent_start" || eventType === "agent_end")
@@ -2501,6 +2819,46 @@ export class PiSessionService {
2501
2819
  if (this.hasActiveWork(session))
2502
2820
  this.publishActivity(session, "正在处理", "active");
2503
2821
  }
2822
+ auditToolExecutionEvent(active, event) {
2823
+ const eventType = getString(event, "type");
2824
+ if (eventType !== "tool_execution_start" && eventType !== "tool_execution_end")
2825
+ return;
2826
+ const isEnd = eventType === "tool_execution_end";
2827
+ const auditEvent = {
2828
+ sessionId: active.runtime.session.sessionId,
2829
+ cwd: active.runtime.cwd,
2830
+ toolName: getString(event, "toolName") ?? "unknown",
2831
+ toolCallId: getString(event, "toolCallId") ?? "unknown",
2832
+ status: isEnd ? (getBoolean(event, "isError") === true ? "failed" : "completed") : "started",
2833
+ };
2834
+ const managementIdentity = active.managementAuditIdentity;
2835
+ this.logger.info({
2836
+ mode: active.managementContextKey === undefined ? "normal" : "management",
2837
+ ...(managementIdentity ?? {}),
2838
+ ...auditEvent,
2839
+ }, "Pi tool execution audit");
2840
+ if (managementIdentity !== undefined) {
2841
+ try {
2842
+ this.managementAudit?.record({ action: "tool_execution", ...managementIdentity, ...auditEvent });
2843
+ }
2844
+ catch (error) {
2845
+ this.logger.info({ sessionId: auditEvent.sessionId, toolCallId: auditEvent.toolCallId, error: error instanceof Error ? error.message : String(error) }, "failed to enqueue management-mode tool audit");
2846
+ }
2847
+ return;
2848
+ }
2849
+ if (this.normalToolAudit === undefined)
2850
+ return;
2851
+ try {
2852
+ this.normalToolAudit.record(auditEvent);
2853
+ }
2854
+ catch (error) {
2855
+ this.logger.info({
2856
+ sessionId: auditEvent.sessionId,
2857
+ toolCallId: auditEvent.toolCallId,
2858
+ error: error instanceof Error ? error.message : String(error),
2859
+ }, "failed to persist ordinary-mode tool audit");
2860
+ }
2861
+ }
2504
2862
  /**
2505
2863
  * Build the reporter for one session construction.
2506
2864
  *
@@ -2589,14 +2947,34 @@ export class PiSessionService {
2589
2947
  if (active.runtime.session === session)
2590
2948
  return active.eventScope;
2591
2949
  }
2950
+ for (const startup of this.startupSessions.values()) {
2951
+ if (startup.session === session)
2952
+ return startup.eventScope;
2953
+ }
2592
2954
  return NORMAL_SESSION_EVENT_SCOPE;
2593
2955
  }
2956
+ dialogSessionKey(session) {
2957
+ const eventScope = this.eventScopeForSession(session);
2958
+ return eventScope === NORMAL_SESSION_EVENT_SCOPE ? session.sessionId : activeSessionKey(session.sessionId, eventScope);
2959
+ }
2960
+ publishStatusForSessionId(sessionId) {
2961
+ const sessions = new Set();
2962
+ for (const active of this.activeSessionsForId(sessionId))
2963
+ sessions.add(active.runtime.session);
2964
+ for (const startup of this.startupSessions.values()) {
2965
+ if (startup.session.sessionId === sessionId)
2966
+ sessions.add(startup.session);
2967
+ }
2968
+ for (const session of sessions)
2969
+ this.publishStatus(session);
2970
+ }
2594
2971
  statusFromSession(session) {
2595
2972
  const stats = session.getSessionStats();
2596
2973
  const model = session.model === undefined ? undefined : modelToClientModel(session.model);
2597
2974
  const contextUsage = session.getContextUsage();
2598
2975
  const warnings = this.warningsForSession(session);
2599
2976
  const pendingAsk = this.pendingAskStore.pendingAsk(session.sessionId);
2977
+ const pendingDialogs = this.pendingExtensionDialogStore.pendingDialogs(this.dialogSessionKey(session));
2600
2978
  return {
2601
2979
  sessionId: session.sessionId,
2602
2980
  persisted: sessionFileExists(session.sessionFile),
@@ -2613,6 +2991,7 @@ export class PiSessionService {
2613
2991
  ...(contextUsage === undefined ? {} : { contextUsage }),
2614
2992
  ...(warnings.length === 0 ? {} : { warnings }),
2615
2993
  ...(pendingAsk === undefined ? {} : { pendingAsk }),
2994
+ ...(pendingDialogs.length === 0 ? {} : { pendingDialogs }),
2616
2995
  };
2617
2996
  }
2618
2997
  /**
@@ -2746,12 +3125,16 @@ export class PiSessionService {
2746
3125
  delegationToolsEnabled,
2747
3126
  ...(options.managementContext === undefined ? {} : { managementContext: options.managementContext }),
2748
3127
  ...(options.initialModel === undefined ? {} : { initialModel: options.initialModel }),
3128
+ ...(options.initialThinkingLevel === undefined ? {} : { initialThinkingLevel: options.initialThinkingLevel }),
2749
3129
  });
2750
3130
  const active = {
2751
3131
  runtime,
2752
3132
  unsubscribe: noop,
2753
3133
  managementContextKey: managementContextKey(options.managementContext),
2754
3134
  eventScope: eventScopeFromManagementContext(options.managementContext),
3135
+ ...(options.managementContext === undefined ? {} : {
3136
+ managementAuditIdentity: await resolveManagementAuditIdentity(cwd, options.managementContext, this.managementProjectIdForCwd, this.logger),
3137
+ }),
2755
3138
  };
2756
3139
  let boundSession = runtime.session;
2757
3140
  let notificationGeneration = options.notificationGeneration;
@@ -2783,7 +3166,7 @@ export class PiSessionService {
2783
3166
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
2784
3167
  }
2785
3168
  startup.report(STARTUP_PHASE_EXTENSIONS);
2786
- await this.bindSessionExtensions(runtime.session, notificationGeneration);
3169
+ await this.bindSessionExtensions(runtime.session, notificationGeneration, active);
2787
3170
  this.bindRuntime(active);
2788
3171
  runtime.setRebindSession(async (session) => {
2789
3172
  const priorGeneration = notificationGeneration;
@@ -2796,8 +3179,12 @@ export class PiSessionService {
2796
3179
  this.notificationGenerationBySession.set(session, candidateGeneration);
2797
3180
  }
2798
3181
  this.bindRuntime(active, session);
3182
+ // The runtime being replaced parked every dialog the store still
3183
+ // holds for this session; settle those waits before the new
3184
+ // runtime's extensions can open fresh dialogs under the same id.
3185
+ this.endSessionExtensionDialogs(boundSession);
2799
3186
  boundSession = session;
2800
- await this.bindSessionExtensions(session, candidateGeneration);
3187
+ await this.bindSessionExtensions(session, candidateGeneration, active);
2801
3188
  if (candidateGeneration !== undefined) {
2802
3189
  this.publishNotificationMutations(this.notificationStore.commitReplacement(candidateGeneration));
2803
3190
  notificationGeneration = candidateGeneration;
@@ -2831,6 +3218,7 @@ export class PiSessionService {
2831
3218
  }
2832
3219
  active.unsubscribe();
2833
3220
  this.forgetUnreadActivity(boundSession);
3221
+ this.endSessionExtensionDialogs(boundSession);
2834
3222
  let removedActive = false;
2835
3223
  for (const [sessionKey, candidate] of this.active.entries()) {
2836
3224
  if (candidate !== active)
@@ -2878,15 +3266,26 @@ export class PiSessionService {
2878
3266
  notificationId: added.notification.id,
2879
3267
  });
2880
3268
  };
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.
3269
+ // PI WEB owns the browser-facing dialog, notification, and text-formatting
3270
+ // boundaries: the three dialog primitives park daemon-held Promises that
3271
+ // the browser answers, while every other UI method delegates to Pi's
3272
+ // headless defaults so unsupported surfaces cancel safely instead of
3273
+ // hanging.
2884
3274
  return new Proxy(baseUiContext, {
2885
- get(target, property, receiver) {
3275
+ get: (target, property, receiver) => {
2886
3276
  if (property === "notify")
2887
3277
  return notify;
2888
3278
  if (property === "theme")
2889
3279
  return plainTextTheme;
3280
+ if (property === "confirm") {
3281
+ return (title, message, opts) => this.openExtensionDialog(session, { kind: "confirm", title, message }, opts);
3282
+ }
3283
+ if (property === "select") {
3284
+ return (title, options, opts) => this.openExtensionDialog(session, { kind: "select", title, options }, opts);
3285
+ }
3286
+ if (property === "input") {
3287
+ return (title, placeholder, opts) => this.openExtensionDialog(session, { kind: "input", title, placeholder }, opts);
3288
+ }
2890
3289
  const value = Reflect.get(target, property, receiver);
2891
3290
  return value;
2892
3291
  },
@@ -3040,7 +3439,7 @@ function uniqueBulkSessionRefs(refs) {
3040
3439
  const seen = new Set();
3041
3440
  const unique = [];
3042
3441
  for (const ref of refs) {
3043
- const key = `${ref.cwd ?? ""}\0${ref.id}`;
3442
+ const key = `${ref.cwd}\0${ref.id}`;
3044
3443
  if (seen.has(key))
3045
3444
  continue;
3046
3445
  seen.add(key);
@@ -3049,15 +3448,13 @@ function uniqueBulkSessionRefs(refs) {
3049
3448
  return unique;
3050
3449
  }
3051
3450
  function bulkRefToLookup(ref) {
3052
- return ref.cwd === undefined ? ref.id : { id: ref.id, cwd: ref.cwd };
3451
+ return ref;
3053
3452
  }
3054
3453
  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)));
3454
+ return records.find((record) => record.cwd === ref.cwd && (record.sessionId === ref.id || record.sessionId.startsWith(ref.id)));
3056
3455
  }
3057
3456
  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);
3457
+ return findSessionByIdOrPrefix(context.sessionsByCwd.get(ref.cwd) ?? [], ref.id);
3061
3458
  }
3062
3459
  function findSessionByIdOrPrefix(sessions, sessionId) {
3063
3460
  return sessions.find((session) => session.id === sessionId) ?? sessions.find((session) => session.id.startsWith(sessionId));
@@ -3352,11 +3749,17 @@ function historyMessages(session) {
3352
3749
  }
3353
3750
  function historyMessagesFromManager(sessionManager) {
3354
3751
  const messages = [];
3752
+ let thinkingLevel;
3355
3753
  for (const entry of sessionManager.getBranch()) {
3356
3754
  if (!isRecord(entry))
3357
3755
  continue;
3358
3756
  if (entry["type"] === "message")
3359
- messages.push(entry["message"]);
3757
+ messages.push(annotateAssistantThinkingLevel(entry["message"], thinkingLevel));
3758
+ else if (entry["type"] === "thinking_level_change") {
3759
+ const level = getString(entry, "thinkingLevel");
3760
+ if (level !== undefined)
3761
+ thinkingLevel = level;
3762
+ }
3360
3763
  else if (entry["type"] === "custom_message" && entry["display"] === true)
3361
3764
  messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
3362
3765
  else if (entry["type"] === "compaction")
@@ -3404,4 +3807,16 @@ function finalAssistantText(messages) {
3404
3807
  function isRecord(value) {
3405
3808
  return typeof value === "object" && value !== null;
3406
3809
  }
3810
+ async function resolveManagementAuditIdentity(cwd, context, resolveProjectId, logger) {
3811
+ let projectId = context.projects[0]?.id ?? "default-project";
3812
+ if (resolveProjectId !== undefined) {
3813
+ try {
3814
+ projectId = await resolveProjectId(cwd, context);
3815
+ }
3816
+ catch (error) {
3817
+ logger.info({ userId: context.user.id, cwd, error: error instanceof Error ? error.message : String(error) }, "failed to resolve management audit project; using the authorized default");
3818
+ }
3819
+ }
3820
+ return { userId: context.user.id, rootUserId: context.user.rootUserId, projectId };
3821
+ }
3407
3822
  //# sourceMappingURL=piSessionService.js.map