santree 0.1.5 → 0.2.1

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.
@@ -4,8 +4,11 @@ import Spinner from "ink-spinner";
4
4
  import { useEffect, useState } from "react";
5
5
  import { exec, execSync } from "child_process";
6
6
  import { promisify } from "util";
7
+ import { createRequire } from "module";
7
8
  import * as fs from "fs";
8
9
  import * as path from "path";
10
+ const require = createRequire(import.meta.url);
11
+ const { version } = require("../../package.json");
9
12
  import { findMainRepoRoot, getSantreeDir, getInitScriptPath } from "../lib/git.js";
10
13
  import { getAuthStatus, getValidTokens } from "../lib/linear.js";
11
14
  const execAsync = promisify(exec);
@@ -336,5 +339,5 @@ export default function Doctor() {
336
339
  const optionalMissing = tools.filter((t) => !t.required && !t.installed);
337
340
  const linearOk = linear?.authenticated && linear?.tokenValid && linear?.repoLinked;
338
341
  const allRequired = requiredMissing.length === 0 && linearOk && shellStatus?.configured;
339
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }) }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), linear && _jsx(LinearRow, { linear: linear }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (linearOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
342
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Santree Doctor" }), _jsxs(Text, { dimColor: true, children: [" v", version] })] }), _jsx(Box, { marginBottom: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "CLI Tools" }) }), tools.map((tool) => (_jsx(ToolRow, { tool: tool }, tool.name))), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Integrations" }) }), linear && _jsx(LinearRow, { linear: linear }), shellStatus && _jsx(ShellRow, { configured: shellStatus.configured, shell: shellStatus.shell }), santreeSetup && _jsx(SantreeSetupRow, { status: santreeSetup }), _jsx(Box, { marginBottom: 1, marginTop: 1, flexDirection: "column", children: _jsx(Text, { bold: true, underline: true, children: "Aesthetics" }) }), statusline && _jsx(StatuslineRow, { status: statusline }), _jsx(Box, { marginTop: 1, borderStyle: "single", borderColor: allRequired ? "green" : "yellow", paddingX: 2, children: allRequired ? (_jsx(Text, { color: "green", children: "All requirements satisfied! Santree is ready to use." })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "yellow", children: [requiredMissing.length + (linearOk ? 0 : 1) + (shellStatus?.configured ? 0 : 1), " ", "required item(s) need attention"] }), optionalMissing.length > 0 && (_jsxs(Text, { dimColor: true, children: [optionalMissing.length, " optional item(s) not installed"] }))] })) })] }));
340
343
  }
@@ -2,8 +2,8 @@ import { z } from "zod/v4";
2
2
  export declare const description = "Render a template to stdout";
3
3
  export declare const args: z.ZodTuple<[z.ZodEnum<{
4
4
  linear: "linear";
5
- "git-changes": "git-changes";
6
5
  pr: "pr";
6
+ "git-changes": "git-changes";
7
7
  "fix-pr": "fix-pr";
8
8
  review: "review";
9
9
  }>], null>;
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
5
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, fetchAndRenderPR, fetchAndRenderDiff, } from "../../lib/ai.js";
6
6
  export const description = "Fix PR review comments";
7
7
  export default function Fix() {
8
8
  const [status, setStatus] = useState("loading");
@@ -54,5 +54,5 @@ export default function Fix() {
54
54
  }
55
55
  init();
56
56
  }, []);
57
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
57
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Fix PR" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "magenta", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "magenta", color: "white", bold: true, children: " fix PR " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), " ", `"<fix-pr prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
58
58
  }
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
5
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, fetchAndRenderDiff, } from "../../lib/ai.js";
6
6
  export const description = "Review changes against ticket requirements";
7
7
  export default function Review() {
8
8
  const [status, setStatus] = useState("loading");
@@ -47,5 +47,5 @@ export default function Review() {
47
47
  }
48
48
  init();
49
49
  }, []);
50
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" happy ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
50
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Review" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : "yellow", paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: "yellow", color: "white", bold: true, children: " review " })] })] }), _jsxs(Box, { marginTop: 1, children: [(status === "loading" || status === "fetching") && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", status === "loading" ? "Loading..." : "Fetching ticket, diff, and PR feedback..."] })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), " ", `"<review prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
51
51
  }
@@ -3,7 +3,9 @@ import { useEffect, useState } from "react";
3
3
  import { Text, Box } from "ink";
4
4
  import Spinner from "ink-spinner";
5
5
  import { z } from "zod";
6
- import { resolveAIContext, renderAIPrompt, launchAgent, cleanupImages, } from "../../lib/ai.js";
6
+ import { resolveAIContext, renderAIPrompt, launchAgent, resolveAgentBinary, cleanupImages, } from "../../lib/ai.js";
7
+ import { randomUUID } from "crypto";
8
+ import { getSessionId, setSessionId } from "../../lib/git.js";
7
9
  export const description = "Launch Claude to work on current ticket";
8
10
  export const options = z.object({
9
11
  plan: z.boolean().optional().describe("Only create implementation plan"),
@@ -45,8 +47,22 @@ export default function Work({ options }) {
45
47
  return;
46
48
  setStatus("launching");
47
49
  const prompt = renderAIPrompt("work", aiContext, { mode });
50
+ // Get or create a session ID for this ticket
51
+ let sessionId;
52
+ let isResume = false;
53
+ if (aiContext.ticketId) {
54
+ const existing = getSessionId(aiContext.mainRoot, aiContext.ticketId);
55
+ if (existing) {
56
+ sessionId = existing;
57
+ isResume = true;
58
+ }
59
+ else {
60
+ sessionId = randomUUID();
61
+ setSessionId(aiContext.mainRoot, aiContext.ticketId, sessionId);
62
+ }
63
+ }
48
64
  try {
49
- const child = launchAgent(prompt, { planMode: mode === "plan" });
65
+ const child = launchAgent(prompt, { planMode: mode === "plan", sessionId, resume: isResume });
50
66
  child.on("error", (err) => {
51
67
  setStatus("error");
52
68
  setError(`Failed to launch agent: ${err.message}`);
@@ -62,5 +78,5 @@ export default function Work({ options }) {
62
78
  setError(err instanceof Error ? err.message : "Failed to launch agent");
63
79
  }
64
80
  }, [status, aiContext, mode]);
65
- return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude (through Happy)..." }), _jsxs(Text, { dimColor: true, children: [" ", "happy", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
81
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, width: "100%", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Work" }) }), _jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: status === "error" ? "red" : getModeColor(mode), paddingX: 1, width: "100%", children: [branch && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "branch:" }), _jsx(Text, { color: "cyan", bold: true, children: branch })] })), ticketId && (_jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "ticket:" }), _jsx(Text, { color: "blue", bold: true, children: ticketId })] })), _jsxs(Box, { gap: 1, children: [_jsx(Text, { dimColor: true, children: "mode:" }), _jsx(Text, { backgroundColor: getModeColor(mode), color: "white", bold: true, children: ` ${getModeLabel(mode)} ` })] })] }), _jsxs(Box, { marginTop: 1, children: [status === "loading" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Loading..." })] })), status === "fetching" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Fetching ticket from Linear..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: "green", bold: true, children: ["\u2713 Launching ", resolveAgentBinary() === "happy" ? "Claude (through Happy)" : "Claude", "..."] }), _jsxs(Text, { dimColor: true, children: [" ", resolveAgentBinary(), mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
66
82
  }
package/dist/lib/ai.d.ts CHANGED
@@ -36,6 +36,11 @@ export declare function fetchAndRenderPR(branch: string): Promise<string | null>
36
36
  * Returns rendered markdown.
37
37
  */
38
38
  export declare function fetchAndRenderDiff(branch: string): Promise<string>;
39
+ /**
40
+ * Resolve which agent binary to use (happy if installed, otherwise claude).
41
+ * Returns the binary name, or null if neither is installed.
42
+ */
43
+ export declare function resolveAgentBinary(): string | null;
39
44
  /**
40
45
  * Launch an interactive agent session with a prompt.
41
46
  * Resolves the agent binary (happy > claude), passes prompt directly
@@ -44,6 +49,8 @@ export declare function fetchAndRenderDiff(branch: string): Promise<string>;
44
49
  */
45
50
  export declare function launchAgent(prompt: string, opts?: {
46
51
  planMode?: boolean;
52
+ sessionId?: string;
53
+ resume?: boolean;
47
54
  }): ChildProcess;
48
55
  export interface RunAgentResult {
49
56
  success: boolean;
package/dist/lib/ai.js CHANGED
@@ -108,7 +108,7 @@ export async function fetchAndRenderDiff(branch) {
108
108
  * Resolve which agent binary to use (happy if installed, otherwise claude).
109
109
  * Returns the binary name, or null if neither is installed.
110
110
  */
111
- function resolveAgentBinary() {
111
+ export function resolveAgentBinary() {
112
112
  for (const bin of ["happy", "claude"]) {
113
113
  try {
114
114
  execSync(`which ${bin}`, { stdio: "ignore" });
@@ -150,6 +150,14 @@ export function launchAgent(prompt, opts) {
150
150
  if (opts?.planMode) {
151
151
  args.push("--permission-mode", "plan");
152
152
  }
153
+ if (opts?.sessionId) {
154
+ if (opts.resume) {
155
+ args.push("--resume", opts.sessionId);
156
+ }
157
+ else {
158
+ args.push("--session-id", opts.sessionId);
159
+ }
160
+ }
153
161
  args.push("--", promptArg(prompt));
154
162
  return spawn(bin, args, { stdio: "inherit" });
155
163
  }
@@ -0,0 +1,11 @@
1
+ import type { DashboardIssue } from "./types.js";
2
+ interface Props {
3
+ issue: DashboardIssue | null;
4
+ scrollOffset: number;
5
+ height: number;
6
+ width: number;
7
+ creatingForTicket: string | null;
8
+ creationLogs: string;
9
+ }
10
+ export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,230 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ function stateColor(type) {
4
+ switch (type) {
5
+ case "started":
6
+ return "green";
7
+ case "unstarted":
8
+ return "blue";
9
+ case "backlog":
10
+ return "gray";
11
+ default:
12
+ return "yellow";
13
+ }
14
+ }
15
+ function parseGitStatus(raw) {
16
+ if (!raw)
17
+ return { staged: 0, unstaged: 0, untracked: 0, files: [] };
18
+ const lines = raw.split("\n").filter(Boolean);
19
+ let staged = 0;
20
+ let unstaged = 0;
21
+ let untracked = 0;
22
+ const files = [];
23
+ for (const line of lines) {
24
+ if (line.length < 2)
25
+ continue;
26
+ const x = line[0];
27
+ const y = line[1];
28
+ const file = line.slice(3);
29
+ if (x === "?") {
30
+ untracked++;
31
+ }
32
+ else {
33
+ if (x !== " ")
34
+ staged++;
35
+ if (y !== " ")
36
+ unstaged++;
37
+ }
38
+ files.push({ xy: line.slice(0, 2), file });
39
+ }
40
+ return { staged, unstaged, untracked, files };
41
+ }
42
+ function fileColor(xy) {
43
+ const x = xy[0];
44
+ if (x !== " " && x !== "?")
45
+ return "green";
46
+ if (xy.startsWith("??"))
47
+ return "gray";
48
+ return "yellow";
49
+ }
50
+ function buildActions(worktree, pr) {
51
+ const items = [];
52
+ // Work/Resume
53
+ if (worktree?.sessionId) {
54
+ items.push({ key: "↵", label: "Resume", color: "cyan" });
55
+ }
56
+ else if (worktree) {
57
+ items.push({ key: "w", label: "Work", color: "cyan" });
58
+ items.push({ key: "↵", label: "Switch", color: "cyan" });
59
+ }
60
+ else {
61
+ items.push({ key: "w", label: "Work", color: "cyan" });
62
+ }
63
+ // Editor
64
+ if (worktree) {
65
+ items.push({ key: "e", label: "Editor", color: "cyan" });
66
+ }
67
+ // Commit
68
+ if (worktree?.dirty) {
69
+ items.push({ key: "C", label: "Commit", color: "cyan" });
70
+ }
71
+ // PR actions
72
+ if (worktree && !pr) {
73
+ items.push({ key: "c", label: "Create PR", color: "cyan" });
74
+ }
75
+ if (pr) {
76
+ items.push({ key: "f", label: "Fix PR", color: "cyan" });
77
+ items.push({ key: "r", label: "Review", color: "cyan" });
78
+ }
79
+ // Links
80
+ items.push({ key: "o", label: "Linear", color: "gray" });
81
+ if (pr)
82
+ items.push({ key: "p", label: "Open PR", color: "gray" });
83
+ // Destructive
84
+ if (worktree) {
85
+ items.push({ key: "d", label: "Remove", color: "red" });
86
+ }
87
+ return [items];
88
+ }
89
+ export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }) {
90
+ // Show creation logs when selected issue is being created
91
+ if (issue && issue.issue.identifier === creatingForTicket) {
92
+ const logLines = creationLogs.split("\n");
93
+ const contentRows = height - 1;
94
+ const startIdx = Math.max(0, logLines.length - contentRows);
95
+ const visible = logLines.slice(startIdx, startIdx + contentRows);
96
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsxs(Text, { color: "yellow", bold: true, children: ["Setting up worktree for ", creatingForTicket, "..."] }), visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: line }) }, i)))] }));
97
+ }
98
+ if (!issue) {
99
+ return (_jsx(Box, { width: width, height: height, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "No issue selected" }) }));
100
+ }
101
+ const { issue: li, worktree, pr } = issue;
102
+ const lines = [];
103
+ const rule = "─".repeat(width);
104
+ // ── Hero: identifier + title ──────────────────────────────────────
105
+ lines.push({ text: `${li.identifier} ${li.title}`, bold: true });
106
+ const meta = [];
107
+ meta.push(li.state.name);
108
+ meta.push(li.priorityLabel);
109
+ if (li.labels.length > 0)
110
+ meta.push(li.labels.join(", "));
111
+ lines.push({ text: meta.join(" · "), color: stateColor(li.state.type) });
112
+ // ── Description ───────────────────────────────────────────────────
113
+ if (li.description) {
114
+ lines.push({ text: rule, dim: true });
115
+ lines.push({ text: "" });
116
+ for (const dLine of li.description.trimEnd().split("\n")) {
117
+ lines.push({ text: dLine });
118
+ }
119
+ lines.push({ text: "" });
120
+ }
121
+ // ── Worktree (enhanced) ───────────────────────────────────────────
122
+ lines.push({ text: rule, dim: true });
123
+ lines.push({ text: "WORKTREE", dim: true });
124
+ if (worktree) {
125
+ lines.push({ text: ` ${worktree.branch}` });
126
+ lines.push({ text: ` ${worktree.path}`, dim: true });
127
+ const gs = parseGitStatus(worktree.gitStatus);
128
+ const statusParts = [];
129
+ if (gs.staged > 0)
130
+ statusParts.push(`+${gs.staged} staged`);
131
+ if (gs.unstaged > 0)
132
+ statusParts.push(`~${gs.unstaged} unstaged`);
133
+ if (gs.untracked > 0)
134
+ statusParts.push(`?${gs.untracked} untracked`);
135
+ if (worktree.commitsAhead > 0)
136
+ statusParts.push(`+${worktree.commitsAhead} ahead`);
137
+ if (statusParts.length > 0) {
138
+ lines.push({
139
+ text: ` ${statusParts.join(" ")}`,
140
+ color: worktree.dirty ? "yellow" : "green",
141
+ });
142
+ }
143
+ else {
144
+ lines.push({ text: " ✓ clean", color: "green" });
145
+ }
146
+ // Show individual files (up to 8)
147
+ const maxFiles = 8;
148
+ for (let i = 0; i < Math.min(gs.files.length, maxFiles); i++) {
149
+ const f = gs.files[i];
150
+ lines.push({ text: ` ${f.xy} ${f.file}`, color: fileColor(f.xy) });
151
+ }
152
+ if (gs.files.length > maxFiles) {
153
+ lines.push({ text: ` +${gs.files.length - maxFiles} more`, dim: true });
154
+ }
155
+ if (worktree.sessionId) {
156
+ lines.push({ text: ` session: ${worktree.sessionId}`, color: "cyan" });
157
+ }
158
+ else {
159
+ lines.push({ text: " session: none", color: "red" });
160
+ }
161
+ }
162
+ else {
163
+ lines.push({ text: " –", dim: true });
164
+ }
165
+ // ── Pull Request ──────────────────────────────────────────────────
166
+ const { checks, reviews } = issue;
167
+ lines.push({ text: rule, dim: true });
168
+ lines.push({ text: "PULL REQUEST", dim: true });
169
+ if (pr) {
170
+ const sc = pr.state === "MERGED" ? "magenta" : pr.state === "OPEN" ? "green" : "red";
171
+ const draft = pr.isDraft ? " draft" : "";
172
+ lines.push({ text: ` #${pr.number} ${pr.state}${draft}`, color: sc });
173
+ if (pr.url) {
174
+ lines.push({ text: ` ${pr.url}`, dim: true });
175
+ }
176
+ }
177
+ else {
178
+ lines.push({ text: " –", dim: true });
179
+ }
180
+ // ── Checks ────────────────────────────────────────────────────────
181
+ if (checks && checks.length > 0) {
182
+ const passCount = checks.filter((c) => c.bucket === "pass").length;
183
+ lines.push({ text: rule, dim: true });
184
+ lines.push({ text: `CHECKS ${passCount}/${checks.length} passing`, dim: true });
185
+ for (const check of checks) {
186
+ if (check.bucket === "pass") {
187
+ lines.push({ text: ` ✓ ${check.name}`, color: "green" });
188
+ }
189
+ else if (check.bucket === "fail") {
190
+ const desc = check.description ? ` — ${check.description}` : "";
191
+ lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
192
+ }
193
+ else {
194
+ lines.push({ text: ` ● ${check.name} (pending)`, color: "yellow" });
195
+ }
196
+ }
197
+ }
198
+ // ── Reviews ───────────────────────────────────────────────────────
199
+ if (reviews && reviews.length > 0) {
200
+ lines.push({ text: rule, dim: true });
201
+ lines.push({ text: "REVIEWS", dim: true });
202
+ for (const review of reviews) {
203
+ const author = review.author.login;
204
+ const rc = review.state === "APPROVED"
205
+ ? "green"
206
+ : review.state === "CHANGES_REQUESTED"
207
+ ? "red"
208
+ : "yellow";
209
+ lines.push({ text: ` ${author} ${review.state}`, color: rc });
210
+ }
211
+ }
212
+ // ── Build actions footer ──────────────────────────────────────────
213
+ const actionRows = buildActions(worktree, pr);
214
+ // +1 for the separator line
215
+ const actionsHeight = actionRows.length + 1;
216
+ const scrollableHeight = height - actionsHeight;
217
+ // ── Render scrollable content ─────────────────────────────────────
218
+ const totalLines = lines.length;
219
+ const canScroll = totalLines > scrollableHeight;
220
+ const contentRows = canScroll ? scrollableHeight - 2 : scrollableHeight;
221
+ const clampedOffset = Math.min(scrollOffset, Math.max(0, totalLines - contentRows));
222
+ const visible = lines.slice(clampedOffset, clampedOffset + contentRows);
223
+ let scrollArrow = null;
224
+ if (canScroll) {
225
+ const atTop = clampedOffset === 0;
226
+ const atBottom = clampedOffset + contentRows >= totalLines;
227
+ scrollArrow = atTop ? "↓ scroll" : atBottom ? "↑ scroll" : "↑↓ scroll";
228
+ }
229
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, children: line.text || " " }) }, i))), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: " " }) })), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: scrollArrow }) })), _jsx(Box, { flexGrow: 1 }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: rule }) }), actionRows.map((row, i) => (_jsx(Box, { children: row.map((item, j) => (_jsxs(Text, { children: [" ", _jsx(Text, { color: item.color, bold: true, children: item.key }), _jsxs(Text, { color: item.color === "gray" ? "gray" : "white", children: [" ", item.label] })] }, j))) }, `a-${i}`)))] }));
230
+ }
@@ -0,0 +1,13 @@
1
+ import type { ProjectGroup, DashboardIssue } from "./types.js";
2
+ interface Props {
3
+ groups: ProjectGroup[];
4
+ flatIssues: DashboardIssue[];
5
+ selectedIndex: number;
6
+ scrollOffset: number;
7
+ height: number;
8
+ width: number;
9
+ creatingForTicket: string | null;
10
+ deletingForTicket: string | null;
11
+ }
12
+ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, creatingForTicket, deletingForTicket, }: Props): import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ function stateColor(type, name) {
4
+ const n = name?.toLowerCase();
5
+ if (n === "blocked")
6
+ return "red";
7
+ if (n === "in review")
8
+ return "green";
9
+ if (n === "in progress")
10
+ return "yellow";
11
+ switch (type) {
12
+ case "started":
13
+ return "green";
14
+ case "unstarted":
15
+ return "blue";
16
+ case "backlog":
17
+ return "gray";
18
+ default:
19
+ return "yellow";
20
+ }
21
+ }
22
+ function priorityIndicator(priority) {
23
+ switch (priority) {
24
+ case 1:
25
+ return { text: "!!!", color: "red" };
26
+ case 2:
27
+ return { text: "!! ", color: "yellow" };
28
+ case 3:
29
+ return { text: "! ", color: "blue" };
30
+ case 4:
31
+ return { text: "· ", color: "gray" };
32
+ default:
33
+ return { text: " ", color: "gray" };
34
+ }
35
+ }
36
+ function checksIndicator(checks) {
37
+ if (!checks || checks.length === 0)
38
+ return { text: "-", color: "gray" };
39
+ if (checks.some((c) => c.bucket === "fail"))
40
+ return { text: "✗", color: "red" };
41
+ if (checks.every((c) => c.bucket === "pass"))
42
+ return { text: "✓", color: "green" };
43
+ return { text: "●", color: "yellow" };
44
+ }
45
+ function prIndicator(pr) {
46
+ if (!pr)
47
+ return { text: "-", color: "gray" };
48
+ const label = `#${pr.number}`;
49
+ if (pr.state === "MERGED")
50
+ return { text: label, color: "magenta" };
51
+ if (pr.state === "CLOSED")
52
+ return { text: label, color: "red" };
53
+ if (pr.isDraft)
54
+ return { text: label, color: "gray" };
55
+ return { text: label, color: "green" };
56
+ }
57
+ function sessionIndicator(wt, isCreating, isDeleting) {
58
+ if (isDeleting)
59
+ return { text: " deleting", color: "red" };
60
+ if (isCreating)
61
+ return { text: " creating", color: "yellow" };
62
+ if (!wt)
63
+ return { text: " -", color: "gray" };
64
+ if (wt.sessionId)
65
+ return { text: " " + wt.sessionId.slice(0, 8), color: "cyan" };
66
+ return { text: " none", color: "red" };
67
+ }
68
+ function buildRows(groups, flatIssues) {
69
+ const rows = [{ kind: "columns" }];
70
+ // Build a map from issue identifier to flat index
71
+ const indexMap = new Map();
72
+ flatIssues.forEach((di, i) => indexMap.set(di.issue.identifier, i));
73
+ for (const group of groups) {
74
+ const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
75
+ rows.push({ kind: "header", name: group.name, count: totalIssues });
76
+ for (const sg of group.statusGroups) {
77
+ rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
78
+ for (const di of sg.issues) {
79
+ rows.push({ kind: "issue", issue: di, flatIndex: indexMap.get(di.issue.identifier) ?? -1 });
80
+ }
81
+ }
82
+ }
83
+ return rows;
84
+ }
85
+ const FOOTER_HEIGHT = 2;
86
+ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, creatingForTicket, deletingForTicket, }) {
87
+ const rows = buildRows(groups, flatIssues);
88
+ const listHeight = height - FOOTER_HEIGHT;
89
+ const visible = rows.slice(scrollOffset, scrollOffset + listHeight);
90
+ // 2 cursor + 2 dot + 4 priority + 11 id + title + 9 session + 1 space + 6 pr + 1 space + 2 checks
91
+ const prColWidth = 6;
92
+ const checksColWidth = 2;
93
+ const sessionColWidth = 9;
94
+ const priorityColWidth = 4;
95
+ const fixedWidth = 2 + 2 + priorityColWidth + 11 + sessionColWidth + 1 + prColWidth + 1 + checksColWidth;
96
+ const titleMaxWidth = Math.max(width - fixedWidth, 10);
97
+ const footerRule = "─".repeat(width);
98
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Box, { flexDirection: "column", height: listHeight, children: visible.map((row, i) => {
99
+ if (row.kind === "columns") {
100
+ const labelPad = 14 + priorityColWidth + titleMaxWidth;
101
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "".padEnd(labelPad) }), _jsx(Text, { dimColor: true, children: "session".padStart(sessionColWidth) }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "pr".padStart(prColWidth) }), _jsx(Text, { dimColor: true, children: " " }), _jsx(Text, { dimColor: true, children: "ci".padStart(checksColWidth) })] }, "col-header"));
102
+ }
103
+ if (row.kind === "header") {
104
+ return (_jsx(Box, { children: _jsxs(Text, { dimColor: true, bold: true, children: ["── ", row.name, " (", row.count, ")", " ──"] }) }, `h-${i}`));
105
+ }
106
+ if (row.kind === "status-header") {
107
+ return (_jsx(Box, { children: _jsxs(Text, { color: stateColor(row.type, row.name), dimColor: true, children: [" ", row.name, " (", row.count, ")"] }) }, `sh-${i}`));
108
+ }
109
+ const { issue, flatIndex } = row;
110
+ const selected = flatIndex === selectedIndex;
111
+ const di = issue;
112
+ const sc = stateColor(di.issue.state.type, di.issue.state.name);
113
+ const isCreating = di.issue.identifier === creatingForTicket;
114
+ const isDeleting = di.issue.identifier === deletingForTicket;
115
+ const sess = sessionIndicator(di.worktree, isCreating, isDeleting);
116
+ const ci = checksIndicator(di.checks);
117
+ const pr = prIndicator(di.pr);
118
+ const prio = priorityIndicator(di.issue.priority);
119
+ const cursor = selected ? ">" : " ";
120
+ const title = di.issue.title.length > titleMaxWidth
121
+ ? di.issue.title.slice(0, titleMaxWidth - 1) + "…"
122
+ : di.issue.title;
123
+ const bg = selected ? "#1e3a5f" : undefined;
124
+ return (_jsxs(Box, { width: width, children: [_jsxs(Text, { backgroundColor: bg, color: selected ? "cyan" : undefined, bold: selected, children: [cursor, " "] }), _jsx(Text, { backgroundColor: bg, color: sc, children: "\u25CF" }), _jsxs(Text, { backgroundColor: bg, color: prio.color, children: [" ", prio.text] }), _jsx(Text, { backgroundColor: bg, color: selected ? "cyan" : undefined, bold: selected, children: di.issue.identifier.padEnd(10) }), _jsx(Text, { backgroundColor: bg, color: selected ? "white" : undefined, bold: selected, children: title.padEnd(titleMaxWidth) }), _jsx(Text, { backgroundColor: bg, color: selected ? (sess.color === "gray" ? "gray" : sess.color) : sess.color, children: sess.text.padStart(sessionColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: selected ? (pr.color === "gray" ? "gray" : pr.color) : pr.color, children: pr.text.padStart(prColWidth) }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: selected ? (ci.color === "gray" ? "gray" : ci.color) : ci.color, children: ci.text.padStart(checksColWidth) })] }, di.issue.identifier));
125
+ }) }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: footerRule }), _jsxs(Text, { children: [" ", _jsx(Text, { color: "cyan", bold: true, children: "j/k" }), _jsx(Text, { color: "white", children: " Navigate" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "Shift + \u2191\u2193" }), _jsx(Text, { color: "white", children: " Scroll detail" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "E" }), _jsx(Text, { color: "white", children: " Workspace" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "R" }), _jsx(Text, { color: "white", children: " Refresh" }), " ", _jsx(Text, { color: "cyan", bold: true, children: "q" }), _jsx(Text, { color: "white", children: " Quit" })] })] })] }));
126
+ }
@@ -0,0 +1,25 @@
1
+ import type { CommitPhase, PrCreatePhase, DashboardAction } from "./types.js";
2
+ interface CommitOverlayProps {
3
+ width: number;
4
+ height: number;
5
+ branch: string | null;
6
+ ticketId: string | null;
7
+ gitStatus: string;
8
+ phase: CommitPhase;
9
+ message: string;
10
+ error: string | null;
11
+ dispatch: React.Dispatch<DashboardAction>;
12
+ onSubmit: (value: string) => void;
13
+ }
14
+ export declare function CommitOverlay({ width, height, branch, ticketId, gitStatus, phase, message, error, dispatch, onSubmit, }: CommitOverlayProps): import("react/jsx-runtime").JSX.Element;
15
+ interface PrCreateOverlayProps {
16
+ width: number;
17
+ height: number;
18
+ branch: string | null;
19
+ ticketId: string | null;
20
+ phase: PrCreatePhase;
21
+ error: string | null;
22
+ url: string | null;
23
+ }
24
+ export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
25
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import TextInput from "ink-text-input";
5
+ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phase, message, error, dispatch, onSubmit, }) {
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
7
+ .split("\n")
8
+ .slice(0, 8)
9
+ .map((line, i) => {
10
+ let color;
11
+ if (line.length >= 2 && line[0] !== " " && line[0] !== "?") {
12
+ color = "green";
13
+ }
14
+ else if (line.startsWith("??")) {
15
+ color = "gray";
16
+ }
17
+ else if (line.startsWith(" ")) {
18
+ color = "yellow";
19
+ }
20
+ return (_jsxs(Text, { color: color, children: [" ", line] }, i));
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 === "awaiting-message" && (_jsxs(Box, { children: [_jsx(Text, { children: "Message: " }), _jsx(TextInput, { value: message, onChange: (v) => dispatch({ type: "COMMIT_MESSAGE", message: v }), onSubmit: onSubmit })] })), 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 }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }));
22
+ }
23
+ export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, }) {
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 auto-fill title & body from commits"] }), _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 === "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" && _jsx(Text, { color: "red", children: error }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "ESC to cancel" })] }));
25
+ }
@@ -0,0 +1,5 @@
1
+ import type { DashboardIssue, ProjectGroup } from "./types.js";
2
+ export declare function loadDashboardData(repoRoot: string): Promise<{
3
+ groups: ProjectGroup[];
4
+ flatIssues: DashboardIssue[];
5
+ }>;