via is a native Rust GUI/terminal hybrid that places a running Neovim instance and an AI coding agent (or reviewer) into the same window, using a high-performance terminal emulator surface. The goal is zero-jank navigation and explicit, structured context sharing instead of scraping or fragile PTY injection.
- Unified environment: one programmatically controlled window with split panes.
- Semantic navigation: OSC 8 hyperlinks and scanned references let you Ctrl-click file paths / symbols in agent output and instantly focus the right location in Neovim; external OSC 8 hyperlinks open in the system browser.
- Shared context without manual shuffling: explicit buffer/selection/diagnostics hand-off from editor to agent (and back via links + review flows).
- Low overhead: the coordination layer should be invisible once you're in flow.
Success criteria called out early:
- Clicking a filename in the agent pane focuses Neovim in <50 ms.
- Adding another "agent-like" surface (reviewer, etc.) should only require a new entry in the message router.
Design philosophy: "The editor is the heart, the agent is the brain, and the terminal wrapper is the nervous system."
Orchestration policy: via transports — panes, sockets, ACP sessions, and
the file-backed mailbox — it does not encode multi-agent workflows in Rust.
Agents orchestrate themselves via skills, CLI (via agent), and
(eventually) MCP tools. The primary interactive pane is always a PTY; spawned
helpers use ACP when the configured driver supports it.
+------------------+ Unix socket / RPC +------------------+
| Neovim | <----------------------------------> | via (Rust) |
| (editor pane) | (nvim-rs + embedded Lua bridges) | |
+------------------+ | Mediator |
| (tokio mpsc) |
+------------------+ PTY (normal mode) or | |
| Agent | <----------------------------------> | - editor_state |
| (claude, etc.) | stdio JSON-RPC (ACP mode) | - acp_client |
+------------------+ | - lsp bridge |
+--------+---------+
|
| UiCommand / Event
v
+------------------+
| GhosttyUi |
| (winit + |
| libghostty-vt) |
| |
| - TerminalPanes |
| (editor, agent|
| , review) |
| - layout/split |
| - font + render |
| - link handling |
| - ACP ratatui |
| sub-pane |
+------------------+
The central async coordinator. It owns:
- An
AppContext(user prefs, runtime paths, and launch/attach context). EditorState(current buffer, diagnostics per path, visual selection, LSP client summaries).- Optional
AcpClient(whenVIA_AGENTends withacp). - PTY sessions for agents in normal mode and the review backend.
- Listeners for Neovim RPC (editor events, diagnostics, LSP clients) and the LSP socket bridge.
- Channels to drive the UI (
EventSender,UiCommandreceiver).
Key paths:
- On
BufferSendRequested(from:ViaBufferSend/<leader>ab): always injects@path/@path:start-endinto the primary PTYagentpane — never a spawned ACP helper. Targeted ACP context usesvia agent send --to <id>(or Luarequire('via').agent.send). - Incoming Neovim events (diagnostics, visual selection, LSP client list)
update
editor_stateand are forwarded where relevant. - Reference navigation (file or symbol clicks) results in
nvim::open_file/open_symbolRPC calls and a focus change command back to the UI. - ACP tool results and session updates are turned into
@tool_result ...lines for the (still-light) ACP UI surface.
The mediator is spawned once; MediatorHandle gives the UI the sending side and
a shutdown future.
This is the "native window host". It owns the winit event loop, softbuffer
surface, and libghostty-vt Terminal instances (one per pane).
Major submodules:
layout.rs: split vs maximized modes,vertical_split_layout,vertical_split_fits,trailing_pane_cols, focus-after-reference helpers. All the math that keeps the editor at ≥80 columns and respects the agent'smin:maxpane width preference.pane_controller.rs+pane.rs: wraps aTerminalPane(the libghostty surface), handles mouse (Ctrl-held reference cues, Ctrl-click for OSC 8 / scanned references, drag for selection, wheel), key forwarding, alt-screen detection for paging behavior, and review pane special cases.links.rs: the OSC 8 / "reference target" scanner.reference_target_from_rowand URI forms turnsrc/main.rs:42or`Foo::bar`orsymbol://...intoFileTargetorSymbolthat the mediator then resolves via Neovim RPC. This runs on visible rows; performance matters.render.rs: pixel buffer drawing, borders, inactive dim, selection highlight, damage tracking, color helpers. Also hosts a few color/luminance tests.font.rs: cosmic-text + swash based glyph rasterization with tunable hinting/shaping/coverage/boost. The actual terminal cells come from libghostty-vt; via renders the glyph bitmaps.acp_modal.rs: in-process Ratatui overlay for ACP permission / ask-question prompts (drawn viadraw_ratatui_buffer). ACP agent transcript/input lives in the PTY-hostedvia --acp-tuichild (src/acp_tui/), not an in-window Buffer pane.input.rs: key and clipboard normalization.
Layout modes (Alt+1/2/Shift variants + Alt+Shift+3 for split direction) live in
the WinitGhosttyApp. The UI also owns an optional review pane (either a nvim
review command or the hunk tool).
Mouse handling for references ultimately emits PaneCommand::OpenRequested,
SymbolOpenRequested, or URL-open commands. File and symbol commands route
through the mediator to Neovim RPC + focus commands; external URLs launch in the
system browser without changing Neovim focus.
- Normal mode (PTY, e.g.
VIA_AGENT="opencode"): aPtySessionis spawned for the agent. Output is fed to a libghosttyTerminalin the agent pane. Context is injected by writing text into the PTY. - ACP mode (spawned helpers, e.g.
opencode acp):AcpClientspawns the agent as a stdio JSON-RPC subprocess. Afterinitialize+new_session, prompts arrive viavia agent send --to <id>/ Luaagent.send(or typed into the ACP TUI). The agent UI is a PTY pane runningvia --acp-tui(plain ratatui), glued to the mediator over a side Unix socket. Permission / ask-question modals stay in-process unless auto-approve matches (see below). Tool permissions and results flow through the mediator.
ACP permission auto-approve (src/acp_auto_approve.rs, wired from
AcpRuntime::handle_agent_event): when a spawned helper sends
session/request_permission, via evaluates allow rules before opening the modal.
Built-ins (always on): any shell command whose executable is via; ACP tool kinds
read and search; read-only shell (ls, pwd, cat, head, tail, rg,
fd, and git status|diff|log|show). User extensions in via.conf
[auto_approve] add commands (base executable names) and kinds — they never
disable built-ins. Kinds edit, delete, and move are never auto-approved.
Miss → modal (never auto-deny). On match, via replies with JSON-RPC
allow-once when available. Policy is global from config, not per-agent state.
Contract: board task via:nce2.
AcpClient is intentionally minimal (handshake, session create, prompt,
tool result serialization). It is not a full agent SDK.
- PTY = interactive / manual (default primary).
--agent opencodestarts a PTY pane (bus idagent). Bus sends are mailbox-only; read withvia agent inbox. - ACP = orchestrated / automatic (spawned). Spawn
orchestrator,reviewer, etc.; via drives them over ACP. Bus sends and pane prompts go throughclient.prompt(). Auto handoff is ACP-only. The PTYagentpane remains for interactive work alongside spawned ACP helpers.
--agent "… acp" is rejected at startup; the primary pane is always a PTY.
src/pty.rs also provides CoalescedOutputNotifier so that bursty PTY readers
don't flood the UI thread with redraw events.
via embeds several small Lua files at compile time via include_str!:
context_bridge.lua: the heart of editor → via notifications. Sets up autocmds forDiagnosticChanged, visual selection tracking,LspAttach/Detach, and the<leader>abmapping. Pushes JSON over a Unix socket (via_editor_socket). Also listens on an LSP bridge socket so agents can drive real LSP requests (textDocument/definitionetc.) inside the live Neovim session.open_file.lua,open_symbol.lua,review.lua,diagnostics.lua: small RPC-invoked scripts for jumping and forvia session diagnostics.
The Rust side (nvim.rs) uses nvim-rs over the nvim socket to execute these
(or dynamic commands) and to parse diagnostics JSON.
Session discovery for CLI tools (via session diagnostics, the agent skill) is
done exclusively via the VIA_SESSION environment variable (written into child
envs by the main process and pointing at the instance manifest under
instances/<pid>/session.json). There is deliberately no "guess the newest
socket" fallback.
Naming convention used in via:
| Term | Meaning | Lifetime | On disk |
|---|---|---|---|
| Instance | Live via process (pid, sockets, agent bus) | Ephemeral | instances/<pid>/ |
| Workspace | Project scope (sanitized canonical cwd) |
Durable | workspaces/<id>/ |
| Board | Task board within a workspace | Durable | workspaces/<id>/boards/<board-id>/ |
Under $XDG_DATA_HOME/via (default ~/.local/share/via):
instances/<pid>/ # ephemeral runtime (prune stale dirs here)
session.json # instance manifest (VIA_SESSION)
agents/ # registry + inbox (agent bus)
logs/
*.sock
workspaces/<workspace-id>/ # durable per-project state
meta.json # cwd, created_at
active_board # pointer to selected board
boards/<board-id>/
meta.json
tasks/*.md # one Markdown file per task (task_store)
- Instance —
SessionGuardcreatesinstances/<pid>/at startup and removes it on clean shutdown. Detached mode setsVIA_RUNTIME_ROOTto the same path.via session listdiscovers live instances by scanninginstances/*/session.json. - Workspace — id is a sanitized absolute path of the canonical working
directory (
src/workspace.rs), e.g./home/you/proj→home_you_proj. Created lazily on first task operation. Browsable under~/.local/share/via/workspaces/. - Board — startup and
via task *reuse the active board (or the most recently used board if the pointer is stale);defaultis created only when the workspace has no boards yet. Explicitvia task board new --id …creates and activates a new board. Boardmeta.jsontrackslast_used_at(updated on activate). Tasks on the active board survive via restarts; agent mailboxes do not (instance-scoped).
ACP sessions (JSON-RPC new_session) are a separate concept — subprocess
conversation handles inside an instance, not file-backed storage.
Startup wiring is split into three ownership-aligned pieces bundled as
AppContext:
UserConfig— durable preferences from CLI > env (VIA_*) >~/.config/via/via.conf(TOML) > built-in defaults (nvim,agent, scroll,auto_approve, spawn presets, …).RuntimePaths— per-instance sockets, agents dir, and bridge module paths (env overrides or defaults under the runtime root).LaunchContext— process launch wiring: working directory and derivedorchestration_enabled.
The primary agent command must be a PTY launch (not ending with acp).
orchestration_enabled reflects whether spawned helpers can resolve to ACP
(known-agent table or acp_agent override). Spawn presets ([agents.orchestrator],
[agents.reviewer], [agents.coder] in via.conf, plus built-in defaults) fill
missing role / command / model when opening helper panes. Each preset may
include an optional model slug; on ACP connect, establish_acp calls
session/set_config_option after session/new and pushes the result to the pane
header via UiCommand::AcpSessionStatus. Explicit --model on via agent spawn
or assign overrides the preset for that spawn. Requires ACP orchestration and
agent support for the model config option.
SessionGuard writes (and removes on drop) a per-instance manifest under
instances/<pid>/ so that via session ... subcommands and agents running
inside the instance can find the right sockets without extra flags.
CLI subcommands live under via session (list, get, diagnostics — live
instances), via agent (list, spawn, send, inbox, whoami — the agent bus),
via task (list, create, show, claim, update, done — task boards), and
via plugin (install/status the plugin skills).
Inter-agent discovery and messaging, all file-backed under a per-instance
agents_dir (recorded in the instance manifest so CLI commands resolve it from
VIA_SESSION):
registry.json— the list of agent panes, written by the UI whenever it spawns a pane (write_agent_registry). This is the discovery surface (via agent list, Luarequire('via').agent.list()).inbox/<id>/*.json— per-recipient mailbox files under<runtime>/agents/.via agent sendappends one file per message (the source of truth) and notifies the mediator for ACP delivery via theagent_sendeditor-socket event;via agent inboxdrains it.
Each agent pane is spawned with VIA_AGENT_ID/VIA_AGENT_ROLE so an agent
knows its own identity.
Structured work items on top of mailbox strings. Tasks live on a board within a workspace — not in the ephemeral instance directory.
Task—id,title,status,assignee,blocked_by, timestamps, optionalbody- Status —
queued | in_progress | review | done | blocked - Storage — one JSON file per task under
workspaces/<id>/boards/<board-id>/tasks/ - CLI —
via task list|create|show|claim|update|done;via task board list|new|use via taskresolves workspace from instancecwd(whenVIA_SESSIONis set) or current directory; no live instance required for read/write- Delivery (
src/task_delivery.rs) — when a live instance is running,create/claim/update/doneenqueue mailbox messages and notify the mediator (same path asvia agent send):- assignee notified on create, assignee change, or status change (skips self-notification when the actor is the assignee)
reviewstatus additionally notifies the primaryagentpane (human gate) and sends areview_requestededitor-socket signal so the mediator opens the configured review surface (nvimdiff or hunk pane) automatically
Core skills (via-editor, via-agents, via-orc) live as files under the
repo's skills/ tree — not embedded in the binary and not installed by via.
Users install them with an external skills CLI (npx skills, skills-rs) or a
manual copy into agent skill roots. This module only maps the session agent to
skill roots, exposes via plugin status / path, and warns at startup when
core skills are missing.
src/lsp_bridge.rs: the socket side of the LSP request forwarding (agents ask, via forwards to Neovim's real clients, responses come back).src/editor.rs: the in-memoryEditorStateupdated by the Lua bridge events.src/event.rs: the enum of all cross-layer messages (EditorEvent, AgentEvent, UiCommand, etc.).build.rs: currently a no-op (just rerun-if-changed). The real compile-time work happens inside thelibghostty-vtcrate's build script.
- Layout (
src/ui/ghostty/layout.rs):vertical_split_layout, fit checks, and focus-after-reflow. These run on every resize and on navigation that changes split/max state. Bad math = jank or collapsed panes. - Link/reference scanning (
src/ui/ghostty/links.rs):reference_target_from_row(and the URI variant) is conceptually per-row of agent output. It must be fast even when the agent dumps hundreds of lines containing paths and`symbols`. Rendering only scans visible agent rows while Ctrl is held, so clickable cues have no steady-state cost during normal output streaming.
We use Criterion for these (see benches/) so that changes have a regression
signal before they land.
Other hot-ish paths (font glyph rasterization, libghostty cell iteration + damage, the render loop at ~60 fps) are harder to micro-benchmark in isolation and are mitigated by the native + GPU nature of the stack.
- Hybrid support for normal mode (PTY) and ACP mode is live. Normal mode agents get the classic two-pane (or review) layout with raw terminal; ACP mode agents get the single-pane (editor-only) layout + a Ratatui transcript/prompt surface.
- Explicit context for the primary PTY pane is
:ViaBufferSend/<leader>ab; ACP helpers get context viavia agent send --to <id>. Auto-push of the active buffer was removed to reduce noise. - The review backend can be
nvim(opens a Neovim diff/review layout) orhunk. - Core skills (
via-editor,via-agents,via-orc) are user-installed (npx skills / skills-rs / manual copy) so agents can pull diagnostics, use the board/bus, and follow orchestration policy. Startup warns if they are missing for the session agent. - Multi-agent spawning: the orchestrator (primary agent) or Neovim Lua
(
require('via').agent.spawn(id, role, command, model)) can request additional agent panes at runtime. A spawned agent whose command ends inacp(seeconfig::is_acp_command) gets an ACP transcript pane and a mediator-owned session; optional per-idmodelin[agents.*]or--modelon spawn/assign is applied during the ACP handshake. Anything else gets aPaneRole::AgentTerminal { id, label, command }PTY pane. Spawned helpers join the registry and are reachable via Alt+1..9 but do not steal focus or reshape the active layout. - Coordination is two-mode: PTY agents are interactive (manual
via agent inbox), ACP agents are orchestrated (bus sends and pane prompts both go throughclient.prompt(), so a sub's turn starts automatically). Automatic handoff is ACP-only; see "Two coordination modes" above. - The vendored Ghostty VT bits are pinned to a specific git rev of a libghostty-rs wrapper. This gives a single static binary but makes clean builds heavy (Zig + git).
See acp.md for more on the ACP direction and open questions (prompt
editing, tool rendering, styling). Per-agent model selection via [agents.*].model
and --model is implemented for ACP spawns; UI model picker is not.
- Unit tests are colocated and cover the pure/logic parts heavily (layout table, link regex-ish scanning, config precedence, CLI parsing, session manifest roundtrips, color math, pane mouse state machines, etc.).
- We are expanding Criterion benches for the two surfaces above.
- Full GUI + Neovim + real agent E2E is done manually today. A future hermetic harness (headless nvim + fake ACP agent + controlled window size) would be valuable
- README.md — user-facing usage, config, font knobs, detached mode, agent pane width rules.
- CONTRIBUTING.md — how to build, test, and submit.
- project.md — the original "lean roadmap" brief.
- acp.md — ACP status and design spikes.
- Source files have targeted comments on invariants (e.g., coalescing, alt-screen paging, inactive dim, etc.).
If you're about to change layout math, the link scanner, or the mediator's ACP/PTY routing, please look at the existing tests in those modules and consider whether a new or updated benchmark is warranted.