@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.
- package/README.md +12 -5
- package/dist/cli.js +14 -2
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/{CodeViewer-YNyWxbqH.js → CodeViewer-BKljKDuK.js} +1 -1
- package/dist/client/assets/{TerminalPanel-B9UStXy9.js → TerminalPanel-DeDTQWls.js} +1 -1
- package/dist/client/assets/index-BuVYTYo8.js +2315 -0
- package/dist/client/index.html +1 -1
- package/dist/config.js +28 -3
- package/dist/config.js.map +1 -1
- package/dist/pi-web-plugins/updates/package.json +1 -1
- package/dist/pi-web-plugins/updates/pi-web-plugin.js +61 -71
- package/dist/pi-web-plugins/updates/updatesLogic.js +65 -0
- package/dist/server/app.js +14 -6
- package/dist/server/app.js.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/machines/machinePluginProxyRoutes.js +8 -0
- package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
- package/dist/server/machines/machineRoutes.js +6 -0
- package/dist/server/machines/machineRoutes.js.map +1 -1
- package/dist/server/machines/machineService.js +97 -5
- package/dist/server/machines/machineService.js.map +1 -1
- package/dist/server/piWebPluginService.js +24 -4
- package/dist/server/piWebPluginService.js.map +1 -1
- package/dist/server/piWebStatus.js +149 -45
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/piWebStatusCache.js +32 -0
- package/dist/server/piWebStatusCache.js.map +1 -0
- package/dist/server/sessiond/sessionProxyRoutes.js +15 -1
- package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
- package/dist/server/sessiond.js +20 -8
- package/dist/server/sessiond.js.map +1 -1
- package/dist/server/sessions/attachmentService.js +61 -0
- package/dist/server/sessions/attachmentService.js.map +1 -0
- package/dist/server/sessions/oauthLoginFlowService.js +7 -0
- package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
- package/dist/server/sessions/piSessionManagerGateway.js +96 -0
- package/dist/server/sessions/piSessionManagerGateway.js.map +1 -0
- package/dist/server/sessions/piSessionService.js +211 -383
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveStore.js +16 -2
- package/dist/server/sessions/sessionArchiveStore.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +156 -43
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +10 -4
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/workingDirectory.js +44 -0
- package/dist/server/workingDirectory.js.map +1 -0
- package/dist/server/workspaces/fileSuggestions.js +96 -16
- package/dist/server/workspaces/fileSuggestions.js.map +1 -1
- package/dist/shared/apiTypes.d.ts +81 -4
- package/dist/shared/apiTypes.js +5 -1
- package/dist/shared/apiTypes.js.map +1 -1
- package/dist/shared/capabilities.js +27 -0
- package/dist/shared/capabilities.js.map +1 -0
- package/dist/shared/federatedRoutes.js +3 -0
- package/dist/shared/federatedRoutes.js.map +1 -1
- package/dist/shared/piWebStatusParsing.js +28 -0
- package/dist/shared/piWebStatusParsing.js.map +1 -1
- package/dist/shared/promptAttachments.js +73 -0
- package/dist/shared/promptAttachments.js.map +1 -0
- package/dist/shared/thinkingLevels.d.ts +27 -0
- package/dist/shared/thinkingLevels.js +31 -0
- package/dist/shared/thinkingLevels.js.map +1 -0
- package/dist/shared/workspaceDeletion.js +1 -1
- package/dist/shared/workspaceDeletion.js.map +1 -1
- package/docs/plugins.md +14 -7
- package/package.json +23 -13
- package/dist/client/assets/index-BH7nkPuT.js +0 -2172
- package/dist/server/sessions/managementPermissionSystem.js +0 -94
- package/dist/server/sessions/managementPermissionSystem.js.map +0 -1
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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 ??
|
|
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,
|
|
370
|
-
|
|
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(
|
|
385
|
-
const session = await this.getOrOpen(
|
|
152
|
+
async messages(ref, page) {
|
|
153
|
+
const session = await this.getOrOpen(ref);
|
|
386
154
|
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
387
155
|
}
|
|
388
|
-
async status(
|
|
389
|
-
return this.statusFromSession(await this.getOrOpen(
|
|
156
|
+
async status(ref) {
|
|
157
|
+
return this.statusFromSession(await this.getOrOpen(ref));
|
|
390
158
|
}
|
|
391
|
-
async availableModels(
|
|
392
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
400
|
-
await this.assertWritable(
|
|
401
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
416
|
-
await this.assertWritable(
|
|
417
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
426
|
-
const session = await this.getOrOpen(
|
|
193
|
+
async availableThinkingLevels(ref) {
|
|
194
|
+
const session = await this.getOrOpen(ref);
|
|
427
195
|
return session.getAvailableThinkingLevels();
|
|
428
196
|
}
|
|
429
|
-
async setThinkingLevel(
|
|
430
|
-
await this.assertWritable(
|
|
431
|
-
const session = await this.getOrOpen(
|
|
432
|
-
session
|
|
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(
|
|
438
|
-
await this.assertWritable(
|
|
439
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
448
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
235
|
+
async prompt(ref, text, streamingBehavior, attachments, _managementContext) {
|
|
236
|
+
void _managementContext;
|
|
462
237
|
const promptText = requirePromptText(text);
|
|
463
238
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
464
|
-
|
|
465
|
-
const
|
|
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:
|
|
484
|
-
const
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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(
|
|
539
|
-
|
|
540
|
-
this.
|
|
541
|
-
|
|
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(
|
|
544
|
-
await this.assertWritable(
|
|
545
|
-
|
|
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(
|
|
548
|
-
const session = await this.getOrOpen(
|
|
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
|
-
|
|
557
|
-
const
|
|
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(
|
|
591
|
-
await this.
|
|
592
|
-
|
|
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(
|
|
595
|
-
const session = await this.getOrOpen(
|
|
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(
|
|
602
|
-
const active = this.
|
|
603
|
-
if (
|
|
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(
|
|
612
|
-
|
|
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(
|
|
715
|
-
if (await this.
|
|
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(
|
|
719
|
-
return (await this.getActive(
|
|
520
|
+
async getOrOpen(ref) {
|
|
521
|
+
return (await this.getActive(ref)).runtime.session;
|
|
720
522
|
}
|
|
721
|
-
async getActive(
|
|
722
|
-
const active = this.
|
|
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
|
|
740
|
-
const match = (
|
|
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
|
|
744
|
-
}
|
|
745
|
-
async
|
|
746
|
-
const
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
}
|