@hheei/omp-optimizer 0.1.0
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 +12 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/package.json +30 -0
- package/src/capability.test.ts +42 -0
- package/src/capability.ts +16 -0
- package/src/caveman.test.ts +203 -0
- package/src/caveman.ts +140 -0
- package/src/edit-guard.test.ts +114 -0
- package/src/edit-guard.ts +137 -0
- package/src/index.ts +50 -0
- package/src/mode.ts +115 -0
- package/src/opt-keys.test.ts +165 -0
- package/src/opt.test.ts +34 -0
- package/src/opt.ts +161 -0
- package/src/persist.test.ts +52 -0
- package/src/persist.ts +58 -0
- package/src/ponytail.test.ts +195 -0
- package/src/ponytail.ts +145 -0
- package/src/rtk.test.ts +277 -0
- package/src/rtk.ts +355 -0
- package/src/status.test.ts +84 -0
- package/src/status.ts +61 -0
- package/src/t2s.test.ts +15 -0
- package/src/t2s.ts +122 -0
- package/src/tool-result-filter.test.ts +96 -0
- package/src/tool-result-filter.ts +53 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { OptimizerStatus, renderStatus, STATUS_KEY, toolIcon } from "./status.ts";
|
|
3
|
+
|
|
4
|
+
/** Tagging colorizer: <accent>X</accent> / <dim>X</dim> for assertions. */
|
|
5
|
+
const tag = (c: string, t: string) => `<${c}>${t}</${c}>`;
|
|
6
|
+
|
|
7
|
+
// Nerd Font glyphs are the extension's stable visual identity in OMP's TUI.
|
|
8
|
+
const CV = toolIcon("caveman");
|
|
9
|
+
const RK = toolIcon("rtk");
|
|
10
|
+
const PT = toolIcon("ponytail");
|
|
11
|
+
const T2S = toolIcon("t2s");
|
|
12
|
+
const EG = toolIcon("edit-guard");
|
|
13
|
+
describe("renderStatus", () => {
|
|
14
|
+
it("shows ALL icons in order, accent when enabled", () => {
|
|
15
|
+
expect(renderStatus({ caveman: true, rtk: true, ponytail: true, t2s: true, "edit-guard": true }, tag)).toBe(
|
|
16
|
+
`<accent>${CV}</accent> <accent>${RK}</accent> <accent>${PT}</accent> <accent>${T2S}</accent> <accent>${EG}</accent> `,
|
|
17
|
+
);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("dims disabled tools but still shows them", () => {
|
|
21
|
+
expect(renderStatus({ caveman: false, rtk: true, ponytail: true, t2s: true, "edit-guard": true }, tag)).toBe(
|
|
22
|
+
`<dim>${CV}</dim> <accent>${RK}</accent> <accent>${PT}</accent> <accent>${T2S}</accent> <accent>${EG}</accent> `,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("all dim when nothing enabled (cell never empty)", () => {
|
|
27
|
+
expect(renderStatus({}, tag)).toBe(`<dim>${CV}</dim> <dim>${RK}</dim> <dim>${PT}</dim> <dim>${T2S}</dim> <dim>${EG}</dim> `);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("preserves fixed order regardless of insertion order", () => {
|
|
31
|
+
expect(renderStatus({ ponytail: true, caveman: true }, tag)).toBe(
|
|
32
|
+
`<accent>${CV}</accent> <dim>${RK}</dim> <accent>${PT}</accent> <dim>${T2S}</dim> <dim>${EG}</dim> `,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("uses the optimizer Nerd Font icon catalog", () => {
|
|
37
|
+
expect([CV, RK, PT, T2S, EG]).toEqual(["\u{F0710}", "\u{F04E5}", "\u{F0190}", "\u{F0AC}", "\u{F132}"]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("OptimizerStatus", () => {
|
|
42
|
+
/** Minimal ui stub capturing setStatus calls. */
|
|
43
|
+
function fakeCtx() {
|
|
44
|
+
const calls: { key: string; text: string }[] = [];
|
|
45
|
+
return {
|
|
46
|
+
calls,
|
|
47
|
+
ui: {
|
|
48
|
+
setStatus: (key: string, text: string | undefined) => calls.push({ key, text: text ?? "" }),
|
|
49
|
+
theme: { fg: (c: string, t: string) => `<${c}>${t}</${c}>` },
|
|
50
|
+
},
|
|
51
|
+
} as const;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
it("paints the shared key with per-icon accent/dim", () => {
|
|
55
|
+
const status = new OptimizerStatus();
|
|
56
|
+
const ctx = fakeCtx();
|
|
57
|
+
status.set("rtk", true, ctx as never);
|
|
58
|
+
const last = ctx.calls.at(-1);
|
|
59
|
+
if (!last) throw new Error("no calls");
|
|
60
|
+
expect(last.key).toBe(STATUS_KEY);
|
|
61
|
+
// caveman + ponytail + edit guard still unset (dim), rtk accent.
|
|
62
|
+
expect(last.text).toBe(`<dim>${CV}</dim> <accent>${RK}</accent> <dim>${PT}</dim> <dim>${T2S}</dim> <dim>${EG}</dim> `);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("accumulates state across tools", () => {
|
|
66
|
+
const status = new OptimizerStatus();
|
|
67
|
+
const ctx = fakeCtx();
|
|
68
|
+
status.set("caveman", true, ctx as never);
|
|
69
|
+
status.set("ponytail", true, ctx as never);
|
|
70
|
+
const last = ctx.calls.at(-1);
|
|
71
|
+
if (!last) throw new Error("no calls");
|
|
72
|
+
expect(last.text).toBe(`<accent>${CV}</accent> <dim>${RK}</dim> <accent>${PT}</accent> <dim>${T2S}</dim> <dim>${EG}</dim> `);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("dims an icon when its tool toggles off (cell stays populated)", () => {
|
|
76
|
+
const status = new OptimizerStatus();
|
|
77
|
+
const ctx = fakeCtx();
|
|
78
|
+
status.set("rtk", true, ctx as never);
|
|
79
|
+
status.set("rtk", false, ctx as never);
|
|
80
|
+
const last = ctx.calls.at(-1);
|
|
81
|
+
if (!last) throw new Error("no calls");
|
|
82
|
+
expect(last.text).toBe(`<dim>${CV}</dim> <dim>${RK}</dim> <dim>${PT}</dim> <dim>${T2S}</dim> <dim>${EG}</dim> `);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Shared status-bar state for the optimizer tools. */
|
|
2
|
+
|
|
3
|
+
import type { ExtensionCommandContext, ExtensionContext, ThemeColor } from "@oh-my-pi/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export interface OptimizerHandle {
|
|
6
|
+
name: OptimizerTool;
|
|
7
|
+
help: string;
|
|
8
|
+
values: readonly string[];
|
|
9
|
+
current(): string;
|
|
10
|
+
run(value: string, ctx: ExtensionCommandContext): Promise<void> | void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const STATUS_KEY = "omp-optimizer";
|
|
14
|
+
export type OptimizerTool = "caveman" | "rtk" | "ponytail" | "t2s" | "edit-guard";
|
|
15
|
+
|
|
16
|
+
/** Optimizer-specific Nerd Font glyphs, matching pix-optimizer's catalog. */
|
|
17
|
+
const TOOL_GLYPH: Record<OptimizerTool, string> = {
|
|
18
|
+
caveman: "\u{F0710}",
|
|
19
|
+
rtk: "\u{F04E5}",
|
|
20
|
+
ponytail: "\u{F0190}",
|
|
21
|
+
t2s: "\u{F0AC}",
|
|
22
|
+
"edit-guard": "\u{F132}",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const OPTIMIZER_ICON = "\u{F0DAB}";
|
|
26
|
+
|
|
27
|
+
export function toolIcon(tool: OptimizerTool): string {
|
|
28
|
+
return TOOL_GLYPH[tool];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TOOL_ORDER: readonly OptimizerTool[] = ["caveman", "rtk", "ponytail", "t2s", "edit-guard"];
|
|
32
|
+
const ENABLED_COLOR: ThemeColor = "accent";
|
|
33
|
+
const DISABLED_COLOR: ThemeColor = "dim";
|
|
34
|
+
|
|
35
|
+
export type Colorize = (color: ThemeColor, text: string) => string;
|
|
36
|
+
|
|
37
|
+
export function renderStatus(
|
|
38
|
+
states: Partial<Record<OptimizerTool, boolean>>,
|
|
39
|
+
color: Colorize,
|
|
40
|
+
): string {
|
|
41
|
+
return `${TOOL_ORDER.map(tool => color(states[tool] === true ? ENABLED_COLOR : DISABLED_COLOR, toolIcon(tool))).join(" ")} `;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class OptimizerStatus {
|
|
45
|
+
#states: Partial<Record<OptimizerTool, boolean>> = {};
|
|
46
|
+
// Status is repainted by each lifecycle context; no global icon listener is needed.
|
|
47
|
+
|
|
48
|
+
get(tool: OptimizerTool): boolean | undefined {
|
|
49
|
+
return this.#states[tool];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
set(tool: OptimizerTool, enabled: boolean, ctx: Pick<ExtensionContext, "ui">): void {
|
|
53
|
+
this.#states[tool] = enabled;
|
|
54
|
+
this.paint(ctx);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
paint(ctx: Pick<ExtensionContext, "ui">): void {
|
|
58
|
+
const text = renderStatus(this.#states, (color, value) => ctx.ui.theme.fg(color, value));
|
|
59
|
+
ctx.ui.setStatus(STATUS_KEY, text);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/t2s.test.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { convertInputText } from "./t2s.ts";
|
|
3
|
+
|
|
4
|
+
describe("optimizer t2s", () => {
|
|
5
|
+
test("converts Traditional Chinese prose", () => {
|
|
6
|
+
expect(convertInputText("ēéŗ¼čØå®éč¦å¹«åæļ¼")).toBe("ä»ä¹č®¾å®éč¦åø®åæļ¼");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("preserves inline and fenced code", () => {
|
|
10
|
+
const input = "č«ē `ēéŗ¼`\n```ts\nēéŗ¼čØå®\n```\n~~~txt\nęå¾čØå®\n~~~\nęå¾čØå®";
|
|
11
|
+
expect(convertInputText(input)).toBe(
|
|
12
|
+
"请ē `ēéŗ¼`\n```ts\nēéŗ¼čØå®\n```\n~~~txt\nęå¾čØå®\n~~~\nęå设å®",
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
});
|
package/src/t2s.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import OpenCC from "opencc-js/t2cn";
|
|
2
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
3
|
+
import { loadOptValue, saveOptValue } from "./persist.ts";
|
|
4
|
+
import type { OptimizerHandle, OptimizerStatus } from "./status.ts";
|
|
5
|
+
|
|
6
|
+
interface Fence {
|
|
7
|
+
readonly char: "`" | "~";
|
|
8
|
+
readonly length: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type T2sLevel = "on" | "off";
|
|
12
|
+
export const T2S_LEVELS: readonly T2sLevel[] = ["on", "off"];
|
|
13
|
+
const toSimplified = OpenCC.Converter({ from: "tw", to: "cn" });
|
|
14
|
+
|
|
15
|
+
function matchFence(line: string): Fence | undefined {
|
|
16
|
+
let start = 0;
|
|
17
|
+
while (start < line.length && (line[start] === " " || line[start] === "\t")) start++;
|
|
18
|
+
const char = line[start];
|
|
19
|
+
if (char !== "`" && char !== "~") return undefined;
|
|
20
|
+
let end = start;
|
|
21
|
+
while (end < line.length && line[end] === char) end++;
|
|
22
|
+
return end - start >= 3 ? { char, length: end - start } : undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function proseLine(line: string): string {
|
|
26
|
+
let output = "";
|
|
27
|
+
let cursor = 0;
|
|
28
|
+
while (cursor < line.length) {
|
|
29
|
+
const openStart = line.indexOf("`", cursor);
|
|
30
|
+
if (openStart < 0) return output + toSimplified(line.slice(cursor)).replaceAll("ēä¹", "ä»ä¹");
|
|
31
|
+
output += toSimplified(line.slice(cursor, openStart)).replaceAll("ēä¹", "ä»ä¹");
|
|
32
|
+
let openEnd = openStart;
|
|
33
|
+
while (openEnd < line.length && line[openEnd] === "`") openEnd++;
|
|
34
|
+
const delimiterLength = openEnd - openStart;
|
|
35
|
+
let searchFrom = openEnd;
|
|
36
|
+
let closeEnd = -1;
|
|
37
|
+
while (searchFrom < line.length) {
|
|
38
|
+
const candidateStart = line.indexOf("`", searchFrom);
|
|
39
|
+
if (candidateStart < 0) break;
|
|
40
|
+
let candidateEnd = candidateStart;
|
|
41
|
+
while (candidateEnd < line.length && line[candidateEnd] === "`") candidateEnd++;
|
|
42
|
+
if (candidateEnd - candidateStart === delimiterLength) {
|
|
43
|
+
closeEnd = candidateEnd;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
searchFrom = candidateEnd;
|
|
47
|
+
}
|
|
48
|
+
if (closeEnd < 0) return output + line.slice(openStart);
|
|
49
|
+
output += line.slice(openStart, closeEnd);
|
|
50
|
+
cursor = closeEnd;
|
|
51
|
+
}
|
|
52
|
+
return output;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function convertInputText(text: string): string {
|
|
56
|
+
const lines = text.split("\n");
|
|
57
|
+
let activeFence: Fence | undefined;
|
|
58
|
+
let output = "";
|
|
59
|
+
for (let index = 0; index < lines.length; index++) {
|
|
60
|
+
const line = lines[index];
|
|
61
|
+
if (line === undefined) continue;
|
|
62
|
+
const hasLineFeed = index < lines.length - 1;
|
|
63
|
+
const fence = matchFence(line);
|
|
64
|
+
if (activeFence) {
|
|
65
|
+
output += line + (hasLineFeed ? "\n" : "");
|
|
66
|
+
if (fence?.char === activeFence.char && fence.length >= activeFence.length) activeFence = undefined;
|
|
67
|
+
} else if (fence) {
|
|
68
|
+
activeFence = fence;
|
|
69
|
+
output += line + (hasLineFeed ? "\n" : "");
|
|
70
|
+
} else {
|
|
71
|
+
output += proseLine(line) + (hasLineFeed ? "\n" : "");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return output;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
export function t2s(pi: ExtensionAPI, status: OptimizerStatus): OptimizerHandle {
|
|
79
|
+
let level: T2sLevel = "on";
|
|
80
|
+
|
|
81
|
+
function syncStatus(ctx: Pick<ExtensionContext, "ui">) {
|
|
82
|
+
status.set("t2s", level === "on", ctx);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pi.on("input", (event, _ctx) => {
|
|
86
|
+
if (level !== "on" || event.source !== "interactive") return undefined;
|
|
87
|
+
const text = convertInputText(event.text);
|
|
88
|
+
return text === event.text ? undefined : { action: "transform", text };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
92
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
93
|
+
if (entry.type !== "custom" || entry.customType !== "t2s-level") continue;
|
|
94
|
+
const saved = entry.data && typeof entry.data === "object" && "level" in entry.data ? entry.data.level : undefined;
|
|
95
|
+
if (saved === "on" || saved === "off") level = saved;
|
|
96
|
+
}
|
|
97
|
+
const saved = loadOptValue("t2s");
|
|
98
|
+
if (saved === "on" || saved === "off") level = saved;
|
|
99
|
+
syncStatus(ctx);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.on("agent_start", async (_event, ctx) => syncStatus(ctx));
|
|
103
|
+
pi.on("agent_end", async (_event, ctx) => syncStatus(ctx));
|
|
104
|
+
|
|
105
|
+
async function run(value: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
106
|
+
if (value !== "on" && value !== "off") return;
|
|
107
|
+
level = value;
|
|
108
|
+
pi.appendEntry("t2s-level", { level });
|
|
109
|
+
saveOptValue("t2s", level);
|
|
110
|
+
syncStatus(ctx);
|
|
111
|
+
ctx.ui.notify(`T2S ${level === "on" ? "enabled" : "disabled"}`, "info");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name: "t2s",
|
|
117
|
+
help: "t2s ā convert Traditional Chinese input to Simplified Chinese",
|
|
118
|
+
values: T2S_LEVELS,
|
|
119
|
+
current: () => level,
|
|
120
|
+
run,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { filterModelWarnings, stripModelWarningParagraphs } from "./tool-result-filter.ts";
|
|
3
|
+
|
|
4
|
+
// āā stripModelWarningParagraphs āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
5
|
+
|
|
6
|
+
describe("stripModelWarningParagraphs", () => {
|
|
7
|
+
it("passes through text with no warnings", () => {
|
|
8
|
+
const text = "Exit code: 0\n\nsome output here";
|
|
9
|
+
expect(stripModelWarningParagraphs(text)).toBe(text);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("strips a BLIND WRITE paragraph", () => {
|
|
13
|
+
const text =
|
|
14
|
+
"Exit code: 0\n\nsome output\n\nā BLIND WRITE ā editing `foo.ts` without reading in the last 5 tool calls. Read the file first to avoid assumptions.";
|
|
15
|
+
const result = stripModelWarningParagraphs(text);
|
|
16
|
+
expect(result).not.toContain("BLIND WRITE");
|
|
17
|
+
expect(result).toContain("some output");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("strips a THRASHING paragraph", () => {
|
|
21
|
+
const text =
|
|
22
|
+
"ok\n\nš“ THRASHING ā 3 consecutive `edit` calls on foo.ts. Consider reading first.";
|
|
23
|
+
const result = stripModelWarningParagraphs(text);
|
|
24
|
+
expect(result).not.toContain("THRASHING");
|
|
25
|
+
expect(result).toContain("ok");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("strips multiple warning paragraphs", () => {
|
|
29
|
+
const text = "output\n\nā BLIND WRITE ā foo\n\nš“ THRASHING ā bar\n\nmore output";
|
|
30
|
+
const result = stripModelWarningParagraphs(text);
|
|
31
|
+
expect(result).not.toContain("BLIND WRITE");
|
|
32
|
+
expect(result).not.toContain("THRASHING");
|
|
33
|
+
expect(result).toContain("output");
|
|
34
|
+
expect(result).toContain("more output");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("returns original reference when nothing stripped", () => {
|
|
38
|
+
const text = "no warnings here";
|
|
39
|
+
expect(stripModelWarningParagraphs(text)).toBe(text);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("trims trailing whitespace after stripping", () => {
|
|
43
|
+
const text = "output\n\nā BLIND WRITE ā foo";
|
|
44
|
+
const result = stripModelWarningParagraphs(text);
|
|
45
|
+
expect(result).toBe("output");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("does not strip mid-paragraph BLIND WRITE mentions", () => {
|
|
49
|
+
// Only strips when the paragraph *starts* with the prefix
|
|
50
|
+
const text = "The BLIND WRITE check is a feature.\n\nsome output";
|
|
51
|
+
expect(stripModelWarningParagraphs(text)).toBe(text);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// āā filterModelWarnings āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
56
|
+
|
|
57
|
+
describe("filterModelWarnings", () => {
|
|
58
|
+
it("returns original array when no warnings present", () => {
|
|
59
|
+
const content = [{ type: "text" as const, text: "clean output" }];
|
|
60
|
+
expect(filterModelWarnings(content)).toBe(content);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("filters BLIND WRITE from text block", () => {
|
|
64
|
+
const content = [
|
|
65
|
+
{
|
|
66
|
+
type: "text" as const,
|
|
67
|
+
text: "Exit code: 0\n\nok\n\nā BLIND WRITE ā editing `x.ts` without reading.",
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
const result = filterModelWarnings(content);
|
|
71
|
+
expect(result).not.toBe(content);
|
|
72
|
+
expect(result[0]?.type).toBe("text");
|
|
73
|
+
if (result[0]?.type === "text") {
|
|
74
|
+
expect((result[0] as { type: string; text: string }).text).not.toContain("BLIND WRITE");
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("leaves image blocks untouched", () => {
|
|
79
|
+
const img = { type: "image", source: { type: "base64", data: "abc" } };
|
|
80
|
+
const content = [img] as never[];
|
|
81
|
+
expect(filterModelWarnings(content)).toBe(content);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("only modifies affected blocks, keeps others by reference", () => {
|
|
85
|
+
const clean = { type: "text" as const, text: "clean" };
|
|
86
|
+
const dirty = {
|
|
87
|
+
type: "text" as const,
|
|
88
|
+
text: "output\n\nā BLIND WRITE ā foo",
|
|
89
|
+
};
|
|
90
|
+
const content = [clean, dirty];
|
|
91
|
+
const result = filterModelWarnings(content);
|
|
92
|
+
// The array is new but clean block is the same reference
|
|
93
|
+
expect(result[0]).toBe(clean);
|
|
94
|
+
expect(result[1]).not.toBe(dirty);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-result-filter.ts ā strip model-guidance text from tool_result content.
|
|
3
|
+
*
|
|
4
|
+
* pi-lens appends diagnostic strings (BLIND WRITE, THRASHING) to tool_result
|
|
5
|
+
* content as guidance for the LLM. These are not useful to the user and clutter
|
|
6
|
+
* the TUI. This module filters them out of content blocks before they render.
|
|
7
|
+
*
|
|
8
|
+
* Patterns are anchored to the start of a paragraph (preceded by \n\n) so we
|
|
9
|
+
* don't accidentally clip real output that happens to contain these strings.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
13
|
+
|
|
14
|
+
export type ToolContent = TextContent | ImageContent;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Paragraph prefixes that identify model-only guidance injected by pi-lens.
|
|
18
|
+
* Each pattern matches from the start of a \n\n-delimited paragraph.
|
|
19
|
+
*/
|
|
20
|
+
const MODEL_WARNING_PREFIXES = ["ā BLIND WRITE ā", "š“ THRASHING ā"] as const;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Strip model-guidance paragraphs from a single text block's content.
|
|
24
|
+
* Returns the original string if nothing was removed.
|
|
25
|
+
*/
|
|
26
|
+
export function stripModelWarningParagraphs(text: string): string {
|
|
27
|
+
// Split on double-newline paragraph boundaries, filter out guidance paragraphs,
|
|
28
|
+
// then rejoin. Trim trailing whitespace from the result.
|
|
29
|
+
const paragraphs = text.split(/\n\n/);
|
|
30
|
+
const kept = paragraphs.filter(
|
|
31
|
+
(p) => !MODEL_WARNING_PREFIXES.some((prefix) => p.trimStart().startsWith(prefix)),
|
|
32
|
+
);
|
|
33
|
+
if (kept.length === paragraphs.length) return text;
|
|
34
|
+
return kept.join("\n\n").trimEnd();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Filter model-guidance warnings from an array of tool content blocks.
|
|
39
|
+
* Returns the original array reference if no blocks were modified.
|
|
40
|
+
*/
|
|
41
|
+
export function filterModelWarnings(content: ToolContent[]): ToolContent[] {
|
|
42
|
+
let changed = false;
|
|
43
|
+
const result = content.map((block) => {
|
|
44
|
+
if (block.type !== "text") return block;
|
|
45
|
+
const text = (block as TextContent).text;
|
|
46
|
+
if (typeof text !== "string") return block;
|
|
47
|
+
const filtered = stripModelWarningParagraphs(text);
|
|
48
|
+
if (filtered === text) return block;
|
|
49
|
+
changed = true;
|
|
50
|
+
return { ...block, text: filtered } as TextContent;
|
|
51
|
+
});
|
|
52
|
+
return changed ? result : content;
|
|
53
|
+
}
|