mintree 0.1.11 → 0.2.0
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 +74 -17
- package/dist/commands/dashboard.js +100 -46
- package/dist/commands/doctor.js +62 -19
- package/dist/commands/init.d.ts +14 -1
- package/dist/commands/init.js +72 -13
- package/dist/commands/worktree/clean.js +2 -19
- package/dist/commands/worktree/create.js +3 -2
- package/dist/commands/worktree/list.js +10 -22
- package/dist/commands/worktree/work.js +5 -4
- package/dist/lib/branch.d.ts +7 -4
- package/dist/lib/branch.js +15 -7
- package/dist/lib/dashboard.d.ts +5 -42
- package/dist/lib/dashboard.js +33 -189
- package/dist/lib/gh.d.ts +16 -0
- package/dist/lib/{github.js → gh.js} +9 -0
- package/dist/lib/metadata.d.ts +15 -0
- package/dist/lib/metadata.js +51 -0
- package/dist/lib/pr.d.ts +26 -0
- package/dist/lib/pr.js +49 -0
- package/dist/lib/providers/github.d.ts +33 -0
- package/dist/lib/providers/github.js +381 -0
- package/dist/lib/providers/index.d.ts +27 -0
- package/dist/lib/providers/index.js +83 -0
- package/dist/lib/providers/plane.d.ts +61 -0
- package/dist/lib/providers/plane.js +749 -0
- package/dist/lib/providers/types.d.ts +113 -0
- package/dist/lib/providers/types.js +12 -0
- package/dist/lib/session-signal.d.ts +3 -2
- package/dist/lib/session-signal.js +4 -3
- package/dist/lib/worktreeCreate.js +4 -1
- package/package.json +1 -1
- package/dist/lib/github.d.ts +0 -7
- package/dist/lib/githubProject.d.ts +0 -55
- package/dist/lib/githubProject.js +0 -277
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
mintree wraps the steps you do manually every time a feature begins:
|
|
6
6
|
|
|
7
|
-
1. Pick an issue assigned to you
|
|
8
|
-
2. Create a git worktree on a branch named after that
|
|
7
|
+
1. Pick an open issue or work item assigned to you (GitHub Issues or Plane).
|
|
8
|
+
2. Create a git worktree on a branch named after that item, following the project's convention.
|
|
9
9
|
3. Launch Claude Code inside the worktree with a session ID you can resume later.
|
|
10
10
|
4. Live-track which Claude sessions are active, idle, or waiting.
|
|
11
11
|
|
|
12
|
-
It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to
|
|
12
|
+
It is a smaller, opinionated cousin of [santree](https://github.com/santiagotoscanini/santree) — built on the same TypeScript + Ink + Pastel stack but stripped to the `<type>/<issue>-<desc>` branch convention and the two issue trackers most likely to be used by a small team: GitHub Issues and Plane.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -48,7 +48,7 @@ EOF
|
|
|
48
48
|
|
|
49
49
|
`mintree doctor` should report **all required checks pass** before you continue. The most common gaps are:
|
|
50
50
|
|
|
51
|
-
- `gh` not authenticated → `gh auth login`
|
|
51
|
+
- `gh` not authenticated → `gh auth login` (still needed when using Plane — mintree uses `gh` for PR status on worktree branches)
|
|
52
52
|
- Claude Code not installed → `npm install -g @anthropic-ai/claude-code`
|
|
53
53
|
- Shell integration not loaded → re-run the `echo … >> ~/.zshrc` step and start a new shell
|
|
54
54
|
|
|
@@ -60,12 +60,58 @@ In every repository where you want to use mintree:
|
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
62
|
cd path/to/repo
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
|
|
64
|
+
# Default: GitHub Issues provider
|
|
65
|
+
mintree init
|
|
66
|
+
|
|
67
|
+
# Or: Plane provider
|
|
68
|
+
mintree init --provider plane --workspace <your-workspace-slug>
|
|
69
|
+
|
|
70
|
+
mintree helpers session-signal install # optional: live session state in the dashboard
|
|
65
71
|
```
|
|
66
72
|
|
|
67
73
|
`init` is idempotent — re-running it is a no-op when everything is already in place.
|
|
68
74
|
|
|
75
|
+
### Picking the issue provider
|
|
76
|
+
|
|
77
|
+
mintree supports two issue providers, selected per repo via `.mintree/metadata.json`:
|
|
78
|
+
|
|
79
|
+
- **`github`** (default): lists issues assigned to you on the current repo via `gh`. Transitions to "In Progress" on a Projects v2 board when present.
|
|
80
|
+
- **`plane`**: lists work items assigned to you across a configured set of [Plane](https://plane.so) projects, via the Plane REST API. Transitions to the project's "started" state on `w`.
|
|
81
|
+
|
|
82
|
+
After `mintree init --provider plane`, edit `.mintree/metadata.json` to add at least one project:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"version": 1,
|
|
87
|
+
"provider": "plane",
|
|
88
|
+
"issues": {},
|
|
89
|
+
"plane": {
|
|
90
|
+
"apiUrl": "https://api.plane.so",
|
|
91
|
+
"workspaceSlug": "my-team",
|
|
92
|
+
"projects": [
|
|
93
|
+
{ "id": "<project-uuid>", "identifier": "BACK", "name": "Backend" },
|
|
94
|
+
{ "id": "<project-uuid>", "identifier": "WEB", "name": "Web" }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
You can find each project's UUID in the Plane URL (`app.plane.so/<workspace>/projects/<uuid>/...`). The `identifier` is the short prefix shown on work-item IDs (the `BACK` in `BACK-100`).
|
|
101
|
+
|
|
102
|
+
Authenticate by setting `PLANE_API_KEY` in your shell, or by writing the key to `~/.mintree/credentials.json`:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
export PLANE_API_KEY=plane_api_XXXXXXXXXXXXXX
|
|
106
|
+
# or
|
|
107
|
+
cat > ~/.mintree/credentials.json <<'EOF'
|
|
108
|
+
{ "plane": { "apiKey": "plane_api_XXXXXXXXXXXXXX" } }
|
|
109
|
+
EOF
|
|
110
|
+
chmod 600 ~/.mintree/credentials.json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`mintree doctor` validates the key + each configured project's reachability when `provider === "plane"`.
|
|
114
|
+
|
|
69
115
|
---
|
|
70
116
|
|
|
71
117
|
## Daily flow
|
|
@@ -76,15 +122,15 @@ mintree helpers session-signal install # optional: live session state in
|
|
|
76
122
|
mintree dashboard
|
|
77
123
|
```
|
|
78
124
|
|
|
79
|
-
Opens a full-screen TUI listing your assigned open issues, each row marked with the live state of its Claude session (`● active`, `! waiting`, `○ idle`, `— exited`, `· no session`). The right pane shows the issue body, labels, worktree info, PR status, and live session message.
|
|
125
|
+
Opens a full-screen TUI listing your assigned open issues (or work items), each row marked with the live state of its Claude session (`● active`, `! waiting`, `○ idle`, `— exited`, `· no session`). Rows are grouped by project board and Status. The right pane shows the issue body, labels, worktree info, PR status, and live session message.
|
|
80
126
|
|
|
81
127
|
| Shortcut | Action |
|
|
82
128
|
|----------|-----------------------------------------------------------------------|
|
|
83
129
|
| `↑/↓` or `j/k` | Move between issues |
|
|
84
130
|
| `↵` | Resume Claude in the existing worktree, or open the create overlay if there's none |
|
|
85
131
|
| `w` | Always open the create overlay (type + kebab description) |
|
|
86
|
-
| `d` |
|
|
87
|
-
| `r` | Manual refresh (auto-refreshes silently every
|
|
132
|
+
| `d` | Delete the selected worktree (confirmation overlay) |
|
|
133
|
+
| `r` | Manual refresh (auto-refreshes silently every 5 min) |
|
|
88
134
|
| `o` | Open the issue in your browser |
|
|
89
135
|
| `q`/`Esc`| Quit (or cancel an open overlay) |
|
|
90
136
|
|
|
@@ -97,10 +143,10 @@ Same building blocks, scriptable from any shell:
|
|
|
97
143
|
```bash
|
|
98
144
|
# Create a worktree, optionally launch Claude with an initial prompt
|
|
99
145
|
mintree worktree create feat/100-validar-patente
|
|
100
|
-
mintree worktree create feat/100-validar-patente --work --prompt "empezar
|
|
146
|
+
mintree worktree create feat/BACK-100-validar-patente --work --prompt "empezar BACK-100"
|
|
101
147
|
|
|
102
148
|
# Resume Claude in the worktree you're currently inside
|
|
103
|
-
cd .mintree/worktrees/100-validar-patente
|
|
149
|
+
cd .mintree/worktrees/BACK-100-validar-patente
|
|
104
150
|
mintree worktree work
|
|
105
151
|
|
|
106
152
|
# Inspect / clean up
|
|
@@ -127,7 +173,14 @@ mintree enforces:
|
|
|
127
173
|
|
|
128
174
|
> `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `build`, `ci`, `perf`, `style`, `revert`
|
|
129
175
|
|
|
130
|
-
`<issue>` is
|
|
176
|
+
`<issue>` is one of:
|
|
177
|
+
|
|
178
|
+
- Bare digits for GitHub Issues (the issue number, no `#`): `42`, `100`, `1234`
|
|
179
|
+
- `<PROJ>-<digits>` for Plane work items (the human identifier): `BACK-100`, `WEB-7`, `PROJ_X-12`. The prefix is uppercase letters / digits / underscores, matching Plane's project-identifier constraints.
|
|
180
|
+
|
|
181
|
+
`<desc>` is lowercase kebab-case.
|
|
182
|
+
|
|
183
|
+
Examples: `feat/42-validacion-patente`, `fix/55-selfie-upload-timeout`, `feat/BACK-100-readme-update`, `fix/WEB-7-modal`.
|
|
131
184
|
|
|
132
185
|
When the dashboard's `w` overlay opens, it suggests a kebab description capped at 5 words. If your repo has a `docs/conventions/git-workflow.md`, `CONTRIBUTING.md`, or `.claude/skills/` directory, mintree mentions it on the overlay so you can verify the suggestion against your project's rules — then edit the description to match.
|
|
133
186
|
|
|
@@ -139,15 +192,18 @@ When the dashboard's `w` overlay opens, it suggests a kebab description capped a
|
|
|
139
192
|
<repo>/
|
|
140
193
|
├── .gitignore # gets `.mintree/worktrees/` + session-states/ + metadata.json appended
|
|
141
194
|
└── .mintree/
|
|
142
|
-
├── metadata.json # gitignored. <issue-id> → { base_branch?, session_id? }
|
|
195
|
+
├── metadata.json # gitignored. provider config + <issue-id> → { base_branch?, session_id? }
|
|
143
196
|
├── worktrees/ # gitignored
|
|
144
|
-
│
|
|
197
|
+
│ ├── 100-validar-patente/ # GH form: <digits>-<desc>
|
|
198
|
+
│ └── BACK-100-readme-update/ # Plane form: <PROJ-digits>-<desc>
|
|
145
199
|
├── session-states/ # gitignored
|
|
146
200
|
│ └── 100.json # live state written by Claude hooks (active/waiting/idle/exited)
|
|
147
201
|
└── init.sh # opt-in. Runs in the new worktree post-create (copy .env, install deps, …)
|
|
148
202
|
```
|
|
149
203
|
|
|
150
|
-
`metadata.json` is gitignored because the `session_id` is local to your machine — sharing it would only generate noise.
|
|
204
|
+
`metadata.json` is gitignored because the `session_id` is local to your machine — sharing it would only generate noise. The `provider` and `plane.*` keys can be re-derived from a Plane workspace if needed; sharing them would just leak local config preference.
|
|
205
|
+
|
|
206
|
+
Plane authentication lives in `~/.mintree/credentials.json` (user-scoped, not per-repo) or the `PLANE_API_KEY` env var.
|
|
151
207
|
|
|
152
208
|
---
|
|
153
209
|
|
|
@@ -163,7 +219,8 @@ When the dashboard's `w` overlay opens, it suggests a kebab description capped a
|
|
|
163
219
|
|
|
164
220
|
- `mintree doctor` is the first stop. It surfaces missing tools, unauthenticated CLIs, missing hooks, and gitignore drift.
|
|
165
221
|
- The shell wrapper exports `MINTREE_SHELL_INTEGRATION=1` — if doctor says it's missing, the wrapper isn't being loaded by your shell init file.
|
|
166
|
-
- If the dashboard ever opens with a stale session state, press `r` to force a refetch (the auto-refresh runs every
|
|
222
|
+
- If the dashboard ever opens with a stale session state, press `r` to force a refetch (the auto-refresh runs every 5 minutes).
|
|
223
|
+
- Plane-side issues (timeouts, rate limits, unexpected response shapes) can be logged to `~/.mintree/plane-debug.log` by running `MINTREE_DEBUG=1 mintree dashboard`. The log is file-only so it never corrupts the Ink-rendered TUI.
|
|
167
224
|
|
|
168
225
|
---
|
|
169
226
|
|
|
@@ -171,7 +228,7 @@ When the dashboard's `w` overlay opens, it suggests a kebab description capped a
|
|
|
171
228
|
|
|
172
229
|
mintree was written for projects that have:
|
|
173
230
|
|
|
174
|
-
- GitHub Issues
|
|
231
|
+
- A small set of trackers (GitHub Issues or Plane) — santree supports both Linear and GitHub but is heavier.
|
|
175
232
|
- An established branch convention without the `gh-` prefix santree imposes.
|
|
176
233
|
- Skills (`.claude/skills/`) that own the SDD + TDD flow — mintree intentionally leaves the rich PR-create / PR-review prompts out of scope.
|
|
177
234
|
|
|
@@ -13,7 +13,8 @@ import { ALLOWED_TYPES } from "../lib/branch.js";
|
|
|
13
13
|
import { runCreate, runCreateDetached } from "../lib/worktreeCreate.js";
|
|
14
14
|
import { runRemove, runRemoveByPath } from "../lib/worktreeRemove.js";
|
|
15
15
|
import { buildCreateMarkers, emitMarkers } from "../lib/markers.js";
|
|
16
|
-
import {
|
|
16
|
+
import { readMetadata } from "../lib/metadata.js";
|
|
17
|
+
import { createProvider } from "../lib/providers/index.js";
|
|
17
18
|
import { loadDashboard } from "../lib/dashboard.js";
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
19
20
|
const { version: mintreeVersion } = require("../../package.json");
|
|
@@ -115,8 +116,8 @@ function kebabize(title) {
|
|
|
115
116
|
* the body inline (issue bodies can be long and contain markdown that
|
|
116
117
|
* doesn't survive argv). User can clear or rewrite freely before Enter.
|
|
117
118
|
*/
|
|
118
|
-
function defaultPromptForIssue(
|
|
119
|
-
return `Empezá a trabajar el issue #${
|
|
119
|
+
function defaultPromptForIssue(id, title) {
|
|
120
|
+
return `Empezá a trabajar el issue #${id} (${title}). Usá \`gh issue view ${id}\` para leer el contexto completo y seguí las convenciones del repo.`;
|
|
120
121
|
}
|
|
121
122
|
/**
|
|
122
123
|
* Sanitises whatever the user typed into the desc field on every keystroke.
|
|
@@ -174,22 +175,22 @@ function FooterRow({ phase, overlayKind, latestVersion, }) {
|
|
|
174
175
|
if (overlayKind === "remove") {
|
|
175
176
|
return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "y/Y" }), _jsx(Text, { dimColor: true, children: " confirm " }), _jsx(Text, { bold: true, children: "n/Esc" }), _jsx(Text, { dimColor: true, children: " cancel" })] }));
|
|
176
177
|
}
|
|
177
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " work (resume / create) " }), _jsx(Text, { bold: true, children: "w" }), _jsx(Text, { dimColor: true, children: " work (always create) " }), _jsx(Text, { bold: true, children: "d" }), _jsx(Text, { dimColor: true, children: "
|
|
178
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "j/k" }), _jsx(Text, { dimColor: true, children: " nav " }), _jsx(Text, { bold: true, children: "\u21B5" }), _jsx(Text, { dimColor: true, children: " work (resume / create) " }), _jsx(Text, { bold: true, children: "w" }), _jsx(Text, { dimColor: true, children: " work (always create) " }), _jsx(Text, { bold: true, children: "d" }), _jsx(Text, { dimColor: true, children: " delete" })] }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, children: "r" }), _jsx(Text, { dimColor: true, children: " refresh " }), _jsx(Text, { bold: true, children: "o" }), _jsx(Text, { dimColor: true, children: " open in browser " }), _jsx(Text, { bold: true, children: "PgUp/PgDn" }), _jsx(Text, { dimColor: true, children: "/" }), _jsx(Text, { bold: true, children: "wheel" }), _jsx(Text, { dimColor: true, children: " scroll detail " }), _jsx(Text, { bold: true, children: "q" }), _jsx(Text, { dimColor: true, children: " quit" })] }), latestVersion && (_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: "(*)" }), _jsx(Text, { dimColor: true, children: ` new version available — v${latestVersion} · npm i -g mintree` })] }))] }));
|
|
178
179
|
}
|
|
179
180
|
function RemoveOverlayView({ overlay }) {
|
|
180
|
-
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove worktree" }), _jsx(Text, { dimColor: true, children: ` for
|
|
181
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Remove worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Branch: " }), _jsx(Text, { color: "cyan", children: overlay.branch ?? `(detached) ${overlay.worktreePath}` })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "State: " }), overlay.dirty ? (_jsx(Text, { color: "yellow", children: "dirty (uncommitted changes will be lost)" })) : (_jsx(Text, { color: "green", children: "clean" }))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "Removing the worktree leaves the branch and the issue's session_id in place. You can re-attach later with `mintree worktree create`." }) }), _jsx(Box, { marginTop: 1, children: overlay.dirty ? (_jsxs(Text, { children: ["This worktree is dirty. Press", " ", _jsx(Text, { bold: true, color: "red", children: "Y" }), " ", "to force-remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) : (_jsxs(Text, { children: ["Press", " ", _jsx(Text, { bold: true, color: "green", children: "y" }), " ", "to remove, ", _jsx(Text, { bold: true, children: "N" }), "/", _jsx(Text, { bold: true, children: "Esc" }), " to cancel."] })) }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) }))] }));
|
|
181
182
|
}
|
|
182
183
|
function CreateOverlayView({ overlay, onDescChange, onPromptChange, }) {
|
|
183
184
|
const labelWidth = 14;
|
|
184
185
|
const isNewBranch = overlay.branchMode === "new";
|
|
185
|
-
const detachedDesc = kebabize(overlay.issue.issue.title) || `issue-${overlay.issue.issue.
|
|
186
|
+
const detachedDesc = kebabize(overlay.issue.issue.title) || `issue-${overlay.issue.issue.id}`;
|
|
186
187
|
const branchPreview = isNewBranch
|
|
187
|
-
? `${overlay.type}/${overlay.issue.issue.
|
|
188
|
+
? `${overlay.type}/${overlay.issue.issue.id}-${overlay.desc}`
|
|
188
189
|
: `detached @ ${overlay.currentBranch ?? "(unknown)"}`;
|
|
189
190
|
const dirPreview = isNewBranch
|
|
190
|
-
? `${overlay.issue.issue.
|
|
191
|
-
: `${overlay.issue.issue.
|
|
192
|
-
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for
|
|
191
|
+
? `${overlay.issue.issue.id}-${overlay.desc}`
|
|
192
|
+
: `${overlay.issue.issue.id}-${detachedDesc}`;
|
|
193
|
+
return (_jsxs(Box, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "Create worktree" }), _jsx(Text, { dimColor: true, children: ` for ${overlay.issue.issue.id}` })] }), _jsx(Box, { marginTop: 0, children: _jsx(Text, { children: overlay.issue.issue.title }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "branchMode", children: overlay.field === "branchMode" ? "▸ Branch:" : " Branch:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "branchMode" ? "cyan" : undefined, bold: overlay.field === "branchMode", children: isNewBranch ? "new" : `current (${overlay.currentBranch ?? "?"})` }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "branchMode" && _jsx(Text, { dimColor: true, children: " (use ← / → to toggle)" })] }), isNewBranch && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "type", children: overlay.field === "type" ? "▸ Type:" : " Type:" }) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "< " }), _jsx(Text, { color: overlay.field === "type" ? "cyan" : undefined, bold: overlay.field === "type", children: overlay.type }), _jsx(Text, { dimColor: true, children: " >" })] }), overlay.field === "type" && _jsx(Text, { dimColor: true, children: " (use ← / → to cycle)" })] }), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "desc", children: overlay.field === "desc" ? "▸ Description:" : " Description:" }) }), _jsx(Box, { children: overlay.field === "desc" ? (_jsx(TextInput, { value: overlay.desc, onChange: onDescChange, placeholder: "kebab-case" })) : (_jsx(Text, { children: overlay.desc || "(empty)" })) })] })] })), _jsxs(Box, { marginTop: 0, children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { bold: overlay.field === "prompt", children: overlay.field === "prompt" ? "▸ Prompt:" : " Prompt:" }) }), _jsx(Box, { children: overlay.field === "prompt" ? (_jsx(TextInput, { value: overlay.prompt, onChange: onPromptChange, placeholder: "(empty = no initial message)" })) : (_jsx(Text, { dimColor: true, children: overlay.prompt || "(empty — Claude starts with no message)" })) })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Checkout:" }) }), _jsx(Text, { color: "green", children: branchPreview })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Worktree:" }) }), _jsxs(Text, { dimColor: true, children: [".mintree/worktrees/", dirPreview] })] }), _jsxs(Box, { children: [_jsx(Box, { width: labelWidth, children: _jsx(Text, { dimColor: true, children: " Mode:" }) }), _jsx(Text, { dimColor: true, children: "--work (Claude launches in the new worktree)" })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [isNewBranch ? (_jsxs(Text, { dimColor: true, children: ["Suggestion is a kebab of the title (capped at ", SUGGESTED_DESC_MAX_WORDS, " words). Edit it to match your repo's branch conventions."] })) : (_jsxs(Text, { dimColor: true, children: ["Detached HEAD at the tip of ", overlay.currentBranch ?? "the current branch", ". No new branch is created \u2014 commit on a new one with `git switch -c` when ready."] })), isNewBranch && overlay.conventionDoc && (_jsx(Text, { dimColor: true, children: `This repo has \`${overlay.conventionDoc}\` — review it before creating.` }))] }), overlay.error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", overlay.error] }) })), overlay.pending && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", overlay.pending] })] }))] }));
|
|
193
194
|
}
|
|
194
195
|
function stateChar(state) {
|
|
195
196
|
if (!state)
|
|
@@ -203,7 +204,11 @@ function stateChar(state) {
|
|
|
203
204
|
return "—";
|
|
204
205
|
}
|
|
205
206
|
function IssueListRow({ d, selected, identifierWidth, maxTitleWidth, }) {
|
|
206
|
-
|
|
207
|
+
// Display the issue id raw (e.g. "AUTH-6", "100"). The `#` prefix is a
|
|
208
|
+
// GitHub convention that reads as noise for Plane's already-prefixed
|
|
209
|
+
// ids, and dropping it across the board keeps the dashboard provider-
|
|
210
|
+
// agnostic.
|
|
211
|
+
const idText = d.issue.id.padEnd(identifierWidth, " ");
|
|
207
212
|
const icon = stateChar(d.session?.state ?? null);
|
|
208
213
|
const title = truncate(d.issue.title, maxTitleWidth);
|
|
209
214
|
// One single Text with a single string so the background highlight is
|
|
@@ -274,35 +279,62 @@ function buildListRows(issues) {
|
|
|
274
279
|
}
|
|
275
280
|
/**
|
|
276
281
|
* Splits the grouped list into a pinned-header region and a scrollable body
|
|
277
|
-
* windowed around the selected issue.
|
|
278
|
-
* Status headers are lifted out of the scroll flow so they stay visible
|
|
279
|
-
* ("sticky") while paging through a long group — without them the user loses
|
|
280
|
-
* track of which project/status the rows below belong to.
|
|
282
|
+
* windowed around the selected issue.
|
|
281
283
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
+
* Sticky pinning only kicks in when the whole list overflows the viewport
|
|
285
|
+
* AND the selected issue's project header would otherwise be scrolled off
|
|
286
|
+
* the top. In every other case (list fits, or selection is above its
|
|
287
|
+
* project header) the layout is rendered natively — moving the project
|
|
288
|
+
* label out of its natural position when there's no scrolling to track
|
|
289
|
+
* is purely confusing.
|
|
290
|
+
*
|
|
291
|
+
* Status headers are never pinned: they're short groups in close vertical
|
|
292
|
+
* proximity to their items, so the user always sees them inline. Pinning
|
|
293
|
+
* them was previously breaking grouping when the body window spanned
|
|
294
|
+
* multiple status sub-groups.
|
|
284
295
|
*/
|
|
285
296
|
function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
286
297
|
const selRow = listRows.findIndex((r) => r.kind === "issue" && r.index === selectedIndex);
|
|
287
298
|
const anchor = selRow >= 0 ? selRow : 0;
|
|
288
|
-
//
|
|
289
|
-
//
|
|
299
|
+
// If the entire list fits in the viewport, no scrolling will happen and
|
|
300
|
+
// pinning would just visually displace headers without serving any
|
|
301
|
+
// purpose. Render the list flat.
|
|
302
|
+
if (listRows.length <= viewportRows) {
|
|
303
|
+
return {
|
|
304
|
+
sticky: [],
|
|
305
|
+
body: [...listRows],
|
|
306
|
+
issuesAbove: 0,
|
|
307
|
+
issuesBelow: 0,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
// Otherwise, decide if the project header needs to be pinned. It does iff
|
|
311
|
+
// the body window we'd render — centred on the anchor — would not include
|
|
312
|
+
// the project header itself. When it would, the user sees the header in
|
|
313
|
+
// place and pinning is again redundant.
|
|
290
314
|
let projIdx = -1;
|
|
291
|
-
let statusIdx = -1;
|
|
292
315
|
for (let i = anchor; i >= 0; i--) {
|
|
293
316
|
const r = listRows[i];
|
|
294
317
|
if (!r)
|
|
295
318
|
continue;
|
|
296
|
-
if (statusIdx === -1 && r.kind === "status")
|
|
297
|
-
statusIdx = i;
|
|
298
319
|
if (r.kind === "project") {
|
|
299
320
|
projIdx = i;
|
|
300
321
|
break;
|
|
301
322
|
}
|
|
302
323
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
324
|
+
const tentativeMaxStart = Math.max(0, listRows.length - viewportRows);
|
|
325
|
+
const tentativeStart = Math.max(0, Math.min(tentativeMaxStart, anchor - Math.floor(viewportRows / 2)));
|
|
326
|
+
const projectVisibleInWindow = projIdx >= 0 && projIdx >= tentativeStart;
|
|
327
|
+
if (projectVisibleInWindow) {
|
|
328
|
+
const end = Math.min(listRows.length, tentativeStart + viewportRows);
|
|
329
|
+
return {
|
|
330
|
+
sticky: [],
|
|
331
|
+
body: listRows.slice(tentativeStart, end),
|
|
332
|
+
issuesAbove: listRows.slice(0, tentativeStart).filter((r) => r.kind === "issue").length,
|
|
333
|
+
issuesBelow: listRows.slice(end).filter((r) => r.kind === "issue").length,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
// Project header is above the visible window — pin it (and drop the
|
|
337
|
+
// preceding spacer so the pane doesn't open with a blank line).
|
|
306
338
|
const pinned = new Set();
|
|
307
339
|
const sticky = [];
|
|
308
340
|
if (projIdx >= 0) {
|
|
@@ -312,10 +344,6 @@ function windowListRows(listRows, selectedIndex, viewportRows) {
|
|
|
312
344
|
if (before && before.kind === "spacer")
|
|
313
345
|
pinned.add(projIdx - 1);
|
|
314
346
|
}
|
|
315
|
-
if (statusIdx >= 0) {
|
|
316
|
-
pinned.add(statusIdx);
|
|
317
|
-
sticky.push(listRows[statusIdx]);
|
|
318
|
-
}
|
|
319
347
|
const body = [];
|
|
320
348
|
let anchorInBody = 0;
|
|
321
349
|
listRows.forEach((r, i) => {
|
|
@@ -418,7 +446,7 @@ function buildDetailLines(d, width) {
|
|
|
418
446
|
const lines = [];
|
|
419
447
|
const blank = () => [{ text: " " }];
|
|
420
448
|
const w = Math.max(20, width);
|
|
421
|
-
const titlePrefix =
|
|
449
|
+
const titlePrefix = `${d.issue.id} `;
|
|
422
450
|
const titleWrapped = wrapLine(d.issue.title, Math.max(8, w - titlePrefix.length));
|
|
423
451
|
titleWrapped.forEach((chunk, i) => {
|
|
424
452
|
if (i === 0) {
|
|
@@ -594,10 +622,31 @@ export default function Dashboard() {
|
|
|
594
622
|
}
|
|
595
623
|
const issues = await loadDashboard(root);
|
|
596
624
|
if (!issues) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
625
|
+
const provider = readMetadata(root).provider ?? "github";
|
|
626
|
+
const message = provider === "plane"
|
|
627
|
+
? "Could not fetch work items from Plane."
|
|
628
|
+
: "Could not fetch issues from GitHub.";
|
|
629
|
+
const hint = provider === "plane"
|
|
630
|
+
? "Check `mintree doctor` — PLANE_API_KEY must be set and the workspace + projects reachable."
|
|
631
|
+
: "Check `mintree doctor` — gh must be authenticated and the repo must live on GitHub.";
|
|
632
|
+
setState((prev) => {
|
|
633
|
+
// Initial load failure → escalate to the full error screen so the
|
|
634
|
+
// user gets the actionable hint front-and-centre. But once the
|
|
635
|
+
// dashboard is up, a transient fetch failure (network blip on the
|
|
636
|
+
// 30s auto-refresh, momentary API slowness) shouldn't blow away
|
|
637
|
+
// what's already on screen — surface a toast and let the next
|
|
638
|
+
// refresh recover.
|
|
639
|
+
if (prev.phase !== "ready") {
|
|
640
|
+
return { phase: "error", message, hint };
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
...prev,
|
|
644
|
+
refreshing: false,
|
|
645
|
+
toast: {
|
|
646
|
+
kind: "error",
|
|
647
|
+
text: `${message} Showing last known data — press \`r\` to retry.`,
|
|
648
|
+
},
|
|
649
|
+
};
|
|
601
650
|
});
|
|
602
651
|
return;
|
|
603
652
|
}
|
|
@@ -706,9 +755,13 @@ export default function Dashboard() {
|
|
|
706
755
|
process.stdout.write(MOUSE_ON);
|
|
707
756
|
};
|
|
708
757
|
}, [state.phase === "ready" && state.overlay ? state.overlay.kind : null]);
|
|
709
|
-
// Auto-refresh every
|
|
710
|
-
//
|
|
711
|
-
//
|
|
758
|
+
// Auto-refresh every 5 minutes while the dashboard is idle. 30s was too
|
|
759
|
+
// aggressive for an issue list — most of the time nothing has changed,
|
|
760
|
+
// and with multiple Plane projects configured each refresh fires N×2
|
|
761
|
+
// API calls. Press `r` for an immediate refresh when something
|
|
762
|
+
// changed externally. Skipped while an overlay is open so we don't yank
|
|
763
|
+
// state from under a confirmation, and while a manual refresh is in
|
|
764
|
+
// flight to avoid stomping on its spinner.
|
|
712
765
|
const stateRef = useRef(state);
|
|
713
766
|
useEffect(() => {
|
|
714
767
|
stateRef.current = state;
|
|
@@ -723,7 +776,7 @@ export default function Dashboard() {
|
|
|
723
776
|
if (s.refreshing)
|
|
724
777
|
return;
|
|
725
778
|
void refresh();
|
|
726
|
-
},
|
|
779
|
+
}, 5 * 60 * 1000);
|
|
727
780
|
return () => clearInterval(id);
|
|
728
781
|
}, []);
|
|
729
782
|
useInput((input, key) => {
|
|
@@ -839,8 +892,8 @@ export default function Dashboard() {
|
|
|
839
892
|
branchMode: "new",
|
|
840
893
|
currentBranch: root ? getCurrentBranch(root) : null,
|
|
841
894
|
type: "feat",
|
|
842
|
-
desc: kebabize(issue.issue.title) || `issue-${issue.issue.
|
|
843
|
-
prompt: defaultPromptForIssue(issue.issue.
|
|
895
|
+
desc: kebabize(issue.issue.title) || `issue-${issue.issue.id}`,
|
|
896
|
+
prompt: defaultPromptForIssue(issue.issue.id, issue.issue.title),
|
|
844
897
|
field: "branchMode",
|
|
845
898
|
error: null,
|
|
846
899
|
conventionDoc: root ? findBranchConventionDoc(root) : null,
|
|
@@ -934,15 +987,15 @@ export default function Dashboard() {
|
|
|
934
987
|
if (state.phase !== "ready")
|
|
935
988
|
return;
|
|
936
989
|
const prompt = overlay.prompt.trim();
|
|
937
|
-
const
|
|
990
|
+
const issueId = overlay.issue.issue.id;
|
|
938
991
|
let result;
|
|
939
992
|
if (overlay.branchMode === "current") {
|
|
940
993
|
// Detached worktree off the main repo's current branch. Desc comes
|
|
941
994
|
// from the issue title (kebabized), not user input — keeping the
|
|
942
995
|
// "current branch" flow as low-friction as possible.
|
|
943
|
-
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${
|
|
996
|
+
const descKebab = kebabize(overlay.issue.issue.title) || `issue-${issueId}`;
|
|
944
997
|
result = runCreateDetached({
|
|
945
|
-
issueId
|
|
998
|
+
issueId,
|
|
946
999
|
descKebab,
|
|
947
1000
|
work: true,
|
|
948
1001
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
@@ -957,7 +1010,7 @@ export default function Dashboard() {
|
|
|
957
1010
|
});
|
|
958
1011
|
return;
|
|
959
1012
|
}
|
|
960
|
-
const branch = `${overlay.type}/${
|
|
1013
|
+
const branch = `${overlay.type}/${issueId}-${desc}`;
|
|
961
1014
|
result = runCreate(branch, {
|
|
962
1015
|
work: true,
|
|
963
1016
|
...(prompt.length > 0 ? { prompt } : {}),
|
|
@@ -981,7 +1034,8 @@ export default function Dashboard() {
|
|
|
981
1034
|
const repoRoot = findMainRepoRoot();
|
|
982
1035
|
if (repoRoot) {
|
|
983
1036
|
try {
|
|
984
|
-
|
|
1037
|
+
const provider = createProvider(repoRoot);
|
|
1038
|
+
await provider.transitionIssueToInProgress(issueId);
|
|
985
1039
|
}
|
|
986
1040
|
catch {
|
|
987
1041
|
// best effort — surface via doctor / next dashboard refresh
|
|
@@ -1056,7 +1110,7 @@ export default function Dashboard() {
|
|
|
1056
1110
|
const listWidthPct = 0.4;
|
|
1057
1111
|
const listWidth = Math.max(32, Math.floor(columns * listWidthPct));
|
|
1058
1112
|
const detailWidth = columns - listWidth - 2; // border slack
|
|
1059
|
-
const identifierWidth = Math.max(3, ...issues.map((d) =>
|
|
1113
|
+
const identifierWidth = Math.max(3, ...issues.map((d) => d.issue.id.length));
|
|
1060
1114
|
// Lista ocupa todo menos: " #N ICON " (2-space nest indent + id + icon).
|
|
1061
1115
|
const maxTitleWidth = Math.max(8, listWidth - identifierWidth - 9);
|
|
1062
1116
|
// Reserve rows: header (2), top borders (1), footer (3).
|