reslop 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +123 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/dashboard/blocks.js +490 -0
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +19 -61
  72. package/lib/render/tiles.js +203 -47
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +166 -59
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +72 -59
  126. package/lib/session/workspace.js +416 -0
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -1,29 +1,24 @@
1
1
  'use strict';
2
2
 
3
- const wrap = require('../wrap.js');
4
- const { cursorInWrap, wrapDoc } = wrap;
3
+ const { cursorInWrap, wrapDoc } = require('../term/wrap.js');
5
4
  const render = require('../render/render.js');
6
- const { noteInnerWidth, codeInnerWidth, todoBodyWidth } = render;
7
- const commits = require('../render/commits.js');
8
- const { commitMessageWidth } = commits;
9
- const commit = require('./commit.js');
10
- const { listedCommits } = commit;
11
- const editor = require('../editor.js');
12
- const { Editor } = editor;
13
- const files = require('../files.js');
14
- const { itemPath, isTaskItem, listPath, isReadOnlyOrigin } = files;
15
- const diff = require('../diff/diff.js');
16
- const { blockAddText } = diff;
17
- const items = require('./items.js');
18
- const { itemFeedKey, canEditCode } = items;
19
- const review = require('../review.js');
20
- const { setFeedback, setCode, checkLabel, rememberTemplate } = review;
21
- const templates = require('./templates.js');
22
- const { TemplatePick } = templates;
23
- const tasks = require('./tasks.js');
24
- const { TaskPage } = tasks;
5
+ const { noteInnerWidth, codeInnerWidth, todoBodyWidth, activeDigits } = render;
6
+ const { commitMessageWidth } = require('../render/commits.js');
7
+ const { listedCommits } = require('./commit.js');
8
+ const { Editor } = require('../input/editor.js');
9
+ const { isTextKey } = require('../input/keys.js');
10
+ const files = require('../common/files.js');
11
+ const { itemPath, isTaskItem, isTasksEntry, listPath } = files;
12
+ const { isReadOnlyOrigin } = files;
13
+ const { blockAddText } = require('../diff/diff.js');
14
+ const { itemFeedKey, canEditCode } = require('./items.js');
15
+ const review = require('../review/review.js');
16
+ const { checkLabel } = review;
17
+ const { TemplatePick } = require('./templates.js');
18
+ const { TaskPage } = require('./tasks.js');
25
19
  const unit = require('./unit.js');
26
- const clipboard = require('../clipboard.js');
20
+ const clipboard = require('../term/clipboard.js');
21
+ const { EDIT_OP, copyLine, cutLine } = require('../input/line-edit.js');
27
22
 
28
23
  const COMPOSE_EDITS = ['backspace', 'delete', 'ctrl-z', 'ctrl-y'];
29
24
  const COMPOSE_CLIP = ['ctrl-x', 'ctrl-v'];
@@ -37,10 +32,11 @@ const KIND = {
37
32
  commit: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
38
33
  branch: { enter: 'save', tab: 'template', escape: 'cancel', move: 'note' },
39
34
  npm: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
35
+ package: { enter: 'save', tab: 'insert-tab', escape: 'cancel', move: 'note' },
40
36
  };
41
37
 
42
- const COMMAND_KINDS = ['commit', 'branch', 'npm'];
43
- const NOTE_VIEWS = ['feedback', 'commit', 'branch', 'npm'];
38
+ const COMMAND_KINDS = ['commit', 'branch', 'npm', 'package'];
39
+ const NOTE_VIEWS = ['feedback', 'commit', 'branch', 'npm', 'package'];
44
40
 
45
41
  const itemNote = (item, text) => {
46
42
  const hunk = item.hunk;
@@ -76,17 +72,13 @@ const blankTail = (text) => {
76
72
  return count;
77
73
  };
78
74
 
75
+ const EDITOR_KEYS = {};
76
+ for (const key of Object.keys(EDIT_OP)) {
77
+ EDITOR_KEYS[key] = (c) => EDIT_OP[key](c.editor);
78
+ }
79
+
79
80
  const COMPOSE_SPECIAL = {
80
- backspace: (c) => c.editor.backspace(),
81
- delete: (c) => c.editor.delete(),
82
- left: (c) => c.editor.move(-1),
83
- right: (c) => c.editor.move(1),
84
- 'shift-left': (c) => c.editor.move(-1, true),
85
- 'shift-right': (c) => c.editor.move(1, true),
86
- 'ctrl-left': (c) => c.editor.moveWord(-1),
87
- 'ctrl-right': (c) => c.editor.moveWord(1),
88
- 'ctrl-shift-left': (c) => c.editor.moveWord(-1, true),
89
- 'ctrl-shift-right': (c) => c.editor.moveWord(1, true),
81
+ ...EDITOR_KEYS,
90
82
  up: (c) => c.moveLine(-1),
91
83
  down: (c) => c.moveLine(1),
92
84
  'shift-up': (c) => c.moveLine(-1, true),
@@ -116,7 +108,9 @@ class Composer {
116
108
  }
117
109
 
118
110
  reset() {
111
+ this.tasks.closePlanMenu();
119
112
  this.fileDirty = false;
113
+ this.saving = false;
120
114
  this.clear();
121
115
  }
122
116
 
@@ -126,6 +120,7 @@ class Composer {
126
120
  this.commitKind = null;
127
121
  this.commitBody = '';
128
122
  this.composeTaskId = null;
123
+ this.composeQuoteKey = null;
129
124
  this.composeBaseline = '';
130
125
  this.templateIndex = -1;
131
126
  this.templateFocus = false;
@@ -155,7 +150,8 @@ class Composer {
155
150
  if (kind !== 'code' && kind !== 'file') return;
156
151
  if (!this.editor) return;
157
152
  const width = this.viewSize().width;
158
- this.editor.reveal(codeInnerWidth(width, this.ui.layout));
153
+ const digits = activeDigits(this.ui);
154
+ this.editor.reveal(codeInnerWidth(width, this.ui.layout, digits));
159
155
  }
160
156
 
161
157
  followFileEdit() {
@@ -171,6 +167,7 @@ class Composer {
171
167
  this.ui.mode = 'compose';
172
168
  this.composeKind = kind;
173
169
  this.composeTaskId = taskId;
170
+ this.composeQuoteKey = null;
174
171
  this.composeBaseline = text;
175
172
  this.fileOpened = kind === 'file' ? text : '';
176
173
  this.editor = new Editor(text);
@@ -180,6 +177,11 @@ class Composer {
180
177
  this.ui.nav.clearSelection();
181
178
  }
182
179
 
180
+ openQuote(key, text) {
181
+ this.openCompose('tasks', text, null);
182
+ this.composeQuoteKey = key;
183
+ }
184
+
183
185
  closeCompose() {
184
186
  const ui = this.ui;
185
187
  ui.mode = 'review';
@@ -203,7 +205,7 @@ class Composer {
203
205
  commitFeedback(text) {
204
206
  const item = this.ui.current();
205
207
  if (!item || isTaskItem(item)) return;
206
- setFeedback(this.notes, itemFeedKey(item), itemNote(item, text));
208
+ this.notes.setFeedback(itemFeedKey(item), itemNote(item, text));
207
209
  }
208
210
 
209
211
  dropCodeNote(item, text) {
@@ -211,34 +213,52 @@ class Composer {
211
213
  if (!notes) return;
212
214
  const key = itemFeedKey(item);
213
215
  if (!notes.code.has(key)) return;
214
- setCode(notes, key, { ...itemNote(item, text), clear: true });
216
+ notes.setCode(key, { ...itemNote(item, text), clear: true });
217
+ }
218
+
219
+ // Edits that land in the worktree or index, not in the review notes
220
+ writesDisk() {
221
+ const kind = this.composeKind;
222
+ if (!this.editor) return false;
223
+ if (kind === 'file') return true;
224
+ return kind === 'code' && this.writesCodeFile(this.ui.current());
215
225
  }
216
226
 
217
227
  commitCode(text) {
228
+ const item = this.ui.current();
229
+ if (!canEditCode(item) || this.writesCodeFile(item)) return;
230
+ const note = itemNote(item, text);
231
+ const clear = text === blockAddText(item.hunk, item.blockId);
232
+ this.notes.setCode(itemFeedKey(item), clear ? { ...note, clear } : note);
233
+ }
234
+
235
+ async write(action) {
218
236
  const ui = this.ui;
219
- const item = ui.current();
220
- if (!canEditCode(item)) return true;
221
- const original = blockAddText(item.hunk, item.blockId);
222
- if (!this.writesCodeFile(item)) {
223
- const note = itemNote(item, text);
224
- const clear = text === original;
225
- setCode(this.notes, itemFeedKey(item), clear ? { ...note, clear } : note);
226
- return true;
237
+ ui.ignoreWatch();
238
+ try {
239
+ await ui.ops.exclusive(action);
240
+ } catch (error) {
241
+ ui.status = error.message;
242
+ return false;
227
243
  }
228
- if (text !== original) {
229
- try {
230
- ui.repo.edit(ui.top, item, text);
231
- } catch (error) {
232
- ui.status = error.message;
233
- return false;
234
- }
244
+ ui.ignoreWatch();
245
+ return true;
246
+ }
247
+
248
+ async writeCode(text) {
249
+ const ui = this.ui;
250
+ const item = ui.current();
251
+ if (text !== blockAddText(item.hunk, item.blockId)) {
252
+ const edit = (top) => ui.repo.edit(top, item, text);
253
+ const written = await this.write(edit);
254
+ if (!written) return false;
235
255
  this.fileDirty = true;
236
256
  }
237
257
  this.dropCodeNote(item, text);
238
258
  return true;
239
259
  }
240
260
 
241
- saveUnitFile(text) {
261
+ async saveUnitFile(text) {
242
262
  const ui = this.ui;
243
263
  const rel = ui.nav.reviewPath;
244
264
  if (!rel || !ui.repo.writeFile) return true;
@@ -246,48 +266,37 @@ class Composer {
246
266
  ui.status = 'read only';
247
267
  return false;
248
268
  }
249
- try {
250
- ui.repo.writeFile(ui.top, rel, text);
251
- } catch (error) {
252
- ui.status = error.message;
253
- return false;
254
- }
255
- ui.ignoreWatch();
269
+ const save = (top) => ui.repo.writeFile(top, rel, text);
270
+ const written = await this.write(save);
271
+ if (!written) return false;
272
+ ui.reads.keepText(rel, text);
256
273
  this.composeBaseline = text;
257
274
  this.fileDirty = true;
258
275
  return true;
259
276
  }
260
277
 
261
- stageEditedFile(rel) {
278
+ stageItems(rel) {
262
279
  const ui = this.ui;
263
280
  const repo = ui.repo;
264
- if (!rel || !this.canChange()) return false;
265
- if (repo.stagePath) {
266
- try {
267
- repo.stagePath(ui.top, rel);
268
- } catch (error) {
269
- ui.status = error.message;
270
- return false;
271
- }
272
- ui.ignoreWatch();
273
- return true;
274
- }
275
- if (!repo.add) return false;
276
- let staged = 0;
277
- for (const item of ui.items) {
278
- if (listPath(item) !== rel) continue;
279
- if (isReadOnlyOrigin(item.origin) || item.origin === 'staged') continue;
280
- try {
281
- repo.add(ui.top, item);
282
- } catch (error) {
283
- ui.status = error.message;
284
- return staged > 0;
285
- }
286
- ui.collection.keepOrigin(item, 'staged');
287
- staged += 1;
288
- }
289
- if (staged) ui.ignoreWatch();
290
- return staged > 0;
281
+ if (!repo.add) return;
282
+ const targets = ui.items.filter((item) => {
283
+ if (listPath(item) !== rel || isReadOnlyOrigin(item.origin)) return false;
284
+ return item.origin !== 'staged';
285
+ });
286
+ if (!targets.length) return;
287
+ for (const item of targets) ui.collection.keepOrigin(item, 'staged');
288
+ ui.ignoreWatch();
289
+ const write = async () => {
290
+ for (const item of targets) await ui.ops.add(item);
291
+ };
292
+ ui.ops.enqueueLazy({ kind: 'staged', done: '', reload: false, write });
293
+ }
294
+
295
+ async stageEditedFile(rel) {
296
+ const repo = this.ui.repo;
297
+ if (!rel || !this.canChange()) return;
298
+ if (!repo.stagePath) return void this.stageItems(rel);
299
+ await this.write((top) => repo.stagePath(top, rel));
291
300
  }
292
301
 
293
302
  finishSave(reload, doneStatus) {
@@ -298,84 +307,112 @@ class Composer {
298
307
  if (!ui.status) ui.status = doneStatus;
299
308
  return;
300
309
  }
310
+ this.fileDirty = false;
301
311
  ui.reloadAfterChange(() => {
302
312
  if (!ui.status) ui.status = doneStatus;
303
313
  });
304
314
  }
305
315
 
306
- saveFileCompose() {
316
+ // Closes the editor after a note or a block edit; reloads if a file changed
317
+ finishNote(doneStatus) {
318
+ const ui = this.ui;
319
+ if (this.fileDirty === true) return void this.finishSave(true, doneStatus);
320
+ ui.flushReview();
321
+ this.closeCompose();
322
+ ui.status = doneStatus;
323
+ }
324
+
325
+ async saveFile() {
307
326
  const rel = this.ui.nav.reviewPath;
308
327
  const opened = this.fileOpened;
309
- const text = this.editor ? this.editor.text : '';
310
- if (this.editor) this.ui.nav.unitLine = this.editor.linePos().line;
311
- if (!this.saveUnitFile(text)) return { kind: 'error' };
328
+ const text = this.editor.text;
329
+ this.ui.nav.unitLine = this.editor.linePos().line;
330
+ const saved = await this.saveUnitFile(text);
331
+ if (!saved) return false;
312
332
  const changed = text !== opened;
313
333
  const reload = this.fileDirty === true;
314
- if (changed) this.stageEditedFile(rel);
334
+ if (changed) await this.stageEditedFile(rel);
315
335
  this.finishSave(reload, changed ? 'staged' : 'saved');
316
- return { kind: 'saved' };
336
+ return true;
337
+ }
338
+
339
+ async saveCode() {
340
+ const ui = this.ui;
341
+ const item = ui.current();
342
+ const unstaged = item.origin !== 'staged';
343
+ const rel = itemPath(item);
344
+ const saved = await this.writeCode(this.editor.text);
345
+ if (!saved) return false;
346
+ const staging = unstaged && this.fileDirty === true;
347
+ if (staging) await this.stageEditedFile(rel);
348
+ this.finishNote(staging ? 'staged' : 'saved');
349
+ return true;
350
+ }
351
+
352
+ saveEdit() {
353
+ return this.composeKind === 'file' ? this.saveFile() : this.saveCode();
354
+ }
355
+
356
+ // One save at a time; `after` runs once the edit is safely on disk
357
+ async saveToDisk(after) {
358
+ if (this.saving) return;
359
+ this.saving = true;
360
+ const settle = () => {
361
+ this.saving = false;
362
+ };
363
+ const saved = await this.saveEdit().finally(settle);
364
+ if (saved && after) after();
365
+ }
366
+
367
+ // Leaves the editor whether or not the edit could be written
368
+ async writeAndClose() {
369
+ const text = this.editor.text;
370
+ if (this.composeKind === 'file') await this.saveUnitFile(text);
371
+ else await this.writeCode(text);
372
+ this.closeCompose();
317
373
  }
318
374
 
319
375
  commitCompose() {
320
376
  const kind = this.composeKind;
321
- if (!kind || !this.editor) return true;
377
+ if (!kind || !this.editor) return;
322
378
  const text = this.editor.text;
323
- if (kind === 'feedback') {
324
- if (this.notes) this.commitFeedback(text);
325
- return true;
326
- }
327
- if (kind === 'tasks') {
328
- this.tasks.commitTask(text);
329
- return true;
330
- }
331
- if (kind === 'code') return this.commitCode(text);
332
- if (kind === 'file') return this.saveUnitFile(text);
333
- return true;
379
+ if (kind === 'feedback' && this.notes) this.commitFeedback(text);
380
+ if (kind === 'tasks') this.tasks.commitTask(text);
381
+ if (kind === 'code') this.commitCode(text);
334
382
  }
335
383
 
384
+ // Notes save at once. An edit to a file answers `saving` and finishes later
336
385
  saveCompose(opts = {}) {
337
386
  const ui = this.ui;
338
387
  const kind = this.composeKind;
339
388
  if (COMMAND_KINDS.includes(kind)) return { kind };
340
- if (kind === 'file') return this.saveFileCompose();
341
- const item = ui.current();
342
- const shouldStage =
343
- kind === 'code' && this.writesCodeFile(item) && item.origin !== 'staged';
344
- const rel = itemPath(item);
389
+ if (this.writesDisk()) {
390
+ ui.ops.hold(this.saveToDisk(opts.after));
391
+ return { kind: 'saving' };
392
+ }
345
393
  const baseline = this.composeBaseline;
346
- if (this.commitCompose() === false) return { kind: 'error' };
347
- const reload = this.fileDirty === true;
394
+ this.commitCompose();
348
395
  if (kind === 'feedback' && this.notes) {
349
396
  const next = this.editor ? this.editor.text : '';
350
- rememberTemplate(this.notes, baseline, next);
351
- }
352
- const staging = shouldStage && reload;
353
- if (staging) this.stageEditedFile(rel);
354
- const doneStatus = staging ? 'staged' : 'saved';
355
- if (reload) {
356
- this.finishSave(true, doneStatus);
357
- } else {
358
- ui.flushReview();
359
- this.closeCompose();
360
- ui.status = doneStatus;
397
+ this.notes.rememberTemplate(baseline, next);
361
398
  }
399
+ this.finishNote('saved');
362
400
  if (opts.editNext === true) this.tasks.editNextTask();
363
401
  return { kind: 'saved' };
364
402
  }
365
403
 
404
+ autosaveFile(text) {
405
+ if (this.saving || text === this.composeBaseline) return;
406
+ this.ui.ops.track(this.saveUnitFile(text));
407
+ }
408
+
366
409
  autosave() {
367
410
  const kind = this.composeKind;
368
411
  const text = this.editor ? this.editor.text : '';
369
- if (kind === 'file' && this.editor) {
370
- if (text === this.composeBaseline) return;
371
- return void this.saveUnitFile(text);
372
- }
373
- const editingCode = kind === 'code';
412
+ if (kind === 'file' && this.editor) return void this.autosaveFile(text);
374
413
  const reviewKind = kind !== null && !COMMAND_KINDS.includes(kind);
375
- const fileEdit = editingCode && this.writesCodeFile(this.ui.current());
376
- if (reviewKind && !fileEdit && (editingCode || text.trim())) {
377
- this.commitCompose();
378
- }
414
+ const noted = reviewKind && !this.writesDisk();
415
+ if (noted && (kind === 'code' || text.trim())) this.commitCompose();
379
416
  if (this.notes && this.notes.dirty) this.ui.flushReview();
380
417
  }
381
418
 
@@ -536,6 +573,12 @@ class Composer {
536
573
  ui.status = 'read only';
537
574
  return;
538
575
  }
576
+ ui.ops.hold(this.editViewed());
577
+ }
578
+
579
+ async editViewed() {
580
+ const ui = this.ui;
581
+ await ui.reads.loadText(ui.nav.reviewPath);
539
582
  const at = unit.editCursor(ui);
540
583
  this.openCompose('file', at.text);
541
584
  this.fileEditRows = at.rows ?? null;
@@ -544,11 +587,24 @@ class Composer {
544
587
  this.followFileEdit();
545
588
  }
546
589
 
590
+ onFileEdit() {
591
+ const ui = this.ui;
592
+ const list = ui.fileList();
593
+ const entry = list[ui.nav.fileCursor];
594
+ if (!entry || isTasksEntry(entry)) {
595
+ ui.status = 'not a diff block';
596
+ return;
597
+ }
598
+ const edit = () => this.onUnitEdit();
599
+ ui.ops.hold(unit.showFile(ui, entry).then(edit));
600
+ }
601
+
547
602
  onCode() {
548
603
  const ui = this.ui;
604
+ if (ui.nav.pane === 'files') return void this.onFileEdit();
549
605
  if (ui.nav.pane === 'unit') return void this.onUnitEdit();
550
606
  const item = ui.current();
551
- if (ui.nav.pane === 'files' || !canEditCode(item)) {
607
+ if (!canEditCode(item)) {
552
608
  ui.status = 'not a diff block';
553
609
  return;
554
610
  }
@@ -594,16 +650,11 @@ class Composer {
594
650
  }
595
651
 
596
652
  copy() {
597
- const text = this.editor.selectedText();
598
- if (!text) return;
599
- const ok = clipboard.copyText(text, this.ui.stdout);
600
- this.ui.status = ok ? 'copied' : 'copy failed';
653
+ copyLine(this.editor, this.ui);
601
654
  }
602
655
 
603
656
  cut() {
604
- if (!this.editor.hasSelect()) return;
605
- this.copy();
606
- this.editor.removeSpan();
657
+ cutLine(this.editor, this.ui);
607
658
  }
608
659
 
609
660
  paste() {
@@ -631,10 +682,7 @@ class Composer {
631
682
  this.afterEdit();
632
683
  return result ?? null;
633
684
  }
634
- if (!key.length || key.startsWith('ctrl-') || key.startsWith('alt-')) {
635
- return null;
636
- }
637
- if (key.charCodeAt(0) >= 32) {
685
+ if (isTextKey(key)) {
638
686
  this.templates.clearTemplatePick();
639
687
  this.editor.insert(key);
640
688
  this.afterEdit();
@@ -645,12 +693,15 @@ class Composer {
645
693
  applyDiskText(text) {
646
694
  const editor = this.editor;
647
695
  if (this.composeKind !== 'file' || !editor) return;
648
- if (text === editor.text) return;
696
+ // The baseline is the last text known on disk: no news, keep the typing
697
+ if (text === editor.text || text === this.composeBaseline) return;
649
698
  const cursor = editor.cursor;
650
699
  editor.text = text;
651
700
  editor.cursor = Math.min(cursor, text.length);
652
701
  editor.anchor = null;
653
702
  this.composeBaseline = text;
703
+ // An outside change is the new starting point, not an edit made here
704
+ this.fileOpened = text;
654
705
  this.fileEditRows = unit.linesFor(this.ui, this.ui.nav.reviewPath, null);
655
706
  this.syncCodeScroll();
656
707
  this.followFileEdit();