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.
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/PKG-INFO +1 -1
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/pyproject.toml +1 -1
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/canvas.js +40 -19
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/editpanel.js +22 -1
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/shortcuts.js +8 -3
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.gitignore +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/.readthedocs.yaml +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/DEVLOG.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/DEVPLAN.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/README.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/TRIAGE.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/.gitignore +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/_static/.gitkeep +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/ai-integration.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/browser-mode.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/commands.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/conf.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/configuration.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/getting-started.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/index.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/interactive-mode.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/item-data.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/managing-the-tree.md +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/docs/requirements.txt +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/scripts/bump_version.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/browser.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/cli.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/config.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/errors.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/help_menu.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/llm_models.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/models.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/ai.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/console.js +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/index.html +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/static/style.css +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/taskai/views.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_cli.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_execution.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.7.0 → taskai_cli-1.7.1}/test/test_view.py +0 -0
|
@@ -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).
|
|
418
|
-
// past
|
|
419
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|