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.
- package/CHANGELOG.md +17 -1
- package/README.md +16 -14
- package/bin/reslop.js +8 -5
- package/lib/agents/agents.js +504 -0
- package/lib/agents/allow.js +71 -0
- package/lib/agents/allow.json +16 -0
- package/lib/agents/models.js +460 -0
- package/lib/api.js +15 -12
- package/lib/cli.js +43 -68
- package/lib/{files.js → common/files.js} +48 -55
- package/lib/{find.js → common/find.js} +1 -1
- package/lib/common/format.js +101 -0
- package/lib/{utilities.js → common/process.js} +7 -95
- package/lib/common/tasks.json +27 -0
- package/lib/common/utilities.js +112 -0
- package/lib/dashboard/git.js +9 -16
- package/lib/dashboard/model.js +90 -49
- package/lib/dashboard/npm.js +17 -13
- package/lib/dashboard/tree.js +27 -30
- package/lib/{session → dashboard}/watch.js +36 -31
- package/lib/{workspace.js → dashboard/workspace.js} +23 -38
- package/lib/{deps.js → deps/deps.js} +31 -45
- package/lib/{deps-diff.js → deps/diff.js} +35 -25
- package/lib/{manifest.js → deps/manifest.js} +47 -30
- package/lib/diff/align.js +1 -2
- package/lib/diff/diff.js +2 -2
- package/lib/diff/display.js +29 -5
- package/lib/diff/inline.js +10 -14
- package/lib/diff/items.js +6 -4
- package/lib/diff/patch.js +39 -3
- package/lib/diff/unit.js +45 -16
- package/lib/{git-branches.js → git/branches.js} +53 -48
- package/lib/{git-deps.js → git/deps.js} +123 -96
- package/lib/git/git.js +71 -0
- package/lib/git/worktree.js +475 -0
- package/lib/highlight/bash.js +1 -2
- package/lib/highlight/csv.js +1 -2
- package/lib/highlight/dot.js +1 -2
- package/lib/{highlight.js → highlight/highlight.js} +42 -11
- package/lib/highlight/html.js +20 -18
- package/lib/highlight/json.js +1 -2
- package/lib/highlight/log.js +1 -2
- package/lib/highlight/txt.js +1 -2
- package/lib/{session → input}/actions.js +111 -48
- package/lib/{editor.js → input/editor.js} +7 -8
- package/lib/{keys.js → input/keys.js} +15 -43
- package/lib/input/line-edit.js +77 -0
- package/lib/input/menu-pick.js +70 -0
- package/lib/render/agents.js +640 -0
- package/lib/render/branches.js +4 -5
- package/lib/render/commits.js +54 -64
- package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
- package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
- package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
- package/lib/render/diff.js +166 -45
- package/lib/render/field.js +131 -0
- package/lib/render/files.js +9 -14
- package/lib/render/footer.js +383 -0
- package/lib/render/frame.js +123 -25
- package/lib/render/header.js +176 -0
- package/lib/render/log.js +319 -0
- package/lib/render/menu.js +60 -0
- package/lib/render/notes.js +16 -11
- package/lib/render/npm.js +322 -196
- package/lib/render/packages.js +9 -13
- package/lib/render/primitives.js +101 -15
- package/lib/render/render.js +7 -11
- package/lib/render/repos.js +13 -25
- package/lib/render/status.js +370 -0
- package/lib/render/status.json +85 -0
- package/lib/render/{dash-table.js → table.js} +13 -60
- package/lib/render/tiles.js +109 -48
- package/lib/render/unit.js +32 -13
- package/lib/{report-model.js → report/model.js} +4 -6
- package/lib/{report-parse.js → report/parse.js} +9 -14
- package/lib/{report-render.js → report/render.js} +46 -14
- package/lib/{report-run.js → report/run.js} +42 -30
- package/lib/{report-test.js → report/test.js} +56 -55
- package/lib/{report-tools.js → report/tools.js} +24 -33
- package/lib/review/agent-instructions.md +9 -0
- package/lib/review/format.js +426 -0
- package/lib/{review-merge.js → review/merge.js} +9 -8
- package/lib/review/review.js +190 -0
- package/lib/review/store.js +281 -0
- package/lib/review/templates.js +83 -0
- package/lib/runs/commands.js +14 -0
- package/lib/runs/logs.js +299 -0
- package/lib/runs/output.js +174 -0
- package/lib/runs/process.js +113 -0
- package/lib/{runs.js → runs/runs.js} +84 -36
- package/lib/runs/scripts.js +150 -0
- package/lib/session/agents/agents.js +958 -0
- package/lib/session/agents/job.js +320 -0
- package/lib/session/agents/jobs.js +355 -0
- package/lib/session/agents/options.js +153 -0
- package/lib/session/agents/plans.js +83 -0
- package/lib/session/branches.js +33 -39
- package/lib/session/change-actions.js +34 -100
- package/lib/session/commit.js +91 -52
- package/lib/session/compose.js +181 -145
- package/lib/session/dashboard.js +23 -22
- package/lib/session/dispatch.js +85 -21
- package/lib/session/files-pane.js +47 -26
- package/lib/session/find.js +16 -29
- package/lib/session/import.js +82 -0
- package/lib/session/items.js +8 -17
- package/lib/session/lifecycle.js +75 -72
- package/lib/session/load.js +2 -4
- package/lib/session/navigation.js +6 -3
- package/lib/session/npm/history.js +249 -0
- package/lib/session/npm/npm.js +815 -0
- package/lib/session/{packages.js → npm/packages.js} +21 -14
- package/lib/session/ops.js +125 -34
- package/lib/session/pointer.js +202 -10
- package/lib/session/prefs.js +112 -0
- package/lib/session/reads.js +109 -0
- package/lib/session/review.js +28 -10
- package/lib/session/run-panel.js +40 -0
- package/lib/{session.js → session/session.js} +156 -63
- package/lib/session/tasks.js +320 -65
- package/lib/session/templates.js +1 -1
- package/lib/session/terminal.js +94 -12
- package/lib/session/unit.js +35 -30
- package/lib/session/update.js +194 -6
- package/lib/session/view.js +65 -65
- package/lib/session/workspace.js +39 -78
- package/lib/{capabilities.js → source/capabilities.js} +3 -12
- package/lib/{github.js → source/github.js} +102 -32
- package/lib/{gitlab.js → source/gitlab.js} +87 -10
- package/lib/{remote.js → source/remote.js} +144 -40
- package/lib/source/review-threads.graphql +20 -0
- package/lib/source/source.js +74 -0
- package/lib/term/ansi.js +396 -0
- package/lib/{clipboard.js → term/clipboard.js} +3 -6
- package/lib/{select.js → term/select.js} +2 -2
- package/lib/term/themes/dark.json +122 -0
- package/lib/term/themes/light.json +122 -0
- package/lib/term/wide.txt +21 -0
- package/lib/{wrap.js → term/wrap.js} +4 -4
- package/package.json +2 -2
- package/lib/ansi.js +0 -545
- package/lib/detect.js +0 -34
- package/lib/diff/edit.js +0 -33
- package/lib/git-worktree.js +0 -455
- package/lib/git.js +0 -79
- package/lib/npm-commands.js +0 -522
- package/lib/render/chrome.js +0 -806
- package/lib/render/present.js +0 -19
- package/lib/render/report.js +0 -43
- package/lib/render/search.js +0 -25
- package/lib/review.js +0 -745
- package/lib/session/accessors.js +0 -26
- package/lib/session/edit-click.js +0 -57
- package/lib/session/npm.js +0 -452
- package/lib/session/progress.js +0 -41
- package/lib/source.js +0 -36
- package/lib/update.js +0 -192
- /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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;
|
package/lib/session/ops.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
3
|
+
const { errorMessage } = require('../common/utilities.js');
|
|
4
|
+
const { npmBusyKind } = require('./items.js');
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
335
|
+
module.exports = { OpsRunner };
|
package/lib/session/pointer.js
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const
|
|
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 {
|
|
7
|
-
const
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
const
|
|
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:
|
|
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 (
|
|
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 = {
|
|
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
|
+
};
|