mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-10 13:31:38 +00:00
opentui(v5b): visual hierarchy — color-code roles + clean turn spacing
The transcript read as one undifferentiated gold blob (user/assistant/tool all
the same color). Adopt the Ink model where color IS the hierarchy, in 3 brightness
tiers:
- USER input → label (gold) — the human's turn stands out.
- ASSISTANT answer → text (bright) — the primary content, brightest.
- TOOL / REASONING → muted (dim) with an ACCENT glyph (⚡/▶/▼ amber) that marks
the block — clearly the secondary 'working area' below the answer.
Spacing: one blank line above every turn (was cramped: user had a blank, the
reply didn't) + the existing gap:1 between parts. Dropped the transcript's extra
marginTop (turns own their spacing now). 85 pass; verified live — gold ask, dim
tool, white answer read as three distinct things.
This commit is contained in:
parent
e44b43ad16
commit
503c1201ff
4 changed files with 18 additions and 10 deletions
|
|
@ -22,12 +22,18 @@ export function MessageLine(props: { message: Message }) {
|
|||
const theme = useTheme()
|
||||
const m = () => props.message
|
||||
const glyph = () => (m().role === 'assistant' ? theme().brand.icon : m().role === 'user' ? theme().brand.prompt : '·')
|
||||
// Role-distinct color IS the hierarchy (Ink model): the human's turn is tinted
|
||||
// GOLD (label), the agent's answer is BRIGHT (text), system notes are DIM (muted).
|
||||
const glyphFg = () =>
|
||||
m().role === 'assistant' ? theme().color.accent : m().role === 'user' ? theme().color.accent : theme().color.muted
|
||||
m().role === 'user' ? theme().color.label : m().role === 'assistant' ? theme().color.accent : theme().color.muted
|
||||
const bodyFg = () =>
|
||||
m().role === 'user' ? theme().color.label : m().role === 'system' ? theme().color.muted : theme().color.text
|
||||
const hasParts = () => (m().parts?.length ?? 0) > 0
|
||||
|
||||
return (
|
||||
<box style={{ flexDirection: 'row', flexShrink: 0, marginTop: m().role === 'user' ? 1 : 0 }}>
|
||||
// One blank line above every turn so user / assistant / tool blocks read as
|
||||
// distinct turns (item: spacing). The gold-vs-bright color split does the rest.
|
||||
<box style={{ flexDirection: 'row', flexShrink: 0, marginTop: 1 }}>
|
||||
<box style={{ flexShrink: 0, width: GUTTER }}>
|
||||
{/* the role glyph is decorative — exclude it from mouse selection (item 4).
|
||||
Bold so the user `❯` / assistant `⚕` turn boundaries pop (item 8). */}
|
||||
|
|
@ -51,7 +57,7 @@ export function MessageLine(props: { message: Message }) {
|
|||
when={m().streaming && !hasParts()}
|
||||
fallback={
|
||||
<text>
|
||||
<span style={{ fg: theme().color.text }}>{m().text}</span>
|
||||
<span style={{ fg: bodyFg() }}>{m().text}</span>
|
||||
</text>
|
||||
}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -47,13 +47,13 @@ export function ReasoningPart(props: { text: string; streaming?: boolean }) {
|
|||
>
|
||||
<box style={{ flexShrink: 0, width: GUTTER }}>
|
||||
<text selectable={false}>
|
||||
<span style={{ fg: theme().color.muted }}>{expanded() ? '▼' : '▶'}</span>
|
||||
<span style={{ fg: theme().color.accent }}>{expanded() ? '▼' : '▶'}</span>
|
||||
</text>
|
||||
</box>
|
||||
<text>
|
||||
{/* label color matches tool headers (label, not warn) so reasoning + tool
|
||||
read as one family of collapsible asides — warn is for warnings (item 8). */}
|
||||
<span style={{ fg: theme().color.label }}>{label()}</span>
|
||||
{/* accent chevron marks it; muted label keeps reasoning in the dim,
|
||||
secondary tier alongside tool calls (Ink hierarchy). */}
|
||||
<span style={{ fg: theme().color.muted }}>{label()}</span>
|
||||
<Show when={summary().title}>
|
||||
<span style={{ fg: theme().color.muted }}>{`: ${summary().title}`}</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ export function ToolPart(props: { part: ToolPartState }) {
|
|||
const body = createMemo(() => collapseToolOutput(result(), EXPANDED_MAX, bodyWidth() - 2))
|
||||
|
||||
const headGlyph = () => (collapsible() ? (expanded() ? '▼' : '▶') : '⚡')
|
||||
const headColor = () => (props.part.error ? theme().color.error : theme().color.muted)
|
||||
// accent glyph MARKS the tool (draws the eye); the rest is muted so tools read
|
||||
// as the dim, secondary tier below the bright assistant answer (Ink hierarchy).
|
||||
const headColor = () => (props.part.error ? theme().color.error : theme().color.accent)
|
||||
const subWidth = () => Math.max(1, bodyWidth() - props.part.name.length - 2)
|
||||
|
||||
return (
|
||||
|
|
@ -102,7 +104,7 @@ export function ToolPart(props: { part: ToolPartState }) {
|
|||
</box>
|
||||
<box style={{ flexDirection: 'row', flexGrow: 1, minWidth: 0 }}>
|
||||
<text>
|
||||
<span style={{ fg: theme().color.label }}>{props.part.name}</span>
|
||||
<span style={{ fg: theme().color.muted }}>{props.part.name}</span>
|
||||
<Show when={running()}>
|
||||
<span style={{ fg: theme().color.muted }}> …</span>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { ScrollAnchorProvider } from './scrollAnchor.tsx'
|
|||
export function Transcript(props: { store: SessionStore }) {
|
||||
const [scroll, setScroll] = createSignal<ScrollBoxRenderable | undefined>()
|
||||
return (
|
||||
<box style={{ flexGrow: 1, minHeight: 0, marginTop: 1 }}>
|
||||
<box style={{ flexGrow: 1, minHeight: 0 }}>
|
||||
<scrollbox ref={setScroll} style={{ flexGrow: 1, minHeight: 0 }} stickyScroll stickyStart="bottom">
|
||||
<ScrollAnchorProvider scroll={scroll}>
|
||||
{/* empty-transcript home screen (item 12); replaced by messages on the first turn */}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue