@code-yeongyu/senpi 2026.10.4 → 2026.10.5
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/CHANGELOG.md +20 -0
- package/dist/bundle/chunks/{anthropic-messages-GE4OCV55.js → anthropic-messages-G5236QNK.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-I2AKSC6I.js → app-server-command-3Q5G4CAF.js} +1 -1
- package/dist/bundle/chunks/{chunk-2CS2GZBE.js → chunk-2WV426OJ.js} +2 -2
- package/dist/bundle/chunks/{chunk-INGR4HFB.js → chunk-ECDLITEY.js} +2 -2
- package/dist/bundle/chunks/{chunk-6O26XO2C.js → chunk-EEOKVSNF.js} +220 -252
- package/dist/bundle/chunks/{chunk-MNLBL232.js → chunk-JVSOW4QU.js} +1 -1
- package/dist/bundle/chunks/chunk-K3BXFPWZ.js +2 -0
- package/dist/bundle/chunks/{chunk-QOT4HXY2.js → chunk-UHRXXTFZ.js} +3 -3
- package/dist/bundle/chunks/chunk-UUAKEDVS.js +14 -0
- package/dist/bundle/chunks/{chunk-ZKLQIAG5.js → chunk-UXIQK5NQ.js} +1 -1
- package/dist/bundle/chunks/chunk-WTJU5B2C.js +51 -0
- package/dist/bundle/chunks/chunk-YSPHACIN.js +2 -0
- package/dist/bundle/chunks/{cli-main-BLXUYHLQ.js → cli-main-VZWSUHFB.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-TQWU6DPG.js → help-fast-path-SHZJ3QWQ.js} +1 -1
- package/dist/bundle/chunks/{host-command-JXNOYPAC.js → host-command-7JIGGOWR.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-CSVSUAS7.js → interactive-mode-JA7AHYYL.js} +1 -1
- package/dist/bundle/chunks/multi-session-host-34WOINP4.js +23 -0
- package/dist/bundle/chunks/{openai-completions-2DUKDEVN.js → openai-completions-VSH22FCN.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-WCB4HIPI.js → openai-responses-WLBHCFYL.js} +1 -1
- package/dist/bundle/chunks/{package-manager-cli-Q5EQZL7I.js → package-manager-cli-2QLVGQSC.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-VZLJIEP5.js → rotation-stream-JUCEXHWU.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-6VMXY46C.js → rpc-mode-QEBSN2OH.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-K3RWX652.js → session-control-endpoint-UGQIP6ZN.js} +1 -1
- package/dist/bundle/chunks/{session-picker-IPG3MKRC.js → session-picker-HX4LOJZ7.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +251 -242
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +5 -0
- package/dist/core/agent-session-runtime.js +5 -0
- package/dist/core/agent-session-services.d.ts +1 -0
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session.d.ts +14 -0
- package/dist/core/agent-session.js +31 -0
- package/dist/core/browser-engine.d.ts +7 -0
- package/dist/core/browser-engine.js +7 -0
- package/dist/core/continue-from-leaf.d.ts +18 -0
- package/dist/core/continue-from-leaf.js +30 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/line-endings.d.ts +28 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/line-endings.js +76 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/parser.js +21 -23
- package/dist/core/extensions/builtin/gpt-apply-patch/patch-replace.js +5 -16
- package/dist/core/extensions/builtin/gpt-apply-patch/streaming-parser.js +3 -1
- package/dist/core/extensions/builtin/gpt-apply-patch/text.d.ts +8 -0
- package/dist/core/extensions/builtin/gpt-apply-patch/text.js +26 -3
- package/dist/core/extensions/builtin/gpt-apply-patch/types.d.ts +2 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.d.ts +1 -0
- package/dist/core/extensions/builtin/terminal/tools/bash.js +8 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/types.d.ts +4 -0
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/sdk.d.ts +3 -0
- package/dist/core/sdk.js +1 -0
- package/dist/core/tools/bash.js +3 -0
- package/dist/main.js +1 -0
- package/dist/modes/interactive/components/session-selector.d.ts +11 -0
- package/dist/modes/interactive/components/session-selector.js +30 -5
- package/dist/modes/rpc/connection-handler.js +18 -1
- package/dist/modes/rpc/custom-capability.d.ts +9 -0
- package/dist/modes/rpc/custom-capability.js +9 -0
- package/dist/modes/rpc/host-daemon-env.js +2 -0
- package/dist/modes/rpc/host-idle-handover.js +1 -0
- package/dist/modes/rpc/media-placeholders.d.ts +23 -1
- package/dist/modes/rpc/media-placeholders.js +16 -13
- package/dist/modes/rpc/rpc-client.d.ts +8 -0
- package/dist/modes/rpc/rpc-client.js +8 -0
- package/dist/modes/rpc/rpc-input-validation.d.ts +6 -0
- package/dist/modes/rpc/rpc-input-validation.js +11 -0
- package/dist/modes/rpc/rpc-types.d.ts +19 -0
- package/dist/modes/rpc/session-command-router.js +11 -2
- package/dist/modes/rpc/session-event-writer.d.ts +4 -0
- package/dist/modes/rpc/session-event-writer.js +12 -1
- package/dist/modes/rpc/session-registry-attach.js +4 -0
- package/dist/modes/rpc/session-worker-client.d.ts +3 -0
- package/dist/modes/rpc/session-worker-client.js +6 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +5 -0
- package/dist/modes/rpc/session-worker.js +6 -0
- package/dist/modes/rpc/tool-media-store.d.ts +59 -0
- package/dist/modes/rpc/tool-media-store.js +190 -0
- package/dist/modes/rpc/worker-session-registry.js +8 -3
- package/docs/environment-variables.md +1 -0
- package/docs/rpc.md +62 -3
- package/package.json +7 -6
- package/dist/bundle/chunks/chunk-KMMECBM2.js +0 -15
- package/dist/bundle/chunks/chunk-LHYG7TK7.js +0 -2
- package/dist/bundle/chunks/chunk-LJWC5IVX.js +0 -2
- package/dist/bundle/chunks/chunk-PC3E2YS6.js +0 -9
- package/dist/bundle/chunks/multi-session-host-5AFJKSSM.js +0 -23
|
@@ -1,14 +1,8 @@
|
|
|
1
|
+
import { replacementsAroundContext, SourceText } from "./line-endings.js";
|
|
1
2
|
import { seekSequenceWithFuzz } from "./seek-sequence.js";
|
|
2
|
-
import { normalizePatchText } from "./text.js";
|
|
3
|
-
function splitFileLines(content) {
|
|
4
|
-
const lines = normalizePatchText(content).split("\n");
|
|
5
|
-
if (lines[lines.length - 1] === "") {
|
|
6
|
-
lines.pop();
|
|
7
|
-
}
|
|
8
|
-
return lines;
|
|
9
|
-
}
|
|
10
3
|
export function replaceChunks(content, filePath, chunks) {
|
|
11
|
-
const
|
|
4
|
+
const source = SourceText.parse(content);
|
|
5
|
+
const originalLines = source.texts;
|
|
12
6
|
const replacements = [];
|
|
13
7
|
let lineIndex = 0;
|
|
14
8
|
let fuzz = 0;
|
|
@@ -37,14 +31,9 @@ export function replaceChunks(content, filePath, chunks) {
|
|
|
37
31
|
if (foundAt === undefined)
|
|
38
32
|
throw new Error(`Failed to find expected lines in ${filePath}:\n${chunk.oldLines.join("\n")}`);
|
|
39
33
|
fuzz += foundAt.fuzz;
|
|
40
|
-
replacements.push(
|
|
34
|
+
replacements.push(...replacementsAroundContext(foundAt.index, pattern.length, newLines, chunk.contextLineIndices));
|
|
41
35
|
lineIndex = foundAt.index + pattern.length;
|
|
42
36
|
}
|
|
43
|
-
|
|
44
|
-
for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
|
|
45
|
-
nextLines.splice(replacement.start, replacement.oldLength, ...replacement.newLines);
|
|
46
|
-
}
|
|
47
|
-
nextLines.push("");
|
|
48
|
-
return { content: nextLines.join("\n"), fuzz };
|
|
37
|
+
return { content: source.replace(replacements), fuzz };
|
|
49
38
|
}
|
|
50
39
|
//# sourceMappingURL=patch-replace.js.map
|
|
@@ -100,7 +100,7 @@ export class StreamingPatchParser {
|
|
|
100
100
|
const hunk = this.currentUpdate();
|
|
101
101
|
let chunk = hunk.chunks[hunk.chunks.length - 1];
|
|
102
102
|
if (!chunk || chunk.isEndOfFile) {
|
|
103
|
-
chunk = { changeContexts: [], oldLines: [], newLines: [], isEndOfFile: false };
|
|
103
|
+
chunk = { changeContexts: [], oldLines: [], newLines: [], contextLineIndices: [], isEndOfFile: false };
|
|
104
104
|
hunk.chunks.push(chunk);
|
|
105
105
|
}
|
|
106
106
|
return chunk;
|
|
@@ -178,6 +178,7 @@ export class StreamingPatchParser {
|
|
|
178
178
|
const value = line.slice(1);
|
|
179
179
|
const chunk = this.currentChunk();
|
|
180
180
|
if (prefix === " ") {
|
|
181
|
+
chunk.contextLineIndices.push([chunk.oldLines.length, chunk.newLines.length]);
|
|
181
182
|
chunk.oldLines.push(value);
|
|
182
183
|
chunk.newLines.push(value);
|
|
183
184
|
}
|
|
@@ -188,6 +189,7 @@ export class StreamingPatchParser {
|
|
|
188
189
|
chunk.newLines.push(value);
|
|
189
190
|
}
|
|
190
191
|
else if (prefix === undefined) {
|
|
192
|
+
chunk.contextLineIndices.push([chunk.oldLines.length, chunk.newLines.length]);
|
|
191
193
|
chunk.oldLines.push("");
|
|
192
194
|
chunk.newLines.push("");
|
|
193
195
|
}
|
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
export declare function normalizePatchText(patchText: string): string;
|
|
2
2
|
export declare function stripHeredoc(input: string): string;
|
|
3
|
+
declare const FILE_HEADER_MARKERS: readonly [readonly ["add", "*** Add File: "], readonly ["delete", "*** Delete File: "], readonly ["update", "*** Update File: "]];
|
|
4
|
+
export type FileHeader = {
|
|
5
|
+
readonly kind: (typeof FILE_HEADER_MARKERS)[number][0];
|
|
6
|
+
readonly path: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function parseFileHeader(line: string): FileHeader | undefined;
|
|
9
|
+
export declare function parseMoveTo(line: string): string | undefined;
|
|
3
10
|
export declare function extractPatchedPaths(patchText: string): string[];
|
|
11
|
+
export {};
|
|
4
12
|
//# sourceMappingURL=text.d.ts.map
|
|
@@ -8,9 +8,32 @@ export function stripHeredoc(input) {
|
|
|
8
8
|
}
|
|
9
9
|
return input;
|
|
10
10
|
}
|
|
11
|
+
const FILE_HEADER_MARKERS = [
|
|
12
|
+
["add", "*** Add File: "],
|
|
13
|
+
["delete", "*** Delete File: "],
|
|
14
|
+
["update", "*** Update File: "],
|
|
15
|
+
];
|
|
16
|
+
const MOVE_TO_MARKER = "*** Move to: ";
|
|
17
|
+
// The parser and extractPatchedPaths both read headers through these two functions, so they share one
|
|
18
|
+
// whitespace definition (String.prototype.trim): every path the parser writes is a path the permission
|
|
19
|
+
// system's per-file approval sees, spelled the same way.
|
|
20
|
+
export function parseFileHeader(line) {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
for (const [kind, marker] of FILE_HEADER_MARKERS) {
|
|
23
|
+
if (trimmed.startsWith(marker))
|
|
24
|
+
return { kind, path: trimmed.slice(marker.length) };
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
export function parseMoveTo(line) {
|
|
29
|
+
const trimmed = line.trimEnd();
|
|
30
|
+
return trimmed.startsWith(MOVE_TO_MARKER) ? trimmed.slice(MOVE_TO_MARKER.length) : undefined;
|
|
31
|
+
}
|
|
11
32
|
export function extractPatchedPaths(patchText) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
const lines = stripHeredoc(normalizePatchText(patchText)).split("\n");
|
|
34
|
+
return lines.flatMap((line) => {
|
|
35
|
+
const path = parseFileHeader(line)?.path ?? parseMoveTo(line.trimStart());
|
|
36
|
+
return path === undefined ? [] : [path];
|
|
37
|
+
});
|
|
15
38
|
}
|
|
16
39
|
//# sourceMappingURL=text.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ToolDefinition } from "../../types.ts";
|
|
2
2
|
import type { APPLY_PATCH_PARAMS } from "./constants.ts";
|
|
3
|
+
import type { ContextLineIndex } from "./line-endings.ts";
|
|
3
4
|
export type ParsedPatch = {
|
|
4
5
|
type: "add";
|
|
5
6
|
filePath: string;
|
|
@@ -17,6 +18,7 @@ export type PatchChunk = {
|
|
|
17
18
|
changeContexts: string[];
|
|
18
19
|
oldLines: string[];
|
|
19
20
|
newLines: string[];
|
|
21
|
+
contextLineIndices: ContextLineIndex[];
|
|
20
22
|
isEndOfFile: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type ApplyPatchWireMode = "freeform" | "json" | "none";
|
|
@@ -79,7 +79,12 @@ function sessionSpawnContext(ctx) {
|
|
|
79
79
|
return {
|
|
80
80
|
...ctx,
|
|
81
81
|
// Explicit deletions survive backends that merge overrides with the host environment.
|
|
82
|
-
getEnv: () => ({
|
|
82
|
+
getEnv: () => ({
|
|
83
|
+
...ctx.getEnv(),
|
|
84
|
+
PI_SESSION_CWD: undefined,
|
|
85
|
+
PI_GOAL_STORE_FILE: undefined,
|
|
86
|
+
OMO_BROWSER_ENGINE: undefined,
|
|
87
|
+
}),
|
|
83
88
|
};
|
|
84
89
|
}
|
|
85
90
|
function sessionEnvOverrides(ctx, execCtx) {
|
|
@@ -101,6 +106,8 @@ function sessionEnvOverrides(ctx, execCtx) {
|
|
|
101
106
|
}
|
|
102
107
|
if (session.thinkingLevel)
|
|
103
108
|
env.PI_REASONING_LEVEL = session.thinkingLevel;
|
|
109
|
+
if (session.browserEngine)
|
|
110
|
+
env.OMO_BROWSER_ENGINE = session.browserEngine;
|
|
104
111
|
return env;
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
@@ -138,6 +138,7 @@ export declare class ExtensionRunner {
|
|
|
138
138
|
private getPromptCacheKeepAliveSettingsFn;
|
|
139
139
|
private getLookAtSettingsFn;
|
|
140
140
|
private getAskUserSettingsFn;
|
|
141
|
+
private getBrowserEngineFn;
|
|
141
142
|
private getImageSettingsFn;
|
|
142
143
|
private sessionSettingsFn;
|
|
143
144
|
private compactFn;
|
|
@@ -248,6 +248,7 @@ export class ExtensionRunner {
|
|
|
248
248
|
enabled: true,
|
|
249
249
|
timeoutMinutes: 30,
|
|
250
250
|
});
|
|
251
|
+
this.getBrowserEngineFn = () => undefined;
|
|
251
252
|
this.getImageSettingsFn = () => ({
|
|
252
253
|
autoResize: true,
|
|
253
254
|
blockImages: false,
|
|
@@ -356,6 +357,8 @@ export class ExtensionRunner {
|
|
|
356
357
|
this.getLookAtSettingsFn = contextActions.getLookAtSettings;
|
|
357
358
|
if (contextActions.getAskUserSettings)
|
|
358
359
|
this.getAskUserSettingsFn = contextActions.getAskUserSettings;
|
|
360
|
+
if (contextActions.getBrowserEngine)
|
|
361
|
+
this.getBrowserEngineFn = contextActions.getBrowserEngine;
|
|
359
362
|
this.getImageSettingsFn = contextActions.getImageSettings;
|
|
360
363
|
this.sessionSettingsFn = contextActions.sessionSettings;
|
|
361
364
|
this.compactFn = contextActions.compact;
|
|
@@ -908,6 +911,10 @@ export class ExtensionRunner {
|
|
|
908
911
|
runner.assertActive();
|
|
909
912
|
return goalFilePath(goalStoreRef(runner.sessionManager, runner.cwd));
|
|
910
913
|
},
|
|
914
|
+
get browserEngine() {
|
|
915
|
+
runner.assertActive();
|
|
916
|
+
return runner.getBrowserEngineFn();
|
|
917
|
+
},
|
|
911
918
|
get modelRegistry() {
|
|
912
919
|
runner.assertActive();
|
|
913
920
|
return runner.modelRegistry;
|
|
@@ -13,6 +13,7 @@ import type { AutocompleteItem, AutocompleteProvider, Component, EditorComponent
|
|
|
13
13
|
import type { Static, TSchema } from "typebox";
|
|
14
14
|
import type { Theme } from "../../modes/interactive/theme/theme.ts";
|
|
15
15
|
import type { BashResult } from "../bash-executor.ts";
|
|
16
|
+
import type { BrowserEngine } from "../browser-engine.ts";
|
|
16
17
|
import type { CompactionPreparation, CompactionResult } from "../compaction/index.ts";
|
|
17
18
|
import type { WarmAnchorSnapshot } from "../compaction/warm-anchor.ts";
|
|
18
19
|
import type { EventBus } from "../event-bus.ts";
|
|
@@ -355,6 +356,8 @@ export interface ExtensionContext {
|
|
|
355
356
|
sessionManager: ReadonlySessionManager;
|
|
356
357
|
/** Absolute goal-store path for this session; reading it does not create the file. */
|
|
357
358
|
readonly goalStoreFile?: string;
|
|
359
|
+
/** Browser engine the opener chose for THIS session (`open_session.browserEngine`); absent when it chose none. */
|
|
360
|
+
readonly browserEngine?: BrowserEngine;
|
|
358
361
|
/** Model registry for API key resolution */
|
|
359
362
|
modelRegistry: ModelRegistry;
|
|
360
363
|
/** Current model (may be undefined) */
|
|
@@ -2366,6 +2369,7 @@ export interface ExtensionContextActions {
|
|
|
2366
2369
|
enabled: boolean;
|
|
2367
2370
|
timeoutMinutes: number;
|
|
2368
2371
|
};
|
|
2372
|
+
getBrowserEngine?: () => BrowserEngine | undefined;
|
|
2369
2373
|
getImageSettings: () => {
|
|
2370
2374
|
autoResize: boolean;
|
|
2371
2375
|
blockImages: boolean;
|
|
@@ -24,7 +24,7 @@ export const defaultModelPerProvider = {
|
|
|
24
24
|
cursor: "auto",
|
|
25
25
|
// Radius resolves its catalog after discovery; "balanced" is the selectable default.
|
|
26
26
|
radius: "balanced",
|
|
27
|
-
nvidia: "nvidia/nemotron-3-
|
|
27
|
+
nvidia: "nvidia/nemotron-3-ultra-550b-a55b",
|
|
28
28
|
deepseek: "deepseek-v4-pro",
|
|
29
29
|
google: "gemini-3.1-pro-preview",
|
|
30
30
|
"google-vertex": "gemini-3.1-pro-preview",
|
package/dist/core/sdk.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ThinkingSelection } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { type Model } from "@earendil-works/pi-ai/compat";
|
|
4
4
|
import { AgentSession } from "./agent-session.ts";
|
|
5
5
|
import { AuthStorage } from "./auth-storage.ts";
|
|
6
|
+
import type { BrowserEngine } from "./browser-engine.ts";
|
|
6
7
|
import type { PromptSurface } from "./dynamic-prompt/types.ts";
|
|
7
8
|
import { type ServiceTier } from "./extensions/builtin/service-tier.ts";
|
|
8
9
|
import type { LoadExtensionsResult, SessionStartEvent, ToolDefinition } from "./extensions/index.ts";
|
|
@@ -83,6 +84,8 @@ export interface CreateAgentSessionOptions {
|
|
|
83
84
|
autoTitleSessions?: boolean;
|
|
84
85
|
/** Where this session's replies render; omitted means `SENPI_PROMPT_SURFACE` decides. */
|
|
85
86
|
promptSurface?: PromptSurface;
|
|
87
|
+
/** Browser engine this session's skills drive (`open_session.browserEngine`); omitted means none was chosen. */
|
|
88
|
+
browserEngine?: BrowserEngine;
|
|
86
89
|
}
|
|
87
90
|
/** Result from createAgentSession */
|
|
88
91
|
export interface CreateAgentSessionResult {
|
package/dist/core/sdk.js
CHANGED
|
@@ -395,6 +395,7 @@ export async function createAgentSession(options = {}) {
|
|
|
395
395
|
sessionStartEvent,
|
|
396
396
|
autoTitleSessions: options.autoTitleSessions,
|
|
397
397
|
promptSurface: options.promptSurface,
|
|
398
|
+
browserEngine: options.browserEngine,
|
|
398
399
|
});
|
|
399
400
|
const liveContextTokens = hasExistingSession
|
|
400
401
|
? existingSession.messages.reduce((total, message) => total + estimateTokens(message), 0)
|
package/dist/core/tools/bash.js
CHANGED
|
@@ -191,6 +191,7 @@ function resolveSpawnContext(command, cwd, spawnHook, exposeSessionEnvironment,
|
|
|
191
191
|
delete env.PI_PROVIDER;
|
|
192
192
|
delete env.PI_MODEL;
|
|
193
193
|
delete env.PI_REASONING_LEVEL;
|
|
194
|
+
delete env.OMO_BROWSER_ENGINE;
|
|
194
195
|
if (exposeSessionEnvironment && ctx) {
|
|
195
196
|
const model = ctx.model;
|
|
196
197
|
env.PI_SESSION_ID = ctx.sessionManager.getSessionId();
|
|
@@ -206,6 +207,8 @@ function resolveSpawnContext(command, cwd, spawnHook, exposeSessionEnvironment,
|
|
|
206
207
|
}
|
|
207
208
|
if (ctx.thinkingLevel)
|
|
208
209
|
env.PI_REASONING_LEVEL = ctx.thinkingLevel;
|
|
210
|
+
if (ctx.browserEngine)
|
|
211
|
+
env.OMO_BROWSER_ENGINE = ctx.browserEngine;
|
|
209
212
|
}
|
|
210
213
|
const baseContext = { command, cwd, env };
|
|
211
214
|
return spawnHook ? spawnHook(baseContext) : baseContext;
|
package/dist/main.js
CHANGED
|
@@ -813,6 +813,7 @@ export function createCliRuntimeFactory(configuration, local = {}) {
|
|
|
813
813
|
customTools: sessionOptions.customTools,
|
|
814
814
|
autoTitleSessions: resolveAutoTitleSessions(appMode, parsed, sessionManager.hasContextMessages(), parseClientCapabilities(envValue("RPC_CLIENT_CAPABILITIES")), launchProfile?.autoTitle),
|
|
815
815
|
promptSurface: launchProfile?.promptSurface,
|
|
816
|
+
browserEngine: launchProfile?.browserEngine,
|
|
816
817
|
});
|
|
817
818
|
markSwitch("createSession");
|
|
818
819
|
const cliThinkingOverride = runtimeParsed.thinking !== undefined || cliThinkingFromModel;
|
|
@@ -49,6 +49,17 @@ declare class SessionList implements Component, Focusable {
|
|
|
49
49
|
handleInput(keyData: string): void;
|
|
50
50
|
}
|
|
51
51
|
type SessionsLoader = (onProgress?: SessionListProgress, signal?: AbortSignal) => Promise<SessionInfo[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Delete a session file, trying the `trash` CLI first, then falling back to unlink. The session's
|
|
54
|
+
* tool images go with it; when they cannot be removed the session is still deleted (its id is gone
|
|
55
|
+
* with the file) and `mediaError` names the leftover directory so the caller can say so.
|
|
56
|
+
*/
|
|
57
|
+
export declare function deleteSessionFile(sessionPath: string): Promise<{
|
|
58
|
+
ok: boolean;
|
|
59
|
+
method: "trash" | "unlink";
|
|
60
|
+
error?: string;
|
|
61
|
+
mediaError?: string;
|
|
62
|
+
}>;
|
|
52
63
|
/**
|
|
53
64
|
* Component that renders a session selector
|
|
54
65
|
*/
|
|
@@ -4,6 +4,7 @@ import { unlink } from "node:fs/promises";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import { Container, getKeybindings, Input, Spacer, Text, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
6
6
|
import { KeybindingsManager } from "../../../core/keybindings.js";
|
|
7
|
+
import { removeToolMedia, toolMediaRoot, toolMediaScopeOfSessionFile } from "../../rpc/tool-media-store.js";
|
|
7
8
|
import { theme } from "../theme/theme.js";
|
|
8
9
|
import { DynamicBorder } from "./dynamic-border.js";
|
|
9
10
|
import { keyHint, keyText } from "./keybinding-hints.js";
|
|
@@ -466,10 +467,29 @@ class SessionList {
|
|
|
466
467
|
}
|
|
467
468
|
}
|
|
468
469
|
}
|
|
470
|
+
function withMediaOutcome(result, mediaError) {
|
|
471
|
+
return mediaError === undefined ? result : { ...result, mediaError };
|
|
472
|
+
}
|
|
469
473
|
/**
|
|
470
|
-
* Delete a session file, trying the `trash` CLI first, then falling back to unlink
|
|
474
|
+
* Delete a session file, trying the `trash` CLI first, then falling back to unlink. The session's
|
|
475
|
+
* tool images go with it; when they cannot be removed the session is still deleted (its id is gone
|
|
476
|
+
* with the file) and `mediaError` names the leftover directory so the caller can say so.
|
|
471
477
|
*/
|
|
472
|
-
async function deleteSessionFile(sessionPath) {
|
|
478
|
+
export async function deleteSessionFile(sessionPath) {
|
|
479
|
+
// The header is read before the file goes: the media directory is named after the session id.
|
|
480
|
+
const mediaScope = await toolMediaScopeOfSessionFile(sessionPath);
|
|
481
|
+
const removeMedia = () => {
|
|
482
|
+
if (mediaScope === undefined)
|
|
483
|
+
return undefined;
|
|
484
|
+
try {
|
|
485
|
+
removeToolMedia(mediaScope);
|
|
486
|
+
return undefined;
|
|
487
|
+
}
|
|
488
|
+
catch (cause) {
|
|
489
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
490
|
+
return `${toolMediaRoot(mediaScope)}: ${reason}`;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
473
493
|
// Try `trash` first (if installed)
|
|
474
494
|
const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
|
|
475
495
|
const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
|
|
@@ -488,12 +508,12 @@ async function deleteSessionFile(sessionPath) {
|
|
|
488
508
|
};
|
|
489
509
|
// If trash reports success, or the file is gone afterwards, treat it as successful
|
|
490
510
|
if (trashResult.status === 0 || !existsSync(sessionPath)) {
|
|
491
|
-
return { ok: true, method: "trash" };
|
|
511
|
+
return withMediaOutcome({ ok: true, method: "trash" }, removeMedia());
|
|
492
512
|
}
|
|
493
513
|
// Fallback to permanent deletion
|
|
494
514
|
try {
|
|
495
515
|
await unlink(sessionPath);
|
|
496
|
-
return { ok: true, method: "unlink" };
|
|
516
|
+
return withMediaOutcome({ ok: true, method: "unlink" }, removeMedia());
|
|
497
517
|
}
|
|
498
518
|
catch (err) {
|
|
499
519
|
const unlinkError = err instanceof Error ? err.message : String(err);
|
|
@@ -628,7 +648,12 @@ export class SessionSelectorComponent extends Container {
|
|
|
628
648
|
const showCwd = this.scope === "all";
|
|
629
649
|
this.sessionList.setSessions(sessions, showCwd);
|
|
630
650
|
const msg = result.method === "trash" ? "Session moved to trash" : "Session deleted";
|
|
631
|
-
|
|
651
|
+
if (result.mediaError) {
|
|
652
|
+
this.header.setStatusMessage({ type: "error", message: `${msg}, but its images were not removed: ${result.mediaError}` }, 8000);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
this.header.setStatusMessage({ type: "info", message: msg }, 2000);
|
|
656
|
+
}
|
|
632
657
|
await this.refreshSessionsAfterMutation();
|
|
633
658
|
}
|
|
634
659
|
else {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as crypto from "node:crypto";
|
|
18
18
|
import { basename, dirname, extname } from "node:path";
|
|
19
|
+
import { modelSupportsAssistantPrefill } from "@earendil-works/pi-ai";
|
|
19
20
|
import { VERSION } from "../../config.js";
|
|
20
21
|
import { authMethodStatus, buildLoginProviderInfos } from "../../core/auth-providers.js";
|
|
22
|
+
import { ContinueFromLeafError } from "../../core/continue-from-leaf.js";
|
|
21
23
|
import { getCredentialAccounts, pinCredentialAccount, removeCredentialAccount, } from "../../core/credential-accounts.js";
|
|
22
24
|
import { AssistantEditError, SessionStreamingError } from "../../core/edited-assistant-message.js";
|
|
23
25
|
import { UserEditError } from "../../core/edited-user-message.js";
|
|
@@ -32,7 +34,7 @@ import { ClientAdmissions } from "./client-admissions.js";
|
|
|
32
34
|
import { handleClientInput } from "./client-input-handler.js";
|
|
33
35
|
import { ClientMessageEvents } from "./client-message-events.js";
|
|
34
36
|
import { ConnectionQuestionBridge, degradeQuestion, sessionQuestionBridges } from "./connection-question-bridge.js";
|
|
35
|
-
import { AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, DEFAULT_CUSTOM_EXTENSION_LABEL, EXTENSION_EVENTS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, } from "./custom-capability.js";
|
|
37
|
+
import { AUTO_TITLE_SESSIONS_CAPABILITY, buildCustomUnsupportedRequest, CONTINUE_FROM_LEAF_CAPABILITY, DEFAULT_CUSTOM_EXTENSION_LABEL, EXTENSION_EVENTS_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, QUESTION_CAPABILITY, } from "./custom-capability.js";
|
|
36
38
|
import { createRpcEventOutputBuffer } from "./event-output-buffer.js";
|
|
37
39
|
import { settleExtensionUiResponse } from "./extension-ui-response.js";
|
|
38
40
|
import { HostSessionControl } from "./host-session-control.js";
|
|
@@ -725,6 +727,7 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
725
727
|
AUTO_TITLE_SESSIONS_CAPABILITY,
|
|
726
728
|
MEDIA_PLACEHOLDERS_CAPABILITY,
|
|
727
729
|
DURABLE_CLIENT_MESSAGE_ID_CAPABILITY,
|
|
730
|
+
CONTINUE_FROM_LEAF_CAPABILITY,
|
|
728
731
|
...(options.capabilities ?? []),
|
|
729
732
|
]),
|
|
730
733
|
],
|
|
@@ -756,6 +759,17 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
756
759
|
session.appendSessionEntry(command.entry);
|
|
757
760
|
return success(id, "append_session_entry");
|
|
758
761
|
}
|
|
762
|
+
case "continue_from_leaf": {
|
|
763
|
+
try {
|
|
764
|
+
await session.continueFromLeaf();
|
|
765
|
+
return success(id, "continue_from_leaf");
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
if (err instanceof ContinueFromLeafError)
|
|
769
|
+
return error(id, command.type, err.message, err.code);
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
759
773
|
case "send_custom_message": {
|
|
760
774
|
await session.sendCustomMessage({
|
|
761
775
|
customType: command.customType,
|
|
@@ -865,6 +879,9 @@ export function createRpcConnectionHandler(runtimeHost, sink, options = {}) {
|
|
|
865
879
|
models: models.map((model) => ({
|
|
866
880
|
...model,
|
|
867
881
|
supportedThinkingLevels: getSupportedThinkingLevels(model),
|
|
882
|
+
supportsAssistantPrefill: modelSupportsAssistantPrefill(model, {
|
|
883
|
+
thinkingEnabled: session.thinkingLevel !== "off",
|
|
884
|
+
}),
|
|
868
885
|
})),
|
|
869
886
|
});
|
|
870
887
|
}
|
|
@@ -18,6 +18,8 @@ import type { RpcExtensionUIRequest } from "./rpc-types.ts";
|
|
|
18
18
|
export declare const CUSTOM_UNSUPPORTED_CAPABILITY = "custom_unsupported";
|
|
19
19
|
export declare const EXTENSION_EVENTS_CAPABILITY = "extension_events";
|
|
20
20
|
export declare const AUTO_TITLE_SESSIONS_CAPABILITY = "auto_title_sessions";
|
|
21
|
+
/** Host capability: the `continue_from_leaf` command starts a turn with no new prompt (#1930). */
|
|
22
|
+
export declare const CONTINUE_FROM_LEAF_CAPABILITY = "continue_from_leaf";
|
|
21
23
|
/**
|
|
22
24
|
* Opt-in: the host replaces inline image bytes inside tool results with `image_ref`
|
|
23
25
|
* placeholders for this connection; the client fetches a block on demand with `get_media`.
|
|
@@ -65,6 +67,13 @@ export declare const DURABLE_SESSION_ID_CAPABILITY = "durable_session_id";
|
|
|
65
67
|
* `SENPI_PROMPT_SURFACE`. A later open that names another surface rebuilds that session's prompt.
|
|
66
68
|
*/
|
|
67
69
|
export declare const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
|
|
70
|
+
/**
|
|
71
|
+
* HOST capability: this host honors `open_session.browserEngine` (`connected` | `builtin` | `none`). The
|
|
72
|
+
* choice is per session, never per process: that session's tool subprocesses and eval kernel see
|
|
73
|
+
* `OMO_BROWSER_ENGINE=<value>` and no other session on the host does. Advertised only because the
|
|
74
|
+
* value reaches every consumer, so a client may rely on it the moment it sees this name.
|
|
75
|
+
*/
|
|
76
|
+
export declare const BROWSER_ENGINE_CAPABILITY = "browser_engine";
|
|
68
77
|
/**
|
|
69
78
|
* HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
|
|
70
79
|
* no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
|
|
@@ -18,6 +18,8 @@ import * as crypto from "node:crypto";
|
|
|
18
18
|
export const CUSTOM_UNSUPPORTED_CAPABILITY = "custom_unsupported";
|
|
19
19
|
export const EXTENSION_EVENTS_CAPABILITY = "extension_events";
|
|
20
20
|
export const AUTO_TITLE_SESSIONS_CAPABILITY = "auto_title_sessions";
|
|
21
|
+
/** Host capability: the `continue_from_leaf` command starts a turn with no new prompt (#1930). */
|
|
22
|
+
export const CONTINUE_FROM_LEAF_CAPABILITY = "continue_from_leaf";
|
|
21
23
|
/**
|
|
22
24
|
* Opt-in: the host replaces inline image bytes inside tool results with `image_ref`
|
|
23
25
|
* placeholders for this connection; the client fetches a block on demand with `get_media`.
|
|
@@ -65,6 +67,13 @@ export const DURABLE_SESSION_ID_CAPABILITY = "durable_session_id";
|
|
|
65
67
|
* `SENPI_PROMPT_SURFACE`. A later open that names another surface rebuilds that session's prompt.
|
|
66
68
|
*/
|
|
67
69
|
export const PROMPT_SURFACE_CAPABILITY = "prompt_surface";
|
|
70
|
+
/**
|
|
71
|
+
* HOST capability: this host honors `open_session.browserEngine` (`connected` | `builtin` | `none`). The
|
|
72
|
+
* choice is per session, never per process: that session's tool subprocesses and eval kernel see
|
|
73
|
+
* `OMO_BROWSER_ENGINE=<value>` and no other session on the host does. Advertised only because the
|
|
74
|
+
* value reaches every consumer, so a client may rely on it the moment it sees this name.
|
|
75
|
+
*/
|
|
76
|
+
export const BROWSER_ENGINE_CAPABILITY = "browser_engine";
|
|
68
77
|
/**
|
|
69
78
|
* HOST capability: `open_session.promptSurface` also accepts `chat` (a chat bridge: no routing line,
|
|
70
79
|
* no handoff block, no todo cues). A host without it refuses `chat` with `invalid_launch_profile`,
|
|
@@ -20,6 +20,7 @@ const ALLOWED_ENV_NAMES = [
|
|
|
20
20
|
/^(PATH|HOME|USER|LOGNAME|SHELL|TMPDIR|TERM|LANG)$/u,
|
|
21
21
|
/^(LC|XDG)_/u,
|
|
22
22
|
/^(SENPI|OMO|PI)_/u,
|
|
23
|
+
/^BSK_(HOME|BIN)$/u,
|
|
23
24
|
/^(HTTPS?_PROXY|NO_PROXY)$/u,
|
|
24
25
|
/^[A-Z0-9_]+_API_KEY$/u,
|
|
25
26
|
/^(ANTHROPIC|OPENAI|GOOGLE|GEMINI|AZURE|AWS|OPENROUTER|XAI|MISTRAL|DEEPSEEK|GROQ|CEREBRAS|MINIMAX)_/u,
|
|
@@ -61,6 +62,7 @@ const TRANSIENT_ENV_NAMES = new Set([
|
|
|
61
62
|
"PI_PROVIDER",
|
|
62
63
|
"PI_MODEL",
|
|
63
64
|
"PI_REASONING_LEVEL",
|
|
65
|
+
"OMO_BROWSER_ENGINE",
|
|
64
66
|
"PI_PROMPT_CACHE_SAFE_WAIT_SECONDS",
|
|
65
67
|
"SENPI_PY_KERNEL_PARENT_PID",
|
|
66
68
|
"SENPI_RPC_HOST_WATCH_FD",
|
|
@@ -22,6 +22,25 @@
|
|
|
22
22
|
* User-authored images (`prompt.images`) stay intact: they are bounded by the client
|
|
23
23
|
* that sent them; the fan-out hazard is tool output.
|
|
24
24
|
*/
|
|
25
|
+
/** Why a host could not keep a tool image on disk for the client to read. */
|
|
26
|
+
export type MediaUnavailableReason = "image_too_large" | "session_limit" | "storage_error";
|
|
27
|
+
/** Where a persisted image lives (absolute), or why there is no file. */
|
|
28
|
+
export type PersistedMedia = {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
} | {
|
|
31
|
+
readonly unavailableReason: MediaUnavailableReason;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Writes one image's bytes to the session's media directory and reports the outcome. Returns
|
|
35
|
+
* `undefined` for a session that has nowhere durable to keep them: its placeholder carries neither field.
|
|
36
|
+
*/
|
|
37
|
+
export type MediaPersister = (ref: {
|
|
38
|
+
readonly toolCallId: string;
|
|
39
|
+
readonly contentIndex: number;
|
|
40
|
+
}, block: {
|
|
41
|
+
readonly data: string;
|
|
42
|
+
readonly mimeType?: string;
|
|
43
|
+
}) => PersistedMedia | undefined;
|
|
25
44
|
/** The block a capable client receives in place of inline image bytes. */
|
|
26
45
|
export interface ImageRefBlock {
|
|
27
46
|
readonly type: "image_ref";
|
|
@@ -31,6 +50,9 @@ export interface ImageRefBlock {
|
|
|
31
50
|
readonly toolCallId: string;
|
|
32
51
|
readonly contentIndex: number;
|
|
33
52
|
};
|
|
53
|
+
/** Absolute path of the stored bytes; written before this placeholder was emitted. */
|
|
54
|
+
readonly path?: string;
|
|
55
|
+
readonly unavailableReason?: MediaUnavailableReason;
|
|
34
56
|
}
|
|
35
57
|
/**
|
|
36
58
|
* Decoded byte length of a base64 string, derived from its length alone.
|
|
@@ -45,5 +67,5 @@ export declare function base64ByteLength(data: string): number;
|
|
|
45
67
|
* Returns the SAME reference when nothing changed, so the caller can skip the second
|
|
46
68
|
* serialization entirely.
|
|
47
69
|
*/
|
|
48
|
-
export declare function omitInlineMedia<T extends object>(record: T): T;
|
|
70
|
+
export declare function omitInlineMedia<T extends object>(record: T, persist?: MediaPersister): T;
|
|
49
71
|
//# sourceMappingURL=media-placeholders.d.ts.map
|
|
@@ -56,29 +56,32 @@ function isInlineImage(value) {
|
|
|
56
56
|
return isPlainObject(value) && value.type === "image" && typeof value.data === "string";
|
|
57
57
|
}
|
|
58
58
|
/** Replace inline images in one tool-result content array; same reference when none. */
|
|
59
|
-
function omitContentImages(content, toolCallId) {
|
|
59
|
+
function omitContentImages(content, toolCallId, persist) {
|
|
60
60
|
let replaced;
|
|
61
61
|
for (let index = 0; index < content.length; index++) {
|
|
62
62
|
const block = content[index];
|
|
63
63
|
if (!isInlineImage(block))
|
|
64
64
|
continue;
|
|
65
65
|
replaced ??= [...content];
|
|
66
|
+
const ref = { toolCallId, contentIndex: index };
|
|
67
|
+
// The bytes reach disk before the placeholder that names them leaves this function.
|
|
66
68
|
replaced[index] = {
|
|
67
69
|
type: "image_ref",
|
|
68
70
|
mimeType: typeof block.mimeType === "string" ? block.mimeType : "application/octet-stream",
|
|
69
71
|
byteLength: base64ByteLength(block.data),
|
|
70
|
-
ref
|
|
72
|
+
ref,
|
|
73
|
+
...persist?.(ref, block),
|
|
71
74
|
};
|
|
72
75
|
}
|
|
73
76
|
return replaced ?? content;
|
|
74
77
|
}
|
|
75
78
|
/** A toolResult message whose content array carries images, rebuilt without them. */
|
|
76
|
-
function omitToolResultImages(message) {
|
|
79
|
+
function omitToolResultImages(message, persist) {
|
|
77
80
|
const content = message.content;
|
|
78
81
|
const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : "";
|
|
79
82
|
if (!Array.isArray(content))
|
|
80
83
|
return message;
|
|
81
|
-
const omitted = omitContentImages(content, toolCallId);
|
|
84
|
+
const omitted = omitContentImages(content, toolCallId, persist);
|
|
82
85
|
return omitted === content ? message : { ...message, content: omitted };
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
@@ -86,11 +89,11 @@ function omitToolResultImages(message) {
|
|
|
86
89
|
* scrubbed, wherever it sits (message arrays, `turn_end.toolResults`, session entries,
|
|
87
90
|
* tree nodes, `state.entries`). Unchanged sub-trees are returned by reference.
|
|
88
91
|
*/
|
|
89
|
-
function walk(value) {
|
|
92
|
+
function walk(value, persist) {
|
|
90
93
|
if (Array.isArray(value)) {
|
|
91
94
|
let replaced;
|
|
92
95
|
for (let index = 0; index < value.length; index++) {
|
|
93
|
-
const next = walk(value[index]);
|
|
96
|
+
const next = walk(value[index], persist);
|
|
94
97
|
if (next === value[index])
|
|
95
98
|
continue;
|
|
96
99
|
replaced ??= [...value];
|
|
@@ -100,14 +103,14 @@ function walk(value) {
|
|
|
100
103
|
}
|
|
101
104
|
if (!isPlainObject(value))
|
|
102
105
|
return value;
|
|
103
|
-
const scrubbed = value.role === "toolResult" ? omitToolResultImages(value) : value;
|
|
106
|
+
const scrubbed = value.role === "toolResult" ? omitToolResultImages(value, persist) : value;
|
|
104
107
|
let replaced = scrubbed === value ? undefined : { ...scrubbed };
|
|
105
108
|
for (const [key, child] of Object.entries(scrubbed)) {
|
|
106
109
|
if (key === "content" && scrubbed !== value)
|
|
107
110
|
continue;
|
|
108
111
|
if (!isPlainObject(child) && !Array.isArray(child))
|
|
109
112
|
continue;
|
|
110
|
-
const next = walk(child);
|
|
113
|
+
const next = walk(child, persist);
|
|
111
114
|
if (next === child)
|
|
112
115
|
continue;
|
|
113
116
|
replaced ??= { ...scrubbed };
|
|
@@ -116,12 +119,12 @@ function walk(value) {
|
|
|
116
119
|
return replaced ?? value;
|
|
117
120
|
}
|
|
118
121
|
/** `tool_execution_end.result.content` is not a toolResult message; scrub it explicitly. */
|
|
119
|
-
function omitToolExecutionEndImages(record) {
|
|
122
|
+
function omitToolExecutionEndImages(record, persist) {
|
|
120
123
|
const result = record.result;
|
|
121
124
|
if (!isPlainObject(result) || !Array.isArray(result.content))
|
|
122
125
|
return record;
|
|
123
126
|
const toolCallId = typeof record.toolCallId === "string" ? record.toolCallId : "";
|
|
124
|
-
const omitted = omitContentImages(result.content, toolCallId);
|
|
127
|
+
const omitted = omitContentImages(result.content, toolCallId, persist);
|
|
125
128
|
return omitted === result.content ? record : { ...record, result: { ...result, content: omitted } };
|
|
126
129
|
}
|
|
127
130
|
/** True when this record type can carry a tool result at all (cheap gate). */
|
|
@@ -140,12 +143,12 @@ function carriesMedia(record) {
|
|
|
140
143
|
* Returns the SAME reference when nothing changed, so the caller can skip the second
|
|
141
144
|
* serialization entirely.
|
|
142
145
|
*/
|
|
143
|
-
export function omitInlineMedia(record) {
|
|
146
|
+
export function omitInlineMedia(record, persist) {
|
|
144
147
|
const typed = record;
|
|
145
148
|
if (!carriesMedia(typed))
|
|
146
149
|
return record;
|
|
147
|
-
const scrubbed = typed.type === "tool_execution_end" ? omitToolExecutionEndImages(typed) : typed;
|
|
148
|
-
const walked = walk(scrubbed);
|
|
150
|
+
const scrubbed = typed.type === "tool_execution_end" ? omitToolExecutionEndImages(typed, persist) : typed;
|
|
151
|
+
const walked = walk(scrubbed, persist);
|
|
149
152
|
return (walked === typed ? record : walked);
|
|
150
153
|
}
|
|
151
154
|
//# sourceMappingURL=media-placeholders.js.map
|