tasktree-manager 2.11.0__tar.gz → 2.12.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.11.0 → tasktree_manager-2.12.0}/CHANGELOG.md +12 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/PKG-INFO +1 -1
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/README.md +3 -2
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/docs/configuration.md +3 -15
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/docs/troubleshooting.md +3 -3
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/docs/user-guide.md +13 -13
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/pyproject.toml +1 -1
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/app.py +33 -50
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/cli.py +1 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/commands.py +10 -2
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/config.py +2 -16
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/models.py +18 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/task_manager.py +16 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/create_modal.py +54 -3
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/task_list.py +5 -5
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/worktree_list.py +2 -3
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_app.py +58 -28
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_config.py +0 -16
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_task_manager.py +42 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.github/workflows/release.yml +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.gitignore +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/CONTRIBUTING.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/LICENSE +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/OPENSPEC.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/ROADMAP.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/docs/installation.md +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/mise.toml +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/scripts/bump_version.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/agent_sessions.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/claude_hooks.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/forge.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/git_ops.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/themes.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/app_header.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/setup_modal.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/status_panel.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/__init__.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/conftest.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_agent_sessions.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_claude_hooks.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_cli.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_commands.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_forge.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_git_ops.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_memory.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_modals.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_status_panel.py +0 -0
- {tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tests/test_worktree_badges.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.12.0] - 2026-07-31
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **tui**: Rename task display label with R
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.11.1] - 2026-07-31
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **tui**: Open editor in new terminal tab, drop Enter shell binding
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
## [2.11.0] - 2026-07-30
|
|
19
31
|
|
|
20
32
|
### Added
|
|
@@ -63,7 +63,7 @@ On first run, you'll configure:
|
|
|
63
63
|
### Basic Workflow
|
|
64
64
|
|
|
65
65
|
1. **Create a task** - Press `n`, enter task name, select repositories
|
|
66
|
-
2. **Work on code** - Press `e` for editor, `g` for lazygit, or `
|
|
66
|
+
2. **Work on code** - Press `e` for editor, `g` for lazygit, or `o` for a shell in a new terminal tab
|
|
67
67
|
3. **Review changes** - Press `h` to view the diff in [hunk](https://github.com/modem-dev/hunk) (all repos from the task panel, one repo from the worktree panel)
|
|
68
68
|
4. **Push changes** - Press `p` to push all worktrees
|
|
69
69
|
5. **Delete task** - Press `d` when finished
|
|
@@ -107,12 +107,13 @@ a Claude Code statusline or shell prompt.
|
|
|
107
107
|
| `a` | Add repo | `h` | Show diff (hunk) |
|
|
108
108
|
| `d` | Delete task | `e` | Open editor |
|
|
109
109
|
| `p` | Push all | `o` | Open folder |
|
|
110
|
-
| `P` | Pull all | `
|
|
110
|
+
| `P` | Pull all | `y` | Clone task |
|
|
111
111
|
| `c` | Claude (resume) | `C` | Claude (new) |
|
|
112
112
|
| `b` | Dispatch agent | `D` | Delete worktree |
|
|
113
113
|
| `r` | Refresh | `s` | Sort tasks |
|
|
114
114
|
| `S` | Group worktrees | `m` | Messages |
|
|
115
115
|
| `t` | Cycle theme | `Tab` | Next panel |
|
|
116
|
+
| `R` | Rename task | | |
|
|
116
117
|
| `?` | Show help | `q` | Quit |
|
|
117
118
|
|
|
118
119
|
**Themes:** The default `tasktree` theme pairs a lazygit-classic ANSI palette with layered panels, numbered title bars, green focus borders, and a keycap footer. Five more design-system palettes ship tuned to the same shape — tokyo-night, catppuccin-mocha, gruvbox, dracula, nord — and every other Textual built-in theme works too (`Ctrl+P` to switch); the whole UI is styled with Textual design tokens, so any theme restyles it consistently.
|
|
@@ -180,11 +180,6 @@ claude_memory_dir = "~/.claude/tasktree-memory"
|
|
|
180
180
|
# Default: true
|
|
181
181
|
claude_repo_memory = true
|
|
182
182
|
|
|
183
|
-
# Preferred shell
|
|
184
|
-
# Leave empty to use $SHELL environment variable
|
|
185
|
-
# Default: "" (uses $SHELL, falls back to /bin/bash)
|
|
186
|
-
shell = ""
|
|
187
|
-
|
|
188
183
|
# Path to glab executable (GitLab CLI, used for MR/CI status)
|
|
189
184
|
# Default: "glab" (searches PATH)
|
|
190
185
|
glab_path = "glab"
|
|
@@ -228,6 +223,7 @@ help = "?" # Show help modal
|
|
|
228
223
|
new_task = "n" # Create new task
|
|
229
224
|
add_repo = "a" # Add repository to current task
|
|
230
225
|
delete_task = "d" # Delete/finish task
|
|
226
|
+
rename_task = "R" # Rename task display label (folder/branch unchanged, shift+r)
|
|
231
227
|
delete_worktree = "D" # Remove single worktree from task (shift+d)
|
|
232
228
|
|
|
233
229
|
# Git operations
|
|
@@ -235,7 +231,6 @@ open_lazygit = "g" # Open lazygit in selected worktree
|
|
|
235
231
|
show_diff = "h" # Open hunk diff (task panel = all repos, worktree panel = one repo)
|
|
236
232
|
open_editor = "e" # Open editor in selected task/worktree
|
|
237
233
|
open_folder = "o" # Open folder in new terminal tab
|
|
238
|
-
open_shell = "enter" # Open shell in selected worktree
|
|
239
234
|
push_all = "p" # Push all worktrees in current task
|
|
240
235
|
pull_all = "P" # Pull all worktrees in current task (shift+p)
|
|
241
236
|
refresh = "r" # Refresh git status
|
|
@@ -399,7 +394,6 @@ refresh_interval = 15 # Refresh every 15 seconds
|
|
|
399
394
|
| `editor` | string | `""` | `EDITOR` | Command to launch your editor |
|
|
400
395
|
| `lazygit_path` | string | `"lazygit"` | - | Path to lazygit executable |
|
|
401
396
|
| `hunk_path` | string | `"hunk"` | - | Path to hunk diff-viewer executable |
|
|
402
|
-
| `shell` | string | `""` | `SHELL` | Shell to use when opening terminals |
|
|
403
397
|
| `glab_path` | string | `"glab"` | - | Path to GitLab CLI (MR/CI status) |
|
|
404
398
|
| `gh_path` | string | `"gh"` | - | Path to GitHub CLI (PR/CI status) |
|
|
405
399
|
|
|
@@ -438,12 +432,6 @@ failures degrade silently — features fall back to plain git behavior.
|
|
|
438
432
|
- Install hunk: `brew install hunk` (macOS) or `npm i -g hunkdiff`
|
|
439
433
|
- Used when pressing `h`: from the task panel it opens a combined diff across all the task's repos; from the worktree panel it opens the selected repo's diff
|
|
440
434
|
|
|
441
|
-
**`shell`:**
|
|
442
|
-
- Leave empty (`""`) to use `$SHELL` environment variable
|
|
443
|
-
- Fallback: `/bin/bash`
|
|
444
|
-
- Examples: `"/bin/zsh"`, `"/bin/fish"`, `"/usr/local/bin/bash"`
|
|
445
|
-
- Used when pressing `Enter` to open shell in worktree
|
|
446
|
-
|
|
447
435
|
## Symlinks
|
|
448
436
|
|
|
449
437
|
When creating worktrees, tasktree-manager automatically symlinks gitignored files (like `.env`) from the source repository. This allows environment files to be shared without copying.
|
|
@@ -596,6 +584,7 @@ All 22 customizable keybindings with descriptions:
|
|
|
596
584
|
| `clone_task` | `y` | Clone current task (same repos, new name) | `clone_task = "ctrl+y"` |
|
|
597
585
|
| `add_repo` | `a` | Add repository to current task | `add_repo = "ctrl+a"` |
|
|
598
586
|
| `delete_task` | `d` | Delete/finish current task | `delete_task = "delete"` |
|
|
587
|
+
| `rename_task` | `R` | Rename task display label (alias only) | `rename_task = "ctrl+r"` |
|
|
599
588
|
|
|
600
589
|
### Git Operations
|
|
601
590
|
|
|
@@ -605,7 +594,6 @@ All 22 customizable keybindings with descriptions:
|
|
|
605
594
|
| `show_diff` | `h` | Open hunk diff (task = all repos, worktree = one) | `show_diff = "ctrl+h"` |
|
|
606
595
|
| `open_editor` | `e` | Open editor in task/worktree folder | `open_editor = "ctrl+e"` |
|
|
607
596
|
| `open_folder` | `o` | Open folder in new terminal tab | `open_folder = "ctrl+o"` |
|
|
608
|
-
| `open_shell` | `enter` | Open shell in selected worktree | `open_shell = "ctrl+t"` |
|
|
609
597
|
| `push_all` | `p` | Push all worktrees in current task | `push_all = "ctrl+p"` |
|
|
610
598
|
| `pull_all` | `P` | Pull all worktrees in current task | `pull_all = "ctrl+shift+p"` |
|
|
611
599
|
| `refresh` | `r` | Refresh git status for all worktrees | `refresh = "F5"` |
|
|
@@ -675,7 +663,7 @@ focus_previous = "h"
|
|
|
675
663
|
[keybindings]
|
|
676
664
|
new_task = "ctrl+n"
|
|
677
665
|
delete_task = "ctrl+d"
|
|
678
|
-
|
|
666
|
+
open_folder = "ctrl+t"
|
|
679
667
|
refresh = "F5"
|
|
680
668
|
quit = "ctrl+q"
|
|
681
669
|
```
|
|
@@ -732,8 +732,8 @@ HEAD detached at abc1234
|
|
|
732
732
|
|
|
733
733
|
**Solution:**
|
|
734
734
|
```bash
|
|
735
|
-
# Open
|
|
736
|
-
Press
|
|
735
|
+
# Open worktree in a new terminal tab
|
|
736
|
+
Press o
|
|
737
737
|
|
|
738
738
|
# Create/checkout branch
|
|
739
739
|
git checkout -b FEAT-123
|
|
@@ -1008,7 +1008,7 @@ Create
|
|
|
1008
1008
|
|
|
1009
1009
|
# Your work is in the remote branch
|
|
1010
1010
|
# Pull it down in the new worktree
|
|
1011
|
-
Press
|
|
1011
|
+
Press o (new terminal tab)
|
|
1012
1012
|
git pull origin FEAT-123
|
|
1013
1013
|
```
|
|
1014
1014
|
|
|
@@ -140,10 +140,10 @@ A **worktree** is a git working directory for a specific repository within a tas
|
|
|
140
140
|
- Exit hunk (press `q`)
|
|
141
141
|
|
|
142
142
|
4. **Or open a shell:**
|
|
143
|
-
- Press `
|
|
143
|
+
- Press `o` to open the worktree in a new terminal tab
|
|
144
144
|
- Make changes with your editor/IDE
|
|
145
145
|
- Run build/test commands
|
|
146
|
-
-
|
|
146
|
+
- Close the tab (or switch back) when done
|
|
147
147
|
|
|
148
148
|
5. **Push changes:**
|
|
149
149
|
- Press `p` to push all worktrees in the task
|
|
@@ -300,6 +300,7 @@ what is displayed. Press `?` for the full reference at any time.
|
|
|
300
300
|
| `y` | Clone task | Create a new task pre-populated with the current task's repos |
|
|
301
301
|
| `a` | Add repository | Add additional repositories to the current task |
|
|
302
302
|
| `d` | Delete/finish | Delete current task and remove worktrees |
|
|
303
|
+
| `R` | Rename task | Set a display alias shown in the task list (folder/branch unchanged; empty input reverts) |
|
|
303
304
|
|
|
304
305
|
**Task creation workflow:**
|
|
305
306
|
1. Press `n`
|
|
@@ -337,8 +338,7 @@ Useful when you're spinning up several similar tasks that share the same set of
|
|
|
337
338
|
|---------|--------------------|------------------------------------------------------|
|
|
338
339
|
| `g` | Open lazygit | Open lazygit in the selected worktree |
|
|
339
340
|
| `h` | Show diff | Open hunk: all repos from the task panel, the selected repo from the worktree panel |
|
|
340
|
-
| `e` | Open editor | Open editor in task/worktree folder (runs `$EDITOR
|
|
341
|
-
| `Enter` | Open shell | Open a shell in the selected worktree |
|
|
341
|
+
| `e` | Open editor | Open editor in a new terminal tab at the task/worktree folder (runs `$EDITOR .`; the TUI keeps running) |
|
|
342
342
|
| `p` | Push all | Push all worktrees in the current task (parallel) |
|
|
343
343
|
| `P` | Pull all | Pull all worktrees in the current task (parallel) |
|
|
344
344
|
| `r` | Refresh status | Refresh git status for all tasks and worktrees |
|
|
@@ -434,9 +434,9 @@ Create
|
|
|
434
434
|
# Work in backend
|
|
435
435
|
Tab (to worktree list)
|
|
436
436
|
j (to highlight backend)
|
|
437
|
-
|
|
437
|
+
o (open new terminal tab)
|
|
438
438
|
|
|
439
|
-
# In
|
|
439
|
+
# In the tab: make changes
|
|
440
440
|
cd src
|
|
441
441
|
vim auth.py
|
|
442
442
|
# ... make changes ...
|
|
@@ -516,7 +516,7 @@ Select: backend
|
|
|
516
516
|
Create
|
|
517
517
|
|
|
518
518
|
Tab
|
|
519
|
-
|
|
519
|
+
o (open new terminal tab)
|
|
520
520
|
git fetch origin feat-789:feat-789
|
|
521
521
|
git checkout feat-789
|
|
522
522
|
exit
|
|
@@ -553,7 +553,7 @@ q (quit tasktree-manager)
|
|
|
553
553
|
tasktree-manager
|
|
554
554
|
j (select FEAT-999)
|
|
555
555
|
Tab
|
|
556
|
-
|
|
556
|
+
o (new tab in first worktree)
|
|
557
557
|
|
|
558
558
|
# Merge main into feature branch
|
|
559
559
|
git fetch origin main:main
|
|
@@ -631,7 +631,7 @@ d (delete task)
|
|
|
631
631
|
**Working in worktree:**
|
|
632
632
|
- Press `e` to open editor directly in worktree
|
|
633
633
|
- Press `g` for lazygit (recommended for git operations)
|
|
634
|
-
- Press `
|
|
634
|
+
- Press `o` for a shell in a new terminal tab (building, testing, other commands)
|
|
635
635
|
|
|
636
636
|
**Git operations:**
|
|
637
637
|
- **Staging/committing**: Use lazygit (`g`)
|
|
@@ -764,9 +764,9 @@ editor = "nvim" # or "vim", "code", "emacs", etc.
|
|
|
764
764
|
|
|
765
765
|
Or set the `$EDITOR` environment variable.
|
|
766
766
|
|
|
767
|
-
**Alternative: Open
|
|
768
|
-
1. Press `
|
|
769
|
-
2. In
|
|
767
|
+
**Alternative: Open a terminal tab first:**
|
|
768
|
+
1. Press `o` (new terminal tab in the worktree)
|
|
769
|
+
2. In the tab:
|
|
770
770
|
```bash
|
|
771
771
|
vim . # Vim
|
|
772
772
|
nvim . # Neovim
|
|
@@ -807,7 +807,7 @@ tasktree-manager creates normal git branches, so CI/CD works seamlessly:
|
|
|
807
807
|
Integrate with GitHub CLI for PR creation:
|
|
808
808
|
|
|
809
809
|
```bash
|
|
810
|
-
# In worktree
|
|
810
|
+
# In a worktree tab (press o in tasktree-manager)
|
|
811
811
|
gh pr create --title "Add authentication" --body "Implements FEAT-123"
|
|
812
812
|
|
|
813
813
|
# Or after pushing all worktrees
|
|
@@ -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.12.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.12.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 12, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -34,6 +34,7 @@ from .widgets.create_modal import (
|
|
|
34
34
|
DispatchAgentModal,
|
|
35
35
|
HelpModal,
|
|
36
36
|
PushResultModal,
|
|
37
|
+
RenameTaskModal,
|
|
37
38
|
SafeDeleteModal,
|
|
38
39
|
)
|
|
39
40
|
from .widgets.messages_panel import MessageLevel, MessagesPanel
|
|
@@ -208,7 +209,6 @@ class TaskTreeApp(App):
|
|
|
208
209
|
Binding("m", "toggle_messages", "Messages", show=False),
|
|
209
210
|
Binding("?", "help", "Help"),
|
|
210
211
|
Binding("q", "quit", "Quit"),
|
|
211
|
-
Binding("enter", "open_shell", "Shell", show=False),
|
|
212
212
|
Binding("C", "open_claude_gui_code", "Claude GUI", show=False),
|
|
213
213
|
Binding("P", "pull_all", "Pull All", show=False),
|
|
214
214
|
Binding("S", "toggle_grouping", "Group", show=False),
|
|
@@ -275,6 +275,7 @@ class TaskTreeApp(App):
|
|
|
275
275
|
Binding(kb.get("clone_task", "y"), "clone_task", "Clone", show=False),
|
|
276
276
|
Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo", show=False),
|
|
277
277
|
Binding(kb.get("delete_task", "d"), "delete_task", "Delete", show=False),
|
|
278
|
+
Binding(kb.get("rename_task", "R"), "rename_task", "Rename", show=False),
|
|
278
279
|
Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit", show=False),
|
|
279
280
|
Binding(kb.get("show_diff", "h"), "show_diff", "Diff", show=False),
|
|
280
281
|
Binding(kb.get("open_editor", "e"), "open_editor", "Editor", show=False),
|
|
@@ -287,7 +288,6 @@ class TaskTreeApp(App):
|
|
|
287
288
|
Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages", show=False),
|
|
288
289
|
Binding(kb.get("help", "?"), "help", "Help"),
|
|
289
290
|
Binding(kb.get("quit", "q"), "quit", "Quit"),
|
|
290
|
-
Binding(kb.get("open_shell", "enter"), "open_shell", "Shell", show=False),
|
|
291
291
|
Binding(
|
|
292
292
|
kb.get("open_claude_gui_code", "C"),
|
|
293
293
|
"open_claude_gui_code",
|
|
@@ -322,6 +322,7 @@ class TaskTreeApp(App):
|
|
|
322
322
|
(kb.get("clone_task", "y"), "clone_task", "Clone"),
|
|
323
323
|
(kb.get("add_repo", "a"), "add_repo", "Add Repo"),
|
|
324
324
|
(kb.get("delete_task", "d"), "delete_task", "Delete"),
|
|
325
|
+
(kb.get("rename_task", "R"), "rename_task", "Rename"),
|
|
325
326
|
(kb.get("show_diff", "h"), "show_diff", "Diff"),
|
|
326
327
|
(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
|
|
327
328
|
(kb.get("cycle_sort", "s"), "cycle_sort", "Sort"),
|
|
@@ -331,7 +332,6 @@ class TaskTreeApp(App):
|
|
|
331
332
|
(kb.get("show_diff", "h"), "show_diff", "Diff"),
|
|
332
333
|
(kb.get("open_editor", "e"), "open_editor", "Editor"),
|
|
333
334
|
(kb.get("open_folder", "o"), "open_folder", "Open"),
|
|
334
|
-
(kb.get("open_shell", "enter"), "open_shell", "Shell"),
|
|
335
335
|
(kb.get("dispatch_agent", "b"), "dispatch_agent", "Agent"),
|
|
336
336
|
(kb.get("push_all", "p"), "push_all", "Push"),
|
|
337
337
|
(kb.get("delete_worktree", "D"), "delete_worktree", "Del WT"),
|
|
@@ -1193,6 +1193,28 @@ class TaskTreeApp(App):
|
|
|
1193
1193
|
|
|
1194
1194
|
self.push_screen(ConfirmModal(title, message), handle_confirm)
|
|
1195
1195
|
|
|
1196
|
+
def action_rename_task(self) -> None:
|
|
1197
|
+
"""Set a display alias for the current task (folder/branch unchanged)."""
|
|
1198
|
+
if not self.current_task:
|
|
1199
|
+
self.notify("No task selected", severity="warning")
|
|
1200
|
+
return
|
|
1201
|
+
task = self.current_task
|
|
1202
|
+
|
|
1203
|
+
def handle_rename(new_name: str | None) -> None:
|
|
1204
|
+
if new_name is None:
|
|
1205
|
+
return
|
|
1206
|
+
try:
|
|
1207
|
+
self.task_manager.set_task_display_name(task, new_name)
|
|
1208
|
+
except OSError as e:
|
|
1209
|
+
self.notify(escape(f"Rename failed: {e}"), severity="error")
|
|
1210
|
+
return
|
|
1211
|
+
label = task.display_label
|
|
1212
|
+
self.notify(escape(f"Task '{task.name}' shown as '{label}'"))
|
|
1213
|
+
self._log_activity(f"Task '{task.name}' renamed to '{label}'", MessageLevel.INFO)
|
|
1214
|
+
self._run_periodic_refresh(force_ui=True)
|
|
1215
|
+
|
|
1216
|
+
self.push_screen(RenameTaskModal(task.name, task.display_name), handle_rename)
|
|
1217
|
+
|
|
1196
1218
|
def _finish_task(self, task: Task, force: bool = False) -> None:
|
|
1197
1219
|
"""Kick off task deletion in a background thread."""
|
|
1198
1220
|
if not self._begin_mutation("task-list"):
|
|
@@ -1515,37 +1537,13 @@ class TaskTreeApp(App):
|
|
|
1515
1537
|
# Restore focus to task list
|
|
1516
1538
|
self.query_one("#task-list", TaskList).focus()
|
|
1517
1539
|
|
|
1518
|
-
def action_open_shell(self) -> None:
|
|
1519
|
-
"""Open a shell in the current worktree."""
|
|
1520
|
-
if not self.current_worktree:
|
|
1521
|
-
self.notify("No worktree selected", severity="warning")
|
|
1522
|
-
return
|
|
1523
|
-
|
|
1524
|
-
worktree_path = self.current_worktree.path
|
|
1525
|
-
if not worktree_path.exists():
|
|
1526
|
-
self.notify("Worktree directory not found", severity="error")
|
|
1527
|
-
return
|
|
1528
|
-
|
|
1529
|
-
# Save selection state BEFORE suspend (as local variables)
|
|
1530
|
-
saved_task_name = self.current_task.name if self.current_task else None
|
|
1531
|
-
saved_worktree_name = self.current_worktree.name
|
|
1532
|
-
|
|
1533
|
-
# Get shell from config
|
|
1534
|
-
shell = self.config.get_shell()
|
|
1535
|
-
|
|
1536
|
-
self.notify("Opening shell...")
|
|
1537
|
-
|
|
1538
|
-
# Suspend app and open shell
|
|
1539
|
-
with self.suspend():
|
|
1540
|
-
self._run_external_command([shell], cwd=worktree_path, name="shell")
|
|
1541
|
-
|
|
1542
|
-
# Refresh status after shell exits, restoring saved selection
|
|
1543
|
-
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
1544
|
-
# Restore focus to worktree list
|
|
1545
|
-
self.query_one("#worktree-list", WorktreeList).focus()
|
|
1546
|
-
|
|
1547
1540
|
def action_open_editor(self) -> None:
|
|
1548
|
-
"""Open editor in
|
|
1541
|
+
"""Open editor in a new terminal tab at the current folder.
|
|
1542
|
+
|
|
1543
|
+
Same mechanism as open_folder: the TUI keeps running instead of
|
|
1544
|
+
suspending, so the editor session and the dashboard live side by
|
|
1545
|
+
side (matches the `o`/`c` new-tab behavior).
|
|
1546
|
+
"""
|
|
1549
1547
|
focused = self.focused
|
|
1550
1548
|
|
|
1551
1549
|
# Determine folder based on focus (like open_folder does)
|
|
@@ -1562,24 +1560,9 @@ class TaskTreeApp(App):
|
|
|
1562
1560
|
self.notify("Folder not found", severity="error")
|
|
1563
1561
|
return
|
|
1564
1562
|
|
|
1565
|
-
# Save selection state BEFORE suspend
|
|
1566
|
-
saved_task_name = self.current_task.name if self.current_task else None
|
|
1567
|
-
saved_worktree_name = self.current_worktree.name if self.current_worktree else None
|
|
1568
|
-
|
|
1569
1563
|
editor = self.config.get_editor()
|
|
1570
|
-
self.
|
|
1571
|
-
|
|
1572
|
-
# Suspend app and run editor with "." to open directory
|
|
1573
|
-
with self.suspend():
|
|
1574
|
-
self._run_external_command([editor, "."], cwd=folder_path, name="editor")
|
|
1575
|
-
|
|
1576
|
-
# Refresh status after editor exits, restoring saved selection
|
|
1577
|
-
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
1578
|
-
# Restore focus to the panel that was focused
|
|
1579
|
-
if isinstance(focused, TaskList):
|
|
1580
|
-
self.query_one("#task-list", TaskList).focus()
|
|
1581
|
-
else:
|
|
1582
|
-
self.query_one("#worktree-list", WorktreeList).focus()
|
|
1564
|
+
self._open_ghostty_tab(folder_path, command=f"{editor} .")
|
|
1565
|
+
self.notify(escape(f"Opened {editor} in new tab"))
|
|
1583
1566
|
|
|
1584
1567
|
def _prepare_claude_session(self, task_path: Path) -> None:
|
|
1585
1568
|
"""Refresh CLAUDE.md files, worktree settings, and hooks before launching Claude."""
|
|
@@ -155,6 +155,7 @@ def cmd_list(manager: TaskManager, config: Config, args: argparse.Namespace) ->
|
|
|
155
155
|
payload = [
|
|
156
156
|
{
|
|
157
157
|
"name": task.name,
|
|
158
|
+
"display_name": task.display_name,
|
|
158
159
|
"path": str(task.path),
|
|
159
160
|
"repos": [wt.name for wt in task.worktrees],
|
|
160
161
|
"dirty": dirty[task.name],
|
|
@@ -12,6 +12,11 @@ APP_COMMANDS: list[tuple[str, str, str]] = [
|
|
|
12
12
|
("clone_task", "Clone Task", "Create a new task with the same repos as the current one"),
|
|
13
13
|
("add_repo", "Add Repo", "Add repositories to the current task"),
|
|
14
14
|
("delete_task", "Delete Task", "Delete the current task (with safety checks)"),
|
|
15
|
+
(
|
|
16
|
+
"rename_task",
|
|
17
|
+
"Rename Task",
|
|
18
|
+
"Set a display alias for the current task (folder/branch unchanged)",
|
|
19
|
+
),
|
|
15
20
|
("delete_worktree", "Delete Worktree", "Delete the selected worktree from the task"),
|
|
16
21
|
("open_lazygit", "Open Lazygit", "Open lazygit in the selected worktree"),
|
|
17
22
|
(
|
|
@@ -19,9 +24,12 @@ APP_COMMANDS: list[tuple[str, str, str]] = [
|
|
|
19
24
|
"Show Diff",
|
|
20
25
|
"Open hunk: all repos from the task panel, the selected repo from the worktree panel",
|
|
21
26
|
),
|
|
22
|
-
(
|
|
27
|
+
(
|
|
28
|
+
"open_editor",
|
|
29
|
+
"Open Editor",
|
|
30
|
+
"Open your editor in a new terminal tab at the focused task/worktree folder",
|
|
31
|
+
),
|
|
23
32
|
("open_folder", "Open Folder", "Open the focused folder in a new terminal tab"),
|
|
24
|
-
("open_shell", "Open Shell", "Open a shell in the selected worktree"),
|
|
25
33
|
("open_claude_resume", "Open Claude", "Open Claude Code for the task (resume if possible)"),
|
|
26
34
|
(
|
|
27
35
|
"dispatch_agent",
|
|
@@ -23,10 +23,10 @@ DEFAULT_KEYBINDINGS: dict[str, str] = {
|
|
|
23
23
|
"clone_task": "y",
|
|
24
24
|
"add_repo": "a",
|
|
25
25
|
"delete_task": "d",
|
|
26
|
+
"rename_task": "R",
|
|
26
27
|
"open_lazygit": "g",
|
|
27
28
|
"show_diff": "h",
|
|
28
29
|
"open_folder": "o",
|
|
29
|
-
"open_shell": "enter",
|
|
30
30
|
"open_editor": "e",
|
|
31
31
|
"open_claude_resume": "c",
|
|
32
32
|
"open_claude_gui_code": "C",
|
|
@@ -122,7 +122,6 @@ class Config:
|
|
|
122
122
|
claude_path: str = "claude"
|
|
123
123
|
claude_memory_dir: str = "~/.claude/tasktree-memory"
|
|
124
124
|
claude_repo_memory: bool = True
|
|
125
|
-
shell: str = ""
|
|
126
125
|
glab_path: str = "glab"
|
|
127
126
|
gh_path: str = "gh"
|
|
128
127
|
|
|
@@ -184,7 +183,6 @@ class Config:
|
|
|
184
183
|
claude_path = tools_config.get("claude_path", "claude")
|
|
185
184
|
claude_memory_dir = tools_config.get("claude_memory_dir", "~/.claude/tasktree-memory")
|
|
186
185
|
claude_repo_memory = bool(tools_config.get("claude_repo_memory", True))
|
|
187
|
-
shell = tools_config.get("shell", "")
|
|
188
186
|
glab_path = tools_config.get("glab_path", "glab")
|
|
189
187
|
gh_path = tools_config.get("gh_path", "gh")
|
|
190
188
|
|
|
@@ -221,8 +219,6 @@ class Config:
|
|
|
221
219
|
default_base_branch = os.environ["TASKTREE_DEFAULT_BRANCH"]
|
|
222
220
|
if "EDITOR" in os.environ and not editor:
|
|
223
221
|
editor = os.environ["EDITOR"]
|
|
224
|
-
if "SHELL" in os.environ and not shell:
|
|
225
|
-
shell = os.environ["SHELL"]
|
|
226
222
|
|
|
227
223
|
return cls(
|
|
228
224
|
repos_dir=repos_dir,
|
|
@@ -243,7 +239,6 @@ class Config:
|
|
|
243
239
|
claude_path=claude_path,
|
|
244
240
|
claude_memory_dir=claude_memory_dir,
|
|
245
241
|
claude_repo_memory=claude_repo_memory,
|
|
246
|
-
shell=shell,
|
|
247
242
|
glab_path=glab_path,
|
|
248
243
|
gh_path=gh_path,
|
|
249
244
|
forge_enabled=forge_enabled,
|
|
@@ -398,9 +393,6 @@ claude_memory_dir = "{self._toml_escape(self.claude_memory_dir)}"
|
|
|
398
393
|
# same repo and with the main checkout. Set to false to disable.
|
|
399
394
|
claude_repo_memory = {str(self.claude_repo_memory).lower()}
|
|
400
395
|
|
|
401
|
-
# Preferred shell (leave empty to use $SHELL)
|
|
402
|
-
shell = "{self._toml_escape(self.shell)}"
|
|
403
|
-
|
|
404
396
|
# Path to glab executable (GitLab CLI, used for MR/CI status)
|
|
405
397
|
glab_path = "{self._toml_escape(self.glab_path)}"
|
|
406
398
|
|
|
@@ -433,10 +425,10 @@ new_task = "{self._toml_escape(self.keybindings.get("new_task", "n"))}"
|
|
|
433
425
|
clone_task = "{self._toml_escape(self.keybindings.get("clone_task", "y"))}"
|
|
434
426
|
add_repo = "{self._toml_escape(self.keybindings.get("add_repo", "a"))}"
|
|
435
427
|
delete_task = "{self._toml_escape(self.keybindings.get("delete_task", "d"))}"
|
|
428
|
+
rename_task = "{self._toml_escape(self.keybindings.get("rename_task", "R"))}"
|
|
436
429
|
open_lazygit = "{self._toml_escape(self.keybindings.get("open_lazygit", "g"))}"
|
|
437
430
|
show_diff = "{self._toml_escape(self.keybindings.get("show_diff", "h"))}"
|
|
438
431
|
open_folder = "{self._toml_escape(self.keybindings.get("open_folder", "o"))}"
|
|
439
|
-
open_shell = "{self._toml_escape(self.keybindings.get("open_shell", "enter"))}"
|
|
440
432
|
open_editor = "{self._toml_escape(self.keybindings.get("open_editor", "e"))}"
|
|
441
433
|
open_claude_resume = "{self._toml_escape(self.keybindings.get("open_claude_resume", "c"))}"
|
|
442
434
|
open_claude_gui_code = "{self._toml_escape(self.keybindings.get("open_claude_gui_code", "C"))}"
|
|
@@ -513,12 +505,6 @@ blocklist = {self._toml_list(self.symlink_blocklist)}
|
|
|
513
505
|
|
|
514
506
|
return sorted(repos)
|
|
515
507
|
|
|
516
|
-
def get_shell(self) -> str:
|
|
517
|
-
"""Get the shell to use, with fallback to environment or /bin/bash."""
|
|
518
|
-
if self.shell:
|
|
519
|
-
return self.shell
|
|
520
|
-
return os.environ.get("SHELL", "/bin/bash")
|
|
521
|
-
|
|
522
508
|
def get_editor(self) -> str:
|
|
523
509
|
"""Get the editor to use, with fallback to environment or vi."""
|
|
524
510
|
if self.editor:
|
|
@@ -48,6 +48,24 @@ class Task:
|
|
|
48
48
|
"""Check if the task has a CLAUDE.md file."""
|
|
49
49
|
return (self.path / "CLAUDE.md").exists()
|
|
50
50
|
|
|
51
|
+
@property
|
|
52
|
+
def display_name(self) -> str | None:
|
|
53
|
+
"""Display alias from <task>/.tasktree_name, or None when unset.
|
|
54
|
+
|
|
55
|
+
Read lazily from disk so every Task construction site picks it up
|
|
56
|
+
and a rename is visible on the next render without reloading.
|
|
57
|
+
"""
|
|
58
|
+
try:
|
|
59
|
+
value = (self.path / ".tasktree_name").read_text(encoding="utf-8").strip()
|
|
60
|
+
except OSError:
|
|
61
|
+
return None
|
|
62
|
+
return value or None
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def display_label(self) -> str:
|
|
66
|
+
"""What the task list shows: the alias when set, else the real name."""
|
|
67
|
+
return self.display_name or self.name
|
|
68
|
+
|
|
51
69
|
|
|
52
70
|
@dataclass
|
|
53
71
|
class GitStatus:
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/task_manager.py
RENAMED
|
@@ -337,6 +337,22 @@ class TaskManager:
|
|
|
337
337
|
if task.path.exists():
|
|
338
338
|
shutil.rmtree(task.path)
|
|
339
339
|
|
|
340
|
+
def set_task_display_name(self, task: Task, display_name: str | None) -> None:
|
|
341
|
+
"""Set or clear the task's display alias (TUI label only).
|
|
342
|
+
|
|
343
|
+
The folder, branches and worktrees keep the real task name; the alias
|
|
344
|
+
lives in <task>/.tasktree_name. Empty input — or an alias equal to the
|
|
345
|
+
real name — clears the file.
|
|
346
|
+
"""
|
|
347
|
+
marker = task.path / ".tasktree_name"
|
|
348
|
+
name = (display_name or "").strip()
|
|
349
|
+
if name == task.name:
|
|
350
|
+
name = ""
|
|
351
|
+
if name:
|
|
352
|
+
marker.write_text(name + "\n", encoding="utf-8")
|
|
353
|
+
elif marker.exists():
|
|
354
|
+
marker.unlink()
|
|
355
|
+
|
|
340
356
|
def archive_task(self, task: Task, notes: list[str] | None = None) -> Path | None:
|
|
341
357
|
"""Write the task's combined diff to the archive directory.
|
|
342
358
|
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/create_modal.py
RENAMED
|
@@ -462,6 +462,53 @@ class SafeDeleteModal(ThemedModalScreen[str | None]):
|
|
|
462
462
|
self.dismiss("force")
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
class RenameTaskModal(ThemedModalScreen[str | None]):
|
|
466
|
+
"""Prompt for a task's display alias (TUI label only).
|
|
467
|
+
|
|
468
|
+
Dismisses with the new alias, "" to revert to the real name, or None
|
|
469
|
+
when cancelled.
|
|
470
|
+
"""
|
|
471
|
+
|
|
472
|
+
CANCEL_RESULT: ClassVar[None] = None
|
|
473
|
+
|
|
474
|
+
def __init__(self, task_name: str, current_alias: str | None, *args, **kwargs):
|
|
475
|
+
super().__init__(*args, **kwargs)
|
|
476
|
+
self.task_name = task_name
|
|
477
|
+
self.current_alias = current_alias
|
|
478
|
+
|
|
479
|
+
def compose(self) -> ComposeResult:
|
|
480
|
+
with Container():
|
|
481
|
+
yield Label("Rename Task (display only)", classes="modal-title")
|
|
482
|
+
yield Static(
|
|
483
|
+
escape(f"Folder/branch stay '{self.task_name}'; empty input reverts."),
|
|
484
|
+
classes="modal-message",
|
|
485
|
+
)
|
|
486
|
+
yield Input(
|
|
487
|
+
value=self.current_alias or "",
|
|
488
|
+
placeholder=self.task_name,
|
|
489
|
+
id="rename-input",
|
|
490
|
+
)
|
|
491
|
+
with Horizontal(classes="button-row"):
|
|
492
|
+
yield Button("Rename", variant="primary", id="rename-btn")
|
|
493
|
+
yield Button("Cancel", variant="default", id="cancel-btn")
|
|
494
|
+
|
|
495
|
+
def on_mount(self) -> None:
|
|
496
|
+
super().on_mount()
|
|
497
|
+
self.query_one("#rename-input", Input).focus()
|
|
498
|
+
|
|
499
|
+
def _submit(self) -> None:
|
|
500
|
+
self.dismiss(self.query_one("#rename-input", Input).value.strip())
|
|
501
|
+
|
|
502
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
503
|
+
self._submit()
|
|
504
|
+
|
|
505
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
506
|
+
if event.button.id == "rename-btn":
|
|
507
|
+
self._submit()
|
|
508
|
+
elif event.button.id == "cancel-btn":
|
|
509
|
+
self.dismiss(None)
|
|
510
|
+
|
|
511
|
+
|
|
465
512
|
class DispatchAgentModal(ThemedModalScreen[str | None]):
|
|
466
513
|
"""Prompt for dispatching a background Claude agent into a worktree.
|
|
467
514
|
|
|
@@ -669,7 +716,12 @@ class HelpModal(ThemedModalScreen[None]):
|
|
|
669
716
|
task_section += (
|
|
670
717
|
self._format_binding("add_repo", "a", "Add repository to current task") + "\n"
|
|
671
718
|
)
|
|
672
|
-
task_section +=
|
|
719
|
+
task_section += (
|
|
720
|
+
self._format_binding("delete_task", "d", "Delete/finish current task") + "\n"
|
|
721
|
+
)
|
|
722
|
+
task_section += self._format_binding(
|
|
723
|
+
"rename_task", "R", "Rename task display label (folder/branch unchanged)"
|
|
724
|
+
)
|
|
673
725
|
sections.append(task_section)
|
|
674
726
|
|
|
675
727
|
# Tools section
|
|
@@ -684,9 +736,8 @@ class HelpModal(ThemedModalScreen[None]):
|
|
|
684
736
|
+ "\n"
|
|
685
737
|
)
|
|
686
738
|
tools_section += (
|
|
687
|
-
self._format_binding("
|
|
739
|
+
self._format_binding("open_editor", "e", "Open editor in new terminal tab") + "\n"
|
|
688
740
|
)
|
|
689
|
-
tools_section += self._format_binding("open_editor", "e", "Open editor in folder") + "\n"
|
|
690
741
|
tools_section += (
|
|
691
742
|
self._format_binding(
|
|
692
743
|
"open_claude_resume", "c", "Open Claude CLI (resume if session exists, else new)"
|
|
@@ -88,10 +88,10 @@ class TaskList(OptionList):
|
|
|
88
88
|
hook_indicator = " [green]✓[/]"
|
|
89
89
|
else:
|
|
90
90
|
hook_indicator = ""
|
|
91
|
-
# Escape the
|
|
92
|
-
#
|
|
91
|
+
# Escape the label: task directories (and aliases) can hold
|
|
92
|
+
# markup-significant brackets which must not style (or crash)
|
|
93
93
|
# the list rendering
|
|
94
|
-
name = escape(task.
|
|
94
|
+
name = escape(task.display_label)
|
|
95
95
|
if task.is_dirty:
|
|
96
96
|
prompt = f"[red]●[/]{claude_md}{name} [red]({task.dirty_count})[/]{hook_indicator}"
|
|
97
97
|
else:
|
|
@@ -220,9 +220,9 @@ class TaskList(OptionList):
|
|
|
220
220
|
"""Sort tasks according to current sort mode."""
|
|
221
221
|
match self._sort_mode:
|
|
222
222
|
case SortMode.NAME_ASC:
|
|
223
|
-
return sorted(tasks, key=lambda t: t.
|
|
223
|
+
return sorted(tasks, key=lambda t: t.display_label.lower())
|
|
224
224
|
case SortMode.NAME_DESC:
|
|
225
|
-
return sorted(tasks, key=lambda t: t.
|
|
225
|
+
return sorted(tasks, key=lambda t: t.display_label.lower(), reverse=True)
|
|
226
226
|
case SortMode.DATE_NEWEST:
|
|
227
227
|
return sorted(
|
|
228
228
|
tasks,
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/worktree_list.py
RENAMED
|
@@ -76,9 +76,8 @@ class WorktreeList(OptionList):
|
|
|
76
76
|
self._max_branch_len: int = 0
|
|
77
77
|
# Footer-visible (key, app action, description) bindings shown while
|
|
78
78
|
# this panel has focus; the keys also exist app-level (hidden) so
|
|
79
|
-
# they keep working regardless of focus.
|
|
80
|
-
#
|
|
81
|
-
# swallow the key before the app's open_shell binding could see it.
|
|
79
|
+
# they keep working regardless of focus. Keys are popped first so a
|
|
80
|
+
# context binding can override an OptionList default for that key.
|
|
82
81
|
for key, action, description in context_bindings or []:
|
|
83
82
|
self._bindings.key_to_bindings.pop(key, None)
|
|
84
83
|
self._bindings.bind(key, f"app.{action}", description, show=True)
|
|
@@ -391,20 +391,71 @@ class TestOpenExternalCommands:
|
|
|
391
391
|
# App should still be running
|
|
392
392
|
assert app.is_running
|
|
393
393
|
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
|
|
395
|
+
class TestRenameTaskAction:
|
|
396
|
+
"""Tests for the rename (display alias) action."""
|
|
397
|
+
|
|
398
|
+
async def test_rename_via_keybinding(self, app, sample_repo, task_manager):
|
|
399
|
+
"""R -> modal -> alias shows in the task list; folder unchanged."""
|
|
400
|
+
from tasktree_manager.widgets.create_modal import RenameTaskModal
|
|
401
|
+
|
|
402
|
+
repo_path, branch = sample_repo
|
|
403
|
+
task = task_manager.create_task("RENAME-TASK", ["sample-repo"], branch)
|
|
404
|
+
|
|
396
405
|
async with app.run_test() as pilot:
|
|
397
406
|
await pilot.pause()
|
|
407
|
+
await pilot.press("R")
|
|
408
|
+
await pilot.pause()
|
|
409
|
+
assert isinstance(app.screen_stack[-1], RenameTaskModal)
|
|
398
410
|
|
|
399
|
-
|
|
400
|
-
|
|
411
|
+
await pilot.press(*"nice label")
|
|
412
|
+
await pilot.press("enter")
|
|
413
|
+
await app.workers.wait_for_complete()
|
|
414
|
+
await pilot.pause()
|
|
415
|
+
|
|
416
|
+
assert (task.path / ".tasktree_name").read_text().strip() == "nice label"
|
|
417
|
+
assert task.path.exists() # folder untouched
|
|
418
|
+
task_list = app.query_one("#task-list", TaskList)
|
|
419
|
+
prompt = str(task_list.get_option("RENAME-TASK").prompt)
|
|
420
|
+
assert "nice label" in prompt
|
|
421
|
+
assert "RENAME-TASK" not in prompt
|
|
422
|
+
|
|
423
|
+
async def test_rename_cancel_changes_nothing(self, app, sample_repo, task_manager):
|
|
424
|
+
repo_path, branch = sample_repo
|
|
425
|
+
task = task_manager.create_task("RENAME-CANCEL", ["sample-repo"], branch)
|
|
426
|
+
|
|
427
|
+
async with app.run_test() as pilot:
|
|
428
|
+
await pilot.pause()
|
|
429
|
+
await pilot.press("R")
|
|
430
|
+
await pilot.pause()
|
|
431
|
+
await pilot.press("escape")
|
|
432
|
+
await pilot.pause()
|
|
433
|
+
assert not (task.path / ".tasktree_name").exists()
|
|
434
|
+
|
|
435
|
+
async def test_rename_empty_reverts(self, app, sample_repo, task_manager):
|
|
436
|
+
repo_path, branch = sample_repo
|
|
437
|
+
task = task_manager.create_task("RENAME-REVERT", ["sample-repo"], branch)
|
|
438
|
+
task_manager.set_task_display_name(task, "old label")
|
|
401
439
|
|
|
402
|
-
|
|
440
|
+
async with app.run_test() as pilot:
|
|
441
|
+
await pilot.pause()
|
|
442
|
+
await pilot.press("R")
|
|
443
|
+
await pilot.pause()
|
|
444
|
+
# Prefilled with the current alias - clear it
|
|
445
|
+
from textual.widgets import Input
|
|
446
|
+
|
|
447
|
+
from tasktree_manager.widgets.create_modal import RenameTaskModal
|
|
448
|
+
|
|
449
|
+
modal = app.screen_stack[-1]
|
|
450
|
+
assert isinstance(modal, RenameTaskModal)
|
|
451
|
+
modal.query_one("#rename-input", Input).value = ""
|
|
403
452
|
await pilot.press("enter")
|
|
453
|
+
await app.workers.wait_for_complete()
|
|
404
454
|
await pilot.pause()
|
|
405
455
|
|
|
406
|
-
|
|
407
|
-
|
|
456
|
+
assert not (task.path / ".tasktree_name").exists()
|
|
457
|
+
task_list = app.query_one("#task-list", TaskList)
|
|
458
|
+
assert "RENAME-REVERT" in str(task_list.get_option("RENAME-REVERT").prompt)
|
|
408
459
|
|
|
409
460
|
|
|
410
461
|
class TestShowDiffAction:
|
|
@@ -651,7 +702,6 @@ class TestContextSensitiveFooter:
|
|
|
651
702
|
assert "app.delete_task" in shown
|
|
652
703
|
# Worktree-context actions are hidden in this context
|
|
653
704
|
assert "app.open_lazygit" not in shown
|
|
654
|
-
assert "app.open_shell" not in shown
|
|
655
705
|
# Globals always visible
|
|
656
706
|
assert "cycle_theme" in shown
|
|
657
707
|
assert "refresh" in shown
|
|
@@ -675,7 +725,6 @@ class TestContextSensitiveFooter:
|
|
|
675
725
|
assert app.focused.id == "worktree-list"
|
|
676
726
|
shown = self._shown_actions(app)
|
|
677
727
|
assert "app.open_lazygit" in shown
|
|
678
|
-
assert "app.open_shell" in shown
|
|
679
728
|
assert "app.push_all" in shown
|
|
680
729
|
# Task-context actions are hidden in this context
|
|
681
730
|
assert "app.new_task" not in shown
|
|
@@ -699,22 +748,3 @@ class TestContextSensitiveFooter:
|
|
|
699
748
|
|
|
700
749
|
create_modals = [s for s in app.screen_stack if isinstance(s, CreateTaskModal)]
|
|
701
750
|
assert len(create_modals) == 1
|
|
702
|
-
|
|
703
|
-
async def test_enter_opens_shell_from_worktree_list(
|
|
704
|
-
self, app, sample_repo, task_manager, monkeypatch
|
|
705
|
-
):
|
|
706
|
-
"""Enter on the worktree list triggers open_shell (was swallowed by select)."""
|
|
707
|
-
repo_path, branch = sample_repo
|
|
708
|
-
task_manager.create_task("FOOTER-ENTER", ["sample-repo"], branch)
|
|
709
|
-
|
|
710
|
-
called = []
|
|
711
|
-
monkeypatch.setattr(app, "action_open_shell", lambda: called.append(True))
|
|
712
|
-
|
|
713
|
-
async with app.run_test() as pilot:
|
|
714
|
-
await pilot.pause()
|
|
715
|
-
await pilot.press("tab")
|
|
716
|
-
await pilot.pause()
|
|
717
|
-
await pilot.press("enter")
|
|
718
|
-
await pilot.pause()
|
|
719
|
-
|
|
720
|
-
assert called
|
|
@@ -131,7 +131,6 @@ class TestConfig:
|
|
|
131
131
|
assert config.editor == ""
|
|
132
132
|
assert config.lazygit_path == "lazygit"
|
|
133
133
|
assert config.hunk_path == "hunk"
|
|
134
|
-
assert config.shell == ""
|
|
135
134
|
assert config.archive_dir == ""
|
|
136
135
|
assert config.agent_poll_interval == 10
|
|
137
136
|
assert config.forge_poll_interval == 60
|
|
@@ -233,7 +232,6 @@ timeout = 60
|
|
|
233
232
|
editor = "nvim"
|
|
234
233
|
lazygit_path = "/usr/local/bin/lazygit"
|
|
235
234
|
hunk_path = "/usr/local/bin/hunk"
|
|
236
|
-
shell = "/bin/zsh"
|
|
237
235
|
'''
|
|
238
236
|
config_file.write_text(config_content)
|
|
239
237
|
|
|
@@ -254,24 +252,12 @@ shell = "/bin/zsh"
|
|
|
254
252
|
assert config.editor == "nvim"
|
|
255
253
|
assert config.lazygit_path == "/usr/local/bin/lazygit"
|
|
256
254
|
assert config.hunk_path == "/usr/local/bin/hunk"
|
|
257
|
-
assert config.shell == "/bin/zsh"
|
|
258
255
|
finally:
|
|
259
256
|
if old_xdg is not None:
|
|
260
257
|
os.environ["XDG_CONFIG_HOME"] = old_xdg
|
|
261
258
|
else:
|
|
262
259
|
os.environ.pop("XDG_CONFIG_HOME", None)
|
|
263
260
|
|
|
264
|
-
def test_get_shell_from_config(self):
|
|
265
|
-
"""Test get_shell returns config value when set."""
|
|
266
|
-
config = Config(shell="/bin/zsh")
|
|
267
|
-
assert config.get_shell() == "/bin/zsh"
|
|
268
|
-
|
|
269
|
-
def test_get_shell_fallback_to_env(self, monkeypatch):
|
|
270
|
-
"""Test get_shell falls back to environment."""
|
|
271
|
-
monkeypatch.setenv("SHELL", "/bin/fish")
|
|
272
|
-
config = Config(shell="")
|
|
273
|
-
assert config.get_shell() == "/bin/fish"
|
|
274
|
-
|
|
275
261
|
def test_get_editor_from_config(self):
|
|
276
262
|
"""Test get_editor returns config value when set."""
|
|
277
263
|
config = Config(editor="nvim")
|
|
@@ -304,7 +290,6 @@ shell = "/bin/zsh"
|
|
|
304
290
|
editor="vim",
|
|
305
291
|
lazygit_path="/opt/bin/lazygit",
|
|
306
292
|
hunk_path="/opt/bin/hunk",
|
|
307
|
-
shell="/bin/zsh",
|
|
308
293
|
glab_path="/opt/bin/glab",
|
|
309
294
|
gh_path="/opt/bin/gh",
|
|
310
295
|
forge_enabled=False,
|
|
@@ -329,7 +314,6 @@ shell = "/bin/zsh"
|
|
|
329
314
|
assert loaded.editor == "vim"
|
|
330
315
|
assert loaded.lazygit_path == "/opt/bin/lazygit"
|
|
331
316
|
assert loaded.hunk_path == "/opt/bin/hunk"
|
|
332
|
-
assert loaded.shell == "/bin/zsh"
|
|
333
317
|
assert loaded.archive_dir == "/tmp/archive"
|
|
334
318
|
assert loaded.agent_poll_interval == 15
|
|
335
319
|
assert loaded.forge_poll_interval == 90
|
|
@@ -433,6 +433,48 @@ class TestTaskBaseRecording:
|
|
|
433
433
|
assert GitOps.get_task_base(worktree_from_repo, "no-such-branch") is None
|
|
434
434
|
|
|
435
435
|
|
|
436
|
+
class TestDisplayName:
|
|
437
|
+
"""Display alias: TUI label only, folder/branch untouched."""
|
|
438
|
+
|
|
439
|
+
def test_set_and_read_alias(self, task_manager, sample_repo):
|
|
440
|
+
repo_path, branch = sample_repo
|
|
441
|
+
task = task_manager.create_task("DIC-1901-argocd-tls", ["sample-repo"], branch)
|
|
442
|
+
|
|
443
|
+
task_manager.set_task_display_name(task, "ArgoCD TLS rollout")
|
|
444
|
+
assert task.display_name == "ArgoCD TLS rollout"
|
|
445
|
+
assert task.display_label == "ArgoCD TLS rollout"
|
|
446
|
+
# Real identity untouched
|
|
447
|
+
assert task.name == "DIC-1901-argocd-tls"
|
|
448
|
+
assert task.path.name == "DIC-1901-argocd-tls"
|
|
449
|
+
assert (task.path / ".tasktree_name").exists()
|
|
450
|
+
# A fresh load sees the alias too (read lazily from disk)
|
|
451
|
+
reloaded = task_manager.get_task("DIC-1901-argocd-tls")
|
|
452
|
+
assert reloaded is not None
|
|
453
|
+
assert reloaded.display_label == "ArgoCD TLS rollout"
|
|
454
|
+
|
|
455
|
+
def test_clear_alias_with_empty(self, task_manager, sample_repo):
|
|
456
|
+
repo_path, branch = sample_repo
|
|
457
|
+
task = task_manager.create_task("ALIAS-CLEAR", ["sample-repo"], branch)
|
|
458
|
+
task_manager.set_task_display_name(task, "temp label")
|
|
459
|
+
task_manager.set_task_display_name(task, "")
|
|
460
|
+
assert task.display_name is None
|
|
461
|
+
assert task.display_label == "ALIAS-CLEAR"
|
|
462
|
+
assert not (task.path / ".tasktree_name").exists()
|
|
463
|
+
|
|
464
|
+
def test_alias_equal_to_real_name_clears(self, task_manager, sample_repo):
|
|
465
|
+
repo_path, branch = sample_repo
|
|
466
|
+
task = task_manager.create_task("ALIAS-SAME", ["sample-repo"], branch)
|
|
467
|
+
task_manager.set_task_display_name(task, "ALIAS-SAME")
|
|
468
|
+
assert not (task.path / ".tasktree_name").exists()
|
|
469
|
+
assert task.display_label == "ALIAS-SAME"
|
|
470
|
+
|
|
471
|
+
def test_no_alias_by_default(self, task_manager, sample_repo):
|
|
472
|
+
repo_path, branch = sample_repo
|
|
473
|
+
task = task_manager.create_task("ALIAS-NONE", ["sample-repo"], branch)
|
|
474
|
+
assert task.display_name is None
|
|
475
|
+
assert task.display_label == "ALIAS-NONE"
|
|
476
|
+
|
|
477
|
+
|
|
436
478
|
class TestForgeAwareSafety:
|
|
437
479
|
"""Squash/rebase merges detected via the forge (glab/gh) MR state."""
|
|
438
480
|
|
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/README.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/data_viewer.py
RENAMED
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/todo_app.py
RENAMED
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/assets/worker_demo.py
RENAMED
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/layouts.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/.claude/skills/textual-tui/references/styling.md
RENAMED
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.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
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/agent_sessions.py
RENAMED
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/services/claude_hooks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/messages_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.11.0 → tasktree_manager-2.12.0}/tasktree_manager/widgets/status_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
|
|
File without changes
|
|
File without changes
|