- [ ] Switch-back rendering is wrong for diff-based TUIs (specifically codex / ratatui). Partial progress; deeper investigation needed — details below in "Switch-back render divergence". - [ ] Killed agents are visible in the command palette. They shouldn't be. - [ ] claude failed to connect to patterm mcp -32601 - [ ] codex doesn't show the patterm mcp at all - [ ] opencode doesn't show the patterm mcp at all - [ ] Open agents/processes should appear above the option to open a new one in the palette - [ ] Some sort of macros in the command pallete would be nice, like if i type `sw ` it would only show the switch entries. Maybe we should have info text greyed out to show these macros. --- ## Switch-back render divergence ### Symptom Switching focus to codex (and back to it again after another tab) leaves codex's input box rendered wrong. The input text and the `›` prompt glyph appear on different rows. Typing more characters in codex makes the box "grow" to 4–5 rows tall even though the content is one short line. Claude (claude-code, ink-based) is mostly fine after the fixes below; codex (Rust/ratatui) is not. Initial spawn of codex looks correct. The bug only appears after a focus switch off codex and then back. ### What's already fixed and committed These actually helped; don't undo them blindly. 1. **`cursorShifter` empty-`\x1b[r` bug** (`internal/app/cursorshift.go`) — `\x1b[r` (reset DECSTBM) was being parsed as `(1,1)` and shifted to `\x1b[4;4r`, producing a one-row scrolling region that scroll-exploded the snapshot. Now rewrites empty params to `\x1b[;r` in host coords. `cursorShifter` carries `childRows` for this. Test: `TestCursorShifterDECSTBMEmptyResetsToViewport`. 2. **Host-state reset prelude in `repaintFocused`** (`internal/app/app.go`) — before replaying, write `\x1b[0m\x1b[?6l\x1b[;r\x1b[?25h\x1b[;H` directly to stdout to clear leftover SGR / DECOM / DECSTBM from the previously-focused child. 3. **Use `SerializeVT` instead of plain text for the snapshot** (`internal/app/app.go: repaintFocused`) — previously `repaintFocused` used `SnapshotChild` (plain text, no SGR). Now it feeds `SerializeChild` bytes through the per-focused-child viewport renderer, preserving colors and cursor state. 4. **Re-emit cursor as a child-space CUP through the renderer** — `SerializeVT`'s output order is: content with CRLFs, `\x1b[0m`, cursor CUP, **DECSTBM**, tabstops. DECSTBM has a documented side effect of moving the cursor to the scrolling region's home, and the trailing tabstop setup uses CHA (`\x1b[NG`) which leaves the renderer's internal `vr.col` parked at the last tab-stop column. Without a fixup the host cursor and the renderer's tracking both drift. The current code re-emits the saved cursor as a child-space `\x1b[;H` through the renderer, so the shifter writes the right host CUP and `trackCSI` updates `vr.row`/`vr.col`. 5. **`NudgeRedraw` on the focused child after replay** (`internal/app/child.go: NudgeRedraw`, called via `defer` in `repaintFocused`) — toggles PTY winsize by one row and back to force the kernel to emit `SIGWINCH`. Intent: make ratatui throw away its internal "last frame" diff state and emit a full frame. After this change the initial load and the post-interaction state of codex are visually equivalent, but both are still wrong. ### What's still broken After all of the above, codex's input box still draws with the input text and the `›` prompt on different rows, and "asdasdasdasd"-style typing makes the box grow vertically instead of staying single-line. Suspected causes, in rough order of likelihood: - **The renderer is over-shifting some row-positioning sequence that ghostty's `SerializeVT` emits but I haven't recognised.** Run the probe pattern below to see what bytes go through. Pay special attention to anything that targets rows after the DECSTBM is in place, anything that uses DECOM, and any `\x1bD`/`\x1bM` (IND/RI) which scroll within the region. - **Ratatui's internal "previous_buffer" isn't actually getting reset by `SIGWINCH`** in this PTY environment, or it's getting reset to a size that doesn't match the emulator's. The one-row toggle in `NudgeRedraw` might be a bad idea — try direct `kill(pid, SIGWINCH)` with no size change (the kernel's `TIOCSWINSZ` skips SIGWINCH when the size is unchanged, so we'd need to send the signal explicitly). See `Child.signal` for the helper. - **`childRows`/`childCols` reported via `TIOCGWINSZ` isn't what codex expects.** If codex reads winsize at startup and caches it, our `tabBarRows` change (1 → 3) might have left the cached size stale in some path. Verify by spawning codex fresh after the chrome change and confirming `stty size` inside codex matches `layout.childCols()` × `layout.childRows()`. ### Investigation tools - `internal/vt/probe_test.go` doesn't exist any more; recreate it to print `SerializeVT` output for representative cases. The relevant call is `(*GhosttyEmulator).SerializeVT()`. Confirmed shape: ``` \x1b[0m\x1b[;H\x1b[;r\x1b[3g\x1b[NG\x1bH... ``` - Add a debug tee around `viewportRenderer.Render` to log the raw bytes codex emits **after** the snapshot replay. That will show whether codex is emitting CUPs that target wrong rows (suggesting its diff state is wrong) or whether it's emitting reasonable CUPs and the renderer is mis-shifting them. - The user said they're building a harness so agents can iterate on this without manual screenshotting; once that exists, the diagnose loop is: replay snapshot → capture host stdout → diff against expected. Start with the simplest reproduction: spawn codex, switch away, switch back, type one character, compare host bytes against a golden file. ### Files touched (so the next agent knows what to read) - `internal/app/app.go` — `repaintFocused` - `internal/app/cursorshift.go` — DECSTBM handling, `childRows` - `internal/app/viewport_renderer.go` — plumbing for `childRows` - `internal/app/child.go` — `NudgeRedraw` - `internal/app/cursorshift_test.go` — DECSTBM reset coverage - Probe what `(*GhosttyEmulator).SerializeVT()` emits — that's the source of truth for what we're replaying.