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.
- package/CHANGELOG.md +29 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +166 -370
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -13
- package/lib/agents/models.js +250 -376
- package/lib/agents/registry.js +437 -0
- package/lib/agents/run.js +234 -0
- package/lib/agents/transcript.js +143 -0
- package/lib/cli.js +2 -1
- package/lib/common/process.js +26 -1
- package/lib/dashboard/git.js +11 -1
- package/lib/dashboard/reports.js +80 -0
- package/lib/dashboard/watch.js +43 -8
- package/lib/dashboard/workspace.js +19 -20
- package/lib/dashboard/worktrees.js +145 -0
- package/lib/git/deps.js +29 -0
- package/lib/git/dir.js +50 -0
- package/lib/git/git.js +1 -1
- package/lib/git/linked.js +152 -0
- package/lib/git/local.js +179 -0
- package/lib/git/status.js +126 -0
- package/lib/git/worktree.js +137 -31
- package/lib/input/actions.js +83 -2
- package/lib/input/editor.js +9 -0
- package/lib/input/keys.js +39 -3
- package/lib/input/line-edit.js +8 -3
- package/lib/render/agents.js +64 -36
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -4
- package/lib/render/frame.js +16 -2
- package/lib/render/log.js +5 -4
- package/lib/render/menu.js +103 -6
- package/lib/render/name-edit.js +31 -0
- package/lib/render/repos.js +52 -8
- package/lib/render/status.js +22 -4
- package/lib/report/render.js +4 -7
- package/lib/report/run.js +16 -23
- package/lib/review/agent-instructions.md +13 -8
- package/lib/review/format.js +0 -6
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +227 -194
- package/lib/session/agents/away.js +48 -0
- package/lib/session/agents/job.js +23 -86
- package/lib/session/agents/jobs.js +14 -27
- package/lib/session/agents/options.js +45 -101
- package/lib/session/agents/plans.js +53 -0
- package/lib/session/agents/view.js +34 -0
- package/lib/session/branches.js +18 -2
- package/lib/session/compose.js +20 -6
- package/lib/session/dispatch.js +43 -3
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/import.js +12 -1
- package/lib/session/lifecycle.js +70 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +3 -1
- package/lib/session/npm/npm.js +111 -13
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +17 -2
- package/lib/session/tasks.js +1 -1
- package/lib/session/terminal.js +2 -2
- package/lib/session/text-menu.js +50 -0
- package/lib/session/view.js +19 -2
- package/lib/session/workspace.js +179 -38
- package/lib/session/worktrees.js +280 -0
- package/lib/term/select.js +2 -1
- package/lib/term/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
package/lib/session/npm/npm.js
CHANGED
|
@@ -30,6 +30,8 @@ const { historyName, historyMark, countText, elapsedText } = historyPart;
|
|
|
30
30
|
const { whenLabel, runLabel, logBase, applyExternal } = historyPart;
|
|
31
31
|
const { recordedItem, sameWindow, noteTests, filteredOutput } = historyPart;
|
|
32
32
|
const { size } = require('../../common/format.js');
|
|
33
|
+
const { TASKS_FILE } = require('../../common/files.js');
|
|
34
|
+
const { stripAnsi } = require('../../term/ansi.js');
|
|
33
35
|
|
|
34
36
|
const NAME_RE = /^[\w:.-]+$/;
|
|
35
37
|
|
|
@@ -54,11 +56,30 @@ class NpmController extends RunPanel {
|
|
|
54
56
|
this.reset();
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// Runs belong to the worktree they started in and go on while another
|
|
60
|
+
// one is shown
|
|
61
|
+
get runs() {
|
|
62
|
+
let list = this.byTop.get(this.ui.top);
|
|
63
|
+
if (!list) {
|
|
64
|
+
list = [];
|
|
65
|
+
this.byTop.set(this.ui.top, list);
|
|
66
|
+
}
|
|
67
|
+
return list;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
set runs(list) {
|
|
71
|
+
this.byTop.set(this.ui.top, list);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
everyRun() {
|
|
75
|
+
return [...this.byTop.values()].flat();
|
|
76
|
+
}
|
|
77
|
+
|
|
57
78
|
reset() {
|
|
58
|
-
for (const run of this.
|
|
79
|
+
for (const run of this.byTop ? this.everyRun() : []) {
|
|
59
80
|
if (run.status === 'running' && run.child) run.child.kill();
|
|
60
81
|
}
|
|
61
|
-
this.
|
|
82
|
+
this.byTop = new Map();
|
|
62
83
|
this.nextId = 1;
|
|
63
84
|
this.viewId = 0;
|
|
64
85
|
this.focus = 'commands';
|
|
@@ -83,7 +104,28 @@ class NpmController extends RunPanel {
|
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
get running() {
|
|
86
|
-
return this.
|
|
107
|
+
return this.everyRun().some((run) => run.status === 'running');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The count of runs in work by the directory they started in
|
|
111
|
+
liveByRoot() {
|
|
112
|
+
const live = new Map();
|
|
113
|
+
for (const run of this.everyRun()) {
|
|
114
|
+
if (run.status !== 'running') continue;
|
|
115
|
+
live.set(run.top, (live.get(run.top) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
return live;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Another worktree is shown: its commands and runs replace these
|
|
121
|
+
retarget() {
|
|
122
|
+
this.viewing = false;
|
|
123
|
+
this.verbose = false;
|
|
124
|
+
this.viewId = 0;
|
|
125
|
+
this.runCursor = 0;
|
|
126
|
+
this.commands = [];
|
|
127
|
+
this.dropName = '';
|
|
128
|
+
this.logBytes = 0;
|
|
87
129
|
}
|
|
88
130
|
|
|
89
131
|
view(active) {
|
|
@@ -103,6 +145,7 @@ class NpmController extends RunPanel {
|
|
|
103
145
|
npmDraftName: this.draftName,
|
|
104
146
|
npmDraftCommand: this.draftCommand,
|
|
105
147
|
npmRunning: this.viewedRunning(),
|
|
148
|
+
npmCanSolve: this.canSolve(),
|
|
106
149
|
npmLogs: active ? this.logLabel : '',
|
|
107
150
|
};
|
|
108
151
|
}
|
|
@@ -173,6 +216,39 @@ class NpmController extends RunPanel {
|
|
|
173
216
|
return Boolean(run && run.status === 'running');
|
|
174
217
|
}
|
|
175
218
|
|
|
219
|
+
canSolve() {
|
|
220
|
+
const run = this.viewedRun();
|
|
221
|
+
if (!this.viewing || !run || run.status !== 'exited') return false;
|
|
222
|
+
return run.exit !== 0 && run.exit !== '0' && run.exit !== '';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
solve() {
|
|
226
|
+
const ui = this.ui;
|
|
227
|
+
if (!this.canSolve() || !ui.review.store) return;
|
|
228
|
+
const run = this.viewedRun();
|
|
229
|
+
this.hydrate(run);
|
|
230
|
+
this.syncOutput(run);
|
|
231
|
+
const root = run.top ?? ui.top;
|
|
232
|
+
const output = stripAnsi(
|
|
233
|
+
run.raw
|
|
234
|
+
? filteredOutput(run.raw, root, run.exit).output
|
|
235
|
+
: reduceOutput(run.output, root, ''),
|
|
236
|
+
).trim();
|
|
237
|
+
const limit = 6000;
|
|
238
|
+
const shortened =
|
|
239
|
+
output.length > limit
|
|
240
|
+
? `${output.slice(0, limit)}\n… (truncated)`
|
|
241
|
+
: output;
|
|
242
|
+
const command = run.command || run.label || runLabel(run);
|
|
243
|
+
const text = `${command} failed (exit ${run.exit})\n\n${shortened}`;
|
|
244
|
+
const task = ui.review.store.addTask(TASKS_FILE, text, false, 'bugs');
|
|
245
|
+
this.closeView();
|
|
246
|
+
const tasks = ui.composer.tasks;
|
|
247
|
+
tasks.openTasksPage();
|
|
248
|
+
tasks.followRow({ task });
|
|
249
|
+
ui.flushReview(true);
|
|
250
|
+
}
|
|
251
|
+
|
|
176
252
|
historyRuns() {
|
|
177
253
|
const runs = this.runs.filter((run) => !launchedReslop(run));
|
|
178
254
|
return foldSteps(runs).slice(0, RUN_KEEP);
|
|
@@ -239,6 +315,7 @@ class NpmController extends RunPanel {
|
|
|
239
315
|
const run = newRun(this.nextId, {
|
|
240
316
|
name: item.name,
|
|
241
317
|
kind: item.kind,
|
|
318
|
+
script: item.script ?? '',
|
|
242
319
|
command: item.command ?? '',
|
|
243
320
|
label: item.kind === 'command' ? historyName(item) : runLabel(item),
|
|
244
321
|
status: item.status,
|
|
@@ -248,6 +325,7 @@ class NpmController extends RunPanel {
|
|
|
248
325
|
logName: item.logName,
|
|
249
326
|
rawName: item.rawName,
|
|
250
327
|
externalId: item.externalId || '',
|
|
328
|
+
top: this.ui.top,
|
|
251
329
|
result: item.result ?? null,
|
|
252
330
|
progress: item.progress ?? null,
|
|
253
331
|
saved: true,
|
|
@@ -321,6 +399,25 @@ class NpmController extends RunPanel {
|
|
|
321
399
|
this.runs = [...done, ...running];
|
|
322
400
|
}
|
|
323
401
|
|
|
402
|
+
followAdded(added) {
|
|
403
|
+
const current = this.viewedRun();
|
|
404
|
+
if (!this.viewing || !current?.externalId || !current.script) return false;
|
|
405
|
+
if (current.followEnd === false) return false;
|
|
406
|
+
const next = added
|
|
407
|
+
.filter(
|
|
408
|
+
(run) =>
|
|
409
|
+
run.script === current.script &&
|
|
410
|
+
run.startedAt >= current.startedAt &&
|
|
411
|
+
sameWindow(current, run),
|
|
412
|
+
)
|
|
413
|
+
.sort((left, right) => left.startedAt - right.startedAt)
|
|
414
|
+
.at(-1);
|
|
415
|
+
if (!next) return false;
|
|
416
|
+
this.viewId = next.id;
|
|
417
|
+
this.focusRun(next);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
324
421
|
followExternal() {
|
|
325
422
|
this.syncProgress();
|
|
326
423
|
if (this.ui.nav.pane === 'npm') this.ui.paint();
|
|
@@ -337,8 +434,9 @@ class NpmController extends RunPanel {
|
|
|
337
434
|
const added = [];
|
|
338
435
|
let changed = this.absorbRecorded(recorded, this.knownLogs(), added);
|
|
339
436
|
if (this.settleLost(recorded)) changed = true;
|
|
340
|
-
if (this.hydrate(this.viewedRun())) changed = true;
|
|
341
437
|
this.storeAdded(added);
|
|
438
|
+
if (this.followAdded(added)) changed = true;
|
|
439
|
+
if (this.hydrate(this.viewedRun())) changed = true;
|
|
342
440
|
if (changed) this.followExternal();
|
|
343
441
|
}
|
|
344
442
|
|
|
@@ -487,8 +585,9 @@ class NpmController extends RunPanel {
|
|
|
487
585
|
if (!run.stale) return false;
|
|
488
586
|
run.stale = false;
|
|
489
587
|
run.shownAt = Date.now();
|
|
490
|
-
|
|
491
|
-
|
|
588
|
+
const top = run.top ?? this.ui.top;
|
|
589
|
+
noteTests(run, this.byTop.get(top) ?? this.runs, run.raw);
|
|
590
|
+
run.output = filteredOutput(run.raw, top, '').output;
|
|
492
591
|
return true;
|
|
493
592
|
}
|
|
494
593
|
|
|
@@ -500,25 +599,23 @@ class NpmController extends RunPanel {
|
|
|
500
599
|
|
|
501
600
|
async finishRun(run, entry, result) {
|
|
502
601
|
const ui = this.ui;
|
|
602
|
+
const top = run.top ?? ui.top;
|
|
503
603
|
const stopped = run.stopping === true;
|
|
504
604
|
const raw = stopped ? withNotice(run.raw, 'terminated') : result.text;
|
|
505
605
|
let slot = null;
|
|
506
606
|
try {
|
|
507
|
-
slot = nextLogFile(
|
|
607
|
+
slot = nextLogFile(top, entry.name);
|
|
508
608
|
} catch (error) {
|
|
509
609
|
ui.status = error.message;
|
|
510
610
|
}
|
|
511
611
|
const rawFile = slot ? slot.rawName : '';
|
|
512
612
|
const stoppedOutput = () => {
|
|
513
|
-
const output = withNotice(
|
|
514
|
-
reduceOutput(run.raw, ui.top, ''),
|
|
515
|
-
'terminated',
|
|
516
|
-
);
|
|
613
|
+
const output = withNotice(reduceOutput(run.raw, top, ''), 'terminated');
|
|
517
614
|
return { output, log: output };
|
|
518
615
|
};
|
|
519
616
|
const filtered = stopped
|
|
520
617
|
? stoppedOutput()
|
|
521
|
-
: filteredOutput(result.text,
|
|
618
|
+
: filteredOutput(result.text, top, result.status, rawFile);
|
|
522
619
|
run.raw = raw;
|
|
523
620
|
run.stale = false;
|
|
524
621
|
run.exit = stopped ? '' : result.status;
|
|
@@ -555,6 +652,7 @@ class NpmController extends RunPanel {
|
|
|
555
652
|
exit: '',
|
|
556
653
|
startedAt: Date.now(),
|
|
557
654
|
endedAt: 0,
|
|
655
|
+
top: ui.top,
|
|
558
656
|
});
|
|
559
657
|
this.nextId += 1;
|
|
560
658
|
this.runs.push(run);
|
|
@@ -638,7 +736,7 @@ class NpmController extends RunPanel {
|
|
|
638
736
|
}
|
|
639
737
|
|
|
640
738
|
stopAll() {
|
|
641
|
-
for (const run of this.
|
|
739
|
+
for (const run of this.everyRun()) this.stop(run);
|
|
642
740
|
}
|
|
643
741
|
|
|
644
742
|
toggleVerbose() {
|
package/lib/session/ops.js
CHANGED
|
@@ -154,6 +154,7 @@ class ChangeQueue {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
enqueue(job) {
|
|
157
|
+
this.runner.writeEpoch += 1;
|
|
157
158
|
if (this.reloading) {
|
|
158
159
|
this.ui.loader.bump();
|
|
159
160
|
this.reloading = false;
|
|
@@ -170,6 +171,7 @@ class OpsRunner {
|
|
|
170
171
|
this.ui = ui;
|
|
171
172
|
this.gitBusy = false;
|
|
172
173
|
this.busy = '';
|
|
174
|
+
this.writeEpoch = 0;
|
|
173
175
|
this.pending = new Set();
|
|
174
176
|
this.writing = Promise.resolve();
|
|
175
177
|
this.writers = 0;
|
|
@@ -186,6 +188,7 @@ class OpsRunner {
|
|
|
186
188
|
}
|
|
187
189
|
|
|
188
190
|
exclusive(write) {
|
|
191
|
+
this.writeEpoch += 1;
|
|
189
192
|
const start = async () => write(this.ui.top);
|
|
190
193
|
const turn = this.writers ? this.writing.then(start) : start();
|
|
191
194
|
const release = () => {
|
package/lib/session/pointer.js
CHANGED
|
@@ -2,52 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
const { extractText, isRange } = require('../term/select.js');
|
|
4
4
|
const { scrollAtThumb, scrollThumb } = require('../render/primitives.js');
|
|
5
|
-
const
|
|
5
|
+
const editorPointer = require('./editor-pointer.js');
|
|
6
|
+
const { hitAt, offsetOf, focusField, beginEditDrag, moveEditDrag } =
|
|
7
|
+
editorPointer;
|
|
6
8
|
const filesPane = require('./files-pane.js');
|
|
7
9
|
const { hitAction } = require('../input/keys.js');
|
|
10
|
+
const { actionLetter, disabledActions } = require('../input/actions.js');
|
|
11
|
+
const { dimFooterActions } = require('../render/footer.js');
|
|
12
|
+
const textMenu = require('./text-menu.js');
|
|
13
|
+
const { TEXT_ACTIONS, textEditor, textDisabled, runTextAction } = textMenu;
|
|
8
14
|
const { copyText } = require('../term/clipboard.js');
|
|
9
15
|
const { stopImport } = require('./import.js');
|
|
10
16
|
|
|
11
|
-
const
|
|
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
|
-
};
|
|
17
|
+
const COMBO_FIELDS = ['model', 'effort', 'context', 'plan'];
|
|
51
18
|
|
|
52
19
|
const mouseCell = (ui, event) => {
|
|
53
20
|
const size = ui.lastSize ?? ui.getSize();
|
|
@@ -112,10 +79,278 @@ const dismissImport = (ui, event) => {
|
|
|
112
79
|
stopImport(ui);
|
|
113
80
|
};
|
|
114
81
|
|
|
82
|
+
const CONTEXT_ACTIONS = {
|
|
83
|
+
repos: ['open', 'worktreeNew', 'worktreeDrop'],
|
|
84
|
+
files: ['open', 'add', 'unstage', 'revert', 'ignore', 'code'],
|
|
85
|
+
branch: [
|
|
86
|
+
'open',
|
|
87
|
+
'newBranch',
|
|
88
|
+
'worktreeBranch',
|
|
89
|
+
'rebase',
|
|
90
|
+
'drop',
|
|
91
|
+
'pull',
|
|
92
|
+
'push',
|
|
93
|
+
],
|
|
94
|
+
tree: ['open', 'worktreeNew', 'worktreeDrop'],
|
|
95
|
+
commits: ['open', 'apply', 'reword', 'drop', 'view'],
|
|
96
|
+
packages: ['packageNew', 'packageDrop', 'packageWanted', 'packageLatest'],
|
|
97
|
+
npm: ['open', 'npmEdit', 'npmDrop', 'npmLogs'],
|
|
98
|
+
npmRuns: ['open'],
|
|
99
|
+
agents: ['open', 'agentFast', 'agentInstall', 'agentStart'],
|
|
100
|
+
agentRuns: ['open', 'agentStart'],
|
|
101
|
+
tasks: ['open', 'check', 'import', 'drop'],
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const EMPTY_ACTIONS = {
|
|
105
|
+
repos: ['worktreeNew'],
|
|
106
|
+
files: [],
|
|
107
|
+
branches: ['newBranch', 'worktreeBranch', 'pull', 'push'],
|
|
108
|
+
commits: ['newCommit', 'view', 'pull', 'push'],
|
|
109
|
+
npm: ['npmNew', 'npmLogs'],
|
|
110
|
+
packages: ['packageNew'],
|
|
111
|
+
agents: [],
|
|
112
|
+
tasks: ['import'],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const rowHit = (hits, cell) => {
|
|
116
|
+
const x = cell.x - 1;
|
|
117
|
+
return (hits ?? []).find((hit) => {
|
|
118
|
+
if (hit.y !== cell.y) return false;
|
|
119
|
+
if (hit.x0 === undefined) return true;
|
|
120
|
+
return x >= hit.x0 && x < hit.x1;
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const selectContextRow = (ui, hit) => {
|
|
125
|
+
const pane = ui.pane;
|
|
126
|
+
if (pane === 'repos') {
|
|
127
|
+
const at = ui.workspace.rows.findIndex((row) => row.name === hit.cursor);
|
|
128
|
+
if (at >= 0) ui.workspace.cursor = at;
|
|
129
|
+
return CONTEXT_ACTIONS.repos;
|
|
130
|
+
}
|
|
131
|
+
if (pane === 'files') {
|
|
132
|
+
ui.nav.fileCursor = hit.cursor;
|
|
133
|
+
return CONTEXT_ACTIONS.files;
|
|
134
|
+
}
|
|
135
|
+
if (pane === 'branches') {
|
|
136
|
+
if (hit.tree) {
|
|
137
|
+
const at = ui.trees.rows.findIndex((row) => row.name === hit.cursor);
|
|
138
|
+
if (at >= 0) ui.trees.cursor = at;
|
|
139
|
+
ui.nav.treeFocus = true;
|
|
140
|
+
return CONTEXT_ACTIONS.tree;
|
|
141
|
+
}
|
|
142
|
+
ui.nav.treeFocus = false;
|
|
143
|
+
ui.nav.branchCursor = hit.cursor;
|
|
144
|
+
return CONTEXT_ACTIONS.branch;
|
|
145
|
+
}
|
|
146
|
+
if (pane === 'commits') {
|
|
147
|
+
ui.nav.commitCursor = hit.cursor;
|
|
148
|
+
return CONTEXT_ACTIONS.commits;
|
|
149
|
+
}
|
|
150
|
+
if (pane === 'packages') {
|
|
151
|
+
ui.nav.packagesCursor = hit.cursor;
|
|
152
|
+
return CONTEXT_ACTIONS.packages;
|
|
153
|
+
}
|
|
154
|
+
if (pane === 'npm') {
|
|
155
|
+
if (hit.side === 'runs') {
|
|
156
|
+
ui.npm.runCursor = hit.cursor;
|
|
157
|
+
ui.npm.focusRuns();
|
|
158
|
+
return CONTEXT_ACTIONS.npmRuns;
|
|
159
|
+
}
|
|
160
|
+
ui.nav.npmCursor = hit.cursor;
|
|
161
|
+
ui.npm.focusCommands();
|
|
162
|
+
return CONTEXT_ACTIONS.npm;
|
|
163
|
+
}
|
|
164
|
+
if (pane === 'agents') {
|
|
165
|
+
if (hit.side === 'runs') {
|
|
166
|
+
ui.agents.runCursor = hit.cursor;
|
|
167
|
+
ui.agents.focus = 'runs';
|
|
168
|
+
return CONTEXT_ACTIONS.agentRuns;
|
|
169
|
+
}
|
|
170
|
+
ui.nav.agentCursor = hit.cursor;
|
|
171
|
+
ui.agents.focusCli();
|
|
172
|
+
return CONTEXT_ACTIONS.agents;
|
|
173
|
+
}
|
|
174
|
+
if (pane === 'tasks') {
|
|
175
|
+
ui.nav.tasksFocus = hit.cursor;
|
|
176
|
+
return CONTEXT_ACTIONS.tasks;
|
|
177
|
+
}
|
|
178
|
+
return [];
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const contextDisabled = (ui, actions) => {
|
|
182
|
+
const pane = ui.pane;
|
|
183
|
+
let target = ui.current();
|
|
184
|
+
if (pane === 'files') target = ui.fileCursorEntry();
|
|
185
|
+
if (pane === 'branches') target = ui.gitBranches.selected();
|
|
186
|
+
if (pane === 'commits') {
|
|
187
|
+
target = {
|
|
188
|
+
...ui.commits.selected(),
|
|
189
|
+
canCommit: ui.commits.staged,
|
|
190
|
+
unstagedCurrent: !ui.rev && !ui.revShort,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const off = new Set([
|
|
194
|
+
...disabledActions(pane, target),
|
|
195
|
+
...dimFooterActions(ui.view()),
|
|
196
|
+
]);
|
|
197
|
+
const branchWrites = [
|
|
198
|
+
'newBranch',
|
|
199
|
+
'worktreeBranch',
|
|
200
|
+
'worktreeNew',
|
|
201
|
+
'worktreeDrop',
|
|
202
|
+
'rebase',
|
|
203
|
+
'drop',
|
|
204
|
+
'pull',
|
|
205
|
+
'push',
|
|
206
|
+
'newCommit',
|
|
207
|
+
'apply',
|
|
208
|
+
'reword',
|
|
209
|
+
];
|
|
210
|
+
if (!ui.capabilities.branches && pane !== 'tasks') {
|
|
211
|
+
for (const id of branchWrites) off.add(id);
|
|
212
|
+
if (pane === 'branches') off.add('open');
|
|
213
|
+
}
|
|
214
|
+
if (!ui.capabilities.changes) {
|
|
215
|
+
for (const id of ['add', 'unstage', 'revert', 'ignore']) off.add(id);
|
|
216
|
+
if (pane === 'files') off.add('code');
|
|
217
|
+
for (const id of actions) {
|
|
218
|
+
if (id.startsWith('package') || id.startsWith('npm')) off.add(id);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (pane === 'repos' || ui.worktrees.focused()) {
|
|
222
|
+
const row = ui.worktrees.selected();
|
|
223
|
+
const current = ui.trees.currentIndex() === ui.trees.cursor;
|
|
224
|
+
if (
|
|
225
|
+
!row ||
|
|
226
|
+
row.main ||
|
|
227
|
+
ui.worktrees.workingIn(row) ||
|
|
228
|
+
(pane === 'branches' && current)
|
|
229
|
+
) {
|
|
230
|
+
off.add('worktreeDrop');
|
|
231
|
+
}
|
|
232
|
+
if (!row || ui.worktrees.busy) off.add('worktreeNew');
|
|
233
|
+
}
|
|
234
|
+
if (pane === 'branches' && !ui.gitBranches.selected()) {
|
|
235
|
+
off.add('open');
|
|
236
|
+
off.add('worktreeBranch');
|
|
237
|
+
}
|
|
238
|
+
if (pane === 'npm') {
|
|
239
|
+
if (ui.npm.selected()?.kind !== 'script') {
|
|
240
|
+
off.add('npmEdit');
|
|
241
|
+
off.add('npmDrop');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (pane === 'tasks') {
|
|
245
|
+
const row = ui.composer.tasks.rowAt(ui.nav.tasksFocus);
|
|
246
|
+
if (!row || row.draft) {
|
|
247
|
+
off.add('drop');
|
|
248
|
+
off.add('check');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return [...off].filter((id) => actions.includes(id));
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const comboAt = (ui, cell) => {
|
|
255
|
+
if (ui.agents.pick || ui.composer.tasks.planOpen) return true;
|
|
256
|
+
const status = rowHit(ui.lastFrame?.statusHits, cell);
|
|
257
|
+
if (status && ['plan', 'plan-item', 'branch'].includes(status.id)) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const hit = rowHit(ui.lastFrame?.fileHits, cell);
|
|
261
|
+
return (
|
|
262
|
+
ui.pane === 'agents' && hit && [...COMBO_FIELDS, 'menu'].includes(hit.field)
|
|
263
|
+
);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const onContextPress = (ui, event) => {
|
|
267
|
+
const frame = ui.lastFrame;
|
|
268
|
+
if (!frame) return;
|
|
269
|
+
const cell = mouseCell(ui, event);
|
|
270
|
+
ui.nav.contextMenu = null;
|
|
271
|
+
if (ui.pane === 'dashboard' || comboAt(ui, cell)) return void ui.paint();
|
|
272
|
+
const editor = textEditor(ui, cell);
|
|
273
|
+
if (editor) {
|
|
274
|
+
const hit = hitAt(frame.editHits ?? [], cell);
|
|
275
|
+
if (ui.mode === 'compose' && (!hit || !focusField(ui, hit))) return;
|
|
276
|
+
if (hit && !editor.hasSelect()) editor.place(offsetOf(hit, cell.x));
|
|
277
|
+
ui.nav.clearSelection();
|
|
278
|
+
ui.nav.contextMenu = {
|
|
279
|
+
x: cell.x - 1,
|
|
280
|
+
y: cell.y,
|
|
281
|
+
actions: TEXT_ACTIONS,
|
|
282
|
+
cursor: 0,
|
|
283
|
+
editor,
|
|
284
|
+
disabled: textDisabled(editor),
|
|
285
|
+
};
|
|
286
|
+
return void ui.paint();
|
|
287
|
+
}
|
|
288
|
+
if (ui.mode !== 'review' || cell.y <= 1 || cell.y >= frame.height - 1) return;
|
|
289
|
+
const hits = ui.pane === 'tasks' ? frame.taskHits : frame.fileHits;
|
|
290
|
+
const hit = rowHit(hits, cell);
|
|
291
|
+
if (!hit && ui.pane === 'branches') ui.nav.treeFocus = false;
|
|
292
|
+
const actions = hit ? selectContextRow(ui, hit) : EMPTY_ACTIONS[ui.pane];
|
|
293
|
+
if (!actions?.length) return void ui.paint();
|
|
294
|
+
ui.nav.clearSelection();
|
|
295
|
+
ui.nav.contextMenu = {
|
|
296
|
+
x: cell.x - 1,
|
|
297
|
+
y: cell.y,
|
|
298
|
+
actions,
|
|
299
|
+
cursor: 0,
|
|
300
|
+
disabled: contextDisabled(ui, actions),
|
|
301
|
+
};
|
|
302
|
+
ui.paint();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const contextKey = (mark) => {
|
|
306
|
+
if (mark === 'del') return 'delete';
|
|
307
|
+
if (mark === 'ins') return 'insert';
|
|
308
|
+
if (mark === 'space') return ' ';
|
|
309
|
+
return mark?.replace('+', '-');
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const runContextAction = (ui, action) => {
|
|
313
|
+
const menu = ui.nav.contextMenu;
|
|
314
|
+
if (!menu || (menu.disabled ?? []).includes(action)) return;
|
|
315
|
+
ui.nav.contextMenu = null;
|
|
316
|
+
if (!runTextAction(ui, menu, action)) ui.dispatch(action);
|
|
317
|
+
ui.paint();
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const handleContextKey = (ui, key) => {
|
|
321
|
+
const menu = ui.nav.contextMenu;
|
|
322
|
+
if (!menu) return false;
|
|
323
|
+
if (key === 'escape') {
|
|
324
|
+
ui.nav.contextMenu = null;
|
|
325
|
+
ui.paint();
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (key === 'up' || key === 'down') {
|
|
329
|
+
const delta = key === 'up' ? -1 : 1;
|
|
330
|
+
const last = menu.actions.length - 1;
|
|
331
|
+
menu.cursor = Math.max(0, Math.min(last, menu.cursor + delta));
|
|
332
|
+
ui.paint();
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
let action = key === 'enter' ? menu.actions[menu.cursor] : null;
|
|
336
|
+
if (!action) {
|
|
337
|
+
action = menu.actions.find((id) => contextKey(actionLetter(id)) === key);
|
|
338
|
+
}
|
|
339
|
+
if (!action) return true;
|
|
340
|
+
runContextAction(ui, action);
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
343
|
+
|
|
115
344
|
const onPress = (ui, event) => {
|
|
116
|
-
dismissImport(ui, event);
|
|
117
345
|
const nav = ui.nav;
|
|
118
346
|
const cell = mouseCell(ui, event);
|
|
347
|
+
if (nav.contextMenu) {
|
|
348
|
+
const hit = rowHit(ui.lastFrame?.contextHits, cell);
|
|
349
|
+
if (hit) return;
|
|
350
|
+
nav.contextMenu = null;
|
|
351
|
+
}
|
|
352
|
+
dismissImport(ui, event);
|
|
353
|
+
if (beginEditDrag(ui, cell, Boolean(event.button & 4))) return;
|
|
119
354
|
if (startLogDrag(ui, cell)) return;
|
|
120
355
|
nav.scrollDrag = null;
|
|
121
356
|
nav.mouseAnchor = cell;
|
|
@@ -130,6 +365,7 @@ const onPress = (ui, event) => {
|
|
|
130
365
|
const onDrag = (ui, event) => {
|
|
131
366
|
const nav = ui.nav;
|
|
132
367
|
const cell = mouseCell(ui, event);
|
|
368
|
+
if (moveEditDrag(ui, cell)) return;
|
|
133
369
|
if (nav.scrollDrag) return void moveLogBar(ui, cell);
|
|
134
370
|
if (!nav.mouseAnchor) return;
|
|
135
371
|
nav.pendingClick = null;
|
|
@@ -190,8 +426,6 @@ const copySelection = (ui) => {
|
|
|
190
426
|
ui.status = ok ? 'copied' : 'copy failed';
|
|
191
427
|
};
|
|
192
428
|
|
|
193
|
-
const COMBO_FIELDS = ['model', 'effort', 'context', 'plan'];
|
|
194
|
-
|
|
195
429
|
const onAgentRunHit = (ui, hit) => {
|
|
196
430
|
ui.agents.focus = 'runs';
|
|
197
431
|
ui.agents.runCursor = hit.cursor;
|
|
@@ -260,6 +494,8 @@ const RELEASE_TARGETS = [
|
|
|
260
494
|
match: (ui) => ui.pane === 'branches',
|
|
261
495
|
hits: 'fileHits',
|
|
262
496
|
onHit: (ui, hit) => {
|
|
497
|
+
if (hit.tree) return void ui.worktrees.pickTile(hit.cursor);
|
|
498
|
+
ui.nav.treeFocus = false;
|
|
263
499
|
const again = armDoubleClick(ui.nav, 'branches', hit.cursor);
|
|
264
500
|
ui.nav.branchCursor = hit.cursor;
|
|
265
501
|
if (again) ui.gitBranches.onCheckoutBranch();
|
|
@@ -348,9 +584,17 @@ const releaseAgentMenu = (ui, cell) => {
|
|
|
348
584
|
};
|
|
349
585
|
|
|
350
586
|
const onRelease = (ui, event) => {
|
|
351
|
-
dismissImport(ui, event);
|
|
352
587
|
const nav = ui.nav;
|
|
353
588
|
const cell = mouseCell(ui, event);
|
|
589
|
+
if (nav.contextMenu) {
|
|
590
|
+
const hit = rowHit(ui.lastFrame?.contextHits, cell);
|
|
591
|
+
if (hit) return void runContextAction(ui, hit.id);
|
|
592
|
+
nav.contextMenu = null;
|
|
593
|
+
ui.paint();
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (moveEditDrag(ui, cell, true)) return;
|
|
597
|
+
dismissImport(ui, event);
|
|
354
598
|
if (nav.scrollDrag) {
|
|
355
599
|
moveLogBar(ui, cell);
|
|
356
600
|
nav.scrollDrag = null;
|
|
@@ -366,7 +610,6 @@ const onRelease = (ui, event) => {
|
|
|
366
610
|
nav.pendingClick = null;
|
|
367
611
|
nav.mouseAnchor = null;
|
|
368
612
|
if (clickButton(ui, cell, clickId)) return;
|
|
369
|
-
if (placeClick(ui, cell)) return;
|
|
370
613
|
if (ui.pane === 'agents' && ui.agents.pick) {
|
|
371
614
|
return void releaseAgentMenu(ui, cell);
|
|
372
615
|
}
|
|
@@ -390,6 +633,20 @@ const POINTER_HANDLERS = {
|
|
|
390
633
|
|
|
391
634
|
const handlePointer = (ui, event) => {
|
|
392
635
|
const kind = event.kind;
|
|
636
|
+
if (ui.nav.contextMenu && ['move', 'drag'].includes(kind)) {
|
|
637
|
+
const hit = rowHit(ui.lastFrame?.contextHits, mouseCell(ui, event));
|
|
638
|
+
if (hit && !hit.disabled && hit.cursor !== ui.nav.contextMenu.cursor) {
|
|
639
|
+
ui.nav.contextMenu.cursor = hit.cursor;
|
|
640
|
+
ui.paint();
|
|
641
|
+
}
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (kind === 'move') return;
|
|
645
|
+
if (kind === 'press' && event.btn === 2) {
|
|
646
|
+
onContextPress(ui, event);
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (event.btn === 2) return;
|
|
393
650
|
const handler = POINTER_HANDLERS[kind];
|
|
394
651
|
if (!handler) return;
|
|
395
652
|
const isWheel = kind === 'wheelUp' || kind === 'wheelDown';
|
|
@@ -399,4 +656,5 @@ const handlePointer = (ui, event) => {
|
|
|
399
656
|
|
|
400
657
|
module.exports = {
|
|
401
658
|
handlePointer,
|
|
659
|
+
handleContextKey,
|
|
402
660
|
};
|