A terminal-native cockpit for coding agents. Bring your own CLI, run them anywhere.
Unlike agent command centers that wrap a single model behind their own chat UI, kolu stays out of the agent's way: the terminal is the universal interface, so claude, opencode, or whatever ships next week works out of the box — and you can drop to a plain shell whenever you want. It's an Agentic Development Environment (ADE) that treats terminals as the thesis, not the substrate.
Two principles shape what kolu is and isn't:
Agent-agnostic. The terminal is the universal interface. Kolu doesn't wrap a specific model or lock you into one CLI — claude, opencode, or whatever ships next week all work the same way, because they're just programs you run in a shell. There's no agent registry to update, no adapter to write, no vendor lock-in. Any new agent CLI picks up first-class features automatically: run it once in any kolu terminal and the next time you create a worktree, it appears in the sub-palette as a launch option — no configuration, no per-agent code. You can always drop to a plain shell without leaving the app.
Auto-detected, zero setup. Kolu populates its UI by watching what you already do — the repos you cd into, the agents you run, the sessions you save — not by asking you to configure it. Recent repos track cd events, branch / PR / CI status derive from the terminal's CWD, Claude Code state is read from the foreground pid, and recent agent CLIs come from preexec command marks emitted by kolu's shell integration. If kolu knows something, it's because the shell already told it. The surface grows with your workflow, not with a preferences pane.
Install Nix and then run:
nix run github:juspay/kolu # serve on 127.0.0.1:7681
nix run github:juspay/kolu -- --host 0.0.0.0 --port 8080 # expose on LANOpen http://127.0.0.1:7681 (or the address you chose above).
- Create, switch, and kill terminals — every terminal renders as a draggable tile on the canvas, with a floating two-level pill tree (repo → branches) for at-a-glance navigation
- Split terminals — Ctrl+` splits a bottom pane per terminal; Ctrl+Shift+` adds tabs, Ctrl+PageDown / Ctrl+PageUp cycles
- Font zoom (Cmd/Ctrl +/-), persisted per terminal across sessions
- WebGL rendering with canvas fallback, clickable URLs, Unicode 11, inline images (sixel, iTerm2, kitty)
- Lazy attach — late-joining clients receive serialized screen state (~4KB) instead of replaying raw buffer
- Mobile key bar — on coarse-pointer devices, a thin row above the terminal sends the keys soft keyboards lack (Esc, Tab, arrows, Ctrl+C) plus an IME-bypassing Enter for Android chat keyboards, with a haptic tick on every tap. Touch-swipe inside the terminal scrolls the scrollback buffer
- Command palette (Cmd/Ctrl+K) — search terminals, switch themes, run actions
- Agent-aware command palette — once you've run a known agent CLI (
claude,aider,opencode,codex,goose,gemini,cursor-agent) in any kolu terminal, it surfaces in two places: underNew terminal → <recent repo>as a sub-palette that creates the worktree and launches the agent in one step, and underDebug → Recent agentsas a prefill-into-active-terminal affordance. Prompt/message flag values (-p/--prompt/-m/--message) are stripped before storage so ephemeral prompt text never lands in the persisted MRU - Pill-tree pings — when an agent is waiting on you (or has finished with an unread completion), its branch pill in the floating tree pulses an alert dot so you can spot it without panning. Ctrl+Tab (or Alt+Tab) cycles terminals in MRU order: hold the modifier, press Tab to advance, release to commit
- Keyboard-driven — Cmd+T new terminal, Cmd+1…Cmd+9 jump, Cmd+Shift+[ / Cmd+Shift+] cycle, Cmd+/ shortcuts help
The desktop workspace is mode-less — every terminal renders as a draggable, resizable tile on an infinite 2D canvas. Per-terminal chrome (theme pill, agent indicator, screenshot, split toggle, find) lives on each tile's title bar. A transparent chrome bar floats at the top carrying logo, command palette, settings, and inspector toggle; the canvas grid reads through it. When a tile is maximized or the inspector panel is open, the chrome bar docks above so the two surfaces don't fight.
- Infinite pan & zoom — two-finger scroll / trackpad to pan, pinch or Ctrl+scroll to zoom. Hold Shift to force pan even with the cursor over a terminal tile (hand-tool style). No boundaries — the canvas extends freely in every direction via CSS
transform: translate() scale()(Figma/Excalidraw model) - Snap-to-grid — tiles snap to a 24px grid on drag and resize for tidy layouts
- Maximize a tile — double-click any tile's title bar (or click the maximize button) to fill the viewport; the maximized posture persists across reload via localStorage so you land back where you left off
- Floating pill tree — a two-level overlay (repo → branches) sits at the top of the canvas, ghosted at rest and behind any tile that overlaps it; hover pops it to full opacity. Branches sort by canvas x-position, so the tree reads left-to-right exactly as the tiles sit. Click a branch pill to pan and center its tile
- Pill border encodes state — each pill's border doubles as identity (repo color) and live status: a conic-gradient sweep while the agent is
thinking/tool_use, a breathing pulse whilewaiting, a static ring when the terminal is just active, and an inset glow when the active tile also has a working agent - Identity-collision suffix — when two terminals share the same repo+branch (or cwd, for non-git), the server assigns each a stable 4-char id suffix (
#a3f2) so the pill tree and tile chrome can disambiguate them at a glance - Keyboard navigation — Cmd/Ctrl+Shift+2 centers on the active tile
- Per-tile theming — title bars and pill swatches derive their colors from each terminal's theme for guaranteed contrast
- Mobile — the canvas, pan/zoom, and the floating pill tree are all disabled; the active tile fills the viewport and swipe-left/right cycles between terminals in pill-tree order. A pull-down chrome sheet at the top reveals the same logo + pill list + controls as a touch-sized drawer
- Auto-detected repo name, branch, and working directory (via OSC 7 +
.git/HEADwatcher) - GitHub PR detection — shows PR number, title, and CI check status (pass/pending/fail) on the tile chrome and inspector
- Per-repo color coding on the pill tree and tile chrome via golden-angle hue spacing
Detects Claude Code sessions running in any terminal and surfaces their state on the tile's chrome (and on its pill in the floating tree).
What we detect:
| State | Indicator | Meaning |
|---|---|---|
| Thinking | Pulsing accent dot | API call in flight — Claude is generating a response |
| Tool use | Pulsing yellow dot | Claude is executing tools or waiting for permission |
| Waiting | Dim dot | Claude finished responding, waiting for user input |
How it works: asks each terminal for its current foreground process pid via tcgetpgrp(fd) (exposed by node-pty's foregroundPid accessor), then checks whether ~/.claude/sessions/<fgpid>.json exists. If it does, that terminal is running claude-code — we tail the session's JSONL transcript to derive state from the last message. Cross-platform (Linux + macOS) since tcgetpgrp is POSIX. Each card also surfaces the session's display title (custom title › auto-generated summary › first prompt) via the Claude Agent SDK's getSessionInfo(), refreshed best-effort on each transcript change. The tile chrome also shows a running token count (compact, e.g. 47K) summed from the latest assistant entry's message.usage — input_tokens + cache_creation_input_tokens + cache_read_input_tokens. Raw count only; window size isn't inferable from the JSONL (1M beta strips its suffix, so a % would lie), and the raw number is the useful signal anyway.
What we can't detect:
- Permission prompts vs tool execution — both show as "tool use" since the JSONL doesn't distinguish them
- Streaming progress — intermediate thinking tokens aren't tracked, only final state transitions
- Wrapped invocations — if claude-code is launched via a wrapper (e.g.
script -q out.log claude), the foreground pid is the wrapper, not claude itself, so the session lookup misses - Sub-agents — nested agent spawns appear as tool use, not as separate tracked sessions
Debugging detection: the command palette has a Debug → Show Claude transcript entry (visible only when the active terminal has a Claude session) that opens a side-by-side view of the server's state-change log next to the raw JSONL events from disk since monitoring began. Use it when state seems stuck or transitions feel missed.
Detects Codex TUI sessions and surfaces their state alongside Claude Code and OpenCode.
How it works: when the foreground process is codex, the provider queries Codex's threads SQLite DB (highest-numbered ~/.codex/state_<N>.sqlite, auto-discovered on startup) to find the most recently updated non-archived cli thread whose cwd matches the terminal's CWD. Thread metadata (title, model) comes straight from indexed columns. The running context-token count and the agent state (thinking / tool*use / waiting) both come from tailing the per-thread rollout JSONL at threads.rollout_path: state from pattern-matching task_started / task_complete / function_call / function_call_output events, and contextTokens from reading info.last_token_usage.input_tokens on the latest token_count event — the same number Codex's own /status command displays. Live updates come from fs.watch on the SQLite WAL file (state*<N>.sqlite-wal); Codex writes the WAL and appends the JSONL in the same cycle (verified: nanosecond-identical mtimes), so one signal covers both sources.
Why last_token_usage.input_tokens alone, not a sum? Claude-code sums three input-side fields (input_tokens + cache_creation + cache_read) because Anthropic's schema makes them disjoint buckets. OpenAI's schema — which Codex emits — is different: input_tokens is already the full prompt, and cached_input_tokens is a breakdown of what portion was a cache hit, not an additional count. Adding them double-counts every cache re-read. The field as-is is what /status reports and what gives users an accurate read on context pressure.
Why not threads.tokens_used? That column holds the session-lifetime cumulative total (total_token_usage.total_tokens summed across every turn). For long-running sessions it climbs into tens of millions — misleading as "how close am I to exhausting the 258 K context window."
Why both SQLite and JSONL? SQLite alone gives us title and model cheaply from indexed columns — no file read, no parsing. JSONL alone would force re-parsing the entire file on every update to recover title. Neither source alone carries both the event stream that drives state AND the per-turn token usage AND the columns that drive metadata — combining them specializes by purpose.
What we detect:
| State | Indicator | How |
|---|---|---|
| Thinking | Pulsing accent dot | Latest lifecycle event is task_started, with no open function_call scoped to the current turn |
| Tool use | Spinning yellow | Latest lifecycle event is task_started, with at least one function_call opened since that task_started and no matching _output yet |
| Waiting | Dim dot | Latest lifecycle event is task_complete |
Open-call tracking is scoped per-turn: a function_call with no matching _output that straddles a task_started boundary (user aborted a prior tool-using turn) does not pin the next turn to tool_use.
What we can't detect (yet):
- Task-progress checklist — Codex has no TodoWrite equivalent (
task_started/task_completeare per-turn lifecycle, not user-visible checklists), so thetaskProgressfield stays permanently null - Column-level schema changes — the filename version is auto-discovered, but if upstream renames or removes a depended-on column in the
threadstable (e.g.rollout_path,cwd,source,archived), session detection silently returns zero matches. SetKOLU_CODEX_DBto pin a known-good DB while a fix ships - Same-directory disambiguation — if multiple Codex threads share a cwd, we pick the most recently updated one (same heuristic as OpenCode)
- Sub-agent threads — Codex's spawned sub-agents get
source = '{"subagent":…}'rows in the same table; we filter them out (they have no foreground terminal to bind to)
Detects OpenCode sessions and shows their state alongside Claude Code on the tile chrome.
How it works: when the foreground process is opencode, the provider queries OpenCode's SQLite database directly at ~/.local/share/opencode/opencode.db to find the most recently updated session whose directory matches the terminal's CWD. State is derived from the latest message: a user message means the assistant is thinking; an assistant message with time.completed set and finish: "stop" means waiting; otherwise still thinking. Todo progress comes from a COUNT(*) over the todo table — much simpler than Claude Code's tool-call parsing since OpenCode stores todos as first-class rows with a status column. The tile chrome also shows the running token count from the latest assistant message's tokens.total (pre-summed by OpenCode — we pass it through). Live updates come from fs.watch on the SQLite WAL file (opencode.db-wal), which OpenCode writes to on every database mutation.
Why SQLite, not REST? The OpenCode TUI doesn't expose an HTTP server by default — that's a separate opencode serve mode. Reading the SQLite DB directly works against the actual TUI users run, with no port discovery and no extra processes. SQLite WAL mode allows concurrent readers while OpenCode is writing, so we can open the DB read-only without blocking it.
What we detect:
| State | Indicator | How |
|---|---|---|
| Thinking | Pulsing accent dot | Latest assistant message has no time.completed |
| Tool use | Spinning yellow | Thinking + any part with type: "tool" and state.status: "running" |
| Waiting | Dim dot | Latest assistant message has time.completed set and finish: "stop" |
What we can't detect (yet):
- Same-directory disambiguation — if multiple OpenCode sessions share a working directory, we pick the most recently updated one
- Non-default DB location — set
KOLU_OPENCODE_DBto override the path
- 200+ color schemes from iTerm2-Color-Schemes, switchable at runtime
- Live preview while browsing themes in the palette
- Shuffle theme — new terminals (and the active one on ⌘J) get a background perceptually distinct from every other open terminal (toggleable; on by default)
- Dark / light / system UI theme
- Ctrl+V pastes images into any agent that accepts paste-as-file-path (Claude Code, codex, …) — the server saves the browser's clipboard image and bracketed-pastes its path into the PTY
Record the Kolu tab — whole canvas or a single maximized terminal — with microphone and optional webcam PiP, straight to a local .webm file. Chromium-only (uses the File System Access API).
- One-click setup popover — mic picker with live 8-segment RMS level meter, webcam toggle + device picker + circular preview, all in a compact popover anchored to the chrome-bar record button
- Streaming to disk — chunks flow from
MediaRecorderinto aFileSystemWritableFileStreamcontinuously, so memory stays flat regardless of recording length. A partial.webmsurvives a browser crash (recoverable with ffmpeg) - Duration-fix pass — Chrome's
MediaRecorderomits the WebMSegmentInfo.Durationheader in streaming mode, which makes players show a ~1 second duration. At stop, the saved file is read back, patched viafix-webm-duration, and rewritten - Pause / resume — ⌘⇧. toggles; the segmented recording capsule in the chrome bar shifts red → amber and the breathing halo suppresses while paused
- Webcam PiP overlay — when enabled, a circular mirrored
<video>pins to the bottom-right above maximized tiles but below the chrome bar, and is baked into the recording by the tab-capture stream (no offscreen compositing) - Browser picker collapses —
getDisplayMedia({ preferCurrentTab: true, selfBrowserSurface: "include" })turns the multi-surface picker into a single "Share this tab" confirmation
pnpm monorepo:
| Package | Stack |
|---|---|
packages/common/ |
oRPC contract + Zod schemas |
packages/server/ |
Hono + node-pty + @xterm/headless |
packages/client/ |
SolidJS + xterm.js + Tailwind CSS v4 |
packages/integrations/claude-code/ |
Claude Code detection — JSONL transcript tailing + Claude Agent SDK; exports a claudeCodeProvider AgentProvider |
packages/integrations/anyagent/ |
Agent-agnostic shared contract (AgentProvider interface, agentInfoEqual), types (Logger, TaskProgress), and agent CLI parsing |
packages/integrations/codex/ |
Codex detection — reads the highest-numbered ~/.codex/state_<N>.sqlite for thread metadata and tails the matched rollout JSONL for state; exports a codexProvider |
packages/integrations/opencode/ |
OpenCode detection — reads OpenCode's SQLite database via Node's built-in node:sqlite; exports an opencodeProvider AgentProvider |
packages/integrations/git/ |
Pure git operations — simple-git wrapper: repo resolution, worktree lifecycle, diff review, path security; schemas re-exported by kolu-common |
packages/integrations/github/ |
GitHub PR schemas + pure helpers (deriveCheckStatus, classifyGhError, prResultEqual); server wraps with gh pr view spawn via KOLU_GH_BIN |
packages/terminal-themes/ |
Terminal color scheme catalog + perceptual-distance picker — themes checked-in as JSON |
packages/memorable-names/ |
ADJ-NOUN random name generator — word lists checked-in as JSON |
All traffic flows over a single WebSocket (/rpc/ws) via oRPC. The contract in packages/common/ is shared by both sides — types checked at compile time, payloads validated by Zod at runtime. Two communication patterns:
| Pattern | Semantics | Client integration | Used for |
|---|---|---|---|
| Request / response | one-shot RPC call | plain client.* calls |
terminal.create, terminal.kill, terminal.reorder |
| Subscription | server pushes values over WebSocket stream | createSubscription → SolidJS signal |
Terminal list, metadata, server state |
Subscriptions use createSubscription — a 150-line primitive that converts an AsyncIterable into a SolidJS signal via createStore + reconcile for fine-grained reactivity. Per-terminal subscriptions use SolidJS's mapArray for automatic lifecycle management.
Two loops drive the system — a terminal I/O loop (the hot path) and a metadata loop (side-channel enrichment). Both flow over the same WebSocket and land in SolidJS signals on the client via createSubscription.
flowchart TB
subgraph Client["Client (SolidJS)"]
User((User)):::user
Xterm["xterm.js\nrender + input"]:::client
Subs["createSubscription\nsignals"]:::cache
UI["UI components\npill tree · tile chrome · chrome bar · palette"]:::client
end
subgraph Server["Server (Hono)"]
PTY["node-pty\nshell process"]:::server
Headless["@xterm/headless\nscreen state"]:::server
Pub["Publisher\nper-terminal channels"]:::server
Providers["Metadata providers"]:::server
end
%% Terminal I/O loop
User -->|"keystroke"| Xterm
Xterm -->|"sendInput\n(request/response)"| PTY
PTY -->|"shell output"| Headless
PTY -->|"shell output"| Pub
Pub -->|"attach stream"| Xterm
%% Metadata loop
PTY -.->|"OSC 7\n(CWD change)"| Providers
Providers -.->|"metadata stream\n(subscription)"| Subs
Pub -.->|"activity stream\n(subscription)"| Subs
Subs -.-> UI
%% Terminal list (server-pushed on create/kill/reorder)
Pub -.->|"terminal list stream\n(subscription)"| Subs
%% User actions
UI -->|"create · kill · reorder\n(request/response)"| PTY
classDef user fill:#f4a261,stroke:#e76f51,color:#000
classDef client fill:#2a9d8f,stroke:#264653,color:#fff
classDef cache fill:#e76f51,stroke:#f4a261,color:#fff
classDef server fill:#264653,stroke:#2a9d8f,color:#fff
style Client fill:none,stroke:#2a9d8f,stroke-width:2px,color:#2a9d8f
style Server fill:none,stroke:#264653,stroke-width:2px,color:#264653
Terminal I/O (solid lines) — keystrokes go through sendInput RPC to node-pty; shell output flows back through the publisher as an attach stream to xterm.js. An @xterm/headless instance parses VT sequences server-side for screen-state snapshots1.
Metadata (dashed lines) — shell activity triggers a provider DAG: CWD changes (OSC 7) → git provider (.git/HEAD watcher) → GitHub provider (gh pr view polling). Agent detection uses a single generic orchestrator (meta/agent.ts) driven by per-agent AgentProvider instances from each integration package. Today three instances are registered: claudeCodeProvider (from kolu-claude-code) wakes on title events (OSC 2) and its own fs.watch on ~/.claude/sessions/; codexProvider (from kolu-codex) queries the highest-numbered ~/.codex/state_<N>.sqlite for thread metadata and tails the matched rollout JSONL for state transitions; opencodeProvider (from kolu-opencode) queries OpenCode's SQLite database directly and watches its WAL file for live state updates. Adding a new agent CLI is one new AgentProvider and one line in startProviders — no server-side adapter file. All providers feed a single metadata channel streamed to the client as a subscription2. Separately, kolu's preexec hook emits an OSC 633;E command mark before each user command; the pty handler republishes the raw payload on a commandRun channel, and meta/agent-command.ts subscribes to match the first token against a known-agents allowlist and fan out to both (a) a bounded recent-agents MRU for the agent-aware command palette and (b) a per-terminal stash keyed by terminal id, so codex/opencode session detection still matches when the agent is an interpreter shim (e.g. npm-installed codex, whose kernel-level process name is node). No /proc lookups or argv scraping.
User actions — command palette, pill tree, and tile chrome dispatch plain oRPC client calls (useTerminalCrud, useWorktreeOps). The server's live subscriptions push updated state to the client automatically. useTerminalMetadata uses SolidJS's mapArray to create per-terminal subscriptions that automatically tear down when terminals are removed3.
Persistence — sessions auto-save to ~/.config/kolu/state.json via conf, debounced at 500 ms4.
PartySocket handles WebSocket auto-reconnect; the stream namespace in packages/client/src/rpc/rpc.ts routes every async-iterator procedure through oRPC's ClientRetryPlugin so consumers transparently re-subscribe after a drop — every server-side streaming handler is already snapshot-then-deltas and the reducer in useTerminalMetadata.ts pattern-matches an ActivityStreamEvent discriminated union (snapshot replaces, delta appends) so re-subscribe resume is structural, not defensive. Transport events (connecting / connected / disconnected / reconnected / restarted) are exposed as a single ServerLifecycleEvent signal, and TransportOverlay pattern-matches it into one dim-backdrop card: disconnected shows "Reconnecting…" (the backdrop is pointer-events-none, so users can still scroll and read buffers underneath), and restarted swaps to "Server updated" with the Reload button inline in the card.
Packaged with Nix. The flake has zero inputs — nixpkgs and other sources are pinned via npins and imported with fetchTarball to keep nix develop fast (~2.6 s cold). Shared env vars are defined once in koluEnv and consumed by both the build and the devShell5.
Requires Nix with flakes enabled.
nix develop # enter devshell
just dev # run server + client with hot reload
just test # e2e tests (full nix build)just ci builds all flake outputs on x86_64-linux and aarch64-darwin in parallel, runs e2e tests, and posts GitHub commit statuses. See ci/ for details and reuse instructions.
just ci # full CI run
just ci::protect # set branch protection
just ci::_summary # check current statusA home-manager module runs kolu as a systemd user service:
{
imports = [ kolu.homeManagerModules.default ];
services.kolu = {
enable = true;
package = kolu.packages.${system}.default;
host = "127.0.0.1"; # default
port = 7681; # default
};
}See nix/home/example/ for a full configuration with a VM test.
If kolu grows unbounded (V8 heap climbing over hours), set services.kolu.diagnostics.dir to an absolute path. Each restart gets its own timestamped subdir there, with a baseline heap snapshot at T+5min, periodic "diag" stats lines (memory bands + terminals/publisherSize/claudeSessions/pendingSummaryFetches), and automatic near-OOM snapshots via V8's --heapsnapshot-near-heap-limit. kill -USR2 <pid> captures an on-demand snapshot into the same dir. Diff two snapshots offline with memlab to name the retainer. Unset = zero overhead; the code path is fully gated.
The marketing site and blog at https://kolu.dev live in website/ — Astro + Tailwind, its own zero-input flake, deployed to GitHub Pages via .github/workflows/pages.yml.
just website::dev # live preview with HMR
just website::nix-build # reproducible buildSee website/README.md for authoring posts and deploy details.
Named after கோலு, the tradition of arranging figures on tiered steps.
Footnotes
-
~4 KB serialized snapshot instead of replaying the full scrollback buffer. ↩
-
Git provider uses simple-git; GitHub provider derives combined CI status from
CheckRun+StatusContext. Agent providers implement the sharedAgentProvidercontract (anyagent):resolveSession(terminalState)→sessionKey(session)for dedup →createWatcher(session, onChange)for per-session state derivation, with an optionalexternalChanges: { isPresent, install }pair for out-of-band match triggers —installfires at most once per process, lazily, the first time any terminal's state reportsisPresenttrue, so a user who has never run the agent pays zero watcher cost.claudeCodeProviderasks the pty fortcgetpgrp(fd)and stats~/.claude/sessions/<fgpid>.json, opts intofs.watchon~/.claude/sessions/as its external-change signal, then tails the matched session's JSONL transcript via anotherfs.watchfor state updates; the session display title comes from a fire-and-forgetgetSessionInfo()call piggybacking on the same transcript watcher.codexProvidermatches when either the foreground process basename iscodexor the preexec stash namescodex(interpreter-shim fallback), queries Codex's threads SQLite DB (highest-numbered~/.codex/state_<N>.sqlite) filtered tosource = 'cli'for the cwd's latest thread, reads mutable metadata (title, model) from indexed columns, and tails the thread's rollout JSONL (threads.rollout_path) to derive state fromtask_started/task_completeboundaries and openfunction_callcall_ids — one sharedfs.watchon the SQLite WAL file doubles as both the external-change signal and the per-session refresh trigger, since Codex writes the WAL and appends the JSONL atomically.opencodeProvidermatches via the same dual signal (foreground basename or preexec stash), queries~/.local/share/opencode/opencode.db(SQLite) for sessions in the terminal's CWD, and watches the WAL file (opencode.db-wal) for live state updates via Node's built-innode:sqlitemodule — it has no external-change subscription because title events cover every match transition. ↩ -
Local-only view state (active terminal, MRU order, attention flags) lives in SolidJS signals and stores inside singleton
useXxx.tsmodules — separate from server-derived subscription state. ↩ -
Schema is versioned with explicit migrations. Stores CWD, sort order, and parent relationships per terminal. ↩
-
koluEnvincludes font paths and the pinnedghbinary. Terminal themes and word lists ship checked-in as JSON (seepackages/terminal-themes/andpackages/memorable-names/). The final derivation is a wrapper script that sets the environment and execstsx. ↩