A Claude Code or Codex spawn now asks for the structured view unless the
operator says otherwise: the two spawn forms open with the box ticked and
`maestro spawn` takes `--terminal` to opt out. Once the operator works the
toggle, a command change no longer re-ticks it under them.
The support predicate moves to `maestro_protocol::display` so all three
clients gate on the same resolution.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two things broke `nix build .#webFrontend`, and with it `nh home apply`
on any host that installs maestro.
`marked` was added to web/package.json without repinning pnpmDeps.hash,
so pnpm install failed offline with ERR_PNPM_NO_OFFLINE_TARBALL.
`tsc -b` also type-checks web/src/toolIcon.test.ts, which imports the
shared ../../fixtures/command-resolution.json. The derivation's src is
./web, so that path does not exist in the sandbox. The production build
now uses tsconfig.build.json, which excludes tests; `pnpm typecheck`
still covers them from a full checkout.
`command FOO=1 claude` and `nohup FOO=1 claude` are invalid shells (they
exec `FOO=1` as the program, exit 127), but the resolver skipped the
assignment unconditionally after any wrapper. Assignment prefixes are
only real at the start of a simple command or after a wrapper that itself
interprets them as env vars (env, sudo) or a keyword that restarts the
assignment grammar (time) — not after command, nohup, or a package
manager's subcommand, which just exec their next argument as the program.
Attach an accepts_assignment flag to each wrapper transition in both the
Rust and TS resolvers and gate the assignment-skip loop on the flag from
the most recently consumed wrapper. Extends the shared fixture with the
two reported cases plus every other wrapper transition and several nested
combinations (sudo+time valid, sudo+command / command+nohup / time+command
invalid, nohup+env valid) to lock the per-wrapper policy in for both
languages.
Fixes six issues in the shared command resolver (Rust display.rs and
its TS mirror toolIcon.ts), all locked in by shared fixture cases:
- An unescaped newline now ends resolution like `;` does, instead of
being folded in as whitespace; a backslash-newline is a line
continuation, removed entirely rather than embedded or splitting
the word.
- Assignment recognition (`FOO=bar`) now requires the `=` to come from
plain, unquoted/unescaped source text. `"FOO=1"` and `FOO\=1` are
executable words named `FOO=1`, not assignments — quote removal was
losing that provenance.
- `command -v`/`-V` (lookup only, no execution), `env -S` (embeds a
command string) and `npx -c`/`--call` (same) no longer resolve
through to a program they don't actually run. Restricted bash
`time`'s options to `-p`, the only flag the keyword itself parses
(`-o` etc. belong to external GNU time) — this corrects a
previously-wrong fixture case.
- `env`, `sudo` and `command`'s options may now bundle (`env -iv`,
`sudo -nE`) or take an attached value (`sudo -uroot`), the way real
getopt-based tools do, with missing-operand forms safely rejected.
- pnpm/yarn `dlx` and `bun x`/`bunx` now consume their own trailing
options (`--package foo`/`--package=foo`, `--bun`) before the
executable, matching each tool's documented CLI syntax.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
command_program/command_tool whitespace-split and only skipped whole
wrapper words, so quoted paths broke, sudo -u/env -i/nohup/time/npx
--yes/pnpm|yarn --silent dlx/bun x were rejected, and pnpm claude /
yarn claude (a package.json script, not `dlx claude`) resolved as a
Claude launch. Replace it with a tokenizer that honours quoting and
escaping without evaluating any expansion, and per-wrapper option
tables (env, sudo, command, nohup, time, npx, pnpm, yarn, bun) that
require dlx/x before looking through a package-manager wrapper.
Ambiguous shell grammar (pipelines, list operators, redirections,
substitution, unterminated quotes) is rejected outright rather than
guessed through; command_program still degrades to the naive first
word for display, but command_tool returns None.
Mirror the same resolver in web/src/toolIcon.ts and add
fixtures/command-resolution.json as a parity fixture both test
suites assert against.
The TUI's structured-view gate already delegates to command_tool, the
same resolver web/spawnForm.ts uses via splitCommand, so paths, env
assignments, wrappers, and configured presets already resolve like
daemon dispatch. Add coverage for those forms plus stale-selection
clearing on both preset switches and custom-command edits, so a
regression back to a naive basename match would be caught.
follow_selection=false stored scroll as a raw rendered-line offset into
lines rebuilt fresh every frame. A ProjectionUpdate::Patch growing a
streaming row above the viewport shifted every line below it, so the
raw offset kept pointing at a numerically-same but visually different
line and the viewport drifted even though the operator had scrolled
away deliberately.
Cache each frame's row-id-per-line layout and, before applying scroll
next frame, translate the old raw offset into (row id, intra-row line)
against that cache, then relocate it in the freshly rebuilt lines. Rows
above or intersecting the viewport growing, shrinking, or being
patched now shift the offset instead of the visible content. Falls
back to the nearest surviving neighbor row when the anchor row itself
is removed, and to the clamped raw offset when nothing nearby
survived. follow_selection and bottom-follow are unaffected — they
already recompute scroll from the live selection/tail each frame.