hermes-agent/ui-tui/src/components/helpHint.tsx
brooklyn! fabca0bdd8
feat(tui): single /model command + unified Sessions overlay (#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>
2026-06-01 22:28:36 -04:00

73 lines
2 KiB
TypeScript

import { Box, Text } from '@hermes/ink'
import { HOTKEYS } from '../content/hotkeys.js'
import type { Theme } from '../theme.js'
const COMMON_COMMANDS: [string, string][] = [
['/help', 'full list of commands + hotkeys'],
['/clear', 'start a new session'],
['/resume', 'switch live or resume past sessions'],
['/details', 'control transcript detail level'],
['/copy', 'copy selection or last assistant message'],
['/quit', 'exit hermes']
]
const HOTKEY_PREVIEW = HOTKEYS.slice(0, 8)
export function HelpHint({ t }: { t: Theme }) {
const labelW = Math.max(
...COMMON_COMMANDS.map(([k]) => k.length),
...HOTKEY_PREVIEW.map(([k]) => k.length)
)
const pad = (s: string) => s + ' '.repeat(Math.max(0, labelW - s.length + 2))
return (
<Box alignItems="flex-start" bottom="100%" flexDirection="column" left={0} position="absolute" right={0}>
<Box
alignSelf="flex-start"
borderColor={t.color.primary}
borderStyle="round"
flexDirection="column"
marginBottom={1}
opaque
paddingX={1}
>
<Text>
<Text bold color={t.color.primary}>
? quick help
</Text>
<Text color={t.color.muted}>
{' · type /help for the full panel · backspace to dismiss'}
</Text>
</Text>
<Box marginTop={1}>
<Text bold color={t.color.accent}>
Common commands
</Text>
</Box>
{COMMON_COMMANDS.map(([k, v]) => (
<Text key={k}>
<Text color={t.color.label}>{pad(k)}</Text>
<Text color={t.color.muted}>{v}</Text>
</Text>
))}
<Box marginTop={1}>
<Text bold color={t.color.accent}>
Hotkeys
</Text>
</Box>
{HOTKEY_PREVIEW.map(([k, v]) => (
<Text key={k}>
<Text color={t.color.label}>{pad(k)}</Text>
<Text color={t.color.muted}>{v}</Text>
</Text>
))}
</Box>
</Box>
)
}