feat(tui): Nx terminal UI parity - #194
Merged
Merged
Conversation
The task list becomes the Nx TUI's flat, state-sectioned shape:
- a bracket-railed running block (│ … └) pinned at the top
- completed tasks below in completion order, ✔/✖ icons
- ⏭ for tasks blocked by a dependency failure (⊘ stays user-cancel)
- the still-queued remainder visible as · rows — the whole plan is on
screen from the first frame
- right-aligned Cache and Duration columns under caption headers, the
Cache cell dropping adaptively on narrow panes
- a prose header ('Running 8 format, lint, compile, … tasks…') and an
Nx-format right-aligned footer ('quit: q help: ? …')
Phase group headers and auto-collapse are gone — tasks display as they
come; the phase remains a filter dimension. Cursor stays identity-
anchored across section moves and navigation skips frame/gap rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bubbles 2.1.1, bubbletea 2.0.8, lipgloss 2.0.5, fang 2.0.2, ultraviolet and charmtone to current — bubbletea's renderer fixes are relevant to the stale-cell ghosting seen on transitioning rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Full Nx-shape parity: one full-width pane at a time (the task list, or the selected task's output via enter — the split pane is gone), '...' pending cells, Nx footer labels (show output: <enter>). Icons stay the frontseat family (✓ ✗ ○ ⊘ ·) rather than Nx's ✔✖⏭ — ⊘ covers both cascade casualties and user cancellation, with the log placeholder naming the cause. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
enter on a task opens the Nx-style framed panel on the right — ┏ title border carrying the status icon, task label, and duration (cache origin when replayed), a ┃ rail, and '┗ <enter> full screen' in the bottom border — with the task list still live on the left. enter again toggles full screen; esc steps back (full → panel → list). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Defence in depth against grid desync: one over-wide line makes the terminal hard-wrap, and from that wrap on the diff renderer's cell grid is desynchronized — ANSI fragments render as text and stale cells smear until a full repaint (seen live on 200-task builds). clampLines clips any such line; the invariant test asserts every composed frame line is exactly terminal width across states and widths. Note: this contains, but does not yet root-cause, the live tearing — which reproduces only on real terminals under rapid updates and only on rows that change every tick. Tracked separately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the live-build tearing: on terminals reporting mode 2027 (iTerm2 3.5+, kitty, Ghostty), bubbletea switches its renderer's cell math to grapheme widths while our frames are composed with lipgloss's wcwidth. Two rulers desynchronize the diff renderer's grid on any row where they disagree — stale cells and raw ANSI fragments smear across the seams of exactly the rows that repaint every tick, and emulator replays (which never answer the mode query) could not reproduce it. The program filter now swallows the ModeUnicodeCore report before bubbletea's internal handler sees it, so renderer and composition agree on wcwidth everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
IntelliJ's reworked terminal applies rapid diff writes unfaithfully: stale cells survive at pane seams on rows repainting every tick, and the renderer's model diverges from the real screen permanently. When TERMINAL_EMULATOR reports JetBrains, reset the renderer with tea.ClearScreen every ~500ms so any drift is bounded instead of accumulating. Other terminals keep pure diff rendering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
THE tearing root cause: real build logs carry tabs (go test's ok\tpkg), carriage returns (progress bars), and tools' own ANSI sequences. The terminal expands/interprets these while width math counts them as ≤1 cell — one such visible log line renders wider than measured, the terminal hard-wraps it, and the diff renderer's grid is desynchronized for everything below. The desync originates in the output panel and persists across view states (the corruption columns in full-list captures match panel geometry), and periodic full repaints re-desync instantly because the offending line stays visible. Displayed log content is now stripped of escape sequences, tabs are expanded against real tab stops, and control characters dropped — displayed width always equals measured width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The complete causal chain, finally: Bubble Tea probes the tty's termios TABDLY flag; IntelliJ's pty reports TAB0, which enables ultraviolet's hard-tab cursor movement — and its CBT (cursor backward tab) sequences are exactly what JetBrains' JediTerm renders incorrectly (upstream: bubbletea#1641 / ultraviolet#100, both still open; minimal repro in mikeschinkel's gist). script(1) ptys report TABDLY differently, which is why no capture-based reproduction ever showed it. ForceSoftTabs sets OXTABS/XTABS on stdout before the program starts (restored on exit), so the probe disables the optimization and CBT is never emitted — on every terminal, since the optimization saves a few bytes per frame at best. The earlier hardening (width clamp, invariant test, single width ruler, log sanitization, JetBrains periodic repaint) remains as defence layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The remaining split-pane corruption (diagonal stale-duration fragments in the seam) is ultraviolet's scroll-region optimization: DECSTBM+ scroll sequences that JetBrains' JediTerm applies incorrectly — the same bug class Windows Terminal has (microsoft/terminal#19016), for which bubbletea already blanket-disables the optimization on Windows. bubbletea v2.0.8 exposes no switch, so charm.land/bubbletea/v2 is replaced with a one-line fork (frontseat-dev/bubbletea, frontseat/jediterm-scroll-optim) honoring TEA_NO_SCROLL_OPTIM, which the TUI launcher sets. Revert the replace once WithoutCaps (bubbletea#1641 / ultraviolet#100) ships. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel's rail spans the framed height (content + 2 border rows) but scroll visibility and thumb math must follow the CONTENT viewport — passing the framed height for both meant logs had to overflow by three lines before any scrollbar appeared, with skewed thumb sizing after. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Split-pane tearing persisted with scroll optimization off: JediTerm also mishandles other TERM-derived optimization sequences (ECH/ICH per -row insert/erase among the suspects). The fork now supports TEA_SAFE_RENDER, zeroing ultraviolet's capability set so the renderer emits only sequences every terminal implements correctly (colors are unaffected — the profile is set post-construction from the real env). The TUI enables it unconditionally: the caps are byte-level micro-optimizations, correctness everywhere is worth more. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
In the output panel or full screen, c puts the full (sanitized) log on the clipboard — OSC 52 for terminal-native copy (works over SSH) plus a pbcopy/wl-copy/xclip fallback — with a transient '✓ copied to clipboard' confirmation in the panel's bottom border. The border hint and footer/help advertise the binding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mise.local.toml/.mise.local.toml are personal, per-machine overrides (conventionally gitignored) — staging them poisoned sandboxes with tools that cannot resolve offline (a dev's hk@latest) and diverged cache keys between machines for identical source. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
goResolveCacheKey in workspace mode hashed only go.work/go.work.sum, so a dependency bump or replace inside a member's go.mod was invisible — the memoized workspace-unified asset set went stale and sandbox actions died on 'module lookup disabled by GOPROXY=off' for the new module. The key now folds in every go.work member's go.mod/go.sum. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every frame line was padded to exactly the terminal width, so every row wrote the final cell — the VT100 deferred auto-wrap edge case. Real terminals defer the wrap; eager-wrapping implementations (JetBrains JediTerm's new engine) insert the line break immediately, and one spurious wrap per full-width row shears the whole frame downward. Canonical TUIs never touch the last column; frames now render to width-1 with the final column always blank. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every frame is now a full erase + absolute redraw (TEA_FULL_REPAINT in the fork): no diff state, no cell-model trust, no error that can survive a single 50ms frame — correctness independent of how faithfully the terminal applies incremental updates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The decisive finding: JetBrains' reworked terminal phantom-wraps ANSI-styled rows near the right edge (a plain-ASCII ruler at the same width does not wrap) — no rendering strategy survives that, which is why diff discipline, safe caps, and full repaints all failed alike. A 5-column margin keeps styled rows clear of the broken boundary and renders clean (user-verified via FRONTSEAT_TUI_MARGIN=5). The margin now applies automatically on JetBrains terminals (env markers; FRONTSEAT_TUI_MARGIN overrides anywhere), and the render- strategy mitigations (no scroll optim, zeroed caps, stateless full repaint) are scoped to JetBrains — proper terminals keep full-width diff rendering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Workspace-mode verification knows the replacement module, and the change flips the pinned go plugin's workspace resolve-memo key (which hashes only go.work/go.work.sum) so CI re-resolves and provisions the fork instead of replaying the stale pre-fork asset set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Round 2 of the TUI, matching the Nx terminal UI captures, plus a hard-won rendering-correctness saga on JetBrains terminals.
The TUI
<enter>opens a framed output panel (┏ title ━ duration / ┗ hints) with the list still live; enter again = full screen; esc steps back;ccopies the full sanitized log (OSC 52 + pbcopy/xclip fallback) with a transient confirmationJetBrains (IntelliJ reworked terminal) rendering
Root cause, established by elimination and a decisive experiment: JediTerm phantom-wraps ANSI-styled rows near the right edge (a plain-ASCII ruler at the identical width does not wrap) — so any full-width styled frame shears, regardless of rendering strategy. A 5-column right margin renders clean (user-verified); it now applies automatically on JetBrains terminals (env-marker detection;
FRONTSEAT_TUI_MARGINoverrides anywhere).Layered hardening that stays for all terminals: log-content sanitization (tabs/CR/ANSI in tool output — go test output is tab-delimited), last-column reservation (deferred auto-wrap), width-clamped frames + a frame-width invariant test, termios soft-tabs (kills CBT sequences JediTerm also corrupts — upstream repro: mikeschinkel's gist), and one width ruler (mode-2027 suppression).
JetBrains-scoped mitigations via a temporary bubbletea fork (
frontseat-dev/bubbletea,replacedirective):TEA_NO_SCROLL_OPTIM(DECSTBM corruption — same class as microsoft/terminal#19016),TEA_SAFE_RENDER(zeroed TERM-derived caps),TEA_FULL_REPAINT(stateless full frames). Retire the fork when charmbracelet/bubbletea#1641 + charmbracelet/ultraviolet#100 (WithoutCaps) ship.Also fixed en route:
mise.local.tomlno longer leaks into hermetic sandboxes (personal config poisoned inputs and split cache keys), and the go plugin's workspace resolve memo now hashes member-module go.mod/go.sum (a member's dependency change previously served stale asset sets → 'GOPROXY=off' sandbox failures).🤖 Generated with Claude Code