mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
chore: clean exit
This commit is contained in:
parent
d9d0ac06b9
commit
c3eeb03e26
2 changed files with 228 additions and 85 deletions
|
|
@ -565,14 +565,19 @@ def _launch_tui():
|
||||||
|
|
||||||
tsx = tui_dir / "node_modules" / ".bin" / "tsx"
|
tsx = tui_dir / "node_modules" / ".bin" / "tsx"
|
||||||
if tsx.exists():
|
if tsx.exists():
|
||||||
sys.exit(subprocess.call([str(tsx), "src/entry.tsx"], cwd=str(tui_dir)))
|
argv = [str(tsx), "src/entry.tsx"]
|
||||||
|
else:
|
||||||
|
npm = shutil.which("npm")
|
||||||
|
if not npm:
|
||||||
|
print("npm not found in PATH. Source your nvm/node setup or set PATH.")
|
||||||
|
sys.exit(1)
|
||||||
|
argv = [npm, "start"]
|
||||||
|
|
||||||
npm = shutil.which("npm")
|
try:
|
||||||
if not npm:
|
code = subprocess.call(argv, cwd=str(tui_dir))
|
||||||
print("npm not found in PATH. Source your nvm/node setup or set PATH.")
|
except KeyboardInterrupt:
|
||||||
sys.exit(1)
|
code = 130
|
||||||
|
sys.exit(code)
|
||||||
sys.exit(subprocess.call([npm, "start"], cwd=str(tui_dir)))
|
|
||||||
|
|
||||||
|
|
||||||
def cmd_chat(args):
|
def cmd_chat(args):
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@ import { homedir, tmpdir } from 'node:os'
|
||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
|
|
||||||
import { Box, Static, Text, useApp, useInput, useStdout } from 'ink'
|
import { Box, Static, Text, useApp, useInput, useStdout } from 'ink'
|
||||||
import { TextInput } from './components/textInput.js'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
||||||
import { Banner, SessionPanel } from './components/branding.js'
|
import { Banner, SessionPanel } from './components/branding.js'
|
||||||
import { MaskedPrompt } from './components/maskedPrompt.js'
|
import { MaskedPrompt } from './components/maskedPrompt.js'
|
||||||
import { MessageLine } from './components/messageLine.js'
|
import { MessageLine } from './components/messageLine.js'
|
||||||
import { ApprovalPrompt, ClarifyPrompt } from './components/prompts.js'
|
import { ApprovalPrompt, ClarifyPrompt } from './components/prompts.js'
|
||||||
import { QueuedMessages } from './components/queuedMessages.js'
|
import { QueuedMessages } from './components/queuedMessages.js'
|
||||||
import { SessionPicker } from './components/sessionPicker.js'
|
import { SessionPicker } from './components/sessionPicker.js'
|
||||||
|
import { TextInput } from './components/textInput.js'
|
||||||
import { Thinking } from './components/thinking.js'
|
import { Thinking } from './components/thinking.js'
|
||||||
import { HOTKEYS, INTERPOLATION_RE, PLACEHOLDERS, TOOL_VERBS, ZERO } from './constants.js'
|
import { HOTKEYS, INTERPOLATION_RE, PLACEHOLDERS, TOOL_VERBS, ZERO } from './constants.js'
|
||||||
import { type GatewayClient, type GatewayEvent } from './gatewayClient.js'
|
import { type GatewayClient, type GatewayEvent } from './gatewayClient.js'
|
||||||
|
|
@ -41,20 +42,33 @@ const introMsg = (info: SessionInfo): Msg => ({
|
||||||
info
|
info
|
||||||
})
|
})
|
||||||
|
|
||||||
function extractTabWord(input: string): string | null {
|
const TAB_PATH_RE = /((?:\.\.?\/|~\/|\/|@)[^\s]*)$/
|
||||||
const m = input.match(/((?:\.\.?\/|~\/|\/|@)[^\s]*)$/)
|
|
||||||
return m?.[1] ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusRule({ cols, color, dimColor, statusColor, parts }: {
|
function StatusRule({
|
||||||
cols: number; color: string; dimColor: string; statusColor: string; parts: (string | false | undefined | null)[]
|
cols,
|
||||||
|
color,
|
||||||
|
dimColor,
|
||||||
|
statusColor,
|
||||||
|
parts
|
||||||
|
}: {
|
||||||
|
cols: number
|
||||||
|
color: string
|
||||||
|
dimColor: string
|
||||||
|
statusColor: string
|
||||||
|
parts: (string | false | undefined | null)[]
|
||||||
}) {
|
}) {
|
||||||
const label = parts.filter(Boolean).join(' · ')
|
const label = parts.filter(Boolean).join(' · ')
|
||||||
|
const lead = String(parts[0] ?? '')
|
||||||
const fill = Math.max(0, cols - label.length - 5)
|
const fill = Math.max(0, cols - label.length - 5)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text color={color}>
|
<Text color={color}>
|
||||||
{'─ '}<Text color={dimColor}><Text color={statusColor}>{parts[0]}</Text>{label.slice(String(parts[0] || '').length)}</Text>{' ' + '─'.repeat(fill)}
|
{'─ '}
|
||||||
|
<Text color={dimColor}>
|
||||||
|
<Text color={statusColor}>{parts[0]}</Text>
|
||||||
|
{label.slice(lead.length)}
|
||||||
|
</Text>
|
||||||
|
{' ' + '─'.repeat(fill)}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -65,10 +79,15 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const [cols, setCols] = useState(stdout?.columns ?? 80)
|
const [cols, setCols] = useState(stdout?.columns ?? 80)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!stdout) return
|
if (!stdout) {
|
||||||
|
return
|
||||||
|
}
|
||||||
const sync = () => setCols(stdout.columns ?? 80)
|
const sync = () => setCols(stdout.columns ?? 80)
|
||||||
stdout.on('resize', sync)
|
stdout.on('resize', sync)
|
||||||
return () => { stdout.off('resize', sync) }
|
|
||||||
|
return () => {
|
||||||
|
stdout.off('resize', sync)
|
||||||
|
}
|
||||||
}, [stdout])
|
}, [stdout])
|
||||||
|
|
||||||
const [input, setInput] = useState('')
|
const [input, setInput] = useState('')
|
||||||
|
|
@ -108,7 +127,6 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const lastEmptyAt = useRef(0)
|
const lastEmptyAt = useRef(0)
|
||||||
const lastStatusNoteRef = useRef('')
|
const lastStatusNoteRef = useRef('')
|
||||||
const protocolWarnedRef = useRef(false)
|
const protocolWarnedRef = useRef(false)
|
||||||
const stderrWarnedRef = useRef(false)
|
|
||||||
const pasteCounterRef = useRef(0)
|
const pasteCounterRef = useRef(0)
|
||||||
|
|
||||||
const empty = !messages.length
|
const empty = !messages.length
|
||||||
|
|
@ -167,31 +185,51 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const compInputRef = useRef('')
|
const compInputRef = useRef('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blocked) { if (completions.length) { setCompletions([]); setCompIdx(0) }; return }
|
if (blocked) {
|
||||||
if (input === compInputRef.current) return
|
if (completions.length) {
|
||||||
|
setCompletions([])
|
||||||
|
setCompIdx(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input === compInputRef.current) {
|
||||||
|
return
|
||||||
|
}
|
||||||
compInputRef.current = input
|
compInputRef.current = input
|
||||||
|
|
||||||
const isSlash = input.startsWith('/')
|
const isSlash = input.startsWith('/')
|
||||||
const pathWord = !isSlash ? extractTabWord(input) : null
|
const pathWord = !isSlash ? (input.match(TAB_PATH_RE)?.[1] ?? null) : null
|
||||||
|
|
||||||
if (!isSlash && !pathWord) {
|
if (!isSlash && !pathWord) {
|
||||||
if (completions.length) { setCompletions([]); setCompIdx(0) }
|
if (completions.length) {
|
||||||
|
setCompletions([])
|
||||||
|
setCompIdx(0)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = setTimeout(() => {
|
const t = setTimeout(() => {
|
||||||
if (compInputRef.current !== input) return
|
if (compInputRef.current !== input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const req = isSlash
|
const req = isSlash
|
||||||
? gw.request('complete.slash', { text: input })
|
? gw.request('complete.slash', { text: input })
|
||||||
: gw.request('complete.path', { word: pathWord })
|
: gw.request('complete.path', { word: pathWord })
|
||||||
|
|
||||||
req.then((r: any) => {
|
req
|
||||||
if (compInputRef.current !== input) return
|
.then((r: any) => {
|
||||||
setCompletions(r?.items ?? [])
|
if (compInputRef.current !== input) {
|
||||||
setCompIdx(0)
|
return
|
||||||
setCompReplace(isSlash ? (r?.replace_from ?? 1) : input.length - (pathWord?.length ?? 0))
|
}
|
||||||
}).catch(() => {})
|
setCompletions(r?.items ?? [])
|
||||||
|
setCompIdx(0)
|
||||||
|
setCompReplace(isSlash ? (r?.replace_from ?? 1) : input.length - (pathWord?.length ?? 0))
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
}, 60)
|
}, 60)
|
||||||
|
|
||||||
return () => clearTimeout(t)
|
return () => clearTimeout(t)
|
||||||
|
|
@ -232,7 +270,6 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
setStatus('ready')
|
setStatus('ready')
|
||||||
lastStatusNoteRef.current = ''
|
lastStatusNoteRef.current = ''
|
||||||
protocolWarnedRef.current = false
|
protocolWarnedRef.current = false
|
||||||
stderrWarnedRef.current = false
|
|
||||||
|
|
||||||
if (r.info) {
|
if (r.info) {
|
||||||
setInfo(r.info)
|
setInfo(r.info)
|
||||||
|
|
@ -277,7 +314,11 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
const expandPastes = (text: string) =>
|
const expandPastes = (text: string) =>
|
||||||
text.replace(PASTE_REF_RE, (m, path) => {
|
text.replace(PASTE_REF_RE, (m, path) => {
|
||||||
try { return readFileSync(path, 'utf8') } catch { return m }
|
try {
|
||||||
|
return readFileSync(path, 'utf8')
|
||||||
|
} catch {
|
||||||
|
return m
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const collapsePaste = (text: string) => {
|
const collapsePaste = (text: string) => {
|
||||||
|
|
@ -285,8 +326,14 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const lineCount = text.split('\n').length
|
const lineCount = text.split('\n').length
|
||||||
const pasteDir = join(process.env.HERMES_HOME ?? join(homedir(), '.hermes'), 'pastes')
|
const pasteDir = join(process.env.HERMES_HOME ?? join(homedir(), '.hermes'), 'pastes')
|
||||||
mkdirSync(pasteDir, { recursive: true })
|
mkdirSync(pasteDir, { recursive: true })
|
||||||
const pasteFile = join(pasteDir, `paste_${pasteCounterRef.current}_${new Date().toTimeString().slice(0, 8).replace(/:/g, '')}.txt`)
|
|
||||||
|
const pasteFile = join(
|
||||||
|
pasteDir,
|
||||||
|
`paste_${pasteCounterRef.current}_${new Date().toTimeString().slice(0, 8).replace(/:/g, '')}.txt`
|
||||||
|
)
|
||||||
|
|
||||||
writeFileSync(pasteFile, text, 'utf8')
|
writeFileSync(pasteFile, text, 'utf8')
|
||||||
|
|
||||||
return `[Pasted text #${pasteCounterRef.current}: ${lineCount} lines → ${pasteFile}]`
|
return `[Pasted text #${pasteCounterRef.current}: ${lineCount} lines → ${pasteFile}]`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -310,9 +357,12 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
gw.request('shell.exec', { command: cmd })
|
gw.request('shell.exec', { command: cmd })
|
||||||
.then((r: any) => {
|
.then((r: any) => {
|
||||||
const out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim()
|
const out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim()
|
||||||
sys(out || `exit ${r.code}`)
|
|
||||||
|
|
||||||
if (r.code !== 0 && out) {
|
if (out) {
|
||||||
|
sys(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r.code !== 0 || !out) {
|
||||||
sys(`exit ${r.code}`)
|
sys(`exit ${r.code}`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -349,8 +399,8 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
unlinkSync(file)
|
unlinkSync(file)
|
||||||
} catch (_) {
|
} catch {
|
||||||
/* cleanup best-effort */
|
/* noop */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,13 +449,18 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completions.length && input && (key.upArrow || key.downArrow)) {
|
if (completions.length && input && (key.upArrow || key.downArrow)) {
|
||||||
setCompIdx(i => key.upArrow ? (i - 1 + completions.length) % completions.length : (i + 1) % completions.length)
|
setCompIdx(i => (key.upArrow ? (i - 1 + completions.length) % completions.length : (i + 1) % completions.length))
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inputBuf.length && key.tab && completions.length) {
|
if (!inputBuf.length && key.tab && completions.length) {
|
||||||
const pick = completions[compIdx]
|
const row = completions[compIdx]
|
||||||
if (pick) setInput(input.slice(0, compReplace) + pick.text)
|
|
||||||
|
if (row) {
|
||||||
|
setInput(input.slice(0, compReplace) + row.text)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,9 +514,11 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
if (busy && sid) {
|
if (busy && sid) {
|
||||||
interruptedRef.current = true
|
interruptedRef.current = true
|
||||||
gw.request('session.interrupt', { session_id: sid }).catch(() => {})
|
gw.request('session.interrupt', { session_id: sid }).catch(() => {})
|
||||||
|
|
||||||
if (buf.current.trim()) {
|
if (buf.current.trim()) {
|
||||||
appendMessage({ role: 'assistant' as const, text: buf.current.trimStart() })
|
appendMessage({ role: 'assistant' as const, text: buf.current.trimStart() })
|
||||||
}
|
}
|
||||||
|
|
||||||
idle()
|
idle()
|
||||||
setStatus('interrupted')
|
setStatus('interrupted')
|
||||||
sys('interrupted by user')
|
sys('interrupted by user')
|
||||||
|
|
@ -482,11 +539,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
if (key.ctrl && ch === 'l') {
|
if (key.ctrl && ch === 'l') {
|
||||||
setStatus('forging session…')
|
setStatus('forging session…')
|
||||||
newSession()
|
newSession()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key.ctrl && ch === 'v') {
|
if (key.ctrl && ch === 'v') {
|
||||||
paste()
|
paste()
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -530,6 +589,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
case 'session.info':
|
case 'session.info':
|
||||||
setInfo(p as SessionInfo)
|
setInfo(p as SessionInfo)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'thinking.delta':
|
case 'thinking.delta':
|
||||||
|
|
@ -559,9 +619,6 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'gateway.stderr':
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'gateway.protocol_error':
|
case 'gateway.protocol_error':
|
||||||
setStatus('protocol warning')
|
setStatus('protocol warning')
|
||||||
|
|
||||||
|
|
@ -579,23 +636,24 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'tool.generating':
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'tool.progress':
|
case 'tool.progress':
|
||||||
if (p?.preview) {
|
if (p?.preview) {
|
||||||
setTools(prev => {
|
setTools(prev => {
|
||||||
const idx = prev.findIndex(t => t.name === p.name)
|
const idx = prev.findIndex(t => t.name === p.name)
|
||||||
if (idx >= 0) return [...prev.slice(0, idx), { ...prev[idx]!, context: p.preview as string }, ...prev.slice(idx + 1)]
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
return [...prev.slice(0, idx), { ...prev[idx]!, context: p.preview as string }, ...prev.slice(idx + 1)]
|
||||||
|
}
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'tool.start': {
|
case 'tool.start': {
|
||||||
const ctx = (p.context as string) || ''
|
const ctx = (p.context as string) || ''
|
||||||
setTools(prev => [...prev, { id: p.tool_id, name: p.name, context: ctx }])
|
setTools(prev => [...prev, { id: p.tool_id, name: p.name, context: ctx }])
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -605,8 +663,10 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
const label = TOOL_VERBS[done?.name ?? p.name] ?? done?.name ?? p.name
|
const label = TOOL_VERBS[done?.name ?? p.name] ?? done?.name ?? p.name
|
||||||
const ctx = done?.context || ''
|
const ctx = done?.context || ''
|
||||||
appendMessage({ role: 'tool', text: `${label}${ctx ? ': ' + ctx : ''} ✓` })
|
appendMessage({ role: 'tool', text: `${label}${ctx ? ': ' + ctx : ''} ✓` })
|
||||||
|
|
||||||
return prev.filter(t => t.id !== p.tool_id)
|
return prev.filter(t => t.id !== p.tool_id)
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'clarify.request':
|
case 'clarify.request':
|
||||||
|
|
@ -644,7 +704,9 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'message.delta':
|
case 'message.delta':
|
||||||
if (!p?.text || interruptedRef.current) break
|
if (!p?.text || interruptedRef.current) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
buf.current += p.rendered ?? p.text
|
buf.current += p.rendered ?? p.text
|
||||||
setStreaming(buf.current.trimStart())
|
setStreaming(buf.current.trimStart())
|
||||||
|
|
@ -732,108 +794,164 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'quit':
|
case 'quit':
|
||||||
|
|
||||||
case 'exit':
|
case 'exit':
|
||||||
|
|
||||||
case 'q':
|
case 'q':
|
||||||
die()
|
die()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'clear':
|
case 'clear':
|
||||||
setStatus('forging session…')
|
setStatus('forging session…')
|
||||||
newSession()
|
newSession()
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'new':
|
case 'new':
|
||||||
setStatus('forging session…')
|
setStatus('forging session…')
|
||||||
newSession('new session started')
|
newSession('new session started')
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'compact':
|
case 'compact':
|
||||||
setCompact(c => (arg ? true : !c))
|
setCompact(c => (arg ? true : !c))
|
||||||
sys(arg ? `compact on, focus: ${arg}` : `compact ${compact ? 'off' : 'on'}`)
|
sys(arg ? `compact on, focus: ${arg}` : `compact ${compact ? 'off' : 'on'}`)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'resume':
|
case 'resume':
|
||||||
if (!sid) { setPicker(true); return true }
|
|
||||||
setPicker(true)
|
setPicker(true)
|
||||||
return true
|
|
||||||
|
|
||||||
|
return true
|
||||||
case 'copy': {
|
case 'copy': {
|
||||||
const all = messages.filter(m => m.role === 'assistant')
|
const all = messages.filter(m => m.role === 'assistant')
|
||||||
const target = all[arg ? Math.min(parseInt(arg), all.length) - 1 : all.length - 1]
|
const target = all[arg ? Math.min(parseInt(arg), all.length) - 1 : all.length - 1]
|
||||||
if (!target) { sys('nothing to copy'); return true }
|
|
||||||
|
if (!target) {
|
||||||
|
sys('nothing to copy')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
writeOsc52Clipboard(target.text)
|
writeOsc52Clipboard(target.text)
|
||||||
sys('copied to clipboard')
|
sys('copied to clipboard')
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'paste':
|
case 'paste':
|
||||||
paste()
|
paste()
|
||||||
return true
|
|
||||||
|
|
||||||
|
return true
|
||||||
case 'logs': {
|
case 'logs': {
|
||||||
const limit = Math.min(80, Math.max(1, parseInt(arg, 10) || 20))
|
const limit = Math.min(80, Math.max(1, parseInt(arg, 10) || 20))
|
||||||
sys(gw.getLogTail(limit) || 'no gateway logs')
|
sys(gw.getLogTail(limit) || 'no gateway logs')
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'statusbar':
|
case 'statusbar':
|
||||||
|
|
||||||
case 'sb':
|
case 'sb':
|
||||||
setStatusBar(v => !v)
|
setStatusBar(v => !v)
|
||||||
sys(`status bar ${statusBar ? 'off' : 'on'}`)
|
sys(`status bar ${statusBar ? 'off' : 'on'}`)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'queue':
|
case 'queue':
|
||||||
if (!arg) { sys(`${queueRef.current.length} queued message(s)`); return true }
|
if (!arg) {
|
||||||
|
sys(`${queueRef.current.length} queued message(s)`)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
enqueue(arg)
|
enqueue(arg)
|
||||||
sys(`queued: "${arg.slice(0, 50)}${arg.length > 50 ? '…' : ''}"`)
|
sys(`queued: "${arg.slice(0, 50)}${arg.length > 50 ? '…' : ''}"`)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'undo':
|
case 'undo':
|
||||||
if (!sid) return true
|
if (!sid) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
rpc('session.undo', { session_id: sid }).then((r: any) => {
|
rpc('session.undo', { session_id: sid }).then((r: any) => {
|
||||||
if (r.removed > 0) {
|
if (r.removed > 0) {
|
||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const q = [...prev]
|
const q = [...prev]
|
||||||
while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') q.pop()
|
|
||||||
if (q.at(-1)?.role === 'user') q.pop()
|
while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') {
|
||||||
|
q.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (q.at(-1)?.role === 'user') {
|
||||||
|
q.pop()
|
||||||
|
}
|
||||||
|
|
||||||
return q
|
return q
|
||||||
})
|
})
|
||||||
sys(`undid ${r.removed} messages`)
|
sys(`undid ${r.removed} messages`)
|
||||||
} else sys('nothing to undo')
|
} else {
|
||||||
|
sys('nothing to undo')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
case 'retry':
|
case 'retry':
|
||||||
if (!lastUserMsg) { sys('nothing to retry'); return true }
|
if (!lastUserMsg) {
|
||||||
if (sid) gw.request('session.undo', { session_id: sid }).catch(() => {})
|
sys('nothing to retry')
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sid) {
|
||||||
|
gw.request('session.undo', { session_id: sid }).catch(() => {})
|
||||||
|
}
|
||||||
setMessages(prev => {
|
setMessages(prev => {
|
||||||
const q = [...prev]
|
const q = [...prev]
|
||||||
while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') q.pop()
|
|
||||||
|
while (q.at(-1)?.role === 'assistant' || q.at(-1)?.role === 'tool') {
|
||||||
|
q.pop()
|
||||||
|
}
|
||||||
|
|
||||||
return q
|
return q
|
||||||
})
|
})
|
||||||
send(lastUserMsg)
|
send(lastUserMsg)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
rpc('slash.exec', { command: cmd.slice(1), session_id: sid })
|
rpc('slash.exec', { command: cmd.slice(1), session_id: sid })
|
||||||
.then((r: any) => {
|
.then((r: any) => {
|
||||||
if (r?.output) sys(r.output)
|
if (r?.output) {
|
||||||
else sys(`/${name}: no output`)
|
sys(r.output)
|
||||||
|
} else {
|
||||||
|
sys(`/${name}: no output`)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
gw.request('command.dispatch', { name: name ?? '', arg, session_id: sid })
|
gw.request('command.dispatch', { name: name ?? '', arg, session_id: sid })
|
||||||
.then((d: any) => {
|
.then((d: any) => {
|
||||||
if (d.type === 'exec') sys(d.output || '(no output)')
|
if (d.type === 'exec') {
|
||||||
else if (d.type === 'alias') slash(`/${d.target}${arg ? ' ' + arg : ''}`)
|
sys(d.output || '(no output)')
|
||||||
else if (d.type === 'plugin') sys(d.output || '(no output)')
|
} else if (d.type === 'alias') {
|
||||||
else if (d.type === 'skill') { sys(`⚡ loading skill: ${d.name}`); send(d.message) }
|
slash(`/${d.target}${arg ? ' ' + arg : ''}`)
|
||||||
|
} else if (d.type === 'plugin') {
|
||||||
|
sys(d.output || '(no output)')
|
||||||
|
} else if (d.type === 'skill') {
|
||||||
|
sys(`⚡ loading skill: ${d.name}`)
|
||||||
|
send(d.message)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(() => sys(`unknown command: /${name}`))
|
.catch(() => sys(`unknown command: /${name}`))
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -899,11 +1017,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
syncQueue()
|
syncQueue()
|
||||||
gw.request('session.interrupt', { session_id: sid }).catch(() => {})
|
gw.request('session.interrupt', { session_id: sid }).catch(() => {})
|
||||||
setStatus('interrupting…')
|
setStatus('interrupting…')
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (picked && sid) {
|
if (picked && sid) {
|
||||||
send(picked)
|
send(picked)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -965,16 +1085,14 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
<Static items={historyItems}>
|
<Static items={historyItems}>
|
||||||
{(m, i) => (
|
{(m, i) => (
|
||||||
<Box flexDirection="column" key={i} paddingX={1}>
|
<Box flexDirection="column" key={i} paddingX={1}>
|
||||||
{m.kind === 'intro' && m.info
|
{m.kind === 'intro' && m.info ? (
|
||||||
? (
|
<Box flexDirection="column" paddingTop={1}>
|
||||||
<Box flexDirection="column" paddingTop={1}>
|
<Banner t={theme} />
|
||||||
<Banner t={theme} />
|
<SessionPanel info={m.info} sid={sid} t={theme} />
|
||||||
<SessionPanel info={m.info} sid={sid} t={theme} />
|
</Box>
|
||||||
</Box>
|
) : (
|
||||||
)
|
<MessageLine cols={cols} compact={compact} msg={m} t={theme} />
|
||||||
: (
|
)}
|
||||||
<MessageLine cols={cols} compact={compact} msg={m} t={theme} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Static>
|
</Static>
|
||||||
|
|
@ -1052,11 +1170,13 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
setSid(r.session_id)
|
setSid(r.session_id)
|
||||||
setMessages([])
|
setMessages([])
|
||||||
setInfo(r.info ?? null)
|
setInfo(r.info ?? null)
|
||||||
if (r.info) appendHistory(introMsg(r.info))
|
|
||||||
|
if (r.info) {
|
||||||
|
appendHistory(introMsg(r.info))
|
||||||
|
}
|
||||||
setUsage(ZERO)
|
setUsage(ZERO)
|
||||||
lastStatusNoteRef.current = ''
|
lastStatusNoteRef.current = ''
|
||||||
protocolWarnedRef.current = false
|
protocolWarnedRef.current = false
|
||||||
stderrWarnedRef.current = false
|
|
||||||
sys(`resumed session (${r.message_count} messages)`)
|
sys(`resumed session (${r.message_count} messages)`)
|
||||||
setStatus('ready')
|
setStatus('ready')
|
||||||
})
|
})
|
||||||
|
|
@ -1071,23 +1191,38 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
|
|
||||||
<QueuedMessages cols={cols} queued={queuedDisplay} queueEditIdx={queueEditIdx} t={theme} />
|
<QueuedMessages cols={cols} queued={queuedDisplay} queueEditIdx={queueEditIdx} t={theme} />
|
||||||
|
|
||||||
<Text>{' '}</Text>
|
<Text> </Text>
|
||||||
|
|
||||||
<StatusRule cols={cols} color={theme.color.bronze} dimColor={theme.color.dim} statusColor={statusColor}
|
<StatusRule
|
||||||
parts={[status, sid, info?.model?.split('/').pop(), usage.total > 0 && `${fmtK(usage.total)} tok`]} />
|
color={theme.color.bronze}
|
||||||
|
cols={cols}
|
||||||
|
dimColor={theme.color.dim}
|
||||||
|
parts={[status, sid, info?.model?.split('/').pop(), usage.total > 0 && `${fmtK(usage.total)} tok`]}
|
||||||
|
statusColor={statusColor}
|
||||||
|
/>
|
||||||
|
|
||||||
{!blocked && (
|
{!blocked && (
|
||||||
<Box>
|
<Box>
|
||||||
<Box width={3}>
|
<Box width={3}>
|
||||||
<Text bold color={theme.color.gold}>{inputBuf.length ? '… ' : `${theme.brand.prompt} `}</Text>
|
<Text bold color={theme.color.gold}>
|
||||||
|
{inputBuf.length ? '… ' : `${theme.brand.prompt} `}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
onChange={setInput}
|
onChange={setInput}
|
||||||
onLargePaste={collapsePaste}
|
onLargePaste={collapsePaste}
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
|
placeholder={
|
||||||
|
empty
|
||||||
|
? PLACEHOLDER
|
||||||
|
: busy
|
||||||
|
? 'Ctrl+C to interrupt…'
|
||||||
|
: inputBuf.length
|
||||||
|
? 'continue (or Enter to send)'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
value={input}
|
value={input}
|
||||||
placeholder={empty ? PLACEHOLDER : busy ? 'Ctrl+C to interrupt…' : inputBuf.length ? 'continue (or Enter to send)' : ''}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1096,9 +1231,12 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
<Box borderColor={theme.color.bronze} borderStyle="single" flexDirection="column" paddingX={1}>
|
<Box borderColor={theme.color.bronze} borderStyle="single" flexDirection="column" paddingX={1}>
|
||||||
{completions.slice(Math.max(0, compIdx - 8), compIdx + 8).map((item, i) => {
|
{completions.slice(Math.max(0, compIdx - 8), compIdx + 8).map((item, i) => {
|
||||||
const active = Math.max(0, compIdx - 8) + i === compIdx
|
const active = Math.max(0, compIdx - 8) + i === compIdx
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text key={item.text}>
|
<Text key={item.text}>
|
||||||
<Text bold={active} color={active ? theme.color.amber : theme.color.cornsilk}>{item.display}</Text>
|
<Text bold={active} color={active ? theme.color.amber : theme.color.cornsilk}>
|
||||||
|
{item.display}
|
||||||
|
</Text>
|
||||||
{item.meta ? <Text color={theme.color.dim}> {item.meta}</Text> : null}
|
{item.meta ? <Text color={theme.color.dim}> {item.meta}</Text> : null}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue