K9s-inspired Bubble Tea TUI for managing multiple git and jj repositories with progressive loading, filtering, GitHub PR integration, and batch maintenance tasks.
Framework: Bubble Tea (Go TUI framework) Theme: Catppuccin Macchiato Design Philosophy: Minimal color, single unified background, borders for hierarchy, vim-style keybindings
├── main.go # CLI entry point
├── go.mod / go.sum # Dependencies
├── internal/
│ ├── app/ # Bubble Tea app
│ │ ├── app.go # Model definition, Init
│ │ ├── update.go # Update function (message handling)
│ │ ├── view.go # View rendering
│ │ ├── keymap.go # Key bindings
│ │ ├── commands.go # Tea commands
│ │ └── messages.go # Message types
│ ├── models/ # Data structures
│ │ ├── repo.go # RepoSummary, WorktreeInfo
│ │ ├── branch.go # BranchInfo
│ │ ├── pr.go # PRInfo, PRDetail, WorkflowSummary
│ │ ├── filter.go # ActiveFilter, ActiveSort
│ │ └── enums.go # FilterMode, SortMode, etc.
│ ├── vcs/ # VCS abstraction
│ │ ├── operations.go # VCSOperations interface
│ │ ├── git.go # Git implementation
│ │ ├── jj.go # JJ implementation
│ │ ├── factory.go # Detection and factory
│ │ └── mock.go # Test mock
│ ├── filters/ # Filter/sort logic
│ │ ├── filter.go # FilterRepos, FilterReposMulti
│ │ ├── sort.go # SortPaths, SortPathsMulti
│ │ └── search.go # Fuzzy search
│ ├── discovery/ # Repo discovery
│ │ └── discovery.go # DiscoverRepos
│ ├── batch/ # Batch operations
│ │ ├── runner.go # Task runner
│ │ └── tasks.go # Task definitions
│ ├── github/ # GitHub integration
│ │ ├── pr.go # PR operations
│ │ └── workflow.go # Workflow runs
│ ├── cache/ # Caching
│ │ └── ttl.go # Generic TTL cache
│ └── ui/styles/ # Styling
│ └── styles.go # Lipgloss styles
└── wip-test-improvements.md # Testing patterns documentation
- Go 1.23+
- git CLI (if managing git repos)
- jj CLI (if managing jj repos)
- gh (GitHub CLI, optional for PR features with both git and jj)
# Build and run
go build -o gh-repo-dashboard .
./gh-repo-dashboard ~/Developer --depth 2
# Or as a GitHub CLI extension
gh extension install .
gh repo-dashboard ~/Developer# Run all tests
go test ./...
# Run with verbose output
go test -v ./...
# Run specific package tests
go test -v ./internal/filters/...
# Run specific test
go test -v -run TestFilterRepos ./internal/filters/
# Run with coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run with race detector
go test -race ./...See test-improvements.md for comprehensive testing patterns including:
- teatest (Golden File Testing) - Visual regression with snapshot comparison
- catwalk (Data-Driven Testing) - Complex interaction sequence testing
- Direct Testing - State transition and business logic testing
# Run golden file tests (if using build tag)
go test -tags=golden ./...
# Update golden files
go test -tags=golden -update ./...The dashboard uses an interface-based abstraction to support multiple version control systems.
VCS Interface Pattern:
VCSOperationsinterface defines the contract for both read and write operationsGitOperationsandJJOperationsimplement the interfaceDetectVCSType()auto-detects VCS by directory presence (.gitor.jj)GetVCSOperations()factory returns the appropriate implementation- Colocated repos (both
.gitand.jj) prefer jj
Key Files:
vcs/operations.go- Interface defining VCS operationsvcs/git.go- Git implementation with full interface supportvcs/jj.go- Jujutsu implementation with full interface supportvcs/factory.go- VCS detection and factory functionbatch/tasks.go- Batch operations using VCS abstraction
| Concept | Git | JJ (Jujutsu) | Notes |
|---|---|---|---|
| Current location | HEAD | @ (working copy) | jj always has a working copy change |
| Branch | branch | bookmark | jj bookmarks are similar to git branches |
| Staged changes | index/staging | N/A | jj automatically tracks all changes |
| Uncommitted | unstaged + staged | working copy | Different mental model |
| Commits ahead/behind | ahead/behind | ahead/behind | Similar concept |
| Remote tracking | upstream branch | tracking bookmark | Similar |
| Stash | stash | N/A | jj doesn't need stashing (can create changes) |
| Worktree | worktree | workspace | Similar but jj workspaces are more powerful |
Read Operations:
GetRepoSummary()- Get repository status and metadataGetCurrentBranch()- Get current branch/bookmark nameGetBranchList()- List all branches/bookmarksGetStashList()- List stashes (git only, jj returns empty)GetWorktreeList()- List worktrees/workspacesGetCommitLog()- Get commit/change historyGetAheadBehind()- Get commits ahead/behind tracking branchGetStagedCount()/GetUnstagedCount()/GetUntrackedCount()- File status countsGetConflictedCount()- Count of conflicted files
Write Operations (batch tasks):
FetchAll()- Fetch from all remotes- Git:
git fetch --all --prune - JJ:
jj git fetch --all-remotes
- Git:
PruneRemote()- Prune stale remote branches- Git:
git remote prune origin - JJ: No-op (jj handles this automatically)
- Git:
CleanupMergedBranches()- Delete merged local branches/bookmarks- Git: Deletes local branches merged into main
- JJ: Deletes bookmarks that are ancestors of main
All write operations return (success bool, message string) for UI feedback.
GitHub integration works for both git and jj repositories via the gh CLI:
- For git repos: Uses standard git directory
- For jj repos (non-colocated): Sets
GIT_DIRenvironment variable to.jj/repo/store/git - For jj repos (colocated): Uses
.gitdirectory like standard git repos
The GetGitHubEnv() helper in vcs/factory.go handles this transparently.
Batch operations execute maintenance tasks across multiple repositories simultaneously.
BatchTaskRunner:
- Runs tasks sequentially across filtered repositories
- Uses VCS factory to get appropriate operations for each repo
- Tracks progress for each operation
- Handles errors gracefully (continues on failure)
- Sends progress messages via Tea commands
-
Add method to
VCSOperationsinterface (vcs/operations.go)type VCSOperations interface { // ... existing methods NewOperation(ctx context.Context, repoPath string) (bool, string) }
-
Implement in both
GitOperationsandJJOperations// vcs/git.go func (g *GitOperations) NewOperation(ctx context.Context, repoPath string) (bool, string) { // Git-specific implementation return true, "Success message" } // vcs/jj.go func (j *JJOperations) NewOperation(ctx context.Context, repoPath string) (bool, string) { // JJ-specific implementation return true, "Success message" }
-
Create task function in
batch/tasks.gofunc TaskNewOperation(vcsOps vcs.VCSOperations, repoPath string) (bool, string) { return vcsOps.NewOperation(context.Background(), repoPath) }
-
Add handler in
app/update.gocase "N": if m.viewMode == ViewModeRepoList { return m, m.startBatchTask("New Operation", batch.TaskNewOperation) }
-
Add keybinding to
app/keymap.gokey.NewBinding( key.WithKeys("N"), key.WithHelp("N", "new operation"), ),
-
Add tests to
internal/batch/batch_test.go
Read-Only by Default:
- All existing functionality remains read-only
- Write operations require explicit user action (keybinding)
Batch Task Safety:
- Only operate on currently filtered repos (explicit scope)
- Progress feedback shows results incrementally
- Failures highlighted but don't stop batch execution
JJ-Specific Considerations:
- Non-colocated repos require GIT_DIR for gh CLI (handled automatically)
- jj operations are generally safer (immutable history)
- Some git concepts don't map to jj (stash, staged changes)
- jj has more powerful undo capabilities
Structure:
- Use lowercase unexported functions for internal helpers
- Place related code in the same package
- Use interfaces for abstraction
- Write small, composable functions with single responsibility
Error handling:
- Return errors explicitly
- Wrap errors with context:
fmt.Errorf("operation failed: %w", err) - Use
context.Contextfor cancellation and timeouts - Validate at system boundaries, trust internal code
Naming:
- Use MixedCaps (PascalCase for exported, camelCase for unexported)
- Acronyms should be all caps:
GetPRInfo,HTTPClient - Interface names should describe behavior:
VCSOperations,Fetcher
Comments:
- Add package comments in a single file per package
- Add doc comments for exported functions/types
- Do not add comments explaining what code does (code should be self-explanatory)
Model structure:
type Model struct {
// State
viewMode ViewMode
loading bool
cursor int
// Data
repoPaths []string
filteredPaths []string
summaries map[string]models.RepoSummary
// UI components
width int
height int
}Update function:
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q":
return m, tea.Quit
case "j", "down":
m.cursor++
}
case RepoSummaryLoadedMsg:
m.summaries[msg.Path] = msg.Summary
}
return m, nil
}Commands:
func loadRepoSummary(path string) tea.Cmd {
return func() tea.Msg {
summary, err := vcs.GetRepoSummary(context.Background(), path)
if err != nil {
return RepoSummaryErrorMsg{Path: path, Err: err}
}
return RepoSummaryLoadedMsg{Path: path, Summary: summary}
}
}View rendering with Lipgloss:
func (m Model) View() string {
var b strings.Builder
// Header
header := lipgloss.NewStyle().
Bold(true).
Foreground(lipgloss.Color("#8aadf4")).
Render("Repository Dashboard")
b.WriteString(header + "\n")
// Content
for i, path := range m.filteredPaths {
style := lipgloss.NewStyle()
if i == m.cursor {
style = style.Background(lipgloss.Color("#363a4f"))
}
b.WriteString(style.Render(path) + "\n")
}
return b.String()
}Catppuccin Macchiato Colors:
- Base:
#24273a(background) - Surface0:
#363a4f(elevated surfaces, cursor) - Text:
#cad3f5(primary text) - Subtext0:
#a5adcb(secondary text) - Blue:
#8aadf4(primary accent, borders) - Mauve:
#c6a0f6(search accent) - Yellow:
#eed49f(filter accent) - Green:
#a6da95(success, PRs) - Peach:
#f5a97f(dirty repos)
Visual hierarchy:
- Borders provide visual separation
- Color is reserved for actionable elements (badges, accents)
- Minimal color usage overall
- Single unified background
- Focus states use Surface0 for cursor
Compositional filtering:
FilterMode -> SearchText -> SortMode -> Display
Example: "DIRTY" filter + "api" search = dirty repos containing "api"
Filter modes:
- ALL - Show all repositories
- DIRTY - Uncommitted changes or unpushed commits
- AHEAD - Commits ahead of tracking branch
- BEHIND - Commits behind tracking branch
- HAS_PR - Has associated GitHub PR
- HAS_STASH - Has stashed changes
Sort modes:
- NAME - Alphabetical by repo name
- MODIFIED - Most recently modified first
- STATUS - Dirty repos first, then by uncommitted count
- BRANCH - By branch name, then repo name
Multi-sort support:
- Go implementation supports multi-field sorting with priority
- Each sort can have ASC/DESC direction
Search:
- Fuzzy matching using sahilm/fuzzy library
- Case-insensitive
- Applied after filter mode, before sort
- Real-time updates as you type
- Repo list appears immediately with placeholder data
- Goroutines load
RepoSummaryfor each repo concurrently - Table updates incrementally as data becomes available via Tea messages
- No blocking on slow git operations
Generic TTL cache with mutex protection:
prCache- GitHub PR informationbranchCache- Branch listssummaryCache- Repository summaries
Refresh clears all caches.
ViewModeRepoList (initial)
- Shows all discovered repositories
- Columns: Name, Branch, Status, PR, Modified
ViewModeRepoDetail (drill-down with Enter)
- Shows branches, stashes, worktrees, PRs
- Tab switching between detail views
ViewModeFilter (f key)
- Filter selection modal
- Multi-filter with AND logic
ViewModeSort (s key)
- Sort selection modal
- Multi-sort with priority
ViewModeHelp (? key)
- Complete keybinding reference
ViewModeBatchProgress
- Progress bar and results during batch operations
- Add const to
FilterModeinmodels/enums.go - Add filter function in
filters/filter.go - Add case to
FilterRepos()infilters/filter.go - Add tests in
filters/filter_test.go
- Add key binding to
keymap.go - Add case to
handleKey()inupdate.go - Update help text in
view.go - Add test in
app_test.go
- Add const to
ViewModeinapp/app.go - Add view rendering in
view.go - Add update handling in
update.go - Add navigation logic (enter/exit)
-
git - For managing git repositories
- Used for: status, branch list, commits, stashes, worktrees
- Assumes git is in PATH
- Not needed if only managing jj repos
-
jj - For managing jujutsu repositories
- Used for: status, bookmark list, changes, workspaces
- Assumes jj is in PATH
- Not needed if only managing git repos
- Install: See https://github.com/martinvonz/jj
- gh (GitHub CLI) - PR features for both git and jj repos
- Used for: fetching PR info, check status, PR details
- Works with both git and jj repositories
- For non-colocated jj repos: automatically sets GIT_DIR
- If missing: PR columns show dash instead of failing
- Install:
brew install gh(macOS) or see https://cli.github.com/
# Run with debug logging
DEBUG=1 ./gh-repo-dashboard ~/Developer
# Log to file
./gh-repo-dashboard ~/Developer 2>debug.logTerminal size issues:
- Model receives
tea.WindowSizeMsgon startup and resize - Ensure
m.widthandm.heightare updated
Message ordering:
- Commands execute asynchronously
- Don't assume message arrival order
- Use state flags to track loading/completion
Goroutine leaks:
- Use
context.Contextfor cancellation - Cancel contexts when leaving views or quitting
- Fuzzy search uses sahilm/fuzzy for efficient matching
- Progressive loading prevents blocking on initial scan
- TTL caching with mutex protection for thread safety
- Goroutines with Tea commands for parallel data loading
- Lipgloss style caching (reuse style objects)
- Run full test suite:
go test ./... - Run with race detector:
go test -race ./... - Test manually with real repositories (both git and jj if available)
- Test batch operations (fetch, prune, cleanup)
- Update version in
main.go - Update
README.mdif features changed - Build for release:
go build -o gh-repo-dashboard .