@chainingintention/pi-web-cn 1.202609.0 → 1.202609.2
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/cli.js +24 -14
- package/dist/cli.js.map +1 -1
- package/dist/client/apple-touch-icon-dev.png +0 -0
- package/dist/client/assets/{CodeViewer-gfeiLsvJ.js → CodeViewer-CY319sHK.js} +1 -1
- package/dist/client/assets/{TerminalPanel-CF7TJrJF.js → TerminalPanel-DvFd2ZkS.js} +1 -1
- package/dist/client/assets/{index-Y2sFZ4_U.js → index-Lt_wrMvn.js} +423 -359
- package/dist/client/favicon-dev.svg +8 -0
- package/dist/client/index.html +1 -1
- package/dist/client/pwa-icon-dev-192.png +0 -0
- package/dist/client/pwa-icon-dev-512.png +0 -0
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/environment.js +4 -0
- package/dist/environment.js.map +1 -0
- package/dist/nativeServices/installedServiceDefinitions.js +601 -0
- package/dist/nativeServices/installedServiceDefinitions.js.map +1 -0
- package/dist/nativeServices/serviceDoctor.js +47 -16
- package/dist/nativeServices/serviceDoctor.js.map +1 -1
- package/dist/{pi-web-plugins → pi-packages}/relays/markdownDocument.js +1 -1
- package/dist/pi-packages/relays/package.json +11 -0
- package/dist/{pi-web-plugins → pi-packages}/relays/pi-web-plugin.js +1 -1
- package/dist/pi-packages/relays/prompts/relay-worktree.md +28 -0
- package/dist/pi-packages/relays/prompts/relay.md +45 -0
- package/dist/{pi-web-plugins → pi-packages}/relays/relayDiscovery.js +2 -2
- package/dist/{pi-web-plugins → pi-packages}/relays/relaysPanelElement.js +2 -2
- package/dist/pi-packages/relays/skills/relay/SKILL.md +94 -0
- package/dist/pi-packages/relays/skills/relay-runner/SKILL.md +245 -0
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +34 -36
- package/dist/server/app.js +14 -3
- package/dist/server/app.js.map +1 -1
- package/dist/server/configRoutes.js +6 -1
- package/dist/server/configRoutes.js.map +1 -1
- package/dist/server/deploymentIdentity.js +67 -0
- package/dist/server/deploymentIdentity.js.map +1 -0
- package/dist/server/deploymentIdentityRoutes.js +22 -0
- package/dist/server/deploymentIdentityRoutes.js.map +1 -0
- package/dist/server/knownAutoInstallPiPackages.js +30 -0
- package/dist/server/knownAutoInstallPiPackages.js.map +1 -0
- package/dist/server/managementEmbed.js +9 -0
- package/dist/server/managementEmbed.js.map +1 -1
- package/dist/server/notices/serverNoticeRoutes.js +43 -0
- package/dist/server/notices/serverNoticeRoutes.js.map +1 -0
- package/dist/server/notices/serverNoticeService.js +26 -0
- package/dist/server/notices/serverNoticeService.js.map +1 -0
- package/dist/server/notices/serverNoticeStore.js +78 -0
- package/dist/server/notices/serverNoticeStore.js.map +1 -0
- package/dist/server/piPackageIdentity.js +25 -0
- package/dist/server/piPackageIdentity.js.map +1 -0
- package/dist/server/piPackageService.js +69 -8
- package/dist/server/piPackageService.js.map +1 -1
- package/dist/server/piWebStatus.js +1 -1
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/plugins/pluginBackendProxyRoutes.js +1 -10
- package/dist/server/plugins/pluginBackendProxyRoutes.js.map +1 -1
- package/dist/server/sessiond/autoInstallPiPackages.js +63 -0
- package/dist/server/sessiond/autoInstallPiPackages.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +3 -10
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +26 -5
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +2 -5
- package/dist/server/sessions/attachmentService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +122 -1
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +213 -104
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/projectSettingsScope.js +12 -31
- package/dist/server/sessions/projectSettingsScope.js.map +1 -1
- package/dist/server/sessions/relaySkill.js +107 -0
- package/dist/server/sessions/relaySkill.js.map +1 -0
- package/dist/server/sessions/sessionModelScope.js +23 -8
- package/dist/server/sessions/sessionModelScope.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +15 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/sessions/transcriptBranchCache.js +81 -0
- package/dist/server/sessions/transcriptBranchCache.js.map +1 -0
- package/dist/server/storage/piPackageDismissalStore.js +78 -0
- package/dist/server/storage/piPackageDismissalStore.js.map +1 -0
- package/dist/server/terminals/terminalService.js +18 -1
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaces/projectPiWebConfig.js +6 -1
- package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js +1 -10
- package/dist/server/workspaces/sessionDaemonWorkspaceCatalog.js.map +1 -1
- package/dist/server/workspaces/workspaceDeletionRoutes.js +1 -10
- package/dist/server/workspaces/workspaceDeletionRoutes.js.map +1 -1
- package/dist/server/workspaces/workspaceRemovalService.js +16 -5
- package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/federatedRoutes.js +3 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebConfigParsing.js +10 -0
- package/dist/shared/piWebConfigParsing.js.map +1 -1
- package/docs/config.md +26 -2
- package/docs/plugins.md +4 -2
- package/package.json +9 -8
- package/skills/relay/SKILL.md +59 -80
- package/dist/pi-web-plugins/relays/package.json +0 -9
- /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/README.md +0 -0
- /package/dist/{pi-web-plugins → pi-packages}/relays/vendor/marked.esm.js +0 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { projectBrowserMessage } from "../browserMessageProjection.js";
|
|
7
6
|
import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
|
|
@@ -14,6 +13,7 @@ import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sess
|
|
|
14
13
|
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
|
15
14
|
import { computeEditPreview } from "./editPreview.js";
|
|
16
15
|
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
|
16
|
+
import { loadEffectiveProjectAttachmentsConfig } from "../workspaces/projectPiWebConfig.js";
|
|
17
17
|
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
|
18
18
|
import { ASK_USER_ANSWERS_CUSTOM_TYPE, SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH, SESSION_UNREAD_LIMIT } from "../../shared/apiTypes.js";
|
|
19
19
|
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
|
@@ -38,12 +38,13 @@ import { createManagedAgentToolOptions, createManagedPythonToolDefinition } from
|
|
|
38
38
|
import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
|
|
39
39
|
import { SessionNotificationStore } from "./sessionNotificationStore.js";
|
|
40
40
|
import { plainTextTheme } from "./plainTextTheme.js";
|
|
41
|
+
import { deduplicateBundledRelaySkill, ensureManagedRelaySkill, MANAGED_RELAY_SKILL_DIRECTORY } from "./relaySkill.js";
|
|
41
42
|
import { SessionUnreadStore } from "./sessionUnreadStore.js";
|
|
42
43
|
import { archiveCandidateFromActiveSession, archiveCandidateFromArchivedRecord, archiveCandidateFromListEntry, archiveInputFromActiveSession, archiveInputFromCandidate, archiveInputFromListEntry, clientSessionFromArchivedRecord, compareArchivedRecords, } from "./sessionArchiveMapping.js";
|
|
43
44
|
import { annotateAssistantThinkingLevel, getBoolean, getProperty, getString, toClientEvent } from "./sessionUiEvents.js";
|
|
44
45
|
import { WorkbenchHttpError } from "../workbench/workbenchClient.js";
|
|
45
46
|
import { createWorkbenchToolDefinitions } from "../workbench/workbenchTools.js";
|
|
46
|
-
import { applyEnabledModelToggle, catalogWithEnabledFirst,
|
|
47
|
+
import { applyEnabledModelToggle, catalogWithEnabledFirst, modelScopeId, persistedEnabledModelPatterns, resolveEnabledModelIds, resolveSessionModelOptions, scopedModelsFromEnabledIds } from "./sessionModelScope.js";
|
|
47
48
|
const noopLogger = { info() { } };
|
|
48
49
|
const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
|
|
49
50
|
/**
|
|
@@ -62,6 +63,7 @@ const STARTUP_PHASE_EXTENSIONS = "正在加载会话扩展";
|
|
|
62
63
|
const STARTUP_CONCURRENT_CATALOG_REFRESH = "服务商模型列表正在刷新";
|
|
63
64
|
const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
|
|
64
65
|
const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
|
|
66
|
+
const IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS = 30_000;
|
|
65
67
|
function auditVisibleAssistantMessage(message) {
|
|
66
68
|
if (!isAuditRecord(message))
|
|
67
69
|
return message;
|
|
@@ -176,8 +178,6 @@ function parsePromptStreamingBehavior(value) {
|
|
|
176
178
|
throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
|
|
177
179
|
}
|
|
178
180
|
const GLOBAL_AGENT_CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
|
|
179
|
-
const MANAGED_RELAY_SKILL_DIRECTORY = join(".pi", "skills", "relay");
|
|
180
|
-
const BUNDLED_RELAY_SKILL_PATH = fileURLToPath(new URL("../../../skills/relay/SKILL.md", import.meta.url));
|
|
181
181
|
export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
|
|
182
182
|
return {
|
|
183
183
|
agentsFiles: base.agentsFiles.filter((file) => {
|
|
@@ -200,25 +200,6 @@ export function filterManagedProjectSkills(cwd, base) {
|
|
|
200
200
|
diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
|
|
201
201
|
};
|
|
202
202
|
}
|
|
203
|
-
export async function ensureManagedRelaySkill(cwd, bundledSkillPath = BUNDLED_RELAY_SKILL_PATH) {
|
|
204
|
-
const projectRoot = realpathSync(cwd);
|
|
205
|
-
const piDirectory = resolve(projectRoot, ".pi");
|
|
206
|
-
const skillsDirectory = join(piDirectory, "skills");
|
|
207
|
-
const skillDirectory = resolve(projectRoot, MANAGED_RELAY_SKILL_DIRECTORY);
|
|
208
|
-
const skillPath = join(skillDirectory, "SKILL.md");
|
|
209
|
-
for (const directory of [piDirectory, skillsDirectory, skillDirectory]) {
|
|
210
|
-
await ensureManagedSkillDirectory(projectRoot, directory);
|
|
211
|
-
}
|
|
212
|
-
try {
|
|
213
|
-
await copyFile(bundledSkillPath, skillPath, constants.COPYFILE_EXCL);
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
if (!isNodeError(error, "EEXIST"))
|
|
217
|
-
throw error;
|
|
218
|
-
}
|
|
219
|
-
assertManagedSkillPath(projectRoot, skillPath, "file");
|
|
220
|
-
return skillPath;
|
|
221
|
-
}
|
|
222
203
|
export function filterManagedWorkbenchSkills(cwd, base, receipt) {
|
|
223
204
|
const projectRoot = realpathSync(cwd);
|
|
224
205
|
const allowedDirectories = new Set([
|
|
@@ -243,27 +224,6 @@ function pathInsideRealDirectory(directory, path) {
|
|
|
243
224
|
return false;
|
|
244
225
|
}
|
|
245
226
|
}
|
|
246
|
-
async function ensureManagedSkillDirectory(projectRoot, path) {
|
|
247
|
-
try {
|
|
248
|
-
await mkdir(path);
|
|
249
|
-
}
|
|
250
|
-
catch (error) {
|
|
251
|
-
if (!isNodeError(error, "EEXIST"))
|
|
252
|
-
throw error;
|
|
253
|
-
}
|
|
254
|
-
assertManagedSkillPath(projectRoot, path, "directory");
|
|
255
|
-
}
|
|
256
|
-
function assertManagedSkillPath(projectRoot, path, expectedType) {
|
|
257
|
-
const realPath = realpathSync(path);
|
|
258
|
-
const stats = statSync(realPath);
|
|
259
|
-
const hasExpectedType = expectedType === "directory" ? stats.isDirectory() : stats.isFile();
|
|
260
|
-
if (!pathInsideOrEqual(projectRoot, realPath) || !hasExpectedType) {
|
|
261
|
-
throw new Error(`Managed skill path is invalid: ${path}`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function isNodeError(error, code) {
|
|
265
|
-
return error instanceof Error && "code" in error && error.code === code;
|
|
266
|
-
}
|
|
267
227
|
function pathInsideOrEqual(parent, child) {
|
|
268
228
|
const normalizedParent = resolve(parent);
|
|
269
229
|
const normalizedChild = resolve(child);
|
|
@@ -431,13 +391,16 @@ function createDefaultRuntimeFactory(normalModelRuntimeForCwd, managementModelRu
|
|
|
431
391
|
globalAgentDir: agentDir,
|
|
432
392
|
mode: "normal",
|
|
433
393
|
});
|
|
434
|
-
const resourceLoaderOptions =
|
|
394
|
+
const resourceLoaderOptions = {
|
|
395
|
+
...piWebResourceLoaderOptions(appendSystemPromptSections),
|
|
396
|
+
skillsOverride: (base) => deduplicateBundledRelaySkill(cwd, base),
|
|
397
|
+
};
|
|
435
398
|
const services = await createAgentSessionServices({
|
|
436
399
|
cwd,
|
|
437
400
|
agentDir,
|
|
438
401
|
modelRuntime: scopedModelRuntime,
|
|
439
402
|
settingsManager,
|
|
440
|
-
|
|
403
|
+
resourceLoaderOptions,
|
|
441
404
|
});
|
|
442
405
|
const modelOptions = await resolveSessionModelOptions({
|
|
443
406
|
services,
|
|
@@ -609,6 +572,13 @@ export class PiSessionService {
|
|
|
609
572
|
this.treeNavigations = new WeakSet();
|
|
610
573
|
/** Counts async operations that may append an entry before they settle. */
|
|
611
574
|
this.sessionEntryMutationCounts = new WeakMap();
|
|
575
|
+
/** Serializes enabled-model read/modify/write operations. */
|
|
576
|
+
this.modelScopeMutationQueue = Promise.resolve();
|
|
577
|
+
/** Effective enabled-model state keyed by the project-scoped settings store. */
|
|
578
|
+
this.modelScopeStates = new Map();
|
|
579
|
+
this.modelScopeInitializations = new Map();
|
|
580
|
+
this.modelScopeRevision = 0;
|
|
581
|
+
this.modelScopeCache = new WeakMap();
|
|
612
582
|
/** Runtime/session-identity reservations for operations that must not overlap tree navigation. */
|
|
613
583
|
this.treeExclusiveRuntimeOperationCounts = new WeakMap();
|
|
614
584
|
this.treeExclusiveSessionOperationCounts = new Map();
|
|
@@ -633,6 +603,7 @@ export class PiSessionService {
|
|
|
633
603
|
*/
|
|
634
604
|
this.subsessionNotifyArmed = new Map();
|
|
635
605
|
this.notificationGenerationBySession = new WeakMap();
|
|
606
|
+
this.idleSessionFileResolutions = new WeakMap();
|
|
636
607
|
/** The parked extension Promise resolvers behind the store's open dialogs. */
|
|
637
608
|
this.dialogWaiters = new ExtensionDialogWaiters();
|
|
638
609
|
this.pendingUnreadMutations = [];
|
|
@@ -671,6 +642,7 @@ export class PiSessionService {
|
|
|
671
642
|
this.pendingExtensionDialogStore = deps.pendingExtensionDialogStore ?? new PendingExtensionDialogStore();
|
|
672
643
|
this.extensionDialogsTimeoutMs = deps.extensionDialogsTimeoutMs ?? DEFAULT_EXTENSION_DIALOGS_TIMEOUT_MS;
|
|
673
644
|
this.catalogRefreshStatus = deps.catalogRefreshStatus;
|
|
645
|
+
this.config = deps.config;
|
|
674
646
|
this.unreadPublicationRetryInitialMs = Math.max(0, deps.unreadPublicationRetryDelayMs ?? DEFAULT_UNREAD_PUBLICATION_RETRY_MS);
|
|
675
647
|
this.unreadPublicationRetryDelayMs = this.unreadPublicationRetryInitialMs;
|
|
676
648
|
// Subsessions are gated behind their own flag, and they
|
|
@@ -987,24 +959,84 @@ export class PiSessionService {
|
|
|
987
959
|
...(modelUsed === undefined ? {} : { model: modelSpecOf(modelUsed) }),
|
|
988
960
|
};
|
|
989
961
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
962
|
+
modelScopeContext(session) {
|
|
963
|
+
const settingsKey = session.settingsManager.piWebScopeKey ?? "global";
|
|
964
|
+
const projectSettings = session.settingsManager.getProjectSettings?.() ?? {};
|
|
965
|
+
if (Object.hasOwn(projectSettings, "enabledModels")) {
|
|
966
|
+
const workspace = canonicalizeStoredCwd(session.sessionManager.getCwd());
|
|
967
|
+
return { key: `${settingsKey}:workspace:${workspace}`, editable: false };
|
|
968
|
+
}
|
|
969
|
+
return { key: settingsKey, editable: session.settingsManager.piWebModelScopeEditable !== false };
|
|
970
|
+
}
|
|
971
|
+
async modelScopeSnapshot(session, context = this.modelScopeContext(session)) {
|
|
972
|
+
const existing = this.modelScopeStates.get(context.key);
|
|
973
|
+
if (existing !== undefined)
|
|
974
|
+
return existing;
|
|
975
|
+
const pending = this.modelScopeInitializations.get(context.key);
|
|
976
|
+
if (pending !== undefined)
|
|
977
|
+
return pending;
|
|
978
|
+
const initialization = (async () => {
|
|
979
|
+
const enabledIds = await resolveEnabledModelIds({
|
|
980
|
+
settingsManager: session.settingsManager,
|
|
981
|
+
// Runtime-backed sessions always hold the SDK ModelRuntime; PiAgentSession keeps a narrow public seam for tests.
|
|
982
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
983
|
+
modelRuntime: session.modelRuntime,
|
|
984
|
+
scopedModels: session.scopedModels,
|
|
985
|
+
});
|
|
986
|
+
const snapshot = { enabledIds: enabledIds === null ? null : [...enabledIds], revision: 0 };
|
|
987
|
+
this.modelScopeStates.set(context.key, snapshot);
|
|
988
|
+
return snapshot;
|
|
989
|
+
})();
|
|
990
|
+
this.modelScopeInitializations.set(context.key, initialization);
|
|
991
|
+
try {
|
|
992
|
+
return await initialization;
|
|
993
|
+
}
|
|
994
|
+
finally {
|
|
995
|
+
if (this.modelScopeInitializations.get(context.key) === initialization)
|
|
996
|
+
this.modelScopeInitializations.delete(context.key);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async synchronizeSessionModelScope(session, available) {
|
|
1000
|
+
const context = this.modelScopeContext(session);
|
|
1001
|
+
const snapshot = await this.modelScopeSnapshot(session, context);
|
|
1002
|
+
const catalogKey = available.map(modelScopeId).join("\0");
|
|
1003
|
+
const cached = this.modelScopeCache.get(session);
|
|
1004
|
+
if (cached?.scopeKey === context.key && cached.revision === snapshot.revision && cached.catalogKey === catalogKey)
|
|
1005
|
+
return;
|
|
1006
|
+
session.setScopedModels(scopedModelsFromEnabledIds(available, snapshot.enabledIds, session.scopedModels));
|
|
1007
|
+
this.modelScopeCache.set(session, { scopeKey: context.key, revision: snapshot.revision, catalogKey });
|
|
1008
|
+
}
|
|
995
1009
|
async sessionModelCandidates(session) {
|
|
996
|
-
// Pi 0.84 keeps request paths local-only; the bounded catalog refresher is
|
|
997
|
-
// the only path allowed to fetch provider catalogs over the network.
|
|
998
1010
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1011
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1012
|
+
await this.synchronizeSessionModelScope(session, available);
|
|
1013
|
+
return session.scopedModels.length > 0 ? session.scopedModels.map((scoped) => scoped.model) : available;
|
|
1002
1014
|
}
|
|
1003
|
-
async enabledModelCatalog(
|
|
1004
|
-
const session = runtime.session;
|
|
1015
|
+
async enabledModelCatalog(session) {
|
|
1005
1016
|
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1017
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1018
|
+
await this.synchronizeSessionModelScope(session, available);
|
|
1019
|
+
const context = this.modelScopeContext(session);
|
|
1020
|
+
return catalogWithEnabledFirst(available, (await this.modelScopeSnapshot(session, context)).enabledIds);
|
|
1021
|
+
}
|
|
1022
|
+
runModelScopeMutation(operation) {
|
|
1023
|
+
const queued = this.modelScopeMutationQueue.then(operation);
|
|
1024
|
+
this.modelScopeMutationQueue = queued.then(() => undefined, () => undefined);
|
|
1025
|
+
return queued;
|
|
1026
|
+
}
|
|
1027
|
+
applyEnabledModelScope(session, available, enabledIds) {
|
|
1028
|
+
const context = this.modelScopeContext(session);
|
|
1029
|
+
const availableIds = available.map(modelScopeId);
|
|
1030
|
+
const persisted = persistedEnabledModelPatterns(enabledIds, availableIds);
|
|
1031
|
+
const nextState = {
|
|
1032
|
+
enabledIds: persisted === undefined ? null : enabledIds === null ? [] : [...enabledIds],
|
|
1033
|
+
revision: ++this.modelScopeRevision,
|
|
1034
|
+
};
|
|
1035
|
+
session.settingsManager.setEnabledModels(persisted);
|
|
1036
|
+
this.modelScopeStates.set(context.key, nextState);
|
|
1037
|
+
session.setScopedModels(scopedModelsFromEnabledIds(available, nextState.enabledIds, session.scopedModels));
|
|
1038
|
+
this.modelScopeCache.set(session, { scopeKey: context.key, revision: nextState.revision, catalogKey: available.map(modelScopeId).join("\0") });
|
|
1039
|
+
this.events.publishGlobal({ type: "models.changed", revision: nextState.revision });
|
|
1008
1040
|
}
|
|
1009
1041
|
/**
|
|
1010
1042
|
* Resolve a strict `provider/model-id` spec from a spawn tool against the
|
|
@@ -1548,10 +1580,14 @@ export class PiSessionService {
|
|
|
1548
1580
|
}
|
|
1549
1581
|
async messages(ref, page, managementContext) {
|
|
1550
1582
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1551
|
-
return pageMessagesAtSafeBoundary(
|
|
1583
|
+
return pageMessagesAtSafeBoundary(historyMessagesFromEntries(await this.readableSessionBranch(session)), page);
|
|
1552
1584
|
}
|
|
1553
1585
|
async status(ref, managementContext) {
|
|
1554
|
-
|
|
1586
|
+
const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
|
|
1587
|
+
if (this.hasActiveWork(session))
|
|
1588
|
+
return this.statusFromSession(session);
|
|
1589
|
+
const branch = await this.readableSessionBranch(session);
|
|
1590
|
+
return this.statusFromSession(session, transcriptMessageCount(branch));
|
|
1555
1591
|
}
|
|
1556
1592
|
async searchCandidates(cwd, normalizedQuery, managementContext) {
|
|
1557
1593
|
const listing = await this.listWorkspaceSessions(cwd, managementContext);
|
|
@@ -1588,38 +1624,60 @@ export class PiSessionService {
|
|
|
1588
1624
|
return models.map(modelToClientModel);
|
|
1589
1625
|
}
|
|
1590
1626
|
async modelCatalog(ref, managementContext) {
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1627
|
+
const session = await this.getOrOpen(ref, managementContext);
|
|
1628
|
+
const scope = this.modelScopeContext(session);
|
|
1629
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry, scope.editable));
|
|
1593
1630
|
}
|
|
1594
1631
|
async setModelEnabled(ref, provider, modelId, enabled, managementContext) {
|
|
1595
1632
|
if (managementContext !== undefined)
|
|
1596
1633
|
throw new Error("管理嵌入模式不允许修改已启用模型范围");
|
|
1597
1634
|
await this.assertWritable(ref);
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1635
|
+
const session = await this.getOrOpen(ref);
|
|
1636
|
+
return this.runModelScopeMutation(async () => {
|
|
1637
|
+
const scope = this.modelScopeContext(session);
|
|
1638
|
+
if (!scope.editable)
|
|
1639
|
+
throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
|
|
1640
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1641
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1642
|
+
const currentIds = (await this.modelScopeSnapshot(session, scope)).enabledIds;
|
|
1643
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1644
|
+
const availableIds = available.map(modelScopeId);
|
|
1645
|
+
const targetId = `${provider}/${modelId}`;
|
|
1646
|
+
if (!availableIds.includes(targetId))
|
|
1647
|
+
throw new Error(`未找到模型:${targetId}`);
|
|
1648
|
+
if (!enabled && session.model !== undefined && modelScopeId(session.model) === targetId)
|
|
1649
|
+
throw new Error("不能停用当前模型");
|
|
1650
|
+
const nextIds = applyEnabledModelToggle(currentIds, availableIds, targetId, enabled);
|
|
1651
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1652
|
+
if (nextIds !== currentIds)
|
|
1653
|
+
this.applyEnabledModelScope(session, available, nextIds);
|
|
1654
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
1655
|
+
});
|
|
1656
|
+
}
|
|
1657
|
+
async setModelScope(ref, mode, managementContext) {
|
|
1658
|
+
if (managementContext !== undefined)
|
|
1659
|
+
throw new Error("管理嵌入模式不允许修改已启用模型范围");
|
|
1660
|
+
await this.assertWritable(ref);
|
|
1661
|
+
const session = await this.getOrOpen(ref);
|
|
1662
|
+
return this.runModelScopeMutation(async () => {
|
|
1663
|
+
const scope = this.modelScopeContext(session);
|
|
1664
|
+
if (!scope.editable)
|
|
1665
|
+
throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
|
|
1666
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1667
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1668
|
+
const available = session.modelRuntime.getAvailableSnapshot();
|
|
1669
|
+
const availableIds = available.map(modelScopeId);
|
|
1670
|
+
let nextIds = null;
|
|
1671
|
+
if (mode === "current") {
|
|
1672
|
+
const current = session.model;
|
|
1673
|
+
if (current === undefined || !availableIds.includes(modelScopeId(current)))
|
|
1674
|
+
throw new Error("当前模型不可用");
|
|
1675
|
+
nextIds = [modelScopeId(current)];
|
|
1676
|
+
}
|
|
1677
|
+
this.assertTreeNavigationInactive(session, "change enabled models");
|
|
1678
|
+
this.applyEnabledModelScope(session, available, nextIds);
|
|
1679
|
+
return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
|
|
1680
|
+
});
|
|
1623
1681
|
}
|
|
1624
1682
|
async setModel(ref, provider, modelId, managementContext) {
|
|
1625
1683
|
await this.assertWritable(ref);
|
|
@@ -1639,7 +1697,11 @@ export class PiSessionService {
|
|
|
1639
1697
|
async cycleModel(ref, direction, managementContext) {
|
|
1640
1698
|
await this.assertWritable(ref);
|
|
1641
1699
|
const session = await this.getOrOpen(ref, managementContext);
|
|
1642
|
-
const result = await this.
|
|
1700
|
+
const result = await this.runModelScopeMutation(async () => {
|
|
1701
|
+
await session.modelRuntime.refresh({ allowNetwork: false });
|
|
1702
|
+
await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
|
|
1703
|
+
return this.runSessionEntryMutation(session, "change models", () => session.cycleModel(direction));
|
|
1704
|
+
});
|
|
1643
1705
|
if (result === undefined)
|
|
1644
1706
|
throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
|
|
1645
1707
|
this.publishActivity(session, `模型:${result.model.id}`, "idle", result.model.provider);
|
|
@@ -1752,7 +1814,13 @@ export class PiSessionService {
|
|
|
1752
1814
|
return [];
|
|
1753
1815
|
await this.assertWritable(ref);
|
|
1754
1816
|
const active = await this.getActive(ref, managementContext);
|
|
1755
|
-
|
|
1817
|
+
const cwd = active.runtime.cwd;
|
|
1818
|
+
const effectiveFolder = folder ?? (await this.workspaceAttachmentsConfig(cwd)).defaultFolder;
|
|
1819
|
+
return saveAttachmentsToWorkspace(cwd, parsed, effectiveFolder === undefined ? {} : { folder: effectiveFolder });
|
|
1820
|
+
}
|
|
1821
|
+
async workspaceAttachmentsConfig(cwd) {
|
|
1822
|
+
const globalConfig = this.config === undefined ? {} : (await this.config.read()).effectiveConfig;
|
|
1823
|
+
return loadEffectiveProjectAttachmentsConfig(cwd, globalConfig);
|
|
1756
1824
|
}
|
|
1757
1825
|
async shell(ref, text, managementContext) {
|
|
1758
1826
|
await this.assertWritable(ref);
|
|
@@ -2492,6 +2560,35 @@ export class PiSessionService {
|
|
|
2492
2560
|
const reachable = this.activeForLookup(ref, managementContext)?.runtime.session ?? this.startupSessionForLookup(ref, managementContext);
|
|
2493
2561
|
return reachable ?? this.getOrOpen(ref, managementContext);
|
|
2494
2562
|
}
|
|
2563
|
+
async readableSessionBranch(session) {
|
|
2564
|
+
if (this.hasActiveWork(session) || this.sessionManager.readBranch === undefined)
|
|
2565
|
+
return session.sessionManager.getBranch();
|
|
2566
|
+
const leafId = session.sessionManager.getLeafId();
|
|
2567
|
+
const latestEntry = session.sessionManager.getEntries?.().at(-1);
|
|
2568
|
+
// Unsummarized tree navigation changes only the SDK's in-memory leaf.
|
|
2569
|
+
if (isRecord(latestEntry) && typeof latestEntry["id"] === "string" && latestEntry["id"] !== leafId)
|
|
2570
|
+
return session.sessionManager.getBranch();
|
|
2571
|
+
const sessionFile = session.sessionFile ?? session.sessionManager.getSessionFile();
|
|
2572
|
+
const resolvedPath = sessionFile === undefined ? await this.idleSessionFilePath(session) : undefined;
|
|
2573
|
+
if (this.hasActiveWork(session))
|
|
2574
|
+
return session.sessionManager.getBranch();
|
|
2575
|
+
const path = sessionFile ?? resolvedPath;
|
|
2576
|
+
if (path === undefined)
|
|
2577
|
+
return session.sessionManager.getBranch();
|
|
2578
|
+
const snapshot = await this.sessionManager.readBranch(path);
|
|
2579
|
+
if (snapshot === undefined)
|
|
2580
|
+
return session.sessionManager.getBranch();
|
|
2581
|
+
return this.hasActiveWork(session) || session.sessionManager.getLeafId() !== leafId ? session.sessionManager.getBranch() : snapshot;
|
|
2582
|
+
}
|
|
2583
|
+
async idleSessionFilePath(session) {
|
|
2584
|
+
const cached = this.idleSessionFileResolutions.get(session);
|
|
2585
|
+
const at = this.now().getTime();
|
|
2586
|
+
if (cached !== undefined && at - cached.at < IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS)
|
|
2587
|
+
return cached.path;
|
|
2588
|
+
const match = await this.sessionManager.resolveSessionFile?.(session.sessionManager.getCwd(), session.sessionId);
|
|
2589
|
+
this.idleSessionFileResolutions.set(session, { at, path: match?.path });
|
|
2590
|
+
return match?.path;
|
|
2591
|
+
}
|
|
2495
2592
|
async getActive(ref, managementContext) {
|
|
2496
2593
|
const active = this.activeForLookup(ref, managementContext);
|
|
2497
2594
|
if (active !== undefined)
|
|
@@ -3153,7 +3250,7 @@ export class PiSessionService {
|
|
|
3153
3250
|
for (const session of sessions)
|
|
3154
3251
|
this.publishStatus(session);
|
|
3155
3252
|
}
|
|
3156
|
-
statusFromSession(session) {
|
|
3253
|
+
statusFromSession(session, messageCount = session.messages.length) {
|
|
3157
3254
|
const stats = session.getSessionStats();
|
|
3158
3255
|
const model = session.model === undefined ? undefined : modelToClientModel(session.model);
|
|
3159
3256
|
const contextUsage = session.getContextUsage();
|
|
@@ -3170,7 +3267,7 @@ export class PiSessionService {
|
|
|
3170
3267
|
isBashRunning: session.isBashRunning,
|
|
3171
3268
|
pendingMessageCount: this.pendingMessageCount(session),
|
|
3172
3269
|
queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
|
|
3173
|
-
messageCount
|
|
3270
|
+
messageCount,
|
|
3174
3271
|
tokens: stats.tokens,
|
|
3175
3272
|
cost: stats.cost,
|
|
3176
3273
|
...(contextUsage === undefined ? {} : { contextUsage }),
|
|
@@ -3684,14 +3781,15 @@ function modelToClientModel(model) {
|
|
|
3684
3781
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
3685
3782
|
};
|
|
3686
3783
|
}
|
|
3687
|
-
function
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3784
|
+
function catalogEntryToClientModel(entry, editable = true) {
|
|
3785
|
+
return {
|
|
3786
|
+
...modelToClientModel(entry.model),
|
|
3787
|
+
provider: entry.model.provider,
|
|
3788
|
+
id: entry.model.id,
|
|
3789
|
+
enabled: entry.enabled,
|
|
3790
|
+
catalogIndex: entry.catalogIndex,
|
|
3791
|
+
...(editable ? {} : { editable: false }),
|
|
3792
|
+
};
|
|
3695
3793
|
}
|
|
3696
3794
|
function notificationIdentityForSession(session) {
|
|
3697
3795
|
return {
|
|
@@ -3963,9 +4061,12 @@ function historyMessages(session) {
|
|
|
3963
4061
|
return historyMessagesFromManager(session.sessionManager);
|
|
3964
4062
|
}
|
|
3965
4063
|
function historyMessagesFromManager(sessionManager) {
|
|
4064
|
+
return historyMessagesFromEntries(sessionManager.getBranch());
|
|
4065
|
+
}
|
|
4066
|
+
function historyMessagesFromEntries(entries) {
|
|
3966
4067
|
const messages = [];
|
|
3967
4068
|
let thinkingLevel;
|
|
3968
|
-
for (const entry of
|
|
4069
|
+
for (const entry of entries) {
|
|
3969
4070
|
if (!isRecord(entry))
|
|
3970
4071
|
continue;
|
|
3971
4072
|
if (entry["type"] === "message")
|
|
@@ -3984,6 +4085,14 @@ function historyMessagesFromManager(sessionManager) {
|
|
|
3984
4085
|
}
|
|
3985
4086
|
return messages;
|
|
3986
4087
|
}
|
|
4088
|
+
function transcriptMessageCount(entries) {
|
|
4089
|
+
let count = 0;
|
|
4090
|
+
for (const entry of entries) {
|
|
4091
|
+
if (isRecord(entry) && entry["type"] === "message")
|
|
4092
|
+
count += 1;
|
|
4093
|
+
}
|
|
4094
|
+
return count;
|
|
4095
|
+
}
|
|
3987
4096
|
/** custom entry type used to persist parent -> child subsession links outside LLM context. */
|
|
3988
4097
|
const SUBSESSION_LINK_CUSTOM_TYPE = "pi-web.subsession.link";
|
|
3989
4098
|
/** custom entry type used to mark a child as created by spawn_subsession. */
|