Skip to content
nikcli
Docs
Documentation

TUI

Terminal UI entrypoint, worker RPC, OpenTUI renderer, command registry, routes, event streams, and remote control wiring.

Overview

The nikcli terminal UI is the default command registered by packages/nikcli/src/cli/cmd/tui/thread.ts. It is not only a renderer. The TUI is a split runtime: the main thread owns terminal input, OpenTUI rendering, dialogs, routes, and local process lifecycle, while a Bun worker owns the server app, project instances, SDK fetches, event streams, upgrades, and cleanup.

$0 [project]

Default route

The bare nikcli command starts the TUI, resolves the working directory, validates any requested session, and passes initial args into the renderer.

Bun Worker

Worker RPC

The worker exposes fetch, server startup, event subscription, reload, upgrade, and shutdown methods through Rpc.client.

Solid

OpenTUI renderer

app.tsx builds the Solid provider tree, command registry, route switch, dialogs, plugin routes, and keyboard handling.

@nikcli-ai/sdk/httpapi

SDK surface

The UI talks to the same Hono app as external clients, either through in-process fetch or through an HTTP server when network access is enabled.

Visual tour

The captures on this page come from two sources: a recorded bun run --cwd packages/nikcli dev session run through a terminal-control PTY at 112×34 with the OpenTUI host profile, and live in-session dialogs opened during real workflows. Bundles, raw .termctrl recordings, accessibility trees, SVGs, PNGs and manifests live under artifacts/tui/tui-check/; the same skill powers the terminal-control docs.

Core shell

Empty home, slash-command autocomplete, and command palette — the surfaces reached before submitting a prompt.

In-app surfaces

Full-screen dialogs that the TUI renders on top of the shell: context-usage telemetry and analytics with KPIs, charts, gauges, and operational health badges.

Workspace view

The workspace route (type: "workspace", defined in context/route.tsx) is a four-tab panel that overlays any session. Each tab is registered as a hidden command in app.tsx (lines 714–787) and reachable from the command palette or as a slash command.

TabSlashPurpose
Sessions/treeSession tree with focus and timeline: shows every session for the active workspace, marks the focused session, and supports the delegated-task hierarchy (root, subagents, plan/decision points).
Changes/changesGit diff with stage/unstage, file and hunk navigation, commit, push, and PR publish.
Graph/graph · /gitgraph · /commitsCommit graph with log search, branch/lane navigation, and one-key checkout or reset.
GitHub/github · /ghGitHub integration: PR list, create/merge PR, run workflow, branch protection status.
Reproducible captures

Re-run the recorded shell capture with bun run --cwd packages/terminal-control control -- start tui-check --host opentui --cols 112 --rows 34 --record /tmp/tui-check.termctrl -- env NIKCLI_TERMINAL=1 bun run --cwd /Volumes/.../packages/nikcli dev, then re-bundle per marker: --at-marker home|slash-menu|palette --result passed ....

Default command

TuiThreadCommand is registered in packages/nikcli/src/index.ts before the one-shot run command. The command name is $0 [project], which makes it the default yargs route. It accepts project path, model, continue, session, prompt, and agent options, plus shared network flags from withNetworkOptions.

InputRuntime effectSource
[project]Resolved by resolveThreadDirectory; relative paths are based on PWD so shell --cwd behavior stays intact.thread.ts
--modelForwarded into the TUI args context as the initial provider/model selection.thread.ts, context/args
--continueSignals that the UI should continue the latest session instead of starting from an empty home route.thread.ts, session route code
--sessionValidated with Session.ID.safeParse and sdk.session.get before the renderer starts.validateSession
--promptCombined with piped stdin when stdin is not interactive, then passed into ArgsProvider.thread.ts
--agentForwarded into the args context so session startup can select the requested agent.thread.ts
--port, --hostname, --mdnsSwitch the TUI from in-process RPC transport to a worker-hosted HTTP server.resolveNetworkOptions
PTY detection matters

Mobile and nikcli-managed PTYs set NIKCLI_TERMINAL=1. The TUI treats that as an interactive terminal even if Bun reports stdin.isTTY as false, avoiding the piped-stdin path that would otherwise block renderer startup.

Worker model

The main process chooses NIKCLI_WORKER_PATH, the built worker file, or the local TypeScript worker source. It then starts a Bun Worker with a sanitized environment where Process.ROLE_ENV is worker and a run id is guaranteed. The worker calls Process.ensureMetadata("worker"), initializes logging, listens for global bus events, and exposes an RPC object.

RPC methodPurposeOperational detail
fetchRuns SDK requests against Server.App().fetch.Adds Basic Auth from server flags when configured and returns status, headers, and text body.
serverStarts or restarts a Bun Hono server from the worker.Stops the old server before replacing it; returns the concrete URL.
subscribeStarts an event stream for a project directory.Creates an SDK client against http://nikcli.local using worker-local fetch and emits each event back through RPC.
unsubscribeStops a worker event stream.Aborts the matching stream controller and removes it from the stream map.
checkUpgradeRuns passive upgrade checks after the renderer starts.Executes inside Instance.provide for the current directory.
upgradeNowRuns an explicit upgrade requested by the TUI.Delegates to upgradeNow(method, version) inside the project instance.
reloadDisposes all project instances after SIGUSR2.Used by development reload flows.
shutdownStops event streams, disposes instances, and stops the HTTP server.The main thread waits up to five seconds before terminating the worker.
Shutdown is explicit

The TUI does not rely on worker process exit for cleanup. onExit calls worker shutdown, onBeforeExit disposes TUI plugin runtime state, and the main thread terminates the worker only after the RPC shutdown path returns or times out.

Transport modes

The TUI has two runtime transports. Both use the generated SDK surface, but they differ in how requests and events reach the server app.

ModeWhen usedFetch pathEvent path
Direct worker transportNo explicit port, hostname, mdns, or non-local configured server values.createWorkerFetch forwards the request over RPC to worker.fetch.createEventSource subscribes through worker RPC and receives event emissions.
Worker HTTP serverNetwork options request an externally reachable server.The SDK uses normal fetch against the returned HTTP URL.SDKProvider opens server SSE through sdk.event.subscribe.
Server on demand

Even when the TUI starts in direct mode, the session UI can later call the startServer callback. That asks the worker to bind a local server on port 0 and returns the URL for local share or preview flows.

Renderer tree

packages/tui/src/app.tsx creates an OpenTUI renderer with a 45 FPS target, Kitty keyboard support, mouse support from TUI config, copy selection handling, and explicit Ctrl-C behavior. The Solid app is mounted inside an error boundary and a deep provider stack.

Provider groupPurpose
ArgsProvider, ExitProvider, ServerProviderExpose command args, restart/exit hooks, server startup, and plugin disposal on exit.
SDKProvider, ProjectProvider, SyncProviderCreate the SDK client, subscribe to project/server events, and keep synchronized runtime state.
AnalyticsProvider, KVProvider, LocalProviderExpose usage, local preferences, model favorites, and small local state to routes and dialogs.
ThemeProvider, KeybindProvider, AttentionProviderApply terminal palette mode, user keybinds, and attention/notification handling.
DialogProvider, CommandProvider, ToastProviderManage dialogs, slash commands, keyboard command triggers, and toast rendering.
PromptHistoryProvider, PromptStashProvider, PromptRefProviderPower prompt history, frecency, stash, and imperative prompt operations.
EditorContextProvider, UpgradeProviderExpose editor integration state and interactive upgrade actions.

Feature plugins

Almost every surface the TUI shows — home tips, sidebar panels, the plugin manager, the wallpaper, the loop and mission managers — is a self-contained feature plugin under packages/tui/src/feature-plugins/, loaded through the same runtime that loads third-party plugins from npm. The larger ones follow the same layout: store.ts for reactive state, sdk.ts for typed SDK bindings, runner.ts for the local control plane, and dialogs.tsx for the Solid components.

PluginPurpose
homeRotating tip line in the home.bottom slot, plus the tips.toggle command.
sidebarSix independent slot plugins: context/cost, MCP, LSP, todo, modified files, and the footer.
systemThe plugin manager (list, toggle, install) and the OpenRouter Fusion preset manager.
backgroundPaints a real image behind the TUI via OpenTUI's native half-block super-sampler; owns /background.
loopsLoops manager, creation wizard and live sidebar. Mirrors the loop SDK contract.
missionMissions manager, planning wizard and live sidebar. Mirrors the mission SDK contract.
brainArms the Brain consolidation scheduler and registers /brain for an on-demand pass.
browserBrowser-use control surface for the browser tool (page navigation, snapshots, action log).
chatbotChat SDK bots — start/stop per platform and show the webhook path each one must be pointed at.
computerProbes the host and reports what the computer tool can do on this machine.
connectorsLive connector health, cache invalidation, and stored-credential removal inside the TUI.
observabilityLive OpenTelemetry spans and OTLP endpoint dialogs (see Observability).
islandToggles the macOS Island notch integration for the current TUI session. Registered only under --island. See Island plugin below.

Each plugin's commands, slash aliases, KV keys and internals are documented in TUI feature plugins.

TUI plugin runtime

Plugins register commands, routes, dialogs, keybinds, and sidebar / status slots through packages/tui/src/plugin/api.tsx. Runtime cleanup runs on exit through TuiPluginRuntime.dispose() — plugins should not rely on process exit for teardown.

Dialogs

All dialogs live in packages/tui/src/component/ and are registered through the DialogProvider and the command palette. The full list (in addition to those in the top-level README):

DialogSourcePurpose
dialog-telemetry-livedialog-telemetry-live.tsxScrolling, filterable list of live OpenTelemetry spans (see Observability).
dialog-opentelemetrydialog-opentelemetry.tsxOTLP configuration dialog: shows the resolved endpoint, the resource block, and a "Copy as env" action.
dialog-advisor-modeldialog-advisor-model.tsxPick the model used by the advisor tool.
dialog-browser-usedialog-browser-use.tsxBrowser-use control surface: live snapshot, navigation, action log for the browser tool.
dialog-routinedialog-routine.tsxRoutine create/edit dialog backed by the routine SDK.
dialog-session-warpdialog-session-warp.tsxQuick-jump to another session by fuzzy match.
dialog-session-renamedialog-session-rename.tsxRename the active session.
dialog-session-delete-faileddialog-session-delete-failed.tsxConfirmation when a session delete failed and the local cache is stale.
dialog-mobile-connectdialog-mobile-connect.tsxShow a pairing QR code and bearer token, plus copy-to-clipboard, from inside the TUI.
dialog-syncdialog-sync.tsxSync status, hub URL/token config, autostart toggle, and reconnect.
dialog-teleportdialog-teleport.tsxPick a session and remote target, then ship the session through nikcli teleport.
dialog-workspace-createdialog-workspace-create.tsxCreate a new multi-project workspace.
dialog-workspace-listdialog-workspace-list.tsxSwitch between workspaces.
dialog-workspace-unavailabledialog-workspace-unavailable.tsxShow the error that prevents entering a workspace.
dialog-workspace-file-changesdialog-workspace-file-changes.tsxAggregate file changes across the workspace's projects.
plugin-route-missingplugin-route-missing.tsxRendered when a TUI plugin navigates to a route id that is not registered. Offers a "go home" action instead of crashing.

Routes

The TUI route state lives in context/route.tsx. It is a Solid store initialized from NIKCLI_ROUTE when present, otherwise from the home route. Route changes are in-memory UI navigation, not browser history.

Route typeFieldsUsed for
homeinitialPrompt, workspaceIDDefault landing route and empty prompt state.
sessionsessionID, initialPrompt, workspaceIDMain transcript and prompt view for one session.
workspacetab, sessionID, workspaceIDUnified workspace panel with tree, changes, graph, and GitHub tabs.
tree, changes, git-graph, githubsessionID, workspaceIDLegacy route shapes redirected into the workspace view when needed.
pluginid, data, workspaceIDTUI plugin routes registered through the plugin runtime.

The app keeps a mutable plugin route map and a reactive route key. When plugin routes change, the key is bumped so the route switch re-renders. Missing plugin routes render PluginRouteMissing with a home navigation action instead of crashing the TUI.

Commands and keybinds

CommandProvider owns the command registry. Components register command options with titles, descriptions, optional keybind names, optional slash aliases, categories, visibility, and an onSelect callback. The registry deduplicates by category plus value, merges local and global registrations, and exposes command palette, slash, and trigger APIs.

MechanismBehavior
Command paletteThe configured command_list keybind opens a DialogSelect of visible commands.
Direct keybindsThe provider listens to keyboard events, skips disabled commands and active dialogs, and calls the matching command callback.
Slash commandsVisible command options with slash metadata become prompt slash entries with aliases.
Programmatic triggercommand.trigger(name) runs the matching command; TUI HTTP events use this path.
Suspensionkeybinds(false) increments a suspend counter so dialogs or text inputs can temporarily suppress command shortcuts.
Permission mode keybindThe permission_mode command opens DialogPermissionMode — see Permission modes. It applies the selected preset (require_approval / approve_for_me / full_access) to the current primary agent.
Session dialogsContext usage (/usage or /context) opens DialogUsage; Analytics (/analytics or /stats) opens DialogAnalytics. See Dialogs for screenshots.

Dialogs

The TUI ships a few session-scoped dialogs that are useful to keep open while running long work. They are reachable from the command palette and as slash commands.

Context usage

DialogUsage (app.tsx line 1074) shows how the active session is spending its 1.0 M-token context window. The dialog breaks the window into used / free / reserved slices, plots a tokens-per-turn sparkline over the last 77 turns, surfaces the top token sources (system prompt, tools, skills, conversation, plus per-tool contributions from opentui, terminal-control, generate_image, browser, etc.), and ends with a four-card Context health strip (Usage, Cache, Headroom, Fragmentation).

Context usage dialog

Analytics

DialogAnalytics (app.tsx line 1086) is a 6-tab dashboard loaded through AnalyticsProvider against the host's /analytics/* HTTP endpoints. The Overview tab shows sessions, messages, cost, tokens with deltas vs. the prior window, the last-30-day token and cost histograms, the usage-composition stack (input / output / cache / reasoning), and the four-card Operational health strip (Activity, Tools, Background, Workspaces). Tabs 2 Tokens, 3 Models, 4 Tools, 5 Projects, 6 Sessions drill into each slice; cycle with tab or 1–6.

Analytics overview tab

Event flow

SDKProvider creates a generated SDK client and an event emitter keyed by event type. It batches event delivery inside Solid batch calls, flushing immediately unless another flush happened in the last 16 milliseconds.

  1. Direct mode passes a custom EventSource from thread.ts; the worker opens the SDK event stream and emits events over RPC.
  2. HTTP mode calls sdk.event.subscribe directly and retries with exponential backoff from 250 ms to 5 seconds.
  3. Queueing batches received events to keep bursty server updates from forcing one render per event.
  4. TUI commands listen for tui.command.execute, tui.toast.show, and tui.session.select events and map them to local UI actions.
  5. Cleanup aborts the SDK signal, active SSE controller, and pending flush timer on provider unmount.
TUI eventPayloadLocal action
tui.prompt.appendtextAppend text to the active prompt.
tui.command.executecommandTrigger a registered command such as session list, submit prompt, page up, compact, or agent cycle.
tui.toast.showtitle, message, variant, durationRender an info, success, warning, or error toast.
tui.session.selectsessionIDNavigate to the requested session after the server validates it exists.
Workspace event catch-up

The session sync layer replays missed events for every workspace when the TUI starts or reconnects, so per-project state is identical to the server at render time.

Island plugin

The Island plugin is the CLI-side mirror of the macOS NikcliIsland.app notch UI. It writes a per-session JSON snapshot under ~/Library/Application Support/NikcliIsland/state.d/ on every relevant event, and the native app polls that directory to render live session state without keeping a separate IPC channel.

The bridge lives in @nikcli-ai/util/island-bridge and is self-activating: Bus.publish itself calls start() on first publish (idempotent, no-op after the first real call). That matters because the TUI's real session / bus activity runs inside a Worker thread with isolated module state — a listener registered only in the parent process would never see those events.

StateSourceDisplay
idlesession.status → idleEmpty pill on the notch.
thinkingsession.status → busy / retry"Thinking…" or "Retrying (N)…".
toolmessage.part.updated tool statusFriendly label plus file basename, lingers for ~1.9 s after completion.
permissionpermission.asked"Awaiting permission" + the request id for one-tap approval.
errorsession.error"Error" pill.
done(reserved)Used by the native app to mark a completed row before archival.

Each snapshot also carries the parent session id, agent title, project, cwd, the process pid, the local server port (when one is running), and a server URL — enough for the native app to call POST /permission/:requestID/reply directly when you approve a pending request from the notch.

Terminal
bash
# Inspect Island state
cat ~/Library/Application\ Support/NikcliIsland/state.d/nikcli-ses_abc.json

# Disable Island for this run without uninstalling the app
NIKCLI_ISLAND_DISABLE=1 nikcli
macOS only

Island is a no-op on Windows and Linux: the native app is a macOS-only notch UI. The NIKCLI_ISLAND_TEST_FORCE_DARWIN=1 flag exists only for the bridge's own test suite — never set it in production.

Toggling from inside the TUI

The Island feature plugin's activate / deactivate calls IslandBridge.setEnabled() — toggling off clears the current snapshots and stops writing, toggling back on resumes through the already-registered listener without any race window.

HTTP control routes

packages/nikcli/src/server/httpapi/tui.ts exposes routes that remote clients, companion surfaces, and automation can use to nudge the running TUI. Most routes publish a typed TuiEvent onto the bus. /tui/control is a queue bridge for request/response handoff.

RoutePublishes or returnsUse
POST /tui/append-promptTuiEvent.PromptAppendAppend text into the active prompt input.
POST /tui/open-helphelp.showOpen the help dialog.
POST /tui/open-sessionssession.listOpen session selection.
POST /tui/open-themestheme.switchOpen theme selection.
POST /tui/open-modelsmodel.listOpen model selection.
POST /tui/submit-promptprompt.submitSubmit the current prompt.
POST /tui/clear-promptprompt.clearClear prompt input.
POST /tui/execute-commandMapped command nameTranslate legacy remote names such as agent_cycle into current command ids.
POST /tui/show-toastTuiEvent.ToastShowShow a notification in the terminal.
POST /tui/publishAny typed TuiEventGeneric event publish endpoint with zod validation.
POST /tui/select-sessionTuiEvent.SessionSelectValidate a session through Session.Service, then navigate the TUI.
GET /tui/control/nextQueued TUI requestPull the next queued control request.
POST /tui/control/responsetruePush the response for a pending queued request.

Config and plugins

TUI-specific configuration is loaded by packages/nikcli/src/config/tui.ts. The loader reads global config directory files, NIKCLI_TUI_CONFIG, project TUI files, .nikcli config directories, and managed config. It migrates old TUI config, flattens nested tui objects, parses keybinds, deduplicates plugin specs, and records plugin metadata with local or global scope.

Config areaRuntime use
mouseControls OpenTUI mouse support; defaults to enabled.
keybindsParsed through shared config keybind parsing and consumed by KeybindProvider.
scroll_speed, scroll_accelerationConsumed by session route scroll logic.
diff_styleUsed by session permission and diff rendering components.
pluginFeeds TuiPluginRuntime.init through createTuiApi.
plugin_metaRecords whether each plugin came from local or global config and the source file.

The TUI plugin API can register commands, plugin routes, and UI contributions. Runtime cleanup is tied to exit through TuiPluginRuntime.dispose(), so plugins should not depend on process exit for teardown.

Remote and attach

The TUI can either host its own local server for external access or attach to an existing server. nikcli attach <url> calls the same tui renderer with the provided URL instead of starting a local worker server. Remote Control uses TUI commands and server routes to drive the active prompt, sessions, scrolling, compacting, sharing, and interruption flows.

Terminal
bash
nikcli --port 4096 --hostname 127.0.0.1
nikcli attach http://127.0.0.1:4096
curl -X POST http://127.0.0.1:4096/tui/open-sessions
curl -X POST http://127.0.0.1:4096/tui/execute-command -H 'content-type: application/json' -d '{"command":"agent_cycle"}'
Companion is separate

nikcli companion serve and nikcli companion open start the desktop browser companion surface. It connects to the same server/event model, but it is not the terminal renderer and it is distinct from the mobile host.

Operational checks

Use these checks when debugging TUI startup, remote control, or event delivery.

Terminal
bash
NIKCLI_TERMINAL=1 bun run dev
nikcli --print-logs --log-level DEBUG
nikcli --port 0 --hostname 127.0.0.1
nikcli attach http://127.0.0.1:4096
curl -i http://127.0.0.1:4096/global
curl -i -X POST http://127.0.0.1:4096/tui/open-help
CheckWhat it proves
Start without network flagsDirect worker fetch and worker event subscription work.
Start with --portWorker server startup, HTTP fetch, and server-side SSE work.
Call /tui/open-helpHTTP route can publish a TUI command event and the renderer is subscribed.
Call /globalServer auth, request middleware, and basic SDK reachability are healthy.
Use SIGUSR2Worker reload disposes project instances without restarting the renderer process.

Failure modes

SymptomLikely causeFix
TUI hangs before drawingBun reports non-TTY stdin and no NIKCLI_TERMINAL=1, so the command is waiting for piped stdin.Run from an interactive terminal, set NIKCLI_TERMINAL=1 for managed PTYs, or provide stdin intentionally.
Session id rejected before UI starts--session failed schema parsing or sdk.session.get returned an error.Use a valid ses... id from nikcli session list and confirm the data root/project matches.
Remote button does nothingThe TUI is in direct mode and no local HTTP server has been started yet, or the server callback failed.Start with --port or trigger the on-demand local server path and inspect worker logs.
HTTP TUI route returns true but UI does not changeThe server published the event, but the renderer SDK event subscription is disconnected.Check SSE reconnect logs, server auth flags, and whether the TUI is attached to the same server URL.
Keyboard shortcut ignoredA dialog or text input has suspended command keybinds, or the command is disabled for the current route.Close the dialog, check route-specific command registration, and inspect TUI keybind config.
Plugin route missingA plugin navigated to a route id that is not registered in the current plugin route map.Verify the TUI plugin was loaded from config and registered its route before navigation.
Worker does not exit cleanlyA server, event stream, or instance finalizer is still shutting down after the five-second timeout.Inspect worker logs and confirm server/event streams stop through shutdown.

Source anchors

FileWhat to inspect
packages/nikcli/src/cli/cmd/tui/thread.tsDefault command, worker startup, network mode selection, session validation, prompt input, restart, and shutdown.
packages/nikcli/src/cli/cmd/tui/worker.tsRPC server, worker fetch, event stream bridge, server startup, reload, upgrade, cleanup, and IslandBridge.stop().
packages/tui/src/app.tsxOpenTUI renderer config, provider tree, route rendering, dialogs, command registration, plugin runtime, and event handlers.
packages/tui/src/context/sdk.tsxSDK client creation, direct event source support, SSE retry loop, event batching, and cleanup.
packages/tui/src/context/route.tsxRoute shapes, route store initialization from NIKCLI_ROUTE, navigation, and override provider.
packages/tui/src/context/remote-sync.tsxRemote sync state and stats used by the sync dialog.
packages/tui/src/component/dialog-command.tsxCommand registry, slash command projection, keybind handling, suspension, and palette dialog.
packages/tui/src/component/dialog-sync.tsxSync dialog: URL / token config, autostart toggle, reconnect.
packages/tui/src/component/dialog-teleport.tsxTeleport dialog: pick a session and remote target, then ship it.
packages/tui/src/component/dialog-mobile-connect.tsxPairing QR + bearer token from inside the TUI.
packages/tui/src/component/dialog-browser-use.tsxBrowser-use control surface for the browser tool.
packages/tui/src/event.tsTyped TUI bus events and zod payload schemas.
packages/nikcli/src/server/httpapi/tui.tsRemote TUI control HTTP routes and queue bridge.
packages/nikcli/src/config/tui.tsTUI config loading, migration, keybind parsing, plugin deduplication, and plugin metadata.
packages/nikcli/src/cli/cmd/tui/attach.tsAttach command path for connecting the renderer to an existing server URL.
@nikcli-ai/util/island-bridgeIsland file-contract writer, setEnabled, and shutdown semantics.
packages/tui/src/feature-plugins/island/index.tsxTUI-side Island plugin (activate / deactivate the bridge).