mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
refactor(tui): stop shadowing python — slash fallback inherits worker output
Python's slash worker already prints every echo/panel command through Rich.
TS was reformatting the same data client-side for 23 commands. Delete those
shadows; let the `slash.exec` fallback in `createSlashHandler` route the
worker's text (via `<Ansi>`) and page-wrap long output.
TS registry now contains 23 commands (down from 45) — only those that:
- mutate React-local state (composer, transcript, overlays, uiStore)
- touch the terminal (OSC52 copy, `$EDITOR`, clipboard)
- open pickers (`/model`, `/resume`)
- trigger history surgery (`/undo`, `/retry`, `/compress`, `/personality`)
- need TS-only composition (`/help` merges HOTKEYS + catalog)
Deleted shadows:
session: yolo, skin, verbose, reasoning, provider, stop, reload-mcp,
save, title, insights, debug, fast, platforms, snapshot,
usage, history, profile
ops: plugins, rollback, agents, tasks, cron, config, toolsets,
browser, skills (list/browse only; `/tools configure` kept
for its history-reset side effect)
Side effects:
- Drops `slash/shared.ts` + `SlashShared` + `shared`/`SLASH_OUTPUT_PAGE` —
generic slash.exec fallback handles titled paging via `createSlashHandler`.
- Prunes 17 now-unreferenced `*Response` interfaces from gatewayTypes.ts.
- `createSlashHandler` fallback now pages long output (len>180 || lines>2)
and uses the command name as title.
session.ts: 670 -> 199 (-70%)
ops.ts: 460 -> 52 (-88%)
gatewayTypes.ts: 450 -> 302 (-33%)
This commit is contained in:
parent
beccd1bc04
commit
0478266831
6 changed files with 69 additions and 835 deletions
|
|
@ -4,15 +4,17 @@ import { asCommandDispatch, rpcErrorMessage } from '../lib/rpc.js'
|
|||
|
||||
import type { SlashHandlerContext } from './interfaces.js'
|
||||
import { findSlashCommand } from './slash/registry.js'
|
||||
import { createSlashShared } from './slash/shared.js'
|
||||
import type { SlashRunCtx } from './slash/types.js'
|
||||
import { getUiState } from './uiStore.js'
|
||||
|
||||
const titleCase = (name: string) => name.charAt(0).toUpperCase() + name.slice(1)
|
||||
|
||||
const isLong = (text: string) => text.length > 180 || text.split('\n').filter(Boolean).length > 2
|
||||
|
||||
export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => boolean {
|
||||
const { gw } = ctx.gateway
|
||||
const { catalog } = ctx.local
|
||||
const { send, sys } = ctx.transcript
|
||||
const shared = createSlashShared({ ...ctx.transcript, gw, slashFlightRef: ctx.slashFlightRef })
|
||||
const { page, send, sys } = ctx.transcript
|
||||
|
||||
const handler = (cmd: string): boolean => {
|
||||
const flight = ++ctx.slashFlightRef.current
|
||||
|
|
@ -37,7 +39,7 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
|||
}
|
||||
}
|
||||
|
||||
const runCtx: SlashRunCtx = { ...ctx, flight, guarded, guardedErr, shared, sid, stale, ui }
|
||||
const runCtx: SlashRunCtx = { ...ctx, flight, guarded, guardedErr, sid, stale, ui }
|
||||
|
||||
const found = findSlashCommand(parsed.name)
|
||||
|
||||
|
|
@ -75,11 +77,10 @@ export function createSlashHandler(ctx: SlashHandlerContext): (cmd: string) => b
|
|||
return
|
||||
}
|
||||
|
||||
sys(
|
||||
r?.warning
|
||||
? `warning: ${r.warning}\n${r?.output || `/${parsed.name}: no output`}`
|
||||
: r?.output || `/${parsed.name}: no output`
|
||||
)
|
||||
const body = r?.output || `/${parsed.name}: no output`
|
||||
const text = r?.warning ? `warning: ${r.warning}\n${body}` : body
|
||||
|
||||
isLong(text) ? page(text, titleCase(parsed.name)) : sys(text)
|
||||
})
|
||||
.catch(() => {
|
||||
gw.request('command.dispatch', { arg: parsed.arg, name: parsed.name, session_id: sid })
|
||||
|
|
|
|||
|
|
@ -1,311 +1,17 @@
|
|||
import type {
|
||||
AgentsListResponse,
|
||||
BrowserManageResponse,
|
||||
ConfigShowResponse,
|
||||
CronListResponse,
|
||||
PluginsListResponse,
|
||||
RollbackActionResponse,
|
||||
RollbackListResponse,
|
||||
SkillsBrowseResponse,
|
||||
SkillsListResponse,
|
||||
SlashExecResponse,
|
||||
ToolsConfigureResponse,
|
||||
ToolsetsListResponse,
|
||||
ToolsListResponse,
|
||||
ToolsShowResponse
|
||||
} from '../../../gatewayTypes.js'
|
||||
import type { PanelSection } from '../../../types.js'
|
||||
import type { SlashCommand, SlashRunCtx } from '../types.js'
|
||||
|
||||
const passthroughSlash = (ctx: SlashRunCtx, cmd: string, fallback: string) =>
|
||||
ctx.gateway.gw
|
||||
.request<SlashExecResponse>('slash.exec', { command: cmd.slice(1), session_id: ctx.sid })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.transcript.sys(r?.warning ? `warning: ${r.warning}\n${r?.output || fallback}` : r?.output || fallback)
|
||||
})
|
||||
.catch(ctx.guardedErr)
|
||||
|
||||
const clip = (s: string, max: number) => (s.length > max ? `${s.slice(0, max)}…` : s)
|
||||
import type { ToolsConfigureResponse } from '../../../gatewayTypes.js'
|
||||
import type { SlashCommand } from '../types.js'
|
||||
|
||||
export const opsCommands: SlashCommand[] = [
|
||||
{
|
||||
help: 'list or restore checkpoints',
|
||||
name: 'rollback',
|
||||
run: (arg, ctx) => {
|
||||
const [sub, ...rest] = (arg || 'list').split(/\s+/)
|
||||
|
||||
if (!sub || sub === 'list') {
|
||||
return ctx.gateway.rpc<RollbackListResponse>('rollback.list', { session_id: ctx.sid }).then(
|
||||
ctx.guarded<RollbackListResponse>(r => {
|
||||
if (!r.checkpoints?.length) {
|
||||
return ctx.transcript.sys('no checkpoints')
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Checkpoints', [
|
||||
{
|
||||
rows: r.checkpoints.map(
|
||||
(c, i) => [`${i + 1} ${c.hash?.slice(0, 8) ?? ''}`, c.message ?? ''] as [string, string]
|
||||
)
|
||||
}
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const isRestoreOrDiff = sub === 'restore' || sub === 'diff'
|
||||
const hash = isRestoreOrDiff ? rest[0] : sub
|
||||
const filePath = (isRestoreOrDiff ? rest.slice(1) : rest).join(' ').trim()
|
||||
const method = sub === 'diff' ? 'rollback.diff' : 'rollback.restore'
|
||||
|
||||
ctx.gateway
|
||||
.rpc<RollbackActionResponse>(method, {
|
||||
hash,
|
||||
session_id: ctx.sid,
|
||||
...(sub === 'diff' || !filePath ? {} : { file_path: filePath })
|
||||
})
|
||||
.then(ctx.guarded<RollbackActionResponse>(r => ctx.transcript.sys(r.rendered || r.diff || r.message || 'done')))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'manage browser connection',
|
||||
name: 'browser',
|
||||
run: (arg, ctx) => {
|
||||
const [action, url] = (arg || 'status').split(/\s+/)
|
||||
|
||||
ctx.gateway
|
||||
.rpc<BrowserManageResponse>('browser.manage', { action, ...(url ? { url } : {}) })
|
||||
.then(
|
||||
ctx.guarded<BrowserManageResponse>(r =>
|
||||
ctx.transcript.sys(r.connected ? `browser: ${r.url}` : 'browser: disconnected')
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'list installed plugins',
|
||||
name: 'plugins',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway.rpc<PluginsListResponse>('plugins.list', {}).then(
|
||||
ctx.guarded<PluginsListResponse>(r => {
|
||||
if (!r.plugins?.length) {
|
||||
return ctx.transcript.sys('no plugins')
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Plugins', [
|
||||
{ items: r.plugins.map(p => `${p.name} v${p.version}${p.enabled ? '' : ' (disabled)'}`) }
|
||||
])
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'list or browse skills',
|
||||
name: 'skills',
|
||||
run: (arg, ctx, cmd) => {
|
||||
const [sub, ...rest] = (arg || '').split(/\s+/).filter(Boolean)
|
||||
|
||||
if (!sub || sub === 'list') {
|
||||
return ctx.gateway.rpc<SkillsListResponse>('skills.manage', { action: 'list' }).then(
|
||||
ctx.guarded<SkillsListResponse>(r => {
|
||||
if (!r.skills || !Object.keys(r.skills).length) {
|
||||
return ctx.transcript.sys('no skills installed')
|
||||
}
|
||||
|
||||
ctx.transcript.panel(
|
||||
'Installed Skills',
|
||||
Object.entries(r.skills).map(([title, items]) => ({ items, title }))
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (sub === 'browse') {
|
||||
const pageNumber = parseInt(rest[0] ?? '1', 10) || 1
|
||||
|
||||
return ctx.gateway.rpc<SkillsBrowseResponse>('skills.manage', { action: 'browse', page: pageNumber }).then(
|
||||
ctx.guarded<SkillsBrowseResponse>(r => {
|
||||
if (!r.items?.length) {
|
||||
return ctx.transcript.sys('no skills found in the hub')
|
||||
}
|
||||
|
||||
const page = r.page ?? 1
|
||||
const totalPages = r.total_pages ?? 1
|
||||
|
||||
const sections: PanelSection[] = [
|
||||
{
|
||||
rows: r.items.map(s => [s.name ?? '', clip(s.description ?? '', 60)] as [string, string])
|
||||
}
|
||||
]
|
||||
|
||||
if (page < totalPages) {
|
||||
sections.push({ text: `/skills browse ${page + 1} → next page` })
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
sections.push({ text: `/skills browse ${page - 1} → prev page` })
|
||||
}
|
||||
|
||||
ctx.transcript.panel(`Skills Hub (page ${page}/${totalPages}, ${r.total ?? 0} total)`, sections)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
passthroughSlash(ctx, cmd, '/skills: no output')
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
aliases: ['tasks'],
|
||||
help: 'running agents',
|
||||
name: 'agents',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<AgentsListResponse>('agents.list', {})
|
||||
.then(
|
||||
ctx.guarded<AgentsListResponse>(r => {
|
||||
const processes = r.processes ?? []
|
||||
const running = processes.filter(p => p.status === 'running')
|
||||
const finished = processes.filter(p => p.status !== 'running')
|
||||
const sections: PanelSection[] = []
|
||||
|
||||
if (running.length) {
|
||||
sections.push({
|
||||
rows: running.map(p => [p.session_id.slice(0, 8), p.command ?? '']),
|
||||
title: `Running (${running.length})`
|
||||
})
|
||||
}
|
||||
|
||||
if (finished.length) {
|
||||
sections.push({
|
||||
rows: finished.map(p => [p.session_id.slice(0, 8), p.command ?? '']),
|
||||
title: `Finished (${finished.length})`
|
||||
})
|
||||
}
|
||||
|
||||
if (!sections.length) {
|
||||
sections.push({ text: 'No active processes' })
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Agents', sections)
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'list or manage cron jobs',
|
||||
name: 'cron',
|
||||
run: (arg, ctx, cmd) => {
|
||||
if (arg && arg !== 'list') {
|
||||
return passthroughSlash(ctx, cmd, '(no output)')
|
||||
}
|
||||
|
||||
ctx.gateway
|
||||
.rpc<CronListResponse>('cron.manage', { action: 'list' })
|
||||
.then(
|
||||
ctx.guarded<CronListResponse>(r => {
|
||||
const jobs = r.jobs ?? []
|
||||
|
||||
if (!jobs.length) {
|
||||
return ctx.transcript.sys('no scheduled jobs')
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Cron', [
|
||||
{
|
||||
rows: jobs.map(
|
||||
j =>
|
||||
[j.name || j.job_id?.slice(0, 12) || '', `${j.schedule ?? ''} · ${j.state ?? 'active'}`] as [
|
||||
string,
|
||||
string
|
||||
]
|
||||
)
|
||||
}
|
||||
])
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'show configuration',
|
||||
name: 'config',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<ConfigShowResponse>('config.show', {})
|
||||
.then(
|
||||
ctx.guarded<ConfigShowResponse>(r =>
|
||||
ctx.transcript.panel(
|
||||
'Config',
|
||||
(r.sections ?? []).map(s => ({ rows: s.rows, title: s.title }))
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'list, enable, disable tools',
|
||||
help: 'enable or disable tools (client-side history reset on change)',
|
||||
name: 'tools',
|
||||
run: (arg, ctx) => {
|
||||
const [subcommand, ...names] = arg.trim().split(/\s+/).filter(Boolean)
|
||||
|
||||
if (!subcommand) {
|
||||
return ctx.gateway
|
||||
.rpc<ToolsShowResponse>('tools.show', { session_id: ctx.sid })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
if (subcommand !== 'disable' && subcommand !== 'enable') {
|
||||
return // py prints lists / show / usage
|
||||
}
|
||||
|
||||
if (!r?.sections?.length) {
|
||||
return ctx.transcript.sys('no tools')
|
||||
}
|
||||
|
||||
ctx.transcript.panel(
|
||||
`Tools${typeof r.total === 'number' ? ` (${r.total})` : ''}`,
|
||||
r.sections.map(section => ({
|
||||
rows: section.tools.map(tool => [tool.name, tool.description] as [string, string]),
|
||||
title: section.name
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
|
||||
if (subcommand === 'list') {
|
||||
return ctx.gateway
|
||||
.rpc<ToolsListResponse>('tools.list', { session_id: ctx.sid })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!r?.toolsets?.length) {
|
||||
return ctx.transcript.sys('no tools')
|
||||
}
|
||||
|
||||
ctx.transcript.panel(
|
||||
'Tools',
|
||||
r.toolsets.map(ts => ({
|
||||
items: ts.tools,
|
||||
title: `${ts.enabled ? '*' : ' '} ${ts.name} [${ts.tool_count} tools]`
|
||||
}))
|
||||
)
|
||||
})
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
|
||||
if (subcommand === 'disable' || subcommand === 'enable') {
|
||||
if (!names.length) {
|
||||
ctx.transcript.sys(`usage: /tools ${subcommand} <name> [name ...]`)
|
||||
ctx.transcript.sys(`built-in toolset: /tools ${subcommand} web`)
|
||||
|
|
@ -314,7 +20,7 @@ export const opsCommands: SlashCommand[] = [
|
|||
return
|
||||
}
|
||||
|
||||
return ctx.gateway
|
||||
ctx.gateway
|
||||
.rpc<ToolsConfigureResponse>('tools.configure', { action: subcommand, names, session_id: ctx.sid })
|
||||
.then(
|
||||
ctx.guarded<ToolsConfigureResponse>(r => {
|
||||
|
|
@ -323,43 +29,21 @@ export const opsCommands: SlashCommand[] = [
|
|||
ctx.session.resetVisibleHistory(r.info)
|
||||
}
|
||||
|
||||
r.changed?.length &&
|
||||
if (r.changed?.length) {
|
||||
ctx.transcript.sys(`${subcommand === 'disable' ? 'disabled' : 'enabled'}: ${r.changed.join(', ')}`)
|
||||
r.unknown?.length && ctx.transcript.sys(`unknown toolsets: ${r.unknown.join(', ')}`)
|
||||
r.missing_servers?.length && ctx.transcript.sys(`missing MCP servers: ${r.missing_servers.join(', ')}`)
|
||||
r.reset && ctx.transcript.sys('session reset. new tool configuration is active.')
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
|
||||
ctx.transcript.sys('usage: /tools [list|disable|enable] ...')
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'list toolsets',
|
||||
name: 'toolsets',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<ToolsetsListResponse>('toolsets.list', { session_id: ctx.sid })
|
||||
.then(
|
||||
ctx.guarded<ToolsetsListResponse>(r => {
|
||||
if (!r.toolsets?.length) {
|
||||
return ctx.transcript.sys('no toolsets')
|
||||
if (r.unknown?.length) {
|
||||
ctx.transcript.sys(`unknown toolsets: ${r.unknown.join(', ')}`)
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Toolsets', [
|
||||
{
|
||||
rows: r.toolsets.map(
|
||||
ts =>
|
||||
[`${ts.enabled ? '(*)' : ' '} ${ts.name}`, `[${ts.tool_count}] ${ts.description}`] as [
|
||||
string,
|
||||
string
|
||||
]
|
||||
)
|
||||
if (r.missing_servers?.length) {
|
||||
ctx.transcript.sys(`missing MCP servers: ${r.missing_servers.join(', ')}`)
|
||||
}
|
||||
|
||||
if (r.reset) {
|
||||
ctx.transcript.sys('session reset. new tool configuration is active.')
|
||||
}
|
||||
])
|
||||
})
|
||||
)
|
||||
.catch(ctx.guardedErr)
|
||||
|
|
|
|||
|
|
@ -2,52 +2,18 @@ import { imageTokenMeta, introMsg, toTranscriptMessages } from '../../../domain/
|
|||
import type {
|
||||
BackgroundStartResponse,
|
||||
BtwStartResponse,
|
||||
ConfigGetValueResponse,
|
||||
ConfigSetResponse,
|
||||
ImageAttachResponse,
|
||||
InsightsResponse,
|
||||
ReloadMcpResponse,
|
||||
SessionBranchResponse,
|
||||
SessionCompressResponse,
|
||||
SessionHistoryResponse,
|
||||
SessionSaveResponse,
|
||||
SessionTitleResponse,
|
||||
SessionUsageResponse,
|
||||
SlashExecResponse,
|
||||
VoiceToggleResponse
|
||||
} from '../../../gatewayTypes.js'
|
||||
import { fmtK } from '../../../lib/text.js'
|
||||
import type { PanelSection } from '../../../types.js'
|
||||
import { patchOverlayState } from '../../overlayStore.js'
|
||||
import { patchUiState } from '../../uiStore.js'
|
||||
import type { SlashCommand } from '../types.js'
|
||||
|
||||
const PAGE_TITLES: Record<string, string> = {
|
||||
debug: 'Debug',
|
||||
fast: 'Fast',
|
||||
platforms: 'Platforms',
|
||||
snapshot: 'Snapshot'
|
||||
}
|
||||
|
||||
const passthrough = (name: string): SlashCommand => ({
|
||||
name,
|
||||
run: (_arg, ctx, cmd) =>
|
||||
ctx.shared.showSlashOutput({
|
||||
command: cmd.slice(1),
|
||||
flight: ctx.flight,
|
||||
sid: ctx.sid,
|
||||
title: PAGE_TITLES[name] ?? name
|
||||
})
|
||||
})
|
||||
|
||||
const historyLabel = (role: string) => (role === 'assistant' ? 'Hermes' : role === 'user' ? 'You' : 'System')
|
||||
|
||||
export const sessionCommands: SlashCommand[] = [
|
||||
passthrough('debug'),
|
||||
passthrough('fast'),
|
||||
passthrough('platforms'),
|
||||
passthrough('snapshot'),
|
||||
|
||||
{
|
||||
aliases: ['bg'],
|
||||
help: 'launch a background prompt',
|
||||
|
|
@ -126,121 +92,34 @@ export const sessionCommands: SlashCommand[] = [
|
|||
const meta = imageTokenMeta(r)
|
||||
|
||||
ctx.transcript.sys(`attached image: ${r.name ?? ''}${meta ? ` · ${meta}` : ''}`)
|
||||
r.remainder && ctx.composer.setInput(r.remainder)
|
||||
|
||||
if (r.remainder) {
|
||||
ctx.composer.setInput(r.remainder)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'show provider details',
|
||||
name: 'provider',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway.gw
|
||||
.request<SlashExecResponse>('slash.exec', { command: 'provider', session_id: ctx.sid })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.transcript.page(
|
||||
r?.warning ? `warning: ${r.warning}\n\n${r?.output || '(no output)'}` : r?.output || '(no output)',
|
||||
'Provider'
|
||||
)
|
||||
})
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'switch theme skin',
|
||||
name: 'skin',
|
||||
run: (arg, ctx) => {
|
||||
if (arg) {
|
||||
return ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'skin', value: arg })
|
||||
.then(ctx.guarded<ConfigSetResponse>(r => r.value && ctx.transcript.sys(`skin → ${r.value}`)))
|
||||
}
|
||||
|
||||
ctx.gateway
|
||||
.rpc<ConfigGetValueResponse>('config.get', { key: 'skin' })
|
||||
.then(ctx.guarded<ConfigGetValueResponse>(r => ctx.transcript.sys(`skin: ${r.value || 'default'}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'toggle yolo mode',
|
||||
name: 'yolo',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'yolo', session_id: ctx.sid })
|
||||
.then(ctx.guarded<ConfigSetResponse>(r => ctx.transcript.sys(`yolo ${r.value === '1' ? 'on' : 'off'}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'inspect or set reasoning mode',
|
||||
name: 'reasoning',
|
||||
run: (arg, ctx) => {
|
||||
if (!arg) {
|
||||
return ctx.gateway
|
||||
.rpc<ConfigGetValueResponse>('config.get', { key: 'reasoning' })
|
||||
.then(
|
||||
ctx.guarded<ConfigGetValueResponse>(
|
||||
r => r.value && ctx.transcript.sys(`reasoning: ${r.value} · display ${r.display || 'hide'}`)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'reasoning', session_id: ctx.sid, value: arg })
|
||||
.then(ctx.guarded<ConfigSetResponse>(r => r.value && ctx.transcript.sys(`reasoning: ${r.value}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'cycle verbose output',
|
||||
name: 'verbose',
|
||||
run: (arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'verbose', session_id: ctx.sid, value: arg || 'cycle' })
|
||||
.then(ctx.guarded<ConfigSetResponse>(r => r.value && ctx.transcript.sys(`verbose: ${r.value}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'personality panel or switch',
|
||||
help: 'switch or reset personality (history reset on set)',
|
||||
name: 'personality',
|
||||
run: (arg, ctx) => {
|
||||
if (arg) {
|
||||
return ctx.gateway
|
||||
.rpc<ConfigSetResponse>('config.set', { key: 'personality', session_id: ctx.sid, value: arg })
|
||||
.then(
|
||||
if (!arg) {
|
||||
return // py handles listing
|
||||
}
|
||||
|
||||
ctx.gateway.rpc<ConfigSetResponse>('config.set', { key: 'personality', session_id: ctx.sid, value: arg }).then(
|
||||
ctx.guarded<ConfigSetResponse>(r => {
|
||||
r.history_reset && ctx.session.resetVisibleHistory(r.info ?? null)
|
||||
ctx.transcript.sys(
|
||||
`personality: ${r.value || 'default'}${r.history_reset ? ' · transcript cleared' : ''}`
|
||||
)
|
||||
if (r.history_reset) {
|
||||
ctx.session.resetVisibleHistory(r.info ?? null)
|
||||
}
|
||||
|
||||
ctx.transcript.sys(`personality: ${r.value || 'default'}${r.history_reset ? ' · transcript cleared' : ''}`)
|
||||
ctx.local.maybeWarn(r)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
ctx.gateway.gw
|
||||
.request<SlashExecResponse>('slash.exec', { command: 'personality', session_id: ctx.sid })
|
||||
.then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.transcript.panel('Personality', [
|
||||
{
|
||||
text: r?.warning ? `warning: ${r.warning}\n\n${r?.output || '(no output)'}` : r?.output || '(no output)'
|
||||
}
|
||||
])
|
||||
})
|
||||
.catch(ctx.guardedErr)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
|
|
@ -260,8 +139,13 @@ export const sessionCommands: SlashCommand[] = [
|
|||
ctx.transcript.setHistoryItems(r.info ? [introMsg(r.info), ...rows] : rows)
|
||||
}
|
||||
|
||||
r.info && patchUiState({ info: r.info })
|
||||
r.usage && patchUiState(state => ({ ...state, usage: { ...state.usage, ...r.usage } }))
|
||||
if (r.info) {
|
||||
patchUiState({ info: r.info })
|
||||
}
|
||||
|
||||
if (r.usage) {
|
||||
patchUiState(state => ({ ...state, usage: { ...state.usage, ...r.usage } }))
|
||||
}
|
||||
|
||||
if ((r.removed ?? 0) <= 0) {
|
||||
return ctx.transcript.sys('nothing to compress')
|
||||
|
|
@ -275,18 +159,6 @@ export const sessionCommands: SlashCommand[] = [
|
|||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'stop background processes',
|
||||
name: 'stop',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<{ killed?: number }>('process.stop', {})
|
||||
.then(
|
||||
ctx.guarded<{ killed?: number }>(r => ctx.transcript.sys(`killed ${r.killed ?? 0} registered process(es)`))
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
aliases: ['fork'],
|
||||
help: 'branch the session',
|
||||
|
|
@ -310,121 +182,6 @@ export const sessionCommands: SlashCommand[] = [
|
|||
}
|
||||
},
|
||||
|
||||
{
|
||||
aliases: ['reload_mcp'],
|
||||
help: 'reload MCP servers',
|
||||
name: 'reload-mcp',
|
||||
run: (_arg, ctx) =>
|
||||
ctx.gateway
|
||||
.rpc<ReloadMcpResponse>('reload.mcp', { session_id: ctx.sid })
|
||||
.then(ctx.guarded(() => ctx.transcript.sys('MCP reloaded')))
|
||||
},
|
||||
|
||||
{
|
||||
help: 'inspect or set session title',
|
||||
name: 'title',
|
||||
run: (arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<SessionTitleResponse>('session.title', { session_id: ctx.sid, ...(arg ? { title: arg } : {}) })
|
||||
.then(ctx.guarded<SessionTitleResponse>(r => ctx.transcript.sys(`title: ${r.title || '(none)'}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'session usage',
|
||||
name: 'usage',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway.rpc<SessionUsageResponse>('session.usage', { session_id: ctx.sid }).then(r => {
|
||||
if (ctx.stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (r) {
|
||||
patchUiState({
|
||||
usage: { calls: r.calls ?? 0, input: r.input ?? 0, output: r.output ?? 0, total: r.total ?? 0 }
|
||||
})
|
||||
}
|
||||
|
||||
if (!r?.calls) {
|
||||
return ctx.transcript.sys('no API calls yet')
|
||||
}
|
||||
|
||||
const f = (v: number | undefined) => (v ?? 0).toLocaleString()
|
||||
const cost = r.cost_usd != null ? `${r.cost_status === 'estimated' ? '~' : ''}$${r.cost_usd.toFixed(4)}` : null
|
||||
|
||||
const rows: [string, string][] = [
|
||||
['Model', r.model ?? ''],
|
||||
['Input tokens', f(r.input)],
|
||||
['Cache read tokens', f(r.cache_read)],
|
||||
['Cache write tokens', f(r.cache_write)],
|
||||
['Output tokens', f(r.output)],
|
||||
['Total tokens', f(r.total)],
|
||||
['API calls', f(r.calls)]
|
||||
]
|
||||
|
||||
const sections: PanelSection[] = [{ rows }]
|
||||
|
||||
cost && rows.push(['Cost', cost])
|
||||
r.context_max &&
|
||||
sections.push({ text: `Context: ${f(r.context_used)} / ${f(r.context_max)} (${r.context_percent}%)` })
|
||||
r.compressions && sections.push({ text: `Compressions: ${r.compressions}` })
|
||||
|
||||
ctx.transcript.panel('Usage', sections)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'save transcript to disk',
|
||||
name: 'save',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway
|
||||
.rpc<SessionSaveResponse>('session.save', { session_id: ctx.sid })
|
||||
.then(ctx.guarded<SessionSaveResponse>(r => r.file && ctx.transcript.sys(`saved: ${r.file}`)))
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'view message history',
|
||||
name: 'history',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway.rpc<SessionHistoryResponse>('session.history', { session_id: ctx.sid }).then(r => {
|
||||
if (ctx.stale() || typeof r?.count !== 'number') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!r.messages?.length) {
|
||||
return ctx.transcript.sys(`${r.count} messages`)
|
||||
}
|
||||
|
||||
const body = r.messages
|
||||
.map((m, i) =>
|
||||
m.role === 'tool'
|
||||
? `[Tool #${i + 1}] ${m.name || 'tool'} ${m.context || ''}`.trim()
|
||||
: `[${historyLabel(m.role)} #${i + 1}] ${m.text || ''}`.trim()
|
||||
)
|
||||
.join('\n\n')
|
||||
|
||||
ctx.transcript.page(body, `History (${r.count})`)
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'show current profile',
|
||||
name: 'profile',
|
||||
run: (_arg, ctx) => {
|
||||
ctx.gateway.rpc<ConfigGetValueResponse>('config.get', { key: 'profile' }).then(
|
||||
ctx.guarded<ConfigGetValueResponse>(r => {
|
||||
const text = r.display || r.home || '(unknown profile)'
|
||||
const lines = text.split('\n').filter(Boolean)
|
||||
|
||||
lines.length <= 2 ? ctx.transcript.panel('Profile', [{ text }]) : ctx.transcript.page(text, 'Profile')
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'toggle voice input',
|
||||
name: 'voice',
|
||||
|
|
@ -438,25 +195,5 @@ export const sessionCommands: SlashCommand[] = [
|
|||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
help: 'view usage insights',
|
||||
name: 'insights',
|
||||
run: (arg, ctx) => {
|
||||
ctx.gateway.rpc<InsightsResponse>('insights.get', { days: parseInt(arg) || 30 }).then(
|
||||
ctx.guarded<InsightsResponse>(r =>
|
||||
ctx.transcript.panel('Insights', [
|
||||
{
|
||||
rows: [
|
||||
['Period', `${r.days ?? 0} days`],
|
||||
['Sessions', `${r.sessions ?? 0}`],
|
||||
['Messages', `${r.messages ?? 0}`]
|
||||
]
|
||||
}
|
||||
])
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
import type { MutableRefObject } from 'react'
|
||||
|
||||
import type { SlashExecResponse } from '../../gatewayTypes.js'
|
||||
import { rpcErrorMessage } from '../../lib/rpc.js'
|
||||
import { getUiState } from '../uiStore.js'
|
||||
|
||||
export interface SlashShared {
|
||||
showSlashOutput: (opts: { command: string; flight: number; sid: null | string; title: string }) => void
|
||||
}
|
||||
|
||||
interface SlashSharedDeps {
|
||||
gw: { request: <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T> }
|
||||
page: (text: string, title?: string) => void
|
||||
slashFlightRef: MutableRefObject<number>
|
||||
sys: (text: string) => void
|
||||
}
|
||||
|
||||
export const createSlashShared = ({ gw, page, slashFlightRef, sys }: SlashSharedDeps): SlashShared => ({
|
||||
showSlashOutput: ({ command, flight, sid, title }) => {
|
||||
const stale = () => flight !== slashFlightRef.current || getUiState().sid !== sid
|
||||
|
||||
gw.request<SlashExecResponse>('slash.exec', { command, session_id: sid })
|
||||
.then(r => {
|
||||
if (stale()) {
|
||||
return
|
||||
}
|
||||
|
||||
const text = r?.warning ? `warning: ${r.warning}\n${r?.output || '(no output)'}` : r?.output || '(no output)'
|
||||
|
||||
text.split('\n').filter(Boolean).length > 2 || text.length > 180 ? page(text, title) : sys(text)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (!stale()) {
|
||||
sys(`error: ${rpcErrorMessage(e)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -2,13 +2,10 @@ import type { MutableRefObject } from 'react'
|
|||
|
||||
import type { SlashHandlerContext, UiState } from '../interfaces.js'
|
||||
|
||||
import type { SlashShared } from './shared.js'
|
||||
|
||||
export interface SlashRunCtx extends SlashHandlerContext {
|
||||
flight: number
|
||||
guarded: <T>(fn: (r: T) => void) => (r: null | T) => void
|
||||
guardedErr: (e: unknown) => void
|
||||
shared: SlashShared
|
||||
sid: null | string
|
||||
slashFlightRef: MutableRefObject<number>
|
||||
stale: () => boolean
|
||||
|
|
|
|||
|
|
@ -110,11 +110,6 @@ export interface SessionUndoResponse {
|
|||
removed?: number
|
||||
}
|
||||
|
||||
export interface SessionHistoryResponse {
|
||||
count?: number
|
||||
messages?: GatewayTranscriptMessage[]
|
||||
}
|
||||
|
||||
export interface SessionCompressResponse {
|
||||
info?: SessionInfo
|
||||
messages?: GatewayTranscriptMessage[]
|
||||
|
|
@ -127,30 +122,6 @@ export interface SessionBranchResponse {
|
|||
title?: string
|
||||
}
|
||||
|
||||
export interface SessionTitleResponse {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface SessionSaveResponse {
|
||||
file?: string
|
||||
}
|
||||
|
||||
export interface SessionUsageResponse {
|
||||
cache_read?: number
|
||||
cache_write?: number
|
||||
calls?: number
|
||||
compressions?: number
|
||||
context_max?: number
|
||||
context_percent?: number
|
||||
context_used?: number
|
||||
cost_status?: 'estimated' | 'exact'
|
||||
cost_usd?: number
|
||||
input?: number
|
||||
model?: string
|
||||
output?: number
|
||||
total?: number
|
||||
}
|
||||
|
||||
export interface SessionCloseResponse {
|
||||
ok?: boolean
|
||||
}
|
||||
|
|
@ -240,34 +211,7 @@ export interface VoiceRecordResponse {
|
|||
text?: string
|
||||
}
|
||||
|
||||
// ── Tools / toolsets ─────────────────────────────────────────────────
|
||||
|
||||
export interface ToolsetDetails {
|
||||
description: string
|
||||
enabled: boolean
|
||||
name: string
|
||||
tool_count: number
|
||||
tools: string[]
|
||||
}
|
||||
|
||||
export interface ToolsListResponse {
|
||||
toolsets?: ToolsetDetails[]
|
||||
}
|
||||
|
||||
export interface ToolSummary {
|
||||
description: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface ToolsShowSection {
|
||||
name: string
|
||||
tools: ToolSummary[]
|
||||
}
|
||||
|
||||
export interface ToolsShowResponse {
|
||||
sections?: ToolsShowSection[]
|
||||
total?: number
|
||||
}
|
||||
// ── Tools (TS keeps configure since it resets local history) ─────────
|
||||
|
||||
export interface ToolsConfigureResponse {
|
||||
changed?: string[]
|
||||
|
|
@ -278,104 +222,7 @@ export interface ToolsConfigureResponse {
|
|||
unknown?: string[]
|
||||
}
|
||||
|
||||
export interface ToolsetsListResponse {
|
||||
toolsets?: {
|
||||
description: string
|
||||
enabled: boolean
|
||||
name: string
|
||||
tool_count: number
|
||||
}[]
|
||||
}
|
||||
|
||||
// ── Ops: rollback / browser / plugins / skills / agents / cron ───────
|
||||
|
||||
export interface RollbackCheckpoint {
|
||||
hash?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface RollbackListResponse {
|
||||
checkpoints?: RollbackCheckpoint[]
|
||||
}
|
||||
|
||||
export interface RollbackActionResponse {
|
||||
diff?: string
|
||||
message?: string
|
||||
rendered?: string
|
||||
}
|
||||
|
||||
export interface BrowserManageResponse {
|
||||
connected?: boolean
|
||||
url?: string
|
||||
}
|
||||
|
||||
export interface PluginInfo {
|
||||
enabled?: boolean
|
||||
name?: string
|
||||
version?: string
|
||||
}
|
||||
|
||||
export interface PluginsListResponse {
|
||||
plugins?: PluginInfo[]
|
||||
}
|
||||
|
||||
export interface SkillsListResponse {
|
||||
skills?: Record<string, string[]>
|
||||
}
|
||||
|
||||
export interface SkillsBrowseItem {
|
||||
description?: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export interface SkillsBrowseResponse {
|
||||
items?: SkillsBrowseItem[]
|
||||
page?: number
|
||||
total?: number
|
||||
total_pages?: number
|
||||
}
|
||||
|
||||
export interface AgentProcess {
|
||||
command?: string
|
||||
session_id: string
|
||||
status?: 'finished' | 'running'
|
||||
}
|
||||
|
||||
export interface AgentsListResponse {
|
||||
processes?: AgentProcess[]
|
||||
}
|
||||
|
||||
export interface CronJob {
|
||||
job_id?: string
|
||||
name?: string
|
||||
schedule?: string
|
||||
state?: string
|
||||
}
|
||||
|
||||
export interface CronListResponse {
|
||||
jobs?: CronJob[]
|
||||
}
|
||||
|
||||
export interface ConfigShowSection {
|
||||
rows?: [string, string][]
|
||||
title?: string
|
||||
}
|
||||
|
||||
export interface ConfigShowResponse {
|
||||
sections?: ConfigShowSection[]
|
||||
}
|
||||
|
||||
// ── Insights / MCP ───────────────────────────────────────────────────
|
||||
|
||||
export interface InsightsResponse {
|
||||
days?: number
|
||||
messages?: number
|
||||
sessions?: number
|
||||
}
|
||||
|
||||
export interface ReloadMcpResponse {
|
||||
ok?: boolean
|
||||
}
|
||||
// ── Model picker ─────────────────────────────────────────────────────
|
||||
|
||||
export interface ModelOptionProvider {
|
||||
is_current?: boolean
|
||||
|
|
@ -392,6 +239,12 @@ export interface ModelOptionsResponse {
|
|||
providers?: ModelOptionProvider[]
|
||||
}
|
||||
|
||||
// ── MCP ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ReloadMcpResponse {
|
||||
ok?: boolean
|
||||
}
|
||||
|
||||
// ── Subagent events ──────────────────────────────────────────────────
|
||||
|
||||
export interface SubagentEventPayload {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue