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.
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/PKG-INFO +1 -1
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/pyproject.toml +1 -1
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/browser.py +21 -2
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/canvas.js +9 -4
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/console.js +13 -7
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/editpanel.js +2 -12
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/shortcuts.js +21 -7
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.gitignore +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/.readthedocs.yaml +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/DEVLOG.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/DEVPLAN.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/README.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/TRIAGE.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/.gitignore +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/_static/.gitkeep +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/ai-integration.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/browser-mode.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/commands.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/conf.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/configuration.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/getting-started.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/index.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/interactive-mode.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/item-data.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/managing-the-tree.md +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/docs/requirements.txt +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/scripts/bump_version.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/cli.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/config.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/errors.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/filters.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/help_menu.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/json_dir_database.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/llm_models.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/models.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/ai.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/pomodoro.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/repair_database.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/services/user_setup.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/index.html +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/static/style.css +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/taskai/views.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_cli.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_execution.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_json_dir_database.py +0 -0
- {taskai_cli-1.8.0 → taskai_cli-1.8.1}/test/test_view.py +0 -0
|
@@ -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":
|
|
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":
|
|
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
|
-
|
|
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
|
-
|
|
746
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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"))
|
|
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: "
|
|
143
|
+
combos: [{ key: "`" }, { key: "~" }], glyphs: ["`"], desc: "Terminal (focus / close)",
|
|
134
144
|
whileTyping: (el) => el === consoleInput, // also closes it from inside
|
|
135
|
-
run: () =>
|
|
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
|
|
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
|
|
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
|