Skip to content

PRD: Legit — GitHub PR Triage TUI #1

Description

@mayfieldiv

Problem Statement

Managing pull requests across GitHub repositories is painful. The GitHub web UI does not surface the information that matters for triage — who is blocking what, which PRs need my attention right now, what is the real size of a change (excluding lockfiles and tests), and what is the overall health of the PR pipeline. Navigating between PRs, checking CI status, reading unresolved comments, and figuring out next actions requires constant context-switching across browser tabs. There is no way to get a fast, keyboard-driven overview that prioritizes actionability.

Solution

Legit is a terminal TUI application (built with OpenTUI + Solid.js, run via Bun) that provides a fast, interactive dashboard for triaging GitHub pull requests. It auto-detects the current repository, supports tracking multiple repos globally, and presents PRs in a keyboard-navigable interface with smart grouping, sorting, filtering, and a blocker model that surfaces what needs attention first.

The user runs legit in any tracked repo (or anywhere, to see all tracked repos). A tabbed interface shows per-repo views plus an "all repos" aggregate view. PRs are grouped by smart status by default, with configurable grouping, sorting, and full-text filtering. A split layout shows a PR list on the left and a summary panel on the right. Pressing Enter drills into a full detail view with rendered markdown description, comment threads, and CI status. Actions let the user open PRs in the browser or Devin.

User Stories

  1. As a developer, I want to run legit in a git repo and immediately see all open PRs for that repo, so that I can quickly triage without opening a browser.
  2. As a developer, I want legit to auto-detect the GitHub remote of my current directory, so that I do not have to configure anything to get started.
  3. As a developer, I want untracked repos auto-added to my global config when I run legit in them, so that they become part of my multi-repo dashboard without manual setup.
  4. As a developer, I want a global config at ~/.config/legit/config.json that stores my tracked repos, identity, file categorization rules, bot logins, label mappings, and UI preferences, so that my setup persists across sessions.
  5. As a developer, I want my GitHub identity auto-detected from gh CLI auth and saved to config, so that legit knows which PRs I am blocking.
  6. As a developer, I want tabs for each tracked repo plus an "All" tab that aggregates PRs across all repos, so that I can see everything in one place.
  7. As a developer, I want to switch tabs using number keys, h/l, or left/right arrows, so that I can quickly jump between repos.
  8. As a developer, I want PRs grouped by "smart status" by default (me-blocking → needs-review → waiting-on-other → waiting-on-author), so that the most actionable items are at the top.
  9. As a developer, I want to press g to open a grouping panel where I can change the grouping key (smart status, author, repo, size category, label, or none for flat list), so that I can slice the data differently.
  10. As a developer, I want columns to be sortable (size, age, last updated, priority), so that I can find what I am looking for.
  11. As a developer, I want to press / to open a full-text search filter that searches across title, description, author, reviewers, labels, PR number (e.g. #5543), and changed file paths, so that I can quickly find specific PRs.
  12. As a developer, I want to navigate PRs with j/k or arrow keys, so that I can move through the list efficiently.
  13. As a developer, I want a summary panel to the right of the PR list that shows details of the currently selected PR (title, author, dates, size breakdown, CI status, reviewers with status, unresolved comment count, merge conflict indicator, labels, blocker), so that I get context without drilling in.
  14. As a developer, I want to press Enter to drill into a full-page detail view for the selected PR, so that I can see the complete picture.
  15. As a developer, I want the detail view to render the PR description as formatted markdown (via marked), so that it is readable in the terminal.
  16. As a developer, I want the detail view to show unresolved comment threads with the full conversation, so that I can understand the discussion.
  17. As a developer, I want resolved comment threads hidden by default in the detail view, with an option to show them, so that I focus on what still needs attention.
  18. As a developer, I want the option to hide bot comments in the detail view, so that noise from CI bots and AI reviewers does not drown out human discussion.
  19. As a developer, I want each comment in the detail view to be deep-linkable — pressing a key opens that specific comment in the browser, so that I can jump to GitHub to respond.
  20. As a developer, I want to press Escape to go back from the detail view to the PR list, so that navigation is intuitive.
  21. As a developer, I want to see real change size vs. noise — application code changes separated from test changes, lockfile changes, and generated file changes, so that I can judge actual complexity.
  22. As a developer, I want file categorization to use path-based heuristics (e.g., *.lock, **/test/**, *.snap, *-lock.json), so that it works out of the box.
  23. As a developer, I want to add custom file categorization rules in config.json, so that I can tune it for my projects.
  24. As a developer, I want to see who the current blocker is for each PR (author if CI failing, requested reviewer if awaiting review, specific person if changes requested), so that I know who needs to act.
  25. As a developer, I want PRs where I am the blocker surfaced first, so that I address my responsibilities before anything else.
  26. As a developer, I want PRs waiting on author hidden by default (unless I am the author), so that I focus on actionable items.
  27. As a developer, I want to see CI/check status (passing, failing, pending) for each PR, so that I do not review something with broken CI.
  28. As a developer, I want to see unresolved review comment counts (human vs. bot separately), so that I know the review workload.
  29. As a developer, I want to see assignees and requested reviewers for each PR, so that I know who is involved.
  30. As a developer, I want to see merge conflict indicators, so that I know which PRs need rebasing.
  31. As a developer, I want to see draft status for PRs, so that I know what is not yet ready for review.
  32. As a developer, I want to press o to open the selected PR in my browser, so that I can take action in GitHub.
  33. As a developer, I want to press d to open the selected PR in Devin, so that I can use AI-assisted review.
  34. As a developer, I want to press y to copy the PR URL to my clipboard, so that I can share it.
  35. As a developer, I want to press r to refresh the current repo tab, so that I get updated data without restarting.
  36. As a developer, I want to press R to refresh all repo tabs, so that everything is up to date.
  37. As a developer, I want a loading state on startup while data is being fetched, so that I know the app is working.
  38. As a developer, I want legit to authenticate using the same token the gh CLI uses, so that I do not have to manage separate credentials.
  39. As a developer, I want labels displayed on PRs, so that I can see priority and categorization at a glance.
  40. As a developer, I want the "all repos" tab to let me group by repo as one of the grouping options, so that I can see cross-repo data organized by project.

Implementation Decisions

Architecture: Deep Modules with Boundary Testing

All modules follow the deep module principle — small interfaces hiding significant implementation. Dependencies are injected so that every module is testable at its boundary without mocking internals.

Module 1: GitHub API Client ✅

  • Responsibility: All communication with GitHub. REST and GraphQL calls, pagination, batched queries, auth token resolution.
  • Interface: fetchOpenPRs(repo, onProgress?), fetchPR(repo, number). Returns typed data objects. Future methods: fetchReviewComments(repo, number), fetchFiles(repo, number), fetchCheckStatus(repo, number) — to be added when the detail view and file categorizer are implemented.
  • Dependency category: True external (Category 4). An HTTP transport is injected (HttpFetch). Production uses globalThis.fetch. Tests provide a mock transport returning canned responses.
  • Auth: Resolves token via gh auth token and username via gh api user. The gh CLI handles its own keychain integration internally. Environment variables GITHUB_TOKEN and GH_TOKEN are stripped before calling gh to avoid pollution from tools like 1Password op:// references. Token resolved once at startup via lazy getter, passed to transport.
  • Implementation: src/lib/github-client.tscreateGitHubClient(token, httpFetch?) returns a GitHubClient object. REST pagination fetches all open PRs, then batched GraphQL (50 PRs per query) enriches with additions/deletions, reviewDecision, mergeable, lastCommitDate. Null-guards on GraphQL responses allow graceful fallback to REST-only data.

Module 2: Config Manager ✅

  • Responsibility: Read/write/validate ~/.config/legit/config.json. Manages tracked repos, user identity, file categorization rules, bot logins, label-to-priority mappings, UI preferences (default grouping, sort).
  • Interface: Standalone functions with injected path — loadConfig(path), saveConfig(path, config), addRepo(config, repo), removeRepo(config, repo). Config is a typed LegitConfig object accessed directly by property.
  • Dependency category: Local-substitutable (Category 2). Config path is injected. Tests use a temp directory.
  • Schema: Validated on load with sensible defaults for missing fields. Partial configs are merged with DEFAULT_CONFIG.
  • Implementation: src/lib/config.ts.

Module 3: PR Data Store 🔜

  • Responsibility: Reactive Solid.js store holding PR data per repo. Manages loading lifecycle (idle → loading → loaded/error), refresh logic, provides signals the UI subscribes to.
  • Interface: createPRStore(apiClient, repos) returning reactive signals — prs(repo), loading(repo), refresh(repo), refreshAll().
  • Dependency category: In-process (Category 1). The API client is injected as a dependency.
  • Current state: Not yet a standalone module. Currently, App.tsx uses SolidJS createResource directly for single-repo reactivity, and the Legit session class provides the data-fetching layer. The multi-repo reactive store described above will be built when multi-repo tabs are implemented.

Module 4: File Categorizer ✅

  • Responsibility: Classifies PR file changes into categories (code, test, generated, docs, config) and computes size breakdowns. Applies built-in heuristics via Bun.Glob plus user-configured rules from config.
  • Interface: categorizeFiles(files: FileChange[], userRules?: FileRule[]) → FileCategorization. Pure function returning per-category stats and annotated file list.
  • Dependency category: In-process (Category 1). No dependencies beyond input data.
  • Categories: code (default), test (**/test/**, *.test.*, *.spec.*), generated (*.lock, *.snap, *.generated.*, **/generated/**), docs (*.md, docs/**, README*), config (.github/**, *.yml, *.toml, tsconfig*.json).
  • Implementation: src/lib/file-categorizer.ts. User rules (from config.fileRules) take precedence over built-in rules. Exposed via Legit.categorizeFiles() and legit files <number> CLI subcommand.

Module 5: Blocker/Priority Engine ✅

  • Responsibility: Determines who is blocking a PR and assigns a priority tier. Accounts for CI status, draft state, merge conflicts, review state, requested reviewers, and the current user's relationship to the PR.
  • Interface: computeBlocker(pr: PR, currentUser: string, opts?: { checks?, reviews? }) → { blocker: string, tier: Tier, reason: string }. Pure function. Optional checks/reviews allow richer computation when summary data is available.
  • Tiers: me-blockingneeds-reviewwaiting-on-otherwaiting-on-author.
  • 7-step decision order (first match wins): (1) CI failing → waiting-on-author, (2) Draft → waiting-on-author, (3) Merge conflict → waiting-on-author, (4) Current user is requested reviewer → me-blocking, (5) Changes requested → waiting-on-author, (6) Another reviewer requested → waiting-on-other, (7) Default → needs-review.
  • Identity: Legit.currentUser accessor centralizes the config.user || auth.user fallback, used by both CLI and TUI.
  • Dependency category: In-process (Category 1).
  • Implementation: src/lib/blocker-engine.ts. Also exports compareTiers() for sort ordering and tierLabel() for display. CLI: legit blocker <number> and legit prs --with-blockers.

Module 6: Grouping/Sort/Filter Engine 🔜

  • Responsibility: Takes a flat list of PRs with computed blocker/priority and applies grouping, sorting, and filtering. Supports full-text search across title, description, author, reviewers, labels, PR number, and file paths.
  • Interface: processPRList(prs: PR[], options: { groupBy, sortBy, sortDir, filterText, currentUser }) → GroupedResult. Pure function.
  • Dependency category: In-process (Category 1).
  • Grouping keys: smart-status, author, repo, size-category, label, none (flat).

Module 7: TUI Shell ✅

  • Responsibility: Top-level layout and view routing. Content area switching between list view and detail view, global keybinding dispatch. Header shows repo slug and PR count. Error display for failed fetches.
  • Components: Uses OpenTUI Solid intrinsics (box, text, scrollbox). Tab bar for repos is deferred to multi-repo implementation.
  • Dependency category: Local-substitutable (Category 2). Tested via testRender from @opentui/solid.
  • Implementation: src/components/AppShell.tsx (layout, view routing), src/App.tsx (data fetching, resource management).

Module 8: PR List View ✅

  • Responsibility: Scrollable table of PRs with columns: number, title, author, size (+/-), age, review/draft/conflict status, blocker. Selection state managed by a reactive createListSelection primitive. j/k/arrow navigation, r to refresh, Enter to navigate to detail.
  • Dependency category: Local-substitutable (Category 2). Tested via testRender.
  • Blocker column: Conditional on currentUser — shows "you" (magenta) for me-blocking or self-authored drafts/conflicts, reviewer name (gray) for waiting-on-other, author name (yellow) for waiting-on-author. Empty cell reserved for needs-review to maintain alignment.
  • Conflict indicator: ! prefix (red) in the Review column for PRs with mergeable === "CONFLICTING".
  • Open in browser: o key opens the selected PR's GitHub URL via open.
  • Future columns: Last commit date, CI status — to be added when their respective modules are implemented.
  • Implementation: src/components/ListView.tsx (keyboard handling, scroll sync), src/components/PRList.tsx (table rendering), src/lib/list-selection.ts (selection primitive), src/lib/format.ts (age, size, review decision formatting).

Module 9: PR Summary Panel ✅

  • Responsibility: Right-side panel showing selected PR's full details — title, author, created/updated dates, size breakdown (code/test/generated/docs/config), CI checks (sorted: failed → pending → passed, collapsed beyond 6), reviewers with their review status, requested reviewers, unresolved comment count (human/bot), merge conflict indicator, labels, blocker tier and reason.
  • Dependency category: Local-substitutable (Category 2). Tested via testRender.
  • Implementation: src/components/SummaryPanel.tsx. Data fetched via Legit.fetchPRSummary() which batches PR detail + checks + reviews + comments + files in parallel. Debounced on selection change (300ms) with session-level cache.

Module 10: PR Detail View 🔜

  • Responsibility: Full-page view. Renders PR description as terminal-formatted markdown via marked. Shows unresolved comment threads with full conversations. Resolved threads hidden by default (toggleable). Bot comments hideable. Each comment has a deep-link action to open in browser. Shows CI check list.
  • Dependency category: Local-substitutable (Category 2). Tested via testRender.

Module 11: CLI Entry Point ✅

  • Responsibility: Subcommand parsing and dispatch. Routes to either JSON output (for detect, auth, config, prs, pr <number>) or TUI launch (no args). Provides a structured, testable interface that exercises the same code paths as the TUI.
  • Interface: runCommand(args, app) → CommandResult where CommandResult is { output?, error?, launchTui? }.
  • Dependency category: In-process (Category 1). The Legit session object is injected.
  • Subcommands: legit detect (repo detection), legit auth (user + token source as JSON), legit config (current config), legit repos (tracked repos), legit prs (all open PRs; supports --repo=<slug>, --all, --with-blockers), legit pr <number> (single PR summary with checks, reviews, comments, files), legit files <number> (file categorization), legit blocker <number> (blocker computation), legit (launch TUI).
  • Implementation: src/cli.ts.

Data Flow

  1. Session initialization: The Legit class (src/lib/legit.ts) is the coordination layer that wires together repo detection, auth, config, and the API client. All dependencies are lazily initialized — repo detection only runs when repo is accessed, auth only resolves when auth or client is accessed. External dependencies (config path, cwd, auth executor, HTTP fetch) are injected via LegitOptions for testability.
  2. CLI dispatch: cli.ts creates a Legit instance and calls runCommand(args, app). For JSON subcommands, it accesses the relevant Legit properties/methods directly. For TUI launch, it dynamically imports the App component and passes the Legit instance as props.
  3. Data fetching: Legit.fetchPRs() auto-adds the repo to config, then delegates to the GitHub API Client. REST pagination fetches all open PRs, then batched GraphQL enriches metadata. Progress is reported via an optional callback.
  4. TUI rendering: App.tsx uses SolidJS createResource with the Legit instance to manage the fetch lifecycle. AppShell renders the layout, ListView handles keyboard navigation and scroll sync, PRList renders the table rows.
  5. PR enrichment: Legit.fetchPRSummary() batches detail + checks + reviews + comments + files in parallel. File Categorizer computes size breakdown. Blocker Engine computes blocker/tier (in PR list rows and summary panel). Future: Grouping/Sort/Filter Engine processes the enriched PR list based on user's current preferences.

External Integrations

  • Open in browser: Launches open https://github.com/{owner}/{repo}/pull/{number}
  • Open in Devin: Launches open https://app.devin.ai/review/{owner}/{repo}/pull/{number}
  • Copy URL: Copies PR URL to system clipboard
  • Comment deep links: open https://github.com/{owner}/{repo}/pull/{number}#discussion_r{id}

Testing Decisions

Testing Philosophy

  • Test at the boundary, not the internals. Every module is tested through its public interface. Internal refactors must not break tests.
  • TDD (red-green-refactor) is the development methodology. Tests are written first, implementation follows.
  • Replace, don't layer. No redundant test layers — boundary tests are sufficient.
  • Observable outcomes only. Tests assert on return values and observable side effects, never on internal state.

Module Testing Strategy

Module Dependency Category Test Approach Status
GitHub API Client True external (Cat 4) Inject mock HTTP transport. Test that correct endpoints are called, pagination is handled, responses are parsed into typed objects, auth token is resolved. tests/github-client.test.ts
Config Manager Local-substitutable (Cat 2) Inject temp directory path. Test load/save roundtrip, schema validation, defaults for missing fields, repo add/remove. tests/config.test.ts
PR Data Store In-process (Cat 1) Inject mock API client. Test reactive signal updates, loading states, refresh behavior, error handling. tests/tui-app-integration.test.tsx (via App.tsx createResource)
File Categorizer In-process (Cat 1) Pure function tests. Test built-in heuristics, user rules, edge cases (empty file list, unknown extensions, overlapping rules). tests/file-categorizer.test.ts
Blocker/Priority Engine In-process (Cat 1) Pure function tests. Test every combination of review state × CI status × reviewer relationship × draft × conflict. Test tier ordering. tests/blocker-engine.test.ts
Grouping/Sort/Filter Engine In-process (Cat 1) Pure function tests. Test each grouping key, sort key, filter text matching, combined operations, empty inputs. 🔜
TUI Components (Shell, List, Summary, Detail) Local-substitutable (Cat 2) testRender from @opentui/solid. Snapshot tests for layout. Interaction tests for keyboard navigation, view transitions, action dispatch. ✅ Shell + List + Summary (tests/tui-app.test.tsx, tests/tui-list-view.test.tsx, tests/tui-pr-list.test.tsx, tests/tui-summary-panel.test.tsx). 🔜 Detail.
CLI Entry Point In-process (Cat 1) Inject mock Legit instance. Test each subcommand returns correct structured output. Subprocess smoke test for end-to-end. tests/cli.test.ts

Prior Art

  • The @opentui/solid package provides testRender(node, options?) for component testing with configurable terminal dimensions.
  • Bun's built-in test runner (bun test) will be used for all tests.

Out of Scope

  • Real-time WebSocket updates via GitHub's alive.github.com — documented as a future goal, not v1.
  • PR diff viewer in the TUI — future feature, not v1.
  • Agent-spawning actions (e.g., spin off an AI agent to review a PR) — future feature, not v1.
  • Graphite integration — future feature, open in Graphite URL TBD.
  • Label-based priority system — the infrastructure exists (labels in data model, label grouping) but custom priority mappings from labels are deferred.
  • Writing/responding to comments from within the TUI — read-only for v1.
  • PR creation or editing — legit is a triage/read tool, not a write tool.
  • Notifications or desktop alerts.
  • Git operations (merge, rebase, checkout) from within the TUI.

Further Notes

  • Dependency constraint: The user wants to minimize adding dependencies beyond what is already in package.json (@opentui/core, @opentui/solid, solid-js, marked). New dependencies should only be added if extremely helpful.
  • Future: WebSocket subscriptions: GitHub's alive.github.com WebSocket endpoint pushes real-time updates for PR state changes. Reverse-engineering the session token and subscription protocol would enable live-updating the TUI without polling. This is a high-value future feature.
  • Future: Agent actions: The ability to spawn agents (e.g., via CLI) to perform code review or evaluation of a PR is a key long-term goal.
  • Future: PR diff viewer: Rendering diffs in the TUI using OpenTUI's diff component is a natural extension once the detail view is stable.
  • Iterative development: The user expects to rapidly discover new requirements once they can interact with the TUI. The architecture should make it easy to add new grouping keys, sort keys, filter dimensions, actions, and detail view sections without restructuring.

Agent Handoff Prompt

Use this prompt to start a fresh agent session for picking up the next issue:

You're working on `legit`, a terminal TUI app for triaging GitHub PRs, built with OpenTUI + SolidJS + Bun. The codebase has a solid foundation from the first PR — auth, config, GitHub API client (REST+GraphQL), CLI subcommands, and a navigable PR list TUI.

**Get oriented:**
1. Read `AGENTS.md` for project conventions
2. Read the PRD: `gh issue view 1` — note the ✅/🔜 status annotations on each module
3. List open issues: `gh issue list --state open` — each has a "Foundation from #2" section explaining what infrastructure already exists
4. Review the current code: `src/lib/` (core logic), `src/components/` (TUI), `src/cli.ts` (CLI entry), `tests/` (boundary tests)
5. Run `bun test` to verify everything passes

**Pick the next issue** by following the dependency graph in the issues' "Blocked by" sections. Choose the highest-value unblocked issue. Create a feature branch (`feat/<short-name>`), implement it, and open a PR when done.

**Development approach:**
- **TDD**: Write failing tests first, then implement. Boundary tests through public interfaces, not internals. Mock HTTP transport for API tests, temp dirs for config tests, `testRender` for TUI components.
- **Atomic commits**: Each commit should be a coherent unit — one test + its implementation, or one refactor. Not giant squashed commits.
- **CLI subcommands**: For any new data-centric logic (blocker computation, file categorization, grouping/filtering), add a CLI subcommand that exposes it as JSON. This serves as both a verification tool and a testable interface. Follow the pattern in `src/cli.ts` — `runCommand()` dispatches to the `Legit` instance.
- **TUI testing**: Use the `legit-test` tmux session (`tmux attach -t legit-test` or create it with `tmux new-session -d -s legit-test`) to manually verify TUI behavior against `~/immybot` (~155 open PRs, read-only). Run `legit` there and exercise the feature interactively.
- **Existing patterns to follow**: Injected dependencies via options objects. `createMockFetch` for HTTP mocks. `createTestLegit` for integration tests. `useKeyboard` for TUI keybindings. `createListSelection` for reactive selection. Props accessed as `props.x` (no destructuring — SolidJS reactivity).

**When done:** Run `bun test` to verify all tests pass. Test the TUI manually in tmux against `~/immybot`. Open a PR linking to the issue.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions