`:Heph next`/`list` rows now render a compact relative do/late date chip (today/tomorrow/yesterday/MM-DD/YYYY-MM-DD, mirroring heph-tui's fmt) and a recurrence ↻, so scheduling is visible at a glance. `<CR>` already jumps to a row's canonical-context doc. e2e: a do-date-chip render assertion. Completes the §14 item-2 task-list UX wave. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.8 KiB
| title | modified | tags | ||
|---|---|---|---|---|
| heph.nvim | 2026-06-03 |
|
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). |
config / init |
setup(opts), socket resolution, default keymaps. The plugin is connect-only — it never spawns a daemon (see Daemon lifecycle). |
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
The plugin is connect-only: it never spawns or supervises a hephd. The
daemon is an explicit, OS-managed service started once with heph daemon start (a launchd agent on macOS, a systemd user service on Linux — see
run-the-daemon); every surface (CLI, TUI, this plugin) is a pure client of
that one daemon. On setup({}) the plugin resolves the socket and connects; if
nothing is serving it, it notifies once with guidance to run heph daemon start (and a dropped connection is retried with a plain reconnect — never a
spawn). The $HEPH_SOCKET / $HEPH_DB env knobs (and mise run dev) point a
dev Neovim at a separate daemon + DB so real data is never touched.
History: earlier iterations had the plugin auto-spawn and supervise its own daemon (
autostart, self-heal, kill-on-exit). That was removed once the CLI became a first-class surface — a daemon owned by one surface can't be shared, so lifecycle moved to an explicit service (design §4).
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 view <name> |
Run a built-in filter view (tom|ondeck|chores|work|tasks, tech-spec §8.2) |
: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. Each row also shows a
compact do/late date chip (and a recurrence ↻). 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.