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