feat(nvim): do/late date chip (+ ↻) on task-view rows (§8)
Some checks failed
Build / validate (pull_request) Failing after 1m17s

`: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>
This commit is contained in:
Erich Blume 2026-06-03 11:05:02 -07:00
commit 9d84eb7427
5 changed files with 56 additions and 5 deletions

View file

@ -27,3 +27,4 @@ Begin the v1 prototype (Phase 1, tech-spec §11.1), built in TDD slices:
- Move-to-project (§8.1): a new `task.set_project` RPC re-files a task under another project (or unfiles it) with OR-set link semantics — the old `in-project` link is tombstoned and a new one added, so a task is never filed under two projects at once. In `heph-tui`, **`m`** opens a list-pick overlay ("(Unfile)" then every project) on the highlighted task. `heph edit <task> --project <name>` now routes through the same RPC (fixing a bug where re-filing piled on a duplicate link), and `--project none` unfiles the task. This closes the last Todoist-parity capture gap.
- `heph-tui` task-list visuals (§8.1): each row now leads with an attention **flag** (`⚑`, colored red/orange/blue; blank for white) and a **project-colored bullet** — the bullet's color is derived stably from the project id (so it survives projects being added/removed), letting you scan a mixed list by project at a glance. The list also grows a **scrollbar** and keeps the selected task scrolled into view when there are more tasks than fit.
- `heph-tui` sort toggle (§8.1): **`s`** flips the task list between two orders — **default** (attention → most-overdue → project → creation) and **by-project** (grouped under dimmed `──── Project ────` separators, then the same sub-order). The view's filter still applies first. (To free `s`, **skip** moved to **`S`**.)
- `heph.nvim` task-view rows (§8): `:Heph next`/`:Heph list` rows now show a compact **do/late date chip** (and a recurrence `↻`), so you can see scheduling at a glance; `<CR>` still jumps to a task's context doc.

View file

@ -1,6 +1,6 @@
---
title: heph.nvim
modified: 2026-06-01
modified: 2026-06-03
tags:
- reference
- design
@ -84,7 +84,8 @@ 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
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

View file

@ -446,11 +446,11 @@ See [[design]] §5§7 for the constraints later phases impose on present choi
> The remaining work is the **UX roadmap agreed 2026-06-03** (design conversation with the owner). It is documented docs-first — the bigger items have design sections above (`heph-tui` UX in §8.1, frontmatter in §8.3, wiki-links-by-id in §8.4) — and built in this order:
1. ✅ **`heph-tui` — move-to-project (§8.1) — DONE:** the **`task.set_project` RPC** (tombstone the old `in-project` link + add the new — OR-set semantics, no task-scalar op; given project must be a live project-kind node) plus the TUI `m` list-pick overlay and `heph edit --project <name>|none` (which also fixed a duplicate-link bug in the old re-file path). Unblocks the project-edit path of the frontmatter surface (§8.3).
2. **`heph-tui` task-list UX wave (§8.1) — no backend (`RankedTask` already carries every field):**
2. **`heph-tui` task-list UX wave + nvim nav polish (§8.1/§8) — DONE (no backend; `RankedTask` already carries every field):**
- ✅ **(a) flag column + project-colored bullets — DONE:** a leading `⚑` colored by attention (red/orange/blue; blank for white/none), and the bullet `●` colored by its project via a stable `hash(project_id) → hue` (FNV-1a → HSL → `Color::Rgb`). Hashing is chosen for stability-under-insertion over golden-angle spread; overlap is acceptable. The bullet **glyph shape** is reserved for future semantics. A per-project **color override** (stored on the project node, editable) is a later refinement — colors are derived client-side for now (no schema change).
- ✅ **(e) scrollbar — DONE:** the task list grows a `ratatui` `Scrollbar` (tracking the selection) when content overflows; a `ListState` selection keeps the highlighted row scrolled into view.
- ✅ **(b) sort toggle `s` — DONE:** **default**: attention (red→orange→white→blue) → days-overdue (descending; no-date = 0) → project (name) → `created_at` (FIFO). **project mode**: project is primary, with dimmed **`──── Name ────` separators** riding atop each group's first task (the cursor only lands on real tasks). View filtering always runs **before** the sort. (`skip` moved to `S` to free `s`.)
- **nvim task-navigation polish (§8)** — show do/late in `next`/`list` rows and a clean jump-to-context gesture (read/navigate, not field-edit). *(The only piece of item 2 left.)*
- **nvim task-navigation polish (§8) — DONE:** `:Heph next`/`list` rows now carry a compact **do/late date chip** (and a recurrence `↻`); `<CR>` already jumps to a row's canonical-context doc (read/navigate, not field-edit).
3. ⏳ **Tags (§4, §8.3) — promoted from deferred:** `NodeKind::Tag` exists but has no machinery. Add **tag-as-node + an OR-set tag link** (mirroring `in-project`) + `tag.add`/`tag.remove` RPCs + enumeration. Prerequisite for the `tags:` line of the frontmatter surface (§8.3) and the eventual zk import ([[design]]); the goal is **one canonical tag set** across all of heph.
4. ⏳ **YAML frontmatter as an edit surface (§8.3) — docs-first C1:** generated-on-read, stripped-and-ignored-on-write in `heph-core`; `heph.nvim` diffs it into structured RPCs. See §8.3.
5. ⏳ **Wiki-links by node id (§8.4) — docs-first C1 (maybe C2):** canonical `[[NODEID]]` at rest, expanded/concealed for display; a `[[` picker; no name-links in the DB. Includes a one-time body fixup. See §8.4.

View file

@ -17,9 +17,46 @@ local HINT = " <CR> open a add d done r refresh"
local ATTENTIONS = { "white", "orange", "red", "blue" }
-- Compact relative date for a do/late epoch-ms value (mirrors heph-tui's fmt):
-- today / tomorrow / yesterday, MM-DD within the year, else YYYY-MM-DD.
local function fmt_date(ms)
local d = os.date("*t", math.floor(ms / 1000))
local n = os.date("*t")
local d_noon = os.time({ year = d.year, month = d.month, day = d.day, hour = 12 })
local n_noon = os.time({ year = n.year, month = n.month, day = n.day, hour = 12 })
local days = math.floor((d_noon - n_noon) / 86400 + 0.5)
if days == 0 then
return "today"
elseif days == 1 then
return "tomorrow"
elseif days == -1 then
return "yesterday"
elseif d.year == n.year then
return string.format("%02d-%02d", d.month, d.day)
else
return string.format("%04d-%02d-%02d", d.year, d.month, d.day)
end
end
-- The right-side date chip: a late marker once past due, else the do-date.
local function date_chip(t)
if t.late_on and os.time() * 1000 > t.late_on then
return "late:" .. fmt_date(t.late_on)
elseif t.do_date then
return "do:" .. fmt_date(t.do_date)
end
return ""
end
local function row(t)
local tag = t.attention and ("[" .. t.attention .. "]") or "[ ]"
return string.format("%s %s", tag, t.title)
local recur = t.recurrence and "" or ""
local left = string.format("%s %s%s", tag, t.title, recur)
local chip = date_chip(t)
if chip ~= "" then
return string.format("%-50s %s", left, chip)
end
return left
end
local function task_on_line(buf)

View file

@ -33,6 +33,18 @@ describe("filter views", function()
assert.is_falsy(text:find("cool thing", 1, true), "blue task should not be in ToM")
end)
it("shows a do-date chip on a dated task row", function()
-- A past do-date keeps the task actionable (so it appears in ToM) and, with
-- no late_on, renders as a `do:` chip rather than a `late:` one.
ctx.q:call("task.create", { title = "dated thing", attention = "red", do_date = 1704067200000 })
require("heph.view").view("tom")
local buf = vim.api.nvim_get_current_buf()
local text = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n")
assert.is_truthy(text:find("dated thing", 1, true), "dated task missing")
assert.is_truthy(text:find("do:", 1, true), "do-date chip missing from the row")
end)
it("scopes the chores view to chore projects via the daemon", function()
local chores = ctx.q:call("node.create", { kind = "project", title = "Chores" })
ctx.q:call("task.create", { title = "take out trash", attention = "white", project_id = chores.id })