` in the browser.
*/
-function Box(t0) {
+function Box(t0: Props) {
const $ = _c(42)
let autoFocus
let children
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/ClockContext.tsx b/ui-tui/packages/hermes-ink/src/ink/components/ClockContext.tsx
index 521cd5751..99dfc2d88 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/ClockContext.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/ClockContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useEffect, useState } from 'react'
+import React, { createContext, type ReactNode, useEffect, useState } from 'react'
import { c as _c } from 'react/compiler-runtime'
import { BLURRED_FRAME_INTERVAL_MS, FRAME_INTERVAL_MS } from '../constants.js'
@@ -87,7 +87,7 @@ export const ClockContext = createContext
(null)
// Own component so App.tsx doesn't re-render when the clock is created.
// The clock value is stable (created once via useState), so the provider
// never causes consumer re-renders on its own.
-export function ClockProvider(t0) {
+export function ClockProvider(t0: { readonly children: ReactNode }) {
const $ = _c(7)
const { children } = t0
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/Link.tsx b/ui-tui/packages/hermes-ink/src/ink/components/Link.tsx
index 72c94fa11..71c491455 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/Link.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/Link.tsx
@@ -11,7 +11,7 @@ export type Props = {
readonly fallback?: ReactNode
}
-export default function Link(t0) {
+export default function Link(t0: Props) {
const $ = _c(5)
const { children, url, fallback } = t0
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/Newline.tsx b/ui-tui/packages/hermes-ink/src/ink/components/Newline.tsx
index 54dfa50fa..4010dc9ff 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/Newline.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/Newline.tsx
@@ -12,7 +12,7 @@ export type Props = {
/**
* Adds one or more newline (\n) characters. Must be used within components.
*/
-export default function Newline(t0) {
+export default function Newline(t0: Props) {
const $ = _c(4)
const { count: t1 } = t0
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/NoSelect.tsx b/ui-tui/packages/hermes-ink/src/ink/components/NoSelect.tsx
index e3da69852..79078189e 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/NoSelect.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/NoSelect.tsx
@@ -33,7 +33,7 @@ type Props = Omit & {
* tracking). No-op in the main-screen scrollback render where the
* terminal's native selection is used instead.
*/
-export function NoSelect(t0) {
+export function NoSelect(t0: Props) {
const $ = _c(8)
let boxProps
let children
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/RawAnsi.tsx b/ui-tui/packages/hermes-ink/src/ink/components/RawAnsi.tsx
index 2c0b2f0fe..b5bd8f253 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/RawAnsi.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/RawAnsi.tsx
@@ -25,7 +25,7 @@ type Props = {
* (width × lines.length) and hands the joined string straight to output.write(),
* which already splits on '\n' and parses ANSI into the screen buffer.
*/
-export function RawAnsi(t0) {
+export function RawAnsi(t0: Props) {
const $ = _c(6)
const { lines, width } = t0
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx b/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx
index e7b55e71d..bed421234 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/ScrollBox.tsx
@@ -252,7 +252,7 @@ function ScrollBox({ children, ref, stickyScroll, ...style }: PropsWithChildren<
// commit, which is too late for the first frame.
return (
{
+ ref={(el: DOMElement | null) => {
domRef.current = el
if (el) {
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/TerminalFocusContext.tsx b/ui-tui/packages/hermes-ink/src/ink/components/TerminalFocusContext.tsx
index 02860485a..e5f1acdd6 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/TerminalFocusContext.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/TerminalFocusContext.tsx
@@ -1,4 +1,4 @@
-import React, { createContext, useSyncExternalStore } from 'react'
+import React, { createContext, type ReactNode, useSyncExternalStore } from 'react'
import { c as _c } from 'react/compiler-runtime'
import {
@@ -23,7 +23,7 @@ TerminalFocusContext.displayName = 'TerminalFocusContext'
// Separate component so App.tsx doesn't re-render on focus changes.
// Children are a stable prop reference, so they don't re-render either —
// only components that consume the context will re-render.
-export function TerminalFocusProvider(t0) {
+export function TerminalFocusProvider(t0: { readonly children: ReactNode }) {
const $ = _c(6)
const { children } = t0
diff --git a/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx b/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx
index f69d338c1..ea2a74c9a 100644
--- a/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/components/Text.tsx
@@ -116,7 +116,7 @@ const memoizedStylesForWrap: Record, Styles> = {
/**
* This component can display text, and change its style to make it colorful, bold, underline, italic or strikethrough.
*/
-export default function Text(t0) {
+export default function Text(t0: Props) {
const $ = _c(29)
const {
diff --git a/ui-tui/packages/hermes-ink/src/ink/devtools.ts b/ui-tui/packages/hermes-ink/src/ink/devtools.ts
new file mode 100644
index 000000000..73b0c9448
--- /dev/null
+++ b/ui-tui/packages/hermes-ink/src/ink/devtools.ts
@@ -0,0 +1,2 @@
+/** Optional react-devtools hook; package may be absent. */
+export {}
diff --git a/ui-tui/packages/hermes-ink/src/ink/events/paste-event.ts b/ui-tui/packages/hermes-ink/src/ink/events/paste-event.ts
new file mode 100644
index 000000000..38a88f317
--- /dev/null
+++ b/ui-tui/packages/hermes-ink/src/ink/events/paste-event.ts
@@ -0,0 +1,10 @@
+import { TerminalEvent } from './terminal-event.js'
+
+export class PasteEvent extends TerminalEvent {
+ readonly text: string
+
+ constructor(text: string) {
+ super('paste', { bubbles: true, cancelable: true })
+ this.text = text
+ }
+}
diff --git a/ui-tui/packages/hermes-ink/src/ink/events/resize-event.ts b/ui-tui/packages/hermes-ink/src/ink/events/resize-event.ts
new file mode 100644
index 000000000..b2627bb29
--- /dev/null
+++ b/ui-tui/packages/hermes-ink/src/ink/events/resize-event.ts
@@ -0,0 +1,12 @@
+import { TerminalEvent } from './terminal-event.js'
+
+export class ResizeEvent extends TerminalEvent {
+ readonly columns: number
+ readonly rows: number
+
+ constructor(columns: number, rows: number) {
+ super('resize', { bubbles: true, cancelable: true })
+ this.columns = columns
+ this.rows = rows
+ }
+}
diff --git a/ui-tui/packages/hermes-ink/src/ink/ink.tsx b/ui-tui/packages/hermes-ink/src/ink/ink.tsx
index e0163f506..96898cee3 100644
--- a/ui-tui/packages/hermes-ink/src/ink/ink.tsx
+++ b/ui-tui/packages/hermes-ink/src/ink/ink.tsx
@@ -339,8 +339,6 @@ export default class Ink {
}
}
- // @ts-expect-error @types/react-reconciler@0.32.3 declares 11 args with transitionCallbacks,
- // but react-reconciler 0.33.0 source only accepts 10 args (no transitionCallbacks)
this.container = reconciler.createContainer(
this.rootNode,
ConcurrentRoot,
@@ -357,7 +355,7 @@ export default class Ink {
noop // onDefaultTransitionIndicator
)
- if ('production' === 'development') {
+ if (process.env.NODE_ENV === 'development') {
reconciler.injectIntoDevTools({
bundleType: 0,
// Reporting React DOM's version, not Ink's
@@ -955,7 +953,6 @@ export default class Ink {
}
pause(): void {
// Flush pending React updates and render before pausing.
- // @ts-expect-error flushSyncFromReconciler exists in react-reconciler 0.31 but not in @types/react-reconciler
reconciler.flushSyncFromReconciler()
this.onRender()
this.isPaused = true
@@ -1783,9 +1780,7 @@ export default class Ink {
)
- // @ts-expect-error updateContainerSync exists in react-reconciler but not in @types/react-reconciler
reconciler.updateContainerSync(tree, this.container, null, noop)
- // @ts-expect-error flushSyncWork exists in react-reconciler but not in @types/react-reconciler
reconciler.flushSyncWork()
}
unmount(error?: Error | number | null): void {
@@ -1857,9 +1852,7 @@ export default class Ink {
this.drainTimer = null
}
- // @ts-expect-error updateContainerSync exists in react-reconciler but not in @types/react-reconciler
reconciler.updateContainerSync(null, this.container, null, noop)
- // @ts-expect-error flushSyncWork exists in react-reconciler but not in @types/react-reconciler
reconciler.flushSyncWork()
instances.delete(this.options.stdout)
@@ -1966,8 +1959,8 @@ export default class Ink {
const intercept = (
chunk: Uint8Array | string,
- encodingOrCb?: BufferEncoding | ((err?: Error) => void),
- cb?: (err?: Error) => void
+ encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
+ cb?: (err?: Error | null) => void
): boolean => {
const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb
diff --git a/ui-tui/packages/hermes-ink/src/ink/reconciler.ts b/ui-tui/packages/hermes-ink/src/ink/reconciler.ts
index 2be8a7d7c..5fdce3bf9 100644
--- a/ui-tui/packages/hermes-ink/src/ink/reconciler.ts
+++ b/ui-tui/packages/hermes-ink/src/ink/reconciler.ts
@@ -176,27 +176,12 @@ export function resetProfileCounters(): void {
}
// --- END ---
-const reconciler = createReconciler<
- ElementNames,
- Props,
- DOMElement,
- DOMElement,
- TextNode,
- DOMElement,
- unknown,
- unknown,
- DOMElement,
- HostContext,
- null, // UpdatePayload - not used in React 19
- NodeJS.Timeout,
- -1,
- null
->({
+const reconciler = createReconciler({
getRootHostContext: () => ({ isInsideText: false }),
prepareForCommit: () => null,
preparePortalMount: () => null,
clearContainer: () => false,
- resetAfterCommit(rootNode) {
+ resetAfterCommit(rootNode: DOMElement) {
_lastCommitMs = _commitStart > 0 ? performance.now() - _commitStart : 0
_commitStart = 0
@@ -261,19 +246,19 @@ const reconciler = createReconciler<
return createTextNode(text)
},
resetTextContent() {},
- hideTextInstance(node) {
+ hideTextInstance(node: TextNode) {
setTextNodeValue(node, '')
},
- unhideTextInstance(node, text) {
+ unhideTextInstance(node: TextNode, text: string) {
setTextNodeValue(node, text)
},
- getPublicInstance: (instance): DOMElement => instance as DOMElement,
- hideInstance(node) {
+ getPublicInstance: (instance: DOMElement): DOMElement => instance,
+ hideInstance(node: DOMElement) {
node.isHidden = true
node.yogaNode?.setDisplay(LayoutDisplay.None)
markDirty(node)
},
- unhideInstance(node) {
+ unhideInstance(node: DOMElement) {
node.isHidden = false
node.yogaNode?.setDisplay(LayoutDisplay.Flex)
markDirty(node)
@@ -344,7 +329,7 @@ const reconciler = createReconciler<
commitTextUpdate(node: TextNode, _oldText: string, newText: string): void {
setTextNodeValue(node, newText)
},
- removeChild(node, removeNode) {
+ removeChild(node: DOMElement, removeNode: DOMElement | TextNode) {
removeChildNode(node, removeNode)
cleanupYogaNode(removeNode)
diff --git a/ui-tui/packages/hermes-ink/src/ink/render-to-screen.ts b/ui-tui/packages/hermes-ink/src/ink/render-to-screen.ts
index bee9f8f1c..57272bd36 100644
--- a/ui-tui/packages/hermes-ink/src/ink/render-to-screen.ts
+++ b/ui-tui/packages/hermes-ink/src/ink/render-to-screen.ts
@@ -63,14 +63,11 @@ export function renderToScreen(el: ReactElement, width: number): { screen: Scree
stylePool = new StylePool()
charPool = new CharPool()
hyperlinkPool = new HyperlinkPool()
- // @ts-expect-error react-reconciler 0.33 takes 10 args; @types says 11
container = reconciler.createContainer(root, LegacyRoot, null, false, null, 'search-render', noop, noop, noop, noop)
}
const t0 = performance.now()
- // @ts-expect-error updateContainerSync exists but not in @types
reconciler.updateContainerSync(el, container, null, noop)
- // @ts-expect-error flushSyncWork exists but not in @types
reconciler.flushSyncWork()
const t1 = performance.now()
@@ -105,9 +102,7 @@ export function renderToScreen(el: ReactElement, width: number): { screen: Scree
const t3 = performance.now()
// Unmount so next call gets a fresh tree. Leaves root/container/pools.
- // @ts-expect-error updateContainerSync exists but not in @types
reconciler.updateContainerSync(null, container, null, noop)
- // @ts-expect-error flushSyncWork exists but not in @types
reconciler.flushSyncWork()
timing.reconcile += t1 - t0
diff --git a/ui-tui/packages/hermes-ink/src/utils/semver.ts b/ui-tui/packages/hermes-ink/src/utils/semver.ts
index ab57ecf72..87025ed0f 100644
--- a/ui-tui/packages/hermes-ink/src/utils/semver.ts
+++ b/ui-tui/packages/hermes-ink/src/utils/semver.ts
@@ -53,5 +53,5 @@ export function order(a: string, b: string): -1 | 0 | 1 {
return Bun.semver.order(a, b)
}
- return getNpmSemver().compare(a, b, { loose: true })
+ return getNpmSemver().compare(a, b, { loose: true }) as -1 | 0 | 1
}
diff --git a/ui-tui/src/app.tsx b/ui-tui/src/app.tsx
index c32692210..35f1448a1 100644
--- a/ui-tui/src/app.tsx
+++ b/ui-tui/src/app.tsx
@@ -1590,12 +1590,17 @@ export function App({ gw }: { gw: GatewayClient }) {
if (!pastes.length) {
sys('no text pastes')
} else {
- panel('Paste Shelf', [{
- rows: pastes.map(p => [
- `#${p.id} ${p.mode}`,
- `${p.lineCount}L · ${p.kind} · ${compactPreview(p.text, 60) || '(empty)'}`
- ] as [string, string])
- }])
+ panel('Paste Shelf', [
+ {
+ rows: pastes.map(
+ p =>
+ [
+ `#${p.id} ${p.mode}`,
+ `${p.lineCount}L · ${p.kind} · ${compactPreview(p.text, 60) || '(empty)'}`
+ ] as [string, string]
+ )
+ }
+ ])
}
return true
@@ -1648,7 +1653,6 @@ export function App({ gw }: { gw: GatewayClient }) {
sys('usage: /paste [list|mode |drop |clear]')
return true
-
case 'logs': {
const logText = gw.getLogTail(Math.min(80, Math.max(1, parseInt(arg, 10) || 20)))
logText ? page(logText, 'Logs') : sys('no gateway logs')
@@ -1761,7 +1765,14 @@ export function App({ gw }: { gw: GatewayClient }) {
case 'model':
if (!arg) {
rpc('config.get', { key: 'provider' }).then((r: any) =>
- panel('Model', [{ rows: [['Model', r.model], ['Provider', r.provider]] }])
+ panel('Model', [
+ {
+ rows: [
+ ['Model', r.model],
+ ['Provider', r.provider]
+ ]
+ }
+ ])
)
} else {
rpc('config.set', { session_id: sid, key: 'model', value: arg.replace('--global', '').trim() }).then(
@@ -1893,6 +1904,7 @@ export function App({ gw }: { gw: GatewayClient }) {
}
const f = (v: number) => (v ?? 0).toLocaleString()
+
const cost =
r.cost_usd != null ? `${r.cost_status === 'estimated' ? '~' : ''}$${r.cost_usd.toFixed(4)}` : null
@@ -1906,7 +1918,9 @@ export function App({ gw }: { gw: GatewayClient }) {
['API calls', f(r.calls)]
]
- if (cost) rows.push(['Cost', cost])
+ if (cost) {
+ rows.push(['Cost', cost])
+ }
const sections: PanelSection[] = [{ rows }]
@@ -1914,7 +1928,9 @@ export function App({ gw }: { gw: GatewayClient }) {
sections.push({ text: `Context: ${f(r.context_used)} / ${f(r.context_max)} (${r.context_percent}%)` })
}
- if (r.compressions) sections.push({ text: `Compressions: ${r.compressions}` })
+ if (r.compressions) {
+ sections.push({ text: `Compressions: ${r.compressions}` })
+ }
panel('Usage', sections)
})
@@ -1959,13 +1975,15 @@ export function App({ gw }: { gw: GatewayClient }) {
case 'insights':
rpc('insights.get', { days: parseInt(arg) || 30 }).then((r: any) =>
- panel('Insights', [{
- rows: [
- ['Period', `${r.days} days`],
- ['Sessions', `${r.sessions}`],
- ['Messages', `${r.messages}`]
- ]
- }])
+ panel('Insights', [
+ {
+ rows: [
+ ['Period', `${r.days} days`],
+ ['Sessions', `${r.sessions}`],
+ ['Messages', `${r.messages}`]
+ ]
+ }
+ ])
)
return true
@@ -1978,12 +1996,13 @@ export function App({ gw }: { gw: GatewayClient }) {
return sys('no checkpoints')
}
- panel('Checkpoints', [{
- rows: r.checkpoints.map((c: any, i: number) => [
- `${i + 1} ${c.hash?.slice(0, 8)}`,
- c.message
- ] as [string, string])
- }])
+ panel('Checkpoints', [
+ {
+ rows: r.checkpoints.map(
+ (c: any, i: number) => [`${i + 1} ${c.hash?.slice(0, 8)}`, c.message] as [string, string]
+ )
+ }
+ ])
})
} else {
const hash = sub === 'restore' || sub === 'diff' ? rArgs[0] : sub
@@ -2016,9 +2035,11 @@ export function App({ gw }: { gw: GatewayClient }) {
return sys('no plugins')
}
- panel('Plugins', [{
- items: r.plugins.map((p: any) => `${p.name} v${p.version}${p.enabled ? '' : ' (disabled)'}`)
- }])
+ panel('Plugins', [
+ {
+ items: r.plugins.map((p: any) => `${p.name} v${p.version}${p.enabled ? '' : ' (disabled)'}`)
+ }
+ ])
})
return true
@@ -2033,10 +2054,13 @@ export function App({ gw }: { gw: GatewayClient }) {
return sys('no skills installed')
}
- panel('Installed Skills', Object.entries(sk).map(([cat, names]) => ({
- title: cat,
- items: names as string[]
- })))
+ panel(
+ 'Installed Skills',
+ Object.entries(sk).map(([cat, names]) => ({
+ title: cat,
+ items: names as string[]
+ }))
+ )
})
return true
@@ -2045,17 +2069,29 @@ export function App({ gw }: { gw: GatewayClient }) {
if (sub === 'browse') {
const pg = parseInt(sArgs[0] ?? '1', 10) || 1
rpc('skills.manage', { action: 'browse', page: pg }).then((r: any) => {
- if (!r.items?.length) return sys('no skills found in the hub')
+ if (!r.items?.length) {
+ return sys('no skills found in the hub')
+ }
- const sections: PanelSection[] = [{
- rows: r.items.map((s: any) => [
- s.name ?? '',
- (s.description ?? '').slice(0, 60) + (s.description?.length > 60 ? '…' : '')
- ] as [string, string])
- }]
+ const sections: PanelSection[] = [
+ {
+ rows: r.items.map(
+ (s: any) =>
+ [s.name ?? '', (s.description ?? '').slice(0, 60) + (s.description?.length > 60 ? '…' : '')] as [
+ string,
+ string
+ ]
+ )
+ }
+ ]
- if (r.page < r.total_pages) sections.push({ text: `/skills browse ${r.page + 1} → next page` })
- if (r.page > 1) sections.push({ text: `/skills browse ${r.page - 1} → prev page` })
+ if (r.page < r.total_pages) {
+ sections.push({ text: `/skills browse ${r.page + 1} → next page` })
+ }
+
+ if (r.page > 1) {
+ sections.push({ text: `/skills browse ${r.page - 1} → prev page` })
+ }
panel(`Skills Hub (page ${r.page}/${r.total_pages}, ${r.total} total)`, sections)
})
@@ -2073,47 +2109,57 @@ export function App({ gw }: { gw: GatewayClient }) {
case 'agents':
case 'tasks':
- rpc('agents.list', {}).then((r: any) => {
- const procs = r.processes ?? []
- const running = procs.filter((p: any) => p.status === 'running')
- const finished = procs.filter((p: any) => p.status !== 'running')
- const sections: PanelSection[] = []
+ rpc('agents.list', {})
+ .then((r: any) => {
+ const procs = r.processes ?? []
+ const running = procs.filter((p: any) => p.status === 'running')
+ const finished = procs.filter((p: any) => p.status !== 'running')
+ const sections: PanelSection[] = []
- if (running.length) {
- sections.push({
- title: `Running (${running.length})`,
- rows: running.map((p: any) => [p.session_id.slice(0, 8), p.command])
- })
- }
+ if (running.length) {
+ sections.push({
+ title: `Running (${running.length})`,
+ rows: running.map((p: any) => [p.session_id.slice(0, 8), p.command])
+ })
+ }
- if (finished.length) {
- sections.push({
- title: `Finished (${finished.length})`,
- rows: finished.map((p: any) => [p.session_id.slice(0, 8), p.command])
- })
- }
+ if (finished.length) {
+ sections.push({
+ title: `Finished (${finished.length})`,
+ rows: finished.map((p: any) => [p.session_id.slice(0, 8), p.command])
+ })
+ }
- if (!sections.length) sections.push({ text: 'No active processes' })
+ if (!sections.length) {
+ sections.push({ text: 'No active processes' })
+ }
- panel('Agents', sections)
- }).catch(() => sys('agents command failed'))
+ panel('Agents', sections)
+ })
+ .catch(() => sys('agents command failed'))
return true
case 'cron':
if (!arg || arg === 'list') {
- rpc('cron.manage', { action: 'list' }).then((r: any) => {
- const jobs = r.jobs ?? []
+ rpc('cron.manage', { action: 'list' })
+ .then((r: any) => {
+ const jobs = r.jobs ?? []
- if (!jobs.length) return sys('no scheduled jobs')
+ if (!jobs.length) {
+ return sys('no scheduled jobs')
+ }
- panel('Cron', [{
- rows: jobs.map((j: any) => [
- j.name || j.job_id?.slice(0, 12),
- `${j.schedule} · ${j.state ?? 'active'}`
- ] as [string, string])
- }])
- }).catch(() => sys('cron command failed'))
+ panel('Cron', [
+ {
+ rows: jobs.map(
+ (j: any) =>
+ [j.name || j.job_id?.slice(0, 12), `${j.schedule} · ${j.state ?? 'active'}`] as [string, string]
+ )
+ }
+ ])
+ })
+ .catch(() => sys('cron command failed'))
} else {
gw.request('slash.exec', { command: cmd.slice(1), session_id: sid })
.then((r: any) => sys(r?.output || '(no output)'))
@@ -2123,38 +2169,59 @@ export function App({ gw }: { gw: GatewayClient }) {
return true
case 'config':
- rpc('config.show', {}).then((r: any) => {
- panel('Config', (r.sections ?? []).map((s: any) => ({
- title: s.title,
- rows: s.rows
- })))
- }).catch(() => sys('config command failed'))
+ rpc('config.show', {})
+ .then((r: any) => {
+ panel(
+ 'Config',
+ (r.sections ?? []).map((s: any) => ({
+ title: s.title,
+ rows: s.rows
+ }))
+ )
+ })
+ .catch(() => sys('config command failed'))
return true
case 'tools':
- rpc('tools.list', { session_id: sid }).then((r: any) => {
- if (!r.toolsets?.length) return sys('no tools')
+ rpc('tools.list', { session_id: sid })
+ .then((r: any) => {
+ if (!r.toolsets?.length) {
+ return sys('no tools')
+ }
- panel('Tools', r.toolsets.map((ts: any) => ({
- title: `${ts.enabled ? '*' : ' '} ${ts.name} [${ts.tool_count} tools]`,
- items: ts.tools
- })))
- }).catch(() => sys('tools command failed'))
+ panel(
+ 'Tools',
+ r.toolsets.map((ts: any) => ({
+ title: `${ts.enabled ? '*' : ' '} ${ts.name} [${ts.tool_count} tools]`,
+ items: ts.tools
+ }))
+ )
+ })
+ .catch(() => sys('tools command failed'))
return true
case 'toolsets':
- rpc('toolsets.list', { session_id: sid }).then((r: any) => {
- if (!r.toolsets?.length) return sys('no toolsets')
+ rpc('toolsets.list', { session_id: sid })
+ .then((r: any) => {
+ if (!r.toolsets?.length) {
+ return sys('no toolsets')
+ }
- panel('Toolsets', [{
- rows: r.toolsets.map((ts: any) => [
- `${ts.enabled ? '(*)' : ' '} ${ts.name}`,
- `[${ts.tool_count}] ${ts.description}`
- ] as [string, string])
- }])
- }).catch(() => sys('toolsets command failed'))
+ panel('Toolsets', [
+ {
+ rows: r.toolsets.map(
+ (ts: any) =>
+ [`${ts.enabled ? '(*)' : ' '} ${ts.name}`, `[${ts.tool_count}] ${ts.description}`] as [
+ string,
+ string
+ ]
+ )
+ }
+ ])
+ })
+ .catch(() => sys('toolsets command failed'))
return true
@@ -2181,7 +2248,23 @@ export function App({ gw }: { gw: GatewayClient }) {
return true
}
},
- [catalog, compact, gw, lastUserMsg, messages, newSession, page, panel, pastes, pushActivity, rpc, send, sid, statusBar, sys]
+ [
+ catalog,
+ compact,
+ gw,
+ lastUserMsg,
+ messages,
+ newSession,
+ page,
+ panel,
+ pastes,
+ pushActivity,
+ rpc,
+ send,
+ sid,
+ statusBar,
+ sys
+ ]
)
slashRef.current = slash
diff --git a/ui-tui/src/components/branding.tsx b/ui-tui/src/components/branding.tsx
index 429996db7..d37f86f71 100644
--- a/ui-tui/src/components/branding.tsx
+++ b/ui-tui/src/components/branding.tsx
@@ -179,4 +179,3 @@ export function Panel({ sections, t, title }: { sections: PanelSection[]; t: The
)
}
-
diff --git a/ui-tui/src/components/textInput.tsx b/ui-tui/src/components/textInput.tsx
index b41a0b27e..ff4f08b00 100644
--- a/ui-tui/src/components/textInput.tsx
+++ b/ui-tui/src/components/textInput.tsx
@@ -1,4 +1,5 @@
import * as Ink from '@hermes/ink'
+import type { InputEvent, Key } from '@hermes/ink'
import { useEffect, useMemo, useRef, useState } from 'react'
type InkExt = typeof Ink & {
@@ -303,7 +304,7 @@ export function TextInput({ columns = 80, value, onChange, onPaste, onSubmit, pl
// ── Input handler ────────────────────────────────────────────────
useInput(
- (inp, k, event) => {
+ (inp: string, k: Key, event: InputEvent) => {
// Some terminals normalize Ctrl+V to "v"; others deliver raw ^V (\x16).
const ctrlPaste = k.ctrl && (inp.toLowerCase() === 'v' || event.keypress.raw === '\x16')
const metaPaste = k.meta && inp.toLowerCase() === 'v'
diff --git a/ui-tui/src/types/hermes-ink.d.ts b/ui-tui/src/types/hermes-ink.d.ts
index db77c9f2a..7c8a8a724 100644
--- a/ui-tui/src/types/hermes-ink.d.ts
+++ b/ui-tui/src/types/hermes-ink.d.ts
@@ -22,7 +22,13 @@ declare module '@hermes/ink' {
readonly [key: string]: boolean
}
- export type InputHandler = (input: string, key: Key) => void
+ export type InputEvent = {
+ readonly input: string
+ readonly key: Key
+ readonly keypress: { readonly raw?: string }
+ }
+
+ export type InputHandler = (input: string, key: Key, event: InputEvent) => void
export type RenderOptions = {
readonly stdin?: NodeJS.ReadStream