@chainingintention/pi-web-cn 1.202606.11 → 1.202606.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +14 -7
  2. package/dist/cli.js +14 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-DzeGsHZ5.js → CodeViewer-D5OA_6r4.js} +1 -1
  5. package/dist/client/assets/{TerminalPanel-BghODb4T.js → TerminalPanel-kfPHfhUe.js} +9 -9
  6. package/dist/client/assets/index-MmspRbMF.js +2403 -0
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +54 -3
  9. package/dist/config.js.map +1 -1
  10. package/dist/pi-web-plugins/updates/package.json +1 -1
  11. package/dist/pi-web-plugins/updates/pi-web-plugin.js +61 -71
  12. package/dist/pi-web-plugins/updates/updatesLogic.js +65 -0
  13. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +1 -5
  14. package/dist/server/app.js +14 -6
  15. package/dist/server/app.js.map +1 -1
  16. package/dist/server/configRoutes.js +10 -0
  17. package/dist/server/configRoutes.js.map +1 -1
  18. package/dist/server/index.js +2 -2
  19. package/dist/server/index.js.map +1 -1
  20. package/dist/server/machines/machinePluginProxyRoutes.js +8 -0
  21. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  22. package/dist/server/machines/machineRoutes.js +6 -0
  23. package/dist/server/machines/machineRoutes.js.map +1 -1
  24. package/dist/server/machines/machineService.js +97 -5
  25. package/dist/server/machines/machineService.js.map +1 -1
  26. package/dist/server/piWebPluginService.js +24 -4
  27. package/dist/server/piWebPluginService.js.map +1 -1
  28. package/dist/server/piWebStatus.js +149 -45
  29. package/dist/server/piWebStatus.js.map +1 -1
  30. package/dist/server/piWebStatusCache.js +32 -0
  31. package/dist/server/piWebStatusCache.js.map +1 -0
  32. package/dist/server/sessiond/sessionProxyRoutes.js +15 -1
  33. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  34. package/dist/server/sessiond.js +34 -9
  35. package/dist/server/sessiond.js.map +1 -1
  36. package/dist/server/sessions/attachmentService.js +61 -0
  37. package/dist/server/sessions/attachmentService.js.map +1 -0
  38. package/dist/server/sessions/builtinCommands.js +21 -21
  39. package/dist/server/sessions/builtinCommands.js.map +1 -1
  40. package/dist/server/sessions/oauthLoginFlowService.js +21 -14
  41. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  42. package/dist/server/sessions/piSessionManagerGateway.js +96 -0
  43. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
  44. package/dist/server/sessions/piSessionService.js +270 -452
  45. package/dist/server/sessions/piSessionService.js.map +1 -1
  46. package/dist/server/sessions/sessionArchiveStore.js +16 -2
  47. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  48. package/dist/server/sessions/sessionCommandService.js +32 -28
  49. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  50. package/dist/server/sessions/sessionRoutes.js +157 -47
  51. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  52. package/dist/server/sessions/spawnSessionTool.js +36 -0
  53. package/dist/server/sessions/spawnSessionTool.js.map +1 -0
  54. package/dist/server/sessions/spawnTargetResolver.js +36 -0
  55. package/dist/server/sessions/spawnTargetResolver.js.map +1 -0
  56. package/dist/server/terminals/terminalRoutes.js +10 -4
  57. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  58. package/dist/server/workingDirectory.js +44 -0
  59. package/dist/server/workingDirectory.js.map +1 -0
  60. package/dist/server/workspaces/fileSuggestions.js +96 -16
  61. package/dist/server/workspaces/fileSuggestions.js.map +1 -1
  62. package/dist/shared/apiTypes.d.ts +77 -5
  63. package/dist/shared/apiTypes.js +5 -1
  64. package/dist/shared/apiTypes.js.map +1 -1
  65. package/dist/shared/capabilities.js +27 -0
  66. package/dist/shared/capabilities.js.map +1 -0
  67. package/dist/shared/federatedRoutes.js +3 -0
  68. package/dist/shared/federatedRoutes.js.map +1 -1
  69. package/dist/shared/piWebStatusParsing.js +28 -0
  70. package/dist/shared/piWebStatusParsing.js.map +1 -1
  71. package/dist/shared/promptAttachments.js +73 -0
  72. package/dist/shared/promptAttachments.js.map +1 -0
  73. package/dist/shared/thinkingLevels.d.ts +27 -0
  74. package/dist/shared/thinkingLevels.js +31 -0
  75. package/dist/shared/thinkingLevels.js.map +1 -0
  76. package/dist/shared/workspaceDeletion.js +1 -1
  77. package/dist/shared/workspaceDeletion.js.map +1 -1
  78. package/docs/plugins.md +17 -10
  79. package/package.json +24 -13
  80. package/dist/client/assets/index-DATsMV4H.js +0 -2203
  81. package/dist/server/sessions/managementPermissionSystem.js +0 -94
  82. package/dist/server/sessions/managementPermissionSystem.js.map +0 -1
@@ -1,9 +1,5 @@
1
- import { spawn } from "node:child_process";
2
- import { constants } from "node:fs";
3
- import { access as fsAccess, mkdir as fsMkdir, readFile, readdir as fsReaddir, realpath as fsRealpath, stat as fsStat, writeFile } from "node:fs/promises";
4
- import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
5
- import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
6
- import { Type } from "typebox";
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
7
3
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
8
4
  import { BUILTIN_COMMANDS } from "./builtinCommands.js";
9
5
  import { SessionCommandService } from "./sessionCommandService.js";
@@ -11,43 +7,37 @@ import { SessionArchiveStore } from "./sessionArchiveStore.js";
11
7
  import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
12
8
  import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
13
9
  import { computeEditPreview } from "./editPreview.js";
14
- import { managementToolAllowed } from "../managementEmbed.js";
15
- import { bubblewrapUnavailableReason, createBubblewrapPythonInvocation, createManagedPythonFallbackPrelude, createManagedSandboxEnvironment, DEFAULT_BUBBLEWRAP_PATHS } from "./managementSandbox.js";
16
- import { PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR, managementAgentToolNames, withRuntimeCreationEnvironment, writeManagementPermissionSystemPolicy } from "./managementPermissionSystem.js";
10
+ import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
11
+ import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
12
+ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
13
+ import { cwdPathsEqual } from "../workingDirectory.js";
14
+ import { createSpawnSessionToolDefinition } from "./spawnSessionTool.js";
15
+ const noopLogger = { info() { } };
17
16
  function noop() {
18
17
  // Intentionally empty default unsubscribe callback.
19
18
  }
19
+ function spawnTargetError(decision) {
20
+ if (decision.reason === "not-registered")
21
+ return new Error("派生会话不在已注册项目中");
22
+ return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
23
+ }
20
24
  function authLossWarningKey(sessionId, provider, modelId) {
21
25
  return `${sessionId}:${provider}/${modelId}`;
22
26
  }
23
- const SUPPORTED_PROMPT_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
24
- const MAX_PROMPT_IMAGES = 4;
27
+ function sessionIdFromLookup(ref) {
28
+ return typeof ref === "string" ? ref : ref.id;
29
+ }
30
+ function isPiSessionRef(ref) {
31
+ return typeof ref !== "string";
32
+ }
33
+ function lookupMatchesActiveSession(ref, active) {
34
+ return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
35
+ }
25
36
  function requirePromptText(value) {
26
37
  if (typeof value !== "string")
27
- throw new Error("Prompt text is required");
38
+ throw new Error("提示文本为必填项");
28
39
  return value;
29
40
  }
30
- function requirePromptImages(value) {
31
- if (value === undefined)
32
- return [];
33
- if (!Array.isArray(value))
34
- throw new Error("Prompt images must be an array");
35
- if (value.length > MAX_PROMPT_IMAGES)
36
- throw new Error(`Prompt images are limited to ${String(MAX_PROMPT_IMAGES)}`);
37
- return value.map((image) => {
38
- if (!isRecord(image))
39
- throw new Error("Prompt image must be an object");
40
- if (image["type"] !== "image")
41
- throw new Error('Prompt image type must be "image"');
42
- const data = image["data"];
43
- const mimeType = image["mimeType"];
44
- if (typeof data !== "string" || data === "")
45
- throw new Error("Prompt image data is required");
46
- if (typeof mimeType !== "string" || !SUPPORTED_PROMPT_IMAGE_MIME_TYPES.has(mimeType))
47
- throw new Error(`Unsupported prompt image MIME type: ${String(mimeType)}`);
48
- return { type: "image", data, mimeType };
49
- });
50
- }
51
41
  function parsePromptStreamingBehavior(value) {
52
42
  if (value === undefined)
53
43
  return undefined;
@@ -60,49 +50,22 @@ function defaultCreateAgentRuntime(createRuntime, options) {
60
50
  throw new Error("Default runtime creation requires an SDK SessionManager");
61
51
  return createAgentSessionRuntime(createRuntime, { ...options, sessionManager: options.sessionManager });
62
52
  }
63
- function createDefaultRuntimeFactory(authStorage, modelRegistry) {
53
+ function createDefaultRuntimeFactory(authStorage, modelRegistry, spawn) {
64
54
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
65
- return withRuntimeCreationEnvironment({}, async () => {
66
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
67
- const customTools = [createPiWebEditToolDefinition(cwd)];
68
- const options = sessionStartEvent === undefined
69
- ? { services, sessionManager, customTools }
70
- : { services, sessionManager, sessionStartEvent, customTools };
71
- const result = await createAgentSessionFromServices(options);
72
- return { ...result, services, diagnostics: services.diagnostics };
73
- });
74
- };
75
- }
76
- function createManagementRuntimeFactory(authStorage, modelRegistry, managementContext) {
77
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
78
- const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
79
- return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
80
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
81
- const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
82
- const options = sessionStartEvent === undefined
83
- ? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
84
- : { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
85
- // @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
86
- const result = await createAgentSessionFromServices(options);
87
- return { ...result, services, diagnostics: services.diagnostics };
88
- });
55
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
56
+ const customTools = [
57
+ createPiWebEditToolDefinition(cwd),
58
+ ...(spawn === undefined ? [] : [createSpawnSessionToolDefinition(cwd, { spawn })]),
59
+ ];
60
+ const options = sessionStartEvent === undefined
61
+ ? { services, sessionManager, customTools }
62
+ : { services, sessionManager, sessionStartEvent, customTools };
63
+ const result = await createAgentSessionFromServices(options);
64
+ return { ...result, services, diagnostics: services.diagnostics };
89
65
  };
90
66
  }
91
- export { managementAgentToolNames };
92
- export function createManagementSandboxToolDefinitions(cwd, context) {
93
- const operations = createManagedFileOperations(cwd);
94
- return [
95
- createReadToolDefinition(cwd, { operations: operations.read }),
96
- createWriteToolDefinition(cwd, { operations: operations.write }),
97
- createPiWebEditToolDefinition(cwd, operations.edit),
98
- createLsToolDefinition(cwd, { operations: operations.ls }),
99
- createGrepToolDefinition(cwd, { operations: operations.grep }),
100
- createFindToolDefinition(cwd, { operations: operations.find }),
101
- createManagedPythonToolDefinition(cwd, context),
102
- ];
103
- }
104
- function createPiWebEditToolDefinition(cwd, operations) {
105
- const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
67
+ function createPiWebEditToolDefinition(cwd) {
68
+ const editTool = createEditToolDefinition(cwd);
106
69
  return defineTool({
107
70
  name: editTool.name,
108
71
  label: editTool.label,
@@ -122,211 +85,6 @@ function createPiWebEditToolDefinition(cwd, operations) {
122
85
  },
123
86
  });
124
87
  }
125
- const pythonSchema = Type.Object({
126
- code: Type.String({ description: "Python code to run in the managed project workspace" }),
127
- timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
128
- });
129
- function createManagedPythonToolDefinition(cwd, context) {
130
- return defineTool({
131
- name: "python",
132
- label: "python",
133
- description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
134
- promptSnippet: "Run Python code in the current project",
135
- promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
136
- parameters: pythonSchema,
137
- async execute(_toolCallId, params, signal) {
138
- const configuredPython = context.sandbox?.pythonExecutable?.trim();
139
- const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
140
- const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
141
- const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
142
- const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
143
- const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
144
- return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
145
- },
146
- });
147
- }
148
- async function runManagedPython(options) {
149
- const root = await fsRealpath(options.cwd);
150
- const invocation = createBubblewrapPythonInvocation({
151
- bubblewrapExecutable: options.bubblewrapExecutable,
152
- pythonExecutable: options.pythonExecutable,
153
- workspaceRoot: root,
154
- env: options.env,
155
- readOnlyPaths: await readableBubblewrapPaths(),
156
- });
157
- const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
158
- const unavailable = bubblewrapUnavailableReason(result.output);
159
- if (unavailable !== undefined) {
160
- return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
161
- }
162
- return pythonToolResult(result.code, result.output);
163
- }
164
- async function runManagedPythonFallback(options) {
165
- const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
166
- const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
167
- return pythonToolResult(result.code, result.output);
168
- }
169
- async function runPythonProcess(options) {
170
- return new Promise((resolvePromise, reject) => {
171
- if (options.signal?.aborted === true) {
172
- reject(new Error("Operation aborted"));
173
- return;
174
- }
175
- const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
176
- let stdout = "";
177
- let stderr = "";
178
- const timer = setTimeout(() => {
179
- child.kill();
180
- reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
181
- }, options.timeoutMs);
182
- const onAbort = () => {
183
- child.kill();
184
- reject(new Error("Operation aborted"));
185
- };
186
- options.signal?.addEventListener("abort", onAbort, { once: true });
187
- child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
188
- child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
189
- child.on("error", (error) => {
190
- clearTimeout(timer);
191
- options.signal?.removeEventListener("abort", onAbort);
192
- if (isNodeErrorWithCode(error, "ENOENT"))
193
- reject(new Error("Python sandbox is unavailable"));
194
- else
195
- reject(error);
196
- });
197
- child.on("close", (codeValue) => {
198
- clearTimeout(timer);
199
- options.signal?.removeEventListener("abort", onAbort);
200
- const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
201
- resolvePromise({ code: codeValue, output });
202
- });
203
- child.stdin.end(options.code);
204
- });
205
- }
206
- function pythonToolResult(codeValue, output) {
207
- const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
208
- return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
209
- }
210
- async function readableBubblewrapPaths() {
211
- const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
212
- try {
213
- await fsAccess(path, constants.R_OK);
214
- return path;
215
- }
216
- catch {
217
- return undefined;
218
- }
219
- }));
220
- return paths.filter(isDefined);
221
- }
222
- function createManagedFileOperations(cwd) {
223
- const read = {
224
- readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
225
- access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
226
- };
227
- const write = {
228
- writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
229
- mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
230
- };
231
- const edit = {
232
- readFile: read.readFile,
233
- writeFile: write.writeFile,
234
- access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
235
- };
236
- const ls = {
237
- exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
238
- stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
239
- readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
240
- };
241
- const grep = {
242
- isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
243
- readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
244
- };
245
- const find = {
246
- exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
247
- glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
248
- };
249
- return { read, write, edit, ls, grep, find };
250
- }
251
- async function pathExistsInside(rootPath, targetPath) {
252
- try {
253
- await assertExistingInside(rootPath, targetPath);
254
- return true;
255
- }
256
- catch {
257
- return false;
258
- }
259
- }
260
- async function managedGlob(rootPath, pattern, searchPath, limit) {
261
- const root = await fsRealpath(rootPath);
262
- const start = await assertExistingInside(root, searchPath);
263
- const regex = globPatternToRegExp(pattern);
264
- const results = [];
265
- async function walk(dir) {
266
- if (results.length >= limit)
267
- return;
268
- const entries = await fsReaddir(dir, { withFileTypes: true });
269
- for (const entry of entries) {
270
- if (results.length >= limit)
271
- return;
272
- if (entry.name === ".git" || entry.name === "node_modules")
273
- continue;
274
- const fullPath = resolve(dir, entry.name);
275
- const safePath = await assertExistingInside(root, fullPath);
276
- const rel = relative(start, safePath).split(sep).join("/");
277
- if (entry.isDirectory()) {
278
- await walk(safePath);
279
- }
280
- else if (regex.test(rel)) {
281
- results.push(safePath);
282
- }
283
- }
284
- }
285
- await walk(start);
286
- return results;
287
- }
288
- function globPatternToRegExp(pattern) {
289
- const escaped = pattern
290
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
291
- .replace(/\*\*/g, "\0")
292
- .replace(/\*/g, "[^/]*")
293
- .replace(/\?/g, "[^/]")
294
- .replace(/\0/g, ".*");
295
- return new RegExp(`^${escaped}$`);
296
- }
297
- async function assertExistingInside(rootPath, targetPath) {
298
- const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
299
- ensurePathInside(root, target);
300
- return target;
301
- }
302
- async function assertWritableInside(rootPath, targetPath) {
303
- try {
304
- return await assertExistingInside(rootPath, targetPath);
305
- }
306
- catch {
307
- const parent = await assertExistingInside(rootPath, dirname(targetPath));
308
- const target = resolve(parent, basename(targetPath));
309
- ensurePathInside(await fsRealpath(rootPath), target);
310
- return target;
311
- }
312
- }
313
- async function assertPotentialInside(rootPath, targetPath) {
314
- const root = await fsRealpath(rootPath);
315
- const target = resolve(targetPath);
316
- ensurePathInside(root, target);
317
- return target;
318
- }
319
- function ensurePathInside(root, target) {
320
- const rel = relative(root, target);
321
- if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
322
- return;
323
- throw new Error("Path is outside the managed project sandbox");
324
- }
325
- function truncateToolOutput(value, limit = 64_000) {
326
- if (value.length <= limit)
327
- return value;
328
- return `${value.slice(0, limit)}\n[output truncated]`;
329
- }
330
88
  export class PiSessionService {
331
89
  constructor(events, deps = {}) {
332
90
  this.events = events;
@@ -335,16 +93,17 @@ export class PiSessionService {
335
93
  this.compactionPromptQueues = new Map();
336
94
  this.compactionDrainTimers = new Map();
337
95
  this.authLossWarnings = new Set();
338
- this.managementContexts = new Map();
339
96
  this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
340
97
  this.agentDir = deps.agentDir ?? getAgentDir();
341
- this.sessionManager = deps.sessionManager ?? SessionManager;
98
+ this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
342
99
  this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
343
- this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry);
100
+ this.spawnTargets = deps.spawnTargets;
101
+ this.logger = deps.logger ?? noopLogger;
102
+ this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input));
344
103
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
345
104
  this.workspaceActivity = deps.workspaceActivity;
346
105
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
347
- this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text), events, {
106
+ this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text, undefined, undefined, { echoUserMessage: false }), events, {
348
107
  onCompactionStart: (session) => {
349
108
  this.publishActivity(session, "compacting", "active");
350
109
  this.publishStatus(session);
@@ -366,7 +125,6 @@ export class PiSessionService {
366
125
  this.activities.clear();
367
126
  this.compactionPromptQueues.clear();
368
127
  this.authLossWarnings.clear();
369
- this.managementContexts.clear();
370
128
  await Promise.all(activeSessions.map(async (active) => {
371
129
  active.unsubscribe();
372
130
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
@@ -389,12 +147,11 @@ export class PiSessionService {
389
147
  .filter(isDefined);
390
148
  return [...unarchivedSessions, ...archivedSessions];
391
149
  }
392
- async start(cwd, managementContext) {
393
- const active = await this.create(this.sessionManager.create(cwd), cwd, managementContext);
150
+ async start(cwd, _managementContext) {
151
+ void _managementContext;
152
+ const active = await this.create(this.sessionManager.create(cwd), cwd);
394
153
  const { session } = active.runtime;
395
- if (managementContext !== undefined)
396
- this.managementContexts.set(session.sessionId, managementContext);
397
- return {
154
+ const created = {
398
155
  id: session.sessionId,
399
156
  path: session.sessionFile ?? "",
400
157
  cwd,
@@ -403,25 +160,45 @@ export class PiSessionService {
403
160
  messageCount: session.messages.length,
404
161
  firstMessage: "",
405
162
  };
406
- }
407
- async messages(sessionId, page) {
408
- const session = await this.getOrOpen(sessionId);
163
+ // Broadcast so other clients (and the spawning agent's UI) can add the new
164
+ // session to their list without a manual reload.
165
+ this.events.publishGlobal({ type: "session.created", session: created });
166
+ return created;
167
+ }
168
+ /**
169
+ * Start a new session on behalf of a LLM and deliver an initial prompt to it.
170
+ * The target cwd is constrained to a workspace of the same registered project
171
+ * as the spawning session so the new session is visible in the web UI.
172
+ */
173
+ async spawnSession(input) {
174
+ if (this.spawnTargets === undefined)
175
+ throw new Error("派生会话已禁用");
176
+ const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd);
177
+ if (!decision.allowed)
178
+ throw spawnTargetError(decision);
179
+ const created = await this.start(decision.cwd);
180
+ await this.prompt(created.id, input.prompt);
181
+ this.logger.info({ spawningCwd: input.spawningCwd, sessionId: created.id, cwd: decision.cwd, promptLength: input.prompt.length }, "spawn_session started a new session");
182
+ return { sessionId: created.id, cwd: decision.cwd };
183
+ }
184
+ async messages(ref, page) {
185
+ const session = await this.getOrOpen(ref);
409
186
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
410
187
  }
411
- async status(sessionId) {
412
- return this.statusFromSession(await this.getOrOpen(sessionId));
188
+ async status(ref) {
189
+ return this.statusFromSession(await this.getOrOpen(ref));
413
190
  }
414
- async availableModels(sessionId) {
415
- const session = await this.getOrOpen(sessionId);
191
+ async availableModels(ref) {
192
+ const session = await this.getOrOpen(ref);
416
193
  session.modelRegistry.refresh();
417
194
  const models = session.scopedModels.length > 0
418
195
  ? session.scopedModels.map((scoped) => scoped.model)
419
196
  : session.modelRegistry.getAvailable();
420
197
  return models.map(modelToClientModel);
421
198
  }
422
- async setModel(sessionId, provider, modelId) {
423
- await this.assertWritable(sessionId);
424
- const session = await this.getOrOpen(sessionId);
199
+ async setModel(ref, provider, modelId) {
200
+ await this.assertWritable(ref);
201
+ const session = await this.getOrOpen(ref);
425
202
  session.modelRegistry.refresh();
426
203
  const candidates = session.scopedModels.length > 0
427
204
  ? session.scopedModels.map((scoped) => scoped.model)
@@ -429,46 +206,52 @@ export class PiSessionService {
429
206
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
430
207
  ?? session.modelRegistry.find(provider, modelId);
431
208
  if (model === undefined)
432
- throw new Error(`Model not found: ${provider}/${modelId}`);
209
+ throw new Error(`未找到模型:${provider}/${modelId}`);
433
210
  await session.setModel(model);
434
211
  this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
435
212
  this.publishStatus(session);
436
213
  return this.statusFromSession(session);
437
214
  }
438
- async cycleModel(sessionId, direction) {
439
- await this.assertWritable(sessionId);
440
- const session = await this.getOrOpen(sessionId);
215
+ async cycleModel(ref, direction) {
216
+ await this.assertWritable(ref);
217
+ const session = await this.getOrOpen(ref);
441
218
  const result = await session.cycleModel(direction);
442
219
  if (result === undefined)
443
- throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
220
+ throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
444
221
  this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
445
222
  this.publishStatus(session);
446
223
  return this.statusFromSession(session);
447
224
  }
448
- async availableThinkingLevels(sessionId) {
449
- const session = await this.getOrOpen(sessionId);
225
+ async availableThinkingLevels(ref) {
226
+ const session = await this.getOrOpen(ref);
450
227
  return session.getAvailableThinkingLevels();
451
228
  }
452
- async setThinkingLevel(sessionId, level) {
453
- await this.assertWritable(sessionId);
454
- const session = await this.getOrOpen(sessionId);
455
- session.setThinkingLevel(level);
229
+ async setThinkingLevel(ref, level) {
230
+ await this.assertWritable(ref);
231
+ const session = await this.getOrOpen(ref);
232
+ // pi owns the valid set; validate against the session's live levels rather
233
+ // than a hardcoded union so this stays correct if pi changes the set.
234
+ const available = session.getAvailableThinkingLevels();
235
+ const match = available.find((candidate) => candidate === level);
236
+ if (match === undefined)
237
+ throw new Error(`无效的思考级别:${level}`);
238
+ session.setThinkingLevel(match);
456
239
  this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
457
240
  this.publishStatus(session);
458
241
  return this.statusFromSession(session);
459
242
  }
460
- async cycleThinkingLevel(sessionId) {
461
- await this.assertWritable(sessionId);
462
- const session = await this.getOrOpen(sessionId);
243
+ async cycleThinkingLevel(ref) {
244
+ await this.assertWritable(ref);
245
+ const session = await this.getOrOpen(ref);
463
246
  const level = session.cycleThinkingLevel();
464
247
  if (level === undefined)
465
- throw new Error("Current model does not support thinking");
248
+ throw new Error("当前模型不支持思考");
466
249
  this.publishActivity(session, `thinking: ${level}`, "idle");
467
250
  this.publishStatus(session);
468
251
  return this.statusFromSession(session);
469
252
  }
470
- async commands(sessionId) {
471
- const session = await this.getOrOpen(sessionId);
253
+ async commands(ref) {
254
+ const session = await this.getOrOpen(ref);
472
255
  const commands = [...BUILTIN_COMMANDS];
473
256
  for (const command of session.extensionRunner.getRegisteredCommands()) {
474
257
  commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
@@ -481,33 +264,40 @@ export class PiSessionService {
481
264
  }
482
265
  return commands.sort((a, b) => a.name.localeCompare(b.name));
483
266
  }
484
- async prompt(sessionId, text, streamingBehavior, imagesValue, managementContext) {
267
+ async prompt(ref, text, streamingBehavior, attachments, options) {
268
+ const _managementContext = options?.managementContext;
269
+ void _managementContext;
485
270
  const promptText = requirePromptText(text);
271
+ // Command-forwarded prompts (e.g. /skill:*) are expanded by the agent, which
272
+ // streams the canonical message back. The client doesn't render the raw
273
+ // command text, so the server must not echo it either, or it would show up
274
+ // as a transient line that vanishes on reload.
275
+ const echoUserMessage = options?.echoUserMessage !== false;
486
276
  const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
487
- const images = requirePromptImages(imagesValue);
488
- await this.assertWritable(sessionId);
489
- const session = await this.getOrOpen(sessionId, managementContext);
490
- if (images.length > 0 && !modelSupportsImageInput(session.model))
491
- throw new Error("当前模型不支持图片输入");
277
+ const parsedAttachments = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
278
+ const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
279
+ await this.assertWritable(ref);
280
+ const session = await this.getOrOpen(ref);
492
281
  this.maybeGenerateSessionName(session, promptText);
493
282
  const isQueued = session.isStreaming || session.isCompacting;
494
283
  const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
495
- if (isQueued && this.hasQueuedMessageText(session, promptText)) {
284
+ if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
496
285
  this.publishActivity(session, "duplicate queued message ignored", "active");
497
286
  this.publishStatus(session);
498
287
  return;
499
288
  }
500
289
  if (session.isCompacting) {
501
- this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images);
290
+ this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images, echoUserMessage);
502
291
  return;
503
292
  }
504
- void this.submitPrompt(session, promptText, behavior, images);
293
+ void this.submitPrompt(session, promptText, behavior, images, echoUserMessage);
505
294
  }
506
- submitPrompt(session, text, behavior, images = []) {
295
+ submitPrompt(session, text, behavior, images = [], echoUserMessage = true) {
507
296
  this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
508
- if (behavior === undefined)
509
- this.events.publish(session.sessionId, { type: "message.append", message: userPromptMessage(text, images) });
510
- const promptPromise = session.prompt(text, promptOptions(behavior, images)).catch((error) => {
297
+ if (behavior === undefined && echoUserMessage)
298
+ this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
299
+ const promptOptions = buildPromptOptions(behavior, images);
300
+ const promptPromise = session.prompt(text, promptOptions).catch((error) => {
511
301
  const message = error instanceof Error ? error.message : String(error);
512
302
  this.publishActivity(session, "error", "error", message);
513
303
  this.events.publish(session.sessionId, { type: "session.error", message });
@@ -515,27 +305,32 @@ export class PiSessionService {
515
305
  void promptPromise;
516
306
  return promptPromise;
517
307
  }
518
- enqueuePromptDuringCompaction(session, text, kind, images = []) {
308
+ enqueuePromptDuringCompaction(session, text, kind, images = [], echoUserMessage = true) {
519
309
  const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
520
- queue.push({ kind, text, ...(images.length === 0 ? {} : { images: [...images] }) });
310
+ queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
521
311
  this.compactionPromptQueues.set(session.sessionId, queue);
522
312
  this.publishActivity(session, "message queued during compaction", "active");
523
313
  this.publishStatus(session);
524
314
  }
525
- async shell(sessionId, text, managementContext) {
526
- await this.assertWritable(sessionId);
527
- this.assertManagedSessionAccess(sessionId, managementContext);
528
- const effectiveContext = managementContext ?? this.managementContexts.get(sessionId);
529
- if (effectiveContext !== undefined && !managementToolAllowed(effectiveContext, "shell"))
530
- throw new Error("Shell commands are disabled in management embed mode");
531
- const active = await this.getActive(sessionId);
315
+ async saveAttachments(ref, attachments, folder) {
316
+ const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
317
+ if (parsed.length === 0)
318
+ return [];
319
+ await this.assertWritable(ref);
320
+ const active = await this.getActive(ref);
321
+ return saveAttachmentsToWorkspace(active.runtime.cwd, parsed, folder === undefined ? {} : { folder });
322
+ }
323
+ async shell(ref, text, _managementContext) {
324
+ void _managementContext;
325
+ await this.assertWritable(ref);
326
+ const active = await this.getActive(ref);
532
327
  const { session } = active.runtime;
533
328
  const isExcluded = text.startsWith("!!");
534
329
  const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
535
330
  if (!command)
536
- throw new Error("Usage: !<shell command>");
331
+ throw new Error("用法:!<shell command>");
537
332
  if (session.isBashRunning)
538
- throw new Error("A bash command is already running");
333
+ throw new Error("已有 bash 命令正在运行");
539
334
  this.publishActivity(session, "running bash", "active", command);
540
335
  this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
541
336
  void session.executeBash(command, (chunk) => {
@@ -561,46 +356,33 @@ export class PiSessionService {
561
356
  this.publishStatus(session);
562
357
  });
563
358
  }
564
- async runCommand(sessionId, text, managementContext) {
565
- await this.assertWritable(sessionId);
566
- this.assertManagedSessionAccess(sessionId, managementContext);
567
- return this.commandService.run(sessionId, text);
359
+ async runCommand(ref, text, _managementContext) {
360
+ void _managementContext;
361
+ await this.assertWritable(ref);
362
+ const active = await this.getActive(ref);
363
+ return this.commandService.run(active.runtime.session.sessionId, text);
568
364
  }
569
- async respondToCommand(sessionId, requestId, value) {
570
- await this.assertWritable(sessionId);
571
- return this.commandService.respond(sessionId, requestId, value);
365
+ async respondToCommand(ref, requestId, value) {
366
+ await this.assertWritable(ref);
367
+ const active = await this.getActive(ref);
368
+ return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
572
369
  }
573
- async archive(sessionId) {
574
- const session = await this.getOrOpen(sessionId);
370
+ async archive(ref) {
371
+ const session = await this.getOrOpen(ref);
575
372
  if (this.hasActiveWork(session))
576
- throw new Error("Stop current session activity before archiving");
373
+ throw new Error("归档前请先停止当前会话活动");
577
374
  const archiveInput = await this.archiveInputForSession(session);
578
375
  await this.closeActive(session.sessionId);
579
- this.managementContexts.delete(session.sessionId);
580
376
  await this.archiveStore.archive(archiveInput);
581
377
  }
582
- assertManagedSessionAccess(sessionId, context) {
583
- const existing = this.managementContexts.get(sessionId);
584
- if (context === undefined || existing === undefined)
585
- return;
586
- if (existing.user.rootUserId !== context.user.rootUserId)
587
- throw new Error("Session is outside the managed embed authorization scope");
588
- }
589
- rememberManagedSessionAccess(sessionId, context) {
590
- if (context === undefined)
591
- return;
592
- this.assertManagedSessionAccess(sessionId, context);
593
- if (!this.managementContexts.has(sessionId))
594
- this.managementContexts.set(sessionId, context);
595
- }
596
- async archiveTree(sessionId) {
597
- const session = await this.getOrOpen(sessionId);
378
+ async archiveTree(ref) {
379
+ const session = await this.getOrOpen(ref);
598
380
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
599
381
  const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
600
382
  const plan = planSessionArchiveTree(root, catalog);
601
383
  const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
602
384
  if (busy !== undefined)
603
- throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
385
+ throw new Error(`归档 ${sessionDisplayName(busy)} 前请先停止当前会话活动`);
604
386
  const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
605
387
  for (const input of archiveInputs)
606
388
  await this.closeActive(input.sessionId);
@@ -613,29 +395,56 @@ export class PiSessionService {
613
395
  skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
614
396
  };
615
397
  }
616
- async restore(sessionId) {
617
- await this.closeActive(sessionId);
618
- await this.archiveStore.restore(sessionId);
398
+ async restore(ref) {
399
+ const archived = await this.getArchived(ref);
400
+ if (archived === undefined)
401
+ throw new Error("未找到会话");
402
+ await this.closeActive(archived.sessionId);
403
+ await this.archiveStore.restore(archived.sessionId);
404
+ }
405
+ async deleteArchived(ref) {
406
+ const record = await this.getArchived(ref);
407
+ if (record === undefined)
408
+ throw new Error("未找到已归档会话");
409
+ if (this.archiveStore.deleteArchived === undefined)
410
+ throw new Error("归档存储不支持删除");
411
+ await this.closeActive(record.sessionId);
412
+ if (record.archivePath === undefined)
413
+ await this.ensureArchivedRecordMoved(record);
414
+ await this.archiveStore.deleteArchived(record.sessionId);
415
+ }
416
+ async reload(ref) {
417
+ await this.assertWritable(ref);
418
+ const session = await this.getOrOpen(ref);
419
+ if (this.hasActiveWork(session))
420
+ throw new Error("重新加载前请先停止当前会话活动");
421
+ await this.closeActive(session.sessionId);
422
+ const reopened = await this.getActive(ref);
423
+ this.publishStatus(reopened.runtime.session);
619
424
  }
620
- async detachParent(sessionId) {
621
- const session = await this.getOrOpen(sessionId);
425
+ async detachParent(ref) {
426
+ const session = await this.getOrOpen(ref);
622
427
  const sessionFile = session.sessionFile;
623
428
  if (sessionFile === undefined || sessionFile === "")
624
- throw new Error("Session is not persisted");
429
+ throw new Error("会话尚未持久化");
625
430
  await clearParentSession(sessionFile);
626
431
  }
627
- async abort(sessionId) {
628
- const active = this.active.get(sessionId);
629
- if (!active)
432
+ async abort(ref) {
433
+ const active = this.activeForLookup(ref);
434
+ if (active === undefined)
630
435
  return;
436
+ const sessionId = active.runtime.session.sessionId;
631
437
  this.clearCompactionPromptQueue(sessionId);
632
438
  clearSessionQueue(active.runtime.session);
633
439
  await active.runtime.session.abort();
634
440
  this.publishActivity(active.runtime.session, "stopped", "idle");
635
441
  this.publishStatus(active.runtime.session);
636
442
  }
637
- stop(sessionId) {
638
- void this.closeActive(sessionId).catch(() => {
443
+ stop(ref) {
444
+ const active = this.activeForLookup(ref);
445
+ if (active === undefined)
446
+ return;
447
+ void this.closeActive(active.runtime.session.sessionId).catch(() => {
639
448
  // Best-effort shutdown; callers that need errors await closeActive directly.
640
449
  });
641
450
  }
@@ -658,11 +467,17 @@ export class PiSessionService {
658
467
  return record;
659
468
  }
660
469
  }
470
+ async ensureArchivedRecordMoved(record) {
471
+ const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
472
+ if (session === undefined)
473
+ return record;
474
+ return this.archiveStore.archive(archiveInputFromListEntry(session));
475
+ }
661
476
  async archiveInputForSession(session) {
662
477
  const cwd = session.sessionManager.getCwd();
663
478
  const sessionFile = session.sessionFile;
664
479
  if (sessionFile === undefined || sessionFile === "")
665
- throw new Error("Session is not persisted");
480
+ throw new Error("会话尚未持久化");
666
481
  const listed = (await this.sessionManager.list(cwd)).find((candidate) => candidate.id === session.sessionId);
667
482
  if (listed !== undefined)
668
483
  return archiveInputFromListEntry(listed);
@@ -723,7 +538,6 @@ export class PiSessionService {
723
538
  if (!active)
724
539
  return;
725
540
  this.active.delete(sessionId);
726
- this.managementContexts.delete(sessionId);
727
541
  this.activities.delete(sessionId);
728
542
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
729
543
  this.clearAuthLossWarningsForSession(sessionId);
@@ -737,67 +551,74 @@ export class PiSessionService {
737
551
  await active.runtime.dispose();
738
552
  }
739
553
  }
740
- async assertWritable(sessionId) {
741
- if (await this.archiveStore.isArchived(sessionId))
742
- throw new Error("Archived sessions are read-only. Restore the session to continue.");
554
+ async assertWritable(ref) {
555
+ if (await this.getArchived(ref) !== undefined)
556
+ throw new Error("已归档会话为只读。请恢复会话后继续。");
743
557
  }
744
- async getOrOpen(sessionId, managementContext) {
745
- return (await this.getActive(sessionId, managementContext)).runtime.session;
558
+ async getOrOpen(ref) {
559
+ return (await this.getActive(ref)).runtime.session;
746
560
  }
747
- async getActive(sessionId, managementContext) {
748
- const active = this.active.get(sessionId);
749
- if (active) {
750
- const activeSessionId = active.runtime.session.sessionId;
751
- const existingContext = this.managementContexts.get(activeSessionId);
752
- if (managementContext !== undefined && existingContext === undefined) {
753
- const sessionFile = active.runtime.session.sessionFile;
754
- if (sessionFile === undefined || sessionFile === "")
755
- throw new Error("Managed embed session must be persisted before it can be resumed safely");
756
- const activeCwd = active.runtime.session.sessionManager.getCwd();
757
- await this.closeActive(activeSessionId);
758
- return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
759
- }
760
- this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
561
+ async getActive(ref) {
562
+ const active = this.activeForLookup(ref);
563
+ if (active !== undefined)
761
564
  return active;
762
- }
763
- const archived = await this.archiveStore.get(sessionId);
565
+ const archived = await this.getArchived(ref);
764
566
  if (archived?.archivePath !== undefined)
765
- return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext);
766
- const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
567
+ return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
568
+ const match = isPiSessionRef(ref)
569
+ ? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
570
+ : (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
767
571
  if (!match)
768
- throw new Error("Session not found");
769
- return this.create(this.sessionManager.open(match.path), match.cwd, managementContext);
770
- }
771
- async create(sessionManager, cwd, managementContext) {
772
- const createRuntime = managementContext === undefined
773
- ? this.createRuntime
774
- : createManagementRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, managementContext);
775
- const runtimeOptions = managementContext === undefined
776
- ? { cwd, agentDir: this.agentDir, sessionManager }
777
- : { cwd, agentDir: this.agentDir, sessionManager, managementContext };
778
- const runtime = await this.createAgentRuntime(createRuntime, runtimeOptions);
572
+ throw new Error("未找到会话");
573
+ return this.create(this.sessionManager.open(match.path), match.cwd);
574
+ }
575
+ async getArchived(ref) {
576
+ const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
577
+ if (archived === undefined)
578
+ return undefined;
579
+ if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
580
+ return undefined;
581
+ return archived;
582
+ }
583
+ activeForLookup(ref) {
584
+ const sessionId = sessionIdFromLookup(ref);
585
+ const exact = this.active.get(sessionId);
586
+ if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
587
+ return exact;
588
+ for (const [candidateId, active] of this.active.entries()) {
589
+ if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
590
+ return active;
591
+ }
592
+ return undefined;
593
+ }
594
+ async create(sessionManager, cwd) {
595
+ const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
596
+ await this.bindSessionExtensions(runtime.session);
779
597
  const active = { runtime, unsubscribe: noop };
780
598
  this.bindRuntime(active);
781
- runtime.setRebindSession(() => {
599
+ runtime.setRebindSession(async (session) => {
600
+ await this.bindSessionExtensions(session);
782
601
  this.bindRuntime(active);
783
- return Promise.resolve();
784
602
  });
785
603
  this.active.set(runtime.session.sessionId, active);
786
- this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
787
604
  this.publishStatus(runtime.session);
788
605
  return active;
789
606
  }
607
+ async bindSessionExtensions(session) {
608
+ await session.bindExtensions({
609
+ onError: (error) => {
610
+ const message = `${error.extensionPath}: ${error.error}`;
611
+ this.publishActivity(session, "extension error", "error", message);
612
+ this.events.publish(session.sessionId, { type: "session.error", message });
613
+ },
614
+ });
615
+ }
790
616
  bindRuntime(active) {
791
617
  active.unsubscribe();
792
618
  const { session } = active.runtime;
793
619
  for (const [sessionId, candidate] of this.active.entries()) {
794
620
  if (candidate === active) {
795
621
  this.active.delete(sessionId);
796
- const context = this.managementContexts.get(sessionId);
797
- if (context !== undefined && sessionId !== session.sessionId) {
798
- this.managementContexts.delete(sessionId);
799
- this.managementContexts.set(session.sessionId, context);
800
- }
801
622
  if (sessionId !== session.sessionId)
802
623
  this.clearCompactionPromptQueue(sessionId);
803
624
  }
@@ -838,14 +659,14 @@ export class PiSessionService {
838
659
  return;
839
660
  this.publishStatus(session);
840
661
  for (const prompt of queued)
841
- void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images ?? []);
662
+ void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images, prompt.echoUserMessage ?? true);
842
663
  return;
843
664
  }
844
665
  const prompt = this.shiftCompactionPrompt(sessionId);
845
666
  if (prompt === undefined)
846
667
  return;
847
668
  this.publishStatus(session);
848
- const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images ?? []);
669
+ const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images, prompt.echoUserMessage ?? true);
849
670
  void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
850
671
  }
851
672
  takeCompactionPromptQueue(sessionId) {
@@ -1080,25 +901,14 @@ function modelToClientModel(model) {
1080
901
  return {};
1081
902
  const name = getString(model, "name");
1082
903
  const reasoning = getProperty(model, "reasoning");
1083
- const input = modelInput(model);
1084
904
  return {
1085
905
  provider: model.provider,
1086
906
  id: model.id,
1087
907
  ...(name === undefined ? {} : { name }),
1088
908
  contextWindow: model.contextWindow,
1089
909
  ...(reasoning === undefined ? {} : { reasoning }),
1090
- ...(input === undefined ? {} : { input }),
1091
910
  };
1092
911
  }
1093
- function modelInput(model) {
1094
- const input = getProperty(model, "input");
1095
- if (!Array.isArray(input))
1096
- return undefined;
1097
- return input.filter((item) => item === "text" || item === "image");
1098
- }
1099
- function modelSupportsImageInput(model) {
1100
- return modelInput(model)?.includes("image") === true;
1101
- }
1102
912
  function clientSessionFromListEntry(session) {
1103
913
  return {
1104
914
  id: session.id,
@@ -1128,7 +938,7 @@ function archiveInputFromListEntry(session) {
1128
938
  function archiveInputFromActiveSession(session) {
1129
939
  const sessionFile = session.sessionFile;
1130
940
  if (sessionFile === undefined || sessionFile === "")
1131
- throw new Error("Session is not persisted");
941
+ throw new Error("会话尚未持久化");
1132
942
  const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
1133
943
  return {
1134
944
  sessionId: session.sessionId,
@@ -1169,7 +979,7 @@ function archiveCandidateFromArchivedRecord(record, fallback) {
1169
979
  function archiveCandidateFromActiveSession(session, archived) {
1170
980
  const sessionFile = session.sessionFile;
1171
981
  if (sessionFile === undefined || sessionFile === "")
1172
- throw new Error("Session is not persisted");
982
+ throw new Error("会话尚未持久化");
1173
983
  const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
1174
984
  return {
1175
985
  id: session.sessionId,
@@ -1232,9 +1042,6 @@ function archivedTimestamp(record) {
1232
1042
  function isDefined(value) {
1233
1043
  return value !== undefined;
1234
1044
  }
1235
- function isNodeErrorWithCode(error, code) {
1236
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
1237
- }
1238
1045
  async function clearParentSession(sessionFile) {
1239
1046
  const content = await readFile(sessionFile, "utf8");
1240
1047
  const newlineIndex = content.indexOf("\n");
@@ -1255,22 +1062,33 @@ function queuedMessagesFromSession(session, extraQueuedMessages = []) {
1255
1062
  return [
1256
1063
  ...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
1257
1064
  ...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
1258
- ...extraQueuedMessages.map((message) => queuedPromptSummary(message)),
1065
+ ...extraQueuedMessages,
1259
1066
  ];
1260
1067
  }
1261
- function queuedPromptSummary(message) {
1262
- const imageCount = message.images?.length ?? 0;
1263
- return { kind: message.kind, text: message.text, ...(imageCount === 0 ? {} : { imageCount }) };
1068
+ function userTextMessage(text) {
1069
+ return { role: "user", content: text };
1264
1070
  }
1265
- function promptOptions(behavior, images) {
1266
- if (behavior === undefined && images.length === 0)
1267
- return undefined;
1268
- return { ...(behavior === undefined ? {} : { streamingBehavior: behavior }), ...(images.length === 0 ? {} : { images: [...images] }) };
1269
- }
1270
- function userPromptMessage(text, images) {
1071
+ /**
1072
+ * Build the optimistic user message echoed to clients. When images are present
1073
+ * we mirror pi's content-array shape (`[{type:"text"}, {type:"image"}, ...]`) so
1074
+ * the local echo matches what pi persists in the session branch.
1075
+ */
1076
+ function userMessage(text, images) {
1271
1077
  if (images.length === 0)
1272
- return { role: "user", content: text };
1273
- return { role: "user", content: [{ type: "text", text }, ...images] };
1078
+ return userTextMessage(text);
1079
+ const content = [];
1080
+ if (text !== "")
1081
+ content.push({ type: "text", text });
1082
+ content.push(...images);
1083
+ return { role: "user", content };
1084
+ }
1085
+ function buildPromptOptions(behavior, images) {
1086
+ const options = {};
1087
+ if (behavior !== undefined)
1088
+ options.streamingBehavior = behavior;
1089
+ if (images.length > 0)
1090
+ options.images = images;
1091
+ return Object.keys(options).length > 0 ? options : undefined;
1274
1092
  }
1275
1093
  function stringValue(value) {
1276
1094
  return typeof value === "string" ? value : "";