reslop 0.2.7 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +573 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +17 -0
- package/lib/agents/models.js +467 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +43 -68
- package/lib/{files.js → common/files.js} +48 -55
- package/lib/{find.js → common/find.js} +1 -1
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +90 -49
- package/lib/dashboard/npm.js +17 -13
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +36 -31
- package/lib/{workspace.js → dashboard/workspace.js} +23 -38
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +151 -95
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +504 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +114 -48
- package/lib/{editor.js → input/editor.js} +16 -8
- package/lib/{keys.js → input/keys.js} +24 -45
- package/lib/input/line-edit.js +78 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +642 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +9 -14
- package/lib/render/footer.js +385 -0
- package/lib/render/frame.js +123 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +320 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +16 -11
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +9 -13
- package/lib/render/primitives.js +101 -15
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +13 -25
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +13 -60
- package/lib/render/tiles.js +109 -48
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +48 -19
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +14 -0
- package/lib/review/format.js +420 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +976 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +33 -40
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +91 -52
- package/lib/session/compose.js +189 -147
- package/lib/session/dashboard.js +23 -22
- package/lib/session/dispatch.js +86 -21
- package/lib/session/files-pane.js +47 -26
- package/lib/session/find.js +16 -29
- package/lib/session/import.js +93 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +78 -72
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +6 -3
- package/lib/session/npm/history.js +250 -0
- package/lib/session/npm/npm.js +836 -0
- package/lib/session/{packages.js → npm/packages.js} +21 -14
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +202 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +158 -63
- package/lib/session/tasks.js +321 -66
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +65 -65
- package/lib/session/workspace.js +39 -78
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +4 -3
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -545
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -455
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -806
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/render/search.js +0 -25
- package/lib/review.js +0 -745
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
package/lib/render/primitives.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const { clamp, trimText } = require('../common/utilities.js');
|
|
4
|
+
const ansi = require('../term/ansi.js');
|
|
5
|
+
const { LIST_PANES } = require('../input/actions.js');
|
|
5
6
|
|
|
6
7
|
const { THEME, paint, visibleWidth, graphemes, graphemeWidth } = ansi;
|
|
7
|
-
const { LIST_PANES } = actions;
|
|
8
8
|
|
|
9
9
|
const GUTTER_W = 2;
|
|
10
10
|
const FILE_MARK = '▶';
|
|
@@ -17,12 +17,14 @@ const fill = (width, fgRgb, bgRgb, color) =>
|
|
|
17
17
|
|
|
18
18
|
const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
|
|
19
19
|
|
|
20
|
-
const isListPane = (pane) =>
|
|
21
|
-
LIST_PANES.includes(pane)
|
|
20
|
+
const isListPane = (pane) => {
|
|
21
|
+
if (LIST_PANES.includes(pane)) return true;
|
|
22
|
+
return pane === 'dashboard' || pane === 'repos';
|
|
23
|
+
};
|
|
22
24
|
|
|
23
25
|
const isTaskView = (view) => view.item?.origin === 'task';
|
|
24
26
|
|
|
25
|
-
const branchLabel = (name) =>
|
|
27
|
+
const branchLabel = (name) => trimText(name);
|
|
26
28
|
|
|
27
29
|
const padVisible = (text, width, align) => {
|
|
28
30
|
const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
|
|
@@ -46,6 +48,11 @@ const paintBodyFill = (width, color, kind) => {
|
|
|
46
48
|
return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
const lazyRows = (length, paint) => ({ length, paint });
|
|
52
|
+
|
|
53
|
+
const bodyRow = (body, index) =>
|
|
54
|
+
Array.isArray(body) ? body[index] : body.paint(index);
|
|
55
|
+
|
|
49
56
|
const paneResult = (fields = {}) => ({
|
|
50
57
|
body: fields.body ?? [],
|
|
51
58
|
fileHits: fields.fileHits ?? [],
|
|
@@ -55,6 +62,7 @@ const paneResult = (fields = {}) => ({
|
|
|
55
62
|
splitBody: fields.splitBody === true,
|
|
56
63
|
listScroll: fields.listScroll ?? 0,
|
|
57
64
|
editHits: fields.editHits ?? [],
|
|
65
|
+
scrollBar: fields.scrollBar ?? null,
|
|
58
66
|
});
|
|
59
67
|
|
|
60
68
|
const editSpan = (x0, textX, x1, start, text) => ({
|
|
@@ -79,7 +87,7 @@ const listWindowStart = (focus, offset, listH, count) => {
|
|
|
79
87
|
let start = offset ?? 0;
|
|
80
88
|
if (focus < start) start = focus;
|
|
81
89
|
if (focus >= start + listH) start = focus - listH + 1;
|
|
82
|
-
return
|
|
90
|
+
return clamp(start, 0, count - listH);
|
|
83
91
|
};
|
|
84
92
|
|
|
85
93
|
const rowCursor = (cursor, row) => {
|
|
@@ -90,10 +98,11 @@ const rowCursor = (cursor, row) => {
|
|
|
90
98
|
|
|
91
99
|
const paintListWindow = (count, focus, pane, lineAt) => {
|
|
92
100
|
const { width, color, bodyH, headerLines, offset } = pane;
|
|
93
|
-
const
|
|
101
|
+
const padded = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
|
|
102
|
+
const pad = pane.pad === 0 ? 0 : padded;
|
|
94
103
|
const listH = bodyH - pad * 2;
|
|
95
104
|
const start = listWindowStart(focus, offset, listH, count);
|
|
96
|
-
const blank = paintBodyFill(width, color, 'files');
|
|
105
|
+
const blank = pane.blank ?? paintBodyFill(width, color, 'files');
|
|
97
106
|
const body = pad ? [blank] : [];
|
|
98
107
|
const fileHits = [];
|
|
99
108
|
const editHits = [];
|
|
@@ -105,7 +114,11 @@ const paintListWindow = (count, focus, pane, lineAt) => {
|
|
|
105
114
|
if (line.cursor) cursor = rowCursor(line.cursor, row);
|
|
106
115
|
for (const edit of line.edits) editHits.push({ ...edit, row });
|
|
107
116
|
if (line.hit) {
|
|
108
|
-
|
|
117
|
+
const y = headerLines + pad + i + 1;
|
|
118
|
+
for (const span of line.spans ?? []) {
|
|
119
|
+
fileHits.push({ y, cursor: line.index, ...span });
|
|
120
|
+
}
|
|
121
|
+
fileHits.push({ y, cursor: line.index });
|
|
109
122
|
}
|
|
110
123
|
}
|
|
111
124
|
if (pad) body.push(blank);
|
|
@@ -116,7 +129,15 @@ const insertedAt = (extra, count) => {
|
|
|
116
129
|
if (extra.at === 'start') return 0;
|
|
117
130
|
if (extra.at !== 'index') return count;
|
|
118
131
|
const index = extra.index ?? count;
|
|
119
|
-
return
|
|
132
|
+
return clamp(index, 0, count);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const lineFromPaint = (painted, index) => {
|
|
136
|
+
if (typeof painted === 'string') return listLine(painted, index);
|
|
137
|
+
const line = listLine(painted.row, index);
|
|
138
|
+
if (painted.hit === false) line.hit = false;
|
|
139
|
+
if (painted.spans) line.spans = painted.spans;
|
|
140
|
+
return line;
|
|
120
141
|
};
|
|
121
142
|
|
|
122
143
|
const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
|
|
@@ -124,7 +145,7 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
|
|
|
124
145
|
const entry = items[index];
|
|
125
146
|
if (!entry) return null;
|
|
126
147
|
const selected = !extra && index === cursor;
|
|
127
|
-
return
|
|
148
|
+
return lineFromPaint(paintRow(entry, selected, index), index);
|
|
128
149
|
};
|
|
129
150
|
if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
|
|
130
151
|
const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
|
|
@@ -134,7 +155,11 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
|
|
|
134
155
|
return paintListWindow(items.length, at, pane, replaced);
|
|
135
156
|
}
|
|
136
157
|
const at = insertedAt(extra, items.length);
|
|
137
|
-
const inserted = (i) =>
|
|
158
|
+
const inserted = (i) => {
|
|
159
|
+
if (i === at) return edit;
|
|
160
|
+
const index = i < at ? i : i - 1;
|
|
161
|
+
return lineAt(index);
|
|
162
|
+
};
|
|
138
163
|
return paintListWindow(items.length + 1, at, pane, inserted);
|
|
139
164
|
};
|
|
140
165
|
|
|
@@ -163,7 +188,7 @@ const sliceVisible = (text, skip, width) => {
|
|
|
163
188
|
|
|
164
189
|
const fieldWindow = (text, cursor, scroll, width) => {
|
|
165
190
|
if (width <= 0) return { text: '', col: 0, scroll: 0 };
|
|
166
|
-
const at =
|
|
191
|
+
const at = clamp(cursor ?? 0, 0, text.length);
|
|
167
192
|
const col = visibleWidth(text.slice(0, at));
|
|
168
193
|
let next = Math.max(0, scroll ?? 0);
|
|
169
194
|
if (col < next) next = col;
|
|
@@ -213,10 +238,67 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
|
|
|
213
238
|
return text;
|
|
214
239
|
};
|
|
215
240
|
|
|
241
|
+
const paintPathText = (text, fg, bg, color, bold, spans) => {
|
|
242
|
+
if (!spans.length) return paint(text, fg, bg, color, bold);
|
|
243
|
+
let out = '';
|
|
244
|
+
let at = 0;
|
|
245
|
+
for (const span of spans) {
|
|
246
|
+
if (span.start > at) {
|
|
247
|
+
out += paint(text.slice(at, span.start), fg, bg, color, bold);
|
|
248
|
+
}
|
|
249
|
+
const on = span.on === true;
|
|
250
|
+
const markFg = on ? THEME.searchOnFg : THEME.searchFg;
|
|
251
|
+
const markBg = on ? THEME.searchOnBg : THEME.searchBg;
|
|
252
|
+
const slice = text.slice(span.start, span.end);
|
|
253
|
+
out += paint(slice, markFg, markBg, color, true);
|
|
254
|
+
at = span.end;
|
|
255
|
+
}
|
|
256
|
+
if (at < text.length) out += paint(text.slice(at), fg, bg, color, bold);
|
|
257
|
+
return out;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const scrollThumb = (count, inner, start) => {
|
|
261
|
+
if (count <= inner || inner < 1) return null;
|
|
262
|
+
let size = Math.max(1, Math.round((inner * inner) / count));
|
|
263
|
+
if (inner > 1) size = Math.min(size, inner - 1);
|
|
264
|
+
const travel = inner - size;
|
|
265
|
+
const maxStart = count - inner;
|
|
266
|
+
const at = maxStart <= 0 ? 0 : Math.round((start * travel) / maxStart);
|
|
267
|
+
return { size, travel, maxStart, at };
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const scrollMark = (index, count, inner, start) => {
|
|
271
|
+
const thumb = scrollThumb(count, inner, start);
|
|
272
|
+
if (!thumb) return '';
|
|
273
|
+
const end = thumb.at + thumb.size;
|
|
274
|
+
if (index >= thumb.at && index < end) return 'thumb';
|
|
275
|
+
return 'track';
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const scrollAtThumb = (top, thumb) => {
|
|
279
|
+
if (!thumb || thumb.travel <= 0) return 0;
|
|
280
|
+
const at = clamp(top, 0, thumb.travel);
|
|
281
|
+
return Math.round((at * thumb.maxStart) / thumb.travel);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const placeHits = (hits, origin, width, side) => {
|
|
285
|
+
const placed = [];
|
|
286
|
+
for (const hit of hits) {
|
|
287
|
+
const x0 = (hit.x0 ?? 0) + origin;
|
|
288
|
+
const x1 = (hit.x1 ?? width) + origin;
|
|
289
|
+
placed.push({ ...hit, x0, x1, side });
|
|
290
|
+
}
|
|
291
|
+
return placed;
|
|
292
|
+
};
|
|
293
|
+
|
|
216
294
|
module.exports = {
|
|
295
|
+
scrollThumb,
|
|
296
|
+
scrollMark,
|
|
297
|
+
scrollAtThumb,
|
|
298
|
+
placeHits,
|
|
217
299
|
GUTTER_W,
|
|
218
|
-
FILE_MARK,
|
|
219
300
|
MARK_W,
|
|
301
|
+
LIST_PAD,
|
|
220
302
|
fill,
|
|
221
303
|
markPrefix,
|
|
222
304
|
isListPane,
|
|
@@ -227,8 +309,11 @@ module.exports = {
|
|
|
227
309
|
paintSplitRule,
|
|
228
310
|
paintBodyFill,
|
|
229
311
|
paneResult,
|
|
312
|
+
lazyRows,
|
|
313
|
+
bodyRow,
|
|
230
314
|
editSpan,
|
|
231
315
|
listLine,
|
|
316
|
+
listWindowStart,
|
|
232
317
|
paintListWindow,
|
|
233
318
|
paintCursorList,
|
|
234
319
|
sliceVisible,
|
|
@@ -237,4 +322,5 @@ module.exports = {
|
|
|
237
322
|
padColumns,
|
|
238
323
|
joinColumns,
|
|
239
324
|
paintColumns,
|
|
325
|
+
paintPathText,
|
|
240
326
|
};
|
package/lib/render/render.js
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const { formatBusyStatus, headerText, layoutButtons } = chrome;
|
|
10
|
-
const { todoBodyWidth, noteInnerWidth } = notes;
|
|
11
|
-
const { paintDiffLine, codeInnerWidth } = diff;
|
|
12
|
-
const { presentCursor, presentRows } = present;
|
|
13
|
-
const { renderFrame } = frame;
|
|
3
|
+
const { formatBusyStatus } = require('./status.js');
|
|
4
|
+
const { headerText } = require('./header.js');
|
|
5
|
+
const { layoutButtons } = require('./footer.js');
|
|
6
|
+
const { todoBodyWidth, noteInnerWidth } = require('./notes.js');
|
|
7
|
+
const { paintDiffLine, codeInnerWidth, activeDigits } = require('./diff.js');
|
|
8
|
+
const { renderFrame, presentCursor, presentRows } = require('./frame.js');
|
|
14
9
|
|
|
15
10
|
module.exports = {
|
|
16
11
|
formatBusyStatus,
|
|
@@ -20,6 +15,7 @@ module.exports = {
|
|
|
20
15
|
noteInnerWidth,
|
|
21
16
|
paintDiffLine,
|
|
22
17
|
codeInnerWidth,
|
|
18
|
+
activeDigits,
|
|
23
19
|
renderFrame,
|
|
24
20
|
presentCursor,
|
|
25
21
|
presentRows,
|
package/lib/render/repos.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
5
|
-
const primitives = require('./primitives.js');
|
|
6
|
-
const { paintBodyFill, paneResult } = primitives;
|
|
3
|
+
const { THEME } = require('../term/ansi.js');
|
|
4
|
+
const { paintBodyFill, paneResult } = require('./primitives.js');
|
|
7
5
|
const tiles = require('./tiles.js');
|
|
6
|
+
const { RUN_ICON } = tiles;
|
|
8
7
|
const { seg, spinner, equalGrid, pageWindow, placeEqualTiles } = tiles;
|
|
9
8
|
const { paintTile, composeBands, tileHits, pageFill } = tiles;
|
|
10
|
-
const { segmentsWidth, clipSegments } = tiles;
|
|
11
|
-
const
|
|
12
|
-
const {
|
|
9
|
+
const { segmentsWidth, clipSegments, blinkTone } = tiles;
|
|
10
|
+
const { size } = require('../common/format.js');
|
|
11
|
+
const { isHot } = require('./table.js');
|
|
13
12
|
|
|
14
13
|
const REPO_MIN_H = 6;
|
|
15
14
|
const MARGIN_MIN_H = 12;
|
|
@@ -19,29 +18,19 @@ const DIM_BACKS = {
|
|
|
19
18
|
tile: THEME.dashDimTileBg,
|
|
20
19
|
row: THEME.dashDimRowBg,
|
|
21
20
|
};
|
|
22
|
-
const RUN_ICON = {
|
|
23
|
-
passed: ['✔', 'add'],
|
|
24
|
-
failed: ['✖', 'error'],
|
|
25
|
-
aborted: ['⊘', 'muted'],
|
|
26
|
-
};
|
|
27
|
-
|
|
28
21
|
const extName = (key) => {
|
|
29
22
|
const text = `${key ?? ''}`;
|
|
30
23
|
if (!text || text === '(none)') return 'none';
|
|
31
24
|
return text.startsWith('.') ? text.slice(1) : text;
|
|
32
25
|
};
|
|
33
26
|
|
|
34
|
-
const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
|
|
35
|
-
|
|
36
27
|
const spin = (frame) => seg(spinner(frame), 'warn', true);
|
|
37
28
|
|
|
38
|
-
const rowPending = (row) =>
|
|
39
|
-
!row.filesReady ||
|
|
40
|
-
!row.
|
|
41
|
-
!row.
|
|
42
|
-
|
|
43
|
-
!row.runsReady ||
|
|
44
|
-
!row.npmReady;
|
|
29
|
+
const rowPending = (row) => {
|
|
30
|
+
if (!row.filesReady || !row.commitsReady) return true;
|
|
31
|
+
if (!row.diffReady || !row.tasksReady) return true;
|
|
32
|
+
return !row.runsReady || !row.npmReady;
|
|
33
|
+
};
|
|
45
34
|
|
|
46
35
|
const fitAside = (left, right, width) => {
|
|
47
36
|
const rightW = segmentsWidth(right);
|
|
@@ -214,13 +203,12 @@ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
|
|
|
214
203
|
const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
|
|
215
204
|
const shown = model.tiles.slice(page.start, page.end);
|
|
216
205
|
const bands = placeEqualTiles(shown, grid);
|
|
217
|
-
model.cols = grid.cols;
|
|
218
206
|
const painted = paintRepos(model, bands, color);
|
|
219
207
|
const rows = composeBands(bands, painted, width, height, color);
|
|
220
208
|
const pad = new Array(margin).fill(pageFill(width, color));
|
|
221
209
|
const body = [...pad, ...rows, ...pad];
|
|
222
210
|
const fileHits = tileHits(bands, headerLines + margin);
|
|
223
|
-
return paneResult({ body, fileHits });
|
|
211
|
+
return { ...paneResult({ body, fileHits }), gridCols: grid.cols };
|
|
224
212
|
};
|
|
225
213
|
|
|
226
|
-
module.exports = {
|
|
214
|
+
module.exports = { paintBodyRepos };
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../term/ansi.js');
|
|
4
|
+
const actions = require('../input/actions.js');
|
|
5
|
+
const files = require('../common/files.js');
|
|
6
|
+
const primitives = require('./primitives.js');
|
|
7
|
+
const { fill, branchLabel } = primitives;
|
|
8
|
+
const { npmStatus } = require('./dashboard/blocks.js');
|
|
9
|
+
const { menuBoxWidth, dropBox, menuFaces } = require('./menu.js');
|
|
10
|
+
const field = require('./field.js');
|
|
11
|
+
const { paintPlanFace, paintImportStatus, paintFindStatus } = field;
|
|
12
|
+
const { fileTotals } = files;
|
|
13
|
+
const { CONFIRM, confirmPrefix, promptFromChoices } = actions;
|
|
14
|
+
const { THEME, paint, visibleWidth } = ansi;
|
|
15
|
+
const { truncateVisible } = ansi;
|
|
16
|
+
const { paintMarkedWord, QUIT_MARK } = require('./footer.js');
|
|
17
|
+
|
|
18
|
+
const WAVE = '·•●•·';
|
|
19
|
+
|
|
20
|
+
const BUSY_BAR = Array.from(WAVE, (_, frame) => {
|
|
21
|
+
const start = (WAVE.length - frame) % WAVE.length;
|
|
22
|
+
return WAVE.slice(start) + WAVE.slice(0, start);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
busy: BUSY_PROGRESS,
|
|
27
|
+
info: INFO_STATUS,
|
|
28
|
+
infoPrefix: INFO_PREFIX,
|
|
29
|
+
} = require('./status.json');
|
|
30
|
+
|
|
31
|
+
const formatBusyStatus = (status, frame = 0) => {
|
|
32
|
+
const kind = `${status ?? ''}`;
|
|
33
|
+
if (!BUSY_PROGRESS.includes(kind)) return kind;
|
|
34
|
+
const i = Math.abs(frame) % BUSY_BAR.length;
|
|
35
|
+
return `${kind} ${BUSY_BAR[i]}`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const STATUS_MARK = /(\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
|
|
39
|
+
|
|
40
|
+
const OLD_LOGS = /old logs \S+/;
|
|
41
|
+
|
|
42
|
+
const isInfoStatus = (status) => {
|
|
43
|
+
const busy = BUSY_PROGRESS.includes(status);
|
|
44
|
+
const known = INFO_STATUS.includes(status);
|
|
45
|
+
const prefixed = INFO_PREFIX.some((prefix) => status.startsWith(prefix));
|
|
46
|
+
return busy || known || prefixed;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const joinStatus = (parts) => parts.filter(Boolean).join(' ');
|
|
50
|
+
|
|
51
|
+
const countsKind = (sourceKind, revShort) => {
|
|
52
|
+
const remote = sourceKind === 'pr' || sourceKind === 'mr';
|
|
53
|
+
if (remote || sourceKind === 'issue') return sourceKind;
|
|
54
|
+
return revShort ? 'commit' : 'worktree';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isWorktree = (view) =>
|
|
58
|
+
countsKind(view.sourceKind, view.revShort) === 'worktree';
|
|
59
|
+
|
|
60
|
+
const notesLine = (counts) => {
|
|
61
|
+
const done = counts.tasksDone ?? 0;
|
|
62
|
+
const total = counts.tasks ?? 0;
|
|
63
|
+
return `tasks ${done}/${total}`;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const PLAN_LEAD = 'Plan file: ';
|
|
67
|
+
|
|
68
|
+
const closedPlan = (view) => `${view.planName ?? ''}`;
|
|
69
|
+
|
|
70
|
+
const planLabel = (view) => {
|
|
71
|
+
const closed = closedPlan(view);
|
|
72
|
+
if (!closed) return '';
|
|
73
|
+
const query = view.planOpen ? `${view.planQuery ?? ''}` : '';
|
|
74
|
+
if (!query) return closed;
|
|
75
|
+
const room = Math.max(visibleWidth(closed), visibleWidth(query));
|
|
76
|
+
const shown = truncateVisible(query, room);
|
|
77
|
+
const pad = Math.max(0, room - visibleWidth(shown));
|
|
78
|
+
return `${shown}${' '.repeat(pad)}`;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const reposLead = (view) => {
|
|
82
|
+
const tiles = view.repos && view.repos.tiles ? view.repos.tiles : [];
|
|
83
|
+
const count = tiles.length;
|
|
84
|
+
if (!count) return 'repos';
|
|
85
|
+
const at = (view.repos.cursor ?? 0) + 1;
|
|
86
|
+
return `${at}/${count}`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const agentsLead = (view) => {
|
|
90
|
+
if (view.agentView) return view.agentStatus || 'log';
|
|
91
|
+
const list = view.agents ?? [];
|
|
92
|
+
const found = list.filter((row) => row.bin).length;
|
|
93
|
+
return `${found}/${list.length}`;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const statusLead = (view) => {
|
|
97
|
+
if (view.pane === 'repos') return reposLead(view);
|
|
98
|
+
if (view.pane === 'agents') return agentsLead(view);
|
|
99
|
+
const counts = view.counts ?? {};
|
|
100
|
+
const kind = countsKind(view.sourceKind, view.revShort);
|
|
101
|
+
if (kind === 'worktree') return branchLabel(view.branch);
|
|
102
|
+
if (kind === 'commit') {
|
|
103
|
+
return `commit ${view.revShort} ${counts.commit ?? 0}`;
|
|
104
|
+
}
|
|
105
|
+
const label = view.sourceLabel || kind;
|
|
106
|
+
return `${kind} ${label} ${counts.pr ?? 0}`;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const STATUS_QUIET = ['npm', 'packages', 'branches', 'agents'];
|
|
110
|
+
|
|
111
|
+
const planStatus = (view) => {
|
|
112
|
+
const combo = planLabel(view);
|
|
113
|
+
if (!combo) return '';
|
|
114
|
+
return `${PLAN_LEAD}${combo}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const statusLeft = (view) => {
|
|
118
|
+
if (view.pane === 'repos') return ` ${statusLead(view)}`;
|
|
119
|
+
if (view.pane === 'tasks') {
|
|
120
|
+
const lead = joinStatus([statusLead(view), planStatus(view)]);
|
|
121
|
+
return ` ${lead}`;
|
|
122
|
+
}
|
|
123
|
+
const quiet = STATUS_QUIET.includes(view.pane);
|
|
124
|
+
const notes = quiet ? '' : notesLine(view.counts ?? {});
|
|
125
|
+
return ` ${joinStatus([statusLead(view), notes])}`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const planAt = (view, text) => {
|
|
129
|
+
const combo = planLabel(view);
|
|
130
|
+
if (!combo) return -1;
|
|
131
|
+
const lead = statusLead(view);
|
|
132
|
+
const head = lead ? ` ${lead} ${PLAN_LEAD}` : ` ${PLAN_LEAD}`;
|
|
133
|
+
const line = `${text ?? ''}`;
|
|
134
|
+
if (!line.startsWith(head)) return -1;
|
|
135
|
+
const rest = line.slice(head.length);
|
|
136
|
+
if (rest.startsWith(combo) || combo.startsWith(rest)) return head.length;
|
|
137
|
+
return -1;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const planFace = (view, text) => {
|
|
141
|
+
const at = planAt(view, text);
|
|
142
|
+
if (at < 0) return null;
|
|
143
|
+
const combo = planLabel(view);
|
|
144
|
+
const rest = text.slice(at);
|
|
145
|
+
const face = rest.startsWith(combo) ? combo : rest;
|
|
146
|
+
return { at, face, x: visibleWidth(text.slice(0, at)) };
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const statusDelta = (total) => {
|
|
150
|
+
const stagedAdd = total.stagedAdded ?? 0;
|
|
151
|
+
const unstagedAdd = total.unstagedAdded ?? 0;
|
|
152
|
+
const stagedDel = total.stagedRemoved ?? 0;
|
|
153
|
+
const unstagedDel = total.unstagedRemoved ?? 0;
|
|
154
|
+
const hasDelta =
|
|
155
|
+
stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
|
|
156
|
+
if (!hasDelta && !total.removed) return '';
|
|
157
|
+
const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
|
|
158
|
+
const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
|
|
159
|
+
return `${plus} ${minus}`;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const DIFF_QUIET = ['npm', 'packages', 'tasks', 'branches', 'agents'];
|
|
163
|
+
|
|
164
|
+
const statusStats = (view) => {
|
|
165
|
+
if (view.pane === 'tasks') return notesLine(view.counts ?? {});
|
|
166
|
+
const counts = view.counts ?? {};
|
|
167
|
+
const staged = counts.staged ?? 0;
|
|
168
|
+
const total = fileTotals(view.files ?? []);
|
|
169
|
+
const { remaining, staged: listed, added, removed } = total;
|
|
170
|
+
const hasGit = remaining || listed || added || removed;
|
|
171
|
+
const unstaged = counts.unstaged ?? 0;
|
|
172
|
+
const n = hasGit ? listed : staged;
|
|
173
|
+
const m = hasGit ? remaining : staged + unstaged;
|
|
174
|
+
const delta = DIFF_QUIET.includes(view.pane) ? '' : statusDelta(total);
|
|
175
|
+
const ratio = STATUS_QUIET.includes(view.pane) ? '' : `${n}/${m}`;
|
|
176
|
+
const packages = view.pane === 'packages' ? npmStatus(view.packages) : '';
|
|
177
|
+
return joinStatus([view.npmLogs, delta, ratio, packages]);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const statusLayout = (view, status, width) => {
|
|
181
|
+
const msg = formatBusyStatus(status, view.progressFrame ?? 0);
|
|
182
|
+
const isError = msg && !isInfoStatus(status);
|
|
183
|
+
const msgFg = isError ? THEME.errorFg : THEME.chromeFg;
|
|
184
|
+
const stats = statusStats(view);
|
|
185
|
+
const right = stats ? `${stats} ` : '';
|
|
186
|
+
const rightW = visibleWidth(right);
|
|
187
|
+
const msgW = visibleWidth(msg);
|
|
188
|
+
const side = msg ? 1 : 0;
|
|
189
|
+
const room = Math.max(0, width - rightW - msgW - side * 2);
|
|
190
|
+
const clipped = truncateVisible(statusLeft(view), room);
|
|
191
|
+
const leftW = visibleWidth(clipped);
|
|
192
|
+
const extra = Math.max(0, width - leftW - rightW - msgW);
|
|
193
|
+
const padLeft = msg ? Math.floor(extra / 2) : extra;
|
|
194
|
+
const padRight = msg ? extra - padLeft : 0;
|
|
195
|
+
return { clipped, msg, msgFg, right, padLeft, padRight };
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const choiceHits = (choices, prefix, y) => {
|
|
199
|
+
let x = visibleWidth(prefix);
|
|
200
|
+
return choices.map((choice) => {
|
|
201
|
+
const w = visibleWidth(`${choice.lead}${choice.label}`);
|
|
202
|
+
const hit = { id: choice.letter, y, x0: x, x1: x + w };
|
|
203
|
+
x += w;
|
|
204
|
+
return hit;
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const paintChoicePrompt = (choices, width, color, prefix) => {
|
|
209
|
+
const bgRgb = THEME.chromeBg;
|
|
210
|
+
const body = `${prefix}${promptFromChoices(choices)}`;
|
|
211
|
+
const pad = Math.max(0, width - visibleWidth(body));
|
|
212
|
+
if (!color) return body + ' '.repeat(pad);
|
|
213
|
+
let out = paint(prefix, THEME.mutedFg, bgRgb, color);
|
|
214
|
+
for (const choice of choices) {
|
|
215
|
+
const label = `${choice.lead}${choice.label}`;
|
|
216
|
+
out += paintMarkedWord(label, choice.letter, color, QUIT_MARK);
|
|
217
|
+
}
|
|
218
|
+
return out + fill(pad, THEME.mutedFg, bgRgb, color);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const paintStatusLeft = (text, color) => {
|
|
222
|
+
if (!color) return text;
|
|
223
|
+
const bgRgb = THEME.chromeBg;
|
|
224
|
+
const found = text.match(OLD_LOGS);
|
|
225
|
+
if (found) {
|
|
226
|
+
const token = found[0];
|
|
227
|
+
const before = text.slice(0, found.index);
|
|
228
|
+
const after = text.slice(found.index + token.length);
|
|
229
|
+
return (
|
|
230
|
+
paintStatusLeft(before, color) +
|
|
231
|
+
paint(token, THEME.shaFg, bgRgb, color) +
|
|
232
|
+
paintStatusLeft(after, color)
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
let out = '';
|
|
236
|
+
let last = 0;
|
|
237
|
+
for (const match of text.matchAll(STATUS_MARK)) {
|
|
238
|
+
const token = match[0];
|
|
239
|
+
const tokenFg = token.startsWith('+') ? THEME.addLineFg : THEME.delLineFg;
|
|
240
|
+
out += paint(text.slice(last, match.index), THEME.mutedFg, bgRgb, color);
|
|
241
|
+
out += paint(token, tokenFg, bgRgb, color);
|
|
242
|
+
last = match.index + token.length;
|
|
243
|
+
}
|
|
244
|
+
return out + paint(text.slice(last), THEME.mutedFg, bgRgb, color);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const paintBranchHead = (text, branch, color) => {
|
|
248
|
+
const name = branchLabel(branch);
|
|
249
|
+
if (!name || !text.startsWith(' ')) return null;
|
|
250
|
+
const shown = text.startsWith(` ${name}`) ? name : text.slice(1);
|
|
251
|
+
if (!shown || !name.startsWith(shown)) return null;
|
|
252
|
+
const rest = text.slice(1 + shown.length);
|
|
253
|
+
const bgRgb = THEME.chromeBg;
|
|
254
|
+
return (
|
|
255
|
+
paint(' ', THEME.mutedFg, bgRgb, color) +
|
|
256
|
+
paint(shown, THEME.buttonHotFg, bgRgb, color, true) +
|
|
257
|
+
paintStatusLeft(rest, color)
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const paintTasksLeft = (view, text, color) => {
|
|
262
|
+
const place = planFace(view, text);
|
|
263
|
+
if (!place) {
|
|
264
|
+
const head = paintBranchHead(text, view.branch, color);
|
|
265
|
+
return head ?? paintStatusLeft(text, color);
|
|
266
|
+
}
|
|
267
|
+
const before = text.slice(0, place.at);
|
|
268
|
+
const after = text.slice(place.at + place.face.length);
|
|
269
|
+
const searching = Boolean(view.planOpen && view.planQuery);
|
|
270
|
+
const comboFg = searching ? THEME.searchFg : THEME.shaFg;
|
|
271
|
+
const comboBg = searching ? THEME.searchBg : THEME.chromeBg;
|
|
272
|
+
const branch = paintBranchHead(before, view.branch, color);
|
|
273
|
+
const head = branch ?? paintStatusLeft(before, color);
|
|
274
|
+
const face = paintPlanFace(view, place.face, comboFg, comboBg, color);
|
|
275
|
+
return head + face + paintStatusLeft(after, color);
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const statusHead = (view, clipped, color) => {
|
|
279
|
+
if (view.pane === 'tasks') return paintTasksLeft(view, clipped, color);
|
|
280
|
+
if (isWorktree(view)) return paintBranchHead(clipped, view.branch, color);
|
|
281
|
+
return null;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const paintStatusLine = (view, status, width, color) => {
|
|
285
|
+
if (view.import) return paintImportStatus(view, width, color);
|
|
286
|
+
if (view.find) return paintFindStatus(view, width, color);
|
|
287
|
+
const confirm = CONFIRM[view.mode];
|
|
288
|
+
if (confirm) {
|
|
289
|
+
const prefix = confirmPrefix(confirm, view);
|
|
290
|
+
return paintChoicePrompt(confirm.choices, width, color, prefix);
|
|
291
|
+
}
|
|
292
|
+
const layout = statusLayout(view, status, width);
|
|
293
|
+
const leftGap = ' '.repeat(layout.padLeft);
|
|
294
|
+
const rightGap = ' '.repeat(layout.padRight);
|
|
295
|
+
if (!color) {
|
|
296
|
+
return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
|
|
297
|
+
}
|
|
298
|
+
const bgRgb = THEME.chromeBg;
|
|
299
|
+
const head = statusHead(view, layout.clipped, color);
|
|
300
|
+
const left = head ?? paintStatusLeft(layout.clipped, color);
|
|
301
|
+
return (
|
|
302
|
+
left +
|
|
303
|
+
paint(leftGap, THEME.mutedFg, bgRgb, color) +
|
|
304
|
+
paint(layout.msg, layout.msgFg, bgRgb, color) +
|
|
305
|
+
paint(rightGap, THEME.mutedFg, bgRgb, color) +
|
|
306
|
+
paintStatusLeft(layout.right, color)
|
|
307
|
+
);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const statusChoiceHits = (view, y) => {
|
|
311
|
+
const spec = CONFIRM[view.mode];
|
|
312
|
+
if (!spec) return [];
|
|
313
|
+
return choiceHits(spec.choices, confirmPrefix(spec, view), y);
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const statusPlanHit = (view, width, y) => {
|
|
317
|
+
if (view.pane !== 'tasks' || view.find || view.import) return null;
|
|
318
|
+
if (CONFIRM[view.mode]) return null;
|
|
319
|
+
const layout = statusLayout(view, view.status ?? '', width);
|
|
320
|
+
const place = planFace(view, layout.clipped);
|
|
321
|
+
if (!place) return null;
|
|
322
|
+
const x0 = place.x;
|
|
323
|
+
return { id: 'plan', y, x0, x1: x0 + visibleWidth(place.face) };
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const planMenu = (view, width, color, statusY) => {
|
|
327
|
+
if (view.pane !== 'tasks' || !view.planOpen || statusY < 3) return null;
|
|
328
|
+
const names = view.planNames ?? [];
|
|
329
|
+
const layout = statusLayout(view, view.status ?? '', width);
|
|
330
|
+
const place = planFace(view, layout.clipped);
|
|
331
|
+
if (!place) return null;
|
|
332
|
+
const x = Math.max(0, place.x - 1);
|
|
333
|
+
const room = Math.max(1, statusY - 2);
|
|
334
|
+
const box = dropBox(names, view.planCursor, view.planScroll, room);
|
|
335
|
+
const boxW = menuBoxWidth(names, visibleWidth(place.face), width - x);
|
|
336
|
+
const faces = menuFaces(names, box, boxW, color);
|
|
337
|
+
const lines = [];
|
|
338
|
+
const hits = [];
|
|
339
|
+
for (let i = 0; i < faces.length; i++) {
|
|
340
|
+
const y = statusY - box.inner + i;
|
|
341
|
+
lines.push({ y, text: faces[i].face });
|
|
342
|
+
if (names[faces[i].index] === undefined) continue;
|
|
343
|
+
const id = 'plan-item';
|
|
344
|
+
const x0 = x;
|
|
345
|
+
const x1 = x + boxW;
|
|
346
|
+
const cursor = faces[i].index;
|
|
347
|
+
const hit = { id, y, x0, x1, cursor };
|
|
348
|
+
hits.push(hit);
|
|
349
|
+
}
|
|
350
|
+
return { x, lines, hits, scroll: box.start };
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const statusBranchHit = (view, width, y) => {
|
|
354
|
+
if (view.find || view.import || CONFIRM[view.mode]) return null;
|
|
355
|
+
const name = branchLabel(view.branch);
|
|
356
|
+
if (!name || !isWorktree(view)) return null;
|
|
357
|
+
const layout = statusLayout(view, view.status ?? '', width);
|
|
358
|
+
const at = layout.clipped.indexOf(name);
|
|
359
|
+
if (at < 0) return null;
|
|
360
|
+
return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
module.exports = {
|
|
364
|
+
formatBusyStatus,
|
|
365
|
+
paintStatusLine,
|
|
366
|
+
statusChoiceHits,
|
|
367
|
+
statusBranchHit,
|
|
368
|
+
statusPlanHit,
|
|
369
|
+
planMenu,
|
|
370
|
+
};
|