feat(terminal): persist tmux layout and last-attached view #83

Closed
ashraf wants to merge 1 commit from development_tmux_persist_session into development
Member

Summary

Make tmux terminal sessions in hero_router persist their layout and window state across two kinds of "reconnect":

  1. Cross-job-restart persistence (Rust): switch the tmux launch command from a bare tmux (which always creates a fresh session) to tmux new-session -A -s <name> — attach if a tmux server-side session named <name> already exists, otherwise create one. The tmux server setsid()s itself off the PTY child, so it survives the hero_proc PTY job being killed; on next launch with the same hero_router session name, splits / window names / scrollback are reattached automatically.
  2. Sibling-tab navigation persistence (JS): persist the terminal page's deep-link view (focused session, multi-pane layout, fullscreen state) to localStorage and restore it on /terminal page load when the URL hash is empty. Without this, clicking another nav tab (Home / Router / Admin / Docs / API / MCP) and clicking Terminal back leaves the terminal pane blank ("No session attached") even though the underlying job is still alive.

Closes #70

Changes

  • crates/hero_router/src/server/terminal.rs

    • New private tmux_session_name(name) -> String helper that maps a hero_router session name to a tmux-server-side session name (/_; remaining allowed chars are already valid in tmux).
    • create_session() ShellType::Tmux arm: launch tmux new-session -A -s <tmux_name> instead of tmux. Nu and Bash arms unchanged.
    • Updated comment block describing the new tmux launch contract.
    • New #[cfg(test)] mod tmux_naming_tests (3 unit tests) pinning the naming contract: pass-through of flat names, /_ substitution, no tmux-reserved chars (:, ., /, ) in the output.
  • crates/hero_router/static/js/terminal.js

    • New DEEP_LINK_KEY = 'hero_router.term.deepLink' constant alongside the existing FS_KEY / NAV_REVEAL_KEY.
    • updateHashParams() now mirrors the deep-link string to localStorage[DEEP_LINK_KEY] (or removes it when there is nothing to persist).
    • On /terminal page load, when location.hash is empty/just #, the load handler falls back to the persisted value before parsing for panes= / session= / fullscreen=. The existing "session no longer exists" guard makes the restore safe across server restarts.

No changes to templates/, partials/, base.html, or any RPC method shapes. Total diff: +76 / -7 across the two files.

Test Results

  • Build: clean (cargo build -p hero_router)
  • Tests: 102 passed, 0 failed, 0 ignored (3 new in tmux_naming_tests + 99 pre-existing)
  • Clippy: no new warnings on the changed file.

End-to-end persistence verification (delete PTY job, recreate same name, splits return) requires a live tmux + hero_proc environment and was performed manually per the spec.

Acceptance Criteria

  • Reconnecting attaches to existing tmux session (tmux new-session -A -s <name> semantics).
  • Window layout is preserved between sessions (inherent to tmux when reattaching to a named server-side session).
  • New session only created if none exists (the -A flag).
  • Sibling-tab navigation no longer leaves the terminal pane blank.
## Summary Make tmux terminal sessions in hero_router persist their layout and window state across two kinds of "reconnect": 1. **Cross-job-restart persistence (Rust)**: switch the tmux launch command from a bare `tmux` (which always creates a fresh session) to `tmux new-session -A -s <name>` — attach if a tmux server-side session named `<name>` already exists, otherwise create one. The tmux server `setsid()`s itself off the PTY child, so it survives the hero_proc PTY job being killed; on next launch with the same hero_router session name, splits / window names / scrollback are reattached automatically. 2. **Sibling-tab navigation persistence (JS)**: persist the terminal page's deep-link view (focused session, multi-pane layout, fullscreen state) to `localStorage` and restore it on `/terminal` page load when the URL hash is empty. Without this, clicking another nav tab (Home / Router / Admin / Docs / API / MCP) and clicking Terminal back leaves the terminal pane blank ("No session attached") even though the underlying job is still alive. ## Related Issue Closes https://forge.ourworld.tf/lhumina_code/hero_router/issues/70 ## Changes - `crates/hero_router/src/server/terminal.rs` - New private `tmux_session_name(name) -> String` helper that maps a hero_router session name to a tmux-server-side session name (`/` → `_`; remaining allowed chars are already valid in tmux). - `create_session()` `ShellType::Tmux` arm: launch `tmux new-session -A -s <tmux_name>` instead of `tmux`. `Nu` and `Bash` arms unchanged. - Updated comment block describing the new tmux launch contract. - New `#[cfg(test)] mod tmux_naming_tests` (3 unit tests) pinning the naming contract: pass-through of flat names, `/` → `_` substitution, no tmux-reserved chars (`:`, `.`, `/`, ` `) in the output. - `crates/hero_router/static/js/terminal.js` - New `DEEP_LINK_KEY = 'hero_router.term.deepLink'` constant alongside the existing `FS_KEY` / `NAV_REVEAL_KEY`. - `updateHashParams()` now mirrors the deep-link string to `localStorage[DEEP_LINK_KEY]` (or removes it when there is nothing to persist). - On `/terminal` page load, when `location.hash` is empty/just `#`, the load handler falls back to the persisted value before parsing for `panes=` / `session=` / `fullscreen=`. The existing "session no longer exists" guard makes the restore safe across server restarts. No changes to `templates/`, `partials/`, `base.html`, or any RPC method shapes. Total diff: +76 / -7 across the two files. ## Test Results - Build: clean (`cargo build -p hero_router`) - Tests: 102 passed, 0 failed, 0 ignored (3 new in `tmux_naming_tests` + 99 pre-existing) - Clippy: no new warnings on the changed file. End-to-end persistence verification (delete PTY job, recreate same name, splits return) requires a live tmux + hero_proc environment and was performed manually per the spec. ## Acceptance Criteria - [x] Reconnecting attaches to existing tmux session (`tmux new-session -A -s <name>` semantics). - [x] Window layout is preserved between sessions (inherent to tmux when reattaching to a named server-side session). - [x] New session only created if none exists (the `-A` flag). - [x] Sibling-tab navigation no longer leaves the terminal pane blank.
feat(terminal): persist tmux layout and last-attached view
All checks were successful
Build & Test / check (pull_request) Successful in 3m39s
a24746f481
Switch the tmux launch command from bare `tmux` (which always creates a
new session) to `tmux new-session -A -s <name>` so reconnecting to a
hero_router session re-attaches to the existing tmux server-side session
when one exists, preserving splits, window names, and scrollback across
hero_proc PTY job restarts. tmux daemonizes its server via setsid(), so
it survives the client (the PTY child) being killed.

Hero_router session names may contain `/`; map `/` -> `_` for the tmux
session name since `:`, `.`, `/` are reserved in tmux session names.
The remaining allowed chars (`[A-Za-z0-9_-]`) are already legal in tmux.

Also persist the terminal page's deep-link view (focused session,
multi-pane layout, fullscreen state) to localStorage so the previously
attached session is restored when navigating to a sibling tab
(Home/Router/Admin/...) and back, where the URL hash is lost.

#70
despiegk closed this pull request 2026-05-08 05:04:24 +00:00
All checks were successful
Build & Test / check (pull_request) Successful in 3m39s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_router!83
No description provided.