fix(render): use readable profile for dense images

This commit is contained in:
Steven Chong
2026-05-25 15:37:14 -04:00
parent 0e95b81728
commit 09fb339b39
4 changed files with 35 additions and 11 deletions
+5 -9
View File
@@ -37,7 +37,7 @@
*/
import type { ContentBlock, ImageBlock, Message, TextBlock, ToolUseBlock, ToolResultBlock } from './types.js';
import { DENSE_CONTENT_CHARS_PER_IMAGE, renderTextToPngsWithCharLimit, shrinkColsToContent } from './render.js';
import { DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_CONTENT_COLS, DENSE_RENDER_STYLE, renderTextToPngsWithCharLimit } from './render.js';
import { bytesToBase64 } from './png.js';
/** Function predicate signature for the break-even gate. Passed in by the
@@ -341,14 +341,10 @@ export async function collapseHistory(
return { messages, info };
}
// Render. No cache_control here — caller decides placement.
// Width-shrink: history collapse always renders a single image-set of
// collapsed conversation prose, which is typically much narrower than
// the configured `cols` (100 default). Shrinking the canvas to the
// longest wrapped line saves ~50% of pixel area on short collapsed
// history blocks. The gate's `imageTokensCost()` is computed with the
// same `shrinkColsToContent` so prediction matches reality.
const effCols = shrinkColsToContent(text, o.cols);
const imgs = await renderTextToPngsWithCharLimit(text, effCols, DENSE_CONTENT_CHARS_PER_IMAGE);
// Dense history is user-visible context, not the static system-slab cache
// anchor. Render it with the readable dense profile instead of the 313-col
// full-canvas profile; otherwise lockfiles/code/config collapse into pixel mush.
const imgs = await renderTextToPngsWithCharLimit(text, DENSE_CONTENT_COLS, DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_RENDER_STYLE);
if (imgs.length === 0) {
info.reason = 'render_empty';
return { messages, info };
+2
View File
@@ -44,6 +44,8 @@ export const READABLE_CHARS_PER_IMAGE = 50000;
* dense for OCR on lockfiles/JSON/code. Keep those blocks paged into smaller
* images so the model can read them reliably. */
export const DENSE_CONTENT_CHARS_PER_IMAGE = 6000;
export const DENSE_CONTENT_COLS = 180;
export const DENSE_RENDER_STYLE: RenderStyle = { cellWBonus: 2, cellHBonus: 2, aa: true };
/** Default columns per row. 1568 px / 5 px-per-cell = 313 cells. We render
* at the full canvas width by default — no shrink-to-content. */
const DEFAULT_COLS = 313;
+3 -1
View File
@@ -35,6 +35,8 @@ import {
CELL_H,
READABLE_CHARS_PER_IMAGE,
DENSE_CONTENT_CHARS_PER_IMAGE,
DENSE_CONTENT_COLS,
DENSE_RENDER_STYLE,
renderTextToPngsWithCharLimit,
} from './render.js';
import { bytesToBase64 } from './png.js';
@@ -1764,7 +1766,7 @@ async function textToImageBlocks(
const imgs =
effectiveNumCols > 1
? await renderTextToPngsMultiCol(text, effectiveCols, effectiveNumCols)
: await renderTextToPngsWithCharLimit(text, effectiveCols, DENSE_CONTENT_CHARS_PER_IMAGE);
: await renderTextToPngsWithCharLimit(text, DENSE_CONTENT_COLS, DENSE_CONTENT_CHARS_PER_IMAGE, DENSE_RENDER_STYLE);
let droppedChars = 0;
let pixels = 0;
const droppedCodepoints = new Map<number, number>();
+25 -1
View File
@@ -24,7 +24,12 @@ import {
} from '../src/core/transform.js';
import { toTrackEvent } from '../src/core/tracker.js';
import type { ProxyEvent } from '../src/core/proxy.js';
import { DENSE_CONTENT_CHARS_PER_IMAGE } from '../src/core/render.js';
import {
DENSE_CONTENT_CHARS_PER_IMAGE,
DENSE_CONTENT_COLS,
DENSE_RENDER_STYLE,
renderTextToPngsWithCharLimit,
} from '../src/core/render.js';
// Default render config: cols=100, 195 lines/img → ~19,500 chars/img if
// lines fully fill the width. For shorter lines, the budget is dominated
@@ -70,6 +75,25 @@ describe('estimateImageCount', () => {
});
});
describe('dense readable render profile', () => {
it('uses larger glyph cells and multiple readable pages for lockfile-shaped text', async () => {
const lockish = Array.from({ length: 200 }, (_, i) =>
` pkg-${i}@npm:1.${i}.0(peer@npm:^${i}.0.0)(typescript@npm:^5.${i % 10}.0): checksum=${'a'.repeat(24)}`,
).join('\n');
const imgs = await renderTextToPngsWithCharLimit(
lockish,
DENSE_CONTENT_COLS,
DENSE_CONTENT_CHARS_PER_IMAGE,
DENSE_RENDER_STYLE,
);
expect(imgs.length).toBeGreaterThanOrEqual(3);
expect(imgs[0]!.width).toBeGreaterThan(1000);
expect(imgs[0]!.width).toBeLessThanOrEqual(1568);
});
});
describe('classifyContent', () => {
it('flags JSON objects as structured', () => {
const json = JSON.stringify({ foo: 'bar', baz: [1, 2, 3] }, null, 2);