Skip to content

feat(scene): render the SessionPicker list inside the scene frame#997

Merged
esengine merged 1 commit into
mainfrom
rust-scene-sessions-picker
May 16, 2026
Merged

feat(scene): render the SessionPicker list inside the scene frame#997
esengine merged 1 commit into
mainfrom
rust-scene-sessions-picker

Conversation

@esengine
Copy link
Copy Markdown
Owner

Fifth sub-PR under the rust direction-B plan (#868) — last of the
B-line items from the original handoff.

Why

Under `REASONIX_RENDERER=rust` the Ink-side SessionPicker rendered
to the null stdout, so a user opening `/sessions` saw nothing but
the "awaiting input" composer they couldn't actually type into.
Keystrokes still flowed (SessionPicker already uses `useKeystroke`),
but the user was navigating an invisible list. The scene producer
now mirrors the picker so the fresh-user-with-saved-sessions path
works without falling back to Ink.

What

```
📂 sessions (3 saved)
▸ feat-foo main · 12 turns
hotfix-bar release/4.5 · 3 turns
spike-baz main · 47 turns
↑↓ navigate · ⏎ open · n new · esc cancel
```

  • New `SceneSessionItem` (`{ title, meta? }`) surfaced via
    `useSceneTrace` as a JSON-encoded array plus a primitive
    `sessionsFocusedIndex`, same primitive-deps pattern as the other
    picker fields.
  • `buildTraceFrame`: when `sessions` is non-empty, replace composer
    (and any approval / slash overlay) with a header row + one focused
    windowed row per session (cap `MAX_SESSION_ROWS = 8`, with a
    `…N more` tail when the list is longer) + a hint footer.
  • `listWindow` extracted as a generic so the slash and sessions
    blocks share centering logic; `slashWindow` becomes a thin wrapper.
  • `SessionPicker` gains a small `onFocusChange` callback —
    `useEffect` publishes the local `focus` state. App.tsx mirrors it
    into a `useState` and into the scene input. No restructuring of
    `SessionPicker` beyond the prop; its `useKeystroke` handling stays
    exactly as is (per the handoff's "don't touch SessionPicker for
    useKeystroke purposes" rule — this is state exposure, not
    restructuring).
  • App.tsx builds `sessionsJson` via `useMemo` from
    `sessionsPickerList` with `${branch} · ${turns} turns` as the
    meta line; gated on `pendingSessionsPicker` so the overlay only
    appears while the picker is actually open.

Out of scope

  • Rename / delete / quit hints are listed in the hint row but the
    scene doesn't reflect the in-progress rename buffer when the user
    presses `r`. The Ink picker still owns that interactive flow.
  • Web-dashboard `pickerPorts` broadcast is untouched.
  • CheckpointPicker / McpHub / Setup get the same treatment in
    later sub-PRs as they show up as felt gaps.

Tests

`tests/scene-trace-frame.test.ts` — 7 new cases:

  • composer replaced by header + N session rows + hint row
  • long lists windowed at 8 with an overflow line
  • composer AND slash overlay suppressed while sessions are active
  • meta line rendered after title
  • `parseSessions` round-trips, drops entries missing a title,
    returns `[]` on undefined / empty / non-array / non-JSON input

`npm run verify` green via prepush gate.

Refs #868

Under REASONIX_RENDERER=rust the Ink-side SessionPicker rendered to
the null stdout, so a user opening /sessions saw nothing but the
"awaiting input" composer they couldn't actually type into. The scene
producer now mirrors the picker:

- New SceneSessionItem ({ title, meta? }) surfaced via useSceneTrace
  as a JSON-encoded array plus a primitive sessionsFocusedIndex.
- buildTraceFrame: when sessions is non-empty, replace composer (and
  any approval / slash overlay) with a header row + one focused
  windowed row per session (cap 8, with a "…N more" tail when the
  list is longer) + a hint footer.
- listWindow extracted as a generic so the same centering logic
  serves the slash and sessions blocks.
- SessionPicker gains a tiny onFocusChange callback (useEffect
  publishes the local focus state); App.tsx mirrors it into a useState
  and into the scene input. No restructuring of SessionPicker beyond
  the prop — its useKeystroke handling stays exactly as is.
- App.tsx builds sessionsJson via useMemo from sessionsPickerList
  with `${branch} · ${turns} turns` as the meta line; gated on
  pendingSessionsPicker so the overlay only appears while the picker
  is actually open.

Refs #868
@esengine esengine merged commit 3114673 into main May 16, 2026
5 checks passed
@esengine esengine deleted the rust-scene-sessions-picker branch May 16, 2026 02:18
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