Skip to content

Status Strip 2.0, module manager UX, and Ghostty physical-QA fixes - #328

Merged
raiseCatError merged 21 commits into
masterfrom
feature/status-strip-2
Oct 8, 2026
Merged

raiseCatError merged 21 commits into
masterfrom
feature/status-strip-2

Conversation

@raiseCatError

Copy link
Copy Markdown
Owner

Physical-QA fixes from the Ghostty 1.3.1 pass (#321), the module manager UX pass, and Status Strip 2.0 on the existing Context Engine. Version stays 0.17.0; no tag, release or Homebrew change.

QA defects fixed

  • Project and session title didn't change after /rename QA. The title read a label cache that only filled while notices were polling. It now reads the session's own identity (one lookup at launch/reattach, the /rename result, notice refreshes), so renames and resets show up at once.
  • Vim title stayed "Thanks for flying Vim". Submitting a passthrough command never marked the screen foreign, so the unchanged title was never rewritten. Now it's marked foreign on entry and again on return, and the title and cwd are re-asserted immediately. Reproduced and verified in a real PTY.
  • Hostile directory titles showed evil�]0;PWNED. A bounded scrubber now removes whole OSC/DCS/APC (BEL/ST/C1, unterminated), CSI/C1 CSI, two-character escapes, controls and bidi characters, so the title is evil. Injection protection is unchanged.
  • tmux mouse wheel cycled composer history. Nested in tmux, NMSh requested no mouse reporting, so Ghostty's alternate scroll sent Up/Down. tmux forwards SGR wheel reports to a pane that asks for them whether its mouse option is on or off (verified against real tmux in both modes), so NMSh now requests 1000/1002/1006 in tmux (no motion, Shift for native selection). Passthrough programs still get the reports unchanged.
  • F1: this Mac's top row sends media keys (fnState unset), which likely explains the physical result. NMSh also now decodes the kitty-protocol CSI P form. Ghostty's actual F1 bytes were not observed.

Module manager

/modules (direct; saves each change as it's made), /prompt → Modules (labelled as part of the draft, A saves module settings only), / local search on all three tabs, Tab/Shift+Tab through a shared helper in every visual tab bar, footers that only offer controls that can act, details that wrap at narrow widths with the preview dropped first, and unavailable surfaces marked with the reason under Starship/P10k/Oh My Posh/None (settings kept).

Status Strip 2.0

Top/Bottom edge through ScreenPlan; Left/Center/Right groups with a strip-only stripZone; deterministic width fitting in display cells; Plain (dot/bar/space) or Powerline using the existing block geometry; /strip Studio with presets previewed before applying, plus U undo; built-ins other than the exit status may route to the strip. Clock/CPU/RAM/battery/uptime are Context Engine facts (new bounded system.cpu and system.uptime) demanded only while the row shows. The old LocalStats poller is removed and rendering does no I/O. Old configs migrate unchanged (Off stays Off, Top stays Top).

Validation

  • npm run verify:release passes except themeBridge.test.ts › applyThemeBridge…, which fails identically on clean master because it assumes no running tmux server. It's environment-dependent and not touched here.
  • typecheck:bench, bench:smoke (all budgets met), test:node22 and git diff --check pass.
  • Automated real-PTY/tmux checks were run; these are not physical QA.

…rub whole control sequences

- Project and session reads this session's own identity (launch/reattach lookup,
  /rename result, notice refresh) instead of the notice-only label cache, so
  /rename QA shows at once without a cd or notices enabled.
- Submitting a passthrough command now marks the screen foreign, and leaving
  passthrough marks it again, so a title a program sets as it exits (Vim's
  "Thanks for flying Vim") is overwritten on return.
- Titles strip complete OSC/DCS/APC/CSI/C1 sequences and bidi controls rather
  than leaving "]0;PWNED" debris; injection protection is unchanged.
…king history

Nested in tmux, NMSh resolved baseline capabilities and never requested mouse
reporting, so the outer terminal's alternate-scroll turned the wheel into
Up/Down keys and the composer cycled history. tmux forwards SGR wheel reports to
a pane that enables 1000/1002/1006 whether its own mouse option is on or off, so
tmux now gets button+SGR reporting (no motion, Shift for native selection).
Outer-host hints stay hidden; screen and zellij stay baseline. Passthrough
programs keep receiving the reports unchanged.
… provider-aware surfaces

- /modules opens the same module manager /prompt embeds and saves each change
  as it is made ("Saved."); Esc only closes. Inside /prompt the manager says it
  edits the /prompt draft, marks unsaved module changes, and A saves module
  settings alone without saving the rest of the draft.
- / starts a local search in Modules, Catalog and Packs over already-loaded
  labels, ids, categories, descriptions and pack metadata; Esc clears it before
  the panel's own Escape; "No matches" is explicit; nothing is collected.
- Footers only advertise controls that can act; Space with nothing
  recommendable and arrows with no other option say why instead of no-oping.
- Details wrap at narrow widths (label over value below 60 columns), the
  prompt preview is dropped first, and long footers wrap at control boundaries.
- Under Starship, Powerlevel10k, Oh My Posh or Prompt None, surfaces that cannot
  show modules are marked with the reason; routing and settings are kept and
  the Status Strip keeps working.
- A shared tab-cycling helper gives Tab/Shift+Tab with wrap-around to every
  visual tab bar (/prompt views, Modules, Settings, Tools, tmux, Theme Studio)
  without taking Tab from text fields.
- /modules is in slash commands, the palette, /help and Ask.
NMSh enables kitty keyboard disambiguation where supported; the protocol's
functional-key table encodes unmodified F1 as CSI P. Legacy SS3 P and CSI 11~
still open the palette. Physical QA (Ghostty 1.3.1, macOS with the top row as
media keys) has not yet confirmed which bytes the host sends.
… facts

- Top or Bottom edge of the NMSh pane through the screen plan (Bottom adds the
  last pane row and moves nothing; inside tmux it sits above tmux's own bar).
- Independent Left, Center and Right groups; routed modules carry a strip-only
  stripZone (absent = Right, where they were). Deterministic fitting in display
  cells: compact forms, then lowest priority, the center before the edges, Keep
  Awake narrowing last; hidden below 30 columns or 8 rows.
- Plain (dot, bar or space separator) or Powerline via the prompt's existing
  block geometry; Safe glyphs, NO_COLOR and every color depth stay valid.
- Clock, CPU, RAM, battery and uptime come from the Context Engine
  (system.time/memory/battery plus new bounded system.cpu and system.uptime),
  demanded only while the row shows; the strip's own stats poller is removed and
  rendering does no I/O. Hostile module text loses whole escape sequences.
- /strip becomes the Status Strip Studio over the canonical settings rows, with
  module groups/order, presets (Minimal, Developer, System, Custom) previewed
  before applying and undoable, and a preview from the real renderer.
- Built-in modules other than the exit status may now route to the strip.
- Old settings migrate unchanged: Off stays Off, Top stays Top.
- Docs, /help, Ask and CHANGELOG (Unreleased) updated.
The sandbox environment had PATH '' but no TMUX_TMPDIR, and resolveCommand falls
back to standard tool directories, so where tmux is installed the test's
reloadTmux ran 'tmux source-file' against the person's default tmux server and
its result depended on whether that server was running. The sandbox now carries
its own TMUX_TMPDIR, the missing-fragment case is asserted on its own, and a new
test reloads into an isolated, test-owned tmux server and kills only that server.
Product behavior is unchanged.
…itles

safeContextText (prompt, modules, path, Status Strip, /prompt and /modules
disclosure), the fact cache boundary, historical prompt blocks and the welcome
screen each replaced control bytes with U+FFFD and kept the rest of the
sequence, so a directory named evil ESC]0;PWNED BEL rendered as evil�]0;PWNED.
They now share the terminal-title scrubber: OSC/DCS/APC to BEL or ST, CSI, C1
forms and unterminated sequences are removed whole, remaining controls and bidi
formatting are dropped, and ordinary Unicode and punctuation are untouched.
Regression covers the real prompt, strip, history and welcome rendering.
…lace

Every frame was wrapped in hide/show (?25l … ?25h). With animated Chroma
(Breathe, rules on), idle NMSh repaints about ten times a second; measured in
real tmux through a byte-logging relay, that was 51 hide/show pairs in 5 s idle,
independent of tmux mouse mode and the Status Strip. Ghostty applies each frame
atomically, but through tmux the pairs restart the blink and can be drawn as a
flicker. A frame whose caret stays visible at the same position now repaints
its rows and restores the position without touching visibility (0 pairs in the
same measurement, rows still repainted). Moving, hiding, showing, resize,
panels and passthrough return keep their explicit visibility writes.
…in DEC 2026

tmux answers the 2026 probe, so NMSh wrapped every frame in its own
synchronized-output block. Measured with tmux 3.7 and Ghostty's terminfo (idle,
animated Chroma), tmux then left the visible outer cursor at intermediate frame
positions after its own sync block in 24 of 25 frames (e.g. 28,99 / 30,44 /
30,90). Ghostty's surface-wide cursor shader animates every such move, so the
trail bumped around while the logical caret was still. tmux already draws to
the real terminal in synchronized updates; without the application block the
cursor is left away from the caret in 4 of 24 frames, all at column 1 of the
divider rows where tmux draws a line outside sync on its own.

The tmux profile records hostSynchronizes; the probe keeps synchronized output
off for it. Other hosts are unchanged. NMSh's own trail engine was verified
not to retarget on idle repaints (103 idle frames, caret unchanged), and a test
now pins that.
Measured from Ghostty's side of tmux 3.7 (your animated appearance): idle,
typing, Left/Right, multiline and resize all expose redraw positions to the
outer terminal, and every one is a row change to column 1 of a neighbouring
row, lasting ~0 ms; genuine composer moves stay on the caret's row. Ghostty's
shader inputs are only the current and previous cursor and the change time,
with no history, so a shader cannot tell a parked redraw position from a real
move, and one shader serves every Ghostty window.

New cursor setting ghosttyTrail (/cursor → Across rows): Tmux-safe, the default,
draws no smear, sparks or ripple for moves that change row; Full trails every
move. Fire on the caret is unchanged. At launch NMSh rewrites only its own
managed shader when it is out of date and says why and how to reload Ghostty.
Only documented Ghostty uniforms are used.
…ls jump inside tmux

A host-native trail (NMSh's Ghostty shader) follows the terminal's real cursor,
which tmux moves while it draws, so it can jump, even at a plain shell prompt.
A shader cannot tell that from a real move, so NMSh does not try to filter it.
- Renderer Portable now leaves the managed Ghostty shader off: NMSh draws the
  trail from its own caret and the host no longer draws a second one. Auto and
  Native are unchanged.
- Inside tmux the managed files stay in step with /cursor even though the
  outer terminal is hidden from NMSh.
- /cursor's Renderer row says this once, only inside tmux.
…ng it raw

Physical QA: the one-detached-session startup screen showed only its heading
and /tmp/evil. The session service reported the hostile cwd without its BEL,
and the picker wrote it raw, so the unterminated OSC (ESC]0;PWNED) swallowed
every following line, including the controls, and could inject terminal
sequences. Session-reported text (cwd via tildePath, running command, program
title) now goes through the shared scrubber in the startup screens, /resume and
the live-session list. Reproduced and verified in a real PTY with an isolated
session service at 100 and 40 columns.
…unch

Physical QA: tmux new -s qa typed in the composer gave a blank tmux. The tmux
server inherited the environment of NMSh's zsh, including the exported private
ZDOTDIR, so every pane's zsh loaded NMSh's bootstrap (prompt blanked every
cycle, ZLE and echo off) and showed nothing. Once startup is over the bootstrap
now gives children the person's own ZDOTDIR back (or unsets it) and stops
exporting POWERLEVEL9K_DISABLE_PROMPT; NMSh's own shell has already read its
files, so nothing changes for it. Any nested zsh benefits the same way.

Multiplexer client invocations (tmux with no command, new, new-session, attach,
attach-session; screen; zellij) now get the terminal from launch instead of
after their first output, as multiplexer-interop.md follow-up 6 planned;
tmux ls, screen -ls and other listing/control commands stay in the transcript.

Real-PTY regression: tmux launched from the composer on a private socket gets
a 100x30 client, a working pane shell (blank before this fix), raw keys and its
own drawing; detach returns to NMSh; attach -t works the same way.
…anscript text

Output from the first ownership evidence (alternate screen, an input mode,
a kitty keyboard push) is the program's, even in the same read as earlier
output, so agy's logo no longer stays behind after it exits. The transcript
parser no longer prints the bytes of sequences it does not draw (ESC ( B
printed a stray B; CSI > 4;2 m became ESC[NaN;2m), keeps zero colour
components, and abandons a CSI interrupted by ESC. Every handoff drops
half-decoded input first.
@raiseCatError
raiseCatError force-pushed the feature/status-strip-2 branch from a2862b6 to 747a8a6 Compare October 7, 2026 23:37
…decoder

The transcript parser keeps at most 8 KiB of one unfinished sequence. Past it
OSC/DCS/SOS/PM/APC are discarded to their terminator (BEL or ST for OSC, ST
only for the rest), CSI and nF escapes are skipped through their final byte,
and none of it becomes text; C1 characters are dropped. NMSh's shell markers
are bounded at 1 MiB and the key decoder holds at most 256 bytes of a partial
key. Chunked adversarial tests assert the retained size after every read.
…top leaking the cache redirect

The physical blank tmux came from a session service started before the
ZDOTDIR fix: the service writes every shell's bootstrap and outlives
frontends, so a restarted window still got the old one, and the tmux server
it started kept NMSh's private ZDOTDIR. A window now says when its service is
an older build. The bootstrap also gives children back the person's own
XDG_CACHE_HOME (the redirect only kept instant prompts out of startup).
…e terminal

agy asks about synchronized output, grapheme clustering and colours in its
first write, before it enters the alternate screen. Those bytes reached only
NMSh's transcript parser, agy got no answers and redrew without synchronized
output, so its cursor visibly jumped. Queries a running command writes while
NMSh still owns the screen now go to the host, and the host's replies go back
to the program instead of the key decoder. Cursor position reports excluded.
Node's raw mode keeps OPOST and sets ONLCR on NMSh's terminal, so every bare
LF NMSh forwarded became CR LF. Raw-mode TUIs use a bare LF to move down a
row in the same column: through NMSh, agy's inline interface overwrote its
prompt marker and suggestion prefixes and its cursor jumped on each redraw
(measured in Ghostty with a recording PTY proxy: 69 bare LFs written, none
arrived). Output processing is now off while a program owns the terminal and
back on when NMSh reclaims it. The terminal-answer router also no longer
holds two-byte Alt+key prefixes; only unmistakable answer prefixes wait.
@raiseCatError
raiseCatError merged commit 4f30ffd into master Oct 8, 2026
12 checks passed
raiseCatError added a commit that referenced this pull request Oct 8, 2026
…rubber

The mods panel cycles tabs with the shared PanelShell helpers, so Tab has one
rule across panels. Agent and mod display text uses the shared terminal
control scrubber line by line, so unterminated, C1, DCS and APC sequences
leave no debris (the local scrubber kept ]0;... from an unterminated OSC).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant