santree 0.6.2 → 0.7.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.
Files changed (35) hide show
  1. package/README.md +7 -7
  2. package/dist/commands/dashboard.js +465 -54
  3. package/dist/commands/issue/setup.d.ts +2 -0
  4. package/dist/commands/issue/setup.js +108 -0
  5. package/dist/commands/issue/switch.d.ts +1 -0
  6. package/dist/commands/issue/switch.js +2 -2
  7. package/dist/commands/worktree/work.js +1 -1
  8. package/dist/lib/ai.js +4 -0
  9. package/dist/lib/dashboard/DetailPanel.d.ts +5 -2
  10. package/dist/lib/dashboard/DetailPanel.js +24 -3
  11. package/dist/lib/dashboard/IssueList.d.ts +2 -0
  12. package/dist/lib/dashboard/IssueList.js +9 -1
  13. package/dist/lib/dashboard/Overlays.d.ts +2 -1
  14. package/dist/lib/dashboard/Overlays.js +17 -3
  15. package/dist/lib/dashboard/data.d.ts +2 -0
  16. package/dist/lib/dashboard/data.js +56 -54
  17. package/dist/lib/dashboard/types.d.ts +80 -2
  18. package/dist/lib/dashboard/types.js +97 -1
  19. package/dist/lib/multiplexer/cmux.js +0 -15
  20. package/dist/lib/multiplexer/none.js +0 -3
  21. package/dist/lib/multiplexer/tmux.js +0 -8
  22. package/dist/lib/multiplexer/types.d.ts +0 -1
  23. package/dist/lib/session-signal.d.ts +5 -3
  24. package/dist/lib/session-signal.js +5 -22
  25. package/dist/lib/trackers/config.js +1 -1
  26. package/dist/lib/trackers/index.d.ts +11 -0
  27. package/dist/lib/trackers/index.js +26 -0
  28. package/dist/lib/trackers/local/frontmatter.d.ts +12 -0
  29. package/dist/lib/trackers/local/frontmatter.js +91 -0
  30. package/dist/lib/trackers/local/index.d.ts +2 -0
  31. package/dist/lib/trackers/local/index.js +102 -0
  32. package/dist/lib/trackers/local/store.d.ts +30 -0
  33. package/dist/lib/trackers/local/store.js +203 -0
  34. package/dist/lib/trackers/types.d.ts +26 -1
  35. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ export declare const description = "Pick and configure the issue tracker for this repo";
2
+ export default function IssueSetup(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,108 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Text, Box, useInput } from "ink";
4
+ import Spinner from "ink-spinner";
5
+ import { findMainRepoRoot } from "../../lib/git.js";
6
+ import { setRepoTracker, getIssueTracker } from "../../lib/trackers/index.js";
7
+ import { setRepoLinearOrg } from "../../lib/trackers/linear/index.js";
8
+ import { readLinearAuthStore } from "../../lib/trackers/auth-store.js";
9
+ import { getAuthenticatedUser, getCurrentRepoNwo } from "../../lib/trackers/github/auth.js";
10
+ export const description = "Pick and configure the issue tracker for this repo";
11
+ const TRACKERS = [
12
+ { kind: "local", label: "Local", hint: "built-in, file-based — no account needed" },
13
+ { kind: "linear", label: "Linear", hint: "OAuth workspace" },
14
+ { kind: "github", label: "GitHub", hint: "GitHub Issues via gh CLI" },
15
+ ];
16
+ export default function IssueSetup() {
17
+ const [phase, setPhase] = useState("checking");
18
+ const [message, setMessage] = useState("");
19
+ const [error, setError] = useState(null);
20
+ const [trackerIdx, setTrackerIdx] = useState(0);
21
+ const [orgs, setOrgs] = useState([]);
22
+ const [orgIdx, setOrgIdx] = useState(0);
23
+ const [repoRoot, setRepoRoot] = useState(null);
24
+ useEffect(() => {
25
+ const root = findMainRepoRoot();
26
+ if (!root) {
27
+ setError("Not inside a git repository");
28
+ setPhase("error");
29
+ return;
30
+ }
31
+ setRepoRoot(root);
32
+ setPhase("pick-tracker");
33
+ }, []);
34
+ function finish(label) {
35
+ setMessage(label);
36
+ setPhase("done");
37
+ }
38
+ async function chooseTracker(kind) {
39
+ const root = repoRoot;
40
+ if (kind === "local") {
41
+ setRepoTracker(root, "local");
42
+ finish(`Active tracker for this repo: ${getIssueTracker(root).displayName}`);
43
+ return;
44
+ }
45
+ if (kind === "linear") {
46
+ const store = readLinearAuthStore();
47
+ const list = Object.entries(store).map(([slug, tokens]) => ({
48
+ slug,
49
+ name: tokens.org_name,
50
+ }));
51
+ if (list.length === 0) {
52
+ setError("No authenticated Linear workspaces. Run: santree linear auth");
53
+ setPhase("error");
54
+ return;
55
+ }
56
+ if (list.length === 1) {
57
+ setRepoLinearOrg(root, list[0].slug);
58
+ setRepoTracker(root, "linear");
59
+ finish(`Linked to ${list[0].name} (${list[0].slug})`);
60
+ return;
61
+ }
62
+ setOrgs(list);
63
+ setOrgIdx(0);
64
+ setPhase("pick-org");
65
+ return;
66
+ }
67
+ // github
68
+ setPhase("checking");
69
+ const user = await getAuthenticatedUser();
70
+ if (!user) {
71
+ setError("GitHub CLI not authenticated. Run: santree github auth");
72
+ setPhase("error");
73
+ return;
74
+ }
75
+ setRepoTracker(root, "github");
76
+ const nwo = await getCurrentRepoNwo(root);
77
+ finish(`Active tracker: GitHub (@${user.login}${nwo ? ` · ${nwo}` : ""})`);
78
+ }
79
+ useInput((_input, key) => {
80
+ if (phase === "pick-tracker") {
81
+ if (key.upArrow)
82
+ setTrackerIdx((i) => Math.max(0, i - 1));
83
+ else if (key.downArrow)
84
+ setTrackerIdx((i) => Math.min(TRACKERS.length - 1, i + 1));
85
+ else if (key.return)
86
+ void chooseTracker(TRACKERS[trackerIdx].kind);
87
+ }
88
+ else if (phase === "pick-org") {
89
+ if (key.upArrow)
90
+ setOrgIdx((i) => Math.max(0, i - 1));
91
+ else if (key.downArrow)
92
+ setOrgIdx((i) => Math.min(orgs.length - 1, i + 1));
93
+ else if (key.return) {
94
+ const org = orgs[orgIdx];
95
+ setRepoLinearOrg(repoRoot, org.slug);
96
+ setRepoTracker(repoRoot, "linear");
97
+ finish(`Linked to ${org.name} (${org.slug})`);
98
+ }
99
+ }
100
+ });
101
+ useEffect(() => {
102
+ if (phase === "done" || phase === "error") {
103
+ const timer = setTimeout(() => process.exit(phase === "error" ? 1 : 0), 100);
104
+ return () => clearTimeout(timer);
105
+ }
106
+ }, [phase]);
107
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "cyan", children: "Issue tracker setup" }) }), phase === "checking" && (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: _jsx(Spinner, { type: "dots" }) }), _jsx(Text, { children: " Checking\u2026" })] })), phase === "pick-tracker" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select the issue tracker for this repo:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: TRACKERS.map((t, i) => (_jsxs(Text, { children: [i === trackerIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), t.label, " ", _jsxs(Text, { dimColor: true, children: ["\u2014 ", t.hint] })] }, t.kind))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), phase === "pick-org" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Select a Linear workspace to link:" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: orgs.map((org, i) => (_jsxs(Text, { children: [i === orgIdx ? (_jsx(Text, { color: "cyan", bold: true, children: "> " })) : (_jsx(Text, { children: " " })), org.name, " (", org.slug, ")"] }, org.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select, Enter to confirm" }) })] })), phase === "done" && _jsxs(Text, { color: "green", children: ["\u2713 ", message] }), phase === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] }));
108
+ }
@@ -3,6 +3,7 @@ export declare const description = "Switch the active issue tracker for this rep
3
3
  export declare const args: z.ZodTuple<[z.ZodEnum<{
4
4
  linear: "linear";
5
5
  github: "github";
6
+ local: "local";
6
7
  }>], null>;
7
8
  type Props = {
8
9
  args: z.infer<typeof args>;
@@ -7,9 +7,9 @@ import { findMainRepoRoot } from "../../lib/git.js";
7
7
  import { setRepoTracker, getIssueTracker } from "../../lib/trackers/index.js";
8
8
  export const description = "Switch the active issue tracker for this repo";
9
9
  export const args = z.tuple([
10
- z.enum(["linear", "github"]).describe(argument({
10
+ z.enum(["linear", "github", "local"]).describe(argument({
11
11
  name: "kind",
12
- description: "Tracker kind: linear or github",
12
+ description: "Tracker kind: linear, github, or local (built-in)",
13
13
  })),
14
14
  ]);
15
15
  export default function IssueSwitch({ args }) {
@@ -99,5 +99,5 @@ export default function Work({ options }) {
99
99
  setError(err instanceof Error ? err.message : "Failed to launch agent");
100
100
  }
101
101
  }, [status, aiContext, mode]);
102
- 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 issue from tracker..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" ", "claude", mode === "plan" ? " --permission-mode plan" : "", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
102
+ 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 issue from tracker..." })] })), status === "launching" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", bold: true, children: "\u2713 Launching Claude..." }), _jsxs(Text, { dimColor: true, children: [" ", "claude --permission-mode ", mode === "plan" ? "plan" : "auto", " ", `"<${getModeLabel(mode)} prompt for ${ticketId}>"`] })] })), status === "error" && (_jsxs(Text, { color: "red", bold: true, children: ["\u2717 ", error] }))] })] }));
103
103
  }
package/dist/lib/ai.js CHANGED
@@ -185,6 +185,10 @@ export function launchAgent(prompt, opts) {
185
185
  throw new Error("Claude CLI not found. Install: npm install -g @anthropic-ai/claude-code");
186
186
  }
187
187
  const args = [];
188
+ // Plan mode uses Claude Code's `--permission-mode plan` (read-only,
189
+ // restrictive); implement runs use `auto`. Auto-acceptance of non-mutating
190
+ // tools while planning is governed by the user's `useAutoModeDuringPlan`
191
+ // setting in ~/.claude/settings.json, not by santree.
188
192
  args.push("--permission-mode", opts?.planMode ? "plan" : "auto");
189
193
  if (opts?.sessionId) {
190
194
  if (opts.resume) {
@@ -1,4 +1,4 @@
1
- import type { DashboardIssue } from "./types.js";
1
+ import type { DashboardIssue, DashboardTab } from "./types.js";
2
2
  interface Props {
3
3
  issue: DashboardIssue | null;
4
4
  scrollOffset: number;
@@ -18,6 +18,9 @@ export type IssueActionItem = {
18
18
  * `trackerName` is the active tracker's `displayName` ("Linear" / "GitHub"),
19
19
  * surfaced as the open-in-browser action label so the panel doesn't hardcode
20
20
  * a vendor name. */
21
- export declare function buildIssueActions(di: DashboardIssue, trackerName: string): IssueActionItem[];
21
+ export declare function buildIssueActions(di: DashboardIssue, trackerName: string, opts?: {
22
+ tab?: DashboardTab;
23
+ canMutate?: boolean;
24
+ }): IssueActionItem[];
22
25
  export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
23
26
  export {};
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
3
  function stateColor(type) {
4
4
  switch (type) {
@@ -57,9 +57,26 @@ function fileColor(xy) {
57
57
  * `trackerName` is the active tracker's `displayName` ("Linear" / "GitHub"),
58
58
  * surfaced as the open-in-browser action label so the panel doesn't hardcode
59
59
  * a vendor name. */
60
- export function buildIssueActions(di, trackerName) {
60
+ export function buildIssueActions(di, trackerName, opts) {
61
61
  const { worktree, pr, issue } = di;
62
62
  const items = [];
63
+ // Issues tab = backlog/planning. No worktree actions here (commit / PR /
64
+ // diff / fix live on the Trees tab). Offer Work (start → creates a
65
+ // worktree, moving the row to Trees) plus issue CRUD when the active
66
+ // tracker supports mutation (built-in Local only — feature-detected via
67
+ // `canMutate`, never a kind string check).
68
+ if (opts?.tab === "issues") {
69
+ items.push({ key: "w", label: "Work", color: "cyan" });
70
+ if (opts.canMutate) {
71
+ items.push({ key: "n", label: "New", color: "cyan" });
72
+ items.push({ key: "e", label: "Edit", color: "cyan" });
73
+ items.push({ key: "d", label: "Delete", color: "red" });
74
+ }
75
+ if (issue.url) {
76
+ items.push({ key: "o", label: trackerName, color: "gray" });
77
+ }
78
+ return items;
79
+ }
63
80
  // The synthetic "Main repo" row is special: no PR/Switch/Resume/Remove,
64
81
  // no work-launching (you're already on it). Only commit / diff /
65
82
  // editor — the actions that make sense for "I have changes in main and
@@ -127,7 +144,11 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
127
144
  const contentRows = height - 1;
128
145
  const startIdx = Math.max(0, logLines.length - contentRows);
129
146
  const visible = logLines.slice(startIdx, startIdx + contentRows);
130
- 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)))] }));
147
+ // Setup-script output is arbitrary-length; clamp each line to the pane
148
+ // width so long lines truncate instead of wrapping/overflowing into the
149
+ // left pane. Ink's default soft-wrap would push the box past `height`.
150
+ const clampLine = (s) => s.length > width ? s.slice(0, Math.max(0, width - 1)) + "…" : s;
151
+ return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { color: "yellow", bold: true, children: clampLine(`Setting up worktree for ${creatingForTicket}...`) }), visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: clampLine(line) }) }, i)))] }));
131
152
  }
132
153
  if (!issue) {
133
154
  return (_jsx(Box, { width: width, height: height, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "No issue selected" }) }));
@@ -10,6 +10,8 @@ interface Props {
10
10
  selectionBg?: string;
11
11
  }
12
12
  export type ListRow = {
13
+ kind: "spacer";
14
+ } | {
13
15
  kind: "header";
14
16
  name: string;
15
17
  count: number;
@@ -64,6 +64,11 @@ export function buildIssueListRows(groups, flatIssues) {
64
64
  }
65
65
  groups.forEach((group, gi) => {
66
66
  const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
67
+ // Blank line between projects. Modelled as a real row (not a render-only
68
+ // `marginTop`) so `rows[]` indices line up 1:1 with rendered rows —
69
+ // otherwise the dashboard's click→row mapping drifts by one per project.
70
+ if (gi > 0)
71
+ rows.push({ kind: "spacer" });
67
72
  rows.push({ kind: "header", name: group.name, count: totalIssues, isFirst: gi === 0 });
68
73
  for (const sg of group.statusGroups) {
69
74
  rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
@@ -86,6 +91,9 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
86
91
  const visible = rows.slice(scrollOffset, scrollOffset + height);
87
92
  const titleMaxWidth = Math.max(width - LEFT_FIXED - 1 /* leading space */ - RIGHT_FIXED - TITLE_GAP, 10);
88
93
  return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Box, { flexDirection: "column", height: height, children: visible.map((row, i) => {
94
+ if (row.kind === "spacer") {
95
+ return _jsx(Box, { height: 1 }, `sp-${i}`);
96
+ }
89
97
  if (row.kind === "header") {
90
98
  // On the first project header, also render the WT/CI column
91
99
  // labels right-aligned to the worktree/CI glyph columns —
@@ -97,7 +105,7 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
97
105
  const labelPad = row.isFirst
98
106
  ? Math.max(2, width - namePart.length - labelText.length)
99
107
  : 0;
100
- return (_jsxs(Box, { marginTop: i === 0 ? 0 : 1, children: [_jsx(Text, { bold: true, children: row.name }), _jsxs(Text, { dimColor: true, children: [" ", row.count] }), row.isFirst && _jsx(Text, { dimColor: true, children: `${" ".repeat(labelPad)}${labelText}` })] }, `h-${i}`));
108
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: row.name }), _jsxs(Text, { dimColor: true, children: [" ", row.count] }), row.isFirst && _jsx(Text, { dimColor: true, children: `${" ".repeat(labelPad)}${labelText}` })] }, `h-${i}`));
101
109
  }
102
110
  if (row.kind === "status-header") {
103
111
  return (_jsxs(Box, { children: [_jsxs(Text, { color: stateColor(row.type, row.name), children: [" ", row.name] }), _jsxs(Text, { dimColor: true, children: [" ", row.count] })] }, `sh-${i}`));
@@ -22,9 +22,10 @@ interface PrCreateOverlayProps {
22
22
  url: string | null;
23
23
  body: string | null;
24
24
  title: string | null;
25
+ draft: boolean;
25
26
  dispatch: React.Dispatch<DashboardAction>;
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
+ export declare function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, draft, dispatch, }: PrCreateOverlayProps): import("react/jsx-runtime").JSX.Element;
28
29
  interface HelpOverlayProps {
29
30
  width: number;
30
31
  height: number;
@@ -20,13 +20,27 @@ export function CommitOverlay({ width, height, branch, ticketId, gitStatus, phas
20
20
  return (_jsxs(Text, { color: color, children: [" ", line] }, i));
21
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" })] }))] }));
22
22
  }
23
- export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, dispatch, }) {
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 ?? "")
23
+ export function PrCreateOverlay({ width, height, branch, ticketId, phase, error, url, body, title, draft, dispatch, }) {
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: [_jsxs(Text, { bold: true, children: ["Create this PR?", " ", _jsxs(Text, { color: draft ? "yellow" : "green", children: ["(", draft ? "draft" : "ready for review", ")"] })] }), _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 ?? "")
25
25
  .split("\n")
26
26
  .slice(0, Math.max(4, height - 12))
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" })] }))] }));
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: "d" }), ` ${draft ? "mark ready" : "mark draft"} `, _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" })] }))] }));
28
28
  }
29
29
  const LEGEND = [
30
+ {
31
+ title: "Tabs & keys",
32
+ rows: [
33
+ { glyph: "1", color: "cyan", meaning: "Issues tab — backlog / planning" },
34
+ { glyph: "2", color: "cyan", meaning: "Trees tab — worktrees in progress" },
35
+ { glyph: "3", color: "cyan", meaning: "Reviews tab — PRs awaiting your review" },
36
+ { glyph: "Tab", color: "cyan", meaning: "Cycle Issues → Trees → Reviews" },
37
+ { glyph: "t", color: "cyan", meaning: "Switch / set up the issue tracker" },
38
+ { glyph: "n", color: "cyan", meaning: "New issue (built-in tracker, Issues tab)" },
39
+ { glyph: "e", color: "cyan", meaning: "Edit issue (built-in tracker, Issues tab)" },
40
+ { glyph: "d", color: "red", meaning: "Delete issue (built-in) / remove worktree (Trees)" },
41
+ { glyph: "w", color: "cyan", meaning: "Start work (creates a worktree → Trees tab)" },
42
+ ],
43
+ },
30
44
  {
31
45
  title: "Issue list",
32
46
  rows: [
@@ -2,6 +2,8 @@ import type { DashboardIssue, ProjectGroup, EnrichedReviewPR } from "./types.js"
2
2
  export declare function loadDashboardData(repoRoot: string): Promise<{
3
3
  groups: ProjectGroup[];
4
4
  flatIssues: DashboardIssue[];
5
+ treeGroups: ProjectGroup[];
6
+ flatTrees: DashboardIssue[];
5
7
  }>;
6
8
  export declare function loadReviewsData(repoRoot: string): Promise<{
7
9
  flatReviews: EnrichedReviewPR[];
@@ -203,16 +203,6 @@ export async function loadDashboardData(repoRoot) {
203
203
  parent.children.push(di);
204
204
  childTicketIds.add(ticketId);
205
205
  }
206
- // Group by project (excluding children — they'll appear nested under parents)
207
- const groupMap = new Map();
208
- for (const di of enriched) {
209
- if (childTicketIds.has(di.issue.identifier))
210
- continue;
211
- const key = di.issue.projectName ?? "No Project";
212
- const list = groupMap.get(key) ?? [];
213
- list.push(di);
214
- groupMap.set(key, list);
215
- }
216
206
  // Status type priority: started > unstarted > backlog > triage
217
207
  const statusTypePriority = {
218
208
  started: 0,
@@ -220,44 +210,35 @@ export async function loadDashboardData(repoRoot) {
220
210
  backlog: 2,
221
211
  triage: 3,
222
212
  };
223
- const groups = [...groupMap.entries()].map(([name, issues]) => {
224
- // Sub-group by status
225
- const statusMap = new Map();
213
+ // Group a set of (non-child) issues by project, then by status.
214
+ function buildProjectGroups(issues) {
215
+ const groupMap = new Map();
226
216
  for (const di of issues) {
227
- const statusName = di.issue.state.name;
228
- const existing = statusMap.get(statusName);
229
- if (existing) {
230
- existing.issues.push(di);
231
- }
232
- else {
233
- statusMap.set(statusName, {
234
- name: statusName,
235
- type: di.issue.state.type,
236
- issues: [di],
237
- });
238
- }
217
+ if (childTicketIds.has(di.issue.identifier))
218
+ continue;
219
+ const key = di.issue.projectName ?? "No Project";
220
+ const list = groupMap.get(key) ?? [];
221
+ list.push(di);
222
+ groupMap.set(key, list);
239
223
  }
240
- // Sort status groups by type priority
241
- const statusGroups = [...statusMap.values()].sort((a, b) => (statusTypePriority[a.type] ?? 99) - (statusTypePriority[b.type] ?? 99));
242
- return {
243
- name,
244
- id: issues[0]?.issue.projectId ?? null,
245
- statusGroups,
246
- };
247
- });
248
- // Append orphaned worktrees as a separate group at the bottom (excluding children)
249
- const topLevelOrphans = orphans.filter((di) => !childTicketIds.has(di.issue.identifier));
250
- if (topLevelOrphans.length > 0) {
251
- groups.push({
252
- name: "Orphaned Worktrees",
253
- id: null,
254
- statusGroups: [
255
- {
256
- name: "Orphaned",
257
- type: "orphaned",
258
- issues: topLevelOrphans,
259
- },
260
- ],
224
+ return [...groupMap.entries()].map(([name, list]) => {
225
+ const statusMap = new Map();
226
+ for (const di of list) {
227
+ const statusName = di.issue.state.name;
228
+ const existing = statusMap.get(statusName);
229
+ if (existing) {
230
+ existing.issues.push(di);
231
+ }
232
+ else {
233
+ statusMap.set(statusName, {
234
+ name: statusName,
235
+ type: di.issue.state.type,
236
+ issues: [di],
237
+ });
238
+ }
239
+ }
240
+ const statusGroups = [...statusMap.values()].sort((a, b) => (statusTypePriority[a.type] ?? 99) - (statusTypePriority[b.type] ?? 99));
241
+ return { name, id: list[0]?.issue.projectId ?? null, statusGroups };
261
242
  });
262
243
  }
263
244
  // Flatten with children inserted right after their parent
@@ -270,21 +251,42 @@ export async function loadDashboardData(repoRoot) {
270
251
  }
271
252
  return result;
272
253
  }
273
- const flatIssues = groups.flatMap((g) => g.statusGroups.flatMap((sg) => sg.issues.flatMap(flattenWithChildren)));
274
- // Synthesize a "Main repo" row at the very top so users can commit /
275
- // view diffs / inspect drift on whatever branch their main checkout
276
- // happens to be on. The row uses the same WorktreeInfo shape but with
277
- // state.type === "main" so the renderer can differentiate.
254
+ function flatten(g) {
255
+ return g.flatMap((grp) => grp.statusGroups.flatMap((sg) => sg.issues.flatMap(flattenWithChildren)));
256
+ }
257
+ // ── Partition: Issues tab (backlog/planning) vs Trees tab (work in
258
+ // progress). A tracker issue with no worktree is backlog; once it gains a
259
+ // worktree it moves to the Trees tab. Children always have a worktree, so
260
+ // they only ever appear nested in Trees. Main-repo + orphaned worktrees
261
+ // belong to Trees (they're active checkouts, not backlog).
262
+ const backlogIssues = enriched.filter((di) => !di.worktree);
263
+ const treeIssues = enriched.filter((di) => di.worktree);
264
+ const groups = buildProjectGroups(backlogIssues);
265
+ const flatIssues = flatten(groups);
266
+ const treeGroups = buildProjectGroups(treeIssues);
267
+ const topLevelOrphans = orphans.filter((di) => !childTicketIds.has(di.issue.identifier));
268
+ if (topLevelOrphans.length > 0) {
269
+ treeGroups.push({
270
+ name: "Orphaned Worktrees",
271
+ id: null,
272
+ statusGroups: [{ name: "Orphaned", type: "orphaned", issues: topLevelOrphans }],
273
+ });
274
+ }
275
+ const flatTrees = flatten(treeGroups);
276
+ // Synthesize a "Main repo" row at the top of the Trees tab so users can
277
+ // commit / view diffs / inspect drift on whatever branch their main
278
+ // checkout happens to be on. state.type === "main" lets the renderer
279
+ // differentiate.
278
280
  const mainEntry = await buildMainEntry(repoRoot);
279
281
  if (mainEntry) {
280
- groups.unshift({
282
+ treeGroups.unshift({
281
283
  name: "Main repo",
282
284
  id: null,
283
285
  statusGroups: [{ name: "Main", type: "main", issues: [mainEntry] }],
284
286
  });
285
- flatIssues.unshift(mainEntry);
287
+ flatTrees.unshift(mainEntry);
286
288
  }
287
- return { groups, flatIssues };
289
+ return { groups, flatIssues, treeGroups, flatTrees };
288
290
  }
289
291
  /** Build the synthetic dashboard row for the main repo checkout — the
290
292
  * non-worktree clone that the user typically commits master/main from.
@@ -72,8 +72,19 @@ export interface EnrichedReviewPR {
72
72
  * Null when the user hasn't set one or the lookup failed. */
73
73
  authorName: string | null;
74
74
  }
75
- export type DashboardTab = "issues" | "reviews";
76
- export type ActionOverlay = "mode-select" | "context-input" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | "diff" | "help" | null;
75
+ export type DashboardTab = "issues" | "trees" | "reviews";
76
+ export type ActionOverlay = "mode-select" | "context-input" | "base-select" | "confirm-delete" | "confirm-setup" | "commit" | "pr-create" | "diff" | "help" | "tracker-select" | "issue-form" | "confirm-delete-issue" | null;
77
+ /** Tracker-selection overlay sub-phase: pick a tracker, then (for Linear with
78
+ * multiple authenticated workspaces) pick the org. */
79
+ export type TrackerSelectPhase = "root" | "linear-org";
80
+ /** Issue create/edit form sub-phase. Title and description are captured in
81
+ * two sequential MultilineTextArea steps (reusing the context-input pattern);
82
+ * "saving" blocks input while the tracker writes the file. */
83
+ export type IssueFormPhase = "title" | "description" | "saving";
84
+ export interface TrackerOrgOption {
85
+ slug: string;
86
+ name: string;
87
+ }
77
88
  export type DiffFileStatus = "M" | "A" | "D" | "R" | "C" | "U" | "?";
78
89
  export interface DiffFile {
79
90
  path: string;
@@ -96,6 +107,21 @@ export interface DashboardState {
96
107
  reviewSelectedIndex: number;
97
108
  reviewListScrollOffset: number;
98
109
  reviewDetailScrollOffset: number;
110
+ treeGroups: ProjectGroup[];
111
+ flatTrees: DashboardIssue[];
112
+ treeSelectedIndex: number;
113
+ treeListScrollOffset: number;
114
+ treeDetailScrollOffset: number;
115
+ trackerSelectPhase: TrackerSelectPhase;
116
+ trackerSelectIndex: number;
117
+ trackerSelectOrgs: TrackerOrgOption[];
118
+ trackerSelectMessage: string | null;
119
+ issueFormMode: "create" | "edit" | null;
120
+ issueFormPhase: IssueFormPhase;
121
+ issueFormId: string | null;
122
+ issueFormTitle: string;
123
+ issueFormDescription: string;
124
+ issueFormError: string | null;
99
125
  loading: boolean;
100
126
  refreshing: boolean;
101
127
  error: string | null;
@@ -120,6 +146,7 @@ export interface DashboardState {
120
146
  prCreateUrl: string | null;
121
147
  prCreateBody: string | null;
122
148
  prCreateTitle: string | null;
149
+ prCreateDraft: boolean;
123
150
  setupMode: "plan" | "implement" | null;
124
151
  baseSelectOptions: string[];
125
152
  baseSelectIndex: number;
@@ -154,9 +181,58 @@ export type DashboardAction = {
154
181
  type: "SET_DATA";
155
182
  groups: ProjectGroup[];
156
183
  flatIssues: DashboardIssue[];
184
+ treeGroups: ProjectGroup[];
185
+ flatTrees: DashboardIssue[];
157
186
  } | {
158
187
  type: "SELECT";
159
188
  index: number;
189
+ } | {
190
+ type: "TREE_SELECT";
191
+ index: number;
192
+ } | {
193
+ type: "TREE_SCROLL_LIST";
194
+ offset: number;
195
+ } | {
196
+ type: "TREE_SCROLL_DETAIL";
197
+ offset: number;
198
+ } | {
199
+ type: "TRACKER_SELECT_OPEN";
200
+ } | {
201
+ type: "TRACKER_SELECT_MOVE";
202
+ index: number;
203
+ } | {
204
+ type: "TRACKER_SELECT_PHASE";
205
+ phase: TrackerSelectPhase;
206
+ orgs?: TrackerOrgOption[];
207
+ } | {
208
+ type: "TRACKER_SELECT_MESSAGE";
209
+ message: string | null;
210
+ } | {
211
+ type: "TRACKER_SELECT_CLOSE";
212
+ } | {
213
+ type: "ISSUE_FORM_OPEN";
214
+ mode: "create" | "edit";
215
+ id: string | null;
216
+ title: string;
217
+ description: string;
218
+ } | {
219
+ type: "ISSUE_FORM_PHASE";
220
+ phase: IssueFormPhase;
221
+ } | {
222
+ type: "ISSUE_FORM_TITLE";
223
+ title: string;
224
+ } | {
225
+ type: "ISSUE_FORM_DESC";
226
+ description: string;
227
+ } | {
228
+ type: "ISSUE_FORM_ERROR";
229
+ error: string;
230
+ } | {
231
+ type: "ISSUE_FORM_CLOSE";
232
+ } | {
233
+ type: "ISSUE_DELETE_OPEN";
234
+ } | {
235
+ type: "ISSUE_DELETE_CLOSE";
160
236
  } | {
161
237
  type: "SCROLL_LIST";
162
238
  offset: number;
@@ -237,6 +313,8 @@ export type DashboardAction = {
237
313
  type: "PR_CREATE_CONFIRM";
238
314
  } | {
239
315
  type: "PR_CREATE_EDIT";
316
+ } | {
317
+ type: "PR_CREATE_TOGGLE_DRAFT";
240
318
  } | {
241
319
  type: "PR_CREATE_DONE";
242
320
  url: string;