@oh-my-pi/pi-coding-agent 16.1.17 → 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 +28 -0
- package/dist/cli.js +17872 -17571
- package/dist/types/cli/usage-cli.d.ts +14 -0
- package/dist/types/commands/token.d.ts +9 -0
- 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 +10 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +43 -27
- 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/components/extensions/extension-dashboard.d.ts +26 -10
- package/dist/types/modes/components/extensions/extension-list.d.ts +13 -0
- 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 +29 -16
- package/scripts/generate-legacy-pi-bundled-registry.ts +404 -0
- package/src/cli/usage-cli.ts +35 -4
- package/src/commands/token.ts +54 -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 +3352 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +264 -131
- 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/components/extensions/extension-dashboard.ts +221 -165
- package/src/modes/components/extensions/extension-list.ts +66 -33
- package/src/modes/controllers/input-controller.ts +0 -71
- package/src/modes/controllers/selector-controller.ts +4 -0
- 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
|
@@ -40,6 +40,9 @@ export class ExtensionList implements Component {
|
|
|
40
40
|
#focused = false;
|
|
41
41
|
#masterSwitchProvider: string | null = null;
|
|
42
42
|
#maxVisible: number;
|
|
43
|
+
#hoveredIndex: number | null = null;
|
|
44
|
+
/** Item rows rendered in the last frame, for mouse hit-testing. */
|
|
45
|
+
#visibleCount = 0;
|
|
43
46
|
|
|
44
47
|
constructor(
|
|
45
48
|
private extensions: Extension[],
|
|
@@ -108,6 +111,7 @@ export class ExtensionList implements Component {
|
|
|
108
111
|
|
|
109
112
|
render(width: number): readonly string[] {
|
|
110
113
|
const lines: string[] = [];
|
|
114
|
+
this.#visibleCount = 0;
|
|
111
115
|
|
|
112
116
|
// Search bar
|
|
113
117
|
const searchPrefix = theme.fg("muted", "Search: ");
|
|
@@ -136,15 +140,20 @@ export class ExtensionList implements Component {
|
|
|
136
140
|
for (let i = startIdx; i < endIdx; i++) {
|
|
137
141
|
const listItem = this.#listItems[i];
|
|
138
142
|
const isSelected = this.#focused && i === this.#selectedIndex;
|
|
143
|
+
const isHovered = this.#focused && i === this.#hoveredIndex && !isSelected;
|
|
139
144
|
|
|
145
|
+
let rowStr: string;
|
|
140
146
|
if (listItem.type === "master") {
|
|
141
|
-
|
|
147
|
+
rowStr = this.#renderMasterSwitch(listItem, isSelected, rowWidth);
|
|
142
148
|
} else if (listItem.type === "kind-header") {
|
|
143
|
-
|
|
149
|
+
rowStr = this.#renderKindHeader(listItem, isSelected, rowWidth);
|
|
144
150
|
} else {
|
|
145
|
-
|
|
151
|
+
rowStr = this.#renderExtensionRow(listItem.item, isSelected, rowWidth, masterDisabled);
|
|
146
152
|
}
|
|
153
|
+
if (isHovered) rowStr = theme.bg("selectedBg", rowStr);
|
|
154
|
+
rows.push(rowStr);
|
|
147
155
|
}
|
|
156
|
+
this.#visibleCount = rows.length;
|
|
148
157
|
|
|
149
158
|
lines.push(
|
|
150
159
|
...renderScrollableList(rows, {
|
|
@@ -387,6 +396,57 @@ export class ExtensionList implements Component {
|
|
|
387
396
|
this.#scrollOffset = next.scrollOffset;
|
|
388
397
|
}
|
|
389
398
|
|
|
399
|
+
/** Toggle the selected item, or flip the provider master switch when on it. */
|
|
400
|
+
#activateSelected(): void {
|
|
401
|
+
const item = this.#listItems[this.#selectedIndex];
|
|
402
|
+
if (item?.type === "master") {
|
|
403
|
+
this.callbacks.onMasterToggle?.(item.providerId);
|
|
404
|
+
} else if (item?.type === "extension") {
|
|
405
|
+
// Only allow toggling if the provider master switch is enabled.
|
|
406
|
+
const masterDisabled = this.#masterSwitchProvider !== null && !isProviderEnabled(this.#masterSwitchProvider);
|
|
407
|
+
if (!masterDisabled) {
|
|
408
|
+
const newEnabled = item.item.state === "disabled";
|
|
409
|
+
this.callbacks.onToggle?.(item.item.id, newEnabled);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Highlight the row under the pointer (null clears). */
|
|
415
|
+
setHoverIndex(index: number | null): void {
|
|
416
|
+
this.#hoveredIndex = index;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Map a 0-based line within this component's render to the absolute list-item
|
|
421
|
+
* index, or null when the line is the search banner, a padding row, or outside
|
|
422
|
+
* the visible window. The first two lines are the search banner and a blank
|
|
423
|
+
* separator; item rows follow, windowed at the current scroll offset.
|
|
424
|
+
*/
|
|
425
|
+
hitTest(line: number): number | null {
|
|
426
|
+
const rowLine = line - 2;
|
|
427
|
+
if (rowLine < 0 || rowLine >= this.#visibleCount) return null;
|
|
428
|
+
const index = this.#scrollOffset + rowLine;
|
|
429
|
+
return index < this.#listItems.length ? index : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** Wheel notch: move the selection (and the inspector) one row. */
|
|
433
|
+
handleWheel(delta: -1 | 1): void {
|
|
434
|
+
if (delta < 0) this.#moveSelectionUp();
|
|
435
|
+
else this.#moveSelectionDown();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Click: select the row under the pointer, or activate it when already selected. */
|
|
439
|
+
handleClick(line: number): void {
|
|
440
|
+
const index = this.hitTest(line);
|
|
441
|
+
if (index === null) return;
|
|
442
|
+
if (index === this.#selectedIndex) {
|
|
443
|
+
this.#activateSelected();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
this.#selectedIndex = index;
|
|
447
|
+
this.#notifySelectionChange();
|
|
448
|
+
}
|
|
449
|
+
|
|
390
450
|
handleInput(data: string): void {
|
|
391
451
|
// Navigation
|
|
392
452
|
if (matchesSelectUp(data) || data === "k") {
|
|
@@ -399,36 +459,9 @@ export class ExtensionList implements Component {
|
|
|
399
459
|
return;
|
|
400
460
|
}
|
|
401
461
|
|
|
402
|
-
// Space:
|
|
403
|
-
if (data === " ") {
|
|
404
|
-
|
|
405
|
-
if (item?.type === "master") {
|
|
406
|
-
this.callbacks.onMasterToggle?.(item.providerId);
|
|
407
|
-
} else if (item?.type === "extension") {
|
|
408
|
-
// Only allow toggling if master is enabled
|
|
409
|
-
const masterDisabled =
|
|
410
|
-
this.#masterSwitchProvider !== null && !isProviderEnabled(this.#masterSwitchProvider);
|
|
411
|
-
if (!masterDisabled) {
|
|
412
|
-
const newEnabled = item.item.state === "disabled";
|
|
413
|
-
this.callbacks.onToggle?.(item.item.id, newEnabled);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Enter: Same as space - toggle selected item
|
|
420
|
-
if (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
421
|
-
const item = this.#listItems[this.#selectedIndex];
|
|
422
|
-
if (item?.type === "master") {
|
|
423
|
-
this.callbacks.onMasterToggle?.(item.providerId);
|
|
424
|
-
} else if (item?.type === "extension") {
|
|
425
|
-
const masterDisabled =
|
|
426
|
-
this.#masterSwitchProvider !== null && !isProviderEnabled(this.#masterSwitchProvider);
|
|
427
|
-
if (!masterDisabled) {
|
|
428
|
-
const newEnabled = item.item.state === "disabled";
|
|
429
|
-
this.callbacks.onToggle?.(item.item.id, newEnabled);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
462
|
+
// Space or Enter: activate the selected row (toggle item / master switch)
|
|
463
|
+
if (data === " " || matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") {
|
|
464
|
+
this.#activateSelected();
|
|
432
465
|
return;
|
|
433
466
|
}
|
|
434
467
|
|
|
@@ -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
|
|
@@ -232,11 +232,15 @@ export class SelectorController {
|
|
|
232
232
|
*/
|
|
233
233
|
async showExtensionsDashboard(): Promise<void> {
|
|
234
234
|
const dashboard = await ExtensionDashboard.create(getProjectDir(), this.ctx.settings, this.ctx.ui.terminal.rows);
|
|
235
|
+
// Fullscreen dashboard on the alternate screen (the /settings idiom): the
|
|
236
|
+
// overlay borrows the terminal's alt buffer and enables mouse tracking for
|
|
237
|
+
// its lifetime, leaving the transcript untouched underneath.
|
|
235
238
|
const overlay = this.ctx.ui.showOverlay(dashboard, {
|
|
236
239
|
width: "100%",
|
|
237
240
|
maxHeight: "100%",
|
|
238
241
|
anchor: "top-left",
|
|
239
242
|
margin: 0,
|
|
243
|
+
fullscreen: true,
|
|
240
244
|
});
|
|
241
245
|
dashboard.onClose = () => {
|
|
242
246
|
overlay.hide();
|
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}`);
|
package/src/tools/read.ts
CHANGED
|
@@ -2083,7 +2083,24 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2083
2083
|
`Invalid selector ':${internalTarget.sel}' on '${internalTarget.path}'. Use :N, :N-M, :N+K, :N- (open-ended), a comma-separated list of ranges, :raw, or a range combined with raw (e.g. :raw:50-100).`,
|
|
2084
2084
|
);
|
|
2085
2085
|
}
|
|
2086
|
-
|
|
2086
|
+
const urlMeta = parseInternalUrl(internalTarget.path);
|
|
2087
|
+
const scheme = urlMeta.protocol.replace(/:$/, "").toLowerCase();
|
|
2088
|
+
if (scheme === "local") {
|
|
2089
|
+
const localFile = await resolveLocalUrlToFile(urlMeta, {
|
|
2090
|
+
cwd: this.session.cwd,
|
|
2091
|
+
settings: this.session.settings,
|
|
2092
|
+
signal,
|
|
2093
|
+
localProtocolOptions: this.session.localProtocolOptions,
|
|
2094
|
+
skills: this.session.skills,
|
|
2095
|
+
});
|
|
2096
|
+
if (localFile) {
|
|
2097
|
+
readPath = internalTarget.sel === undefined ? localFile.path : `${localFile.path}:${internalTarget.sel}`;
|
|
2098
|
+
} else {
|
|
2099
|
+
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2100
|
+
}
|
|
2101
|
+
} else {
|
|
2102
|
+
return this.#handleInternalUrl(internalTarget.path, parsed, signal);
|
|
2103
|
+
}
|
|
2087
2104
|
}
|
|
2088
2105
|
|
|
2089
2106
|
// One suffix-glob memo per read call — archive, sqlite, and plain-path
|
|
@@ -2393,23 +2410,31 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2393
2410
|
// counts in `truncation` keep reflecting the source, not the trimmed
|
|
2394
2411
|
// view — column truncation surfaces separately via `.limits()`.
|
|
2395
2412
|
const rawSelector = isRawSelector(parsed);
|
|
2396
|
-
// Binary sniff: NUL bytes
|
|
2397
|
-
//
|
|
2398
|
-
//
|
|
2399
|
-
//
|
|
2413
|
+
// Binary sniff: NUL bytes mean the file is not displayable text
|
|
2414
|
+
// (binary, or UTF-16 which has NULs in the ASCII range) — emit a
|
|
2415
|
+
// notice instead of mojibake filling the line budget. `:raw`
|
|
2416
|
+
// stays an explicit escape hatch.
|
|
2417
|
+
//
|
|
2418
|
+
// `collectedLines` covers the common case where at least one
|
|
2419
|
+
// physical line terminates within the byte budget. Binary blobs
|
|
2420
|
+
// without newlines (videos, archives, packed JSON) leave it
|
|
2421
|
+
// empty; their bytes only land in `firstLinePreview`, which the
|
|
2422
|
+
// `firstLineExceedsLimit` branch below would otherwise emit
|
|
2423
|
+
// verbatim. Sniffing the preview here keeps the refusal uniform.
|
|
2400
2424
|
if (!rawSelector) {
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
)
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2425
|
+
const hasNul = (text: string): boolean => text.includes("\u0000");
|
|
2426
|
+
const binaryDetected =
|
|
2427
|
+
collectedLines.some(hasNul) || (firstLinePreview !== undefined && hasNul(firstLinePreview.text));
|
|
2428
|
+
if (binaryDetected) {
|
|
2429
|
+
return toolResult<ReadToolDetails>({ resolvedPath: absolutePath, suffixResolution })
|
|
2430
|
+
.text(
|
|
2431
|
+
prependSuffixResolutionNotice(
|
|
2432
|
+
`[Cannot read binary file '${formatPathRelativeToCwd(absolutePath, this.session.cwd)}' (${formatBytes(fileSize)}); content contains NUL bytes (binary or UTF-16 encoded)]`,
|
|
2433
|
+
suffixResolution,
|
|
2434
|
+
),
|
|
2435
|
+
)
|
|
2436
|
+
.sourcePath(absolutePath)
|
|
2437
|
+
.done();
|
|
2413
2438
|
}
|
|
2414
2439
|
}
|
|
2415
2440
|
const maxColumns = resolveOutputMaxColumns(this.session.settings);
|
|
@@ -2771,6 +2796,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
2771
2796
|
settings: this.session.settings,
|
|
2772
2797
|
signal,
|
|
2773
2798
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
2799
|
+
skills: this.session.skills,
|
|
2774
2800
|
});
|
|
2775
2801
|
const details: ReadToolDetails = { resolvedPath: resource.sourcePath, contentType: resource.contentType };
|
|
2776
2802
|
|
package/src/tools/search.ts
CHANGED
|
@@ -579,6 +579,7 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
579
579
|
signal?: AbortSignal;
|
|
580
580
|
archiveDisplayMap: ReadonlyMap<string, string>;
|
|
581
581
|
localProtocolOptions?: LocalProtocolOptions;
|
|
582
|
+
skills?: ResolveContext["skills"];
|
|
582
583
|
}): Promise<InternalSearchInputResolution> {
|
|
583
584
|
const internalRouter = InternalUrlRouter.instance();
|
|
584
585
|
const paths = opts.resolvedPaths.slice();
|
|
@@ -592,6 +593,7 @@ async function resolveInternalSearchInputs(opts: {
|
|
|
592
593
|
settings: opts.settings,
|
|
593
594
|
signal: opts.signal,
|
|
594
595
|
localProtocolOptions: opts.localProtocolOptions,
|
|
596
|
+
skills: opts.skills,
|
|
595
597
|
};
|
|
596
598
|
|
|
597
599
|
for (let idx = 0; idx < paths.length; idx++) {
|
|
@@ -725,6 +727,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
725
727
|
signal,
|
|
726
728
|
archiveDisplayMap,
|
|
727
729
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
730
|
+
skills: this.session.skills,
|
|
728
731
|
});
|
|
729
732
|
const searchablePaths = internalResolution.paths;
|
|
730
733
|
const { virtualResources, virtualPathSet, virtualInputIndexes } = internalResolution;
|
|
@@ -797,6 +800,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
797
800
|
settings: this.session.settings,
|
|
798
801
|
signal,
|
|
799
802
|
localProtocolOptions: this.session.localProtocolOptions,
|
|
803
|
+
skills: this.session.skills,
|
|
800
804
|
});
|
|
801
805
|
searchPath = scope.searchPath;
|
|
802
806
|
isDirectory = scope.isDirectory;
|