santree 0.5.3 → 0.5.4

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 (66) hide show
  1. package/README.md +145 -52
  2. package/dist/commands/dashboard.d.ts +1 -1
  3. package/dist/commands/dashboard.js +22 -18
  4. package/dist/commands/doctor.js +33 -71
  5. package/dist/commands/github/auth.d.ts +2 -0
  6. package/dist/commands/github/auth.js +56 -0
  7. package/dist/commands/github/index.d.ts +1 -0
  8. package/dist/commands/github/index.js +1 -0
  9. package/dist/commands/helpers/template.d.ts +1 -0
  10. package/dist/commands/helpers/template.js +13 -10
  11. package/dist/commands/issue/index.d.ts +1 -0
  12. package/dist/commands/issue/index.js +1 -0
  13. package/dist/commands/issue/open.d.ts +2 -0
  14. package/dist/commands/{linear → issue}/open.js +13 -11
  15. package/dist/commands/issue/switch.d.ts +11 -0
  16. package/dist/commands/issue/switch.js +38 -0
  17. package/dist/commands/linear/auth.js +23 -10
  18. package/dist/commands/linear/switch.js +7 -3
  19. package/dist/commands/pr/create.js +7 -5
  20. package/dist/commands/worktree/create.js +4 -6
  21. package/dist/commands/worktree/work.js +1 -1
  22. package/dist/lib/ai.d.ts +8 -6
  23. package/dist/lib/ai.js +29 -15
  24. package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
  25. package/dist/lib/dashboard/DetailPanel.js +6 -3
  26. package/dist/lib/dashboard/data.js +17 -9
  27. package/dist/lib/dashboard/types.d.ts +3 -16
  28. package/dist/lib/git.d.ts +16 -33
  29. package/dist/lib/git.js +20 -74
  30. package/dist/lib/metadata.d.ts +3 -0
  31. package/dist/lib/metadata.js +27 -0
  32. package/dist/lib/multiplexer/cmux.js +1 -1
  33. package/dist/lib/multiplexer/types.d.ts +1 -1
  34. package/dist/lib/prompts.d.ts +4 -3
  35. package/dist/lib/prompts.js +4 -3
  36. package/dist/lib/session-signal.d.ts +2 -3
  37. package/dist/lib/session-signal.js +3 -29
  38. package/dist/lib/trackers/auth-store.d.ts +16 -0
  39. package/dist/lib/trackers/auth-store.js +57 -0
  40. package/dist/lib/trackers/config.d.ts +8 -0
  41. package/dist/lib/trackers/config.js +21 -0
  42. package/dist/lib/trackers/github/api.d.ts +3 -0
  43. package/dist/lib/trackers/github/api.js +90 -0
  44. package/dist/lib/trackers/github/auth.d.ts +5 -0
  45. package/dist/lib/trackers/github/auth.js +27 -0
  46. package/dist/lib/trackers/github/images.d.ts +2 -0
  47. package/dist/lib/trackers/github/images.js +42 -0
  48. package/dist/lib/trackers/github/index.d.ts +2 -0
  49. package/dist/lib/trackers/github/index.js +78 -0
  50. package/dist/lib/trackers/index.d.ts +12 -0
  51. package/dist/lib/trackers/index.js +34 -0
  52. package/dist/lib/trackers/linear/api.d.ts +4 -0
  53. package/dist/lib/trackers/linear/api.js +128 -0
  54. package/dist/lib/trackers/linear/auth.d.ts +11 -0
  55. package/dist/lib/trackers/linear/auth.js +206 -0
  56. package/dist/lib/trackers/linear/images.d.ts +2 -0
  57. package/dist/lib/trackers/linear/images.js +44 -0
  58. package/dist/lib/trackers/linear/index.d.ts +3 -0
  59. package/dist/lib/trackers/linear/index.js +100 -0
  60. package/dist/lib/trackers/types.d.ts +52 -0
  61. package/dist/lib/trackers/types.js +1 -0
  62. package/package.json +1 -1
  63. package/prompts/ticket.njk +3 -3
  64. package/dist/commands/linear/open.d.ts +0 -2
  65. package/dist/lib/linear.d.ts +0 -83
  66. package/dist/lib/linear.js +0 -482
package/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
 
17
17
  <p align="center">
18
18
  Create, switch, and manage Git worktrees with ease.<br/>
19
- Integrates with GitHub PRs and Linear tickets via Claude AI.
19
+ Integrates with GitHub PRs and pluggable issue trackers (Linear, GitHub Issues) via Claude AI.
20
20
  </p>
21
21
 
22
22
  ---
@@ -112,13 +112,29 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
112
112
  | `santree pr fix` | Fix PR review comments with AI |
113
113
  | `santree pr review` | Review changes against ticket with AI |
114
114
 
115
- ### Linear (`santree linear`)
115
+ ### Issue trackers
116
+
117
+ Santree supports Linear and GitHub Issues behind a single interface. Each repo picks one. Use the generic `santree issue` commands for tracker-agnostic actions; use `santree linear` / `santree github` for backend-specific auth.
118
+
119
+ #### Generic — `santree issue`
120
+
121
+ | Command | Description |
122
+ | -------------------------------------- | ------------------------------------------------------------ |
123
+ | `santree issue switch <linear\|github>` | Pick the active tracker for this repo |
124
+ | `santree issue open` | Open the current branch's issue in the browser |
125
+
126
+ #### Linear (`santree linear`)
116
127
 
117
128
  | Command | Description |
118
129
  | ----------------------- | --------------------------------------------- |
119
130
  | `santree linear auth` | Authenticate with Linear (OAuth) |
120
131
  | `santree linear switch` | Switch Linear workspace for this repo |
121
- | `santree linear open` | Open the current Linear ticket in the browser |
132
+
133
+ #### GitHub Issues (`santree github`)
134
+
135
+ | Command | Description |
136
+ | --------------------- | ---------------------------------------------------------------------- |
137
+ | `santree github auth` | Verify `gh auth status`, run `gh auth login` if needed, set tracker=github |
122
138
 
123
139
  ### Helpers (`santree helpers`)
124
140
 
@@ -137,16 +153,75 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
137
153
 
138
154
  | Command | Description |
139
155
  | ------------------- | ----------------------------------------------- |
140
- | `santree dashboard` | Interactive dashboard of all your Linear issues |
156
+ | `santree dashboard` | Interactive dashboard of all your assigned issues |
141
157
  | `santree doctor` | Check system requirements and integrations |
142
158
 
143
159
  ---
144
160
 
145
161
  ## Features
146
162
 
163
+ ### Workflow
164
+
165
+ End-to-end flow, from picking an issue to merging the PR. Everything runs inside the terminal multiplexer; the dashboard orchestrates and the AI agent does the heavy lifting at two distinct stages.
166
+
167
+ ```mermaid
168
+ flowchart TB
169
+ subgraph Term["Terminal · tmux / cmux"]
170
+ direction TB
171
+ Dashboard(["① Dashboard<br/>pick issue · add context"]):::action
172
+ Editor["Editor<br/><sub>zed · cursor · vscode · nvim · jetbrains</sub>"]:::tool
173
+ AI(("② / ⑤ AI agent<br/>Claude Code")):::agent
174
+ Pager["③ Diff pager<br/><sub>delta · diff-so-fancy · built-in</sub>"]:::tool
175
+ Decision{good?}:::decision
176
+ Ship(["④ git commit + PR"]):::action
177
+ GitHub[("GitHub<br/>PRs · CI")]:::external
178
+
179
+ Dashboard ==>|implement| AI
180
+ Dashboard -. write context .-> Editor
181
+ Editor -. context · hand-edit .-> AI
182
+ AI ==> Pager
183
+ Pager ==> Decision
184
+ Decision -->|hand-edit| Editor
185
+ Decision -->|resume| AI
186
+ Decision ==>|ship| Ship
187
+ Ship ==> GitHub
188
+ GitHub -->|reviews · CI| AI
189
+ AI -->|push fix| Ship
190
+ end
191
+
192
+ classDef action fill:#dbeafe,stroke:#3b82f6,stroke-width:2px,color:#1e3a8a
193
+ classDef tool fill:#f3f4f6,stroke:#9ca3af,color:#374151
194
+ classDef agent fill:#ede9fe,stroke:#8b5cf6,stroke-width:2px,color:#4c1d95
195
+ classDef external fill:#fef3c7,stroke:#f59e0b,stroke-width:2px,color:#78350f
196
+ classDef decision fill:#fee2e2,stroke:#ef4444,stroke-width:2px,color:#7f1d1d
197
+ ```
198
+
199
+ Bold edges (`==>`) follow the happy path; thin edges (`-->`) are branches/loops; dotted edges (`-.->`) are optional editor side-trips. Colors group nodes by role: blue actions, purple agent, gray tools, yellow external service, red decision.
200
+
201
+ **Stages, in order:**
202
+
203
+ 1. **Pick + add context** — open the dashboard, browse assigned issues, pick one. The inline context box lets you add custom instructions to the prompt; `Ctrl+O` drops into your editor for longer prose.
204
+ 2. **AI agent (Claude)** — runs in the worktree's mux window with the rendered ticket + your context. Implements or plans.
205
+ 3. **Diff pager (review)** — `[v]` opens the diff overlay. From here you iterate: hand-edit in your editor, or resume the Claude session to keep going.
206
+ 4. **Ship** — once the diff looks right, `[C]` for inline commit + push, then `[c]` to create the PR (`--fill` runs Claude non-interactively against the PR template).
207
+ 5. **PR feedback loop** — `[f]` (fix) and `[r]` (self-review) re-launch the AI with PR comments + CI failures as input. Patches are pushed; CI re-runs; loop until merged.
208
+
209
+ **Supported tools per stage:**
210
+
211
+ | Stage | Supported today | Planned |
212
+ |---|---|---|
213
+ | **Issue tracker** (① Dashboard) | Linear, GitHub Issues | Jira, Shortcut, etc. |
214
+ | **AI agent** (②, ⑤) | Claude Code (`claude` CLI) | OpenAI Codex, OpenCode, Cursor agent |
215
+ | **Diff pager** (③) | delta, diff-so-fancy, any unified-diff pager — built-in colorizer when none set | — |
216
+ | **Editor** (side branch) | Anything taking a path: zed, nvim, jetbrains, cursor, vscode. cursor + vscode also handle `.code-workspace` files via `[E]` workspace shortcut | — |
217
+ | **Forge** (④) | GitHub via `gh` CLI | GitLab, Bitbucket, Gitea |
218
+ | **Terminal multiplexer** (outer frame) | tmux, cmux *(macOS-only — see [#1472](https://github.com/manaflow-ai/cmux/issues/1472))*, none | zellij |
219
+
220
+ The AI agent and issue tracker are already behind interfaces (`lib/trackers/`, plus the `claude` CLI shim in `lib/ai.ts`). Adding a new option in either category means writing one new module and wiring it into the factory — no changes to UI/dashboard code.
221
+
147
222
  ### Interactive Dashboard
148
223
 
149
- `santree dashboard` opens a full-screen TUI to manage all your work in one place. It shows your Linear issues grouped by project, with live status for worktrees, PRs, CI checks, and reviews.
224
+ `santree dashboard` opens a full-screen TUI to manage all your work in one place. It shows your assigned issues from the active tracker (Linear or GitHub Issues) grouped by project, with live status for worktrees, PRs, CI checks, and reviews.
150
225
 
151
226
  **Left pane** — issue list. Issues without worktrees show as a single row (priority + ID + title). Issues with worktrees expand into nested sub-rows below the title showing `· diff` (files/adds/deletes/commits-ahead), `· pr` (number, state, CI, review count), and `· session` (state + Claude session ID). Click any row (main or sub) to select; scroll wheel navigates; drag the divider to resize panes.
152
227
 
@@ -162,7 +237,7 @@ With the `stw` alias: `stw create`, `stw list`, `stw switch`, `stw work`, `stw c
162
237
  | `c` | Create PR (fill from commits or open in browser) |
163
238
  | `v` | Inline diff overlay — file tree + diff content (mouse + keyboard) |
164
239
  | `f` / `r` | Fix PR / Review PR (launches in tmux) |
165
- | `o` / `p` | Open Linear ticket / PR in browser |
240
+ | `o` / `p` | Open issue (in Linear or GitHub) / PR in browser |
166
241
  | `d` | Remove worktree |
167
242
 
168
243
  Commit, PR creation, and diff review happen inline without leaving the dashboard. Work, fix, and review open in new tmux windows.
@@ -177,9 +252,9 @@ Create isolated worktrees for each feature branch. No more stashing or committin
177
252
 
178
253
  See PR status directly in your worktree list. Clean up worktrees automatically when PRs are merged or closed.
179
254
 
180
- ### Linear Integration
255
+ ### Issue Tracker Integration
181
256
 
182
- Santree fetches Linear ticket data (title, description, comments, images) and injects it into prompts when running `santree worktree work`. See [Linear Integration](#linear-integration-1) for setup.
257
+ Santree fetches issue data (title, description, comments, images) from the active tracker — Linear or GitHub Issues — and injects it into prompts when running `santree worktree work`. See [Issue Tracker Integration](#issue-tracker-integration-1) for setup.
183
258
 
184
259
  ### Claude AI Integration
185
260
 
@@ -210,16 +285,46 @@ npm install
210
285
 
211
286
  ### Branch Naming
212
287
 
213
- For Linear integration, use branch names with ticket IDs:
288
+ Branch names need to encode the issue ID so santree can link the worktree back to the right ticket. The accepted format depends on the active tracker:
214
289
 
215
290
  ```
291
+ # Linear (uppercased letter prefix + dash + digits)
216
292
  user/TEAM-123-feature-description
217
293
  feature/PROJ-456-add-auth
294
+
295
+ # GitHub Issues (explicit prefix required, to avoid `fix-typo-1` matching)
296
+ feature/issue-42-add-auth
297
+ gh-42-add-auth
298
+ 42-add-auth
299
+ ```
300
+
301
+ GitHub's parser is strict on purpose: a commit-style branch like `fix-typo-1` will *not* match (no explicit prefix, no slash-led number).
302
+
303
+ ### Issue Tracker Integration
304
+
305
+ Each repo picks one tracker. The active tracker is resolved in this order: `SANTREE_TRACKER` env var → per-repo `_tracker.kind` (in `.santree/metadata.json`) → legacy `_linear.org` (treated as Linear) → auto-detect (any Linear creds → Linear, else GitHub).
306
+
307
+ #### Choosing a tracker
308
+
309
+ ```bash
310
+ # Explicitly pick the active tracker for this repo
311
+ santree issue switch linear
312
+ santree issue switch github
313
+
314
+ # Or let an auth command set it as a side effect:
315
+ santree linear auth # Sets _tracker.kind = "linear" after OAuth
316
+ santree github auth # Sets _tracker.kind = "github" after `gh auth login`
317
+ ```
318
+
319
+ For a one-off override (testing, scripting):
320
+
321
+ ```bash
322
+ SANTREE_TRACKER=github santree dashboard
218
323
  ```
219
324
 
220
- ### Linear Integration
325
+ #### Linear
221
326
 
222
- Santree fetches Linear ticket data (title, description, comments, images) and injects it into prompts when running `santree worktree work`.
327
+ Santree fetches Linear ticket data via the GraphQL API (OAuth PKCE).
223
328
 
224
329
  ```bash
225
330
  # Authenticate with Linear (opens browser for OAuth)
@@ -233,12 +338,26 @@ santree linear auth --test TEAM-123
233
338
 
234
339
  # Log out
235
340
  santree linear auth --logout
341
+
342
+ # Switch between authenticated workspaces
343
+ santree linear switch
236
344
  ```
237
345
 
238
346
  On first run, `santree linear auth` opens your browser to authorize the app with your Linear workspace. Tokens are stored in `$XDG_CONFIG_HOME/santree/auth.json` (defaults to `~/.config/santree/auth.json`) and auto-refresh transparently.
239
347
 
240
348
  If you have multiple workspaces authenticated, running `santree linear auth` in a new repo will let you pick which one to link. Images from tickets are downloaded to a temp directory and cleaned up after Claude exits.
241
349
 
350
+ #### GitHub Issues
351
+
352
+ Santree uses the existing `gh` CLI — no separate OAuth flow.
353
+
354
+ ```bash
355
+ # Verify gh is authenticated; flips this repo's tracker to GitHub
356
+ santree github auth
357
+ ```
358
+
359
+ Issues are listed via `gh search issues --assignee=@me --state=open --repo <owner>/<name>` (current repo only — cross-repo issues aren't surfaced today). Priority is derived from labels matching `P0`/`P1`/`P2`/`P3`/`urgent`/`critical`/`high`/`medium`/`low`, falling back to `No priority`. Attached images on `user-images.githubusercontent.com` and `github.com/.../assets/` are downloaded so Claude can read them when filling PR templates.
360
+
242
361
  ### Claude Code Statusline (Optional)
243
362
 
244
363
  Santree provides a custom statusline for Claude Code showing git info, model, context usage, and cost.
@@ -286,45 +405,11 @@ To preview the JSON without writing: `santree helpers session-signal install --d
286
405
 
287
406
  Verify with `santree doctor` — look for the "Session Signal Hooks" row under Claude Code.
288
407
 
289
- ### Session Hooks (Optional)
290
-
291
- Run custom scripts when Claude's session state changes. Create executable scripts in `.santree/hooks/`:
292
-
293
- ```
294
- .santree/hooks/
295
- on-waiting.sh # Runs when session needs permission approval
296
- on-active.sh # Runs when user submits a new prompt
297
- on-idle.sh # Runs when session finishes and waits for next prompt
298
- on-exited.sh # Runs when session ends
299
- ```
300
-
301
- Each script receives these environment variables:
302
-
303
- | Variable | Description |
304
- |----------|-------------|
305
- | `SANTREE_TICKET_ID` | e.g. `TEAM-123` |
306
- | `SANTREE_SESSION_STATE` | `waiting`, `active`, `idle`, or `exited` |
307
- | `SANTREE_SESSION_ID` | The Claude session ID |
308
- | `SANTREE_WORKTREE_PATH` | Absolute path to the worktree |
309
- | `SANTREE_REPO_ROOT` | Absolute path to the main repo |
310
- | `SANTREE_MESSAGE` | Notification message (only for `waiting`) |
311
-
312
- Scripts are optional — only executed if they exist and are executable. They run fire-and-forget with a 5-second timeout.
313
-
314
- **Example** — log when Claude is waiting for approval:
315
-
316
- ```bash
317
- #!/bin/bash
318
- # .santree/hooks/on-waiting.sh
319
- echo "$(date): $SANTREE_TICKET_ID waiting — $SANTREE_MESSAGE" >> /tmp/santree-hooks.log
320
- ```
321
-
322
- Make it executable: `chmod +x .santree/hooks/on-waiting.sh`
323
-
324
408
  ### Environment Variables
325
409
 
326
410
  | Variable | Effect |
327
411
  |---|---|
412
+ | `SANTREE_TRACKER` | Override the active issue tracker for a single invocation: `linear` or `github`. Takes precedence over the per-repo `_tracker.kind`. If unset, falls back to repo config → legacy `_linear.org` → auto-detect. |
328
413
  | `SANTREE_EDITOR` | Editor used by `e` (open in editor) actions in the dashboard. Defaults to `code`. Examples: `cursor`, `zed`, `code`, `nvim`. |
329
414
  | `SANTREE_MULTIPLEXER` | Terminal multiplexer used by the dashboard and `worktree create --window`. One of `tmux`, `cmux`, `none`. If unset, auto-detects from `$TMUX` / `$CMUX_SURFACE_ID`. cmux is macOS-only and limited by [manaflow-ai/cmux#1472](https://github.com/manaflow-ai/cmux/issues/1472). |
330
415
  | `SANTREE_DIFF_TOOL` | Pager used by `worktree diff` (CLI) and the dashboard diff overlay. Passed to git as `-c core.pager=<tool>` for the CLI, and used to pipe content for the overlay. Examples: `delta`, `diff-so-fancy`. Must accept a unified diff on stdin. Names are restricted to `[A-Za-z0-9_\-/.+]`. |
@@ -384,7 +469,7 @@ Shows a branch-only unified diff against the base branch's merge-base — same s
384
469
  | -------- | ------------------------------- |
385
470
  | `--plan` | Only create implementation plan |
386
471
 
387
- Automatically fetches Linear ticket data if authenticated. Degrades gracefully if not.
472
+ Automatically fetches issue data from the active tracker (Linear or GitHub Issues) if authenticated. Degrades gracefully if not.
388
473
 
389
474
  ### pr create
390
475
 
@@ -426,16 +511,16 @@ Santree currently locks in to specific providers for some integrations and is in
426
511
 
427
512
  | Area | Supported | Not yet supported |
428
513
  | --- | --- | --- |
429
- | **Ticket system** | Linear (via `santree linear auth`) | Jira, GitHub Issues, Shortcut, Asana, Linear-equivalents |
430
514
  | **Source control / forge** | GitHub (via `gh` CLI) | GitLab, Bitbucket, Gitea, Codeberg, self-hosted Forgejo |
431
515
  | **Coding agent** | Claude Code (`claude` CLI) | OpenAI Codex, OpenCode, Cursor agent, Aider, others |
432
516
 
433
- These are hardwired in `lib/linear.ts`, `lib/github.ts`, and `lib/ai.ts` respectively. Adding a second provider means abstracting an interface (similar to `lib/multiplexer/`) and wiring a selection mechanism.
517
+ These are hardwired in `lib/github.ts` and `lib/ai.ts` respectively. Adding a second provider means abstracting an interface (similar to `lib/trackers/` or `lib/multiplexer/`) and wiring a selection mechanism.
434
518
 
435
519
  ### Multi-provider (already interchangeable)
436
520
 
437
521
  | Area | How to switch | Examples |
438
522
  | --- | --- | --- |
523
+ | **Issue tracker** | `santree issue switch <linear\|github>` (or `SANTREE_TRACKER` env var, or as a side effect of `santree linear auth` / `santree github auth`) | Linear (OAuth + GraphQL), GitHub Issues (via `gh` CLI). Adding a third tracker = one new directory under `lib/trackers/`. |
439
524
  | **Editor** | `SANTREE_EDITOR` env var (or `--editor` flag on `worktree open`) | `code`, `cursor`, `zed`, `nvim`, `subl`, `webstorm` — any executable that takes a path argument |
440
525
  | **Terminal multiplexer** | `SANTREE_MULTIPLEXER` env var | `tmux` (default, all platforms), `cmux` (macOS only — see [#1472](https://github.com/manaflow-ai/cmux/issues/1472)), `none`. Zellij is planned but not implemented. |
441
526
  | **Diff renderer** | `SANTREE_DIFF_TOOL` env var | `delta`, `diff-so-fancy`, or any pager that accepts a unified diff. Falls back to plain `git diff` colorization when unset. |
@@ -502,11 +587,17 @@ source/
502
587
  ├── cli.tsx # Entry point (Pastel app runner)
503
588
  ├── lib/
504
589
  │ ├── ai.ts # Shared AI logic (context, prompt, launch)
505
- │ ├── git.ts # Git helpers (worktrees, branches, metadata)
590
+ │ ├── git.ts # Git helpers (worktrees, branches); extractTicketId is a tracker shim
506
591
  │ ├── github.ts # GitHub CLI wrapper (PR info, auth, push, checks, reviews)
507
- │ ├── linear.ts # Linear GraphQL API client (OAuth, tickets, images)
508
592
  │ ├── exec.ts # Shell command helpers
593
+ │ ├── metadata.ts # .santree/metadata.json r/w (extracted to break import cycles)
509
594
  │ ├── prompts.ts # Nunjucks template renderer
595
+ │ ├── trackers/ # Issue tracker abstraction (Linear, GitHub Issues)
596
+ │ │ ├── types.ts # IssueTracker interface + generic Issue/AssignedIssue types
597
+ │ │ ├── index.ts # getIssueTracker(repoRoot) factory
598
+ │ │ ├── linear/ # OAuth PKCE + GraphQL + image rewriter
599
+ │ │ └── github/ # `gh` CLI wrappers; priority derived from labels
600
+ │ ├── multiplexer/ # tmux/cmux/none abstraction (windows/sessions)
510
601
  │ └── dashboard/ # Dashboard UI components
511
602
  │ ├── types.ts # State types, action types, phase enums
512
603
  │ ├── IssueList.tsx # Left pane — issue list with priority, session, PR, CI columns
@@ -516,7 +607,9 @@ source/
516
607
  ├── dashboard.tsx # Top-level: interactive dashboard
517
608
  ├── worktree/ # Worktree management (create, list, switch, etc.)
518
609
  ├── pr/ # PR lifecycle (create, open, fix, review)
519
- ├── linear/ # Linear integration (auth, open)
610
+ ├── linear/ # Linear-specific OAuth (auth, switch)
611
+ ├── github/ # GitHub-specific auth (gh wrapper)
612
+ ├── issue/ # Tracker-agnostic actions (switch, open)
520
613
  └── helpers/ # Shell init, statusline
521
614
  prompts/ # Nunjucks templates: implement, plan, review, fix-pr, fill-pr, ticket
522
615
  shell/ # Shell integration templates: init.zsh.njk, init.bash.njk
@@ -1,2 +1,2 @@
1
- export declare const description = "Interactive dashboard of your Linear issues";
1
+ export declare const description = "Interactive dashboard of your assigned issues";
2
2
  export default function Dashboard(): import("react/jsx-runtime").JSX.Element;
@@ -17,7 +17,7 @@ import { extractTicketId } from "../lib/git.js";
17
17
  import { getMultiplexer } from "../lib/multiplexer/index.js";
18
18
  import { getPRTemplate } from "../lib/github.js";
19
19
  import { renderPrompt, renderDiff, renderTicket } from "../lib/prompts.js";
20
- import { getTicketContent } from "../lib/linear.js";
20
+ import { getIssueTracker } from "../lib/trackers/index.js";
21
21
  import * as os from "os";
22
22
  import { initialState, reducer } from "../lib/dashboard/types.js";
23
23
  import { loadDashboardData, loadReviewsData } from "../lib/dashboard/data.js";
@@ -30,7 +30,7 @@ import { CommitOverlay, PrCreateOverlay } from "../lib/dashboard/Overlays.js";
30
30
  import { MultilineTextArea } from "../lib/dashboard/MultilineTextArea.js";
31
31
  import DiffOverlay, { flattenTreeFiles, computeDiffLayout, clampDiffLeftWidth, DIFF_DIVIDER_WIDTH, } from "../lib/dashboard/DiffOverlay.js";
32
32
  import { CURRENT_VERSION, CLAUDE_CODE_PACKAGE, getLatestVersion, getCachedLatestVersion, getLatestVersionFor, getCachedLatestVersionFor, isUpdateAvailable, } from "../lib/version.js";
33
- export const description = "Interactive dashboard of your Linear issues";
33
+ export const description = "Interactive dashboard of your assigned issues";
34
34
  const execAsync = promisify(exec);
35
35
  // Resolved at module load — cheap. Honors cmux's bundled binary when running
36
36
  // inside cmux so the header reflects the binary santree will actually use.
@@ -104,6 +104,8 @@ function runPipedDiff(cwd, gitArgs, tool, themeMode) {
104
104
  GIT_CONFIG_PARAMETERS: "'delta.hyperlinks=false' 'delta.line-numbers=false'",
105
105
  }
106
106
  : process.env;
107
+ // We use the pager only for its rendering — the dashboard owns
108
+ // scrolling/search itself in Ink, so we capture stdout as a string.
107
109
  const pager = spawn(tool, pagerArgs, {
108
110
  stdio: ["pipe", "pipe", "pipe"],
109
111
  env: pagerEnv,
@@ -797,13 +799,13 @@ export default function Dashboard() {
797
799
  type: "SET_ACTION_MESSAGE",
798
800
  message: resumeCmd
799
801
  ? `Resumed session in new window: ${windowName}`
800
- : `Launched ${mode} in ${mux.kind} window: ${windowName}`,
802
+ : `Launched ${mode} in new window: ${windowName}`,
801
803
  });
802
804
  }
803
805
  else {
804
806
  dispatch({
805
807
  type: "SET_ACTION_MESSAGE",
806
- message: `Failed to create ${mux.kind} window${created.message ? `: ${created.message}` : ""}`,
808
+ message: `Failed to create window${created.message ? `: ${created.message}` : ""}`,
807
809
  });
808
810
  }
809
811
  }
@@ -832,7 +834,7 @@ export default function Dashboard() {
832
834
  else {
833
835
  dispatch({
834
836
  type: "SET_ACTION_MESSAGE",
835
- message: `Worktree created, but ${mux.kind} failed${created.message ? `: ${created.message}` : ""}`,
837
+ message: `Worktree created, but window launch failed${created.message ? `: ${created.message}` : ""}`,
836
838
  });
837
839
  }
838
840
  setTimeout(() => refresh(), 3000);
@@ -1161,12 +1163,14 @@ export default function Dashboard() {
1161
1163
  dispatch({ type: "PR_CREATE_PHASE", phase: "filling" });
1162
1164
  const ticketId = extractTicketId(s.prCreateBranch) ?? "";
1163
1165
  const mainRepoRoot = findMainRepoRoot();
1164
- // Fetch ticket content (downloads images for Linear tickets)
1166
+ // Fetch issue content from the active tracker (downloads images
1167
+ // inline so Claude can read them via --allowedTools Read).
1165
1168
  let ticketContent;
1166
1169
  if (ticketId && mainRepoRoot) {
1167
- const ticket = await getTicketContent(ticketId, mainRepoRoot);
1168
- if (ticket) {
1169
- ticketContent = renderTicket(ticket);
1170
+ const tracker = getIssueTracker(mainRepoRoot);
1171
+ const result = await tracker.getIssue(ticketId, mainRepoRoot);
1172
+ if (result.ok) {
1173
+ ticketContent = renderTicket(result.value, tracker.displayName);
1170
1174
  }
1171
1175
  }
1172
1176
  const commitLog = run(`git log ${base}..HEAD --format="- %s"`, { cwd }) || null;
@@ -1823,7 +1827,7 @@ export default function Dashboard() {
1823
1827
  dispatch({
1824
1828
  type: "SET_ACTION_MESSAGE",
1825
1829
  message: created.ok
1826
- ? `Launched AI review in ${mux.kind}`
1830
+ ? "Launched AI review in new window"
1827
1831
  : `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
1828
1832
  });
1829
1833
  })();
@@ -1928,7 +1932,7 @@ export default function Dashboard() {
1928
1932
  if (!created.ok) {
1929
1933
  dispatch({
1930
1934
  type: "SET_ACTION_MESSAGE",
1931
- message: `Failed to switch ${mux.kind} window${created.message ? `: ${created.message}` : ""}`,
1935
+ message: `Failed to switch window${created.message ? `: ${created.message}` : ""}`,
1932
1936
  });
1933
1937
  }
1934
1938
  })();
@@ -1940,10 +1944,10 @@ export default function Dashboard() {
1940
1944
  }
1941
1945
  return;
1942
1946
  }
1943
- // Open in Linear
1947
+ // Open issue in tracker (Linear/GitHub web UI)
1944
1948
  if (input === "o") {
1945
1949
  if (!di.issue.url) {
1946
- dispatch({ type: "SET_ACTION_MESSAGE", message: "No Linear ticket URL" });
1950
+ dispatch({ type: "SET_ACTION_MESSAGE", message: "No issue URL available" });
1947
1951
  return;
1948
1952
  }
1949
1953
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
@@ -1999,7 +2003,7 @@ export default function Dashboard() {
1999
2003
  dispatch({
2000
2004
  type: "SET_ACTION_MESSAGE",
2001
2005
  message: created.ok
2002
- ? `Launched review in ${mux.kind}`
2006
+ ? "Launched review in new window"
2003
2007
  : `Failed to launch review${created.message ? `: ${created.message}` : ""}`,
2004
2008
  });
2005
2009
  })();
@@ -2066,7 +2070,7 @@ export default function Dashboard() {
2066
2070
  dispatch({
2067
2071
  type: "SET_ACTION_MESSAGE",
2068
2072
  message: created.ok
2069
- ? `Launched PR fix in ${mux.kind}`
2073
+ ? "Launched PR fix in new window"
2070
2074
  : `Failed to launch PR fix${created.message ? `: ${created.message}` : ""}`,
2071
2075
  });
2072
2076
  })();
@@ -2127,14 +2131,14 @@ export default function Dashboard() {
2127
2131
  }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "j/k to navigate, Enter to select, ESC to cancel" })] }) })) : state.overlay === "confirm-delete" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "red", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "red", children: "Remove worktree?" }), _jsx(Text, { children: " " }), _jsx(Text, { children: selectedIssue?.worktree?.branch ?? "" }), selectedIssue?.worktree?.dirty && (_jsx(Text, { color: "yellow", children: "Warning: worktree has uncommitted changes" })), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "red", bold: true, children: "y" }), " Confirm"] }), _jsxs(Text, { children: [_jsx(Text, { color: "cyan", bold: true, children: "n" }), " Cancel"] })] }) })) : state.overlay === "diff" ? (_jsx(DiffOverlay, { width: innerWidth, height: contentHeight, ticketId: state.diffTicketId ?? "", baseBranch: state.diffBaseBranch ?? "", files: state.diffFiles, fileIndex: state.diffFileIndex, fileScrollOffset: state.diffFileScrollOffset, content: state.diffContent, contentScrollOffset: state.diffContentScrollOffset, loadingFiles: state.diffLoadingFiles, loadingContent: state.diffLoadingContent, error: state.diffError, selectionBg: theme.selectionBg, leftWidthOverride: diffLeftWidth ?? undefined, pendingDiscard: state.diffPendingDiscard })) : state.overlay === "confirm-setup" ? (_jsx(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, children: "Run setup script?" }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: ".santree/init.sh" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " Run setup"] }), _jsxs(Text, { children: [_jsx(Text, { color: "yellow", bold: true, children: "n" }), " Skip"] })] }) })) : (_jsxs(Box, { flexGrow: 1, children: [_jsx(Box, { width: leftWidth, children: state.activeTab === "reviews" ? (_jsx(ReviewList, { flatReviews: state.flatReviews, selectedIndex: state.reviewSelectedIndex, scrollOffset: state.reviewListScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) : state.flatIssues.length === 0 ? (_jsx(Box, { width: leftWidth, height: contentHeight, justifyContent: "center", alignItems: "center", children: _jsx(Text, { color: "yellow", children: "No active issues" }) })) : (_jsx(IssueList, { groups: state.groups, flatIssues: state.flatIssues, selectedIndex: state.selectedIndex, scrollOffset: state.listScrollOffset, height: contentHeight, width: leftWidth, selectionBg: theme.selectionBg })) }), _jsx(Box, { flexDirection: "column", width: 3, children: Array.from({ length: contentHeight }).map((_, i) => (_jsx(Text, { dimColor: true, children: " │ " }, i))) }), _jsx(Box, { width: rightWidth, children: state.activeTab === "reviews" && state.creatingForTicket ? (_jsxs(Box, { flexDirection: "column", width: rightWidth, height: contentHeight, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", state.creatingForTicket, "..."] }), state.creationLogs
2128
2132
  .split("\n")
2129
2133
  .slice(-(contentHeight - 1))
2130
- .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay }) })] })) })] })] }));
2134
+ .map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] })) : state.activeTab === "reviews" ? (_jsx(ReviewDetailPanel, { item: selectedReview, scrollOffset: state.reviewDetailScrollOffset, height: contentHeight, width: rightWidth })) : state.overlay === "commit" ? (_jsx(CommitOverlay, { width: rightWidth, height: contentHeight, branch: state.commitBranch, ticketId: state.commitTicketId, gitStatus: state.commitGitStatus, phase: state.commitPhase, message: state.commitMessage, error: state.commitError, dispatch: dispatch, onSubmit: handleCommitSubmit })) : state.overlay === "pr-create" ? (_jsx(PrCreateOverlay, { width: rightWidth, height: contentHeight, branch: state.prCreateBranch, ticketId: state.prCreateTicketId, phase: state.prCreatePhase, error: state.prCreateError, url: state.prCreateUrl, body: state.prCreateBody, title: state.prCreateTitle, dispatch: dispatch })) : (_jsx(DetailPanel, { issue: selectedIssue, scrollOffset: state.detailScrollOffset, height: contentHeight, width: rightWidth, creatingForTicket: state.creatingForTicket, creationLogs: state.creationLogs })) })] })), _jsx(Box, { children: state.overlay === "diff" ? (_jsx(Box, { width: innerWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "diff" }) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { width: leftWidth + separatorWidth, paddingX: 1, children: _jsx(CommandBar, { showWorkspace: hasWorkspaceFile, mode: "default" }) }), _jsx(Box, { width: rightWidth, children: _jsx(ActionRow, { activeTab: state.activeTab, selectedIssue: selectedIssue, selectedReview: selectedReview, overlay: state.overlay, trackerName: getIssueTracker(repoRootRef.current).displayName }) })] })) })] })] }));
2131
2135
  }
2132
2136
  /**
2133
2137
  * Renders the per-issue action key hints (Resume / Editor / View diff / …)
2134
2138
  * lifted out of the detail panels so they sit on the same row as the global
2135
2139
  * command bar. Empty when nothing is selected.
2136
2140
  */
2137
- function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, }) {
2141
+ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, trackerName, }) {
2138
2142
  // During the diff overlay, none of the per-issue actions apply (View diff
2139
2143
  // is what got us here, Commit/PR/etc. need the detail panel context). Keep
2140
2144
  // the row blank so the diff-specific CommandBar reads cleanly.
@@ -2145,7 +2149,7 @@ function ActionRow({ activeTab, selectedIssue, selectedReview, overlay, }) {
2145
2149
  ? buildReviewActions(selectedReview)
2146
2150
  : []
2147
2151
  : selectedIssue
2148
- ? buildIssueActions(selectedIssue)
2152
+ ? buildIssueActions(selectedIssue, trackerName)
2149
2153
  : [];
2150
2154
  if (items.length === 0)
2151
2155
  return _jsx(Text, { children: " " });