generated from eblume/project-template
Some checks failed
Build / validate (pull_request) Failing after 18m44s
Make the owner's saved filters first-class so the agenda isn't one flat list. `list` now takes a ListFilter predicate-as-data (heph-core::filter): attention include/exclude sets, project-id scope, exclude_projects, and an actionable do-date gate. New Store::view(name) resolves a built-in ViewSpec — looking project names up to ids and subtree-expanding them through parent links — then lists. Five built-ins seeded from the Todoist queries (design §6.2.1): tom, ondeck, chores, work, tasks (Schedule dropped — time-of-day isn't modeled on date-grained do-dates). Surfaced as `heph view <name>` (no name lists them), the `view` RPC + RemoteStore forward, and `:Heph view <name>` in nvim. The list RPC/RemoteStore/CLI/heph.nvim migrate to the filter wire; legacy --scope/--attention/--no-blue map onto it (nvim view.lua updated). Tests: filter unit predicate, a views integration suite (subtree scope+exclude, actionable gate, unknown-view error, absent-project empties), a socket list/view dispatch test, two nvim e2e specs. 154 Rust tests + 18 nvim e2e green; clippy/fmt clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
114 lines
6.7 KiB
Markdown
114 lines
6.7 KiB
Markdown
---
|
|
title: heph.nvim
|
|
modified: 2026-06-01
|
|
tags:
|
|
- 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). |
|
|
| `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. 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.
|
|
|
|
## Related
|
|
|
|
- [[tech-spec]] — §8 surface spec, §6 RPC API, §9 testing strategy
|
|
- [[design]] — the mode model (Tactical/Strategic/Organizational) and rationale
|