gitdirector 1.7.2__tar.gz → 1.7.3__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 (60) hide show
  1. {gitdirector-1.7.2 → gitdirector-1.7.3}/PKG-INFO +5 -2
  2. {gitdirector-1.7.2 → gitdirector-1.7.3}/README.md +3 -1
  3. {gitdirector-1.7.2 → gitdirector-1.7.3}/pyproject.toml +2 -1
  4. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/capture.py +1 -2
  5. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/gd_send.py +2 -1
  6. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/gd_tmux.py +6 -19
  7. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/help.py +4 -1
  8. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/info.py +11 -20
  9. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/link.py +15 -0
  10. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/__init__.py +1 -2
  11. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app.py +12 -19
  12. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app_groups.py +1 -1
  13. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app_panels.py +16 -15
  14. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app_repos.py +16 -8
  15. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app_sessions.py +4 -7
  16. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/app_ui.py +10 -0
  17. gitdirector-1.7.3/src/gitdirector/commands/tui/panel_view.py +211 -0
  18. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/panels.py +112 -16
  19. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/panels.py +16 -6
  20. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/terminal_widget.py +6 -2
  21. gitdirector-1.7.3/src/gitdirector/commands/unlink.py +48 -0
  22. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/config.py +45 -17
  23. gitdirector-1.7.3/src/gitdirector/integrations/tmux/__init__.py +106 -0
  24. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/integrations/tmux/core.py +28 -9
  25. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/integrations/tmux/monitor.py +6 -1
  26. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/integrations/tmux/panels.py +7 -1
  27. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/manager.py +83 -1
  28. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/repo.py +7 -0
  29. gitdirector-1.7.2/src/gitdirector/commands/tui/panel_view.py +0 -478
  30. gitdirector-1.7.2/src/gitdirector/commands/unlink.py +0 -48
  31. gitdirector-1.7.2/src/gitdirector/integrations/tmux/__init__.py +0 -5
  32. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/__init__.py +0 -0
  33. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/cli.py +0 -0
  34. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/__init__.py +0 -0
  35. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/autoclean.py +0 -0
  36. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/cd.py +0 -0
  37. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/completion.py +0 -0
  38. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/listt.py +0 -0
  39. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/pull.py +0 -0
  40. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/reset.py +0 -0
  41. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/status.py +0 -0
  42. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/constants.py +0 -0
  43. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/diff_renderer.py +0 -0
  44. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/__init__.py +0 -0
  45. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/_shared.py +0 -0
  46. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/commit.py +0 -0
  47. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/diff.py +0 -0
  48. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/diff_files.py +0 -0
  49. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/groups.py +0 -0
  50. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/repos.py +0 -0
  51. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/session_actions.py +0 -0
  52. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/screens/sessions.py +0 -0
  53. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/table_text.py +0 -0
  54. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/commands/tui/terminal_caps.py +0 -0
  55. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/github_credential_helper.py +0 -0
  56. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/info.py +0 -0
  57. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/integrations/__init__.py +0 -0
  58. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/storage.py +0 -0
  59. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/ui_theme.py +0 -0
  60. {gitdirector-1.7.2 → gitdirector-1.7.3}/src/gitdirector/version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gitdirector
3
- Version: 1.7.2
3
+ Version: 1.7.3
4
4
  Summary: A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place.
5
5
  Keywords: git,repository,manager,cli,synchronization,batch
6
6
  Author: Anito Anto
@@ -24,6 +24,7 @@ Requires-Dist: rich>=12.0
24
24
  Requires-Dist: textual>=8.2.1
25
25
  Requires-Dist: tiktoken>=0.5.0
26
26
  Requires-Dist: pyte>=0.8.2
27
+ Requires-Dist: pathspec>=1.0.0
27
28
  Requires-Dist: pytest>=7.0 ; extra == 'dev'
28
29
  Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
29
30
  Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
@@ -67,7 +68,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
67
68
  | Command | Description |
68
69
  | ---------------------------------------------------------- | ------------------------------------------------------ |
69
70
  | `gitdirector console` | Open the interactive TUI dashboard |
70
- | `gitdirector link PATH [--discover]` | Link a repository or discover all under a path |
71
+ | `gitdirector link PATH [--discover]` | Link a repository or discover all under a path (no-op if none) |
71
72
  | `gitdirector unlink PATH\|NAME [--discover]` | Unlink a repository by path, name, or all under a path |
72
73
  | `gitdirector list` | List all tracked repositories with live status |
73
74
  | `gitdirector status` | Show repositories with staged/unstaged files |
@@ -89,6 +90,8 @@ gitdirector link /path/to/repo
89
90
  gitdirector link /path/to/folder --discover # recursively find and link all repos
90
91
  ```
91
92
 
93
+ `link --discover` succeeds without changes when the path contains no new repositories.
94
+
92
95
  ### console
93
96
 
94
97
  ```bash
@@ -25,7 +25,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
25
25
  | Command | Description |
26
26
  | ---------------------------------------------------------- | ------------------------------------------------------ |
27
27
  | `gitdirector console` | Open the interactive TUI dashboard |
28
- | `gitdirector link PATH [--discover]` | Link a repository or discover all under a path |
28
+ | `gitdirector link PATH [--discover]` | Link a repository or discover all under a path (no-op if none) |
29
29
  | `gitdirector unlink PATH\|NAME [--discover]` | Unlink a repository by path, name, or all under a path |
30
30
  | `gitdirector list` | List all tracked repositories with live status |
31
31
  | `gitdirector status` | Show repositories with staged/unstaged files |
@@ -47,6 +47,8 @@ gitdirector link /path/to/repo
47
47
  gitdirector link /path/to/folder --discover # recursively find and link all repos
48
48
  ```
49
49
 
50
+ `link --discover` succeeds without changes when the path contains no new repositories.
51
+
50
52
  ### console
51
53
 
52
54
  ```bash
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "gitdirector"
7
- version = "1.7.2"
7
+ version = "1.7.3"
8
8
  description = "A terminal based control plane for developers working across multiple repositories. Launch multiple AI coding agents, multiple tmux sessions and track changes across all your repos in one place."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -37,6 +37,7 @@ dependencies = [
37
37
  "textual>=8.2.1",
38
38
  "tiktoken>=0.5.0",
39
39
  "pyte>=0.8.2",
40
+ "pathspec>=1.0.0",
40
41
  ]
41
42
 
42
43
  [project.optional-dependencies]
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
  import click
19
19
 
20
20
  from ..integrations.tmux import capture_pane
21
+ from ..integrations.tmux.core import _parse_gd_session_name
21
22
 
22
23
 
23
24
  def _resolve_session_name(name: str) -> str:
@@ -26,8 +27,6 @@ def _resolve_session_name(name: str) -> str:
26
27
  gd-capture refuses anything that doesn't match the ``gd/{repo}/{purpose}/{N}``
27
28
  shape — that way a typo can't be silently routed to the wrong session.
28
29
  """
29
- from ..integrations.tmux import _parse_gd_session_name
30
-
31
30
  parsed = _parse_gd_session_name(name)
32
31
  if parsed is None:
33
32
  raise click.BadParameter(
@@ -2,7 +2,8 @@ from __future__ import annotations
2
2
 
3
3
  import click
4
4
 
5
- from ..integrations.tmux import _parse_gd_session_name, send_key_to_session, send_text_to_session
5
+ from ..integrations.tmux import send_key_to_session, send_text_to_session
6
+ from ..integrations.tmux.core import _parse_gd_session_name
6
7
 
7
8
  SUPPORTED_KEYS = ("C-c",)
8
9
 
@@ -3,7 +3,6 @@ from pathlib import Path
3
3
  import click
4
4
 
5
5
  from ..manager import RepositoryManager
6
- from ..storage import normalize_repository_path
7
6
  from . import console
8
7
  from .completion import complete_repository_names
9
8
 
@@ -17,24 +16,12 @@ def _resolve_repo(target: str) -> Path | None:
17
16
  or ambiguous targets.
18
17
  """
19
18
  manager = RepositoryManager()
20
- candidate = Path(target)
21
- is_path_like = (
22
- "/" in target
23
- or "\\" in target
24
- or target in (".", "..")
25
- or candidate.is_absolute()
26
- or target.startswith("~")
27
- or candidate.exists()
28
- )
29
-
30
- if is_path_like:
31
- normalized = normalize_repository_path(candidate)
32
- if manager.config.has_repository(normalized):
33
- return normalized
34
- console.print(f"\n [red]No tracked repository at path: {normalized}[/red]\n")
19
+ repo_path, matches, path_attempted = manager.resolve_repository_target(target)
20
+ if repo_path is not None:
21
+ return repo_path
22
+ if path_attempted:
23
+ console.print(f"\n [red]No tracked repository at path: {target}[/red]\n")
35
24
  return None
36
-
37
- matches = [r for r in manager.config.repositories if r.name == target]
38
25
  if not matches:
39
26
  console.print(f"\n [red]No tracked repository named: {target}[/red]\n")
40
27
  return None
@@ -45,7 +32,7 @@ def _resolve_repo(target: str) -> Path | None:
45
32
  f"{paths_list}\n"
46
33
  )
47
34
  return None
48
- return matches[0]
35
+ return None
49
36
 
50
37
 
51
38
  def register(cli: click.Group):
@@ -26,7 +26,10 @@ def show_help():
26
26
  cmd_table.add_column("desc", style="dim")
27
27
 
28
28
  for cmd, desc in [
29
- ("link PATH [--discover]", "Link a repository or discover all repos under a path"),
29
+ (
30
+ "link PATH [--discover]",
31
+ "Link a repository or discover all repos under a path (no-op if none)",
32
+ ),
30
33
  ("unlink PATH|NAME [--discover]", "Unlink a repository or all repos under a path"),
31
34
  ("list", "List all tracked repositories"),
32
35
  ("status", "Show status summary and per-repo details"),
@@ -46,29 +46,20 @@ def register(cli: click.Group):
46
46
  def info(target: str, full: bool):
47
47
  """Show file statistics for a repository."""
48
48
  manager = RepositoryManager()
49
- candidate_path = Path(target).expanduser()
50
-
51
- if candidate_path.is_dir() and (candidate_path / ".git").is_dir():
52
- repo_path = candidate_path.resolve()
53
- else:
54
- repos = manager.config.repositories
55
- target_lower = target.lower()
56
- exact = [r for r in repos if r.name.lower() == target_lower]
57
- if exact:
58
- matches = exact
59
- else:
60
- matches = [r for r in repos if target_lower in r.name.lower()]
61
-
49
+ repo_path, matches, _path_attempted = manager.resolve_repository_target(
50
+ target,
51
+ allow_untracked_git_path=True,
52
+ fuzzy_names=True,
53
+ )
54
+ if repo_path is None:
62
55
  if not matches:
63
56
  console.print(f"\n [red]Repository '{target}' not found[/red]\n")
64
57
  raise SystemExit(1)
65
- if len(matches) > 1:
66
- console.print(f"\n [red]Multiple repositories match '{target}':[/red]")
67
- for m in matches:
68
- console.print(f" {m}")
69
- console.print()
70
- raise SystemExit(1)
71
- repo_path = matches[0]
58
+ console.print(f"\n [red]Multiple repositories match '{target}':[/red]")
59
+ for match in matches:
60
+ console.print(f" {match}")
61
+ console.print()
62
+ raise SystemExit(1)
72
63
 
73
64
  result = gather_repo_info(repo_path, full=full)
74
65
  _render_info_cli(result, repo_path.name, repo_path)
@@ -28,6 +28,21 @@ def register(cli: click.Group):
28
28
  else:
29
29
  for repo_path in repos:
30
30
  console.print(f" [green]+[/green] {repo_path}")
31
+ elif (
32
+ discover
33
+ and not repos
34
+ and message
35
+ in {
36
+ "No git repositories found",
37
+ "No new repositories found",
38
+ }
39
+ ):
40
+ console.print(f" {message}")
41
+ for repo_path in skipped:
42
+ console.print(
43
+ f" [dim yellow]\\[skipped][/dim yellow] "
44
+ f"[bright_black]{repo_path}[/bright_black]"
45
+ )
31
46
  else:
32
47
  console.print(f" [red]{message}[/red]")
33
48
  console.print()
@@ -13,7 +13,7 @@ from .constants import (
13
13
  _changes_label,
14
14
  _changes_sort_key,
15
15
  )
16
- from .panel_view import PanelViewScreen, PaneWidget
16
+ from .panel_view import PaneWidget
17
17
  from .panels import Panel, PanelStore
18
18
  from .screens import SortMenuScreen
19
19
  from .screens.diff import DiffReviewScreen
@@ -51,7 +51,6 @@ __all__ = [
51
51
  "Panel",
52
52
  "PanelActionMenuScreen",
53
53
  "PanelStore",
54
- "PanelViewScreen",
55
54
  "_PANELS_SORT_COLUMN_NAMES",
56
55
  "PullLoadingScreen",
57
56
  "PullResultScreen",
@@ -126,25 +126,18 @@ class GitDirectorConsole(
126
126
  padding: 0 1;
127
127
  scrollbar-size-horizontal: 0;
128
128
  }
129
- #no-repos-message {
130
- height: 1fr;
131
- display: none;
132
- align: center middle;
133
- color: $text-muted;
134
- padding: 2 4;
135
- content-align: center middle;
136
- }
137
- #no-sessions-message {
138
- height: 1fr;
129
+ #repo-table,
130
+ #sessions-table,
131
+ #panels-table,
132
+ #no-repos-message,
133
+ #no-sessions-message,
134
+ #no-panels-message {
139
135
  display: none;
140
- align: center middle;
141
- color: $text-muted;
142
- padding: 2 4;
143
- content-align: center middle;
144
136
  }
137
+ #no-repos-message,
138
+ #no-sessions-message,
145
139
  #no-panels-message {
146
140
  height: 1fr;
147
- display: none;
148
141
  align: center middle;
149
142
  color: $text-muted;
150
143
  padding: 2 4;
@@ -204,7 +197,7 @@ class GitDirectorConsole(
204
197
  Binding("1", "tab_repos", "Repos", show=False),
205
198
  Binding("2", "tab_sessions", "Sessions", show=False),
206
199
  Binding("3", "tab_panels", "Panels", show=False),
207
- Binding("space", "toggle_group", "Toggle Group", show=True),
200
+ Binding("space", "toggle_group", "Toggle", show=True),
208
201
  Binding("n", "new_panel", "New Panel", show=True),
209
202
  ]
210
203
 
@@ -493,7 +486,7 @@ class GitDirectorConsole(
493
486
  skip_config_sync: bool = False,
494
487
  callback: Callable[[object], None] | None = None,
495
488
  ) -> None:
496
- from ...integrations.tmux import _parse_gd_session_name
489
+ from ...integrations.tmux.core import _parse_gd_session_name
497
490
 
498
491
  title = "session"
499
492
  loading_hint = "waiting for session to initialize\u2026"
@@ -690,7 +683,7 @@ class GitDirectorConsole(
690
683
  """Open the description editor for the currently highlighted session row."""
691
684
  if self._active_tab != "sessions":
692
685
  return
693
- from ...integrations.tmux import _get_session_description
686
+ from ...integrations.tmux.core import _get_session_description
694
687
 
695
688
  table = self.query_one("#sessions-table", DataTable)
696
689
  session_name = self._get_selected_row_key(table)
@@ -705,7 +698,7 @@ class GitDirectorConsole(
705
698
  def _handle_description_edit(self, session_name: str, value: str | None) -> None:
706
699
  if value is None:
707
700
  return
708
- from ...integrations.tmux import _set_session_description
701
+ from ...integrations.tmux.core import _set_session_description
709
702
 
710
703
  _set_session_description(session_name, value)
711
704
  for entry in self._sessions_entries:
@@ -44,7 +44,7 @@ def detect_repo_groups(repo_paths: Iterable[Path]) -> list[RepoGroup]:
44
44
 
45
45
 
46
46
  def group_session_repo_label(path: Path) -> str:
47
- from ...integrations.tmux import _sanitize_repo_name
47
+ from ...integrations.tmux.core import _sanitize_repo_name
48
48
 
49
49
  return f"group_{_sanitize_repo_name(path.name) or 'repo'}"
50
50
 
@@ -71,7 +71,7 @@ class ConsolePanelsMixin:
71
71
  def _panel_matches_search(
72
72
  self, panel: Panel, query: str, live_sessions: set[str] | None = None
73
73
  ) -> bool:
74
- from ...integrations.tmux import make_panel_session_name
74
+ from ...integrations.tmux.core import make_panel_session_name
75
75
 
76
76
  normalized_query = query.replace("×", "x")
77
77
  live_panes = self._live_panel_pane_count(panel, live_sessions)
@@ -88,7 +88,7 @@ class ConsolePanelsMixin:
88
88
  return any(normalized_query in haystack.replace("×", "x") for haystack in haystacks)
89
89
 
90
90
  def _panel_sort_key_func(self):
91
- from ...integrations.tmux import make_panel_session_name
91
+ from ...integrations.tmux.core import make_panel_session_name
92
92
 
93
93
  col = self._panels_sort_column
94
94
  if col == 1:
@@ -115,7 +115,7 @@ class ConsolePanelsMixin:
115
115
  return lambda panel: panel.name.lower()
116
116
 
117
117
  def _apply_panels_filter_and_sort(self, live_sessions: set[str] | None = None) -> None:
118
- from ...integrations.tmux import _list_sessions, make_panel_session_name
118
+ from ...integrations.tmux.core import _list_sessions, make_panel_session_name
119
119
 
120
120
  try:
121
121
  table = self.query_one("#panels-table", DataTable)
@@ -128,7 +128,6 @@ class ConsolePanelsMixin:
128
128
  preserved_row_key, preserved_row_index, restore_focus = self._capture_table_selection(
129
129
  table
130
130
  )
131
- table.clear()
132
131
  no_msg = self.query_one("#no-panels-message", Static)
133
132
 
134
133
  panels = list(self._panels_entries)
@@ -148,12 +147,10 @@ class ConsolePanelsMixin:
148
147
 
149
148
  panels.sort(key=self._panel_sort_key_func(), reverse=self._panels_sort_reverse)
150
149
 
151
- if not panels and total == 0 and not self._search_query:
152
- table.display = False
153
- no_msg.display = True
154
- else:
155
- table.display = True
156
- no_msg.display = False
150
+ is_empty = not panels and total == 0 and not self._search_query
151
+ self._set_table_empty_state(table, no_msg, is_empty=is_empty)
152
+ table.clear()
153
+ if not is_empty:
157
154
  for panel in panels:
158
155
  filled = self._live_panel_pane_count(panel, live_sessions)
159
156
  total_panes = panel.total_panes
@@ -279,19 +276,23 @@ class ConsolePanelsMixin:
279
276
  def _do_rename_panel(self, old_name: str, new_name: str | None) -> None:
280
277
  if new_name is None or new_name == old_name:
281
278
  return
282
- existing = self._panel_store.get(new_name)
283
- if existing:
284
- self._update_status(f"Panel '{new_name}' already exists")
279
+ validation_message = CreatePanelScreen.validate_new_panel_name(
280
+ self._panel_store,
281
+ new_name,
282
+ current_name=old_name,
283
+ )
284
+ if validation_message:
285
+ self._update_status(validation_message)
285
286
  return
286
287
  self._panel_store.rename(old_name, new_name)
287
288
  self._load_panels()
288
289
 
289
290
  def _open_panel(self, panel_name: str) -> None:
290
- from ...integrations.tmux import (
291
+ from ...integrations.tmux import rebuild_panel_tmux_session
292
+ from ...integrations.tmux.core import (
291
293
  _protect_session,
292
294
  _session_exists,
293
295
  make_panel_session_name,
294
- rebuild_panel_tmux_session,
295
296
  )
296
297
 
297
298
  panel = self._panel_store.get(panel_name)
@@ -98,8 +98,8 @@ class ConsoleReposMixin:
98
98
 
99
99
  def _populate_initial_rows(self) -> None:
100
100
  table = self.query_one("#repo-table", DataTable)
101
- self.query_one("#no-repos-message", Static).display = False
102
- table.display = True
101
+ no_msg = self.query_one("#no-repos-message", Static)
102
+ self._set_table_empty_state(table, no_msg, is_empty=False)
103
103
  preserved_row_key = None
104
104
  preserved_row_index = None
105
105
  restore_focus = False
@@ -133,8 +133,10 @@ class ConsoleReposMixin:
133
133
  logger.debug("Failed to update repo row %s", row_key, exc_info=True)
134
134
 
135
135
  def _show_no_repos(self) -> None:
136
- self.query_one("#repo-table", DataTable).display = False
137
- self.query_one("#no-repos-message", Static).display = True
136
+ table = self.query_one("#repo-table", DataTable)
137
+ no_msg = self.query_one("#no-repos-message", Static)
138
+ self._set_table_empty_state(table, no_msg, is_empty=True)
139
+ table.clear()
138
140
  self._visible_repo_count = 0
139
141
  self._visible_group_count = 0
140
142
  self._update_status("No repositories linked")
@@ -314,8 +316,7 @@ class ConsoleReposMixin:
314
316
 
315
317
  def _apply_filter_and_sort(self) -> None:
316
318
  table = self.query_one("#repo-table", DataTable)
317
- self.query_one("#no-repos-message", Static).display = False
318
- table.display = True
319
+ no_msg = self.query_one("#no-repos-message", Static)
319
320
  preserved_row_key = None
320
321
  preserved_row_index = None
321
322
  restore_focus = False
@@ -328,7 +329,14 @@ class ConsoleReposMixin:
328
329
  infos = list(self._results.values())
329
330
  total = len(infos)
330
331
  infos = self._filter_repo_infos_for_search(infos)
331
- self._render_repo_info_rows(table, infos)
332
+ is_empty = total == 0 and not self._repo_paths and not self._search_query
333
+ self._set_table_empty_state(table, no_msg, is_empty=is_empty)
334
+ table.clear()
335
+ if is_empty:
336
+ self._visible_repo_count = 0
337
+ self._visible_group_count = 0
338
+ else:
339
+ self._render_repo_info_rows(table, infos)
332
340
 
333
341
  if self._resume_selection_tab == "repos":
334
342
  self._restore_resume_selection("repos")
@@ -369,7 +377,7 @@ class ConsoleReposMixin:
369
377
 
370
378
  msg += " ↑↓/jk navigate [enter] actions"
371
379
  if group_count:
372
- msg += " [space] toggle group"
380
+ msg += " [space] toggle"
373
381
  msg += " g git / search s sort r refresh q quit"
374
382
  if self._search_query:
375
383
  msg += " [esc] clear search"
@@ -97,7 +97,6 @@ class ConsoleSessionsMixin:
97
97
  preserved_row_key, preserved_row_index, restore_focus = self._capture_table_selection(
98
98
  table
99
99
  )
100
- table.clear()
101
100
  no_msg = self.query_one("#no-sessions-message", Static)
102
101
 
103
102
  entries = list(self._sessions_entries)
@@ -130,12 +129,10 @@ class ConsoleSessionsMixin:
130
129
  )
131
130
  entries.sort(key=key_func, reverse=self._sessions_sort_reverse)
132
131
 
133
- if not entries and total == 0 and not self._search_query:
134
- table.display = False
135
- no_msg.display = True
136
- else:
137
- table.display = True
138
- no_msg.display = False
132
+ is_empty = not entries and total == 0 and not self._search_query
133
+ self._set_table_empty_state(table, no_msg, is_empty=is_empty)
134
+ table.clear()
135
+ if not is_empty:
139
136
  for entry in entries:
140
137
  status = entry.get("status", "running")
141
138
  description = _wrap_session_description(
@@ -24,6 +24,16 @@ logger = logging.getLogger(__name__)
24
24
 
25
25
 
26
26
  class ConsoleUIHelpersMixin:
27
+ def _set_table_empty_state(
28
+ self,
29
+ table: DataTable,
30
+ empty_message: Static,
31
+ *,
32
+ is_empty: bool,
33
+ ) -> None:
34
+ table.display = not is_empty
35
+ empty_message.display = is_empty
36
+
27
37
  def _compose_status_message(self, message: str) -> str:
28
38
  notice = getattr(self, "_update_notice", None)
29
39
  if not notice: