reslop 0.2.7 → 0.3.0

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 (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +156 -63
  120. package/lib/session/tasks.js +320 -65
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,9 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const utilities = require('../utilities.js');
4
- const { IS_WIN, npmBin, oneLine, runProc } = utilities;
5
-
6
- const INSTALL_MS = 120000;
3
+ const {
4
+ IS_WIN,
5
+ npmBin,
6
+ runProc,
7
+ INSTALL_MS,
8
+ } = require('../../common/process.js');
9
+ const { oneLine } = require('../../common/utilities.js');
7
10
 
8
11
  class PackageController {
9
12
  constructor(ui) {
@@ -115,16 +118,20 @@ class PackageController {
115
118
  ui.ops.setBusy(busy);
116
119
  ui.ignoreWatch(INSTALL_MS);
117
120
  ui.paint();
118
- let result;
119
- try {
120
- const hook = ui.repo && ui.repo.runPackage;
121
- const options = { cwd: ui.top, shell: IS_WIN };
122
- result = hook
123
- ? await hook(ui.top, args)
124
- : await runProc(npmBin(), args, options);
125
- } catch (error) {
126
- result = { error, status: 1, stderr: '' };
127
- }
121
+ const run = async () => {
122
+ try {
123
+ const hook = ui.repo && ui.repo.runPackage;
124
+ const options = { cwd: ui.top, shell: IS_WIN };
125
+ const started = hook
126
+ ? hook(ui.top, args)
127
+ : runProc(npmBin(), args, options);
128
+ const result = await started;
129
+ return result;
130
+ } catch (error) {
131
+ return { error, status: 1, stderr: '' };
132
+ }
133
+ };
134
+ const result = await run();
128
135
  this.running = false;
129
136
  ui.ops.clearBusy();
130
137
  const failed = !result || result.error || result.status;
@@ -1,16 +1,17 @@
1
1
  'use strict';
2
2
 
3
- const ASYNC_WITHOUT_UI = ['pull', 'push'];
4
- const QUIET_KIND = new Set(['staged', 'unstaged']);
3
+ const { errorMessage } = require('../common/utilities.js');
4
+ const { npmBusyKind } = require('./items.js');
5
5
 
6
- const pickRepoWrite = (repo, name, uiOpen) => {
7
- const asyncFn = repo[`${name}Async`];
8
- const allowAsync = uiOpen === true || ASYNC_WITHOUT_UI.includes(name);
9
- const isAsync = allowAsync && typeof asyncFn === 'function';
10
- return { async: isAsync, write: isAsync ? asyncFn : repo[name] };
11
- };
6
+ const QUIET_KIND = ['staged', 'unstaged'];
7
+ const POLL_MS = 5;
12
8
 
13
- const showsBusy = (kind) => Boolean(kind) && !QUIET_KIND.has(kind);
9
+ const pause = (ms) =>
10
+ new Promise((resolve) => {
11
+ setTimeout(resolve, ms);
12
+ });
13
+
14
+ const showsBusy = (kind) => Boolean(kind) && !QUIET_KIND.includes(kind);
14
15
 
15
16
  class ChangeQueue {
16
17
  constructor(ui, runner) {
@@ -22,6 +23,7 @@ class ChangeQueue {
22
23
  this.draining = false;
23
24
  this.pumpQueued = false;
24
25
  this.holding = false;
26
+ this.long = false;
25
27
  this.resume = null;
26
28
  this.reloading = false;
27
29
  }
@@ -43,6 +45,7 @@ class ChangeQueue {
43
45
  this.draining = false;
44
46
  this.pumpQueued = false;
45
47
  this.holding = false;
48
+ this.long = false;
46
49
  this.reloading = false;
47
50
  this.noteFree();
48
51
  }
@@ -87,6 +90,7 @@ class ChangeQueue {
87
90
  async takeJob(stamp) {
88
91
  const job = this.jobs[this.head];
89
92
  this.head += 1;
93
+ this.long = showsBusy(job.kind);
90
94
  this.armBusy(job.kind);
91
95
  try {
92
96
  await job.write(this.ui.top);
@@ -101,12 +105,20 @@ class ChangeQueue {
101
105
  return job.reload ? 'reload' : 'ok';
102
106
  }
103
107
 
108
+ // Only writes that land in milliseconds are queued or running: none shows
109
+ // a spinner and none waits behind a long git operation
110
+ quick() {
111
+ const working = this.draining || this.pumpQueued;
112
+ if (!working || this.long || this.hasPendingBusy()) return false;
113
+ return this.holding || !this.runner.gitBusy;
114
+ }
115
+
104
116
  schedule() {
105
117
  if (this.draining || this.pumpQueued) return;
106
118
  this.pumpQueued = true;
107
119
  queueMicrotask(() => {
108
120
  this.pumpQueued = false;
109
- void this.run();
121
+ this.runner.track(this.run());
110
122
  });
111
123
  }
112
124
 
@@ -158,9 +170,89 @@ class OpsRunner {
158
170
  this.ui = ui;
159
171
  this.gitBusy = false;
160
172
  this.busy = '';
173
+ this.pending = new Set();
174
+ this.writing = Promise.resolve();
175
+ this.writers = 0;
176
+ this.holds = 0;
177
+ this.held = [];
161
178
  this.queue = new ChangeQueue(ui, this);
162
179
  }
163
180
 
181
+ track(promise) {
182
+ const settled = () => this.pending.delete(promise);
183
+ this.pending.add(promise);
184
+ promise.then(settled, settled);
185
+ return promise;
186
+ }
187
+
188
+ exclusive(write) {
189
+ const start = async () => write(this.ui.top);
190
+ const turn = this.writers ? this.writing.then(start) : start();
191
+ const release = () => {
192
+ this.writers -= 1;
193
+ };
194
+ this.writers += 1;
195
+ this.writing = turn.then(release, release);
196
+ return turn;
197
+ }
198
+
199
+ // Stages one item in its turn. An install can run for minutes, so it gets
200
+ // the lock to take around its own file and index steps instead
201
+ add(item) {
202
+ const { repo, top } = this.ui;
203
+ const locked = (write) => this.exclusive(write);
204
+ if (npmBusyKind(item)) return repo.add(top, item, { locked });
205
+ return locked((root) => repo.add(root, item));
206
+ }
207
+
208
+ unstage(item) {
209
+ return this.exclusive((top) => this.ui.repo.unstage(top, item));
210
+ }
211
+
212
+ // Input that arrives while a read is held waits for it, so keys keep order
213
+ hold(reading) {
214
+ const fail = (error) => {
215
+ this.ui.status = errorMessage(error);
216
+ };
217
+ const release = (value) => {
218
+ this.holds -= 1;
219
+ if (!this.holds) this.resume();
220
+ return value;
221
+ };
222
+ this.holds += 1;
223
+ return this.track(reading.catch(fail).then(release));
224
+ }
225
+
226
+ defer(event) {
227
+ if (!this.holds) return false;
228
+ this.held.push(event);
229
+ return true;
230
+ }
231
+
232
+ resume() {
233
+ const { ui, held } = this;
234
+ while (held.length && !this.holds && !ui.done) ui.handleEvent(held.shift());
235
+ if (ui.done) return void ui.finishInput();
236
+ ui.paint();
237
+ }
238
+
239
+ unlanded() {
240
+ return this.writers > 0 || this.queue.quick();
241
+ }
242
+
243
+ // Waits for quick repo writes to land, but never longer than the limit
244
+ async landed(limit) {
245
+ const deadline = Date.now() + limit;
246
+ while (this.unlanded() && Date.now() < deadline) await pause(POLL_MS);
247
+ }
248
+
249
+ async idle() {
250
+ await Promise.resolve();
251
+ while (this.pending.size || this.queue.pumpQueued) {
252
+ await Promise.allSettled([...this.pending]);
253
+ }
254
+ }
255
+
164
256
  setBusy(kind) {
165
257
  this.busy = kind;
166
258
  this.ui.startProgress();
@@ -174,21 +266,10 @@ class OpsRunner {
174
266
  reset() {
175
267
  this.gitBusy = false;
176
268
  this.busy = '';
269
+ this.held.length = 0;
177
270
  this.queue.reset();
178
271
  }
179
272
 
180
- runRepo(label, write) {
181
- const { ui } = this;
182
- try {
183
- write(ui.top);
184
- ui.status = label;
185
- return true;
186
- } catch (error) {
187
- ui.status = error.message;
188
- return false;
189
- }
190
- }
191
-
192
273
  finishBusy(done, after) {
193
274
  const { ui } = this;
194
275
  if (ui.done || ui.mode === 'confirmPush') return void ui.paint();
@@ -197,7 +278,11 @@ class OpsRunner {
197
278
  ui.paint();
198
279
  }
199
280
 
200
- async runBusy(kind, done, write, after) {
281
+ runBusy(kind, done, write, after) {
282
+ return this.track(this.busyWrite(kind, done, write, after));
283
+ }
284
+
285
+ async busyWrite(kind, done, write, after) {
201
286
  const { ui } = this;
202
287
  if (this.gitBusy) return;
203
288
  this.gitBusy = true;
@@ -223,17 +308,23 @@ class OpsRunner {
223
308
 
224
309
  runWrite(name, args, kind, done, after, leave = null) {
225
310
  const { ui } = this;
226
- const picked = pickRepoWrite(ui.repo, name, ui.uiOpen);
227
- const write = (top) => picked.write(top, ...args);
228
- if (picked.async) {
229
- if (leave) leave();
230
- const refresh = () => after({ doneStatus: done });
231
- return void this.runBusy(kind, done, write, refresh);
232
- }
233
- if (!this.runRepo(done, write)) return;
311
+ const write = (top) => ui.repo[name](top, ...args);
234
312
  if (leave) leave();
235
- after({});
236
- if (!ui.status) ui.status = done;
313
+ const refresh = () => after({ doneStatus: done });
314
+ return this.runBusy(kind, done, write, refresh);
315
+ }
316
+
317
+ async apply(label, write, after) {
318
+ const { ui } = this;
319
+ try {
320
+ await this.exclusive(write);
321
+ ui.status = label;
322
+ if (after) after();
323
+ return true;
324
+ } catch (error) {
325
+ ui.status = error.message;
326
+ return false;
327
+ }
237
328
  }
238
329
 
239
330
  enqueueLazy(job) {
@@ -241,4 +332,4 @@ class OpsRunner {
241
332
  }
242
333
  }
243
334
 
244
- module.exports = { pickRepoWrite, OpsRunner };
335
+ module.exports = { OpsRunner };
@@ -1,13 +1,53 @@
1
1
  'use strict';
2
2
 
3
- const select = require('../select.js');
4
- const editClick = require('./edit-click.js');
3
+ const { extractText, isRange } = require('../term/select.js');
4
+ const { scrollAtThumb, scrollThumb } = require('../render/primitives.js');
5
+ const { indexAtRow } = require('../term/wrap.js');
5
6
  const filesPane = require('./files-pane.js');
6
- const { extractText, isRange } = select;
7
- const keys = require('../keys.js');
8
- const { hitAction } = keys;
9
- const clipboard = require('../clipboard.js');
10
- const { copyText } = clipboard;
7
+ const { hitAction } = require('../input/keys.js');
8
+ const { copyText } = require('../term/clipboard.js');
9
+ const { stopImport } = require('./import.js');
10
+
11
+ const hitAt = (hits, cell) => {
12
+ for (const hit of hits) {
13
+ if (hit.y !== cell.y) continue;
14
+ const x0 = hit.x0 ?? hit.textX;
15
+ if (cell.x < x0) continue;
16
+ if (hit.x1 !== undefined && cell.x >= hit.x1) continue;
17
+ return hit;
18
+ }
19
+ return null;
20
+ };
21
+
22
+ const offsetOf = (hit, cellX) => {
23
+ const shown = cellX - hit.textX;
24
+ const col = Math.max(0, shown) + (hit.scroll || 0);
25
+ const at = indexAtRow(hit.start || 0, hit.text, col);
26
+ if (hit.limit !== undefined && at > hit.limit) return hit.limit;
27
+ return at;
28
+ };
29
+
30
+ const focusField = (ui, hit) => {
31
+ if (!hit.field || !ui.npm) return true;
32
+ if (hit.field === ui.npm.editField) return true;
33
+ return ui.npm.focusField(hit.field);
34
+ };
35
+
36
+ const placeClick = (ui, cell) => {
37
+ if (ui.mode !== 'compose' || !ui.editor) return false;
38
+ const hits = ui.lastFrame && ui.lastFrame.editHits;
39
+ if (!hits || !hits.length) return false;
40
+ const hit = hitAt(hits, cell);
41
+ if (!hit) return false;
42
+ if (!focusField(ui, hit)) {
43
+ ui.paint();
44
+ return true;
45
+ }
46
+ ui.editor.place(offsetOf(hit, cell.x));
47
+ ui.composer.syncCodeScroll();
48
+ ui.paint();
49
+ return true;
50
+ };
11
51
 
12
52
  const mouseCell = (ui, event) => {
13
53
  const size = ui.lastSize ?? ui.getSize();
@@ -16,9 +56,68 @@ const mouseCell = (ui, event) => {
16
56
  return { x, y };
17
57
  };
18
58
 
59
+ const onLogBar = (bar, cell) => {
60
+ if (!bar || cell.x !== bar.x) return false;
61
+ const index = cell.y - bar.y;
62
+ return index >= 0 && index < bar.height;
63
+ };
64
+
65
+ const logLocal = (bar, cell) => {
66
+ const local = cell.y - bar.y - bar.track;
67
+ if (local < 0) return 0;
68
+ const last = bar.rows - 1;
69
+ if (local > last) return last;
70
+ return local;
71
+ };
72
+
73
+ const logGrab = (local, thumb) => {
74
+ if (local <= thumb.at) return 0;
75
+ const end = thumb.at + thumb.size - 1;
76
+ if (local >= end) return thumb.size - 1;
77
+ return local - thumb.at;
78
+ };
79
+
80
+ const moveLogBar = (ui, cell) => {
81
+ const bar = ui.lastFrame && ui.lastFrame.scrollBar;
82
+ const drag = ui.nav.scrollDrag;
83
+ if (!bar || !drag) return;
84
+ const top = logLocal(bar, cell) - drag.grab;
85
+ const at = scrollAtThumb(top, drag.thumb);
86
+ const panel = ui.pane === 'npm' ? ui.npm : ui.agents;
87
+ if (panel.viewing) panel.placeLog(at);
88
+ else panel.placeRuns(at);
89
+ };
90
+
91
+ const startLogDrag = (ui, cell) => {
92
+ const bar = ui.lastFrame && ui.lastFrame.scrollBar;
93
+ if (!onLogBar(bar, cell)) return false;
94
+ const thumb = scrollThumb(bar.count, bar.rows, bar.start);
95
+ if (!thumb) return false;
96
+ const nav = ui.nav;
97
+ const local = logLocal(bar, cell);
98
+ nav.scrollDrag = { grab: logGrab(local, thumb), thumb };
99
+ nav.mouseAnchor = null;
100
+ nav.pendingClick = null;
101
+ nav.selection = null;
102
+ moveLogBar(ui, cell);
103
+ return true;
104
+ };
105
+
106
+ const dismissImport = (ui, event) => {
107
+ if (ui.mode !== 'import') return;
108
+ const frame = ui.lastFrame;
109
+ const cell = mouseCell(ui, event);
110
+ const onField = frame && cell.y === frame.height - 1;
111
+ if (onField) return;
112
+ stopImport(ui);
113
+ };
114
+
19
115
  const onPress = (ui, event) => {
116
+ dismissImport(ui, event);
20
117
  const nav = ui.nav;
21
118
  const cell = mouseCell(ui, event);
119
+ if (startLogDrag(ui, cell)) return;
120
+ nav.scrollDrag = null;
22
121
  nav.mouseAnchor = cell;
23
122
  nav.pendingClick = null;
24
123
  nav.selection = null;
@@ -30,11 +129,13 @@ const onPress = (ui, event) => {
30
129
 
31
130
  const onDrag = (ui, event) => {
32
131
  const nav = ui.nav;
132
+ const cell = mouseCell(ui, event);
133
+ if (nav.scrollDrag) return void moveLogBar(ui, cell);
33
134
  if (!nav.mouseAnchor) return;
34
135
  nav.pendingClick = null;
35
136
  nav.selection = {
36
137
  start: nav.mouseAnchor,
37
- end: mouseCell(ui, event),
138
+ end: cell,
38
139
  };
39
140
  };
40
141
 
@@ -89,12 +190,48 @@ const copySelection = (ui) => {
89
190
  ui.status = ok ? 'copied' : 'copy failed';
90
191
  };
91
192
 
193
+ const COMBO_FIELDS = ['model', 'effort', 'context', 'plan'];
194
+
195
+ const onAgentRunHit = (ui, hit) => {
196
+ ui.agents.focus = 'runs';
197
+ ui.agents.runCursor = hit.cursor;
198
+ const again = armDoubleClick(ui.nav, 'agent-runs', hit.cursor);
199
+ if (!again || ui.mode !== 'review') return;
200
+ const session = ui.agents.selectedSession();
201
+ if (ui.agents.openRunRow(session)) return;
202
+ ui.agents.start();
203
+ };
204
+
205
+ const onAgentHit = (ui, hit) => {
206
+ if (hit.side === 'runs') return void onAgentRunHit(ui, hit);
207
+ ui.agents.focusCli();
208
+ const changed = ui.nav.agentCursor !== hit.cursor;
209
+ ui.nav.agentCursor = hit.cursor;
210
+ if (changed) {
211
+ ui.agents.runCursor = 0;
212
+ ui.agents.runScroll = 0;
213
+ }
214
+ const combo = COMBO_FIELDS.includes(hit.field);
215
+ if (combo) return void ui.agents.openPick(hit.field);
216
+ if (hit.field === 'fast') {
217
+ ui.agents.toggleFast();
218
+ return;
219
+ }
220
+ const again = armDoubleClick(ui.nav, 'agents', hit.cursor);
221
+ if (again && ui.mode === 'review') ui.agents.start();
222
+ };
223
+
92
224
  const RELEASE_TARGETS = [
93
225
  {
94
226
  match: () => true,
95
227
  hits: 'statusHits',
96
228
  onHit: (ui, hit) => {
97
229
  if (hit.id === 'branch') return void ui.gitBranches.onBranch();
230
+ if (hit.id === 'plan') return void ui.composer.tasks.togglePlanMenu();
231
+ if (hit.id === 'plan-item') {
232
+ return void ui.composer.tasks.choosePlan(hit.cursor);
233
+ }
234
+ if (ui.composer.tasks.planOpen) ui.composer.tasks.closePlanMenu();
98
235
  ui.confirmChoice(hit.id);
99
236
  },
100
237
  },
@@ -145,12 +282,26 @@ const RELEASE_TARGETS = [
145
282
  ui.nav.packagesCursor = hit.cursor;
146
283
  },
147
284
  },
285
+ {
286
+ match: (ui) => ui.pane === 'agents',
287
+ hits: 'fileHits',
288
+ onHit: onAgentHit,
289
+ },
148
290
  {
149
291
  match: (ui) => ui.pane === 'npm',
150
292
  hits: 'fileHits',
151
293
  onHit: (ui, hit) => {
294
+ if (hit.side === 'runs') {
295
+ ui.npm.runCursor = hit.cursor;
296
+ ui.npm.focusRuns();
297
+ const again = armDoubleClick(ui.nav, 'npm-runs', hit.cursor);
298
+ if (again && ui.mode === 'review') ui.npm.run();
299
+ return;
300
+ }
301
+ ui.npm.focusCommands();
152
302
  const again = armDoubleClick(ui.nav, 'npm', hit.cursor);
153
303
  ui.nav.npmCursor = hit.cursor;
304
+ ui.npm.clampRuns();
154
305
  if (again && ui.mode === 'review') ui.npm.run();
155
306
  },
156
307
  },
@@ -169,9 +320,45 @@ const RELEASE_TARGETS = [
169
320
  },
170
321
  ];
171
322
 
323
+ const frameHits = (ui, accept) => (ui.lastFrame?.fileHits ?? []).filter(accept);
324
+
325
+ const switchAgentCombo = (ui, cell) => {
326
+ const hits = frameHits(ui, (hit) => COMBO_FIELDS.includes(hit.field));
327
+ return clickHit(ui, hits, cell, (ui, hit) => {
328
+ const same =
329
+ ui.agents.pick.field === hit.field && ui.nav.agentCursor === hit.cursor;
330
+ if (same) {
331
+ ui.agents.closePick();
332
+ return;
333
+ }
334
+ ui.agents.dismissPick();
335
+ ui.agents.focusCli();
336
+ ui.nav.agentCursor = hit.cursor;
337
+ ui.agents.openPick(hit.field);
338
+ if (!ui.agents.pick) ui.paint();
339
+ });
340
+ };
341
+
342
+ const releaseAgentMenu = (ui, cell) => {
343
+ const hits = frameHits(ui, (hit) => hit.field === 'menu');
344
+ const accept = (ui, hit) => ui.agents.acceptPick(hit.cursor);
345
+ if (clickHit(ui, hits, cell, accept)) return;
346
+ if (switchAgentCombo(ui, cell)) return;
347
+ ui.agents.closePick();
348
+ };
349
+
172
350
  const onRelease = (ui, event) => {
351
+ dismissImport(ui, event);
173
352
  const nav = ui.nav;
174
353
  const cell = mouseCell(ui, event);
354
+ if (nav.scrollDrag) {
355
+ moveLogBar(ui, cell);
356
+ nav.scrollDrag = null;
357
+ nav.mouseAnchor = null;
358
+ nav.pendingClick = null;
359
+ nav.selection = null;
360
+ return;
361
+ }
175
362
  if (nav.mouseAnchor) {
176
363
  nav.selection = { start: nav.mouseAnchor, end: cell };
177
364
  }
@@ -179,7 +366,10 @@ const onRelease = (ui, event) => {
179
366
  nav.pendingClick = null;
180
367
  nav.mouseAnchor = null;
181
368
  if (clickButton(ui, cell, clickId)) return;
182
- if (editClick.placeClick(ui, cell)) return;
369
+ if (placeClick(ui, cell)) return;
370
+ if (ui.pane === 'agents' && ui.agents.pick) {
371
+ return void releaseAgentMenu(ui, cell);
372
+ }
183
373
  const frame = ui.lastFrame;
184
374
  for (const target of RELEASE_TARGETS) {
185
375
  if (!target.match(ui)) continue;
@@ -207,4 +397,6 @@ const handlePointer = (ui, event) => {
207
397
  handler(ui, event);
208
398
  };
209
399
 
210
- module.exports = { handlePointer };
400
+ module.exports = {
401
+ handlePointer,
402
+ };
@@ -0,0 +1,112 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { isHashObject } = require('metautil');
6
+ const { THEME_NAMES } = require('../term/ansi.js');
7
+ const { REVIEW_DIR } = require('../common/files.js');
8
+ const { readJson, writeJson } = require('../common/utilities.js');
9
+
10
+ const PREFS = '.reslop';
11
+
12
+ const LAYOUT_LABEL = {
13
+ unified: 'unified',
14
+ mixed: 'mixed',
15
+ side: 'side-by-side',
16
+ };
17
+
18
+ const LAYOUT_VALUE = {
19
+ unified: 'unified',
20
+ mixed: 'mixed',
21
+ 'side-by-side': 'side',
22
+ };
23
+
24
+ const prefsFile = (root) => path.join(root, PREFS);
25
+
26
+ const planPath = (root, name) => path.join(root, REVIEW_DIR, name);
27
+
28
+ const readPrefs = (root) => {
29
+ const data = readJson(prefsFile(root));
30
+ if (!isHashObject(data)) return { agents: {} };
31
+ const agents = isHashObject(data.agents) ? data.agents : {};
32
+ return { ...data, agents };
33
+ };
34
+
35
+ const writePrefs = (root, data) => {
36
+ const stored = { ...data };
37
+ delete stored.runs;
38
+ try {
39
+ fs.writeFileSync(prefsFile(root), `${JSON.stringify(stored, null, 2)}\n`);
40
+ } catch {
41
+ // The repo root can be read-only.
42
+ }
43
+ };
44
+
45
+ const section = (data, name) => {
46
+ const value = data[name];
47
+ if (!isHashObject(value)) return {};
48
+ return { ...value };
49
+ };
50
+
51
+ const readUiPrefs = (root) => {
52
+ const data = readPrefs(root);
53
+ const editor = section(data, 'editor');
54
+ const general = section(data, 'general');
55
+ const commits = section(data, 'commits');
56
+ let lineNumbers = false;
57
+ if (typeof editor.lineNumbers === 'boolean') {
58
+ lineNumbers = editor.lineNumbers;
59
+ } else if (typeof data.lineNumbers === 'boolean') {
60
+ lineNumbers = data.lineNumbers;
61
+ }
62
+ const layout = LAYOUT_VALUE[editor.mode] || 'unified';
63
+ const theme = THEME_NAMES.includes(general.theme) ? general.theme : 'dark';
64
+ const commitView = commits.view === 'full' ? 'full' : 'brief';
65
+ return { lineNumbers, layout, theme, commitView };
66
+ };
67
+
68
+ const saveUiPrefs = (root, patch) => {
69
+ if (!root) return;
70
+ const data = readPrefs(root);
71
+ const editor = section(data, 'editor');
72
+ const legacy = typeof data.lineNumbers === 'boolean';
73
+ if (legacy && editor.lineNumbers === undefined) {
74
+ editor.lineNumbers = data.lineNumbers;
75
+ }
76
+ delete data.lineNumbers;
77
+ if (patch.lineNumbers !== undefined) {
78
+ editor.lineNumbers = patch.lineNumbers === true;
79
+ }
80
+ if (patch.layout) editor.mode = LAYOUT_LABEL[patch.layout] || 'unified';
81
+ const next = { ...data, editor };
82
+ if (patch.theme) {
83
+ const general = section(data, 'general');
84
+ general.theme = patch.theme;
85
+ next.general = general;
86
+ }
87
+ if (patch.commitView) {
88
+ const commits = section(data, 'commits');
89
+ const full = patch.commitView === 'full';
90
+ commits.view = full ? 'full' : 'brief';
91
+ next.commits = commits;
92
+ }
93
+ writePrefs(root, next);
94
+ };
95
+
96
+ const writeStored = (file, data) => {
97
+ try {
98
+ writeJson(file, data);
99
+ } catch {
100
+ // The plan directory can be read-only.
101
+ }
102
+ };
103
+
104
+ module.exports = {
105
+ LAYOUT_LABEL,
106
+ planPath,
107
+ readPrefs,
108
+ writePrefs,
109
+ readUiPrefs,
110
+ saveUiPrefs,
111
+ writeStored,
112
+ };