taskai-cli 1.8.0__tar.gz → 1.8.1__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 (47) hide show
  1. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/PKG-INFO +1 -1
  2. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/pyproject.toml +1 -1
  3. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/browser.py +21 -2
  4. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/canvas.js +9 -4
  5. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/console.js +13 -7
  6. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/editpanel.js +2 -12
  7. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/shortcuts.js +21 -7
  8. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.github/workflows/publish-to-pypi.yml +0 -0
  9. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.gitignore +0 -0
  10. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.readthedocs.yaml +0 -0
  11. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/DEVLOG.md +0 -0
  12. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/DEVPLAN.md +0 -0
  13. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/README.md +0 -0
  14. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/TRIAGE.md +0 -0
  15. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/.gitignore +0 -0
  16. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/_static/.gitkeep +0 -0
  17. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/ai-integration.md +0 -0
  18. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/browser-mode.md +0 -0
  19. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/commands.md +0 -0
  20. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/conf.py +0 -0
  21. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/configuration.md +0 -0
  22. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/getting-started.md +0 -0
  23. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/index.md +0 -0
  24. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/interactive-mode.md +0 -0
  25. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/item-data.md +0 -0
  26. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/managing-the-tree.md +0 -0
  27. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/requirements.txt +0 -0
  28. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/scripts/bump_version.py +0 -0
  29. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/cli.py +0 -0
  30. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/config.py +0 -0
  31. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/errors.py +0 -0
  32. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/filters.py +0 -0
  33. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/help_menu.py +0 -0
  34. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/json_dir_database.py +0 -0
  35. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/llm_models.py +0 -0
  36. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/models.py +0 -0
  37. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/ai.py +0 -0
  38. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/pomodoro.py +0 -0
  39. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/repair_database.py +0 -0
  40. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/user_setup.py +0 -0
  41. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/index.html +0 -0
  42. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/style.css +0 -0
  43. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/views.py +0 -0
  44. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_cli.py +0 -0
  45. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_execution.py +0 -0
  46. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_json_dir_database.py +0 -0
  47. {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: taskai-cli
3
- Version: 1.8.0
3
+ Version: 1.8.1
4
4
  Author-email: Alex Paskal <alexcpaskal@gmail.com>
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: fastapi>=0.141.1
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskai-cli"
3
- version = "1.8.0"
3
+ version = "1.8.1"
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):
@@ -403,7 +403,9 @@ function fitAll(duration = STYLE.zoom.focusDurationMs) {
403
403
 
404
404
  const pad = 60; // screen px of breathing room around the content
405
405
  const scale = Math.min(
406
- STYLE.zoom.max,
406
+ // don't auto-zoom in past the focus threshold — a small subset
407
+ // shouldn't fill the screen; the user can still wheel in past this
408
+ STYLE.zoom.focusScale,
407
409
  Math.max(
408
410
  STYLE.zoom.min,
409
411
  Math.min(
@@ -741,10 +743,13 @@ canvas.addEventListener("dblclick", (e) => {
741
743
  const rect = canvas.getBoundingClientRect();
742
744
  const { x, y } = screenToWorld(e.clientX - rect.left, e.clientY - rect.top);
743
745
  const clicked = nodes.find(node => hitTest(node, x, y));
746
+ if (!clicked) return;
744
747
 
745
- if (clicked) {
746
- focusOnNode(clicked);
747
- }
748
+ // single click already selected it; double click also opens the editor
749
+ selectedNode = clicked;
750
+ if (typeof onNodeSelected === "function") onNodeSelected(itemForNode(clicked));
751
+ if (typeof openEditPanel === "function") openEditPanel();
752
+ focusOnNode(clicked); // after openEditPanel so the ease tracks the narrowed canvas
748
753
  })
749
754
 
750
755
  canvas.addEventListener("mousedown", (e) => {
@@ -29,6 +29,12 @@ function appendLine(text) {
29
29
  consoleScrollback.scrollTop = consoleScrollback.scrollHeight;
30
30
  }
31
31
 
32
+ // raw filter args from the last `show <attr><op>value ...` query (e.g.
33
+ // `priority>3 status="IN PROGRESS"`), or null. Sent with every /api/command
34
+ // POST so a mutation's refreshed tree stays scoped to the same filtered view.
35
+ // Cleared by `show all` and by navigating to a specific node.
36
+ let activeFilter = null;
37
+
32
38
  const PAN_AMOUNT = 250; // screen pixels per pan command
33
39
  const ZOOM_FACTOR = 1.5;
34
40
 
@@ -62,10 +68,12 @@ function resolveDotRefs(command) {
62
68
  // POST a raw command string to the shared endpoint; returns the parsed
63
69
  // { output, tree, focus } (or throws). Callers decide what to echo.
64
70
  async function postCommand(input) {
71
+ const body = { input };
72
+ if (activeFilter) body.filter = activeFilter;
65
73
  const res = await fetch("/api/command", {
66
74
  method: "POST",
67
75
  headers: { "Content-Type": "application/json" },
68
- body: JSON.stringify({ input }),
76
+ body: JSON.stringify(body),
69
77
  });
70
78
  return res.json();
71
79
  }
@@ -82,6 +90,7 @@ async function showAll() {
82
90
  return;
83
91
  }
84
92
  applyTree(tree);
93
+ activeFilter = null; // `show all` drops any filter
85
94
  selectedNode = rootNode;
86
95
  if (typeof onNodeSelected === "function") onNodeSelected(null);
87
96
  fitAll();
@@ -153,6 +162,7 @@ consoleInput.addEventListener("keydown", async (e) => {
153
162
 
154
163
  // `show <attr><op><value> ...` — server returns a pruned tree + a flag
155
164
  if (data.filtered) {
165
+ activeFilter = target; // remember it so mutations keep this view
156
166
  selectedNode = rootNode;
157
167
  if (typeof onNodeSelected === "function") onNodeSelected(null);
158
168
  fitAll();
@@ -161,6 +171,7 @@ consoleInput.addEventListener("keydown", async (e) => {
161
171
  }
162
172
 
163
173
  if (data.focus) {
174
+ activeFilter = null; // navigated to a specific node — no longer filtered
164
175
  const node = nodes.find(n => n.id === String(data.focus));
165
176
  if (node) {
166
177
  selectedNode = node;
@@ -208,12 +219,7 @@ consoleInput.addEventListener("keydown", async (e) => {
208
219
 
209
220
  let data;
210
221
  try {
211
- const res = await fetch("/api/command", {
212
- method: "POST",
213
- headers: { "Content-Type": "application/json" },
214
- body: JSON.stringify({ input: command }),
215
- });
216
- data = await res.json();
222
+ data = await postCommand(command);
217
223
  } catch (err) {
218
224
  appendLine("Error: " + err.message);
219
225
  return;
@@ -64,12 +64,7 @@ async function sendFieldUpdate(item, def, value) {
64
64
 
65
65
  let data;
66
66
  try {
67
- const res = await fetch("/api/command", {
68
- method: "POST",
69
- headers: { "Content-Type": "application/json" },
70
- body: JSON.stringify({ input: command }),
71
- });
72
- data = await res.json();
67
+ data = await postCommand(command); // carries the active filter, if any
73
68
  } catch (err) {
74
69
  appendLine(`Error updating item ${item.id}: ${err.message}`);
75
70
  return;
@@ -87,12 +82,7 @@ async function sendComment(itemId, text) {
87
82
 
88
83
  let data;
89
84
  try {
90
- const res = await fetch("/api/command", {
91
- method: "POST",
92
- headers: { "Content-Type": "application/json" },
93
- body: JSON.stringify({ input: command }),
94
- });
95
- data = await res.json();
85
+ data = await postCommand(command); // carries the active filter, if any
96
86
  } catch (err) {
97
87
  appendLine(`Error adding comment to item ${itemId}: ${err.message}`);
98
88
  return;
@@ -31,10 +31,19 @@ function toggleShortcutPanel(force) {
31
31
  shortcutToggle.addEventListener("click", () => toggleShortcutPanel());
32
32
 
33
33
  function toggleEditPanel() {
34
- if (editPanel.classList.contains("expanded")) closeEditPanel();
34
+ if (editPanel.classList.contains("expanded")) closeEditPanelAndRefocus();
35
35
  else openEditPanel();
36
36
  }
37
37
 
38
+ // close the edit panel, then ease the selected node back to center as the
39
+ // canvas widens out — without this the panel's width animation re-anchors on
40
+ // the old center and leaves the node cut off at the edge
41
+ function closeEditPanelAndRefocus() {
42
+ closeEditPanel();
43
+ const node = selectedRealNode();
44
+ if (node) focusOnNode(node);
45
+ }
46
+
38
47
  // --- action helpers --------------------------------------------------------
39
48
 
40
49
  // the selected node, or null when the selection is the synthetic root
@@ -69,9 +78,6 @@ function toggleDoneSelected() {
69
78
  function deleteSelected() {
70
79
  const node = selectedRealNode();
71
80
  if (!node) return;
72
- const item = itemForNode(node);
73
- const label = item ? item.name : node.id;
74
- if (!window.confirm(`Delete "${label}" and everything under it?`)) return;
75
81
  runMutation(`delete ${node.id}`);
76
82
  }
77
83
 
@@ -115,6 +121,10 @@ function handleEscape() {
115
121
  el.blur();
116
122
  return;
117
123
  }
124
+ if (editPanel.classList.contains("expanded")) {
125
+ closeEditPanelAndRefocus();
126
+ return;
127
+ }
118
128
  // deselect, but leave the camera where it is — Esc shouldn't move/zoom
119
129
  selectedNode = rootNode;
120
130
  if (typeof onNodeSelected === "function") onNodeSelected(null);
@@ -130,9 +140,13 @@ const PAN_VEC = { ArrowUp: [0, 1], ArrowDown: [0, -1], ArrowLeft: [1, 0], ArrowR
130
140
  const SHORTCUTS = [
131
141
  { section: "Panels" },
132
142
  {
133
- combos: [{ key: "`" }], glyphs: ["`"], desc: "Toggle terminal",
143
+ combos: [{ key: "`" }, { key: "~" }], glyphs: ["`"], desc: "Terminal (focus / close)",
134
144
  whileTyping: (el) => el === consoleInput, // also closes it from inside
135
- run: () => toggleConsole(),
145
+ run: () => {
146
+ if (document.activeElement === consoleInput) toggleConsole(false); // close from inside
147
+ else if (consolePanel.classList.contains("expanded")) consoleInput.focus(); // open but unfocused -> jump in
148
+ else toggleConsole(true); // closed -> open (focuses input)
149
+ },
136
150
  },
137
151
  {
138
152
  combos: [{ key: "?" }], glyphs: ["?"], desc: "Toggle this panel",
@@ -185,7 +199,7 @@ const SHORTCUTS = [
185
199
  run: () => toggleDoneSelected(),
186
200
  },
187
201
  {
188
- combos: [{ key: "Delete" }], glyphs: ["Del"], desc: "Delete (asks first)",
202
+ combos: [{ key: "Delete" }], glyphs: ["Del"], desc: "Delete",
189
203
  run: () => deleteSelected(),
190
204
  },
191
205
  ];
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes