mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: menus
This commit is contained in:
parent
4fe78d5b88
commit
e1df13cf20
4 changed files with 193 additions and 32 deletions
|
|
@ -496,8 +496,11 @@ def do_inspect(identifier: str, console: Optional[Console] = None) -> None:
|
||||||
c.print()
|
c.print()
|
||||||
|
|
||||||
|
|
||||||
def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> list[dict]:
|
def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> dict:
|
||||||
"""Paginated hub browse for programmatic callers (e.g. TUI gateway)."""
|
"""Paginated hub browse for programmatic callers (e.g. TUI gateway).
|
||||||
|
|
||||||
|
Returns ``{"items": [...], "page": int, "total_pages": int, "total": int}``.
|
||||||
|
"""
|
||||||
from tools.skills_hub import GitHubAuth, create_source_router
|
from tools.skills_hub import GitHubAuth, create_source_router
|
||||||
|
|
||||||
page_size = max(1, min(page_size, 100))
|
page_size = max(1, min(page_size, 100))
|
||||||
|
|
@ -517,7 +520,7 @@ def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> li
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if not all_results:
|
if not all_results:
|
||||||
return []
|
return {"items": [], "page": 1, "total_pages": 1, "total": 0}
|
||||||
seen: dict = {}
|
seen: dict = {}
|
||||||
for r in all_results:
|
for r in all_results:
|
||||||
rank = _TRUST_RANK.get(r.trust_level, 0)
|
rank = _TRUST_RANK.get(r.trust_level, 0)
|
||||||
|
|
@ -530,7 +533,13 @@ def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> li
|
||||||
page = max(1, min(page, total_pages))
|
page = max(1, min(page, total_pages))
|
||||||
start = (page - 1) * page_size
|
start = (page - 1) * page_size
|
||||||
page_items = deduped[start : min(start + page_size, total)]
|
page_items = deduped[start : min(start + page_size, total)]
|
||||||
return [{"name": r.name, "description": r.description} for r in page_items]
|
return {
|
||||||
|
"items": [{"name": r.name, "description": r.description, "source": r.source,
|
||||||
|
"trust": r.trust_level} for r in page_items],
|
||||||
|
"page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"total": total,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def inspect_skill(identifier: str) -> Optional[dict]:
|
def inspect_skill(identifier: str) -> Optional[dict]:
|
||||||
|
|
|
||||||
|
|
@ -903,31 +903,74 @@ def _(rid, params: dict) -> dict:
|
||||||
return _err(rid, 5015, str(e))
|
return _err(rid, 5015, str(e))
|
||||||
|
|
||||||
|
|
||||||
|
_TUI_HIDDEN: frozenset[str] = frozenset({
|
||||||
|
"sethome", "set-home", "update", "commands", "status", "approve", "deny",
|
||||||
|
})
|
||||||
|
|
||||||
|
_TUI_EXTRA: list[tuple[str, str, str]] = [
|
||||||
|
("/compact", "Toggle compact display mode", "TUI"),
|
||||||
|
("/logs", "Show recent gateway log lines", "TUI"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@method("commands.catalog")
|
@method("commands.catalog")
|
||||||
def _(rid, params: dict) -> dict:
|
def _(rid, params: dict) -> dict:
|
||||||
"""Registry-backed slash metadata (same surface as SlashCommandCompleter)."""
|
"""Registry-backed slash metadata for the TUI — categorized, no aliases."""
|
||||||
try:
|
try:
|
||||||
from hermes_cli.commands import COMMAND_REGISTRY, COMMANDS, SUBCOMMANDS
|
from hermes_cli.commands import COMMAND_REGISTRY, SUBCOMMANDS, _build_description
|
||||||
|
|
||||||
pairs = sorted(COMMANDS.items(), key=lambda kv: kv[0])
|
all_pairs: list[list[str]] = []
|
||||||
sub = {k: v[:] for k, v in SUBCOMMANDS.items()}
|
|
||||||
canon: dict[str, str] = {}
|
canon: dict[str, str] = {}
|
||||||
|
categories: list[dict] = []
|
||||||
|
cat_map: dict[str, list[list[str]]] = {}
|
||||||
|
cat_order: list[str] = []
|
||||||
|
|
||||||
for cmd in COMMAND_REGISTRY:
|
for cmd in COMMAND_REGISTRY:
|
||||||
if cmd.gateway_only:
|
|
||||||
continue
|
|
||||||
c = f"/{cmd.name}"
|
c = f"/{cmd.name}"
|
||||||
canon[c.lower()] = c
|
canon[c.lower()] = c
|
||||||
for a in cmd.aliases:
|
for a in cmd.aliases:
|
||||||
canon[f"/{a}".lower()] = c
|
canon[f"/{a}".lower()] = c
|
||||||
skills = []
|
|
||||||
|
if cmd.name in _TUI_HIDDEN:
|
||||||
|
continue
|
||||||
|
|
||||||
|
desc = _build_description(cmd)
|
||||||
|
all_pairs.append([c, desc])
|
||||||
|
|
||||||
|
cat = cmd.category
|
||||||
|
if cat not in cat_map:
|
||||||
|
cat_map[cat] = []
|
||||||
|
cat_order.append(cat)
|
||||||
|
cat_map[cat].append([c, desc])
|
||||||
|
|
||||||
|
for name, desc, cat in _TUI_EXTRA:
|
||||||
|
all_pairs.append([name, desc])
|
||||||
|
if cat not in cat_map:
|
||||||
|
cat_map[cat] = []
|
||||||
|
cat_order.append(cat)
|
||||||
|
cat_map[cat].append([name, desc])
|
||||||
|
|
||||||
|
skill_count = 0
|
||||||
try:
|
try:
|
||||||
from agent.skill_commands import scan_skill_commands
|
from agent.skill_commands import scan_skill_commands
|
||||||
for k, info in scan_skill_commands().items():
|
for k, info in sorted(scan_skill_commands().items()):
|
||||||
d = str(info.get("description", "Skill"))
|
d = str(info.get("description", "Skill"))
|
||||||
skills.append([k, f"⚡ {d[:120]}{'…' if len(d) > 120 else ''}"])
|
all_pairs.append([k, d[:120] + ("…" if len(d) > 120 else "")])
|
||||||
|
skill_count += 1
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return _ok(rid, {"pairs": pairs + skills, "sub": sub, "canon": canon})
|
|
||||||
|
for cat in cat_order:
|
||||||
|
categories.append({"name": cat, "pairs": cat_map[cat]})
|
||||||
|
|
||||||
|
sub = {k: v[:] for k, v in SUBCOMMANDS.items()}
|
||||||
|
return _ok(rid, {
|
||||||
|
"pairs": all_pairs,
|
||||||
|
"sub": sub,
|
||||||
|
"canon": canon,
|
||||||
|
"categories": categories,
|
||||||
|
"skill_count": skill_count,
|
||||||
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return _err(rid, 5020, str(e))
|
return _err(rid, 5020, str(e))
|
||||||
|
|
||||||
|
|
@ -1416,8 +1459,8 @@ def _(rid, params: dict) -> dict:
|
||||||
return _ok(rid, {"installed": True, "name": query})
|
return _ok(rid, {"installed": True, "name": query})
|
||||||
if action == "browse":
|
if action == "browse":
|
||||||
from hermes_cli.skills_hub import browse_skills
|
from hermes_cli.skills_hub import browse_skills
|
||||||
return _ok(rid, {"results": [{"name": r.get("name", ""), "description": r.get("description", "")}
|
pg = int(params.get("page", 0) or 0) or (int(query) if query.isdigit() else 1)
|
||||||
for r in (browse_skills(page=int(query) if query.isdigit() else 1) or [])]})
|
return _ok(rid, browse_skills(page=pg, page_size=int(params.get("page_size", 20))))
|
||||||
if action == "inspect":
|
if action == "inspect":
|
||||||
from hermes_cli.skills_hub import inspect_skill
|
from hermes_cli.skills_hub import inspect_skill
|
||||||
return _ok(rid, {"info": inspect_skill(query) or {}})
|
return _ok(rid, {"info": inspect_skill(query) or {}})
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
||||||
const [bgTasks, setBgTasks] = useState<Set<string>>(new Set())
|
const [bgTasks, setBgTasks] = useState<Set<string>>(new Set())
|
||||||
const [catalog, setCatalog] = useState<SlashCatalog | null>(null)
|
const [catalog, setCatalog] = useState<SlashCatalog | null>(null)
|
||||||
|
const [pager, setPager] = useState<{ lines: string[]; offset: number } | null>(null)
|
||||||
|
|
||||||
// ── Refs ─────────────────────────────────────────────────────────
|
// ── Refs ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -312,7 +313,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, blocked(), gw)
|
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, blocked(), gw)
|
||||||
|
|
||||||
function blocked() {
|
function blocked() {
|
||||||
return !!(clarify || approval || pasteReview || picker || secret || sudo)
|
return !!(clarify || approval || pasteReview || picker || secret || sudo || pager)
|
||||||
}
|
}
|
||||||
|
|
||||||
const empty = !messages.length
|
const empty = !messages.length
|
||||||
|
|
@ -349,6 +350,11 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
const sys = useCallback((text: string) => appendMessage({ role: 'system' as const, text }), [appendMessage])
|
const sys = useCallback((text: string) => appendMessage({ role: 'system' as const, text }), [appendMessage])
|
||||||
|
|
||||||
|
const page = useCallback((text: string) => {
|
||||||
|
const lines = text.split('\n')
|
||||||
|
setPager({ lines, offset: 0 })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const pushActivity = useCallback((text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) => {
|
const pushActivity = useCallback((text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) => {
|
||||||
setActivity(prev => {
|
setActivity(prev => {
|
||||||
const base = replaceLabel ? prev.filter(a => !sameToolTrailGroup(replaceLabel, a.text)) : prev
|
const base = replaceLabel ? prev.filter(a => !sameToolTrailGroup(replaceLabel, a.text)) : prev
|
||||||
|
|
@ -803,8 +809,26 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
const ctrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target
|
const ctrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target
|
||||||
|
|
||||||
|
const pagerPageSize = Math.max(5, (stdout?.rows ?? 24) - 6)
|
||||||
|
|
||||||
useInput((ch, key) => {
|
useInput((ch, key) => {
|
||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
|
if (pager) {
|
||||||
|
if (key.return || ch === ' ') {
|
||||||
|
const next = pager.offset + pagerPageSize
|
||||||
|
|
||||||
|
if (next >= pager.lines.length) {
|
||||||
|
setPager(null)
|
||||||
|
} else {
|
||||||
|
setPager({ ...pager, offset: next })
|
||||||
|
}
|
||||||
|
} else if (key.escape || ctrl(key, ch, 'c') || ch === 'q') {
|
||||||
|
setPager(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (pasteReview) {
|
if (pasteReview) {
|
||||||
if (key.return) {
|
if (key.return) {
|
||||||
setPasteReview(null)
|
setPasteReview(null)
|
||||||
|
|
@ -970,7 +994,9 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
setCatalog({
|
setCatalog({
|
||||||
canon: (r.canon ?? {}) as Record<string, string>,
|
canon: (r.canon ?? {}) as Record<string, string>,
|
||||||
|
categories: (r.categories ?? []) as SlashCatalog['categories'],
|
||||||
pairs: r.pairs as [string, string][],
|
pairs: r.pairs as [string, string][],
|
||||||
|
skillCount: (r.skill_count ?? 0) as number,
|
||||||
sub: (r.sub ?? {}) as Record<string, string[]>
|
sub: (r.sub ?? {}) as Record<string, string[]>
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -1272,21 +1298,26 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'help': {
|
case 'help': {
|
||||||
const rows = catalog?.pairs ?? []
|
const cats = catalog?.categories ?? []
|
||||||
const cap = 52
|
const skills = catalog?.skillCount ?? 0
|
||||||
|
const lines: string[] = []
|
||||||
|
|
||||||
sys(
|
for (const { name: catName, pairs } of cats) {
|
||||||
[
|
if (lines.length) lines.push('')
|
||||||
' Commands:',
|
lines.push(` ${catName}:`)
|
||||||
...rows.slice(0, cap).map(([c, d]) => ` ${c.padEnd(16)} ${d}`),
|
for (const [c, d] of pairs) lines.push(` ${c.padEnd(18)} ${d}`)
|
||||||
rows.length > cap ? ` … ${rows.length - cap} more` : '',
|
}
|
||||||
'',
|
|
||||||
' Hotkeys:',
|
if (!lines.length) lines.push(' (no commands loaded)')
|
||||||
...HOTKEYS.map(([k, d]) => ` ${k.padEnd(14)} ${d}`)
|
|
||||||
]
|
if (skills > 0) {
|
||||||
.filter(Boolean)
|
lines.push('', ` ${skills} skill commands available — /skills to browse`)
|
||||||
.join('\n')
|
}
|
||||||
)
|
|
||||||
|
lines.push('', ' Hotkeys:')
|
||||||
|
for (const [k, d] of HOTKEYS) lines.push(` ${k.padEnd(14)} ${d}`)
|
||||||
|
|
||||||
|
sys(lines.join('\n'))
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -1527,6 +1558,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case 'provider':
|
||||||
|
gw.request('slash.exec', { command: 'provider', session_id: sid })
|
||||||
|
.then((r: any) => page(r?.output || '(no output)'))
|
||||||
|
.catch(() => sys('provider command failed'))
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
case 'skin':
|
case 'skin':
|
||||||
if (arg) {
|
if (arg) {
|
||||||
rpc('config.set', { key: 'skin', value: arg }).then((r: any) => sys(`skin → ${r.value}`))
|
rpc('config.set', { key: 'skin', value: arg }).then((r: any) => sys(`skin → ${r.value}`))
|
||||||
|
|
@ -1714,6 +1752,56 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
case 'skills': {
|
||||||
|
const [sub, ...sArgs] = (arg || '').split(/\s+/).filter(Boolean)
|
||||||
|
|
||||||
|
if (!sub || sub === 'list') {
|
||||||
|
rpc('skills.manage', { action: 'list' }).then((r: any) => {
|
||||||
|
const sk = r.skills as Record<string, string[]> | undefined
|
||||||
|
|
||||||
|
if (!sk || !Object.keys(sk).length) return sys('no skills installed')
|
||||||
|
|
||||||
|
const lines: string[] = []
|
||||||
|
|
||||||
|
for (const [cat, names] of Object.entries(sk)) {
|
||||||
|
lines.push(` ${cat}: ${(names as string[]).join(', ')}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
sys(lines.join('\n'))
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sub === 'browse') {
|
||||||
|
const page = parseInt(sArgs[0] ?? '1', 10) || 1
|
||||||
|
rpc('skills.manage', { action: 'browse', page }).then((r: any) => {
|
||||||
|
if (!r.items?.length) return sys('no skills found in the hub')
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
` Skills Hub (page ${r.page}/${r.total_pages}, ${r.total} total)`,
|
||||||
|
'',
|
||||||
|
...r.items.map((s: any) =>
|
||||||
|
` ${(s.name ?? '').padEnd(28)} ${(s.description ?? '').slice(0, 60)}${s.description?.length > 60 ? '…' : ''}`
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if (r.page < r.total_pages) lines.push('', ` /skills browse ${r.page + 1} → next page`)
|
||||||
|
if (r.page > 1) lines.push(` /skills browse ${r.page - 1} → prev page`)
|
||||||
|
|
||||||
|
sys(lines.join('\n'))
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
gw.request('slash.exec', { command: cmd.slice(1), session_id: sid })
|
||||||
|
.then((r: any) => sys(r?.output || '/skills: no output'))
|
||||||
|
.catch(() => sys(`skills: ${sub} failed`))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
gw.request('slash.exec', { command: cmd.slice(1), session_id: sid })
|
gw.request('slash.exec', { command: cmd.slice(1), session_id: sid })
|
||||||
.then((r: any) => sys(r?.output || `/${name}: no output`))
|
.then((r: any) => sys(r?.output || `/${name}: no output`))
|
||||||
|
|
@ -1737,7 +1825,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[catalog, compact, gw, lastUserMsg, messages, newSession, pastes, pushActivity, rpc, send, sid, statusBar, sys]
|
[catalog, compact, gw, lastUserMsg, messages, newSession, page, pastes, pushActivity, rpc, send, sid, statusBar, sys]
|
||||||
)
|
)
|
||||||
|
|
||||||
slashRef.current = slash
|
slashRef.current = slash
|
||||||
|
|
@ -1974,6 +2062,20 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{pager && (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{pager.lines.slice(pager.offset, pager.offset + pagerPageSize).map((line, i) => (
|
||||||
|
<Text key={i}>{line}</Text>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Text color={theme.color.dim}>
|
||||||
|
{pager.offset + pagerPageSize < pager.lines.length
|
||||||
|
? `── Enter/Space for more · q to close (${Math.min(pager.offset + pagerPageSize, pager.lines.length)}/${pager.lines.length}) ──`
|
||||||
|
: `── end · q to close (${pager.lines.length} lines) ──`}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
{!isBlocked && (
|
{!isBlocked && (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
{inputBuf.map((line, i) => (
|
{inputBuf.map((line, i) => (
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,13 @@ export interface PendingPaste {
|
||||||
|
|
||||||
export interface SlashCatalog {
|
export interface SlashCatalog {
|
||||||
canon: Record<string, string>
|
canon: Record<string, string>
|
||||||
|
categories: SlashCategory[]
|
||||||
pairs: [string, string][]
|
pairs: [string, string][]
|
||||||
|
skillCount: number
|
||||||
sub: Record<string, string[]>
|
sub: Record<string, string[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SlashCategory {
|
||||||
|
name: string
|
||||||
|
pairs: [string, string][]
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue