Skip to content

Vim-style window management: command mode, split windows, per-window live channels#87

Open
gammons wants to merge 29 commits into
mainfrom
window-management-phase3
Open

Vim-style window management: command mode, split windows, per-window live channels#87
gammons wants to merge 29 commits into
mainfrom
window-management-phase3

Conversation

@gammons

@gammons gammons commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Combined PR for the full vim-style window-management feature (supersedes #81 and #86, which will be closed). Spec: docs/superpowers/specs/2026-06-11-window-management-design.md.

Summary

: command mode (phase 1)

  • : opens a status-bar prompt with vim semantics (Esc / backspace-past-: cancel, Enter executes, unknown commands toast); command registry as the extension point
  • ctrl+w reclaimed from the workspace finder (now :ws; 1-9 still switch directly)

Window tree (phase 2)

  • New pure-data internal/ui/wintree package: vim-style split tree, exact-tiling geometry, simulate-and-check minimum-size refusal, geometric ctrl+w h/j/k/l navigation
  • :sp :vsp :q :only/:on + ctrl+w chords (s v h j k l w q c o) behind a pending-key state with status-bar hint
  • Errors per spec: "Not enough room", "Cannot close last window" (never quits the app)

Per-window live channels (phase 3)

  • Every window owns a live messages.Model; splits seed deep-copied clones; focus changes are instant pointer swaps
  • Channel-scoped events (messages, edits, deletes, reactions, sends, read marks, history) fan out to all windows viewing the channel; workspace/global events (names, emoji, avatars, theme, spinner) reach every window
  • Read-state rule: only focused selection advances the read marker — watch a channel in a split without marking it read
  • Unfocused panes render live read-only content with per-window render caches; single-window rendering byte-identical to before (test-pinned)

Notable fixes hardened along the way: render-cache window-identity keys (wrong-frame-after-focus-swap), reaction-array aliasing across clones, workspace-switch state leak, resize-after-split crash clamps, frozen spinner in unfocused loading panes, stranded syncing indicator, and the lipgloss border-inclusive-Width re-wrap bug in unfocused panes (zero-measurement border assembly, matching the focused hot path).

Deferred to phase 4 (tracked in the spec): per-window mouse routing & drag-resize (split-mode clicks are guarded off), resize chords (ctrl+w = < > + -), Tab focus-cycle window-walk, mark-read on window focus, typing rows in unfocused panes.

Test Plan

  • ~80 new tests across wintree (27), command mode, chords, fan-out, per-window models, rendering (incl. fail-first regression tests for every bug found in review)
  • Full suite green (go test ./... -count=1), go vet clean, -race clean on the new paths, every commit builds
  • Manually test-driven: splits with independent live channels, chord navigation, scrollbar rendering in unfocused panes

gammons added 28 commits June 11, 2026 07:01
…rder-inclusive Width)

lipgloss v2's Style.Width is border-inclusive, so renderUnfocusedWindow's
UnfocusedBorder.Width(W-2).Render(view) gave the model's exactly
(W-2)-cell lines a (W-4)-cell budget. Any line with real content in its
last 2 cells re-wrapped onto an injected remainder row — and the
scrollbar puts a glyph in the LAST cell of every line, garbling every
scrolled-back unfocused pane; exactSize's MaxHeight clamp then ate the
bottom border and masked the row-count blowup. Lines ending in blank
padding were silently truncated by 2 cells instead (trailing-whitespace
overflow is dropped, not wrapped), which is why sparse panes and the
dimension tests looked fine.

Mirror the focused path: refactor borderedTopPane's body into
borderedPaneCore(bottomEdge bool) and add borderedPane, the
fully-enclosed sibling that emits exactly Rect.W x Rect.H by
concatenation around the model's width-padded lines — no lipgloss
re-measure (terminal-probed emoji widths can disagree with x/ansi),
no exactSize pass, per-window cache unchanged.

Tests: regression pinning that every model line survives intact between
the side verticals with the bottom edge present (RED on HEAD),
TestBorderedPane_MatchesLipgloss equivalence gate against the corrected
Width(innerWidth+2) lipgloss form, and per-row width assertions in
TestRegion_SplitOutputDimensionsStable.
…nagement

Integration decisions:
- keys.go/mode_normal.go: union of search bindings (/ n N ctrl+f) and
  window-command bindings (ctrl+w prefix, keyless :ws WorkspaceFinder)
- statusbar: the :command prompt owns the left side while active;
  search segment renders otherwise
- OlderMessagesLoadedMsg: main's AnchorTS replaced-buffer guard applied
  PER WINDOW inside the phase-3 fan-out loop (strictly safer: siblings
  with the original buffer still receive the block); fetchingOlder
  stays the per-channel map, cleared even when no window views the
  channel
- focusWindow clears active in-channel search before the pane pointer
  swap (search match list + highlights are focused-pane state) + test
- main's live-reaction tests adapted to phase-3 channel routing (pane
  must view the channel); fanout backfill test supplies AnchorTS
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