santree 0.5.3 → 0.5.5
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.
- package/README.md +156 -46
- package/dist/commands/dashboard.d.ts +1 -1
- package/dist/commands/dashboard.js +22 -18
- package/dist/commands/doctor.js +97 -76
- package/dist/commands/github/auth.d.ts +2 -0
- package/dist/commands/github/auth.js +56 -0
- package/dist/commands/github/index.d.ts +1 -0
- package/dist/commands/github/index.js +1 -0
- package/dist/commands/helpers/english-tutor/index.d.ts +1 -0
- package/dist/commands/helpers/english-tutor/index.js +1 -0
- package/dist/commands/helpers/english-tutor/install.d.ts +8 -0
- package/dist/commands/helpers/english-tutor/install.js +24 -0
- package/dist/commands/helpers/english-tutor/prompt.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/prompt.js +16 -0
- package/dist/commands/helpers/english-tutor/session-start.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/session-start.js +34 -0
- package/dist/commands/helpers/english-tutor/uninstall.d.ts +2 -0
- package/dist/commands/helpers/english-tutor/uninstall.js +15 -0
- package/dist/commands/helpers/template.d.ts +1 -0
- package/dist/commands/helpers/template.js +13 -10
- package/dist/commands/issue/index.d.ts +1 -0
- package/dist/commands/issue/index.js +1 -0
- package/dist/commands/issue/open.d.ts +2 -0
- package/dist/commands/{linear → issue}/open.js +13 -11
- package/dist/commands/issue/switch.d.ts +11 -0
- package/dist/commands/issue/switch.js +38 -0
- package/dist/commands/linear/auth.js +23 -10
- package/dist/commands/linear/switch.js +7 -3
- package/dist/commands/pr/create.js +7 -5
- package/dist/commands/worktree/create.js +4 -6
- package/dist/commands/worktree/work.js +1 -1
- package/dist/lib/ai.d.ts +8 -6
- package/dist/lib/ai.js +29 -15
- package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
- package/dist/lib/dashboard/DetailPanel.js +6 -3
- package/dist/lib/dashboard/data.js +17 -9
- package/dist/lib/dashboard/types.d.ts +3 -16
- package/dist/lib/english-tutor.d.ts +13 -0
- package/dist/lib/english-tutor.js +125 -0
- package/dist/lib/git.d.ts +16 -33
- package/dist/lib/git.js +20 -74
- package/dist/lib/metadata.d.ts +3 -0
- package/dist/lib/metadata.js +27 -0
- package/dist/lib/multiplexer/cmux.js +1 -1
- package/dist/lib/multiplexer/index.js +5 -12
- package/dist/lib/multiplexer/types.d.ts +1 -1
- package/dist/lib/prompts.d.ts +4 -3
- package/dist/lib/prompts.js +4 -3
- package/dist/lib/session-signal.d.ts +2 -3
- package/dist/lib/session-signal.js +3 -29
- package/dist/lib/trackers/auth-store.d.ts +16 -0
- package/dist/lib/trackers/auth-store.js +57 -0
- package/dist/lib/trackers/config.d.ts +8 -0
- package/dist/lib/trackers/config.js +21 -0
- package/dist/lib/trackers/github/api.d.ts +3 -0
- package/dist/lib/trackers/github/api.js +90 -0
- package/dist/lib/trackers/github/auth.d.ts +5 -0
- package/dist/lib/trackers/github/auth.js +27 -0
- package/dist/lib/trackers/github/images.d.ts +2 -0
- package/dist/lib/trackers/github/images.js +42 -0
- package/dist/lib/trackers/github/index.d.ts +2 -0
- package/dist/lib/trackers/github/index.js +78 -0
- package/dist/lib/trackers/index.d.ts +12 -0
- package/dist/lib/trackers/index.js +34 -0
- package/dist/lib/trackers/linear/api.d.ts +4 -0
- package/dist/lib/trackers/linear/api.js +128 -0
- package/dist/lib/trackers/linear/auth.d.ts +11 -0
- package/dist/lib/trackers/linear/auth.js +206 -0
- package/dist/lib/trackers/linear/images.d.ts +2 -0
- package/dist/lib/trackers/linear/images.js +44 -0
- package/dist/lib/trackers/linear/index.d.ts +3 -0
- package/dist/lib/trackers/linear/index.js +100 -0
- package/dist/lib/trackers/types.d.ts +52 -0
- package/dist/lib/trackers/types.js +1 -0
- package/package.json +1 -1
- package/prompts/english-tutor-prompt.njk +15 -0
- package/prompts/ticket.njk +3 -3
- package/dist/commands/linear/open.d.ts +0 -2
- package/dist/lib/linear.d.ts +0 -83
- 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
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
###
|
|
255
|
+
### Issue Tracker Integration
|
|
181
256
|
|
|
182
|
-
Santree fetches
|
|
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
|
-
|
|
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
|
|
218
299
|
```
|
|
219
300
|
|
|
220
|
-
|
|
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
|
+
```
|
|
221
318
|
|
|
222
|
-
|
|
319
|
+
For a one-off override (testing, scripting):
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
SANTREE_TRACKER=github santree dashboard
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
#### Linear
|
|
326
|
+
|
|
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,47 +405,30 @@ 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
|
-
###
|
|
408
|
+
### English Tutor (Optional)
|
|
290
409
|
|
|
291
|
-
|
|
410
|
+
When enabled, Claude Code spots grammar mistakes in your prompts and replies with a one-line correction (`original -> correction (reason)`) before doing the actual work. Mistake-free prompts are ignored. Useful if English isn't your first language and you'd like ambient feedback while you code.
|
|
292
411
|
|
|
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:
|
|
412
|
+
**Install:**
|
|
302
413
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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`) |
|
|
414
|
+
```bash
|
|
415
|
+
santree helpers english-tutor install
|
|
416
|
+
```
|
|
311
417
|
|
|
312
|
-
|
|
418
|
+
This adds two hooks (`UserPromptSubmit`, `SessionStart`) and a scoped `Edit` permission for the practice log to `~/.claude/settings.json`. Existing hooks and permissions are preserved.
|
|
313
419
|
|
|
314
|
-
|
|
420
|
+
To preview the JSON without writing: `santree helpers english-tutor install --dry`
|
|
315
421
|
|
|
316
|
-
|
|
317
|
-
#!/bin/bash
|
|
318
|
-
# .santree/hooks/on-waiting.sh
|
|
319
|
-
echo "$(date): $SANTREE_TICKET_ID waiting — $SANTREE_MESSAGE" >> /tmp/santree-hooks.log
|
|
320
|
-
```
|
|
422
|
+
Corrections are appended to `~/.config/santree/english-practice-log.md` (or `$XDG_CONFIG_HOME/santree/english-practice-log.md`). The `SessionStart` hook replays this log into context on new sessions so Claude can spot recurring patterns.
|
|
321
423
|
|
|
322
|
-
|
|
424
|
+
Remove with `santree helpers english-tutor uninstall`.
|
|
323
425
|
|
|
324
426
|
### Environment Variables
|
|
325
427
|
|
|
326
428
|
| Variable | Effect |
|
|
327
429
|
|---|---|
|
|
430
|
+
| `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
431
|
| `SANTREE_EDITOR` | Editor used by `e` (open in editor) actions in the dashboard. Defaults to `code`. Examples: `cursor`, `zed`, `code`, `nvim`. |
|
|
329
|
-
| `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
432
|
| `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_\-/.+]`. |
|
|
331
433
|
| `SANTREE_THEME` | Dashboard color theme: `light`, `dark`, or `auto` (default). In `auto` mode, santree queries the terminal's background via OSC 11 and re-detects on each refresh cycle (≤30s) so theme switches propagate automatically. Set explicitly when your terminal doesn't respond to OSC 11. |
|
|
332
434
|
|
|
@@ -384,7 +486,7 @@ Shows a branch-only unified diff against the base branch's merge-base — same s
|
|
|
384
486
|
| -------- | ------------------------------- |
|
|
385
487
|
| `--plan` | Only create implementation plan |
|
|
386
488
|
|
|
387
|
-
Automatically fetches Linear
|
|
489
|
+
Automatically fetches issue data from the active tracker (Linear or GitHub Issues) if authenticated. Degrades gracefully if not.
|
|
388
490
|
|
|
389
491
|
### pr create
|
|
390
492
|
|
|
@@ -426,18 +528,18 @@ Santree currently locks in to specific providers for some integrations and is in
|
|
|
426
528
|
|
|
427
529
|
| Area | Supported | Not yet supported |
|
|
428
530
|
| --- | --- | --- |
|
|
429
|
-
| **Ticket system** | Linear (via `santree linear auth`) | Jira, GitHub Issues, Shortcut, Asana, Linear-equivalents |
|
|
430
531
|
| **Source control / forge** | GitHub (via `gh` CLI) | GitLab, Bitbucket, Gitea, Codeberg, self-hosted Forgejo |
|
|
431
532
|
| **Coding agent** | Claude Code (`claude` CLI) | OpenAI Codex, OpenCode, Cursor agent, Aider, others |
|
|
432
533
|
|
|
433
|
-
These are hardwired in `lib/
|
|
534
|
+
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
535
|
|
|
435
536
|
### Multi-provider (already interchangeable)
|
|
436
537
|
|
|
437
538
|
| Area | How to switch | Examples |
|
|
438
539
|
| --- | --- | --- |
|
|
540
|
+
| **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
541
|
| **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
|
-
| **Terminal multiplexer** | `
|
|
542
|
+
| **Terminal multiplexer** | Auto-detected via `$TMUX` / `$CMUX_SURFACE_ID`. No config needed — each adapter's `isActive()` declares its own runtime check. | `tmux` (all platforms), `cmux` (macOS only — see [#1472](https://github.com/manaflow-ai/cmux/issues/1472)). Zellij is planned but not implemented. |
|
|
441
543
|
| **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. |
|
|
442
544
|
| **Color theme** | `SANTREE_THEME` env var (`light`/`dark`/`auto`) | Auto-detects via OSC 11; manual override available. Re-detects every refresh so theme switches show up within 30s. |
|
|
443
545
|
| **Shell integration** | `santree helpers shell-init` detects the shell | `zsh`, `bash` (templates in `shell/init.{zsh,bash}.njk`) |
|
|
@@ -502,11 +604,17 @@ source/
|
|
|
502
604
|
├── cli.tsx # Entry point (Pastel app runner)
|
|
503
605
|
├── lib/
|
|
504
606
|
│ ├── ai.ts # Shared AI logic (context, prompt, launch)
|
|
505
|
-
│ ├── git.ts # Git helpers (worktrees, branches
|
|
607
|
+
│ ├── git.ts # Git helpers (worktrees, branches); extractTicketId is a tracker shim
|
|
506
608
|
│ ├── github.ts # GitHub CLI wrapper (PR info, auth, push, checks, reviews)
|
|
507
|
-
│ ├── linear.ts # Linear GraphQL API client (OAuth, tickets, images)
|
|
508
609
|
│ ├── exec.ts # Shell command helpers
|
|
610
|
+
│ ├── metadata.ts # .santree/metadata.json r/w (extracted to break import cycles)
|
|
509
611
|
│ ├── prompts.ts # Nunjucks template renderer
|
|
612
|
+
│ ├── trackers/ # Issue tracker abstraction (Linear, GitHub Issues)
|
|
613
|
+
│ │ ├── types.ts # IssueTracker interface + generic Issue/AssignedIssue types
|
|
614
|
+
│ │ ├── index.ts # getIssueTracker(repoRoot) factory
|
|
615
|
+
│ │ ├── linear/ # OAuth PKCE + GraphQL + image rewriter
|
|
616
|
+
│ │ └── github/ # `gh` CLI wrappers; priority derived from labels
|
|
617
|
+
│ ├── multiplexer/ # tmux/cmux/none abstraction (windows/sessions)
|
|
510
618
|
│ └── dashboard/ # Dashboard UI components
|
|
511
619
|
│ ├── types.ts # State types, action types, phase enums
|
|
512
620
|
│ ├── IssueList.tsx # Left pane — issue list with priority, session, PR, CI columns
|
|
@@ -516,7 +624,9 @@ source/
|
|
|
516
624
|
├── dashboard.tsx # Top-level: interactive dashboard
|
|
517
625
|
├── worktree/ # Worktree management (create, list, switch, etc.)
|
|
518
626
|
├── pr/ # PR lifecycle (create, open, fix, review)
|
|
519
|
-
├── linear/ # Linear
|
|
627
|
+
├── linear/ # Linear-specific OAuth (auth, switch)
|
|
628
|
+
├── github/ # GitHub-specific auth (gh wrapper)
|
|
629
|
+
├── issue/ # Tracker-agnostic actions (switch, open)
|
|
520
630
|
└── helpers/ # Shell init, statusline
|
|
521
631
|
prompts/ # Nunjucks templates: implement, plan, review, fix-pr, fill-pr, ticket
|
|
522
632
|
shell/ # Shell integration templates: init.zsh.njk, init.bash.njk
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const description = "Interactive dashboard of your
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
-
?
|
|
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
|
-
?
|
|
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: " " });
|