mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
feat(tui): collapsible sections in startup banner (skills, system prompt, MCP)
The TUI SessionPanel banner now uses collapsible \u25b8/\u25be toggle sections matching the existing Chevron convention used for runtime agent details. Skills, system prompt, and MCP server lists are collapsed by default; tools remain expanded as the most actionable info. - tui_gateway/server.py: _session_info() now passes agent._cached_system_prompt through to the TUI frontend - ui-tui/src/types.ts: added system_prompt?: string to SessionInfo - ui-tui/src/components/branding.tsx: rewrote SessionPanel with CollapseToggle helper + per-section useState toggles Default states: tools=open, skills=collapsed, system=collapsed, mcp=collapsed. Clicking any \u25b8/\u25be header toggles that section.
This commit is contained in:
parent
3ebdd26449
commit
d78c34928f
3 changed files with 177 additions and 47 deletions
|
|
@ -1413,6 +1413,10 @@ def _session_info(agent) -> dict:
|
|||
info["mcp_servers"] = get_mcp_status()
|
||||
except Exception:
|
||||
info["mcp_servers"] = []
|
||||
try:
|
||||
info["system_prompt"] = getattr(agent, "_cached_system_prompt", "") or ""
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from hermes_cli.banner import get_update_result
|
||||
from hermes_cli.config import recommended_update_command
|
||||
|
|
|
|||
|
|
@ -58,6 +58,44 @@ export function Banner({ t }: { t: Theme }) {
|
|||
)
|
||||
}
|
||||
|
||||
// ── Collapsible helpers ──────────────────────────────────────────────
|
||||
|
||||
function CollapseToggle({
|
||||
count,
|
||||
open,
|
||||
suffix,
|
||||
t,
|
||||
title,
|
||||
onToggle
|
||||
}: {
|
||||
count?: number
|
||||
open: boolean
|
||||
suffix?: string
|
||||
t: Theme
|
||||
title: string
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<Box onClick={onToggle}>
|
||||
<Text color={t.color.accent}>{open ? '▾ ' : '▸ '}</Text>
|
||||
<Text bold color={t.color.accent}>
|
||||
{title}
|
||||
</Text>
|
||||
{typeof count === 'number' ? (
|
||||
<Text color={t.color.muted}> ({count})</Text>
|
||||
) : null}
|
||||
{suffix ? (
|
||||
<Text color={t.color.muted}> {suffix}</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// ── SessionPanel ─────────────────────────────────────────────────────
|
||||
|
||||
const SKILLS_MAX = 8
|
||||
const TOOLSETS_MAX = 8
|
||||
|
||||
export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
||||
const cols = useStdout().stdout?.columns ?? 100
|
||||
const heroLines = caduceus(t.color, t.bannerHero || undefined)
|
||||
|
|
@ -67,6 +105,12 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
|||
const lineBudget = Math.max(12, w - 2)
|
||||
const strip = (s: string) => (s.endsWith('_tools') ? s.slice(0, -6) : s)
|
||||
|
||||
// ── Local collapse state for each section ──
|
||||
const [toolsOpen, setToolsOpen] = useState(true)
|
||||
const [skillsOpen, setSkillsOpen] = useState(false)
|
||||
const [systemOpen, setSystemOpen] = useState(false)
|
||||
const [mcpOpen, setMcpOpen] = useState(false)
|
||||
|
||||
const truncLine = (pfx: string, items: string[]) => {
|
||||
let line = ''
|
||||
let shown = 0
|
||||
|
|
@ -85,35 +129,89 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
|||
return line
|
||||
}
|
||||
|
||||
const section = (title: string, data: Record<string, string[]>, max = 8, overflowLabel = 'more…') => {
|
||||
const entries = Object.entries(data).sort()
|
||||
const shown = entries.slice(0, max)
|
||||
const overflow = entries.length - max
|
||||
const skeleton = info.lazy && entries.length === 0
|
||||
// ── Collapsible skills section ──
|
||||
const skillEntries = Object.entries(info.skills).sort()
|
||||
const skillsTotal = flat(info.skills).length
|
||||
const skillsCatCount = skillEntries.length
|
||||
|
||||
const skillsBody = () => {
|
||||
if (info.lazy && skillEntries.length === 0) {
|
||||
return <InlineLoader label="scanning skills" t={t} />
|
||||
}
|
||||
|
||||
const shown = skillEntries.slice(0, SKILLS_MAX)
|
||||
const overflow = skillEntries.length - SKILLS_MAX
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color={t.color.accent}>
|
||||
Available {title}
|
||||
</Text>
|
||||
|
||||
{skeleton ? (
|
||||
<InlineLoader label={title === 'Tools' ? 'discovering tools' : 'scanning skills'} t={t} />
|
||||
) : (
|
||||
shown.map(([k, vs]) => (
|
||||
<Text key={k} wrap="truncate">
|
||||
<Text color={t.color.muted}>{strip(k)}: </Text>
|
||||
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
|
||||
</Text>
|
||||
))
|
||||
)}
|
||||
|
||||
{overflow > 0 && (
|
||||
<Text color={t.color.muted}>
|
||||
(and {overflow} {overflowLabel})
|
||||
<>
|
||||
{shown.map(([k, vs]) => (
|
||||
<Text key={k} wrap="truncate">
|
||||
<Text color={t.color.muted}>{strip(k)}: </Text>
|
||||
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
|
||||
</Text>
|
||||
))}
|
||||
{overflow > 0 && (
|
||||
<Text color={t.color.muted}>(and {overflow} more categories…)</Text>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Collapsible tools section ──
|
||||
const toolEntries = Object.entries(info.tools).sort()
|
||||
const toolsTotal = flat(info.tools).length
|
||||
|
||||
const toolsBody = () => {
|
||||
const shown = toolEntries.slice(0, TOOLSETS_MAX)
|
||||
const overflow = toolEntries.length - TOOLSETS_MAX
|
||||
|
||||
return (
|
||||
<>
|
||||
{shown.map(([k, vs]) => (
|
||||
<Text key={k} wrap="truncate">
|
||||
<Text color={t.color.muted}>{strip(k)}: </Text>
|
||||
<Text color={t.color.text}>{truncLine(strip(k) + ': ', vs)}</Text>
|
||||
</Text>
|
||||
))}
|
||||
{overflow > 0 && (
|
||||
<Text color={t.color.muted}>(and {overflow} more toolsets…)</Text>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ── Collapsible MCP section ──
|
||||
const mcpBody = () => (
|
||||
<>
|
||||
{(info.mcp_servers ?? []).map(s => (
|
||||
<Text key={s.name} wrap="truncate">
|
||||
<Text color={t.color.muted}>{` ${s.name} `}</Text>
|
||||
<Text color={t.color.muted}>{`[${s.transport}]`}</Text>
|
||||
<Text color={t.color.muted}>: </Text>
|
||||
{s.connected ? (
|
||||
<Text color={t.color.text}>
|
||||
{s.tools} tool{s.tools === 1 ? '' : 's'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={t.color.error}>failed</Text>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
// ── System prompt body ──
|
||||
const sysPromptLen = (info.system_prompt ?? '').length
|
||||
|
||||
const systemBody = () => {
|
||||
if (sysPromptLen === 0) {
|
||||
return <Text color={t.color.muted}>No system prompt loaded.</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<Text color={t.color.muted}>
|
||||
{info.system_prompt}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -151,37 +249,64 @@ export function SessionPanel({ info, sid, t }: SessionPanelProps) {
|
|||
</Text>
|
||||
</Box>
|
||||
|
||||
{section('Tools', info.tools, 8, 'more toolsets…')}
|
||||
{section('Skills', info.skills)}
|
||||
{/* ── Tools (expanded by default) ── */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
onToggle={() => setToolsOpen(v => !v)}
|
||||
open={toolsOpen}
|
||||
t={t}
|
||||
title="Available Tools"
|
||||
/>
|
||||
{toolsOpen && toolsBody()}
|
||||
</Box>
|
||||
|
||||
{/* ── Skills (collapsed by default) ── */}
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
count={skillsTotal}
|
||||
onToggle={() => setSkillsOpen(v => !v)}
|
||||
open={skillsOpen}
|
||||
suffix={skillsCatCount > 0 ? `in ${skillsCatCount} categor${skillsCatCount === 1 ? 'y' : 'ies'}` : undefined}
|
||||
t={t}
|
||||
title="Available Skills"
|
||||
/>
|
||||
{skillsOpen && skillsBody()}
|
||||
</Box>
|
||||
|
||||
{/* ── System Prompt (collapsed by default) ── */}
|
||||
{sysPromptLen > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<CollapseToggle
|
||||
onToggle={() => setSystemOpen(v => !v)}
|
||||
open={systemOpen}
|
||||
suffix={`— ${sysPromptLen.toLocaleString()} chars`}
|
||||
t={t}
|
||||
title="System Prompt"
|
||||
/>
|
||||
{systemOpen && systemBody()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ── MCP Servers (collapsed by default) ── */}
|
||||
{info.mcp_servers && info.mcp_servers.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text bold color={t.color.accent}>
|
||||
MCP Servers
|
||||
</Text>
|
||||
|
||||
{info.mcp_servers.map(s => (
|
||||
<Text key={s.name} wrap="truncate">
|
||||
<Text color={t.color.muted}>{` ${s.name} `}</Text>
|
||||
<Text color={t.color.muted}>{`[${s.transport}]`}</Text>
|
||||
<Text color={t.color.muted}>: </Text>
|
||||
{s.connected ? (
|
||||
<Text color={t.color.text}>
|
||||
{s.tools} tool{s.tools === 1 ? '' : 's'}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={t.color.error}>failed</Text>
|
||||
)}
|
||||
</Text>
|
||||
))}
|
||||
<CollapseToggle
|
||||
count={info.mcp_servers.length}
|
||||
onToggle={() => setMcpOpen(v => !v)}
|
||||
open={mcpOpen}
|
||||
suffix="connected"
|
||||
t={t}
|
||||
title="MCP Servers"
|
||||
/>
|
||||
{mcpOpen && mcpBody()}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Text />
|
||||
|
||||
<Text color={t.color.text}>
|
||||
{flat(info.tools).length} tools{' · '}
|
||||
{flat(info.skills).length} skills
|
||||
{toolsTotal} tools{' · '}
|
||||
{skillsTotal} skills
|
||||
{info.mcp_servers?.length ? ` · ${info.mcp_servers.length} MCP` : ''}
|
||||
{' · '}
|
||||
<Text color={t.color.muted}>/help for commands</Text>
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export interface SessionInfo {
|
|||
release_date?: string
|
||||
service_tier?: string
|
||||
skills: Record<string, string[]>
|
||||
system_prompt?: string
|
||||
tools: Record<string, string[]>
|
||||
update_behind?: number | null
|
||||
update_command?: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue