diff --git a/web/src/fixtures/panes/README.md b/web/src/fixtures/panes/README.md index 2b5570baf..7504be335 100644 --- a/web/src/fixtures/panes/README.md +++ b/web/src/fixtures/panes/README.md @@ -1858,6 +1858,11 @@ signature. | `muse--approval-ls-147.txt` | Muse Code 1.4.7 `ls` approval, `⟩` on option 1 (#424) | `blocked` | | `muse--approval-date-147.txt` | Muse Code 1.4.7 `date` approval, `⟩` on option 2 (#424) | `blocked` | | `muse--trust-prompt-147.txt` | Muse Code 1.4.7 workspace trust prompt (#424) | `blocked` | +| `muse--palette-hint.txt` | Muse Code 1.4.8, `/model` typed: the box reads `❯ /model []`, the hint in the bottom rule's colour, over one `/model` suggestion. The read is `/model` | `idle` | +| `muse--palette-hint-skills.txt` | `/resume` typed: its hint, and three suggestions, the bold `/resume` first and two built-in `resume-*` skills under it. The read is `/resume`, since Enter runs the bold row | `idle` | +| `muse--palette-hint-moved.txt` | Same, after Down: the bold has moved to `/resume-claude`, so the read must not verify `/resume` | `idle` | +| `muse--palette-typed-arg.txt` | `/model x` typed: the hint is gone and `x` wears the typed-text colour. The read is `/model x` | `idle` | +| `muse--palette-skill.txt` | `/plan` typed: its one suggestion is a built-in skill row, ` /plan · built-in · default skill On an explicit…`, tags between the command and the gap. The read is `/plan`. Visible-screen read 2026-10-15, cwd swapped for the sandbox path and model label renamed `mini` | `idle` | **Nothing was approved blindly.** The one approved command was `ls -la` on the empty sandbox (output verified); the trust prompt covered a throwaway `/tmp` @@ -1866,6 +1871,11 @@ left open at the end were dismissed with `Escape`. A second session (2026-09-26, tasks popup, same throwaway sandbox) approved `sleep 120/100/110/150` (backgrounded, no output) and `ls -la /tmp` (listing verified). +The four `muse--palette-*` rows of 2026-10-15 are visible-screen captures of Muse Code 1.4.8 in a +throwaway Herdr session (`herdr pane read --source visible --format ansi`). The statusline's +shortened cwd was swapped for `/private/tmp/collie-muse-sandbox`, the same 32 characters, and its +model label renamed `mini`. Nothing was submitted while capturing. + ## opencode corpus (captured 2026-09-26, opencode 1.18.32, herdr 0.9.0, private Herdr session) diff --git a/web/src/fixtures/panes/muse--palette-hint-moved.txt b/web/src/fixtures/panes/muse--palette-hint-moved.txt new file mode 100644 index 000000000..d3090bed1 --- /dev/null +++ b/web/src/fixtures/panes/muse--palette-hint-moved.txt @@ -0,0 +1,10 @@ + + Muse Code 1.4.8 + +── Voice input (⌥ + v to start) ─────────────────────────────────────────────────────────────────────────────────────── +❯ /resume [--last|] + /resume Resume an earlier Muse Code session + /resume-claude · built-in · default skill Continue a Claude Code session here + /resume-codex · built-in · default skill Continue a Codex session here +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mini · high · /private/tmp/collie-muse-sandbox · Auto-review \ No newline at end of file diff --git a/web/src/fixtures/panes/muse--palette-hint-skills.txt b/web/src/fixtures/panes/muse--palette-hint-skills.txt new file mode 100644 index 000000000..e8a47732e --- /dev/null +++ b/web/src/fixtures/panes/muse--palette-hint-skills.txt @@ -0,0 +1,10 @@ + + Muse Code 1.4.8 + +── Voice input (⌥ + v to start) ─────────────────────────────────────────────────────────────────────────────────────── +❯ /resume [--last|] + /resume Resume an earlier Muse Code session + /resume-claude · built-in · default skill Continue a Claude Code session here + /resume-codex · built-in · default skill Continue a Codex session here +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mini · high · /private/tmp/collie-muse-sandbox · Auto-review \ No newline at end of file diff --git a/web/src/fixtures/panes/muse--palette-hint.txt b/web/src/fixtures/panes/muse--palette-hint.txt new file mode 100644 index 000000000..2e45a6edb --- /dev/null +++ b/web/src/fixtures/panes/muse--palette-hint.txt @@ -0,0 +1,8 @@ + + Muse Code 1.4.8 + +── Voice input (⌥ + v to start) ─────────────────────────────────────────────────────────────────────────────────────── +❯ /model [] + /model Choose model +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mini · high · /private/tmp/collie-muse-sandbox · Auto-review \ No newline at end of file diff --git a/web/src/fixtures/panes/muse--palette-skill.txt b/web/src/fixtures/panes/muse--palette-skill.txt new file mode 100644 index 000000000..5939696d4 --- /dev/null +++ b/web/src/fixtures/panes/muse--palette-skill.txt @@ -0,0 +1,8 @@ + + Muse Code 1.4.8 + +── Voice input (⌥ + v to start) ─────────────────────────────────────────────────────────────────────────────────────── +❯ /plan + /plan · built-in · default skill On an explicit planning request, always call `read_skill` for this skill before an… +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mini · high · /private/tmp/collie-muse-sandbox · Auto-review \ No newline at end of file diff --git a/web/src/fixtures/panes/muse--palette-typed-arg.txt b/web/src/fixtures/panes/muse--palette-typed-arg.txt new file mode 100644 index 000000000..c64a3d20c --- /dev/null +++ b/web/src/fixtures/panes/muse--palette-typed-arg.txt @@ -0,0 +1,7 @@ + + Muse Code 1.4.8 + +── Voice input (⌥ + v to start) ─────────────────────────────────────────────────────────────────────────────────────── +❯ /model x +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mini · high · /private/tmp/collie-muse-sandbox · Auto-review \ No newline at end of file diff --git a/web/src/lib/harness/muse.test.ts b/web/src/lib/harness/muse.test.ts index 005e7d45e..82525c957 100644 --- a/web/src/lib/harness/muse.test.ts +++ b/web/src/lib/harness/muse.test.ts @@ -61,7 +61,12 @@ const PINNED = [ "muse--draft-wrapped.txt", "muse--fresh-idle.txt", "muse--palette-exact.txt", + "muse--palette-hint-moved.txt", + "muse--palette-hint-skills.txt", + "muse--palette-hint.txt", "muse--palette-partial.txt", + "muse--palette-skill.txt", + "muse--palette-typed-arg.txt", "muse--quoted-dialogs-bare.txt", "muse--tasks-popup-approval.txt", "muse--tasks-popup-draft.txt", diff --git a/web/src/lib/harness/muse/chrome.test.ts b/web/src/lib/harness/muse/chrome.test.ts index 1df088bc0..3d5a01328 100644 --- a/web/src/lib/harness/muse/chrome.test.ts +++ b/web/src/lib/harness/muse/chrome.test.ts @@ -55,6 +55,30 @@ describe("extractInputDraft — the verify half of type-then-verify", () => { ); }); + it("drops Muse 1.4.8's argument hint, painted in the rule's colour, after an exact command", () => { + // `❯ /model []` over one `/model` suggestion: the hint is not typed text. + expect(extractInputDraft(lines("muse--palette-hint.txt"))).toBe("/model"); + }); + + it("reads the command alone when the highlighted first suggestion of several names it", () => { + // `/resume` lists two built-in skills under it; the bold first row is the one Enter runs. + expect(extractInputDraft(lines("muse--palette-hint-skills.txt"))).toBe("/resume"); + }); + + it("stalls safe once the highlight has moved off the typed command", () => { + // After Down, Enter would run `/resume-claude`, so the typed text must not verify. + expect(extractInputDraft(lines("muse--palette-hint-moved.txt"))).not.toBe("/resume"); + }); + + it("reads a command whose one suggestion is a built-in skill row, tags and all", () => { + // ` /plan · built-in · default skill On an explicit planning request…` (Muse 1.4.8). + expect(extractInputDraft(lines("muse--palette-skill.txt"))).toBe("/plan"); + }); + + it("keeps a typed argument, which arrives in the typed-text colour once the hint is gone", () => { + expect(extractInputDraft(lines("muse--palette-typed-arg.txt"))).toBe("/model x"); + }); + it("reads attach tokens verbatim (the attach grammar verifies them, not this) (#278)", () => { expect(extractInputDraft(lines("muse--draft-image-chip.txt"))).toBe("[Image #1]"); expect(extractInputDraft(lines("muse--draft-quoted-path.txt"))).toBe( diff --git a/web/src/lib/harness/muse/chrome.ts b/web/src/lib/harness/muse/chrome.ts index 876dea792..14b1a360c 100644 --- a/web/src/lib/harness/muse/chrome.ts +++ b/web/src/lib/harness/muse/chrome.ts @@ -144,14 +144,20 @@ export function extractInputDraft(lines: StyledLine[]): string | null { // keeps the legacy read and stalls safe: Enter on partial input accepts the highlighted // suggestion (probed: `/us` ran `/usage`), so verifying the typed text there would bless a // command the operator did not type. + // + // Muse 1.4.8 adds two things (live, 2026-10-15): an argument hint after a complete command + // (`❯ /model []`), and skills listed under a command they prefix (`/resume` over + // `/resume-claude` and `/resume-codex`). Several rows qualify only when the first one is the + // highlighted one, bold, since Enter runs the highlighted row (probed: Down moved the bold to + // `/resume-claude`). const trimmed = prompt.trim(); if (trimmed.startsWith("/")) { - const run: string[] = []; - for (let i = tail.prompt + 1; i < tail.rule; i++) run.push(texts[i]!); - if (run.length === 1) { - const suggestion = parsePaletteRow(run[0]!); - if (suggestion !== null && suggestion.command === trimmed) return trimmed; - } + const typed = withoutArgumentHint(lines[tail.prompt]!, lines[tail.rule]!, trimmed); + const run: number[] = []; + for (let i = tail.prompt + 1; i < tail.rule; i++) run.push(i); + const head = run.length === 0 ? null : parsePaletteRow(texts[run[0]!]!); + const several = run.length > 1 && run.every((i) => texts[i]!.startsWith(" /")) && rowIsHighlighted(lines[run[0]!]!, typed); + if (head !== null && head.command === typed && (run.length === 1 || several)) return typed; } const parts = [prompt.trim()]; for (let i = tail.prompt + 1; i < tail.rule; i++) { @@ -166,6 +172,26 @@ export function extractInputDraft(lines: StyledLine[]): string | null { return draft; } +/** + * The typed slash command with Muse's argument hint taken off. The hint (` []`) follows a + * complete command in Muse's muted chrome colour, the colour of the box's own bottom rule, and goes + * the moment an argument is typed, which arrives in the typed-text colour. So a trailing ` […]` run + * is the hint only when it wears the rule's colour; a literally typed `[x]` is kept. + */ +function withoutArgumentHint(row: StyledLine, rule: StyledLine, typed: string): string { + const last = row.segments.findLast((seg) => seg.text.trim() !== ""); + const hint = last?.text.trim() ?? ""; + if (last === undefined || !/^\[[^\]]*\]$/.test(hint) || !last.text.startsWith(" ")) return typed; + const ruleFg = rule.segments.find((seg) => seg.text.trim() !== "")?.fg; + if (ruleFg === undefined || last.fg !== ruleFg || !typed.endsWith(hint)) return typed; + return typed.slice(0, -hint.length).trimEnd(); +} + +/** Whether a palette row's command is painted bold, Muse's mark for the highlighted suggestion. */ +function rowIsHighlighted(row: StyledLine, command: string): boolean { + return row.segments.some((seg) => seg.bold === true && seg.text.trim() === command); +} + /** Whether Muse's free-text composer is on screen at the tail (the weaker, purely geometric claim). */ export function hasComposer(lines: StyledLine[]): boolean { const tail = locateTail(lines); diff --git a/web/src/lib/harness/muse/markers.ts b/web/src/lib/harness/muse/markers.ts index 451364736..2652f0d0f 100644 --- a/web/src/lib/harness/muse/markers.ts +++ b/web/src/lib/harness/muse/markers.ts @@ -263,7 +263,10 @@ export function parseSubmitRow(text: string): { n: number; checked: number } | n // trimmed: the indent is structural. Only ever read under a slash-led prompt row (chrome.ts): the // palette opens for slash input alone, so that gate keeps a prose continuation starting with `/` // (a path) from parsing as a suggestion. -const PALETTE_ROW = /^ (\/\S+)\s{2,}(\S.*)$/; +// +// Muse 1.4.8 lists skills in the same palette with their tags between the command and the gap +// (` /plan · built-in · default skill On an explicit…`), so ` · tag` runs may follow the command. +const PALETTE_ROW = /^ (\/\S+)(?: · [^·]+?)*\s{2,}(\S.*)$/; /** Parse a palette suggestion row into its command, or null. */ export function parsePaletteRow(text: string): { command: string } | null {