fix(tui): route /skills subcommands through skills.manage instead of curses slash.exec

/skills install, inspect, search, browse, list now call the typed skills.manage RPC
and render results via panel/page. Previously they fell through to slash.exec which
invokes v1's curses code path — that hangs or crashes inside the Ink worker per the
§2 parity-audit finding.

Also drop Enter-as-install from the Skills Hub action stage since the Hub lists
locally installed skills; primary action is inspect-and-close. x still triggers a
manual reinstall for power users.
This commit is contained in:
Brooklyn Nicholson 2026-04-18 09:46:36 -05:00
parent 949b8f5521
commit 5e148ca3d0
3 changed files with 198 additions and 22 deletions

View file

@ -1,26 +1,158 @@
import type { SlashExecResponse, ToolsConfigureResponse } from '../../../gatewayTypes.js'
import type { ToolsConfigureResponse } from '../../../gatewayTypes.js'
import type { PanelSection } from '../../../types.js'
import { patchOverlayState } from '../../overlayStore.js'
import type { SlashCommand } from '../types.js'
interface SkillInfo {
category?: string
description?: string
name?: string
path?: string
}
interface SkillsListResponse {
skills?: Record<string, string[]>
}
interface SkillsInspectResponse {
info?: SkillInfo
}
interface SkillsSearchResponse {
results?: { description?: string; name: string }[]
}
interface SkillsInstallResponse {
installed?: boolean
name?: string
}
export const opsCommands: SlashCommand[] = [
{
help: 'browse, inspect, and install skills',
help: 'browse, inspect, install skills',
name: 'skills',
run: (arg, ctx) => {
if (!arg.trim()) {
const text = arg.trim()
if (!text) {
return patchOverlayState({ skillsHub: true })
}
ctx.gateway
.rpc<SlashExecResponse>('slash.exec', { command: `skills ${arg}`, session_id: ctx.sid })
.then(
ctx.guarded<SlashExecResponse>(r => {
if (r.output) {
ctx.transcript.page(r.output, 'Skills')
}
})
)
.catch(ctx.guardedErr)
const [sub, ...rest] = text.split(/\s+/)
const query = rest.join(' ').trim()
const { rpc } = ctx.gateway
const { page, panel, sys } = ctx.transcript
if (sub === 'list') {
rpc<SkillsListResponse>('skills.manage', { action: 'list' })
.then(
ctx.guarded<SkillsListResponse>(r => {
const cats = Object.entries(r.skills ?? {}).sort()
if (!cats.length) {
return sys('no skills available')
}
panel(
'Skills',
cats.map<PanelSection>(([title, items]) => ({ items, title }))
)
})
)
.catch(ctx.guardedErr)
return
}
if (sub === 'inspect') {
if (!query) {
return sys('usage: /skills inspect <name>')
}
rpc<SkillsInspectResponse>('skills.manage', { action: 'inspect', query })
.then(
ctx.guarded<SkillsInspectResponse>(r => {
const info = r.info ?? {}
if (!info.name) {
return sys(`unknown skill: ${query}`)
}
const rows: [string, string][] = [
['Name', String(info.name)],
['Category', String(info.category ?? '')],
['Path', String(info.path ?? '')]
]
const sections: PanelSection[] = [{ rows }]
if (info.description) {
sections.push({ text: String(info.description) })
}
panel('Skill', sections)
})
)
.catch(ctx.guardedErr)
return
}
if (sub === 'search') {
if (!query) {
return sys('usage: /skills search <query>')
}
rpc<SkillsSearchResponse>('skills.manage', { action: 'search', query })
.then(
ctx.guarded<SkillsSearchResponse>(r => {
const results = r.results ?? []
if (!results.length) {
return sys(`no results for: ${query}`)
}
panel(`Search: ${query}`, [{ rows: results.map(s => [s.name, s.description ?? '']) }])
})
)
.catch(ctx.guardedErr)
return
}
if (sub === 'install') {
if (!query) {
return sys('usage: /skills install <name or url>')
}
sys(`installing ${query}`)
rpc<SkillsInstallResponse>('skills.manage', { action: 'install', query })
.then(
ctx.guarded<SkillsInstallResponse>(r =>
sys(r.installed ? `installed ${r.name ?? query}` : 'install failed')
)
)
.catch(ctx.guardedErr)
return
}
if (sub === 'browse') {
const pageNum = parseInt(query, 10) || 1
rpc<Record<string, unknown>>('skills.manage', { action: 'browse', page: pageNum })
.then(
ctx.guarded<Record<string, unknown>>(r =>
page(JSON.stringify(r, null, 2).slice(0, 4000), `Browse Skills — p${pageNum}`)
)
)
.catch(ctx.guardedErr)
return
}
sys('usage: /skills [list | inspect <n> | install <n> | search <q> | browse [page]]')
}
},