reslop 0.3.1 → 0.3.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/CHANGELOG.md +28 -1
- package/README.md +30 -4
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +274 -438
- 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/script.js +93 -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 +84 -34
- 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/agents.js
CHANGED
|
@@ -17,10 +17,11 @@ const { flexCell, tableLines, titleAside } = table;
|
|
|
17
17
|
const logPaint = require('./log.js');
|
|
18
18
|
const { LOG_PAD_X } = logPaint;
|
|
19
19
|
const { paintNpmLog, withLogScroller } = logPaint;
|
|
20
|
-
const { paintField } = require('./field.js');
|
|
20
|
+
const { paintField, queryTones, fieldHit } = require('./field.js');
|
|
21
21
|
|
|
22
22
|
const BUBBLE = '●';
|
|
23
23
|
const BUBBLE_GAP = 1;
|
|
24
|
+
const COLUMN_GAP = 2;
|
|
24
25
|
const BUBBLE_PHASE = ['key', 'add', 'sha', 'warn'];
|
|
25
26
|
const PAD_W = 2;
|
|
26
27
|
const TAIL = 2;
|
|
@@ -45,14 +46,8 @@ const COL_TITLES = {
|
|
|
45
46
|
|
|
46
47
|
const agentFields = (row) => {
|
|
47
48
|
if (!row.bin) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
model: row.model || '',
|
|
51
|
-
effort: '',
|
|
52
|
-
fast: '',
|
|
53
|
-
context: '',
|
|
54
|
-
plan: 'not installed',
|
|
55
|
-
};
|
|
49
|
+
const none = { model: '', effort: '', fast: '', context: '', plan: '' };
|
|
50
|
+
return { name: row.name, ...none };
|
|
56
51
|
}
|
|
57
52
|
const levels = row.efforts ?? [];
|
|
58
53
|
return {
|
|
@@ -166,7 +161,28 @@ const cellFace = (text, width, query, end = false) => {
|
|
|
166
161
|
return end && !query ? `${pad}${shown}` : `${shown}${pad}`;
|
|
167
162
|
};
|
|
168
163
|
|
|
164
|
+
// A missing agent has no settings: the row says what would bring it
|
|
165
|
+
const paintHintRow = (row, width, color, selected, cols) => {
|
|
166
|
+
const prefix = ' '.repeat(PAD_W);
|
|
167
|
+
const tail = ' '.repeat(TAIL);
|
|
168
|
+
const gap = cols.gap || ' ';
|
|
169
|
+
const name = cellFace(row.name, cols.nameW, '');
|
|
170
|
+
const taken = PAD_W + TAIL + visibleWidth(name) + visibleWidth(gap);
|
|
171
|
+
const hint = cellFace(row.hint || 'not installed', width - taken, '');
|
|
172
|
+
const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
|
|
173
|
+
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
174
|
+
const text = `${prefix}${name}${gap}${hint}${tail}`;
|
|
175
|
+
if (!color) return { row: text, spans: [] };
|
|
176
|
+
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
177
|
+
out += paint(`${prefix}${name}`, THEME.mutedFg, bgRgb, color);
|
|
178
|
+
out += paint(gap, fgRgb, bgRgb, color);
|
|
179
|
+
out += paint(hint, THEME.warnFg, bgRgb, color);
|
|
180
|
+
out += paint(tail, fgRgb, bgRgb, color);
|
|
181
|
+
return { row: out, spans: [] };
|
|
182
|
+
};
|
|
183
|
+
|
|
169
184
|
const paintAgentRow = (row, width, color, selected, cols, search) => {
|
|
185
|
+
if (!row.bin) return paintHintRow(row, width, color, selected, cols);
|
|
170
186
|
const fields = agentFields(row);
|
|
171
187
|
const prefix = ' '.repeat(PAD_W);
|
|
172
188
|
const rest = Math.max(1, width - PAD_W - TAIL);
|
|
@@ -183,6 +199,17 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
|
|
|
183
199
|
),
|
|
184
200
|
plan: cellFace(fields.plan, cols.planW, typedQuery(search, 'plan'), true),
|
|
185
201
|
};
|
|
202
|
+
if (!color && search?.editor) {
|
|
203
|
+
const size = colW(cols, search.field);
|
|
204
|
+
search.editor.reveal(size);
|
|
205
|
+
cells[search.field] = paintField(
|
|
206
|
+
search.query,
|
|
207
|
+
search.editor,
|
|
208
|
+
size,
|
|
209
|
+
false,
|
|
210
|
+
queryTones(THEME.warnFg, THEME.buttonBg),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
186
213
|
let mid = name;
|
|
187
214
|
for (const field of VALUE_FIELDS) {
|
|
188
215
|
if (colW(cols, field)) mid += `${gap}${cells[field]}`;
|
|
@@ -193,11 +220,9 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
|
|
|
193
220
|
const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
|
|
194
221
|
const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
|
|
195
222
|
if (!color) return { row: `${prefix}${mid}${pad}${tail}`, spans };
|
|
196
|
-
const
|
|
197
|
-
const modelFg = row.bin ? THEME.shaFg : THEME.shaDarkFg;
|
|
198
|
-
const valueFg = row.bin ? THEME.buttonHotFg : fgRgb;
|
|
223
|
+
const valueFg = THEME.buttonHotFg;
|
|
199
224
|
const tones = {
|
|
200
|
-
model: { fg:
|
|
225
|
+
model: { fg: THEME.shaFg, bold: true },
|
|
201
226
|
effort: { fg: valueFg, bold: false },
|
|
202
227
|
fast: { fg: valueFg, bold: false, glue: true },
|
|
203
228
|
context: { fg: valueFg, bold: false },
|
|
@@ -205,7 +230,7 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
|
|
|
205
230
|
};
|
|
206
231
|
let out = `${seq(fgRgb, bgRgb)}${EL}`;
|
|
207
232
|
out += paint(prefix, fgRgb, bgRgb, color);
|
|
208
|
-
out += paint(name,
|
|
233
|
+
out += paint(name, valueFg, bgRgb, color, true);
|
|
209
234
|
for (const field of VALUE_FIELDS) {
|
|
210
235
|
const size = colW(cols, field);
|
|
211
236
|
if (!size) continue;
|
|
@@ -219,14 +244,10 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
|
|
|
219
244
|
out += paint(gap, valueFg, bgRgb, color);
|
|
220
245
|
if (query && search.editor) {
|
|
221
246
|
search.editor.reveal(size);
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
select: { fg: THEME.chromeFg, bg: THEME.checkBg },
|
|
225
|
-
caret: { fg: THEME.searchOnFg, bg: THEME.searchOnBg },
|
|
226
|
-
};
|
|
227
|
-
out += paintField(query, search.editor, size, color, queryTones);
|
|
247
|
+
const typed = queryTones(THEME.warnFg, bgRgb);
|
|
248
|
+
out += paintField(query, search.editor, size, color, typed);
|
|
228
249
|
} else if (query) {
|
|
229
|
-
out += paint(face, THEME.
|
|
250
|
+
out += paint(face, THEME.warnFg, bgRgb, color, true);
|
|
230
251
|
} else {
|
|
231
252
|
out += paint(face, tone.fg, bgRgb, color, tone.bold);
|
|
232
253
|
}
|
|
@@ -273,7 +294,7 @@ const overlayMenu = (painted, view, cols, gap, width, color, headerLines) => {
|
|
|
273
294
|
const boxW = menuBoxWidth(options, columnW, width - x);
|
|
274
295
|
if (x + boxW > width) x = Math.max(0, width - boxW);
|
|
275
296
|
const origin = down ? rowAt + 1 : Math.max(0, rowAt - box.span);
|
|
276
|
-
const faces = menuFaces(options, box, boxW, color, off);
|
|
297
|
+
const faces = menuFaces(options, box, boxW, color, view.agentQuery, off);
|
|
277
298
|
const body = painted.body.slice();
|
|
278
299
|
const fileHits = painted.fileHits.slice();
|
|
279
300
|
for (let i = 0; i < faces.length && origin + i < body.length; i++) {
|
|
@@ -289,7 +310,13 @@ const overlayMenu = (painted, view, cols, gap, width, color, headerLines) => {
|
|
|
289
310
|
field: 'menu',
|
|
290
311
|
});
|
|
291
312
|
}
|
|
292
|
-
|
|
313
|
+
const editHits = painted.editHits.slice();
|
|
314
|
+
if (view.agentEditor) {
|
|
315
|
+
const start = fieldX(cols, gap, field);
|
|
316
|
+
const hit = fieldHit(view.agentEditor, start, columnW, rowHit.y);
|
|
317
|
+
editHits.push({ ...hit, row: rowAt });
|
|
318
|
+
}
|
|
319
|
+
return { ...painted, body, fileHits, editHits, menuScroll: box.start };
|
|
293
320
|
};
|
|
294
321
|
|
|
295
322
|
const chooserSplit = (width) => {
|
|
@@ -616,27 +643,50 @@ const agentLead = (item, frame) => {
|
|
|
616
643
|
return cellSegs(segs);
|
|
617
644
|
};
|
|
618
645
|
|
|
619
|
-
|
|
646
|
+
// The last column clips its names when the tile is narrow
|
|
647
|
+
const agentCells = (item, frame, last) => {
|
|
648
|
+
if (!item) return [];
|
|
620
649
|
const tone = item.bin ? 'text' : 'muted';
|
|
621
|
-
const name = flexCell(item.name, tone);
|
|
650
|
+
const name = last ? flexCell(item.name, tone) : cell(item.name, tone);
|
|
622
651
|
name.gap = BUBBLE_GAP;
|
|
623
|
-
return name;
|
|
652
|
+
return [agentLead(item, frame), name];
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// Two columns while both fit their widest agent, else one
|
|
656
|
+
const columnCount = (items, width) => {
|
|
657
|
+
let lead = 0;
|
|
658
|
+
let name = 0;
|
|
659
|
+
for (const item of items) {
|
|
660
|
+
const sessions = item.running ? ` ${item.running}` : '';
|
|
661
|
+
lead = Math.max(lead, visibleWidth(`${BUBBLE}${sessions}`));
|
|
662
|
+
name = Math.max(name, visibleWidth(item.name));
|
|
663
|
+
}
|
|
664
|
+
const entry = lead + BUBBLE_GAP + name;
|
|
665
|
+
return entry * 2 + COLUMN_GAP <= width ? 2 : 1;
|
|
624
666
|
};
|
|
625
667
|
|
|
668
|
+
// Agents fill the left column first
|
|
626
669
|
const agentsBlock = (model, width, height, ctx, tile) => {
|
|
627
670
|
const data = model.agents;
|
|
628
|
-
if (!data || !data.ready) return waiting('detecting
|
|
671
|
+
if (!data || !data.ready) return waiting('detecting agents…');
|
|
629
672
|
const aside = [seg(`${data.running || 0}`, 'key', true)];
|
|
630
673
|
const titleLine = titleAside(tile, aside, width);
|
|
631
674
|
const items = data.items ?? [];
|
|
632
675
|
if (!items.length) return notice('no agents', titleLine);
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
676
|
+
const columns = columnCount(items, width);
|
|
677
|
+
const perColumn = Math.ceil(items.length / columns);
|
|
678
|
+
const count = Math.min(Math.max(0, height), perColumn);
|
|
679
|
+
const rows = [];
|
|
680
|
+
for (let i = 0; i < count; i++) {
|
|
681
|
+
const left = agentCells(items[i], ctx.frame, columns === 1);
|
|
682
|
+
if (columns === 1) {
|
|
683
|
+
rows.push(left);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
const right = agentCells(items[i + count], ctx.frame, true);
|
|
687
|
+
rows.push([...left, ...right]);
|
|
688
|
+
}
|
|
689
|
+
return { titleLine, lines: tableLines(rows, width, COLUMN_GAP) };
|
|
640
690
|
};
|
|
641
691
|
|
|
642
692
|
module.exports = { paintBodyAgents, agentsBlock };
|
package/lib/render/branches.js
CHANGED
|
@@ -5,9 +5,12 @@ const { shortAge } = require('../common/format.js');
|
|
|
5
5
|
const primitives = require('./primitives.js');
|
|
6
6
|
|
|
7
7
|
const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
|
|
8
|
-
const {
|
|
9
|
-
const {
|
|
8
|
+
const { markPrefix, branchLabel, paintCursorList } = primitives;
|
|
9
|
+
const { paintBodyFill } = primitives;
|
|
10
10
|
const { measureColumns, padColumns, joinColumns, paintColumns } = primitives;
|
|
11
|
+
const { paintNameEdit } = require('./name-edit.js');
|
|
12
|
+
const { paintTreeBand } = require('./repos.js');
|
|
13
|
+
const { tileHits, pageFill } = require('./tiles.js');
|
|
11
14
|
|
|
12
15
|
const BRANCH_COL_ALIGN = {
|
|
13
16
|
sha: 'end',
|
|
@@ -92,27 +95,23 @@ const paintBranchRow = (entry, width, color, selected, cols) => {
|
|
|
92
95
|
return out + paint(tail, fgRgb, bgRgb, color);
|
|
93
96
|
};
|
|
94
97
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const textX = MARK_W + 1;
|
|
111
|
-
const edit = editSpan(1, textX, textX + rest, 0, text);
|
|
112
|
-
const cursor = { x: textX + win.col, scroll: win.scroll };
|
|
113
|
-
return { row, cursor, edits: [{ ...edit, scroll: win.scroll }] };
|
|
114
|
-
};
|
|
98
|
+
// A title and the five lines of a tile
|
|
99
|
+
const TREES_H = 6;
|
|
100
|
+
const LIST_MIN_H = 4;
|
|
101
|
+
// A blank line above the tiles, one under them, one under the list
|
|
102
|
+
const TREES_GAPS = 3;
|
|
103
|
+
|
|
104
|
+
// The tiles of the worktrees show when the list of branches can spare it
|
|
105
|
+
const treesHeight = (bodyH) =>
|
|
106
|
+
bodyH - TREES_GAPS - LIST_MIN_H >= TREES_H ? TREES_H : 0;
|
|
107
|
+
|
|
108
|
+
const shiftRows = (list, by) => ({
|
|
109
|
+
...list,
|
|
110
|
+
cursor: list.cursor ? { ...list.cursor, row: list.cursor.row + by } : null,
|
|
111
|
+
editHits: list.editHits.map((span) => ({ ...span, row: span.row + by })),
|
|
112
|
+
});
|
|
115
113
|
|
|
114
|
+
// The worktrees above the branches; one element is selected, a tile or a branch
|
|
116
115
|
const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
117
116
|
const branches = view.branches ?? [];
|
|
118
117
|
const cols = measureColumns(branches, branchRowFields, BRANCH_COL_ALIGN);
|
|
@@ -123,12 +122,41 @@ const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
|
|
|
123
122
|
cols.name = Math.min(BRANCH_NAME_MAX, cols.name);
|
|
124
123
|
const cursor = view.branchCursor ?? 0;
|
|
125
124
|
const { compose } = view;
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
125
|
+
const trees = view.trees ? treesHeight(bodyH) : 0;
|
|
126
|
+
const typing = compose?.kind === 'branch' || compose?.kind === 'worktree';
|
|
127
|
+
const typed = typing ? paintNameEdit(compose, width, color) : null;
|
|
128
|
+
const above = trees && compose?.kind === 'worktree' ? typed : null;
|
|
129
|
+
const edit = above ? null : typed;
|
|
130
|
+
const lead = above ? 1 : 0;
|
|
131
|
+
const top = trees ? lead + 1 + trees + 1 : 0;
|
|
132
|
+
const room = trees ? bodyH - top - 1 : bodyH;
|
|
133
|
+
const pane = { width, color, bodyH: room, headerLines: headerLines + top };
|
|
134
|
+
pane.offset = view.listScroll;
|
|
135
|
+
if (trees) pane.pad = 0;
|
|
136
|
+
const picked = !view.treeFocus;
|
|
129
137
|
const paintRow = (entry, selected) =>
|
|
130
|
-
paintBranchRow(entry, width, color, selected, cols);
|
|
131
|
-
|
|
138
|
+
paintBranchRow(entry, width, color, selected && picked, cols);
|
|
139
|
+
const list = paintCursorList(branches, cursor, pane, paintRow, edit);
|
|
140
|
+
if (!trees) return list;
|
|
141
|
+
const band = paintTreeBand(view.trees, width, trees, color);
|
|
142
|
+
const hits = tileHits(band.bands, headerLines + lead + 1);
|
|
143
|
+
const treeHits = hits.map((hit) => ({ ...hit, tree: true }));
|
|
144
|
+
const head = above ? [above.row] : [];
|
|
145
|
+
const gap = pageFill(width, color);
|
|
146
|
+
const tail = paintBodyFill(width, color, 'files');
|
|
147
|
+
const body = [...head, gap, ...band.rows, gap, ...list.body, tail];
|
|
148
|
+
const fileHits = [...treeHits, ...list.fileHits];
|
|
149
|
+
const moved = shiftRows(list, top);
|
|
150
|
+
const typedAbove = above ? { ...above.cursor, row: 0 } : null;
|
|
151
|
+
const spans = above ? above.edits.map((span) => ({ ...span, row: 0 })) : [];
|
|
152
|
+
return {
|
|
153
|
+
...moved,
|
|
154
|
+
body,
|
|
155
|
+
fileHits,
|
|
156
|
+
cursor: typedAbove ?? moved.cursor,
|
|
157
|
+
editHits: [...spans, ...moved.editHits],
|
|
158
|
+
gridCols: band.cols,
|
|
159
|
+
};
|
|
132
160
|
};
|
|
133
161
|
|
|
134
162
|
module.exports = { paintBodyBranches };
|
package/lib/render/field.js
CHANGED
|
@@ -4,6 +4,16 @@ const ansi = require('../term/ansi.js');
|
|
|
4
4
|
|
|
5
5
|
const { THEME, paint, graphemes, graphemeWidth, visibleWidth } = ansi;
|
|
6
6
|
const { fileMatches } = require('../common/find.js');
|
|
7
|
+
const { editSpan } = require('./primitives.js');
|
|
8
|
+
|
|
9
|
+
// Inline fields paint their own selection; their spans map pointer positions.
|
|
10
|
+
const fieldHit = (editor, x, width, y) => ({
|
|
11
|
+
...editSpan(x + 1, x + 1, x + width + 1, 0, editor.text),
|
|
12
|
+
editor,
|
|
13
|
+
y,
|
|
14
|
+
scroll: editor.scrollCol,
|
|
15
|
+
live: false,
|
|
16
|
+
});
|
|
7
17
|
|
|
8
18
|
const pieceKind = (index, cursor, lo, hi) => {
|
|
9
19
|
if (index === cursor) return 'caret';
|
|
@@ -62,6 +72,12 @@ const paintField = (text, editor, room, color, tones) => {
|
|
|
62
72
|
return out + paint(pad, plain.fg, plain.bg, color);
|
|
63
73
|
};
|
|
64
74
|
|
|
75
|
+
const queryTones = (fgRgb, bgRgb) => ({
|
|
76
|
+
plain: { fg: fgRgb, bg: bgRgb },
|
|
77
|
+
select: { fg: THEME.chromeFg, bg: THEME.checkBg },
|
|
78
|
+
caret: { fg: THEME.searchOnFg, bg: THEME.searchOnBg },
|
|
79
|
+
});
|
|
80
|
+
|
|
65
81
|
const paintPlanFace = (view, face, fgRgb, bgRgb, color) => {
|
|
66
82
|
const editor = view.planEditor;
|
|
67
83
|
if (!editor || !view.planQuery) {
|
|
@@ -69,11 +85,7 @@ const paintPlanFace = (view, face, fgRgb, bgRgb, color) => {
|
|
|
69
85
|
}
|
|
70
86
|
const room = visibleWidth(face);
|
|
71
87
|
if (room) editor.reveal(room);
|
|
72
|
-
const tones =
|
|
73
|
-
plain: { fg: THEME.searchFg, bg: THEME.searchBg },
|
|
74
|
-
select: { fg: THEME.chromeFg, bg: THEME.checkBg },
|
|
75
|
-
caret: { fg: THEME.searchOnFg, bg: THEME.searchOnBg },
|
|
76
|
-
};
|
|
88
|
+
const tones = queryTones(fgRgb, bgRgb);
|
|
77
89
|
return paintField(view.planQuery, editor, room, color, tones);
|
|
78
90
|
};
|
|
79
91
|
|
|
@@ -125,7 +137,9 @@ const paintFindStatus = (view, width, color) => {
|
|
|
125
137
|
|
|
126
138
|
module.exports = {
|
|
127
139
|
paintField,
|
|
140
|
+
queryTones,
|
|
128
141
|
paintPlanFace,
|
|
129
142
|
paintImportStatus,
|
|
130
143
|
paintFindStatus,
|
|
144
|
+
fieldHit,
|
|
131
145
|
};
|
package/lib/render/footer.js
CHANGED
|
@@ -296,9 +296,10 @@ const agentListDim = (view) => {
|
|
|
296
296
|
const off = [];
|
|
297
297
|
if (!installed || levels.length < 2) off.push('agentEffort');
|
|
298
298
|
if (!installed) off.push('agentFast');
|
|
299
|
-
if (!installed ||
|
|
299
|
+
if (!installed || sizes.length < 2) off.push('agentContext');
|
|
300
300
|
if ((view.agentPlanCount ?? 0) < 2) off.push('agentReview');
|
|
301
|
-
if (!
|
|
301
|
+
if (installed || !row || !row.installable) off.push('agentInstall');
|
|
302
|
+
if (!view.agentCanStart) off.push('agentStart');
|
|
302
303
|
return off;
|
|
303
304
|
};
|
|
304
305
|
|
|
@@ -331,14 +332,16 @@ const dimFooterActions = (view) => {
|
|
|
331
332
|
const GIT_FOOTER = ['add', 'unstage', 'revert', 'ignore'];
|
|
332
333
|
|
|
333
334
|
const FOOTER_EXTRA = {
|
|
334
|
-
repos: ['theme'],
|
|
335
|
+
repos: ['theme', 'worktreeNew', 'worktreeDrop'],
|
|
335
336
|
dashboard: ['theme', 'pull', 'push'],
|
|
336
337
|
unit: ['add', 'unstage', 'revert', 'feedback', 'code', 'lines'],
|
|
337
338
|
files: [...GIT_FOOTER, 'code'],
|
|
338
339
|
diff: [...GIT_FOOTER, 'feedback', 'code', 'layout', 'lines'],
|
|
339
|
-
branches: ['newBranch', 'rebase', 'drop', 'pull', 'push'],
|
|
340
|
+
branches: ['newBranch', 'worktreeBranch', 'rebase', 'drop', 'pull', 'push'],
|
|
341
|
+
trees: ['worktreeNew', 'worktreeDrop', 'pull', 'push'],
|
|
340
342
|
commits: ['newCommit', 'apply', 'reword', 'drop', 'view', 'pull', 'push'],
|
|
341
343
|
npmLog: ['npmVerbose', 'npmStop', 'npmRerun'],
|
|
344
|
+
npmFailedLog: ['npmVerbose', 'npmSolve', 'npmRerun'],
|
|
342
345
|
npm: ['npmEdit', 'npmNew', 'npmDrop', 'npmLogs'],
|
|
343
346
|
packages: ['packageNew', 'packageDrop', 'packageWanted', 'packageLatest'],
|
|
344
347
|
agentLog: ['agentStop', 'agentRerun'],
|
|
@@ -348,14 +351,18 @@ const FOOTER_EXTRA = {
|
|
|
348
351
|
'agentFast',
|
|
349
352
|
'agentContext',
|
|
350
353
|
'agentReview',
|
|
351
|
-
'
|
|
354
|
+
'agentInstall',
|
|
355
|
+
'agentStart',
|
|
352
356
|
],
|
|
353
357
|
tasks: ['plan', 'import', 'drop', 'check', 'taskNudge'],
|
|
354
358
|
};
|
|
355
359
|
|
|
356
360
|
const extraFooterActions = (view) => {
|
|
357
361
|
const { pane } = view;
|
|
358
|
-
if (pane === '
|
|
362
|
+
if (pane === 'branches' && view.treeFocus) return FOOTER_EXTRA.trees;
|
|
363
|
+
if (pane === 'npm' && view.npmView) {
|
|
364
|
+
return view.npmCanSolve ? FOOTER_EXTRA.npmFailedLog : FOOTER_EXTRA.npmLog;
|
|
365
|
+
}
|
|
359
366
|
if (pane === 'npm') return FOOTER_EXTRA.npm;
|
|
360
367
|
if (pane === 'agents' && view.agentView) return FOOTER_EXTRA.agentLog;
|
|
361
368
|
if (pane === 'agents') return FOOTER_EXTRA.agents;
|
|
@@ -382,4 +389,5 @@ module.exports = {
|
|
|
382
389
|
paintFooter,
|
|
383
390
|
footerHits,
|
|
384
391
|
footerLayout,
|
|
392
|
+
dimFooterActions,
|
|
385
393
|
};
|
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 };
|