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.
- package/CHANGELOG.md +16 -1
- package/README.md +21 -3
- package/lib/agents/acp.js +309 -0
- package/lib/agents/agents.js +163 -436
- package/lib/agents/allow.js +141 -52
- package/lib/agents/allow.json +16 -14
- package/lib/agents/models.js +250 -383
- 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/dir.js +50 -0
- 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 +113 -36
- package/lib/input/actions.js +83 -5
- package/lib/input/keys.js +30 -1
- package/lib/input/line-edit.js +5 -1
- package/lib/render/agents.js +50 -24
- package/lib/render/branches.js +55 -27
- package/lib/render/field.js +19 -5
- package/lib/render/footer.js +14 -6
- package/lib/render/frame.js +16 -2
- 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/run.js +16 -23
- package/lib/review/agent-instructions.md +2 -2
- package/lib/runs/process.js +3 -3
- package/lib/runs/runs.js +27 -4
- package/lib/session/agents/agents.js +220 -205
- 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 -1
- package/lib/session/compose.js +13 -5
- package/lib/session/dispatch.js +43 -4
- package/lib/session/editor-pointer.js +98 -0
- package/lib/session/files-pane.js +3 -0
- package/lib/session/lifecycle.js +67 -10
- package/lib/session/navigation.js +5 -0
- package/lib/session/npm/history.js +2 -1
- package/lib/session/npm/npm.js +89 -12
- package/lib/session/ops.js +3 -0
- package/lib/session/pointer.js +304 -46
- package/lib/session/session.js +15 -2
- 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/themes/dark.json +4 -0
- package/lib/term/themes/light.json +4 -0
- package/package.json +2 -2
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
|
};
|
package/lib/session/session.js
CHANGED
|
@@ -27,6 +27,7 @@ const dispatch = require('./dispatch.js');
|
|
|
27
27
|
const { dispatchAction, confirmChoice, handleEvent } = dispatch;
|
|
28
28
|
const { Dashboard } = require('./dashboard.js');
|
|
29
29
|
const { Workspace } = require('./workspace.js');
|
|
30
|
+
const { WorktreeController } = require('./worktrees.js');
|
|
30
31
|
|
|
31
32
|
const bindAccessors = (target, owner, names) => {
|
|
32
33
|
for (const name of names) {
|
|
@@ -134,6 +135,9 @@ const assignParts = (session, options) => {
|
|
|
134
135
|
session.agents = new AgentsController(session);
|
|
135
136
|
session.dashboard = new Dashboard(session);
|
|
136
137
|
session.workspace = new Workspace(session);
|
|
138
|
+
const infoOf = () => session.treesInfo;
|
|
139
|
+
session.trees = new Workspace(session, { pane: 'branches', infoOf });
|
|
140
|
+
session.worktrees = new WorktreeController(session);
|
|
137
141
|
session.lifecycle = new Lifecycle(session);
|
|
138
142
|
};
|
|
139
143
|
|
|
@@ -181,6 +185,9 @@ class Session {
|
|
|
181
185
|
this.agents.reset();
|
|
182
186
|
this.dashboard.reset();
|
|
183
187
|
this.workspace.reset();
|
|
188
|
+
this.worktrees.reset();
|
|
189
|
+
this.trees.reset();
|
|
190
|
+
this.treesInfo = null;
|
|
184
191
|
this.top = this.cwd;
|
|
185
192
|
this.branch = '';
|
|
186
193
|
this.repositoryUrl = '';
|
|
@@ -262,7 +269,8 @@ class Session {
|
|
|
262
269
|
panel.runScroll = frame.runScroll;
|
|
263
270
|
}
|
|
264
271
|
if (typeof frame.gridCols === 'number') {
|
|
265
|
-
this.
|
|
272
|
+
const tiles = this.nav.pane === 'branches' ? this.trees : this.workspace;
|
|
273
|
+
tiles.cols = frame.gridCols;
|
|
266
274
|
}
|
|
267
275
|
if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
|
|
268
276
|
const editScroll = frame.cursor && frame.cursor.scroll;
|
|
@@ -274,6 +282,7 @@ class Session {
|
|
|
274
282
|
this.scheduleAutosave();
|
|
275
283
|
this.dashboard.afterDraw();
|
|
276
284
|
this.workspace.afterDraw();
|
|
285
|
+
this.trees.afterDraw();
|
|
277
286
|
}
|
|
278
287
|
|
|
279
288
|
scheduleAutosave() {
|
|
@@ -364,6 +373,7 @@ class Session {
|
|
|
364
373
|
if (!result) return;
|
|
365
374
|
if (result.kind === 'commit') this.commits.finishGitCommit();
|
|
366
375
|
if (result.kind === 'branch') this.gitBranches.finishCreateBranch();
|
|
376
|
+
if (result.kind === 'worktree') this.worktrees.finishAdd();
|
|
367
377
|
if (result.kind === 'npm') this.npm.finishEdit();
|
|
368
378
|
if (result.kind === 'package') this.packages.finishAdd();
|
|
369
379
|
}
|
|
@@ -393,6 +403,8 @@ class Session {
|
|
|
393
403
|
this.collection.reset();
|
|
394
404
|
this.reads.reset();
|
|
395
405
|
this.review.reset();
|
|
406
|
+
this.npm.retarget();
|
|
407
|
+
this.agents.retarget();
|
|
396
408
|
this.nav.pane = pane;
|
|
397
409
|
this.nav.tasksOpen = false;
|
|
398
410
|
this.nav.scroll = 0;
|
|
@@ -491,7 +503,7 @@ class Session {
|
|
|
491
503
|
this.openLoad();
|
|
492
504
|
// Populate the agent model cache while the initial screen is loading.
|
|
493
505
|
// Opening the Agents pane can then use the cache instead of being the
|
|
494
|
-
// first place that
|
|
506
|
+
// first place that asks each agent for its models.
|
|
495
507
|
void this.agents.loadModels();
|
|
496
508
|
this.openUpdate();
|
|
497
509
|
this.draw();
|
|
@@ -520,6 +532,7 @@ class Session {
|
|
|
520
532
|
this.lifecycle.stopWatch();
|
|
521
533
|
this.dashboard.stop();
|
|
522
534
|
this.workspace.stop();
|
|
535
|
+
this.trees.stop();
|
|
523
536
|
this.term.stopListening();
|
|
524
537
|
this.progress.clear();
|
|
525
538
|
this.term.clearTimers();
|
package/lib/session/terminal.js
CHANGED
|
@@ -32,9 +32,9 @@ const watchResize = (stdout, proc, onResize, signal) => {
|
|
|
32
32
|
return stop;
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?
|
|
35
|
+
const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1003h\x1b[?1006h';
|
|
36
36
|
const LEAVE_TERM =
|
|
37
|
-
'\x1b[?1006l\x1b[?
|
|
37
|
+
'\x1b[?1006l\x1b[?1003l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
|
|
38
38
|
|
|
39
39
|
const sameCursor = (a, b) => {
|
|
40
40
|
if (!a || !b) return !a && !b;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TEXT_ACTIONS = [
|
|
4
|
+
'textCut',
|
|
5
|
+
'textCopy',
|
|
6
|
+
'textPaste',
|
|
7
|
+
'textDelete',
|
|
8
|
+
'textSelectAll',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const textEditor = (ui, cell) => {
|
|
12
|
+
if (ui.mode === 'compose') return ui.editor;
|
|
13
|
+
const statusY = ui.lastFrame?.height - 1;
|
|
14
|
+
if (cell.y !== statusY) return null;
|
|
15
|
+
if (ui.mode === 'find') return ui.nav.find?.editor;
|
|
16
|
+
if (ui.mode === 'import') return ui.nav.import?.editor;
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const textDisabled = (editor) => {
|
|
21
|
+
const off = editor.hasSelect() ? [] : ['textCut', 'textCopy'];
|
|
22
|
+
if (!editor.hasSelect() && editor.cursor >= editor.text.length) {
|
|
23
|
+
off.push('textDelete');
|
|
24
|
+
}
|
|
25
|
+
if (!editor.text) off.push('textSelectAll');
|
|
26
|
+
return off;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const TEXT_KEYS = {
|
|
30
|
+
textCut: 'ctrl-x',
|
|
31
|
+
textCopy: 'ctrl-c',
|
|
32
|
+
textPaste: 'ctrl-v',
|
|
33
|
+
textDelete: 'delete',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const runTextAction = (ui, menu, action) => {
|
|
37
|
+
const editor = menu.editor;
|
|
38
|
+
if (!editor) return false;
|
|
39
|
+
if (action === 'textSelectAll') {
|
|
40
|
+
editor.anchor = 0;
|
|
41
|
+
editor.cursor = editor.text.length;
|
|
42
|
+
if (ui.mode === 'compose') ui.composer.afterEdit();
|
|
43
|
+
} else {
|
|
44
|
+
const key = TEXT_KEYS[action];
|
|
45
|
+
if (key) ui.handleEvent({ type: 'key', key });
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = { TEXT_ACTIONS, textEditor, textDisabled, runTextAction };
|
package/lib/session/view.js
CHANGED
|
@@ -137,6 +137,7 @@ const navView = (ui) => {
|
|
|
137
137
|
scroll: nav.scroll,
|
|
138
138
|
listScroll: nav.listScroll,
|
|
139
139
|
selection: nav.selection,
|
|
140
|
+
contextMenu: nav.contextMenu,
|
|
140
141
|
pane: nav.pane,
|
|
141
142
|
fileScope: nav.fileScope,
|
|
142
143
|
fileCursor: nav.fileCursor,
|
|
@@ -151,9 +152,22 @@ const navView = (ui) => {
|
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
const dropNameOf = (ui) => {
|
|
154
|
-
const { packages, gitBranches, commits, npm } = ui;
|
|
155
|
+
const { packages, gitBranches, commits, npm, worktrees } = ui;
|
|
155
156
|
const name = packages.dropName || gitBranches.dropName;
|
|
156
|
-
return name || commits.dropName || npm.dropName;
|
|
157
|
+
return name || commits.dropName || npm.dropName || worktrees.dropName;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// One tile looks selected: the one under the cursor while the worktrees have
|
|
161
|
+
// the focus, else the worktree that is shown
|
|
162
|
+
const treesView = (ui) => {
|
|
163
|
+
const model = ui.trees.view();
|
|
164
|
+
const picked = ui.worktrees.focused() ? model.cursor : model.current;
|
|
165
|
+
return { ...model, cursor: picked < 0 ? model.cursor : picked };
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const paneTrees = (ui, pane) => {
|
|
169
|
+
if (pane !== 'branches' || !ui.worktrees.hasPanels()) return null;
|
|
170
|
+
return treesView(ui);
|
|
157
171
|
};
|
|
158
172
|
|
|
159
173
|
const repoView = (ui) => {
|
|
@@ -180,6 +194,7 @@ const repoView = (ui) => {
|
|
|
180
194
|
updateFrom: updater.from,
|
|
181
195
|
updateTo: updater.to,
|
|
182
196
|
dropName: dropNameOf(ui),
|
|
197
|
+
worktreeTarget: ui.worktrees.branchTarget,
|
|
183
198
|
};
|
|
184
199
|
};
|
|
185
200
|
|
|
@@ -200,6 +215,8 @@ const screenView = (ui) => {
|
|
|
200
215
|
dashboard: pane === 'dashboard' ? ui.dashboard.view() : null,
|
|
201
216
|
packages: pane === 'packages' ? ui.dashboard.packagesView() : null,
|
|
202
217
|
repos: pane === 'repos' ? ui.workspace.view() : null,
|
|
218
|
+
trees: paneTrees(ui, pane),
|
|
219
|
+
treeFocus: ui.worktrees.focused(),
|
|
203
220
|
};
|
|
204
221
|
};
|
|
205
222
|
|