heph.nvim: :Heph journals — recent-days picker with preview + @create
All checks were successful
Build / validate (pull_request) Successful in 16m0s

A dailies picker (zkd-style): lists the last `journal_days` (default 7) days
newest-first, previews existing journals and shows "@create" for new ones, and
opens the chosen day (creating if new). Journals resolve by their ISO-date
title, so no new RPC is needed. picker.select gains an optional Telescope
preview pane. e2e covers the recent-days list (exists/@create across a month
boundary) and open-on-pick via a stubbed vim.ui.select.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erich Blume 2026-06-02 11:26:45 -07:00
commit d0930aa6a3
7 changed files with 112 additions and 3 deletions

View file

@ -18,5 +18,5 @@ Begin the v1 prototype (Phase 1, tech-spec §11.1), built in TDD slices:
- `heph.nvim` slice 11b (§8) — task views: `list` is enriched to return titled rows (the same shape as `next`, with the canonical-context id) so the Organizational survey needs no per-row `node.get`. The plugin gains the Tactical **`:Heph next`** and Organizational **`:Heph list`** views (`<CR>` opens a task's canonical-context doc), task **capture**, **set-attention**, **done/drop**, **skip**, and per-task **`log`** append — each resolving "the current task" from the buffer (a task node, or a context doc via its `canonical-context` backlink). A `vim.ui.select` picker (Telescope auto-upgrade when installed) backs `:Heph search`/`capture`/`attention`. Headless e2e adds the capture→next→context→checklist→done workflow and the recurring fresh-checklist workflow (completing a recurring task rolls it forward and the next occurrence presents an all-unchecked checklist).
- `heph.nvim` slice 11c (§8) — promotion + CI: `task.promote` mints a committed task from a `- [ ]` context-item line (addressed by its 1-based index) and rewrites that line into a `[[link]]` to the new task; `:Heph promote` does this for the line under the cursor. Wiki-link resolution now excludes a task's canonical-context doc, so `[[Task Title]]` resolves to the task itself (not its identically-titled context doc). The headless e2e suite runs in CI via a Dagger function that bakes a pinned, arch-detected Neovim onto a Rust image and runs the same self-contained suite developers run natively with `mise run test-nvim`; the runner fails on a zero-spec discovery so a misconfigured path can't pass silently.
- `heph.nvim` managed daemon — plug-and-play by default: `require("heph").setup({})` spawns and supervises a local `hephd` against the default paths when none is running, kills only the daemon it spawned on exit, and self-heals (respawns + reconnects if the daemon dies mid-session). A daemon you started yourself (a `server`/`client` architecture, or a service) is always respected — the plugin only spawns when nothing is serving the socket; with `autostart = false` it connects only and warns if unreachable. `$HEPH_SOCKET` / `$HEPH_DB` isolate a development Neovim onto a separate daemon + DB.
- `heph.nvim` follow-or-create: pressing `<CR>` on a `[[wiki-link]]` whose target doesn't exist yet now **creates** a doc with that title and opens it (the zettelkasten gesture), materializing the source's backlink — so you can link a journal entry to a brand-new note in one keystroke. Plus `:Heph doc <title>` to create a standalone wiki entry, and `:Heph home` — a single designated landing/index page (open-or-create by title, configurable via `opts.home`) to grow a map of content around.
- `heph.nvim` follow-or-create: pressing `<CR>` on a `[[wiki-link]]` whose target doesn't exist yet now **creates** a doc with that title and opens it (the zettelkasten gesture), materializing the source's backlink — so you can link a journal entry to a brand-new note in one keystroke. Plus `:Heph doc <title>` to create a standalone wiki entry, and `:Heph home` — a single designated landing/index page (open-or-create by title, configurable via `opts.home`) to grow a map of content around. `:Heph journals` opens a recent-days picker (preview existing days, `@create` for new ones; count via `opts.journal_days`, default 7) — the dailies workflow. Pickers (Telescope) now support a preview pane.
- Dev/installed isolation tooling: a `mise run dev` task runs the working-tree `hephd` on isolated `.dev/` paths, and a how-to ([[install-heph]]) covers installing `heph`/`hephd` from the forge (build-from-source), the lazy.nvim plugin setup, and pointing a dev Neovim at the dev daemon via `$HEPH_SOCKET`/`$HEPH_DB` so it never touches the installed store.

View file

@ -61,6 +61,7 @@ edits, else adding the `wiki` link directly).
|---|---|
| `: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 |

View file

@ -19,6 +19,9 @@ M.subs = {
journal = function(args)
require("heph.journal").open(args[1])
end,
journals = function()
require("heph.journal").pick()
end,
follow = function()
require("heph.link").follow()
end,

View file

@ -15,6 +15,8 @@ M.defaults = {
bin = "hephd",
--- Title of the home / index page (`:Heph home`).
home = "Home",
--- How many recent days the `:Heph journals` picker offers.
journal_days = 7,
--- Set the default `<leader>h*` keymaps. `false` to opt out.
keymaps = true,
}

View file

@ -16,4 +16,47 @@ function M.open(date)
return node
end
local function iso_to_time(iso)
local y, m, d = iso:match("(%d+)-(%d+)-(%d+)")
return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = 12 })
end
--- Entries for the `n` most recent days ending at `today` (ISO; default today),
--- newest first. Each: `{ date, node|nil, exists }`. Journals are titled by
--- their ISO date, so each day resolves directly.
function M.recent_entries(n, today)
today = (today and #today > 0) and today or util.iso_today()
local t0 = iso_to_time(today)
local entries = {}
for i = 0, n - 1 do
local date = os.date("%Y-%m-%d", t0 - i * 86400)
local node = rpc.call("node.resolve", { title = date })
entries[#entries + 1] = { date = date, node = node, exists = node ~= nil }
end
return entries
end
--- Pick among recent journal days — existing days preview their content, new
--- days show `@create` — and open the chosen day's journal (creating if new).
function M.pick(opts)
opts = opts or {}
local days = opts.days or (require("heph").config or {}).journal_days or 7
require("heph.picker").select(M.recent_entries(days), {
prompt = "heph journals",
format = function(e)
return e.exists and e.date or (e.date .. " @create")
end,
preview = function(e)
if e.exists and e.node and e.node.body and #e.node.body > 0 then
return vim.split(e.node.body, "\n", { plain = true })
end
return { "@create — new journal for " .. e.date }
end,
}, function(e)
if e then
M.open(e.date)
end
end)
end
return M

View file

@ -9,18 +9,27 @@ local function telescope_available()
return pcall(require, "telescope")
end
--- Select one of `items`. `opts.prompt`, `opts.format(item)->string`.
--- Select one of `items`. `opts.prompt`, `opts.format(item)->string`, and an
--- optional `opts.preview(item)->lines` (Telescope only; markdown-rendered).
--- `on_choice(item|nil, index|nil)` — nil when cancelled.
function M.select(items, opts, on_choice)
opts = opts or {}
if not vim.g.heph_force_ui_select and telescope_available() then
-- Telescope path: a thin wrapper so fuzzy UX is available when present.
-- (The dropdown is intentionally minimal; richer pickers can come later.)
local pickers = require("telescope.pickers")
local finders = require("telescope.finders")
local conf = require("telescope.config").values
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local previewer = nil
if opts.preview then
previewer = require("telescope.previewers").new_buffer_previewer({
define_preview = function(self, entry)
vim.api.nvim_buf_set_lines(self.state.bufnr, 0, -1, false, opts.preview(entry.value) or {})
vim.bo[self.state.bufnr].filetype = "markdown"
end,
})
end
pickers
.new({}, {
prompt_title = opts.prompt or "heph",
@ -32,6 +41,7 @@ function M.select(items, opts, on_choice)
end,
}),
sorter = conf.generic_sorter({}),
previewer = previewer,
attach_mappings = function(bufnr)
actions.select_default:replace(function()
actions.close(bufnr)

View file

@ -0,0 +1,50 @@
-- The recent-days journal picker (the `zkd`-style dailies picker).
local h = require("e2e.helpers")
describe("journal picker", function()
local ctx
before_each(function()
ctx = h.start()
end)
after_each(function()
h.stop(ctx)
end)
it("lists recent days newest-first with @create state", function()
local entries = require("heph.journal").recent_entries(5, "2026-06-02")
assert.are.equal(5, #entries)
assert.are.equal("2026-06-02", entries[1].date) -- newest first
assert.are.equal("2026-05-29", entries[5].date) -- crosses the month boundary
for _, e in ipairs(entries) do
assert.is_false(e.exists) -- nothing created yet
end
-- Create one day's journal; it now reports as existing (with its node).
ctx.q:call("journal.open_or_create", { date = "2026-05-31" })
local found
for _, e in ipairs(require("heph.journal").recent_entries(5, "2026-06-02")) do
if e.date == "2026-05-31" then
found = e
end
end
assert.is_true(found.exists)
assert.is_truthy(found.node)
end)
it("opens the picked day's journal", function()
vim.g.heph_force_ui_select = true
local orig, picked = vim.ui.select, nil
vim.ui.select = function(items, _opts, on_choice)
picked = items[1] -- choose the newest day
on_choice(items[1])
end
require("heph.journal").pick()
vim.ui.select, vim.g.heph_force_ui_select = orig, nil
assert.is_truthy(picked)
local buf = vim.api.nvim_get_current_buf()
assert.are.equal("journal", vim.b[buf].heph_node_kind)
assert.are.equal(picked.date, ctx.q:call("node.get", { id = vim.b[buf].heph_node_id }).title)
end)
end)