The global ⌘' quick-add overlay is a borderless, transparent, always-on-top
accessory window that winit hides with `Visible(false)`. That orders the window
out visually but leaves heph-quickadd the *active* application — so after a
capture (or Esc / toggle) keyboard focus never returns to the app the user was
in, and the lingering overlay can keep intercepting clicks where it used to sit.
Hide at the application level instead via `NSApplication.hide:`, which fully
orders our windows out and activates the next app in line (the previously
focused one). On re-show, `unhide:` clears that hidden flag before the existing
viewport `Focus` command makes the field key again. Both are macOS-only no-ops
elsewhere, wired through new `app_yield_focus`/`app_take_focus` helpers backed by
objc2 / objc2-app-kit (unified to the 0.6/0.3 line global-hotkey already pulls).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Client` connected to the unix socket once and never reconnected, so after an
opt-in self-update or `heph daemon restart` dropped the socket, every later
`call()` failed — `heph-tui` would sit on errors until relaunched (and the work
we just shipped makes restarts more frequent).
`Client` now stores the socket path and reconnects on a dropped connection,
classifying the failure to stay safe:
- write-side failure (request never reached the daemon) → reconnect + retry once;
- reply lost after sending (daemon closed mid-request) → reconnect for next time
but surface this one, so a mutation is never silently double-applied;
- genuine RPC errors are passed through untouched.
heph-tui and the CLI use `Client` unchanged, so the TUI self-heals on its next
refresh tick. Adds an integration test driving a mock daemon that drops the
connection after each request.
Closes the "heph-tui: reconnect on a dropped daemon socket" backlog task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The spoke OAuth path funneled every failure into one `AuthError::Provider`
whose Display was hardcoded "identity provider unreachable". So a reachable IdP
returning `400 invalid_grant` on a refresh was reported as "unreachable",
misdirecting incident response toward the network when the fix is re-auth. The
real refresh cause was also swallowed — `bearer()` logged it and returned None,
so sync health only ever showed the downstream 401 on /sync/pull.
Wording fix (auth.rs / oauth.rs):
- Split AuthError into Unreachable (transport), Rejected (IdP returned an HTTP
error — carries the RFC 6749 §5.2 error/error_description), and Other
(keyring / malformed response, previously mislabeled too).
- refresh()/discover()/start()/poll() classify transport vs status; refresh
reads the OAuth error body on a non-2xx.
- Hub-side token verify maps IdP-infra failures → 503, token failures → 401.
Recovery UX (server.rs / heph / heph-tui):
- bearer() returns Result; the sync paths record the real acquisition failure
(with a re-login hint when it's a rejection) instead of a masked 401.
- sync health's last_error carries the exact `heph auth login --hub-url …
--issuer … --client-id …` command (keyed to the configured hub); sync.status
also returns issuer/client_id + the command.
- New `heph auth status` prints auth health and the re-login command.
- heph-tui's auth chip points at it: `⚠ auth · heph auth status`.
Closes the duplicate "misleading identity provider unreachable" tasks and the
"actionable re-auth guidance" task. Also corrects a now-stale set-up-sync-hub
gap note (daemon config baking landed in the prior PR).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`restart` bootstrapped immediately after `bootout`, but `launchctl bootout` is
asynchronous: launchd may still be killing/reaping the job and removing its
label when the command returns. Bootstrapping into that transitional domain
fails with a generic `5: Input/output error`, intermittently — the odds depend
on how fast hephd (sync client + SQLite + a heph-quickadd child) shuts down.
- Wait for the label to actually clear (poll `launchctl print`, bounded) before
re-bootstrapping, and retry the bootstrap to cover the residual settle window.
- When the plist is unchanged (the common binary-upgrade restart), use
`launchctl kickstart -k` to restart the loaded job atomically — no
bootout/bootstrap, no race. The full reload path is reserved for genuine
config changes, where launchd must re-read the plist.
Start's bootstrap shares the same retry helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`heph daemon start`/`restart` previously hardcoded `hephd --mode local` and
only wired the bare `--self-update` bool — the poll interval and all spoke/hub
sync config (`--hub-url`, `--http-addr`, `--oidc-*`) could not be set on the
managed service without hand-editing the plist/unit (which a later
start/restart would clobber).
Generate the hephd arg vector from a DaemonConfig and add the corresponding
`heph daemon start/restart` flags: --mode, --hub-url, --http-addr,
--oidc-issuer, --oidc-audience, --oidc-client-id, and
--self-update-interval-secs. Regenerating now reads the existing service file
and preserves any flags not passed (start as well as restart), so a bare
invocation never silently drops baked config.
Closes the "pass through --self-update-interval-secs" and "bake hub/spoke
config into the generated service" backlog tasks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The background sync loop runs every 30s, so the last-sync age never crossed
the 60s 'just now' threshold — the chip always read 'just now', which also
masked the first missed sync (age 30-60s looked identical to a fresh one).
Show seconds under a minute ('⟳ 26s') so the chip is a visible heartbeat and a
stalled sync surfaces ~30s sooner.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A spoke could be silently failing to sync (expired token → 401, or hub
unreachable) with the only signal buried in the daemon log. Now:
- hephd tracks SyncHealth (last attempt/success time, last error, auth-failure
flag) from the background sync loop and sync.now, classifying a 401 as an auth
failure. sync.status returns it plus the pending merge-conflict count.
- heph-tui shows a live status-line indicator (spoke only): '⟳ <age>' since the
last good sync, red '⚠ auth' when re-login is needed, '⚠ offline' when the hub
is unreachable, and '⚠ N conflicts' when conflicts are pending. The event loop
polls on a 2s tick so the age advances and failures appear while idle.
- docs: recommended Authentik access/refresh token validity to stop frequent
re-logins (with the iOS PWA localStorage-eviction caveat).
Closes the 'Add hub connection status to heph-tui' and 'Spoke sync health:
surface unhealthy state instead of silent 401 spam' backlog items.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bundles the cosmetic/UI-polish backlog for the agenda surfaces. All read-side;
no schema or sync change (see hub-spoke-data-evolution).
- humanize_rrule (hephd::datespec): inverse of parse_recurrence — renders an
RRULE as 'every other week', 'weekdays', 'yearly on Apr 15', etc.; falls back
to the raw rule for unmodeled parts (COUNT/UNTIL/ordinal BYDAY). Mirrored in
the PWA's datespec.js. Shown in the TUI recurs detail line and PWA task/qa
previews instead of the raw FREQ= string.
- project.overview RPC + Store::project_overview: each project's parent (via the
existing 'parent' links) and direct outstanding-task count, a read-only query.
- TUI sidebar: subprojects indented by depth, per-project counts, wider pane,
and ListState + scrollbar so it scrolls instead of clipping on overflow.
Tests: humanize parity (Rust + JS), round-trip through parse_recurrence,
raw-passthrough; project_overview count/parent; sidebar tree ordering + cycle
safety.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the manual bearer-token paste with a proper browser OIDC sign-in.
- Hub: unauthenticated GET /config -> {issuer, client_id} (added after the auth
layer), sourced from the verifier's new TokenVerifier::oidc_config(). Lets the
PWA self-configure when served from the hub. Tests in web_serve.rs.
- PWA: src/oauth.js implements PKCE (S256), the authorize redirect, the callback
token exchange, and silent refresh (offline_access). Settings gains a "Login
with Authentik" button (manual token kept under a fallback disclosure); rpc.js
retries once on 401 via a refresh hook; app.js completes the callback / refreshes
on load; sw.js skips caching the callback URL and ships oauth.js in the shell.
Requires the PWA origin registered as a redirect URI on the Authentik provider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Popover supervision was gated to Mode::Local, so running the store-owning
daemon in server mode (now needed to host heph-pwa) silently dropped the
desktop quick-capture popover. Server mode is local + an HTTP hub and owns the
same store/socket, so it should drive the popover too; broaden the guard to
Local | Server (client, a thin proxy, still opts out).
Also: when the PWA shell is served from the hub, default the hub URL to its own
origin so the app is zero-config on first open (Settings still overrides). Bump
the service-worker cache to v2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a permissive CORS middleware (answers the browser OPTIONS preflight and
stamps Access-Control-* on every response) and an optional --web-root static
file handler with an index.html SPA fallback. Together these let a browser
surface — the forthcoming heph-pwa mobile app — call /rpc cross-origin or be
hosted same-origin straight from the hub. No new crate dependencies; file
reads run on the blocking pool. Covered by tests/web_serve.rs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
hephd's reqwest client is built default-features=false with no TLS
feature, so the self-update release poll's HTTPS GET always failed
('release check failed: requesting forge releases/latest') — the bug
never surfaced before because nothing in production used reqwest over
HTTPS (hub sync is plain http://). Switch the poll to ureq, which is
already a dependency and ships a rustls/ring TLS stack needing no system
libs (notably no cmake/aws-lc-sys, which would break the rust:bookworm CI
image). Verified end-to-end: a 0.0.0 build now detects v1.1.0, installs,
and restarts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo is public, so self-update needs no credentials: cargo install
--git is a plain anonymous clone (NOT the access-restricted Forgejo cargo
registry, which is what required forge.ops.eblu.me). Point INSTALL_GIT_URL
and the releases poll at the canonical public host over HTTPS — verified
end-to-end (cargo install --git https://forge.eblu.me/... --tag v1.0.3
builds a working hephd with zero auth).
Make the headless service able to run the apply path: 'heph daemon
start --self-update' (default off) generates a launchd/systemd service
that passes --self-update and bakes a PATH (incl ~/.cargo/bin) + HOME so
the minimal service env can find cargo. restart preserves the setting.
Default (no flag) services are byte-identical to before. Template + URL
behavior covered by unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The poller now installs + restarts (not just logs); fix the stale doc and
point at service-env-forge-access as the deployment step that makes the
apply path operational.
Add a Restarter trait + ProcessRestarter (exit 0 so launchd KeepAlive /
systemd Restart=always respawn the new binary). apply_update now installs
then restarts, and the restart fires only on a successful install. Wired
into the poll loop. Unit-tested with fake installer+restarter: restart on
success, no restart after a failed install. Real process exit is never
run in tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an Installer trait + CargoInstaller (runs cargo install --locked
--git <ssh> --tag <tag> for heph/hephd/heph-tui/heph-quickadd — the
documented install command, via the SSH host that sidesteps the
cargo/forge canonical-name mismatch), and apply_update() which runs the
blocking install on the blocking pool. The poll loop now applies on a
detected update. Apply path is unit-tested with a fake installer (call +
failure paths); the real cargo subprocess is never run in tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lock in the base-case guarantee that a self-updating hub (which restarts
under its spokes) relies on. New sync_http test: a spoke whose hub is
unreachable keeps serving + accepting writes, a sync attempt fails fast
(Err, not hang/panic), and when the hub returns the accumulated ops
reconcile with no special recovery.
The verification surfaced one non-graceful path — the daemon's shared
reqwest client had no timeout, so a black-hole hub (connects, never
replies) could stall the sync/self-update loop. Give it a 30s timeout so
'the hub can vanish at any moment' holds even mid-request.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Self-restart works by exiting cleanly and letting the service manager
respawn the new binary. launchd already does this (KeepAlive=true), but
the systemd user unit was Restart=on-failure, which ignores a clean
exit (code 0). Switch to Restart=always + RestartSec=1, update the unit
test, and note in run-the-daemon that existing Linux installs must
`heph daemon restart` once to regenerate the unit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a ReleaseSource trait (real ForgeReleaseSource over HTTP; injectable
for tests), check_release() returning a CheckOutcome
(UpToDate/UpdateAvailable/Failed) that never errors so a flaky forge
can't stall the daemon, and run_poll_loop() that ticks on the configured
interval and logs when a newer release is available. spawn_self_update_loop
now spawns the real poller. Detection is unit-tested with a stubbed source.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add --self-update (default off) and --self-update-interval-secs to the
hephd CLI, a SelfUpdateConfig (Some => enabled), and thread it into the
Daemon (with_self_update) for every mode. spawn_self_update_loop()
currently just announces the mode at startup ('self-update enabled')
so the opt-in is observable; the poll/apply cycle is wired in later
leaves. Omitting the flag leaves behaviour unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add crates/hephd/src/selfupdate.rs: a pure update_available() that
compares the running heph_core::VERSION (e.g. "1.0.3 (sha)") against a
release tag ("v1.0.4") via semver, ignoring the build suffix and v
prefix; plus parse_latest_tag() / fetch_latest_tag() for the forge
releases/latest feed. Decision logic and JSON parsing are unit-tested
against sample payloads; the network fetch is isolated. Adds the semver
workspace dep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The project.resolve socket test had a node.create call over rustfmt's
line limit; CI's `cargo fmt --all --check` flagged it. Wrap it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RPC clients (the hephaestus.nvim plugin, for its `:Heph version`
command) had no way to learn which hephd they are talking to — `health`
returns counts, not a version. Add a tiny `version` method returning
`{ version: heph_core::VERSION }`, the same `X.Y.Z (sha)` string the
binaries print for --version. No store access needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Editing a task's canonical-context doc body previously meant looking up
its `canonical_context_id` (e.g. via `heph list --json`) and then
`heph node update <doc-id> --body`. Add a `heph context <task-id>`
command that resolves the canonical-context doc from the task's outgoing
links and:
* prints the body with no flag,
* `--body <text>` replaces it (`-` reads stdin, matching `node update`),
* `--append <text>` adds a blank-line-separated paragraph.
Errors clearly when the id has no canonical-context doc (e.g. a plain
doc node rather than a task). Purely a client-side CLI convenience — no
new RPC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `--project <name>` argument matched titles case-sensitively and
exactly, so `--project hephaestus` or `--project heph` failed against a
`Hephaestus` project. Make project-name resolution forgiving but
deterministic, via a tiered match in `resolve_project_id`:
1. exact (case-sensitive) — the historical behavior; always wins
2. case-insensitive exact — only when unambiguous
3. case-insensitive prefix — only when unambiguous
Ambiguous fuzzy matches resolve to None (callers report "no project
named X") rather than silently picking one. This single resolver already
backed `heph list --project` (via project_scope); route the CLI's
task/edit/promote/parent path through it too with a new `project.resolve`
RPC + `Store::resolve_project`, so every `--project` surface behaves the
same.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
heph-tui was the one daily-driver binary that did not answer --version.
Add the same clap `version = heph_core::VERSION` attribute that heph and
hephd already carry, so all three report `X.Y.Z (sha)` consistently.
Addresses the heph-tui half of the cross-binary --version task.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The build-time dirty check used `git status --porcelain`, which counts
untracked files — including cargo's own `.cargo-ok` marker in a
`cargo install --git` checkout — so a clean tagged build reported e.g.
`1.0.1 (bcab3c16b-dirty)`. Use `git diff --quiet HEAD` (tracked changes only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
heph-core gains a build.rs that captures the short git SHA and a
`heph_core::VERSION` const ("<crate-version> (<sha>)"); heph and hephd use it
for clap's --version. The crate version stays sourced from Cargo.toml.
release.yaml now bumps the workspace version into Cargo.toml + Cargo.lock on a
commit that only the tag points at, tags it manually, and pushes just the tag —
so cargo install --git --tag vX.Y.Z reports the real version while main stays at
0.0.0. The changelog commit moved ahead of the tag so the release includes its
own changelog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
keyring 4's `keyring` meta-crate has no feature gating and compiles every
platform credential backend for the target. On Linux that dragged in the zbus
async stack, a redundant libdbus secret-service, the keyutils store, a
sqlite/zstd db-keystore, and OpenSSL (~290 crates in its subtree) — a real cost
on the RAM/CPU-constrained CI runner building with CARGO_BUILD_JOBS=1.
Depend on keyring-core (the API) + exactly one store crate per OS instead:
- macOS -> apple-native-keyring-store (keychain feature)
- Linux -> dbus-secret-service-keyring-store (crypto-rust; libdbus, no openssl)
oauth.rs registers the per-target store as the keyring-core default itself
(replacing keyring::use_native_store). Runtime behavior is unchanged (tokens
still go to the macOS Keychain / Linux Secret Service).
hephd's Linux dependency graph: 401 -> 235 crates (-166), dropping the zbus
ecosystem and two C builds (zstd-sys, plus the redundant secret-service path).
macOS builds + the full suite are green here (228 tests, clippy -D warnings,
fmt, prek); the Linux store path is CI-verified (API confirmed from source).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The cheap "seam" that keeps the single-owner hub from calcifying, ahead of
the gilbert -> indri bring-up:
- Replace the single-tenant gate `Store::authorize_owner_sub(sub) -> bool`
with `resolve_owner(sub) -> Option<owner_id>`. The hub auth middleware now
resolves the token's identity to the owner it may act as (Some -> allow,
None -> 403). Behavior is identical for the single-owner hub (claim-on-first;
strangers still 403), but the contract no longer assumes one global owner, so
serving N owners later is additive, not a rewrite. The per-request owner is
marked at the exact line where downstream scoping wires through.
- New how-to docs/how-to/set-up-sync-hub.md: stand up the hub and connect an
existing device as an offline-capable spoke, the data-safe way (Path A: the
hub adopts the device's identity rather than rewriting the device).
The decision (cheap seam now, defer full multi-tenancy + adoption rewrite) is
recorded in the Adoption + multi-tenant task's context doc. Two enabler gaps
the how-to surfaced (heph daemon hub/spoke service flags; Path-A seeding tool)
are filed as Hephaestus tasks.
Green: 228 tests, clippy -D warnings + fmt + prek clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump all external crates to latest stable ahead of v1.0.0 (tech-spec §14.9):
keyring 3→4 (the keyring_core split + register-the-native-store model),
rusqlite 0.32→0.40, ratatui 0.29→0.30, rrule 0.13→0.14, yrs 0.26→0.27,
plus a cargo update for semver-compatible bumps.
keyring 4 moves Entry/Error into keyring_core and requires a credential
store to be registered before use; KeyringTokenStore now registers the
OS-native store once (lazily, via Once) and uses keyring_core types.
not_keyutils=true so Linux prefers Secret Service over the logout-wiped
kernel keyutils store.
Drop the fs4 dependency in favor of std::fs::File::try_lock (stable since
Rust 1.89); raise workspace MSRV 1.85→1.89. Remove the orphaned
.forgejo/scripts/build hook — CI invokes Dagger directly.
Green: 228 Rust tests + 25 heph.nvim headless e2e, clippy -D warnings + fmt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI on main failed on a clippy::single_match lint in the heph-tui sidebar
key handler. Rewrite as `if let`. Also add a `cargo clippy -D warnings`
prek hook mirroring cargo-fmt, so lints are caught locally before CI
(prek previously only ran cargo fmt).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`heph list --project <name>` lists a project's outstanding tasks by name
(subtree-expanded, resolved server-side via a new project.scope path that
reuses the view machinery; errors on unknown names). `--json` prints raw
rows — node_id, canonical_context_id, attention/state/do_date/late_on/
recurrence/project_id — for scripting and agents. Store::project_scope on
the trait + LocalStore + RemoteStore; new project.scope RPC and a flattened
ListParams so `list` accepts an optional project name. Test covers
resolve-by-name + unknown-name error.
AGENTS.md thinned to tight command/pattern sections: dropped the historical
parity narrative and the verbose roadmap section; added a "Working state"
section documenting `heph list --project Hephaestus [--json]` as the way to
inspect heph's self-hosted roadmap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run rustfmt over files landed in earlier commits this session that weren't
fmt-checked (heph-quickadd, the heph-tui undo/move wave, the hephd quickadd
supervisor). Pure formatting (struct/if-else expansion, line wrapping); no
behavior change. Restores `cargo fmt --check` clean for CI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Project delete previously tombstoned only the project node, leaving its
tasks with a live in-project link to a dead project — orphaned (not in the
Inbox, unbrowsable, blank project) rather than unfiled as intended. New
atomic Store::delete_project tombstones every in-project link to the project
(tasks fall to the Inbox), then tombstones the project node; tasks are never
deleted. Exposed as the project.delete RPC (LocalStore + RemoteStore); the
heph-tui sidebar `D` now routes through it. Core test asserts the task
survives and becomes unfiled; the project node is tombstoned.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Triage gestures (x/d/S/A/b/e/m/D) now fire only when the task pane is
focused, so a stray key while in the sidebar can't drop or delete a task;
hints are focus-aware. `u` undoes the last triage action (drop/done/skip/
attention/move) and Ctrl-z redoes it, restoring from a pre-action snapshot
(multi-level, cap 200; tombstone-delete excluded — no restore path yet). `D`
in the sidebar deletes the highlighted project (y/N), unfiling its tasks to
the Inbox. The sidebar's Projects section now rebuilds after create/delete,
so a new project appears without a restart. Tests cover undo/redo, the
empty-undo no-op, and sidebar project delete.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `m` move-to-project overlay is now a filterable picker: a prompt line
narrows the project list by fuzzy subsequence match as you type (↑/↓ or
Ctrl-n/p move, Enter selects, Esc cancels), so there's no scrolling a long
list. When the filter names no existing project, a "+ New project" row
creates it and files the task there in one step (Backend::create_project →
node.create). Tests cover fuzzy narrowing and the create-then-file flow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A sixth built-in filter view (listed below On Deck) showing every
outstanding task filed under no project — the capture inbox to triage. New
ListFilter.unfiled predicate kept purely in matches(); the inbox ViewSpec is
un-gated (no attention/do-date filter) so nothing hides from triage. Surfaces
automatically in the heph-tui sidebar and `heph view`. Tests cover the
predicate and the view spec; navigation tests updated for the 6th view.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A macOS global quick-capture popover to retire Todoist. New `heph-quickadd`
crate: an always-warm eframe/egui agent that registers ⌘' (global-hotkey,
Carbon — no Accessibility permission) and toggles a hidden, pre-created
window visible + focused on press — never spawning on the keypress, so it's
a muscle reflex. A single field live-parses Todoist-style inline syntax via
the shared parser; chips show ⚑ attention · 📁 project · ⏰ do-date · ↻
recurrence as you type. Enter saves optimistically (hide now, task.create
on a bg thread; a failed RPC re-shows with the text restored). #project
autocomplete (Tab/↑↓/click; focus-locked so Tab completes instead of
traversing). Example hints rotate, fading in only after ~2s idle.
Parser lifted from heph-tui into hephd's lib (hephd::quickadd) so the TUI
and the popover share one parser. hephd supervises the helper as a child in
local mode on macOS (opt-in HEPH_QUICKADD=1, set by the installed launchd
plist) — one service to manage, no second launch agent; the helper
self-exits when orphaned so killing hephd leaves nothing behind.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A one-cell gap between the ⚑ attention flag and the project-colored ●
bullet; the blank-flag case already reserves the same width, so rows stay
aligned whether or not a flag is present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- `<Enter>` now opens the selected task's context doc in nvim (App::enter:
from the sidebar it drills into the task list first); the `o` binding is
retired. Hint line updated.
- BUILTIN_VIEWS reordered to the owner's preference — Top of Mind, Tasks,
Work Tasks, Chores, On Deck — which drives the TUI sidebar and
`heph view`. Tests that walked to On Deck by a fixed offset now seek it
by title.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The picker listed every node, so each task showed up twice (itself + its
same-titled canonical-context doc) plus tag/log noise — and the new
preview made the duplicates look identical. New `Store::list_linkable_nodes`
/ `node.linkable` returns non-tombstoned nodes minus `tag`s and the docs
that are a task's canonical-context or log attachment (you link the task,
not its body). The Telescope picker now sources from it.
Tests: a socket test (5 nodes → 2 linkable: task + standalone doc).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`wikilink::to_ids` rewrites name-addressed links to the canonical id
(id-first resolve: an already-id target is left alone, a name → its id
with any label preserved). `Store::migrate_wikilinks_to_ids` runs it over
every body and re-saves through update_node (which collapses + materializes
by id); idempotent. Surfaced as the `migrate.wikilinks` RPC + RemoteStore
forward + the `heph migrate-links` CLI command (not auto-run — the owner
runs it once per store).
Name-resolution + the canonical-context hack stay for now so legacy links
keep resolving pre-migration; retiring them is a later tidy. Tests:
to_ids unit + a heph-core migrate integration (rewrite + materialize +
idempotency).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keep canonical `[[NODEID]]` links readable without storing names. New pure
`heph-core::wikilink` (injected id→title): `expand` turns a bare `[[id]]`
into `[[id|Current Name]]`, `collapse` turns a name-matching `[[id|text]]`
back to bare (a custom label is preserved as an override).
- `node.get` expands on every read (nvim buffer + TUI preview both
readable), then prepends frontmatter when asked.
- `update_node` strips frontmatter, then collapses links, then CRDT-diffs
— so neither projection ever persists and an unchanged read→write is a
no-op to the bare id.
Tests: wikilink unit (expand/collapse/round-trip), a heph-core collapse
+ materialize-by-id integration test, and a socket expand→collapse
round-trip. `heph export` still emits raw ids (later polish).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend: `links::resolve_id` now checks for an exact live node id before
alias/title, so a canonical `[[NODEID]]` link resolves to its node and
can't be shadowed by a like-named node. Legacy `[[Name]]` links still
resolve by name (until the migration), so this is additive.
heph.nvim: `link.insert` (bound to insert-mode `[[` and `:Heph link`)
searches via the `search` RPC and inserts `[[NODEID]]`, with a "+ Create
new doc" entry; `<CR>` follow resolves the id directly. e2e covers
search→insert→materialize and the create path.
Remaining (§8.4): read-expansion/conceal display + the one-time
[[Title]]→[[NODEID]] migration (then retire name-resolution + the hack).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The store-side half of the frontmatter edit surface:
- heph-core `frontmatter::strip` runs in `update_node` before the yrs
diff, so frontmatter never enters the body or CRDT. Conservative (only
a leading `---` block whose first line is a YAML key; a prose hrule
survives) and idempotent → read→write round-trip is a no-op.
- hephd `frontmatter::render` (local-tz dates via new `datespec::fmt_iso`)
behind `node.get {frontmatter: true}`: id/kind/title/tags, and for a
task or its canonical-context doc the owning task's scalars + a `task:`
ref. Subject-task + project-name resolution in dispatch.
Safe against any client (inbound frontmatter always stripped). Tests:
strip unit (incl. hrule/idempotency), render unit, socket round-trip +
task-context-doc projection. The heph.nvim diff-into-RPCs layer is next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A tag is a `tag`-kind node with a deterministic id in (owner, name)
(`tag:<owner>:<name>`, like the journal), so a name is one canonical tag
shared across nodes and replicas converge with no duplicates. Tagging is
an OR-set `tagged` link (mirroring in-project):
- heph-core: `nodes::open_or_create_tag` (bodyless, deterministic id),
`tags::{add,remove,of}`, and `Store::{add_tag,remove_tag,tags_of}`.
Enumerate all tags via the existing `list_nodes(Tag)`.
- hephd: `tag.add`/`tag.remove`/`tag.list` RPCs + RemoteStore forwarding.
- heph: `heph tag add|rm|list` (a node's tags, or every tag).
Names are trimmed; canonical case/spelling normalization is deferred to
the zk import. Unblocks the `tags:` line of the frontmatter surface.
Tests: core add/dedupe/remove/canonical-id/trim/missing-node + a socket
add/list/enumerate/remove test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`s` flips the task list between two orders:
- default: attention (red→orange→white→blue) → most-overdue (desc) →
project name → created_at (FIFO)
- project: project first, with dimmed ──── Name ──── separators riding
atop each group's first task (the cursor only lands on real tasks)
The view filter still runs before the sort. Pure comparator (`cmp_tasks`/
`sort_tasks`, today injected) with unit tests for both modes + a
navigation test for the toggle. `skip` moved from `s` to `S` to free `s`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each task row now leads with a colored attention flag (⚑ for
red/orange/blue, blank for white/none) and a project-colored bullet (●).
The bullet color is derived stably from the project id (FNV-1a → HSL →
truecolor RGB) so it survives projects being added/removed; a per-project
override on the model is a later refinement. The glyph shape is reserved
for future semantics.
The task list also gains a scrollbar and ListState-driven
scroll-to-visible so a selected task below the fold stays reachable.
Tests: fmt::project_color determinism unit; a flag-glyph render assertion.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`m` opens a list-pick overlay on the highlighted task — "(Unfile)" then
every project — and re-files it via `task.set_project` (cursor starts on
the task's current project). j/k navigate, Enter applies, Esc cancels.
Adds `Backend::set_project`, a `Mode::MoveToProject` overlay, and its
render. Navigation tests cover refile + cancel.
Closes the last Todoist-parity capture gap (§14 item 1).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>