reslop 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +163 -436
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/dir.js +50 -0
  19. package/lib/git/linked.js +152 -0
  20. package/lib/git/local.js +179 -0
  21. package/lib/git/status.js +126 -0
  22. package/lib/git/worktree.js +113 -36
  23. package/lib/input/actions.js +83 -5
  24. package/lib/input/keys.js +30 -1
  25. package/lib/input/line-edit.js +5 -1
  26. package/lib/render/agents.js +50 -24
  27. package/lib/render/branches.js +55 -27
  28. package/lib/render/field.js +19 -5
  29. package/lib/render/footer.js +14 -6
  30. package/lib/render/frame.js +16 -2
  31. package/lib/render/menu.js +103 -6
  32. package/lib/render/name-edit.js +31 -0
  33. package/lib/render/repos.js +52 -8
  34. package/lib/render/status.js +22 -4
  35. package/lib/report/run.js +16 -23
  36. package/lib/review/agent-instructions.md +2 -2
  37. package/lib/runs/process.js +3 -3
  38. package/lib/runs/runs.js +27 -4
  39. package/lib/session/agents/agents.js +220 -205
  40. package/lib/session/agents/away.js +48 -0
  41. package/lib/session/agents/job.js +23 -86
  42. package/lib/session/agents/jobs.js +14 -27
  43. package/lib/session/agents/options.js +45 -101
  44. package/lib/session/agents/plans.js +53 -0
  45. package/lib/session/agents/view.js +34 -0
  46. package/lib/session/branches.js +18 -1
  47. package/lib/session/compose.js +13 -5
  48. package/lib/session/dispatch.js +43 -4
  49. package/lib/session/editor-pointer.js +98 -0
  50. package/lib/session/files-pane.js +3 -0
  51. package/lib/session/lifecycle.js +67 -10
  52. package/lib/session/navigation.js +5 -0
  53. package/lib/session/npm/history.js +2 -1
  54. package/lib/session/npm/npm.js +89 -12
  55. package/lib/session/ops.js +3 -0
  56. package/lib/session/pointer.js +304 -46
  57. package/lib/session/session.js +15 -2
  58. package/lib/session/terminal.js +2 -2
  59. package/lib/session/text-menu.js +50 -0
  60. package/lib/session/view.js +19 -2
  61. package/lib/session/workspace.js +179 -38
  62. package/lib/session/worktrees.js +280 -0
  63. package/lib/term/themes/dark.json +4 -0
  64. package/lib/term/themes/light.json +4 -0
  65. package/package.json +2 -2
@@ -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 = () => {
@@ -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 { indexAtRow } = require('../term/wrap.js');
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 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
- };
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
  };
@@ -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.workspace.cols = frame.gridCols;
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 starts its CLI probes.
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();
@@ -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[?1002h\x1b[?1006h';
35
+ const ENTER_TERM = '\x1b[?1049h\x1b[?25l\x1b[?7l\x1b[?1003h\x1b[?1006h';
36
36
  const LEAVE_TERM =
37
- '\x1b[?1006l\x1b[?1002l\x1b[?7h\x1b[0 q\x1b[?12l\x1b[?25h\x1b[?1049l';
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 };
@@ -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