santree 0.5.6 → 0.6.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/dist/commands/dashboard.js +210 -33
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/helpers/squirrel.d.ts +2 -0
- package/dist/commands/helpers/squirrel.js +12 -0
- package/dist/commands/worktree/commit.d.ts +9 -1
- package/dist/commands/worktree/commit.js +58 -14
- package/dist/lib/ai.d.ts +26 -0
- package/dist/lib/ai.js +53 -0
- package/dist/lib/claude-todos.d.ts +37 -0
- package/dist/lib/claude-todos.js +98 -0
- package/dist/lib/dashboard/DetailPanel.js +99 -9
- package/dist/lib/dashboard/IssueList.js +2 -0
- package/dist/lib/dashboard/MultilineTextArea.js +14 -1
- package/dist/lib/dashboard/Overlays.d.ts +5 -0
- package/dist/lib/dashboard/Overlays.js +75 -2
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +7 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +269 -77
- package/dist/lib/dashboard/ReviewList.js +12 -15
- package/dist/lib/dashboard/data.js +158 -7
- package/dist/lib/dashboard/types.d.ts +45 -5
- package/dist/lib/dashboard/types.js +40 -0
- package/dist/lib/diff-parse.d.ts +25 -0
- package/dist/lib/diff-parse.js +60 -0
- package/dist/lib/git.d.ts +22 -0
- package/dist/lib/git.js +41 -0
- package/dist/lib/github.d.ts +6 -0
- package/dist/lib/github.js +29 -0
- package/dist/lib/open-url.d.ts +10 -0
- package/dist/lib/open-url.js +20 -0
- package/dist/lib/squirrel-loader.d.ts +9 -0
- package/dist/lib/squirrel-loader.js +322 -0
- package/dist/lib/trackers/index.d.ts +13 -0
- package/dist/lib/trackers/index.js +19 -0
- package/package.json +1 -1
- package/prompts/fill-commit.njk +79 -0
package/dist/lib/ai.js
CHANGED
|
@@ -226,6 +226,36 @@ export function runAgent(prompt, opts) {
|
|
|
226
226
|
output: result.stdout?.trim() ?? "",
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* Async version of runAgent. Use this from Ink renderers — spawnSync
|
|
231
|
+
* blocks Node's event loop, freezing the UI (no spinner animation, no
|
|
232
|
+
* keystroke processing) for the entire duration of Claude's generation.
|
|
233
|
+
* spawn() lets the loop run during the call.
|
|
234
|
+
*/
|
|
235
|
+
export function runAgentAsync(prompt, opts) {
|
|
236
|
+
const bin = resolveAgentBinary();
|
|
237
|
+
if (!bin) {
|
|
238
|
+
return Promise.reject(new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code"));
|
|
239
|
+
}
|
|
240
|
+
const toolArgs = opts?.allowedTools?.length ? ["--allowedTools", ...opts.allowedTools] : [];
|
|
241
|
+
const args = [
|
|
242
|
+
"--permission-mode",
|
|
243
|
+
"auto",
|
|
244
|
+
...toolArgs,
|
|
245
|
+
"-p",
|
|
246
|
+
"--output-format",
|
|
247
|
+
"text",
|
|
248
|
+
"--",
|
|
249
|
+
promptArg(prompt),
|
|
250
|
+
];
|
|
251
|
+
return new Promise((resolve) => {
|
|
252
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
253
|
+
let stdout = "";
|
|
254
|
+
child.stdout?.on("data", (chunk) => (stdout += chunk.toString("utf-8")));
|
|
255
|
+
child.on("error", () => resolve({ success: false, output: "" }));
|
|
256
|
+
child.on("close", (code) => resolve({ success: code === 0, output: stdout.trim() }));
|
|
257
|
+
});
|
|
258
|
+
}
|
|
229
259
|
/**
|
|
230
260
|
* Clean up cached image downloads for an issue identifier on the active tracker.
|
|
231
261
|
*/
|
|
@@ -233,3 +263,26 @@ export function cleanupImages(ticketId) {
|
|
|
233
263
|
const repoRoot = findMainRepoRoot();
|
|
234
264
|
getIssueTracker(repoRoot).cleanupCache(ticketId);
|
|
235
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Generate a short imperative commit message from a staged diff.
|
|
268
|
+
* Async so callers (the Ink dashboard, the CLI commit flow) keep the
|
|
269
|
+
* event loop turning during Claude's ~5–30s generation — using the sync
|
|
270
|
+
* runAgent here freezes the renderer.
|
|
271
|
+
*
|
|
272
|
+
* Returns the trimmed message string (no quotes, no preamble) on success,
|
|
273
|
+
* or null if Claude failed. Caller is responsible for ensuring the diff
|
|
274
|
+
* is non-empty.
|
|
275
|
+
*/
|
|
276
|
+
export async function fillCommitMessage(opts) {
|
|
277
|
+
const prompt = renderPrompt("fill-commit", {
|
|
278
|
+
branch_name: opts.branch,
|
|
279
|
+
ticket_id: opts.ticketId ?? "",
|
|
280
|
+
ticket_content: opts.ticketContent,
|
|
281
|
+
diff_content: opts.diffContent,
|
|
282
|
+
});
|
|
283
|
+
const result = await runAgentAsync(prompt);
|
|
284
|
+
if (!result.success)
|
|
285
|
+
return null;
|
|
286
|
+
// Trim quotes/whitespace; Claude occasionally wraps despite instructions.
|
|
287
|
+
return result.output.trim().replace(/^["'`]|["'`]$/g, "");
|
|
288
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type ClaudeTodoStatus = "pending" | "in_progress" | "completed";
|
|
2
|
+
export interface ClaudeTodo {
|
|
3
|
+
id: string;
|
|
4
|
+
content: string;
|
|
5
|
+
status: ClaudeTodoStatus;
|
|
6
|
+
}
|
|
7
|
+
/** Read the main-agent todo list for a Claude Code session.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code persists `TodoWrite` state to
|
|
10
|
+
* `~/.claude/todos/<sessionId>-agent-<agentId>.json`. The file with
|
|
11
|
+
* `agentId === sessionId` is the user-visible list; sub-agent files
|
|
12
|
+
* (different agentId) are noise and ignored here.
|
|
13
|
+
*
|
|
14
|
+
* Returns null when the file is missing, empty, or unparseable. The
|
|
15
|
+
* dashboard treats a null/empty result as "hide the section" so a
|
|
16
|
+
* stray malformed file never blocks rendering. */
|
|
17
|
+
export declare function readMainAgentTodos(sessionId: string): ClaudeTodo[] | null;
|
|
18
|
+
/** Locate the cwd from which a Claude Code session is resumable.
|
|
19
|
+
*
|
|
20
|
+
* Claude stores transcripts at
|
|
21
|
+
* `~/.claude/projects/<encodedCwd>/<sessionId>.jsonl`, where `encodedCwd`
|
|
22
|
+
* replaces every `/` and `.` with `-`. `claude --resume <id>` is cwd-scoped:
|
|
23
|
+
* a session created at the worktree root is NOT resumable from a
|
|
24
|
+
* subdirectory like `backend/canary`, even though the file exists somewhere
|
|
25
|
+
* under `~/.claude/projects/`. The dashboard's tmux send-keys flow has no
|
|
26
|
+
* control over where the user's shell init / direnv leaves the window's
|
|
27
|
+
* cwd, so we resolve the original launch cwd here and prepend a `cd` to
|
|
28
|
+
* the resume command.
|
|
29
|
+
*
|
|
30
|
+
* Returns the real path of the cwd where the session is resumable —
|
|
31
|
+
* constrained to the worktree subtree so we never recommend `cd`-ing
|
|
32
|
+
* outside it. Returns null when the file isn't found anywhere matching
|
|
33
|
+
* the worktree (the file was deleted, or the session was created in a
|
|
34
|
+
* cwd we can't reconstruct). The encoding is lossy (`-` could come from
|
|
35
|
+
* `/` or `.`), so we verify candidates against real filesystem paths
|
|
36
|
+
* under `worktreeRoot` rather than guessing. */
|
|
37
|
+
export declare function findClaudeSessionCwd(worktreeRoot: string, sessionId: string): string | null;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
/** Read the main-agent todo list for a Claude Code session.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code persists `TodoWrite` state to
|
|
7
|
+
* `~/.claude/todos/<sessionId>-agent-<agentId>.json`. The file with
|
|
8
|
+
* `agentId === sessionId` is the user-visible list; sub-agent files
|
|
9
|
+
* (different agentId) are noise and ignored here.
|
|
10
|
+
*
|
|
11
|
+
* Returns null when the file is missing, empty, or unparseable. The
|
|
12
|
+
* dashboard treats a null/empty result as "hide the section" so a
|
|
13
|
+
* stray malformed file never blocks rendering. */
|
|
14
|
+
export function readMainAgentTodos(sessionId) {
|
|
15
|
+
const file = path.join(os.homedir(), ".claude", "todos", `${sessionId}-agent-${sessionId}.json`);
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
raw = fs.readFileSync(file, "utf-8");
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(raw);
|
|
25
|
+
if (!Array.isArray(parsed))
|
|
26
|
+
return null;
|
|
27
|
+
const out = [];
|
|
28
|
+
for (const item of parsed) {
|
|
29
|
+
if (!item || typeof item !== "object")
|
|
30
|
+
continue;
|
|
31
|
+
const { id, content, status } = item;
|
|
32
|
+
if (typeof id !== "string" || typeof content !== "string")
|
|
33
|
+
continue;
|
|
34
|
+
if (status !== "pending" && status !== "in_progress" && status !== "completed")
|
|
35
|
+
continue;
|
|
36
|
+
out.push({ id, content, status });
|
|
37
|
+
}
|
|
38
|
+
return out.length > 0 ? out : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function encodeCwd(cwd) {
|
|
45
|
+
return cwd.replace(/[/.]/g, "-");
|
|
46
|
+
}
|
|
47
|
+
/** Locate the cwd from which a Claude Code session is resumable.
|
|
48
|
+
*
|
|
49
|
+
* Claude stores transcripts at
|
|
50
|
+
* `~/.claude/projects/<encodedCwd>/<sessionId>.jsonl`, where `encodedCwd`
|
|
51
|
+
* replaces every `/` and `.` with `-`. `claude --resume <id>` is cwd-scoped:
|
|
52
|
+
* a session created at the worktree root is NOT resumable from a
|
|
53
|
+
* subdirectory like `backend/canary`, even though the file exists somewhere
|
|
54
|
+
* under `~/.claude/projects/`. The dashboard's tmux send-keys flow has no
|
|
55
|
+
* control over where the user's shell init / direnv leaves the window's
|
|
56
|
+
* cwd, so we resolve the original launch cwd here and prepend a `cd` to
|
|
57
|
+
* the resume command.
|
|
58
|
+
*
|
|
59
|
+
* Returns the real path of the cwd where the session is resumable —
|
|
60
|
+
* constrained to the worktree subtree so we never recommend `cd`-ing
|
|
61
|
+
* outside it. Returns null when the file isn't found anywhere matching
|
|
62
|
+
* the worktree (the file was deleted, or the session was created in a
|
|
63
|
+
* cwd we can't reconstruct). The encoding is lossy (`-` could come from
|
|
64
|
+
* `/` or `.`), so we verify candidates against real filesystem paths
|
|
65
|
+
* under `worktreeRoot` rather than guessing. */
|
|
66
|
+
export function findClaudeSessionCwd(worktreeRoot, sessionId) {
|
|
67
|
+
const projectsRoot = path.join(os.homedir(), ".claude", "projects");
|
|
68
|
+
const wtEncoded = encodeCwd(worktreeRoot);
|
|
69
|
+
// Fast path: session was created at the worktree root itself.
|
|
70
|
+
if (fs.existsSync(path.join(projectsRoot, wtEncoded, `${sessionId}.jsonl`))) {
|
|
71
|
+
return worktreeRoot;
|
|
72
|
+
}
|
|
73
|
+
// Slow path: session was created in a subdir of the worktree (e.g.
|
|
74
|
+
// project conventions auto-cd into `backend/canary` via direnv).
|
|
75
|
+
let dirs;
|
|
76
|
+
try {
|
|
77
|
+
dirs = fs.readdirSync(projectsRoot);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const prefix = `${wtEncoded}-`;
|
|
83
|
+
for (const dir of dirs) {
|
|
84
|
+
if (!dir.startsWith(prefix))
|
|
85
|
+
continue;
|
|
86
|
+
if (!fs.existsSync(path.join(projectsRoot, dir, `${sessionId}.jsonl`)))
|
|
87
|
+
continue;
|
|
88
|
+
// Decode the suffix back to a real path under the worktree. The
|
|
89
|
+
// encoding is lossy, so we verify candidates against the filesystem
|
|
90
|
+
// rather than guessing — only return a path that actually exists.
|
|
91
|
+
const suffix = dir.slice(prefix.length);
|
|
92
|
+
const candidate = path.join(worktreeRoot, ...suffix.split("-"));
|
|
93
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
94
|
+
return candidate;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
@@ -10,6 +10,8 @@ function stateColor(type) {
|
|
|
10
10
|
return "gray";
|
|
11
11
|
case "orphaned":
|
|
12
12
|
return "gray";
|
|
13
|
+
case "main":
|
|
14
|
+
return "magenta";
|
|
13
15
|
default:
|
|
14
16
|
return "yellow";
|
|
15
17
|
}
|
|
@@ -58,6 +60,19 @@ function fileColor(xy) {
|
|
|
58
60
|
export function buildIssueActions(di, trackerName) {
|
|
59
61
|
const { worktree, pr, issue } = di;
|
|
60
62
|
const items = [];
|
|
63
|
+
// The synthetic "Main repo" row is special: no PR/Switch/Resume/Remove,
|
|
64
|
+
// no work-launching (you're already on it). Only commit / diff /
|
|
65
|
+
// editor — the actions that make sense for "I have changes in main and
|
|
66
|
+
// want to review or land them".
|
|
67
|
+
if (issue.state.type === "main") {
|
|
68
|
+
if (worktree) {
|
|
69
|
+
items.push({ key: "e", label: "Editor", color: "cyan" });
|
|
70
|
+
if (worktree.dirty)
|
|
71
|
+
items.push({ key: "C", label: "Commit", color: "cyan" });
|
|
72
|
+
items.push({ key: "v", label: "View diff", color: "cyan" });
|
|
73
|
+
}
|
|
74
|
+
return items;
|
|
75
|
+
}
|
|
61
76
|
if (worktree?.sessionId) {
|
|
62
77
|
items.push({ key: "↵", label: "Resume", color: "cyan" });
|
|
63
78
|
}
|
|
@@ -164,19 +179,21 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
164
179
|
lines.push({ text: ` ${worktree.path}`, dim: true });
|
|
165
180
|
// Single metric row: files / +ins / -dels / commits ahead.
|
|
166
181
|
const ds = worktree.diffStats;
|
|
167
|
-
|
|
182
|
+
const behind = worktree.commitsBehind ?? 0;
|
|
183
|
+
const hasDiff = ds && (ds.insertions > 0 || ds.deletions > 0 || ds.filesChanged > 0);
|
|
184
|
+
if (hasDiff || worktree.commitsAhead > 0 || behind > 0) {
|
|
168
185
|
const segs = [{ text: " " }];
|
|
169
|
-
if (ds.filesChanged > 0) {
|
|
186
|
+
if (ds && ds.filesChanged > 0) {
|
|
170
187
|
segs.push({
|
|
171
188
|
text: `${ds.filesChanged} file${ds.filesChanged === 1 ? "" : "s"}`,
|
|
172
189
|
});
|
|
173
190
|
}
|
|
174
|
-
if (ds.insertions > 0) {
|
|
191
|
+
if (ds && ds.insertions > 0) {
|
|
175
192
|
if (segs.length > 1)
|
|
176
193
|
segs.push({ text: " " });
|
|
177
194
|
segs.push({ text: `+${ds.insertions}`, color: "green" });
|
|
178
195
|
}
|
|
179
|
-
if (ds.deletions > 0) {
|
|
196
|
+
if (ds && ds.deletions > 0) {
|
|
180
197
|
if (segs.length > 1)
|
|
181
198
|
segs.push({ text: " " });
|
|
182
199
|
segs.push({ text: `−${ds.deletions}`, color: "red" });
|
|
@@ -186,6 +203,11 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
186
203
|
segs.push({ text: " " });
|
|
187
204
|
segs.push({ text: `↑ ${worktree.commitsAhead}`, color: "cyan" });
|
|
188
205
|
}
|
|
206
|
+
if (behind > 0) {
|
|
207
|
+
if (segs.length > 1)
|
|
208
|
+
segs.push({ text: " " });
|
|
209
|
+
segs.push({ text: `↓ ${behind} behind`, color: "yellow" });
|
|
210
|
+
}
|
|
189
211
|
lines.push({ text: "", segments: segs });
|
|
190
212
|
}
|
|
191
213
|
// Per-status counts only when there's something dirty — when the tree is
|
|
@@ -277,10 +299,78 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
277
299
|
lines.push(sectionHeader("⎇", "Worktree"));
|
|
278
300
|
lines.push({ text: " no worktree for this ticket", dim: true });
|
|
279
301
|
}
|
|
302
|
+
// ── Claude tasks ──────────────────────────────────────────────────
|
|
303
|
+
// Reads `~/.claude/todos/<sessionId>-agent-<sessionId>.json` (main-agent
|
|
304
|
+
// list only — sub-agent todos are noise). Section is hidden when the
|
|
305
|
+
// session has no todos or has exited; the header shows done/total at a
|
|
306
|
+
// glance. Up to 6 rows are rendered before collapsing into "+ N more".
|
|
307
|
+
const todos = worktree?.claudeTodos ?? null;
|
|
308
|
+
if (todos && todos.length > 0) {
|
|
309
|
+
const completed = todos.filter((t) => t.status === "completed").length;
|
|
310
|
+
const inProgress = todos.filter((t) => t.status === "in_progress").length;
|
|
311
|
+
lines.push(ruleLine);
|
|
312
|
+
const headerSegs = [
|
|
313
|
+
{ text: "⎈ ", color: "cyan", bold: true },
|
|
314
|
+
{ text: "Tasks", bold: true },
|
|
315
|
+
{ text: " " },
|
|
316
|
+
{
|
|
317
|
+
text: `${completed}/${todos.length}`,
|
|
318
|
+
color: completed === todos.length ? "green" : "cyan",
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
if (inProgress > 0) {
|
|
322
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
323
|
+
headerSegs.push({ text: `${inProgress} in progress`, color: "yellow" });
|
|
324
|
+
}
|
|
325
|
+
lines.push({ text: "", segments: headerSegs });
|
|
326
|
+
const maxRows = 6;
|
|
327
|
+
// Surface in-progress first so the active task is always visible even
|
|
328
|
+
// when the list is long; pending next; completed last (most likely to
|
|
329
|
+
// be elided when truncating).
|
|
330
|
+
const ordered = [
|
|
331
|
+
...todos.filter((t) => t.status === "in_progress"),
|
|
332
|
+
...todos.filter((t) => t.status === "pending"),
|
|
333
|
+
...todos.filter((t) => t.status === "completed"),
|
|
334
|
+
];
|
|
335
|
+
for (const t of ordered.slice(0, maxRows)) {
|
|
336
|
+
if (t.status === "in_progress") {
|
|
337
|
+
lines.push({
|
|
338
|
+
text: "",
|
|
339
|
+
segments: [
|
|
340
|
+
{ text: " ◐ ", color: "yellow", bold: true },
|
|
341
|
+
{ text: t.content, color: "yellow" },
|
|
342
|
+
],
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
else if (t.status === "completed") {
|
|
346
|
+
lines.push({
|
|
347
|
+
text: "",
|
|
348
|
+
segments: [
|
|
349
|
+
{ text: " ✓ ", color: "green" },
|
|
350
|
+
{ text: t.content, dim: true },
|
|
351
|
+
],
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
lines.push({
|
|
356
|
+
text: "",
|
|
357
|
+
segments: [{ text: " ◯ ", dim: true }, { text: t.content }],
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (ordered.length > maxRows) {
|
|
362
|
+
lines.push({ text: ` + ${ordered.length - maxRows} more`, dim: true });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
280
365
|
// ── Pull Request ──────────────────────────────────────────────────
|
|
366
|
+
// Skip PR/Checks/Reviews sections entirely for the synthetic main row
|
|
367
|
+
// — those concepts don't apply to "the user's main checkout".
|
|
368
|
+
const isMain = li.state.type === "main";
|
|
281
369
|
const { checks, reviews } = issue;
|
|
282
|
-
|
|
283
|
-
|
|
370
|
+
if (!isMain) {
|
|
371
|
+
lines.push(ruleLine);
|
|
372
|
+
}
|
|
373
|
+
if (!isMain && pr) {
|
|
284
374
|
const prColor = pr.state === "MERGED" ? "magenta" : pr.state === "OPEN" ? "green" : "red";
|
|
285
375
|
const draft = pr.isDraft ? " · draft" : "";
|
|
286
376
|
lines.push({
|
|
@@ -299,12 +389,12 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
299
389
|
lines.push({ text: ` ${pr.url}`, dim: true });
|
|
300
390
|
}
|
|
301
391
|
}
|
|
302
|
-
else {
|
|
392
|
+
else if (!isMain) {
|
|
303
393
|
lines.push(sectionHeader("◉", "Pull Request"));
|
|
304
394
|
lines.push({ text: " no PR yet", dim: true });
|
|
305
395
|
}
|
|
306
396
|
// ── Checks ────────────────────────────────────────────────────────
|
|
307
|
-
if (checks && checks.length > 0) {
|
|
397
|
+
if (!isMain && checks && checks.length > 0) {
|
|
308
398
|
const passing = checks.filter((c) => c.bucket === "pass");
|
|
309
399
|
const failing = checks.filter((c) => c.bucket === "fail");
|
|
310
400
|
const pending = checks.filter((c) => c.bucket !== "pass" && c.bucket !== "fail");
|
|
@@ -338,7 +428,7 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
338
428
|
}
|
|
339
429
|
}
|
|
340
430
|
// ── Reviews ───────────────────────────────────────────────────────
|
|
341
|
-
if (reviews && reviews.length > 0) {
|
|
431
|
+
if (!isMain && reviews && reviews.length > 0) {
|
|
342
432
|
lines.push(ruleLine);
|
|
343
433
|
lines.push(sectionHeader("★", "Reviews"));
|
|
344
434
|
for (const review of reviews) {
|
|
@@ -297,7 +297,20 @@ export function MultilineTextArea({ value, onChange, onSubmit, onCancel, placeho
|
|
|
297
297
|
return;
|
|
298
298
|
if (!input)
|
|
299
299
|
return;
|
|
300
|
-
|
|
300
|
+
// Strip OSC sequences (terminal-side responses to OSC 11/52 etc.
|
|
301
|
+
// queries) — they leak into stdin while a refresh is querying
|
|
302
|
+
// the background color and would otherwise type themselves into
|
|
303
|
+
// the buffer. Pattern: anything starting with `]` followed by a
|
|
304
|
+
// number, semicolon, payload, then BEL or ST. We strip both the
|
|
305
|
+
// fully-formed OSC `\x1b]…\x07` and the bracket-only fragment
|
|
306
|
+
// that arrives when Ink consumed the leading ESC as a separate
|
|
307
|
+
// keypress (which it does for almost all OSC responses).
|
|
308
|
+
let cleaned = input
|
|
309
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, "")
|
|
310
|
+
.replace(/^\][0-9]+;[^\x07]*\x07?/, "");
|
|
311
|
+
if (!cleaned)
|
|
312
|
+
return;
|
|
313
|
+
insertAt(cursor, cleaned.replace(/\r\n?/g, "\n"));
|
|
301
314
|
}, { isActive: focus });
|
|
302
315
|
const innerWidth = Math.max(1, (width ?? 80) - 4);
|
|
303
316
|
const rows = buildVisualRows(value, innerWidth);
|
|
@@ -25,4 +25,9 @@ interface PrCreateOverlayProps {
|
|
|
25
25
|
dispatch: React.Dispatch<DashboardAction>;
|
|
26
26
|
}
|
|
27
27
|
export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
interface HelpOverlayProps {
|
|
29
|
+
width: number;
|
|
30
|
+
height: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function HelpOverlay({ width, height }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element;
|
|
28
33
|
export {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import Spinner from "ink-spinner";
|
|
4
|
-
import TextInput from "ink-text-input";
|
|
5
4
|
import { MultilineTextArea } from "./MultilineTextArea.js";
|
|
6
5
|
export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phase, message, error, dispatch, onSubmit, }) {
|
|
7
6
|
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Commit & Push" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), gitStatus ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: "Changes:" }), gitStatus
|
|
@@ -19,7 +18,7 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
|
|
|
19
18
|
color = "yellow";
|
|
20
19
|
}
|
|
21
20
|
return (_jsxs(Text, { color: color, children: [" ", line] }, i));
|
|
22
|
-
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "awaiting-message" && (_jsxs(
|
|
21
|
+
}), gitStatus.split("\n").length > 8 && (_jsxs(Text, { dimColor: true, children: [" +", gitStatus.split("\n").length - 8, " more"] }))] })) : null, _jsx(Text, { children: " " }), phase === "confirm-stage" && (_jsxs(Text, { children: ["Stage all changes?", " ", _jsx(Text, { color: "cyan", bold: true, children: "y" }), "/", _jsx(Text, { color: "cyan", bold: true, children: "n" })] })), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to write the message?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 let Claude draft a short message"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "m" }), " ", "Manual \u2014 type it yourself"] })] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Drafting commit message with Claude..."] })), phase === "awaiting-message" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit commit message" }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: message, onChange: (v) => dispatch({ type: "COMMIT_MESSAGE", message: v }), onSubmit: () => onSubmit(message), onCancel: () => dispatch({ type: "COMMIT_CANCEL" }), width: width, height: Math.max(3, Math.min(6, height - 12)), placeholder: "(empty)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " commit · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] })), phase === "committing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Committing..."] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing..."] })), phase === "done" && (_jsx(Text, { color: "green", bold: true, children: "Committed and pushed!" })), phase === "error" && _jsx(Text, { color: "red", children: error }), phase !== "awaiting-message" && phase !== "done" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
23
22
|
}
|
|
24
23
|
export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }) {
|
|
25
24
|
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { bold: true, color: "cyan", children: "Create Pull Request" }), _jsx(Text, { dimColor: true, children: "─".repeat(Math.min(width, 50)) }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "branch: " }), _jsx(Text, { children: branch })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "ticket: " }), _jsx(Text, { children: ticketId })] }), _jsx(Text, { children: " " }), phase === "choose-mode" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "How do you want to create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "f" }), " ", "Fill \u2014 use AI to fill the PR template"] }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "Web \u2014 open in browser to edit manually"] })] })), phase === "pushing" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Pushing branch..."] })), phase === "filling" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Filling PR template with AI..."] })), phase === "review" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Edit PR description" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsx(MultilineTextArea, { value: body ?? "", onChange: (v) => dispatch({ type: "PR_CREATE_BODY_CHANGE", body: v }), onSubmit: () => dispatch({ type: "PR_CREATE_CONFIRM" }), onCancel: () => dispatch({ type: "PR_CREATE_CANCEL" }), width: width, height: Math.max(6, height - 10), placeholder: "(empty PR body)" }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "Ctrl+D" }), " send · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+O" }), " editor · ", _jsx(Text, { color: "cyan", bold: true, children: "Ctrl+G" }), " cancel"] })] })), phase === "confirm" && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Create this PR?" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "title: " }), _jsx(Text, { children: title })] }), _jsx(Text, { children: " " }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1, children: [(body ?? "")
|
|
@@ -27,3 +26,77 @@ export function PrCreateOverlay({ width, height, branch, ticketId, phase, error,
|
|
|
27
26
|
.slice(0, Math.max(4, height - 12))
|
|
28
27
|
.map((line, i) => (_jsx(Text, { wrap: "truncate", children: line || " " }, i))), (body ?? "").split("\n").length > Math.max(4, height - 12) && (_jsxs(Text, { dimColor: true, children: ["\u2026+", (body ?? "").split("\n").length - Math.max(4, height - 12), " more lines"] }))] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [_jsx(Text, { color: "green", bold: true, children: "y" }), " / ", _jsx(Text, { color: "green", bold: true, children: "Enter" }), " create ", _jsx(Text, { color: "yellow", bold: true, children: "e" }), " keep editing ", _jsx(Text, { color: "cyan", bold: true, children: "w" }), " open in browser ", _jsx(Text, { color: "red", bold: true, children: "ESC" }), " cancel"] })] })), phase === "creating" && (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), " ", "Creating PR..."] })), phase === "done" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "green", bold: true, children: "PR created!" }), url ? _jsx(Text, { dimColor: true, children: url }) : null] })), phase === "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [_jsx(Text, { color: "cyan", bold: true, children: "w" }), " ", "open in browser ESC cancel"] })] })), phase !== "review" && phase !== "confirm" && phase !== "error" && (_jsxs(_Fragment, { children: [_jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }))] }));
|
|
29
28
|
}
|
|
29
|
+
const LEGEND = [
|
|
30
|
+
{
|
|
31
|
+
title: "Issue list",
|
|
32
|
+
rows: [
|
|
33
|
+
{ glyph: "▎", color: "red", meaning: "Urgent (P1) priority" },
|
|
34
|
+
{ glyph: "▎", color: "yellow", meaning: "High (P2) priority" },
|
|
35
|
+
{ glyph: "●", color: "green", meaning: "State: started / In Progress" },
|
|
36
|
+
{ glyph: "●", color: "blue", meaning: "State: unstarted / In Review" },
|
|
37
|
+
{ glyph: "●", color: "gray", meaning: "State: backlog / orphaned" },
|
|
38
|
+
{ glyph: "●", color: "magenta", meaning: "State: main repo (your non-worktree checkout)" },
|
|
39
|
+
{ glyph: "✓", color: "green", meaning: "WT column: worktree exists" },
|
|
40
|
+
{ glyph: "·", color: "gray", meaning: "WT column: no worktree" },
|
|
41
|
+
{ glyph: "✓", color: "green", meaning: "CI column: all checks passing" },
|
|
42
|
+
{ glyph: "✗", color: "red", meaning: "CI column: a check is failing" },
|
|
43
|
+
{ glyph: "●", color: "yellow", meaning: "CI column: checks pending / running" },
|
|
44
|
+
{ glyph: "·", color: "gray", meaning: "CI column: no PR or no checks" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Detail panel — Worktree",
|
|
49
|
+
rows: [
|
|
50
|
+
{ glyph: "● dirty", color: "yellow", meaning: "Uncommitted changes" },
|
|
51
|
+
{ glyph: "✓ clean", color: "green", meaning: "Working tree clean" },
|
|
52
|
+
{ glyph: "↑ N", color: "cyan", meaning: "N commits ahead of base" },
|
|
53
|
+
{ glyph: "↓ N behind", color: "yellow", meaning: "Main repo: N commits to pull from origin" },
|
|
54
|
+
{ glyph: "◆", color: "red", meaning: "Session needs input (permission prompt)" },
|
|
55
|
+
{ glyph: "◆", color: "green", meaning: "Session active (Claude is working)" },
|
|
56
|
+
{ glyph: "◆", color: "yellow", meaning: "Session idle (waiting for prompt)" },
|
|
57
|
+
{ glyph: "◇", color: "cyan", meaning: "Session id stored, no live signal" },
|
|
58
|
+
{ glyph: "◇", color: "gray", meaning: "No session" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
title: "Detail panel — Tasks (Claude todos)",
|
|
63
|
+
rows: [
|
|
64
|
+
{ glyph: "◐", color: "yellow", meaning: "Task in progress" },
|
|
65
|
+
{ glyph: "◯", color: "gray", meaning: "Task pending" },
|
|
66
|
+
{ glyph: "✓", color: "green", meaning: "Task completed" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
title: "Section icons",
|
|
71
|
+
rows: [
|
|
72
|
+
{ glyph: "⎇", color: "cyan", meaning: "Worktree / Branch" },
|
|
73
|
+
{ glyph: "◉", color: "cyan", meaning: "Pull Request" },
|
|
74
|
+
{ glyph: "✓", color: "cyan", meaning: "Checks" },
|
|
75
|
+
{ glyph: "★", color: "cyan", meaning: "Reviews" },
|
|
76
|
+
{ glyph: "⎈", color: "cyan", meaning: "Tasks (Claude todos)" },
|
|
77
|
+
{ glyph: "◎", color: "cyan", meaning: "Linked tracker ticket (review tab)" },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
export function HelpOverlay({ width, height }) {
|
|
82
|
+
const lines = [];
|
|
83
|
+
for (const section of LEGEND) {
|
|
84
|
+
lines.push({ text: section.title, bold: true });
|
|
85
|
+
for (const row of section.rows) {
|
|
86
|
+
lines.push({
|
|
87
|
+
text: "",
|
|
88
|
+
segments: [
|
|
89
|
+
{ text: " " },
|
|
90
|
+
{ text: row.glyph.padEnd(3, " "), color: row.color, bold: true },
|
|
91
|
+
{ text: " " },
|
|
92
|
+
{ text: row.meaning, dim: true },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
lines.push({ text: "" });
|
|
97
|
+
}
|
|
98
|
+
// Trim trailing blank
|
|
99
|
+
if (lines[lines.length - 1]?.text === "")
|
|
100
|
+
lines.pop();
|
|
101
|
+
return (_jsx(Box, { width: width, height: height, flexDirection: "column", alignItems: "center", justifyContent: "center", children: _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 3, paddingY: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Dashboard glyph reference" }), _jsx(Text, { children: " " }), lines.map((line, i) => (_jsx(Box, { children: line.segments ? (_jsx(Text, { children: line.segments.map((seg, j) => (_jsx(Text, { color: seg.color, bold: seg.bold, dimColor: seg.dim, children: seg.text }, j))) })) : (_jsx(Text, { bold: line.bold, dimColor: line.dim, children: line.text || " " })) }, i))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Press ? or Esc to close" })] }) }));
|
|
102
|
+
}
|
|
@@ -10,6 +10,13 @@ export type ReviewActionItem = {
|
|
|
10
10
|
label: string;
|
|
11
11
|
color: string;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Action footer for the reviews tab. The factory mirrors `buildIssueActions`
|
|
15
|
+
* over in DetailPanel — same shape so the dashboard's action-row renderer
|
|
16
|
+
* doesn't need a per-tab branch. Disabled-state semantics: when an action
|
|
17
|
+
* doesn't apply (no ticket, no worktree), we omit the entry rather than
|
|
18
|
+
* dimming it, matching the issues tab's convention.
|
|
19
|
+
*/
|
|
13
20
|
export declare function buildReviewActions(item: EnrichedReviewPR): ReviewActionItem[];
|
|
14
21
|
export default function ReviewDetailPanel({ item, scrollOffset, height, width }: Props): import("react/jsx-runtime").JSX.Element;
|
|
15
22
|
export {};
|