tasktree-manager 2.4.0__tar.gz → 2.6.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.4.0 → tasktree_manager-2.6.0}/CHANGELOG.md +12 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/PKG-INFO +1 -1
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/README.md +9 -7
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/docs/configuration.md +29 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/docs/user-guide.md +40 -5
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/pyproject.toml +1 -1
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/app.py +108 -3
- tasktree_manager-2.6.0/tasktree_manager/commands.py +74 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/claude_hooks.py +9 -1
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/config.py +16 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/git_ops.py +70 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/create_modal.py +62 -62
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_app.py +55 -2
- tasktree_manager-2.6.0/tests/test_claude_hooks.py +106 -0
- tasktree_manager-2.6.0/tests/test_commands.py +55 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_config.py +16 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_git_ops.py +75 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_modals.py +103 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.github/workflows/release.yml +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/.gitignore +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/CONTRIBUTING.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/LICENSE +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/OPENSPEC.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/ROADMAP.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/docs/installation.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/docs/troubleshooting.md +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/mise.toml +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/scripts/bump_version.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/models.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/services/task_manager.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/setup_modal.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/status_panel.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/task_list.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tasktree_manager/widgets/worktree_list.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/__init__.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/conftest.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_memory.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.0}/tests/test_status_panel.py +0 -0
- {tasktree_manager-2.4.0 → tasktree_manager-2.6.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.6.0] - 2026-07-13
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Hunk diff viewer, shared Claude memory pool, repo filter fix (#8)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.5.0] - 2026-06-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Searchable app actions in the command palette (#7)
|
|
28
|
+
|
|
29
|
+
|
|
18
30
|
## [2.4.0] - 2026-06-10
|
|
19
31
|
|
|
20
32
|
### Added
|
|
@@ -57,8 +57,9 @@ On first run, you'll configure:
|
|
|
57
57
|
|
|
58
58
|
1. **Create a task** - Press `n`, enter task name, select repositories
|
|
59
59
|
2. **Work on code** - Press `e` for editor, `g` for lazygit, or `Enter` for shell
|
|
60
|
-
3. **
|
|
61
|
-
4. **
|
|
60
|
+
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)
|
|
61
|
+
4. **Push changes** - Press `p` to push all worktrees
|
|
62
|
+
5. **Delete task** - Press `d` when finished
|
|
62
63
|
|
|
63
64
|
For detailed workflows and examples, see the [User Guide](docs/user-guide.md).
|
|
64
65
|
|
|
@@ -75,14 +76,15 @@ For detailed workflows and examples, see the [User Guide](docs/user-guide.md).
|
|
|
75
76
|
| Key | Action | Key | Action |
|
|
76
77
|
|-----|--------|-----|--------|
|
|
77
78
|
| `n` | New task | `g` | Open lazygit |
|
|
78
|
-
| `a` | Add repo | `
|
|
79
|
-
| `d` | Delete task | `
|
|
80
|
-
| `p` | Push all | `
|
|
79
|
+
| `a` | Add repo | `h` | Show diff (hunk) |
|
|
80
|
+
| `d` | Delete task | `e` | Open editor |
|
|
81
|
+
| `p` | Push all | `o` | Open folder |
|
|
82
|
+
| `P` | Pull all | `Enter` | Open shell |
|
|
81
83
|
| `c` | Claude (resume) | `C` | Claude (new) |
|
|
82
84
|
| `r` | Refresh | `s` | Sort tasks |
|
|
83
85
|
| `S` | Group worktrees | `m` | Messages |
|
|
84
|
-
| `
|
|
85
|
-
|
|
|
86
|
+
| `Tab` | Next panel | `?` | Show help |
|
|
87
|
+
| `q` | Quit | | |
|
|
86
88
|
|
|
87
89
|
**Themes:** Press `Ctrl+P` to switch between 7 built-in themes (textual-dark, nord, gruvbox, tokyo-night, monokai, dracula, textual-light).
|
|
88
90
|
|
|
@@ -133,6 +133,25 @@ editor = ""
|
|
|
133
133
|
# Default: "lazygit" (searches PATH)
|
|
134
134
|
lazygit_path = "lazygit"
|
|
135
135
|
|
|
136
|
+
# Path to hunk executable (terminal diff viewer)
|
|
137
|
+
# https://github.com/modem-dev/hunk
|
|
138
|
+
# Default: "hunk" (searches PATH)
|
|
139
|
+
hunk_path = "hunk"
|
|
140
|
+
|
|
141
|
+
# Path to claude CLI executable
|
|
142
|
+
# Default: "claude" (searches PATH)
|
|
143
|
+
claude_path = "claude"
|
|
144
|
+
|
|
145
|
+
# Shared Claude Code auto-memory directory for task sessions.
|
|
146
|
+
# Task folders are not git repositories, so by default Claude Code would key
|
|
147
|
+
# its auto memory to the task path — memory that is orphaned when the task is
|
|
148
|
+
# deleted. tasktree-manager writes this path as "autoMemoryDirectory" into each
|
|
149
|
+
# task's .claude/settings.local.json so every task session reads and writes one
|
|
150
|
+
# shared memory pool that persists across tasks.
|
|
151
|
+
# Set to "" to disable (each task keeps its own throwaway memory).
|
|
152
|
+
# Default: "~/.claude/tasktree-memory"
|
|
153
|
+
claude_memory_dir = "~/.claude/tasktree-memory"
|
|
154
|
+
|
|
136
155
|
# Preferred shell
|
|
137
156
|
# Leave empty to use $SHELL environment variable
|
|
138
157
|
# Default: "" (uses $SHELL, falls back to /bin/bash)
|
|
@@ -158,6 +177,7 @@ delete_task = "d" # Delete/finish task
|
|
|
158
177
|
|
|
159
178
|
# Git operations
|
|
160
179
|
open_lazygit = "g" # Open lazygit in selected worktree
|
|
180
|
+
show_diff = "h" # Open hunk diff (task panel = all repos, worktree panel = one repo)
|
|
161
181
|
open_editor = "e" # Open editor in selected task/worktree
|
|
162
182
|
open_folder = "o" # Open folder in new terminal tab
|
|
163
183
|
open_shell = "enter" # Open shell in selected worktree
|
|
@@ -305,6 +325,7 @@ refresh_interval = 15 # Refresh every 15 seconds
|
|
|
305
325
|
|---------------|--------|-------------|---------------------|--------------------------------------|
|
|
306
326
|
| `editor` | string | `""` | `EDITOR` | Command to launch your editor |
|
|
307
327
|
| `lazygit_path` | string | `"lazygit"` | - | Path to lazygit executable |
|
|
328
|
+
| `hunk_path` | string | `"hunk"` | - | Path to hunk diff-viewer executable |
|
|
308
329
|
| `shell` | string | `""` | `SHELL` | Shell to use when opening terminals |
|
|
309
330
|
|
|
310
331
|
### Details
|
|
@@ -320,6 +341,13 @@ refresh_interval = 15 # Refresh every 15 seconds
|
|
|
320
341
|
- Use absolute path for custom location: `"/opt/homebrew/bin/lazygit"`
|
|
321
342
|
- Install lazygit: `brew install lazygit` (macOS), `apt install lazygit` (Ubuntu)
|
|
322
343
|
|
|
344
|
+
**`hunk_path`:**
|
|
345
|
+
- [hunk](https://github.com/modem-dev/hunk) is a review-first terminal diff viewer
|
|
346
|
+
- Default `"hunk"` searches PATH
|
|
347
|
+
- Use absolute path for custom location: `"/opt/homebrew/bin/hunk"`
|
|
348
|
+
- Install hunk: `brew install hunk` (macOS) or `npm i -g hunkdiff`
|
|
349
|
+
- 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
|
|
350
|
+
|
|
323
351
|
**`shell`:**
|
|
324
352
|
- Leave empty (`""`) to use `$SHELL` environment variable
|
|
325
353
|
- Fallback: `/bin/bash`
|
|
@@ -441,6 +469,7 @@ All 22 customizable keybindings with descriptions:
|
|
|
441
469
|
| Action | Default | Description | Customization Example |
|
|
442
470
|
|----------------|----------|---------------------------------------|-----------------------------|
|
|
443
471
|
| `open_lazygit` | `g` | Open lazygit in selected worktree | `open_lazygit = "ctrl+g"` |
|
|
472
|
+
| `show_diff` | `h` | Open hunk diff (task = all repos, worktree = one) | `show_diff = "ctrl+h"` |
|
|
444
473
|
| `open_editor` | `e` | Open editor in task/worktree folder | `open_editor = "ctrl+e"` |
|
|
445
474
|
| `open_folder` | `o` | Open folder in new terminal tab | `open_folder = "ctrl+o"` |
|
|
446
475
|
| `open_shell` | `enter` | Open shell in selected worktree | `open_shell = "ctrl+t"` |
|
|
@@ -134,17 +134,22 @@ A **worktree** is a git working directory for a specific repository within a tas
|
|
|
134
134
|
- Stage files, commit changes, resolve conflicts
|
|
135
135
|
- Exit lazygit (press `q` in lazygit)
|
|
136
136
|
|
|
137
|
-
3. **
|
|
137
|
+
3. **Review the diff:**
|
|
138
|
+
- Press `h` from the worktree panel to review that repo's changes in [hunk](https://github.com/modem-dev/hunk)
|
|
139
|
+
- Press `h` from the task panel to review a combined diff across **all** the task's repos at once
|
|
140
|
+
- Exit hunk (press `q`)
|
|
141
|
+
|
|
142
|
+
4. **Or open a shell:**
|
|
138
143
|
- Press `Enter` to open a shell in the worktree
|
|
139
144
|
- Make changes with your editor/IDE
|
|
140
145
|
- Run build/test commands
|
|
141
146
|
- Exit shell (type `exit`)
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
5. **Push changes:**
|
|
144
149
|
- Press `p` to push all worktrees in the task
|
|
145
150
|
- Or use lazygit (`g`) to push individually
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
6. **Delete the task:**
|
|
148
153
|
- Press `d` to delete/finish the task
|
|
149
154
|
- Follow prompts if there are uncommitted or unpushed changes
|
|
150
155
|
- Worktrees are removed (branches remain on remote)
|
|
@@ -168,7 +173,7 @@ tasktree-manager uses a **3-panel layout**:
|
|
|
168
173
|
│ M src/auth.py │
|
|
169
174
|
│ ?? README.md │
|
|
170
175
|
└──────────────────────────────────────────────────────────────────────────┘
|
|
171
|
-
n New d Delete g Lazygit p Push r Refresh ? Help q Quit
|
|
176
|
+
n New d Delete g Lazygit h Diff p Push r Refresh ? Help q Quit
|
|
172
177
|
```
|
|
173
178
|
|
|
174
179
|
### Left Panel: Task List
|
|
@@ -325,6 +330,7 @@ Useful when you're spinning up several similar tasks that share the same set of
|
|
|
325
330
|
| Key | Action | Description |
|
|
326
331
|
|---------|--------------------|------------------------------------------------------|
|
|
327
332
|
| `g` | Open lazygit | Open lazygit in the selected worktree |
|
|
333
|
+
| `h` | Show diff | Open hunk: all repos from the task panel, the selected repo from the worktree panel |
|
|
328
334
|
| `e` | Open editor | Open editor in task/worktree folder (runs `$EDITOR .`) |
|
|
329
335
|
| `Enter` | Open shell | Open a shell in the selected worktree |
|
|
330
336
|
| `p` | Push all | Push all worktrees in the current task (parallel) |
|
|
@@ -359,12 +365,13 @@ Useful when you're spinning up several similar tasks that share the same set of
|
|
|
359
365
|
- `C` opens the Claude desktop app via the `claude://code/new?folder=<task-folder>` URL scheme.
|
|
360
366
|
- The CLI variant (`c`) generates `CLAUDE.md` files with task/worktree context and installs hooks that report session status back to tasktree-manager.
|
|
361
367
|
- Session status (`⟳` running, `!` waiting, `✓` ended) appears next to the task name
|
|
368
|
+
- Both variants point the session's Claude Code auto-memory at a shared pool (`claude_memory_dir`, default `~/.claude/tasktree-memory`). Task folders are not git repos, so without this each task would get its own memory directory that is orphaned when the task is deleted; with the shared pool, what Claude learns in one task carries over to future tasks.
|
|
362
369
|
|
|
363
370
|
### General
|
|
364
371
|
|
|
365
372
|
| Key | Action | Description |
|
|
366
373
|
|---------|-------------------------|----------------------------------------------|
|
|
367
|
-
| `Ctrl+P`| Open Command Palette |
|
|
374
|
+
| `Ctrl+P`| Open Command Palette | Search and run any action (hotkeys shown), switch themes |
|
|
368
375
|
| `o` | Open folder | Open current folder in new terminal tab |
|
|
369
376
|
| `m` | Toggle messages | Show/hide the activity messages panel |
|
|
370
377
|
| `?` | Show help | Display help modal with keybindings |
|
|
@@ -666,6 +673,34 @@ apt install lazygit
|
|
|
666
673
|
# See: https://github.com/jesseduffield/lazygit
|
|
667
674
|
```
|
|
668
675
|
|
|
676
|
+
### Hunk
|
|
677
|
+
|
|
678
|
+
[hunk](https://github.com/modem-dev/hunk) is a review-first terminal diff viewer, well suited to reviewing agent-authored changesets across a multi-repo task.
|
|
679
|
+
|
|
680
|
+
**Why hunk:**
|
|
681
|
+
- Fast, keyboard-driven diff review with a desktop-inspired layout
|
|
682
|
+
- Reviews an entire task's changes across all its repos in one view
|
|
683
|
+
- Complements lazygit (which stages/commits per repo)
|
|
684
|
+
|
|
685
|
+
**Using hunk:**
|
|
686
|
+
1. From the **worktree panel**, press `h` to review the selected repo's uncommitted changes
|
|
687
|
+
2. From the **task panel**, press `h` to review a combined diff across **all** the task's repos at once (each file is grouped under its repo name)
|
|
688
|
+
3. tasktree-manager suspends, hunk opens
|
|
689
|
+
4. Press `q` in hunk to return; status auto-refreshes and selection is preserved
|
|
690
|
+
|
|
691
|
+
The combined task view includes both tracked (staged and unstaged) changes and untracked files.
|
|
692
|
+
|
|
693
|
+
**Install hunk:**
|
|
694
|
+
```bash
|
|
695
|
+
# macOS
|
|
696
|
+
brew install hunk
|
|
697
|
+
|
|
698
|
+
# npm
|
|
699
|
+
npm i -g hunkdiff
|
|
700
|
+
|
|
701
|
+
# See: https://github.com/modem-dev/hunk
|
|
702
|
+
```
|
|
703
|
+
|
|
669
704
|
### Editors
|
|
670
705
|
|
|
671
706
|
**Direct editor integration:**
|
|
@@ -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.6.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.6.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 6, 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 {
|
|
@@ -221,12 +226,13 @@ class TaskTreeApp(App):
|
|
|
221
226
|
Binding("a", "add_repo", "Add Repo", show=False),
|
|
222
227
|
Binding("d", "delete_task", "Delete", show=False),
|
|
223
228
|
Binding("g", "open_lazygit", "Lazygit", show=False),
|
|
229
|
+
Binding("h", "show_diff", "Diff", show=False),
|
|
224
230
|
Binding("e", "open_editor", "Editor", show=False),
|
|
225
231
|
Binding("o", "open_folder", "Open", show=False),
|
|
226
232
|
Binding("c", "open_claude_resume", "Claude", show=False),
|
|
227
233
|
Binding("p", "push_all", "Push", show=False),
|
|
228
234
|
Binding("r", "refresh", "Refresh"),
|
|
229
|
-
Binding("m", "toggle_messages", "Messages"
|
|
235
|
+
Binding("m", "toggle_messages", "Messages"),
|
|
230
236
|
Binding("q", "quit", "Quit"),
|
|
231
237
|
Binding("?", "help", "Help"),
|
|
232
238
|
Binding("enter", "open_shell", "Shell", show=False),
|
|
@@ -282,12 +288,13 @@ class TaskTreeApp(App):
|
|
|
282
288
|
Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo", show=False),
|
|
283
289
|
Binding(kb.get("delete_task", "d"), "delete_task", "Delete", show=False),
|
|
284
290
|
Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit", show=False),
|
|
291
|
+
Binding(kb.get("show_diff", "h"), "show_diff", "Diff", show=False),
|
|
285
292
|
Binding(kb.get("open_editor", "e"), "open_editor", "Editor", show=False),
|
|
286
293
|
Binding(kb.get("open_folder", "o"), "open_folder", "Open", show=False),
|
|
287
294
|
Binding(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude", show=False),
|
|
288
295
|
Binding(kb.get("push_all", "p"), "push_all", "Push", show=False),
|
|
289
296
|
Binding(kb.get("refresh", "r"), "refresh", "Refresh"),
|
|
290
|
-
Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages"
|
|
297
|
+
Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages"),
|
|
291
298
|
Binding(kb.get("quit", "q"), "quit", "Quit"),
|
|
292
299
|
Binding(kb.get("help", "?"), "help", "Help"),
|
|
293
300
|
Binding(kb.get("open_shell", "enter"), "open_shell", "Shell", show=False),
|
|
@@ -325,11 +332,13 @@ class TaskTreeApp(App):
|
|
|
325
332
|
(kb.get("clone_task", "y"), "clone_task", "Clone"),
|
|
326
333
|
(kb.get("add_repo", "a"), "add_repo", "Add Repo"),
|
|
327
334
|
(kb.get("delete_task", "d"), "delete_task", "Delete"),
|
|
335
|
+
(kb.get("show_diff", "h"), "show_diff", "Diff"),
|
|
328
336
|
(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
|
|
329
337
|
(kb.get("cycle_sort", "s"), "cycle_sort", "Sort"),
|
|
330
338
|
]
|
|
331
339
|
worktree_panel_bindings = [
|
|
332
340
|
(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit"),
|
|
341
|
+
(kb.get("show_diff", "h"), "show_diff", "Diff"),
|
|
333
342
|
(kb.get("open_editor", "e"), "open_editor", "Editor"),
|
|
334
343
|
(kb.get("open_folder", "o"), "open_folder", "Open"),
|
|
335
344
|
(kb.get("open_shell", "enter"), "open_shell", "Shell"),
|
|
@@ -1106,6 +1115,101 @@ class TaskTreeApp(App):
|
|
|
1106
1115
|
# Restore focus to worktree list
|
|
1107
1116
|
self.query_one("#worktree-list", WorktreeList).focus()
|
|
1108
1117
|
|
|
1118
|
+
def action_show_diff(self) -> None:
|
|
1119
|
+
"""Open the hunk diff viewer, scoped by which panel is focused.
|
|
1120
|
+
|
|
1121
|
+
From the task panel, shows a combined diff across all the task's repos.
|
|
1122
|
+
From the worktree panel, shows the diff for the selected worktree only.
|
|
1123
|
+
"""
|
|
1124
|
+
focused = self.focused
|
|
1125
|
+
if isinstance(focused, TaskList):
|
|
1126
|
+
self._show_task_diff()
|
|
1127
|
+
elif isinstance(focused, WorktreeList):
|
|
1128
|
+
self._show_worktree_diff()
|
|
1129
|
+
else:
|
|
1130
|
+
self.notify("Select a task or worktree first", severity="warning")
|
|
1131
|
+
|
|
1132
|
+
def _show_worktree_diff(self) -> None:
|
|
1133
|
+
"""Open hunk on the selected worktree's working-tree changes."""
|
|
1134
|
+
if not self.current_worktree:
|
|
1135
|
+
self.notify("No worktree selected", severity="warning")
|
|
1136
|
+
return
|
|
1137
|
+
|
|
1138
|
+
worktree_path = self.current_worktree.path
|
|
1139
|
+
if not worktree_path.exists():
|
|
1140
|
+
self.notify("Worktree directory not found", severity="error")
|
|
1141
|
+
return
|
|
1142
|
+
|
|
1143
|
+
# Save selection state BEFORE suspend (as local variables)
|
|
1144
|
+
saved_task_name = self.current_task.name if self.current_task else None
|
|
1145
|
+
saved_worktree_name = self.current_worktree.name
|
|
1146
|
+
|
|
1147
|
+
self.notify("Opening hunk...")
|
|
1148
|
+
|
|
1149
|
+
# Suspend app and run hunk on this worktree's changes
|
|
1150
|
+
with self.suspend():
|
|
1151
|
+
self._run_external_command(
|
|
1152
|
+
[self.config.hunk_path, "diff"],
|
|
1153
|
+
cwd=worktree_path,
|
|
1154
|
+
name="hunk",
|
|
1155
|
+
install_hint="brew install hunk",
|
|
1156
|
+
)
|
|
1157
|
+
|
|
1158
|
+
# Refresh status after hunk exits, restoring saved selection
|
|
1159
|
+
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
1160
|
+
# Restore focus to worktree list
|
|
1161
|
+
self.query_one("#worktree-list", WorktreeList).focus()
|
|
1162
|
+
|
|
1163
|
+
def _show_task_diff(self) -> None:
|
|
1164
|
+
"""Open hunk on a combined diff across all of the task's repos."""
|
|
1165
|
+
import os
|
|
1166
|
+
import tempfile
|
|
1167
|
+
|
|
1168
|
+
if not self.current_task:
|
|
1169
|
+
self.notify("No task selected", severity="warning")
|
|
1170
|
+
return
|
|
1171
|
+
|
|
1172
|
+
task = self.current_task
|
|
1173
|
+
|
|
1174
|
+
# Building the diff runs local git commands only (no network), so a
|
|
1175
|
+
# brief synchronous call before suspend is fine. The fresh git read
|
|
1176
|
+
# also makes the "no changes" check accurate regardless of stale UI.
|
|
1177
|
+
diff = GitOps.build_task_diff(task)
|
|
1178
|
+
if not diff.strip():
|
|
1179
|
+
self.notify("No changes to show", severity="information")
|
|
1180
|
+
return
|
|
1181
|
+
|
|
1182
|
+
# Save selection state BEFORE suspend (as local variables)
|
|
1183
|
+
saved_task_name = task.name
|
|
1184
|
+
saved_worktree_name = self.current_worktree.name if self.current_worktree else None
|
|
1185
|
+
|
|
1186
|
+
# hunk reads the combined patch from a file, so keyboard input stays on
|
|
1187
|
+
# the terminal (a stdin pipe would fight the interactive TUI for it).
|
|
1188
|
+
with tempfile.NamedTemporaryFile("w", suffix=".patch", delete=False, encoding="utf-8") as f:
|
|
1189
|
+
f.write(diff)
|
|
1190
|
+
patch_path = f.name
|
|
1191
|
+
|
|
1192
|
+
self.notify("Opening hunk...")
|
|
1193
|
+
|
|
1194
|
+
try:
|
|
1195
|
+
with self.suspend():
|
|
1196
|
+
self._run_external_command(
|
|
1197
|
+
[self.config.hunk_path, "patch", patch_path],
|
|
1198
|
+
cwd=task.path,
|
|
1199
|
+
name="hunk",
|
|
1200
|
+
install_hint="brew install hunk",
|
|
1201
|
+
)
|
|
1202
|
+
finally:
|
|
1203
|
+
try:
|
|
1204
|
+
os.unlink(patch_path)
|
|
1205
|
+
except OSError:
|
|
1206
|
+
pass
|
|
1207
|
+
|
|
1208
|
+
# Refresh status after hunk exits, restoring saved selection
|
|
1209
|
+
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
1210
|
+
# Restore focus to task list
|
|
1211
|
+
self.query_one("#task-list", TaskList).focus()
|
|
1212
|
+
|
|
1109
1213
|
def action_open_shell(self) -> None:
|
|
1110
1214
|
"""Open a shell in the current worktree."""
|
|
1111
1215
|
if not self.current_worktree:
|
|
@@ -1194,7 +1298,7 @@ class TaskTreeApp(App):
|
|
|
1194
1298
|
if fresh_task:
|
|
1195
1299
|
self.task_manager.ensure_claude_md_files(fresh_task)
|
|
1196
1300
|
|
|
1197
|
-
ensure_claude_hooks(task_path)
|
|
1301
|
+
ensure_claude_hooks(task_path, self.config.claude_memory_dir)
|
|
1198
1302
|
if has_claude_session(task_path):
|
|
1199
1303
|
self._open_ghostty_tab(task_path, command=f"{self.config.claude_path} -r")
|
|
1200
1304
|
self.notify("Opened Claude Code in new tab (resume)")
|
|
@@ -1215,6 +1319,7 @@ class TaskTreeApp(App):
|
|
|
1215
1319
|
self.notify("Task directory not found", severity="error")
|
|
1216
1320
|
return
|
|
1217
1321
|
|
|
1322
|
+
ensure_claude_hooks(task_path, self.config.claude_memory_dir)
|
|
1218
1323
|
encoded_path = quote(str(task_path), safe="")
|
|
1219
1324
|
url = f"claude://code/new?folder={encoded_path}"
|
|
1220
1325
|
try:
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
(
|
|
18
|
+
"show_diff",
|
|
19
|
+
"Show Diff",
|
|
20
|
+
"Open hunk: all repos from the task panel, the selected repo from the worktree panel",
|
|
21
|
+
),
|
|
22
|
+
("open_editor", "Open Editor", "Open your editor in the focused task/worktree folder"),
|
|
23
|
+
("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
|
+
("open_claude_resume", "Open Claude", "Open Claude Code for the task (resume if possible)"),
|
|
26
|
+
(
|
|
27
|
+
"open_claude_gui_code",
|
|
28
|
+
"Open Claude Desktop",
|
|
29
|
+
"Open the Claude desktop app on the Code page",
|
|
30
|
+
),
|
|
31
|
+
("push_all", "Push All", "Push all worktrees in the current task"),
|
|
32
|
+
("pull_all", "Pull All", "Pull all worktrees in the current task"),
|
|
33
|
+
("refresh", "Refresh", "Refresh git status for all tasks"),
|
|
34
|
+
("toggle_messages", "Toggle Messages", "Show or hide the activity messages panel"),
|
|
35
|
+
("toggle_grouping", "Toggle Grouping", "Group worktrees by dirty/clean status"),
|
|
36
|
+
("cycle_sort", "Cycle Sort", "Cycle through task sort modes"),
|
|
37
|
+
("help", "Help", "Show the keybinding reference"),
|
|
38
|
+
("quit", "Quit", "Exit tasktree-manager"),
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TaskTreeCommands(Provider):
|
|
43
|
+
"""Expose all keybound tasktree actions in the command palette."""
|
|
44
|
+
|
|
45
|
+
def _entries(self):
|
|
46
|
+
"""Yield (display title with hotkey, action, help) tuples."""
|
|
47
|
+
config = getattr(self.app, "config", None)
|
|
48
|
+
keybindings = config.keybindings if config else {}
|
|
49
|
+
for action, title, help_text in APP_COMMANDS:
|
|
50
|
+
key = keybindings.get(action, "")
|
|
51
|
+
display = f"{title} ({key})" if key else title
|
|
52
|
+
yield display, action, help_text
|
|
53
|
+
|
|
54
|
+
async def discover(self) -> Hits:
|
|
55
|
+
"""List all commands when the palette opens with an empty query."""
|
|
56
|
+
for display, action, help_text in self._entries():
|
|
57
|
+
yield DiscoveryHit(
|
|
58
|
+
display,
|
|
59
|
+
partial(self.app.run_action, action),
|
|
60
|
+
help=help_text,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def search(self, query: str) -> Hits:
|
|
64
|
+
"""Fuzzy-match commands against the query."""
|
|
65
|
+
matcher = self.matcher(query)
|
|
66
|
+
for display, action, help_text in self._entries():
|
|
67
|
+
score = matcher.match(display)
|
|
68
|
+
if score > 0:
|
|
69
|
+
yield Hit(
|
|
70
|
+
score,
|
|
71
|
+
matcher.highlight(display),
|
|
72
|
+
partial(self.app.run_action, action),
|
|
73
|
+
help=help_text,
|
|
74
|
+
)
|
|
@@ -38,11 +38,16 @@ def _build_hooks_config(task_path: Path) -> dict:
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def ensure_claude_hooks(task_path: Path) -> None:
|
|
41
|
+
def ensure_claude_hooks(task_path: Path, memory_dir: str = "") -> None:
|
|
42
42
|
"""Create .claude/settings.local.json with status-reporting hooks.
|
|
43
43
|
|
|
44
44
|
Merges with existing settings if the file already exists, preserving
|
|
45
45
|
user's own configuration while adding/updating the status hooks.
|
|
46
|
+
|
|
47
|
+
Task folders are not git repositories, so Claude Code keys its auto
|
|
48
|
+
memory to the task path — memory that becomes orphaned when the task
|
|
49
|
+
is deleted. When memory_dir is set, autoMemoryDirectory redirects all
|
|
50
|
+
task sessions to one shared pool that persists across tasks.
|
|
46
51
|
"""
|
|
47
52
|
claude_dir = task_path / ".claude"
|
|
48
53
|
claude_dir.mkdir(exist_ok=True)
|
|
@@ -60,4 +65,7 @@ def ensure_claude_hooks(task_path: Path) -> None:
|
|
|
60
65
|
# Merge hooks into existing settings
|
|
61
66
|
existing["hooks"] = _build_hooks_config(task_path)
|
|
62
67
|
|
|
68
|
+
if memory_dir:
|
|
69
|
+
existing["autoMemoryDirectory"] = str(Path(memory_dir).expanduser())
|
|
70
|
+
|
|
63
71
|
settings_file.write_text(json.dumps(existing, indent=2) + "\n")
|
|
@@ -24,6 +24,7 @@ DEFAULT_KEYBINDINGS: dict[str, str] = {
|
|
|
24
24
|
"add_repo": "a",
|
|
25
25
|
"delete_task": "d",
|
|
26
26
|
"open_lazygit": "g",
|
|
27
|
+
"show_diff": "h",
|
|
27
28
|
"open_folder": "o",
|
|
28
29
|
"open_shell": "enter",
|
|
29
30
|
"open_editor": "e",
|
|
@@ -90,7 +91,9 @@ class Config:
|
|
|
90
91
|
# External tools
|
|
91
92
|
editor: str = ""
|
|
92
93
|
lazygit_path: str = "lazygit"
|
|
94
|
+
hunk_path: str = "hunk"
|
|
93
95
|
claude_path: str = "claude"
|
|
96
|
+
claude_memory_dir: str = "~/.claude/tasktree-memory"
|
|
94
97
|
shell: str = ""
|
|
95
98
|
|
|
96
99
|
# Keybindings (action -> key mapping)
|
|
@@ -144,7 +147,9 @@ class Config:
|
|
|
144
147
|
tools_config = config_data.get("tools", {})
|
|
145
148
|
editor = tools_config.get("editor", "")
|
|
146
149
|
lazygit_path = tools_config.get("lazygit_path", "lazygit")
|
|
150
|
+
hunk_path = tools_config.get("hunk_path", "hunk")
|
|
147
151
|
claude_path = tools_config.get("claude_path", "claude")
|
|
152
|
+
claude_memory_dir = tools_config.get("claude_memory_dir", "~/.claude/tasktree-memory")
|
|
148
153
|
shell = tools_config.get("shell", "")
|
|
149
154
|
|
|
150
155
|
# Keybindings - start with defaults and override with config
|
|
@@ -184,7 +189,9 @@ class Config:
|
|
|
184
189
|
git_timeout=git_timeout,
|
|
185
190
|
editor=editor,
|
|
186
191
|
lazygit_path=lazygit_path,
|
|
192
|
+
hunk_path=hunk_path,
|
|
187
193
|
claude_path=claude_path,
|
|
194
|
+
claude_memory_dir=claude_memory_dir,
|
|
188
195
|
shell=shell,
|
|
189
196
|
keybindings=keybindings,
|
|
190
197
|
symlink_blocklist=symlink_blocklist,
|
|
@@ -308,9 +315,17 @@ editor = "{self._toml_escape(self.editor)}"
|
|
|
308
315
|
# Path to lazygit executable
|
|
309
316
|
lazygit_path = "{self._toml_escape(self.lazygit_path)}"
|
|
310
317
|
|
|
318
|
+
# Path to hunk executable (terminal diff viewer, see https://github.com/modem-dev/hunk)
|
|
319
|
+
hunk_path = "{self._toml_escape(self.hunk_path)}"
|
|
320
|
+
|
|
311
321
|
# Path to claude CLI executable
|
|
312
322
|
claude_path = "{self._toml_escape(self.claude_path)}"
|
|
313
323
|
|
|
324
|
+
# Shared Claude Code auto-memory directory for task sessions.
|
|
325
|
+
# Task folders are not git repos, so without this each task gets its own
|
|
326
|
+
# memory that is orphaned when the task is deleted. Set to "" to disable.
|
|
327
|
+
claude_memory_dir = "{self._toml_escape(self.claude_memory_dir)}"
|
|
328
|
+
|
|
314
329
|
# Preferred shell (leave empty to use $SHELL)
|
|
315
330
|
shell = "{self._toml_escape(self.shell)}"
|
|
316
331
|
|
|
@@ -328,6 +343,7 @@ clone_task = "{self._toml_escape(self.keybindings.get("clone_task", "y"))}"
|
|
|
328
343
|
add_repo = "{self._toml_escape(self.keybindings.get("add_repo", "a"))}"
|
|
329
344
|
delete_task = "{self._toml_escape(self.keybindings.get("delete_task", "d"))}"
|
|
330
345
|
open_lazygit = "{self._toml_escape(self.keybindings.get("open_lazygit", "g"))}"
|
|
346
|
+
show_diff = "{self._toml_escape(self.keybindings.get("show_diff", "h"))}"
|
|
331
347
|
open_folder = "{self._toml_escape(self.keybindings.get("open_folder", "o"))}"
|
|
332
348
|
open_shell = "{self._toml_escape(self.keybindings.get("open_shell", "enter"))}"
|
|
333
349
|
open_editor = "{self._toml_escape(self.keybindings.get("open_editor", "e"))}"
|
|
@@ -159,6 +159,76 @@ class GitOps:
|
|
|
159
159
|
except (subprocess.SubprocessError, OSError) as e:
|
|
160
160
|
return False, str(e)
|
|
161
161
|
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _git_stdout(worktree: Worktree, args: list[str]) -> str:
|
|
164
|
+
"""Run a local read-only git command in the worktree, returning stdout.
|
|
165
|
+
|
|
166
|
+
Returns "" on any failure. The exit code is intentionally ignored:
|
|
167
|
+
``git diff --no-index`` returns non-zero whenever files differ, and a
|
|
168
|
+
failed command leaves stdout empty anyway.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
["git", *args],
|
|
173
|
+
cwd=worktree.path,
|
|
174
|
+
capture_output=True,
|
|
175
|
+
text=True,
|
|
176
|
+
timeout=GitOps.LOCAL_TIMEOUT,
|
|
177
|
+
)
|
|
178
|
+
except (subprocess.SubprocessError, OSError):
|
|
179
|
+
return ""
|
|
180
|
+
return result.stdout
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _list_untracked(worktree: Worktree) -> list[str]:
|
|
184
|
+
"""List untracked (but not ignored) files in a worktree."""
|
|
185
|
+
out = GitOps._git_stdout(worktree, ["ls-files", "--others", "--exclude-standard", "-z"])
|
|
186
|
+
return [f for f in out.split("\0") if f]
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
def get_worktree_diff(worktree: Worktree, label: str | None = None) -> str:
|
|
190
|
+
"""Return a unified diff of all uncommitted changes in a worktree.
|
|
191
|
+
|
|
192
|
+
Combines staged and unstaged tracked changes (``git diff HEAD``) with
|
|
193
|
+
untracked files, so the result mirrors what a working-tree review shows.
|
|
194
|
+
When *label* is given, every file path is prefixed with ``<label>/`` so
|
|
195
|
+
diffs from several repos can be concatenated into one view without
|
|
196
|
+
colliding on identical relative paths.
|
|
197
|
+
|
|
198
|
+
Returns an empty string when the worktree is clean or missing.
|
|
199
|
+
"""
|
|
200
|
+
if not worktree.path.exists():
|
|
201
|
+
return ""
|
|
202
|
+
|
|
203
|
+
# Without a label, git's default a/ b/ prefixes are used.
|
|
204
|
+
prefixes = [f"--src-prefix=a/{label}/", f"--dst-prefix=b/{label}/"] if label else []
|
|
205
|
+
parts: list[str] = []
|
|
206
|
+
|
|
207
|
+
# Tracked changes (staged + unstaged) relative to HEAD.
|
|
208
|
+
parts.append(GitOps._git_stdout(worktree, ["diff", "HEAD", "--no-color", *prefixes]))
|
|
209
|
+
|
|
210
|
+
# Untracked files, rendered as additions against /dev/null.
|
|
211
|
+
for untracked in GitOps._list_untracked(worktree):
|
|
212
|
+
parts.append(
|
|
213
|
+
GitOps._git_stdout(
|
|
214
|
+
worktree,
|
|
215
|
+
["diff", "--no-index", "--no-color", *prefixes, "--", "/dev/null", untracked],
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
return "".join(parts)
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def build_task_diff(task: Task) -> str:
|
|
223
|
+
"""Build a combined diff across all of a task's worktrees.
|
|
224
|
+
|
|
225
|
+
Each worktree's changes are labelled with its repo name so a single
|
|
226
|
+
review shows every repo without path collisions. Returns an empty
|
|
227
|
+
string when nothing in the task has changed.
|
|
228
|
+
"""
|
|
229
|
+
parts = [GitOps.get_worktree_diff(wt, label=wt.name) for wt in task.worktrees]
|
|
230
|
+
return "".join(p for p in parts if p)
|
|
231
|
+
|
|
162
232
|
@staticmethod
|
|
163
233
|
def get_default_branch(worktree: Worktree) -> str:
|
|
164
234
|
"""Get the default branch (main/master) for a worktree's repo.
|