opentui(v5b/item1): Ink-parity startup banner panel
Rebuilt the home screen to match hermes --tui: the HERMES-AGENT banner + tagline,
then a session info block (model · Nous Research / dir (branch) / Session: <id>),
then SEPARATE collapsible sections — Available Tools (enabled toolsets each as
'name: tool1, tool2', capped + '(and N more toolsets…)'), Available Skills (N) in
M categories, MCP Servers (N) connected — and a '… /help for commands' summary.
Previously it was one combined '▶ N tools · M skills · K MCP' dropdown that only
listed tools and showed no model/dir/session.
- gateway startup.catalog now returns per-toolset {enabled, tools} (resolved_tools,
session-aware enabled set — mirrors tools.list); py_compile OK.
- store Catalog gains toolset.enabled/tools; new sessionId field + setSessionId,
set on session create/resume (alongside the active-session-file write).
- homeHint takes the store, reads info (model/cwd/branch) + sessionId + catalog.
85 pass; verified live (model·Nous·dir·session + enabled toolsets w/ tools).
This commit is contained in:
parent
06762a0f5e
commit
fb04e85a14
@ -8528,11 +8528,24 @@ def _(rid, params: dict) -> dict:
|
|||||||
try:
|
try:
|
||||||
from toolsets import get_all_toolsets, get_toolset_info
|
from toolsets import get_all_toolsets, get_toolset_info
|
||||||
|
|
||||||
|
# enabled toolsets for THIS session (or the config default), mirroring tools.list
|
||||||
|
session = _sessions.get(params.get("session_id", ""))
|
||||||
|
enabled = (
|
||||||
|
set(getattr(session["agent"], "enabled_toolsets", []) or [])
|
||||||
|
if session
|
||||||
|
else set(_load_enabled_toolsets() or [])
|
||||||
|
)
|
||||||
for name in sorted(get_all_toolsets().keys()):
|
for name in sorted(get_all_toolsets().keys()):
|
||||||
info = get_toolset_info(name)
|
info = get_toolset_info(name)
|
||||||
if not info:
|
if not info:
|
||||||
continue
|
continue
|
||||||
tools["toolsets"].append({"name": name, "count": int(info["tool_count"])})
|
is_on = name in enabled if enabled else True
|
||||||
|
# the startup panel lists ENABLED toolsets with their tools (Ink parity)
|
||||||
|
tool_names = [str(t) for t in (info.get("resolved_tools") or [])]
|
||||||
|
tools["toolsets"].append(
|
||||||
|
{"name": name, "count": int(info["tool_count"]), "enabled": is_on, "tools": tool_names}
|
||||||
|
)
|
||||||
|
if is_on:
|
||||||
tools["total"] += int(info["tool_count"])
|
tools["total"] += int(info["tool_count"])
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@ -82,6 +82,7 @@ const writeActiveSession = (sid: string | undefined) => {
|
|||||||
const resumeInto = (gateway: GatewayServiceShape, store: SessionStore, sid: string, cols: number) =>
|
const resumeInto = (gateway: GatewayServiceShape, store: SessionStore, sid: string, cols: number) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
writeActiveSession(sid) // the session we're switching to is now the active one (#5)
|
writeActiveSession(sid) // the session we're switching to is now the active one (#5)
|
||||||
|
store.setSessionId(sid)
|
||||||
store.beginBuffer()
|
store.beginBuffer()
|
||||||
const t0 = Date.now()
|
const t0 = Date.now()
|
||||||
const resumed = yield* gateway.request<{ messages?: unknown; info?: Record<string, unknown> }>('session.resume', {
|
const resumed = yield* gateway.request<{ messages?: unknown; info?: Record<string, unknown> }>('session.resume', {
|
||||||
@ -148,6 +149,7 @@ const bootstrapSession = (gateway: GatewayServiceShape, store: SessionStore, inp
|
|||||||
}
|
}
|
||||||
if (created?.info) store.applyInfo(created.info)
|
if (created?.info) store.applyInfo(created.info)
|
||||||
writeActiveSession(sid) // record the new session for the launcher's exit epilogue (#5)
|
writeActiveSession(sid) // record the new session for the launcher's exit epilogue (#5)
|
||||||
|
store.setSessionId(sid)
|
||||||
log.info('bootstrap', 'session created', { sid })
|
log.info('bootstrap', 'session created', { sid })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -140,9 +140,12 @@ export interface SessionInfo {
|
|||||||
compressions?: number
|
compressions?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Startup catalog (tools/skills/MCP) for the home-screen panel (item 9). */
|
/** Startup catalog (tools/skills/MCP) for the home-screen panel (item 9 / banner parity). */
|
||||||
export interface Catalog {
|
export interface Catalog {
|
||||||
readonly tools: { readonly total: number; readonly toolsets: ReadonlyArray<{ name: string; count: number }> }
|
readonly tools: {
|
||||||
|
readonly total: number
|
||||||
|
readonly toolsets: ReadonlyArray<{ name: string; count: number; enabled: boolean; tools: ReadonlyArray<string> }>
|
||||||
|
}
|
||||||
readonly skills: { readonly total: number; readonly categories: ReadonlyArray<{ name: string; count: number }> }
|
readonly skills: { readonly total: number; readonly categories: ReadonlyArray<{ name: string; count: number }> }
|
||||||
readonly mcp: { readonly servers: ReadonlyArray<string> }
|
readonly mcp: { readonly servers: ReadonlyArray<string> }
|
||||||
}
|
}
|
||||||
@ -178,6 +181,8 @@ export interface StoreState {
|
|||||||
hint: string | undefined
|
hint: string | undefined
|
||||||
/** Startup tools/skills/MCP catalog (from `startup.catalog`) for the home panel (item 9). */
|
/** Startup tools/skills/MCP catalog (from `startup.catalog`) for the home panel (item 9). */
|
||||||
catalog: Catalog | undefined
|
catalog: Catalog | undefined
|
||||||
|
/** The current session id (shown in the home panel; updated on create/resume). */
|
||||||
|
sessionId: string | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const LRU_LIMIT = 1000
|
const LRU_LIMIT = 1000
|
||||||
@ -264,7 +269,8 @@ export function createSessionStore() {
|
|||||||
status: undefined,
|
status: undefined,
|
||||||
info: {},
|
info: {},
|
||||||
hint: undefined,
|
hint: undefined,
|
||||||
catalog: undefined
|
catalog: undefined,
|
||||||
|
sessionId: undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// Monotonic part id (stable `key` per part so a new tool part below a streaming
|
// Monotonic part id (stable `key` per part so a new tool part below a streaming
|
||||||
@ -669,26 +675,36 @@ export function createSessionStore() {
|
|||||||
v && typeof v === 'object' ? (v as { readonly [k: string]: unknown }) : {}
|
v && typeof v === 'object' ? (v as { readonly [k: string]: unknown }) : {}
|
||||||
const num = (v: unknown): number => (typeof v === 'number' ? v : 0)
|
const num = (v: unknown): number => (typeof v === 'number' ? v : 0)
|
||||||
const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : [])
|
const list = (v: unknown): unknown[] => (Array.isArray(v) ? v : [])
|
||||||
|
const strs = (v: unknown): string[] => list(v).filter((s): s is string => typeof s === 'string')
|
||||||
const pair = (v: unknown) => {
|
const pair = (v: unknown) => {
|
||||||
const o = obj(v)
|
const o = obj(v)
|
||||||
return { count: num(o.count), name: readStr(o, 'name') ?? '' }
|
return { count: num(o.count), name: readStr(o, 'name') ?? '' }
|
||||||
}
|
}
|
||||||
|
const toolset = (v: unknown) => {
|
||||||
|
const o = obj(v)
|
||||||
|
return { count: num(o.count), enabled: o.enabled !== false, name: readStr(o, 'name') ?? '', tools: strs(o.tools) }
|
||||||
|
}
|
||||||
const tools = obj(root.tools)
|
const tools = obj(root.tools)
|
||||||
const skills = obj(root.skills)
|
const skills = obj(root.skills)
|
||||||
const mcp = obj(root.mcp)
|
const mcp = obj(root.mcp)
|
||||||
setState('catalog', {
|
setState('catalog', {
|
||||||
mcp: { servers: list(mcp.servers).filter((s): s is string => typeof s === 'string') },
|
mcp: { servers: strs(mcp.servers) },
|
||||||
skills: { categories: list(skills.categories).map(pair).filter(p => p.name), total: num(skills.total) },
|
skills: { categories: list(skills.categories).map(pair).filter(p => p.name), total: num(skills.total) },
|
||||||
tools: { toolsets: list(tools.toolsets).map(pair).filter(p => p.name), total: num(tools.total) }
|
tools: { toolsets: list(tools.toolsets).map(toolset).filter(p => p.name), total: num(tools.total) }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSessionId(sid: string | undefined): void {
|
||||||
|
setState('sessionId', sid)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
apply,
|
apply,
|
||||||
pushUser,
|
pushUser,
|
||||||
pushSystem,
|
pushSystem,
|
||||||
setCatalog,
|
setCatalog,
|
||||||
|
setSessionId,
|
||||||
clearTranscript,
|
clearTranscript,
|
||||||
setConfirm,
|
setConfirm,
|
||||||
openPager,
|
openPager,
|
||||||
|
|||||||
@ -241,16 +241,15 @@ describe('App render (Phase 1, themed)', () => {
|
|||||||
<App store={store} />
|
<App store={store} />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
),
|
),
|
||||||
{ until: '/help', width: 72, height: 20 }
|
{ until: 'Nous Research', width: 72, height: 20 }
|
||||||
)
|
)
|
||||||
|
|
||||||
// (theme-independent assertions — testRender reuses a global root, so a prior
|
// (theme-independent assertions — testRender reuses a global root, so a prior
|
||||||
// test's skin/brand can bleed; the real app has one store. The home hint's
|
// test's skin/brand can bleed; the real app has one store. The home hint's
|
||||||
// content is what matters here.)
|
// content is what matters here.)
|
||||||
expect(frame).toContain('/help') // common command
|
expect(frame).toContain('Nous Research') // the tagline
|
||||||
expect(frame).toContain('/agents')
|
|
||||||
expect(frame).toContain('resume a session')
|
|
||||||
expect(frame).toContain('to mention') // the input tips line
|
expect(frame).toContain('to mention') // the input tips line
|
||||||
|
expect(frame).toContain('Ctrl+C to stop/quit')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('the home screen shows a collapsible tools/skills/MCP catalog panel (item 9)', async () => {
|
test('the home screen shows a collapsible tools/skills/MCP catalog panel (item 9)', async () => {
|
||||||
|
|||||||
@ -137,14 +137,24 @@ describe('session store — ordered parts (Phase 2b)', () => {
|
|||||||
test('setCatalog maps the loose startup.catalog response defensively (item 9)', () => {
|
test('setCatalog maps the loose startup.catalog response defensively (item 9)', () => {
|
||||||
const store = createSessionStore()
|
const store = createSessionStore()
|
||||||
store.setCatalog({
|
store.setCatalog({
|
||||||
tools: { total: 42, toolsets: [{ name: 'core', count: 12 }, { name: '', count: 1 }] },
|
tools: {
|
||||||
|
total: 42,
|
||||||
|
toolsets: [
|
||||||
|
{ name: 'core', count: 12, enabled: true, tools: ['a', 'b', 3] },
|
||||||
|
{ name: 'off', count: 5, enabled: false, tools: [] },
|
||||||
|
{ name: '', count: 1 }
|
||||||
|
]
|
||||||
|
},
|
||||||
skills: { total: 7, categories: [{ name: 'dev', count: 7 }] },
|
skills: { total: 7, categories: [{ name: 'dev', count: 7 }] },
|
||||||
mcp: { servers: ['railway', 123, 'beeper'] },
|
mcp: { servers: ['railway', 123, 'beeper'] },
|
||||||
junk: 'ignored'
|
junk: 'ignored'
|
||||||
})
|
})
|
||||||
const c = store.state.catalog!
|
const c = store.state.catalog!
|
||||||
expect(c.tools.total).toBe(42)
|
expect(c.tools.total).toBe(42)
|
||||||
expect(c.tools.toolsets).toEqual([{ name: 'core', count: 12 }]) // nameless entry dropped
|
expect(c.tools.toolsets).toEqual([
|
||||||
|
{ name: 'core', count: 12, enabled: true, tools: ['a', 'b'] }, // non-string tool dropped
|
||||||
|
{ name: 'off', count: 5, enabled: false, tools: [] } // enabled flag preserved
|
||||||
|
]) // nameless entry dropped
|
||||||
expect(c.skills.total).toBe(7)
|
expect(c.skills.total).toBe(7)
|
||||||
expect(c.mcp.servers).toEqual(['railway', 'beeper']) // non-string dropped
|
expect(c.mcp.servers).toEqual(['railway', 'beeper']) // non-string dropped
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,25 +1,17 @@
|
|||||||
/**
|
/**
|
||||||
* HomeHint — the empty-transcript home screen (items 12 + 9). Shows the full
|
* HomeHint — the empty-transcript home screen (items 12 + 9; Ink `branding.tsx`
|
||||||
* HERMES-AGENT banner (the canonical CLI logo, width-guarded → compact brand on
|
* parity). The HERMES-AGENT banner + a tagline, then a session info block
|
||||||
* narrow terminals), the welcome line, a COLLAPSIBLE tools/skills/MCP catalog
|
* (model · Nous Research / dir / Session id), then SEPARATE collapsible sections —
|
||||||
* panel (item 9), the common commands, and the key tips. Fully themed; decorative,
|
* Available Tools (enabled toolsets + their tools), Available Skills, MCP Servers —
|
||||||
* so `selectable={false}` (item 4).
|
* and a summary line. Fully themed; decorative, so `selectable={false}` (item 4).
|
||||||
*/
|
*/
|
||||||
|
import { createSignal, For, type JSX, Show } from 'solid-js'
|
||||||
|
|
||||||
|
import type { SessionStore } from '../logic/store.ts'
|
||||||
|
import { truncate } from '../logic/toolOutput.ts'
|
||||||
import { useDimensions } from './dimensions.tsx'
|
import { useDimensions } from './dimensions.tsx'
|
||||||
import { createSignal, For, Show } from 'solid-js'
|
|
||||||
|
|
||||||
import type { Catalog } from '../logic/store.ts'
|
|
||||||
import { useTheme } from './theme.tsx'
|
import { useTheme } from './theme.tsx'
|
||||||
|
|
||||||
const COMMANDS: ReadonlyArray<readonly [string, string]> = [
|
|
||||||
['/help', 'list all commands'],
|
|
||||||
['/model', 'switch model'],
|
|
||||||
['/sessions', 'resume a session'],
|
|
||||||
['/skills', 'browse skills'],
|
|
||||||
['/agents', 'live delegation trace'],
|
|
||||||
['/clear', 'clear the transcript']
|
|
||||||
]
|
|
||||||
|
|
||||||
// The canonical HERMES-AGENT block logo (hermes_cli/banner.py), gold→amber→bronze.
|
// The canonical HERMES-AGENT block logo (hermes_cli/banner.py), gold→amber→bronze.
|
||||||
const BANNER: ReadonlyArray<readonly [string, 'primary' | 'accent' | 'border']> = [
|
const BANNER: ReadonlyArray<readonly [string, 'primary' | 'accent' | 'border']> = [
|
||||||
['██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗', 'primary'],
|
['██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗', 'primary'],
|
||||||
@ -30,13 +22,47 @@ const BANNER: ReadonlyArray<readonly [string, 'primary' | 'accent' | 'border']>
|
|||||||
['╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝', 'border']
|
['╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝', 'border']
|
||||||
]
|
]
|
||||||
const BANNER_W = 102
|
const BANNER_W = 102
|
||||||
|
const TOOLSETS_MAX = 10
|
||||||
|
|
||||||
export function HomeHint(props: { catalog: Catalog | undefined }) {
|
/** `anthropic/claude-opus-4-8` → `claude-opus-4-8`. */
|
||||||
|
const shortModel = (m: string) => (m.includes('/') ? (m.split('/').at(-1) ?? m) : m)
|
||||||
|
const HOME = process.env.HOME ?? ''
|
||||||
|
const shortCwd = (cwd: string) => (HOME && cwd.startsWith(HOME) ? '~' + cwd.slice(HOME.length) : cwd)
|
||||||
|
|
||||||
|
export function HomeHint(props: { store: SessionStore }) {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const dims = useDimensions()
|
const dims = useDimensions()
|
||||||
const [open, setOpen] = createSignal(false)
|
|
||||||
const wide = () => dims().width >= BANNER_W
|
const wide = () => dims().width >= BANNER_W
|
||||||
const cat = () => props.catalog
|
const cat = () => props.store.state.catalog
|
||||||
|
const info = () => props.store.state.info
|
||||||
|
const enabledToolsets = () => (cat()?.tools.toolsets ?? []).filter(t => t.enabled)
|
||||||
|
|
||||||
|
// A collapsible section: ▸/▾ accent chevron + label title + optional muted suffix.
|
||||||
|
function Section(p: { title: string; suffix?: string; open?: boolean; children: JSX.Element }) {
|
||||||
|
const [open, setOpen] = createSignal(p.open ?? false)
|
||||||
|
return (
|
||||||
|
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
||||||
|
<box style={{ flexDirection: 'row', flexShrink: 0 }} onMouseDown={() => setOpen(o => !o)}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.accent }}>{open() ? '▾ ' : '▸ '}</span>
|
||||||
|
<span style={{ fg: theme().color.label }}>{p.title}</span>
|
||||||
|
<Show when={p.suffix}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{` ${p.suffix}`}</span>
|
||||||
|
</Show>
|
||||||
|
</text>
|
||||||
|
</box>
|
||||||
|
<Show when={open()}>
|
||||||
|
<box
|
||||||
|
style={{ flexDirection: 'column', marginLeft: 2, paddingLeft: 1 }}
|
||||||
|
border={['left']}
|
||||||
|
borderColor={theme().color.border}
|
||||||
|
>
|
||||||
|
{p.children}
|
||||||
|
</box>
|
||||||
|
</Show>
|
||||||
|
</box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<box style={{ flexDirection: 'column', flexShrink: 0, paddingLeft: 1, marginTop: 1 }}>
|
<box style={{ flexDirection: 'column', flexShrink: 0, paddingLeft: 1, marginTop: 1 }}>
|
||||||
@ -60,69 +86,83 @@ export function HomeHint(props: { catalog: Catalog | undefined }) {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<box style={{ marginTop: 1 }}>
|
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.muted }}>{theme().brand.welcome}</span>
|
<span style={{ fg: theme().color.accent }}>{`${theme().brand.icon} `}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}>Nous Research · Messenger of the Digital Gods</span>
|
||||||
</text>
|
</text>
|
||||||
|
|
||||||
|
{/* session info block: model · Nous Research / dir / Session id */}
|
||||||
|
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
||||||
|
<Show when={info().model}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.accent }}>{shortModel(info().model!)}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}> · Nous Research</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<Show when={info().cwd}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{shortCwd(info().cwd!)}</span>
|
||||||
|
<Show when={info().branch}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{` (${info().branch})`}</span>
|
||||||
|
</Show>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
<Show when={props.store.state.sessionId}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>Session: </span>
|
||||||
|
<span style={{ fg: theme().color.border }}>{props.store.state.sessionId}</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
|
|
||||||
{/* collapsible tools / skills / MCP catalog (item 9) */}
|
{/* SEPARATE collapsible sections (Ink parity) + summary */}
|
||||||
<Show when={cat()}>
|
<Show when={cat()}>
|
||||||
{c => (
|
{c => (
|
||||||
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
<box style={{ flexDirection: 'column' }}>
|
||||||
<box style={{ flexDirection: 'row', flexShrink: 0 }} onMouseDown={() => setOpen(o => !o)}>
|
<Section title="Available Tools" open>
|
||||||
|
<For each={enabledToolsets().slice(0, TOOLSETS_MAX)}>
|
||||||
|
{ts => (
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.muted }}>{open() ? '▼ ' : '▶ '}</span>
|
<span style={{ fg: theme().color.label }}>{`${ts.name}: `}</span>
|
||||||
<span style={{ fg: theme().color.label }}>{`${c().tools.total} tools`}</span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{' · '}</span>
|
|
||||||
<span style={{ fg: theme().color.label }}>{`${c().skills.total} skills`}</span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{' · '}</span>
|
|
||||||
<span style={{ fg: theme().color.label }}>{`${c().mcp.servers.length} MCP`}</span>
|
|
||||||
</text>
|
|
||||||
</box>
|
|
||||||
<Show when={open()}>
|
|
||||||
<box
|
|
||||||
style={{ flexDirection: 'column', marginLeft: 2, paddingLeft: 1 }}
|
|
||||||
border={['left']}
|
|
||||||
borderColor={theme().color.border}
|
|
||||||
>
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.label }}>toolsets </span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>
|
<span style={{ fg: theme().color.muted }}>
|
||||||
{c().tools.toolsets.map(t => `${t.name}(${t.count})`).join(' ')}
|
{truncate(ts.tools.join(', ') || `${ts.count} tools`, Math.max(20, dims().width - ts.name.length - 8))}
|
||||||
</span>
|
</span>
|
||||||
</text>
|
</text>
|
||||||
<Show when={c().skills.categories.length > 0}>
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.label }}>skills </span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>
|
|
||||||
{c().skills.categories.map(s => `${s.name}(${s.count})`).join(' ')}
|
|
||||||
</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
<Show when={c().mcp.servers.length > 0}>
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.label }}>mcp </span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ')}</span>
|
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
</Show>
|
|
||||||
</box>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
|
||||||
<For each={COMMANDS}>
|
|
||||||
{([cmd, desc]) => (
|
|
||||||
<text selectable={false}>
|
|
||||||
<span style={{ fg: theme().color.accent }}>{cmd.padEnd(12)}</span>
|
|
||||||
<span style={{ fg: theme().color.muted }}>{desc}</span>
|
|
||||||
</text>
|
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
<Show when={enabledToolsets().length > TOOLSETS_MAX}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{`(and ${enabledToolsets().length - TOOLSETS_MAX} more toolsets…)`}</span>
|
||||||
|
</text>
|
||||||
|
</Show>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={`Available Skills (${c().skills.total})`} suffix={`in ${c().skills.categories.length} categories`}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>
|
||||||
|
{c().skills.categories.map(s => `${s.name} (${s.count})`).join(' ')}
|
||||||
|
</span>
|
||||||
|
</text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section title={`MCP Servers (${c().mcp.servers.length})`} suffix={c().mcp.servers.length ? 'connected' : ''}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{c().mcp.servers.join(' ') || 'none configured'}</span>
|
||||||
|
</text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<box style={{ marginTop: 1 }}>
|
||||||
|
<text selectable={false}>
|
||||||
|
<span style={{ fg: theme().color.text }}>{`${c().tools.total} tools`}</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}>{` · ${c().skills.total} skills · ${c().mcp.servers.length} MCP · `}</span>
|
||||||
|
<span style={{ fg: theme().color.accent }}>/help</span>
|
||||||
|
<span style={{ fg: theme().color.muted }}> for commands</span>
|
||||||
|
</text>
|
||||||
</box>
|
</box>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
|
||||||
<box style={{ marginTop: 1 }}>
|
<box style={{ marginTop: 1 }}>
|
||||||
<text selectable={false}>
|
<text selectable={false}>
|
||||||
<span style={{ fg: theme().color.muted }}>
|
<span style={{ fg: theme().color.muted }}>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ export function Transcript(props: { store: SessionStore }) {
|
|||||||
<ScrollAnchorProvider scroll={scroll}>
|
<ScrollAnchorProvider scroll={scroll}>
|
||||||
{/* empty-transcript home screen (item 12); replaced by messages on the first turn */}
|
{/* empty-transcript home screen (item 12); replaced by messages on the first turn */}
|
||||||
<Show when={props.store.state.messages.length === 0}>
|
<Show when={props.store.state.messages.length === 0}>
|
||||||
<HomeHint catalog={props.store.state.catalog} />
|
<HomeHint store={props.store} />
|
||||||
</Show>
|
</Show>
|
||||||
<For each={props.store.state.messages}>{message => <MessageLine message={message} />}</For>
|
<For each={props.store.state.messages}>{message => <MessageLine message={message} />}</For>
|
||||||
</ScrollAnchorProvider>
|
</ScrollAnchorProvider>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user