taskai-cli 1.7.0__tar.gz → 1.7.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 (46) hide show
  1. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/PKG-INFO +1 -1
  2. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/pyproject.toml +1 -1
  3. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/canvas.js +40 -19
  4. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/editpanel.js +22 -1
  5. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/shortcuts.js +8 -3
  6. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.github/workflows/publish-to-pypi.yml +0 -0
  7. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.gitignore +0 -0
  8. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.readthedocs.yaml +0 -0
  9. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/DEVLOG.md +0 -0
  10. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/DEVPLAN.md +0 -0
  11. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/README.md +0 -0
  12. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/TRIAGE.md +0 -0
  13. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/.gitignore +0 -0
  14. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/_static/.gitkeep +0 -0
  15. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/ai-integration.md +0 -0
  16. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/browser-mode.md +0 -0
  17. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/commands.md +0 -0
  18. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/conf.py +0 -0
  19. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/configuration.md +0 -0
  20. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/getting-started.md +0 -0
  21. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/index.md +0 -0
  22. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/interactive-mode.md +0 -0
  23. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/item-data.md +0 -0
  24. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/managing-the-tree.md +0 -0
  25. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/requirements.txt +0 -0
  26. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/scripts/bump_version.py +0 -0
  27. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/browser.py +0 -0
  28. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/cli.py +0 -0
  29. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/config.py +0 -0
  30. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/errors.py +0 -0
  31. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/help_menu.py +0 -0
  32. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/json_dir_database.py +0 -0
  33. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/llm_models.py +0 -0
  34. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/models.py +0 -0
  35. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/ai.py +0 -0
  36. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/pomodoro.py +0 -0
  37. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/repair_database.py +0 -0
  38. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/user_setup.py +0 -0
  39. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/console.js +0 -0
  40. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/index.html +0 -0
  41. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/style.css +0 -0
  42. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/views.py +0 -0
  43. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_cli.py +0 -0
  44. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_execution.py +0 -0
  45. {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_json_dir_database.py +0 -0
  46. {taskai_cli-1.7.0 → taskai_cli-1.7.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.7.0
3
+ Version: 1.7.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.7.0"
3
+ version = "1.7.1"
4
4
  description = ""
5
5
  authors = [{name = "Alex Paskal", email = "alexcpaskal@gmail.com"}]
6
6
  readme = "README.md"
@@ -327,9 +327,6 @@ function focusOnNode(node, scale = STYLE.zoom.focusScale, duration = STYLE.zoom.
327
327
  const startOffsetY = view.offsetY;
328
328
  const startScale = view.scale;
329
329
 
330
- const targetOffsetX = canvas.width / 2 - node.x * scale;
331
- const targetOffsetY = canvas.height * STYLE.zoom.focusYRatio - node.y * scale;
332
-
333
330
  const startTime = performance.now();
334
331
 
335
332
  function step(now) {
@@ -337,6 +334,15 @@ function focusOnNode(node, scale = STYLE.zoom.focusScale, duration = STYLE.zoom.
337
334
  const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
338
335
 
339
336
  view.scale = startScale + (scale - startScale) * eased;
337
+
338
+ // recompute the target each frame from the current canvas size: the
339
+ // canvas can be resizing under us when a side panel opens in step with
340
+ // this animation (e.g. the `a` add-node shortcut), and a target
341
+ // captured once up front would leave the node off-centre by half the
342
+ // width change — often hidden behind the panel that just opened.
343
+ const targetOffsetX = canvas.width / 2 - node.x * scale;
344
+ const targetOffsetY = canvas.height * STYLE.zoom.focusYRatio - node.y * scale;
345
+
340
346
  view.offsetX = startOffsetX + (targetOffsetX - startOffsetX) * eased;
341
347
  view.offsetY = startOffsetY + (targetOffsetY - startOffsetY) * eased;
342
348
 
@@ -412,35 +418,50 @@ function fitAll(duration = STYLE.zoom.focusDurationMs) {
412
418
  easeView(canvas.width / 2 - cx * scale, canvas.height / 2 - cy * scale, scale, duration);
413
419
  }
414
420
 
421
+ // per-parent memory of the last child navigated to, keyed by parent id (node
422
+ // objects are rebuilt on every applyTree, so ids not references). Lets `down`
423
+ // return to where you last were under a parent; stale ids are harmless — the
424
+ // lookup just misses and falls back to the first child.
425
+ const lastChildByParent = {};
426
+
415
427
  // moves the selection relative to the current node along the node tree.
416
428
  // The move is equivalent to a `show <target>`: the node becomes selected and
417
- // the view eases + zooms to it (focusOnNode). Wraps around on both axes —
418
- // past the last sibling loops to the first, past the bottom leaf loops back
419
- // to rootNode (the top), and `up` from rootNode drops to the deepest node.
429
+ // the view eases + zooms to it (focusOnNode). up/down don't wrap — off the
430
+ // top or past a leaf does nothing; only left/right wrap, looping to the
431
+ // first (or last) node of the current depth level.
420
432
  function navigate(direction) {
421
433
  const cur = selectedNode || rootNode;
422
434
  let target = null;
423
435
 
424
436
  if (direction === "down") {
425
- target = cur.children[0] || rootNode; // past the bottom -> wrap to the top
437
+ if (!cur.children.length) return; // at a leaf -> nothing below
438
+ // return to the last child visited under `cur`, else its first child
439
+ const remembered = lastChildByParent[cur.id];
440
+ target = cur.children.find(c => c.id === remembered) || cur.children[0];
426
441
  } else if (direction === "up") {
427
- if (cur.parent) {
428
- target = cur.parent;
429
- } else {
430
- // at rootNode (the top) -> wrap to the bottom: follow the
431
- // first-child chain down to the deepest leaf
432
- target = cur;
433
- while (target.children[0]) target = target.children[0];
434
- }
442
+ if (!cur.parent) return; // at the top -> nothing above
443
+ target = cur.parent;
435
444
  } else if (direction === "left" || direction === "right") {
436
- const sibs = cur.parent && cur.parent.children;
437
- if (!sibs || !sibs.length) return;
438
- const n = sibs.length;
439
- target = sibs[(sibs.indexOf(cur) + (direction === "right" ? 1 : -1) + n) % n];
445
+ // step to the node immediately left/right at the same depth, across
446
+ // the whole level — so you cross into a cousin subtree rather than
447
+ // wrapping inside the current parent. The layout gives every node at a
448
+ // given depth the same y, so the level is just "nodes sharing cur.y"
449
+ // sorted by x; no explicit depth/level bookkeeping. Wrap to the first
450
+ // (or last) node of the level only when you run off its end.
451
+ const row = nodes
452
+ .filter(n => Math.abs(n.y - cur.y) < 1)
453
+ .sort((a, b) => a.x - b.x);
454
+ const i = row.indexOf(cur);
455
+ if (i === -1 || row.length < 2) return;
456
+ const step = direction === "right" ? 1 : -1;
457
+ target = row[(i + step + row.length) % row.length];
440
458
  }
441
459
 
442
460
  if (!target || target === cur) return;
443
461
 
462
+ // remember this child so a later `down` into its parent returns here
463
+ if (target.parent) lastChildByParent[target.parent.id] = target.id;
464
+
444
465
  selectedNode = target;
445
466
  if (typeof onNodeSelected === "function") onNodeSelected(itemForNode(target));
446
467
  if (target === rootNode) fitAll();
@@ -102,7 +102,7 @@ async function sendComment(itemId, text) {
102
102
  applyTree(data.tree);
103
103
  }
104
104
 
105
- function handleFieldChange(item, def, value) {
105
+ function handleFieldChange(item, def, value, immediate = false) {
106
106
  if (def.type === "checkbox") {
107
107
  sendFieldUpdate(item, def, value ? "true" : "false");
108
108
  return;
@@ -115,6 +115,15 @@ function handleFieldChange(item, def, value) {
115
115
 
116
116
  const timerKey = `${item.id}:${def.key}`;
117
117
  clearTimeout(pendingFieldUpdates[timerKey]);
118
+
119
+ // Enter (see buildField) commits right away rather than waiting out the
120
+ // debounce — the edit is done, no reason to sit on it
121
+ if (immediate) {
122
+ delete pendingFieldUpdates[timerKey];
123
+ sendFieldUpdate(item, def, value);
124
+ return;
125
+ }
126
+
118
127
  pendingFieldUpdates[timerKey] = setTimeout(() => {
119
128
  delete pendingFieldUpdates[timerKey];
120
129
  sendFieldUpdate(item, def, value);
@@ -173,6 +182,18 @@ function buildField(item, def) {
173
182
  handleFieldChange(item, def, value);
174
183
  });
175
184
 
185
+ // Enter on a single-line field ends the edit: flush the pending update
186
+ // now and blur. Skipped for the textarea (Enter is a newline there) and
187
+ // the checkbox (toggled with space, no debounce to flush).
188
+ if (def.type !== "textarea" && def.type !== "checkbox") {
189
+ input.addEventListener("keydown", (e) => {
190
+ if (e.key !== "Enter") return;
191
+ e.preventDefault();
192
+ handleFieldChange(item, def, input.value, true);
193
+ input.blur();
194
+ });
195
+ }
196
+
176
197
  return wrapper;
177
198
  }
178
199
 
@@ -91,8 +91,10 @@ async function addNodeAndEdit() {
91
91
 
92
92
  selectedNode = node;
93
93
  if (typeof onNodeSelected === "function") onNodeSelected(itemForNode(node));
94
- focusOnNode(node);
94
+ // open the panel first so focusOnNode eases into the already-narrowing
95
+ // canvas and its per-frame target tracks the new width (see focusOnNode)
95
96
  if (typeof openEditPanel === "function") openEditPanel();
97
+ focusOnNode(node);
96
98
 
97
99
  requestAnimationFrame(() => {
98
100
  const nameInput = document.querySelector('#edit-panel [data-field-key="name"]');
@@ -113,7 +115,10 @@ function handleEscape() {
113
115
  el.blur();
114
116
  return;
115
117
  }
116
- showAll(); // deselects (selectedNode = rootNode) and fits the whole forest
118
+ // deselect, but leave the camera where it is — Esc shouldn't move/zoom
119
+ selectedNode = rootNode;
120
+ if (typeof onNodeSelected === "function") onNodeSelected(null);
121
+ draw();
117
122
  }
118
123
 
119
124
  // --- registry ------------------------------------------------------------
@@ -138,7 +143,7 @@ const SHORTCUTS = [
138
143
  run: () => toggleEditPanel(),
139
144
  },
140
145
  {
141
- combos: [{ key: "Escape" }], glyphs: ["Esc"], desc: "Leave field · deselect · show all",
146
+ combos: [{ key: "Escape" }], glyphs: ["Esc"], desc: "Leave field · deselect",
142
147
  whileTyping: true, keepDefault: true,
143
148
  run: handleEscape,
144
149
  },
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