santree 0.6.3 → 0.7.2
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 +463 -78
- package/dist/lib/ai.d.ts +13 -0
- package/dist/lib/ai.js +16 -0
- package/dist/lib/dashboard/DetailPanel.d.ts +18 -2
- package/dist/lib/dashboard/DetailPanel.js +150 -3
- package/dist/lib/dashboard/IssueList.d.ts +11 -1
- package/dist/lib/dashboard/IssueList.js +44 -9
- package/dist/lib/dashboard/Overlays.d.ts +2 -1
- package/dist/lib/dashboard/Overlays.js +23 -8
- package/dist/lib/dashboard/TriageScheduleOverlay.d.ts +16 -0
- package/dist/lib/dashboard/TriageScheduleOverlay.js +78 -0
- package/dist/lib/dashboard/data.d.ts +2 -0
- package/dist/lib/dashboard/data.js +26 -4
- package/dist/lib/dashboard/due.d.ts +24 -0
- package/dist/lib/dashboard/due.js +32 -0
- package/dist/lib/dashboard/types.d.ts +93 -5
- package/dist/lib/dashboard/types.js +133 -4
- package/dist/lib/git.d.ts +1 -1
- package/dist/lib/git.js +7 -1
- package/dist/lib/trackers/linear/api.d.ts +2 -1
- package/dist/lib/trackers/linear/api.js +137 -1
- package/dist/lib/trackers/linear/index.js +17 -1
- package/dist/lib/trackers/types.d.ts +58 -0
- package/dist/lib/trackers/types.js +5 -1
- package/package.json +1 -1
- package/prompts/ask.njk +10 -0
package/dist/lib/ai.d.ts
CHANGED
|
@@ -88,6 +88,19 @@ export declare function runAgentAsync(prompt: string, opts?: {
|
|
|
88
88
|
* Clean up cached image downloads for an issue identifier on the active tracker.
|
|
89
89
|
*/
|
|
90
90
|
export declare function cleanupImages(ticketId: string): void;
|
|
91
|
+
/**
|
|
92
|
+
* Ask Claude a clarifying question about a triage issue. The full issue —
|
|
93
|
+
* description plus every comment — is rendered via the shared `ticket.njk`
|
|
94
|
+
* template and injected into `ask.njk`. Read-only codebase tools are granted
|
|
95
|
+
* (Read for downloaded issue images, Grep/Glob so Claude can judge whether the
|
|
96
|
+
* issue is fixable against the real code). Runs non-interactively and returns
|
|
97
|
+
* the captured answer text. Async so the Ink dashboard keeps animating.
|
|
98
|
+
*/
|
|
99
|
+
export declare function askTicketQuestion(opts: {
|
|
100
|
+
ticket: Issue;
|
|
101
|
+
trackerName: string;
|
|
102
|
+
question: string;
|
|
103
|
+
}): Promise<RunAgentResult>;
|
|
91
104
|
export interface FillCommitOpts {
|
|
92
105
|
branch: string;
|
|
93
106
|
ticketId: string | null;
|
package/dist/lib/ai.js
CHANGED
|
@@ -267,6 +267,22 @@ export function cleanupImages(ticketId) {
|
|
|
267
267
|
const repoRoot = findMainRepoRoot();
|
|
268
268
|
getIssueTracker(repoRoot).cleanupCache(ticketId);
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* Ask Claude a clarifying question about a triage issue. The full issue —
|
|
272
|
+
* description plus every comment — is rendered via the shared `ticket.njk`
|
|
273
|
+
* template and injected into `ask.njk`. Read-only codebase tools are granted
|
|
274
|
+
* (Read for downloaded issue images, Grep/Glob so Claude can judge whether the
|
|
275
|
+
* issue is fixable against the real code). Runs non-interactively and returns
|
|
276
|
+
* the captured answer text. Async so the Ink dashboard keeps animating.
|
|
277
|
+
*/
|
|
278
|
+
export async function askTicketQuestion(opts) {
|
|
279
|
+
const prompt = renderPrompt("ask", {
|
|
280
|
+
ticket_id: opts.ticket.identifier,
|
|
281
|
+
ticket_content: renderTicket(opts.ticket, opts.trackerName),
|
|
282
|
+
user_question: opts.question,
|
|
283
|
+
});
|
|
284
|
+
return runAgentAsync(prompt, { allowedTools: ["Read", "Grep", "Glob"] });
|
|
285
|
+
}
|
|
270
286
|
/**
|
|
271
287
|
* Generate a short imperative commit message from a staged diff.
|
|
272
288
|
* Async so callers (the Ink dashboard, the CLI commit flow) keep the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DashboardIssue, DashboardTab } from "./types.js";
|
|
1
|
+
import type { Comment, DashboardIssue, DashboardTab, DeleteStatus } from "./types.js";
|
|
2
2
|
interface Props {
|
|
3
3
|
issue: DashboardIssue | null;
|
|
4
4
|
scrollOffset: number;
|
|
@@ -6,6 +6,22 @@ interface Props {
|
|
|
6
6
|
width: number;
|
|
7
7
|
creatingForTicket: string | null;
|
|
8
8
|
creationLogs: string;
|
|
9
|
+
/** Deletion progress for the selected worktree, when one is being removed. */
|
|
10
|
+
deleteStatus?: DeleteStatus;
|
|
11
|
+
/** Triage mode: hide worktree/PR/checks sections (they never apply to an
|
|
12
|
+
* inbox issue) and show the discussion instead. */
|
|
13
|
+
triage?: boolean;
|
|
14
|
+
/** Comments for the selected triage issue. `undefined` = not yet loaded
|
|
15
|
+
* (shows "loading…"); an array (possibly empty) = loaded. Only consulted in
|
|
16
|
+
* triage mode. */
|
|
17
|
+
comments?: Comment[];
|
|
18
|
+
/** Compact triage on-call summary, shown as the first line in triage mode. */
|
|
19
|
+
onCall?: {
|
|
20
|
+
currentName: string | null;
|
|
21
|
+
currentIsMe: boolean;
|
|
22
|
+
/** Formatted start of the viewer's next shift (e.g. "Jun 5"), if any. */
|
|
23
|
+
myNext: string | null;
|
|
24
|
+
};
|
|
9
25
|
}
|
|
10
26
|
export type IssueActionItem = {
|
|
11
27
|
key: string;
|
|
@@ -22,5 +38,5 @@ export declare function buildIssueActions(di: DashboardIssue, trackerName: strin
|
|
|
22
38
|
tab?: DashboardTab;
|
|
23
39
|
canMutate?: boolean;
|
|
24
40
|
}): IssueActionItem[];
|
|
25
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
41
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, triage, comments, onCall, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
26
42
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { formatDueDate } from "./due.js";
|
|
4
|
+
import { issueReadiness } from "../trackers/types.js";
|
|
3
5
|
function stateColor(type) {
|
|
4
6
|
switch (type) {
|
|
5
7
|
case "started":
|
|
@@ -77,6 +79,18 @@ export function buildIssueActions(di, trackerName, opts) {
|
|
|
77
79
|
}
|
|
78
80
|
return items;
|
|
79
81
|
}
|
|
82
|
+
// Triage tab = the incoming inbox. Read the discussion, ask Claude a
|
|
83
|
+
// clarifying question, and — once it looks fixable — send it to a tree
|
|
84
|
+
// (`w`, same worktree-creation flow as the Issues tab).
|
|
85
|
+
if (opts?.tab === "triage") {
|
|
86
|
+
items.push({ key: "w", label: "Send to tree", color: "cyan" });
|
|
87
|
+
items.push({ key: "a", label: "Ask", color: "cyan" });
|
|
88
|
+
items.push({ key: "s", label: "Schedule", color: "cyan" });
|
|
89
|
+
if (issue.url) {
|
|
90
|
+
items.push({ key: "o", label: trackerName, color: "gray" });
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
80
94
|
// The synthetic "Main repo" row is special: no PR/Switch/Resume/Remove,
|
|
81
95
|
// no work-launching (you're already on it). Only commit / diff /
|
|
82
96
|
// editor — the actions that make sense for "I have changes in main and
|
|
@@ -137,14 +151,33 @@ function sectionHeader(icon, label, iconColor = "cyan") {
|
|
|
137
151
|
],
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
|
-
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }) {
|
|
154
|
+
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, deleteStatus, triage = false, comments, onCall, }) {
|
|
155
|
+
// Show deletion progress when the selected worktree is being removed.
|
|
156
|
+
if (issue && deleteStatus) {
|
|
157
|
+
const logLines = deleteStatus.logs.split("\n");
|
|
158
|
+
const contentRows = height - 1;
|
|
159
|
+
const startIdx = Math.max(0, logLines.length - contentRows);
|
|
160
|
+
const visible = logLines.slice(startIdx, startIdx + contentRows);
|
|
161
|
+
const clampLine = (s) => s.length > width ? s.slice(0, Math.max(0, width - 1)) + "…" : s;
|
|
162
|
+
const headerColor = deleteStatus.phase === "error" ? "red" : "yellow";
|
|
163
|
+
const header = deleteStatus.phase === "error"
|
|
164
|
+
? `Failed to remove ${issue.issue.identifier}`
|
|
165
|
+
: deleteStatus.phase === "done"
|
|
166
|
+
? `Removed ${issue.issue.identifier}`
|
|
167
|
+
: `Removing worktree for ${issue.issue.identifier}…`;
|
|
168
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(Text, { color: deleteStatus.phase === "done" ? "green" : headerColor, bold: true, children: clampLine(header) }), visible.map((line, i) => (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: clampLine(line) }) }, i))), deleteStatus.phase === "error" && deleteStatus.error ? (_jsx(Text, { color: "red", children: clampLine(deleteStatus.error) })) : null] }));
|
|
169
|
+
}
|
|
141
170
|
// Show creation logs when selected issue is being created
|
|
142
171
|
if (issue && issue.issue.identifier === creatingForTicket) {
|
|
143
172
|
const logLines = creationLogs.split("\n");
|
|
144
173
|
const contentRows = height - 1;
|
|
145
174
|
const startIdx = Math.max(0, logLines.length - contentRows);
|
|
146
175
|
const visible = logLines.slice(startIdx, startIdx + contentRows);
|
|
147
|
-
|
|
176
|
+
// Setup-script output is arbitrary-length; clamp each line to the pane
|
|
177
|
+
// width so long lines truncate instead of wrapping/overflowing into the
|
|
178
|
+
// left pane. Ink's default soft-wrap would push the box past `height`.
|
|
179
|
+
const clampLine = (s) => s.length > width ? s.slice(0, Math.max(0, width - 1)) + "…" : s;
|
|
180
|
+
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)))] }));
|
|
148
181
|
}
|
|
149
182
|
if (!issue) {
|
|
150
183
|
return (_jsx(Box, { width: width, height: height, justifyContent: "center", alignItems: "center", children: _jsx(Text, { dimColor: true, children: "No issue selected" }) }));
|
|
@@ -153,6 +186,27 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
153
186
|
const lines = [];
|
|
154
187
|
const rule = "─".repeat(width);
|
|
155
188
|
const ruleLine = { text: rule, dim: true };
|
|
189
|
+
// ── Triage on-call (compact) ───────────────────────────────────────
|
|
190
|
+
// First line in triage mode: who's on call now + when the viewer is next up.
|
|
191
|
+
// The full rotation lives behind the [s] schedule overlay.
|
|
192
|
+
if (triage && onCall && onCall.currentName) {
|
|
193
|
+
const segs = [
|
|
194
|
+
{ text: "◷ ", color: onCall.currentIsMe ? "cyan" : "green", bold: true },
|
|
195
|
+
{ text: "on call: ", dim: true },
|
|
196
|
+
{
|
|
197
|
+
text: onCall.currentName,
|
|
198
|
+
color: onCall.currentIsMe ? "cyan" : "green",
|
|
199
|
+
bold: true,
|
|
200
|
+
},
|
|
201
|
+
];
|
|
202
|
+
if (onCall.myNext) {
|
|
203
|
+
segs.push({ text: " · you're up ", dim: true });
|
|
204
|
+
segs.push({ text: onCall.myNext, color: "cyan" });
|
|
205
|
+
}
|
|
206
|
+
segs.push({ text: " · [s] schedule", dim: true });
|
|
207
|
+
lines.push({ text: "", segments: segs });
|
|
208
|
+
lines.push(ruleLine);
|
|
209
|
+
}
|
|
156
210
|
// ── Hero: identifier + title, then a status pill row ───────────────
|
|
157
211
|
lines.push({ text: `${li.identifier} ${li.title}`, bold: true });
|
|
158
212
|
const sc = stateColor(li.state.type);
|
|
@@ -167,6 +221,19 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
167
221
|
heroSegs.push({ text: li.labels.join(", "), dim: true });
|
|
168
222
|
}
|
|
169
223
|
lines.push({ text: "", segments: heroSegs });
|
|
224
|
+
// ── Due date ──────────────────────────────────────────────────────
|
|
225
|
+
// Urgency-coded; shown whenever the issue carries one (most relevant on the
|
|
226
|
+
// Triage tab, harmless elsewhere).
|
|
227
|
+
const due = formatDueDate(li.dueDate);
|
|
228
|
+
if (due) {
|
|
229
|
+
lines.push({
|
|
230
|
+
text: "",
|
|
231
|
+
segments: [
|
|
232
|
+
{ text: "◷ ", color: due.color, bold: due.urgent },
|
|
233
|
+
{ text: due.label, color: due.color, bold: due.urgent },
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
}
|
|
170
237
|
// ── Description ───────────────────────────────────────────────────
|
|
171
238
|
if (li.description) {
|
|
172
239
|
lines.push({ text: "" });
|
|
@@ -174,6 +241,81 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
174
241
|
lines.push({ text: dLine });
|
|
175
242
|
}
|
|
176
243
|
}
|
|
244
|
+
// ── Dependencies ──────────────────────────────────────────────────
|
|
245
|
+
// Blocking relations from the tracker. Header carries a readiness badge so
|
|
246
|
+
// the user can tell at a glance whether the issue is startable.
|
|
247
|
+
const blockedBy = li.blockedBy ?? [];
|
|
248
|
+
const blocking = li.blocking ?? [];
|
|
249
|
+
if (!triage && (blockedBy.length > 0 || blocking.length > 0)) {
|
|
250
|
+
const readiness = issueReadiness(li.blockedBy);
|
|
251
|
+
lines.push(ruleLine);
|
|
252
|
+
const headerSegs = [
|
|
253
|
+
{ text: "⇄ ", color: "cyan", bold: true },
|
|
254
|
+
{ text: "Dependencies", bold: true },
|
|
255
|
+
{ text: " " },
|
|
256
|
+
readiness === "ready"
|
|
257
|
+
? { text: "✓ ready to start", color: "green", bold: true }
|
|
258
|
+
: { text: "⊘ blocked", color: "yellow", bold: true },
|
|
259
|
+
];
|
|
260
|
+
lines.push({ text: "", segments: headerSegs });
|
|
261
|
+
if (blockedBy.length > 0) {
|
|
262
|
+
lines.push({ text: " blocked by", dim: true });
|
|
263
|
+
for (const b of blockedBy) {
|
|
264
|
+
lines.push({
|
|
265
|
+
text: "",
|
|
266
|
+
segments: [
|
|
267
|
+
{ text: ` ${b.done ? "✓" : "○"} `, color: b.done ? "green" : "yellow" },
|
|
268
|
+
{ text: b.identifier, color: b.done ? undefined : "yellow", dim: b.done },
|
|
269
|
+
...(b.done ? [{ text: " done", dim: true }] : []),
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (blocking.length > 0) {
|
|
275
|
+
lines.push({ text: " blocks", dim: true });
|
|
276
|
+
lines.push({
|
|
277
|
+
text: "",
|
|
278
|
+
segments: [{ text: " " }, { text: blocking.map((b) => b.identifier).join(", ") }],
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ── Triage: discussion only ───────────────────────────────────────
|
|
283
|
+
// Triage issues live in the inbox — no worktree/PR/checks apply yet. Show
|
|
284
|
+
// the comment thread (lazily fetched by the dashboard) so the user can read
|
|
285
|
+
// the back-and-forth before deciding to ask a question or send it to a tree.
|
|
286
|
+
if (triage) {
|
|
287
|
+
lines.push(ruleLine);
|
|
288
|
+
lines.push(sectionHeader("≡", "Comments", "cyan"));
|
|
289
|
+
if (comments === undefined) {
|
|
290
|
+
lines.push({ text: " loading…", dim: true });
|
|
291
|
+
}
|
|
292
|
+
else if (comments.length === 0) {
|
|
293
|
+
lines.push({ text: " no comments", dim: true });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const renderComment = (c, depth) => {
|
|
297
|
+
const indent = " ".repeat(depth + 1);
|
|
298
|
+
lines.push({
|
|
299
|
+
text: "",
|
|
300
|
+
segments: [
|
|
301
|
+
{ text: indent },
|
|
302
|
+
{ text: c.author, color: "cyan", bold: true },
|
|
303
|
+
{ text: ` ${new Date(c.createdAt).toLocaleDateString()}`, dim: true },
|
|
304
|
+
],
|
|
305
|
+
});
|
|
306
|
+
for (const bodyLine of c.body.trimEnd().split("\n")) {
|
|
307
|
+
lines.push({ text: `${indent}${bodyLine}` });
|
|
308
|
+
}
|
|
309
|
+
lines.push({ text: "" });
|
|
310
|
+
for (const child of c.children)
|
|
311
|
+
renderComment(child, depth + 1);
|
|
312
|
+
};
|
|
313
|
+
for (const c of comments)
|
|
314
|
+
renderComment(c, 0);
|
|
315
|
+
}
|
|
316
|
+
// Skip the worktree/PR/checks/reviews sections entirely for triage.
|
|
317
|
+
return renderLines(lines, scrollOffset, height, width);
|
|
318
|
+
}
|
|
177
319
|
// ── Worktree ──────────────────────────────────────────────────────
|
|
178
320
|
lines.push(ruleLine);
|
|
179
321
|
if (worktree) {
|
|
@@ -464,6 +606,11 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
464
606
|
// Action footer is rendered by the dashboard one row outside the panel,
|
|
465
607
|
// alongside the global command bar, so left- and right-pane key hints sit
|
|
466
608
|
// on the same row. The panel itself uses its full height for content.
|
|
609
|
+
return renderLines(lines, scrollOffset, height, width);
|
|
610
|
+
}
|
|
611
|
+
/** Scroll-clamp a built line list and render it into the panel box. Shared by
|
|
612
|
+
* the full detail view and the triage (discussion-only) view. */
|
|
613
|
+
function renderLines(lines, scrollOffset, height, width) {
|
|
467
614
|
const totalLines = lines.length;
|
|
468
615
|
const canScroll = totalLines > height;
|
|
469
616
|
const contentRows = canScroll ? height - 2 : height;
|
|
@@ -8,8 +8,18 @@ interface Props {
|
|
|
8
8
|
width: number;
|
|
9
9
|
/** Theme-adapted selection background (light/dark). Falls back to dark navy. */
|
|
10
10
|
selectionBg?: string;
|
|
11
|
+
/** Right-column variant (row structure — and click→row mapping — is identical):
|
|
12
|
+
* "default" — WT + CI status columns (Trees tab)
|
|
13
|
+
* "triage" — a colored due-date badge
|
|
14
|
+
* "issues" — a readiness glyph (ready / blocked by dependencies) */
|
|
15
|
+
variant?: "default" | "triage" | "issues";
|
|
16
|
+
/** Ticket ids whose worktree is currently being removed — shown with a
|
|
17
|
+
* distinct WT-column glyph so concurrent deletions are visible in the list. */
|
|
18
|
+
deletingIds?: Set<string>;
|
|
11
19
|
}
|
|
12
20
|
export type ListRow = {
|
|
21
|
+
kind: "spacer";
|
|
22
|
+
} | {
|
|
13
23
|
kind: "header";
|
|
14
24
|
name: string;
|
|
15
25
|
count: number;
|
|
@@ -26,5 +36,5 @@ export type ListRow = {
|
|
|
26
36
|
depth: number;
|
|
27
37
|
};
|
|
28
38
|
export declare function buildIssueListRows(groups: ProjectGroup[], flatIssues: DashboardIssue[]): ListRow[];
|
|
29
|
-
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg, variant, deletingIds, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
30
40
|
export {};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
+
import { formatDueDate } from "./due.js";
|
|
4
|
+
import { issueReadiness } from "../trackers/types.js";
|
|
3
5
|
function stateColor(type, name) {
|
|
4
6
|
const n = name?.toLowerCase();
|
|
5
7
|
if (n === "blocked")
|
|
@@ -64,6 +66,11 @@ export function buildIssueListRows(groups, flatIssues) {
|
|
|
64
66
|
}
|
|
65
67
|
groups.forEach((group, gi) => {
|
|
66
68
|
const totalIssues = group.statusGroups.reduce((sum, sg) => sum + sg.issues.length, 0);
|
|
69
|
+
// Blank line between projects. Modelled as a real row (not a render-only
|
|
70
|
+
// `marginTop`) so `rows[]` indices line up 1:1 with rendered rows —
|
|
71
|
+
// otherwise the dashboard's click→row mapping drifts by one per project.
|
|
72
|
+
if (gi > 0)
|
|
73
|
+
rows.push({ kind: "spacer" });
|
|
67
74
|
rows.push({ kind: "header", name: group.name, count: totalIssues, isFirst: gi === 0 });
|
|
68
75
|
for (const sg of group.statusGroups) {
|
|
69
76
|
rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
|
|
@@ -80,12 +87,33 @@ export function buildIssueListRows(groups, flatIssues) {
|
|
|
80
87
|
// Glyphs are 1 char and rendered right-aligned within their column.
|
|
81
88
|
const LEFT_FIXED = 1 + 1 + 1 + 2 + 11; // 16 — left-aligned columns
|
|
82
89
|
const RIGHT_FIXED = 2 + 2 + 2; // 6 — WT + 2 spaces + CI
|
|
90
|
+
// Triage variant: a single right-aligned due-date column. Widest badge is
|
|
91
|
+
// "⚠ overdue 99d" (13 chars); pad/clamp everything to this so titles align.
|
|
92
|
+
const DUE_COL_WIDTH = 13;
|
|
93
|
+
// Issues variant: a single readiness glyph under a "RDY" header.
|
|
94
|
+
const READY_COL_WIDTH = 3;
|
|
83
95
|
const TITLE_GAP = 2; // minimum spacing between title and the right columns
|
|
84
|
-
|
|
96
|
+
function readinessGlyph(di) {
|
|
97
|
+
switch (issueReadiness(di.issue.blockedBy)) {
|
|
98
|
+
case "ready":
|
|
99
|
+
return { glyph: "✓", color: "green" };
|
|
100
|
+
case "blocked":
|
|
101
|
+
return { glyph: "⊘", color: "yellow" };
|
|
102
|
+
default:
|
|
103
|
+
return { glyph: "·", color: "gray" };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", variant = "default", deletingIds, }) {
|
|
107
|
+
const isTriage = variant === "triage";
|
|
108
|
+
const isIssues = variant === "issues";
|
|
109
|
+
const rightFixed = isTriage ? DUE_COL_WIDTH : isIssues ? READY_COL_WIDTH : RIGHT_FIXED;
|
|
85
110
|
const rows = buildIssueListRows(groups, flatIssues);
|
|
86
111
|
const visible = rows.slice(scrollOffset, scrollOffset + height);
|
|
87
|
-
const titleMaxWidth = Math.max(width - LEFT_FIXED - 1 /* leading space */ -
|
|
112
|
+
const titleMaxWidth = Math.max(width - LEFT_FIXED - 1 /* leading space */ - rightFixed - TITLE_GAP, 10);
|
|
88
113
|
return (_jsx(Box, { flexDirection: "column", width: width, height: height, children: _jsx(Box, { flexDirection: "column", height: height, children: visible.map((row, i) => {
|
|
114
|
+
if (row.kind === "spacer") {
|
|
115
|
+
return _jsx(Box, { height: 1 }, `sp-${i}`);
|
|
116
|
+
}
|
|
89
117
|
if (row.kind === "header") {
|
|
90
118
|
// On the first project header, also render the WT/CI column
|
|
91
119
|
// labels right-aligned to the worktree/CI glyph columns —
|
|
@@ -93,11 +121,11 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
93
121
|
// Label "WT CI" is 5 chars; the "W" lines up with the WT
|
|
94
122
|
// glyph at column (width - RIGHT_FIXED + 1).
|
|
95
123
|
const namePart = `${row.name} ${row.count}`;
|
|
96
|
-
const labelText = "WT CI";
|
|
124
|
+
const labelText = isTriage ? "DUE" : isIssues ? "RDY" : "WT CI";
|
|
97
125
|
const labelPad = row.isFirst
|
|
98
126
|
? Math.max(2, width - namePart.length - labelText.length)
|
|
99
127
|
: 0;
|
|
100
|
-
return (_jsxs(Box, {
|
|
128
|
+
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
129
|
}
|
|
102
130
|
if (row.kind === "status-header") {
|
|
103
131
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: stateColor(row.type, row.name), children: [" ", row.name] }), _jsxs(Text, { dimColor: true, children: [" ", row.count] })] }, `sh-${i}`));
|
|
@@ -107,7 +135,8 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
107
135
|
const di = issue;
|
|
108
136
|
const sc = stateColor(di.issue.state.type, di.issue.state.name);
|
|
109
137
|
const prio = priorityMarker(di.issue.priority);
|
|
110
|
-
const
|
|
138
|
+
const isDeleting = deletingIds?.has(di.issue.identifier) ?? false;
|
|
139
|
+
const work = isDeleting ? { glyph: "⌫", color: "yellow" } : workIndicator(di.worktree);
|
|
111
140
|
const ci = ciIndicator(di.checks);
|
|
112
141
|
const nestPrefix = depth > 0 ? " ".repeat(depth - 1) + "└ " : "";
|
|
113
142
|
const adjustedTitleWidth = Math.max(titleMaxWidth - nestPrefix.length, 5);
|
|
@@ -115,9 +144,15 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
115
144
|
? di.issue.title.slice(0, adjustedTitleWidth - 1) + "…"
|
|
116
145
|
: di.issue.title;
|
|
117
146
|
const bg = selected ? selectionBg : undefined;
|
|
118
|
-
// Pad between title and the right columns so the
|
|
147
|
+
// Pad between title and the right columns so the markers stay
|
|
119
148
|
// pinned to the right edge regardless of title length.
|
|
120
|
-
const trailingPad = Math.max(0, width - LEFT_FIXED - 1 - nestPrefix.length - title.length -
|
|
121
|
-
|
|
149
|
+
const trailingPad = Math.max(0, width - LEFT_FIXED - 1 - nestPrefix.length - title.length - rightFixed);
|
|
150
|
+
// Triage variant: a single right-aligned due-date badge in place
|
|
151
|
+
// of the WT/CI columns.
|
|
152
|
+
const due = isTriage ? formatDueDate(di.issue.dueDate) : null;
|
|
153
|
+
const dueText = (due?.label ?? "").padStart(DUE_COL_WIDTH).slice(-DUE_COL_WIDTH);
|
|
154
|
+
// Issues variant: a single readiness glyph right-aligned under "RDY".
|
|
155
|
+
const ready = isIssues ? readinessGlyph(di) : null;
|
|
156
|
+
return (_jsxs(Box, { width: width, children: [_jsx(Text, { backgroundColor: bg, color: prio.color, children: prio.glyph }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: sc, children: "\u25CF" }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsxs(Text, { backgroundColor: bg, dimColor: true, children: [nestPrefix, di.issue.identifier.padEnd(10)] }), _jsxs(Text, { backgroundColor: bg, bold: selected, children: [" ", title] }), _jsx(Text, { backgroundColor: bg, children: " ".repeat(trailingPad) }), isTriage ? (_jsx(Text, { backgroundColor: bg, color: due?.color, bold: due?.urgent, children: dueText })) : isIssues ? (_jsx(Text, { backgroundColor: bg, color: ready?.color, children: ` ${ready?.glyph ?? " "}` })) : (_jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: work.color, children: work.glyph }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, children: " " }), _jsx(Text, { backgroundColor: bg, color: ci.color, children: ci.glyph })] }))] }, di.issue.identifier));
|
|
122
157
|
}) }) }));
|
|
123
158
|
}
|
|
@@ -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,25 +20,32 @@ 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: [
|
|
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
30
|
{
|
|
31
31
|
title: "Tabs & keys",
|
|
32
32
|
rows: [
|
|
33
|
-
{ glyph: "1", color: "cyan", meaning: "
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
{ glyph: "1–4", color: "cyan", meaning: "Switch tab by number (Tab cycles)" },
|
|
34
|
+
{
|
|
35
|
+
glyph: "Triage",
|
|
36
|
+
color: "cyan",
|
|
37
|
+
meaning: "Incoming inbox — due dates + comments (Linear only)",
|
|
38
|
+
},
|
|
39
|
+
{ glyph: "Issues", color: "cyan", meaning: "Backlog / planning" },
|
|
40
|
+
{ glyph: "Trees", color: "cyan", meaning: "Worktrees in progress" },
|
|
41
|
+
{ glyph: "Reviews", color: "cyan", meaning: "PRs awaiting your review" },
|
|
37
42
|
{ glyph: "t", color: "cyan", meaning: "Switch / set up the issue tracker" },
|
|
38
43
|
{ glyph: "n", color: "cyan", meaning: "New issue (built-in tracker, Issues tab)" },
|
|
39
44
|
{ glyph: "e", color: "cyan", meaning: "Edit issue (built-in tracker, Issues tab)" },
|
|
40
45
|
{ glyph: "d", color: "red", meaning: "Delete issue (built-in) / remove worktree (Trees)" },
|
|
41
|
-
{ glyph: "w", color: "cyan", meaning: "Start work (creates a worktree
|
|
46
|
+
{ glyph: "w", color: "cyan", meaning: "Start work / send to tree (creates a worktree)" },
|
|
47
|
+
{ glyph: "a", color: "cyan", meaning: "Ask Claude about the issue + comments (Triage tab)" },
|
|
48
|
+
{ glyph: "s", color: "cyan", meaning: "Triage on-call schedule (Triage tab)" },
|
|
42
49
|
],
|
|
43
50
|
},
|
|
44
51
|
{
|
|
@@ -56,6 +63,14 @@ const LEGEND = [
|
|
|
56
63
|
{ glyph: "✗", color: "red", meaning: "CI column: a check is failing" },
|
|
57
64
|
{ glyph: "●", color: "yellow", meaning: "CI column: checks pending / running" },
|
|
58
65
|
{ glyph: "·", color: "gray", meaning: "CI column: no PR or no checks" },
|
|
66
|
+
{ glyph: "◷", color: "red", meaning: "DUE column (Triage): overdue / due today" },
|
|
67
|
+
{ glyph: "◷", color: "yellow", meaning: "DUE column (Triage): due within 2 days" },
|
|
68
|
+
{ glyph: "✓", color: "green", meaning: "RDY column (Issues): ready — no open blockers" },
|
|
69
|
+
{
|
|
70
|
+
glyph: "⊘",
|
|
71
|
+
color: "yellow",
|
|
72
|
+
meaning: "RDY column (Issues): blocked by an open dependency",
|
|
73
|
+
},
|
|
59
74
|
],
|
|
60
75
|
},
|
|
61
76
|
{
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TriageSchedule } from "./types.js";
|
|
2
|
+
type Seg = {
|
|
3
|
+
text: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
bold?: boolean;
|
|
6
|
+
dim?: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Flatten the schedules into colored segment-lines. Exported for testing. */
|
|
9
|
+
export declare function buildScheduleLines(schedules: TriageSchedule[]): Seg[][];
|
|
10
|
+
export default function TriageScheduleOverlay({ schedules, scrollOffset, width, height, }: {
|
|
11
|
+
schedules: TriageSchedule[];
|
|
12
|
+
scrollOffset: number;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
function fmt(iso) {
|
|
4
|
+
const d = new Date(iso);
|
|
5
|
+
if (Number.isNaN(d.getTime()))
|
|
6
|
+
return iso;
|
|
7
|
+
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
8
|
+
}
|
|
9
|
+
/** Flatten the schedules into colored segment-lines. Exported for testing. */
|
|
10
|
+
export function buildScheduleLines(schedules) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
if (schedules.length === 0) {
|
|
13
|
+
lines.push([{ text: "No triage on-call schedule found for your teams.", dim: true }]);
|
|
14
|
+
return lines;
|
|
15
|
+
}
|
|
16
|
+
schedules.forEach((sch, si) => {
|
|
17
|
+
if (si > 0)
|
|
18
|
+
lines.push([{ text: "" }]);
|
|
19
|
+
lines.push([
|
|
20
|
+
{ text: "≡ ", color: "cyan", bold: true },
|
|
21
|
+
{ text: sch.scheduleName, bold: true },
|
|
22
|
+
{ text: ` ${sch.teamName}`, dim: true },
|
|
23
|
+
]);
|
|
24
|
+
if (sch.currentName) {
|
|
25
|
+
lines.push([
|
|
26
|
+
{ text: " on call now: ", dim: true },
|
|
27
|
+
{ text: sch.currentName, color: sch.currentIsMe ? "cyan" : "green", bold: true },
|
|
28
|
+
...(sch.currentIsMe ? [{ text: " ← you", color: "cyan" }] : []),
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
for (const sh of sch.shifts) {
|
|
32
|
+
const range = `${fmt(sh.startsAt)} – ${fmt(sh.endsAt)}`.padEnd(18);
|
|
33
|
+
const segs = [
|
|
34
|
+
{ text: ` ${sh.isCurrent ? "●" : "·"} `, color: sh.isCurrent ? "green" : "gray" },
|
|
35
|
+
{ text: range, dim: !sh.isCurrent && !sh.isMe },
|
|
36
|
+
{
|
|
37
|
+
text: sh.name,
|
|
38
|
+
color: sh.isCurrent ? "green" : sh.isMe ? "cyan" : undefined,
|
|
39
|
+
bold: sh.isCurrent || sh.isMe,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
if (sh.isCurrent)
|
|
43
|
+
segs.push({ text: " now", color: "green", bold: true });
|
|
44
|
+
else if (sh.isMe)
|
|
45
|
+
segs.push({ text: " you", color: "cyan" });
|
|
46
|
+
lines.push(segs);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return lines;
|
|
50
|
+
}
|
|
51
|
+
export default function TriageScheduleOverlay({ schedules, scrollOffset, width, height, }) {
|
|
52
|
+
const all = buildScheduleLines(schedules);
|
|
53
|
+
// Title (1) + blank (1) leave the rest for the body. The footer/close hint is
|
|
54
|
+
// owned by the dashboard's global command-bar row, so the panel stays pure
|
|
55
|
+
// content and lines up with the left pane.
|
|
56
|
+
const bodyHeight = Math.max(1, height - 2);
|
|
57
|
+
const maxOffset = Math.max(0, all.length - bodyHeight);
|
|
58
|
+
const off = Math.min(scrollOffset, maxOffset);
|
|
59
|
+
const visible = all.slice(off, off + bodyHeight);
|
|
60
|
+
const clampSegs = (segs) => {
|
|
61
|
+
let remaining = width - 1;
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const s of segs) {
|
|
64
|
+
if (remaining <= 0)
|
|
65
|
+
break;
|
|
66
|
+
if (s.text.length <= remaining) {
|
|
67
|
+
out.push(s);
|
|
68
|
+
remaining -= s.text.length;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
out.push({ ...s, text: s.text.slice(0, Math.max(0, remaining - 1)) + "…" });
|
|
72
|
+
remaining = 0;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
};
|
|
77
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Triage on-call schedule" }), _jsx(Text, { children: " " }), visible.map((segs, i) => (_jsx(Box, { children: _jsx(Text, { children: clampSegs(segs).map((s, j) => (_jsx(Text, { color: s.color, bold: s.bold, dimColor: s.dim, children: s.text || " " }, j))) }) }, i))), off + bodyHeight < all.length ? _jsx(Text, { dimColor: true, children: "\u2193 more (press s to scroll)" }) : null] }));
|
|
78
|
+
}
|
|
@@ -4,6 +4,8 @@ export declare function loadDashboardData(repoRoot: string): Promise<{
|
|
|
4
4
|
flatIssues: DashboardIssue[];
|
|
5
5
|
treeGroups: ProjectGroup[];
|
|
6
6
|
flatTrees: DashboardIssue[];
|
|
7
|
+
triageGroups: ProjectGroup[];
|
|
8
|
+
flatTriage: DashboardIssue[];
|
|
7
9
|
}>;
|
|
8
10
|
export declare function loadReviewsData(repoRoot: string): Promise<{
|
|
9
11
|
flatReviews: EnrichedReviewPR[];
|