tasktree-manager 2.3.0__tar.gz → 2.5.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.
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/CHANGELOG.md +12 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/PKG-INFO +1 -1
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/docs/user-guide.md +7 -1
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/pyproject.toml +1 -1
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/app.py +58 -28
- tasktree_manager-2.5.0/tasktree_manager/commands.py +69 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/task_list.py +15 -1
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/worktree_list.py +14 -1
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_app.py +94 -0
- tasktree_manager-2.5.0/tests/test_commands.py +55 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.github/workflows/release.yml +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.gitignore +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/CONTRIBUTING.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/LICENSE +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/OPENSPEC.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/README.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/ROADMAP.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/docs/configuration.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/docs/installation.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/docs/troubleshooting.md +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/mise.toml +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/scripts/bump_version.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/claude_hooks.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/config.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/git_ops.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/models.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/services/task_manager.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/create_modal.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/setup_modal.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/status_panel.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/__init__.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/conftest.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_config.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_git_ops.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_memory.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_modals.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_status_panel.py +0 -0
- {tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tests/test_task_manager.py +0 -0
|
@@ -15,6 +15,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
- Git hooks support
|
|
16
16
|
- Custom task metadata
|
|
17
17
|
|
|
18
|
+
## [2.5.0] - 2026-06-10
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Searchable app actions in the command palette (#7)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.4.0] - 2026-06-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Context-sensitive footer showing the focused panel's keys (#6)
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
## [2.3.0] - 2026-06-10
|
|
19
31
|
|
|
20
32
|
### Added
|
|
@@ -246,6 +246,12 @@ Shows detailed git status for the selected worktree:
|
|
|
246
246
|
|
|
247
247
|
All keybindings can be customized in [`config.toml`](configuration.md).
|
|
248
248
|
|
|
249
|
+
The footer is context-sensitive, lazygit-style: it shows the actions for the
|
|
250
|
+
focused panel (task actions in the Task List, worktree/git actions in the
|
|
251
|
+
Worktree List) plus the always-available `r` Refresh, `?` Help, and `q` Quit.
|
|
252
|
+
Every key works regardless of which panel has focus — the footer only changes
|
|
253
|
+
what is displayed. Press `?` for the full reference at any time.
|
|
254
|
+
|
|
249
255
|
### Navigation
|
|
250
256
|
|
|
251
257
|
| Key | Action | Description |
|
|
@@ -358,7 +364,7 @@ Useful when you're spinning up several similar tasks that share the same set of
|
|
|
358
364
|
|
|
359
365
|
| Key | Action | Description |
|
|
360
366
|
|---------|-------------------------|----------------------------------------------|
|
|
361
|
-
| `Ctrl+P`| Open Command Palette |
|
|
367
|
+
| `Ctrl+P`| Open Command Palette | Search and run any action (hotkeys shown), switch themes |
|
|
362
368
|
| `o` | Open folder | Open current folder in new terminal tab |
|
|
363
369
|
| `m` | Toggle messages | Show/hide the activity messages panel |
|
|
364
370
|
| `?` | Show help | Display help modal with keybindings |
|
|
@@ -70,7 +70,7 @@ version-file = "tasktree_manager/_version.py"
|
|
|
70
70
|
# Semantic Release Configuration
|
|
71
71
|
[tool.semantic_release]
|
|
72
72
|
version_toml = ["pyproject.toml:tool.semantic_release.version"]
|
|
73
|
-
version = "2.
|
|
73
|
+
version = "2.5.0"
|
|
74
74
|
tag_format = "v{version}"
|
|
75
75
|
branch = "main"
|
|
76
76
|
commit_message = "chore(release): {version} [skip ci]"
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.5.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 5, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -10,6 +10,7 @@ from textual.binding import Binding
|
|
|
10
10
|
from textual.containers import Horizontal, Vertical
|
|
11
11
|
from textual.widgets import Footer, Header, Static
|
|
12
12
|
|
|
13
|
+
from .commands import TaskTreeCommands
|
|
13
14
|
from .services.claude_hooks import ensure_claude_hooks, has_claude_session
|
|
14
15
|
from .services.config import Config
|
|
15
16
|
from .services.git_ops import GitOps
|
|
@@ -35,6 +36,10 @@ class TaskTreeApp(App):
|
|
|
35
36
|
|
|
36
37
|
TITLE = "tasktree-manager"
|
|
37
38
|
|
|
39
|
+
# System commands plus all keybound tasktree actions (searchable by
|
|
40
|
+
# name, displayed with their configured hotkey)
|
|
41
|
+
COMMANDS = App.COMMANDS | {TaskTreeCommands}
|
|
42
|
+
|
|
38
43
|
CSS = """
|
|
39
44
|
/* Main layout */
|
|
40
45
|
Screen {
|
|
@@ -210,19 +215,21 @@ class TaskTreeApp(App):
|
|
|
210
215
|
}
|
|
211
216
|
"""
|
|
212
217
|
|
|
213
|
-
# Default bindings - will be overridden in __init__ with config values
|
|
214
|
-
#
|
|
215
|
-
#
|
|
218
|
+
# Default bindings - will be overridden in __init__ with config values.
|
|
219
|
+
# The footer is context-sensitive: app-level bindings keep every key
|
|
220
|
+
# working globally but stay hidden, except the always-visible globals
|
|
221
|
+
# (refresh/help/quit). The focused panel contributes its own visible
|
|
222
|
+
# bindings (see compose), lazygit-style.
|
|
216
223
|
BINDINGS = [
|
|
217
|
-
Binding("n", "new_task", "New
|
|
218
|
-
Binding("y", "clone_task", "Clone
|
|
219
|
-
Binding("a", "add_repo", "Add Repo"),
|
|
220
|
-
Binding("d", "delete_task", "Delete
|
|
221
|
-
Binding("g", "open_lazygit", "Lazygit"),
|
|
222
|
-
Binding("e", "open_editor", "Editor"),
|
|
223
|
-
Binding("o", "open_folder", "Open"),
|
|
224
|
-
Binding("c", "open_claude_resume", "Claude"),
|
|
225
|
-
Binding("p", "push_all", "Push
|
|
224
|
+
Binding("n", "new_task", "New", show=False),
|
|
225
|
+
Binding("y", "clone_task", "Clone", show=False),
|
|
226
|
+
Binding("a", "add_repo", "Add Repo", show=False),
|
|
227
|
+
Binding("d", "delete_task", "Delete", show=False),
|
|
228
|
+
Binding("g", "open_lazygit", "Lazygit", show=False),
|
|
229
|
+
Binding("e", "open_editor", "Editor", show=False),
|
|
230
|
+
Binding("o", "open_folder", "Open", show=False),
|
|
231
|
+
Binding("c", "open_claude_resume", "Claude", show=False),
|
|
232
|
+
Binding("p", "push_all", "Push", show=False),
|
|
226
233
|
Binding("r", "refresh", "Refresh"),
|
|
227
234
|
Binding("m", "toggle_messages", "Messages"),
|
|
228
235
|
Binding("q", "quit", "Quit"),
|
|
@@ -231,7 +238,7 @@ class TaskTreeApp(App):
|
|
|
231
238
|
Binding("C", "open_claude_gui_code", "Claude GUI", show=False),
|
|
232
239
|
Binding("P", "pull_all", "Pull All", show=False),
|
|
233
240
|
Binding("S", "toggle_grouping", "Group", show=False),
|
|
234
|
-
Binding("D", "delete_worktree", "Del
|
|
241
|
+
Binding("D", "delete_worktree", "Del WT", show=False),
|
|
235
242
|
Binding("tab", "focus_next", "Next Panel", show=False),
|
|
236
243
|
Binding("shift+tab", "focus_previous", "Prev Panel", show=False),
|
|
237
244
|
Binding("j", "cursor_down", "Down", show=False),
|
|
@@ -264,24 +271,26 @@ class TaskTreeApp(App):
|
|
|
264
271
|
self._last_tasks_fingerprint: tuple | None = None
|
|
265
272
|
|
|
266
273
|
def _build_bindings_from_config(self) -> list[Binding]:
|
|
267
|
-
"""Build keybindings
|
|
274
|
+
"""Build app-level keybindings from config.
|
|
275
|
+
|
|
276
|
+
Every key works globally regardless of focus, but only the global
|
|
277
|
+
actions (refresh/help/quit) are shown in the footer; the focused
|
|
278
|
+
panel contributes its own visible bindings (see compose).
|
|
268
279
|
|
|
269
280
|
Returns:
|
|
270
281
|
List of Binding objects with keys from config
|
|
271
282
|
"""
|
|
272
283
|
kb = self.config.keybindings
|
|
273
|
-
# Footer order: n, a, d, g, o, c, p, r, m, q, ?
|
|
274
|
-
# Uppercase shown only when shift is pressed: C, P, S
|
|
275
284
|
return [
|
|
276
|
-
Binding(kb.get("new_task", "n"), "new_task", "New
|
|
277
|
-
Binding(kb.get("clone_task", "y"), "clone_task", "Clone
|
|
278
|
-
Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo"),
|
|
279
|
-
Binding(kb.get("delete_task", "d"), "delete_task", "Delete
|
|
280
|
-
Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit"),
|
|
281
|
-
Binding(kb.get("open_editor", "e"), "open_editor", "Editor"),
|
|
282
|
-
Binding(kb.get("open_folder", "o"), "open_folder", "Open"),
|
|
283
|
-
Binding(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
|
|
284
|
-
Binding(kb.get("push_all", "p"), "push_all", "Push
|
|
285
|
+
Binding(kb.get("new_task", "n"), "new_task", "New", show=False),
|
|
286
|
+
Binding(kb.get("clone_task", "y"), "clone_task", "Clone", show=False),
|
|
287
|
+
Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo", show=False),
|
|
288
|
+
Binding(kb.get("delete_task", "d"), "delete_task", "Delete", show=False),
|
|
289
|
+
Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit", show=False),
|
|
290
|
+
Binding(kb.get("open_editor", "e"), "open_editor", "Editor", show=False),
|
|
291
|
+
Binding(kb.get("open_folder", "o"), "open_folder", "Open", show=False),
|
|
292
|
+
Binding(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude", show=False),
|
|
293
|
+
Binding(kb.get("push_all", "p"), "push_all", "Push", show=False),
|
|
285
294
|
Binding(kb.get("refresh", "r"), "refresh", "Refresh"),
|
|
286
295
|
Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages"),
|
|
287
296
|
Binding(kb.get("quit", "q"), "quit", "Quit"),
|
|
@@ -295,7 +304,7 @@ class TaskTreeApp(App):
|
|
|
295
304
|
),
|
|
296
305
|
Binding(kb.get("pull_all", "P"), "pull_all", "Pull All", show=False),
|
|
297
306
|
Binding(kb.get("toggle_grouping", "S"), "toggle_grouping", "Group", show=False),
|
|
298
|
-
Binding(kb.get("delete_worktree", "D"), "delete_worktree", "Del
|
|
307
|
+
Binding(kb.get("delete_worktree", "D"), "delete_worktree", "Del WT", show=False),
|
|
299
308
|
Binding(kb.get("focus_next", "tab"), "focus_next", "Next Panel", show=False),
|
|
300
309
|
Binding(
|
|
301
310
|
kb.get("focus_previous", "shift+tab"), "focus_previous", "Prev Panel", show=False
|
|
@@ -312,16 +321,37 @@ class TaskTreeApp(App):
|
|
|
312
321
|
|
|
313
322
|
def compose(self) -> ComposeResult:
|
|
314
323
|
"""Compose the app layout."""
|
|
324
|
+
kb = self.config.keybindings
|
|
325
|
+
# Footer bindings shown only while the respective panel has focus.
|
|
326
|
+
# The keys themselves also exist app-level (hidden) so they work
|
|
327
|
+
# regardless of focus; these entries only add footer visibility.
|
|
328
|
+
task_panel_bindings = [
|
|
329
|
+
(kb.get("new_task", "n"), "new_task", "New"),
|
|
330
|
+
(kb.get("clone_task", "y"), "clone_task", "Clone"),
|
|
331
|
+
(kb.get("add_repo", "a"), "add_repo", "Add Repo"),
|
|
332
|
+
(kb.get("delete_task", "d"), "delete_task", "Delete"),
|
|
333
|
+
(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
|
|
334
|
+
(kb.get("cycle_sort", "s"), "cycle_sort", "Sort"),
|
|
335
|
+
]
|
|
336
|
+
worktree_panel_bindings = [
|
|
337
|
+
(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit"),
|
|
338
|
+
(kb.get("open_editor", "e"), "open_editor", "Editor"),
|
|
339
|
+
(kb.get("open_folder", "o"), "open_folder", "Open"),
|
|
340
|
+
(kb.get("open_shell", "enter"), "open_shell", "Shell"),
|
|
341
|
+
(kb.get("push_all", "p"), "push_all", "Push"),
|
|
342
|
+
(kb.get("delete_worktree", "D"), "delete_worktree", "Del WT"),
|
|
343
|
+
]
|
|
344
|
+
|
|
315
345
|
yield Header()
|
|
316
346
|
with Horizontal(id="main-container"):
|
|
317
347
|
# Left column: Tasks (top) and Worktrees (bottom)
|
|
318
348
|
with Vertical(id="left-column"):
|
|
319
349
|
with Vertical(id="task-panel"):
|
|
320
350
|
yield Static("Tasks", classes="panel-title", id="task-panel-title")
|
|
321
|
-
yield TaskList(id="task-list")
|
|
351
|
+
yield TaskList(id="task-list", context_bindings=task_panel_bindings)
|
|
322
352
|
with Vertical(id="worktree-panel"):
|
|
323
353
|
yield Static("Worktrees", classes="panel-title", id="worktree-panel-title")
|
|
324
|
-
yield WorktreeList(id="worktree-list")
|
|
354
|
+
yield WorktreeList(id="worktree-list", context_bindings=worktree_panel_bindings)
|
|
325
355
|
# Right column: Info/Status panel
|
|
326
356
|
with Vertical(id="right-column"):
|
|
327
357
|
with Vertical(id="status-panel"):
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Command palette provider for tasktree-manager actions."""
|
|
2
|
+
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
from textual.command import DiscoveryHit, Hit, Hits, Provider
|
|
6
|
+
|
|
7
|
+
# (action, title, help) for every keybound app action. The key for each
|
|
8
|
+
# action is looked up from the app's config at search time so customized
|
|
9
|
+
# keybindings display correctly.
|
|
10
|
+
APP_COMMANDS: list[tuple[str, str, str]] = [
|
|
11
|
+
("new_task", "New Task", "Create a new task with worktrees"),
|
|
12
|
+
("clone_task", "Clone Task", "Create a new task with the same repos as the current one"),
|
|
13
|
+
("add_repo", "Add Repo", "Add repositories to the current task"),
|
|
14
|
+
("delete_task", "Delete Task", "Delete the current task (with safety checks)"),
|
|
15
|
+
("delete_worktree", "Delete Worktree", "Delete the selected worktree from the task"),
|
|
16
|
+
("open_lazygit", "Open Lazygit", "Open lazygit in the selected worktree"),
|
|
17
|
+
("open_editor", "Open Editor", "Open your editor in the focused task/worktree folder"),
|
|
18
|
+
("open_folder", "Open Folder", "Open the focused folder in a new terminal tab"),
|
|
19
|
+
("open_shell", "Open Shell", "Open a shell in the selected worktree"),
|
|
20
|
+
("open_claude_resume", "Open Claude", "Open Claude Code for the task (resume if possible)"),
|
|
21
|
+
(
|
|
22
|
+
"open_claude_gui_code",
|
|
23
|
+
"Open Claude Desktop",
|
|
24
|
+
"Open the Claude desktop app on the Code page",
|
|
25
|
+
),
|
|
26
|
+
("push_all", "Push All", "Push all worktrees in the current task"),
|
|
27
|
+
("pull_all", "Pull All", "Pull all worktrees in the current task"),
|
|
28
|
+
("refresh", "Refresh", "Refresh git status for all tasks"),
|
|
29
|
+
("toggle_messages", "Toggle Messages", "Show or hide the activity messages panel"),
|
|
30
|
+
("toggle_grouping", "Toggle Grouping", "Group worktrees by dirty/clean status"),
|
|
31
|
+
("cycle_sort", "Cycle Sort", "Cycle through task sort modes"),
|
|
32
|
+
("help", "Help", "Show the keybinding reference"),
|
|
33
|
+
("quit", "Quit", "Exit tasktree-manager"),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TaskTreeCommands(Provider):
|
|
38
|
+
"""Expose all keybound tasktree actions in the command palette."""
|
|
39
|
+
|
|
40
|
+
def _entries(self):
|
|
41
|
+
"""Yield (display title with hotkey, action, help) tuples."""
|
|
42
|
+
config = getattr(self.app, "config", None)
|
|
43
|
+
keybindings = config.keybindings if config else {}
|
|
44
|
+
for action, title, help_text in APP_COMMANDS:
|
|
45
|
+
key = keybindings.get(action, "")
|
|
46
|
+
display = f"{title} ({key})" if key else title
|
|
47
|
+
yield display, action, help_text
|
|
48
|
+
|
|
49
|
+
async def discover(self) -> Hits:
|
|
50
|
+
"""List all commands when the palette opens with an empty query."""
|
|
51
|
+
for display, action, help_text in self._entries():
|
|
52
|
+
yield DiscoveryHit(
|
|
53
|
+
display,
|
|
54
|
+
partial(self.app.run_action, action),
|
|
55
|
+
help=help_text,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
async def search(self, query: str) -> Hits:
|
|
59
|
+
"""Fuzzy-match commands against the query."""
|
|
60
|
+
matcher = self.matcher(query)
|
|
61
|
+
for display, action, help_text in self._entries():
|
|
62
|
+
score = matcher.match(display)
|
|
63
|
+
if score > 0:
|
|
64
|
+
yield Hit(
|
|
65
|
+
score,
|
|
66
|
+
matcher.highlight(display),
|
|
67
|
+
partial(self.app.run_action, action),
|
|
68
|
+
help=help_text,
|
|
69
|
+
)
|
|
@@ -45,12 +45,26 @@ class TaskList(OptionList):
|
|
|
45
45
|
self.label = label
|
|
46
46
|
super().__init__()
|
|
47
47
|
|
|
48
|
-
def __init__(
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
*args,
|
|
51
|
+
context_bindings: list[tuple[str, str, str]] | None = None,
|
|
52
|
+
**kwargs,
|
|
53
|
+
):
|
|
49
54
|
super().__init__(*args, **kwargs)
|
|
50
55
|
self.tasks: list[Task] = []
|
|
51
56
|
self._sort_mode: SortMode = SortMode.NAME_ASC
|
|
52
57
|
# Last known Claude session statuses, kept so indicators survive reloads
|
|
53
58
|
self._claude_statuses: dict[str, str] = {}
|
|
59
|
+
# Footer-visible (key, app action, description) bindings shown while
|
|
60
|
+
# this panel has focus; the keys also exist app-level (hidden) so
|
|
61
|
+
# they keep working regardless of focus
|
|
62
|
+
for key, action, description in context_bindings or []:
|
|
63
|
+
# Drop any inherited binding on the same key - dispatch and the
|
|
64
|
+
# footer both use the first binding found, so an OptionList
|
|
65
|
+
# default (e.g. "enter") would otherwise shadow the app action
|
|
66
|
+
self._bindings.key_to_bindings.pop(key, None)
|
|
67
|
+
self._bindings.bind(key, f"app.{action}", description, show=True)
|
|
54
68
|
|
|
55
69
|
@property
|
|
56
70
|
def index(self) -> int | None:
|
|
@@ -31,12 +31,25 @@ class WorktreeList(OptionList):
|
|
|
31
31
|
self.enabled = enabled
|
|
32
32
|
super().__init__()
|
|
33
33
|
|
|
34
|
-
def __init__(
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
*args,
|
|
37
|
+
context_bindings: list[tuple[str, str, str]] | None = None,
|
|
38
|
+
**kwargs,
|
|
39
|
+
):
|
|
35
40
|
super().__init__(*args, **kwargs)
|
|
36
41
|
self.worktrees: list[Worktree] = []
|
|
37
42
|
self._grouping_enabled: bool = False
|
|
38
43
|
# Maps option index to worktree index (for handling headers)
|
|
39
44
|
self._option_to_worktree: dict[int, int] = {}
|
|
45
|
+
# Footer-visible (key, app action, description) bindings shown while
|
|
46
|
+
# this panel has focus; the keys also exist app-level (hidden) so
|
|
47
|
+
# they keep working regardless of focus. Replacing "enter" here also
|
|
48
|
+
# fixes shell opening: OptionList's own select binding used to
|
|
49
|
+
# swallow the key before the app's open_shell binding could see it.
|
|
50
|
+
for key, action, description in context_bindings or []:
|
|
51
|
+
self._bindings.key_to_bindings.pop(key, None)
|
|
52
|
+
self._bindings.bind(key, f"app.{action}", description, show=True)
|
|
40
53
|
|
|
41
54
|
@property
|
|
42
55
|
def index(self) -> int | None:
|
|
@@ -570,3 +570,97 @@ class TestAppConfiguration:
|
|
|
570
570
|
from tasktree_manager.services.git_ops import GitOps
|
|
571
571
|
|
|
572
572
|
assert GitOps.network_timeout == app.config.git_timeout
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
class TestContextSensitiveFooter:
|
|
576
|
+
"""Tests for the context-sensitive footer bindings."""
|
|
577
|
+
|
|
578
|
+
@staticmethod
|
|
579
|
+
def _shown_actions(app) -> set[str]:
|
|
580
|
+
"""Actions currently visible in the footer."""
|
|
581
|
+
return {
|
|
582
|
+
active.binding.action
|
|
583
|
+
for active in app.screen.active_bindings.values()
|
|
584
|
+
if active.binding.show
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async def test_task_panel_footer_shows_task_actions(self, app, sample_repo, task_manager):
|
|
588
|
+
"""Task list focused: task actions plus globals are visible."""
|
|
589
|
+
repo_path, branch = sample_repo
|
|
590
|
+
task_manager.create_task("FOOTER-TASKS", ["sample-repo"], branch)
|
|
591
|
+
|
|
592
|
+
async with app.run_test() as pilot:
|
|
593
|
+
await pilot.pause()
|
|
594
|
+
|
|
595
|
+
assert app.focused.id == "task-list"
|
|
596
|
+
shown = self._shown_actions(app)
|
|
597
|
+
assert "app.new_task" in shown
|
|
598
|
+
assert "app.clone_task" in shown
|
|
599
|
+
assert "app.delete_task" in shown
|
|
600
|
+
# Worktree-context actions are hidden in this context
|
|
601
|
+
assert "app.open_lazygit" not in shown
|
|
602
|
+
assert "app.open_shell" not in shown
|
|
603
|
+
# Globals always visible
|
|
604
|
+
assert "refresh" in shown
|
|
605
|
+
assert "toggle_messages" in shown
|
|
606
|
+
assert "help" in shown
|
|
607
|
+
assert "quit" in shown
|
|
608
|
+
|
|
609
|
+
async def test_worktree_panel_footer_shows_worktree_actions(
|
|
610
|
+
self, app, sample_repo, task_manager
|
|
611
|
+
):
|
|
612
|
+
"""Worktree list focused: worktree actions plus globals are visible."""
|
|
613
|
+
repo_path, branch = sample_repo
|
|
614
|
+
task_manager.create_task("FOOTER-WT", ["sample-repo"], branch)
|
|
615
|
+
|
|
616
|
+
async with app.run_test() as pilot:
|
|
617
|
+
await pilot.pause()
|
|
618
|
+
await pilot.press("tab")
|
|
619
|
+
await pilot.pause()
|
|
620
|
+
|
|
621
|
+
assert app.focused.id == "worktree-list"
|
|
622
|
+
shown = self._shown_actions(app)
|
|
623
|
+
assert "app.open_lazygit" in shown
|
|
624
|
+
assert "app.open_shell" in shown
|
|
625
|
+
assert "app.push_all" in shown
|
|
626
|
+
# Task-context actions are hidden in this context
|
|
627
|
+
assert "app.new_task" not in shown
|
|
628
|
+
# Globals always visible
|
|
629
|
+
assert "refresh" in shown
|
|
630
|
+
assert "quit" in shown
|
|
631
|
+
|
|
632
|
+
async def test_keys_work_outside_their_panel(self, app, sample_repos, task_manager):
|
|
633
|
+
"""A task-context key still works while the worktree list is focused."""
|
|
634
|
+
_, branch = sample_repos
|
|
635
|
+
task_manager.create_task("FOOTER-KEYS", ["repo-alpha"], branch)
|
|
636
|
+
|
|
637
|
+
async with app.run_test() as pilot:
|
|
638
|
+
await pilot.pause()
|
|
639
|
+
await pilot.press("tab") # focus worktree list
|
|
640
|
+
await pilot.pause()
|
|
641
|
+
|
|
642
|
+
# 'n' is a task-panel binding but must still open the modal
|
|
643
|
+
await pilot.press("n")
|
|
644
|
+
await pilot.pause()
|
|
645
|
+
|
|
646
|
+
create_modals = [s for s in app.screen_stack if isinstance(s, CreateTaskModal)]
|
|
647
|
+
assert len(create_modals) == 1
|
|
648
|
+
|
|
649
|
+
async def test_enter_opens_shell_from_worktree_list(
|
|
650
|
+
self, app, sample_repo, task_manager, monkeypatch
|
|
651
|
+
):
|
|
652
|
+
"""Enter on the worktree list triggers open_shell (was swallowed by select)."""
|
|
653
|
+
repo_path, branch = sample_repo
|
|
654
|
+
task_manager.create_task("FOOTER-ENTER", ["sample-repo"], branch)
|
|
655
|
+
|
|
656
|
+
called = []
|
|
657
|
+
monkeypatch.setattr(app, "action_open_shell", lambda: called.append(True))
|
|
658
|
+
|
|
659
|
+
async with app.run_test() as pilot:
|
|
660
|
+
await pilot.pause()
|
|
661
|
+
await pilot.press("tab")
|
|
662
|
+
await pilot.pause()
|
|
663
|
+
await pilot.press("enter")
|
|
664
|
+
await pilot.pause()
|
|
665
|
+
|
|
666
|
+
assert called
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Tests for the command palette provider."""
|
|
2
|
+
|
|
3
|
+
from tasktree_manager.app import TaskTreeApp
|
|
4
|
+
from tasktree_manager.commands import APP_COMMANDS, TaskTreeCommands
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestTaskTreeCommands:
|
|
8
|
+
"""Tests for the TaskTreeCommands palette provider."""
|
|
9
|
+
|
|
10
|
+
async def test_app_registers_provider(self, app):
|
|
11
|
+
"""The app exposes the provider alongside the system commands."""
|
|
12
|
+
assert TaskTreeCommands in TaskTreeApp.COMMANDS
|
|
13
|
+
|
|
14
|
+
async def test_discover_lists_all_commands(self, app):
|
|
15
|
+
"""Opening the palette with an empty query lists every action."""
|
|
16
|
+
async with app.run_test() as pilot:
|
|
17
|
+
await pilot.pause()
|
|
18
|
+
|
|
19
|
+
provider = TaskTreeCommands(app.screen)
|
|
20
|
+
hits = [hit async for hit in provider.discover()]
|
|
21
|
+
|
|
22
|
+
assert len(hits) == len(APP_COMMANDS)
|
|
23
|
+
|
|
24
|
+
async def test_search_finds_action_with_hotkey(self, app):
|
|
25
|
+
"""Searching shows the matching command with its hotkey."""
|
|
26
|
+
async with app.run_test() as pilot:
|
|
27
|
+
await pilot.pause()
|
|
28
|
+
|
|
29
|
+
provider = TaskTreeCommands(app.screen)
|
|
30
|
+
hits = [hit async for hit in provider.search("lazygit")]
|
|
31
|
+
|
|
32
|
+
assert hits
|
|
33
|
+
assert any("(g)" in str(hit.match_display) for hit in hits)
|
|
34
|
+
|
|
35
|
+
async def test_search_shows_customized_key(self, app):
|
|
36
|
+
"""A remapped keybinding shows the configured key, not the default."""
|
|
37
|
+
app.config.keybindings["push_all"] = "x"
|
|
38
|
+
|
|
39
|
+
async with app.run_test() as pilot:
|
|
40
|
+
await pilot.pause()
|
|
41
|
+
|
|
42
|
+
provider = TaskTreeCommands(app.screen)
|
|
43
|
+
hits = [hit async for hit in provider.search("push all")]
|
|
44
|
+
|
|
45
|
+
assert any("(x)" in str(hit.match_display) for hit in hits)
|
|
46
|
+
|
|
47
|
+
async def test_search_no_match_returns_nothing(self, app):
|
|
48
|
+
"""A nonsense query yields no hits."""
|
|
49
|
+
async with app.run_test() as pilot:
|
|
50
|
+
await pilot.pause()
|
|
51
|
+
|
|
52
|
+
provider = TaskTreeCommands(app.screen)
|
|
53
|
+
hits = [hit async for hit in provider.search("zzz-no-such-command")]
|
|
54
|
+
|
|
55
|
+
assert hits == []
|
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/README.md
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/dashboard_app.py
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/data_viewer.py
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/todo_app.py
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/assets/worker_demo.py
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/layouts.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/styling.md
RENAMED
|
File without changes
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/.claude/skills/textual-tui/references/widgets.md
RENAMED
|
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
|
{tasktree_manager-2.3.0 → tasktree_manager-2.5.0}/tasktree_manager/widgets/messages_panel.py
RENAMED
|
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
|