reslop 0.3.0 → 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.
Files changed (74) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +166 -370
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -13
  7. package/lib/agents/models.js +250 -376
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/deps.js +29 -0
  19. package/lib/git/dir.js +50 -0
  20. package/lib/git/git.js +1 -1
  21. package/lib/git/linked.js +152 -0
  22. package/lib/git/local.js +179 -0
  23. package/lib/git/status.js +126 -0
  24. package/lib/git/worktree.js +137 -31
  25. package/lib/input/actions.js +83 -2
  26. package/lib/input/editor.js +9 -0
  27. package/lib/input/keys.js +39 -3
  28. package/lib/input/line-edit.js +8 -3
  29. package/lib/render/agents.js +64 -36
  30. package/lib/render/branches.js +55 -27
  31. package/lib/render/field.js +19 -5
  32. package/lib/render/footer.js +14 -4
  33. package/lib/render/frame.js +16 -2
  34. package/lib/render/log.js +5 -4
  35. package/lib/render/menu.js +103 -6
  36. package/lib/render/name-edit.js +31 -0
  37. package/lib/render/repos.js +52 -8
  38. package/lib/render/status.js +22 -4
  39. package/lib/report/render.js +4 -7
  40. package/lib/report/run.js +16 -23
  41. package/lib/review/agent-instructions.md +13 -8
  42. package/lib/review/format.js +0 -6
  43. package/lib/runs/process.js +3 -3
  44. package/lib/runs/runs.js +27 -4
  45. package/lib/session/agents/agents.js +227 -194
  46. package/lib/session/agents/away.js +48 -0
  47. package/lib/session/agents/job.js +23 -86
  48. package/lib/session/agents/jobs.js +14 -27
  49. package/lib/session/agents/options.js +45 -101
  50. package/lib/session/agents/plans.js +53 -0
  51. package/lib/session/agents/view.js +34 -0
  52. package/lib/session/branches.js +18 -2
  53. package/lib/session/compose.js +20 -6
  54. package/lib/session/dispatch.js +43 -3
  55. package/lib/session/editor-pointer.js +98 -0
  56. package/lib/session/files-pane.js +3 -0
  57. package/lib/session/import.js +12 -1
  58. package/lib/session/lifecycle.js +70 -10
  59. package/lib/session/navigation.js +5 -0
  60. package/lib/session/npm/history.js +3 -1
  61. package/lib/session/npm/npm.js +111 -13
  62. package/lib/session/ops.js +3 -0
  63. package/lib/session/pointer.js +304 -46
  64. package/lib/session/session.js +17 -2
  65. package/lib/session/tasks.js +1 -1
  66. package/lib/session/terminal.js +2 -2
  67. package/lib/session/text-menu.js +50 -0
  68. package/lib/session/view.js +19 -2
  69. package/lib/session/workspace.js +179 -38
  70. package/lib/session/worktrees.js +280 -0
  71. package/lib/term/select.js +2 -1
  72. package/lib/term/themes/dark.json +4 -0
  73. package/lib/term/themes/light.json +4 -0
  74. package/package.json +2 -2
@@ -17,7 +17,7 @@ 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;
@@ -45,14 +45,8 @@ const COL_TITLES = {
45
45
 
46
46
  const agentFields = (row) => {
47
47
  if (!row.bin) {
48
- return {
49
- name: row.name,
50
- model: row.model || '',
51
- effort: '',
52
- fast: '',
53
- context: '',
54
- plan: 'not installed',
55
- };
48
+ const none = { model: '', effort: '', fast: '', context: '', plan: '' };
49
+ return { name: row.name, ...none };
56
50
  }
57
51
  const levels = row.efforts ?? [];
58
52
  return {
@@ -166,7 +160,28 @@ const cellFace = (text, width, query, end = false) => {
166
160
  return end && !query ? `${pad}${shown}` : `${shown}${pad}`;
167
161
  };
168
162
 
163
+ // A missing agent has no settings: the row says what would bring it
164
+ const paintHintRow = (row, width, color, selected, cols) => {
165
+ const prefix = ' '.repeat(PAD_W);
166
+ const tail = ' '.repeat(TAIL);
167
+ const gap = cols.gap || ' ';
168
+ const name = cellFace(row.name, cols.nameW, '');
169
+ const taken = PAD_W + TAIL + visibleWidth(name) + visibleWidth(gap);
170
+ const hint = cellFace(row.hint || 'not installed', width - taken, '');
171
+ const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
172
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
173
+ const text = `${prefix}${name}${gap}${hint}${tail}`;
174
+ if (!color) return { row: text, spans: [] };
175
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
176
+ out += paint(`${prefix}${name}`, THEME.mutedFg, bgRgb, color);
177
+ out += paint(gap, fgRgb, bgRgb, color);
178
+ out += paint(hint, THEME.warnFg, bgRgb, color);
179
+ out += paint(tail, fgRgb, bgRgb, color);
180
+ return { row: out, spans: [] };
181
+ };
182
+
169
183
  const paintAgentRow = (row, width, color, selected, cols, search) => {
184
+ if (!row.bin) return paintHintRow(row, width, color, selected, cols);
170
185
  const fields = agentFields(row);
171
186
  const prefix = ' '.repeat(PAD_W);
172
187
  const rest = Math.max(1, width - PAD_W - TAIL);
@@ -183,6 +198,17 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
183
198
  ),
184
199
  plan: cellFace(fields.plan, cols.planW, typedQuery(search, 'plan'), true),
185
200
  };
201
+ if (!color && search?.editor) {
202
+ const size = colW(cols, search.field);
203
+ search.editor.reveal(size);
204
+ cells[search.field] = paintField(
205
+ search.query,
206
+ search.editor,
207
+ size,
208
+ false,
209
+ queryTones(THEME.warnFg, THEME.buttonBg),
210
+ );
211
+ }
186
212
  let mid = name;
187
213
  for (const field of VALUE_FIELDS) {
188
214
  if (colW(cols, field)) mid += `${gap}${cells[field]}`;
@@ -193,11 +219,9 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
193
219
  const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
194
220
  const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
195
221
  if (!color) return { row: `${prefix}${mid}${pad}${tail}`, spans };
196
- const nameFg = row.bin ? THEME.buttonHotFg : THEME.mutedFg;
197
- const modelFg = row.bin ? THEME.shaFg : THEME.shaDarkFg;
198
- const valueFg = row.bin ? THEME.buttonHotFg : fgRgb;
222
+ const valueFg = THEME.buttonHotFg;
199
223
  const tones = {
200
- model: { fg: modelFg, bold: Boolean(row.bin) },
224
+ model: { fg: THEME.shaFg, bold: true },
201
225
  effort: { fg: valueFg, bold: false },
202
226
  fast: { fg: valueFg, bold: false, glue: true },
203
227
  context: { fg: valueFg, bold: false },
@@ -205,7 +229,7 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
205
229
  };
206
230
  let out = `${seq(fgRgb, bgRgb)}${EL}`;
207
231
  out += paint(prefix, fgRgb, bgRgb, color);
208
- out += paint(name, nameFg, bgRgb, color, Boolean(row.bin));
232
+ out += paint(name, valueFg, bgRgb, color, true);
209
233
  for (const field of VALUE_FIELDS) {
210
234
  const size = colW(cols, field);
211
235
  if (!size) continue;
@@ -219,14 +243,10 @@ const paintAgentRow = (row, width, color, selected, cols, search) => {
219
243
  out += paint(gap, valueFg, bgRgb, color);
220
244
  if (query && search.editor) {
221
245
  search.editor.reveal(size);
222
- const queryTones = {
223
- plain: { fg: THEME.searchFg, bg: THEME.searchBg },
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);
246
+ const typed = queryTones(THEME.warnFg, bgRgb);
247
+ out += paintField(query, search.editor, size, color, typed);
228
248
  } else if (query) {
229
- out += paint(face, THEME.searchFg, THEME.searchBg, color, true);
249
+ out += paint(face, THEME.warnFg, bgRgb, color, true);
230
250
  } else {
231
251
  out += paint(face, tone.fg, bgRgb, color, tone.bold);
232
252
  }
@@ -273,7 +293,7 @@ const overlayMenu = (painted, view, cols, gap, width, color, headerLines) => {
273
293
  const boxW = menuBoxWidth(options, columnW, width - x);
274
294
  if (x + boxW > width) x = Math.max(0, width - boxW);
275
295
  const origin = down ? rowAt + 1 : Math.max(0, rowAt - box.span);
276
- const faces = menuFaces(options, box, boxW, color, off);
296
+ const faces = menuFaces(options, box, boxW, color, view.agentQuery, off);
277
297
  const body = painted.body.slice();
278
298
  const fileHits = painted.fileHits.slice();
279
299
  for (let i = 0; i < faces.length && origin + i < body.length; i++) {
@@ -289,7 +309,13 @@ const overlayMenu = (painted, view, cols, gap, width, color, headerLines) => {
289
309
  field: 'menu',
290
310
  });
291
311
  }
292
- return { ...painted, body, fileHits, menuScroll: box.start };
312
+ const editHits = painted.editHits.slice();
313
+ if (view.agentEditor) {
314
+ const start = fieldX(cols, gap, field);
315
+ const hit = fieldHit(view.agentEditor, start, columnW, rowHit.y);
316
+ editHits.push({ ...hit, row: rowAt });
317
+ }
318
+ return { ...painted, body, fileHits, editHits, menuScroll: box.start };
293
319
  };
294
320
 
295
321
  const chooserSplit = (width) => {
@@ -501,16 +527,15 @@ const paintAgentLog = (view, width, color, bodyH) => {
501
527
  return log;
502
528
  };
503
529
 
504
- const clipRunHits = (hits, width) => {
505
- const last = Math.max(0, width - 1);
506
- return hits.map((hit) => {
507
- if (hit.side !== 'runs') return hit;
508
- const x1 = Math.min(hit.x1 ?? last, last);
509
- return { ...hit, x1 };
510
- });
511
- };
512
-
513
- const withRunScroller = (painted, count, start, bodyH, width, color) => {
530
+ const withRunScroller = (
531
+ painted,
532
+ count,
533
+ start,
534
+ bodyH,
535
+ width,
536
+ color,
537
+ runCursor,
538
+ ) => {
514
539
  const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
515
540
  const rows = Math.max(1, bodyH - pad * 2);
516
541
  const overflow = count > rows;
@@ -520,9 +545,11 @@ const withRunScroller = (painted, count, start, bodyH, width, color) => {
520
545
  let mark = '';
521
546
  if (overflow && onText) mark = scrollMark(at, count, rows, start);
522
547
  if (overflow && !onText) mark = 'track';
523
- return withLogScroller(line, mark, width, color);
548
+ const selected = onText && runCursor === start + at;
549
+ const rowBg = selected ? THEME.checkBg : null;
550
+ return withLogScroller(line, mark, width, color, rowBg);
524
551
  });
525
- const fileHits = clipRunHits(painted.fileHits, width);
552
+ const fileHits = painted.fileHits;
526
553
  if (!overflow) return { ...painted, body, fileHits };
527
554
  const scrollBar = {
528
555
  col: width,
@@ -599,6 +626,7 @@ const paintBodyAgents = (view, width, color, bodyH, headerLines) => {
599
626
  bodyH,
600
627
  width,
601
628
  color,
629
+ runCursor,
602
630
  );
603
631
  };
604
632
 
@@ -623,7 +651,7 @@ const agentName = (item) => {
623
651
 
624
652
  const agentsBlock = (model, width, height, ctx, tile) => {
625
653
  const data = model.agents;
626
- if (!data || !data.ready) return waiting('detecting clis…');
654
+ if (!data || !data.ready) return waiting('detecting agents…');
627
655
  const aside = [seg(`${data.running || 0}`, 'key', true)];
628
656
  const titleLine = titleAside(tile, aside, width);
629
657
  const items = data.items ?? [];
@@ -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 { MARK_W, markPrefix, branchLabel, editSpan } = primitives;
9
- const { paintCursorList, fieldWindow } = primitives;
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
- const paintBranchEdit = (compose, width, color) => {
96
- const prefix = markPrefix(true);
97
- const rest = Math.max(1, width - MARK_W);
98
- const text = `${compose.text ?? ''}`.replaceAll('\n', ' ');
99
- const win = fieldWindow(text, compose.cursor, compose.scrollCol, rest);
100
- const gap = ' '.repeat(Math.max(0, rest - visibleWidth(win.text)));
101
- const fgRgb = THEME.chromeFg;
102
- const bgRgb = THEME.buttonBg;
103
- let row = `${prefix}${win.text}${gap}`;
104
- if (color) {
105
- row = `${seq(fgRgb, bgRgb)}${EL}`;
106
- row += paint(prefix, fgRgb, bgRgb, color);
107
- row += paint(win.text, THEME.buttonHotFg, bgRgb, color);
108
- row += paint(gap, fgRgb, bgRgb, color);
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 branchEdit = compose?.kind === 'branch';
127
- const edit = branchEdit ? paintBranchEdit(compose, width, color) : null;
128
- const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
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
- return paintCursorList(branches, cursor, pane, paintRow, edit);
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 };
@@ -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
  };
@@ -296,8 +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 || !sizes.length) off.push('agentContext');
299
+ if (!installed || sizes.length < 2) off.push('agentContext');
300
300
  if ((view.agentPlanCount ?? 0) < 2) off.push('agentReview');
301
+ if (installed || !row || !row.installable) off.push('agentInstall');
302
+ if (!view.agentCanStart) off.push('agentStart');
301
303
  return off;
302
304
  };
303
305
 
@@ -330,14 +332,16 @@ const dimFooterActions = (view) => {
330
332
  const GIT_FOOTER = ['add', 'unstage', 'revert', 'ignore'];
331
333
 
332
334
  const FOOTER_EXTRA = {
333
- repos: ['theme'],
335
+ repos: ['theme', 'worktreeNew', 'worktreeDrop'],
334
336
  dashboard: ['theme', 'pull', 'push'],
335
337
  unit: ['add', 'unstage', 'revert', 'feedback', 'code', 'lines'],
336
338
  files: [...GIT_FOOTER, 'code'],
337
339
  diff: [...GIT_FOOTER, 'feedback', 'code', 'layout', 'lines'],
338
- branches: ['newBranch', 'rebase', 'drop', 'pull', 'push'],
340
+ branches: ['newBranch', 'worktreeBranch', 'rebase', 'drop', 'pull', 'push'],
341
+ trees: ['worktreeNew', 'worktreeDrop', 'pull', 'push'],
339
342
  commits: ['newCommit', 'apply', 'reword', 'drop', 'view', 'pull', 'push'],
340
343
  npmLog: ['npmVerbose', 'npmStop', 'npmRerun'],
344
+ npmFailedLog: ['npmVerbose', 'npmSolve', 'npmRerun'],
341
345
  npm: ['npmEdit', 'npmNew', 'npmDrop', 'npmLogs'],
342
346
  packages: ['packageNew', 'packageDrop', 'packageWanted', 'packageLatest'],
343
347
  agentLog: ['agentStop', 'agentRerun'],
@@ -347,13 +351,18 @@ const FOOTER_EXTRA = {
347
351
  'agentFast',
348
352
  'agentContext',
349
353
  'agentReview',
354
+ 'agentInstall',
355
+ 'agentStart',
350
356
  ],
351
357
  tasks: ['plan', 'import', 'drop', 'check', 'taskNudge'],
352
358
  };
353
359
 
354
360
  const extraFooterActions = (view) => {
355
361
  const { pane } = view;
356
- if (pane === 'npm' && view.npmView) return FOOTER_EXTRA.npmLog;
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
+ }
357
366
  if (pane === 'npm') return FOOTER_EXTRA.npm;
358
367
  if (pane === 'agents' && view.agentView) return FOOTER_EXTRA.agentLog;
359
368
  if (pane === 'agents') return FOOTER_EXTRA.agents;
@@ -380,4 +389,5 @@ module.exports = {
380
389
  paintFooter,
381
390
  footerHits,
382
391
  footerLayout,
392
+ dimFooterActions,
383
393
  };
@@ -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 = [...windowed.editHits, ...hits.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/log.js CHANGED
@@ -263,14 +263,15 @@ const paintLogRow = (view, lines, index, width, color) => {
263
263
  return fill(width, THEME.ctxFg, THEME.ctxBg, color);
264
264
  };
265
265
 
266
- const paintScrollCell = (mark, color) => {
267
- const bgRgb = mark === 'thumb' ? THEME.taskHeadBg : THEME.buttonBg;
266
+ const paintScrollCell = (mark, color, rowBg) => {
267
+ let bgRgb = rowBg ?? THEME.buttonBg;
268
+ if (mark === 'thumb') bgRgb = THEME.taskHeadBg;
268
269
  return paint(' ', THEME.chromeFg, bgRgb, color);
269
270
  };
270
271
 
271
- const withLogScroller = (line, mark, width, color) => {
272
+ const withLogScroller = (line, mark, width, color, rowBg) => {
272
273
  const text = clipAnsi(line, Math.max(0, width - 1));
273
- return `${text}${paintScrollCell(mark, color)}`;
274
+ return `${text}${paintScrollCell(mark, color, rowBg)}`;
274
275
  };
275
276
 
276
277
  const paintNpmLog = (view, width, color, bodyH) => {
@@ -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 menuItem = (label, boxW, color, selected, mark, disabled) => {
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 = paint(face, hotFg, bgRgb, color, hot);
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
- module.exports = { menuBoxWidth, dropBox, menuFaces };
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 };