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.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +163 -436
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  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/dir.js +50 -0
  19. package/lib/git/linked.js +152 -0
  20. package/lib/git/local.js +179 -0
  21. package/lib/git/status.js +126 -0
  22. package/lib/git/worktree.js +113 -36
  23. package/lib/input/actions.js +83 -5
  24. package/lib/input/keys.js +30 -1
  25. package/lib/input/line-edit.js +5 -1
  26. package/lib/render/agents.js +50 -24
  27. package/lib/render/branches.js +55 -27
  28. package/lib/render/field.js +19 -5
  29. package/lib/render/footer.js +14 -6
  30. package/lib/render/frame.js +16 -2
  31. package/lib/render/menu.js +103 -6
  32. package/lib/render/name-edit.js +31 -0
  33. package/lib/render/repos.js +52 -8
  34. package/lib/render/status.js +22 -4
  35. package/lib/report/run.js +16 -23
  36. package/lib/review/agent-instructions.md +2 -2
  37. package/lib/runs/process.js +3 -3
  38. package/lib/runs/runs.js +27 -4
  39. package/lib/session/agents/agents.js +220 -205
  40. package/lib/session/agents/away.js +48 -0
  41. package/lib/session/agents/job.js +23 -86
  42. package/lib/session/agents/jobs.js +14 -27
  43. package/lib/session/agents/options.js +45 -101
  44. package/lib/session/agents/plans.js +53 -0
  45. package/lib/session/agents/view.js +34 -0
  46. package/lib/session/branches.js +18 -1
  47. package/lib/session/compose.js +13 -5
  48. package/lib/session/dispatch.js +43 -4
  49. package/lib/session/editor-pointer.js +98 -0
  50. package/lib/session/files-pane.js +3 -0
  51. package/lib/session/lifecycle.js +67 -10
  52. package/lib/session/navigation.js +5 -0
  53. package/lib/session/npm/history.js +2 -1
  54. package/lib/session/npm/npm.js +89 -12
  55. package/lib/session/ops.js +3 -0
  56. package/lib/session/pointer.js +304 -46
  57. package/lib/session/session.js +15 -2
  58. package/lib/session/terminal.js +2 -2
  59. package/lib/session/text-menu.js +50 -0
  60. package/lib/session/view.js +19 -2
  61. package/lib/session/workspace.js +179 -38
  62. package/lib/session/worktrees.js +280 -0
  63. package/lib/term/themes/dark.json +4 -0
  64. package/lib/term/themes/light.json +4 -0
  65. package/package.json +2 -2
@@ -59,6 +59,7 @@ const NPM_ACTIONS = {
59
59
  npmDrop: ['delete'],
60
60
  npmLogs: ['l'],
61
61
  npmStop: ['s'],
62
+ npmSolve: ['s'],
62
63
  npmVerbose: ['v'],
63
64
  npmRerun: ['r'],
64
65
  };
@@ -69,7 +70,8 @@ const AGENT_ACTIONS = {
69
70
  agentFast: ['a'],
70
71
  agentContext: ['t'],
71
72
  agentReview: ['p'],
72
- agentContinue: ['c'],
73
+ agentInstall: ['i'],
74
+ agentStart: ['s'],
73
75
  agentStop: ['s'],
74
76
  agentRerun: ['r'],
75
77
  };
@@ -80,7 +82,8 @@ const AGENT_LIST_ACTIONS = {
80
82
  agentFast: AGENT_ACTIONS.agentFast,
81
83
  agentContext: AGENT_ACTIONS.agentContext,
82
84
  agentReview: AGENT_ACTIONS.agentReview,
83
- agentContinue: AGENT_ACTIONS.agentContinue,
85
+ agentInstall: AGENT_ACTIONS.agentInstall,
86
+ agentStart: AGENT_ACTIONS.agentStart,
84
87
  };
85
88
 
86
89
  const DASH_TILES = [
@@ -102,6 +105,11 @@ for (const tile of DASH_TILES) {
102
105
  DASH_KEYS[tile.key] = tile.action;
103
106
  }
104
107
  const DASH_IDS = Object.keys(DASH_ACTIONS);
108
+ const WORKTREE_ACTIONS = {
109
+ worktreeNew: ['insert'],
110
+ worktreeDrop: ['delete'],
111
+ worktreeBranch: ['w'],
112
+ };
105
113
  const DASH_ALLOWED = [
106
114
  ...Object.keys(DASH_BLOCKS),
107
115
  'back',
@@ -117,17 +125,28 @@ const REPOS_ALLOWED = [
117
125
  'repoDown',
118
126
  'open',
119
127
  'back',
128
+ 'worktreeNew',
129
+ 'worktreeDrop',
120
130
  'quit',
121
131
  'theme',
122
132
  ];
123
133
  const DASH_HIDDEN = ACTION_IDS.filter((id) => id !== 'quit');
124
134
 
125
135
  const ACTION_HOT = {
136
+ textCut: 'ctrl+x',
137
+ textCopy: 'ctrl+c',
138
+ textPaste: 'ctrl+v',
139
+ textDelete: 'del',
140
+ textSelectAll: 'ctrl+a',
141
+ open: 'enter',
126
142
  prev: '←',
127
143
  next: '→',
128
144
  drop: 'del',
145
+ worktreeDrop: 'del',
146
+ worktreeBranch: 'w',
129
147
  newBranch: 'ins',
130
148
  newCommit: 'ins',
149
+ worktreeNew: 'ins',
131
150
  feedback: 'q',
132
151
  check: 'space',
133
152
  taskUp: 'ctrl+up',
@@ -145,7 +164,13 @@ const ACTION_HOT = {
145
164
  };
146
165
 
147
166
  const ACTION_LABEL = {
167
+ textCut: 'ctrl+x cut',
168
+ textCopy: 'ctrl+c copy',
169
+ textPaste: 'ctrl+v paste',
170
+ textDelete: 'delete',
171
+ textSelectAll: 'ctrl+a select all',
148
172
  ...DASH_BLOCKS,
173
+ open: 'enter open',
149
174
  revert: 'drop',
150
175
  check: 'space',
151
176
  taskUp: 'ctrl+up',
@@ -163,6 +188,9 @@ const ACTION_LABEL = {
163
188
  drop: 'delete',
164
189
  newBranch: 'insert',
165
190
  newCommit: 'insert',
191
+ worktreeNew: 'insert',
192
+ worktreeDrop: 'delete',
193
+ worktreeBranch: 'worktree',
166
194
  feedback: 'quote',
167
195
  npmEdit: 'edit',
168
196
  npmNew: 'insert',
@@ -173,6 +201,7 @@ const ACTION_LABEL = {
173
201
  packageWanted: 'wanted',
174
202
  packageLatest: 'latest',
175
203
  npmStop: 'stop',
204
+ npmSolve: 'solve',
176
205
  npmVerbose: 'verbose',
177
206
  npmRerun: 're-run',
178
207
  agentModel: 'model',
@@ -180,7 +209,8 @@ const ACTION_LABEL = {
180
209
  agentFast: 'fast',
181
210
  agentContext: 'context',
182
211
  agentReview: 'plan',
183
- agentContinue: 'continue',
212
+ agentInstall: 'install',
213
+ agentStart: 'start',
184
214
  agentStop: 'stop',
185
215
  agentRerun: 're-run',
186
216
  quit: 'q',
@@ -387,7 +417,12 @@ const keysForActions = (actions) => {
387
417
  const PANE_ONLY = ['branch', 'file', 'diff'];
388
418
  const REVIEW_DROP = { d: 'revert' };
389
419
  const DIFF_KEYS = { ...REVIEW_DROP, q: 'feedback', 'ctrl-l': 'lines' };
390
- const BRANCH_KEYS = keysForActions(BRANCH_ACTIONS);
420
+ const BRANCH_KEYS = {
421
+ ...keysForActions(BRANCH_ACTIONS),
422
+ w: 'worktreeBranch',
423
+ left: 'treeLeft',
424
+ right: 'treeRight',
425
+ };
391
426
  const COMMIT_KEYS = { ...keysForActions(COMMIT_ACTIONS), s: 'push' };
392
427
  const NPM_KEYS = {
393
428
  e: 'npmEdit',
@@ -416,6 +451,8 @@ const PANE_KEYS = {
416
451
  j: 'repoDown',
417
452
  p: 'repoLeft',
418
453
  n: 'repoRight',
454
+ insert: 'worktreeNew',
455
+ delete: 'worktreeDrop',
419
456
  },
420
457
  };
421
458
 
@@ -430,7 +467,8 @@ const KEY_TO_ACTION = {
430
467
  };
431
468
 
432
469
  const actionFromKey = (key, pane) => {
433
- if (LIST_PANES.includes(pane) && (key === 'left' || key === 'right')) {
470
+ const sideways = key === 'left' || key === 'right';
471
+ if (LIST_PANES.includes(pane) && sideways && pane !== 'branches') {
434
472
  return null;
435
473
  }
436
474
  const paneKeys = PANE_KEYS[pane];
@@ -446,6 +484,7 @@ const ACTION_KEYS = {
446
484
  ...NPM_ACTIONS,
447
485
  ...COMMIT_ACTIONS,
448
486
  ...BRANCH_ACTIONS,
487
+ ...WORKTREE_ACTIONS,
449
488
  ...ACTIONS,
450
489
  };
451
490
 
@@ -453,6 +492,17 @@ const actionKeys = (id) => ACTION_KEYS[id] ?? [];
453
492
 
454
493
  const actionLetter = (id) => ACTION_HOT[id] ?? actionKeys(id)[0];
455
494
 
495
+ const ACTION_CAPTION = {
496
+ open: 'open',
497
+ check: 'check',
498
+ textCut: 'cut',
499
+ textCopy: 'copy',
500
+ textPaste: 'paste',
501
+ textSelectAll: 'select all',
502
+ };
503
+
504
+ const actionCaption = (id) => ACTION_CAPTION[id] ?? ACTION_LABEL[id] ?? id;
505
+
456
506
  const buttonWord = (id) => {
457
507
  const word = ACTION_LABEL[id] ?? ACTION_HOT[id] ?? id;
458
508
  const mark = actionLetter(id);
@@ -489,6 +539,15 @@ const CONFIRM = {
489
539
  escape: 'decline',
490
540
  },
491
541
  },
542
+ confirmAgentInstall: {
543
+ choices: YES_NO,
544
+ prefix: (view) => ` install ${view.agentInstallPackage ?? ''}?`,
545
+ keys: {
546
+ y: 'confirm',
547
+ n: 'cancel',
548
+ escape: 'cancel',
549
+ },
550
+ },
492
551
  confirmDrop: {
493
552
  choices: YES_NO,
494
553
  prefix: (view) => ` drop ${view.dropName ?? ''}?`,
@@ -498,6 +557,24 @@ const CONFIRM = {
498
557
  escape: 'cancel',
499
558
  },
500
559
  },
560
+ confirmWorktree: {
561
+ choices: YES_NO,
562
+ prefix: (view) => ` remove ${view.dropName ?? ''}?`,
563
+ keys: {
564
+ y: 'confirm',
565
+ n: 'cancel',
566
+ escape: 'cancel',
567
+ },
568
+ },
569
+ confirmCreateWorktree: {
570
+ choices: YES_NO,
571
+ prefix: (view) => ` create ${view.worktreeTarget ?? ''}?`,
572
+ keys: {
573
+ y: 'confirm',
574
+ n: 'cancel',
575
+ escape: 'cancel',
576
+ },
577
+ },
501
578
  confirmPush: {
502
579
  choices: [{ letter: 'f', label: 'force push', lead: ' ' }],
503
580
  prefix: ' Need force-push?',
@@ -574,6 +651,7 @@ module.exports = {
574
651
  isFixupCommit,
575
652
  actionFromKey,
576
653
  actionLetter,
654
+ actionCaption,
577
655
  buttonWord,
578
656
  CONFIRM,
579
657
  CONFIRM_MODE,
package/lib/input/keys.js CHANGED
@@ -45,10 +45,15 @@ const CSI_TILDE = {
45
45
  24: 'f12',
46
46
  };
47
47
 
48
+ // Kitty CSI-u and xterm modifyOtherKeys. 13 is Enter, 10 is line feed.
48
49
  const CSI_U = {
50
+ 10: 'enter',
51
+ 13: 'enter',
49
52
  127: 'backspace',
50
53
  };
51
54
 
55
+ const MODIFY_OTHER = '27';
56
+
52
57
  const CSI_MAX = 16;
53
58
 
54
59
  const LITERAL_KEY = {
@@ -80,6 +85,12 @@ const modifiedKey = (base, modText) => {
80
85
  const ctrl = (bits & 4) !== 0;
81
86
  const shift = (bits & 1) !== 0;
82
87
  if (base === 'backspace' && ctrl) return 'ctrl-backspace';
88
+ if (base === 'enter') {
89
+ if (ctrl && shift) return 'ctrl-shift-enter';
90
+ if (ctrl) return 'ctrl-enter';
91
+ if (shift) return 'shift-enter';
92
+ return 'enter';
93
+ }
83
94
  if (!arrow && !end) return base;
84
95
  if (ctrl && shift && arrow) return `ctrl-shift-${base}`;
85
96
  if (ctrl && arrow) return `ctrl-${base}`;
@@ -87,8 +98,19 @@ const modifiedKey = (base, modText) => {
87
98
  return base;
88
99
  };
89
100
 
101
+ const NEWLINE_KEYS = ['ctrl-enter', 'shift-enter', 'ctrl-shift-enter'];
102
+
103
+ const isNewlineKey = (key) => NEWLINE_KEYS.includes(key);
104
+
105
+ const otherModified = (params) => {
106
+ const base = CSI_U[params[2]];
107
+ if (!base) return null;
108
+ return modifiedKey(base, params[1] ?? '1');
109
+ };
110
+
90
111
  const keyFromCsi = (paramsText, final) => {
91
112
  const params = paramsText ? paramsText.split(';') : [];
113
+ if (final === '~' && params[0] === MODIFY_OTHER) return otherModified(params);
92
114
  let base = CSI_KEYS[final];
93
115
  if (final === '~') base = CSI_TILDE[params[0]];
94
116
  if (final === 'u') base = CSI_U[params[0]];
@@ -115,6 +137,7 @@ const mouseKind = (button, press) => {
115
137
  const wheel = MOUSE_WHEEL[button];
116
138
  if (wheel) return wheel;
117
139
  if (!press) return 'release';
140
+ if ((button & 35) === 35) return 'move';
118
141
  return button & 32 ? 'drag' : 'press';
119
142
  };
120
143
 
@@ -197,4 +220,10 @@ const hitAction = (hits, x) => {
197
220
  return null;
198
221
  };
199
222
 
200
- module.exports = { ACTIONS, decodeChunk, hitAction, isTextKey };
223
+ module.exports = {
224
+ ACTIONS,
225
+ decodeChunk,
226
+ hitAction,
227
+ isTextKey,
228
+ isNewlineKey,
229
+ };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { Editor } = require('./editor.js');
4
- const { isTextKey } = require('./keys.js');
4
+ const { isTextKey, isNewlineKey } = require('./keys.js');
5
5
  const clipboard = require('../term/clipboard.js');
6
6
 
7
7
  const copyLine = (editor, ui) => {
@@ -57,6 +57,10 @@ const lineKey = (editor, key, ui, ends) => {
57
57
  copyLine(editor, ui);
58
58
  return 'caret';
59
59
  }
60
+ if (isNewlineKey(key)) {
61
+ editor.insert('\n');
62
+ return 'text';
63
+ }
60
64
  const op = (ends ? LINE_ENDS : LINE_OP)[key];
61
65
  if (!op && !isTextKey(key)) return null;
62
66
  const before = editor.text;
@@ -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) => {
@@ -625,7 +651,7 @@ const agentName = (item) => {
625
651
 
626
652
  const agentsBlock = (model, width, height, ctx, tile) => {
627
653
  const data = model.agents;
628
- if (!data || !data.ready) return waiting('detecting clis…');
654
+ if (!data || !data.ready) return waiting('detecting agents…');
629
655
  const aside = [seg(`${data.running || 0}`, 'key', true)];
630
656
  const titleLine = titleAside(tile, aside, width);
631
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,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 || !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 (!view.agentCanContinue) off.push('agentContinue');
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
- 'agentContinue',
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 === '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
+ }
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
  };