@chainingintention/pi-web-cn 1.202606.10 → 1.202606.12

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 (71) hide show
  1. package/README.md +12 -5
  2. package/dist/cli.js +14 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/{CodeViewer-YNyWxbqH.js → CodeViewer-BKljKDuK.js} +1 -1
  5. package/dist/client/assets/{TerminalPanel-B9UStXy9.js → TerminalPanel-DeDTQWls.js} +1 -1
  6. package/dist/client/assets/index-BuVYTYo8.js +2315 -0
  7. package/dist/client/index.html +1 -1
  8. package/dist/config.js +28 -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/server/app.js +14 -6
  14. package/dist/server/app.js.map +1 -1
  15. package/dist/server/index.js +2 -2
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/server/machines/machinePluginProxyRoutes.js +8 -0
  18. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  19. package/dist/server/machines/machineRoutes.js +6 -0
  20. package/dist/server/machines/machineRoutes.js.map +1 -1
  21. package/dist/server/machines/machineService.js +97 -5
  22. package/dist/server/machines/machineService.js.map +1 -1
  23. package/dist/server/piWebPluginService.js +24 -4
  24. package/dist/server/piWebPluginService.js.map +1 -1
  25. package/dist/server/piWebStatus.js +149 -45
  26. package/dist/server/piWebStatus.js.map +1 -1
  27. package/dist/server/piWebStatusCache.js +32 -0
  28. package/dist/server/piWebStatusCache.js.map +1 -0
  29. package/dist/server/sessiond/sessionProxyRoutes.js +15 -1
  30. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  31. package/dist/server/sessiond.js +20 -8
  32. package/dist/server/sessiond.js.map +1 -1
  33. package/dist/server/sessions/attachmentService.js +61 -0
  34. package/dist/server/sessions/attachmentService.js.map +1 -0
  35. package/dist/server/sessions/oauthLoginFlowService.js +7 -0
  36. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  37. package/dist/server/sessions/piSessionManagerGateway.js +96 -0
  38. package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
  39. package/dist/server/sessions/piSessionService.js +211 -383
  40. package/dist/server/sessions/piSessionService.js.map +1 -1
  41. package/dist/server/sessions/sessionArchiveStore.js +16 -2
  42. package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
  43. package/dist/server/sessions/sessionRoutes.js +156 -43
  44. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  45. package/dist/server/terminals/terminalRoutes.js +10 -4
  46. package/dist/server/terminals/terminalRoutes.js.map +1 -1
  47. package/dist/server/workingDirectory.js +44 -0
  48. package/dist/server/workingDirectory.js.map +1 -0
  49. package/dist/server/workspaces/fileSuggestions.js +96 -16
  50. package/dist/server/workspaces/fileSuggestions.js.map +1 -1
  51. package/dist/shared/apiTypes.d.ts +81 -4
  52. package/dist/shared/apiTypes.js +5 -1
  53. package/dist/shared/apiTypes.js.map +1 -1
  54. package/dist/shared/capabilities.js +27 -0
  55. package/dist/shared/capabilities.js.map +1 -0
  56. package/dist/shared/federatedRoutes.js +3 -0
  57. package/dist/shared/federatedRoutes.js.map +1 -1
  58. package/dist/shared/piWebStatusParsing.js +28 -0
  59. package/dist/shared/piWebStatusParsing.js.map +1 -1
  60. package/dist/shared/promptAttachments.js +73 -0
  61. package/dist/shared/promptAttachments.js.map +1 -0
  62. package/dist/shared/thinkingLevels.d.ts +27 -0
  63. package/dist/shared/thinkingLevels.js +31 -0
  64. package/dist/shared/thinkingLevels.js.map +1 -0
  65. package/dist/shared/workspaceDeletion.js +1 -1
  66. package/dist/shared/workspaceDeletion.js.map +1 -1
  67. package/docs/plugins.md +14 -7
  68. package/package.json +23 -13
  69. package/dist/client/assets/index-BH7nkPuT.js +0 -2172
  70. package/dist/server/sessions/managementPermissionSystem.js +0 -94
  71. 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,15 +7,25 @@ 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";
17
14
  function noop() {
18
15
  // Intentionally empty default unsubscribe callback.
19
16
  }
20
17
  function authLossWarningKey(sessionId, provider, modelId) {
21
18
  return `${sessionId}:${provider}/${modelId}`;
22
19
  }
20
+ function sessionIdFromLookup(ref) {
21
+ return typeof ref === "string" ? ref : ref.id;
22
+ }
23
+ function isPiSessionRef(ref) {
24
+ return typeof ref !== "string";
25
+ }
26
+ function lookupMatchesActiveSession(ref, active) {
27
+ return !isPiSessionRef(ref) || cwdPathsEqual(active.runtime.cwd, ref.cwd);
28
+ }
23
29
  function requirePromptText(value) {
24
30
  if (typeof value !== "string")
25
31
  throw new Error("Prompt text is required");
@@ -39,47 +45,17 @@ function defaultCreateAgentRuntime(createRuntime, options) {
39
45
  }
40
46
  function createDefaultRuntimeFactory(authStorage, modelRegistry) {
41
47
  return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
42
- return withRuntimeCreationEnvironment({}, async () => {
43
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
44
- const customTools = [createPiWebEditToolDefinition(cwd)];
45
- const options = sessionStartEvent === undefined
46
- ? { services, sessionManager, customTools }
47
- : { services, sessionManager, sessionStartEvent, customTools };
48
- const result = await createAgentSessionFromServices(options);
49
- return { ...result, services, diagnostics: services.diagnostics };
50
- });
51
- };
52
- }
53
- function createManagementRuntimeFactory(authStorage, modelRegistry, managementContext) {
54
- return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
55
- const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
56
- return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
57
- const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
58
- const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
59
- const options = sessionStartEvent === undefined
60
- ? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
61
- : { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
62
- // @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
63
- const result = await createAgentSessionFromServices(options);
64
- return { ...result, services, diagnostics: services.diagnostics };
65
- });
48
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
49
+ const customTools = [createPiWebEditToolDefinition(cwd)];
50
+ const options = sessionStartEvent === undefined
51
+ ? { services, sessionManager, customTools }
52
+ : { services, sessionManager, sessionStartEvent, customTools };
53
+ const result = await createAgentSessionFromServices(options);
54
+ return { ...result, services, diagnostics: services.diagnostics };
66
55
  };
67
56
  }
68
- export { managementAgentToolNames };
69
- export function createManagementSandboxToolDefinitions(cwd, context) {
70
- const operations = createManagedFileOperations(cwd);
71
- return [
72
- createReadToolDefinition(cwd, { operations: operations.read }),
73
- createWriteToolDefinition(cwd, { operations: operations.write }),
74
- createPiWebEditToolDefinition(cwd, operations.edit),
75
- createLsToolDefinition(cwd, { operations: operations.ls }),
76
- createGrepToolDefinition(cwd, { operations: operations.grep }),
77
- createFindToolDefinition(cwd, { operations: operations.find }),
78
- createManagedPythonToolDefinition(cwd, context),
79
- ];
80
- }
81
- function createPiWebEditToolDefinition(cwd, operations) {
82
- const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
57
+ function createPiWebEditToolDefinition(cwd) {
58
+ const editTool = createEditToolDefinition(cwd);
83
59
  return defineTool({
84
60
  name: editTool.name,
85
61
  label: editTool.label,
@@ -99,211 +75,6 @@ function createPiWebEditToolDefinition(cwd, operations) {
99
75
  },
100
76
  });
101
77
  }
102
- const pythonSchema = Type.Object({
103
- code: Type.String({ description: "Python code to run in the managed project workspace" }),
104
- timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
105
- });
106
- function createManagedPythonToolDefinition(cwd, context) {
107
- return defineTool({
108
- name: "python",
109
- label: "python",
110
- description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
111
- promptSnippet: "Run Python code in the current project",
112
- promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
113
- parameters: pythonSchema,
114
- async execute(_toolCallId, params, signal) {
115
- const configuredPython = context.sandbox?.pythonExecutable?.trim();
116
- const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
117
- const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
118
- const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
119
- const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
120
- const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
121
- return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
122
- },
123
- });
124
- }
125
- async function runManagedPython(options) {
126
- const root = await fsRealpath(options.cwd);
127
- const invocation = createBubblewrapPythonInvocation({
128
- bubblewrapExecutable: options.bubblewrapExecutable,
129
- pythonExecutable: options.pythonExecutable,
130
- workspaceRoot: root,
131
- env: options.env,
132
- readOnlyPaths: await readableBubblewrapPaths(),
133
- });
134
- const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
135
- const unavailable = bubblewrapUnavailableReason(result.output);
136
- if (unavailable !== undefined) {
137
- return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
138
- }
139
- return pythonToolResult(result.code, result.output);
140
- }
141
- async function runManagedPythonFallback(options) {
142
- const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
143
- const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
144
- return pythonToolResult(result.code, result.output);
145
- }
146
- async function runPythonProcess(options) {
147
- return new Promise((resolvePromise, reject) => {
148
- if (options.signal?.aborted === true) {
149
- reject(new Error("Operation aborted"));
150
- return;
151
- }
152
- const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
153
- let stdout = "";
154
- let stderr = "";
155
- const timer = setTimeout(() => {
156
- child.kill();
157
- reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
158
- }, options.timeoutMs);
159
- const onAbort = () => {
160
- child.kill();
161
- reject(new Error("Operation aborted"));
162
- };
163
- options.signal?.addEventListener("abort", onAbort, { once: true });
164
- child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
165
- child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
166
- child.on("error", (error) => {
167
- clearTimeout(timer);
168
- options.signal?.removeEventListener("abort", onAbort);
169
- if (isNodeErrorWithCode(error, "ENOENT"))
170
- reject(new Error("Python sandbox is unavailable"));
171
- else
172
- reject(error);
173
- });
174
- child.on("close", (codeValue) => {
175
- clearTimeout(timer);
176
- options.signal?.removeEventListener("abort", onAbort);
177
- const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
178
- resolvePromise({ code: codeValue, output });
179
- });
180
- child.stdin.end(options.code);
181
- });
182
- }
183
- function pythonToolResult(codeValue, output) {
184
- const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
185
- return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
186
- }
187
- async function readableBubblewrapPaths() {
188
- const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
189
- try {
190
- await fsAccess(path, constants.R_OK);
191
- return path;
192
- }
193
- catch {
194
- return undefined;
195
- }
196
- }));
197
- return paths.filter(isDefined);
198
- }
199
- function createManagedFileOperations(cwd) {
200
- const read = {
201
- readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
202
- access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
203
- };
204
- const write = {
205
- writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
206
- mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
207
- };
208
- const edit = {
209
- readFile: read.readFile,
210
- writeFile: write.writeFile,
211
- access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
212
- };
213
- const ls = {
214
- exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
215
- stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
216
- readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
217
- };
218
- const grep = {
219
- isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
220
- readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
221
- };
222
- const find = {
223
- exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
224
- glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
225
- };
226
- return { read, write, edit, ls, grep, find };
227
- }
228
- async function pathExistsInside(rootPath, targetPath) {
229
- try {
230
- await assertExistingInside(rootPath, targetPath);
231
- return true;
232
- }
233
- catch {
234
- return false;
235
- }
236
- }
237
- async function managedGlob(rootPath, pattern, searchPath, limit) {
238
- const root = await fsRealpath(rootPath);
239
- const start = await assertExistingInside(root, searchPath);
240
- const regex = globPatternToRegExp(pattern);
241
- const results = [];
242
- async function walk(dir) {
243
- if (results.length >= limit)
244
- return;
245
- const entries = await fsReaddir(dir, { withFileTypes: true });
246
- for (const entry of entries) {
247
- if (results.length >= limit)
248
- return;
249
- if (entry.name === ".git" || entry.name === "node_modules")
250
- continue;
251
- const fullPath = resolve(dir, entry.name);
252
- const safePath = await assertExistingInside(root, fullPath);
253
- const rel = relative(start, safePath).split(sep).join("/");
254
- if (entry.isDirectory()) {
255
- await walk(safePath);
256
- }
257
- else if (regex.test(rel)) {
258
- results.push(safePath);
259
- }
260
- }
261
- }
262
- await walk(start);
263
- return results;
264
- }
265
- function globPatternToRegExp(pattern) {
266
- const escaped = pattern
267
- .replace(/[.+^${}()|[\]\\]/g, "\\$&")
268
- .replace(/\*\*/g, "\0")
269
- .replace(/\*/g, "[^/]*")
270
- .replace(/\?/g, "[^/]")
271
- .replace(/\0/g, ".*");
272
- return new RegExp(`^${escaped}$`);
273
- }
274
- async function assertExistingInside(rootPath, targetPath) {
275
- const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
276
- ensurePathInside(root, target);
277
- return target;
278
- }
279
- async function assertWritableInside(rootPath, targetPath) {
280
- try {
281
- return await assertExistingInside(rootPath, targetPath);
282
- }
283
- catch {
284
- const parent = await assertExistingInside(rootPath, dirname(targetPath));
285
- const target = resolve(parent, basename(targetPath));
286
- ensurePathInside(await fsRealpath(rootPath), target);
287
- return target;
288
- }
289
- }
290
- async function assertPotentialInside(rootPath, targetPath) {
291
- const root = await fsRealpath(rootPath);
292
- const target = resolve(targetPath);
293
- ensurePathInside(root, target);
294
- return target;
295
- }
296
- function ensurePathInside(root, target) {
297
- const rel = relative(root, target);
298
- if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
299
- return;
300
- throw new Error("Path is outside the managed project sandbox");
301
- }
302
- function truncateToolOutput(value, limit = 64_000) {
303
- if (value.length <= limit)
304
- return value;
305
- return `${value.slice(0, limit)}\n[output truncated]`;
306
- }
307
78
  export class PiSessionService {
308
79
  constructor(events, deps = {}) {
309
80
  this.events = events;
@@ -312,10 +83,9 @@ export class PiSessionService {
312
83
  this.compactionPromptQueues = new Map();
313
84
  this.compactionDrainTimers = new Map();
314
85
  this.authLossWarnings = new Set();
315
- this.managementContexts = new Map();
316
86
  this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
317
87
  this.agentDir = deps.agentDir ?? getAgentDir();
318
- this.sessionManager = deps.sessionManager ?? SessionManager;
88
+ this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
319
89
  this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
320
90
  this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry);
321
91
  this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
@@ -343,7 +113,6 @@ export class PiSessionService {
343
113
  this.activities.clear();
344
114
  this.compactionPromptQueues.clear();
345
115
  this.authLossWarnings.clear();
346
- this.managementContexts.clear();
347
116
  await Promise.all(activeSessions.map(async (active) => {
348
117
  active.unsubscribe();
349
118
  this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
@@ -366,11 +135,10 @@ export class PiSessionService {
366
135
  .filter(isDefined);
367
136
  return [...unarchivedSessions, ...archivedSessions];
368
137
  }
369
- async start(cwd, managementContext) {
370
- const active = await this.create(this.sessionManager.create(cwd), cwd, managementContext);
138
+ async start(cwd, _managementContext) {
139
+ void _managementContext;
140
+ const active = await this.create(this.sessionManager.create(cwd), cwd);
371
141
  const { session } = active.runtime;
372
- if (managementContext !== undefined)
373
- this.managementContexts.set(session.sessionId, managementContext);
374
142
  return {
375
143
  id: session.sessionId,
376
144
  path: session.sessionFile ?? "",
@@ -381,24 +149,24 @@ export class PiSessionService {
381
149
  firstMessage: "",
382
150
  };
383
151
  }
384
- async messages(sessionId, page) {
385
- const session = await this.getOrOpen(sessionId);
152
+ async messages(ref, page) {
153
+ const session = await this.getOrOpen(ref);
386
154
  return pageMessagesAtSafeBoundary(historyMessages(session), page);
387
155
  }
388
- async status(sessionId) {
389
- return this.statusFromSession(await this.getOrOpen(sessionId));
156
+ async status(ref) {
157
+ return this.statusFromSession(await this.getOrOpen(ref));
390
158
  }
391
- async availableModels(sessionId) {
392
- const session = await this.getOrOpen(sessionId);
159
+ async availableModels(ref) {
160
+ const session = await this.getOrOpen(ref);
393
161
  session.modelRegistry.refresh();
394
162
  const models = session.scopedModels.length > 0
395
163
  ? session.scopedModels.map((scoped) => scoped.model)
396
164
  : session.modelRegistry.getAvailable();
397
165
  return models.map(modelToClientModel);
398
166
  }
399
- async setModel(sessionId, provider, modelId) {
400
- await this.assertWritable(sessionId);
401
- const session = await this.getOrOpen(sessionId);
167
+ async setModel(ref, provider, modelId) {
168
+ await this.assertWritable(ref);
169
+ const session = await this.getOrOpen(ref);
402
170
  session.modelRegistry.refresh();
403
171
  const candidates = session.scopedModels.length > 0
404
172
  ? session.scopedModels.map((scoped) => scoped.model)
@@ -412,9 +180,9 @@ export class PiSessionService {
412
180
  this.publishStatus(session);
413
181
  return this.statusFromSession(session);
414
182
  }
415
- async cycleModel(sessionId, direction) {
416
- await this.assertWritable(sessionId);
417
- const session = await this.getOrOpen(sessionId);
183
+ async cycleModel(ref, direction) {
184
+ await this.assertWritable(ref);
185
+ const session = await this.getOrOpen(ref);
418
186
  const result = await session.cycleModel(direction);
419
187
  if (result === undefined)
420
188
  throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
@@ -422,21 +190,27 @@ export class PiSessionService {
422
190
  this.publishStatus(session);
423
191
  return this.statusFromSession(session);
424
192
  }
425
- async availableThinkingLevels(sessionId) {
426
- const session = await this.getOrOpen(sessionId);
193
+ async availableThinkingLevels(ref) {
194
+ const session = await this.getOrOpen(ref);
427
195
  return session.getAvailableThinkingLevels();
428
196
  }
429
- async setThinkingLevel(sessionId, level) {
430
- await this.assertWritable(sessionId);
431
- const session = await this.getOrOpen(sessionId);
432
- session.setThinkingLevel(level);
197
+ async setThinkingLevel(ref, level) {
198
+ await this.assertWritable(ref);
199
+ const session = await this.getOrOpen(ref);
200
+ // pi owns the valid set; validate against the session's live levels rather
201
+ // than a hardcoded union so this stays correct if pi changes the set.
202
+ const available = session.getAvailableThinkingLevels();
203
+ const match = available.find((candidate) => candidate === level);
204
+ if (match === undefined)
205
+ throw new Error(`Invalid thinking level: ${level}`);
206
+ session.setThinkingLevel(match);
433
207
  this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
434
208
  this.publishStatus(session);
435
209
  return this.statusFromSession(session);
436
210
  }
437
- async cycleThinkingLevel(sessionId) {
438
- await this.assertWritable(sessionId);
439
- const session = await this.getOrOpen(sessionId);
211
+ async cycleThinkingLevel(ref) {
212
+ await this.assertWritable(ref);
213
+ const session = await this.getOrOpen(ref);
440
214
  const level = session.cycleThinkingLevel();
441
215
  if (level === undefined)
442
216
  throw new Error("Current model does not support thinking");
@@ -444,8 +218,8 @@ export class PiSessionService {
444
218
  this.publishStatus(session);
445
219
  return this.statusFromSession(session);
446
220
  }
447
- async commands(sessionId) {
448
- const session = await this.getOrOpen(sessionId);
221
+ async commands(ref) {
222
+ const session = await this.getOrOpen(ref);
449
223
  const commands = [...BUILTIN_COMMANDS];
450
224
  for (const command of session.extensionRunner.getRegisteredCommands()) {
451
225
  commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
@@ -458,30 +232,34 @@ export class PiSessionService {
458
232
  }
459
233
  return commands.sort((a, b) => a.name.localeCompare(b.name));
460
234
  }
461
- async prompt(sessionId, text, streamingBehavior, managementContext) {
235
+ async prompt(ref, text, streamingBehavior, attachments, _managementContext) {
236
+ void _managementContext;
462
237
  const promptText = requirePromptText(text);
463
238
  const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
464
- await this.assertWritable(sessionId);
465
- const session = await this.getOrOpen(sessionId, managementContext);
239
+ const parsedAttachments = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
240
+ const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
241
+ await this.assertWritable(ref);
242
+ const session = await this.getOrOpen(ref);
466
243
  this.maybeGenerateSessionName(session, promptText);
467
244
  const isQueued = session.isStreaming || session.isCompacting;
468
245
  const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
469
- if (isQueued && this.hasQueuedMessageText(session, promptText)) {
246
+ if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
470
247
  this.publishActivity(session, "duplicate queued message ignored", "active");
471
248
  this.publishStatus(session);
472
249
  return;
473
250
  }
474
251
  if (session.isCompacting) {
475
- this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp");
252
+ this.enqueuePromptDuringCompaction(session, promptText, behavior ?? "followUp", images);
476
253
  return;
477
254
  }
478
- void this.submitPrompt(session, promptText, behavior);
255
+ void this.submitPrompt(session, promptText, behavior, images);
479
256
  }
480
- submitPrompt(session, text, behavior) {
257
+ submitPrompt(session, text, behavior, images = []) {
481
258
  this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
482
259
  if (behavior === undefined)
483
- this.events.publish(session.sessionId, { type: "message.append", message: userTextMessage(text) });
484
- const promptPromise = session.prompt(text, behavior === undefined ? undefined : { streamingBehavior: behavior }).catch((error) => {
260
+ this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) });
261
+ const promptOptions = buildPromptOptions(behavior, images);
262
+ const promptPromise = session.prompt(text, promptOptions).catch((error) => {
485
263
  const message = error instanceof Error ? error.message : String(error);
486
264
  this.publishActivity(session, "error", "error", message);
487
265
  this.events.publish(session.sessionId, { type: "session.error", message });
@@ -489,20 +267,25 @@ export class PiSessionService {
489
267
  void promptPromise;
490
268
  return promptPromise;
491
269
  }
492
- enqueuePromptDuringCompaction(session, text, kind) {
270
+ enqueuePromptDuringCompaction(session, text, kind, images = []) {
493
271
  const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
494
- queue.push({ kind, text });
272
+ queue.push({ kind, text, ...(images.length > 0 ? { images } : {}) });
495
273
  this.compactionPromptQueues.set(session.sessionId, queue);
496
274
  this.publishActivity(session, "message queued during compaction", "active");
497
275
  this.publishStatus(session);
498
276
  }
499
- async shell(sessionId, text, managementContext) {
500
- await this.assertWritable(sessionId);
501
- this.assertManagedSessionAccess(sessionId, managementContext);
502
- const effectiveContext = managementContext ?? this.managementContexts.get(sessionId);
503
- if (effectiveContext !== undefined && !managementToolAllowed(effectiveContext, "shell"))
504
- throw new Error("Shell commands are disabled in management embed mode");
505
- const active = await this.getActive(sessionId);
277
+ async saveAttachments(ref, attachments, folder) {
278
+ const parsed = parsePromptAttachments(attachments, { enforceInlineSizeLimit: false });
279
+ if (parsed.length === 0)
280
+ return [];
281
+ await this.assertWritable(ref);
282
+ const active = await this.getActive(ref);
283
+ return saveAttachmentsToWorkspace(active.runtime.cwd, parsed, folder === undefined ? {} : { folder });
284
+ }
285
+ async shell(ref, text, _managementContext) {
286
+ void _managementContext;
287
+ await this.assertWritable(ref);
288
+ const active = await this.getActive(ref);
506
289
  const { session } = active.runtime;
507
290
  const isExcluded = text.startsWith("!!");
508
291
  const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
@@ -535,40 +318,27 @@ export class PiSessionService {
535
318
  this.publishStatus(session);
536
319
  });
537
320
  }
538
- async runCommand(sessionId, text, managementContext) {
539
- await this.assertWritable(sessionId);
540
- this.assertManagedSessionAccess(sessionId, managementContext);
541
- return this.commandService.run(sessionId, text);
321
+ async runCommand(ref, text, _managementContext) {
322
+ void _managementContext;
323
+ await this.assertWritable(ref);
324
+ const active = await this.getActive(ref);
325
+ return this.commandService.run(active.runtime.session.sessionId, text);
542
326
  }
543
- async respondToCommand(sessionId, requestId, value) {
544
- await this.assertWritable(sessionId);
545
- return this.commandService.respond(sessionId, requestId, value);
327
+ async respondToCommand(ref, requestId, value) {
328
+ await this.assertWritable(ref);
329
+ const active = await this.getActive(ref);
330
+ return this.commandService.respond(active.runtime.session.sessionId, requestId, value);
546
331
  }
547
- async archive(sessionId) {
548
- const session = await this.getOrOpen(sessionId);
332
+ async archive(ref) {
333
+ const session = await this.getOrOpen(ref);
549
334
  if (this.hasActiveWork(session))
550
335
  throw new Error("Stop current session activity before archiving");
551
336
  const archiveInput = await this.archiveInputForSession(session);
552
337
  await this.closeActive(session.sessionId);
553
- this.managementContexts.delete(session.sessionId);
554
338
  await this.archiveStore.archive(archiveInput);
555
339
  }
556
- assertManagedSessionAccess(sessionId, context) {
557
- const existing = this.managementContexts.get(sessionId);
558
- if (context === undefined || existing === undefined)
559
- return;
560
- if (existing.user.rootUserId !== context.user.rootUserId)
561
- throw new Error("Session is outside the managed embed authorization scope");
562
- }
563
- rememberManagedSessionAccess(sessionId, context) {
564
- if (context === undefined)
565
- return;
566
- this.assertManagedSessionAccess(sessionId, context);
567
- if (!this.managementContexts.has(sessionId))
568
- this.managementContexts.set(sessionId, context);
569
- }
570
- async archiveTree(sessionId) {
571
- const session = await this.getOrOpen(sessionId);
340
+ async archiveTree(ref) {
341
+ const session = await this.getOrOpen(ref);
572
342
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
573
343
  const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
574
344
  const plan = planSessionArchiveTree(root, catalog);
@@ -587,29 +357,56 @@ export class PiSessionService {
587
357
  skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
588
358
  };
589
359
  }
590
- async restore(sessionId) {
591
- await this.closeActive(sessionId);
592
- await this.archiveStore.restore(sessionId);
360
+ async restore(ref) {
361
+ const archived = await this.getArchived(ref);
362
+ if (archived === undefined)
363
+ throw new Error("Session not found");
364
+ await this.closeActive(archived.sessionId);
365
+ await this.archiveStore.restore(archived.sessionId);
366
+ }
367
+ async deleteArchived(ref) {
368
+ const record = await this.getArchived(ref);
369
+ if (record === undefined)
370
+ throw new Error("Archived session not found");
371
+ if (this.archiveStore.deleteArchived === undefined)
372
+ throw new Error("Archive store does not support deletion");
373
+ await this.closeActive(record.sessionId);
374
+ if (record.archivePath === undefined)
375
+ await this.ensureArchivedRecordMoved(record);
376
+ await this.archiveStore.deleteArchived(record.sessionId);
377
+ }
378
+ async reload(ref) {
379
+ await this.assertWritable(ref);
380
+ const session = await this.getOrOpen(ref);
381
+ if (this.hasActiveWork(session))
382
+ throw new Error("Stop current session activity before reloading");
383
+ await this.closeActive(session.sessionId);
384
+ const reopened = await this.getActive(ref);
385
+ this.publishStatus(reopened.runtime.session);
593
386
  }
594
- async detachParent(sessionId) {
595
- const session = await this.getOrOpen(sessionId);
387
+ async detachParent(ref) {
388
+ const session = await this.getOrOpen(ref);
596
389
  const sessionFile = session.sessionFile;
597
390
  if (sessionFile === undefined || sessionFile === "")
598
391
  throw new Error("Session is not persisted");
599
392
  await clearParentSession(sessionFile);
600
393
  }
601
- async abort(sessionId) {
602
- const active = this.active.get(sessionId);
603
- if (!active)
394
+ async abort(ref) {
395
+ const active = this.activeForLookup(ref);
396
+ if (active === undefined)
604
397
  return;
398
+ const sessionId = active.runtime.session.sessionId;
605
399
  this.clearCompactionPromptQueue(sessionId);
606
400
  clearSessionQueue(active.runtime.session);
607
401
  await active.runtime.session.abort();
608
402
  this.publishActivity(active.runtime.session, "stopped", "idle");
609
403
  this.publishStatus(active.runtime.session);
610
404
  }
611
- stop(sessionId) {
612
- void this.closeActive(sessionId).catch(() => {
405
+ stop(ref) {
406
+ const active = this.activeForLookup(ref);
407
+ if (active === undefined)
408
+ return;
409
+ void this.closeActive(active.runtime.session.sessionId).catch(() => {
613
410
  // Best-effort shutdown; callers that need errors await closeActive directly.
614
411
  });
615
412
  }
@@ -632,6 +429,12 @@ export class PiSessionService {
632
429
  return record;
633
430
  }
634
431
  }
432
+ async ensureArchivedRecordMoved(record) {
433
+ const session = (await this.sessionManager.list(record.cwd)).find((candidate) => candidate.id === record.sessionId);
434
+ if (session === undefined)
435
+ return record;
436
+ return this.archiveStore.archive(archiveInputFromListEntry(session));
437
+ }
635
438
  async archiveInputForSession(session) {
636
439
  const cwd = session.sessionManager.getCwd();
637
440
  const sessionFile = session.sessionFile;
@@ -697,7 +500,6 @@ export class PiSessionService {
697
500
  if (!active)
698
501
  return;
699
502
  this.active.delete(sessionId);
700
- this.managementContexts.delete(sessionId);
701
503
  this.activities.delete(sessionId);
702
504
  this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
703
505
  this.clearAuthLossWarningsForSession(sessionId);
@@ -711,67 +513,74 @@ export class PiSessionService {
711
513
  await active.runtime.dispose();
712
514
  }
713
515
  }
714
- async assertWritable(sessionId) {
715
- if (await this.archiveStore.isArchived(sessionId))
516
+ async assertWritable(ref) {
517
+ if (await this.getArchived(ref) !== undefined)
716
518
  throw new Error("Archived sessions are read-only. Restore the session to continue.");
717
519
  }
718
- async getOrOpen(sessionId, managementContext) {
719
- return (await this.getActive(sessionId, managementContext)).runtime.session;
520
+ async getOrOpen(ref) {
521
+ return (await this.getActive(ref)).runtime.session;
720
522
  }
721
- async getActive(sessionId, managementContext) {
722
- const active = this.active.get(sessionId);
723
- if (active) {
724
- const activeSessionId = active.runtime.session.sessionId;
725
- const existingContext = this.managementContexts.get(activeSessionId);
726
- if (managementContext !== undefined && existingContext === undefined) {
727
- const sessionFile = active.runtime.session.sessionFile;
728
- if (sessionFile === undefined || sessionFile === "")
729
- throw new Error("Managed embed session must be persisted before it can be resumed safely");
730
- const activeCwd = active.runtime.session.sessionManager.getCwd();
731
- await this.closeActive(activeSessionId);
732
- return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
733
- }
734
- this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
523
+ async getActive(ref) {
524
+ const active = this.activeForLookup(ref);
525
+ if (active !== undefined)
735
526
  return active;
736
- }
737
- const archived = await this.archiveStore.get(sessionId);
527
+ const archived = await this.getArchived(ref);
738
528
  if (archived?.archivePath !== undefined)
739
- return this.create(this.sessionManager.open(archived.archivePath), archived.cwd, managementContext);
740
- const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
529
+ return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
530
+ const match = isPiSessionRef(ref)
531
+ ? (await this.sessionManager.list(ref.cwd)).find((s) => s.id === ref.id || s.id.startsWith(ref.id))
532
+ : (await this.sessionManager.listAll?.() ?? []).find((s) => s.id === ref || s.id.startsWith(ref));
741
533
  if (!match)
742
534
  throw new Error("Session not found");
743
- return this.create(this.sessionManager.open(match.path), match.cwd, managementContext);
744
- }
745
- async create(sessionManager, cwd, managementContext) {
746
- const createRuntime = managementContext === undefined
747
- ? this.createRuntime
748
- : createManagementRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry, managementContext);
749
- const runtimeOptions = managementContext === undefined
750
- ? { cwd, agentDir: this.agentDir, sessionManager }
751
- : { cwd, agentDir: this.agentDir, sessionManager, managementContext };
752
- const runtime = await this.createAgentRuntime(createRuntime, runtimeOptions);
535
+ return this.create(this.sessionManager.open(match.path), match.cwd);
536
+ }
537
+ async getArchived(ref) {
538
+ const archived = await this.archiveStore.get(sessionIdFromLookup(ref));
539
+ if (archived === undefined)
540
+ return undefined;
541
+ if (isPiSessionRef(ref) && archived.cwd !== ref.cwd)
542
+ return undefined;
543
+ return archived;
544
+ }
545
+ activeForLookup(ref) {
546
+ const sessionId = sessionIdFromLookup(ref);
547
+ const exact = this.active.get(sessionId);
548
+ if (exact !== undefined && lookupMatchesActiveSession(ref, exact))
549
+ return exact;
550
+ for (const [candidateId, active] of this.active.entries()) {
551
+ if (candidateId.startsWith(sessionId) && lookupMatchesActiveSession(ref, active))
552
+ return active;
553
+ }
554
+ return undefined;
555
+ }
556
+ async create(sessionManager, cwd) {
557
+ const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
558
+ await this.bindSessionExtensions(runtime.session);
753
559
  const active = { runtime, unsubscribe: noop };
754
560
  this.bindRuntime(active);
755
- runtime.setRebindSession(() => {
561
+ runtime.setRebindSession(async (session) => {
562
+ await this.bindSessionExtensions(session);
756
563
  this.bindRuntime(active);
757
- return Promise.resolve();
758
564
  });
759
565
  this.active.set(runtime.session.sessionId, active);
760
- this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
761
566
  this.publishStatus(runtime.session);
762
567
  return active;
763
568
  }
569
+ async bindSessionExtensions(session) {
570
+ await session.bindExtensions({
571
+ onError: (error) => {
572
+ const message = `${error.extensionPath}: ${error.error}`;
573
+ this.publishActivity(session, "extension error", "error", message);
574
+ this.events.publish(session.sessionId, { type: "session.error", message });
575
+ },
576
+ });
577
+ }
764
578
  bindRuntime(active) {
765
579
  active.unsubscribe();
766
580
  const { session } = active.runtime;
767
581
  for (const [sessionId, candidate] of this.active.entries()) {
768
582
  if (candidate === active) {
769
583
  this.active.delete(sessionId);
770
- const context = this.managementContexts.get(sessionId);
771
- if (context !== undefined && sessionId !== session.sessionId) {
772
- this.managementContexts.delete(sessionId);
773
- this.managementContexts.set(session.sessionId, context);
774
- }
775
584
  if (sessionId !== session.sessionId)
776
585
  this.clearCompactionPromptQueue(sessionId);
777
586
  }
@@ -812,14 +621,14 @@ export class PiSessionService {
812
621
  return;
813
622
  this.publishStatus(session);
814
623
  for (const prompt of queued)
815
- void this.submitPrompt(session, prompt.text, prompt.kind);
624
+ void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images);
816
625
  return;
817
626
  }
818
627
  const prompt = this.shiftCompactionPrompt(sessionId);
819
628
  if (prompt === undefined)
820
629
  return;
821
630
  this.publishStatus(session);
822
- const submitted = this.submitPrompt(session, prompt.text, undefined);
631
+ const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images);
823
632
  void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
824
633
  }
825
634
  takeCompactionPromptQueue(sessionId) {
@@ -1195,9 +1004,6 @@ function archivedTimestamp(record) {
1195
1004
  function isDefined(value) {
1196
1005
  return value !== undefined;
1197
1006
  }
1198
- function isNodeErrorWithCode(error, code) {
1199
- return typeof error === "object" && error !== null && "code" in error && error.code === code;
1200
- }
1201
1007
  async function clearParentSession(sessionFile) {
1202
1008
  const content = await readFile(sessionFile, "utf8");
1203
1009
  const newlineIndex = content.indexOf("\n");
@@ -1224,6 +1030,28 @@ function queuedMessagesFromSession(session, extraQueuedMessages = []) {
1224
1030
  function userTextMessage(text) {
1225
1031
  return { role: "user", content: text };
1226
1032
  }
1033
+ /**
1034
+ * Build the optimistic user message echoed to clients. When images are present
1035
+ * we mirror pi's content-array shape (`[{type:"text"}, {type:"image"}, ...]`) so
1036
+ * the local echo matches what pi persists in the session branch.
1037
+ */
1038
+ function userMessage(text, images) {
1039
+ if (images.length === 0)
1040
+ return userTextMessage(text);
1041
+ const content = [];
1042
+ if (text !== "")
1043
+ content.push({ type: "text", text });
1044
+ content.push(...images);
1045
+ return { role: "user", content };
1046
+ }
1047
+ function buildPromptOptions(behavior, images) {
1048
+ const options = {};
1049
+ if (behavior !== undefined)
1050
+ options.streamingBehavior = behavior;
1051
+ if (images.length > 0)
1052
+ options.images = images;
1053
+ return Object.keys(options).length > 0 ? options : undefined;
1054
+ }
1227
1055
  function stringValue(value) {
1228
1056
  return typeof value === "string" ? value : "";
1229
1057
  }