Skip to content

Commit 67edd9a

Browse files
Handle when a clanker fails to format an edit request
On occassion, clankers will fail to properly escape json when making a large edit request. The validate edit extension gives a decent error that you can copy/paste to the clanker so they can fix their mistake. Also added a little /thinking command to set the thinking level interactively.
1 parent 5891700 commit 67edd9a

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

mappings/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
["nvim/*", "~/.config/nvim"],
1010
["pi/answer-extension.ts", "~/.pi/agent/extensions/answer-extension.ts"],
1111
["pi/cursor-rules-extension.ts", "~/.pi/agent/extensions/cursor-rules-extension.ts"],
12+
["pi/thinking-extension.ts", "~/.pi/agent/extensions/thinking-extension.ts"],
13+
["pi/validate-edit-extension.ts", "~/.pi/agent/extensions/validate-edit-extension.ts"],
1214
["pi/models.json", "~/.pi/agent/models.json"],
1315
["pi/save-last-extension.ts", "~/.pi/agent/extensions/save-last-extension.ts"],
1416
["pi/settings.json", "~/.pi/agent/settings.json"],

pi/thinking-extension.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const DESCRIPTION = {
2+
off: "No reasoning (fastest)",
3+
minimal: "Minimal internal reasoning",
4+
low: "Low-effort reasoning",
5+
medium: "Balanced reasoning and speed",
6+
high: "Deep reasoning",
7+
xhigh: "Maximum reasoning (slowest)",
8+
};
9+
const LEVELS = Object.keys(DESCRIPTION)
10+
11+
export default function thinkingExtension(pi) {
12+
pi.registerCommand("thinking", {
13+
description: "Set thinking level",
14+
getArgumentCompletions: (prefix) =>
15+
LEVELS
16+
.filter(l => l.startsWith(prefix))
17+
.map(l => ({ value: l, label: `${l} - ${DESCRIPTION[l]}` })),
18+
handler: (args, ctx) => handleThinking(args, ctx, pi),
19+
});
20+
}
21+
22+
async function handleThinking(args, ctx, pi) {
23+
const arg = args.trim();
24+
const level = LEVELS.includes(arg)
25+
? arg
26+
: await ctx.ui.select("Thinking level:", LEVELS);
27+
28+
if (level && level !== pi.getThinkingLevel()) {
29+
pi.setThinkingLevel(level);
30+
ctx.ui.notify(`${level} (${DESCRIPTION[level]})`, "success");
31+
}
32+
}

pi/validate-edit-extension.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2+
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
3+
4+
export default function (pi: ExtensionAPI) {
5+
pi.on("tool_call", async (event, ctx) => {
6+
if (!isToolCallEventType("edit", event)) return;
7+
8+
const error = validateEdits(event.input.edits);
9+
if (error) {
10+
ctx.ui.notify(error, "error");
11+
return { block: true, reason: "Invalid edit parameters" };
12+
}
13+
});
14+
}
15+
16+
function validateEdits(edits: unknown): string | null {
17+
if (!Array.isArray(edits)) {
18+
return "❌ edits must be an array";
19+
}
20+
21+
if (edits.length === 0) return null;
22+
23+
for (let i = 0; i < edits.length; i++) {
24+
const item = edits[i];
25+
26+
if (!isValidEditItem(item)) {
27+
return `❌ Edit ${i}: must have oldText (string) and newText (string)`;
28+
}
29+
30+
const jsonError = trySerializeEdit(item as EditItem);
31+
if (jsonError) {
32+
return `❌ Edit ${i} has JSON serialization error:\n ${jsonError}\n\n` +
33+
`Common cause: Unescaped quotes in oldText or newText.\n` +
34+
`Tip: Break into smaller edits or escape special characters.`;
35+
}
36+
}
37+
38+
const overlapError = findOverlap(edits as EditItem[]);
39+
if (overlapError) {
40+
return `❌ ${overlapError}\n\n` +
41+
`Solution: Ensure each oldText is unique and non-overlapping.`;
42+
}
43+
44+
return null;
45+
}
46+
47+
function isValidEditItem(item: unknown): boolean {
48+
if (typeof item !== "object" || item === null) return false;
49+
const e = item as Record<string, unknown>;
50+
return typeof e.oldText === "string" && typeof e.newText === "string";
51+
}
52+
53+
function trySerializeEdit(edit: EditItem): string | null {
54+
try {
55+
JSON.stringify({ edits: [edit] });
56+
return null;
57+
} catch (err) {
58+
return (err as Error).message;
59+
}
60+
}
61+
62+
function findOverlap(edits: EditItem[]): string | null {
63+
for (let i = 0; i < edits.length; i++) {
64+
for (let j = i + 1; j < edits.length; j++) {
65+
const a = edits[i].oldText;
66+
const b = edits[j].oldText;
67+
68+
if (a === b) {
69+
return `Edits ${i} and ${j}: identical oldText`;
70+
}
71+
72+
if (a.includes(b) || b.includes(a)) {
73+
return `Edits ${i} and ${j}: overlapping oldText (one contains the other)`;
74+
}
75+
}
76+
}
77+
78+
return null;
79+
}
80+
81+
interface EditItem {
82+
oldText: string;
83+
newText: string;
84+
}

0 commit comments

Comments
 (0)