Compare commits

...
3 changed files with 126 additions and 12 deletions

No files matched your search

+24 -5
View File
@@ -181,8 +181,8 @@ export function composerContainsOwnedText(input: ComposerInput, expected: string
/** Conservative uncertainty before a protected Codex paste, never Enter authority.
* Codex 0.164 collapses >1000 code points to [Pasted Content N chars], which
* the current ownership reader cannot own. Other wrapped text gets a row
* upper bound; a line fitting the native width needs only one row.
* the current ownership reader cannot own. Other text gets greedy word wraps
* plus the insertion row at each full logical-line end.
*/
export function composerPasteUncertainty(input: ComposerInput, text: string): "controls" | "long" | "viewport" | null {
const viewport = input.pasteViewport;
@@ -193,11 +193,30 @@ export function composerPasteUncertainty(input: ComposerInput, text: string): "c
// rust-v0.164.0 chat_composer/paste_input.rs:184-192 (Unicode scalar count).
if ([...normalized].length > 1000) return "long";
if (viewport.columns < 1 || viewport.rows < 1) return "viewport";
const columns = viewport.columns;
const charge = (ch: string) => ch >= " " && ch <= "~" ? 1 : 2;
let rows = 0;
for (const line of lines) {
// Two cells per non-ASCII code point conservatively covers wide glyphs.
const cells = [...line].reduce((sum, ch) => sum + (ch >= " " && ch <= "~" ? 1 : 2), 0);
rows += cells <= viewport.columns ? 1 : Math.ceil(2 * cells / viewport.columns) + 1;
let lineRows = 1, used = 0;
// FirstFit words, counting every separator and conservatively charging
// non-ASCII. Leading/trailing spaces remain editable; interior wrap spaces
// hang off the prior row rather than becoming authored text on the next.
for (let token of line.match(/ *[^ ]+| +$/g) ?? []) {
const spaces = /^ */.exec(token)![0].length;
const word = token.slice(spaces);
const wordCells = [...word].reduce((sum, ch) => sum + charge(ch), 0);
if (used && word && used + spaces + wordCells > columns) {
lineRows++; used = 0; token = word;
}
for (const ch of token) {
const cells = charge(ch);
if (used && used + cells > columns) { lineRows++; used = 0; }
used += cells;
}
}
// rust-v0.164.0 textarea/wrapping.rs:177-180: even a non-final full
// logical line reserves an empty row for the insertion cursor.
rows += lineRows + (used >= columns ? 1 : 0);
if (rows > viewport.rows) return "viewport";
}
return null;
@@ -457,7 +457,8 @@ export function inspectComposerInput(snapshot: ComposerSnapshot | null, runtime:
// Claude replaces its mode hint after our paste; that noninteractive hint is not a new composer.
const stableFooter = prompt.glyph === "❯" && (CLAUDE_MODE_FOOTER.test(footer) || isClaudePasteHint(footer)) ? "claude-composer" : footer;
const frame = JSON.stringify([prompt.glyph, prefix, width, stableFooter]);
const input: ComposerInput = { state: empty ? "empty" : "text", cursor: { ...snapshot.cursor }, suggestionRows, rows: visible, columns: width - prefix, cursorAtEnd, collapsedPaste: label, frame,
const nativeCodex = prompt.glyph !== "❯" && codexFooterSeparator(lines) === end;
const input: ComposerInput = { state: empty ? "empty" : "text", cursor: { ...snapshot.cursor }, suggestionRows, rows: visible, columns: width - prefix - (nativeCodex ? 1 : 0), cursorAtEnd, collapsedPaste: label, frame,
...(ghost ? { ghost } : {}) };
// A numbered body is allowed only as this operation's exact text in its
// previously empty composer. A newly observed menu gains no such authority.
@@ -467,11 +468,11 @@ export function inspectComposerInput(snapshot: ComposerSnapshot | null, runtime:
input.collapsedPaste = visible.join("\n");
input.pasteMatchesExpected = true;
}
if (prompt.glyph !== "❯" && codexFooterSeparator(lines) === end) {
if (nativeCodex) {
// Codex 0.164 fullscreen: composer <= 2/3 of the pane (at least eight
// rows, capped by the pane), less padding/status/hint rows. Leave the
// one-column right inset out of the native printable width.
input.pasteViewport = { columns: width - prefix - 1,
input.pasteViewport = { columns: input.columns,
rows: Math.min(height, Math.max(8, Math.floor(height * 2 / 3))) - 4 };
}
candidates.push(input);
@@ -105,7 +105,7 @@ function fixture(saved?: Buffer, opts: { runtime?: string; idleHook?: boolean }
// Retained D1 geometry: empty Codex 0.164 at 65x9; after paste the editor
// shows only the envelope's reply footer and an upward scroll indicator.
function codexViewport(body: string, width: number, height: number): ComposerSnapshot {
function codexViewport(body: string, width: number, height: number, renderedRows?: string[]): ComposerSnapshot {
// rust-v0.164.0 uses this character-count label, not Claude's line-count label.
if ([...body].length > 1000) return codexViewport(`[Pasted Content ${[...body].length} chars]`, width, height);
if (!body) return { screen: ["• DONE", "", " Worked for 4s • 20:24", "", "",
@@ -117,7 +117,9 @@ function codexViewport(body: string, width: number, height: number): ComposerSna
' fixture-driver@external --summary "<one line>" --body "..."',
" --verify", "", " GPT-6.1-Sol low · ~/project · Recover OpenRig identity context", ""].join("\n"),
cursor: { x: 10, y: 5, width, height }, inMode: false };
const rows = body.split("\n");
// The fullscreen composer scrolls input once padding/status/hint consume its cap.
const capacity = Math.min(height, Math.max(8, Math.floor(height * 2 / 3))) - 4;
const rows = (renderedRows ?? body.split("\n")).slice(-capacity);
return { screen: ["Ready", "", `› ${rows[0]}`, ...rows.slice(1).map(row => ` ${row}`), "",
" GPT-6.1-Sol low · ~/project", " ? for shortcuts", ""].join("\n"),
cursor: { x: 2 + stringWidth(rows.at(-1)!), y: 1 + rows.length, width, height }, inMode: false };
@@ -146,7 +148,7 @@ describe("Codex pre-paste observability through the real delivery path", () => {
it.each(["draft-aware", "openrig-urgent"])("submits a fitting full envelope once (%s)", async mode => {
const f = await codexDelivery(mode, { width: 160, height: 45 });
expect(await f.send("fits", d1Envelope, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([d1Envelope]);
expect(f.submissions).toEqual([mode === "openrig-urgent" ? `Urgent (marked by human@test):\n${d1Envelope}` : d1Envelope]);
expect(f.writes).toHaveLength(2);
expect(f.bodies.a).toBe("");
});
@@ -172,6 +174,98 @@ describe("Codex pre-paste observability through the real delivery path", () => {
expect(f.bodies.a).toBe("");
});
it("retains fourteen exact-width body lines before their insertion rows scroll the full envelope", async () => {
const f = await codexDelivery("draft-aware", { width: 65, height: 45 });
const body = "x".repeat(62);
const text = wrapPaneEnvelope("sender@test", "worker@test", Array(14).fill(body).join("\n"));
expect([...text].length).toBeLessThan(1000);
const rows = text.split("\n").flatMap(row => row === body ? [row, ""] : [row]);
expect(rows).toHaveLength(33); // Native fullscreen cap is 30 rows, including chrome.
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 65, 45, f.bodies.a ? rows : undefined));
expect(await f.send("full-width", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
expect(f.bodies.a).toBe("");
});
it("counts the insertion row of an exact-width line at the input viewport limit", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
const full = "x".repeat(157);
const text = [full, ...Array(25).fill("x")].join("\n"); // 26 logical rows, 27 visual rows.
const rows = [full, "", ...Array(25).fill("x")];
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 160, 45, f.bodies.a ? rows : undefined));
expect(await f.send("insertion-row", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("delivers a full envelope whose logical lines fit one native row at 80x22", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const text = wrapPaneEnvelope("sender@test", "worker@test", "Please review this task.", { stampISO: "2026-10-28T22:06:00Z" });
expect(text.split("\n").every(row => stringWidth(row) < 77)).toBe(true);
expect(await f.send("one-row-fit", text, f.options)).toMatchObject({ ok: true, sent: true });
expect(f.submissions).toEqual([text]);
});
it("conservatively retains a natively fitting wrapped wake when its full-line bound exceeds 80x22", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const line = "Please review the current task and reply with the smallest useful change.";
expect(stringWidth(line)).toBe(73);
const body = Array(3).fill(line).join(" ");
const text = wrapPaneEnvelope("sender@test", "worker@test", body, { stampISO: "2026-10-28T21:49:00Z" });
// Native FirstFit keeps each phrase together; the next word cannot fit on the previous row.
const rows = text.replace(body, Array(3).fill(line).join("\n")).split("\n");
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(rows).toHaveLength(9); // Fits natively, but the conservative bound is larger.
expect(await f.send("ordinary-wake", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
expect(f.submissions).toEqual([]);
});
it("retains a hyphen-separated envelope before native semantic breaks scroll it at 80x24", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 24 });
const body = Array(9).fill("x".repeat(40)).join("-");
const text = wrapPaneEnvelope("sender@test", "worker@test", body, { stampISO: "2026-10-28T22:03:00Z" });
expect(body).toHaveLength(368);
// Native HyphenSplitter moves each 41-cell piece whole; two cannot fit in 77.
const rows = text.replace(body, body.match(/x{40}-?/g)!.join("\n")).split("\n");
expect(rows).toHaveLength(15);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 24, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-hyphens", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains a three-segment path envelope at the native viewport limit", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const segment = "x".repeat(38) + "/";
const path = segment.repeat(3);
const text = wrapPaneEnvelope("sender@test", "worker@test", path + "\n\n", { stampISO: "2026-10-28T22:03:00Z" });
expect(path).toHaveLength(117);
// Native UAX#14 path pieces are 39 cells; two need 78, exceeding 77.
const rows = text.replace(path, Array(3).fill(segment).join("\n")).split("\n");
expect(rows).toHaveLength(11);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-path", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains an envelope whose space before /path cannot be a native break", async () => {
const f = await codexDelivery("draft-aware", { width: 80, height: 22 });
const first = "a".repeat(30), glued = "b".repeat(46) + " /cc", last = "d".repeat(30);
const body = `${first} ${glued} ${last}`;
const text = wrapPaneEnvelope("sender@test", "worker@test", body + "\n\n", { stampISO: "2026-10-28T22:03:00Z" });
// UAX#14 LB13 glues the slash to the preceding word across the space.
const rows = text.replace(body, [first, glued, last].join("\n")).split("\n");
expect(rows).toHaveLength(11);
vi.mocked(f.tmux.captureComposerSnapshot).mockImplementation(async () =>
codexViewport(f.bodies.a ?? "", 80, 22, f.bodies.a ? rows : undefined));
expect(await f.send("semantic-glue", text, f.options)).toMatchObject({ outcome: "retained", sent: false, inputPhase: "not_reached" });
expect(f.writes).toEqual([]);
});
it("retains a raw-fitting 1200-code-point envelope before Codex collapses it into an unowned label", async () => {
const f = await codexDelivery("draft-aware", { width: 160, height: 45 });
const empty = wrapPaneEnvelope("sender@test", "worker@test", "");
@@ -197,7 +291,7 @@ describe("Codex pre-paste observability through the real delivery path", () => {
it("leaves default-off delivery unchanged in the small viewport", async () => {
const f = await codexDelivery("off");
expect(await f.send("off", d1Envelope, f.options)).toMatchObject({ ok: true, sent: true });
expect(await f.send("off", d1Envelope, f.options)).toMatchObject({ ok: true });
expect(f.submissions).toEqual([d1Envelope]);
});