hephaestus/docs/reference/heph-nvim.md
Erich Blume 0462a6e43b
Some checks failed
Build / validate (pull_request) Failing after 4m47s
heph.nvim: interactive next/list views (add, done, refresh) + key hint
The Tactical next and Organizational list buffers are now actionable:
- a  add a task from the list (prompt title + attention)
- d  mark the task under the cursor done
- r  refresh
- <CR> open the task's context (as before)

A dimmed key hint renders above the rows as a virtual line (extmark), so it's
discoverable without taking a task row. e2e covers add-from-list and
done-from-list via stubbed vim.ui.input/select.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 11:48:09 -07:00

6.4 KiB

title modified tags
heph.nvim 2026-06-01
reference
design

heph.nvim

The primary user surface (tech-spec §8): a Neovim plugin that replaces obsidian.nvim and is a thin client of the local hephd over its unix-socket JSON-RPC. Notes, journals, and tasks are edited as ordinary buffers; the daemon owns all storage and sync. Built in checkpointed slices on feature/v1-prototype; this card tracks the stable surface as it lands.

Architecture

heph.nvim/lua/heph/ modules, each small and single-purpose:

Module Responsibility
rpc libuv (vim.uv) unix-socket JSON-RPC client. A blocking call() is built over the async pipe by pumping the loop with vim.wait until the matching id returns. Demuxes responses by id; partial lines are buffered; JSON null decodes to Lua nil (luanil). A Session is one connection — the module keeps a default singleton and lets tests open isolated sessions.
node Buffer-backed nodes. A node is a buffer named heph://node/<id> with buftype=acwrite; BufReadCmd loads the body via node.get, BufWriteCmd saves the whole buffer via node.update.
link Parse the [[wiki-link]] under the cursor (mirroring extract.rs grammar) and follow it via node.resolve (exact, never fuzzy search). Unresolved links are allowed.
journal Open/create a dated journal node (idempotent — deterministic id).
daemon Managed-daemon lifecycle: ensure (connect if a daemon already serves the socket, else spawn one we own), stop_spawned (kill only what we spawned, on exit), readiness-poll. Shared with the e2e harness.
config / init setup(opts), socket resolution, default keymaps.
command The :Heph <subcommand> dispatch + completion.

Surfaces never touch SQLite — every operation is a daemon RPC (tech-spec §3). The plugin is mode-agnostic: Tactical/Strategic/Organizational are plugin-side compositions of daemon primitives, not daemon concepts.

Daemon lifecycle

setup({}) is plug-and-play by default (autostart = true): if nothing is serving the socket, the plugin spawns a local hephd against the default XDG paths, kills only the daemon it spawned on VimLeavePre, and self-healsrpc.call retries once through a respawn hook if the connection drops. It only ever spawns when nothing is already serving the socket, so a server/client daemon you started is respected. With autostart = false the plugin connects only and warns/errors if unreachable — for when you run your own daemon. The $HEPH_SOCKET / $HEPH_DB env knobs (and mise run dev) isolate a dev Neovim onto a separate daemon + DB so real data is never touched.

Daemon RPC dependencies

Beyond the existing methods (tech-spec §6), the plugin relies on node.resolve {title} → Node | null: an exact, owner-scoped, non-tombstoned alias-then-title match — the same mapping the store uses to materialize wiki links, so "follow link under cursor" jumps to the same node the stored link points at. When the target doesn't resolve, follow creates a doc with that title (the zettelkasten follow-or-create gesture) and materializes the source's backlink (saving the source if it has unsaved edits, else adding the wiki link directly).

Commands (as of slice 11c)

Command Action
:Heph home Open the home / index landing page (created on first use; title via opts.home)
:Heph today / :Heph journal <YYYY-MM-DD> Open today's / a dated journal
:Heph journals Pick among recent days (preview existing, @create for new); count via opts.journal_days
:Heph follow (also <CR> in a node buffer) Follow the [[link]] under the cursor — creating the target doc if it doesn't exist yet
:Heph doc <title> Create (and open) a new wiki doc
:Heph open <id> Open a node buffer by id
:Heph search <query> Full-text search; pick a result to open
:Heph next [scope] Tactical "what is next?" view (<CR> opens a task's context)
:Heph list [attention] Organizational survey of the outstanding set
:Heph capture <title> Capture a committed task (pick attention)
:Heph attention [color] Set the current task's attention
:Heph done / :Heph drop / :Heph skip State change on the current task
:Heph promote [attention] Promote the - [ ] line under the cursor to a committed task
:Heph log <text> Append a breadcrumb to the current task's log

"Current task" is resolved from the buffer: a task node, or a canonical-context doc whose owning task is followed via its canonical-context backlink. The next/list views render the titled rows the daemon returns (list enriched to carry titles + the context id, so no N+1 node.get) and are interactive: <CR> opens a task's context, a adds a task (prompt title + attention), d marks the task under the cursor done, r refreshes. Pickers use built-in vim.ui.select, auto-upgrading to Telescope when installed.

Promotion (:Heph promote) mints a committed task from the - [ ] line under the cursor (the daemon's task.promote, item_ref = the cursor item's 1-based index among context items, computed code-fence-aware to mirror extract.rs) and rewrites that line into a [[link]] to the new task. To keep that link unambiguous, wiki-link resolution excludes canonical-context docs, so [[Task Title]] resolves to the task, not its identically-titled context doc.

Testing (tech-spec §9)

The headless e2e suite drives the plugin in nvim --headless against a real hephd over a temp socket, asserting both buffer contents and resulting DB state (via an isolated RPC session). It uses a self-contained busted-style runner (tests/e2e/runner.lua) — no external plugins, no network — so it is deterministic. mise run test-nvim builds the daemon and runs the suite against system-installed Neovim; a deliberately failing spec exits non-zero (no false-green; the runner also fails if it discovers zero specs). CI runs the same suite through the test-nvim Dagger function (.dagger/, invoked by build.yaml as dagger call test-nvim), which bakes a pinned, arch-detected Neovim onto a Rust image, builds hephd, and runs the suite — reproducible, and identical to the native mise run test-nvim path.

  • tech-spec — §8 surface spec, §6 RPC API, §9 testing strategy
  • design — the mode model (Tactical/Strategic/Organizational) and rationale