@oh-my-pi/pi-coding-agent 16.1.18 → 16.1.19
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 +9 -0
- package/dist/cli.js +15389 -15103
- package/dist/types/edit/hashline/filesystem.d.ts +1 -18
- package/dist/types/extensibility/plugins/legacy-pi-bundled-keys.d.ts +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-bundled-registry.d.ts +4 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +12 -0
- package/dist/types/internal-urls/skill-protocol.d.ts +2 -2
- package/dist/types/internal-urls/types.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +0 -2
- package/dist/types/modes/controllers/input-controller.d.ts +0 -1
- package/dist/types/tools/path-utils.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/build-binary.ts +20 -0
- package/scripts/generate-legacy-pi-bundled-registry.ts +404 -0
- package/src/edit/hashline/filesystem.ts +14 -0
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +987 -0
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +3330 -18
- package/src/extensibility/plugins/legacy-pi-compat.ts +29 -14
- package/src/internal-urls/local-protocol.ts +116 -9
- package/src/internal-urls/skill-protocol.ts +3 -3
- package/src/internal-urls/types.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +7 -5
- package/src/modes/components/custom-editor.ts +6 -16
- package/src/modes/controllers/input-controller.ts +0 -71
- package/src/session/messages.ts +70 -47
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/read.ts +43 -17
- package/src/tools/search.ts +4 -0
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import * as url from "node:url";
|
|
4
4
|
import { isCompiledBinary } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { BUNDLED_PI_REGISTRY_KEYS } from "./legacy-pi-bundled-keys";
|
|
5
6
|
|
|
6
7
|
const IS_COMPILED_BINARY = isCompiledBinary();
|
|
7
8
|
|
|
@@ -344,20 +345,34 @@ export function __validateLegacyPiPackageRootOverrides(
|
|
|
344
345
|
);
|
|
345
346
|
}
|
|
346
347
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
}
|
|
348
|
+
/**
|
|
349
|
+
* Compute the override map keyed by every canonical specifier the host serves
|
|
350
|
+
* directly: the pi-ai / pi-coding-agent roots (compat shims that re-attach
|
|
351
|
+
* legacy helpers) plus, in compiled-binary mode, every other canonical pi-*
|
|
352
|
+
* package root AND every non-wildcard subpath registered in the bundled
|
|
353
|
+
* registry (see `legacy-pi-bundled-keys.ts`). Subpath coverage is what stops
|
|
354
|
+
* `@(scope)/pi-ai/oauth` and friends from falling through to the extension's
|
|
355
|
+
* own — possibly absent — peer install when bunfs filesystem walks fail
|
|
356
|
+
* (issue #3442 follow-up to #3423). Exported as a test seam so the
|
|
357
|
+
* compiled-binary branch is verifiable from dev tests.
|
|
358
|
+
*/
|
|
359
|
+
export function __buildLegacyPiPackageRootOverrides(isCompiled: boolean): Record<string, string> {
|
|
360
|
+
const candidates: Record<string, string> = {
|
|
361
|
+
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
|
|
362
|
+
[`${CANONICAL_PI_SCOPE}/pi-coding-agent`]: LEGACY_PI_CODING_AGENT_SHIM_PATH,
|
|
363
|
+
};
|
|
364
|
+
if (isCompiled) {
|
|
365
|
+
for (const key of BUNDLED_PI_REGISTRY_KEYS) {
|
|
366
|
+
// Shim-bearing roots above already mapped to their compat surface;
|
|
367
|
+
// the bundled typebox shim has a dedicated TYPEBOX_SHIM_PATH route.
|
|
368
|
+
if (key in candidates || key === TYPEBOX_BUNDLED_REGISTRY_KEY) continue;
|
|
369
|
+
candidates[key] = bundledRegistryVirtualSpecifier(key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return __validateLegacyPiPackageRootOverrides(candidates);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES = __buildLegacyPiPackageRootOverrides(IS_COMPILED_BINARY);
|
|
361
376
|
|
|
362
377
|
let isLegacyPiSpecifierShimInstalled = false;
|
|
363
378
|
|
|
@@ -49,6 +49,121 @@ function getContentType(filePath: string): InternalResource["contentType"] {
|
|
|
49
49
|
return "text/plain";
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
const LOCAL_TEXT_SNIFF_BYTES = 8 * 1024;
|
|
53
|
+
const LOCAL_TEXT_RESOURCE_MAX_BYTES = 1024 * 1024;
|
|
54
|
+
const BINARY_FILE_EXTENSIONS = new Set([
|
|
55
|
+
".7z",
|
|
56
|
+
".avi",
|
|
57
|
+
".bmp",
|
|
58
|
+
".bz2",
|
|
59
|
+
".db",
|
|
60
|
+
".doc",
|
|
61
|
+
".docx",
|
|
62
|
+
".gif",
|
|
63
|
+
".gz",
|
|
64
|
+
".ico",
|
|
65
|
+
".jpeg",
|
|
66
|
+
".jpg",
|
|
67
|
+
".m4v",
|
|
68
|
+
".mkv",
|
|
69
|
+
".mov",
|
|
70
|
+
".mp4",
|
|
71
|
+
".pdf",
|
|
72
|
+
".png",
|
|
73
|
+
".ppt",
|
|
74
|
+
".pptx",
|
|
75
|
+
".rar",
|
|
76
|
+
".sqlite",
|
|
77
|
+
".tgz",
|
|
78
|
+
".webm",
|
|
79
|
+
".webp",
|
|
80
|
+
".wmv",
|
|
81
|
+
".xls",
|
|
82
|
+
".xlsx",
|
|
83
|
+
".xz",
|
|
84
|
+
".zip",
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
function formatLocalByteSize(bytes: number): string {
|
|
88
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
89
|
+
const kib = bytes / 1024;
|
|
90
|
+
if (kib < 1024) return `${kib.toFixed(1)} KiB`;
|
|
91
|
+
const mib = kib / 1024;
|
|
92
|
+
if (mib < 1024) return `${mib.toFixed(1)} MiB`;
|
|
93
|
+
return `${(mib / 1024).toFixed(1)} GiB`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildNonTextLocalResource(url: InternalUrl, filePath: string, size: number, reason: string): InternalResource {
|
|
97
|
+
const content = `[Cannot read binary local:// file '${url.href}' (${formatLocalByteSize(size)}): ${reason}. This resource is not text. Use a metadata/key-frame/video-specific workflow instead.]`;
|
|
98
|
+
return {
|
|
99
|
+
url: url.href,
|
|
100
|
+
content,
|
|
101
|
+
contentType: "text/plain",
|
|
102
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
103
|
+
sourcePath: filePath,
|
|
104
|
+
notes: [LOCAL_WRITE_NOTE],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildLargeLocalTextResource(url: InternalUrl, filePath: string, size: number): InternalResource {
|
|
109
|
+
const content = `[Cannot materialize local:// file '${url.href}' as an internal text resource (${formatLocalByteSize(size)} exceeds ${formatLocalByteSize(LOCAL_TEXT_RESOURCE_MAX_BYTES)}). Use the read tool's filesystem path handling or a line selector so content is streamed with file-size safeguards.]`;
|
|
110
|
+
return {
|
|
111
|
+
url: url.href,
|
|
112
|
+
content,
|
|
113
|
+
contentType: "text/plain",
|
|
114
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
115
|
+
sourcePath: filePath,
|
|
116
|
+
notes: [LOCAL_WRITE_NOTE],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readFilePrefix(filePath: string, maxBytes: number): Promise<Uint8Array> {
|
|
121
|
+
if (maxBytes <= 0) return new Uint8Array();
|
|
122
|
+
const handle = await fs.open(filePath, "r");
|
|
123
|
+
try {
|
|
124
|
+
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
125
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
126
|
+
return buffer.subarray(0, bytesRead);
|
|
127
|
+
} finally {
|
|
128
|
+
await handle.close();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isUtf8Text(bytes: Uint8Array): boolean {
|
|
133
|
+
if (bytes.indexOf(0) !== -1) return false;
|
|
134
|
+
try {
|
|
135
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function buildFileResource(
|
|
143
|
+
url: InternalUrl,
|
|
144
|
+
resolved: Extract<ResolvedLocalTarget, { kind: "file" }>,
|
|
145
|
+
): Promise<InternalResource> {
|
|
146
|
+
if (BINARY_FILE_EXTENSIONS.has(path.extname(resolved.path).toLowerCase())) {
|
|
147
|
+
return buildNonTextLocalResource(url, resolved.path, resolved.size, "extension is a known binary/container type");
|
|
148
|
+
}
|
|
149
|
+
const sniffBytes = await readFilePrefix(resolved.path, Math.min(resolved.size, LOCAL_TEXT_SNIFF_BYTES));
|
|
150
|
+
if (!isUtf8Text(sniffBytes)) {
|
|
151
|
+
return buildNonTextLocalResource(url, resolved.path, resolved.size, "content is not valid UTF-8 text");
|
|
152
|
+
}
|
|
153
|
+
if (resolved.size > LOCAL_TEXT_RESOURCE_MAX_BYTES) {
|
|
154
|
+
return buildLargeLocalTextResource(url, resolved.path, resolved.size);
|
|
155
|
+
}
|
|
156
|
+
const content = await Bun.file(resolved.path).text();
|
|
157
|
+
return {
|
|
158
|
+
url: url.href,
|
|
159
|
+
content,
|
|
160
|
+
contentType: getContentType(resolved.path),
|
|
161
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
162
|
+
sourcePath: resolved.path,
|
|
163
|
+
notes: [LOCAL_WRITE_NOTE],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
52
167
|
async function listFilesRecursively(rootPath: string): Promise<string[]> {
|
|
53
168
|
const pending = [""];
|
|
54
169
|
const files: string[] = [];
|
|
@@ -336,15 +451,7 @@ export class LocalProtocolHandler implements ProtocolHandler {
|
|
|
336
451
|
return buildDirectoryResource(url.href, resolved.path, [LOCAL_WRITE_NOTE]);
|
|
337
452
|
}
|
|
338
453
|
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
url: url.href,
|
|
342
|
-
content,
|
|
343
|
-
contentType: getContentType(resolved.path),
|
|
344
|
-
size: Buffer.byteLength(content, "utf-8"),
|
|
345
|
-
sourcePath: resolved.path,
|
|
346
|
-
notes: [LOCAL_WRITE_NOTE],
|
|
347
|
-
};
|
|
454
|
+
return buildFileResource(url, resolved);
|
|
348
455
|
}
|
|
349
456
|
|
|
350
457
|
async complete(_query?: string, context?: ResolveContext): Promise<UrlCompletion[]> {
|
|
@@ -13,7 +13,7 @@ import * as path from "node:path";
|
|
|
13
13
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
14
14
|
import { getActiveSkills } from "../extensibility/skills";
|
|
15
15
|
import { buildDirectoryResource } from "./filesystem-resource";
|
|
16
|
-
import type { InternalResource, InternalUrl, ProtocolHandler, UrlCompletion } from "./types";
|
|
16
|
+
import type { InternalResource, InternalUrl, ProtocolHandler, ResolveContext, UrlCompletion } from "./types";
|
|
17
17
|
|
|
18
18
|
function getContentType(filePath: string): InternalResource["contentType"] {
|
|
19
19
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -42,8 +42,8 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
42
42
|
readonly scheme = "skill";
|
|
43
43
|
readonly immutable = true;
|
|
44
44
|
|
|
45
|
-
async resolve(url: InternalUrl): Promise<InternalResource> {
|
|
46
|
-
const skills = getActiveSkills();
|
|
45
|
+
async resolve(url: InternalUrl, context?: ResolveContext): Promise<InternalResource> {
|
|
46
|
+
const skills = context?.skills ?? getActiveSkills();
|
|
47
47
|
|
|
48
48
|
const skillName = url.rawHost || url.hostname;
|
|
49
49
|
if (!skillName) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* providing access to agent outputs and server resources without exposing filesystem paths.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { Skill } from "../extensibility/skills";
|
|
8
9
|
import type { LocalProtocolOptions } from "./local-protocol";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -90,6 +91,8 @@ export interface ResolveContext {
|
|
|
90
91
|
* [#1608](https://github.com/can1357/oh-my-pi/issues/1608).
|
|
91
92
|
*/
|
|
92
93
|
localProtocolOptions?: LocalProtocolOptions;
|
|
94
|
+
/** Calling session's loaded skills. Prefer this over process-global skill state. */
|
|
95
|
+
skills?: readonly Skill[];
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/**
|
|
@@ -93,15 +93,17 @@ describe("CustomEditor bracketed path paste", () => {
|
|
|
93
93
|
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/report.csv"))).toBeUndefined();
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
it("
|
|
96
|
+
it("inserts non-image path pastes as literal text instead of attaching them", () => {
|
|
97
97
|
const { editor } = makeEditor();
|
|
98
|
-
|
|
99
|
-
editor.
|
|
98
|
+
let imagePathCalls = 0;
|
|
99
|
+
editor.onPasteImagePath = () => {
|
|
100
|
+
imagePathCalls++;
|
|
101
|
+
};
|
|
100
102
|
|
|
101
103
|
editor.handleInput(bracketedPaste("/tmp/report.csv"));
|
|
102
104
|
|
|
103
|
-
expect(
|
|
104
|
-
expect(
|
|
105
|
+
expect(editor.getText()).toBe("/tmp/report.csv");
|
|
106
|
+
expect(imagePathCalls).toBe(0);
|
|
105
107
|
});
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -318,8 +318,6 @@ export class CustomEditor extends Editor {
|
|
|
318
318
|
onPasteImage?: () => Promise<boolean>;
|
|
319
319
|
/** Called when a bracketed paste contains one or more image-file paths. */
|
|
320
320
|
onPasteImagePath?: (path: string) => void | Promise<void>;
|
|
321
|
-
/** Called when a bracketed paste contains one or more non-image file paths. */
|
|
322
|
-
onPasteFilePath?: (path: string) => void | Promise<void>;
|
|
323
321
|
/** Called when the configured raw text-paste shortcut is pressed. */
|
|
324
322
|
onPasteTextRaw?: () => void;
|
|
325
323
|
/** Called when the configured dequeue shortcut is pressed. */
|
|
@@ -505,20 +503,12 @@ export class CustomEditor extends Editor {
|
|
|
505
503
|
return;
|
|
506
504
|
}
|
|
507
505
|
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
);
|
|
513
|
-
|
|
514
|
-
void (async () => {
|
|
515
|
-
for (const path of pastedPaths) {
|
|
516
|
-
if (isImagePath(path)) await this.onPasteImagePath?.(path);
|
|
517
|
-
else await this.onPasteFilePath?.(path);
|
|
518
|
-
}
|
|
519
|
-
})();
|
|
520
|
-
return;
|
|
521
|
-
}
|
|
506
|
+
const pastedImagePaths = extractBracketedImagePastePaths(data);
|
|
507
|
+
if (pastedImagePaths && this.onPasteImagePath) {
|
|
508
|
+
void (async () => {
|
|
509
|
+
for (const path of pastedImagePaths) await this.onPasteImagePath?.(path);
|
|
510
|
+
})();
|
|
511
|
+
return;
|
|
522
512
|
}
|
|
523
513
|
|
|
524
514
|
const parsedKey = parseKey(data);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
5
4
|
import { type AutocompleteProvider, matchesKey, type SlashCommand } from "@oh-my-pi/pi-tui";
|
|
6
5
|
import { $env, isEnoent, logger, sanitizeText } from "@oh-my-pi/pi-utils";
|
|
@@ -20,7 +19,6 @@ import { isTinyTitleLocalModelKey } from "../../tiny/models";
|
|
|
20
19
|
import { isLowSignalTitleInput } from "../../tiny/text";
|
|
21
20
|
import { tinyTitleClient } from "../../tiny/title-client";
|
|
22
21
|
import type { TinyTitleProgressEvent } from "../../tiny/title-protocol";
|
|
23
|
-
import { resolveReadPath } from "../../tools/path-utils";
|
|
24
22
|
import { shortenPath, TRUNCATE_LENGTHS, truncateToWidth } from "../../tools/render-utils";
|
|
25
23
|
import { copyToClipboard, readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
|
|
26
24
|
import { EnhancedPasteController } from "../../utils/enhanced-paste";
|
|
@@ -104,20 +102,6 @@ function wrapPasteInAttachmentBlock(content: string): string {
|
|
|
104
102
|
return `<attachment>\n${content}\n</attachment>`;
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
const FILE_URI_REGEX = /^file:\/\//i;
|
|
108
|
-
|
|
109
|
-
function pastedFileAttachmentExtension(sourcePath: string): string {
|
|
110
|
-
const ext = path.extname(sourcePath);
|
|
111
|
-
const bareExt = ext.slice(1);
|
|
112
|
-
if (!bareExt || bareExt.length > 32 || !/^[a-z0-9][a-z0-9._-]*$/i.test(bareExt)) return "";
|
|
113
|
-
return ext;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolvePastedFilePath(filePath: string, cwd: string): string {
|
|
117
|
-
if (FILE_URI_REGEX.test(filePath)) return fileURLToPath(filePath);
|
|
118
|
-
return resolveReadPath(filePath, cwd);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
105
|
const TINY_TITLE_PROGRESS_DONE_TTL_MS = 3_000;
|
|
122
106
|
// A cached model fires its file-load events in a short burst and then goes silent
|
|
123
107
|
// while onnxruntime builds the session; a genuine download keeps streaming progress
|
|
@@ -392,7 +376,6 @@ export class InputController {
|
|
|
392
376
|
);
|
|
393
377
|
this.ctx.editor.onPasteImage = () => this.handleImagePaste();
|
|
394
378
|
this.ctx.editor.onPasteImagePath = path => this.handleImagePathPaste(path);
|
|
395
|
-
this.ctx.editor.onPasteFilePath = path => this.handleFilePathPaste(path);
|
|
396
379
|
this.ctx.editor.setActionKeys(
|
|
397
380
|
"app.clipboard.pasteTextRaw",
|
|
398
381
|
this.ctx.keybindings.getKeys("app.clipboard.pasteTextRaw"),
|
|
@@ -1261,37 +1244,6 @@ export class InputController {
|
|
|
1261
1244
|
}
|
|
1262
1245
|
}
|
|
1263
1246
|
|
|
1264
|
-
async handleFilePathPaste(filePath: string): Promise<void> {
|
|
1265
|
-
try {
|
|
1266
|
-
const resolvedPath = resolvePastedFilePath(filePath, this.ctx.sessionManager.getCwd());
|
|
1267
|
-
const stat = await Bun.file(resolvedPath).stat();
|
|
1268
|
-
if (!stat.isFile()) {
|
|
1269
|
-
this.ctx.editor.pasteText(filePath);
|
|
1270
|
-
this.ctx.ui.requestRender();
|
|
1271
|
-
this.ctx.showStatus("Pasted path is not a file");
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
const reference = await this.#attachExistingFileAsLocal(resolvedPath);
|
|
1276
|
-
this.ctx.editor.insertText(`${reference} `);
|
|
1277
|
-
this.ctx.ui.requestRender();
|
|
1278
|
-
this.ctx.showStatus(`Attached file as ${reference}`);
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
if (isEnoent(error)) {
|
|
1281
|
-
this.ctx.editor.pasteText(filePath);
|
|
1282
|
-
this.ctx.ui.requestRender();
|
|
1283
|
-
this.ctx.showStatus("Pasted file path was not found");
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
logger.warn("failed to attach pasted file path", {
|
|
1287
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1288
|
-
});
|
|
1289
|
-
this.ctx.editor.pasteText(filePath);
|
|
1290
|
-
this.ctx.ui.requestRender();
|
|
1291
|
-
this.ctx.showError("Failed to attach pasted file path — pasted path inline instead");
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
1247
|
async handleImagePathPaste(path: string): Promise<void> {
|
|
1296
1248
|
try {
|
|
1297
1249
|
const image = await loadImageInput({
|
|
@@ -1462,29 +1414,6 @@ export class InputController {
|
|
|
1462
1414
|
this.ctx.ui.requestRender();
|
|
1463
1415
|
}
|
|
1464
1416
|
|
|
1465
|
-
async #attachExistingFileAsLocal(sourcePath: string): Promise<string> {
|
|
1466
|
-
const localRoot = resolveLocalRoot({
|
|
1467
|
-
getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
|
|
1468
|
-
getSessionId: () => this.ctx.sessionManager.getSessionId(),
|
|
1469
|
-
});
|
|
1470
|
-
await fs.mkdir(localRoot, { recursive: true });
|
|
1471
|
-
const ext = pastedFileAttachmentExtension(sourcePath);
|
|
1472
|
-
let name: string;
|
|
1473
|
-
let filePath: string;
|
|
1474
|
-
do {
|
|
1475
|
-
this.#attachmentCounter++;
|
|
1476
|
-
name = `attachment-${this.#attachmentCounter}${ext}`;
|
|
1477
|
-
filePath = path.join(localRoot, name);
|
|
1478
|
-
} while (await Bun.file(filePath).exists());
|
|
1479
|
-
|
|
1480
|
-
try {
|
|
1481
|
-
await fs.link(sourcePath, filePath);
|
|
1482
|
-
} catch {
|
|
1483
|
-
await fs.copyFile(sourcePath, filePath);
|
|
1484
|
-
}
|
|
1485
|
-
return `local://${name}`;
|
|
1486
|
-
}
|
|
1487
|
-
|
|
1488
1417
|
/**
|
|
1489
1418
|
* Save a large paste to the session's `local://` store and insert a clean `local://attachment-N`
|
|
1490
1419
|
* reference into the editor so the agent can `read` it on demand — instead of inlining the text or
|
package/src/session/messages.ts
CHANGED
|
@@ -482,65 +482,88 @@ export function sanitizeRehydratedOpenAIResponsesAssistantMessage(message: Assis
|
|
|
482
482
|
* - Custom extensions and tools
|
|
483
483
|
*/
|
|
484
484
|
export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
485
|
-
return messages
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
485
|
+
return messages.flatMap((m): Message[] => {
|
|
486
|
+
switch (m.role) {
|
|
487
|
+
case "bashExecution":
|
|
488
|
+
if (m.excludeFromContext) {
|
|
489
|
+
return [];
|
|
490
|
+
}
|
|
491
|
+
return [
|
|
492
|
+
{
|
|
493
493
|
role: "user",
|
|
494
494
|
content: [{ type: "text", text: bashExecutionToText(m) }],
|
|
495
495
|
attribution: "user",
|
|
496
496
|
timestamp: m.timestamp,
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
497
|
+
},
|
|
498
|
+
];
|
|
499
|
+
case "pythonExecution":
|
|
500
|
+
if (m.excludeFromContext) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
return [
|
|
504
|
+
{
|
|
503
505
|
role: "user",
|
|
504
506
|
content: [{ type: "text", text: pythonExecutionToText(m) }],
|
|
505
507
|
attribution: "user",
|
|
506
508
|
timestamp: m.timestamp,
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
509
|
+
},
|
|
510
|
+
];
|
|
511
|
+
case "fileMention": {
|
|
512
|
+
// One `fileMention` can mix `@notes.md` (text) and `@screenshot.png` (image)
|
|
513
|
+
// in the same turn (`generateFileMentionMessages` packs every `@…` into a
|
|
514
|
+
// single message). Splitting by image presence keeps text-only mentions on
|
|
515
|
+
// the higher-priority `developer` slot while routing image attachments
|
|
516
|
+
// through `user`, the only Responses content slot that legitimately accepts
|
|
517
|
+
// `input_image` (Codex chatgpt.com /codex/responses rejects everything else
|
|
518
|
+
// with `Invalid value: 'input_image'`, #3443).
|
|
519
|
+
const wrap = (file: FileMentionMessage["files"][number]): string => {
|
|
520
|
+
const inner = file.content ? `\n${file.content}\n` : "\n";
|
|
521
|
+
return `<file path="${file.path}">${inner}</file>`;
|
|
522
|
+
};
|
|
523
|
+
const textFiles = m.files.filter(file => !file.image);
|
|
524
|
+
const imageFiles = m.files.filter(file => file.image);
|
|
525
|
+
const out: Message[] = [];
|
|
526
|
+
if (textFiles.length > 0) {
|
|
527
|
+
out.push({
|
|
522
528
|
role: "developer",
|
|
529
|
+
content: [{ type: "text" as const, text: textFiles.map(wrap).join("\n") }],
|
|
530
|
+
attribution: "user",
|
|
531
|
+
timestamp: m.timestamp,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
if (imageFiles.length > 0) {
|
|
535
|
+
const content: (TextContent | ImageContent)[] = [
|
|
536
|
+
{ type: "text" as const, text: imageFiles.map(wrap).join("\n") },
|
|
537
|
+
];
|
|
538
|
+
for (const file of imageFiles) {
|
|
539
|
+
if (file.image) content.push(file.image);
|
|
540
|
+
}
|
|
541
|
+
out.push({
|
|
542
|
+
role: "user",
|
|
523
543
|
content,
|
|
524
544
|
attribution: "user",
|
|
525
545
|
timestamp: m.timestamp,
|
|
526
|
-
};
|
|
546
|
+
});
|
|
527
547
|
}
|
|
528
|
-
|
|
529
|
-
case "hookMessage":
|
|
530
|
-
case "branchSummary":
|
|
531
|
-
case "compactionSummary":
|
|
532
|
-
case "user":
|
|
533
|
-
case "developer":
|
|
534
|
-
case "assistant":
|
|
535
|
-
case "toolResult":
|
|
536
|
-
// Core roles share one transformer with agent-core —
|
|
537
|
-
// duplicating them here is how snapcompact frames once
|
|
538
|
-
// silently fell off the provider request.
|
|
539
|
-
return convertMessageToLlm(m);
|
|
540
|
-
default:
|
|
541
|
-
m satisfies never;
|
|
542
|
-
return undefined;
|
|
548
|
+
return out;
|
|
543
549
|
}
|
|
544
|
-
|
|
545
|
-
|
|
550
|
+
case "custom":
|
|
551
|
+
case "hookMessage":
|
|
552
|
+
case "branchSummary":
|
|
553
|
+
case "compactionSummary":
|
|
554
|
+
case "user":
|
|
555
|
+
case "developer":
|
|
556
|
+
case "assistant":
|
|
557
|
+
case "toolResult": {
|
|
558
|
+
// Core roles share one transformer with agent-core —
|
|
559
|
+
// duplicating them here is how snapcompact frames once
|
|
560
|
+
// silently fell off the provider request.
|
|
561
|
+
const converted = convertMessageToLlm(m);
|
|
562
|
+
return converted ? [converted] : [];
|
|
563
|
+
}
|
|
564
|
+
default:
|
|
565
|
+
m satisfies never;
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
});
|
|
546
569
|
}
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -283,6 +283,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
|
|
|
283
283
|
settings: this.session.settings,
|
|
284
284
|
signal,
|
|
285
285
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
286
|
+
skills: this.session.skills,
|
|
286
287
|
});
|
|
287
288
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
288
289
|
|
package/src/tools/ast-grep.ts
CHANGED
|
@@ -185,6 +185,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
|
|
|
185
185
|
settings: this.session.settings,
|
|
186
186
|
signal,
|
|
187
187
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
188
|
+
skills: this.session.skills,
|
|
188
189
|
});
|
|
189
190
|
const { searchPath: resolvedSearchPath, scopePath, isDirectory, multiTargets, globFilter } = scope;
|
|
190
191
|
|
package/src/tools/find.ts
CHANGED
|
@@ -166,6 +166,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
|
|
166
166
|
settings: this.session.settings,
|
|
167
167
|
signal,
|
|
168
168
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
169
|
+
skills: this.session.skills,
|
|
169
170
|
});
|
|
170
171
|
if (!resource.sourcePath) {
|
|
171
172
|
throw new ToolError(`Cannot find internal URL without a backing file: ${rawPattern}`);
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as os from "node:os";
|
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
5
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import type { Skill } from "../extensibility/skills";
|
|
6
7
|
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
7
8
|
import { ToolError } from "./tool-errors";
|
|
8
9
|
|
|
@@ -986,6 +987,8 @@ export interface ToolScopeOptions {
|
|
|
986
987
|
signal?: AbortSignal;
|
|
987
988
|
/** Calling session's `local://` root mapping — pins resolutions to the calling session. */
|
|
988
989
|
localProtocolOptions?: LocalProtocolOptions;
|
|
990
|
+
/** Calling session's loaded skills — lets skill:// resolve without process-global state. */
|
|
991
|
+
skills?: readonly Skill[];
|
|
989
992
|
}
|
|
990
993
|
|
|
991
994
|
export interface ToolScopeResolution {
|
|
@@ -1042,6 +1045,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
1042
1045
|
settings: opts.settings,
|
|
1043
1046
|
signal: opts.signal,
|
|
1044
1047
|
localProtocolOptions: opts.localProtocolOptions,
|
|
1048
|
+
skills: opts.skills,
|
|
1045
1049
|
});
|
|
1046
1050
|
if (!resource.sourcePath) {
|
|
1047
1051
|
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|