santree 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -2
- package/dist/commands/dashboard.js +425 -95
- package/dist/commands/doctor.js +103 -17
- package/dist/commands/helpers/statusline.js +10 -2
- package/dist/commands/update.d.ts +15 -0
- package/dist/commands/update.js +72 -0
- package/dist/commands/worktree/diff.d.ts +13 -0
- package/dist/commands/worktree/diff.js +76 -0
- package/dist/lib/ai.d.ts +12 -2
- package/dist/lib/ai.js +37 -6
- package/dist/lib/dashboard/DetailPanel.d.ts +9 -0
- package/dist/lib/dashboard/DetailPanel.js +235 -89
- package/dist/lib/dashboard/DiffOverlay.d.ts +50 -0
- package/dist/lib/dashboard/DiffOverlay.js +243 -0
- package/dist/lib/dashboard/IssueList.d.ts +20 -3
- package/dist/lib/dashboard/IssueList.js +74 -103
- package/dist/lib/dashboard/ReviewDetailPanel.d.ts +6 -0
- package/dist/lib/dashboard/ReviewDetailPanel.js +4 -7
- package/dist/lib/dashboard/ReviewList.d.ts +3 -1
- package/dist/lib/dashboard/ReviewList.js +3 -3
- package/dist/lib/dashboard/data.js +10 -4
- package/dist/lib/dashboard/theme.d.ts +24 -0
- package/dist/lib/dashboard/theme.js +113 -0
- package/dist/lib/dashboard/types.d.ts +52 -1
- package/dist/lib/dashboard/types.js +81 -0
- package/dist/lib/git.d.ts +20 -0
- package/dist/lib/git.js +37 -0
- package/dist/lib/version.d.ts +55 -0
- package/dist/lib/version.js +224 -0
- package/package.json +1 -1
|
@@ -49,10 +49,12 @@ function fileColor(xy) {
|
|
|
49
49
|
return "gray";
|
|
50
50
|
return "yellow";
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/** Returns the context-sensitive action key list for the selected issue.
|
|
53
|
+
* Lifted out of the panel so the dashboard can render it on the same row as
|
|
54
|
+
* the global command bar (so left- and right-pane key hints align). */
|
|
55
|
+
export function buildIssueActions(di) {
|
|
53
56
|
const { worktree, pr, issue } = di;
|
|
54
57
|
const items = [];
|
|
55
|
-
// Work/Resume
|
|
56
58
|
if (worktree?.sessionId) {
|
|
57
59
|
items.push({ key: "↵", label: "Resume", color: "cyan" });
|
|
58
60
|
}
|
|
@@ -63,15 +65,15 @@ function buildActions(di) {
|
|
|
63
65
|
else {
|
|
64
66
|
items.push({ key: "w", label: "Work", color: "cyan" });
|
|
65
67
|
}
|
|
66
|
-
// Editor
|
|
67
68
|
if (worktree) {
|
|
68
69
|
items.push({ key: "e", label: "Editor", color: "cyan" });
|
|
69
70
|
}
|
|
70
|
-
// Commit
|
|
71
71
|
if (worktree?.dirty) {
|
|
72
72
|
items.push({ key: "C", label: "Commit", color: "cyan" });
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
if (worktree) {
|
|
75
|
+
items.push({ key: "v", label: "View diff", color: "cyan" });
|
|
76
|
+
}
|
|
75
77
|
if (worktree && !pr) {
|
|
76
78
|
items.push({ key: "c", label: "Create PR", color: "cyan" });
|
|
77
79
|
}
|
|
@@ -79,17 +81,26 @@ function buildActions(di) {
|
|
|
79
81
|
items.push({ key: "f", label: "Fix PR", color: "cyan" });
|
|
80
82
|
items.push({ key: "r", label: "Review", color: "cyan" });
|
|
81
83
|
}
|
|
82
|
-
// Links
|
|
83
84
|
if (issue.url) {
|
|
84
85
|
items.push({ key: "o", label: "Linear", color: "gray" });
|
|
85
86
|
}
|
|
86
87
|
if (pr)
|
|
87
88
|
items.push({ key: "p", label: "Open PR", color: "gray" });
|
|
88
|
-
// Destructive
|
|
89
89
|
if (worktree) {
|
|
90
90
|
items.push({ key: "d", label: "Remove", color: "red" });
|
|
91
91
|
}
|
|
92
|
-
return
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
/** Section title with a colored leading icon and a bold name. Kept consistent
|
|
95
|
+
* across all sections so the eye can immediately find the next block. */
|
|
96
|
+
function sectionHeader(icon, label, iconColor = "cyan") {
|
|
97
|
+
return {
|
|
98
|
+
text: "",
|
|
99
|
+
segments: [
|
|
100
|
+
{ text: `${icon} `, color: iconColor, bold: true },
|
|
101
|
+
{ text: label, bold: true },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
93
104
|
}
|
|
94
105
|
export default function DetailPanel({ issue, scrollOffset, height, width, creatingForTicket, creationLogs, }) {
|
|
95
106
|
// Show creation logs when selected issue is being created
|
|
@@ -106,116 +117,227 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
106
117
|
const { issue: li, worktree, pr } = issue;
|
|
107
118
|
const lines = [];
|
|
108
119
|
const rule = "─".repeat(width);
|
|
109
|
-
|
|
120
|
+
const ruleLine = { text: rule, dim: true };
|
|
121
|
+
// ── Hero: identifier + title, then a status pill row ───────────────
|
|
110
122
|
lines.push({ text: `${li.identifier} ${li.title}`, bold: true });
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
const sc = stateColor(li.state.type);
|
|
124
|
+
const heroSegs = [
|
|
125
|
+
{ text: "● ", color: sc },
|
|
126
|
+
{ text: li.state.name, color: sc },
|
|
127
|
+
{ text: " · ", dim: true },
|
|
128
|
+
{ text: li.priorityLabel },
|
|
129
|
+
];
|
|
130
|
+
if (li.labels.length > 0) {
|
|
131
|
+
heroSegs.push({ text: " · ", dim: true });
|
|
132
|
+
heroSegs.push({ text: li.labels.join(", "), dim: true });
|
|
133
|
+
}
|
|
134
|
+
lines.push({ text: "", segments: heroSegs });
|
|
117
135
|
// ── Description ───────────────────────────────────────────────────
|
|
118
136
|
if (li.description) {
|
|
119
|
-
lines.push({ text: rule, dim: true });
|
|
120
137
|
lines.push({ text: "" });
|
|
121
138
|
for (const dLine of li.description.trimEnd().split("\n")) {
|
|
122
139
|
lines.push({ text: dLine });
|
|
123
140
|
}
|
|
124
|
-
lines.push({ text: "" });
|
|
125
141
|
}
|
|
126
|
-
// ── Worktree
|
|
127
|
-
lines.push(
|
|
128
|
-
lines.push({ text: "WORKTREE", dim: true });
|
|
142
|
+
// ── Worktree ──────────────────────────────────────────────────────
|
|
143
|
+
lines.push(ruleLine);
|
|
129
144
|
if (worktree) {
|
|
145
|
+
// Header carries a quick status badge (clean / dirty) so the user can tell
|
|
146
|
+
// at a glance without reading further.
|
|
147
|
+
const dirty = worktree.dirty;
|
|
148
|
+
lines.push({
|
|
149
|
+
text: "",
|
|
150
|
+
segments: [
|
|
151
|
+
{ text: "⎇ ", color: "cyan", bold: true },
|
|
152
|
+
{ text: "Worktree", bold: true },
|
|
153
|
+
{ text: " " },
|
|
154
|
+
{
|
|
155
|
+
text: dirty ? "● dirty" : "✓ clean",
|
|
156
|
+
color: dirty ? "yellow" : "green",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
});
|
|
130
160
|
lines.push({ text: ` ${worktree.branch}` });
|
|
131
161
|
lines.push({ text: ` ${worktree.path}`, dim: true });
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
text:
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (gs.files.length > maxFiles) {
|
|
158
|
-
lines.push({ text: ` +${gs.files.length - maxFiles} more`, dim: true });
|
|
159
|
-
}
|
|
160
|
-
if (worktree.sessionId) {
|
|
161
|
-
lines.push({ text: ` session: ${worktree.sessionId}`, color: "cyan" });
|
|
162
|
+
// Single metric row: files / +ins / -dels / commits ahead.
|
|
163
|
+
const ds = worktree.diffStats;
|
|
164
|
+
if (ds && (ds.insertions > 0 || ds.deletions > 0 || ds.filesChanged > 0)) {
|
|
165
|
+
const segs = [{ text: " " }];
|
|
166
|
+
if (ds.filesChanged > 0) {
|
|
167
|
+
segs.push({
|
|
168
|
+
text: `${ds.filesChanged} file${ds.filesChanged === 1 ? "" : "s"}`,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (ds.insertions > 0) {
|
|
172
|
+
if (segs.length > 1)
|
|
173
|
+
segs.push({ text: " " });
|
|
174
|
+
segs.push({ text: `+${ds.insertions}`, color: "green" });
|
|
175
|
+
}
|
|
176
|
+
if (ds.deletions > 0) {
|
|
177
|
+
if (segs.length > 1)
|
|
178
|
+
segs.push({ text: " " });
|
|
179
|
+
segs.push({ text: `−${ds.deletions}`, color: "red" });
|
|
180
|
+
}
|
|
181
|
+
if (worktree.commitsAhead > 0) {
|
|
182
|
+
if (segs.length > 1)
|
|
183
|
+
segs.push({ text: " " });
|
|
184
|
+
segs.push({ text: `↑ ${worktree.commitsAhead}`, color: "cyan" });
|
|
185
|
+
}
|
|
186
|
+
lines.push({ text: "", segments: segs });
|
|
162
187
|
}
|
|
163
|
-
|
|
164
|
-
|
|
188
|
+
// Per-status counts only when there's something dirty — when the tree is
|
|
189
|
+
// clean the badge in the section header already says so.
|
|
190
|
+
const gs = parseGitStatus(worktree.gitStatus);
|
|
191
|
+
if (dirty) {
|
|
192
|
+
const statusSegs = [{ text: " " }];
|
|
193
|
+
if (gs.staged > 0) {
|
|
194
|
+
if (statusSegs.length > 1)
|
|
195
|
+
statusSegs.push({ text: " " });
|
|
196
|
+
statusSegs.push({ text: `+${gs.staged} staged`, color: "green" });
|
|
197
|
+
}
|
|
198
|
+
if (gs.unstaged > 0) {
|
|
199
|
+
if (statusSegs.length > 1)
|
|
200
|
+
statusSegs.push({ text: " " });
|
|
201
|
+
statusSegs.push({ text: `~${gs.unstaged} unstaged`, color: "yellow" });
|
|
202
|
+
}
|
|
203
|
+
if (gs.untracked > 0) {
|
|
204
|
+
if (statusSegs.length > 1)
|
|
205
|
+
statusSegs.push({ text: " " });
|
|
206
|
+
statusSegs.push({ text: `?${gs.untracked} untracked`, color: "gray" });
|
|
207
|
+
}
|
|
208
|
+
if (statusSegs.length > 1) {
|
|
209
|
+
lines.push({ text: "", segments: statusSegs });
|
|
210
|
+
}
|
|
211
|
+
// Show individual files (up to 8)
|
|
212
|
+
const maxFiles = 8;
|
|
213
|
+
for (let i = 0; i < Math.min(gs.files.length, maxFiles); i++) {
|
|
214
|
+
const f = gs.files[i];
|
|
215
|
+
lines.push({ text: ` ${f.xy} ${f.file}`, color: fileColor(f.xy) });
|
|
216
|
+
}
|
|
217
|
+
if (gs.files.length > maxFiles) {
|
|
218
|
+
lines.push({ text: ` +${gs.files.length - maxFiles} more`, dim: true });
|
|
219
|
+
}
|
|
165
220
|
}
|
|
221
|
+
// Session state — single line, color reflects state.
|
|
166
222
|
if (worktree.sessionState === "waiting") {
|
|
167
223
|
const msg = worktree.sessionMessage
|
|
168
224
|
? `NEEDS INPUT: ${worktree.sessionMessage}`
|
|
169
225
|
: "NEEDS INPUT";
|
|
170
|
-
lines.push({
|
|
226
|
+
lines.push({
|
|
227
|
+
text: "",
|
|
228
|
+
segments: [
|
|
229
|
+
{ text: " ◆ ", color: "red" },
|
|
230
|
+
{ text: msg, color: "red", bold: true },
|
|
231
|
+
],
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else if (worktree.sessionState === "active") {
|
|
235
|
+
lines.push({
|
|
236
|
+
text: "",
|
|
237
|
+
segments: [
|
|
238
|
+
{ text: " ◆ ", color: "green" },
|
|
239
|
+
{ text: "session active", color: "green" },
|
|
240
|
+
],
|
|
241
|
+
});
|
|
171
242
|
}
|
|
172
243
|
else if (worktree.sessionState === "idle") {
|
|
173
|
-
lines.push({
|
|
244
|
+
lines.push({
|
|
245
|
+
text: "",
|
|
246
|
+
segments: [
|
|
247
|
+
{ text: " ◆ ", color: "yellow" },
|
|
248
|
+
{ text: "session idle", color: "yellow" },
|
|
249
|
+
{ text: " (waiting for prompt)", dim: true },
|
|
250
|
+
],
|
|
251
|
+
});
|
|
174
252
|
}
|
|
175
|
-
else if (worktree.
|
|
176
|
-
lines.push({
|
|
253
|
+
else if (worktree.sessionId) {
|
|
254
|
+
lines.push({
|
|
255
|
+
text: "",
|
|
256
|
+
segments: [
|
|
257
|
+
{ text: " ◇ ", color: "cyan" },
|
|
258
|
+
{ text: "session ", dim: true },
|
|
259
|
+
{ text: worktree.sessionId.slice(0, 8), color: "cyan" },
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
lines.push({
|
|
265
|
+
text: "",
|
|
266
|
+
segments: [
|
|
267
|
+
{ text: " ◇ ", dim: true },
|
|
268
|
+
{ text: "no session", dim: true },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
177
271
|
}
|
|
178
272
|
}
|
|
179
273
|
else {
|
|
180
|
-
lines.push(
|
|
274
|
+
lines.push(sectionHeader("⎇", "Worktree"));
|
|
275
|
+
lines.push({ text: " no worktree for this ticket", dim: true });
|
|
181
276
|
}
|
|
182
277
|
// ── Pull Request ──────────────────────────────────────────────────
|
|
183
278
|
const { checks, reviews } = issue;
|
|
184
|
-
lines.push(
|
|
185
|
-
lines.push({ text: "PULL REQUEST", dim: true });
|
|
279
|
+
lines.push(ruleLine);
|
|
186
280
|
if (pr) {
|
|
187
|
-
const
|
|
188
|
-
const draft = pr.isDraft ? " draft" : "";
|
|
189
|
-
lines.push({
|
|
281
|
+
const prColor = pr.state === "MERGED" ? "magenta" : pr.state === "OPEN" ? "green" : "red";
|
|
282
|
+
const draft = pr.isDraft ? " · draft" : "";
|
|
283
|
+
lines.push({
|
|
284
|
+
text: "",
|
|
285
|
+
segments: [
|
|
286
|
+
{ text: "◉ ", color: "cyan", bold: true },
|
|
287
|
+
{ text: "Pull Request", bold: true },
|
|
288
|
+
{ text: " " },
|
|
289
|
+
{ text: `#${pr.number}`, color: prColor, bold: true },
|
|
290
|
+
{ text: " " },
|
|
291
|
+
{ text: pr.state, color: prColor },
|
|
292
|
+
{ text: draft, dim: true },
|
|
293
|
+
],
|
|
294
|
+
});
|
|
190
295
|
if (pr.url) {
|
|
191
296
|
lines.push({ text: ` ${pr.url}`, dim: true });
|
|
192
297
|
}
|
|
193
298
|
}
|
|
194
299
|
else {
|
|
195
|
-
lines.push(
|
|
300
|
+
lines.push(sectionHeader("◉", "Pull Request"));
|
|
301
|
+
lines.push({ text: " no PR yet", dim: true });
|
|
196
302
|
}
|
|
197
303
|
// ── Checks ────────────────────────────────────────────────────────
|
|
198
304
|
if (checks && checks.length > 0) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
305
|
+
const passing = checks.filter((c) => c.bucket === "pass");
|
|
306
|
+
const failing = checks.filter((c) => c.bucket === "fail");
|
|
307
|
+
const pending = checks.filter((c) => c.bucket !== "pass" && c.bucket !== "fail");
|
|
308
|
+
const headerColor = failing.length > 0 ? "red" : pending.length > 0 ? "yellow" : "green";
|
|
309
|
+
lines.push(ruleLine);
|
|
310
|
+
const headerSegs = [
|
|
311
|
+
{ text: "✓ ", color: "cyan", bold: true },
|
|
312
|
+
{ text: "Checks", bold: true },
|
|
313
|
+
{ text: " " },
|
|
314
|
+
{ text: `${passing.length}/${checks.length} passing`, color: headerColor },
|
|
315
|
+
];
|
|
316
|
+
if (failing.length > 0) {
|
|
317
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
318
|
+
headerSegs.push({ text: `${failing.length} failing`, color: "red" });
|
|
319
|
+
}
|
|
320
|
+
if (pending.length > 0) {
|
|
321
|
+
headerSegs.push({ text: " · ", dim: true });
|
|
322
|
+
headerSegs.push({ text: `${pending.length} pending`, color: "yellow" });
|
|
323
|
+
}
|
|
324
|
+
lines.push({ text: "", segments: headerSegs });
|
|
325
|
+
// Order: failing first (most important), then pending, then passing.
|
|
326
|
+
for (const check of failing) {
|
|
327
|
+
const desc = check.description ? ` — ${check.description}` : "";
|
|
328
|
+
lines.push({ text: ` ✗ ${check.name}${desc}`, color: "red" });
|
|
329
|
+
}
|
|
330
|
+
for (const check of pending) {
|
|
331
|
+
lines.push({ text: ` ● ${check.name}`, color: "yellow" });
|
|
332
|
+
}
|
|
333
|
+
for (const check of passing) {
|
|
334
|
+
lines.push({ text: ` ✓ ${check.name}`, color: "green" });
|
|
213
335
|
}
|
|
214
336
|
}
|
|
215
337
|
// ── Reviews ───────────────────────────────────────────────────────
|
|
216
338
|
if (reviews && reviews.length > 0) {
|
|
217
|
-
lines.push(
|
|
218
|
-
lines.push(
|
|
339
|
+
lines.push(ruleLine);
|
|
340
|
+
lines.push(sectionHeader("★", "Reviews"));
|
|
219
341
|
for (const review of reviews) {
|
|
220
342
|
const author = review.author.login;
|
|
221
343
|
const rc = review.state === "APPROVED"
|
|
@@ -223,18 +345,18 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
223
345
|
: review.state === "CHANGES_REQUESTED"
|
|
224
346
|
? "red"
|
|
225
347
|
: "yellow";
|
|
226
|
-
lines.push({
|
|
348
|
+
lines.push({
|
|
349
|
+
text: "",
|
|
350
|
+
segments: [{ text: ` ${author}` }, { text: " " }, { text: review.state, color: rc }],
|
|
351
|
+
});
|
|
227
352
|
}
|
|
228
353
|
}
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
//
|
|
232
|
-
const actionsHeight = actionRows.length + 1;
|
|
233
|
-
const scrollableHeight = height - actionsHeight;
|
|
234
|
-
// ── Render scrollable content ─────────────────────────────────────
|
|
354
|
+
// Action footer is rendered by the dashboard one row outside the panel,
|
|
355
|
+
// alongside the global command bar, so left- and right-pane key hints sit
|
|
356
|
+
// on the same row. The panel itself uses its full height for content.
|
|
235
357
|
const totalLines = lines.length;
|
|
236
|
-
const canScroll = totalLines >
|
|
237
|
-
const contentRows = canScroll ?
|
|
358
|
+
const canScroll = totalLines > height;
|
|
359
|
+
const contentRows = canScroll ? height - 2 : height;
|
|
238
360
|
const clampedOffset = Math.min(scrollOffset, Math.max(0, totalLines - contentRows));
|
|
239
361
|
const visible = lines.slice(clampedOffset, clampedOffset + contentRows);
|
|
240
362
|
let scrollArrow = null;
|
|
@@ -243,5 +365,29 @@ export default function DetailPanel({ issue, scrollOffset, height, width, creati
|
|
|
243
365
|
const atBottom = clampedOffset + contentRows >= totalLines;
|
|
244
366
|
scrollArrow = atTop ? "↓ scroll" : atBottom ? "↑ scroll" : "↑↓ scroll";
|
|
245
367
|
}
|
|
246
|
-
|
|
368
|
+
// Pre-truncate to keep long URLs/paths/descriptions from wrapping into the
|
|
369
|
+
// row below — Ink's Text wrap is unreliable at the box's right edge and was
|
|
370
|
+
// causing content to bleed into the next line and shift everything down.
|
|
371
|
+
const clamp = (s) => (s.length > width ? s.slice(0, Math.max(0, width - 1)) + "…" : s);
|
|
372
|
+
const clampSegments = (segs) => {
|
|
373
|
+
let remaining = width;
|
|
374
|
+
const out = [];
|
|
375
|
+
for (const seg of segs) {
|
|
376
|
+
if (remaining <= 0)
|
|
377
|
+
break;
|
|
378
|
+
if (seg.text.length <= remaining) {
|
|
379
|
+
out.push(seg);
|
|
380
|
+
remaining -= seg.text.length;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
out.push({
|
|
384
|
+
...seg,
|
|
385
|
+
text: seg.text.slice(0, Math.max(0, remaining - 1)) + "…",
|
|
386
|
+
});
|
|
387
|
+
remaining = 0;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return out;
|
|
391
|
+
};
|
|
392
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [visible.map((line, i) => (_jsx(Box, { children: line.segments ? (_jsx(Text, { children: clampSegments(line.segments).map((seg, j) => (_jsx(Text, { color: seg.color, bold: seg.bold, dimColor: seg.dim, children: seg.text }, j))) })) : (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, children: line.text ? clamp(line.text) : " " })) }, i))), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: " " }) })), scrollArrow && (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: scrollArrow }) }))] }));
|
|
247
393
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { DiffFile } from "./types.js";
|
|
2
|
+
interface Props {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
ticketId: string;
|
|
6
|
+
baseBranch: string;
|
|
7
|
+
files: DiffFile[];
|
|
8
|
+
fileIndex: number;
|
|
9
|
+
fileScrollOffset: number;
|
|
10
|
+
content: string | null;
|
|
11
|
+
contentScrollOffset: number;
|
|
12
|
+
loadingFiles: boolean;
|
|
13
|
+
loadingContent: boolean;
|
|
14
|
+
error: string | null;
|
|
15
|
+
/** Theme-adapted selection background. Falls back to dark navy. */
|
|
16
|
+
selectionBg?: string;
|
|
17
|
+
}
|
|
18
|
+
interface RenderedRow {
|
|
19
|
+
prefix: string;
|
|
20
|
+
label: string;
|
|
21
|
+
color?: string;
|
|
22
|
+
dim?: boolean;
|
|
23
|
+
bold?: boolean;
|
|
24
|
+
fileIndex: number | null;
|
|
25
|
+
}
|
|
26
|
+
export declare function flattenTreeFiles(files: DiffFile[]): DiffFile[];
|
|
27
|
+
export interface DiffLayout {
|
|
28
|
+
bodyHeight: number;
|
|
29
|
+
leftWidth: number;
|
|
30
|
+
rightWidth: number;
|
|
31
|
+
rows: RenderedRow[];
|
|
32
|
+
effectiveScroll: number;
|
|
33
|
+
selectedRowIdx: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Computes the diff overlay layout — body height, pane widths, rendered tree
|
|
37
|
+
* rows, and the effective scroll offset (clamped to keep selection visible).
|
|
38
|
+
*
|
|
39
|
+
* Shared between DiffOverlay (rendering) and the dashboard mouse handler
|
|
40
|
+
* (mapping click coords back to file indices).
|
|
41
|
+
*/
|
|
42
|
+
export declare function computeDiffLayout(opts: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
files: DiffFile[];
|
|
46
|
+
fileIndex: number;
|
|
47
|
+
fileScrollOffset: number;
|
|
48
|
+
}): DiffLayout;
|
|
49
|
+
export default function DiffOverlay({ width, height, ticketId, baseBranch, files, fileIndex, fileScrollOffset, content, contentScrollOffset, loadingFiles, loadingContent, error, selectionBg, }: Props): import("react/jsx-runtime").JSX.Element;
|
|
50
|
+
export {};
|