feat: hyphen-tag fix, contexts, table view, richer capture - #10
Merged
Conversation
TW3's expression parser reads the hyphen in a bare +tag token as a subtraction operator, so tags like +ais-research-taste broke three ways: filters errored with "Cannot subtract from a Boolean value" (rendering a silently empty buffer), `add` dumped the tag into the description, and `modify +tag` exited 2. - shell_export now routes parsed args through normalize_tag_filters (same rewrite tw_export already used): +t → tags.has:t, -t → tags.hasnt:t - fields_to_args emits one `tags:a,b` replacement arg instead of per-tag +a/-b deltas; this also fixes partial tag removal on buffer save, which +t deltas never applied - new taskmd.tw_change_tag(uuid, tag, remove) merges a single-tag delta into the full set and replaces; modify_tag picker and :TwInbox use it Verified against a real task 3.4.2 DB (unit + e2e specs included). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
"added task" alone doesn't tell you whether the right thing went through (tw 978fb9d1) — include the first ~40 chars of the parsed description (ellipsized beyond that). The e2e spec also locks in that the capture float starts in insert mode (tw 0f0b9bbf), observed over RPC in a child nvim since :startinsert never engages inside an in-process headless spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
confirm = false already skips the save popup and applies immediately; the summary notification is then the user's only checkpoint, so make the revert path discoverable right there (tw fca82462). Documented the confirm=false → notify → :TwUndo workflow in README + :help, and added an e2e spec that saves without a picker, checks the summary + hint, and verifies :TwUndo actually reverts the mutation in Taskwarrior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
:TwContext shows the active context and lists defined ones; a name activates it (task context <name>), "none" clears it (idempotent), and open task buffers refresh (tw c12b4cbd). TW 3.x applies contexts to reports but NOT to `export`, so the render path injects the active context's read filter itself. The injected tokens land in the rendered taskmd header, which keeps the save path on the same effective filter — the e2e spec proves saving a context-narrowed buffer never touches the tasks the context hides. uuid-targeted filters are exempt so opening one specific task can't come up empty. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
The capture form was a single uncolored line (tw 14d722e8). Now: - lines below the first become annotations on the created task; <C-CR> opens one, <CR> still submits everything. capture_annotations = false opts out, capture_height sizes the form. - the capture buffer runs the task buffer's highlighter, so project:, priority:, due:, +tags and UDAs are colored as you type - every remaining `field:value` token gets the new neutral TaskField group, so a UDA never renders as plain description text, and the new field_colors option overrides any field by name (mirrors tag_colors). Clock times (09:30) and URL schemes are excluded. Also fixed: <Esc> discard-confirm and its cursor restore only looked at line 1, so annotation text could be dropped without a prompt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
One row per task with aligned columns so every data modality is visible
at once (tw 6e9f911d), sorted by urgency (or custom_urgency). <CR> opens
the task under the cursor in an editable :Tw buffer, r refreshes, q
closes; the view itself is read-only.
Columns come from the new table_columns option — field name or
{ field, label, width, align, format }. Omitting width on one column
makes it absorb the leftover window width; cells longer than their
column are ellipsized rather than overflowed. Unrecognised field names
are read straight off the exported task, so UDA columns need no wiring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Follow-ups from testing PR #10 against real data. :TwRepairTags — the parser fix stops NEW tasks from losing their tags, but tasks added before it still have the literal +tag text sitting in their description with no tag created, so they stay unfindable. This moves those tokens back into real tags. It previews every change in a tab and confirms before writing, defaults to status:pending, and skips a +tag written inside quotes (that's someone describing a tag, not a misparse — e.g. a bug report about the filter failing). :TwContext — bare :TwContext now clears the context (the common case); `show` prints the summary. Added `define [name] [filter]` and `delete [name]`, both prompting for anything omitted, so contexts can be managed without dropping to the CLI. :TwTable — column widths were computed from vim.o.columns, which overcounts by the number/sign gutter, so the widest rows wrapped past the right edge. Now measured from the window's text area (width - textoff), with a re-render on resize. Capture annotations — the key is now configurable via capture_annotation_key, defaulting to <M-CR> plus <C-CR>. <C-CR> only reaches Neovim from terminals speaking CSI-u (in tmux, only with extended-keys on), so binding it alone would silently submit instead. Dropped the insert-mode <C-o> binding, which shadowed Vim's built-in. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
The repair runs once, ever, to clean up damage from the TW3 hyphen
misparse. A registered command would sit in :Tw<Tab> forever for every
user in exchange for a flow almost nobody runs twice. Invoke it
explicitly instead:
:lua require("taskwarrior.repair_tags").run()
Documented under Data safety in the README and |taskwarrior-repair| in
the help file.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
The taskmd header comment is metadata, not content, but it still cost two screen rows: `conceal` blanks a line without removing it, and render put a spacer underneath. Tasks therefore started on the third row. - hide the header row outright with extmark `conceal_lines` (Neovim 0.11+, feature-probed rather than version-compared; older Neovim keeps the previous blank-row behaviour) - drop the spacer line after the header in render() - park the cursor on the first task on open, in both the split and float paths — with the header row gone, line 1 is not somewhere the cursor should sit Verified by screen geometry, not extmark presence: winline() == 1 for the cursor on the first task proves the header occupies no row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Sort order, grouping, context, saved views and reports each have a small
known answer set, so none of them should require remembering syntax like
`urgency-`. All five now open a picker; new <leader>tc (context),
<leader>tv (saved view) and <leader>tr (report) join the existing
<leader>ts / <leader>tg, and a bare :TwSort opens the picker instead of
erroring.
Built on vim.ui.select so the list renders through whatever picker the
user already configured (dressing/telescope, snacks, fzf-lua) and
inherits its fuzzy matching, degrading to Neovim's built-in list. The
active value is marked, and the context/report pickers show each entry's
filter. The filter prompt stays free text — a Taskwarrior filter is not
a finite set.
Sort specs and group fields lived in three copies (two completion
callbacks plus the command); they now come from taskwarrior.choices.
Two real bugs surfaced while testing this, both fixed:
- sorting by a field only SOME tasks have (priority, due, project) raised
"attempt to compare two boolean values" and failed the entire render —
the comparator compared two is-missing booleans with `<`. Missing
values now sort last in both directions.
- priority sorted alphabetically, so `priority-` ("most important first",
matching urgency- beside it) listed L above H. Now ranked H > M > L.
Also: :TwLoad with no argument silently did nothing, because the command
passes "" and `if name then` is true for the empty string.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Hiding the header row with conceal_lines also hides anything anchored to it — a concealed-away line draws no virt_lines (verified directly against Neovim 0.11.6). The empty-state hint IS virt_lines on the header, so an empty filter rendered a completely blank window with no explanation: exactly the issue #5 symptom the hint was added to prevent. With no tasks there is nothing to pull to the top anyway, so the header row now stays whenever the buffer holds no task lines, and is removed only when there is content to lift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Hiding the header row put the first task on the top screen row, but line 1 still existed and was navigable. `gg`, `:1`, `k` from the first task, or a restored cursor position all parked the cursor on a row the user cannot see, while the first task LOOKED selected — so typing edited the header and raised "header is read-only", apparently in response to editing the first task. CursorMoved/CursorMovedI now bounce the cursor from the header onto the first task line, preserving the column. Exempt when there is nothing to bounce to (an empty filter keeps the header row visible) and in visual mode, where moving the cursor would silently reshape the selection; the existing read-only guard still covers that path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Populating a task buffer is not a user edit, but it was recorded as one. Pressing u in a freshly opened :Tw therefore reverted the population and left an empty buffer — which the save path reads as "every task in this filter was removed" and offers to mark them all done. The header guard restoring line 1 on top of that produced the reported sequence: blank screen, "header is read-only", then a prompt to delete everything. set_buf_lines now applies the render with undolevels = -1 (:h undolevels), making it non-undoable. The user's own edits still undo normally. Pre-existing: reproduced on main (7 lines -> 1 line on u), not introduced by this branch. The regression spec asserts what a SAVE would do after u, not just the line count — the destructive part is what matters. Verified it fails against the unfixed set_buf_lines (3 of 5 assertions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
Four separate reasons the suites were red; none of them a product bug. - issue_fixes lint required >= 4 v:lua completion specs. Converting sort and group to pickers removed two (pickers need no v:lua indirection), leaving 3. Floor lowered with a note that changing it is the intended review signal. - the icons e2e test asserted `icons = true` forces nerd-font glyphs, contradicting both config.lua's documented contract and the passing unit test in icons_spec. `true` means auto-detect; `"force-nf"` is the opt-in. The test predated that escape hatch. Now covers both. - :TaskSync's e2e test waited for the "syncing" PROGRESS message, so it restored vim.notify before the async failure arrived; the ERROR then escaped to stderr and made headless Neovim exit non-zero. Waits for a terminal state now. - undo_render_e2e_spec seeded project:undodemo, colliding with the existing :TaskUndo test's fixture (specs share one Neovim instance), which made that test read the wrong task. Renamed. Both suites now exit 0. Worth noting the e2e runner had been exiting non-zero on main for a while — CI only runs the unit suite, so it went unnoticed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx
MattHandzel
changed the base branch from
agent/centralize-taskwarrior-commands
to
main
August 16, 2026 22:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Works through the seven open Taskwarrior tasks tagged/projected
task_nvim/taskwarrior_nvim.Based on
agent/centralize-taskwarrior-commands(PR #9), since it builds on the centralizedcommand.read/command.mutateboundary.Fixes
Hyphenated tag names (
3b9dfcab— "TwFilterwith+ais-research-tastefails to pull something up")TW3's expression parser reads the hyphen in a bare
+tagtoken as subtraction, which broke three separate paths::TwFilter +ais-research-tasteCannot subtract from a Boolean value→ silently empty buffertask add … +foo-barmodify +foo-barshell_exportnow runs parsed args throughnormalize_tag_filters(the rewritetw_exportalready used):+t→tags.has:t,-t→tags.hasnt:t, virtual tags verbatim.fields_to_argsemits onetags:a,breplacement instead of per-tag+a/-bdeltas. This also fixes partial tag removal on save, which the+tdeltas never applied.taskmd.tw_change_tag(uuid, tag, remove)merges a single-tag delta into the full set and replaces; used by thegmtag picker and:TwInbox.Features
:TwContext(c12b4cbd— contexts like work/home)Shows the active context, sets one by name,
noneclears it (idempotent —task context noneexits 2 when already clear). TW 3.x applies contexts to reports but not toexport, so the render path injects the context's read filter itself. Those tokens land in the rendered taskmd header, so the save path re-exports on the same effective filter — the e2e spec proves saving a context-narrowed buffer never touches the tasks the context hides.uuid:-targeted filters are exempt so opening one task can't come up empty.:TwTable(6e9f911d— vit-like configurable table view)One row per task, aligned columns, urgency-sorted (honors
custom_urgency).<CR>opens the task under the cursor in an editable:Twbuffer,rrefreshes,qcloses; the view is read-only. Columns come from the newtable_columnsoption — a field name or{ field, label, width, align, format }. Omitwidthon one column and it absorbs the leftover window width; over-long cells ellipsize rather than overflow. Unrecognised field names are read straight off the exported task, so UDA columns need no wiring.Richer capture window (
14d722e8— bigger, colored capture form)<C-CR>opens one,<CR>still submits everything.capture_annotations = falseopts out;capture_heightsizes the form.project:,priority:,due:,+tagsand UDAs color as you type.field:valuetoken gets the new neutralTaskFieldgroup — a UDA never renders as plain description text — and the newfield_colorsoption overrides any field by name (mirrorstag_colors). Clock times (09:30) and URL schemes are excluded.<Esc>discard-confirm and its cursor restore only looked at line 1, so annotation text could be dropped without a prompt.Add confirmation shows the task (
978fb9d1)added taskdidn't tell you whether the right thing went through — the notification now echoes the first ~40 chars of the parsed description, plus an annotation count when there is one.Save popup / undo (
fca82462)confirm = falsealready skipped the popup and applied immediately; the summary notification is then the only checkpoint, so it now points at:TwUndo. Documented the confirm=false → notify → undo workflow.0f0b9bbf(capture starts in insert mode) was already the behavior — it now has a regression test.Verification
Every change is covered by specs driving the real
taskCLI (3.4.2) against an isolatedTASKDATA, plus a unit spec for the pure tag-normalization logic.Two pre-existing failures on this branch, both unrelated to these changes:
e2e_spec"icons=true → NF glyph" — fails identically on a clean tree.capture_relative_dates_spec"due:today" — the assertion compares a UTC-now date prefix against a local-midnight timestamp, so it fails whenever the run happens between UTC midnight and local midnight.Three of the e2e specs caught real bugs during development, all fixed here: the table view's flex column kept its content width when the fixed columns had eaten the budget (300-char rows), the generic field highlighter colored
https://as a field, andtask context noneexits 2 when no context is set.🤖 Generated with Claude Code
https://claude.ai/code/session_01EkK2z7n2ziYSsXnGbmX8Fx