From 82c571f777b6758f6dec7600406674845433dbdd Mon Sep 17 00:00:00 2001 From: gggodlin <26342074+GGGODLIN@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:17:21 +0800 Subject: [PATCH 1/4] fix(grok): a fresh pane's input box is usable under the welcome card and the API-key chip Grok Build 1.0.52 puts its changelog welcome card directly on the composer, with no Tip row between, and signed in with an API key prints 'Logged in with API key | [stable]' under the box. The first failed the 'unknown rounded widget above the box' fail-closed, the second the literal chip check. The card is now named by its startup menu (New worktree, Resume session, Quit), and only that literal API-key chip form is added. One live capture (2026-10-15, cwd swapped). Canary on grok 1.0.52: idle now passes, drafts 15/15, start-exit 4/4, bar 2/2. Not fixed: narrow at 50 columns, where the row reads 'Logged in with API key | Grok Build 1.0.52 [s' cut off, which no literal check can bless. Co-Authored-By: Claude --- web/src/fixtures/panes/README.md | 1 + web/src/fixtures/panes/grok--welcome-card.txt | 40 +++++++++++++++++++ web/src/lib/harness/grok.test.ts | 10 +++++ web/src/lib/harness/grok/chrome.ts | 23 ++++++++++- web/src/lib/harness/grok/markers.ts | 7 +++- 5 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 web/src/fixtures/panes/grok--welcome-card.txt diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index 2b5570baf..93e43fef1 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -519,6 +519,7 @@ Grok's composer is a rounded box at the tail: `╭─…─╮` / `│ ❯ … | `grok--output-scrollbar.txt` | Grok Build 1.0.46 on macOS, 2026-10-08: tail of the live browser-send capture, from the code reply through the composer, byte-faithful except one status line, whose text was replaced with neutral text of the same width; its ANSI styling is unchanged. The code keeps its rgb(28,28,28) surface; the right-edge track is a separate `█` segment with foreground and background rgb(25,25,25), preceded by uncoloured rgb(20,20,20) canvas padding. A dark block following coloured message text must remain. Its empty viewport rows and full terminal-width padding must not inflate the wrapped phone mirror. Original screen rows remain intact for grammar and guard probes | `done` | | `grok--working.txt` | Mid-turn; empty box; working hint row under the box | `working` | | `grok--startup.txt` | Fresh-session welcome screen: banner box (logo, menu) above an idle composer whose under-box row is the bare `[stable]` channel chip, not the hint bar. composerReady must be TRUE. Byte-faithful `format:ansi` 2026-08-22 | `idle` | +| `grok--welcome-card.txt` | Grok Build 1.0.52, signed in with an API key: the changelog welcome card sits right on top of the composer with no `Tip:` row between, and the under-box row reads `Logged in with API key │ [stable]`. composerReady must be TRUE. Visible-screen `format:ansi` read 2026-10-15 in a throwaway Herdr session; the cwd row became `/tmp/collie-grok-sandbox` and the model label `mini` | `idle` | | `grok--done.txt` | Square user-message bubble ABOVE an idle composer — the bubble must survive the strip | `idle` | | `grok--user-bubble.txt` | Torn frame: square bubble, no composer. `locateComposer` must return null | — | | `grok--permission-rm.txt` | Bash `rm` permission card at the tail; `●` on option 1 (always-approve). Composer replaced. Byte-faithful `format:ansi` 2026-08-21 | `blocked` | diff --git a/web/src/fixtures/panes/grok--welcome-card.txt b/web/src/fixtures/panes/grok--welcome-card.txt new file mode 100644 index 000000000..2bc442c5e --- /dev/null +++ b/web/src/fixtures/panes/grok--welcome-card.txt @@ -0,0 +1,40 @@ +  + /tmp/collie-grok-sandbox  +  +  +  +  +  + ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────╮  + │ │  + │ ⠀⠀⠀⠀⠀⠀⣀⣀⡀⠀⠀⠀⢀⠄ Grok Build 1.0.52 │  + │ ⠀⠀⠀⣠⣾⠿⠛⠛⠛⠛⢀⡴⠁⠀ │  + │ ⠀⠀⣼⡟⠁⠀⠀⠀⢀⡴⠻⣿⡀⠀ Changelog │  + │ ⠀⠀⣿⡇⠀⠀⠀⠔⠁⠀⠀⣿⡇⠀ │  + │ ⠀⠀⢹⣷⠀⠀⠀⠀⠀⢀⣴⡿⠀⠀  • Rex terminal support: Grok now reports its status (working, waiting for you, done, or… │  + │ ⠀⢀⠞⠁⠠⢶⣶⣶⣶⠿⠋⠀⠀⠀  • Tool call density improvements: expanded tool call groups now stay expanded as new ca… │  + │ ⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀  • Grep searches that time out or fail now show an error instead of appearing as empty r… │  + │ │  + │ New worktree ctrl+w │  + │ Resume session ctrl+r │  + │ Changelog │  + │ Quit ctrl+q │  + │ │  + ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────╯  +  +  +  +  +  +  +  +  +  +  +  + ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮  + │ ❯  │  + ╰────────────────────────────────────────────────────────────────────────────────────────────────────────── mini ─╯  +  + Logged in with API key │ [stable]  +  \ No newline at end of file diff --git a/web/src/lib/harness/grok.test.ts b/web/src/lib/harness/grok.test.ts index 279bbfdf6..a0707cb22 100644 --- a/web/src/lib/harness/grok.test.ts +++ b/web/src/lib/harness/grok.test.ts @@ -64,6 +64,7 @@ const PINNED = [ "grok--reporter-294-draft-newline-hint.txt", "grok--startup.txt", "grok--user-bubble.txt", + "grok--welcome-card.txt", "grok--working.txt", ]; @@ -553,6 +554,15 @@ describe("grokBuildBlocks", () => { expect(text).toContain("Grok 4.6 is here!"); }); + it("Grok 1.0.52's welcome card right on top of the box does not hold the keyboard", () => { + // The changelog card comes with no tip row under it, so it sits directly on the composer, where + // an unknown rounded widget would fail closed. Typing lands in the box and the card goes (live, + // 2026-10-15). + const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--welcome-card.txt"), "utf8"))); + expect(grokAdapter.composerReady!(lines)).toBe(true); + expect(grokAdapter.extractInputDraft(lines)).toBeNull(); + }); + it("blank rows render unpainted in every block; glyph rows keep their backgrounds", () => { const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--permission-edit.txt"), "utf8"))); // The live frame paints base-coat colour across fully blank rows (the gray-stripe bug) and diff --git a/web/src/lib/harness/grok/chrome.ts b/web/src/lib/harness/grok/chrome.ts index 2873a3793..f42d7e7ab 100644 --- a/web/src/lib/harness/grok/chrome.ts +++ b/web/src/lib/harness/grok/chrome.ts @@ -319,6 +319,27 @@ export function hasComposer(lines: StyledLine[]): boolean { // not captured yet. const USER_BUBBLE_EDGE = /^\s*[┌└]/; +// Grok's welcome card: a rounded box holding the logo and its startup menu, `New worktree`, +// `Resume session`, `Changelog`, `Quit` (every startup capture since 1.0.4x). It does not own the +// keyboard: typing lands in the box below and the card goes (live, Grok Build 1.0.52, 2026-10-15). +// Older builds printed a `Tip:` row between it and the box; the 1.0.52 changelog card sits right on +// top of the box, where an unknown rounded widget fails closed, so it is named here. +const WELCOME_MENU = [/\bNew worktree\b/, /\bResume session\b/, /\bQuit\b/]; +const ROUNDED_BOTTOM = /^\s*╰/; +const ROUNDED_TOP = /^\s*╭/; +const WELCOME_SCAN_ROWS = 40; + +/** Whether the rounded box closing on row `bottom` is Grok's welcome card. */ +function closesWelcomeCard(texts: string[], bottom: number): boolean { + if (!ROUNDED_BOTTOM.test(texts[bottom]!)) return false; + for (let top = bottom - 1; top >= 0 && bottom - top <= WELCOME_SCAN_ROWS; top--) { + if (!ROUNDED_TOP.test(texts[top]!)) continue; + const inner = texts.slice(top + 1, bottom); + return WELCOME_MENU.every((row) => inner.some((t) => row.test(t))); + } + return false; +} + /** * Whether a phone reply would reach Grok's composer rather than a widget sitting on top of it. * @@ -339,7 +360,7 @@ export function composerReady(lines: StyledLine[]): boolean { const t = texts[i]!; if (isBlank(t)) continue; if (USER_BUBBLE_EDGE.test(t)) return true; - if (opensBox(t)) return false; + if (opensBox(t)) return closesWelcomeCard(texts, i); return true; } return true; diff --git a/web/src/lib/harness/grok/markers.ts b/web/src/lib/harness/grok/markers.ts index 6dfbd4768..072ffb354 100644 --- a/web/src/lib/harness/grok/markers.ts +++ b/web/src/lib/harness/grok/markers.ts @@ -142,9 +142,12 @@ export function isComposerHint(text: string): boolean { // qualifies: a looser bracket-run pattern would bless torn transcript ending in a bracket tag // (`[waiting]`, `[ERROR]`, …) — reproduced in review — and reopen exactly the hazard the // refusal guards. A new channel chip fails closed until a capture blesses it. -const CHIP_ROW = /^\[stable\]$/; +// +// Grok Build 1.0.52 signed in with an API key prints that fact before the chip, `Logged in with API +// key │ [stable]` (live, 2026-10-15, grok--welcome-card.txt). Only that literal phrase is added. +const CHIP_ROW = /^(?:Logged in with API key\s+│\s+)?\[stable\]$/; -/** True when the row is exactly the startup screen's `[stable]` channel chip. */ +/** True when the row is exactly the startup screen's `[stable]` channel chip, or its API-key form. */ export function isStatusChipRow(text: string): boolean { return CHIP_ROW.test(rstrip(text).trimStart()); } From 0584ad03bc95ec685612181baf7050b122a62349 Mon Sep 17 00:00:00 2001 From: gggodlin <26342074+GGGODLIN@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:32:46 +0800 Subject: [PATCH 2/4] fix(grok): the slash-command popup is part of the input box, and a partial command does not verify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grok 1.0.52 paints its / completion between two rules directly on the composer, which the 'unknown widget above the box' fail-closed read as owning the keyboard; a bar or palette send went through only when the guard's read beat the popup. A rule-bounded run holding a '❯ /' row is now the popup, and the draft folds in the highlighted command when it is not the typed one, since Enter runs the highlighted row. One live capture and one hand-built partial (README). Live readback on Grok 1.0.52: /statusline, /new, /model, '/statusline x' verify; /stat (another command highlighted) and /resume-c (box completed to a longer command) do not. Canary grok: idle, drafts 15/15, start-exit 2/2, bar 2/2. Co-Authored-By: Claude --- web/src/fixtures/panes/README.md | 2 + .../panes/grok--slash-popup-exact.txt | 40 +++++++++++++++++++ .../panes/grok--slash-popup-partial.txt | 40 +++++++++++++++++++ web/src/lib/harness/grok.test.ts | 16 ++++++++ web/src/lib/harness/grok/chrome.ts | 40 ++++++++++++++++++- 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 web/src/fixtures/panes/grok--slash-popup-exact.txt create mode 100644 web/src/fixtures/panes/grok--slash-popup-partial.txt diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index 93e43fef1..00b138ec8 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -520,6 +520,8 @@ Grok's composer is a rounded box at the tail: `╭─…─╮` / `│ ❯ … | `grok--working.txt` | Mid-turn; empty box; working hint row under the box | `working` | | `grok--startup.txt` | Fresh-session welcome screen: banner box (logo, menu) above an idle composer whose under-box row is the bare `[stable]` channel chip, not the hint bar. composerReady must be TRUE. Byte-faithful `format:ansi` 2026-08-22 | `idle` | | `grok--welcome-card.txt` | Grok Build 1.0.52, signed in with an API key: the changelog welcome card sits right on top of the composer with no `Tip:` row between, and the under-box row reads `Logged in with API key │ [stable]`. composerReady must be TRUE. Visible-screen `format:ansi` read 2026-10-15 in a throwaway Herdr session; the cwd row became `/tmp/collie-grok-sandbox` and the model label `mini` | `idle` | +| `grok--slash-popup-exact.txt` | Grok Build 1.0.52, `/statusline` typed: the slash-command popup between two rules right on top of the composer, `❯ /statusline` highlighted. The box stays live and the draft reads `/statusline`. Visible-screen read 2026-10-15; a local hook notice above was blanked and the cwd row became `/tmp/collie-grok-sandbox` | `idle` | +| `grok--slash-popup-partial.txt` | Hand-built from the capture above: the box holds `/stat` while `❯ /statusline` stays highlighted, the shape a partial command takes (live, `/stat` highlighted another command). Enter would run the highlighted row, so the draft must not read `/stat` | `idle` | | `grok--done.txt` | Square user-message bubble ABOVE an idle composer — the bubble must survive the strip | `idle` | | `grok--user-bubble.txt` | Torn frame: square bubble, no composer. `locateComposer` must return null | — | | `grok--permission-rm.txt` | Bash `rm` permission card at the tail; `●` on option 1 (always-approve). Composer replaced. Byte-faithful `format:ansi` 2026-08-21 | `blocked` | diff --git a/web/src/fixtures/panes/grok--slash-popup-exact.txt b/web/src/fixtures/panes/grok--slash-popup-exact.txt new file mode 100644 index 000000000..506bfba5d --- /dev/null +++ b/web/src/fixtures/panes/grok--slash-popup-exact.txt @@ -0,0 +1,40 @@ +  + master /tmp/collie-grok-sandbox  1.3K / 256K │ [Dashboard]  +  + + +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + Help improve Grok [Opt out] [Opt in]  + Off by default. Opt-in to allow SpaceXAI to retain coding data, e.g., prompts, traces, &  + ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────1─  +  ❯ /statusline Configure the Grok Build status line.   + ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────  + ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮  + │ ❯ /statusline │  + ╰────────────────────────────────────────────────────────────────────────────────────────── Grok 4.7 Fast (high) ─╯  +  + Enter:send │ Shift+Enter/Opt+Enter:newline │ Shift+Tab:mode │ Ctrl+.:shortcuts   +  \ No newline at end of file diff --git a/web/src/fixtures/panes/grok--slash-popup-partial.txt b/web/src/fixtures/panes/grok--slash-popup-partial.txt new file mode 100644 index 000000000..696bc75c1 --- /dev/null +++ b/web/src/fixtures/panes/grok--slash-popup-partial.txt @@ -0,0 +1,40 @@ +  + master /tmp/collie-grok-sandbox  1.3K / 256K │ [Dashboard]  +  + + +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + Help improve Grok [Opt out] [Opt in]  + Off by default. Opt-in to allow SpaceXAI to retain coding data, e.g., prompts, traces, &  + ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────1─  +  ❯ /statusline Configure the Grok Build status line.   + ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────  + ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮  + │ ❯ /stat  │  + ╰────────────────────────────────────────────────────────────────────────────────────────── Grok 4.7 Fast (high) ─╯  +  + Enter:send │ Shift+Enter/Opt+Enter:newline │ Shift+Tab:mode │ Ctrl+.:shortcuts   +  \ No newline at end of file diff --git a/web/src/lib/harness/grok.test.ts b/web/src/lib/harness/grok.test.ts index a0707cb22..eb7c78967 100644 --- a/web/src/lib/harness/grok.test.ts +++ b/web/src/lib/harness/grok.test.ts @@ -62,6 +62,8 @@ const PINNED = [ "grok--reply-table-highlighted.txt", "grok--reply-table-scrollbar.txt", "grok--reporter-294-draft-newline-hint.txt", + "grok--slash-popup-exact.txt", + "grok--slash-popup-partial.txt", "grok--startup.txt", "grok--user-bubble.txt", "grok--welcome-card.txt", @@ -563,6 +565,20 @@ describe("grokBuildBlocks", () => { expect(grokAdapter.extractInputDraft(lines)).toBeNull(); }); + it("the slash-command popup above the box leaves the box live, and an exact command verifies", () => { + // Grok paints its `/` completion between two rules right on top of the composer (1.0.52). Enter + // runs the highlighted row, so the typed command verifies when that row is the command itself. + const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--slash-popup-exact.txt"), "utf8"))); + expect(grokAdapter.composerReady!(lines)).toBe(true); + expect(grokAdapter.extractInputDraft(lines)).toBe("/statusline"); + }); + + it("partial input under the popup does not verify, since Enter would run the highlighted row", () => { + const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--slash-popup-partial.txt"), "utf8"))); + expect(grokAdapter.composerReady!(lines)).toBe(true); + expect(grokAdapter.extractInputDraft(lines)).not.toBe("/stat"); + }); + it("blank rows render unpainted in every block; glyph rows keep their backgrounds", () => { const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--permission-edit.txt"), "utf8"))); // The live frame paints base-coat colour across fully blank rows (the gray-stripe bug) and diff --git a/web/src/lib/harness/grok/chrome.ts b/web/src/lib/harness/grok/chrome.ts index f42d7e7ab..b0dd4117f 100644 --- a/web/src/lib/harness/grok/chrome.ts +++ b/web/src/lib/harness/grok/chrome.ts @@ -304,7 +304,14 @@ export function extractInputDraft(lines: StyledLine[]): string | null { } const draft = parts.filter((p) => p.length > 0).join(" "); - return draft.length === 0 ? null : draft; + if (draft.length === 0) return null; + // Under the slash popup, Enter runs the HIGHLIGHTED row. The typed text verifies only when that + // row is the typed command; on partial input (`/stat` over `❯ /statusline`) the highlighted command + // is folded in, so the reply guard cannot match and refuses rather than run a command nobody typed. + const texts = lines.map((l) => rstrip(lineText(l))); + const pointed = popupOnComposer(texts, box.top); + if (pointed !== null && pointed !== draft.split(/\s+/)[0]) return `${draft} ${pointed}`; + return draft; } /** Whether Grok's free-text composer is on screen at the tail. */ @@ -329,6 +336,35 @@ const ROUNDED_BOTTOM = /^\s*╰/; const ROUNDED_TOP = /^\s*╭/; const WELCOME_SCAN_ROWS = 40; +// Grok's slash-command completion popup (1.0.52, live 2026-10-15): its rows sit between two rules +// directly on top of the composer, the highlighted one led by `❯ /`, the rest by spaces and `/`. The +// box below stays live: typing lands in it. A rule-bounded run holding a `❯ /` row is what tells it +// from the model picker, whose pointer rows name a model, not a slash command. +const POPUP_RULE = /^\s*─+(?:\d+─+)?$/; +const POPUP_POINTED = /^\s*❯ (\/\S+)/; +const POPUP_ROWS = 40; + +/** The highlighted command of a slash popup whose bottom rule is row `bottom`, or null for no popup. */ +function slashPopupPointed(texts: string[], bottom: number): string | null { + if (!POPUP_RULE.test(texts[bottom]!)) return null; + let pointed: string | null = null; + for (let row = bottom - 1; row >= 0 && bottom - row <= POPUP_ROWS; row--) { + const t = texts[row]!; + if (POPUP_RULE.test(t)) return pointed; + pointed = POPUP_POINTED.exec(t)?.[1] ?? pointed; + } + return null; +} + +/** The slash popup's highlighted command when one sits right on top of the composer at `top`. */ +function popupOnComposer(texts: string[], top: number): string | null { + for (let i = top - 1; i >= 0; i--) { + if (isBlank(texts[i]!)) continue; + return slashPopupPointed(texts, i); + } + return null; +} + /** Whether the rounded box closing on row `bottom` is Grok's welcome card. */ function closesWelcomeCard(texts: string[], bottom: number): boolean { if (!ROUNDED_BOTTOM.test(texts[bottom]!)) return false; @@ -360,7 +396,7 @@ export function composerReady(lines: StyledLine[]): boolean { const t = texts[i]!; if (isBlank(t)) continue; if (USER_BUBBLE_EDGE.test(t)) return true; - if (opensBox(t)) return closesWelcomeCard(texts, i); + if (opensBox(t)) return closesWelcomeCard(texts, i) || slashPopupPointed(texts, i) !== null; return true; } return true; From b81a2948c62e4e1b180bb9b899355fdc76e344aa Mon Sep 17 00:00:00 2001 From: gggodlin <26342074+GGGODLIN@users.noreply.github.com> Date: Thu, 15 Oct 2026 14:25:10 +0800 Subject: [PATCH 3/4] feat(harness): Grok Build's folder trust prompt lifts as a trust card Grok 1.0.52 asks 'Do you trust the contents of this directory?' under its logo in an untrusted folder that holds something for it to load (a project skill did; an empty repository did not). The two options print their keys at the right: 'Yes, proceed y' and 'No, quit n'. grok/trust.ts lifts them as a trust-family prompt-select, each key sent alone; the card starts at the first option so the directory and the warning stay in the mirror, and the signature runs from the question to the tail. Live check 2026-10-15 in a throwaway Herdr session with a moved GROK_HOME: the prompt lifted twice, n quit to the shell, and y went on to the composer and wrote the folder into the throwaway trusted_folders.toml. Co-Authored-By: Claude --- web/src/fixtures/panes/README.md | 1 + web/src/fixtures/panes/grok--trust-prompt.txt | 40 +++++++++ web/src/lib/harness/grok.test.ts | 33 ++++++++ web/src/lib/harness/grok/index.ts | 10 +++ web/src/lib/harness/grok/trust.ts | 82 +++++++++++++++++++ 5 files changed, 166 insertions(+) create mode 100644 web/src/fixtures/panes/grok--trust-prompt.txt create mode 100644 web/src/lib/harness/grok/trust.ts diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index 00b138ec8..aa53a4d75 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -522,6 +522,7 @@ Grok's composer is a rounded box at the tail: `╭─…─╮` / `│ ❯ … | `grok--welcome-card.txt` | Grok Build 1.0.52, signed in with an API key: the changelog welcome card sits right on top of the composer with no `Tip:` row between, and the under-box row reads `Logged in with API key │ [stable]`. composerReady must be TRUE. Visible-screen `format:ansi` read 2026-10-15 in a throwaway Herdr session; the cwd row became `/tmp/collie-grok-sandbox` and the model label `mini` | `idle` | | `grok--slash-popup-exact.txt` | Grok Build 1.0.52, `/statusline` typed: the slash-command popup between two rules right on top of the composer, `❯ /statusline` highlighted. The box stays live and the draft reads `/statusline`. Visible-screen read 2026-10-15; a local hook notice above was blanked and the cwd row became `/tmp/collie-grok-sandbox` | `idle` | | `grok--slash-popup-partial.txt` | Hand-built from the capture above: the box holds `/stat` while `❯ /statusline` stays highlighted, the shape a partial command takes (live, `/stat` highlighted another command). Enter would run the highlighted row, so the draft must not read `/stat` | `idle` | +| `grok--trust-prompt.txt` | Grok Build 1.0.52 started in an untrusted folder that holds a project skill (`.agents/skills`): under the logo, `Do you trust the contents of this directory?`, the directory, a two-row warning, then `Yes, proceed y` and `No, quit n`, the version chip alone below. Lifts as a trust card, `y` and `n` each sent alone: live 2026-10-15, `n` quit to the shell and `y` went on to the composer and wrote the folder into the throwaway `trusted_folders.toml`. A folder with nothing for Grok to load showed no prompt. Recent-buffer read in a throwaway Herdr session with a moved GROK_HOME; the truncated scratch path became `/tmp/collie-grok-sandbox` at the same width | `blocked` | | `grok--done.txt` | Square user-message bubble ABOVE an idle composer — the bubble must survive the strip | `idle` | | `grok--user-bubble.txt` | Torn frame: square bubble, no composer. `locateComposer` must return null | — | | `grok--permission-rm.txt` | Bash `rm` permission card at the tail; `●` on option 1 (always-approve). Composer replaced. Byte-faithful `format:ansi` 2026-08-21 | `blocked` | diff --git a/web/src/fixtures/panes/grok--trust-prompt.txt b/web/src/fixtures/panes/grok--trust-prompt.txt new file mode 100644 index 000000000..cb9562d84 --- /dev/null +++ b/web/src/fixtures/panes/grok--trust-prompt.txt @@ -0,0 +1,40 @@ +  + /t/collie-grok-sandbox/trustprobe  +  +  +  +  + ⠀⠀⠀⠀⠀⠀⣀⣀⡀⠀⠀⠀⢀⠄  + ⠀⠀⠀⣠⣾⠿⠛⠛⠛⠛⢀⡴⠁⠀  + ⠀⠀⣼⡟⠁⠀⠀⠀⢀⡴⠻⣿⡀⠀  + ⠀⠀⣿⡇⠀⠀⠀⠔⠁⠀⠀⣿⡇⠀  + ⠀⠀⢹⣷⠀⠀⠀⠀⠀⢀⣴⡿⠀⠀  + ⠀⢀⠞⠁⠠⢶⣶⣶⣶⠿⠋⠀⠀⠀  + ⠐⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀  +  +  + Do you trust the contents of this directory?  +/tmp/collie-grok-sandbox  +  + Grok Build may run or modify contents in this directory,  + posing security risks.  +  + Yes, proceed y  + No, quit n  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + Grok Build 1.0.52 [stable]  +  \ No newline at end of file diff --git a/web/src/lib/harness/grok.test.ts b/web/src/lib/harness/grok.test.ts index eb7c78967..3224ff24f 100644 --- a/web/src/lib/harness/grok.test.ts +++ b/web/src/lib/harness/grok.test.ts @@ -65,6 +65,7 @@ const PINNED = [ "grok--slash-popup-exact.txt", "grok--slash-popup-partial.txt", "grok--startup.txt", + "grok--trust-prompt.txt", "grok--user-bubble.txt", "grok--welcome-card.txt", "grok--working.txt", @@ -93,6 +94,7 @@ const DIALOG = [ "grok--plan-approval.txt", "grok--plan-request-changes.txt", "grok--plan-tab-prompt.txt", + "grok--trust-prompt.txt", ]; const ownFixtures = DIALOG; @@ -609,3 +611,34 @@ describe("grokBuildBlocks", () => { expect(lines.map(lineText).join("\n")).toContain("╭"); }); }); + +describe("grok: the folder trust prompt lifts as a trust card (1.0.52)", () => { + const lines = () => splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--trust-prompt.txt"), "utf8"))); + const promptOf = (rows: ReturnType) => { + const block = grokAdapter.buildBlocks(rows).find((b) => b.kind === "prompt-select"); + return block?.kind === "prompt-select" ? block.prompt : null; + }; + + it("offers the two printed keys, y and n, as the trust family", () => { + const prompt = promptOf(lines()); + expect(prompt?.family).toBe("trust"); + expect(prompt?.question).toBe("Do you trust the contents of this directory?"); + expect(prompt?.options.map((o) => [o.label, o.keys.join("+")])).toEqual([ + ["Yes, proceed", "y"], + ["No, quit", "n"], + ]); + }); + + it("starts the card at the first option, so the directory and the warning stay in the mirror", () => { + const [raw, card] = grokAdapter.buildBlocks(lines()); + expect(card?.kind === "prompt-select" && lineText(card.lines[0]!).trim()).toMatch(/^Yes, proceed\s+y$/); + const mirror = raw?.kind === "raw" ? raw.lines.map((l) => lineText(l)).join("\n") : ""; + expect(mirror).toContain("/tmp/collie-grok-sandbox"); + expect(mirror).toContain("posing security risks."); + }); + + it("does not lift the options without the question above them", () => { + const rows = lines().map((l) => lineText(l).replace("Do you trust the contents of this directory?", "")); + expect(promptOf(splitLines(parseAnsi(rows.join("\n"))))).toBeNull(); + }); +}); diff --git a/web/src/lib/harness/grok/index.ts b/web/src/lib/harness/grok/index.ts index e2a252329..ec672832b 100644 --- a/web/src/lib/harness/grok/index.ts +++ b/web/src/lib/harness/grok/index.ts @@ -42,6 +42,7 @@ import { detectAskRegion } from "./ask"; import { detectPermissionRegion } from "./permission"; import { detectPlanMenuRegion } from "./plan-menu"; import { detectModelPickerRegion } from "./model-picker"; +import { detectTrustRegion } from "./trust"; export function grokBuildBlocks(lines: StyledLine[]): Block[] { // Drop Grok's full-screen theme-canvas paint before any block is built, so every rendered @@ -88,6 +89,15 @@ export function grokBuildBlocks(lines: StyledLine[]): Block[] { return blocks; } + const trust = detectTrustRegion(lines); + if (trust) { + const before = trimTrailingBlank(lines.slice(0, trust.startLine)); + const blocks: Block[] = []; + if (before.length > 0) blocks.push({ kind: "raw", lines: before }); + blocks.push({ kind: "prompt-select", prompt: trust.model, lines: lines.slice(trust.startLine) }); + return blocks; + } + return [{ kind: "raw", lines: stripChrome(lines) }]; } diff --git a/web/src/lib/harness/grok/trust.ts b/web/src/lib/harness/grok/trust.ts new file mode 100644 index 000000000..40c6ac0fb --- /dev/null +++ b/web/src/lib/harness/grok/trust.ts @@ -0,0 +1,82 @@ +// Grok Build's folder-trust prompt, the first screen in an untrusted directory (1.0.52, live +// 2026-10-15). It paints under the logo in place of the composer: +// +// Do you trust the contents of this directory? +// +// Grok Build may run or modify contents in this directory, +// posing security risks. +// +// Yes, proceed y +// No, quit n +// …blank rows… Grok Build 1.0.52 [stable] +// +// Each option prints its own key at its right, and the key alone answers: `y` went on to the +// composer and wrote the folder into `trusted_folders.toml` under GROK_HOME, `n` quit Grok to the +// shell (both measured). No Enter follows, which is the trust family's key-alone recipe. Anything off +// this layout refuses (fail-closed null). Pure; no pane access. + +import type { StyledLine } from "../../blocks"; +import type { PromptModel } from "../prompt-model"; +import { lastNonBlankIndex, lineText, regionSignature, rstrip } from "./markers"; + +export interface TrustRegion { + model: PromptModel; + startLine: number; +} + +const QUESTION_TEXT = "Do you trust the contents of this directory?"; +const QUESTION = /^\s*Do you trust the contents of this directory\?$/; +const YES = /^\s*Yes, proceed\s{2,}y$/; +const NO = /^\s*No, quit\s{2,}n$/; +// Rows between the question and the options: the directory (a long one wraps), the two-row warning +// and the blanks around it. +const QUESTION_SCAN_ROWS = 8; +// Non-blank rows under the options: the version chip at the bottom right. +const STATUS_ROWS = 1; + +/** The trust prompt at the tail, or null. */ +export function detectTrustRegion(lines: StyledLine[]): TrustRegion | null { + const texts = lines.map((l) => rstrip(lineText(l))); + const last = lastNonBlankIndex(texts); + let noRow = -1; + let seen = 0; + for (let i = last; i >= 0; i--) { + if (texts[i]!.trim() === "") continue; + if (NO.test(texts[i]!)) { + noRow = i; + break; + } + if (++seen > STATUS_ROWS) return null; + } + if (noRow < 1 || !YES.test(texts[noRow - 1]!)) return null; + const yesRow = noRow - 1; + + let question = -1; + for (let i = yesRow - 1; i >= 0 && yesRow - 1 - i < QUESTION_SCAN_ROWS; i--) { + if (QUESTION.test(texts[i]!)) { + question = i; + break; + } + } + if (question < 0) return null; + + // From the question through the tail: a different directory, or anything new under the options, + // makes a tap read on the old screen stale. + const signature = regionSignature(lines, question, last + 1); + if (signature === "") return null; + return { + // The card prints the question alone, so it starts at the first option: the directory and the + // warning stay in the mirror above it rather than vanish from both. + startLine: yesRow, + model: { + question: QUESTION_TEXT, + options: [ + { label: "Yes, proceed", keys: ["y"] }, + { label: "No, quit", keys: ["n"] }, + ], + family: "trust", + coreSignature: regionSignature(lines, question, noRow + 1), + signature, + }, + }; +} From 77cd7c90428d3f227b51caf02e3b2d1ebf5203e7 Mon Sep 17 00:00:00 2001 From: gggodlin <26342074+GGGODLIN@users.noreply.github.com> Date: Thu, 15 Oct 2026 16:15:09 +0800 Subject: [PATCH 4/4] fix(grok): a slash command's grey argument hint is not read as draft After the harness bar's Model button, Grok Build 1.0.52 keeps '/model' in its box over the model list with the argument hint ' [window] [effort]' painted in grey rgb(108,108,108). extractInputDraft read the hint as typed text, so the phone showed '/model [window] [effort]' as a terminal draft and Take over would have sent it. Found in a PWA e2e on the dev lane against a throwaway Herdr session (2026-10-15); the canary's bar scenario cannot see it, since it checks only that the picker opens. The hint is dropped only in that shape, in that grey, as the row's last text after a slash command; the same words in the typed-text colour stay. One capture from the canary bar scenario, with a local hook notice blanked, the cwd swapped and the model label renamed. Tests: the hint capture reads '/model' (red before the fix); a recoloured copy keeps the words, and fails when the colour check is removed. The capture's box fails closed (the model list is not a slash popup), so it joins composerReady's false list. Co-Authored-By: Claude --- web/src/fixtures/panes/README.md | 1 + .../fixtures/panes/grok--model-arg-hint.txt | 40 +++++++++++++++++++ web/src/lib/harness/grok.test.ts | 24 ++++++++++- web/src/lib/harness/grok/chrome.ts | 26 +++++++++++- 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 web/src/fixtures/panes/grok--model-arg-hint.txt diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index aa53a4d75..6d54071a9 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -521,6 +521,7 @@ Grok's composer is a rounded box at the tail: `╭─…─╮` / `│ ❯ … | `grok--startup.txt` | Fresh-session welcome screen: banner box (logo, menu) above an idle composer whose under-box row is the bare `[stable]` channel chip, not the hint bar. composerReady must be TRUE. Byte-faithful `format:ansi` 2026-08-22 | `idle` | | `grok--welcome-card.txt` | Grok Build 1.0.52, signed in with an API key: the changelog welcome card sits right on top of the composer with no `Tip:` row between, and the under-box row reads `Logged in with API key │ [stable]`. composerReady must be TRUE. Visible-screen `format:ansi` read 2026-10-15 in a throwaway Herdr session; the cwd row became `/tmp/collie-grok-sandbox` and the model label `mini` | `idle` | | `grok--slash-popup-exact.txt` | Grok Build 1.0.52, `/statusline` typed: the slash-command popup between two rules right on top of the composer, `❯ /statusline` highlighted. The box stays live and the draft reads `/statusline`. Visible-screen read 2026-10-15; a local hook notice above was blanked and the cwd row became `/tmp/collie-grok-sandbox` | `idle` | +| `grok--model-arg-hint.txt` | Grok Build 1.0.52 after the harness bar's Model button: the model list between two rules over the composer, and the box holding `❯ /model [window] [effort]`, the hint in grey `rgb(108,108,108)`. The draft reads `/model`; the list is not a slash popup, so composerReady is FALSE. Visible-screen read 2026-10-15 by the harness canary's bar scenario; a local hook notice above was blanked, the cwd row became `/tmp/collie-grok-sandbox` at the same width, and the model label `mini` | `idle` | | `grok--slash-popup-partial.txt` | Hand-built from the capture above: the box holds `/stat` while `❯ /statusline` stays highlighted, the shape a partial command takes (live, `/stat` highlighted another command). Enter would run the highlighted row, so the draft must not read `/stat` | `idle` | | `grok--trust-prompt.txt` | Grok Build 1.0.52 started in an untrusted folder that holds a project skill (`.agents/skills`): under the logo, `Do you trust the contents of this directory?`, the directory, a two-row warning, then `Yes, proceed y` and `No, quit n`, the version chip alone below. Lifts as a trust card, `y` and `n` each sent alone: live 2026-10-15, `n` quit to the shell and `y` went on to the composer and wrote the folder into the throwaway `trusted_folders.toml`. A folder with nothing for Grok to load showed no prompt. Recent-buffer read in a throwaway Herdr session with a moved GROK_HOME; the truncated scratch path became `/tmp/collie-grok-sandbox` at the same width | `blocked` | | `grok--done.txt` | Square user-message bubble ABOVE an idle composer — the bubble must survive the strip | `idle` | diff --git a/web/src/fixtures/panes/grok--model-arg-hint.txt b/web/src/fixtures/panes/grok--model-arg-hint.txt new file mode 100644 index 000000000..54f41f2f9 --- /dev/null +++ b/web/src/fixtures/panes/grok--model-arg-hint.txt @@ -0,0 +1,40 @@ +  + master /tmp/collie-grok-sandbox  ⠋ MCP (2/2)  │ 1.3K / 200K │ [Dashboard]  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────3─  +  ❯ Grok 4.6 SpaceXAI's latest frontier model   +  Grok 4.5   +  mini (current)   + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────  + ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮  + │ ❯ /model  [window] [effort] │  + ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────── mini ─╯  +  + Enter:send │ Shift+Enter/Opt+Enter:newline │ Shift+Tab:mode │ Ctrl+.:shortcuts   +  diff --git a/web/src/lib/harness/grok.test.ts b/web/src/lib/harness/grok.test.ts index 3224ff24f..f1b1e7c9a 100644 --- a/web/src/lib/harness/grok.test.ts +++ b/web/src/lib/harness/grok.test.ts @@ -45,6 +45,7 @@ const PINNED = [ "grok--draft-single.txt", "grok--draft-wrapped.txt", "grok--fresh-idle.txt", + "grok--model-arg-hint.txt", "grok--model-effort-moved.txt", "grok--model-effort.txt", "grok--model-picker-moved.txt", @@ -122,13 +123,17 @@ describe("the grok corpus", () => { describe("composerReady — the gate the reply path pre-flights on", () => { it.each( - neutralFixtures.filter((f) => f !== "grok--user-bubble.txt" && !f.includes("ask-multi")), + neutralFixtures.filter( + (f) => f !== "grok--user-bubble.txt" && f !== "grok--model-arg-hint.txt" && !f.includes("ask-multi"), + ), )("%s: the composer is on screen ⇒ true", (name) => { const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, name), "utf8"))); expect(grokAdapter.composerReady!(lines)).toBe(true); }); - it.each(["grok--user-bubble.txt", "grok--ask-multi.txt", "grok--ask-multi-checked.txt", ...DIALOG])( + // The model list Grok opens over the box after `/model` is not a slash popup, so the box fails + // closed there: a phone reply would append to the `/model` it holds. + it.each(["grok--user-bubble.txt", "grok--ask-multi.txt", "grok--ask-multi-checked.txt", "grok--model-arg-hint.txt", ...DIALOG])( "%s: a modal or torn frame ⇒ false", (name) => { const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, name), "utf8"))); @@ -581,6 +586,21 @@ describe("grokBuildBlocks", () => { expect(grokAdapter.extractInputDraft(lines)).not.toBe("/stat"); }); + it("a slash command's grey argument hint is not part of the draft", () => { + // The bar's Model button leaves `❯ /model [window] [effort]` in the box over the model + // list (1.0.52, live 2026-10-15); the phone offered the hint back as a terminal draft to take over. + const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--model-arg-hint.txt"), "utf8"))); + expect(grokAdapter.extractInputDraft(lines)).toBe("/model"); + }); + + it("keeps the same words when typed, since typed text is not the hint's grey", () => { + const raw = readFileSync(join(PANES_DIR, "grok--model-arg-hint.txt"), "utf8").replace( + "\u001b[38;2;108;108;108m\u001b[48;2;20;20;20m", + "\u001b[38;2;225;225;225m\u001b[48;2;20;20;20m", + ); + expect(grokAdapter.extractInputDraft(splitLines(parseAnsi(raw)))).toBe("/model [window] [effort]"); + }); + it("blank rows render unpainted in every block; glyph rows keep their backgrounds", () => { const lines = splitLines(parseAnsi(readFileSync(join(PANES_DIR, "grok--permission-edit.txt"), "utf8"))); // The live frame paints base-coat colour across fully blank rows (the gray-stripe bug) and diff --git a/web/src/lib/harness/grok/chrome.ts b/web/src/lib/harness/grok/chrome.ts index b0dd4117f..af5ed31b7 100644 --- a/web/src/lib/harness/grok/chrome.ts +++ b/web/src/lib/harness/grok/chrome.ts @@ -280,6 +280,29 @@ function draftLineText(line: StyledLine): string { return text; } +// After a slash command Grok paints its argument hint in grey (`❯ /model [window] [effort]`, +// 1.0.52, live 2026-10-15), and it stays while the model list is open. Read as draft, the phone +// offered the hint back as a terminal draft to take over. Only that shape, in that grey, as the +// row's last text, so the same words typed by the user (typed-text colour) stay in the draft. +const ARG_HINT = /^(?:<[^<>]+>|\[[^[\]]+\])(?:\s+(?:<[^<>]+>|\[[^[\]]+\]))*$/; + +function withoutArgumentHint(line: StyledLine, text: string): string { + let offset = 0; + let hint: { at: number; length: number } | null = null; + for (const segment of line.segments) { + const body = segment.text.trim(); + if (body !== "" && body !== "│") { + hint = + segment.style.color === "rgb(108,108,108)" && ARG_HINT.test(body) + ? { at: offset + segment.text.indexOf(body), length: body.length } + : null; + } + offset += segment.text.length; + } + if (hint === null || !/❯\s*\//.test(text.slice(0, hint.at))) return text; + return text.slice(0, hint.at) + " ".repeat(hint.length) + text.slice(hint.at + hint.length); +} + /** * The user's draft stranded in the composer. Grok writes it on the `│ ❯ … │` row and wraps onto * indented continuation rows below. Fragments join with a single space (soft wrap). Empty box → null. @@ -296,7 +319,8 @@ export function extractInputDraft(lines: StyledLine[]): string | null { const box = locateComposer(lines); if (box === null) return null; const parts: string[] = []; - const prompt = composerPromptText(draftLineText(lines[box.firstDraftRow]!)); + const first = lines[box.firstDraftRow]!; + const prompt = composerPromptText(withoutArgumentHint(first, draftLineText(first))); if (prompt === null) return null; parts.push(prompt.trim()); for (let i = box.firstDraftRow + 1; i < box.bottom; i++) {