Two TUI fixes:
1. Hyperlinks are now clickable (Cmd+Click / Ctrl+Click) in terminals
that support OSC 8. The markdown renderer was rendering links as
plain colored text — now wraps them in the existing <Link> component
from @hermes/ink which emits OSC 8 escape sequences.
2. Skill slash commands (e.g. /hermes-agent-dev) now work in the TUI.
The slash.exec handler was delegating to the _SlashWorker subprocess
which calls cli.process_command(). For skills, process_command()
queues the invocation message onto _pending_input — a Queue that
nobody reads in the worker subprocess. The skill message was lost.
Now slash.exec detects skill commands early and rejects them so
the TUI falls through to command.dispatch, which correctly builds
and returns the skill payload for the client to send().
Adds a minimal hand-rolled highlighter for ts/js/jsx/tsx, py, sh/bash, go, rust,
json, yaml, sql. Recognizes whole-line comments, single/double/backtick strings,
numbers, and per-language keyword sets. Unknown langs fall through to the current
plain rendering; the existing diff-specific colorization is preserved.
Closes the §8 "Markdown syntax highlighting is missing (only diff gets colored)"
finding from the TUI v2 audit without pulling in a highlighter library.
/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.
Intercept bare /skills locally and flip overlay.skillsHub, so the
overlay opens instantly without waiting on slash.exec. /skills <args>
still forwards to slash.exec and paginates any output. Tests cover
both branches.
- turnController gates scheduleStreaming / reasoning recorders on
streaming + showReasoning so disabling them keeps the buffer silent
until message.complete flushes
- createGatewayEventHandler only surfaces inline_diff previews when
inlineDiffs is on
- StatusRule takes a showCost prop and renders `· $X.XXXX` with the
same toFixed(4) formatting as /usage when usage.cost_usd is present
- Usage grows cost_usd?: number to match the gateway payload
- Existing handler tests flip showReasoning on in beforeEach so
reasoning-flow assertions keep their meaning
Extends ConfigDisplayConfig and UiState so the four new display flags
flow from `config.get {key:"full"}` into the nanostore. applyDisplay is
exported to keep the fan-out testable without an Ink harness.
Defaults mirror v1 parity: streaming + inline_diffs default true
(opt-out via `=== false`), show_cost + show_reasoning default false
(opt-in via plain truthy check).
- tui_gateway: new `setup.status` RPC that reuses CLI's
`_has_any_provider_configured()`, so the TUI can ask the same question
the CLI bootstrap asks before launching a session
- useSessionLifecycle: preflight `setup.status` before both `newSession`
and `resumeById`, and render a clear "Setup Required" panel when no
provider is configured instead of booting a session that immediately
fails with `agent init failed`
- createGatewayEventHandler: drop duplicate startup resume logic in
favor of the preflighted `resumeById`, and special-case the
no-provider agent-init error as a last-mile fallback to the same
setup panel
- add regression tests for both paths
The status bar was showing stale lifecycle text ("running…") while the
face+verb stream flickered through the thinking panel as Python pushed
thinking.delta events. That's backwards — the face ticker is the
primary "I'm alive" signal, it belongs in the status bar; the thinking
panel is for substantive reasoning and tool activity.
Status bar now reads `ui.busy`: when true, renders a local `<FaceTicker>`
cycling FACES × VERBS on a 2.5s interval, unaffected by server events.
When false, the bar shows the actual status string (ready, starting
agent…, interrupted, etc.).
Side effect: `scheduleThinkingStatus` still patches `ui.status` with
Python's face text, but while busy the bar ignores that string and uses
the ticker instead. No server-side changes needed — Python keeps
emitting thinking.delta as a liveness heartbeat, the TUI just doesn't
let it fight the status bar.
Hoist turn state from a 286-line hook into $turnState atom + turnController
singleton. createGatewayEventHandler becomes a typed dispatch over the
controller; its ctx shrinks from 30 fields to 5. Event-handler refs and 16
threaded actions are gone.
Fold three createSlash*Handler factories into a data-driven SlashCommand[]
registry under slash/commands/{core,session,ops}.ts. Aliases are data;
findSlashCommand does name+alias lookup. Shared guarded/guardedErr combinator
in slash/guarded.ts.
Split constants.ts + app/helpers.ts into config/ (timing/limits/env),
content/ (faces/placeholders/hotkeys/verbs/charms/fortunes), domain/ (roles/
details/messages/paths/slash/viewport/usage), protocol/ (interpolation/paste).
Type every RPC response in gatewayTypes.ts (26 new interfaces); drop all
`(r: any)` across slash + main app.
Shrink useMainApp from 1216 -> 646 lines by extracting useSessionLifecycle,
useSubmission, useConfigSync. Add <Fg> themed primitive and strip ~50
`as any` color casts.
Tests: 50 passing. Build + type-check clean.