tasktree-manager 2.0.1__tar.gz → 2.2.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.0.1 → tasktree_manager-2.2.0}/CHANGELOG.md +16 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/PKG-INFO +1 -1
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/README.md +16 -22
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/docs/configuration.md +40 -5
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/docs/user-guide.md +35 -8
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/pyproject.toml +1 -1
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/_version.py +2 -2
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/app.py +157 -50
- tasktree_manager-2.2.0/tasktree_manager/services/claude_hooks.py +50 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/services/config.py +8 -1
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/services/git_ops.py +7 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/status_panel.py +69 -2
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/task_list.py +30 -6
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/worktree_list.py +2 -2
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/SKILL.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/README.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/styling.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.github/templates/CHANGELOG.md.j2 +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.github/workflows/ci.yml +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.github/workflows/release.yml +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.gitignore +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/CONTRIBUTING.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/LICENSE +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/OPENSPEC.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/ROADMAP.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/docs/claude-code-integration-spec.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/docs/installation.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/docs/troubleshooting.md +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/mise.toml +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/scripts/bump_version.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/__init__.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/services/__init__.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/services/models.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/services/task_manager.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/__init__.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/create_modal.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/messages_panel.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/setup_modal.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/__init__.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/conftest.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_app.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_config.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_git_ops.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_integration_git.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_memory.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_messages_panel.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_modals.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_setup_modal.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_status_panel.py +0 -0
- {tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tests/test_task_manager.py +0 -0
|
@@ -15,6 +15,22 @@ 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.2.0] - 2026-03-06
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Add periodic auto-refresh for git status with selection preserv… (#3)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [2.1.0] - 2026-02-10
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- Add context-aware info panel with task summary and focus-driven updates
|
|
28
|
+
- Add Claude session status monitoring with Ghostty integration
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- Update example task names in README and user guide
|
|
32
|
+
|
|
33
|
+
|
|
18
34
|
## [2.0.1] - 2026-02-07
|
|
19
35
|
|
|
20
36
|
### Fixed
|
|
@@ -19,26 +19,18 @@
|
|
|
19
19
|
- 🚀 **Fast & Responsive** - Parallel git operations with loading indicators
|
|
20
20
|
- 🔧 **Flexible Configuration** - TOML config file, custom keybindings, environment variables
|
|
21
21
|
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
│ Sync: ↑2 ↓1 │
|
|
35
|
-
│ │
|
|
36
|
-
│ M src/main.py │
|
|
37
|
-
│ ?? README.md │
|
|
38
|
-
│ M tests/test_app.py │
|
|
39
|
-
└──────────────────────────────────────────────────────────────────────────┘
|
|
40
|
-
n New task d Delete g Lazygit p Push r Refresh ? Help q Quit
|
|
41
|
-
```
|
|
22
|
+
## 📊 Status Indicators
|
|
23
|
+
|
|
24
|
+
| Indicator | Meaning |
|
|
25
|
+
|-----------|---------|
|
|
26
|
+
| `●` red | Uncommitted changes |
|
|
27
|
+
| `◆` blue | Has CLAUDE.md file |
|
|
28
|
+
| `⟳` magenta | Claude Code session running |
|
|
29
|
+
| `!` yellow | Claude Code session waiting for input |
|
|
30
|
+
| `✓` green | Clean worktree / Claude session ended |
|
|
31
|
+
| `✗ N` red | N files changed |
|
|
32
|
+
| `↑N` green | N commits ahead of remote |
|
|
33
|
+
| `↓N` yellow | N commits behind remote |
|
|
42
34
|
|
|
43
35
|
## 🚀 Quick Start
|
|
44
36
|
|
|
@@ -85,9 +77,11 @@ For detailed workflows and examples, see the [User Guide](docs/user-guide.md).
|
|
|
85
77
|
| `n` | New task | `g` | Open lazygit |
|
|
86
78
|
| `a` | Add repo | `e` | Open editor |
|
|
87
79
|
| `d` | Delete task | `o` | Open folder |
|
|
88
|
-
| `p` | Push all | `
|
|
80
|
+
| `p` | Push all | `P` | Pull all |
|
|
81
|
+
| `c` | Claude (resume) | `C` | Claude (new) |
|
|
89
82
|
| `r` | Refresh | `s` | Sort tasks |
|
|
90
|
-
| `S` | Group worktrees | `
|
|
83
|
+
| `S` | Group worktrees | `m` | Messages |
|
|
84
|
+
| `Enter` | Open shell | `Tab` | Next panel |
|
|
91
85
|
| `?` | Show help | `q` | Quit |
|
|
92
86
|
|
|
93
87
|
**Themes:** Press `Ctrl+P` to switch between 7 built-in themes (textual-dark, nord, gruvbox, tokyo-night, monokai, dracula, textual-light).
|
|
@@ -93,6 +93,11 @@ theme = "textual-dark"
|
|
|
93
93
|
# Default: false
|
|
94
94
|
show_hidden_files = false
|
|
95
95
|
|
|
96
|
+
# Auto-refresh interval in seconds (0 = disabled)
|
|
97
|
+
# Periodically refreshes git status in the background
|
|
98
|
+
# Default: 30
|
|
99
|
+
refresh_interval = 30
|
|
100
|
+
|
|
96
101
|
# ============================================================================
|
|
97
102
|
# Git Settings
|
|
98
103
|
# ============================================================================
|
|
@@ -160,6 +165,13 @@ push_all = "p" # Push all worktrees in current task
|
|
|
160
165
|
pull_all = "P" # Pull all worktrees in current task (shift+p)
|
|
161
166
|
refresh = "r" # Refresh git status
|
|
162
167
|
|
|
168
|
+
# Claude Code integration
|
|
169
|
+
open_claude_resume = "c" # Resume Claude Code session in task directory
|
|
170
|
+
open_claude_new = "C" # Start new Claude Code session (shift+c)
|
|
171
|
+
|
|
172
|
+
# Messages panel
|
|
173
|
+
toggle_messages = "m" # Show/hide the activity messages panel
|
|
174
|
+
|
|
163
175
|
# Navigation
|
|
164
176
|
focus_next = "tab" # Switch to next panel
|
|
165
177
|
focus_previous = "shift+tab" # Switch to previous panel
|
|
@@ -224,10 +236,11 @@ tasks_dir = "/Volumes/FastSSD/worktrees"
|
|
|
224
236
|
|
|
225
237
|
## UI Settings
|
|
226
238
|
|
|
227
|
-
| Option | Type | Default | Environment Variable | Description
|
|
228
|
-
|
|
229
|
-
| `theme` | string | `textual-dark` | `TASKTREE_THEME` | Color theme for the interface
|
|
230
|
-
| `show_hidden_files` | boolean | `false` | - | Show hidden files in listings
|
|
239
|
+
| Option | Type | Default | Environment Variable | Description |
|
|
240
|
+
|--------------------|---------|-----------------|-----------------------|------------------------------------------|
|
|
241
|
+
| `theme` | string | `textual-dark` | `TASKTREE_THEME` | Color theme for the interface |
|
|
242
|
+
| `show_hidden_files` | boolean | `false` | - | Show hidden files in listings |
|
|
243
|
+
| `refresh_interval` | integer | `30` | - | Auto-refresh interval in seconds (0 = disabled) |
|
|
231
244
|
|
|
232
245
|
### `theme`
|
|
233
246
|
|
|
@@ -245,6 +258,20 @@ Available themes (press `Ctrl+P` in app to switch):
|
|
|
245
258
|
|
|
246
259
|
Currently unused (reserved for future file browser features).
|
|
247
260
|
|
|
261
|
+
### `refresh_interval`
|
|
262
|
+
|
|
263
|
+
Automatically refreshes git status for all tasks and worktrees in the background:
|
|
264
|
+
|
|
265
|
+
- Default: `30` (every 30 seconds)
|
|
266
|
+
- Set to `0` to disable auto-refresh
|
|
267
|
+
- Runs in a background thread — does not block the UI
|
|
268
|
+
- Preserves current task and worktree selection during refresh
|
|
269
|
+
|
|
270
|
+
```toml
|
|
271
|
+
[ui]
|
|
272
|
+
refresh_interval = 15 # Refresh every 15 seconds
|
|
273
|
+
```
|
|
274
|
+
|
|
248
275
|
## Git Settings
|
|
249
276
|
|
|
250
277
|
| Option | Type | Default | Environment Variable | Description |
|
|
@@ -390,7 +417,7 @@ blocklist = ["*"]
|
|
|
390
417
|
|
|
391
418
|
## Keybindings Reference
|
|
392
419
|
|
|
393
|
-
All
|
|
420
|
+
All 21 customizable keybindings with descriptions:
|
|
394
421
|
|
|
395
422
|
### Application Control
|
|
396
423
|
|
|
@@ -419,6 +446,14 @@ All 18 customizable keybindings with descriptions:
|
|
|
419
446
|
| `pull_all` | `P` | Pull all worktrees in current task | `pull_all = "ctrl+shift+p"` |
|
|
420
447
|
| `refresh` | `r` | Refresh git status for all worktrees | `refresh = "F5"` |
|
|
421
448
|
|
|
449
|
+
### Claude Code Integration
|
|
450
|
+
|
|
451
|
+
| Action | Default | Description | Customization Example |
|
|
452
|
+
|----------------------|---------|------------------------------------------|--------------------------------------|
|
|
453
|
+
| `open_claude_resume` | `c` | Resume Claude Code session in task dir | `open_claude_resume = "ctrl+c"` |
|
|
454
|
+
| `open_claude_new` | `C` | Start new Claude Code session | `open_claude_new = "ctrl+shift+c"` |
|
|
455
|
+
| `toggle_messages` | `m` | Show/hide activity messages panel | `toggle_messages = "ctrl+m"` |
|
|
456
|
+
|
|
422
457
|
### Navigation
|
|
423
458
|
|
|
424
459
|
| Action | Default | Description | Customization Example |
|
|
@@ -60,7 +60,7 @@ Task: FEAT-123
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
**Characteristics:**
|
|
63
|
-
- Created with a unique name (e.g., `FEAT-123`, `
|
|
63
|
+
- Created with a unique name (e.g., `FEAT-123`, `BUG-456-auth`)
|
|
64
64
|
- Contains one or more worktrees
|
|
65
65
|
- Stored in `TASKS_DIR/task-name/`
|
|
66
66
|
- Each task gets its own set of branches across selected repos
|
|
@@ -177,7 +177,9 @@ Shows all your tasks:
|
|
|
177
177
|
|
|
178
178
|
- **Name**: Task identifier (e.g., `FEAT-123`)
|
|
179
179
|
- **Count**: Number of worktrees in parentheses (e.g., `(3)`)
|
|
180
|
-
- **Dirty indicator** `●`:
|
|
180
|
+
- **Dirty indicator** `●`: Red dot — task has uncommitted changes
|
|
181
|
+
- **CLAUDE.md indicator** `◆`: Blue diamond — task has a CLAUDE.md file
|
|
182
|
+
- **Claude session status**: `⟳` running (magenta), `!` waiting (yellow), `✓` ended (green)
|
|
181
183
|
- **Sort mode**: Press `s` to cycle through sort modes (name ↑/↓, date ↑/↓, dirty/clean first)
|
|
182
184
|
|
|
183
185
|
**Navigation:**
|
|
@@ -221,13 +223,24 @@ Shows detailed git status for the selected worktree:
|
|
|
221
223
|
|
|
222
224
|
### Visual Indicators
|
|
223
225
|
|
|
226
|
+
**Task & Worktree Status:**
|
|
227
|
+
|
|
224
228
|
| Indicator | Meaning |
|
|
225
229
|
|-----------|--------------------------------------------|
|
|
226
|
-
| `●` | Task has uncommitted changes
|
|
227
|
-
| `✗ N` | N files staged or modified
|
|
228
|
-
| `↑N` | N commits ahead of remote (unpushed)
|
|
229
|
-
| `↓N` | N commits behind remote (need to pull)
|
|
230
|
-
| `✓` | Clean worktree, in sync
|
|
230
|
+
| `●` | Task/worktree has uncommitted changes |
|
|
231
|
+
| `✗ N` | N files staged or modified |
|
|
232
|
+
| `↑N` | N commits ahead of remote (unpushed) |
|
|
233
|
+
| `↓N` | N commits behind remote (need to pull) |
|
|
234
|
+
| `✓` | Clean worktree, in sync |
|
|
235
|
+
|
|
236
|
+
**Claude Code Integration:**
|
|
237
|
+
|
|
238
|
+
| Indicator | Color | Meaning |
|
|
239
|
+
|-----------|---------|------------------------------------------|
|
|
240
|
+
| `◆` | Blue | Task/worktree has a CLAUDE.md file |
|
|
241
|
+
| `⟳` | Magenta | Claude Code session is running |
|
|
242
|
+
| `!` | Yellow | Claude Code session is waiting for input |
|
|
243
|
+
| `✓` | Green | Claude Code session has ended |
|
|
231
244
|
|
|
232
245
|
## Complete Keyboard Reference
|
|
233
246
|
|
|
@@ -318,12 +331,26 @@ All keybindings can be customized in [`config.toml`](configuration.md).
|
|
|
318
331
|
- When focused on task list: Opens the task folder (`~/tasks/TASK-NAME`)
|
|
319
332
|
- When focused on worktree list: Opens the selected worktree folder
|
|
320
333
|
|
|
334
|
+
### Claude Code Integration
|
|
335
|
+
|
|
336
|
+
| Key | Action | Description |
|
|
337
|
+
|---------|-------------------------|----------------------------------------------|
|
|
338
|
+
| `c` | Open Claude (resume) | Resume Claude Code session in task directory |
|
|
339
|
+
| `C` | Open Claude (new) | Start new Claude Code session in task directory |
|
|
340
|
+
|
|
341
|
+
**How it works:**
|
|
342
|
+
- Opens Claude Code in a new Ghostty terminal tab at the task directory
|
|
343
|
+
- Generates `CLAUDE.md` files with task/worktree context
|
|
344
|
+
- Installs hooks that report session status back to tasktree-manager
|
|
345
|
+
- Session status (`⟳` running, `!` waiting, `✓` ended) appears next to the task name
|
|
346
|
+
|
|
321
347
|
### General
|
|
322
348
|
|
|
323
349
|
| Key | Action | Description |
|
|
324
350
|
|---------|-------------------------|----------------------------------------------|
|
|
325
351
|
| `Ctrl+P`| Open Command Palette | Switch themes and run commands |
|
|
326
352
|
| `o` | Open folder | Open current folder in new terminal tab |
|
|
353
|
+
| `m` | Toggle messages | Show/hide the activity messages panel |
|
|
327
354
|
| `?` | Show help | Display help modal with keybindings |
|
|
328
355
|
| `q` | Quit | Exit tasktree-manager |
|
|
329
356
|
|
|
@@ -696,7 +723,7 @@ gh pr create --head FEAT-123
|
|
|
696
723
|
### Naming Conventions
|
|
697
724
|
|
|
698
725
|
**Task names:**
|
|
699
|
-
- Use issue tracker IDs: `JIRA-1234`, `
|
|
726
|
+
- Use issue tracker IDs: `JIRA-1234`, `PROJ-567`
|
|
700
727
|
- Or descriptive names: `feat-auth`, `bugfix-login`, `refactor-api`
|
|
701
728
|
- Avoid spaces: Use `-` or `_`
|
|
702
729
|
- Keep short but meaningful
|
|
@@ -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.0
|
|
73
|
+
version = "2.2.0"
|
|
74
74
|
tag_format = "v{version}"
|
|
75
75
|
branch = "main"
|
|
76
76
|
commit_message = "chore(release): {version} [skip ci]"
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.0
|
|
32
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.2.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Main application for tasktree-manager."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import subprocess
|
|
4
5
|
|
|
5
6
|
from textual import work
|
|
@@ -8,6 +9,7 @@ from textual.binding import Binding
|
|
|
8
9
|
from textual.containers import Horizontal, Vertical
|
|
9
10
|
from textual.widgets import Footer, Header, Static
|
|
10
11
|
|
|
12
|
+
from .services.claude_hooks import ensure_claude_hooks
|
|
11
13
|
from .services.config import Config
|
|
12
14
|
from .services.git_ops import GitOps
|
|
13
15
|
from .services.models import GitStatus, Task, Worktree
|
|
@@ -141,7 +143,7 @@ class TaskTreeApp(App):
|
|
|
141
143
|
/* Status panel styling */
|
|
142
144
|
#status-display {
|
|
143
145
|
height: 100%;
|
|
144
|
-
padding: 0 1;
|
|
146
|
+
padding: 1 1 0 1;
|
|
145
147
|
background: $background;
|
|
146
148
|
color: $text;
|
|
147
149
|
overflow-y: auto;
|
|
@@ -250,6 +252,8 @@ class TaskTreeApp(App):
|
|
|
250
252
|
self._show_messages_panel: bool = False
|
|
251
253
|
# Used to preserve worktree selection during task list reload
|
|
252
254
|
self._preserved_worktree_name: str | None = None
|
|
255
|
+
# Claude session statuses: task_name -> status string
|
|
256
|
+
self._claude_statuses: dict[str, str] = {}
|
|
253
257
|
|
|
254
258
|
def _build_bindings_from_config(self) -> list[Binding]:
|
|
255
259
|
"""Build keybindings list from config.
|
|
@@ -306,6 +310,7 @@ class TaskTreeApp(App):
|
|
|
306
310
|
# Right column: Info/Status panel
|
|
307
311
|
with Vertical(id="right-column"):
|
|
308
312
|
with Vertical(id="status-panel"):
|
|
313
|
+
yield Static("Info", classes="panel-title", id="status-panel-title")
|
|
309
314
|
yield StatusPanel(id="status-display")
|
|
310
315
|
with Vertical(id="messages-panel"):
|
|
311
316
|
yield MessagesPanel(id="messages-display")
|
|
@@ -330,6 +335,13 @@ class TaskTreeApp(App):
|
|
|
330
335
|
task_list = self.query_one("#task-list", TaskList)
|
|
331
336
|
task_list.focus()
|
|
332
337
|
|
|
338
|
+
# Poll Claude session status files every 5 seconds
|
|
339
|
+
self.set_interval(5, self._poll_claude_statuses)
|
|
340
|
+
|
|
341
|
+
# Periodic auto-refresh of git status
|
|
342
|
+
if self.config.refresh_interval > 0:
|
|
343
|
+
self.set_interval(self.config.refresh_interval, self._periodic_git_refresh)
|
|
344
|
+
|
|
333
345
|
def watch_theme(self, theme: str) -> None:
|
|
334
346
|
"""Save theme to config when changed."""
|
|
335
347
|
if hasattr(self, "config") and self.config.theme != theme:
|
|
@@ -420,6 +432,62 @@ class TaskTreeApp(App):
|
|
|
420
432
|
finally:
|
|
421
433
|
task_list.loading = False
|
|
422
434
|
|
|
435
|
+
def _poll_claude_statuses(self) -> None:
|
|
436
|
+
"""Check .claude_status files and update task indicators."""
|
|
437
|
+
try:
|
|
438
|
+
task_list = self.query_one("#task-list", TaskList)
|
|
439
|
+
except Exception:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
statuses: dict[str, str] = {}
|
|
443
|
+
for task in task_list.tasks:
|
|
444
|
+
status_file = task.path / ".claude_status"
|
|
445
|
+
if status_file.exists():
|
|
446
|
+
try:
|
|
447
|
+
data = json.loads(status_file.read_text())
|
|
448
|
+
statuses[task.name] = data.get("status", "unknown")
|
|
449
|
+
except (json.JSONDecodeError, OSError):
|
|
450
|
+
pass
|
|
451
|
+
|
|
452
|
+
if statuses != self._claude_statuses:
|
|
453
|
+
self._claude_statuses = statuses
|
|
454
|
+
task_list.refresh_claude_indicators(statuses)
|
|
455
|
+
|
|
456
|
+
def _periodic_git_refresh(self) -> None:
|
|
457
|
+
"""Trigger a background git status refresh."""
|
|
458
|
+
self._run_periodic_refresh()
|
|
459
|
+
|
|
460
|
+
@work(thread=True, exclusive=True, group="auto_refresh")
|
|
461
|
+
def _run_periodic_refresh(self) -> None:
|
|
462
|
+
"""Refresh git status in background thread."""
|
|
463
|
+
tasks = self.task_manager.list_tasks()
|
|
464
|
+
all_worktrees = [wt for task in tasks for wt in task.worktrees]
|
|
465
|
+
GitOps.update_all_worktree_statuses(all_worktrees)
|
|
466
|
+
|
|
467
|
+
current_task_name = self.current_task.name if self.current_task else None
|
|
468
|
+
current_worktree_name = self.current_worktree.name if self.current_worktree else None
|
|
469
|
+
|
|
470
|
+
def _apply():
|
|
471
|
+
try:
|
|
472
|
+
task_list = self.query_one("#task-list", TaskList)
|
|
473
|
+
if current_worktree_name:
|
|
474
|
+
self._preserved_worktree_name = current_worktree_name
|
|
475
|
+
task_list.load_tasks(tasks, preserve_selection=current_task_name)
|
|
476
|
+
|
|
477
|
+
if self.current_task:
|
|
478
|
+
for t in tasks:
|
|
479
|
+
if t.name == self.current_task.name:
|
|
480
|
+
self.current_task = t
|
|
481
|
+
worktree_list = self.query_one("#worktree-list", WorktreeList)
|
|
482
|
+
worktree_list.load_worktrees(
|
|
483
|
+
t.worktrees, preserve_selection=current_worktree_name
|
|
484
|
+
)
|
|
485
|
+
break
|
|
486
|
+
except Exception:
|
|
487
|
+
pass
|
|
488
|
+
|
|
489
|
+
self.call_from_thread(_apply)
|
|
490
|
+
|
|
423
491
|
def _refresh_current_task(self, preserve_selection: bool = False) -> None:
|
|
424
492
|
"""Refresh the current task's worktrees and status.
|
|
425
493
|
|
|
@@ -469,6 +537,26 @@ class TaskTreeApp(App):
|
|
|
469
537
|
self.notify(f"Failed to run {name}: {e}", severity="error")
|
|
470
538
|
return False
|
|
471
539
|
|
|
540
|
+
def on_descendant_focus(self, event) -> None:
|
|
541
|
+
"""Handle focus changes to update info panel mode."""
|
|
542
|
+
try:
|
|
543
|
+
status_panel = self.query_one("#status-display", StatusPanel)
|
|
544
|
+
except Exception:
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
if isinstance(event.widget, TaskList):
|
|
548
|
+
# Task list focused → show task changed files
|
|
549
|
+
if self.current_task:
|
|
550
|
+
status_panel.set_loading(True)
|
|
551
|
+
self._update_task_summary(self.current_task)
|
|
552
|
+
elif isinstance(event.widget, WorktreeList):
|
|
553
|
+
# Worktree list focused → show worktree details
|
|
554
|
+
if self.current_worktree and self.current_status:
|
|
555
|
+
status_panel.update_status(self.current_worktree, self.current_status)
|
|
556
|
+
elif self.current_worktree:
|
|
557
|
+
status_panel.set_loading(True)
|
|
558
|
+
self._update_worktree_status(self.current_worktree)
|
|
559
|
+
|
|
472
560
|
def on_task_list_task_highlighted(self, event: TaskList.TaskHighlighted) -> None:
|
|
473
561
|
"""Handle task highlight in task list."""
|
|
474
562
|
self.current_task = event.task
|
|
@@ -484,6 +572,12 @@ class TaskTreeApp(App):
|
|
|
484
572
|
preserved = self._preserved_worktree_name
|
|
485
573
|
self._preserved_worktree_name = None # Clear after use
|
|
486
574
|
worktree_list.load_worktrees(event.task.worktrees, preserve_selection=preserved)
|
|
575
|
+
|
|
576
|
+
# Show task summary if task list is focused
|
|
577
|
+
task_list = self.query_one("#task-list", TaskList)
|
|
578
|
+
if task_list.has_focus:
|
|
579
|
+
status_panel.set_loading(True)
|
|
580
|
+
self._update_task_summary(event.task)
|
|
487
581
|
else:
|
|
488
582
|
worktree_list.clear_worktrees()
|
|
489
583
|
status_panel.clear_status()
|
|
@@ -499,12 +593,19 @@ class TaskTreeApp(App):
|
|
|
499
593
|
# Widget not mounted yet during app startup
|
|
500
594
|
return
|
|
501
595
|
|
|
596
|
+
# Only update status panel if worktree list is focused
|
|
597
|
+
worktree_list = self.query_one("#worktree-list", WorktreeList)
|
|
502
598
|
if event.worktree:
|
|
503
|
-
|
|
504
|
-
|
|
599
|
+
if worktree_list.has_focus:
|
|
600
|
+
status_panel.set_loading(True)
|
|
601
|
+
self._update_worktree_status(event.worktree)
|
|
602
|
+
else:
|
|
603
|
+
# Still fetch status in background for when user switches focus
|
|
604
|
+
self._update_worktree_status(event.worktree)
|
|
505
605
|
else:
|
|
506
606
|
self.current_status = None
|
|
507
|
-
|
|
607
|
+
if worktree_list.has_focus:
|
|
608
|
+
status_panel.clear_status()
|
|
508
609
|
|
|
509
610
|
@work(thread=True, exclusive=True, group="status_update")
|
|
510
611
|
def _update_worktree_status(self, worktree: Worktree) -> None:
|
|
@@ -520,7 +621,31 @@ class TaskTreeApp(App):
|
|
|
520
621
|
self.current_status = status
|
|
521
622
|
try:
|
|
522
623
|
status_panel = self.query_one("#status-display", StatusPanel)
|
|
523
|
-
|
|
624
|
+
# Only show worktree status if worktree list is focused
|
|
625
|
+
worktree_list = self.query_one("#worktree-list", WorktreeList)
|
|
626
|
+
if worktree_list.has_focus:
|
|
627
|
+
status_panel.update_status(worktree, status)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
@work(thread=True, exclusive=True, group="task_summary_update")
|
|
632
|
+
def _update_task_summary(self, task: Task) -> None:
|
|
633
|
+
"""Fetch git status for all dirty worktrees in background thread."""
|
|
634
|
+
statuses: dict[str, GitStatus] = {}
|
|
635
|
+
for wt in task.worktrees:
|
|
636
|
+
if wt.is_dirty:
|
|
637
|
+
statuses[wt.name] = GitOps.get_status(wt)
|
|
638
|
+
self.call_from_thread(self._apply_task_summary, task, statuses)
|
|
639
|
+
|
|
640
|
+
def _apply_task_summary(self, task: Task, statuses: dict[str, GitStatus]) -> None:
|
|
641
|
+
"""Apply fetched task summary to the UI (called on main thread)."""
|
|
642
|
+
# Only update if this task is still selected and task list is focused
|
|
643
|
+
if self.current_task and self.current_task.name == task.name:
|
|
644
|
+
try:
|
|
645
|
+
status_panel = self.query_one("#status-display", StatusPanel)
|
|
646
|
+
task_list = self.query_one("#task-list", TaskList)
|
|
647
|
+
if task_list.has_focus:
|
|
648
|
+
status_panel.update_task_summary(task, statuses)
|
|
524
649
|
except Exception:
|
|
525
650
|
pass
|
|
526
651
|
|
|
@@ -607,8 +732,8 @@ class TaskTreeApp(App):
|
|
|
607
732
|
except Exception as e:
|
|
608
733
|
failed.append((repo, e))
|
|
609
734
|
|
|
610
|
-
self._load_tasks()
|
|
611
|
-
self._refresh_current_task()
|
|
735
|
+
self._load_tasks(preserve_selection=True)
|
|
736
|
+
self._refresh_current_task(preserve_selection=True)
|
|
612
737
|
|
|
613
738
|
if added:
|
|
614
739
|
self.notify(f"Added {len(added)} repo(s) to task")
|
|
@@ -918,7 +1043,7 @@ class TaskTreeApp(App):
|
|
|
918
1043
|
self.query_one("#worktree-list", WorktreeList).focus()
|
|
919
1044
|
|
|
920
1045
|
def action_open_claude_resume(self) -> None:
|
|
921
|
-
"""Open Claude Code
|
|
1046
|
+
"""Open Claude Code in a new Ghostty tab (resume session)."""
|
|
922
1047
|
if not self.current_task:
|
|
923
1048
|
self.notify("No task selected", severity="warning")
|
|
924
1049
|
return
|
|
@@ -928,30 +1053,17 @@ class TaskTreeApp(App):
|
|
|
928
1053
|
self.notify("Task directory not found", severity="error")
|
|
929
1054
|
return
|
|
930
1055
|
|
|
931
|
-
saved_task_name = self.current_task.name
|
|
932
|
-
saved_worktree_name = self.current_worktree.name if self.current_worktree else None
|
|
933
|
-
|
|
934
1056
|
# Fetch fresh task data to ensure worktrees are up-to-date
|
|
935
|
-
fresh_task = self.task_manager.get_task(
|
|
1057
|
+
fresh_task = self.task_manager.get_task(self.current_task.name)
|
|
936
1058
|
if fresh_task:
|
|
937
|
-
# Create claude.md files if they don't exist
|
|
938
1059
|
self.task_manager.ensure_claude_md_files(fresh_task)
|
|
939
1060
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
self._run_external_command(
|
|
944
|
-
[self.config.claude_path, "-r"],
|
|
945
|
-
cwd=task_path,
|
|
946
|
-
name="Claude Code",
|
|
947
|
-
install_hint="npm install -g @anthropic-ai/claude-code",
|
|
948
|
-
)
|
|
949
|
-
|
|
950
|
-
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
951
|
-
self.query_one("#task-list", TaskList).focus()
|
|
1061
|
+
ensure_claude_hooks(task_path)
|
|
1062
|
+
self._open_ghostty_tab(task_path, command=f"{self.config.claude_path} -r")
|
|
1063
|
+
self.notify("Opened Claude Code in new tab (resume)")
|
|
952
1064
|
|
|
953
1065
|
def action_open_claude_new(self) -> None:
|
|
954
|
-
"""Open a new Claude Code session in
|
|
1066
|
+
"""Open a new Claude Code session in a new Ghostty tab."""
|
|
955
1067
|
if not self.current_task:
|
|
956
1068
|
self.notify("No task selected", severity="warning")
|
|
957
1069
|
return
|
|
@@ -961,27 +1073,14 @@ class TaskTreeApp(App):
|
|
|
961
1073
|
self.notify("Task directory not found", severity="error")
|
|
962
1074
|
return
|
|
963
1075
|
|
|
964
|
-
saved_task_name = self.current_task.name
|
|
965
|
-
saved_worktree_name = self.current_worktree.name if self.current_worktree else None
|
|
966
|
-
|
|
967
1076
|
# Fetch fresh task data to ensure worktrees are up-to-date
|
|
968
|
-
fresh_task = self.task_manager.get_task(
|
|
1077
|
+
fresh_task = self.task_manager.get_task(self.current_task.name)
|
|
969
1078
|
if fresh_task:
|
|
970
|
-
# Create claude.md files if they don't exist
|
|
971
1079
|
self.task_manager.ensure_claude_md_files(fresh_task)
|
|
972
1080
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
self._run_external_command(
|
|
977
|
-
[self.config.claude_path],
|
|
978
|
-
cwd=task_path,
|
|
979
|
-
name="Claude Code",
|
|
980
|
-
install_hint="npm install -g @anthropic-ai/claude-code",
|
|
981
|
-
)
|
|
982
|
-
|
|
983
|
-
self._load_tasks_with_selection(saved_task_name, saved_worktree_name)
|
|
984
|
-
self.query_one("#task-list", TaskList).focus()
|
|
1081
|
+
ensure_claude_hooks(task_path)
|
|
1082
|
+
self._open_ghostty_tab(task_path, command=self.config.claude_path)
|
|
1083
|
+
self.notify("Opened new Claude Code session in new tab")
|
|
985
1084
|
|
|
986
1085
|
def action_open_folder(self) -> None:
|
|
987
1086
|
"""Open current folder in a new terminal tab."""
|
|
@@ -1006,15 +1105,23 @@ class TaskTreeApp(App):
|
|
|
1006
1105
|
# Open in new terminal tab
|
|
1007
1106
|
self._open_ghostty_tab(folder_path)
|
|
1008
1107
|
|
|
1009
|
-
def _open_ghostty_tab(self, path) -> None:
|
|
1010
|
-
"""Open a new terminal tab at the given path.
|
|
1108
|
+
def _open_ghostty_tab(self, path, command: str | None = None) -> None:
|
|
1109
|
+
"""Open a new terminal tab at the given path, optionally running a command.
|
|
1011
1110
|
|
|
1012
1111
|
NOTE: This implementation is specific to the Ghostty terminal emulator
|
|
1013
1112
|
on macOS, using AppleScript. It will silently fail on other terminals.
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
path: Directory to cd into in the new tab
|
|
1116
|
+
command: Optional command to run after cd (e.g., "claude -r")
|
|
1014
1117
|
"""
|
|
1015
1118
|
# Ghostty: Use AppleScript to activate and open new tab, then cd
|
|
1016
1119
|
# Escape single quotes for AppleScript string interpolation
|
|
1017
1120
|
safe_path = str(path).replace("'", "'\\''")
|
|
1121
|
+
if command:
|
|
1122
|
+
shell_cmd = f"cd '{safe_path}' && clear && {command}"
|
|
1123
|
+
else:
|
|
1124
|
+
shell_cmd = f"cd '{safe_path}' && clear"
|
|
1018
1125
|
script = f"""
|
|
1019
1126
|
tell application "Ghostty"
|
|
1020
1127
|
activate
|
|
@@ -1023,7 +1130,7 @@ class TaskTreeApp(App):
|
|
|
1023
1130
|
tell process "Ghostty"
|
|
1024
1131
|
keystroke "t" using command down
|
|
1025
1132
|
delay 0.1
|
|
1026
|
-
keystroke "
|
|
1133
|
+
keystroke "{shell_cmd}"
|
|
1027
1134
|
key code 36
|
|
1028
1135
|
end tell
|
|
1029
1136
|
end tell
|
|
@@ -1061,7 +1168,7 @@ class TaskTreeApp(App):
|
|
|
1061
1168
|
task_name,
|
|
1062
1169
|
)
|
|
1063
1170
|
|
|
1064
|
-
self._refresh_current_task()
|
|
1171
|
+
self._refresh_current_task(preserve_selection=True)
|
|
1065
1172
|
finally:
|
|
1066
1173
|
worktree_list.loading = False
|
|
1067
1174
|
|
|
@@ -1096,14 +1203,14 @@ class TaskTreeApp(App):
|
|
|
1096
1203
|
task_name,
|
|
1097
1204
|
)
|
|
1098
1205
|
|
|
1099
|
-
self._refresh_current_task()
|
|
1206
|
+
self._refresh_current_task(preserve_selection=True)
|
|
1100
1207
|
finally:
|
|
1101
1208
|
worktree_list.loading = False
|
|
1102
1209
|
|
|
1103
1210
|
def action_refresh(self) -> None:
|
|
1104
1211
|
"""Refresh all status."""
|
|
1105
|
-
self._load_tasks()
|
|
1106
|
-
self._refresh_current_task()
|
|
1212
|
+
self._load_tasks(preserve_selection=True)
|
|
1213
|
+
self._refresh_current_task(preserve_selection=True)
|
|
1107
1214
|
self.notify("Refreshed")
|
|
1108
1215
|
|
|
1109
1216
|
def action_toggle_messages(self) -> None:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Claude Code hook configuration for status monitoring."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _make_hook(status: str, status_file: str) -> dict:
|
|
8
|
+
"""Create a single hook entry that writes status to the given file path."""
|
|
9
|
+
safe_path = status_file.replace("'", "'\\''")
|
|
10
|
+
return {
|
|
11
|
+
"type": "command",
|
|
12
|
+
"command": f'printf \'{{"status":"{status}","ts":%d}}\' $(date +%s) > \'{safe_path}\'',
|
|
13
|
+
"async": True,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_hooks_config(task_path: Path) -> dict:
|
|
18
|
+
"""Build hooks config with absolute path to status file."""
|
|
19
|
+
status_file = str(task_path / ".claude_status")
|
|
20
|
+
return {
|
|
21
|
+
"SessionStart": [{"hooks": [_make_hook("running", status_file)]}],
|
|
22
|
+
"UserPromptSubmit": [{"hooks": [_make_hook("running", status_file)]}],
|
|
23
|
+
"Stop": [{"hooks": [_make_hook("waiting", status_file)]}],
|
|
24
|
+
"SessionEnd": [{"hooks": [_make_hook("ended", status_file)]}],
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def ensure_claude_hooks(task_path: Path) -> None:
|
|
29
|
+
"""Create .claude/settings.local.json with status-reporting hooks.
|
|
30
|
+
|
|
31
|
+
Merges with existing settings if the file already exists, preserving
|
|
32
|
+
user's own configuration while adding/updating the status hooks.
|
|
33
|
+
"""
|
|
34
|
+
claude_dir = task_path / ".claude"
|
|
35
|
+
claude_dir.mkdir(exist_ok=True)
|
|
36
|
+
|
|
37
|
+
settings_file = claude_dir / "settings.local.json"
|
|
38
|
+
|
|
39
|
+
if settings_file.exists():
|
|
40
|
+
try:
|
|
41
|
+
existing = json.loads(settings_file.read_text())
|
|
42
|
+
except (json.JSONDecodeError, OSError):
|
|
43
|
+
existing = {}
|
|
44
|
+
else:
|
|
45
|
+
existing = {}
|
|
46
|
+
|
|
47
|
+
# Merge hooks into existing settings
|
|
48
|
+
existing["hooks"] = _build_hooks_config(task_path)
|
|
49
|
+
|
|
50
|
+
settings_file.write_text(json.dumps(existing, indent=2) + "\n")
|
|
@@ -59,6 +59,7 @@ class Config:
|
|
|
59
59
|
# UI settings
|
|
60
60
|
theme: str = "textual-dark"
|
|
61
61
|
show_hidden_files: bool = False
|
|
62
|
+
refresh_interval: int = 30 # Auto-refresh interval in seconds (0 = disabled)
|
|
62
63
|
|
|
63
64
|
# Git settings
|
|
64
65
|
default_base_branch: str = "main"
|
|
@@ -103,7 +104,8 @@ class Config:
|
|
|
103
104
|
Priority: Environment variables > Config file > Defaults
|
|
104
105
|
"""
|
|
105
106
|
config_dir = (
|
|
106
|
-
Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
|
|
107
|
+
Path(os.environ.get("XDG_CONFIG_HOME", str(Path.home() / ".config")))
|
|
108
|
+
/ "tasktree-manager"
|
|
107
109
|
)
|
|
108
110
|
config_file = config_dir / "config.toml"
|
|
109
111
|
|
|
@@ -122,6 +124,7 @@ class Config:
|
|
|
122
124
|
ui_config = config_data.get("ui", {})
|
|
123
125
|
theme = ui_config.get("theme", "textual-dark")
|
|
124
126
|
show_hidden_files = ui_config.get("show_hidden_files", False)
|
|
127
|
+
refresh_interval = ui_config.get("refresh_interval", 30)
|
|
125
128
|
|
|
126
129
|
# Git settings
|
|
127
130
|
git_config = config_data.get("git", {})
|
|
@@ -185,6 +188,7 @@ class Config:
|
|
|
185
188
|
config_dir=config_dir,
|
|
186
189
|
theme=theme,
|
|
187
190
|
show_hidden_files=show_hidden_files,
|
|
191
|
+
refresh_interval=refresh_interval,
|
|
188
192
|
default_base_branch=default_base_branch,
|
|
189
193
|
auto_push=auto_push,
|
|
190
194
|
git_timeout=git_timeout,
|
|
@@ -286,6 +290,9 @@ theme = "{self._toml_escape(self.theme)}"
|
|
|
286
290
|
# Show hidden files in file listings
|
|
287
291
|
show_hidden_files = {str(self.show_hidden_files).lower()}
|
|
288
292
|
|
|
293
|
+
# Auto-refresh interval in seconds (0 = disabled)
|
|
294
|
+
refresh_interval = {self.refresh_interval}
|
|
295
|
+
|
|
289
296
|
# ============================================================================
|
|
290
297
|
# Git Settings
|
|
291
298
|
# ============================================================================
|
|
@@ -190,6 +190,13 @@ class GitOps:
|
|
|
190
190
|
True if current branch is merged into base_branch, False otherwise.
|
|
191
191
|
"""
|
|
192
192
|
try:
|
|
193
|
+
# Fetch latest remote refs so we detect merges done via GitLab/GitHub UI
|
|
194
|
+
subprocess.run(
|
|
195
|
+
["git", "fetch", "origin", base_branch],
|
|
196
|
+
cwd=worktree.path,
|
|
197
|
+
capture_output=True,
|
|
198
|
+
timeout=10,
|
|
199
|
+
)
|
|
193
200
|
# Use git merge-base --is-ancestor to check if HEAD is reachable from base
|
|
194
201
|
# This checks if the current branch has been merged
|
|
195
202
|
result = subprocess.run(
|
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
from rich.text import Text
|
|
4
4
|
from textual.widgets import Static
|
|
5
5
|
|
|
6
|
-
from ..services.models import GitStatus, Worktree
|
|
6
|
+
from ..services.models import GitStatus, Task, Worktree
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class StatusPanel(Static):
|
|
10
|
-
"""Panel displaying git status for selected worktree."""
|
|
10
|
+
"""Panel displaying git status for selected worktree or task summary."""
|
|
11
11
|
|
|
12
12
|
def __init__(self, *args, **kwargs):
|
|
13
13
|
super().__init__(*args, **kwargs)
|
|
14
14
|
self._worktree_name: str = ""
|
|
15
15
|
self._status: GitStatus | None = None
|
|
16
|
+
self._current_task: Task | None = None
|
|
17
|
+
self._task_statuses: dict[str, GitStatus] = {}
|
|
18
|
+
self._mode: str = "worktree" # "worktree" or "task"
|
|
16
19
|
|
|
17
20
|
def update_status(self, worktree: Worktree | None, status: GitStatus | None) -> None:
|
|
18
21
|
"""Update the status display for a worktree."""
|
|
22
|
+
self._mode = "worktree"
|
|
19
23
|
if worktree is None or status is None:
|
|
20
24
|
self._worktree_name = ""
|
|
21
25
|
self._status = None
|
|
@@ -26,8 +30,69 @@ class StatusPanel(Static):
|
|
|
26
30
|
self._status = status
|
|
27
31
|
self._update_display()
|
|
28
32
|
|
|
33
|
+
def update_task_summary(
|
|
34
|
+
self, task: Task | None, statuses: dict[str, GitStatus] | None = None
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Update the display with a task summary.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
task: The task to display
|
|
40
|
+
statuses: Optional dict of worktree_name -> GitStatus with file details
|
|
41
|
+
"""
|
|
42
|
+
self._mode = "task"
|
|
43
|
+
self._current_task = task
|
|
44
|
+
self._task_statuses = statuses or {}
|
|
45
|
+
self._update_display()
|
|
46
|
+
|
|
29
47
|
def _update_display(self) -> None:
|
|
30
48
|
"""Update the display content."""
|
|
49
|
+
if self._mode == "task":
|
|
50
|
+
self._render_task_summary()
|
|
51
|
+
else:
|
|
52
|
+
self._render_worktree_status()
|
|
53
|
+
|
|
54
|
+
def _render_task_summary(self) -> None:
|
|
55
|
+
"""Render the task summary view — changed files grouped by repo."""
|
|
56
|
+
if self._current_task is None:
|
|
57
|
+
self.update(Text("No task selected", style="dim"))
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
task = self._current_task
|
|
61
|
+
text = Text()
|
|
62
|
+
|
|
63
|
+
dirty_worktrees = [wt for wt in task.worktrees if wt.is_dirty]
|
|
64
|
+
|
|
65
|
+
if not dirty_worktrees:
|
|
66
|
+
text.append("All repos clean", style="green")
|
|
67
|
+
self.update(text)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
for wt in dirty_worktrees:
|
|
71
|
+
# Repo header
|
|
72
|
+
text.append(f"{wt.name}", style="bold")
|
|
73
|
+
text.append("\n")
|
|
74
|
+
|
|
75
|
+
status = self._task_statuses.get(wt.name)
|
|
76
|
+
if status:
|
|
77
|
+
for status_code, filename in status.all_changes:
|
|
78
|
+
if status_code.strip().startswith("?"):
|
|
79
|
+
text.append(f" {status_code} ", style="red")
|
|
80
|
+
text.append(f"{filename}\n", style="red")
|
|
81
|
+
elif status_code.strip().startswith("M") or status_code.strip() == "M":
|
|
82
|
+
text.append(f" {status_code} ", style="yellow")
|
|
83
|
+
text.append(f"{filename}\n")
|
|
84
|
+
else:
|
|
85
|
+
text.append(f" {status_code} ", style="green")
|
|
86
|
+
text.append(f"{filename}\n")
|
|
87
|
+
else:
|
|
88
|
+
text.append(f" {wt.changed_files} files changed\n", style="dim italic")
|
|
89
|
+
|
|
90
|
+
text.append("\n")
|
|
91
|
+
|
|
92
|
+
self.update(text)
|
|
93
|
+
|
|
94
|
+
def _render_worktree_status(self) -> None:
|
|
95
|
+
"""Render the worktree-specific status view."""
|
|
31
96
|
if not self._worktree_name or self._status is None:
|
|
32
97
|
self.update(Text("No worktree selected", style="dim"))
|
|
33
98
|
return
|
|
@@ -81,6 +146,8 @@ class StatusPanel(Static):
|
|
|
81
146
|
"""Clear the status display."""
|
|
82
147
|
self._worktree_name = ""
|
|
83
148
|
self._status = None
|
|
149
|
+
self._current_task = None
|
|
150
|
+
self._mode = "worktree"
|
|
84
151
|
self.update(Text("No worktree selected", style="dim"))
|
|
85
152
|
|
|
86
153
|
def set_loading(self, loading: bool = True) -> None:
|
|
@@ -60,27 +60,42 @@ class TaskList(OptionList):
|
|
|
60
60
|
"""Compatibility property - sets highlighted index."""
|
|
61
61
|
self.highlighted = value
|
|
62
62
|
|
|
63
|
-
def _format_task_option(self, task: Task) -> Option:
|
|
63
|
+
def _format_task_option(self, task: Task, claude_status: str | None = None) -> Option:
|
|
64
64
|
"""Format a task as an Option for display."""
|
|
65
|
-
|
|
65
|
+
claude_md = "[blue]◆[/]" if task.has_claude_md else " "
|
|
66
|
+
if claude_status == "running":
|
|
67
|
+
hook_indicator = " [magenta]⟳[/]"
|
|
68
|
+
elif claude_status == "waiting":
|
|
69
|
+
hook_indicator = " [yellow]![/]"
|
|
70
|
+
elif claude_status == "ended":
|
|
71
|
+
hook_indicator = " [green]✓[/]"
|
|
72
|
+
else:
|
|
73
|
+
hook_indicator = ""
|
|
66
74
|
if task.is_dirty:
|
|
67
|
-
prompt = f"[red]●[/]{
|
|
75
|
+
prompt = f"[red]●[/]{claude_md}{task.name} [red]({task.dirty_count})[/]{hook_indicator}"
|
|
68
76
|
else:
|
|
69
|
-
prompt = f" {
|
|
77
|
+
prompt = f" {claude_md}{task.name}{hook_indicator}"
|
|
70
78
|
return Option(prompt, id=task.name)
|
|
71
79
|
|
|
72
|
-
def load_tasks(
|
|
80
|
+
def load_tasks(
|
|
81
|
+
self,
|
|
82
|
+
tasks: list[Task],
|
|
83
|
+
preserve_selection: str | None = None,
|
|
84
|
+
claude_statuses: dict[str, str] | None = None,
|
|
85
|
+
) -> None:
|
|
73
86
|
"""Load tasks into the list.
|
|
74
87
|
|
|
75
88
|
Args:
|
|
76
89
|
tasks: List of tasks to load
|
|
77
90
|
preserve_selection: Optional task name to preserve selection for
|
|
91
|
+
claude_statuses: Optional dict of task_name -> claude status string
|
|
78
92
|
"""
|
|
79
93
|
sorted_tasks = self._sort_tasks(tasks)
|
|
80
94
|
self.tasks = sorted_tasks
|
|
81
95
|
self.clear_options()
|
|
96
|
+
statuses = claude_statuses or {}
|
|
82
97
|
for task in sorted_tasks:
|
|
83
|
-
self.add_option(self._format_task_option(task))
|
|
98
|
+
self.add_option(self._format_task_option(task, statuses.get(task.name)))
|
|
84
99
|
|
|
85
100
|
# Select item - preserve previous selection if specified
|
|
86
101
|
if self.tasks and self.option_count > 0:
|
|
@@ -131,6 +146,15 @@ class TaskList(OptionList):
|
|
|
131
146
|
self.highlighted = current_index
|
|
132
147
|
self._emit_highlighted()
|
|
133
148
|
|
|
149
|
+
def refresh_claude_indicators(self, statuses: dict[str, str]) -> None:
|
|
150
|
+
"""Update Claude status indicators without full reload.
|
|
151
|
+
|
|
152
|
+
Re-renders all option labels with updated claude status indicators.
|
|
153
|
+
"""
|
|
154
|
+
for i, task in enumerate(self.tasks):
|
|
155
|
+
new_option = self._format_task_option(task, statuses.get(task.name))
|
|
156
|
+
self.replace_option_prompt_at_index(i, new_option.prompt)
|
|
157
|
+
|
|
134
158
|
def cycle_sort_mode(self) -> None:
|
|
135
159
|
"""Cycle through sort modes and reload tasks."""
|
|
136
160
|
modes = list(SortMode)
|
|
@@ -140,9 +140,9 @@ class WorktreeList(OptionList):
|
|
|
140
140
|
claude_indicator = "[blue]◆[/]" if worktree.has_claude_md else " "
|
|
141
141
|
|
|
142
142
|
if worktree.is_dirty:
|
|
143
|
-
prompt = f" {claude_indicator}{name_col}
|
|
143
|
+
prompt = f" {claude_indicator}{name_col} {branch_col} [red]✗ {worktree.changed_files} files[/]"
|
|
144
144
|
else:
|
|
145
|
-
prompt = f" {claude_indicator}{name_col}
|
|
145
|
+
prompt = f" {claude_indicator}{name_col} {branch_col} [green]✓[/]"
|
|
146
146
|
self.add_option(Option(prompt, id=worktree.name))
|
|
147
147
|
|
|
148
148
|
def _emit_highlighted(self) -> None:
|
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/README.md
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/dashboard_app.py
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/data_viewer.py
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/todo_app.py
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/assets/worker_demo.py
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/layouts.md
RENAMED
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/styling.md
RENAMED
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/.claude/skills/textual-tui/references/widgets.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tasktree_manager-2.0.1 → tasktree_manager-2.2.0}/tasktree_manager/widgets/messages_panel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|