santree 0.7.2 → 0.7.3
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/lib/dashboard/DetailPanel.js +22 -14
- package/dist/lib/dashboard/IssueList.d.ts +1 -1
- package/dist/lib/dashboard/IssueList.js +24 -14
- package/dist/lib/dashboard/Overlays.js +4 -3
- package/dist/lib/dashboard/data.js +27 -14
- package/dist/lib/dashboard/sla.d.ts +27 -0
- package/dist/lib/dashboard/sla.js +35 -0
- package/dist/lib/trackers/linear/api.js +6 -3
- package/dist/lib/trackers/types.d.ts +10 -5
- package/package.json +1 -1
- package/dist/lib/dashboard/due.d.ts +0 -24
- package/dist/lib/dashboard/due.js +0 -32
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import {
|
|
3
|
+
import { formatSla, isSnoozed } from "./sla.js";
|
|
4
4
|
import { issueReadiness } from "../trackers/types.js";
|
|
5
5
|
function stateColor(type) {
|
|
6
6
|
switch (type) {
|
|
@@ -210,30 +210,38 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
210
210
|
// ── Hero: identifier + title, then a status pill row ───────────────
|
|
211
211
|
lines.push({ text: `${li.identifier} ${li.title}`, bold: true });
|
|
212
212
|
const sc = stateColor(li.state.type);
|
|
213
|
-
const heroSegs = [
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
{ text: li.
|
|
218
|
-
|
|
213
|
+
const heroSegs = [];
|
|
214
|
+
// On the Triage tab every issue is in the "Triage" state by definition, so the
|
|
215
|
+
// status pill is noise — lead with priority instead.
|
|
216
|
+
if (!triage) {
|
|
217
|
+
heroSegs.push({ text: "● ", color: sc }, { text: li.state.name, color: sc }, { text: " · ", dim: true });
|
|
218
|
+
}
|
|
219
|
+
heroSegs.push({ text: li.priorityLabel });
|
|
219
220
|
if (li.labels.length > 0) {
|
|
220
221
|
heroSegs.push({ text: " · ", dim: true });
|
|
221
222
|
heroSegs.push({ text: li.labels.join(", "), dim: true });
|
|
222
223
|
}
|
|
223
224
|
lines.push({ text: "", segments: heroSegs });
|
|
224
|
-
// ──
|
|
225
|
-
// Urgency-coded; shown whenever the issue carries
|
|
226
|
-
// Triage tab,
|
|
227
|
-
const
|
|
228
|
-
|
|
225
|
+
// ── SLA countdown ─────────────────────────────────────────────────
|
|
226
|
+
// Urgency-coded time-to-breach; shown whenever the issue carries an SLA
|
|
227
|
+
// (Triage tab). Snoozed issues are parked, so render greyed with a marker.
|
|
228
|
+
const sla = formatSla(li.slaBreachesAt);
|
|
229
|
+
const snoozed = isSnoozed(li.snoozedUntilAt);
|
|
230
|
+
if (sla) {
|
|
231
|
+
const col = snoozed ? "gray" : sla.color;
|
|
232
|
+
const bold = !snoozed && sla.urgent;
|
|
229
233
|
lines.push({
|
|
230
234
|
text: "",
|
|
231
235
|
segments: [
|
|
232
|
-
{ text: "◷ ", color:
|
|
233
|
-
{ text:
|
|
236
|
+
{ text: "◷ ", color: col, bold },
|
|
237
|
+
{ text: sla.label, color: col, bold },
|
|
238
|
+
...(snoozed ? [{ text: " · snoozed", color: "gray" }] : []),
|
|
234
239
|
],
|
|
235
240
|
});
|
|
236
241
|
}
|
|
242
|
+
else if (snoozed) {
|
|
243
|
+
lines.push({ text: "", segments: [{ text: "◷ snoozed", color: "gray" }] });
|
|
244
|
+
}
|
|
237
245
|
// ── Description ───────────────────────────────────────────────────
|
|
238
246
|
if (li.description) {
|
|
239
247
|
lines.push({ text: "" });
|
|
@@ -10,7 +10,7 @@ interface Props {
|
|
|
10
10
|
selectionBg?: string;
|
|
11
11
|
/** Right-column variant (row structure — and click→row mapping — is identical):
|
|
12
12
|
* "default" — WT + CI status columns (Trees tab)
|
|
13
|
-
* "triage" — a colored
|
|
13
|
+
* "triage" — a colored SLA-countdown badge
|
|
14
14
|
* "issues" — a readiness glyph (ready / blocked by dependencies) */
|
|
15
15
|
variant?: "default" | "triage" | "issues";
|
|
16
16
|
/** Ticket ids whose worktree is currently being removed — shown with a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import {
|
|
3
|
+
import { formatSla, isSnoozed } from "./sla.js";
|
|
4
4
|
import { issueReadiness } from "../trackers/types.js";
|
|
5
5
|
function stateColor(type, name) {
|
|
6
6
|
const n = name?.toLowerCase();
|
|
@@ -73,7 +73,13 @@ export function buildIssueListRows(groups, flatIssues) {
|
|
|
73
73
|
rows.push({ kind: "spacer" });
|
|
74
74
|
rows.push({ kind: "header", name: group.name, count: totalIssues, isFirst: gi === 0 });
|
|
75
75
|
for (const sg of group.statusGroups) {
|
|
76
|
-
|
|
76
|
+
// A blank status name suppresses the per-status sub-header (used by the
|
|
77
|
+
// Triage tab, where the single group needs no "Triage" header). Both the
|
|
78
|
+
// renderer and the click→row mapper call this builder, so skipping a row
|
|
79
|
+
// here keeps their indices aligned automatically.
|
|
80
|
+
if (sg.name) {
|
|
81
|
+
rows.push({ kind: "status-header", name: sg.name, type: sg.type, count: sg.issues.length });
|
|
82
|
+
}
|
|
77
83
|
for (const di of sg.issues) {
|
|
78
84
|
pushIssueWithChildren(di, 0);
|
|
79
85
|
}
|
|
@@ -87,9 +93,9 @@ export function buildIssueListRows(groups, flatIssues) {
|
|
|
87
93
|
// Glyphs are 1 char and rendered right-aligned within their column.
|
|
88
94
|
const LEFT_FIXED = 1 + 1 + 1 + 2 + 11; // 16 — left-aligned columns
|
|
89
95
|
const RIGHT_FIXED = 2 + 2 + 2; // 6 — WT + 2 spaces + CI
|
|
90
|
-
// Triage variant: a single right-aligned
|
|
91
|
-
// "
|
|
92
|
-
const
|
|
96
|
+
// Triage variant: a single right-aligned SLA-countdown column. Widest badge is
|
|
97
|
+
// "breached" (8 chars); pad/clamp everything to this so titles align.
|
|
98
|
+
const SLA_COL_WIDTH = 8;
|
|
93
99
|
// Issues variant: a single readiness glyph under a "RDY" header.
|
|
94
100
|
const READY_COL_WIDTH = 3;
|
|
95
101
|
const TITLE_GAP = 2; // minimum spacing between title and the right columns
|
|
@@ -106,7 +112,7 @@ function readinessGlyph(di) {
|
|
|
106
112
|
export default function IssueList({ groups, flatIssues, selectedIndex, scrollOffset, height, width, selectionBg = "#1e3a5f", variant = "default", deletingIds, }) {
|
|
107
113
|
const isTriage = variant === "triage";
|
|
108
114
|
const isIssues = variant === "issues";
|
|
109
|
-
const rightFixed = isTriage ?
|
|
115
|
+
const rightFixed = isTriage ? SLA_COL_WIDTH : isIssues ? READY_COL_WIDTH : RIGHT_FIXED;
|
|
110
116
|
const rows = buildIssueListRows(groups, flatIssues);
|
|
111
117
|
const visible = rows.slice(scrollOffset, scrollOffset + height);
|
|
112
118
|
const titleMaxWidth = Math.max(width - LEFT_FIXED - 1 /* leading space */ - rightFixed - TITLE_GAP, 10);
|
|
@@ -121,7 +127,7 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
121
127
|
// Label "WT CI" is 5 chars; the "W" lines up with the WT
|
|
122
128
|
// glyph at column (width - RIGHT_FIXED + 1).
|
|
123
129
|
const namePart = `${row.name} ${row.count}`;
|
|
124
|
-
const labelText = isTriage ? "
|
|
130
|
+
const labelText = isTriage ? "SLA" : isIssues ? "RDY" : "WT CI";
|
|
125
131
|
const labelPad = row.isFirst
|
|
126
132
|
? Math.max(2, width - namePart.length - labelText.length)
|
|
127
133
|
: 0;
|
|
@@ -133,8 +139,11 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
133
139
|
const { issue, flatIndex, depth } = row;
|
|
134
140
|
const selected = flatIndex === selectedIndex;
|
|
135
141
|
const di = issue;
|
|
136
|
-
|
|
137
|
-
|
|
142
|
+
// Snoozed triage rows are parked work: grey them out (dot + id +
|
|
143
|
+
// title + SLA) so the active items the user should pick up stand out.
|
|
144
|
+
const snoozed = isTriage && isSnoozed(di.issue.snoozedUntilAt);
|
|
145
|
+
const sc = snoozed ? "gray" : stateColor(di.issue.state.type, di.issue.state.name);
|
|
146
|
+
const prio = snoozed ? { glyph: " ", color: "gray" } : priorityMarker(di.issue.priority);
|
|
138
147
|
const isDeleting = deletingIds?.has(di.issue.identifier) ?? false;
|
|
139
148
|
const work = isDeleting ? { glyph: "⌫", color: "yellow" } : workIndicator(di.worktree);
|
|
140
149
|
const ci = ciIndicator(di.checks);
|
|
@@ -147,12 +156,13 @@ export default function IssueList({ groups, flatIssues, selectedIndex, scrollOff
|
|
|
147
156
|
// Pad between title and the right columns so the markers stay
|
|
148
157
|
// pinned to the right edge regardless of title length.
|
|
149
158
|
const trailingPad = Math.max(0, width - LEFT_FIXED - 1 - nestPrefix.length - title.length - rightFixed);
|
|
150
|
-
// Triage variant: a single right-aligned
|
|
151
|
-
// of the WT/CI columns.
|
|
152
|
-
const
|
|
153
|
-
const
|
|
159
|
+
// Triage variant: a single right-aligned SLA-countdown badge in
|
|
160
|
+
// place of the WT/CI columns. Snoozed rows show it greyed.
|
|
161
|
+
const sla = isTriage ? formatSla(di.issue.slaBreachesAt) : null;
|
|
162
|
+
const slaColor = snoozed ? "gray" : sla?.color;
|
|
163
|
+
const slaText = (sla?.label ?? "").padStart(SLA_COL_WIDTH).slice(-SLA_COL_WIDTH);
|
|
154
164
|
// Issues variant: a single readiness glyph right-aligned under "RDY".
|
|
155
165
|
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:
|
|
166
|
+
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, color: snoozed ? "gray" : undefined, children: [" ", title] }), _jsx(Text, { backgroundColor: bg, children: " ".repeat(trailingPad) }), isTriage ? (_jsx(Text, { backgroundColor: bg, color: slaColor, bold: !snoozed && sla?.urgent, children: slaText })) : 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));
|
|
157
167
|
}) }) }));
|
|
158
168
|
}
|
|
@@ -34,7 +34,7 @@ const LEGEND = [
|
|
|
34
34
|
{
|
|
35
35
|
glyph: "Triage",
|
|
36
36
|
color: "cyan",
|
|
37
|
-
meaning: "Incoming inbox —
|
|
37
|
+
meaning: "Incoming inbox — SLA countdown + comments (Linear only)",
|
|
38
38
|
},
|
|
39
39
|
{ glyph: "Issues", color: "cyan", meaning: "Backlog / planning" },
|
|
40
40
|
{ glyph: "Trees", color: "cyan", meaning: "Worktrees in progress" },
|
|
@@ -63,8 +63,9 @@ const LEGEND = [
|
|
|
63
63
|
{ glyph: "✗", color: "red", meaning: "CI column: a check is failing" },
|
|
64
64
|
{ glyph: "●", color: "yellow", meaning: "CI column: checks pending / running" },
|
|
65
65
|
{ glyph: "·", color: "gray", meaning: "CI column: no PR or no checks" },
|
|
66
|
-
{ glyph: "◷", color: "red", meaning: "
|
|
67
|
-
{ glyph: "◷", color: "yellow", meaning: "
|
|
66
|
+
{ glyph: "◷", color: "red", meaning: "SLA column (Triage): breached / under a day" },
|
|
67
|
+
{ glyph: "◷", color: "yellow", meaning: "SLA column (Triage): under two days" },
|
|
68
|
+
{ glyph: "●", color: "gray", meaning: "Triage row greyed: snoozed (parked, sunk to bottom)" },
|
|
68
69
|
{ glyph: "✓", color: "green", meaning: "RDY column (Issues): ready — no open blockers" },
|
|
69
70
|
{
|
|
70
71
|
glyph: "⊘",
|
|
@@ -3,6 +3,7 @@ import { runAsync } from "../exec.js";
|
|
|
3
3
|
import { readMainAgentTodos, findClaudeSessionCwd } from "../claude-todos.js";
|
|
4
4
|
import { getPRInfoAsync, getPRChecksAsync, getPRReviewsAsync, getPRConversationCommentsAsync, getPRViewAsync, getReviewRequestedPRsAsync, getRepoNameAsync, getGitHubUserNameAsync, } from "../github.js";
|
|
5
5
|
import { getIssueTracker, getCandidateTrackers } from "../trackers/index.js";
|
|
6
|
+
import { isSnoozed } from "./sla.js";
|
|
6
7
|
export async function loadDashboardData(repoRoot) {
|
|
7
8
|
// Fetch issues and worktrees in parallel
|
|
8
9
|
const tracker = getIssueTracker(repoRoot);
|
|
@@ -266,24 +267,36 @@ export async function loadDashboardData(repoRoot) {
|
|
|
266
267
|
const triageIssues = enriched.filter(isTriage);
|
|
267
268
|
const backlogIssues = enriched.filter((di) => !di.worktree && !isTriage(di));
|
|
268
269
|
const treeIssues = enriched.filter((di) => di.worktree);
|
|
269
|
-
//
|
|
270
|
-
// (
|
|
271
|
-
//
|
|
272
|
-
|
|
270
|
+
// Order the triage inbox so the work that needs attention now is on top:
|
|
271
|
+
// active (non-snoozed) items first, snoozed parked at the bottom; within
|
|
272
|
+
// each, by SLA breach time ascending (breached/soonest first), SLA-less last.
|
|
273
|
+
const slaRank = (di) => {
|
|
274
|
+
const s = di.issue.slaBreachesAt;
|
|
275
|
+
const t = s ? Date.parse(s) : NaN;
|
|
276
|
+
return Number.isNaN(t) ? Number.POSITIVE_INFINITY : t;
|
|
277
|
+
};
|
|
273
278
|
triageIssues.sort((a, b) => {
|
|
274
|
-
const
|
|
275
|
-
const
|
|
276
|
-
if (
|
|
277
|
-
return
|
|
278
|
-
|
|
279
|
-
return -1;
|
|
280
|
-
if (db)
|
|
281
|
-
return 1;
|
|
282
|
-
return 0;
|
|
279
|
+
const sa = isSnoozed(a.issue.snoozedUntilAt) ? 1 : 0;
|
|
280
|
+
const sb = isSnoozed(b.issue.snoozedUntilAt) ? 1 : 0;
|
|
281
|
+
if (sa !== sb)
|
|
282
|
+
return sa - sb;
|
|
283
|
+
return slaRank(a) - slaRank(b);
|
|
283
284
|
});
|
|
284
285
|
const groups = buildProjectGroups(backlogIssues);
|
|
285
286
|
const flatIssues = flatten(groups);
|
|
286
|
-
|
|
287
|
+
// Triage is scoped to issues assigned to the viewer, so project grouping and
|
|
288
|
+
// the redundant "Triage" status header add noise. Render one flat group under
|
|
289
|
+
// a single "Assigned to me" header (its column label is the SLA badge). The
|
|
290
|
+
// empty status-group name makes IssueList skip the per-status sub-header.
|
|
291
|
+
const triageGroups = triageIssues.length
|
|
292
|
+
? [
|
|
293
|
+
{
|
|
294
|
+
name: "Assigned to me",
|
|
295
|
+
id: null,
|
|
296
|
+
statusGroups: [{ name: "", type: "triage", issues: triageIssues }],
|
|
297
|
+
},
|
|
298
|
+
]
|
|
299
|
+
: [];
|
|
287
300
|
const flatTriage = flatten(triageGroups);
|
|
288
301
|
const treeGroups = buildProjectGroups(treeIssues);
|
|
289
302
|
const topLevelOrphans = orphans.filter((di) => !childTicketIds.has(di.issue.identifier));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triage SLA + snooze formatting for the Triage tab. Linear issues in triage
|
|
3
|
+
* carry an `slaBreachesAt` ISO timestamp (when the response SLA is breached);
|
|
4
|
+
* we render a short, urgency-coded countdown badge — the same "2d 23h" / "13h"
|
|
5
|
+
* shape Linear itself shows — used by both the issue list (right column) and
|
|
6
|
+
* the detail panel.
|
|
7
|
+
*
|
|
8
|
+
* Urgency buckets (by time until breach):
|
|
9
|
+
* - breached (≤ 0) → red, "breached"
|
|
10
|
+
* - imminent (< 24h) → red, "5h" / "45m"
|
|
11
|
+
* - soon (< 48h) → yellow, "1d 6h"
|
|
12
|
+
* - later (≥ 48h) → gray, "3d 2h"
|
|
13
|
+
*
|
|
14
|
+
* Returns null when there is no SLA so callers can render nothing.
|
|
15
|
+
*/
|
|
16
|
+
export interface SlaInfo {
|
|
17
|
+
/** Short badge text, e.g. "2d 23h", "13h", or "breached". */
|
|
18
|
+
label: string;
|
|
19
|
+
/** Ink color name keyed to urgency. */
|
|
20
|
+
color: "red" | "yellow" | "gray";
|
|
21
|
+
/** True for breached / < 24h — the cases worth a bold warning. */
|
|
22
|
+
urgent: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function formatSla(slaBreachesAt: string | null | undefined, now?: Date): SlaInfo | null;
|
|
25
|
+
/** True when the issue is snoozed past the current moment. A snoozed triage
|
|
26
|
+
* issue is parked — the UI greys it and sinks it below active work. */
|
|
27
|
+
export declare function isSnoozed(snoozedUntilAt: string | null | undefined, now?: Date): boolean;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function formatSla(slaBreachesAt, now = new Date()) {
|
|
2
|
+
if (!slaBreachesAt)
|
|
3
|
+
return null;
|
|
4
|
+
const breach = Date.parse(slaBreachesAt);
|
|
5
|
+
if (Number.isNaN(breach))
|
|
6
|
+
return null;
|
|
7
|
+
const ms = breach - now.getTime();
|
|
8
|
+
if (ms <= 0)
|
|
9
|
+
return { label: "breached", color: "red", urgent: true };
|
|
10
|
+
const totalMin = Math.floor(ms / 60_000);
|
|
11
|
+
const days = Math.floor(totalMin / 1440);
|
|
12
|
+
const hours = Math.floor((totalMin % 1440) / 60);
|
|
13
|
+
const mins = totalMin % 60;
|
|
14
|
+
let label;
|
|
15
|
+
if (days >= 1)
|
|
16
|
+
label = `${days}d ${hours}h`;
|
|
17
|
+
else if (hours >= 1)
|
|
18
|
+
label = `${hours}h`;
|
|
19
|
+
else
|
|
20
|
+
label = `${mins}m`;
|
|
21
|
+
const hoursLeft = ms / 3_600_000;
|
|
22
|
+
if (hoursLeft < 24)
|
|
23
|
+
return { label, color: "red", urgent: true };
|
|
24
|
+
if (hoursLeft < 48)
|
|
25
|
+
return { label, color: "yellow", urgent: false };
|
|
26
|
+
return { label, color: "gray", urgent: false };
|
|
27
|
+
}
|
|
28
|
+
/** True when the issue is snoozed past the current moment. A snoozed triage
|
|
29
|
+
* issue is parked — the UI greys it and sinks it below active work. */
|
|
30
|
+
export function isSnoozed(snoozedUntilAt, now = new Date()) {
|
|
31
|
+
if (!snoozedUntilAt)
|
|
32
|
+
return false;
|
|
33
|
+
const until = Date.parse(snoozedUntilAt);
|
|
34
|
+
return !Number.isNaN(until) && until > now.getTime();
|
|
35
|
+
}
|
|
@@ -37,7 +37,8 @@ query GetIssue($id: String!) {
|
|
|
37
37
|
title
|
|
38
38
|
description
|
|
39
39
|
url
|
|
40
|
-
|
|
40
|
+
slaBreachesAt
|
|
41
|
+
snoozedUntilAt
|
|
41
42
|
state { name type }
|
|
42
43
|
priority
|
|
43
44
|
labels { nodes { name } }
|
|
@@ -73,7 +74,8 @@ query AssignedIssues {
|
|
|
73
74
|
title
|
|
74
75
|
description
|
|
75
76
|
url
|
|
76
|
-
|
|
77
|
+
slaBreachesAt
|
|
78
|
+
snoozedUntilAt
|
|
77
79
|
priority
|
|
78
80
|
state { name type }
|
|
79
81
|
labels { nodes { name } }
|
|
@@ -97,7 +99,8 @@ function mapAssigned(issue) {
|
|
|
97
99
|
title: issue.title,
|
|
98
100
|
description: issue.description ?? null,
|
|
99
101
|
url: issue.url,
|
|
100
|
-
|
|
102
|
+
slaBreachesAt: issue.slaBreachesAt ?? null,
|
|
103
|
+
snoozedUntilAt: issue.snoozedUntilAt ?? null,
|
|
101
104
|
priority: issue.priority,
|
|
102
105
|
priorityLabel: PRIORITY_MAP[issue.priority] ?? "No priority",
|
|
103
106
|
state: {
|
|
@@ -38,11 +38,16 @@ export interface AssignedIssue {
|
|
|
38
38
|
blockedBy?: IssueRef[];
|
|
39
39
|
/** Issues this one blocks (downstream dependents). */
|
|
40
40
|
blocking?: IssueRef[];
|
|
41
|
-
/**
|
|
42
|
-
* Only trackers with a native
|
|
43
|
-
* others leave it undefined. Surfaced on the Triage tab as a
|
|
44
|
-
* urgency-coded badge. */
|
|
45
|
-
|
|
41
|
+
/** When the issue's triage SLA breaches, as an ISO timestamp, or null when
|
|
42
|
+
* no SLA applies. Only trackers with a native SLA concept populate it
|
|
43
|
+
* (Linear today); others leave it undefined. Surfaced on the Triage tab as a
|
|
44
|
+
* colored, urgency-coded countdown badge. */
|
|
45
|
+
slaBreachesAt?: string | null;
|
|
46
|
+
/** When the issue is snoozed until, as an ISO timestamp, or null when not
|
|
47
|
+
* snoozed. A snooze in the future means the issue is parked — surfaced on the
|
|
48
|
+
* Triage tab as a greyed, sunk-to-the-bottom row so active work stands out.
|
|
49
|
+
* Linear-only today. */
|
|
50
|
+
snoozedUntilAt?: string | null;
|
|
46
51
|
}
|
|
47
52
|
export interface Issue extends AssignedIssue {
|
|
48
53
|
comments: Comment[];
|
package/package.json
CHANGED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Due-date formatting for the Triage tab. Turns Linear's `YYYY-MM-DD`
|
|
3
|
-
* `dueDate` string into a short, urgency-coded badge used by both the issue
|
|
4
|
-
* list (right column) and the detail panel.
|
|
5
|
-
*
|
|
6
|
-
* Urgency buckets (by whole calendar days from today, local time):
|
|
7
|
-
* - overdue (< 0) → red, "⚠ overdue Nd"
|
|
8
|
-
* - due today (0) → red, "⚠ due today"
|
|
9
|
-
* - soon (1–2 days) → yellow, "due in Nd"
|
|
10
|
-
* - later (≥ 3 days) → gray, "due Mon D"
|
|
11
|
-
*
|
|
12
|
-
* Returns null when there is no due date so callers can render nothing.
|
|
13
|
-
*/
|
|
14
|
-
export interface DueInfo {
|
|
15
|
-
/** Short badge text, e.g. "⚠ overdue 3d" or "due Jun 12". */
|
|
16
|
-
label: string;
|
|
17
|
-
/** Ink color name keyed to urgency. */
|
|
18
|
-
color: "red" | "yellow" | "gray";
|
|
19
|
-
/** True for overdue/today — the cases worth a warning glyph. */
|
|
20
|
-
urgent: boolean;
|
|
21
|
-
/** Whole days until due (negative when overdue). */
|
|
22
|
-
days: number;
|
|
23
|
-
}
|
|
24
|
-
export declare function formatDueDate(dueDate: string | null | undefined, now?: Date): DueInfo | null;
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
/** Whole-day difference between two dates, ignoring time-of-day. */
|
|
2
|
-
function dayDiff(from, to) {
|
|
3
|
-
const a = Date.UTC(from.getFullYear(), from.getMonth(), from.getDate());
|
|
4
|
-
const b = Date.UTC(to.getFullYear(), to.getMonth(), to.getDate());
|
|
5
|
-
return Math.round((b - a) / 86_400_000);
|
|
6
|
-
}
|
|
7
|
-
export function formatDueDate(dueDate, now = new Date()) {
|
|
8
|
-
if (!dueDate)
|
|
9
|
-
return null;
|
|
10
|
-
// Parse `YYYY-MM-DD` as a local date (not UTC) so "today" matches the user's
|
|
11
|
-
// wall clock. `new Date("2026-06-12")` would parse as UTC midnight; build the
|
|
12
|
-
// date from parts instead.
|
|
13
|
-
const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(dueDate);
|
|
14
|
-
if (!m)
|
|
15
|
-
return null;
|
|
16
|
-
const due = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
|
17
|
-
if (Number.isNaN(due.getTime()))
|
|
18
|
-
return null;
|
|
19
|
-
const days = dayDiff(now, due);
|
|
20
|
-
const monthDay = due.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
21
|
-
if (days < 0) {
|
|
22
|
-
const n = Math.abs(days);
|
|
23
|
-
return { label: `⚠ overdue ${n}d`, color: "red", urgent: true, days };
|
|
24
|
-
}
|
|
25
|
-
if (days === 0) {
|
|
26
|
-
return { label: "⚠ due today", color: "red", urgent: true, days };
|
|
27
|
-
}
|
|
28
|
-
if (days <= 2) {
|
|
29
|
-
return { label: `due in ${days}d`, color: "yellow", urgent: false, days };
|
|
30
|
-
}
|
|
31
|
-
return { label: `due ${monthDay}`, color: "gray", urgent: false, days };
|
|
32
|
-
}
|