fix(snapcompact): selected silver for cjk auto shape
- Made resolveShapeForText choose silver16-bw for CJK-heavy auto transcripts while preserving explicit variants and unsafe glyph protection. - Added silver16-bw to the snapcompact shape settings submenu and renamed unsupported-glyph warnings. - Covered auto shape selection, explicit variant precedence, unsafe glyph scans, and settings option parity. Fixes #4486
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the `snapcompact.shape` settings UI to expose `silver16-bw` and updated snapcompact renderability warnings to report unsupported glyphs instead of a misleading non-ASCII rate. ([#4486](https://github.com/can1357/oh-my-pi/issues/4486))
|
||||
|
||||
## [16.3.5] - 2026-07-04
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -2268,6 +2268,11 @@ export const SETTINGS_SCHEMA = {
|
||||
description:
|
||||
"8x13 glyphs on an 11x16 cell — extra letter spacing so characters don't merge. Default for Anthropic.",
|
||||
},
|
||||
{
|
||||
value: "silver16-bw",
|
||||
label: "Silver 16, CJK",
|
||||
description: "Embedded Silver TrueType font on a 16px grid for CJK and other non-Latin text.",
|
||||
},
|
||||
{
|
||||
value: "doc-8on16-bw",
|
||||
label: "Doc 8on16, black",
|
||||
|
||||
@@ -9529,11 +9529,11 @@ export class AgentSession {
|
||||
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
||||
this.emitNotice(
|
||||
"warning",
|
||||
`snapcompact disabled: high non-ASCII rate detected (${percent}%). No LLM fallback was attempted.`,
|
||||
`snapcompact disabled: unsupported characters for selected snapcompact font (${percent}%). No LLM fallback was attempted.`,
|
||||
"compaction",
|
||||
);
|
||||
throw new Error(
|
||||
`snapcompact cannot render this conversation locally: high non-ASCII rate detected (${percent}%).`,
|
||||
`snapcompact cannot render this conversation locally: unsupported characters for selected snapcompact font (${percent}%).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -12188,7 +12188,7 @@ export class AgentSession {
|
||||
// + a summary message carrying the imaged archive at FRAME_TOKEN_ESTIMATE
|
||||
// per frame; #computeSnapcompactMaxFrames sizes the frame cap from the
|
||||
// live window so we don't run snapcompact just to overflow every threshold
|
||||
// tick. Any local blocker (non-ASCII transcript, kept-history too large,
|
||||
// tick. Any local blocker (unsupported snapcompact glyphs, kept-history too large,
|
||||
// post-render overflow) downgrades auto maintenance to a context-full LLM
|
||||
// summary instead of wedging the session (#3659) — auto runs the default
|
||||
// strategy on the user's behalf, so a fallback that lets the session keep
|
||||
@@ -12210,11 +12210,11 @@ export class AgentSession {
|
||||
const renderScan = snapcompact.scanRenderability(probeText, { shape });
|
||||
if (!renderScan.isSafe) {
|
||||
const percent = (renderScan.unrenderableRatio * 100).toFixed(1);
|
||||
logger.warn("Snapcompact disabled: high non-ASCII rate detected", {
|
||||
logger.warn("Snapcompact disabled: unsupported characters for selected snapcompact font", {
|
||||
model: this.model?.id,
|
||||
unrenderableRatio: renderScan.unrenderableRatio,
|
||||
});
|
||||
snapcompactBlocker = `snapcompact disabled: high non-ASCII rate detected (${percent}%); using context-full auto-compaction instead.`;
|
||||
snapcompactBlocker = `snapcompact disabled: unsupported characters for selected snapcompact font (${percent}%); using context-full auto-compaction instead.`;
|
||||
} else {
|
||||
const maxFrames = this.#computeSnapcompactMaxFrames(preparation, compactionSettings);
|
||||
if (maxFrames < 1) {
|
||||
|
||||
@@ -524,7 +524,7 @@ describe("AgentSession handoff", () => {
|
||||
(event): event is Extract<AgentSessionEvent, { type: "notice" }> =>
|
||||
event.type === "notice" &&
|
||||
event.source === "compaction" &&
|
||||
event.message.startsWith("snapcompact disabled: high non-ASCII rate detected"),
|
||||
event.message.startsWith("snapcompact disabled: unsupported characters for selected snapcompact font"),
|
||||
);
|
||||
expect(downgradeNotice?.message).toContain("using context-full auto-compaction instead.");
|
||||
});
|
||||
|
||||
@@ -147,8 +147,8 @@ describe("AgentSession auto-snapcompact local-blocker fallback", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("downgrades to context-full when the transcript is too non-ASCII for snapcompact to render", async () => {
|
||||
tempDir = TempDir.createSync("@pi-snapcompact-non-ascii-");
|
||||
it("downgrades to context-full when unsupported glyphs make snapcompact unsafe", async () => {
|
||||
tempDir = TempDir.createSync("@pi-snapcompact-unsupported-glyphs-");
|
||||
authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db"));
|
||||
const harness = await createHarness(tempDir, authStorage, {
|
||||
activeModel: { provider: "aimlapi", id: "claude-sonnet-4-5-20250929" },
|
||||
@@ -167,11 +167,11 @@ describe("AgentSession auto-snapcompact local-blocker fallback", () => {
|
||||
expect(result.action).toBe("context-full");
|
||||
expect(result.errorMessage).toBeUndefined();
|
||||
expect(compactionModule.compact).toHaveBeenCalled();
|
||||
const cjkNotice = harness.notices.find(message =>
|
||||
message.startsWith("snapcompact disabled: high non-ASCII rate detected"),
|
||||
const unsupportedGlyphNotice = harness.notices.find(message =>
|
||||
message.startsWith("snapcompact disabled: unsupported characters for selected snapcompact font"),
|
||||
);
|
||||
expect(cjkNotice).toBeDefined();
|
||||
expect(cjkNotice).toContain("using context-full auto-compaction instead.");
|
||||
expect(unsupportedGlyphNotice).toBeDefined();
|
||||
expect(unsupportedGlyphNotice).toContain("using context-full auto-compaction instead.");
|
||||
expect(harness.sessionManager.getBranch().find(entry => entry.type === "compaction")).toMatchObject({
|
||||
type: "compaction",
|
||||
summary: "compacted",
|
||||
|
||||
@@ -70,6 +70,16 @@ describe("settings layout", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes every accepted snapcompact shape in the settings submenu", () => {
|
||||
const def = getSettingsForTab("context").find(def => def.path === "snapcompact.shape");
|
||||
|
||||
expect(def?.type).toBe("submenu");
|
||||
if (def?.type !== "submenu") throw new Error("snapcompact.shape should render as a submenu");
|
||||
const values = def.options.map(option => option.value);
|
||||
expect(values).toContain("silver16-bw");
|
||||
expect(values).toEqual([...SETTINGS_SCHEMA["snapcompact.shape"].values]);
|
||||
});
|
||||
|
||||
it("hides advisor dependent settings when advisor is disabled", () => {
|
||||
const advisorDependentPaths: SettingPath[] = ["advisor.subagents", "advisor.syncBacklog", "advisor.immuneTurns"];
|
||||
const advisorDependentPathSet = new Set(advisorDependentPaths);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `resolveShapeForText(..., "auto")` to select the existing `silver16-bw` shape for CJK-heavy transcript text while preserving explicit shape overrides. ([#4486](https://github.com/can1357/oh-my-pi/issues/4486))
|
||||
|
||||
## [16.2.8] - 2026-06-30
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -385,15 +385,39 @@ export function resolveShape(model?: ShapeTarget, variant?: ShapeVariantName | "
|
||||
return priceShape(ideal?.frameSize ? { ...base, frameSize: ideal.frameSize } : base, family);
|
||||
}
|
||||
|
||||
const CJK_HEAVY_MIN_WIDE_CHARS = 8;
|
||||
const CJK_HEAVY_WIDE_RATIO = 0.25;
|
||||
|
||||
function isCjkHeavyText(text: string): boolean {
|
||||
const chars = normalizedInputChars(text);
|
||||
let graphicChars = 0;
|
||||
let wideChars = 0;
|
||||
for (const ch of chars) {
|
||||
if (ch === " " || ch === DIM_ON || ch === DIM_OFF || ch === NEWLINE_GLYPH) continue;
|
||||
const cp = ch.codePointAt(0);
|
||||
if (cp === undefined || UNRENDERABLE.test(ch)) continue;
|
||||
graphicChars++;
|
||||
if (isWideCodePoint(cp)) wideChars++;
|
||||
}
|
||||
return wideChars >= CJK_HEAVY_MIN_WIDE_CHARS && wideChars / graphicChars >= CJK_HEAVY_WIDE_RATIO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the frame shape for `text` without changing the selected shape.
|
||||
*
|
||||
* Glyph-level Silver fallback happens during normalization/rendering, so this
|
||||
* helper exists for callers that need a text-aware API name while preserving
|
||||
* explicit and provider-selected shapes.
|
||||
* Pick the frame shape for `text`. Explicit variants remain forced. Auto first
|
||||
* resolves the model/provider default, then selects the Silver CJK grid when
|
||||
* the default font cannot safely render the text or wide CJK glyphs dominate
|
||||
* the transcript and Silver can render it safely.
|
||||
*/
|
||||
export function resolveShapeForText(_text: string, model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape {
|
||||
return resolveShape(model, variant);
|
||||
export function resolveShapeForText(text: string, model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape {
|
||||
const shape = resolveShape(model, variant);
|
||||
if (variant && variant !== "auto") return shape;
|
||||
const silver = resolveShape(model, "silver16-bw");
|
||||
if (!scanRenderability(text, { shape }).isSafe) {
|
||||
return scanRenderability(text, { shape: silver }).isSafe ? silver : shape;
|
||||
}
|
||||
return shape.font !== "silver" && isCjkHeavyText(text) && scanRenderability(text, { shape: silver }).isSafe
|
||||
? silver
|
||||
: shape;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -372,15 +372,45 @@ describe("shape resolution", () => {
|
||||
expect(snapcompact.isShape({ ...snapcompact.SHAPES.openai, imageDetail: "original" })).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps selected shapes while Silver fallback covers non-Latin glyphs", () => {
|
||||
const text = "こんにちは 你好 안녕";
|
||||
it("keeps bitmap shapes render-safe via Silver fallback while CJK-heavy auto archives use Silver", () => {
|
||||
const cjkHeavyText = "こんにちは 你好 안녕 世界 한국어";
|
||||
const silver = snapcompact.resolveShape({ api: "anthropic-messages" }, "silver16-bw");
|
||||
|
||||
expect(snapcompact.scanRenderability(cjkHeavyText).isSafe).toBe(true);
|
||||
expect(snapcompact.scanRenderability(cjkHeavyText, { shape: silver }).isSafe).toBe(true);
|
||||
expect(snapcompact.normalize(cjkHeavyText)).toBe(cjkHeavyText);
|
||||
expect(snapcompact.normalize(cjkHeavyText, { shape: silver })).toBe(cjkHeavyText);
|
||||
expect(snapcompact.resolveShapeForText(cjkHeavyText, { api: "anthropic-messages" }, "auto")).toEqual(silver);
|
||||
});
|
||||
|
||||
it("keeps ASCII-heavy auto archives on the provider/model shape", () => {
|
||||
const model = { api: "openai-responses" as const, id: "gpt-5.5" };
|
||||
const expected = snapcompact.resolveShape(model, "auto");
|
||||
const actual = snapcompact.resolveShapeForText(
|
||||
"function render(value: string) { return value.trim().toLowerCase(); }",
|
||||
model,
|
||||
"auto",
|
||||
);
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(actual.font).not.toBe("silver");
|
||||
});
|
||||
|
||||
it("respects explicit non-auto variants even for CJK-heavy text", () => {
|
||||
const model = { api: "anthropic-messages" as const };
|
||||
const expected = snapcompact.resolveShape(model, "8on16-bw");
|
||||
const actual = snapcompact.resolveShapeForText("こんにちは 你好 안녕 世界 한국어", model, "8on16-bw");
|
||||
|
||||
expect(actual).toEqual(expected);
|
||||
expect(actual.font).not.toBe("silver");
|
||||
});
|
||||
|
||||
it("reports unsupported CJK ideographs unsafe even with the Silver shape selected", () => {
|
||||
const silver = snapcompact.resolveShape(undefined, "silver16-bw");
|
||||
expect(snapcompact.scanRenderability(text).isSafe).toBe(true);
|
||||
expect(snapcompact.scanRenderability(text, { shape: silver }).isSafe).toBe(true);
|
||||
expect(snapcompact.normalize(text)).toBe(text);
|
||||
expect(snapcompact.normalize(text, { shape: silver })).toBe(text);
|
||||
expect(snapcompact.resolveShapeForText(text).font).not.toBe("silver");
|
||||
expect(snapcompact.resolveShapeForText("plain ascii").font).not.toBe("silver");
|
||||
const res = snapcompact.scanRenderability("\u{31350}".repeat(12), { shape: silver });
|
||||
|
||||
expect(res.isSafe).toBe(false);
|
||||
expect(res.unrenderableRatio).toBe(1);
|
||||
});
|
||||
|
||||
it("images forwards the per-frame detail hint", () => {
|
||||
|
||||
Reference in New Issue
Block a user