mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Merge branch 'feat/ink-refactor' of github.com:NousResearch/hermes-agent into feat/ink-refactor
This commit is contained in:
commit
8c1ba639c6
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()
|
||||
|
||||
|
||||
def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> list[dict]:
|
||||
"""Paginated hub browse for programmatic callers (e.g. TUI gateway)."""
|
||||
def browse_skills(page: int = 1, page_size: int = 20, source: str = "all") -> dict:
|
||||
"""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
|
||||
|
||||
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:
|
||||
continue
|
||||
if not all_results:
|
||||
return []
|
||||
return {"items": [], "page": 1, "total_pages": 1, "total": 0}
|
||||
seen: dict = {}
|
||||
for r in all_results:
|
||||
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))
|
||||
start = (page - 1) * page_size
|
||||
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]:
|
||||
|
|
|
|||
|
|
@ -903,31 +903,74 @@ def _(rid, params: dict) -> dict:
|
|||
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")
|
||||
def _(rid, params: dict) -> dict:
|
||||
"""Registry-backed slash metadata (same surface as SlashCommandCompleter)."""
|
||||
"""Registry-backed slash metadata for the TUI — categorized, no aliases."""
|
||||
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])
|
||||
sub = {k: v[:] for k, v in SUBCOMMANDS.items()}
|
||||
all_pairs: list[list[str]] = []
|
||||
canon: dict[str, str] = {}
|
||||
categories: list[dict] = []
|
||||
cat_map: dict[str, list[list[str]]] = {}
|
||||
cat_order: list[str] = []
|
||||
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.gateway_only:
|
||||
continue
|
||||
c = f"/{cmd.name}"
|
||||
canon[c.lower()] = c
|
||||
for a in cmd.aliases:
|
||||
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:
|
||||
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"))
|
||||
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:
|
||||
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:
|
||||
return _err(rid, 5020, str(e))
|
||||
|
||||
|
|
@ -1416,8 +1459,8 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"installed": True, "name": query})
|
||||
if action == "browse":
|
||||
from hermes_cli.skills_hub import browse_skills
|
||||
return _ok(rid, {"results": [{"name": r.get("name", ""), "description": r.get("description", "")}
|
||||
for r in (browse_skills(page=int(query) if query.isdigit() else 1) or [])]})
|
||||
pg = int(params.get("page", 0) or 0) or (int(query) if query.isdigit() else 1)
|
||||
return _ok(rid, browse_skills(page=pg, page_size=int(params.get("page_size", 20))))
|
||||
if action == "inspect":
|
||||
from hermes_cli.skills_hub import inspect_skill
|
||||
return _ok(rid, {"info": inspect_skill(query) or {}})
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
const [turnTrail, setTurnTrail] = useState<string[]>([])
|
||||
const [bgTasks, setBgTasks] = useState<Set<string>>(new Set())
|
||||
const [catalog, setCatalog] = useState<SlashCatalog | null>(null)
|
||||
const [pager, setPager] = useState<{ lines: string[]; offset: number } | null>(null)
|
||||
|
||||
// ── Refs ─────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -312,7 +313,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
const { completions, compIdx, setCompIdx, compReplace } = useCompletion(input, blocked(), gw)
|
||||
|
||||
function blocked() {
|
||||
return !!(clarify || approval || pasteReview || picker || secret || sudo)
|
||||
return !!(clarify || approval || pasteReview || picker || secret || sudo || pager)
|
||||
}
|
||||
|
||||
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 page = useCallback((text: string) => {
|
||||
const lines = text.split('\n')
|
||||
setPager({ lines, offset: 0 })
|
||||
}, [])
|
||||
|
||||
const pushActivity = useCallback((text: string, tone: ActivityItem['tone'] = 'info', replaceLabel?: string) => {
|
||||
setActivity(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 pagerPageSize = Math.max(5, (stdout?.rows ?? 24) - 6)
|
||||
|
||||
useInput((ch, key) => {
|
||||
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 (key.return) {
|
||||
setPasteReview(null)
|
||||
|
|
@ -970,7 +994,9 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
setCatalog({
|
||||
canon: (r.canon ?? {}) as Record<string, string>,
|
||||
categories: (r.categories ?? []) as SlashCatalog['categories'],
|
||||
pairs: r.pairs as [string, string][],
|
||||
skillCount: (r.skill_count ?? 0) as number,
|
||||
sub: (r.sub ?? {}) as Record<string, string[]>
|
||||
})
|
||||
})
|
||||
|
|
@ -1272,21 +1298,26 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
switch (name) {
|
||||
case 'help': {
|
||||
const rows = catalog?.pairs ?? []
|
||||
const cap = 52
|
||||
const cats = catalog?.categories ?? []
|
||||
const skills = catalog?.skillCount ?? 0
|
||||
const lines: string[] = []
|
||||
|
||||
sys(
|
||||
[
|
||||
' Commands:',
|
||||
...rows.slice(0, cap).map(([c, d]) => ` ${c.padEnd(16)} ${d}`),
|
||||
rows.length > cap ? ` … ${rows.length - cap} more` : '',
|
||||
'',
|
||||
' Hotkeys:',
|
||||
...HOTKEYS.map(([k, d]) => ` ${k.padEnd(14)} ${d}`)
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
)
|
||||
for (const { name: catName, pairs } of cats) {
|
||||
if (lines.length) lines.push('')
|
||||
lines.push(` ${catName}:`)
|
||||
for (const [c, d] of pairs) lines.push(` ${c.padEnd(18)} ${d}`)
|
||||
}
|
||||
|
||||
if (!lines.length) lines.push(' (no commands loaded)')
|
||||
|
||||
if (skills > 0) {
|
||||
lines.push('', ` ${skills} skill commands available — /skills to browse`)
|
||||
}
|
||||
|
||||
lines.push('', ' Hotkeys:')
|
||||
for (const [k, d] of HOTKEYS) lines.push(` ${k.padEnd(14)} ${d}`)
|
||||
|
||||
sys(lines.join('\n'))
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
@ -1527,6 +1558,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
|
||||
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':
|
||||
if (arg) {
|
||||
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
|
||||
|
||||
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:
|
||||
gw.request('slash.exec', { command: cmd.slice(1), session_id: sid })
|
||||
.then((r: any) => sys(r?.output || `/${name}: no output`))
|
||||
|
|
@ -1737,7 +1825,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
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
|
||||
|
|
@ -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 && (
|
||||
<Box flexDirection="column">
|
||||
{inputBuf.map((line, i) => (
|
||||
|
|
|
|||
|
|
@ -77,6 +77,13 @@ export interface PendingPaste {
|
|||
|
||||
export interface SlashCatalog {
|
||||
canon: Record<string, string>
|
||||
categories: SlashCategory[]
|
||||
pairs: [string, string][]
|
||||
skillCount: number
|
||||
sub: Record<string, string[]>
|
||||
}
|
||||
|
||||
export interface SlashCategory {
|
||||
name: string
|
||||
pairs: [string, string][]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue