reslop 0.3.1 → 0.3.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/CHANGELOG.md +16 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +163 -436
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +50 -24
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/render/frame.js
CHANGED
|
@@ -18,11 +18,12 @@ const dashboard = require('./dashboard/dashboard.js');
|
|
|
18
18
|
const packages = require('./packages.js');
|
|
19
19
|
const agents = require('./agents.js');
|
|
20
20
|
const repos = require('./repos.js');
|
|
21
|
+
const { contextMenu } = require('./menu.js');
|
|
21
22
|
|
|
22
23
|
const { isListPane, isTaskView, paintBodyFill, paneResult } = primitives;
|
|
23
24
|
const { bodyRow } = primitives;
|
|
24
25
|
const { paintStatusLine, statusBranchHit, statusChoiceHits } = status;
|
|
25
|
-
const { statusPlanHit, planMenu } = status;
|
|
26
|
+
const { statusPlanHit, statusEditHits, planMenu } = status;
|
|
26
27
|
|
|
27
28
|
const PANE_BODY = {
|
|
28
29
|
dashboard: dashboard.paintBodyDashboard,
|
|
@@ -250,8 +251,20 @@ const renderFrame = (view, options = {}) => {
|
|
|
250
251
|
}
|
|
251
252
|
statusHits.push(...menu.hits);
|
|
252
253
|
}
|
|
253
|
-
const editHits = [
|
|
254
|
+
const editHits = [
|
|
255
|
+
...windowed.editHits,
|
|
256
|
+
...hits.editHits,
|
|
257
|
+
...statusEditHits(view, width, statusY),
|
|
258
|
+
];
|
|
254
259
|
const shown = markEditorSelection(rows, view, editHits, color);
|
|
260
|
+
const popup = contextMenu(view.contextMenu, width, rows.length, color);
|
|
261
|
+
if (popup) {
|
|
262
|
+
for (const line of popup.lines) {
|
|
263
|
+
const index = line.y - 1;
|
|
264
|
+
if (index <= 0 || index >= rows.length - 1) continue;
|
|
265
|
+
shown[index] = stampVisible(shown[index], popup.x, line.text);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
255
268
|
const text = shown.map((row) => (color ? row + RESET : row)).join('\n');
|
|
256
269
|
return {
|
|
257
270
|
text,
|
|
@@ -262,6 +275,7 @@ const renderFrame = (view, options = {}) => {
|
|
|
262
275
|
templateHits: hits.templateHits,
|
|
263
276
|
editHits,
|
|
264
277
|
statusHits,
|
|
278
|
+
contextHits: popup ? popup.hits : [],
|
|
265
279
|
footerY: rows.length,
|
|
266
280
|
height: rows.length,
|
|
267
281
|
scroll: windowed.scroll,
|
package/lib/render/menu.js
CHANGED
|
@@ -4,6 +4,11 @@ const { clamp } = require('../common/utilities.js');
|
|
|
4
4
|
const ansi = require('../term/ansi.js');
|
|
5
5
|
const { listWindowStart, scrollMark } = require('./primitives.js');
|
|
6
6
|
const { THEME, paint, visibleWidth, trimVisible } = ansi;
|
|
7
|
+
const {
|
|
8
|
+
actionLetter,
|
|
9
|
+
actionCaption,
|
|
10
|
+
buttonWord,
|
|
11
|
+
} = require('../input/actions.js');
|
|
7
12
|
|
|
8
13
|
const menuBoxWidth = (options, columnW, room) => {
|
|
9
14
|
let widest = Math.max(columnW, 4);
|
|
@@ -13,7 +18,24 @@ const menuBoxWidth = (options, columnW, room) => {
|
|
|
13
18
|
return clamp(widest + 2, 4, room);
|
|
14
19
|
};
|
|
15
20
|
|
|
16
|
-
const
|
|
21
|
+
const paintQuery = (text, query, fgRgb, bgRgb, color, bold) => {
|
|
22
|
+
const needle = `${query ?? ''}`.trim().toLowerCase();
|
|
23
|
+
if (!color || !needle) return paint(text, fgRgb, bgRgb, color, bold);
|
|
24
|
+
const hay = text.toLowerCase();
|
|
25
|
+
let out = '';
|
|
26
|
+
let from = 0;
|
|
27
|
+
let at = hay.indexOf(needle, from);
|
|
28
|
+
while (at >= 0) {
|
|
29
|
+
const end = at + needle.length;
|
|
30
|
+
out += paint(text.slice(from, at), fgRgb, bgRgb, color, bold);
|
|
31
|
+
out += paint(text.slice(at, end), THEME.warnFg, bgRgb, color, true);
|
|
32
|
+
from = end;
|
|
33
|
+
at = hay.indexOf(needle, from);
|
|
34
|
+
}
|
|
35
|
+
return out + paint(text.slice(from), fgRgb, bgRgb, color, bold);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const menuItem = (label, boxW, color, selected, mark, disabled, query) => {
|
|
17
39
|
const barW = mark ? 1 : 0;
|
|
18
40
|
const faceW = Math.max(1, boxW - barW);
|
|
19
41
|
const room = Math.max(0, faceW - 1);
|
|
@@ -25,7 +47,7 @@ const menuItem = (label, boxW, color, selected, mark, disabled) => {
|
|
|
25
47
|
const fgRgb = disabled ? THEME.mutedFg : THEME.chromeFg;
|
|
26
48
|
const hotFg = hot ? THEME.buttonHotFg : fgRgb;
|
|
27
49
|
const bgRgb = hot ? THEME.checkBg : THEME.noteBg;
|
|
28
|
-
let row =
|
|
50
|
+
let row = paintQuery(face, query, hotFg, bgRgb, color, hot);
|
|
29
51
|
if (!mark) return row;
|
|
30
52
|
const barBg = mark === 'thumb' ? THEME.taskHeadBg : THEME.buttonBg;
|
|
31
53
|
row += paint(' ', fgRgb, barBg, color);
|
|
@@ -40,7 +62,7 @@ const dropBox = (options, cursor, scroll, room) => {
|
|
|
40
62
|
return { focus, inner, span: inner, start, count };
|
|
41
63
|
};
|
|
42
64
|
|
|
43
|
-
const menuFaces = (options, box, boxW, color, off = new Set()) => {
|
|
65
|
+
const menuFaces = (options, box, boxW, color, query = '', off = new Set()) => {
|
|
44
66
|
const faces = [];
|
|
45
67
|
for (let i = 0; i < box.inner; i++) {
|
|
46
68
|
const index = box.start + i;
|
|
@@ -50,11 +72,86 @@ const menuFaces = (options, box, boxW, color, off = new Set()) => {
|
|
|
50
72
|
const focused = index === box.focus;
|
|
51
73
|
const face =
|
|
52
74
|
label === undefined
|
|
53
|
-
? menuItem('none', boxW, color, false, mark, false)
|
|
54
|
-
: menuItem(label, boxW, color, focused, mark, disabled);
|
|
75
|
+
? menuItem('none', boxW, color, false, mark, false, '')
|
|
76
|
+
: menuItem(label, boxW, color, focused, mark, disabled, query);
|
|
55
77
|
faces.push({ index, face });
|
|
56
78
|
}
|
|
57
79
|
return faces;
|
|
58
80
|
};
|
|
59
81
|
|
|
60
|
-
|
|
82
|
+
const contextText = (id, keyWidth) => {
|
|
83
|
+
if (!keyWidth) return buttonWord(id);
|
|
84
|
+
const mark = actionLetter(id) ?? '';
|
|
85
|
+
const caption = actionCaption(id);
|
|
86
|
+
const key = mark.padEnd(keyWidth);
|
|
87
|
+
return `${key} ${caption}`;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const contextLabel = (id, width, color, selected, disabled, keyWidth) => {
|
|
91
|
+
const label = contextText(id, keyWidth);
|
|
92
|
+
const mark = actionLetter(id) ?? '';
|
|
93
|
+
const shown = trimVisible(label, Math.max(1, width - 2));
|
|
94
|
+
const pad = ' '.repeat(Math.max(0, width - 2 - visibleWidth(shown)));
|
|
95
|
+
const face = ` ${shown}${pad} `;
|
|
96
|
+
if (!color || !mark) return face;
|
|
97
|
+
const bgRgb = selected && !disabled ? THEME.popupHotBg : THEME.popupBg;
|
|
98
|
+
const fgRgb = disabled ? THEME.popupDisabledFg : THEME.popupFg;
|
|
99
|
+
if (disabled) return paint(face, fgRgb, bgRgb, color);
|
|
100
|
+
const at = shown.indexOf(mark);
|
|
101
|
+
if (at < 0) return paint(face, fgRgb, bgRgb, color);
|
|
102
|
+
let out = paint(face.slice(0, at + 1), fgRgb, bgRgb, color);
|
|
103
|
+
out += paint(mark, THEME.buttonHotFg, bgRgb, color, true);
|
|
104
|
+
return out + paint(face.slice(at + mark.length + 1), fgRgb, bgRgb, color);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const contextMenu = (model, width, height, color) => {
|
|
108
|
+
if (!model || !model.actions || !model.actions.length) return null;
|
|
109
|
+
const keyWidth = model.editor
|
|
110
|
+
? Math.max(
|
|
111
|
+
...model.actions.map((id) => visibleWidth(actionLetter(id) ?? '')),
|
|
112
|
+
)
|
|
113
|
+
: 0;
|
|
114
|
+
const labels = model.actions.map((id) => contextText(id, keyWidth));
|
|
115
|
+
const widest = labels.reduce(
|
|
116
|
+
(size, label) => Math.max(size, visibleWidth(label)),
|
|
117
|
+
4,
|
|
118
|
+
);
|
|
119
|
+
const boxW = Math.min(width, widest + 2);
|
|
120
|
+
const count = model.actions.length;
|
|
121
|
+
const inner = Math.min(count, Math.max(1, height - 3));
|
|
122
|
+
const x = clamp(model.x ?? 0, 0, Math.max(0, width - boxW));
|
|
123
|
+
const anchorY = clamp(model.y ?? 2, 2, Math.max(2, height - 1));
|
|
124
|
+
const y = Math.min(anchorY, Math.max(2, height - inner - 1));
|
|
125
|
+
const cursor = clamp(model.cursor ?? 0, 0, count - 1);
|
|
126
|
+
const start = listWindowStart(cursor, 0, inner, count);
|
|
127
|
+
const lines = [];
|
|
128
|
+
const hits = [];
|
|
129
|
+
for (let row = 0; row < inner; row++) {
|
|
130
|
+
const index = start + row;
|
|
131
|
+
const id = model.actions[index];
|
|
132
|
+
const disabled = (model.disabled ?? []).includes(id);
|
|
133
|
+
const face = contextLabel(
|
|
134
|
+
id,
|
|
135
|
+
boxW,
|
|
136
|
+
color,
|
|
137
|
+
index === cursor,
|
|
138
|
+
disabled,
|
|
139
|
+
keyWidth,
|
|
140
|
+
);
|
|
141
|
+
lines.push({
|
|
142
|
+
y: y + row,
|
|
143
|
+
text: face,
|
|
144
|
+
});
|
|
145
|
+
hits.push({
|
|
146
|
+
id,
|
|
147
|
+
cursor: index,
|
|
148
|
+
disabled,
|
|
149
|
+
y: y + row,
|
|
150
|
+
x0: x,
|
|
151
|
+
x1: x + boxW,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { x, lines, hits };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
module.exports = { menuBoxWidth, dropBox, menuFaces, contextMenu };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ansi = require('../term/ansi.js');
|
|
4
|
+
const primitives = require('./primitives.js');
|
|
5
|
+
|
|
6
|
+
const { THEME, paint, visibleWidth, seq, EL } = ansi;
|
|
7
|
+
const { MARK_W, markPrefix, editSpan, fieldWindow } = primitives;
|
|
8
|
+
|
|
9
|
+
// The row where a name is typed: of a new branch or of a new worktree
|
|
10
|
+
const paintNameEdit = (compose, width, color) => {
|
|
11
|
+
const prefix = markPrefix(true);
|
|
12
|
+
const rest = Math.max(1, width - MARK_W);
|
|
13
|
+
const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
|
|
14
|
+
const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
|
|
15
|
+
const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
|
|
16
|
+
const fgRgb = THEME.chromeFg;
|
|
17
|
+
const bgRgb = THEME.buttonBg;
|
|
18
|
+
let row = `${prefix}${win.text}${gap}`;
|
|
19
|
+
if (color) {
|
|
20
|
+
row = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
21
|
+
row += paint(prefix, fgRgb, bgRgb, color);
|
|
22
|
+
row += paint(win.text, THEME.buttonHotFg, bgRgb, color);
|
|
23
|
+
row += paint(gap, fgRgb, bgRgb, color);
|
|
24
|
+
}
|
|
25
|
+
const textX = MARK_W + 1;
|
|
26
|
+
const edit = editSpan(1, textX, textX + rest, 0, text);
|
|
27
|
+
const cursor = { x: textX + win.col, scroll: win.scroll };
|
|
28
|
+
return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = { paintNameEdit };
|
package/lib/render/repos.js
CHANGED
|
@@ -9,6 +9,7 @@ const { paintTile, composeBands, tileHits, pageFill } = tiles;
|
|
|
9
9
|
const { segmentsWidth, clipSegments, blinkTone } = tiles;
|
|
10
10
|
const { size } = require('../common/format.js');
|
|
11
11
|
const { isHot } = require('./table.js');
|
|
12
|
+
const { paintNameEdit } = require('./name-edit.js');
|
|
12
13
|
|
|
13
14
|
const REPO_MIN_H = 6;
|
|
14
15
|
const MARGIN_MIN_H = 12;
|
|
@@ -40,11 +41,24 @@ const fitAside = (left, right, width) => {
|
|
|
40
41
|
return [...clipped, seg(' '.repeat(gap)), ...right];
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
// A linked worktree hangs under its main one, the branch tells them apart
|
|
45
|
+
const branchSegs = (row) => {
|
|
46
|
+
if (!row.branch && !row.head) return [];
|
|
47
|
+
const label = row.branch || `(${row.head})`;
|
|
48
|
+
const segs = [seg(` ⎇ ${label}`, 'sha')];
|
|
49
|
+
if (row.dirty) segs.push(seg('*', 'warn', true));
|
|
50
|
+
if (row.locked) segs.push(seg(' locked', 'muted'));
|
|
51
|
+
return segs;
|
|
52
|
+
};
|
|
53
|
+
|
|
43
54
|
const titleLine = (row, ctx, selected, width) => {
|
|
44
55
|
const hot = isHot(row.activityAt, ctx.now);
|
|
45
56
|
const tone = hot ? blinkTone(ctx.frame) : 'text';
|
|
46
|
-
const left = [
|
|
57
|
+
const left = [];
|
|
58
|
+
if (row.linked) left.push(seg('↳ ', 'muted'));
|
|
59
|
+
left.push(seg(row.name, tone, selected || hot));
|
|
47
60
|
if (hot) left.push(seg(' ●', tone, true));
|
|
61
|
+
left.push(...branchSegs(row));
|
|
48
62
|
const right = [];
|
|
49
63
|
if (rowPending(row)) right.push(spin(ctx.frame), seg(' '));
|
|
50
64
|
const label = row.filesReady ? size(row.bytes) : '…';
|
|
@@ -90,7 +104,7 @@ const resultNote = (run) => {
|
|
|
90
104
|
return [];
|
|
91
105
|
};
|
|
92
106
|
|
|
93
|
-
const
|
|
107
|
+
const runState = (row, ctx) => {
|
|
94
108
|
if (!row.runsReady) return [seg('run: ', 'muted'), spin(ctx.frame)];
|
|
95
109
|
const run = row.run;
|
|
96
110
|
if (!run) return [seg('run: ', 'muted'), seg('idle', 'muted')];
|
|
@@ -113,6 +127,17 @@ const runLine = (row, ctx) => {
|
|
|
113
127
|
];
|
|
114
128
|
};
|
|
115
129
|
|
|
130
|
+
// The agents working in the directory go on the line of the run
|
|
131
|
+
const agentsNote = (row) => {
|
|
132
|
+
const count = row.agents ?? 0;
|
|
133
|
+
return [
|
|
134
|
+
seg(' agents active: ', 'muted'),
|
|
135
|
+
seg(`${count}`, count ? 'warn' : 'muted', count > 0),
|
|
136
|
+
];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const runLine = (row, ctx) => [...runState(row, ctx), ...agentsNote(row)];
|
|
140
|
+
|
|
116
141
|
const npmLine = (row, ctx) => {
|
|
117
142
|
if (!row.npmReady) return [seg('deps: ', 'muted'), spin(ctx.frame)];
|
|
118
143
|
const npm = row.npm;
|
|
@@ -191,14 +216,29 @@ const paintRepos = (model, bands, color) => {
|
|
|
191
216
|
return painted;
|
|
192
217
|
};
|
|
193
218
|
|
|
219
|
+
// One row of tiles above another list, paged sideways
|
|
220
|
+
const paintTreeBand = (model, width, height, color) => {
|
|
221
|
+
const grid = equalGrid(width, height, model.tiles.length, 34, height);
|
|
222
|
+
const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
|
|
223
|
+
const shown = model.tiles.slice(page.start, page.end);
|
|
224
|
+
const bands = placeEqualTiles(shown, grid);
|
|
225
|
+
const painted = paintRepos(model, bands, color);
|
|
226
|
+
const rows = composeBands(bands, painted, width, height, color);
|
|
227
|
+
return { rows, bands, cols: grid.cols };
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// The name of a new worktree is typed in a row above the tiles
|
|
194
231
|
const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
|
|
195
232
|
const model = view.repos;
|
|
196
233
|
if (!model || !model.tiles.length) {
|
|
197
234
|
const blank = paintBodyFill(width, color, 'files');
|
|
198
235
|
return paneResult({ body: new Array(bodyH).fill(blank) });
|
|
199
236
|
}
|
|
200
|
-
const
|
|
201
|
-
const
|
|
237
|
+
const naming = view.compose?.kind === 'worktree';
|
|
238
|
+
const edit = naming ? paintNameEdit(view.compose, width, color) : null;
|
|
239
|
+
const lead = edit ? 1 : 0;
|
|
240
|
+
const margin = bodyH - lead >= MARGIN_MIN_H ? 1 : 0;
|
|
241
|
+
const height = Math.max(1, bodyH - lead - margin * 2);
|
|
202
242
|
const grid = equalGrid(width, height, model.tiles.length, 34, REPO_MIN_H);
|
|
203
243
|
const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
|
|
204
244
|
const shown = model.tiles.slice(page.start, page.end);
|
|
@@ -206,9 +246,13 @@ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
|
|
|
206
246
|
const painted = paintRepos(model, bands, color);
|
|
207
247
|
const rows = composeBands(bands, painted, width, height, color);
|
|
208
248
|
const pad = new Array(margin).fill(pageFill(width, color));
|
|
209
|
-
const
|
|
210
|
-
const
|
|
211
|
-
|
|
249
|
+
const head = edit ? [edit.row] : [];
|
|
250
|
+
const body = [...head, ...pad, ...rows, ...pad];
|
|
251
|
+
const fileHits = tileHits(bands, headerLines + lead + margin);
|
|
252
|
+
const typing = edit ? { cursor: { ...edit.cursor, row: 0 } } : {};
|
|
253
|
+
const editHits = edit ? edit.edits.map((span) => ({ ...span, row: 0 })) : [];
|
|
254
|
+
const result = paneResult({ body, fileHits, editHits, ...typing });
|
|
255
|
+
return { ...result, gridCols: grid.cols };
|
|
212
256
|
};
|
|
213
257
|
|
|
214
|
-
module.exports = { paintBodyRepos };
|
|
258
|
+
module.exports = { paintBodyRepos, paintTreeBand };
|
package/lib/render/status.js
CHANGED
|
@@ -9,6 +9,7 @@ const { npmStatus } = require('./dashboard/blocks.js');
|
|
|
9
9
|
const { menuBoxWidth, dropBox, menuFaces } = require('./menu.js');
|
|
10
10
|
const field = require('./field.js');
|
|
11
11
|
const { paintPlanFace, paintImportStatus, paintFindStatus } = field;
|
|
12
|
+
const { fieldHit } = field;
|
|
12
13
|
const { fileTotals } = files;
|
|
13
14
|
const { CONFIRM, confirmPrefix, promptFromChoices } = actions;
|
|
14
15
|
const { THEME, paint, visibleWidth } = ansi;
|
|
@@ -267,8 +268,8 @@ const paintTasksLeft = (view, text, color) => {
|
|
|
267
268
|
const before = text.slice(0, place.at);
|
|
268
269
|
const after = text.slice(place.at + place.face.length);
|
|
269
270
|
const searching = Boolean(view.planOpen && view.planQuery);
|
|
270
|
-
const comboFg = searching ? THEME.
|
|
271
|
-
const comboBg =
|
|
271
|
+
const comboFg = searching ? THEME.warnFg : THEME.shaFg;
|
|
272
|
+
const comboBg = THEME.chromeBg;
|
|
272
273
|
const branch = paintBranchHead(before, view.branch, color);
|
|
273
274
|
const head = branch ?? paintStatusLeft(before, color);
|
|
274
275
|
const face = paintPlanFace(view, place.face, comboFg, comboBg, color);
|
|
@@ -293,7 +294,10 @@ const paintStatusLine = (view, status, width, color) => {
|
|
|
293
294
|
const leftGap = ' '.repeat(layout.padLeft);
|
|
294
295
|
const rightGap = ' '.repeat(layout.padRight);
|
|
295
296
|
if (!color) {
|
|
296
|
-
|
|
297
|
+
const left = view.planOpen
|
|
298
|
+
? paintTasksLeft(view, layout.clipped, false)
|
|
299
|
+
: layout.clipped;
|
|
300
|
+
return left + leftGap + layout.msg + rightGap + layout.right;
|
|
297
301
|
}
|
|
298
302
|
const bgRgb = THEME.chromeBg;
|
|
299
303
|
const head = statusHead(view, layout.clipped, color);
|
|
@@ -323,6 +327,19 @@ const statusPlanHit = (view, width, y) => {
|
|
|
323
327
|
return { id: 'plan', y, x0, x1: x0 + visibleWidth(place.face) };
|
|
324
328
|
};
|
|
325
329
|
|
|
330
|
+
const statusEditHits = (view, width, y) => {
|
|
331
|
+
const editor = view.import?.editor ?? view.find?.editor;
|
|
332
|
+
if (editor) {
|
|
333
|
+
const x = view.import ? 5 : 2;
|
|
334
|
+
return [fieldHit(editor, x, Math.max(0, width - x), y)];
|
|
335
|
+
}
|
|
336
|
+
if (!view.planOpen || !view.planEditor) return [];
|
|
337
|
+
const layout = statusLayout(view, view.status ?? '', width);
|
|
338
|
+
const place = planFace(view, layout.clipped);
|
|
339
|
+
if (!place) return [];
|
|
340
|
+
return [fieldHit(view.planEditor, place.x, visibleWidth(place.face), y)];
|
|
341
|
+
};
|
|
342
|
+
|
|
326
343
|
const planMenu = (view, width, color, statusY) => {
|
|
327
344
|
if (view.pane !== 'tasks' || !view.planOpen || statusY < 3) return null;
|
|
328
345
|
const names = view.planNames ?? [];
|
|
@@ -333,7 +350,7 @@ const planMenu = (view, width, color, statusY) => {
|
|
|
333
350
|
const room = Math.max(1, statusY - 2);
|
|
334
351
|
const box = dropBox(names, view.planCursor, view.planScroll, room);
|
|
335
352
|
const boxW = menuBoxWidth(names, visibleWidth(place.face), width - x);
|
|
336
|
-
const faces = menuFaces(names, box, boxW, color);
|
|
353
|
+
const faces = menuFaces(names, box, boxW, color, view.planQuery);
|
|
337
354
|
const lines = [];
|
|
338
355
|
const hits = [];
|
|
339
356
|
for (let i = 0; i < faces.length; i++) {
|
|
@@ -366,5 +383,6 @@ module.exports = {
|
|
|
366
383
|
statusChoiceHits,
|
|
367
384
|
statusBranchHit,
|
|
368
385
|
statusPlanHit,
|
|
386
|
+
statusEditHits,
|
|
369
387
|
planMenu,
|
|
370
388
|
};
|
package/lib/report/run.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const path = require('node:path');
|
|
4
|
-
const { spawn } = require('node:child_process');
|
|
5
4
|
|
|
6
|
-
const { spawnBase } = require('../common/process.js');
|
|
5
|
+
const { spawnBase, spawnCmd, winCommand } = require('../common/process.js');
|
|
7
6
|
const { withNewline } = require('../common/utilities.js');
|
|
8
7
|
const { nextLogFile, saveLogs } = require('../runs/logs.js');
|
|
9
8
|
const { toolHint, buildDocument } = require('./parse.js');
|
|
@@ -16,29 +15,12 @@ const {
|
|
|
16
15
|
|
|
17
16
|
const OUTPUTS = ['md', 'raw'];
|
|
18
17
|
const PIPE = ['ignore', 'pipe', 'pipe'];
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const quoteCmdArg = (arg) => {
|
|
22
|
-
const text = `${arg}`;
|
|
23
|
-
if (text && CMD_SAFE.test(text)) return text;
|
|
24
|
-
const escaped = text.replaceAll('"', '""').replaceAll('%', '"%"');
|
|
25
|
-
return `"${escaped}"`;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const winCommand = (program, args) => {
|
|
29
|
-
const parts = [program, ...args].map(quoteCmdArg);
|
|
30
|
-
return `"${parts.join(' ')}"`;
|
|
31
|
-
};
|
|
18
|
+
const EXIT_DRAIN_MS = 100;
|
|
32
19
|
|
|
33
20
|
const spawnCommand = (command, options) => {
|
|
34
21
|
const { program, args } = command;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
const shell = process.env.comspec || 'cmd.exe';
|
|
39
|
-
const line = winCommand(program, args);
|
|
40
|
-
const opts = spawnBase({ ...options, windowsVerbatimArguments: true });
|
|
41
|
-
return spawn(shell, ['/d', '/s', '/c', line], opts);
|
|
22
|
+
const shell = process.platform === 'win32';
|
|
23
|
+
return spawnCmd(program, args, spawnBase({ ...options, shell }));
|
|
42
24
|
};
|
|
43
25
|
|
|
44
26
|
const collect = (child, onOut, onErr) =>
|
|
@@ -46,9 +28,15 @@ const collect = (child, onOut, onErr) =>
|
|
|
46
28
|
let out = '';
|
|
47
29
|
let err = '';
|
|
48
30
|
let settled = false;
|
|
49
|
-
|
|
31
|
+
let exitTimer = null;
|
|
32
|
+
const finish = (status, release = false) => {
|
|
50
33
|
if (settled) return;
|
|
51
34
|
settled = true;
|
|
35
|
+
clearTimeout(exitTimer);
|
|
36
|
+
if (release) {
|
|
37
|
+
if (child.stdout) child.stdout.destroy();
|
|
38
|
+
if (child.stderr) child.stderr.destroy();
|
|
39
|
+
}
|
|
52
40
|
resolve({ status, stdout: out, stderr: err });
|
|
53
41
|
};
|
|
54
42
|
if (child.stdout) {
|
|
@@ -69,6 +57,10 @@ const collect = (child, onOut, onErr) =>
|
|
|
69
57
|
err += `${error.message}\n`;
|
|
70
58
|
finish(1);
|
|
71
59
|
});
|
|
60
|
+
child.on('exit', (status) => {
|
|
61
|
+
if (settled) return;
|
|
62
|
+
exitTimer = setTimeout(() => finish(status ?? 1, true), EXIT_DRAIN_MS);
|
|
63
|
+
});
|
|
72
64
|
child.on('close', (status) => finish(status ?? 1));
|
|
73
65
|
});
|
|
74
66
|
|
|
@@ -182,6 +174,7 @@ const runCapture = async (proc, deps = {}) => {
|
|
|
182
174
|
};
|
|
183
175
|
|
|
184
176
|
module.exports = {
|
|
177
|
+
collect,
|
|
185
178
|
winCommand,
|
|
186
179
|
runCapture,
|
|
187
180
|
};
|
|
@@ -7,8 +7,8 @@ Work through unchecked items.
|
|
|
7
7
|
- For `code` items, replace the added lines at the given location
|
|
8
8
|
- Set `status` to `partial` if any items remain, or `done` if all are `[x]`
|
|
9
9
|
- Do not redo items already marked `[x]`
|
|
10
|
-
- Run
|
|
11
|
-
- Run
|
|
10
|
+
- Run `npm t` for lint and tests that do not start processes
|
|
11
|
+
- Run tests that start processes with `node --test <files>`
|
|
12
12
|
- An item prefixed with `F<number>:` gets its own commit in this repo
|
|
13
13
|
- Leave items without that prefix unstaged, do not commit or stage them
|
|
14
14
|
- Do not add `Authored-by` or `Co-authored-by` unless the issue asks for it
|
package/lib/runs/process.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { spawn } = require('node:child_process');
|
|
4
|
-
const { spawnBase } = require('../common/process.js');
|
|
4
|
+
const { spawnBase, spawnCmd } = require('../common/process.js');
|
|
5
5
|
|
|
6
6
|
const signalPid = (pid, signal) => {
|
|
7
7
|
try {
|
|
@@ -80,7 +80,7 @@ class RunProcess {
|
|
|
80
80
|
class PipeProcess extends RunProcess {
|
|
81
81
|
start(cmd, args, options) {
|
|
82
82
|
try {
|
|
83
|
-
this.child =
|
|
83
|
+
this.child = spawnCmd(cmd, args, options);
|
|
84
84
|
} catch (error) {
|
|
85
85
|
this.text = `${error.message}\n`;
|
|
86
86
|
this.finish(1);
|
|
@@ -110,4 +110,4 @@ class PipeProcess extends RunProcess {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
module.exports = { commandEnv, RunProcess, PipeProcess };
|
|
113
|
+
module.exports = { killTree, commandEnv, RunProcess, PipeProcess };
|
package/lib/runs/runs.js
CHANGED
|
@@ -13,6 +13,8 @@ const { nextLogFile } = require('./logs.js');
|
|
|
13
13
|
const RUNS_REL = `${LOG_DIR}/.runs`;
|
|
14
14
|
const RUNS_KEEP = 24;
|
|
15
15
|
const WRITE_MS = 150;
|
|
16
|
+
const HEARTBEAT_MS = 1000;
|
|
17
|
+
const HEARTBEAT_STALE_MS = 5000;
|
|
16
18
|
const FAILING_TEST = /^\s*(?:not ok \d+|[✖✗×]) /;
|
|
17
19
|
const LIFECYCLE_EVENT = 'npm_lifecycle_event';
|
|
18
20
|
|
|
@@ -73,9 +75,19 @@ const listJson = (dir) => listDir(dir).filter((name) => name.endsWith('.json'));
|
|
|
73
75
|
const isCommandRun = (record) =>
|
|
74
76
|
Boolean(record && record.id && record.progress && !record.cliId);
|
|
75
77
|
|
|
76
|
-
const settleRecord = (record, alive = pidAlive) => {
|
|
77
|
-
if (record.status !== 'running'
|
|
78
|
-
|
|
78
|
+
const settleRecord = (record, alive = pidAlive, now = Date.now) => {
|
|
79
|
+
if (record.status !== 'running') return record;
|
|
80
|
+
const activeAt = Number(record.activeAt);
|
|
81
|
+
const current = Number(now());
|
|
82
|
+
const fresh =
|
|
83
|
+
record.v >= 2 &&
|
|
84
|
+
Number.isFinite(activeAt) &&
|
|
85
|
+
Number.isFinite(current) &&
|
|
86
|
+
current - activeAt <= HEARTBEAT_STALE_MS;
|
|
87
|
+
if (fresh && alive(record.pid)) return record;
|
|
88
|
+
const endedAt =
|
|
89
|
+
Number(record.endedAt) || activeAt || Number(record.startedAt) || 0;
|
|
90
|
+
return { ...record, status: 'aborted', endedAt };
|
|
79
91
|
};
|
|
80
92
|
|
|
81
93
|
const pushRuns = (records, dir, names) => {
|
|
@@ -198,6 +210,7 @@ class RunRecorder {
|
|
|
198
210
|
this.tracker = new Tracker();
|
|
199
211
|
this.lastWrite = 0;
|
|
200
212
|
this.closed = false;
|
|
213
|
+
this.heartbeat = null;
|
|
201
214
|
this.slot = Object.hasOwn(options, 'slot') ? options.slot : null;
|
|
202
215
|
if (!Object.hasOwn(options, 'slot')) {
|
|
203
216
|
try {
|
|
@@ -209,7 +222,7 @@ class RunRecorder {
|
|
|
209
222
|
const startedAt = this.clock();
|
|
210
223
|
const env = options.env || process.env;
|
|
211
224
|
this.record = {
|
|
212
|
-
v:
|
|
225
|
+
v: 2,
|
|
213
226
|
id: `${startedAt}-${process.pid}`,
|
|
214
227
|
pid: process.pid,
|
|
215
228
|
command,
|
|
@@ -217,6 +230,7 @@ class RunRecorder {
|
|
|
217
230
|
cwd: root,
|
|
218
231
|
startedAt,
|
|
219
232
|
endedAt: 0,
|
|
233
|
+
activeAt: startedAt,
|
|
220
234
|
status: 'running',
|
|
221
235
|
exit: null,
|
|
222
236
|
progress: { ...this.tracker.progress, expected: 0 },
|
|
@@ -231,10 +245,17 @@ class RunRecorder {
|
|
|
231
245
|
pruneRuns(root);
|
|
232
246
|
this.write();
|
|
233
247
|
});
|
|
248
|
+
if (this.slot) {
|
|
249
|
+
this.heartbeat = setInterval(() => {
|
|
250
|
+
safely(() => this.write());
|
|
251
|
+
}, HEARTBEAT_MS);
|
|
252
|
+
this.heartbeat.unref();
|
|
253
|
+
}
|
|
234
254
|
}
|
|
235
255
|
|
|
236
256
|
write() {
|
|
237
257
|
if (!this.slot) return;
|
|
258
|
+
if (!this.closed) this.record.activeAt = this.clock();
|
|
238
259
|
Object.assign(this.record.progress, this.tracker.progress);
|
|
239
260
|
writeRecord(this.slot.jsonFull, this.record);
|
|
240
261
|
}
|
|
@@ -251,6 +272,8 @@ class RunRecorder {
|
|
|
251
272
|
finish(status, result = null) {
|
|
252
273
|
if (this.closed) return;
|
|
253
274
|
this.closed = true;
|
|
275
|
+
clearInterval(this.heartbeat);
|
|
276
|
+
this.heartbeat = null;
|
|
254
277
|
const record = this.record;
|
|
255
278
|
record.endedAt = this.clock();
|
|
256
279
|
record.exit = status;
|