mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-10 13:31:38 +00:00
Window title: a render-nothing <TerminalChrome> tracks session.info — the
native renderer.setTerminalTitle (frame-safe, zig-side OSC emit) shows
'{session title} — Hermes' once the session is titled, 'Hermes Agent' until
then. The user's previous title is bracketed with the XTWINOPS title stack
(save on boot, best-effort restore on quit). Gateway: _session_info now
carries the live title (DB row, pending_title fallback) and a session.info
refresh follows every title change — pending-title application, the
auto-title worker landing (via maybe_auto_title's title_callback), and
session.title renames — so the window retitles without waiting for the
next turn.
Notifications: when the TUI starts waiting on the user — any blocking
prompt (clarify/approval/sudo/secret/confirm) or turn completion — three
dialect sequences go out through renderer.writeOut: OSC 9 (iTerm2/wezterm),
OSC 99 (kitty), OSC 777 (urxvt/foot); terminals ignore what they don't
speak. Suppressed while the terminal reports focused (core's mode-1004
focus/blur events; until a first blur proves reporting works, notify
unconditionally). HERMES_TUI_NOTIFY=0/false/off kills notifications; the
title is not gated. All text is OSC-sanitized (control chars stripped,
777's semicolon fields spliced-proof, length-capped).
13 new TUI tests (pure shaping/sequences/env gate + store-edge wiring via
an injected seam) and 2 gateway tests (title resolution order, thread-safe
refresh emitter). Live-smoked: tmux pane_title shows 'Hermes Agent' from
the native title path.
68 lines
3.5 KiB
TypeScript
68 lines
3.5 KiB
TypeScript
/**
|
|
* Transcript — the scrolling message pane (spec v4 §2 `view/transcript.tsx`).
|
|
*
|
|
* ONE full-height <scrollbox> with a reactive <For> (opencode's model — the
|
|
* viewport clips growing output so terminal scrollback is never corrupted; no
|
|
* `writeToScrollback`). Carries the §8 #2 gotchas EXACTLY:
|
|
* - `minHeight:0` on BOTH the wrapper box AND the <scrollbox> (so the flex
|
|
* child can shrink below content height instead of pushing the composer off),
|
|
* - NO `flexDirection` on the <scrollbox> ROOT style (it has internal
|
|
* viewport/content children; setting it there breaks content-height
|
|
* measurement → phantom scroll offset that clips the top + leaves a gap),
|
|
* - `stickyScroll` + `stickyStart="bottom"` to pin the latest line.
|
|
*
|
|
* A `ScrollAnchorProvider` gives collapse/expand toggles (tool/thinking) a handle
|
|
* to hold the viewport in place so expanding doesn't yank to the bottom (#4).
|
|
*/
|
|
import type { ScrollBoxRenderable } from '@opentui/core'
|
|
import { createMemo, createSignal, For, Show } from 'solid-js'
|
|
|
|
import type { SessionStore } from '../logic/store.ts'
|
|
import { DisplayProvider } from './display.tsx'
|
|
import { HomeHint } from './homeHint.tsx'
|
|
import { MessageLine } from './messageLine.tsx'
|
|
import { ScrollAnchorProvider } from './scrollAnchor.tsx'
|
|
import { useTheme } from './theme.tsx'
|
|
|
|
export function Transcript(props: { store: SessionStore }) {
|
|
const [scroll, setScroll] = createSignal<ScrollBoxRenderable | undefined>()
|
|
const theme = useTheme()
|
|
const dropped = () => props.store.state.dropped
|
|
const sid = () => props.store.state.sessionId
|
|
// The NEWEST assistant answer's index — gold is earned (design pass): only
|
|
// that turn's `⚕` glyph stays primary; older answers demote to grey.
|
|
const latestAssistant = createMemo(() => {
|
|
const messages = props.store.state.messages
|
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
if (messages[i]?.role === 'assistant') return i
|
|
}
|
|
return -1
|
|
})
|
|
return (
|
|
<box style={{ flexGrow: 1, minHeight: 0 }}>
|
|
<scrollbox ref={setScroll} style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
|
|
<ScrollAnchorProvider scroll={scroll}>
|
|
{/* display flags (/compact, /details — Epic 3) for the rows below */}
|
|
<DisplayProvider flags={() => ({ compact: props.store.state.compact, details: props.store.state.details })}>
|
|
{/* empty-transcript home screen (item 12); replaced by messages on the first turn */}
|
|
<Show when={props.store.state.messages.length === 0}>
|
|
<HomeHint store={props.store} />
|
|
</Show>
|
|
{/* Honest truncation notice: the rolling cap hides the OLDEST rows from the
|
|
DISPLAY (never the model's context — that lives on the gateway). Point to
|
|
the dashboard for the full transcript. selectable=false → it's chrome,
|
|
excluded from copy/selection. */}
|
|
<Show when={dropped() > 0}>
|
|
<text selectable={false} style={{ fg: theme().color.muted }}>
|
|
{`⤒ ${dropped()} earlier message${dropped() === 1 ? '' : 's'} — scroll-back capped; full transcript on the dashboard${sid() ? ` · session ${sid()}` : ''}`}
|
|
</text>
|
|
</Show>
|
|
<For each={props.store.state.messages}>
|
|
{(message, i) => <MessageLine message={message} latest={i() === latestAssistant()} />}
|
|
</For>
|
|
</DisplayProvider>
|
|
</ScrollAnchorProvider>
|
|
</scrollbox>
|
|
</box>
|
|
)
|
|
}
|