feat(tui): single /model command + unified Sessions overlay#37112
Conversation
Collapse the redundant `/provider` alias so `/model` is the only name everywhere (it already drove the same 2-step ModelPicker in the TUI). Merge the separate `/resume` (cold history browser) and `/sessions` (live switcher) surfaces into one Sessions overlay reached by `/resume`, `/sessions`, `/session`, and `/switch`. It pins a "+ new" row at the top (always visible), lists live sessions with status, and lists resumable history below — dispatching session.activate for live rows vs resume for cold ones, with close/delete in place. Fixes `/session` opening an empty live-only switcher and the hidden new-session affordance.
🔎 Lint report:
|
There was a problem hiding this comment.
Pull request overview
This PR streamlines session/model command UX across the TUI and CLI by (1) removing the /provider alias in favor of a single /model command name, and (2) merging previously split live-session switching and persisted-session resuming into one unified “Sessions” overlay in the TUI.
Changes:
- Remove the standalone TUI resume picker overlay (
SessionPicker) and fold resumable history into the existing live session switcher overlay (pinned “+ new” row, live + resumable sections, history delete). - Consolidate overlay state by removing the
pickerflag and routing/resume,/session, and/switchto/sessions. - Drop the CLI
/provideralias for themodelcommand and update TUI slash handler tests accordingly.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| ui-tui/src/components/sessionPicker.tsx | Deletes the old resume-only picker overlay component. |
| ui-tui/src/components/helpHint.tsx | Updates the help hint text for /resume to match the unified overlay behavior. |
| ui-tui/src/components/appOverlays.tsx | Removes SessionPicker overlay wiring; routes resume via the sessions overlay. |
| ui-tui/src/components/appLayout.tsx | Renames overlay callback prop from picker-specific to resume-specific. |
| ui-tui/src/components/activeSessionSwitcher.tsx | Implements the unified Sessions overlay (new row + live + resumable history, delete/resume actions, reduced polling of history). |
| ui-tui/src/app/useSessionLifecycle.ts | Updates resume flow to close the unified sessions overlay instead of the removed picker overlay. |
| ui-tui/src/app/useInputHandlers.ts | Updates global escape/close behavior from overlay.picker to overlay.sessions. |
| ui-tui/src/app/slash/commands/session.ts | Makes /sessions the unified entrypoint with aliases (/resume, /session, /switch) and adds direct resume-by-arg behavior. |
| ui-tui/src/app/slash/commands/core.ts | Removes the legacy /resume core command implementation. |
| ui-tui/src/app/overlayStore.ts | Removes the picker overlay flag and updates overlay blocking/reset logic. |
| ui-tui/src/app/interfaces.ts | Removes picker from OverlayState; renames overlay prop to onResumeSelect. |
| ui-tui/src/tests/createSlashHandler.test.ts | Updates/expands tests for unified sessions overlay and /resume argument behavior. |
| hermes_cli/commands.py | Removes the provider alias from the model command definition. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Track the armed history-delete by session id instead of row index so the 1.5s live-status poll re-indexing rows can't redirect the second `d` to a different session. - Re-add the busy-session guard to immediate `/resume <id>` and `/sessions new` actions (browsing the bare overlay stays allowed) so resuming/switching can't corrupt an in-flight turn's streaming/busy state.
…verlay Copilot flagged that overlay actions bypassed the busy guard. Only cold resume actually closes the current session, so only it is guarded — both from the slash path and now from the overlay (appActions.resumeById). Switching between live sessions and starting a `+ new` live session keep the current session running in the background, so they stay unguarded: that concurrency is the orchestrator's whole purpose. Also dropped the over-broad guard on `/sessions new` for the same reason.
- The 1.5s poll now re-derives the resumable list from the RAW session.list results (rawHistoryRef) against the current live set, so a session hidden while live reappears in history once it closes — instead of being lost until a full reload. Delete also prunes the raw ref. - Drop the dead `/provider` entry from the desktop PICKER_OWNED_COMMANDS now that the alias is gone, so the desktop client no longer advertises it.
…polls - A garbled session.list response now surfaces an error and preserves the last good raw history, instead of silently blanking the resumable section. - The 1.5s poll re-anchors the selection to the same row by session id (live or history) when the live list grows/shrinks, so the highlight no longer drifts to a different row mid-interaction.
- Fetch active_list and session.list via Promise.allSettled so a failing session.list no longer rejects the whole load: live sessions still render and only the resumable history degrades (with an error). - Add unit tests for the new helpers (sessionRowKindAt row ordering, resumableHistory dedupe, sessionsCountLabel, relativeSessionAge).
The CI test_complete_slash_includes_provider_alias asserted the removed `/provider` alias still autocompleted. Flip it to lock in the removal: `/pro` no longer offers `provider`, and `/mod` still completes `model`.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
ui-tui/src/components/activeSessionSwitcher.tsx:367
Promise.allSettledmeans a rejectedsession.active_listrequest no longer hits thecatchblock. In that caseliveRes.status !== 'fulfilled'and you currently reportinvalid response: session.active_list, which is misleading and drops the real RPC/network error message. Handle the rejected case explicitly (usingrpcErrorMessage(liveRes.reason)) before attempting to parse the fulfilled value.
const [liveRes, histRes] = await Promise.allSettled([
gw.request<SessionActiveListResponse>('session.active_list', {
current_session_id: currentSessionId
}),
includeHistory ? gw.request<SessionListResponse>('session.list', { limit: 200 }) : Promise.resolve(null)
])
const r = liveRes.status === 'fulfilled' ? asRpcResult<SessionActiveListResponse>(liveRes.value) : null
if (!r) {
setErr('invalid response: session.active_list')
setLoading(false)
…arch#37112) * feat(tui): single /model command + unified Sessions overlay Collapse the redundant `/provider` alias so `/model` is the only name everywhere (it already drove the same 2-step ModelPicker in the TUI). Merge the separate `/resume` (cold history browser) and `/sessions` (live switcher) surfaces into one Sessions overlay reached by `/resume`, `/sessions`, `/session`, and `/switch`. It pins a "+ new" row at the top (always visible), lists live sessions with status, and lists resumable history below — dispatching session.activate for live rows vs resume for cold ones, with close/delete in place. Fixes `/session` opening an empty live-only switcher and the hidden new-session affordance. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix(tui): address Copilot review on the Sessions overlay - Track the armed history-delete by session id instead of row index so the 1.5s live-status poll re-indexing rows can't redirect the second `d` to a different session. - Re-add the busy-session guard to immediate `/resume <id>` and `/sessions new` actions (browsing the bare overlay stays allowed) so resuming/switching can't corrupt an in-flight turn's streaming/busy state. * fix(tui): guard cold-resume (not live-switch/new) from the Sessions overlay Copilot flagged that overlay actions bypassed the busy guard. Only cold resume actually closes the current session, so only it is guarded — both from the slash path and now from the overlay (appActions.resumeById). Switching between live sessions and starting a `+ new` live session keep the current session running in the background, so they stay unguarded: that concurrency is the orchestrator's whole purpose. Also dropped the over-broad guard on `/sessions new` for the same reason. * fix(tui): address Copilot review (history dedup + desktop /provider) - The 1.5s poll now re-derives the resumable list from the RAW session.list results (rawHistoryRef) against the current live set, so a session hidden while live reappears in history once it closes — instead of being lost until a full reload. Delete also prunes the raw ref. - Drop the dead `/provider` entry from the desktop PICKER_OWNED_COMMANDS now that the alias is gone, so the desktop client no longer advertises it. * fix(tui): surface session.list errors + keep selection stable across polls - A garbled session.list response now surfaces an error and preserves the last good raw history, instead of silently blanking the resumable section. - The 1.5s poll re-anchors the selection to the same row by session id (live or history) when the live list grows/shrinks, so the highlight no longer drifts to a different row mid-interaction. * fix(tui): degrade session.list independently + cover overlay helpers - Fetch active_list and session.list via Promise.allSettled so a failing session.list no longer rejects the whole load: live sessions still render and only the resumable history degrades (with an error). - Add unit tests for the new helpers (sessionRowKindAt row ordering, resumableHistory dedupe, sessionsCountLabel, relativeSessionAge). * test(tui-gateway): assert /provider alias is gone, /model remains The CI test_complete_slash_includes_provider_alias asserted the removed `/provider` alias still autocompleted. Flip it to lock in the removal: `/pro` no longer offers `provider`, and `/mod` still completes `model`. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Summary
Two TUI command cleanups:
1.
/providerremoved —/modelis the only name.providerwas just an alias ofmodelin the shared registry, and in the TUI both already opened the same 2-step ModelPicker. Dropping the alias removes the duplicate from the CLI, TUI, Telegram/Slack menus, and autocomplete while keeping the nicer overlay UX.2.
/resumeand/sessionsmerged into one Sessions overlay. Previously these were two different surfaces backed by different data —/resumebrowsed cold/persisted sessions (session.list, resume + delete) while/sessionsswitched between live in-process sessions (session.active_list, switch/close/new)./session(singular) had no command and prefix-matched the live-only switcher, so it looked empty/broken.Now
/resume,/sessions,/session, and/switchall open a single Sessions overlay:Enterswitches (instant re-attach viasession.activate),Ctrl+Dcloses.Enterresumes from disk (session.resume),ddeletes.Collapsed the duplicate
pickeroverlay flag into the singlesessionsflag and removed the now-unusedsessionPicker.tsx. The 1.5s live-status poll no longer re-queries the 200-row history.Test plan
npm run type-check— clean forsrc/(pre-existingpackages/hermes-inkerrors untouched)npm run lint— no errors in changed filescreateSlashHandler,activeSessionSwitcher,slashParity,orchestratorPromptSession); full TUI suite green except 2 pre-existing, unrelated text-wrapping failures (virtualHeights,cursorDriftRegression— confirmed failing on cleanmain)tests/hermes_cli/test_commands.py,tests/gateway/test_resume_command.py,tests/gateway/test_unknown_command.py) — 169 testshermes --tui:/model,/session//resume//sessionsopen the unified overlay, pinned "+ new" row aligned and reachable