tasktree-manager 2.3.0__tar.gz → 2.4.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 (57) hide show
  1. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/CHANGELOG.md +6 -0
  2. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/PKG-INFO +1 -1
  3. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/docs/user-guide.md +6 -0
  4. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/pyproject.toml +1 -1
  5. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/_version.py +2 -2
  6. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/app.py +55 -30
  7. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/task_list.py +15 -1
  8. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/worktree_list.py +14 -1
  9. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_app.py +93 -0
  10. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/SKILL.md +0 -0
  11. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/assets/README.md +0 -0
  12. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/assets/dashboard_app.py +0 -0
  13. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/assets/data_viewer.py +0 -0
  14. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/assets/todo_app.py +0 -0
  15. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/assets/worker_demo.py +0 -0
  16. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/references/layouts.md +0 -0
  17. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/references/official-guides-index.md +0 -0
  18. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/references/styling.md +0 -0
  19. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.claude/skills/textual-tui/references/widgets.md +0 -0
  20. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.github/templates/CHANGELOG.md.j2 +0 -0
  21. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.github/workflows/ci.yml +0 -0
  22. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.github/workflows/release.yml +0 -0
  23. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/.gitignore +0 -0
  24. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/CONTRIBUTING.md +0 -0
  25. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/LICENSE +0 -0
  26. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/OPENSPEC.md +0 -0
  27. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/README.md +0 -0
  28. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/ROADMAP.md +0 -0
  29. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/docs/claude-code-integration-spec.md +0 -0
  30. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/docs/configuration.md +0 -0
  31. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/docs/installation.md +0 -0
  32. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/docs/troubleshooting.md +0 -0
  33. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/mise.toml +0 -0
  34. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/scripts/bump_version.py +0 -0
  35. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/__init__.py +0 -0
  36. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/__init__.py +0 -0
  37. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/claude_hooks.py +0 -0
  38. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/config.py +0 -0
  39. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/git_ops.py +0 -0
  40. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/models.py +0 -0
  41. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/services/task_manager.py +0 -0
  42. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/__init__.py +0 -0
  43. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/create_modal.py +0 -0
  44. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/messages_panel.py +0 -0
  45. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/setup_modal.py +0 -0
  46. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tasktree_manager/widgets/status_panel.py +0 -0
  47. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/__init__.py +0 -0
  48. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/conftest.py +0 -0
  49. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_config.py +0 -0
  50. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_git_ops.py +0 -0
  51. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_integration_git.py +0 -0
  52. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_memory.py +0 -0
  53. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_messages_panel.py +0 -0
  54. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_modals.py +0 -0
  55. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_setup_modal.py +0 -0
  56. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_status_panel.py +0 -0
  57. {tasktree_manager-2.3.0 → tasktree_manager-2.4.0}/tests/test_task_manager.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.4.0] - 2026-06-10
19
+
20
+ ### Added
21
+ - Context-sensitive footer showing the focused panel's keys (#6)
22
+
23
+
18
24
  ## [2.3.0] - 2026-06-10
19
25
 
20
26
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree-manager
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: TUI application for managing worktree-based tasks
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -246,6 +246,12 @@ Shows detailed git status for the selected worktree:
246
246
 
247
247
  All keybindings can be customized in [`config.toml`](configuration.md).
248
248
 
249
+ The footer is context-sensitive, lazygit-style: it shows the actions for the
250
+ focused panel (task actions in the Task List, worktree/git actions in the
251
+ Worktree List) plus the always-available `r` Refresh, `?` Help, and `q` Quit.
252
+ Every key works regardless of which panel has focus — the footer only changes
253
+ what is displayed. Press `?` for the full reference at any time.
254
+
249
255
  ### Navigation
250
256
 
251
257
  | Key | Action | Description |
@@ -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.3.0"
73
+ version = "2.4.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.3.0'
22
- __version_tuple__ = version_tuple = (2, 3, 0)
21
+ __version__ = version = '2.4.0'
22
+ __version_tuple__ = version_tuple = (2, 4, 0)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -210,28 +210,30 @@ class TaskTreeApp(App):
210
210
  }
211
211
  """
212
212
 
213
- # Default bindings - will be overridden in __init__ with config values
214
- # Footer order: n, a, d, g, o, c, p, r, m, q, ?
215
- # Uppercase shown only when shift is pressed: C, P, S
213
+ # Default bindings - will be overridden in __init__ with config values.
214
+ # The footer is context-sensitive: app-level bindings keep every key
215
+ # working globally but stay hidden, except the always-visible globals
216
+ # (refresh/help/quit). The focused panel contributes its own visible
217
+ # bindings (see compose), lazygit-style.
216
218
  BINDINGS = [
217
- Binding("n", "new_task", "New Task"),
218
- Binding("y", "clone_task", "Clone Task"),
219
- Binding("a", "add_repo", "Add Repo"),
220
- Binding("d", "delete_task", "Delete Task"),
221
- Binding("g", "open_lazygit", "Lazygit"),
222
- Binding("e", "open_editor", "Editor"),
223
- Binding("o", "open_folder", "Open"),
224
- Binding("c", "open_claude_resume", "Claude"),
225
- Binding("p", "push_all", "Push All"),
219
+ Binding("n", "new_task", "New", show=False),
220
+ Binding("y", "clone_task", "Clone", show=False),
221
+ Binding("a", "add_repo", "Add Repo", show=False),
222
+ Binding("d", "delete_task", "Delete", show=False),
223
+ Binding("g", "open_lazygit", "Lazygit", show=False),
224
+ Binding("e", "open_editor", "Editor", show=False),
225
+ Binding("o", "open_folder", "Open", show=False),
226
+ Binding("c", "open_claude_resume", "Claude", show=False),
227
+ Binding("p", "push_all", "Push", show=False),
226
228
  Binding("r", "refresh", "Refresh"),
227
- Binding("m", "toggle_messages", "Messages"),
229
+ Binding("m", "toggle_messages", "Messages", show=False),
228
230
  Binding("q", "quit", "Quit"),
229
231
  Binding("?", "help", "Help"),
230
232
  Binding("enter", "open_shell", "Shell", show=False),
231
233
  Binding("C", "open_claude_gui_code", "Claude GUI", show=False),
232
234
  Binding("P", "pull_all", "Pull All", show=False),
233
235
  Binding("S", "toggle_grouping", "Group", show=False),
234
- Binding("D", "delete_worktree", "Del Worktree", show=False),
236
+ Binding("D", "delete_worktree", "Del WT", show=False),
235
237
  Binding("tab", "focus_next", "Next Panel", show=False),
236
238
  Binding("shift+tab", "focus_previous", "Prev Panel", show=False),
237
239
  Binding("j", "cursor_down", "Down", show=False),
@@ -264,26 +266,28 @@ class TaskTreeApp(App):
264
266
  self._last_tasks_fingerprint: tuple | None = None
265
267
 
266
268
  def _build_bindings_from_config(self) -> list[Binding]:
267
- """Build keybindings list from config.
269
+ """Build app-level keybindings from config.
270
+
271
+ Every key works globally regardless of focus, but only the global
272
+ actions (refresh/help/quit) are shown in the footer; the focused
273
+ panel contributes its own visible bindings (see compose).
268
274
 
269
275
  Returns:
270
276
  List of Binding objects with keys from config
271
277
  """
272
278
  kb = self.config.keybindings
273
- # Footer order: n, a, d, g, o, c, p, r, m, q, ?
274
- # Uppercase shown only when shift is pressed: C, P, S
275
279
  return [
276
- Binding(kb.get("new_task", "n"), "new_task", "New Task"),
277
- Binding(kb.get("clone_task", "y"), "clone_task", "Clone Task"),
278
- Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo"),
279
- Binding(kb.get("delete_task", "d"), "delete_task", "Delete Task"),
280
- Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit"),
281
- Binding(kb.get("open_editor", "e"), "open_editor", "Editor"),
282
- Binding(kb.get("open_folder", "o"), "open_folder", "Open"),
283
- Binding(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
284
- Binding(kb.get("push_all", "p"), "push_all", "Push All"),
280
+ Binding(kb.get("new_task", "n"), "new_task", "New", show=False),
281
+ Binding(kb.get("clone_task", "y"), "clone_task", "Clone", show=False),
282
+ Binding(kb.get("add_repo", "a"), "add_repo", "Add Repo", show=False),
283
+ Binding(kb.get("delete_task", "d"), "delete_task", "Delete", show=False),
284
+ Binding(kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit", show=False),
285
+ Binding(kb.get("open_editor", "e"), "open_editor", "Editor", show=False),
286
+ Binding(kb.get("open_folder", "o"), "open_folder", "Open", show=False),
287
+ Binding(kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude", show=False),
288
+ Binding(kb.get("push_all", "p"), "push_all", "Push", show=False),
285
289
  Binding(kb.get("refresh", "r"), "refresh", "Refresh"),
286
- Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages"),
290
+ Binding(kb.get("toggle_messages", "m"), "toggle_messages", "Messages", show=False),
287
291
  Binding(kb.get("quit", "q"), "quit", "Quit"),
288
292
  Binding(kb.get("help", "?"), "help", "Help"),
289
293
  Binding(kb.get("open_shell", "enter"), "open_shell", "Shell", show=False),
@@ -295,7 +299,7 @@ class TaskTreeApp(App):
295
299
  ),
296
300
  Binding(kb.get("pull_all", "P"), "pull_all", "Pull All", show=False),
297
301
  Binding(kb.get("toggle_grouping", "S"), "toggle_grouping", "Group", show=False),
298
- Binding(kb.get("delete_worktree", "D"), "delete_worktree", "Del Worktree", show=False),
302
+ Binding(kb.get("delete_worktree", "D"), "delete_worktree", "Del WT", show=False),
299
303
  Binding(kb.get("focus_next", "tab"), "focus_next", "Next Panel", show=False),
300
304
  Binding(
301
305
  kb.get("focus_previous", "shift+tab"), "focus_previous", "Prev Panel", show=False
@@ -312,16 +316,37 @@ class TaskTreeApp(App):
312
316
 
313
317
  def compose(self) -> ComposeResult:
314
318
  """Compose the app layout."""
319
+ kb = self.config.keybindings
320
+ # Footer bindings shown only while the respective panel has focus.
321
+ # The keys themselves also exist app-level (hidden) so they work
322
+ # regardless of focus; these entries only add footer visibility.
323
+ task_panel_bindings = [
324
+ (kb.get("new_task", "n"), "new_task", "New"),
325
+ (kb.get("clone_task", "y"), "clone_task", "Clone"),
326
+ (kb.get("add_repo", "a"), "add_repo", "Add Repo"),
327
+ (kb.get("delete_task", "d"), "delete_task", "Delete"),
328
+ (kb.get("open_claude_resume", "c"), "open_claude_resume", "Claude"),
329
+ (kb.get("cycle_sort", "s"), "cycle_sort", "Sort"),
330
+ ]
331
+ worktree_panel_bindings = [
332
+ (kb.get("open_lazygit", "g"), "open_lazygit", "Lazygit"),
333
+ (kb.get("open_editor", "e"), "open_editor", "Editor"),
334
+ (kb.get("open_folder", "o"), "open_folder", "Open"),
335
+ (kb.get("open_shell", "enter"), "open_shell", "Shell"),
336
+ (kb.get("push_all", "p"), "push_all", "Push"),
337
+ (kb.get("delete_worktree", "D"), "delete_worktree", "Del WT"),
338
+ ]
339
+
315
340
  yield Header()
316
341
  with Horizontal(id="main-container"):
317
342
  # Left column: Tasks (top) and Worktrees (bottom)
318
343
  with Vertical(id="left-column"):
319
344
  with Vertical(id="task-panel"):
320
345
  yield Static("Tasks", classes="panel-title", id="task-panel-title")
321
- yield TaskList(id="task-list")
346
+ yield TaskList(id="task-list", context_bindings=task_panel_bindings)
322
347
  with Vertical(id="worktree-panel"):
323
348
  yield Static("Worktrees", classes="panel-title", id="worktree-panel-title")
324
- yield WorktreeList(id="worktree-list")
349
+ yield WorktreeList(id="worktree-list", context_bindings=worktree_panel_bindings)
325
350
  # Right column: Info/Status panel
326
351
  with Vertical(id="right-column"):
327
352
  with Vertical(id="status-panel"):
@@ -45,12 +45,26 @@ class TaskList(OptionList):
45
45
  self.label = label
46
46
  super().__init__()
47
47
 
48
- def __init__(self, *args, **kwargs):
48
+ def __init__(
49
+ self,
50
+ *args,
51
+ context_bindings: list[tuple[str, str, str]] | None = None,
52
+ **kwargs,
53
+ ):
49
54
  super().__init__(*args, **kwargs)
50
55
  self.tasks: list[Task] = []
51
56
  self._sort_mode: SortMode = SortMode.NAME_ASC
52
57
  # Last known Claude session statuses, kept so indicators survive reloads
53
58
  self._claude_statuses: dict[str, str] = {}
59
+ # Footer-visible (key, app action, description) bindings shown while
60
+ # this panel has focus; the keys also exist app-level (hidden) so
61
+ # they keep working regardless of focus
62
+ for key, action, description in context_bindings or []:
63
+ # Drop any inherited binding on the same key - dispatch and the
64
+ # footer both use the first binding found, so an OptionList
65
+ # default (e.g. "enter") would otherwise shadow the app action
66
+ self._bindings.key_to_bindings.pop(key, None)
67
+ self._bindings.bind(key, f"app.{action}", description, show=True)
54
68
 
55
69
  @property
56
70
  def index(self) -> int | None:
@@ -31,12 +31,25 @@ class WorktreeList(OptionList):
31
31
  self.enabled = enabled
32
32
  super().__init__()
33
33
 
34
- def __init__(self, *args, **kwargs):
34
+ def __init__(
35
+ self,
36
+ *args,
37
+ context_bindings: list[tuple[str, str, str]] | None = None,
38
+ **kwargs,
39
+ ):
35
40
  super().__init__(*args, **kwargs)
36
41
  self.worktrees: list[Worktree] = []
37
42
  self._grouping_enabled: bool = False
38
43
  # Maps option index to worktree index (for handling headers)
39
44
  self._option_to_worktree: dict[int, int] = {}
45
+ # Footer-visible (key, app action, description) bindings shown while
46
+ # this panel has focus; the keys also exist app-level (hidden) so
47
+ # they keep working regardless of focus. Replacing "enter" here also
48
+ # fixes shell opening: OptionList's own select binding used to
49
+ # swallow the key before the app's open_shell binding could see it.
50
+ for key, action, description in context_bindings or []:
51
+ self._bindings.key_to_bindings.pop(key, None)
52
+ self._bindings.bind(key, f"app.{action}", description, show=True)
40
53
 
41
54
  @property
42
55
  def index(self) -> int | None:
@@ -570,3 +570,96 @@ class TestAppConfiguration:
570
570
  from tasktree_manager.services.git_ops import GitOps
571
571
 
572
572
  assert GitOps.network_timeout == app.config.git_timeout
573
+
574
+
575
+ class TestContextSensitiveFooter:
576
+ """Tests for the context-sensitive footer bindings."""
577
+
578
+ @staticmethod
579
+ def _shown_actions(app) -> set[str]:
580
+ """Actions currently visible in the footer."""
581
+ return {
582
+ active.binding.action
583
+ for active in app.screen.active_bindings.values()
584
+ if active.binding.show
585
+ }
586
+
587
+ async def test_task_panel_footer_shows_task_actions(self, app, sample_repo, task_manager):
588
+ """Task list focused: task actions plus globals are visible."""
589
+ repo_path, branch = sample_repo
590
+ task_manager.create_task("FOOTER-TASKS", ["sample-repo"], branch)
591
+
592
+ async with app.run_test() as pilot:
593
+ await pilot.pause()
594
+
595
+ assert app.focused.id == "task-list"
596
+ shown = self._shown_actions(app)
597
+ assert "app.new_task" in shown
598
+ assert "app.clone_task" in shown
599
+ assert "app.delete_task" in shown
600
+ # Worktree-context actions are hidden in this context
601
+ assert "app.open_lazygit" not in shown
602
+ assert "app.open_shell" not in shown
603
+ # Globals always visible
604
+ assert "refresh" in shown
605
+ assert "help" in shown
606
+ assert "quit" in shown
607
+
608
+ async def test_worktree_panel_footer_shows_worktree_actions(
609
+ self, app, sample_repo, task_manager
610
+ ):
611
+ """Worktree list focused: worktree actions plus globals are visible."""
612
+ repo_path, branch = sample_repo
613
+ task_manager.create_task("FOOTER-WT", ["sample-repo"], branch)
614
+
615
+ async with app.run_test() as pilot:
616
+ await pilot.pause()
617
+ await pilot.press("tab")
618
+ await pilot.pause()
619
+
620
+ assert app.focused.id == "worktree-list"
621
+ shown = self._shown_actions(app)
622
+ assert "app.open_lazygit" in shown
623
+ assert "app.open_shell" in shown
624
+ assert "app.push_all" in shown
625
+ # Task-context actions are hidden in this context
626
+ assert "app.new_task" not in shown
627
+ # Globals always visible
628
+ assert "refresh" in shown
629
+ assert "quit" in shown
630
+
631
+ async def test_keys_work_outside_their_panel(self, app, sample_repos, task_manager):
632
+ """A task-context key still works while the worktree list is focused."""
633
+ _, branch = sample_repos
634
+ task_manager.create_task("FOOTER-KEYS", ["repo-alpha"], branch)
635
+
636
+ async with app.run_test() as pilot:
637
+ await pilot.pause()
638
+ await pilot.press("tab") # focus worktree list
639
+ await pilot.pause()
640
+
641
+ # 'n' is a task-panel binding but must still open the modal
642
+ await pilot.press("n")
643
+ await pilot.pause()
644
+
645
+ create_modals = [s for s in app.screen_stack if isinstance(s, CreateTaskModal)]
646
+ assert len(create_modals) == 1
647
+
648
+ async def test_enter_opens_shell_from_worktree_list(
649
+ self, app, sample_repo, task_manager, monkeypatch
650
+ ):
651
+ """Enter on the worktree list triggers open_shell (was swallowed by select)."""
652
+ repo_path, branch = sample_repo
653
+ task_manager.create_task("FOOTER-ENTER", ["sample-repo"], branch)
654
+
655
+ called = []
656
+ monkeypatch.setattr(app, "action_open_shell", lambda: called.append(True))
657
+
658
+ async with app.run_test() as pilot:
659
+ await pilot.pause()
660
+ await pilot.press("tab")
661
+ await pilot.pause()
662
+ await pilot.press("enter")
663
+ await pilot.pause()
664
+
665
+ assert called