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

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