15 KiB
Tasks: Tasks index, query blocks and Tasks view (roadmap item 11)
One task = one agent's work (OpenCode brief template, specs/agents/opencode-runbook.md). [ ] open.
Spec: spec.md (D-numbers point into it). Rules for every task: AGENTS.md, docs/CODE-MAP.md.
Phases run in order; tasks inside a phase marked "parallel" touch disjoint files.
First tool call rule: after at most three reads (the FILES TO OPEN you need), your FIRST write is the
first OWNS file: a compiling stub with the signatures from GOAL and ONE failing test. Then make it pass.
Do not explore the repo; docs/CODE-MAP.md already says where things are.
Standing VERIFY: cargo test -p <pkg> ; cargo clippy -p <pkg> --all-targets -- -D warnings ;
rustfmt --edition 2024 <owned files>. UI/i18n tasks add cargo xtask i18n; last task of a phase adds
cargo xtask layers and cargo xtask tree. Always export PATH="$HOME/.cargo/bin:$PATH".
Standing DO NOT: edit Cargo.toml/Cargo.lock, files outside OWNS, git state, cargo fmt --all; add
allow attributes; use unwrap/expect/panic/[i]/[a..b] on input-derived data; add a dependency.
Owner/lead step 0 (not an agent task)
Create the empty crate crates/tasks (package notescraft-tasks, [lints] workspace = true, deps:
notescraft-markdown, notescraft-templates, notescraft-regex, thiserror), register it in the root
[workspace.dependencies] and in xtask/src/layers.rs at L3. Answer Q1 of the spec.
Phase 1 - the pure crate notescraft-tasks (no I/O)
- 1. Task line parse and format (brief
briefs/09-task-line-parser.md). OWNS:crates/tasks/src/lib.rs,crates/tasks/src/error.rs,crates/tasks/src/task.rs,crates/tasks/src/parse.rs,crates/tasks/tests/parse.rs,crates/tasks/tests/fixtures/lines.txt. FILES TO OPEN:spec.mdD2,crates/engine/src/calendar/tasks.rs(the existingparse_line, do not edit),crates/templates/src/date.rs(Date),crates/markdown/src/scanning/scan.rs(all_tags). GOAL:pub fn parse_line(line: &str) -> Option<Task>(path/line/heading left empty, filled byscan_note),pub fn format_line(task: &Task) -> String, types of D2 withDefault. TESTS: goldenlines.txt(one task per line,|||then the expected debug-free summary) built from the Tasks emoji page; round trip for fully signified lines; unknown emoji stays in description; 3 000-char line isNone;- [ ]without text is a task with empty description;* [x],1. [ ],+ [-]markers; bad date📅 2026-13-40keeps the text and sets no date. VERIFY: standing,-p notescraft-tasks. DO NOT: scan notes (task 5); write a second tag regex. - 2. Status types,
next_status, toggle without recurrence. OWNS:crates/tasks/src/status.rs,crates/tasks/src/toggle.rs,crates/tasks/tests/toggle.rs,crates/tasks/src/lib.rs(twomodlines). FILES TO OPEN:spec.mdD3,crates/tasks/src/task.rs. GOAL:status_type(char) -> StatusType,next_status(char) -> char,pub struct ToggleCfg { set_done_date: bool, remove_scheduled_on_recurrence: bool, next_below: bool }withDefault,pub struct Toggle { lines: Vec<String>, next_index: Option<usize> },pub fn toggle(line: &str, today: Date, cfg: &ToggleCfg) -> Option<Toggle>(non-recurring paths only; a recurring line returns the plain toggle until task 4). TESTS: TODO to DONE adds✅ 2026-10-22; DONE to TODO removes it;/to DONE;-to TODO;set_done_date=false;🏁 deletegives emptylines; text outside signifiers byte-preserved (compare with the original minus the changed span); indent and marker kept. VERIFY: standing. DO NOT: handle recurrence; touch the calendar crate. - 3. Recurrence rule parse and next date.
OWNS:
crates/tasks/src/recur.rs,crates/tasks/tests/recur.rs,crates/tasks/src/lib.rs(onemod+pub use). FILES TO OPEN:spec.mdD4,crates/templates/src/date.rs(Date,Weekday, add-days API),crates/calendar/src/recur.rs(style and limits only; do NOT depend on it). GOAL:Rule::parse(&str) -> Result<Rule>,Rule::next(&self, reference: Date) -> Option<Date>,Rule::when_done(&self) -> bool,Rule::to_text(&self) -> String(canonical). Grammar table of D4. Limits: N <= 1 000, text 128 chars. TESTS: every example row of the Tasks Recurring page (copy them as table-driven cases with a start date and the expected next date);every month on the 31stskips Nov;every monthfrom 2026-01-31 gives 2026-02-28 then 2026-03-28;every 3 monthsfrom Jan 31 gives Apr 30;every weekdayfrom Friday gives Monday;every month on the last Friday;every April and December on the 1st and 24th; invalid text (every,daily,FREQ=WEEKLY) isErr. VERIFY: standing. DO NOT: add end conditions; loop without a bound (cap the month walk at 48 steps). - 4. Toggle with recurrence.
OWNS:
crates/tasks/src/toggle.rs(edit),crates/tasks/tests/toggle_recur.rs. FILES TO OPEN:spec.mdD3 and D4,crates/tasks/src/recur.rs,crates/tasks/src/toggle.rs. GOAL: complete a recurring task: two lines (next first, or below withnext_below), next task has shifteddue/scheduled/startkeeping distances, no✅, status TODO,🆔/⛔dropped,scheduledremoved when the cfg says so; no date on the task or an invalid next date means plain toggle, no new line;when doneusestodayas reference. TESTS: the three examples of the Tasks page (late completion default vswhen done; scheduled+due distance kept); task with only🛫; task with no date and a rule;🏁 deleteplus rule deletes the original and keeps the next. VERIFY: standing. DO NOT: change the signature oftoggle. - 5. Scan a note into tasks.
OWNS:
crates/tasks/src/scan.rs,crates/tasks/tests/scan.rs,crates/tasks/src/lib.rs(mod,pub use). FILES TO OPEN:spec.mdD5,crates/markdown/src/frontmatter/mod.rs(frontmatter end offset),crates/markdown/src/scanning/scan.rs(fence handling). GOAL:pub struct ScanCfg { global_filter: Option<String> },pub fn scan_note(path: &str, text: &str, cfg: &ScanCfg) -> Vec<Task>fillingpath,line,heading,parent_line,original. TESTS: tasks in a fence are ignored; frontmatter lines ignored; heading text tracked across levels; nested sub-taskparent_line; global filter on and off; 1 500 tasks in one note cap at 1 000; CRLF text keepsoriginalwithout\r. VERIFY: standing. DO NOT: read files. - 6. Query parse (parallel with 7 only after 5 compiles; they share
Query, so run 6 then 7). OWNS:crates/tasks/src/query/mod.rs,crates/tasks/src/query/parse.rs,crates/tasks/src/query/dates.rs,crates/tasks/tests/query_parse.rs. FILES TO OPEN:spec.mdD7,crates/tasks/src/task.rs. GOAL:Query::parse(&str) -> Result<Query, QueryError>; filter ASTFilter { Not(Box), And(Vec), Or(Vec), Xor(Box,Box), Leaf(Leaf) }; date word parserparse_date_word(&str, today, week_start) -> Option<DateRange>(absolute,today,next monday,this|next|last week|month|quarter|year,2026-10,2026-Q4). TESTS: every filter line of D7 parses;(due today) AND (priority is high); unknown line givesQueryError { line: 3 }; 101 lines refused;filter by functiongives the clear "not supported" error;{{query.file.path}}placeholder recorded. VERIFY: standing. DO NOT: evaluate anything. - 7. Query run: filter, sort, group, limit.
OWNS:
crates/tasks/src/query/run.rs,crates/tasks/src/query/sort.rs,crates/tasks/src/query/group.rs,crates/tasks/tests/query_run.rs. FILES TO OPEN:spec.mdD7,crates/tasks/src/query/mod.rs,crates/regex/src/lib.rs(public API ofnotescraft-regex). GOAL:run(&Query, &[Task], today, week_start, &QueryCtx) -> Ran;QueryCtx { path: &str }substitutes placeholders; urgency sort uses the Tasks formula (documented in the file header). TESTS: 30-task fixture with a golden result per feature (each filter family, boolean forms, sort by two keys + reverse, group by heading/due/tags withreverse,limit 5,limit groups to 2); regex filter; invalid regex is aQueryErrorat parse time, not at run; 40 000 tasks, one filter + sort under 30 ms (#[ignore]perf test). VERIFY: standing +cargo xtask layers. DO NOT: cloneTasks (groups hold indexes).
Phase 2 - engine
- 8. Task index and
tasks.list/tasks.query/tasks.reindex. OWNS:crates/engine/src/tasks/mod.rs,crates/engine/src/tasks/store.rs,crates/engine/src/tasks/commands.rs,crates/engine/src/prefs/tasks_config.rs, onemod tasks;incrates/engine/src/lib.rs, oneregister_tasks(reg)call incrates/engine/src/commands.rs::register_core,crates/engine/src/ai/assistant_gate.rs(READ list),crates/engine/tests/core_commands.rs(CORE_IDS),crates/engine/tests/tasks_query.rs. FILES TO OPEN:crates/engine/src/bases/mod.rs+commands.rs(the group to copy),crates/engine/src/bases/source.rs(index generation cache),crates/engine/src/prefs/files_config.rs+crates/engine/tests/files_config_viewers.rs(settings pattern),crates/engine/src/calendar/tasks.rs(do not edit). GOAL: D5 store (lazy worker-thread build, mtime-incremental refresh,.notescraft/cache/tasks.jsonversioned and bounded), D8 read commands,[tasks]settings,Sessionaccessor named in the report if one is unavoidable. TESTS:tasks_query.rsdrives the registry on aTempVaultof 50 notes: golden JSON for a query; edit a note throughwrite_noteand the next query sees it; fence tasks ignored;global_filtersetting; invalid query returns{error:{line}}not a command error; cache file reloaded after restart (newSession); read-only session still reads. VERIFY: standing fornotescraft-engine+cargo test -p notescraft-engine --test tasks_query --test core_commands. DO NOT: scan on the UI thread; add a file watcher. - 9.
tasks.toggleandtasks.setwith the stale-line guard. OWNS:crates/engine/src/tasks/write.rs,crates/engine/src/tasks/commands.rs(append),crates/engine/src/ai/assistant_gate.rs(WRITE list),crates/engine/tests/core_commands.rs,crates/engine/tests/tasks_write.rs. FILES TO OPEN:spec.mdD6,crates/engine/src/vault/properties.rs(how an open buffer gets an undoable edit),crates/engine/src/vault/buffers.rs,crates/engine/src/vault/notes.rs(write_note). GOAL: D6 exactly: expect check, +/-20 line search,conflict, buffer edit when open,write_notewhen closed, re-scan of that note and generation bump. TESTS: toggle plain task (byte compare: only that line changes); recurring task adds the next line; open buffer gets one undo step; stale line found 3 lines lower; vanished line givesconflict; read-only session refused;tasks.setdue null removes📅; note with CRLF keeps CRLF. VERIFY: standing +--test tasks_write. DO NOT: write outside the vault; touch.obsidian/. - 10. Editor hook and calendar agenda on the index (small, parallel with 11).
OWNS:
crates/editor/src/keys.rs(addon_toggle_task_at, leaveon_toggle_task),crates/editor/tests/toggle_task_at.rs,crates/engine/src/calendar/tasks.rs(switchscanto the shared parser, keep the outputItems identical),crates/engine/tests/calendar_tasks.rs(existing test must stay green). FILES TO OPEN:crates/editor/src/keys.rs:123,crates/engine/src/calendar/tasks.rs,crates/engine/src/calendar/mod.rs. GOAL:on_toggle_task_at(text, pos, today: Date) -> Option<Edit>: a line with a recognised signifier set goes throughnotescraft_tasks::toggle(returns a multi-lineEdit), all other lines behave exactly likeon_toggle_task; agenda also shows⏳and🛫only when the lead answers Q2 yes (leave aconst SHOW_SCHEDULED: bool). TESTS: recurring line produces 2 lines and a caret on the original; plain line equals old behaviour for 20 existing cases; agenda output unchanged for the existing fixtures. VERIFY: standing fornotescraft-editorandnotescraft-engine. DO NOT: changeon_toggle_taskbehaviour; add a dependency cycle (editor L5 -> tasks L3 is fine, checkcargo xtask layers).
Phase 3 - UI (lead wires the shell hook lines)
- 11.
TabKind::Tasks, the Tasks view list and query bar. OWNS:crates/ui-egui/src/views/tasks_view/mod.rs,.../state.rs,.../list.rs,.../query_bar.rs,.../presets.rs,crates/ui-egui/src/widgets/tabs/kind.rs(constTASKS_PATH, enum arm, icon, label),crates/ui-egui/src/text/views/tasks_view/mod.rs, new[ui.tasks]table appended at the END ofcrates/i18n/locales/en.toml,crates/ui-egui/tests/tasks_view.rs. FILES TO OPEN:crates/ui-egui/src/views/base_view/state.rs(queue engine calls,pump),crates/ui-egui/src/views/base_view/table/grid.rs(virtualised rows),.claude/skills/notescraft-egui-screen/,docs/CODE-MAP.mdUI table. GOAL: D9 tab with presets, query bar (multi-line, error under the field with line number), grouped virtualised rows, checkbox callstasks.toggle, path link opens the note. Dispatch arm inshell/app/window/views.rs::showand the palette entry are lead work, listed in the report. TESTS: kittest on a fixture vault: preset Today lists the due task, ticking a recurring task shows the next one, a bad query shows the error line; only visible rows built (counter) with 2 000 tasks. VERIFY: standing +cargo xtask i18n+cargo xtask tree; render the snapshot PNG and read it. DO NOT: hardcode strings; use raw egui buttons where a widget exists. - 12.
```tasksquery block in the note view. OWNS:crates/ui-egui/src/views/note_view/blocks/tasks_block/mod.rs,.../rows.rs(reusetasks_view::listrow painter viapub(crate)),crates/ui-egui/tests/tasks_block.rs. FILES TO OPEN:crates/ui-egui/src/views/note_view/blocks/query/mod.rs(copy: fence detection, refresh),crates/ui-egui/src/views/note_view/blocks.rs. GOAL: fence infotasksrenders results withfrom_path= the note; checkbox works; refresh on generation change; errors inline; a "Edit query" toggle shows the source. TESTS: block in a note lists tasks, ticking updates the source note on disk, error line shown. VERIFY: standing +cargo xtask i18n. DO NOT: query on every frame (only on generation change or edit). - 13. Rich note view checkbox click uses
tasks.toggle; docs and status. OWNS:crates/ui-egui/src/views/note_view/blocks/rich/text_line.rs(checkbox click handler only),crates/ui-egui/tests/note_task_click.rs,ROADMAP.md,docs/ARCHITECTURE.md,docs/CODE-MAP.md(add tasks row),specs/plugins-parity/roadmap.md(mark 11). FILES TO OPEN:crates/ui-egui/src/views/note_view/blocks/rich/text_line.rs,crates/ui-egui/src/shell/app/window/commands.rs(engine_command). GOAL: clicking a checkbox on a task line with signifiers callstasks.toggle; lines without signifiers keep the old path. Update the three docs and SPEC 6.1 mismatch note. TESTS: click on a recurring task inserts the next line; plain click unchanged. VERIFY:cargo xtask ci(whole set). DO NOT: change the checkbox look.