@oh-my-pi/pi-coding-agent 15.5.4 → 15.5.7
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 +48 -2
- package/dist/types/config/settings-schema.d.ts +50 -2
- package/dist/types/edit/hashline/diff.d.ts +6 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -2
- package/dist/types/edit/hashline/params.d.ts +4 -5
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +23 -0
- package/dist/types/lib/xai-http.d.ts +40 -0
- package/dist/types/session/agent-session.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +19 -0
- package/dist/types/tools/find.d.ts +7 -0
- package/dist/types/tools/image-gen.d.ts +6 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/plan-mode-guard.d.ts +5 -6
- package/dist/types/tools/tts.d.ts +18 -0
- package/package.json +8 -8
- package/scripts/build-binary.ts +11 -0
- package/src/config/model-registry.ts +41 -9
- package/src/config/settings-schema.ts +43 -2
- package/src/edit/diff.ts +5 -3
- package/src/edit/hashline/diff.ts +11 -4
- package/src/edit/hashline/execute.ts +3 -10
- package/src/edit/hashline/params.ts +10 -3
- package/src/edit/index.ts +9 -12
- package/src/edit/renderer.ts +14 -7
- package/src/edit/streaming.ts +15 -128
- package/src/extensibility/legacy-pi-ai-shim.ts +24 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -3
- package/src/lib/xai-http.ts +124 -0
- package/src/main.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -1
- package/src/prompts/tools/find.md +3 -2
- package/src/sdk.ts +15 -9
- package/src/session/agent-session.ts +48 -5
- package/src/tools/fetch.ts +145 -74
- package/src/tools/find.ts +38 -6
- package/src/tools/image-gen.ts +205 -7
- package/src/tools/index.ts +1 -0
- package/src/tools/plan-mode-guard.ts +14 -6
- package/src/tools/read.ts +57 -3
- package/src/tools/search.ts +2 -2
- package/src/tools/tts.ts +133 -0
|
@@ -2036,6 +2036,15 @@ export const SETTINGS_SCHEMA = {
|
|
|
2036
2036
|
},
|
|
2037
2037
|
},
|
|
2038
2038
|
|
|
2039
|
+
"tts.enabled": {
|
|
2040
|
+
type: "boolean",
|
|
2041
|
+
default: false,
|
|
2042
|
+
ui: {
|
|
2043
|
+
tab: "tools",
|
|
2044
|
+
label: "Text-to-Speech",
|
|
2045
|
+
description: "Enable the tts tool for xAI Grok Voice speech synthesis",
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2039
2048
|
"recipe.enabled": {
|
|
2040
2049
|
type: "boolean",
|
|
2041
2050
|
default: true,
|
|
@@ -2698,7 +2707,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2698
2707
|
},
|
|
2699
2708
|
"providers.image": {
|
|
2700
2709
|
type: "enum",
|
|
2701
|
-
values: ["auto", "openai", "gemini", "openrouter"] as const,
|
|
2710
|
+
values: ["auto", "openai", "antigravity", "xai", "gemini", "openrouter"] as const,
|
|
2702
2711
|
default: "auto",
|
|
2703
2712
|
ui: {
|
|
2704
2713
|
tab: "providers",
|
|
@@ -2708,9 +2717,19 @@ export const SETTINGS_SCHEMA = {
|
|
|
2708
2717
|
{
|
|
2709
2718
|
value: "auto",
|
|
2710
2719
|
label: "Auto",
|
|
2711
|
-
description: "Priority: GPT model image tool > Antigravity > OpenRouter > Gemini",
|
|
2720
|
+
description: "Priority: GPT model image tool > Antigravity > xAI > OpenRouter > Gemini",
|
|
2712
2721
|
},
|
|
2713
2722
|
{ value: "openai", label: "OpenAI", description: "Uses the active GPT Responses/Codex model" },
|
|
2723
|
+
{
|
|
2724
|
+
value: "antigravity",
|
|
2725
|
+
label: "Antigravity",
|
|
2726
|
+
description: "Requires google-antigravity OAuth",
|
|
2727
|
+
},
|
|
2728
|
+
{
|
|
2729
|
+
value: "xai",
|
|
2730
|
+
label: "xAI Grok Imagine",
|
|
2731
|
+
description: "Requires xAI Grok OAuth or XAI_API_KEY",
|
|
2732
|
+
},
|
|
2714
2733
|
{ value: "gemini", label: "Gemini", description: "Requires GEMINI_API_KEY" },
|
|
2715
2734
|
{ value: "openrouter", label: "OpenRouter", description: "Requires OPENROUTER_API_KEY" },
|
|
2716
2735
|
],
|
|
@@ -2748,6 +2767,28 @@ export const SETTINGS_SCHEMA = {
|
|
|
2748
2767
|
},
|
|
2749
2768
|
},
|
|
2750
2769
|
|
|
2770
|
+
"providers.openrouterVariant": {
|
|
2771
|
+
type: "enum",
|
|
2772
|
+
values: ["default", "nitro", "floor", "online", "exacto"] as const,
|
|
2773
|
+
default: "default",
|
|
2774
|
+
ui: {
|
|
2775
|
+
tab: "providers",
|
|
2776
|
+
label: "OpenRouter Routing",
|
|
2777
|
+
description:
|
|
2778
|
+
"Default routing-variant suffix appended to OpenRouter model IDs (overridden when the selector already names a variant)",
|
|
2779
|
+
options: [
|
|
2780
|
+
{ value: "default", label: "Default", description: "No suffix; use OpenRouter's default routing" },
|
|
2781
|
+
{ value: "nitro", label: ":nitro", description: "Prioritize throughput / lowest latency" },
|
|
2782
|
+
{ value: "floor", label: ":floor", description: "Prioritize cheapest available provider" },
|
|
2783
|
+
{ value: "online", label: ":online", description: "Enable OpenRouter's web-search plugin" },
|
|
2784
|
+
{
|
|
2785
|
+
value: "exacto",
|
|
2786
|
+
label: ":exacto",
|
|
2787
|
+
description: "Cherry-picked high-quality providers (only defined for select models)",
|
|
2788
|
+
},
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
},
|
|
2751
2792
|
"providers.parallelFetch": {
|
|
2752
2793
|
type: "boolean",
|
|
2753
2794
|
default: true,
|
package/src/edit/diff.ts
CHANGED
|
@@ -58,7 +58,7 @@ function formatNumberedDiffLine(prefix: "+" | "-" | " ", lineNum: number, conten
|
|
|
58
58
|
* Generate a unified diff string with line numbers and context.
|
|
59
59
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
60
60
|
*/
|
|
61
|
-
export function generateDiffString(oldContent: string, newContent: string, contextLines =
|
|
61
|
+
export function generateDiffString(oldContent: string, newContent: string, contextLines = 2): DiffResult {
|
|
62
62
|
const parts = Diff.diffLines(oldContent, newContent);
|
|
63
63
|
const output: string[] = [];
|
|
64
64
|
|
|
@@ -119,8 +119,9 @@ export function generateDiffString(oldContent: string, newContent: string, conte
|
|
|
119
119
|
linesToShow = raw.slice(0, contextLimit);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// Leading-skip placeholder is omitted: the first emitted line's
|
|
123
|
+
// number already conveys that earlier lines were trimmed.
|
|
122
124
|
if (leadingSkip > 0) {
|
|
123
|
-
output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
|
|
124
125
|
oldLineNum += leadingSkip;
|
|
125
126
|
newLineNum += leadingSkip;
|
|
126
127
|
}
|
|
@@ -143,8 +144,9 @@ export function generateDiffString(oldContent: string, newContent: string, conte
|
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
// Trailing-skip placeholder is omitted for the same reason: the
|
|
148
|
+
// final emitted line's number tells the reader the file continues.
|
|
146
149
|
if (trailingSkip > 0) {
|
|
147
|
-
output.push(formatNumberedDiffLine(" ", oldLineNum, "..."));
|
|
148
150
|
oldLineNum += trailingSkip;
|
|
149
151
|
newLineNum += trailingSkip;
|
|
150
152
|
}
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* and no auto-generated-file refusal — those belong on the write path.
|
|
11
11
|
*/
|
|
12
12
|
import {
|
|
13
|
-
applyEdits,
|
|
14
13
|
computeFileHash,
|
|
15
14
|
Patch as HashlinePatch,
|
|
16
15
|
normalizeToLF,
|
|
@@ -24,6 +23,12 @@ import { readEditFileText } from "../read-file";
|
|
|
24
23
|
|
|
25
24
|
export interface HashlineDiffOptions {
|
|
26
25
|
autoDropPureInsertDuplicates?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Use the streaming-tolerant applier ({@link PatchSection.applyPartialTo})
|
|
28
|
+
* so trailing in-flight ops do not throw or emit phantom edits. Streaming
|
|
29
|
+
* preview path only.
|
|
30
|
+
*/
|
|
31
|
+
streaming?: boolean;
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
async function readSectionText(absolutePath: string, sectionPath: string): Promise<string> {
|
|
@@ -62,7 +67,9 @@ export async function computeHashlineSectionDiff(
|
|
|
62
67
|
const normalized = normalizeToLF(content);
|
|
63
68
|
const hashError = validateSectionHash(section, normalized);
|
|
64
69
|
if (hashError) return { error: hashError };
|
|
65
|
-
const result =
|
|
70
|
+
const result = options.streaming
|
|
71
|
+
? section.applyPartialTo(normalized, options)
|
|
72
|
+
: section.applyTo(normalized, options);
|
|
66
73
|
if (normalized === result.text) return { error: `No changes would be made to ${section.path}.` };
|
|
67
74
|
return generateDiffString(normalized, result.text);
|
|
68
75
|
} catch (err) {
|
|
@@ -71,13 +78,13 @@ export async function computeHashlineSectionDiff(
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
export async function computeHashlineDiff(
|
|
74
|
-
input: { input: string
|
|
81
|
+
input: { input: string },
|
|
75
82
|
cwd: string,
|
|
76
83
|
options: HashlineDiffOptions = {},
|
|
77
84
|
): Promise<{ diff: string; firstChangedLine: number | undefined } | { error: string }> {
|
|
78
85
|
let patch: Patch;
|
|
79
86
|
try {
|
|
80
|
-
patch = HashlinePatch.parse(input.input, { cwd
|
|
87
|
+
patch = HashlinePatch.parse(input.input, { cwd });
|
|
81
88
|
} catch (err) {
|
|
82
89
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
83
90
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Coding-agent runner that drives the hashline {@link Patcher} on behalf of
|
|
3
|
-
* the `edit` tool. Converts a `{input
|
|
3
|
+
* the `edit` tool. Converts a `{input}` tool-call payload into a
|
|
4
4
|
* fully-applied patch, wraps the result in the agent's
|
|
5
5
|
* {@link AgentToolResult} shape, and attaches LSP diagnostics + `outputMeta`
|
|
6
6
|
* for the renderer.
|
|
@@ -31,7 +31,6 @@ import { type HashlineParams, hashlineEditParamsSchema } from "./params";
|
|
|
31
31
|
export interface ExecuteHashlineSingleOptions {
|
|
32
32
|
session: ToolSession;
|
|
33
33
|
input: string;
|
|
34
|
-
path?: string;
|
|
35
34
|
signal?: AbortSignal;
|
|
36
35
|
batchRequest?: LspBatchRequest;
|
|
37
36
|
writethrough: WritethroughCallback;
|
|
@@ -91,16 +90,10 @@ function renderSection(result: PatchSectionResult, diagnostics: FileDiagnosticsR
|
|
|
91
90
|
|
|
92
91
|
const warningsBlock = result.warnings.length > 0 ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
|
|
93
92
|
const previewBlock = preview.preview ? `\n${preview.preview}` : "";
|
|
94
|
-
const headline = preview.preview
|
|
95
|
-
? `${result.path}:`
|
|
96
|
-
: result.op === "create"
|
|
97
|
-
? `Created ${result.path}`
|
|
98
|
-
: `Updated ${result.path}`;
|
|
99
|
-
|
|
100
93
|
const firstChangedLine = result.firstChangedLine ?? diff.firstChangedLine;
|
|
101
94
|
return {
|
|
102
95
|
toolResult: {
|
|
103
|
-
content: [{ type: "text", text: `${
|
|
96
|
+
content: [{ type: "text", text: `${result.header}${previewBlock}${warningsBlock}` }],
|
|
104
97
|
details: {
|
|
105
98
|
diff: diff.diff,
|
|
106
99
|
firstChangedLine,
|
|
@@ -122,7 +115,7 @@ function renderSection(result: PatchSectionResult, diagnostics: FileDiagnosticsR
|
|
|
122
115
|
export async function executeHashlineSingle(
|
|
123
116
|
options: ExecuteHashlineSingleOptions,
|
|
124
117
|
): Promise<AgentToolResult<EditToolDetails, typeof hashlineEditParamsSchema>> {
|
|
125
|
-
const patch = Patch.parse(options.input, { cwd: options.session.cwd
|
|
118
|
+
const patch = Patch.parse(options.input, { cwd: options.session.cwd });
|
|
126
119
|
if (patch.sections.length === 0) {
|
|
127
120
|
throw new Error("No hashline sections found in input.");
|
|
128
121
|
}
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zod schema for the `edit` tool's hashline mode payload. The schema is
|
|
3
3
|
* deliberately permissive (`.passthrough()`) so providers can attach extra
|
|
4
|
-
* keys without rejection; only `input` is required
|
|
5
|
-
*
|
|
4
|
+
* keys without rejection; only `input` is required. `_input` is accepted as a
|
|
5
|
+
* provider-emitted alias for `input`.
|
|
6
6
|
*/
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
|
|
9
|
-
export const hashlineEditParamsSchema = z.
|
|
9
|
+
export const hashlineEditParamsSchema = z.preprocess(raw => {
|
|
10
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return raw;
|
|
11
|
+
|
|
12
|
+
const record = raw as Record<string, unknown>;
|
|
13
|
+
if (typeof record.input === "string" || typeof record._input !== "string") return raw;
|
|
14
|
+
|
|
15
|
+
return { ...record, input: record._input };
|
|
16
|
+
}, z.object({ input: z.string() }).passthrough());
|
|
10
17
|
|
|
11
18
|
export type HashlineParams = z.infer<typeof hashlineEditParamsSchema>;
|
package/src/edit/index.ts
CHANGED
|
@@ -262,19 +262,17 @@ async function executeSinglePathEntries(
|
|
|
262
262
|
|
|
263
263
|
function extractApprovalPath(args: unknown): string {
|
|
264
264
|
const record = args && typeof args === "object" ? (args as Record<string, unknown>) : {};
|
|
265
|
-
const targetPath = record.path;
|
|
266
|
-
if (typeof targetPath === "string" && targetPath.length > 0) {
|
|
267
|
-
return targetPath;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
265
|
const input = typeof record.input === "string" ? record.input : undefined;
|
|
271
|
-
if (
|
|
266
|
+
if (input) {
|
|
267
|
+
const hashlineMatch = /^(?:¶|§|@)([^\s#]+)/m.exec(input);
|
|
268
|
+
if (hashlineMatch?.[1]) return hashlineMatch[1];
|
|
272
269
|
|
|
273
|
-
|
|
274
|
-
|
|
270
|
+
const applyPatchMatch = /^\*\*\* (?:Add|Update|Delete) File:\s*(.+)$/m.exec(input);
|
|
271
|
+
if (applyPatchMatch?.[1]) return applyPatchMatch[1].trim();
|
|
272
|
+
}
|
|
275
273
|
|
|
276
|
-
const
|
|
277
|
-
return
|
|
274
|
+
const targetPath = record.path;
|
|
275
|
+
return typeof targetPath === "string" && targetPath.length > 0 ? targetPath : "(unknown)";
|
|
278
276
|
}
|
|
279
277
|
|
|
280
278
|
export class EditTool implements AgentTool<TInput> {
|
|
@@ -430,11 +428,10 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
430
428
|
batchRequest: LspBatchRequest | undefined,
|
|
431
429
|
_onUpdate?: (partialResult: AgentToolResult<EditToolDetails, TInput>) => void,
|
|
432
430
|
) => {
|
|
433
|
-
const { input
|
|
431
|
+
const { input } = params as HashlineParams;
|
|
434
432
|
return executeHashlineSingle({
|
|
435
433
|
session: tool.session,
|
|
436
434
|
input,
|
|
437
|
-
path,
|
|
438
435
|
signal,
|
|
439
436
|
batchRequest,
|
|
440
437
|
writethrough: tool.#writethrough,
|
package/src/edit/renderer.ts
CHANGED
|
@@ -235,14 +235,21 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
|
|
|
235
235
|
|
|
236
236
|
function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
|
|
237
237
|
if (!diff) return "";
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
238
|
+
// Hunk-aware truncation keeps the change rows themselves visible and
|
|
239
|
+
// trims surrounding context proportionally so a multi-hunk diff doesn't
|
|
240
|
+
// turn into just the tail of the last hunk while streaming.
|
|
241
|
+
const {
|
|
242
|
+
text: truncatedDiff,
|
|
243
|
+
hiddenHunks,
|
|
244
|
+
hiddenLines,
|
|
245
|
+
} = truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, EDIT_STREAMING_PREVIEW_LINES);
|
|
242
246
|
let text = "\n\n";
|
|
243
|
-
text += renderDiffColored(
|
|
244
|
-
if (
|
|
245
|
-
|
|
247
|
+
text += renderDiffColored(truncatedDiff, { filePath: rawPath });
|
|
248
|
+
if (hiddenHunks > 0 || hiddenLines > 0) {
|
|
249
|
+
const remainder: string[] = [];
|
|
250
|
+
if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
|
|
251
|
+
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
252
|
+
text += uiTheme.fg("dim", `\n… (${label} +${remainder.join(", ")})`);
|
|
246
253
|
} else {
|
|
247
254
|
text += uiTheme.fg("dim", `\n(${label})`);
|
|
248
255
|
}
|
package/src/edit/streaming.ts
CHANGED
|
@@ -20,11 +20,8 @@ import {
|
|
|
20
20
|
END_PATCH_MARKER,
|
|
21
21
|
type PatchSection as HashlineInputSection,
|
|
22
22
|
Patch as HashlinePatch,
|
|
23
|
-
Tokenizer as HashlineTokenizer,
|
|
24
23
|
} from "@oh-my-pi/hashline";
|
|
25
|
-
import { sanitizeText } from "@oh-my-pi/pi-utils";
|
|
26
24
|
import type { Theme } from "../modes/theme/theme";
|
|
27
|
-
import { replaceTabs, truncateToWidth } from "../tools/render-utils";
|
|
28
25
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
29
26
|
import { computeEditDiff, type DiffError, type DiffResult } from "./diff";
|
|
30
27
|
import { computeHashlineDiff, computeHashlineSectionDiff } from "./hashline/diff";
|
|
@@ -73,48 +70,6 @@ export interface EditStreamingStrategy<Args = unknown> {
|
|
|
73
70
|
renderStreamingFallback(args: Args, uiTheme: Theme): string;
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
const STREAMING_FALLBACK_LINES = 12;
|
|
77
|
-
const STREAMING_FALLBACK_WIDTH = 80;
|
|
78
|
-
|
|
79
|
-
// Streaming-preview classification reuses one tokenizer instance for the
|
|
80
|
-
// stateless predicates and `tokenize`/`tokenizeAll` helpers; instances are
|
|
81
|
-
// cheap, but keeping a single module-level reference matches the rest of
|
|
82
|
-
// the hashline package.
|
|
83
|
-
const HASHLINE_TOKENIZER = new HashlineTokenizer();
|
|
84
|
-
|
|
85
|
-
function trimHashlineStreamingSyntax(lines: string[]): string[] {
|
|
86
|
-
let index = lines.findIndex(line => line.trim().length > 0);
|
|
87
|
-
if (index === -1) return [];
|
|
88
|
-
|
|
89
|
-
if (HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "envelope-begin") {
|
|
90
|
-
index++;
|
|
91
|
-
while (index < lines.length && lines[index].trim().length === 0) index++;
|
|
92
|
-
}
|
|
93
|
-
if (index < lines.length && HASHLINE_TOKENIZER.tokenize(lines[index]).kind === "header") {
|
|
94
|
-
index++;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return lines.slice(index).filter(line => !HASHLINE_TOKENIZER.isEnvelopeMarker(line));
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function renderHashlineInputFallback(input: string, uiTheme: Theme): string {
|
|
101
|
-
const lines = trimHashlineStreamingSyntax(sanitizeText(input).split("\n"));
|
|
102
|
-
if (!lines.some(line => line.trim().length > 0)) return "";
|
|
103
|
-
|
|
104
|
-
const displayLines = lines.slice(-STREAMING_FALLBACK_LINES);
|
|
105
|
-
const hidden = lines.length - displayLines.length;
|
|
106
|
-
let text = "\n\n";
|
|
107
|
-
text += displayLines
|
|
108
|
-
.map(line => uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), STREAMING_FALLBACK_WIDTH)))
|
|
109
|
-
.join("\n");
|
|
110
|
-
if (hidden > 0) {
|
|
111
|
-
text += uiTheme.fg("dim", `\n… (streaming +${hidden} lines)`);
|
|
112
|
-
} else {
|
|
113
|
-
text += uiTheme.fg("dim", "\n(streaming)");
|
|
114
|
-
}
|
|
115
|
-
return text;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
73
|
// -----------------------------------------------------------------------------
|
|
119
74
|
// Partial-JSON handling
|
|
120
75
|
// -----------------------------------------------------------------------------
|
|
@@ -273,7 +228,6 @@ const patchStrategy: EditStreamingStrategy<PatchArgs> = {
|
|
|
273
228
|
|
|
274
229
|
interface HashlineArgs {
|
|
275
230
|
input?: string;
|
|
276
|
-
path?: string;
|
|
277
231
|
__partialJson?: string;
|
|
278
232
|
}
|
|
279
233
|
|
|
@@ -353,75 +307,6 @@ function buildApplyPatchNaturalOrderPreviews(input: string): PerFileDiffPreview[
|
|
|
353
307
|
return previews.length > 0 ? previews : null;
|
|
354
308
|
}
|
|
355
309
|
|
|
356
|
-
/**
|
|
357
|
-
* Hashline equivalent: emit each payload line as a `+added` line in the
|
|
358
|
-
* order the model typed it. We deliberately omit op headers and removal
|
|
359
|
-
* targets from the streaming preview because their content lives in the file
|
|
360
|
-
* and would require a costly re-apply per tick; the complete unified diff is
|
|
361
|
-
* shown once streaming finishes.
|
|
362
|
-
*/
|
|
363
|
-
function buildHashlineNaturalOrderPreviews(
|
|
364
|
-
input: string,
|
|
365
|
-
defaultPath: string | undefined,
|
|
366
|
-
): PerFileDiffPreview[] | null {
|
|
367
|
-
const groups = new Map<string, string[]>();
|
|
368
|
-
let currentPath = defaultPath ?? "";
|
|
369
|
-
const ensure = (sectionPath: string): string[] => {
|
|
370
|
-
let bucket = groups.get(sectionPath);
|
|
371
|
-
if (!bucket) {
|
|
372
|
-
bucket = [];
|
|
373
|
-
groups.set(sectionPath, bucket);
|
|
374
|
-
}
|
|
375
|
-
return bucket;
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
// Per-call instance: the streaming preview re-runs each tick with the
|
|
379
|
-
// cumulative input, and we need the line counter to start at 1. A
|
|
380
|
-
// dedicated tokenizer keeps the shared HASHLINE_TOKENIZER above free
|
|
381
|
-
// for stateless predicate use elsewhere in this module.
|
|
382
|
-
const streamer = new HashlineTokenizer();
|
|
383
|
-
for (const token of streamer.tokenizeAll(input)) {
|
|
384
|
-
switch (token.kind) {
|
|
385
|
-
case "envelope-begin":
|
|
386
|
-
case "envelope-end":
|
|
387
|
-
case "abort":
|
|
388
|
-
case "op-delete":
|
|
389
|
-
continue;
|
|
390
|
-
case "blank":
|
|
391
|
-
case "raw":
|
|
392
|
-
continue;
|
|
393
|
-
case "header":
|
|
394
|
-
currentPath = token.path;
|
|
395
|
-
if (currentPath) ensure(currentPath);
|
|
396
|
-
continue;
|
|
397
|
-
case "op-insert":
|
|
398
|
-
case "op-replace":
|
|
399
|
-
// Inline body on the op line itself (`N↓payload`, `A-B:payload`) is
|
|
400
|
-
// payload content that just happens to share a line with the op
|
|
401
|
-
// header — render it the same as a standalone payload token so
|
|
402
|
-
// the very first character the model types after the sigil shows
|
|
403
|
-
// up in the streaming preview. Without this, the preview is
|
|
404
|
-
// empty until a newline arrives, and the renderer falls back to
|
|
405
|
-
// raw input ("A-B: bla bla bla") instead of "+ bla bla bla".
|
|
406
|
-
if (!currentPath || token.inlineBody === undefined) continue;
|
|
407
|
-
ensure(currentPath).push(`+${token.inlineBody}`);
|
|
408
|
-
continue;
|
|
409
|
-
case "payload":
|
|
410
|
-
if (!currentPath) continue;
|
|
411
|
-
ensure(currentPath).push(`+${token.text}`);
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (groups.size === 0) return null;
|
|
417
|
-
const previews: PerFileDiffPreview[] = [];
|
|
418
|
-
for (const [sectionPath, body] of groups) {
|
|
419
|
-
if (body.length === 0) continue;
|
|
420
|
-
previews.push({ path: sectionPath, diff: body.join("\n") });
|
|
421
|
-
}
|
|
422
|
-
return previews.length > 0 ? previews : null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
310
|
const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
426
311
|
extractCompleteEdits(args) {
|
|
427
312
|
return args;
|
|
@@ -430,25 +315,21 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
430
315
|
if (typeof args.input !== "string" || args.input.length === 0) return null;
|
|
431
316
|
const input = trimTrailingPartialLine(args.input, ctx.isStreaming);
|
|
432
317
|
if (input.length === 0) return null;
|
|
433
|
-
if (ctx.isStreaming) {
|
|
434
|
-
// Skip the costly per-tick re-apply and avoid `Diff.structuredPatch`
|
|
435
|
-
// reordering by showing payload lines in input order.
|
|
436
|
-
return buildHashlineNaturalOrderPreviews(input, args.path);
|
|
437
|
-
}
|
|
438
318
|
ctx.signal.throwIfAborted();
|
|
439
319
|
|
|
440
320
|
let sections: readonly HashlineInputSection[];
|
|
441
321
|
try {
|
|
442
|
-
sections = HashlinePatch.parse(input, { cwd: ctx.cwd
|
|
322
|
+
sections = HashlinePatch.parse(input, { cwd: ctx.cwd }).sections;
|
|
443
323
|
} catch {
|
|
444
|
-
//
|
|
445
|
-
//
|
|
446
|
-
|
|
324
|
+
// While streaming, the trailing op may still be mid-typed and fail
|
|
325
|
+
// to parse; suppress until the next chunk arrives. Once args are
|
|
326
|
+
// complete, surface the error so the model sees what went wrong.
|
|
327
|
+
if (ctx.isStreaming) return null;
|
|
328
|
+
const result = await computeHashlineDiff({ input }, ctx.cwd, {
|
|
447
329
|
autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
|
|
448
330
|
});
|
|
449
331
|
ctx.signal.throwIfAborted();
|
|
450
|
-
|
|
451
|
-
return [toPerFilePreview(args.path ?? "", result)];
|
|
332
|
+
return [toPerFilePreview("", result)];
|
|
452
333
|
}
|
|
453
334
|
if (sections.length === 0) return null;
|
|
454
335
|
|
|
@@ -467,6 +348,7 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
467
348
|
const section = sectionsToProcess[i];
|
|
468
349
|
const result = await computeHashlineSectionDiff(section, ctx.cwd, {
|
|
469
350
|
autoDropPureInsertDuplicates: ctx.hashlineAutoDropPureInsertDuplicates,
|
|
351
|
+
streaming: ctx.isStreaming,
|
|
470
352
|
});
|
|
471
353
|
ctx.signal.throwIfAborted();
|
|
472
354
|
// In a multi-section preview, ignore parse/apply errors from the
|
|
@@ -479,8 +361,13 @@ const hashlineStrategy: EditStreamingStrategy<HashlineArgs> = {
|
|
|
479
361
|
}
|
|
480
362
|
return previews.length > 0 ? previews : null;
|
|
481
363
|
},
|
|
482
|
-
renderStreamingFallback(
|
|
483
|
-
|
|
364
|
+
renderStreamingFallback() {
|
|
365
|
+
// Never leak raw hashline syntax (`64:`, `|payload`, `¶path#hash`)
|
|
366
|
+
// to the user — the streaming preview already projects every
|
|
367
|
+
// parseable op onto the real file via applyPartialTo, and an
|
|
368
|
+
// unparseable trailing chunk renders as "no preview yet" rather
|
|
369
|
+
// than a sigil dump.
|
|
370
|
+
return "";
|
|
484
371
|
},
|
|
485
372
|
};
|
|
486
373
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compatibility shim for legacy extensions importing the package root of
|
|
3
|
+
* `@oh-my-pi/pi-ai` (or one of its aliased scopes like `@earendil-works/pi-ai`
|
|
4
|
+
* or `@mariozechner/pi-ai`).
|
|
5
|
+
*
|
|
6
|
+
* pi-ai 15.1.0 removed the historical TypeBox root exports (`Type`, plus the
|
|
7
|
+
* runtime-relevant half of the `Static`/`TSchema` pair) from the package
|
|
8
|
+
* entrypoint. Legacy extensions still author parameter schemas as
|
|
9
|
+
* `Type.Object({ ... })`, so this file is served by `legacy-pi-compat.ts` in
|
|
10
|
+
* place of the real pi-ai entrypoint whenever a legacy extension imports the
|
|
11
|
+
* bare package root. Subpath imports (`@oh-my-pi/pi-ai/utils/oauth`, etc.)
|
|
12
|
+
* continue to resolve directly against the bundled pi-ai package.
|
|
13
|
+
*
|
|
14
|
+
* The `Type` runtime is borrowed from the Zod-backed TypeBox shim that
|
|
15
|
+
* already serves bare `@sinclair/typebox` imports for the same extension
|
|
16
|
+
* class, keeping the legacy-compat surface internally consistent.
|
|
17
|
+
*
|
|
18
|
+
* Type-level `Static` and `TSchema` continue to come from pi-ai's own
|
|
19
|
+
* `types.ts` via the `export *` below — pi-ai still exports both as types,
|
|
20
|
+
* only the runtime `Type` builder was removed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export * from "@oh-my-pi/pi-ai";
|
|
24
|
+
export { Type } from "./typebox";
|
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs/promises";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
+
import { isCompiledBinary } from "@oh-my-pi/pi-utils";
|
|
5
6
|
|
|
6
7
|
// Canonical scope for in-process pi packages. Plugins published against any of
|
|
7
8
|
// the aliased scopes below (mariozechner's original publish, earendil-works'
|
|
@@ -56,7 +57,34 @@ const resolvedSpecifierFallbacks = new Map<string, string>();
|
|
|
56
57
|
// relying on them must vendor `@sinclair/typebox` directly.
|
|
57
58
|
const TYPEBOX_SPECIFIER = "@sinclair/typebox";
|
|
58
59
|
const TYPEBOX_SPECIFIER_FILTER = /^@sinclair\/typebox$/;
|
|
59
|
-
|
|
60
|
+
|
|
61
|
+
// In-process compat shim paths. In dev `import.meta.dir` is the source folder of
|
|
62
|
+
// this file, so the dev branches resolve to the real `.ts` source. In compiled
|
|
63
|
+
// binaries `import.meta.dir` collapses to `/$bunfs/root`, so the runtime cannot
|
|
64
|
+
// recover the source layout that way; instead, each shim file is registered as
|
|
65
|
+
// a `--compile` entrypoint in `scripts/build-binary.ts`, which Bun emits into
|
|
66
|
+
// bunfs at a deterministic `--root`-relative path with a `.js` extension. The
|
|
67
|
+
// literals below must stay in sync with that listing — if either path drifts,
|
|
68
|
+
// every legacy plugin loading the shim fails with a missing-module error in
|
|
69
|
+
// release builds (without affecting `bun test`/dev).
|
|
70
|
+
const TYPEBOX_SHIM_PATH = isCompiledBinary()
|
|
71
|
+
? "/$bunfs/root/packages/coding-agent/src/extensibility/typebox.js"
|
|
72
|
+
: path.resolve(import.meta.dir, "../typebox.ts");
|
|
73
|
+
|
|
74
|
+
// Legacy extensions historically imported `Type` (and `Static`/`TSchema`) from
|
|
75
|
+
// the package root of `@(scope)/pi-ai`. pi-ai 15.1.0 removed the runtime `Type`
|
|
76
|
+
// export (see `packages/ai/CHANGELOG.md`), so the bare canonical specifier no
|
|
77
|
+
// longer satisfies those imports. The override below redirects only the bare
|
|
78
|
+
// pi-ai package root onto a sibling shim that re-exports the canonical surface
|
|
79
|
+
// plus the borrowed `Type` runtime from the Zod-backed TypeBox shim. Subpath
|
|
80
|
+
// imports such as `@oh-my-pi/pi-ai/utils/oauth` continue to resolve directly
|
|
81
|
+
// against the bundled pi-ai package.
|
|
82
|
+
const LEGACY_PI_AI_SHIM_PATH = isCompiledBinary()
|
|
83
|
+
? "/$bunfs/root/packages/coding-agent/src/extensibility/legacy-pi-ai-shim.js"
|
|
84
|
+
: path.resolve(import.meta.dir, "../legacy-pi-ai-shim.ts");
|
|
85
|
+
const LEGACY_PI_PACKAGE_ROOT_OVERRIDES: Record<string, string> = {
|
|
86
|
+
[`${CANONICAL_PI_SCOPE}/pi-ai`]: LEGACY_PI_AI_SHIM_PATH,
|
|
87
|
+
};
|
|
60
88
|
|
|
61
89
|
let isLegacyPiSpecifierShimInstalled = false;
|
|
62
90
|
|
|
@@ -85,6 +113,22 @@ function getResolvedSpecifier(specifier: string): string {
|
|
|
85
113
|
return resolved;
|
|
86
114
|
}
|
|
87
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a canonical `@oh-my-pi/*` specifier to a filesystem path, preferring
|
|
118
|
+
* a bundled compat shim when one is registered for the package root.
|
|
119
|
+
*
|
|
120
|
+
* Falls back to `getResolvedSpecifier` (which may throw under compiled binary
|
|
121
|
+
* mode); callers handle that the same way they would for non-overridden
|
|
122
|
+
* specifiers.
|
|
123
|
+
*/
|
|
124
|
+
function resolveCanonicalPiSpecifier(remappedSpecifier: string): string {
|
|
125
|
+
const override = LEGACY_PI_PACKAGE_ROOT_OVERRIDES[remappedSpecifier];
|
|
126
|
+
if (override) {
|
|
127
|
+
return override;
|
|
128
|
+
}
|
|
129
|
+
return getResolvedSpecifier(remappedSpecifier);
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
function toImportSpecifier(resolvedPath: string): string {
|
|
89
133
|
return url.pathToFileURL(resolvedPath).href;
|
|
90
134
|
}
|
|
@@ -99,7 +143,7 @@ function rewriteLegacyPiImports(source: string): string {
|
|
|
99
143
|
}
|
|
100
144
|
|
|
101
145
|
try {
|
|
102
|
-
return `${prefix}${toImportSpecifier(
|
|
146
|
+
return `${prefix}${toImportSpecifier(resolveCanonicalPiSpecifier(remappedSpecifier))}${suffix}`;
|
|
103
147
|
} catch {
|
|
104
148
|
// Resolution failed — typically in compiled binary mode where
|
|
105
149
|
// Bun.resolveSync cannot walk up from /$bunfs/root to find the
|
|
@@ -250,7 +294,7 @@ function resolveLegacyPiSpecifier(args: { path: string; importer: string }): { p
|
|
|
250
294
|
// Primary: resolve the canonical @oh-my-pi/* specifier from the host binary
|
|
251
295
|
// location. Works in dev mode and in source-link installs.
|
|
252
296
|
try {
|
|
253
|
-
return { path:
|
|
297
|
+
return { path: resolveCanonicalPiSpecifier(remappedSpecifier) };
|
|
254
298
|
} catch {
|
|
255
299
|
// Fallback for compiled binary mode: the bundled packages live inside
|
|
256
300
|
// /$bunfs/root and aren't reachable by filesystem resolution. Try the
|