@chainingintention/pi-web-cn 1.202606.11 → 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-DzeGsHZ5.js → CodeViewer-BKljKDuK.js} +1 -1
- package/dist/client/assets/{TerminalPanel-BghODb4T.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 +208 -428
- 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 +157 -47
- 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 +70 -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-DATsMV4H.js +0 -2203
- 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,43 +7,30 @@ 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
|
}
|
|
23
|
-
|
|
24
|
-
|
|
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
|
+
}
|
|
25
29
|
function requirePromptText(value) {
|
|
26
30
|
if (typeof value !== "string")
|
|
27
31
|
throw new Error("Prompt text is required");
|
|
28
32
|
return value;
|
|
29
33
|
}
|
|
30
|
-
function requirePromptImages(value) {
|
|
31
|
-
if (value === undefined)
|
|
32
|
-
return [];
|
|
33
|
-
if (!Array.isArray(value))
|
|
34
|
-
throw new Error("Prompt images must be an array");
|
|
35
|
-
if (value.length > MAX_PROMPT_IMAGES)
|
|
36
|
-
throw new Error(`Prompt images are limited to ${String(MAX_PROMPT_IMAGES)}`);
|
|
37
|
-
return value.map((image) => {
|
|
38
|
-
if (!isRecord(image))
|
|
39
|
-
throw new Error("Prompt image must be an object");
|
|
40
|
-
if (image["type"] !== "image")
|
|
41
|
-
throw new Error('Prompt image type must be "image"');
|
|
42
|
-
const data = image["data"];
|
|
43
|
-
const mimeType = image["mimeType"];
|
|
44
|
-
if (typeof data !== "string" || data === "")
|
|
45
|
-
throw new Error("Prompt image data is required");
|
|
46
|
-
if (typeof mimeType !== "string" || !SUPPORTED_PROMPT_IMAGE_MIME_TYPES.has(mimeType))
|
|
47
|
-
throw new Error(`Unsupported prompt image MIME type: ${String(mimeType)}`);
|
|
48
|
-
return { type: "image", data, mimeType };
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
34
|
function parsePromptStreamingBehavior(value) {
|
|
52
35
|
if (value === undefined)
|
|
53
36
|
return undefined;
|
|
@@ -62,47 +45,17 @@ function defaultCreateAgentRuntime(createRuntime, options) {
|
|
|
62
45
|
}
|
|
63
46
|
function createDefaultRuntimeFactory(authStorage, modelRegistry) {
|
|
64
47
|
return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
73
|
-
});
|
|
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 };
|
|
74
55
|
};
|
|
75
56
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
const policyAgentDir = await writeManagementPermissionSystemPolicy(agentDir, cwd, managementContext);
|
|
79
|
-
return withRuntimeCreationEnvironment({ [PI_PERMISSION_SYSTEM_POLICY_AGENT_DIR]: policyAgentDir }, async () => {
|
|
80
|
-
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
|
|
81
|
-
const customTools = createManagementSandboxToolDefinitions(cwd, managementContext);
|
|
82
|
-
const options = sessionStartEvent === undefined
|
|
83
|
-
? { services, sessionManager, customTools, tools: managementAgentToolNames(managementContext) }
|
|
84
|
-
: { services, sessionManager, sessionStartEvent, customTools, tools: managementAgentToolNames(managementContext) };
|
|
85
|
-
// @ts-expect-error SDK customTools accepts concrete ToolDefinition instances at runtime, but the published type is invariant in render callbacks.
|
|
86
|
-
const result = await createAgentSessionFromServices(options);
|
|
87
|
-
return { ...result, services, diagnostics: services.diagnostics };
|
|
88
|
-
});
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
export { managementAgentToolNames };
|
|
92
|
-
export function createManagementSandboxToolDefinitions(cwd, context) {
|
|
93
|
-
const operations = createManagedFileOperations(cwd);
|
|
94
|
-
return [
|
|
95
|
-
createReadToolDefinition(cwd, { operations: operations.read }),
|
|
96
|
-
createWriteToolDefinition(cwd, { operations: operations.write }),
|
|
97
|
-
createPiWebEditToolDefinition(cwd, operations.edit),
|
|
98
|
-
createLsToolDefinition(cwd, { operations: operations.ls }),
|
|
99
|
-
createGrepToolDefinition(cwd, { operations: operations.grep }),
|
|
100
|
-
createFindToolDefinition(cwd, { operations: operations.find }),
|
|
101
|
-
createManagedPythonToolDefinition(cwd, context),
|
|
102
|
-
];
|
|
103
|
-
}
|
|
104
|
-
function createPiWebEditToolDefinition(cwd, operations) {
|
|
105
|
-
const editTool = createEditToolDefinition(cwd, operations === undefined ? undefined : { operations });
|
|
57
|
+
function createPiWebEditToolDefinition(cwd) {
|
|
58
|
+
const editTool = createEditToolDefinition(cwd);
|
|
106
59
|
return defineTool({
|
|
107
60
|
name: editTool.name,
|
|
108
61
|
label: editTool.label,
|
|
@@ -122,211 +75,6 @@ function createPiWebEditToolDefinition(cwd, operations) {
|
|
|
122
75
|
},
|
|
123
76
|
});
|
|
124
77
|
}
|
|
125
|
-
const pythonSchema = Type.Object({
|
|
126
|
-
code: Type.String({ description: "Python code to run in the managed project workspace" }),
|
|
127
|
-
timeoutMs: Type.Optional(Type.Number({ description: "Execution timeout in milliseconds" })),
|
|
128
|
-
});
|
|
129
|
-
function createManagedPythonToolDefinition(cwd, context) {
|
|
130
|
-
return defineTool({
|
|
131
|
-
name: "python",
|
|
132
|
-
label: "python",
|
|
133
|
-
description: "Run Python code inside the managed project workspace. Shell commands and paths outside the project are blocked.",
|
|
134
|
-
promptSnippet: "Run Python code in the current project",
|
|
135
|
-
promptGuidelines: ["Use python for scripts and calculations. Do not use it to run shell commands."],
|
|
136
|
-
parameters: pythonSchema,
|
|
137
|
-
async execute(_toolCallId, params, signal) {
|
|
138
|
-
const configuredPython = context.sandbox?.pythonExecutable?.trim();
|
|
139
|
-
const pythonExecutable = configuredPython !== undefined && configuredPython !== "" ? configuredPython : "python3";
|
|
140
|
-
const timeoutMs = Math.max(1_000, Math.min(params.timeoutMs ?? 30_000, 120_000));
|
|
141
|
-
const configuredBubblewrap = process.env["PI_WEB_BWRAP_EXECUTABLE"]?.trim();
|
|
142
|
-
const bubblewrapExecutable = configuredBubblewrap !== undefined && configuredBubblewrap !== "" ? configuredBubblewrap : "bwrap";
|
|
143
|
-
const env = createManagedSandboxEnvironment({ hostEnv: process.env, context });
|
|
144
|
-
return runManagedPython({ pythonExecutable, bubblewrapExecutable, cwd, code: params.code, timeoutMs, env, signal });
|
|
145
|
-
},
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
async function runManagedPython(options) {
|
|
149
|
-
const root = await fsRealpath(options.cwd);
|
|
150
|
-
const invocation = createBubblewrapPythonInvocation({
|
|
151
|
-
bubblewrapExecutable: options.bubblewrapExecutable,
|
|
152
|
-
pythonExecutable: options.pythonExecutable,
|
|
153
|
-
workspaceRoot: root,
|
|
154
|
-
env: options.env,
|
|
155
|
-
readOnlyPaths: await readableBubblewrapPaths(),
|
|
156
|
-
});
|
|
157
|
-
const result = await runPythonProcess({ command: invocation.command, args: invocation.args, cwd: root, env: options.env, code: options.code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
158
|
-
const unavailable = bubblewrapUnavailableReason(result.output);
|
|
159
|
-
if (unavailable !== undefined) {
|
|
160
|
-
return runManagedPythonFallback({ pythonExecutable: options.pythonExecutable, cwd: root, code: options.code, timeoutMs: options.timeoutMs, env: options.env, signal: options.signal });
|
|
161
|
-
}
|
|
162
|
-
return pythonToolResult(result.code, result.output);
|
|
163
|
-
}
|
|
164
|
-
async function runManagedPythonFallback(options) {
|
|
165
|
-
const code = `${createManagedPythonFallbackPrelude(options.cwd)}\n${options.code}`;
|
|
166
|
-
const result = await runPythonProcess({ command: options.pythonExecutable, args: ["-I", "-"], cwd: options.cwd, env: options.env, code, timeoutMs: options.timeoutMs, signal: options.signal });
|
|
167
|
-
return pythonToolResult(result.code, result.output);
|
|
168
|
-
}
|
|
169
|
-
async function runPythonProcess(options) {
|
|
170
|
-
return new Promise((resolvePromise, reject) => {
|
|
171
|
-
if (options.signal?.aborted === true) {
|
|
172
|
-
reject(new Error("Operation aborted"));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, stdio: ["pipe", "pipe", "pipe"], shell: false });
|
|
176
|
-
let stdout = "";
|
|
177
|
-
let stderr = "";
|
|
178
|
-
const timer = setTimeout(() => {
|
|
179
|
-
child.kill();
|
|
180
|
-
reject(new Error(`Python execution timed out after ${String(options.timeoutMs)}ms`));
|
|
181
|
-
}, options.timeoutMs);
|
|
182
|
-
const onAbort = () => {
|
|
183
|
-
child.kill();
|
|
184
|
-
reject(new Error("Operation aborted"));
|
|
185
|
-
};
|
|
186
|
-
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
187
|
-
child.stdout.on("data", (chunk) => { stdout += chunk.toString("utf8"); });
|
|
188
|
-
child.stderr.on("data", (chunk) => { stderr += chunk.toString("utf8"); });
|
|
189
|
-
child.on("error", (error) => {
|
|
190
|
-
clearTimeout(timer);
|
|
191
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
192
|
-
if (isNodeErrorWithCode(error, "ENOENT"))
|
|
193
|
-
reject(new Error("Python sandbox is unavailable"));
|
|
194
|
-
else
|
|
195
|
-
reject(error);
|
|
196
|
-
});
|
|
197
|
-
child.on("close", (codeValue) => {
|
|
198
|
-
clearTimeout(timer);
|
|
199
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
200
|
-
const output = truncateToolOutput([stdout.trimEnd(), stderr.trimEnd()].filter((part) => part !== "").join("\n"));
|
|
201
|
-
resolvePromise({ code: codeValue, output });
|
|
202
|
-
});
|
|
203
|
-
child.stdin.end(options.code);
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
function pythonToolResult(codeValue, output) {
|
|
207
|
-
const prefix = codeValue === 0 ? "" : `Python exited with code ${String(codeValue)}\n`;
|
|
208
|
-
return { content: [{ type: "text", text: `${prefix}${output}`.trimEnd() }], details: undefined };
|
|
209
|
-
}
|
|
210
|
-
async function readableBubblewrapPaths() {
|
|
211
|
-
const paths = await Promise.all(DEFAULT_BUBBLEWRAP_PATHS.map(async (path) => {
|
|
212
|
-
try {
|
|
213
|
-
await fsAccess(path, constants.R_OK);
|
|
214
|
-
return path;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
}));
|
|
220
|
-
return paths.filter(isDefined);
|
|
221
|
-
}
|
|
222
|
-
function createManagedFileOperations(cwd) {
|
|
223
|
-
const read = {
|
|
224
|
-
readFile: async (absolutePath) => readFile(await assertExistingInside(cwd, absolutePath)),
|
|
225
|
-
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK); },
|
|
226
|
-
};
|
|
227
|
-
const write = {
|
|
228
|
-
writeFile: async (absolutePath, content) => { await writeFile(await assertWritableInside(cwd, absolutePath), content, "utf8"); },
|
|
229
|
-
mkdir: async (dir) => { await fsMkdir(await assertPotentialInside(cwd, dir), { recursive: true }); },
|
|
230
|
-
};
|
|
231
|
-
const edit = {
|
|
232
|
-
readFile: read.readFile,
|
|
233
|
-
writeFile: write.writeFile,
|
|
234
|
-
access: async (absolutePath) => { await fsAccess(await assertExistingInside(cwd, absolutePath), constants.R_OK | constants.W_OK); },
|
|
235
|
-
};
|
|
236
|
-
const ls = {
|
|
237
|
-
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
238
|
-
stat: async (absolutePath) => fsStat(await assertExistingInside(cwd, absolutePath)),
|
|
239
|
-
readdir: async (absolutePath) => fsReaddir(await assertExistingInside(cwd, absolutePath)),
|
|
240
|
-
};
|
|
241
|
-
const grep = {
|
|
242
|
-
isDirectory: async (absolutePath) => (await fsStat(await assertExistingInside(cwd, absolutePath))).isDirectory(),
|
|
243
|
-
readFile: async (absolutePath) => (await readFile(await assertExistingInside(cwd, absolutePath), "utf8")),
|
|
244
|
-
};
|
|
245
|
-
const find = {
|
|
246
|
-
exists: async (absolutePath) => pathExistsInside(cwd, absolutePath),
|
|
247
|
-
glob: async (pattern, searchPath, options) => managedGlob(cwd, pattern, searchPath, options.limit),
|
|
248
|
-
};
|
|
249
|
-
return { read, write, edit, ls, grep, find };
|
|
250
|
-
}
|
|
251
|
-
async function pathExistsInside(rootPath, targetPath) {
|
|
252
|
-
try {
|
|
253
|
-
await assertExistingInside(rootPath, targetPath);
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
async function managedGlob(rootPath, pattern, searchPath, limit) {
|
|
261
|
-
const root = await fsRealpath(rootPath);
|
|
262
|
-
const start = await assertExistingInside(root, searchPath);
|
|
263
|
-
const regex = globPatternToRegExp(pattern);
|
|
264
|
-
const results = [];
|
|
265
|
-
async function walk(dir) {
|
|
266
|
-
if (results.length >= limit)
|
|
267
|
-
return;
|
|
268
|
-
const entries = await fsReaddir(dir, { withFileTypes: true });
|
|
269
|
-
for (const entry of entries) {
|
|
270
|
-
if (results.length >= limit)
|
|
271
|
-
return;
|
|
272
|
-
if (entry.name === ".git" || entry.name === "node_modules")
|
|
273
|
-
continue;
|
|
274
|
-
const fullPath = resolve(dir, entry.name);
|
|
275
|
-
const safePath = await assertExistingInside(root, fullPath);
|
|
276
|
-
const rel = relative(start, safePath).split(sep).join("/");
|
|
277
|
-
if (entry.isDirectory()) {
|
|
278
|
-
await walk(safePath);
|
|
279
|
-
}
|
|
280
|
-
else if (regex.test(rel)) {
|
|
281
|
-
results.push(safePath);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
await walk(start);
|
|
286
|
-
return results;
|
|
287
|
-
}
|
|
288
|
-
function globPatternToRegExp(pattern) {
|
|
289
|
-
const escaped = pattern
|
|
290
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
291
|
-
.replace(/\*\*/g, "\0")
|
|
292
|
-
.replace(/\*/g, "[^/]*")
|
|
293
|
-
.replace(/\?/g, "[^/]")
|
|
294
|
-
.replace(/\0/g, ".*");
|
|
295
|
-
return new RegExp(`^${escaped}$`);
|
|
296
|
-
}
|
|
297
|
-
async function assertExistingInside(rootPath, targetPath) {
|
|
298
|
-
const [root, target] = await Promise.all([fsRealpath(rootPath), fsRealpath(targetPath)]);
|
|
299
|
-
ensurePathInside(root, target);
|
|
300
|
-
return target;
|
|
301
|
-
}
|
|
302
|
-
async function assertWritableInside(rootPath, targetPath) {
|
|
303
|
-
try {
|
|
304
|
-
return await assertExistingInside(rootPath, targetPath);
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
const parent = await assertExistingInside(rootPath, dirname(targetPath));
|
|
308
|
-
const target = resolve(parent, basename(targetPath));
|
|
309
|
-
ensurePathInside(await fsRealpath(rootPath), target);
|
|
310
|
-
return target;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
async function assertPotentialInside(rootPath, targetPath) {
|
|
314
|
-
const root = await fsRealpath(rootPath);
|
|
315
|
-
const target = resolve(targetPath);
|
|
316
|
-
ensurePathInside(root, target);
|
|
317
|
-
return target;
|
|
318
|
-
}
|
|
319
|
-
function ensurePathInside(root, target) {
|
|
320
|
-
const rel = relative(root, target);
|
|
321
|
-
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel) && (sep === "/" || !rel.split(sep).includes(".."))))
|
|
322
|
-
return;
|
|
323
|
-
throw new Error("Path is outside the managed project sandbox");
|
|
324
|
-
}
|
|
325
|
-
function truncateToolOutput(value, limit = 64_000) {
|
|
326
|
-
if (value.length <= limit)
|
|
327
|
-
return value;
|
|
328
|
-
return `${value.slice(0, limit)}\n[output truncated]`;
|
|
329
|
-
}
|
|
330
78
|
export class PiSessionService {
|
|
331
79
|
constructor(events, deps = {}) {
|
|
332
80
|
this.events = events;
|
|
@@ -335,10 +83,9 @@ export class PiSessionService {
|
|
|
335
83
|
this.compactionPromptQueues = new Map();
|
|
336
84
|
this.compactionDrainTimers = new Map();
|
|
337
85
|
this.authLossWarnings = new Set();
|
|
338
|
-
this.managementContexts = new Map();
|
|
339
86
|
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
|
|
340
87
|
this.agentDir = deps.agentDir ?? getAgentDir();
|
|
341
|
-
this.sessionManager = deps.sessionManager ??
|
|
88
|
+
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
|
|
342
89
|
this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
|
|
343
90
|
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry);
|
|
344
91
|
this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
|
|
@@ -366,7 +113,6 @@ export class PiSessionService {
|
|
|
366
113
|
this.activities.clear();
|
|
367
114
|
this.compactionPromptQueues.clear();
|
|
368
115
|
this.authLossWarnings.clear();
|
|
369
|
-
this.managementContexts.clear();
|
|
370
116
|
await Promise.all(activeSessions.map(async (active) => {
|
|
371
117
|
active.unsubscribe();
|
|
372
118
|
this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
|
|
@@ -389,11 +135,10 @@ export class PiSessionService {
|
|
|
389
135
|
.filter(isDefined);
|
|
390
136
|
return [...unarchivedSessions, ...archivedSessions];
|
|
391
137
|
}
|
|
392
|
-
async start(cwd,
|
|
393
|
-
|
|
138
|
+
async start(cwd, _managementContext) {
|
|
139
|
+
void _managementContext;
|
|
140
|
+
const active = await this.create(this.sessionManager.create(cwd), cwd);
|
|
394
141
|
const { session } = active.runtime;
|
|
395
|
-
if (managementContext !== undefined)
|
|
396
|
-
this.managementContexts.set(session.sessionId, managementContext);
|
|
397
142
|
return {
|
|
398
143
|
id: session.sessionId,
|
|
399
144
|
path: session.sessionFile ?? "",
|
|
@@ -404,24 +149,24 @@ export class PiSessionService {
|
|
|
404
149
|
firstMessage: "",
|
|
405
150
|
};
|
|
406
151
|
}
|
|
407
|
-
async messages(
|
|
408
|
-
const session = await this.getOrOpen(
|
|
152
|
+
async messages(ref, page) {
|
|
153
|
+
const session = await this.getOrOpen(ref);
|
|
409
154
|
return pageMessagesAtSafeBoundary(historyMessages(session), page);
|
|
410
155
|
}
|
|
411
|
-
async status(
|
|
412
|
-
return this.statusFromSession(await this.getOrOpen(
|
|
156
|
+
async status(ref) {
|
|
157
|
+
return this.statusFromSession(await this.getOrOpen(ref));
|
|
413
158
|
}
|
|
414
|
-
async availableModels(
|
|
415
|
-
const session = await this.getOrOpen(
|
|
159
|
+
async availableModels(ref) {
|
|
160
|
+
const session = await this.getOrOpen(ref);
|
|
416
161
|
session.modelRegistry.refresh();
|
|
417
162
|
const models = session.scopedModels.length > 0
|
|
418
163
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
419
164
|
: session.modelRegistry.getAvailable();
|
|
420
165
|
return models.map(modelToClientModel);
|
|
421
166
|
}
|
|
422
|
-
async setModel(
|
|
423
|
-
await this.assertWritable(
|
|
424
|
-
const session = await this.getOrOpen(
|
|
167
|
+
async setModel(ref, provider, modelId) {
|
|
168
|
+
await this.assertWritable(ref);
|
|
169
|
+
const session = await this.getOrOpen(ref);
|
|
425
170
|
session.modelRegistry.refresh();
|
|
426
171
|
const candidates = session.scopedModels.length > 0
|
|
427
172
|
? session.scopedModels.map((scoped) => scoped.model)
|
|
@@ -435,9 +180,9 @@ export class PiSessionService {
|
|
|
435
180
|
this.publishStatus(session);
|
|
436
181
|
return this.statusFromSession(session);
|
|
437
182
|
}
|
|
438
|
-
async cycleModel(
|
|
439
|
-
await this.assertWritable(
|
|
440
|
-
const session = await this.getOrOpen(
|
|
183
|
+
async cycleModel(ref, direction) {
|
|
184
|
+
await this.assertWritable(ref);
|
|
185
|
+
const session = await this.getOrOpen(ref);
|
|
441
186
|
const result = await session.cycleModel(direction);
|
|
442
187
|
if (result === undefined)
|
|
443
188
|
throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
|
|
@@ -445,21 +190,27 @@ export class PiSessionService {
|
|
|
445
190
|
this.publishStatus(session);
|
|
446
191
|
return this.statusFromSession(session);
|
|
447
192
|
}
|
|
448
|
-
async availableThinkingLevels(
|
|
449
|
-
const session = await this.getOrOpen(
|
|
193
|
+
async availableThinkingLevels(ref) {
|
|
194
|
+
const session = await this.getOrOpen(ref);
|
|
450
195
|
return session.getAvailableThinkingLevels();
|
|
451
196
|
}
|
|
452
|
-
async setThinkingLevel(
|
|
453
|
-
await this.assertWritable(
|
|
454
|
-
const session = await this.getOrOpen(
|
|
455
|
-
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);
|
|
456
207
|
this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
|
|
457
208
|
this.publishStatus(session);
|
|
458
209
|
return this.statusFromSession(session);
|
|
459
210
|
}
|
|
460
|
-
async cycleThinkingLevel(
|
|
461
|
-
await this.assertWritable(
|
|
462
|
-
const session = await this.getOrOpen(
|
|
211
|
+
async cycleThinkingLevel(ref) {
|
|
212
|
+
await this.assertWritable(ref);
|
|
213
|
+
const session = await this.getOrOpen(ref);
|
|
463
214
|
const level = session.cycleThinkingLevel();
|
|
464
215
|
if (level === undefined)
|
|
465
216
|
throw new Error("Current model does not support thinking");
|
|
@@ -467,8 +218,8 @@ export class PiSessionService {
|
|
|
467
218
|
this.publishStatus(session);
|
|
468
219
|
return this.statusFromSession(session);
|
|
469
220
|
}
|
|
470
|
-
async commands(
|
|
471
|
-
const session = await this.getOrOpen(
|
|
221
|
+
async commands(ref) {
|
|
222
|
+
const session = await this.getOrOpen(ref);
|
|
472
223
|
const commands = [...BUILTIN_COMMANDS];
|
|
473
224
|
for (const command of session.extensionRunner.getRegisteredCommands()) {
|
|
474
225
|
commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
|
|
@@ -481,18 +232,18 @@ export class PiSessionService {
|
|
|
481
232
|
}
|
|
482
233
|
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
483
234
|
}
|
|
484
|
-
async prompt(
|
|
235
|
+
async prompt(ref, text, streamingBehavior, attachments, _managementContext) {
|
|
236
|
+
void _managementContext;
|
|
485
237
|
const promptText = requirePromptText(text);
|
|
486
238
|
const requestedBehavior = parsePromptStreamingBehavior(streamingBehavior);
|
|
487
|
-
const
|
|
488
|
-
await
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
throw new Error("当前模型不支持图片输入");
|
|
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);
|
|
492
243
|
this.maybeGenerateSessionName(session, promptText);
|
|
493
244
|
const isQueued = session.isStreaming || session.isCompacting;
|
|
494
245
|
const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
|
|
495
|
-
if (isQueued && this.hasQueuedMessageText(session, promptText)) {
|
|
246
|
+
if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
|
|
496
247
|
this.publishActivity(session, "duplicate queued message ignored", "active");
|
|
497
248
|
this.publishStatus(session);
|
|
498
249
|
return;
|
|
@@ -506,8 +257,9 @@ export class PiSessionService {
|
|
|
506
257
|
submitPrompt(session, text, behavior, images = []) {
|
|
507
258
|
this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
|
|
508
259
|
if (behavior === undefined)
|
|
509
|
-
this.events.publish(session.sessionId, { type: "message.append", message:
|
|
510
|
-
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) => {
|
|
511
263
|
const message = error instanceof Error ? error.message : String(error);
|
|
512
264
|
this.publishActivity(session, "error", "error", message);
|
|
513
265
|
this.events.publish(session.sessionId, { type: "session.error", message });
|
|
@@ -517,18 +269,23 @@ export class PiSessionService {
|
|
|
517
269
|
}
|
|
518
270
|
enqueuePromptDuringCompaction(session, text, kind, images = []) {
|
|
519
271
|
const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
|
|
520
|
-
queue.push({ kind, text, ...(images.length
|
|
272
|
+
queue.push({ kind, text, ...(images.length > 0 ? { images } : {}) });
|
|
521
273
|
this.compactionPromptQueues.set(session.sessionId, queue);
|
|
522
274
|
this.publishActivity(session, "message queued during compaction", "active");
|
|
523
275
|
this.publishStatus(session);
|
|
524
276
|
}
|
|
525
|
-
async
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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);
|
|
532
289
|
const { session } = active.runtime;
|
|
533
290
|
const isExcluded = text.startsWith("!!");
|
|
534
291
|
const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
|
|
@@ -561,40 +318,27 @@ export class PiSessionService {
|
|
|
561
318
|
this.publishStatus(session);
|
|
562
319
|
});
|
|
563
320
|
}
|
|
564
|
-
async runCommand(
|
|
565
|
-
|
|
566
|
-
this.
|
|
567
|
-
|
|
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);
|
|
568
326
|
}
|
|
569
|
-
async respondToCommand(
|
|
570
|
-
await this.assertWritable(
|
|
571
|
-
|
|
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);
|
|
572
331
|
}
|
|
573
|
-
async archive(
|
|
574
|
-
const session = await this.getOrOpen(
|
|
332
|
+
async archive(ref) {
|
|
333
|
+
const session = await this.getOrOpen(ref);
|
|
575
334
|
if (this.hasActiveWork(session))
|
|
576
335
|
throw new Error("Stop current session activity before archiving");
|
|
577
336
|
const archiveInput = await this.archiveInputForSession(session);
|
|
578
337
|
await this.closeActive(session.sessionId);
|
|
579
|
-
this.managementContexts.delete(session.sessionId);
|
|
580
338
|
await this.archiveStore.archive(archiveInput);
|
|
581
339
|
}
|
|
582
|
-
|
|
583
|
-
const
|
|
584
|
-
if (context === undefined || existing === undefined)
|
|
585
|
-
return;
|
|
586
|
-
if (existing.user.rootUserId !== context.user.rootUserId)
|
|
587
|
-
throw new Error("Session is outside the managed embed authorization scope");
|
|
588
|
-
}
|
|
589
|
-
rememberManagedSessionAccess(sessionId, context) {
|
|
590
|
-
if (context === undefined)
|
|
591
|
-
return;
|
|
592
|
-
this.assertManagedSessionAccess(sessionId, context);
|
|
593
|
-
if (!this.managementContexts.has(sessionId))
|
|
594
|
-
this.managementContexts.set(sessionId, context);
|
|
595
|
-
}
|
|
596
|
-
async archiveTree(sessionId) {
|
|
597
|
-
const session = await this.getOrOpen(sessionId);
|
|
340
|
+
async archiveTree(ref) {
|
|
341
|
+
const session = await this.getOrOpen(ref);
|
|
598
342
|
const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
|
|
599
343
|
const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
|
|
600
344
|
const plan = planSessionArchiveTree(root, catalog);
|
|
@@ -613,29 +357,56 @@ export class PiSessionService {
|
|
|
613
357
|
skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
|
|
614
358
|
};
|
|
615
359
|
}
|
|
616
|
-
async restore(
|
|
617
|
-
await this.
|
|
618
|
-
|
|
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);
|
|
619
386
|
}
|
|
620
|
-
async detachParent(
|
|
621
|
-
const session = await this.getOrOpen(
|
|
387
|
+
async detachParent(ref) {
|
|
388
|
+
const session = await this.getOrOpen(ref);
|
|
622
389
|
const sessionFile = session.sessionFile;
|
|
623
390
|
if (sessionFile === undefined || sessionFile === "")
|
|
624
391
|
throw new Error("Session is not persisted");
|
|
625
392
|
await clearParentSession(sessionFile);
|
|
626
393
|
}
|
|
627
|
-
async abort(
|
|
628
|
-
const active = this.
|
|
629
|
-
if (
|
|
394
|
+
async abort(ref) {
|
|
395
|
+
const active = this.activeForLookup(ref);
|
|
396
|
+
if (active === undefined)
|
|
630
397
|
return;
|
|
398
|
+
const sessionId = active.runtime.session.sessionId;
|
|
631
399
|
this.clearCompactionPromptQueue(sessionId);
|
|
632
400
|
clearSessionQueue(active.runtime.session);
|
|
633
401
|
await active.runtime.session.abort();
|
|
634
402
|
this.publishActivity(active.runtime.session, "stopped", "idle");
|
|
635
403
|
this.publishStatus(active.runtime.session);
|
|
636
404
|
}
|
|
637
|
-
stop(
|
|
638
|
-
|
|
405
|
+
stop(ref) {
|
|
406
|
+
const active = this.activeForLookup(ref);
|
|
407
|
+
if (active === undefined)
|
|
408
|
+
return;
|
|
409
|
+
void this.closeActive(active.runtime.session.sessionId).catch(() => {
|
|
639
410
|
// Best-effort shutdown; callers that need errors await closeActive directly.
|
|
640
411
|
});
|
|
641
412
|
}
|
|
@@ -658,6 +429,12 @@ export class PiSessionService {
|
|
|
658
429
|
return record;
|
|
659
430
|
}
|
|
660
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
|
+
}
|
|
661
438
|
async archiveInputForSession(session) {
|
|
662
439
|
const cwd = session.sessionManager.getCwd();
|
|
663
440
|
const sessionFile = session.sessionFile;
|
|
@@ -723,7 +500,6 @@ export class PiSessionService {
|
|
|
723
500
|
if (!active)
|
|
724
501
|
return;
|
|
725
502
|
this.active.delete(sessionId);
|
|
726
|
-
this.managementContexts.delete(sessionId);
|
|
727
503
|
this.activities.delete(sessionId);
|
|
728
504
|
this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
|
|
729
505
|
this.clearAuthLossWarningsForSession(sessionId);
|
|
@@ -737,67 +513,74 @@ export class PiSessionService {
|
|
|
737
513
|
await active.runtime.dispose();
|
|
738
514
|
}
|
|
739
515
|
}
|
|
740
|
-
async assertWritable(
|
|
741
|
-
if (await this.
|
|
516
|
+
async assertWritable(ref) {
|
|
517
|
+
if (await this.getArchived(ref) !== undefined)
|
|
742
518
|
throw new Error("Archived sessions are read-only. Restore the session to continue.");
|
|
743
519
|
}
|
|
744
|
-
async getOrOpen(
|
|
745
|
-
return (await this.getActive(
|
|
520
|
+
async getOrOpen(ref) {
|
|
521
|
+
return (await this.getActive(ref)).runtime.session;
|
|
746
522
|
}
|
|
747
|
-
async getActive(
|
|
748
|
-
const active = this.
|
|
749
|
-
if (active)
|
|
750
|
-
const activeSessionId = active.runtime.session.sessionId;
|
|
751
|
-
const existingContext = this.managementContexts.get(activeSessionId);
|
|
752
|
-
if (managementContext !== undefined && existingContext === undefined) {
|
|
753
|
-
const sessionFile = active.runtime.session.sessionFile;
|
|
754
|
-
if (sessionFile === undefined || sessionFile === "")
|
|
755
|
-
throw new Error("Managed embed session must be persisted before it can be resumed safely");
|
|
756
|
-
const activeCwd = active.runtime.session.sessionManager.getCwd();
|
|
757
|
-
await this.closeActive(activeSessionId);
|
|
758
|
-
return this.create(this.sessionManager.open(sessionFile), activeCwd, managementContext);
|
|
759
|
-
}
|
|
760
|
-
this.rememberManagedSessionAccess(active.runtime.session.sessionId, managementContext);
|
|
523
|
+
async getActive(ref) {
|
|
524
|
+
const active = this.activeForLookup(ref);
|
|
525
|
+
if (active !== undefined)
|
|
761
526
|
return active;
|
|
762
|
-
|
|
763
|
-
const archived = await this.archiveStore.get(sessionId);
|
|
527
|
+
const archived = await this.getArchived(ref);
|
|
764
528
|
if (archived?.archivePath !== undefined)
|
|
765
|
-
return this.create(this.sessionManager.open(archived.archivePath), archived.cwd
|
|
766
|
-
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));
|
|
767
533
|
if (!match)
|
|
768
534
|
throw new Error("Session not found");
|
|
769
|
-
return this.create(this.sessionManager.open(match.path), match.cwd
|
|
770
|
-
}
|
|
771
|
-
async
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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);
|
|
779
559
|
const active = { runtime, unsubscribe: noop };
|
|
780
560
|
this.bindRuntime(active);
|
|
781
|
-
runtime.setRebindSession(() => {
|
|
561
|
+
runtime.setRebindSession(async (session) => {
|
|
562
|
+
await this.bindSessionExtensions(session);
|
|
782
563
|
this.bindRuntime(active);
|
|
783
|
-
return Promise.resolve();
|
|
784
564
|
});
|
|
785
565
|
this.active.set(runtime.session.sessionId, active);
|
|
786
|
-
this.rememberManagedSessionAccess(runtime.session.sessionId, managementContext);
|
|
787
566
|
this.publishStatus(runtime.session);
|
|
788
567
|
return active;
|
|
789
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
|
+
}
|
|
790
578
|
bindRuntime(active) {
|
|
791
579
|
active.unsubscribe();
|
|
792
580
|
const { session } = active.runtime;
|
|
793
581
|
for (const [sessionId, candidate] of this.active.entries()) {
|
|
794
582
|
if (candidate === active) {
|
|
795
583
|
this.active.delete(sessionId);
|
|
796
|
-
const context = this.managementContexts.get(sessionId);
|
|
797
|
-
if (context !== undefined && sessionId !== session.sessionId) {
|
|
798
|
-
this.managementContexts.delete(sessionId);
|
|
799
|
-
this.managementContexts.set(session.sessionId, context);
|
|
800
|
-
}
|
|
801
584
|
if (sessionId !== session.sessionId)
|
|
802
585
|
this.clearCompactionPromptQueue(sessionId);
|
|
803
586
|
}
|
|
@@ -838,14 +621,14 @@ export class PiSessionService {
|
|
|
838
621
|
return;
|
|
839
622
|
this.publishStatus(session);
|
|
840
623
|
for (const prompt of queued)
|
|
841
|
-
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images
|
|
624
|
+
void this.submitPrompt(session, prompt.text, prompt.kind, prompt.images);
|
|
842
625
|
return;
|
|
843
626
|
}
|
|
844
627
|
const prompt = this.shiftCompactionPrompt(sessionId);
|
|
845
628
|
if (prompt === undefined)
|
|
846
629
|
return;
|
|
847
630
|
this.publishStatus(session);
|
|
848
|
-
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images
|
|
631
|
+
const submitted = this.submitPrompt(session, prompt.text, undefined, prompt.images);
|
|
849
632
|
void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
|
|
850
633
|
}
|
|
851
634
|
takeCompactionPromptQueue(sessionId) {
|
|
@@ -1080,25 +863,14 @@ function modelToClientModel(model) {
|
|
|
1080
863
|
return {};
|
|
1081
864
|
const name = getString(model, "name");
|
|
1082
865
|
const reasoning = getProperty(model, "reasoning");
|
|
1083
|
-
const input = modelInput(model);
|
|
1084
866
|
return {
|
|
1085
867
|
provider: model.provider,
|
|
1086
868
|
id: model.id,
|
|
1087
869
|
...(name === undefined ? {} : { name }),
|
|
1088
870
|
contextWindow: model.contextWindow,
|
|
1089
871
|
...(reasoning === undefined ? {} : { reasoning }),
|
|
1090
|
-
...(input === undefined ? {} : { input }),
|
|
1091
872
|
};
|
|
1092
873
|
}
|
|
1093
|
-
function modelInput(model) {
|
|
1094
|
-
const input = getProperty(model, "input");
|
|
1095
|
-
if (!Array.isArray(input))
|
|
1096
|
-
return undefined;
|
|
1097
|
-
return input.filter((item) => item === "text" || item === "image");
|
|
1098
|
-
}
|
|
1099
|
-
function modelSupportsImageInput(model) {
|
|
1100
|
-
return modelInput(model)?.includes("image") === true;
|
|
1101
|
-
}
|
|
1102
874
|
function clientSessionFromListEntry(session) {
|
|
1103
875
|
return {
|
|
1104
876
|
id: session.id,
|
|
@@ -1232,9 +1004,6 @@ function archivedTimestamp(record) {
|
|
|
1232
1004
|
function isDefined(value) {
|
|
1233
1005
|
return value !== undefined;
|
|
1234
1006
|
}
|
|
1235
|
-
function isNodeErrorWithCode(error, code) {
|
|
1236
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
1237
|
-
}
|
|
1238
1007
|
async function clearParentSession(sessionFile) {
|
|
1239
1008
|
const content = await readFile(sessionFile, "utf8");
|
|
1240
1009
|
const newlineIndex = content.indexOf("\n");
|
|
@@ -1255,22 +1024,33 @@ function queuedMessagesFromSession(session, extraQueuedMessages = []) {
|
|
|
1255
1024
|
return [
|
|
1256
1025
|
...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
|
|
1257
1026
|
...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
|
|
1258
|
-
...extraQueuedMessages
|
|
1027
|
+
...extraQueuedMessages,
|
|
1259
1028
|
];
|
|
1260
1029
|
}
|
|
1261
|
-
function
|
|
1262
|
-
|
|
1263
|
-
return { kind: message.kind, text: message.text, ...(imageCount === 0 ? {} : { imageCount }) };
|
|
1030
|
+
function userTextMessage(text) {
|
|
1031
|
+
return { role: "user", content: text };
|
|
1264
1032
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
function
|
|
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) {
|
|
1271
1039
|
if (images.length === 0)
|
|
1272
|
-
return
|
|
1273
|
-
|
|
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;
|
|
1274
1054
|
}
|
|
1275
1055
|
function stringValue(value) {
|
|
1276
1056
|
return typeof value === "string" ? value : "";
|