@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.
- package/dist/client/assets/{CodeViewer-BUT2Idcq.js → CodeViewer-CWKEeX0r.js} +1 -1
- package/dist/client/assets/{TerminalPanel-b8CZY7en.js → TerminalPanel-BmA7Ayzk.js} +1 -1
- package/dist/client/assets/{UnifiedDiffViewer-CXY7AXrD.js → UnifiedDiffViewer-2DACwxFY.js} +1 -1
- package/dist/client/assets/{index-80SI7dCz.js → index-D6j67Izj.js} +491 -300
- package/dist/client/index.html +1 -1
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/relays/relaysPanelElement.js +112 -46
- package/dist/server/piWebStatus.js +7 -1
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/sessiond/sessionServiceDependencies.js +1 -0
- package/dist/server/sessiond/sessionServiceDependencies.js.map +1 -1
- package/dist/server/sessiond.js +1 -0
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/extensionDialogWaiters.js +76 -0
- package/dist/server/sessions/extensionDialogWaiters.js.map +1 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js +196 -0
- package/dist/server/sessions/pendingExtensionDialogStore.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +375 -71
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +48 -25
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/sessionUiEvents.js +9 -2
- package/dist/server/sessions/sessionUiEvents.js.map +1 -1
- package/dist/server/sessions/spawnSessionTool.js +8 -1
- package/dist/server/sessions/spawnSessionTool.js.map +1 -1
- package/dist/server/sessions/spawnSubsessionTool.js +7 -1
- package/dist/server/sessions/spawnSubsessionTool.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +116 -1
- package/dist/shared/apiTypes.js +8 -0
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +2 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/docs/config.md +21 -1
- package/docs/plugins.md +14 -0
- 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 =
|
|
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
|
|
273
|
-
//
|
|
274
|
-
//
|
|
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 &&
|
|
344
|
-
...(delegationToolsEnabled &&
|
|
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 (
|
|
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
|
-
|
|
783
|
-
|
|
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 {
|
|
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("
|
|
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
|
-
...(
|
|
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 {
|
|
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
|
-
*
|
|
899
|
-
*
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
1273
|
-
const candidates = session
|
|
1274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
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
|
|
2883
|
-
//
|
|
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
|
|
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
|
|
3352
|
+
return ref;
|
|
3053
3353
|
}
|
|
3054
3354
|
function findArchivedRecordForBulkRef(records, ref) {
|
|
3055
|
-
return records.find((record) =>
|
|
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
|
-
|
|
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")
|