gitdirector 1.7.0__tar.gz → 1.7.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 (58) hide show
  1. {gitdirector-1.7.0 → gitdirector-1.7.2}/PKG-INFO +27 -6
  2. {gitdirector-1.7.0 → gitdirector-1.7.2}/README.md +26 -5
  3. {gitdirector-1.7.0 → gitdirector-1.7.2}/pyproject.toml +1 -1
  4. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/cli.py +2 -0
  5. gitdirector-1.7.2/src/gitdirector/commands/completion.py +74 -0
  6. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/help.py +4 -2
  7. gitdirector-1.7.2/src/gitdirector/commands/reset.py +82 -0
  8. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app.py +48 -8
  9. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app_sessions.py +1 -0
  10. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app_ui.py +2 -0
  11. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/commit.py +38 -13
  12. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/diff.py +1 -0
  13. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/panels.py +1 -1
  14. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/repos.py +7 -7
  15. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/terminal_caps.py +4 -0
  16. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/terminal_widget.py +337 -20
  17. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/config.py +85 -48
  18. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/integrations/tmux/core.py +121 -10
  19. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/integrations/tmux/monitor.py +2 -1
  20. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/integrations/tmux/panels.py +78 -20
  21. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/repo.py +4 -0
  22. gitdirector-1.7.0/src/gitdirector/commands/completion.py +0 -41
  23. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/__init__.py +0 -0
  24. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/__init__.py +0 -0
  25. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/autoclean.py +0 -0
  26. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/capture.py +0 -0
  27. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/cd.py +0 -0
  28. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/gd_send.py +0 -0
  29. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/gd_tmux.py +0 -0
  30. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/info.py +0 -0
  31. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/link.py +0 -0
  32. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/listt.py +0 -0
  33. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/pull.py +0 -0
  34. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/status.py +0 -0
  35. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/__init__.py +0 -0
  36. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app_groups.py +0 -0
  37. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app_panels.py +0 -0
  38. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/app_repos.py +0 -0
  39. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/constants.py +0 -0
  40. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/diff_renderer.py +0 -0
  41. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/panel_view.py +0 -0
  42. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/panels.py +0 -0
  43. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/__init__.py +0 -0
  44. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/_shared.py +0 -0
  45. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/diff_files.py +0 -0
  46. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/groups.py +0 -0
  47. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/session_actions.py +0 -0
  48. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/screens/sessions.py +0 -0
  49. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/tui/table_text.py +0 -0
  50. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/commands/unlink.py +0 -0
  51. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/github_credential_helper.py +0 -0
  52. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/info.py +0 -0
  53. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/integrations/__init__.py +0 -0
  54. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/integrations/tmux/__init__.py +0 -0
  55. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/manager.py +0 -0
  56. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/storage.py +0 -0
  57. {gitdirector-1.7.0 → gitdirector-1.7.2}/src/gitdirector/ui_theme.py +0 -0
  58. {gitdirector-1.7.0 → gitdirector-1.7.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.7.0
3
+ Version: 1.7.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,6 +74,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
74
74
  | `gitdirector pull [--yes]` | Pull latest changes for all tracked repositories |
75
75
  | `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
76
76
  | `gitdirector autoclean` | Remove broken repository links from tracking |
77
+ | `gitdirector reset [--yes]` | Kill all sessions and panels, wipe ~/.gitdirector, and recreate the config |
77
78
  | `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
78
79
  | `gitdirector gd-tmux PATH\|NAME "command" [--description TEXT]` | Create a gd tmux session and run a command in it |
79
80
  | `gitdirector gd-capture SESSION [--lines N] [--full]` | Print the scrollback of a live gd tmux session |
@@ -104,17 +105,17 @@ Features:
104
105
  - `/` to filter the active tab
105
106
  - `s` to sort the active table
106
107
  - `i` to show repository info (file count, lines, tokens, max depth, top file types)
107
- - `g` on the Repositories tab to open Git operations: status, timeline, branches, remotes, pull, and push
108
+ - `g` on the Repositories tab to open Git operations: status, timeline, branches, remotes, pull, push, and a **Diff Viewer** section with **Review Diff** for uncommitted changes
108
109
  - `r` to refresh all statuses
109
110
  - Press `enter` on any repository to open an action menu:
110
111
  - **New tmux session** — create and attach a session for the repository
111
- - **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.
112
112
  - **Attach existing session** — switch to any already-running tmux session
113
113
  - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
114
114
  - **Remove session** — kill a running tmux session
115
+ - **Review Diff** (from the `g` Git menu) — 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.
115
116
  - **Repository groups** appear directly in the Repositories tab when linked repositories share the same parent directory. Group rows can be expanded/collapsed with `space`; press `enter` on a group to start, attach, remove, or launch AI-agent sessions rooted at that parent directory.
116
117
  - **Sessions tab** (press `2`) lists every active `gd/*` tmux session with its status, purpose, repository, **description**, and full session name. The description column is free-form text stored on the session (default `"-"`), with a width that scales to your terminal and wraps long text. Highlight a row and press `d` to edit its description.
117
- - **Panels tab** (press `3`) manages reusable tmux panel layouts. Press `n` to create a panel, or `enter` on a panel to open, reconfigure, rename, or delete it.
118
+ - **Panels tab** (press `3`) manages reusable tmux panel layouts. Press `n` to create a panel, or `enter` on a panel to open, reconfigure, rename, or delete it. Inside a panel you can **drag with the mouse to select text and copy it to the OS clipboard** on release; pressing `y` copies the entire visible pane as a fallback. GitDirector prefers native clipboard tools such as `pbcopy`, `wl-copy`, `xclip`, `xsel`, or `clip.exe`, and falls back to OSC 52 when needed.
118
119
 
119
120
  ### unlink
120
121
 
@@ -223,6 +224,15 @@ gitdirector autoclean
223
224
 
224
225
  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.
225
226
 
227
+ ### reset
228
+
229
+ ```bash
230
+ gitdirector reset # asks for confirmation
231
+ gitdirector reset --yes # skip the confirmation prompt
232
+ ```
233
+
234
+ Destructive cleanup. Kills every `gd/*` tmux session (regular sessions and panels), wipes the entire `~/.gitdirector/` directory, and recreates an empty `config.yaml` with defaults. Useful when the local state is corrupted or you want to start fresh — all linked repositories, panel layouts, version-check cache, and tmux theme files are removed in one step.
235
+
226
236
  ### gd-capture
227
237
 
228
238
  ```bash
@@ -263,7 +273,7 @@ If you are an AI coding agent (Claude Code, OpenCode, GitHub Copilot, Codex, Pi,
263
273
 
264
274
  ## Configuration
265
275
 
266
- Config is stored at `~/.gitdirector/config.yaml`.
276
+ Main config is stored at `~/.gitdirector/config.yaml`:
267
277
 
268
278
  ```yaml
269
279
  repositories:
@@ -271,6 +281,11 @@ repositories:
271
281
  - /path/to/repo2
272
282
  max_workers: 10 # optional, valid range 1-32, default 10
273
283
  theme: rose-pine # optional, default rose-pine
284
+ ```
285
+
286
+ Secrets (GitHub credentials) are stored in a separate file at `~/.gitdirector/secrets.yaml`:
287
+
288
+ ```yaml
274
289
  github_username: your-github-username # optional
275
290
  github_PAT: github_pat_or_classic_pat # optional
276
291
  ```
@@ -281,7 +296,7 @@ GitDirector first runs git commands using your normal local git credentials. If
281
296
 
282
297
  - This only applies to HTTPS GitHub remotes.
283
298
  - SSH remotes still use your normal SSH key/agent setup.
284
- - The PAT is stored in plaintext in `~/.gitdirector/config.yaml`; use an appropriately scoped token and protect that file.
299
+ - The PAT is stored in plaintext in `~/.gitdirector/secrets.yaml`; use an appropriately scoped token and protect that file.
285
300
  - The PAT is not passed on the command line or shown in TUI command output.
286
301
 
287
302
  ### Available Themes
@@ -304,6 +319,12 @@ eval "$(gitdirector completion zsh)"
304
319
  gitdirector completion fish | source
305
320
  ```
306
321
 
322
+ The `zsh` script auto-loads `compinit` if `compdef` is not yet available, so the `eval` form works in fresh shells. For the fastest setup (no subprocess on every `TAB`), write the script to a file in your `$fpath` (commonly `~/.zsh/completions/_gitdirector`) and let `compinit` autoload it:
323
+
324
+ ```bash
325
+ gitdirector completion zsh > "${fpath[1]}/_gitdirector"
326
+ ```
327
+
307
328
  ## License
308
329
 
309
330
  MIT
@@ -32,6 +32,7 @@ If you find GitDirector useful, please star this repository on GitHub, we need m
32
32
  | `gitdirector pull [--yes]` | Pull latest changes for all tracked repositories |
33
33
  | `gitdirector cd NAME` | Open or switch to a tmux session for a repository |
34
34
  | `gitdirector autoclean` | Remove broken repository links from tracking |
35
+ | `gitdirector reset [--yes]` | Kill all sessions and panels, wipe ~/.gitdirector, and recreate the config |
35
36
  | `gitdirector info PATH\|NAME [--full]` | Show file statistics for a repository |
36
37
  | `gitdirector gd-tmux PATH\|NAME "command" [--description TEXT]` | Create a gd tmux session and run a command in it |
37
38
  | `gitdirector gd-capture SESSION [--lines N] [--full]` | Print the scrollback of a live gd tmux session |
@@ -62,17 +63,17 @@ Features:
62
63
  - `/` to filter the active tab
63
64
  - `s` to sort the active table
64
65
  - `i` to show repository info (file count, lines, tokens, max depth, top file types)
65
- - `g` on the Repositories tab to open Git operations: status, timeline, branches, remotes, pull, and push
66
+ - `g` on the Repositories tab to open Git operations: status, timeline, branches, remotes, pull, push, and a **Diff Viewer** section with **Review Diff** for uncommitted changes
66
67
  - `r` to refresh all statuses
67
68
  - Press `enter` on any repository to open an action menu:
68
69
  - **New tmux session** — create and attach a session for the repository
69
- - **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.
70
70
  - **Attach existing session** — switch to any already-running tmux session
71
71
  - **Launch AI agent** — open OpenCode, Claude Code, GitHub Copilot, Codex, or Pi in a new tmux session
72
72
  - **Remove session** — kill a running tmux session
73
+ - **Review Diff** (from the `g` Git menu) — 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.
73
74
  - **Repository groups** appear directly in the Repositories tab when linked repositories share the same parent directory. Group rows can be expanded/collapsed with `space`; press `enter` on a group to start, attach, remove, or launch AI-agent sessions rooted at that parent directory.
74
75
  - **Sessions tab** (press `2`) lists every active `gd/*` tmux session with its status, purpose, repository, **description**, and full session name. The description column is free-form text stored on the session (default `"-"`), with a width that scales to your terminal and wraps long text. Highlight a row and press `d` to edit its description.
75
- - **Panels tab** (press `3`) manages reusable tmux panel layouts. Press `n` to create a panel, or `enter` on a panel to open, reconfigure, rename, or delete it.
76
+ - **Panels tab** (press `3`) manages reusable tmux panel layouts. Press `n` to create a panel, or `enter` on a panel to open, reconfigure, rename, or delete it. Inside a panel you can **drag with the mouse to select text and copy it to the OS clipboard** on release; pressing `y` copies the entire visible pane as a fallback. GitDirector prefers native clipboard tools such as `pbcopy`, `wl-copy`, `xclip`, `xsel`, or `clip.exe`, and falls back to OSC 52 when needed.
76
77
 
77
78
  ### unlink
78
79
 
@@ -181,6 +182,15 @@ gitdirector autoclean
181
182
 
182
183
  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.
183
184
 
185
+ ### reset
186
+
187
+ ```bash
188
+ gitdirector reset # asks for confirmation
189
+ gitdirector reset --yes # skip the confirmation prompt
190
+ ```
191
+
192
+ Destructive cleanup. Kills every `gd/*` tmux session (regular sessions and panels), wipes the entire `~/.gitdirector/` directory, and recreates an empty `config.yaml` with defaults. Useful when the local state is corrupted or you want to start fresh — all linked repositories, panel layouts, version-check cache, and tmux theme files are removed in one step.
193
+
184
194
  ### gd-capture
185
195
 
186
196
  ```bash
@@ -221,7 +231,7 @@ If you are an AI coding agent (Claude Code, OpenCode, GitHub Copilot, Codex, Pi,
221
231
 
222
232
  ## Configuration
223
233
 
224
- Config is stored at `~/.gitdirector/config.yaml`.
234
+ Main config is stored at `~/.gitdirector/config.yaml`:
225
235
 
226
236
  ```yaml
227
237
  repositories:
@@ -229,6 +239,11 @@ repositories:
229
239
  - /path/to/repo2
230
240
  max_workers: 10 # optional, valid range 1-32, default 10
231
241
  theme: rose-pine # optional, default rose-pine
242
+ ```
243
+
244
+ Secrets (GitHub credentials) are stored in a separate file at `~/.gitdirector/secrets.yaml`:
245
+
246
+ ```yaml
232
247
  github_username: your-github-username # optional
233
248
  github_PAT: github_pat_or_classic_pat # optional
234
249
  ```
@@ -239,7 +254,7 @@ GitDirector first runs git commands using your normal local git credentials. If
239
254
 
240
255
  - This only applies to HTTPS GitHub remotes.
241
256
  - SSH remotes still use your normal SSH key/agent setup.
242
- - The PAT is stored in plaintext in `~/.gitdirector/config.yaml`; use an appropriately scoped token and protect that file.
257
+ - The PAT is stored in plaintext in `~/.gitdirector/secrets.yaml`; use an appropriately scoped token and protect that file.
243
258
  - The PAT is not passed on the command line or shown in TUI command output.
244
259
 
245
260
  ### Available Themes
@@ -262,6 +277,12 @@ eval "$(gitdirector completion zsh)"
262
277
  gitdirector completion fish | source
263
278
  ```
264
279
 
280
+ The `zsh` script auto-loads `compinit` if `compdef` is not yet available, so the `eval` form works in fresh shells. For the fastest setup (no subprocess on every `TAB`), write the script to a file in your `$fpath` (commonly `~/.zsh/completions/_gitdirector`) and let `compinit` autoload it:
281
+
282
+ ```bash
283
+ gitdirector completion zsh > "${fpath[1]}/_gitdirector"
284
+ ```
285
+
265
286
  ## License
266
287
 
267
288
  MIT
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "gitdirector"
7
- version = "1.7.0"
7
+ version = "1.7.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" }
@@ -20,6 +20,7 @@ from .commands import (
20
20
  listt,
21
21
  print_update_notice,
22
22
  pull,
23
+ reset,
23
24
  status,
24
25
  tui,
25
26
  unlink,
@@ -59,6 +60,7 @@ cd.register(cli)
59
60
  help.register(cli)
60
61
  tui.register(cli)
61
62
  autoclean.register(cli)
63
+ reset.register(cli)
62
64
  info.register(cli)
63
65
  gd_tmux.register(cli)
64
66
  capture.register(cli)
@@ -0,0 +1,74 @@
1
+ import click
2
+ from click.shell_completion import CompletionItem, get_completion_class
3
+
4
+ from ..config import Config
5
+
6
+ SUPPORTED_SHELLS = ("bash", "zsh", "fish")
7
+
8
+
9
+ def _completion_var(prog_name: str) -> str:
10
+ return f"_{prog_name.replace('-', '_').replace('.', '_').upper()}_COMPLETE"
11
+
12
+
13
+ _ZSH_COMPINIT_BOOTSTRAP = (
14
+ "\n"
15
+ "# Ensure ``compdef`` is available when this script is ``eval``'d in a\n"
16
+ "# shell that has not yet loaded ``compinit``. Without this guard the\n"
17
+ "# ``compdef`` call below fails with ``command not found: compdef``.\n"
18
+ "if ! typeset -f compdef >/dev/null 2>&1; then\n"
19
+ " autoload -U +X compinit\n"
20
+ " compinit\n"
21
+ "fi\n"
22
+ )
23
+
24
+
25
+ def _patch_zsh_source(source: str) -> str:
26
+ """Inject a compinit bootstrap so ``eval`` works without prior setup.
27
+
28
+ Click's zsh output ends with ``compdef ...`` which requires the
29
+ ``compdef`` builtin provided by ``compinit``. When the script is
30
+ eval'd in a fresh zsh that hasn't loaded ``compinit`` yet, that
31
+ final call fails. Inserting a bootstrap right after the
32
+ ``#compdef`` directive ensures ``compdef`` is available without
33
+ changing behavior when the script is dropped into ``$fpath`` and
34
+ autoloaded by ``compinit`` (in which case ``compdef`` already
35
+ exists and the bootstrap is a no-op).
36
+ """
37
+ marker = "\n_gitdirector_completion() {"
38
+ if marker not in source:
39
+ return source
40
+ return source.replace(marker, _ZSH_COMPINIT_BOOTSTRAP + "\n_gitdirector_completion() {", 1)
41
+
42
+
43
+ def completion_source(cli: click.Group, shell: str, *, prog_name: str = "gitdirector") -> str:
44
+ complete_cls = get_completion_class(shell)
45
+ if complete_cls is None:
46
+ raise click.ClickException(f"Unsupported shell: {shell}")
47
+ source = complete_cls(cli, {}, prog_name, _completion_var(prog_name)).source()
48
+ if shell == "zsh":
49
+ source = _patch_zsh_source(source)
50
+ return source
51
+
52
+
53
+ def complete_repository_names(
54
+ _ctx: click.Context, _param: click.Parameter, incomplete: str
55
+ ) -> list[CompletionItem]:
56
+ try:
57
+ repositories = Config().repositories
58
+ except (OSError, RuntimeError, ValueError):
59
+ return []
60
+
61
+ prefix = incomplete.lower()
62
+ return [
63
+ CompletionItem(repo.name, help=str(repo))
64
+ for repo in sorted(repositories, key=lambda path: (path.name.lower(), str(path)))
65
+ if repo.name.lower().startswith(prefix)
66
+ ]
67
+
68
+
69
+ def register(cli: click.Group):
70
+ @cli.command()
71
+ @click.argument("shell", type=click.Choice(SUPPORTED_SHELLS))
72
+ def completion(shell: str):
73
+ """Print shell completion setup for bash, zsh, or fish."""
74
+ click.echo(completion_source(cli, shell), nl=False)
@@ -1,5 +1,6 @@
1
1
  import click
2
2
  from rich.table import Table
3
+ from rich.text import Text
3
4
 
4
5
  from . import console, get_version, print_update_notice
5
6
 
@@ -33,6 +34,7 @@ def show_help():
33
34
  ("cd NAME", "Open or switch to a tmux session for a repository"),
34
35
  ("console", "Interactive TUI for browsing and opening repositories"),
35
36
  ("autoclean", "Remove broken repository links from tracking"),
37
+ ("reset [--yes]", "Kill all sessions and panels, wipe ~/.gitdirector, recreate config"),
36
38
  ("info PATH|NAME [--full]", "Show file statistics for a repository"),
37
39
  (
38
40
  'gd-tmux PATH|NAME "cmd" [--description "..."]',
@@ -42,11 +44,11 @@ def show_help():
42
44
  "gd-capture SESSION [--lines N] [--full]",
43
45
  "Print the current scrollback of a live gd tmux session",
44
46
  ),
45
- ("gd-send SESSION [TEXT] [--enter|--key C-c]", "Send text or C-c to a gd session"),
47
+ ("gd-send SESSION [TEXT] [--enter | --key C-c]", "Send text or C-c to a gd session"),
46
48
  ("completion SHELL", "Print shell completion setup for bash, zsh, or fish"),
47
49
  ("help", "Show this help message"),
48
50
  ]:
49
- cmd_table.add_row(cmd, desc)
51
+ cmd_table.add_row(Text(cmd), desc)
50
52
 
51
53
  console.print(cmd_table)
52
54
 
@@ -0,0 +1,82 @@
1
+ import shutil
2
+ from pathlib import Path
3
+
4
+ import click
5
+
6
+ from ..config import Config
7
+ from . import console
8
+
9
+
10
+ def register(cli: click.Group):
11
+ @cli.command()
12
+ @click.option(
13
+ "--yes",
14
+ "-y",
15
+ is_flag=True,
16
+ help="Skip the confirmation prompt and reset immediately.",
17
+ )
18
+ def reset(yes: bool):
19
+ """Kill all sessions and panels, wipe ~/.gitdirector, and recreate config."""
20
+ _reset(confirm=not yes)
21
+
22
+
23
+ def _reset(*, confirm: bool = True) -> None:
24
+ config_dir = Path.home() / ".gitdirector"
25
+
26
+ if confirm and not click.confirm(
27
+ f" This will kill every gd tmux session and permanently delete {config_dir}. Continue?",
28
+ default=False,
29
+ ):
30
+ console.print()
31
+ console.print(" [dim]Cancelled.[/dim]")
32
+ console.print()
33
+ return
34
+
35
+ console.print()
36
+
37
+ killed = _kill_all_sessions()
38
+ if killed:
39
+ console.print(f" [green]Killed {len(killed)} session(s):[/green]")
40
+ for name in killed:
41
+ console.print(f" [dim]-[/dim] {name}")
42
+ else:
43
+ console.print(" [dim]No active gd tmux sessions to kill.[/dim]")
44
+
45
+ _wipe_config_dir(config_dir)
46
+
47
+ config = _recreate_config()
48
+
49
+ console.print()
50
+ console.print(f" [green]Wiped and recreated:[/green] {config.config_file}")
51
+ console.print()
52
+
53
+
54
+ def _kill_all_sessions() -> list[str]:
55
+ try:
56
+ from ..integrations.tmux import kill_all_gd_sessions
57
+ except ImportError:
58
+ console.print(" [yellow]tmux integration unavailable; skipping session kill.[/yellow]")
59
+ return []
60
+
61
+ try:
62
+ return kill_all_gd_sessions()
63
+ except Exception as exc:
64
+ console.print(f" [yellow]Failed to kill some sessions: {exc}[/yellow]")
65
+ return []
66
+
67
+
68
+ def _wipe_config_dir(config_dir: Path) -> None:
69
+ if not config_dir.exists():
70
+ console.print(f" [dim]No existing {config_dir} to remove.[/dim]")
71
+ return
72
+
73
+ try:
74
+ shutil.rmtree(config_dir)
75
+ except OSError as exc:
76
+ raise RuntimeError(f"Failed to remove {config_dir}: {exc}") from exc
77
+
78
+
79
+ def _recreate_config() -> Config:
80
+ config = Config()
81
+ config.clear()
82
+ return config
@@ -46,6 +46,7 @@ from .screens.repos import (
46
46
  RepoInfoScreen,
47
47
  )
48
48
  from .screens.sessions import EditSessionDescriptionScreen, RemoveSessionScreen
49
+ from .terminal_caps import host_color_system, no_color_requested
49
50
 
50
51
  _panel_row_height = _app_panels._panel_row_height
51
52
  _render_panel_preview = _app_panels._render_panel_preview
@@ -88,6 +89,9 @@ class GitDirectorConsole(
88
89
  background: $surface;
89
90
  overflow: hidden;
90
91
  }
92
+ HeaderTitle {
93
+ padding: 0 10 0 8;
94
+ }
91
95
  #status-bar {
92
96
  dock: bottom;
93
97
  height: 1;
@@ -120,6 +124,7 @@ class GitDirectorConsole(
120
124
  overflow-x: auto;
121
125
  overflow-y: auto;
122
126
  padding: 0 1;
127
+ scrollbar-size-horizontal: 0;
123
128
  }
124
129
  #no-repos-message {
125
130
  height: 1fr;
@@ -194,7 +199,7 @@ class GitDirectorConsole(
194
199
  Binding("s", "sort", "Sort", show=True),
195
200
  Binding("g", "show_git_menu", "Git", show=True),
196
201
  Binding("i", "show_info", "Info", show=True),
197
- Binding("d", "edit_session_description", "Description", show=False),
202
+ Binding("d", "edit_session_description", "Description", show=True),
198
203
  Binding("escape", "close_search", show=False),
199
204
  Binding("1", "tab_repos", "Repos", show=False),
200
205
  Binding("2", "tab_sessions", "Sessions", show=False),
@@ -204,6 +209,19 @@ class GitDirectorConsole(
204
209
  ]
205
210
 
206
211
  def __init__(self) -> None:
212
+ # Ensure Textual's ``App.console`` auto-detects ``"truecolor"`` for
213
+ # its render Console. ``Strip.render()`` uses ``console._color_system``
214
+ # to pick the SGR format; if it resolves to ``"256"``, every
215
+ # truecolor segment produced by child agents gets quantised to
216
+ # the 256-colour palette — visible banding in gradients.
217
+ #
218
+ # Rich's auto-detection runs inside ``App.__init__`` using a
219
+ # snapshot of ``os.environ``, so the variable must be set
220
+ # *before* ``super().__init__()`` is called. We only force
221
+ # ``COLORTERM=truecolor`` when the host actually advertises
222
+ # truecolor — never downgrade a host that can't render it.
223
+ if not no_color_requested() and host_color_system() == "truecolor":
224
+ os.environ["COLORTERM"] = "truecolor"
207
225
  super().__init__()
208
226
  from ...integrations.tmux import TmuxMonitor
209
227
 
@@ -246,7 +264,7 @@ class GitDirectorConsole(
246
264
  self._repo_status_executor: ThreadPoolExecutor | None = None
247
265
 
248
266
  def compose(self) -> ComposeResult:
249
- yield Header(show_clock=True)
267
+ yield Header(show_clock=True, icon="☰")
250
268
  with TabbedContent(id="tabs"):
251
269
  with TabPane("[1] Repositories", id="repos"):
252
270
  yield Static("", id="repo-search-indicator", classes="search-indicator")
@@ -295,6 +313,7 @@ class GitDirectorConsole(
295
313
  self._panels_col_keys = panels_table.add_columns(
296
314
  "Map", "Name", "TMUX", "Layout", "Panes", "Status"
297
315
  )
316
+ self._disable_tabs_widget_arrow_keybindings()
298
317
  self.app_resume_signal.subscribe(self, self._handle_app_resume)
299
318
  self._sync_tmux_theme_config(self.theme)
300
319
  self._poll_timer = self.set_interval(
@@ -305,6 +324,21 @@ class GitDirectorConsole(
305
324
  self._load_update_notice()
306
325
  self._load_repos()
307
326
 
327
+ def _disable_tabs_widget_arrow_keybindings(self) -> None:
328
+ """Remove the ``left``/``right`` bindings from every Tabs widget.
329
+
330
+ The ``Tabs`` widget binds ``left``/``right`` to ``previous_tab``/``next_tab``
331
+ so it can switch tabs when focused. We want those keys to scroll the
332
+ active table horizontally instead, so strip the bindings on the
333
+ underlying ContentTabs instances. Users can still switch tabs with the
334
+ ``1``/``2``/``3`` number keys (and ``tab``/``shift+tab``).
335
+ """
336
+ from textual.widgets._tabs import Tabs
337
+
338
+ for tabs_widget in self.query(Tabs):
339
+ tabs_widget._bindings.key_to_bindings.pop("left", None)
340
+ tabs_widget._bindings.key_to_bindings.pop("right", None)
341
+
308
342
  def _background_shutdown_requested(self, worker: Worker | None = None) -> bool:
309
343
  return self._shutdown_requested or (worker is not None and worker.is_cancelled)
310
344
 
@@ -434,8 +468,11 @@ class GitDirectorConsole(
434
468
  kill_tmux_session(session_name)
435
469
  self._update_status(f"tmux agent launch failed: {exc}")
436
470
  return
437
- self.push_screen(
438
- AgentLoadingScreen(agent_cmd, session_name, ready_marker),
471
+ self._show_attach_loading_screen(
472
+ session_name,
473
+ path,
474
+ ready_marker=ready_marker,
475
+ skip_config_sync=True,
439
476
  callback=refresh_after_launch,
440
477
  )
441
478
  else:
@@ -451,6 +488,7 @@ class GitDirectorConsole(
451
488
  session_name: str,
452
489
  path: Path | None = None,
453
490
  row_key: str | None = None,
491
+ ready_marker: Path | None = None,
454
492
  *,
455
493
  skip_config_sync: bool = False,
456
494
  callback: Callable[[object], None] | None = None,
@@ -478,6 +516,7 @@ class GitDirectorConsole(
478
516
  AgentLoadingScreen(
479
517
  title,
480
518
  session_name,
519
+ ready_marker,
481
520
  loading_hint=loading_hint,
482
521
  on_attach=attach,
483
522
  ),
@@ -724,6 +763,8 @@ class GitDirectorConsole(
724
763
  self._show_repo_git_remotes(path)
725
764
  elif action == "push":
726
765
  self._prompt_repo_push(path)
766
+ elif action == "review_diff":
767
+ self._open_review_diff(path)
727
768
 
728
769
  def _show_repo_git_output(
729
770
  self,
@@ -996,8 +1037,6 @@ class GitDirectorConsole(
996
1037
  session_name = action[len("attach:") :]
997
1038
  path = self._get_selected_path()
998
1039
  self._attach_to_session(session_name, path)
999
- elif action == "review_diff":
1000
- self._open_review_diff()
1001
1040
  elif action == "remove_session":
1002
1041
  path = self._get_selected_path()
1003
1042
  if path:
@@ -1006,8 +1045,9 @@ class GitDirectorConsole(
1006
1045
  callback=self._handle_remove_selection,
1007
1046
  )
1008
1047
 
1009
- def _open_review_diff(self) -> None:
1010
- path = self._get_selected_path()
1048
+ def _open_review_diff(self, path: Path | None = None) -> None:
1049
+ if path is None:
1050
+ path = self._get_selected_path()
1011
1051
  if path is None:
1012
1052
  return
1013
1053
  info = self._results.get(str(path))
@@ -147,6 +147,7 @@ class ConsoleSessionsMixin:
147
147
  entry["repo"],
148
148
  entry["session_name"],
149
149
  description,
150
+ height=None,
150
151
  key=entry["session_name"],
151
152
  )
152
153
 
@@ -62,6 +62,8 @@ class ConsoleUIHelpersMixin:
62
62
  return self._active_tab == "repos"
63
63
  if action in {"show_git_menu", "show_info"}:
64
64
  return self._active_tab == "repos"
65
+ if action == "edit_session_description":
66
+ return self._active_tab == "sessions"
65
67
  return super().check_action(action, parameters)
66
68
 
67
69
  def _arm_resume_new_panel_guard(self, restore_tab: str) -> None:
@@ -119,6 +119,20 @@ class StageFilesConfirmScreen(ModalScreen[bool]):
119
119
  _COMMIT_MESSAGE_RE = re.compile(r"[\s\S]+", re.MULTILINE)
120
120
 
121
121
 
122
+ class _CommitActionOptionList(OptionList):
123
+ """OptionList used as the commit/push picker with vim-style j/k keys.
124
+
125
+ The ``j``/``k`` bindings live on the widget itself so they only
126
+ fire when the picker has focus. This keeps typing ``j`` or ``k``
127
+ inside the commit message input working as expected.
128
+ """
129
+
130
+ BINDINGS = [
131
+ Binding("j", "cursor_down", show=False),
132
+ Binding("k", "cursor_up", show=False),
133
+ ]
134
+
135
+
122
136
  class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
123
137
  """Collect a commit message and the final action (commit / commit & push).
124
138
 
@@ -127,18 +141,22 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
127
141
  "commit & push" so the caller should run ``git push`` after the
128
142
  commit succeeds.
129
143
 
130
- Two ``OptionList`` entries below the input act as the action
144
+ Two picker entries below the input act as the action
131
145
  picker; the first option ("commit") is highlighted by default.
132
146
  Pressing ``enter`` from the input field commits with the currently
133
147
  highlighted action, matching the project's existing form
134
148
  conventions (see ``CreatePanelScreen``).
149
+
150
+ Focus toggles between the input and the action picker with
151
+ ``tab`` / ``shift+tab``. While the picker is focused, ``j`` /
152
+ ``k`` (and ``up`` / ``down``) move the selection.
135
153
  """
136
154
 
137
155
  BINDINGS = [
138
156
  Binding("escape", "cancel", "Esc cancel", show=True),
139
157
  Binding("ctrl+enter", "confirm", "Confirm", show=False),
140
- Binding("tab", "focus_action", "Tab focus action", show=False),
141
- Binding("shift+tab", "focus_message", "Shift+Tab focus message", show=False),
158
+ Binding("tab", "focus_toggle", "Tab switch focus", show=False),
159
+ Binding("shift+tab", "focus_toggle", "Shift+Tab switch focus", show=False),
142
160
  Binding("down", "action_cursor_down", "\u2193 action", show=False),
143
161
  Binding("up", "action_cursor_up", "\u2191 action", show=False),
144
162
  ]
@@ -218,13 +236,14 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
218
236
  placeholder="Commit message\u2026",
219
237
  id="commit-message-input",
220
238
  )
221
- yield OptionList(
239
+ yield _CommitActionOptionList(
222
240
  Option("[white]\u2713[/white] [bold]Commit[/bold]", id="commit"),
223
241
  Option("[cyan]\u2191[/cyan] [bold]Commit and push[/bold]", id="commit_push"),
224
242
  id="commit-message-action-list",
225
243
  )
226
244
  yield Static(
227
- "type message [\u2191\u2193] pick action [enter] confirm [esc] cancel",
245
+ "type message [tab] switch [\u2191\u2193/jk] pick action"
246
+ " [enter] confirm [esc] cancel",
228
247
  id="commit-message-hint",
229
248
  )
230
249
 
@@ -261,20 +280,26 @@ class CommitMessageScreen(ModalScreen[Optional[tuple[str, bool]]]):
261
280
  self._in_message = True
262
281
  self.query_one("#commit-message-input", Input).focus()
263
282
 
283
+ def action_focus_toggle(self) -> None:
284
+ """Toggle focus between the message input and the action picker."""
285
+ if self._action_list_has_focus():
286
+ self.action_focus_message()
287
+ else:
288
+ self.action_focus_action()
289
+
290
+ def _action_list_has_focus(self) -> bool:
291
+ try:
292
+ action_list = self.query_one("#commit-message-action-list", OptionList)
293
+ except Exception:
294
+ return False
295
+ return self.focused is action_list
296
+
264
297
  def action_cursor_down(self) -> None:
265
298
  self.query_one("#commit-message-action-list", OptionList).action_cursor_down()
266
299
 
267
300
  def action_cursor_up(self) -> None:
268
301
  self.query_one("#commit-message-action-list", OptionList).action_cursor_up()
269
302
 
270
- def action_cursor_down_global(self) -> None:
271
- # Bound for the ``down`` key while focus is on the Input —
272
- # moves the action list, not the cursor inside the Input.
273
- if self._in_message:
274
- self.action_cursor_down()
275
- else:
276
- super().action_cursor_down() # type: ignore[misc]
277
-
278
303
  def _message(self) -> str:
279
304
  return self.query_one("#commit-message-input", Input).value.strip()
280
305