gitdirector 1.4.5__tar.gz → 1.5.2__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 (49) hide show
  1. {gitdirector-1.4.5 → gitdirector-1.5.2}/PKG-INFO +67 -3
  2. {gitdirector-1.4.5 → gitdirector-1.5.2}/README.md +66 -2
  3. {gitdirector-1.4.5 → gitdirector-1.5.2}/pyproject.toml +1 -1
  4. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/cli.py +2 -0
  5. gitdirector-1.5.2/src/gitdirector/commands/autoclean.py +39 -0
  6. gitdirector-1.5.2/src/gitdirector/commands/gd_tmux.py +79 -0
  7. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/help.py +2 -1
  8. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/__init__.py +12 -5
  9. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app.py +49 -10
  10. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_panels.py +6 -1
  11. gitdirector-1.5.2/src/gitdirector/commands/tui/diff_renderer.py +832 -0
  12. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/panel_view.py +1 -1
  13. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/__init__.py +64 -0
  14. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/_shared.py +115 -0
  15. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/commit.py +474 -0
  16. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/diff.py +793 -0
  17. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/diff_files.py +543 -0
  18. gitdirector-1.4.5/src/gitdirector/commands/tui/screens.py → gitdirector-1.5.2/src/gitdirector/commands/tui/screens/panels.py +146 -863
  19. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/repos.py +549 -0
  20. gitdirector-1.5.2/src/gitdirector/commands/tui/screens/sessions.py +143 -0
  21. gitdirector-1.5.2/src/gitdirector/commands/tui/terminal_caps.py +133 -0
  22. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/terminal_widget.py +28 -13
  23. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/core.py +31 -5
  24. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/monitor.py +14 -5
  25. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/repo.py +112 -0
  26. gitdirector-1.4.5/src/gitdirector/commands/autoclean.py +0 -85
  27. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/__init__.py +0 -0
  28. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/__init__.py +0 -0
  29. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/cd.py +0 -0
  30. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/info.py +0 -0
  31. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/link.py +0 -0
  32. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/listt.py +0 -0
  33. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/pull.py +0 -0
  34. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/status.py +0 -0
  35. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_repos.py +0 -0
  36. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_sessions.py +0 -0
  37. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/app_ui.py +0 -0
  38. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/constants.py +0 -0
  39. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/tui/panels.py +0 -0
  40. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/commands/unlink.py +0 -0
  41. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/config.py +0 -0
  42. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/info.py +0 -0
  43. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/__init__.py +0 -0
  44. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/__init__.py +0 -0
  45. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/integrations/tmux/panels.py +0 -0
  46. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/manager.py +0 -0
  47. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/storage.py +0 -0
  48. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/ui_theme.py +0 -0
  49. {gitdirector-1.4.5 → gitdirector-1.5.2}/src/gitdirector/version_check.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: gitdirector
3
- Version: 1.4.5
3
+ Version: 1.5.2
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
@@ -74,8 +74,9 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
74
74
  | `gitdirector status` | Show repositories with staged/unstaged files |
75
75
  | `gitdirector pull` | Pull latest changes for all tracked repositories |
76
76
  | `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
77
- | `gitdirector autoclean links\|sessions` | Clean broken links or stale tmux sessions |
77
+ | `gitdirector autoclean` | Remove broken repository links from tracking |
78
78
  | `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
79
+ | `gitdirector gd-tmux PATH\|NAME "command"` | Create a gd tmux session and run a command in it |
79
80
  | `gitdirector help` | Show help |
80
81
 
81
82
  ### link
@@ -103,8 +104,9 @@ Features:
103
104
  - `r` to refresh all statuses
104
105
  - Press `enter` on any repository to open an action menu:
105
106
  - **New tmux session** — create and attach a session for the repository
107
+ - **Review Diff** — open a two-pane viewer for uncommitted changes (left: file list with GitHub-style status pills and +/- counts; right: per-file unified diff with GitHub-dark syntax highlighting, line numbers, and green/red backgrounds for added/removed lines). Navigate files with `j`/`k`, `n`/`p`, `g`/`G`, or `]`/`[`. Switch the focused panel with `tab`. `r` refreshes, `esc` closes.
106
108
  - **Attach existing session** — switch to any already-running tmux session
107
- - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, or Codex in a new tmux session
109
+ - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
108
110
  - **Remove session** — kill a running tmux session
109
111
 
110
112
  ### unlink
@@ -174,6 +176,68 @@ Opens a [tmux](https://github.com/tmux/tmux) session rooted at the repository di
174
176
  > Debian/Ubuntu: `sudo apt install tmux`
175
177
  > Arch: `sudo pacman -S tmux`
176
178
 
179
+ ### gd-tmux
180
+
181
+ ```bash
182
+ gitdirector gd-tmux my-repo "npm test"
183
+ gitdirector gd-tmux /path/to/my-repo "make build"
184
+ gitdirector gd-tmux my-repo 'echo "hello world"'
185
+ ```
186
+
187
+ Creates a new gd tmux session rooted at the given repository, runs the supplied command inside it, and attaches the session. The session is created using the same code path the TUI's **Launch AI agent** menu uses: it is a real `gd/{repo}/{command}/{N}` session, visible in the Sessions tab of `gitdirector console`, and it self-destructs when the command exits.
188
+
189
+ - `PATH|NAME` — accept either an absolute path to a tracked repository or its basename, with the same disambiguation rules as `gitdirector unlink`.
190
+ - `"command"` — the shell command to run inside the new session. Pass it quoted so the shell hands the whole string to gitdirector; quotes and arguments are preserved verbatim.
191
+
192
+ The command is the same as opening the repo in the console, picking **Launch AI agent** from the action menu, and entering the command — but you can do it from any shell without entering the TUI first. The new session shows up in the Sessions tab immediately.
193
+
194
+ ### autoclean
195
+
196
+ ```bash
197
+ gitdirector autoclean
198
+ ```
199
+
200
+ Scans the tracked repositories in `~/.gitdirector/config.yaml` for paths that no longer exist on disk and removes them from the config. Lists any broken links for review and asks for confirmation before deleting them.
201
+
202
+ ## Note for AI coding agents
203
+
204
+ If you are an AI coding agent reading this project: when the user asks you to run a long-lived process inside a tracked repository — a dev server, a frontend bundler, a backend API, a watcher, a database, a test runner with a long timeout, or anything else that does not exit on its own — **do not run it in the foreground**. A foreground process will block your shell for the rest of the session and the user will not be able to interact with you.
205
+
206
+ Use `gitdirector gd-tmux` to launch the process in a detached gd tmux session that the user can monitor and tear down themselves. The session shows up in the Sessions tab of `gitdirector console`, auto-cleans up when the process exits, and supports path-or-name lookup the same way every other gitdirector command does.
207
+
208
+ **Always prefer the absolute path over the repo name.** The by-name lookup matches the *directory name on disk* verbatim (via `Path.name`) — case-sensitive, dots and spaces included — and is ambiguous if two tracked repos share the same basename. When in doubt, look up the path with `gitdirector list` and use the absolute path. Quoting a path is fine; an absolute path is never ambiguous.
209
+
210
+ Examples:
211
+
212
+ ```bash
213
+ # Frontend dev server in the user's web repo — use the absolute path
214
+ gitdirector gd-tmux /Users/me/projects/webapp "npm run dev"
215
+
216
+ # Backend API in the api repo
217
+ gitdirector gd-tmux /Users/me/projects/api "uvicorn app.main:app --reload --port 8000"
218
+
219
+ # A long-running integration test with output captured to a log
220
+ gitdirector gd-tmux /Users/me/projects/myrepo "pytest -x -v 2>&1 | tee /tmp/test.log"
221
+
222
+ # A watcher or build loop
223
+ gitdirector gd-tmux /Users/me/projects/myrepo "npm run watch"
224
+
225
+ # A repo whose directory name contains a space — quote both the path and the command
226
+ gitdirector gd-tmux "/Users/me/projects/My Repo" "npm run dev"
227
+ ```
228
+
229
+ The by-name form is supported for ergonomic use by humans in interactive shells, but agents should default to the absolute path to avoid any chance of matching the wrong repo.
230
+
231
+ Rules of thumb:
232
+
233
+ - **Use the absolute path.** It is unambiguous and survives the user renaming or moving the directory. The by-name form is convenient shorthand, not a slug — it is the raw directory name, case-sensitive.
234
+ - **Repo names with spaces, dots, or punctuation are matched verbatim** when you do use the by-name form. A repo at `/Users/me/My Repo` is matched by `"My Repo"`, not by `My` or `Repo`. Quote the name to keep the shell from splitting it.
235
+ - **Quoting the command matters.** The command string is handed to `sh -lc` inside the new session, so write it as a single quoted string. `'echo "hi"'` (single-quote outer, double-quote inner) is the safest pattern for embedded double quotes.
236
+ - **Path separators in the command are fine.** Commands like `python /path/to/script.py` or `find . -name foo/bar` work; the command is embedded verbatim into the inner shell, while the session-name label is auto-sanitized.
237
+ - **The session self-destructs** when the command exits (success or failure), so you do not need to clean it up.
238
+ - **If you need to capture output**, redirect it to a file in the command itself (`> /tmp/out 2>&1`) and read the file later — the session is gone once the command exits.
239
+ - **If the user asks you to stop a running process**, kill the matching session: `gitdirector console` → Sessions tab, or directly with `tmux kill-session -t =<session-name>`.
240
+
177
241
  ## Configuration
178
242
 
179
243
  Config is stored at `~/.gitdirector/config.yaml`.
@@ -31,8 +31,9 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
31
31
  | `gitdirector status` | Show repositories with staged/unstaged files |
32
32
  | `gitdirector pull` | Pull latest changes for all tracked repositories |
33
33
  | `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
34
- | `gitdirector autoclean links\|sessions` | Clean broken links or stale tmux sessions |
34
+ | `gitdirector autoclean` | Remove broken repository links from tracking |
35
35
  | `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
36
+ | `gitdirector gd-tmux PATH\|NAME "command"` | Create a gd tmux session and run a command in it |
36
37
  | `gitdirector help` | Show help |
37
38
 
38
39
  ### link
@@ -60,8 +61,9 @@ Features:
60
61
  - `r` to refresh all statuses
61
62
  - Press `enter` on any repository to open an action menu:
62
63
  - **New tmux session** — create and attach a session for the repository
64
+ - **Review Diff** — open a two-pane viewer for uncommitted changes (left: file list with GitHub-style status pills and +/- counts; right: per-file unified diff with GitHub-dark syntax highlighting, line numbers, and green/red backgrounds for added/removed lines). Navigate files with `j`/`k`, `n`/`p`, `g`/`G`, or `]`/`[`. Switch the focused panel with `tab`. `r` refreshes, `esc` closes.
63
65
  - **Attach existing session** — switch to any already-running tmux session
64
- - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, or Codex in a new tmux session
66
+ - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
65
67
  - **Remove session** — kill a running tmux session
66
68
 
67
69
  ### unlink
@@ -131,6 +133,68 @@ Opens a [tmux](https://github.com/tmux/tmux) session rooted at the repository di
131
133
  > Debian/Ubuntu: `sudo apt install tmux`
132
134
  > Arch: `sudo pacman -S tmux`
133
135
 
136
+ ### gd-tmux
137
+
138
+ ```bash
139
+ gitdirector gd-tmux my-repo "npm test"
140
+ gitdirector gd-tmux /path/to/my-repo "make build"
141
+ gitdirector gd-tmux my-repo 'echo "hello world"'
142
+ ```
143
+
144
+ Creates a new gd tmux session rooted at the given repository, runs the supplied command inside it, and attaches the session. The session is created using the same code path the TUI's **Launch AI agent** menu uses: it is a real `gd/{repo}/{command}/{N}` session, visible in the Sessions tab of `gitdirector console`, and it self-destructs when the command exits.
145
+
146
+ - `PATH|NAME` — accept either an absolute path to a tracked repository or its basename, with the same disambiguation rules as `gitdirector unlink`.
147
+ - `"command"` — the shell command to run inside the new session. Pass it quoted so the shell hands the whole string to gitdirector; quotes and arguments are preserved verbatim.
148
+
149
+ The command is the same as opening the repo in the console, picking **Launch AI agent** from the action menu, and entering the command — but you can do it from any shell without entering the TUI first. The new session shows up in the Sessions tab immediately.
150
+
151
+ ### autoclean
152
+
153
+ ```bash
154
+ gitdirector autoclean
155
+ ```
156
+
157
+ Scans the tracked repositories in `~/.gitdirector/config.yaml` for paths that no longer exist on disk and removes them from the config. Lists any broken links for review and asks for confirmation before deleting them.
158
+
159
+ ## Note for AI coding agents
160
+
161
+ If you are an AI coding agent reading this project: when the user asks you to run a long-lived process inside a tracked repository — a dev server, a frontend bundler, a backend API, a watcher, a database, a test runner with a long timeout, or anything else that does not exit on its own — **do not run it in the foreground**. A foreground process will block your shell for the rest of the session and the user will not be able to interact with you.
162
+
163
+ Use `gitdirector gd-tmux` to launch the process in a detached gd tmux session that the user can monitor and tear down themselves. The session shows up in the Sessions tab of `gitdirector console`, auto-cleans up when the process exits, and supports path-or-name lookup the same way every other gitdirector command does.
164
+
165
+ **Always prefer the absolute path over the repo name.** The by-name lookup matches the *directory name on disk* verbatim (via `Path.name`) — case-sensitive, dots and spaces included — and is ambiguous if two tracked repos share the same basename. When in doubt, look up the path with `gitdirector list` and use the absolute path. Quoting a path is fine; an absolute path is never ambiguous.
166
+
167
+ Examples:
168
+
169
+ ```bash
170
+ # Frontend dev server in the user's web repo — use the absolute path
171
+ gitdirector gd-tmux /Users/me/projects/webapp "npm run dev"
172
+
173
+ # Backend API in the api repo
174
+ gitdirector gd-tmux /Users/me/projects/api "uvicorn app.main:app --reload --port 8000"
175
+
176
+ # A long-running integration test with output captured to a log
177
+ gitdirector gd-tmux /Users/me/projects/myrepo "pytest -x -v 2>&1 | tee /tmp/test.log"
178
+
179
+ # A watcher or build loop
180
+ gitdirector gd-tmux /Users/me/projects/myrepo "npm run watch"
181
+
182
+ # A repo whose directory name contains a space — quote both the path and the command
183
+ gitdirector gd-tmux "/Users/me/projects/My Repo" "npm run dev"
184
+ ```
185
+
186
+ The by-name form is supported for ergonomic use by humans in interactive shells, but agents should default to the absolute path to avoid any chance of matching the wrong repo.
187
+
188
+ Rules of thumb:
189
+
190
+ - **Use the absolute path.** It is unambiguous and survives the user renaming or moving the directory. The by-name form is convenient shorthand, not a slug — it is the raw directory name, case-sensitive.
191
+ - **Repo names with spaces, dots, or punctuation are matched verbatim** when you do use the by-name form. A repo at `/Users/me/My Repo` is matched by `"My Repo"`, not by `My` or `Repo`. Quote the name to keep the shell from splitting it.
192
+ - **Quoting the command matters.** The command string is handed to `sh -lc` inside the new session, so write it as a single quoted string. `'echo "hi"'` (single-quote outer, double-quote inner) is the safest pattern for embedded double quotes.
193
+ - **Path separators in the command are fine.** Commands like `python /path/to/script.py` or `find . -name foo/bar` work; the command is embedded verbatim into the inner shell, while the session-name label is auto-sanitized.
194
+ - **The session self-destructs** when the command exits (success or failure), so you do not need to clean it up.
195
+ - **If you need to capture output**, redirect it to a file in the command itself (`> /tmp/out 2>&1`) and read the file later — the session is gone once the command exits.
196
+ - **If the user asks you to stop a running process**, kill the matching session: `gitdirector console` → Sessions tab, or directly with `tmux kill-session -t =<session-name>`.
197
+
134
198
  ## Configuration
135
199
 
136
200
  Config is stored at `~/.gitdirector/config.yaml`.
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "gitdirector"
7
- version = "1.4.5"
7
+ version = "1.5.2"
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" }
@@ -10,6 +10,7 @@ from .commands import (
10
10
  autoclean,
11
11
  cd,
12
12
  console,
13
+ gd_tmux,
13
14
  help,
14
15
  info,
15
16
  link,
@@ -55,6 +56,7 @@ help.register(cli)
55
56
  tui.register(cli)
56
57
  autoclean.register(cli)
57
58
  info.register(cli)
59
+ gd_tmux.register(cli)
58
60
 
59
61
 
60
62
  def main():
@@ -0,0 +1,39 @@
1
+ import click
2
+
3
+ from ..config import Config
4
+ from . import console
5
+
6
+
7
+ def register(cli: click.Group):
8
+ @cli.command()
9
+ def autoclean():
10
+ _autoclean_links()
11
+
12
+
13
+ def _autoclean_links():
14
+ config = Config()
15
+ broken = [p for p in config.repositories if not p.exists()]
16
+
17
+ if not broken:
18
+ console.print()
19
+ console.print(" [green]All links are valid.[/green]")
20
+ console.print()
21
+ return
22
+
23
+ console.print()
24
+ console.print(f" Found [yellow]{len(broken)}[/yellow] broken link(s):\n")
25
+ for p in broken:
26
+ console.print(f" [red]✕[/red] {p}", soft_wrap=True)
27
+ console.print()
28
+
29
+ if not click.confirm(" Remove these broken links?"):
30
+ console.print()
31
+ console.print(" [dim]Cancelled.[/dim]")
32
+ console.print()
33
+ return
34
+
35
+ config.remove_repositories(broken)
36
+
37
+ console.print()
38
+ console.print(f" [green]Removed {len(broken)} broken link(s).[/green]")
39
+ console.print()
@@ -0,0 +1,79 @@
1
+ from pathlib import Path
2
+
3
+ import click
4
+
5
+ from ..manager import RepositoryManager
6
+ from ..storage import normalize_repository_path
7
+ from . import console
8
+
9
+
10
+ def _resolve_repo(target: str) -> Path | None:
11
+ """Resolve *target* to a tracked repository path.
12
+
13
+ Tries the value as a filesystem path first. If no path-based match is
14
+ found and the value does not look path-like, falls back to looking it up
15
+ by repository name. Prints a rich error and returns ``None`` on missing
16
+ or ambiguous targets.
17
+ """
18
+ manager = RepositoryManager()
19
+ candidate = Path(target)
20
+ is_path_like = (
21
+ "/" in target
22
+ or "\\" in target
23
+ or target in (".", "..")
24
+ or candidate.is_absolute()
25
+ or target.startswith("~")
26
+ or candidate.exists()
27
+ )
28
+
29
+ if is_path_like:
30
+ normalized = normalize_repository_path(candidate)
31
+ if manager.config.has_repository(normalized):
32
+ return normalized
33
+ console.print(f"\n [red]No tracked repository at path: {normalized}[/red]\n")
34
+ return None
35
+
36
+ matches = [r for r in manager.config.repositories if r.name == target]
37
+ if not matches:
38
+ console.print(f"\n [red]No tracked repository named: {target}[/red]\n")
39
+ return None
40
+ if len(matches) > 1:
41
+ paths_list = "\n".join(f" {p}" for p in matches)
42
+ console.print(
43
+ f"\n [red]Multiple repositories named '{target}' — use the full path:[/red]\n"
44
+ f"{paths_list}\n"
45
+ )
46
+ return None
47
+ return matches[0]
48
+
49
+
50
+ def register(cli: click.Group):
51
+ @cli.command()
52
+ @click.argument("target", metavar="PATH|NAME")
53
+ @click.argument("command")
54
+ def gd_tmux(target: str, command: str):
55
+ """Create a gd tmux session for a repository and run a command in it."""
56
+ if not command.strip():
57
+ console.print("\n [red]Command must not be empty.[/red]\n")
58
+ raise SystemExit(1)
59
+
60
+ repo_path = _resolve_repo(target)
61
+ if repo_path is None:
62
+ raise SystemExit(1)
63
+
64
+ try:
65
+ from ..integrations.tmux import (
66
+ attach_tmux_session,
67
+ create_tmux_session,
68
+ launch_command_in_tmux_session,
69
+ )
70
+ except ImportError:
71
+ console.print(
72
+ "\n [red]The tmux integration is unavailable for the gd-tmux command.[/red]\n"
73
+ " Reinstall gitdirector or check your installation.\n"
74
+ )
75
+ raise SystemExit(1)
76
+
77
+ session_name = create_tmux_session(repo_path.name, repo_path, purpose=command)
78
+ launch_command_in_tmux_session(session_name, command)
79
+ attach_tmux_session(session_name, skip_config_sync=True)
@@ -32,8 +32,9 @@ def show_help():
32
32
  ("pull", "Pull latest changes for all tracked repositories"),
33
33
  ("cd NAME", "Open or switch to a tmux session for a repository"),
34
34
  ("console", "Interactive TUI for browsing and opening repositories"),
35
- ("autoclean links|sessions", "Clean broken links or stale tmux sessions"),
35
+ ("autoclean", "Remove broken repository links from tracking"),
36
36
  ("info PATH|NAME [--full]", "Show file statistics for a repository"),
37
+ ('gd-tmux PATH|NAME "cmd"', "Create a gd tmux session and run a command in it"),
37
38
  ("help", "Show this help message"),
38
39
  ]:
39
40
  cmd_table.add_row(cmd, desc)
@@ -14,20 +14,24 @@ from .constants import (
14
14
  )
15
15
  from .panel_view import PanelViewScreen, PaneWidget
16
16
  from .panels import Panel, PanelStore
17
- from .screens import (
18
- ActionMenuScreen,
17
+ from .screens import SortMenuScreen
18
+ from .screens.diff import DiffReviewScreen
19
+ from .screens.panels import (
19
20
  AgentLoadingScreen,
20
21
  ConfirmScreen,
21
22
  CreatePanelScreen,
23
+ PanelActionMenuScreen,
24
+ RenamePanelScreen,
25
+ )
26
+ from .screens.repos import (
27
+ ActionMenuScreen,
22
28
  GitCommandResultScreen,
23
29
  GitOperationsMenuScreen,
24
30
  PullLoadingScreen,
25
31
  PullResultScreen,
26
- RemoveSessionScreen,
27
32
  RepoInfoScreen,
28
- SelectSessionScreen,
29
- SortMenuScreen,
30
33
  )
34
+ from .screens.sessions import RemoveSessionScreen, SelectSessionScreen
31
35
  from .terminal_widget import TerminalWidget
32
36
 
33
37
  __all__ = [
@@ -35,18 +39,21 @@ __all__ = [
35
39
  "AgentLoadingScreen",
36
40
  "ConfirmScreen",
37
41
  "CreatePanelScreen",
42
+ "DiffReviewScreen",
38
43
  "GitCommandResultScreen",
39
44
  "GitOperationsMenuScreen",
40
45
  "_DEFAULT_PANELS_SORT_COLUMN",
41
46
  "GitDirectorConsole",
42
47
  "PaneWidget",
43
48
  "Panel",
49
+ "PanelActionMenuScreen",
44
50
  "PanelStore",
45
51
  "PanelViewScreen",
46
52
  "_PANELS_SORT_COLUMN_NAMES",
47
53
  "PullLoadingScreen",
48
54
  "PullResultScreen",
49
55
  "RemoveSessionScreen",
56
+ "RenamePanelScreen",
50
57
  "RepoInfoScreen",
51
58
  "SelectSessionScreen",
52
59
  "SortMenuScreen",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import logging
5
6
  from concurrent.futures import ThreadPoolExecutor
6
7
  from pathlib import Path
7
8
  from typing import Callable
@@ -31,17 +32,17 @@ from .constants import (
31
32
  _SESSION_STATUS_POLL_INTERVAL_SECS,
32
33
  )
33
34
  from .panels import Panel, PanelStore
34
- from .screens import (
35
+ from .screens.diff import DiffReviewScreen
36
+ from .screens.panels import AgentLoadingScreen, ConfirmScreen
37
+ from .screens.repos import (
35
38
  ActionMenuScreen,
36
- AgentLoadingScreen,
37
- ConfirmScreen,
38
39
  GitCommandResultScreen,
39
40
  GitOperationsMenuScreen,
40
41
  PullLoadingScreen,
41
42
  PullResultScreen,
42
- RemoveSessionScreen,
43
43
  RepoInfoScreen,
44
44
  )
45
+ from .screens.sessions import RemoveSessionScreen
45
46
 
46
47
  _panel_row_height = _app_panels._panel_row_height
47
48
  _render_panel_preview = _app_panels._render_panel_preview
@@ -55,6 +56,9 @@ __all__ = [
55
56
  ]
56
57
 
57
58
 
59
+ logger = logging.getLogger(__name__)
60
+
61
+
58
62
  class GitDirectorConsole(
59
63
  ConsolePanelsMixin,
60
64
  ConsoleSessionsMixin,
@@ -355,25 +359,32 @@ class GitDirectorConsole(
355
359
  if path is None:
356
360
  return
357
361
 
358
- from ...integrations.tmux import create_tmux_session, launch_agent_in_tmux_session
362
+ from ...integrations.tmux import create_tmux_session, launch_command_in_tmux_session
359
363
 
360
364
  purpose = agent_cmd if agent_cmd else "shell"
361
365
  session_name = create_tmux_session(path.name, path, purpose=purpose)
362
366
 
363
367
  if agent_cmd:
364
- ready_marker = launch_agent_in_tmux_session(session_name, agent_cmd)
368
+ ready_marker = launch_command_in_tmux_session(session_name, agent_cmd)
365
369
  self.push_screen(
366
370
  AgentLoadingScreen(agent_cmd, session_name, ready_marker),
367
371
  callback=lambda _: self.set_timer(0.2, lambda: self._refresh_repo_for_path(path)),
368
372
  )
369
373
  else:
370
- self._suspend_and_attach(session_name, path)
374
+ # create_tmux_session already called sync_panel_tmux_config, so skip
375
+ # the redundant sync in attach_tmux_session. Without this skip the
376
+ # sync re-lists every tmux session, rewrites gd-tmux.conf, and runs
377
+ # `tmux source-file` between the manual screen clear and tmux's
378
+ # first redraw — long enough to expose a visible empty alt-screen.
379
+ self._suspend_and_attach(session_name, path, skip_config_sync=True)
371
380
 
372
381
  def _suspend_and_attach(
373
382
  self,
374
383
  session_name: str,
375
384
  path: Path | None = None,
376
385
  row_key: str | None = None,
386
+ *,
387
+ skip_config_sync: bool = False,
377
388
  ) -> None:
378
389
  """Suspend the TUI and attach to the tmux session."""
379
390
  import sys
@@ -402,7 +413,7 @@ class GitDirectorConsole(
402
413
  with self.suspend():
403
414
  sys.stdout.write("\033[?1049h\033[H\033[2J\033[?25l")
404
415
  sys.stdout.flush()
405
- attach_tmux_session(session_name)
416
+ attach_tmux_session(session_name, skip_config_sync=skip_config_sync)
406
417
  sys.stdout.write("\033[?25h")
407
418
  sys.stdout.flush()
408
419
  try:
@@ -650,10 +661,27 @@ class GitDirectorConsole(
650
661
 
651
662
  from ..pull import pull_repository
652
663
 
653
- result = pull_repository(path)
664
+ try:
665
+ result = pull_repository(path)
666
+ except Exception as exc:
667
+ logger.exception("pull worker crashed")
668
+ error_result = (path.name, False, f"Pull failed: {exc}")
669
+ if self._background_shutdown_requested(worker):
670
+ return
671
+ try:
672
+ self.call_from_thread(
673
+ self._show_pull_result, loading_screen, path, command, error_result
674
+ )
675
+ except Exception:
676
+ logger.debug("Failed to post pull error to UI", exc_info=True)
677
+ return
678
+
654
679
  if self._background_shutdown_requested(worker):
655
680
  return
656
- self.call_from_thread(self._show_pull_result, loading_screen, path, command, result)
681
+ try:
682
+ self.call_from_thread(self._show_pull_result, loading_screen, path, command, result)
683
+ except Exception:
684
+ logger.debug("Failed to post pull result to UI", exc_info=True)
657
685
 
658
686
  def _show_pull_result(
659
687
  self,
@@ -677,6 +705,7 @@ class GitDirectorConsole(
677
705
  "agent:claude": "claude",
678
706
  "agent:copilot": "copilot",
679
707
  "agent:codex": "codex",
708
+ "agent:pi": "pi",
680
709
  }
681
710
 
682
711
  def _handle_menu_action(self, action: str | None) -> None:
@@ -690,6 +719,8 @@ class GitDirectorConsole(
690
719
  session_name = action[len("attach:") :]
691
720
  path = self._get_selected_path()
692
721
  self._attach_to_session(session_name, path)
722
+ elif action == "review_diff":
723
+ self._open_review_diff()
693
724
  elif action == "remove_session":
694
725
  path = self._get_selected_path()
695
726
  if path:
@@ -698,6 +729,14 @@ class GitDirectorConsole(
698
729
  callback=self._handle_remove_selection,
699
730
  )
700
731
 
732
+ def _open_review_diff(self) -> None:
733
+ path = self._get_selected_path()
734
+ if path is None:
735
+ return
736
+ info = self._results.get(str(path))
737
+ branch = info.branch if info else None
738
+ self.push_screen(DiffReviewScreen(path.name, path, branch=branch))
739
+
701
740
  def _handle_remove_selection(self, session_name: str | None) -> None:
702
741
  if session_name is None:
703
742
  return
@@ -12,7 +12,12 @@ from .constants import (
12
12
  _PANELS_SORT_COLUMN_NAMES,
13
13
  )
14
14
  from .panels import Panel, render_panel_layout_preview
15
- from .screens import ConfirmScreen, CreatePanelScreen, PanelActionMenuScreen, RenamePanelScreen
15
+ from .screens.panels import (
16
+ ConfirmScreen,
17
+ CreatePanelScreen,
18
+ PanelActionMenuScreen,
19
+ RenamePanelScreen,
20
+ )
16
21
 
17
22
  _PANEL_PREVIEW_FILLED = "■"
18
23
  _PANEL_PREVIEW_OPEN = "□"