taskai-cli 1.8.0__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/DEVLOG.md +115 -0
  2. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/DEVPLAN.md +210 -23
  3. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/PKG-INFO +1 -1
  4. taskai_cli-1.9.0/docs/browser-mode.md +51 -0
  5. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/getting-started.md +9 -0
  6. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/index.md +2 -0
  7. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/interactive-mode.md +2 -1
  8. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/managing-the-tree.md +16 -0
  9. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/pyproject.toml +1 -1
  10. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/browser.py +21 -2
  11. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/cli.py +76 -5
  12. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/help_menu.py +23 -2
  13. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/json_dir_database.py +85 -4
  14. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/models.py +4 -0
  15. taskai_cli-1.9.0/taskai/static/camera.js +208 -0
  16. taskai_cli-1.9.0/taskai/static/canvas.js +138 -0
  17. taskai_cli-1.9.0/taskai/static/config.js +79 -0
  18. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/static/console.js +38 -42
  19. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/static/editpanel.js +7 -18
  20. taskai_cli-1.9.0/taskai/static/graph.js +196 -0
  21. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/static/index.html +5 -0
  22. taskai_cli-1.9.0/taskai/static/navigation.js +44 -0
  23. taskai_cli-1.9.0/taskai/static/render.js +285 -0
  24. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/static/shortcuts.js +38 -23
  25. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/views.py +10 -0
  26. taskai_cli-1.8.0/docs/browser-mode.md +0 -29
  27. taskai_cli-1.8.0/taskai/static/canvas.js +0 -816
  28. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/.github/workflows/publish-to-pypi.yml +0 -0
  29. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/.gitignore +0 -0
  30. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/.readthedocs.yaml +0 -0
  31. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/README.md +0 -0
  32. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/TRIAGE.md +0 -0
  33. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/.gitignore +0 -0
  34. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/_static/.gitkeep +0 -0
  35. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/ai-integration.md +0 -0
  36. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/commands.md +0 -0
  37. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/conf.py +0 -0
  38. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/configuration.md +0 -0
  39. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/item-data.md +0 -0
  40. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/docs/requirements.txt +0 -0
  41. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/scripts/bump_version.py +0 -0
  42. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/config.py +0 -0
  43. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/errors.py +0 -0
  44. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/filters.py +0 -0
  45. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/llm_models.py +0 -0
  46. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/services/ai.py +0 -0
  47. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/services/pomodoro.py +0 -0
  48. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/services/repair_database.py +0 -0
  49. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/services/user_setup.py +0 -0
  50. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/taskai/static/style.css +0 -0
  51. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/test/test_cli.py +0 -0
  52. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/test/test_execution.py +0 -0
  53. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/test/test_json_dir_database.py +0 -0
  54. {taskai_cli-1.8.0 → taskai_cli-1.9.0}/test/test_view.py +0 -0
@@ -1,3 +1,118 @@
1
+ # 9-14
2
+
3
+ Gave the browser DAG chain support (backend chain fields already existed,
4
+ frontend didn't know about them) and cleaned up `canvas.js` along the way.
5
+ Durable versions in DEVPLAN 1.8 (chains) and Phase 3 (frontend cleanup).
6
+
7
+ - **Layout rewrite.** Bottom-up subtree-width algorithm replaces the old
8
+ fixed-grid layout — a chain renders as a straight line, its real children
9
+ push into a row to the right instead of centering below it. Found and
10
+ fixed a real overlap bug from a screenshot: a chain node's footprint is
11
+ lopsided (all reach to the right), so layout now tracks separate
12
+ `left`/`right` extents per node instead of one symmetric width.
13
+ - **Navigation.** Arrow keys are pure tree nav (chain or not); `f`/`b`
14
+ (and `forward`/`back` from the console) are a separate chain axis along
15
+ `chainNext`/`chainPrev`. Split out after realizing one direction can't
16
+ mean "sibling," "child," and "next in chain" at once for a node that's
17
+ both a chain head and has its own tree children.
18
+ - **Frontend split.** `canvas.js` (one file, ~900 lines) is now
19
+ `config.js`/`graph.js`/`camera.js`/`navigation.js`/`render.js` plus a
20
+ slim `canvas.js` holding a `state` object and the event wiring — the
21
+ three panel scripts (`console.js`/`editpanel.js`/`shortcuts.js`) updated
22
+ to match.
23
+
24
+ # 9-10
25
+
26
+ Browser navigation polish, then a new feature: attribute filters for `task
27
+ show`. Durable versions in DEVPLAN 1.7 (nav) and Phase 5 (filters).
28
+
29
+ ## Canvas navigation (`canvas.js`)
30
+
31
+ - **Arrow left/right walks the whole depth level, not just direct siblings.**
32
+ The layout gives every node at a given depth the same `y`, so the "level" is
33
+ just `nodes` sharing `selectedNode.y` sorted by `x` — no explicit depth
34
+ bookkeeping. `→` off the last child of a parent now crosses into the next
35
+ cousin subtree instead of wrapping; wrap happens only at the ends of the
36
+ level (right off the last → level's first node, left off the first → last).
37
+ - **Arrow up/down has per-parent memory.** `lastChildByParent` (a plain
38
+ `parentId → childId` map, ids not refs since nodes are rebuilt each
39
+ `applyTree`) records every arrow move against its parent; `down` returns to
40
+ the last child visited under that node, falling back to `children[0]`. Also
41
+ keyed for the synthetic `__root__`, so `down` from the top re-enters the
42
+ root subtree you were last in. Stale ids just miss and fall back.
43
+ - **Removed up/down wrap-around.** `down` from a leaf and `up` from the top
44
+ now do nothing (were: wrap to rootNode / dive to the deepest leaf).
45
+ Left/right still wrap within a level.
46
+ - **`focusOnNode` recomputes its target every frame** from the current
47
+ `canvas.width/height` instead of once up front. A side panel opening in
48
+ step with the ease (the `a` shortcut, closing the edit panel) resizes the
49
+ canvas mid-animation; a target captured once left the node cut off at the
50
+ edge.
51
+ - **`fitAll` (show all / `0`) clamps zoom-in to `STYLE.zoom.focusScale`**
52
+ (0.85). A filtered `show` on a handful of nodes was blowing them up to fill
53
+ the screen; the user can still wheel in past that.
54
+ - **Double-click a node opens the edit panel** (+ selects + eases to it), not
55
+ just center/zoom.
56
+
57
+ ## Edit panel (`editpanel.js`) + shortcuts (`shortcuts.js`)
58
+
59
+ - **`Enter` on a single-line field** (name/status/priority/due_by) flushes the
60
+ 1s debounce immediately and blurs — the edit is done. `handleFieldChange`
61
+ got an `immediate` flag; textarea and checkbox are excluded.
62
+ - **`a` (add child): open the edit panel before `focusOnNode`** so the ease
63
+ tracks the narrowing canvas and lands centred (pairs with the per-frame
64
+ target change above).
65
+ - **`Esc` no longer moves the camera.** Was `showAll()` → `fitAll()`. Now:
66
+ leave a focused field → close the edit panel (with `focusOnNode` on the
67
+ selection so it re-centres as the canvas widens) → clear the selection.
68
+ Closing the panel with `e` routes through the same
69
+ `closeEditPanelAndRefocus()` helper, so `e` and `Esc` behave identically.
70
+ - **Backtick when the console is open but unfocused → focus the input**
71
+ instead of toggling it closed. Also bound `~`.
72
+ - **`Delete` shortcut: no more `confirm()` dialog** — it broke the flow.
73
+
74
+ ## `task show` attribute filters — new `taskai/filters.py`
75
+
76
+ task show 'status=IN PROGRESS' 'priority>=2' 'due_by<12-31-2026'
77
+
78
+ Implemented logic → CLI → browser, in that order.
79
+
80
+ - **Logic** (`filters.py`): each arg is one `attr<op>value` test, AND-ed. Ops
81
+ `= > < >= <=`. `=` on a text attr (`name`/`description`/`status`) is an
82
+ fnmatch; `priority`/`id`/`parent_id` → int, `due_by`/`created_on` → date
83
+ (`MM-DD-YYYY`, time dropped), `completed` → bool. `None` never satisfies an
84
+ ordering op. `looks_like_filters(tokens)` decides filter-query vs. plain
85
+ id/name target (structural: known attr + operator). `match_items(db, exprs)`
86
+ → matching ids; `with_ancestors(db, ids)` pads with every match's parent
87
+ chain so the result is a set of complete root-to-node paths. `FilterError`
88
+ (a `ValueError`) for a bad token/value.
89
+ - **Renderer** (`views.py`): `view_lists` gained `only_ids` — `_recursive_print`
90
+ and linked-item display skip anything not in the set. Ancestors are always
91
+ in the set, so the pruned walk stays a contiguous tree.
92
+ - **CLI** (`cli.py`): `Controller.show_filtered(tokens)` — parse, match,
93
+ expand, render pruned, print `"N items matched"`. `show` dispatch checks
94
+ `looks_like_filters` first, else the old `all` / id / name path. Fixed a
95
+ latent `IndexError` on bare `task show` in passing (`args[1]` with no
96
+ guard). Documented in `help_menu.py` (also the AI's command reference) and
97
+ the `docs/` prose pages (`managing-the-tree`, `getting-started`, `index`,
98
+ `interactive-mode`, `browser-mode`); `commands.md` tracks `help_general`
99
+ automatically.
100
+ - **Browser** (`browser.py` + `console.js`): `_run_show` routes a filter query
101
+ to `_run_filter` → a pruned tree (`_filtered_tree` narrows each item's
102
+ `child_ids`/`linked_ids` to the kept set so the canvas never walks into a
103
+ removed node) + a `filtered` flag. `console.js`'s `show` handler sees the
104
+ flag, fits the view, and stores the raw filter args in `activeFilter`.
105
+ - **Filter persistence.** `CommandRequest` gained an optional `filter` string;
106
+ `_response_tree()` re-prunes every mutation's returned tree to it. `console.js`
107
+ attaches `activeFilter` to every `/api/command` POST (via `postCommand`,
108
+ which `sendFieldUpdate`/`sendComment` now also use). So a filtered view
109
+ survives delete / done / field edits — it was snapping back to the whole
110
+ forest before. `show all` / navigating to a specific node clears it. Known
111
+ gap: `a` (add child) under an active filter creates the item but it won't
112
+ match the filter, so it doesn't appear or get selected.
113
+
114
+ ---
115
+
1
116
  # 9-3
2
117
 
3
118
  Browser session — console-driven navigation and selection, mostly beyond the
@@ -60,7 +60,14 @@ opening a second connection.
60
60
 
61
61
  **Views.** [taskai/views.py](taskai/views.py): Rich-based tree rendering
62
62
  (`view_lists`, `view_item`), display format configurable via
63
- `DISPLAY_STRING`/`DISPLAY_COLORS`.
63
+ `DISPLAY_STRING`/`DISPLAY_COLORS`. `view_lists` takes an optional `only_ids`
64
+ set to prune the walk (used by `task show`'s filters).
65
+
66
+ **Filters.** [taskai/filters.py](taskai/filters.py): parsing + evaluation for
67
+ `task show 'attr<op>value' ...` — `looks_like_filters` / `parse_filters`
68
+ (→ `FilterExpr`, `FilterError`), `match_items(db, exprs)`, and
69
+ `with_ancestors(db, ids)` which pads a match set out with parent chains so it
70
+ renders as a coherent tree. Pure; imported by both `cli.py` and `browser.py`.
64
71
 
65
72
  **AI services.** [taskai/services/ai.py](taskai/services/ai.py):
66
73
  `ai_headstart_service` (single item → LLM suggests a next concrete step,
@@ -404,14 +411,26 @@ the original checklist.
404
411
  `edit <id|name>`, mirroring the canvas's single-click vs. double-click
405
412
  split.
406
413
  - [x] **Tree navigation** — `navigate(direction)` in `canvas.js`, wired to
407
- `up` / `down` / `left` / `right` console commands. Nodes carry a
408
- `parent` back-reference (set in `buildTree` / `applyTree`) so the walk
409
- goes up as well as down: `down` → first child, `up` → parent,
410
- `left`/`right` → previous/next sibling. Wraps on both axes — past the
411
- last sibling loops to the first; `down` past the deepest leaf and `up`
412
- from `rootNode` loop between the top and the deepest first-child leaf.
413
- Each move behaves like a `show <target>` (select + ease/zoom; `fitAll`
414
- when the target is `rootNode`).
414
+ `up` / `down` / `left` / `right` console commands and the arrow keys.
415
+ Nodes carry a `parent` back-reference (set in `buildTree` / `applyTree`)
416
+ so the walk goes up as well as down. Each move behaves like a `show
417
+ <target>` (select + ease/zoom; `fitAll` when the target is `rootNode`).
418
+ Reworked 9-10:
419
+ - `up` → parent, `down` → a child. **No wrap** — `down` from a leaf and
420
+ `up` from the top do nothing (was: loop to rootNode / dive to the
421
+ deepest leaf).
422
+ - `down` consults **`lastChildByParent`** (a `parentId → childId` map
423
+ every arrow move updates) and returns to the last child visited under
424
+ that node, else `children[0]`. Also keyed for `__root__`.
425
+ - `left`/`right` step through the **whole depth level**, not just direct
426
+ siblings — the level is `nodes` sharing `selectedNode.y` (the layout
427
+ gives one `y` per depth) sorted by `x`, so `→` crosses into a cousin
428
+ subtree. Wrap only at the ends of the level.
429
+ - `focusOnNode` recomputes its target every frame from the live canvas
430
+ size, so an ease that overlaps a panel width animation still lands
431
+ centred. `fitAll` caps zoom-in at `STYLE.zoom.focusScale`.
432
+ - Double-click a node also opens the edit panel now; `Esc` no longer
433
+ moves the camera (leave field → close panel + re-centre → deselect).
415
434
  - [x] **`.` = the selected node** (`resolveDotRefs` in `console.js`). A bare
416
435
  `.` token in any console command is swapped for `selectedNode.id`
417
436
  before dispatch, so `update . --priority 3`, `done .`, `edit .` act on
@@ -422,6 +441,36 @@ the original checklist.
422
441
  collapsing the right panel and restoring canvas width. Complements the
423
442
  existing `edit <id|name>` that opens it.
424
443
 
444
+ ### 1.8 — Chain rendering & navigation (post-plan, beyond original scope)
445
+
446
+ The backend chain fields (`is_chain_head`/`prev_chain_id`/`next_chain_id`,
447
+ `task next`) existed with no browser support at all; this gives the DAG a
448
+ view onto them.
449
+
450
+ - [x] **Layout rewrite.** The old depth-first "leaf counter" layout
451
+ (`canvas.js`) is now a bottom-up subtree-width algorithm (`graph.js`,
452
+ `measure`/`place`) — every node's footprint is the sum of its
453
+ children's real footprints instead of a fixed grid, so sibling
454
+ subtrees pack at minimum distance instead of getting a fixed spacing
455
+ regardless of content.
456
+ - [x] **Chains render as a straight line.** A chain node's real children
457
+ (`child_ids`) push into a row to its right instead of centering below
458
+ it; its chain successor (`chainNext`) drops straight down, waiting
459
+ until that side row's height clears so nothing overlaps. Fixed a real
460
+ bug found via a screenshot: a chain node's footprint is lopsided (all
461
+ reach is to the right, none to the left), so layout tracks separate
462
+ `left`/`right` extents per node instead of one symmetric width —
463
+ otherwise a deep chain could bleed into an unrelated cousin subtree.
464
+ Chain edges render as bold arrows (`render.js`, `drawChainEdges`),
465
+ distinct from plain tree edges and dashed soft-link edges.
466
+ - [x] **Navigation.** Arrow keys stay pure tree navigation (up/down =
467
+ parent/children, left/right = the depth level) — a chain node's real
468
+ children are still its tree children for this purpose, they just
469
+ render to the side. `f`/`b` (`navigation.js`, `shortcuts.js`) are a
470
+ separate chain axis: forward/back along `chainNext`/`chainPrev`,
471
+ independent of tree position. Also usable from the console as
472
+ `forward`/`back`.
473
+
425
474
  ---
426
475
 
427
476
  ## Phase 2 — AI tools
@@ -580,11 +629,17 @@ Re-scope this section before starting 2.4.
580
629
 
581
630
  ## Phase 3 — Polish
582
631
 
583
- - [ ] **Frontend cleanup.** Once 1.3–1.6 are functional, split `app.js` into
584
- small single-purpose files (`api.js`, `dag.js`, `console.js`,
585
- `editmenu.js`) and delete anything exploratory left over from getting
586
- the DAG layout working. Still no bundler — just multiple `<script>`
587
- tags loaded in order.
632
+ - [x] **Frontend cleanup.** `canvas.js` split into `config.js` (`STYLE`),
633
+ `graph.js` (`Graph` class — tree/chain construction + layout),
634
+ `camera.js` (`Camera` class — pan/zoom/panel-width transform, never
635
+ holds a `Graph`, takes one as an argument where it needs one),
636
+ `navigation.js` (`navigateGraph`, pure aside from its own "last child
637
+ visited" memory), and `render.js` (drawing only). `canvas.js` itself is
638
+ now just the `state` object (`{graph, camera, selectedNode,
639
+ hoveredNode}`) plus DOM event wiring — the readable top-level flow.
640
+ `console.js`/`editpanel.js`/`shortcuts.js` read and act through `state`
641
+ instead of the old flat globals. Still no bundler — six `<script>` tags
642
+ loaded in dependency order.
588
643
  - [~] **Docs.** README is synced with `execute_commands` (command list,
589
644
  `task ai` flags, Web UI section, `task setup` walkthrough);
590
645
  `help_menu.py` lists `task browser` / `task help`, and its stale
@@ -616,10 +671,10 @@ Re-scope this section before starting 2.4.
616
671
  needs no edit — it `literalinclude`s the `builder-inited`-generated
617
672
  dump of `help_general`.
618
673
  - [ ] **UX pass.** Empty/loading state for the DAG when a user has no tasks
619
- yet (currently the app would just render nothing); a keyboard shortcut
620
- to toggle the console (e.g. backtick); consistent spacing/typography
621
- in `style.css`. (Edge legend dropped — depended on 1.4's dependency/
622
- link edges, now deferred to 2.0.)
674
+ yet (currently the app would just render nothing); consistent
675
+ spacing/typography in `style.css`. (Console-toggle keybind — done in
676
+ Phase 4's `shortcuts.js` (backtick). Edge legend dropped — depended on
677
+ 1.4's dependency/link edges, now deferred to 2.0.)
623
678
  - [ ] Re-check the Phase 0 fixes are still holding once the web UI is
624
679
  exercising more command paths than the CLI alone did.
625
680
 
@@ -715,16 +770,30 @@ endpoints.
715
770
  **`console.js` change:** extracted `toggleConsole(force)` (focus on
716
771
  open / blur on close), `showAll()`, and a `postCommand(input)` helper,
717
772
  all reused by `shortcuts.js`.
773
+ **Refinements (9-10):** `Delete` lost its `confirm()` (broke the flow);
774
+ `Esc` no longer moves the camera (leave field → close edit panel with a
775
+ re-centre → deselect), and `e` closing the panel shares that path;
776
+ backtick focuses the console input when the console is open but
777
+ unfocused rather than toggling it shut; double-click a node opens the
778
+ edit panel; `Enter` in a single-line edit-panel field commits + blurs.
779
+ `sendFieldUpdate` / `sendComment` moved onto `postCommand`, which now
780
+ also carries the active `show` filter (see Phase 5).
718
781
 
719
782
  ---
720
783
 
721
784
  ## Phase 5 — CLI view
722
785
 
723
- - [ ] **Expand what `task show` renders, and its options.** Exact scope TBD —
724
- needs a working session with Alex to pin down. Candidates: filter by
725
- status / priority / completion, depth limits, flat vs. tree layout,
726
- sort order, inline comments / links, format and colour toggles. Prompt
727
- before implementing.
786
+ - [~] **Expand what `task show` renders, and its options.** Attribute
787
+ **filtering landed early** (9-10), out of band, driven by a concrete
788
+ ask: `task show 'attr<op>value' ...` in `taskai/filters.py` —
789
+ AND-ed tests, ops `= > < >= <=`, `=` on text is fnmatch, matches shown
790
+ with their parent chain (`view_lists` gained `only_ids`). Wired through
791
+ the CLI (`Controller.show_filtered`, `show` dispatch) and the browser
792
+ (`_run_show` → `_run_filter` → pruned tree + `filtered` flag, plus a
793
+ persisted `activeFilter` on `/api/command` so mutations keep the view).
794
+ Still open, and still needs a session with Alex to scope: depth limits,
795
+ flat vs. tree layout, sort order, inline comments / links, format and
796
+ colour toggles.
728
797
  - [ ] **New view-settings config keys.** Extend `CLIConfig` (today just
729
798
  `DISPLAY_STRING` / `DISPLAY_COLORS`) with keys backing whatever the
730
799
  expanded view supports; document them in `docs/configuration.md` and
@@ -757,3 +826,121 @@ endpoints.
757
826
  - [ ] Write a short "show and tell" (what it is, the everything-is-an-item
758
827
  model, the AI layer, the canvas view) built around the gif, and decide
759
828
  on channels (Show HN, r/commandline, r/productivity, GitHub topics).
829
+
830
+ ---
831
+
832
+ ## Phase 8 — Chain CLI & data integrity
833
+
834
+ The chain data model (`is_chain_head`/`prev_chain_id`/`next_chain_id` on
835
+ `TodoItem`, `JsonDirectoryDatabase.insert_node_into_chain` /
836
+ `remove_node_from_chain` / `delete_chain`) and the browser rendering/nav for
837
+ it (DEVPLAN 1.8) already exist. This phase is CLI-side: finish the command
838
+ surface and fix the bugs found while designing it, so a chain is a fully
839
+ first-class thing a user can build, browse, and tear down from the terminal
840
+ — blocking the next release. Intended use cases: **(1)** define a sequential
841
+ chain of tasks, **(2)** delete individual items out of a chain — linked-list
842
+ style, the rest re-links around the hole — and **(3)** delete an entire
843
+ chain in one shot. A fourth, explicitly called out as a primary workflow:
844
+ navigate along an in-progress chain and `clear` away the items already
845
+ marked done as you go, tetris-style, leaving just the part still in
846
+ progress.
847
+
848
+ **Bugs to fix (foundational — the new commands below depend on these):**
849
+
850
+ - [x] **Deleting a chain head orphans the rest of the chain.**
851
+ `remove_node_from_chain`'s "node has only a next" branch (fires
852
+ whenever the item being removed from a chain is currently the head)
853
+ promotes the next link to head and copies the old head's `parent_id`
854
+ onto it, but never updates the actual parent's `child_ids` to swap the
855
+ deleted head's id for the new one. The parent keeps pointing at a
856
+ dead id; the rest of the chain becomes unreachable from the tree
857
+ (not anyone's child, not a root) even though the JSON records
858
+ survive. Fix this at the `remove_node_from_chain`/`delete_item` level
859
+ itself (not as a special case bolted onto one call site) — `clear`'s
860
+ item-by-item deletion loop and a plain `task delete <head>` both need
861
+ to go through this same path and come out correct.
862
+ - [x] **`_get_root_ids` (`cli.py`) doesn't exclude chain members.** It
863
+ filters on `parent_id is None` only, so `task show all` lists every
864
+ non-head chain link as a spurious top-level root. Add the same
865
+ `prev_chain_id is None` check the browser's `Graph` already uses.
866
+ - [x] **`insert_node_into_chain` doesn't detach `node` from a chain it's
867
+ already in**, and doesn't clear a stale `is_chain_head` on it. Not
868
+ hit today because the only caller (`next_node`) always hands it a
869
+ freshly-created item — becomes a real risk once `task chain` (below)
870
+ lets you splice two *existing* items together, possibly ones already
871
+ mid-chain.
872
+
873
+ **New/wired CLI commands:**
874
+
875
+ - [x] `task next <prev> <name>` — already exists (create + append), keep as
876
+ is. Fix in passing: `execute_commands`'s `case "next":` doesn't
877
+ forward `**kwargs` to `Controller.next_node`, so e.g. `--due_by` on
878
+ `task next` is silently dropped — same landmine class as the old
879
+ `task ai --context` bug (DEVLOG 8-22). Thread `**kwargs` through like
880
+ `case "ai":` was fixed to do.
881
+ - [x] **`task chain <prev_id> <node_id>`** — new dispatch wiring the
882
+ already-written, currently-unreachable `Controller.insert_node_as_chain`.
883
+ Links two *existing* items. Per Alex: **do not refuse if `node`
884
+ already has a parent — pop it off that parent (remove from
885
+ `child_ids`, clear `parent_id`) and splice it into the chain
886
+ instead.** This is the "malleable graph" call: moving a node from
887
+ tree-child to chain-member is one command, not detach-then-chain.
888
+ `node`'s own `child_ids` are untouched either way — a chain member is
889
+ allowed real children of its own (they render to the side; see
890
+ DEVPLAN 1.8), only its *parent* relationship is exclusive between
891
+ "tree child" and "chain member."
892
+ - [x] **`task unchain <node_id>`** — new dispatch wiring the
893
+ already-written, currently-unreachable `Controller.remove_node_as_chain`.
894
+ Detaches one item from its chain without deleting it. Per Alex:
895
+ **symmetric to `chain` above** — after splicing `node` out
896
+ (`remove_node_from_chain`), add it as a child of *the chain head's
897
+ parent* (walk `prev_chain_id` to the true head, read that head's
898
+ `parent_id`, `add_child_to_parent` there; if the head is itself a
899
+ root, `node` becomes a root too). Pulls it out of the sequence and
900
+ into the parent's set of children. A no-op on the reparenting step if
901
+ `node` being unchained *is* the head — it's already that parent's
902
+ child.
903
+ - [x] **`task delete <id> -chain`** — new flag on the existing `delete`
904
+ command (same convention as `complete`/`done`'s `-r`/`-recursive`),
905
+ wiring the already-written, currently-unreachable `db.delete_chain`.
906
+ Deletes the *entire* chain `id` belongs to, not just `id` onward —
907
+ walk `prev_chain_id` back to the true head first, so it works no
908
+ matter which link you target, then cascade forward and detach the
909
+ head from its tree parent the normal way.
910
+ - [x] **`clear` needs to actually reach chain members.**
911
+ `Controller._flatten_item_descendants` (used by `task clear <parent>`
912
+ to scope which completed items to sweep) walks `child_ids` only. A
913
+ chain's non-head members are never in anyone's `child_ids` — they're
914
+ only reachable by walking `next_chain_id` from the head — so today
915
+ `task clear <chain-head-or-ancestor>` silently misses every
916
+ completed item past the head. Extend the walk to also follow
917
+ `next_chain_id` (and recurse into each chain member's own
918
+ `child_ids`, same as the browser's `flatten`/`buildTree` do). Bare
919
+ `task clear` (no parent) already iterates every item id in the db, so
920
+ it isn't blind to chain members the same way — it just needs the
921
+ head-deletion bug fixed above so clearing a *done* head doesn't
922
+ orphan the still-in-progress rest of the chain out from under itself.
923
+ This is the primary use case Alex called out: navigate along a chain,
924
+ `clear` the done items as you pass them, tetris-style, and be left
925
+ with just the in-progress remainder correctly re-linked (and, if the
926
+ head itself was cleared, correctly reattached to the tree parent).
927
+
928
+ **Data structuring:**
929
+
930
+ - [x] **Drop the stored `is_chain_head` field, derive it instead**
931
+ (`prev_chain_id is None and next_chain_id is not None`). It's exactly
932
+ the field that's gone out of sync in the bugs above, the frontend
933
+ already doesn't use it (uses `chainNext`/`chainPrev` presence
934
+ instead — DEVPLAN 1.8), and removing it is backward-compatible
935
+ (Pydantic silently ignores the leftover key in old JSON — no
936
+ migration needed). Replace the one real remaining consumer,
937
+ `delete_item`'s cascade check
938
+ (`get_item_attr(child_id, "is_chain_head")`), with the derived check.
939
+
940
+ **Docs:** [x] `next`/`chain`/`unchain`/`delete -chain` are now documented in
941
+ [taskai/help_menu.py](taskai/help_menu.py) (a new "Chains:" section, an
942
+ updated "Deleting:" section, and a worked example) — which is also
943
+ `task ai`'s verbatim command reference, so the AI can use all of this too
944
+ now.
945
+
946
+ Phase 8 is complete.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: taskai-cli
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Author-email: Alex Paskal <alexcpaskal@gmail.com>
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: fastapi>=0.141.1
@@ -0,0 +1,51 @@
1
+ # Browser mode
2
+
3
+ ```bash
4
+ task browser
5
+ ```
6
+
7
+ This starts a local web server and serves a visual view of your task tree.
8
+
9
+ ## The canvas
10
+
11
+ The whole tree is drawn as connected cards. Drag to pan, scroll to zoom.
12
+ Double-click a card to center on it and open its edit panel.
13
+
14
+ - **Completed** items are tinted green.
15
+ - **`status`** text appears in the top corner of a card.
16
+ - **Soft links** (`task link`) are drawn as a dashed copy of the linked item
17
+ under its host, distinct from the solid parent/child connectors.
18
+
19
+ ## The edit panel
20
+
21
+ Click a card to select it; double-click it, or press `e`, to open the edit
22
+ panel on the right. It has a field for the name, description, status, priority,
23
+ due date, and completion, plus a comment feed. Changes are saved as you make
24
+ them — there is no save button. In a single-line field, `Enter` commits the
25
+ change and leaves the field.
26
+
27
+ ## The console
28
+
29
+ The panel at the bottom takes raw `task` commands and runs them against the
30
+ same data, printing their output and refreshing the canvas. Anything you can
31
+ type on the command line works here.
32
+
33
+ Filtering sticks: after `show 'priority>=2'` the canvas shows only the matches
34
+ (and their parents), and it stays scoped that way as you edit and delete —
35
+ until `show all` clears it.
36
+
37
+ ## Keyboard
38
+
39
+ Press `?` for the full list in the app. The essentials:
40
+
41
+ | Key | Action |
42
+ |---|---|
43
+ | arrows | move the selection through the tree |
44
+ | `Shift`+arrows | pan the view |
45
+ | `+` / `-` / `0` | zoom in / out / fit |
46
+ | `e` | toggle the edit panel |
47
+ | `` ` `` | focus / close the console |
48
+ | `a` | add a child of the selected card and name it |
49
+ | `d` | toggle done on the selected card |
50
+ | `Delete` | delete the selected card |
51
+ | `Esc` | leave a field, then close the panel, then clear the selection |
@@ -82,6 +82,15 @@ task show "Write first post"
82
82
  task show 2
83
83
  ```
84
84
 
85
+ Or narrow the tree to items matching a set of attribute tests (each shown with
86
+ its parents for context):
87
+
88
+ ```bash
89
+ task show 'priority>=2' 'completed=false'
90
+ ```
91
+
92
+ See [Managing the tree](managing-the-tree.md) for the operators.
93
+
85
94
  ## Work an item and finish it
86
95
 
87
96
  ```bash
@@ -13,6 +13,8 @@ A command-line task manager with AI features.
13
13
  items at any depth; there's no separate notion of "lists"
14
14
  - **Attach data to each item** — description, due date, priority, status,
15
15
  completion, comments
16
+ - **Filter** — `task show 'priority>=2' 'due_by<10-01-2026'` narrows the tree to
17
+ matching items and their parents
16
18
  - **Soft-link** an item under more than one parent without moving it
17
19
  - **Interactive mode** — run `task` with no arguments for a live view that
18
20
  redraws as you type commands
@@ -18,6 +18,7 @@ redraws with the updated tree.
18
18
 
19
19
  - A `show` command sets the view that stays on screen — `task show "Launch
20
20
  blog"` keeps that subtree visible as you keep working; `show all` goes back to
21
- the whole tree.
21
+ the whole tree. A [filter](managing-the-tree.md) such as `show 'priority>=2'`
22
+ works here too, and stays applied as the view redraws.
22
23
  - Every other command runs, then the view refreshes.
23
24
  - Leave with `exit`, `quit`, or Ctrl-C.
@@ -32,6 +32,22 @@ task show 4 # one item in full, by id
32
32
  task show "Set up hosting" # one item in full, by name
33
33
  ```
34
34
 
35
+ ## Filter the tree
36
+
37
+ Give `task show` one or more `attribute<operator>value` tests and it prints only
38
+ the items matching *all* of them, each with its parent chain for context:
39
+
40
+ ```bash
41
+ task show 'name=Write*' # = on text is a glob match
42
+ task show 'priority>=2' 'completed=false'
43
+ task show 'status=in review' 'due_by<10-01-2026'
44
+ ```
45
+
46
+ Operators are `=`, `>`, `<`, `>=`, `<=`. Text fields (`name`, `description`,
47
+ `status`) glob-match on `=`; `priority`, `due_by`, `created_on`, and `id`
48
+ compare numerically / by date. Put no space around the operator, and quote each
49
+ test so your shell doesn't treat `>` and `<` as redirects.
50
+
35
51
  ## Move items
36
52
 
37
53
  ```bash
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskai-cli"
3
- version = "1.8.0"
3
+ version = "1.9.0"
4
4
  description = ""
5
5
  authors = [{name = "Alex Paskal", email = "alexcpaskal@gmail.com"}]
6
6
  readme = "README.md"
@@ -81,6 +81,25 @@ def get_tree():
81
81
 
82
82
  class CommandRequest(BaseModel):
83
83
  input: str
84
+ # raw filter args from the last `show <attr><op>value ...` the frontend
85
+ # ran, if any — so a mutation's refreshed tree stays scoped to that view
86
+ # instead of snapping back to the whole forest
87
+ filter: str | None = None
88
+
89
+
90
+ def _response_tree(filter_str):
91
+ """The tree a mutation should return: pruned to `filter_str`'s matches
92
+ (+ their parent chains) when one is active, else the whole forest."""
93
+ if not filter_str:
94
+ return _full_tree()
95
+ tokens = _parse_arg_string(filter_str)
96
+ if not looks_like_filters(tokens):
97
+ return _full_tree()
98
+ try:
99
+ exprs = parse_filters(tokens)
100
+ except FilterError:
101
+ return _full_tree()
102
+ return _filtered_tree(with_ancestors(db, match_items(db, exprs)))
84
103
 
85
104
 
86
105
  @app.post("/api/command")
@@ -89,7 +108,7 @@ def run_command(request: CommandRequest):
89
108
  args, kwargs = _parse_remaining(arg_parts)
90
109
 
91
110
  if not args:
92
- return {"output": "", "tree": _full_tree(), "focus": None}
111
+ return {"output": "", "tree": _response_tree(request.filter), "focus": None}
93
112
 
94
113
  if args[0] == "show":
95
114
  return _run_show(args)
@@ -101,7 +120,7 @@ def run_command(request: CommandRequest):
101
120
  except TaskCLIError:
102
121
  pass # Controller.throw_error() already printed the error into `output`
103
122
 
104
- return {"output": output.getvalue(), "tree": _full_tree(), "focus": None}
123
+ return {"output": output.getvalue(), "tree": _response_tree(request.filter), "focus": None}
105
124
 
106
125
 
107
126
  def _run_show(args):