mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(delivery): accept Claude's wrap points, which fall short of the input box (#2436)
A long one-line delivery to a Claude seat is now submitted when Claude wraps it short of the input box's width. The check that the input holds only the delivery's own text accepts Claude's wrap points, up to 6 columns short, and each row must still match exactly. Codex is unchanged. Reported in #2429 by @billbenetti.
This commit is contained in:
1 parent
e3c461a7e3
commit
1593295db6
3 files changed
+21
-3
No files matched your search
@@ -137,6 +137,8 @@ export interface ComposerInput {
|
||||
state: "empty" | "text" | "unknown";
|
||||
rows: string[];
|
||||
columns: number;
|
||||
/** How many columns short of `columns` the harness may wrap its text: a row boundary at a width in that range is a wrap. */
|
||||
wrapSlack?: number;
|
||||
/** Conservative visible capacity of the recognized Codex fullscreen composer. */
|
||||
pasteViewport?: { columns: number; rows: number };
|
||||
cursor?: ComposerSnapshot["cursor"];
|
||||
@@ -158,6 +160,7 @@ export const COMPOSER_COLLAPSED_PASTE_PATTERN = /^\[Pasted text #\d+ \+(\d+) lin
|
||||
export function composerContainsOwnedText(input: ComposerInput, expected: string): boolean {
|
||||
if (input.state !== "text" || !input.cursorAtEnd || input.collapsedPaste || !input.rows.length) return false;
|
||||
const text = expected.replace(/\r\n/g, "\n");
|
||||
const narrowest = input.columns - (input.wrapSlack ?? 0);
|
||||
let offsets = new Set([0]);
|
||||
for (let index = 0; index < input.rows.length; index++) {
|
||||
const row = input.rows[index]!;
|
||||
@@ -167,10 +170,10 @@ export function composerContainsOwnedText(input: ComposerInput, expected: string
|
||||
const end = offset + row.length;
|
||||
if (index === input.rows.length - 1) { if (end === text.length) return true; continue; }
|
||||
if (text[end] === "\n") next.add(end + 1);
|
||||
if (stringWidth(row) >= input.columns) next.add(end);
|
||||
if (stringWidth(row) >= narrowest) next.add(end);
|
||||
if (text[end] === " ") {
|
||||
const word = /^\S+/.exec(text.slice(end + 1))?.[0] ?? "";
|
||||
if (word && stringWidth(row + " " + word) > input.columns) next.add(end + 1);
|
||||
if (word && stringWidth(row + " " + word) > narrowest) next.add(end + 1);
|
||||
}
|
||||
}
|
||||
offsets = next;
|
||||
|
||||
@@ -103,6 +103,9 @@ const PROMPT_DRAFT_PATTERNS = [
|
||||
// Claude's permission-mode footers: default, accept edits, bypass, auto and plan, optionally after a
|
||||
// vim-mode marker such as "-- INSERT --" (#808). A suffix such as "· 1 shell" can follow the mode.
|
||||
const CLAUDE_MODE_FOOTER = /^(?:-- [A-Z]+ -- )?(?:⏵⏵ (?:accept edits|bypass permissions|auto mode) on\b|⏸ plan mode on\b|\? for shortcuts\b)/;
|
||||
// Claude Code wraps its input short of the box (#2429). In its native 2.1.296 wrapped frame (test fixtures/input-box) a
|
||||
// 118-column box wraps a 112-column row before a word that would still fit, so its text width there is 112 to 117.
|
||||
const CLAUDE_WRAP_SLACK = 6;
|
||||
|
||||
// Sibling pattern identical to CLAUDE_MODE_FOOTER except that it ends in \? for shortcuts$,
|
||||
// keeping unframed-composer and selectionLines checks anchored on the default footer (#808).
|
||||
@@ -464,7 +467,7 @@ export function inspectComposerInput(snapshot: ComposerSnapshot | null, runtime:
|
||||
const stableFooter = prompt.glyph === "❯" && (CLAUDE_MODE_FOOTER.test(footer) || isClaudePasteHint(footer)) ? "claude-composer" : footer;
|
||||
const frame = JSON.stringify([prompt.glyph, prefix, width, stableFooter]);
|
||||
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,
|
||||
const input: ComposerInput = { state: empty ? "empty" : "text", cursor: { ...snapshot.cursor }, suggestionRows, rows: visible, columns: width - prefix - (nativeCodex ? 1 : 0), ...(prompt.glyph === "❯" ? { wrapSlack: CLAUDE_WRAP_SLACK } : {}), 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.
|
||||
|
||||
@@ -165,6 +165,18 @@ describe("cursor-bound composer input", () => {
|
||||
expect(composerContainsOwnedText(inspectComposerInput(claude("one\ntwo")), "one two")).toBe(false);
|
||||
});
|
||||
|
||||
it("finds a delivery's own text in Claude's native wrapped rows, which wrap short of the box (#2429)", () => {
|
||||
const fixtures = join(import.meta.dirname, "fixtures/input-box");
|
||||
const lines = readFileSync(join(fixtures, "claude-code-2.1.296/staged-wrapped.raw.txt"), "utf8").replace(/\n$/, "").split("\n");
|
||||
const text = readFileSync(join(fixtures, "texts/wrapped.txt"), "utf8").replace(/\n$/, "");
|
||||
const last = lines.findLastIndex(line => line.startsWith("❯")) + 2;
|
||||
const input = inspectComposerInput({ screen: lines.join("\n"), cursor: { x: lines[last]!.trimEnd().length, y: last, width: 120, height: lines.length }, inMode: false });
|
||||
expect(input).toMatchObject({ state: "text", cursorAtEnd: true });
|
||||
expect(input.rows).toHaveLength(3);
|
||||
expect(composerContainsOwnedText(input, text)).toBe(true);
|
||||
expect(composerContainsOwnedText(input, text.replace("asked", "ASKED"))).toBe(false);
|
||||
});
|
||||
|
||||
it("matches Unicode cell widths and treats an opaque paste as separately owned evidence", () => {
|
||||
const input = inspectComposerInput(claude("你好世界\nagain", { x: 7, y: 3 }, 10));
|
||||
expect(composerContainsOwnedText(input, "你好世界again")).toBe(true);
|
||||
|
||||
Reference in new issue
Block a user