Skip to content

feat: Window state persistence (persist_state) — toggle preserves buffers for instant restore #228

@jensenojs

Description

@jensenojs

Problem

:Opencode toggle closes windows and deletes buffers every time. Reopening recreates the UI from scratch, re-renders all output, and loses input draft, scroll/cursor position, and focused pane.

Expected Behavior

When ui.persist_state = true (proposed default):

  1. Hide UI windows without deleting buffers
  2. Restore existing buffers into new split windows on next toggle
  3. Preserve: input draft, scroll/cursor position, focused pane, input_hidden state
  4. Respect user intent: if user clicks a line after restore, do NOT force-scroll to bottom

When ui.persist_state = false, behavior remains unchanged.

Design: Three-State Window Model

closed ──(open)──> visible ──(hide)──> hidden ──(restore)──> visible
   ^                  |                   |                     |
   +──(close)─────────+                   +──(close_hidden)─────+

State is derivedget_window_status() checks window validity + hidden buffer existence. A pure-function decision engine (resolve_toggle_decision) maps (status, persist_state) → action.

Performance

100 toggle cycles, same session with output content:

Metric persist_state=false persist_state=true
Mean 27.15 ms 15.09 ms
P50 ~22 ms ~12.4 ms
P95 ~37 ms ~24.6 ms

~44% mean improvement from skipping buffer recreation and full session re-render.

Pre-requisite Fixes Discovered

Issues found during implementation that should be fixed independently:

  • curl.lua: job.pid persists after exit — health check false positive. Fix: explicit is_running flag
  • topbar.lua: missing pcall + win_is_valid guard on async callbacks
  • loading_animation.lua: missing buf_is_valid guard on extmark operations
  • output_window.lua: is_at_bottom() viewport check always true in streaming — fix: cursor-based check
  • output_window.lua: update_dimensions errors on split windows — fix: detect floating vs split
  • input_window.lua: same floating/split issue in apply_dimensions
  • renderer.lua: render_full_session has no debounce — rapid toggles cause request storms
  • event_manager.lua: _poll_external_messages hidden→visible transition causes redundant render + scroll override
  • reference_picker.lua: normal! zz on jump causes visual noise — make configurable

Implementation Status

  • Full implementation on feat/hide-window-toggle (commit c894fd9)
  • 604-line test suite (persist_state_spec.lua)
  • Performance benchmark (scripts/bench_toggle.lua)
  • All existing tests pass
  • Split into individual PR branches (in progress, see comments)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions