mirror of
https://github.com/SikongJueluo/pi-extensions.git
synced 2026-10-05 11:52:55 +08:00
fix(ai-judge): truncate advice widget with visible-width awareness
- delegate widget line truncation to pi-tui truncateToWidth so ANSI styling and double-width CJK reasons count visible cells only - replace the code-point clamp that crashed the TUI when a Chinese reason rendered wider than the terminal - add @earendil-works/pi-tui peer and dev dependencies - cover CJK reasons at several terminal widths in tests
This commit is contained in:
@@ -31,11 +31,13 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@earendil-works/pi-ai": "*",
|
"@earendil-works/pi-ai": "*",
|
||||||
"@earendil-works/pi-coding-agent": "*",
|
"@earendil-works/pi-coding-agent": "*",
|
||||||
|
"@earendil-works/pi-tui": "*",
|
||||||
"@gotgenes/pi-permission-system": ">=32.0.0"
|
"@gotgenes/pi-permission-system": ">=32.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@earendil-works/pi-ai": "^0.85.1",
|
"@earendil-works/pi-ai": "^0.85.1",
|
||||||
"@earendil-works/pi-coding-agent": "^0.85.1",
|
"@earendil-works/pi-coding-agent": "^0.85.1",
|
||||||
|
"@earendil-works/pi-tui": "^0.85.1",
|
||||||
"@gotgenes/pi-permission-system": "^32.0.2",
|
"@gotgenes/pi-permission-system": "^32.0.2",
|
||||||
"@types/node": "^26.5.1",
|
"@types/node": "^26.5.1",
|
||||||
"@vitest/coverage-istanbul": "5.0.1",
|
"@vitest/coverage-istanbul": "5.0.1",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
||||||
import type { HighRiskCategory } from "../authority/highrisk";
|
import type { HighRiskCategory } from "../authority/highrisk";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,18 +79,18 @@ function sanitizeLine(text: string, maxChars: number): string {
|
|||||||
return `${chars.slice(0, maxChars - 1).join("")}…`;
|
return `${chars.slice(0, maxChars - 1).join("")}…`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clamp a rendered line to the live terminal width. */
|
/** Clamp a rendered line to the live terminal width.
|
||||||
function clampToWidth(line: string, width: number): string {
|
*
|
||||||
|
* Delegates to pi-tui's ANSI- and wide-char-aware truncation: theme-styled
|
||||||
|
* text counts only visible cells, so CJK reasons cannot overflow the
|
||||||
|
* terminal (the crash class pi-tui reports as "Rendered line exceeds
|
||||||
|
* terminal width").
|
||||||
|
*/
|
||||||
|
function clampToWidth(styledLine: string, width: number): string {
|
||||||
if (width <= 0) {
|
if (width <= 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const chars = [...line];
|
return truncateToWidth(styledLine, width, "…");
|
||||||
if (chars.length <= width) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return width >= 2
|
|
||||||
? `${chars.slice(0, width - 1).join("")}…`
|
|
||||||
: chars.slice(0, width).join("");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdviceLines {
|
export interface AdviceLines {
|
||||||
@@ -168,7 +169,10 @@ export class AdvicePresenter {
|
|||||||
this.ui.setWidget(ADVICE_WIDGET_KEY, (_tui, theme) => ({
|
this.ui.setWidget(ADVICE_WIDGET_KEY, (_tui, theme) => ({
|
||||||
render: (width: number) =>
|
render: (width: number) =>
|
||||||
lines.map((line, index) =>
|
lines.map((line, index) =>
|
||||||
theme.fg(colors[index] ?? "dim", clampToWidth(line, width)),
|
clampToWidth(
|
||||||
|
theme.fg(colors[index] ?? "dim", line),
|
||||||
|
width,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
invalidate: () => {},
|
invalidate: () => {},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
||||||
import {
|
import {
|
||||||
ADVICE_WIDGET_KEY,
|
ADVICE_WIDGET_KEY,
|
||||||
AdvicePresenter,
|
AdvicePresenter,
|
||||||
@@ -8,8 +9,9 @@ import {
|
|||||||
type AdviceWidgetUi,
|
type AdviceWidgetUi,
|
||||||
} from "../src/advice/widget";
|
} from "../src/advice/widget";
|
||||||
|
|
||||||
|
/** ANSI-styled fake: invisible to truncateToWidth, like a real theme. */
|
||||||
const theme: AdviceTheme = {
|
const theme: AdviceTheme = {
|
||||||
fg: (color, text) => `<${color}>${text}</>`,
|
fg: (_color, text) => `\x1b[35m${text}\x1b[0m`,
|
||||||
};
|
};
|
||||||
|
|
||||||
function render(component: AdviceComponent, width = 200): string[] {
|
function render(component: AdviceComponent, width = 200): string[] {
|
||||||
@@ -139,14 +141,38 @@ describe("AdvicePresenter", () => {
|
|||||||
) => AdviceComponent;
|
) => AdviceComponent;
|
||||||
const component = factory({}, theme);
|
const component = factory({}, theme);
|
||||||
expect(render(component, 200)).toEqual([
|
expect(render(component, 200)).toEqual([
|
||||||
"<accent>ai-judge defer — ambiguous intent</>",
|
"\x1b[35mai-judge defer — ambiguous intent\x1b[0m",
|
||||||
]);
|
]);
|
||||||
expect(render(component, 10)[0]).toBe(
|
const clamped = render(component, 10)[0]!;
|
||||||
"<accent>ai-judge …</>",
|
expect(clamped.startsWith("\x1b[35mai-judge ")).toBe(true);
|
||||||
);
|
expect(clamped.endsWith("…"));
|
||||||
|
expect(visibleWidth(clamped)).toBeLessThanOrEqual(10);
|
||||||
component.invalidate();
|
component.invalidate();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("never renders wider than the terminal for CJK reasons", () => {
|
||||||
|
const ui = recordingUi();
|
||||||
|
const presenter = new AdvicePresenter(ui, true);
|
||||||
|
presenter.present("req-1", {
|
||||||
|
state: "judgment",
|
||||||
|
verdict: "defer",
|
||||||
|
reason: "命令会重写已发布的历史记录且用户意图未确立".repeat(10),
|
||||||
|
shadow: false,
|
||||||
|
});
|
||||||
|
const factory = ui.calls[0]!.content as (
|
||||||
|
tui: unknown,
|
||||||
|
theme: AdviceTheme,
|
||||||
|
) => AdviceComponent;
|
||||||
|
const component = factory({}, theme);
|
||||||
|
for (const width of [5, 20, 80, 120]) {
|
||||||
|
const lines = render(component, width);
|
||||||
|
for (const line of lines) {
|
||||||
|
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(visibleWidth(render(component, 20)[0]!)).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
it("is a no-op end to end when disabled", () => {
|
it("is a no-op end to end when disabled", () => {
|
||||||
const ui = recordingUi();
|
const ui = recordingUi();
|
||||||
const presenter = new AdvicePresenter(ui, false);
|
const presenter = new AdvicePresenter(ui, false);
|
||||||
|
|||||||
Generated
+3
@@ -223,6 +223,9 @@ importers:
|
|||||||
'@earendil-works/pi-coding-agent':
|
'@earendil-works/pi-coding-agent':
|
||||||
specifier: ^0.85.1
|
specifier: ^0.85.1
|
||||||
version: 0.85.1(supports-color@7.2.0)(ws@8.21.3)(zod@4.6.5)
|
version: 0.85.1(supports-color@7.2.0)(ws@8.21.3)(zod@4.6.5)
|
||||||
|
'@earendil-works/pi-tui':
|
||||||
|
specifier: ^0.85.1
|
||||||
|
version: 0.85.1
|
||||||
'@gotgenes/pi-permission-system':
|
'@gotgenes/pi-permission-system':
|
||||||
specifier: ^32.0.2
|
specifier: ^32.0.2
|
||||||
version: 32.0.2(@earendil-works/pi-coding-agent@0.85.1)(@earendil-works/pi-tui@0.85.1)
|
version: 32.0.2(@earendil-works/pi-coding-agent@0.85.1)(@earendil-works/pi-tui@0.85.1)
|
||||||
|
|||||||
Reference in New Issue
Block a user