tasktree-manager 2.11.0__tar.gz → 2.11.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/CHANGELOG.md +6 -0
  2. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/PKG-INFO +1 -1
  3. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/README.md +2 -2
  4. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/docs/configuration.md +1 -15
  5. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/docs/troubleshooting.md +3 -3
  6. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/docs/user-guide.md +12 -13
  7. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/pyproject.toml +1 -1
  8. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/_version.py +2 -2
  9. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/app.py +8 -50
  10. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/commands.py +5 -2
  11. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/config.py +0 -16
  12. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/create_modal.py +1 -2
  13. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/worktree_list.py +2 -3
  14. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_app.py +0 -36
  15. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_config.py +0 -16
  16. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/SKILL.md +0 -0
  17. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/assets/README.md +0 -0
  18. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
  19. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
  20. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
  21. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
  22. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/references/layouts.md +0 -0
  23. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
  24. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/references/styling.md +0 -0
  25. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.claude/skills/textual-tui/references/widgets.md +0 -0
  26. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.github/templates/CHANGELOG.md.j2 +0 -0
  27. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.github/workflows/ci.yml +0 -0
  28. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.github/workflows/release.yml +0 -0
  29. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/.gitignore +0 -0
  30. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/CONTRIBUTING.md +0 -0
  31. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/LICENSE +0 -0
  32. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/OPENSPEC.md +0 -0
  33. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/ROADMAP.md +0 -0
  34. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/docs/claude-code-integration-spec.md +0 -0
  35. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/docs/installation.md +0 -0
  36. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/mise.toml +0 -0
  37. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/scripts/bump_version.py +0 -0
  38. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/__init__.py +0 -0
  39. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/cli.py +0 -0
  40. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/__init__.py +0 -0
  41. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/agent_sessions.py +0 -0
  42. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/claude_hooks.py +0 -0
  43. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/forge.py +0 -0
  44. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/git_ops.py +0 -0
  45. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/models.py +0 -0
  46. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/services/task_manager.py +0 -0
  47. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/themes.py +0 -0
  48. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/__init__.py +0 -0
  49. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/app_header.py +0 -0
  50. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/messages_panel.py +0 -0
  51. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/setup_modal.py +0 -0
  52. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/status_panel.py +0 -0
  53. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tasktree_manager/widgets/task_list.py +0 -0
  54. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/__init__.py +0 -0
  55. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/conftest.py +0 -0
  56. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_agent_sessions.py +0 -0
  57. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_claude_hooks.py +0 -0
  58. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_cli.py +0 -0
  59. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_commands.py +0 -0
  60. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_forge.py +0 -0
  61. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_git_ops.py +0 -0
  62. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_integration_git.py +0 -0
  63. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_memory.py +0 -0
  64. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_messages_panel.py +0 -0
  65. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_modals.py +0 -0
  66. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_setup_modal.py +0 -0
  67. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_status_panel.py +0 -0
  68. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_task_manager.py +0 -0
  69. {tasktree_manager-2.11.0 → tasktree_manager-2.11.1}/tests/test_worktree_badges.py +0 -0
@@ -15,6 +15,12 @@ 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.11.1] - 2026-07-31
19
+
20
+ ### Fixed
21
+ - **tui**: Open editor in new terminal tab, drop Enter shell binding
22
+
23
+
18
24
  ## [2.11.0] - 2026-07-30
19
25
 
20
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree-manager
3
- Version: 2.11.0
3
+ Version: 2.11.1
4
4
  Summary: TUI application for managing worktree-based tasks
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -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 `Enter` for shell
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,7 +107,7 @@ 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 | `Enter` | Open shell |
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 |
@@ -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"
@@ -235,7 +230,6 @@ open_lazygit = "g" # Open lazygit in selected worktree
235
230
  show_diff = "h" # Open hunk diff (task panel = all repos, worktree panel = one repo)
236
231
  open_editor = "e" # Open editor in selected task/worktree
237
232
  open_folder = "o" # Open folder in new terminal tab
238
- open_shell = "enter" # Open shell in selected worktree
239
233
  push_all = "p" # Push all worktrees in current task
240
234
  pull_all = "P" # Pull all worktrees in current task (shift+p)
241
235
  refresh = "r" # Refresh git status
@@ -399,7 +393,6 @@ refresh_interval = 15 # Refresh every 15 seconds
399
393
  | `editor` | string | `""` | `EDITOR` | Command to launch your editor |
400
394
  | `lazygit_path` | string | `"lazygit"` | - | Path to lazygit executable |
401
395
  | `hunk_path` | string | `"hunk"` | - | Path to hunk diff-viewer executable |
402
- | `shell` | string | `""` | `SHELL` | Shell to use when opening terminals |
403
396
  | `glab_path` | string | `"glab"` | - | Path to GitLab CLI (MR/CI status) |
404
397
  | `gh_path` | string | `"gh"` | - | Path to GitHub CLI (PR/CI status) |
405
398
 
@@ -438,12 +431,6 @@ failures degrade silently — features fall back to plain git behavior.
438
431
  - Install hunk: `brew install hunk` (macOS) or `npm i -g hunkdiff`
439
432
  - 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
433
 
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
434
  ## Symlinks
448
435
 
449
436
  When creating worktrees, tasktree-manager automatically symlinks gitignored files (like `.env`) from the source repository. This allows environment files to be shared without copying.
@@ -605,7 +592,6 @@ All 22 customizable keybindings with descriptions:
605
592
  | `show_diff` | `h` | Open hunk diff (task = all repos, worktree = one) | `show_diff = "ctrl+h"` |
606
593
  | `open_editor` | `e` | Open editor in task/worktree folder | `open_editor = "ctrl+e"` |
607
594
  | `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
595
  | `push_all` | `p` | Push all worktrees in current task | `push_all = "ctrl+p"` |
610
596
  | `pull_all` | `P` | Pull all worktrees in current task | `pull_all = "ctrl+shift+p"` |
611
597
  | `refresh` | `r` | Refresh git status for all worktrees | `refresh = "F5"` |
@@ -675,7 +661,7 @@ focus_previous = "h"
675
661
  [keybindings]
676
662
  new_task = "ctrl+n"
677
663
  delete_task = "ctrl+d"
678
- open_shell = "ctrl+t"
664
+ open_folder = "ctrl+t"
679
665
  refresh = "F5"
680
666
  quit = "ctrl+q"
681
667
  ```
@@ -732,8 +732,8 @@ HEAD detached at abc1234
732
732
 
733
733
  **Solution:**
734
734
  ```bash
735
- # Open shell in worktree
736
- Press Enter
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 Enter (shell)
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 `Enter` to open a shell in the worktree
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
- - Exit shell (type `exit`)
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
@@ -337,8 +337,7 @@ Useful when you're spinning up several similar tasks that share the same set of
337
337
  |---------|--------------------|------------------------------------------------------|
338
338
  | `g` | Open lazygit | Open lazygit in the selected worktree |
339
339
  | `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 |
340
+ | `e` | Open editor | Open editor in a new terminal tab at the task/worktree folder (runs `$EDITOR .`; the TUI keeps running) |
342
341
  | `p` | Push all | Push all worktrees in the current task (parallel) |
343
342
  | `P` | Pull all | Pull all worktrees in the current task (parallel) |
344
343
  | `r` | Refresh status | Refresh git status for all tasks and worktrees |
@@ -434,9 +433,9 @@ Create
434
433
  # Work in backend
435
434
  Tab (to worktree list)
436
435
  j (to highlight backend)
437
- Enter (open shell)
436
+ o (open new terminal tab)
438
437
 
439
- # In shell: make changes
438
+ # In the tab: make changes
440
439
  cd src
441
440
  vim auth.py
442
441
  # ... make changes ...
@@ -516,7 +515,7 @@ Select: backend
516
515
  Create
517
516
 
518
517
  Tab
519
- Enter (open shell)
518
+ o (open new terminal tab)
520
519
  git fetch origin feat-789:feat-789
521
520
  git checkout feat-789
522
521
  exit
@@ -553,7 +552,7 @@ q (quit tasktree-manager)
553
552
  tasktree-manager
554
553
  j (select FEAT-999)
555
554
  Tab
556
- Enter (shell in first worktree)
555
+ o (new tab in first worktree)
557
556
 
558
557
  # Merge main into feature branch
559
558
  git fetch origin main:main
@@ -631,7 +630,7 @@ d (delete task)
631
630
  **Working in worktree:**
632
631
  - Press `e` to open editor directly in worktree
633
632
  - Press `g` for lazygit (recommended for git operations)
634
- - Press `Enter` for shell (for building, testing, or other commands)
633
+ - Press `o` for a shell in a new terminal tab (building, testing, other commands)
635
634
 
636
635
  **Git operations:**
637
636
  - **Staging/committing**: Use lazygit (`g`)
@@ -764,9 +763,9 @@ editor = "nvim" # or "vim", "code", "emacs", etc.
764
763
 
765
764
  Or set the `$EDITOR` environment variable.
766
765
 
767
- **Alternative: Open shell first:**
768
- 1. Press `Enter` (open shell)
769
- 2. In shell:
766
+ **Alternative: Open a terminal tab first:**
767
+ 1. Press `o` (new terminal tab in the worktree)
768
+ 2. In the tab:
770
769
  ```bash
771
770
  vim . # Vim
772
771
  nvim . # Neovim
@@ -807,7 +806,7 @@ tasktree-manager creates normal git branches, so CI/CD works seamlessly:
807
806
  Integrate with GitHub CLI for PR creation:
808
807
 
809
808
  ```bash
810
- # In worktree shell (press Enter in tasktree-manager)
809
+ # In a worktree tab (press o in tasktree-manager)
811
810
  gh pr create --title "Add authentication" --body "Implements FEAT-123"
812
811
 
813
812
  # 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.11.0"
73
+ version = "2.11.1"
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.11.0'
22
- __version_tuple__ = version_tuple = (2, 11, 0)
21
+ __version__ = version = '2.11.1'
22
+ __version_tuple__ = version_tuple = (2, 11, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -208,7 +208,6 @@ class TaskTreeApp(App):
208
208
  Binding("m", "toggle_messages", "Messages", show=False),
209
209
  Binding("?", "help", "Help"),
210
210
  Binding("q", "quit", "Quit"),
211
- Binding("enter", "open_shell", "Shell", show=False),
212
211
  Binding("C", "open_claude_gui_code", "Claude GUI", show=False),
213
212
  Binding("P", "pull_all", "Pull All", show=False),
214
213
  Binding("S", "toggle_grouping", "Group", show=False),
@@ -287,7 +286,6 @@ class TaskTreeApp(App):
287
286
  Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages", show=False),
288
287
  Binding(kb.get("help", "?"), "help", "Help"),
289
288
  Binding(kb.get("quit", "q"), "quit", "Quit"),
290
- Binding(kb.get("open_shell", "enter"), "open_shell", "Shell", show=False),
291
289
  Binding(
292
290
  kb.get("open_claude_gui_code", "C"),
293
291
  "open_claude_gui_code",
@@ -331,7 +329,6 @@ class TaskTreeApp(App):
331
329
  (kb.get("show_diff", "h"), "show_diff", "Diff"),
332
330
  (kb.get("open_editor", "e"), "open_editor", "Editor"),
333
331
  (kb.get("open_folder", "o"), "open_folder", "Open"),
334
- (kb.get("open_shell", "enter"), "open_shell", "Shell"),
335
332
  (kb.get("dispatch_agent", "b"), "dispatch_agent", "Agent"),
336
333
  (kb.get("push_all", "p"), "push_all", "Push"),
337
334
  (kb.get("delete_worktree", "D"), "delete_worktree", "Del WT"),
@@ -1515,37 +1512,13 @@ class TaskTreeApp(App):
1515
1512
  # Restore focus to task list
1516
1513
  self.query_one("#task-list", TaskList).focus()
1517
1514
 
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
1515
  def action_open_editor(self) -> None:
1548
- """Open editor in the current folder (task or worktree based on focus)."""
1516
+ """Open editor in a new terminal tab at the current folder.
1517
+
1518
+ Same mechanism as open_folder: the TUI keeps running instead of
1519
+ suspending, so the editor session and the dashboard live side by
1520
+ side (matches the `o`/`c` new-tab behavior).
1521
+ """
1549
1522
  focused = self.focused
1550
1523
 
1551
1524
  # Determine folder based on focus (like open_folder does)
@@ -1562,24 +1535,9 @@ class TaskTreeApp(App):
1562
1535
  self.notify("Folder not found", severity="error")
1563
1536
  return
1564
1537
 
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
1538
  editor = self.config.get_editor()
1570
- self.notify(escape(f"Opening {editor}..."))
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()
1539
+ self._open_ghostty_tab(folder_path, command=f"{editor} .")
1540
+ self.notify(escape(f"Opened {editor} in new tab"))
1583
1541
 
1584
1542
  def _prepare_claude_session(self, task_path: Path) -> None:
1585
1543
  """Refresh CLAUDE.md files, worktree settings, and hooks before launching Claude."""
@@ -19,9 +19,12 @@ APP_COMMANDS: list[tuple[str, str, str]] = [
19
19
  "Show Diff",
20
20
  "Open hunk: all repos from the task panel, the selected repo from the worktree panel",
21
21
  ),
22
- ("open_editor", "Open Editor", "Open your editor in the focused task/worktree folder"),
22
+ (
23
+ "open_editor",
24
+ "Open Editor",
25
+ "Open your editor in a new terminal tab at the focused task/worktree folder",
26
+ ),
23
27
  ("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
28
  ("open_claude_resume", "Open Claude", "Open Claude Code for the task (resume if possible)"),
26
29
  (
27
30
  "dispatch_agent",
@@ -26,7 +26,6 @@ DEFAULT_KEYBINDINGS: dict[str, str] = {
26
26
  "open_lazygit": "g",
27
27
  "show_diff": "h",
28
28
  "open_folder": "o",
29
- "open_shell": "enter",
30
29
  "open_editor": "e",
31
30
  "open_claude_resume": "c",
32
31
  "open_claude_gui_code": "C",
@@ -122,7 +121,6 @@ class Config:
122
121
  claude_path: str = "claude"
123
122
  claude_memory_dir: str = "~/.claude/tasktree-memory"
124
123
  claude_repo_memory: bool = True
125
- shell: str = ""
126
124
  glab_path: str = "glab"
127
125
  gh_path: str = "gh"
128
126
 
@@ -184,7 +182,6 @@ class Config:
184
182
  claude_path = tools_config.get("claude_path", "claude")
185
183
  claude_memory_dir = tools_config.get("claude_memory_dir", "~/.claude/tasktree-memory")
186
184
  claude_repo_memory = bool(tools_config.get("claude_repo_memory", True))
187
- shell = tools_config.get("shell", "")
188
185
  glab_path = tools_config.get("glab_path", "glab")
189
186
  gh_path = tools_config.get("gh_path", "gh")
190
187
 
@@ -221,8 +218,6 @@ class Config:
221
218
  default_base_branch = os.environ["TASKTREE_DEFAULT_BRANCH"]
222
219
  if "EDITOR" in os.environ and not editor:
223
220
  editor = os.environ["EDITOR"]
224
- if "SHELL" in os.environ and not shell:
225
- shell = os.environ["SHELL"]
226
221
 
227
222
  return cls(
228
223
  repos_dir=repos_dir,
@@ -243,7 +238,6 @@ class Config:
243
238
  claude_path=claude_path,
244
239
  claude_memory_dir=claude_memory_dir,
245
240
  claude_repo_memory=claude_repo_memory,
246
- shell=shell,
247
241
  glab_path=glab_path,
248
242
  gh_path=gh_path,
249
243
  forge_enabled=forge_enabled,
@@ -398,9 +392,6 @@ claude_memory_dir = "{self._toml_escape(self.claude_memory_dir)}"
398
392
  # same repo and with the main checkout. Set to false to disable.
399
393
  claude_repo_memory = {str(self.claude_repo_memory).lower()}
400
394
 
401
- # Preferred shell (leave empty to use $SHELL)
402
- shell = "{self._toml_escape(self.shell)}"
403
-
404
395
  # Path to glab executable (GitLab CLI, used for MR/CI status)
405
396
  glab_path = "{self._toml_escape(self.glab_path)}"
406
397
 
@@ -436,7 +427,6 @@ delete_task = "{self._toml_escape(self.keybindings.get("delete_task", "d"))}"
436
427
  open_lazygit = "{self._toml_escape(self.keybindings.get("open_lazygit", "g"))}"
437
428
  show_diff = "{self._toml_escape(self.keybindings.get("show_diff", "h"))}"
438
429
  open_folder = "{self._toml_escape(self.keybindings.get("open_folder", "o"))}"
439
- open_shell = "{self._toml_escape(self.keybindings.get("open_shell", "enter"))}"
440
430
  open_editor = "{self._toml_escape(self.keybindings.get("open_editor", "e"))}"
441
431
  open_claude_resume = "{self._toml_escape(self.keybindings.get("open_claude_resume", "c"))}"
442
432
  open_claude_gui_code = "{self._toml_escape(self.keybindings.get("open_claude_gui_code", "C"))}"
@@ -513,12 +503,6 @@ blocklist = {self._toml_list(self.symlink_blocklist)}
513
503
 
514
504
  return sorted(repos)
515
505
 
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
506
  def get_editor(self) -> str:
523
507
  """Get the editor to use, with fallback to environment or vi."""
524
508
  if self.editor:
@@ -684,9 +684,8 @@ class HelpModal(ThemedModalScreen[None]):
684
684
  + "\n"
685
685
  )
686
686
  tools_section += (
687
- self._format_binding("open_shell", "enter", "Open shell in worktree") + "\n"
687
+ self._format_binding("open_editor", "e", "Open editor in new terminal tab") + "\n"
688
688
  )
689
- tools_section += self._format_binding("open_editor", "e", "Open editor in folder") + "\n"
690
689
  tools_section += (
691
690
  self._format_binding(
692
691
  "open_claude_resume", "c", "Open Claude CLI (resume if session exists, else new)"
@@ -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. Replacing "enter" here also
80
- # fixes shell opening: OptionList's own select binding used to
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,21 +391,6 @@ class TestOpenExternalCommands:
391
391
  # App should still be running
392
392
  assert app.is_running
393
393
 
394
- async def test_open_shell_no_worktree_warning(self, app, sample_repo, task_manager):
395
- """Test that shell with no worktree selected shows warning."""
396
- async with app.run_test() as pilot:
397
- await pilot.pause()
398
-
399
- # Clear current worktree
400
- app.current_worktree = None
401
-
402
- # Press enter for shell
403
- await pilot.press("enter")
404
- await pilot.pause()
405
-
406
- # App should still be running
407
- assert app.is_running
408
-
409
394
 
410
395
  class TestShowDiffAction:
411
396
  """Tests for the hunk diff action (h)."""
@@ -651,7 +636,6 @@ class TestContextSensitiveFooter:
651
636
  assert "app.delete_task" in shown
652
637
  # Worktree-context actions are hidden in this context
653
638
  assert "app.open_lazygit" not in shown
654
- assert "app.open_shell" not in shown
655
639
  # Globals always visible
656
640
  assert "cycle_theme" in shown
657
641
  assert "refresh" in shown
@@ -675,7 +659,6 @@ class TestContextSensitiveFooter:
675
659
  assert app.focused.id == "worktree-list"
676
660
  shown = self._shown_actions(app)
677
661
  assert "app.open_lazygit" in shown
678
- assert "app.open_shell" in shown
679
662
  assert "app.push_all" in shown
680
663
  # Task-context actions are hidden in this context
681
664
  assert "app.new_task" not in shown
@@ -699,22 +682,3 @@ class TestContextSensitiveFooter:
699
682
 
700
683
  create_modals = [s for s in app.screen_stack if isinstance(s, CreateTaskModal)]
701
684
  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