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,15 +1,12 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const files = require('../files.js');
5
- const { THEME_NAMES, setTheme, themeName } = ansi;
6
- const { isTasksEntry, TASKS_FILE } = files;
7
- const npm = require('../render/npm.js');
8
- const { logViewRows } = npm;
9
- const unit = require('./unit.js');
10
- const actions = require('./actions.js');
11
- const { LIST_PANES } = actions;
12
- const { setFileScope, openUnitFile, moveUnitBlock } = unit;
3
+ const { clamp } = require('../common/utilities.js');
4
+ const { THEME_NAMES, setTheme, themeName } = require('../term/ansi.js');
5
+ const { isTasksEntry, TASKS_FILE } = require('../common/files.js');
6
+ const { logViewRows } = require('../render/log.js');
7
+ const { setFileScope, openUnitFile, moveUnitBlock } = require('./unit.js');
8
+ const { LIST_PANES } = require('../input/actions.js');
9
+ const { saveUiPrefs, LAYOUT_LABEL } = require('./prefs.js');
13
10
 
14
11
  const LAYOUTS = ['unified', 'mixed', 'side'];
15
12
 
@@ -31,11 +28,8 @@ const syncFileCursor = (ui) => {
31
28
 
32
29
  const showFiles = (ui) => {
33
30
  const nav = ui.nav;
34
- const held =
35
- nav.tasksOpen === true ||
36
- nav.pane === 'branches' ||
37
- nav.pane === 'commits' ||
38
- nav.pane === 'npm';
31
+ const listed = nav.pane !== 'files' && LIST_PANES.includes(nav.pane);
32
+ const held = nav.tasksOpen === true || listed;
39
33
  nav.pane = 'files';
40
34
  nav.resetListScroll('files');
41
35
  nav.clearSelection();
@@ -66,15 +60,26 @@ const showDashboard = (ui) => {
66
60
  ui.dashboard.enter();
67
61
  };
68
62
 
69
- const HOME_CHILDREN = ['files', 'branches', 'commits', 'npm'];
70
-
71
63
  const returnsHome = (ui) => {
72
64
  if (!ui.dashboardHome) return false;
73
65
  const { pane, tasksOpen } = ui.nav;
74
- return HOME_CHILDREN.includes(pane) || tasksOpen === true;
66
+ return LIST_PANES.includes(pane) || tasksOpen === true;
75
67
  };
76
68
 
77
69
  const onEscape = (ui) => {
70
+ if (ui.nav.pane === 'agents' && ui.agents.pick) {
71
+ return void ui.agents.closePick();
72
+ }
73
+ if (ui.nav.pane === 'agents' && ui.agents.viewing) {
74
+ return void ui.agents.closeView();
75
+ }
76
+ if (ui.nav.pane === 'npm' && ui.npm.viewing) {
77
+ return void ui.npm.closeView();
78
+ }
79
+ if (ui.nav.pane === 'repos') return void ui.onQuit();
80
+ if (ui.nav.pane === 'dashboard' && ui.workspaceRoot) {
81
+ return void ui.workspace.show();
82
+ }
78
83
  if (ui.nav.pane === 'dashboard') return void ui.onQuit();
79
84
  const commitDiff = ui.nav.pane === 'diff' && viewingCommit(ui);
80
85
  if (returnsHome(ui) || (ui.dashboardHome && commitDiff)) {
@@ -101,11 +106,13 @@ const onFiles = (ui) => {
101
106
  };
102
107
 
103
108
  const onOpenFile = (ui) => {
109
+ if (ui.nav.pane === 'packages') return;
104
110
  if (ui.nav.pane === 'branches') {
105
111
  return void ui.gitBranches.onCheckoutBranch();
106
112
  }
107
113
  if (ui.nav.pane === 'commits') return void ui.commits.onShowDiff();
108
114
  if (ui.nav.pane === 'npm') return void ui.npm.run();
115
+ if (ui.nav.pane === 'agents') return void ui.agents.start();
109
116
  if (ui.nav.pane !== 'files') {
110
117
  if (ui.nav.tasksOpen) ui.composer.tasks.editFocusedTask();
111
118
  return;
@@ -138,7 +145,7 @@ const onFileMove = (ui, delta) => {
138
145
  }
139
146
  const last = list.length - 1;
140
147
  const moved = ui.nav.fileCursor + delta;
141
- const next = Math.min(last, Math.max(0, moved));
148
+ const next = clamp(moved, 0, last);
142
149
  if (next === ui.nav.fileCursor) return;
143
150
  ui.nav.fileCursor = next;
144
151
  const entry = list[next];
@@ -185,7 +192,8 @@ const scrollStep = (ui, fraction) => {
185
192
  const fallback = Math.max(1, rows - 4);
186
193
  const bodyH = frame?.bodyH ?? fallback;
187
194
  const viewing = ui.nav.pane === 'npm' && ui.npm.viewing;
188
- const viewRows = viewing ? logViewRows(bodyH) : bodyH;
195
+ const agentLog = ui.nav.pane === 'agents' && ui.agents.viewing;
196
+ const viewRows = viewing || agentLog ? logViewRows(bodyH) : bodyH;
189
197
  const height = Math.max(1, viewRows);
190
198
  const span = Math.floor(height * Math.abs(fraction));
191
199
  const n = Math.max(1, span);
@@ -194,6 +202,10 @@ const scrollStep = (ui, fraction) => {
194
202
  };
195
203
 
196
204
  const onScroll = (ui, delta) => {
205
+ if (ui.composer.tasks.planOpen && ui.nav.pane === 'tasks') {
206
+ const step = delta < 0 ? -1 : 1;
207
+ return void ui.composer.tasks.movePlan(step);
208
+ }
197
209
  if (ui.nav.pane === 'dashboard') return;
198
210
  if (ui.nav.pane === 'files') return void onFileMove(ui, delta);
199
211
  if (ui.nav.pane === 'branches') {
@@ -203,6 +215,16 @@ const onScroll = (ui, delta) => {
203
215
  return void ui.commits.onCommitMove(delta);
204
216
  }
205
217
  if (ui.nav.pane === 'npm') return void ui.npm.move(delta);
218
+ if (ui.nav.pane === 'agents') return void ui.agents.move(delta);
219
+ if (ui.nav.pane === 'packages') {
220
+ const count = ui.dashboard.packageCount();
221
+ const nav = ui.nav;
222
+ const next = nav.packagesCursor + delta;
223
+ const last = Math.max(0, count - 1);
224
+ nav.packagesCursor = clamp(next, 0, last);
225
+ ui.status = '';
226
+ return;
227
+ }
206
228
  if (ui.nav.tasksOpen && ui.composer.tasks.taskRowCount() > 1) {
207
229
  return void ui.composer.tasks.moveTaskFocus(delta);
208
230
  }
@@ -238,10 +260,22 @@ const onLayout = (ui) => {
238
260
  const i = LAYOUTS.indexOf(ui.layout);
239
261
  const next = (i < 0 ? 0 : i + 1) % LAYOUTS.length;
240
262
  ui.layout = LAYOUTS[next];
241
- const name = LAYOUTS[next] === 'side' ? 'side-by-side' : LAYOUTS[next];
242
- ui.status = name;
263
+ ui.status = LAYOUT_LABEL[ui.layout] || ui.layout;
243
264
  ui.nav.scroll = 0;
244
265
  ui.nav.clearSelection();
266
+ saveUiPrefs(ui.top || ui.cwd, { layout: ui.layout });
267
+ };
268
+
269
+ const saveLineNumbers = (ui) => {
270
+ const shown = ui.lineNumbers === true;
271
+ saveUiPrefs(ui.top || ui.cwd, { lineNumbers: shown });
272
+ };
273
+
274
+ const onLineNumbers = (ui) => {
275
+ ui.lineNumbers = ui.lineNumbers !== true;
276
+ ui.status = ui.lineNumbers ? 'line numbers' : 'no line numbers';
277
+ saveLineNumbers(ui);
278
+ ui.composer.syncCodeScroll();
245
279
  };
246
280
 
247
281
  const onTheme = (ui) => {
@@ -249,12 +283,13 @@ const onTheme = (ui) => {
249
283
  const next = THEME_NAMES[(i + 1) % THEME_NAMES.length];
250
284
  setTheme(next);
251
285
  ui.status = next;
286
+ saveUiPrefs(ui.top || ui.cwd, { theme: next });
252
287
  };
253
288
 
254
289
  const onHome = (ui) => onJump(ui, false);
255
290
  const onEnd = (ui) => onJump(ui, true);
256
- const onFileScope = (ui) => setFileScope(ui, 'file');
257
- const onDiffScope = (ui) => setFileScope(ui, 'diff');
291
+ const onFileScope = (ui, quiet) => setFileScope(ui, 'file', quiet === true);
292
+ const onDiffScope = (ui, quiet) => setFileScope(ui, 'diff', quiet === true);
258
293
 
259
294
  module.exports = {
260
295
  fileCursorEntry,
@@ -272,6 +307,7 @@ module.exports = {
272
307
  onHome,
273
308
  onEnd,
274
309
  onLayout,
310
+ onLineNumbers,
275
311
  onTheme,
276
312
  onFileScope,
277
313
  onDiffScope,
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const { isTasksEntry, TASKS_FILE } = require('../common/files.js');
4
+ const { fileMatches } = require('../common/find.js');
5
+ const { lineKey, lineField } = require('../input/line-edit.js');
6
+
7
+ const FIND_PANES = ['files', 'diff'];
8
+
9
+ const startFind = (ui) => {
10
+ if (ui.mode !== 'review') return;
11
+ if (!FIND_PANES.includes(ui.pane)) return;
12
+ ui.mode = 'find';
13
+ const find = lineField('query');
14
+ find.at = 0;
15
+ ui.nav.find = find;
16
+ ui.status = '';
17
+ };
18
+
19
+ const stopFind = (ui) => {
20
+ ui.mode = 'review';
21
+ ui.nav.find = null;
22
+ ui.status = '';
23
+ };
24
+
25
+ const focusMatch = (ui, hit, jump) => {
26
+ const entry = ui.fileList()[hit.index];
27
+ if (!entry) return;
28
+ const moved = ui.nav.fileCursor !== hit.index;
29
+ ui.nav.fileCursor = hit.index;
30
+ if (isTasksEntry(entry)) {
31
+ ui.nav.reviewPath = TASKS_FILE;
32
+ return;
33
+ }
34
+ ui.nav.reviewPath = entry.path;
35
+ if (ui.nav.pane !== 'diff') {
36
+ ui.nav.index = entry.firstIndex;
37
+ return;
38
+ }
39
+ if (!moved && !jump) return;
40
+ ui.nav.index = entry.openIndex ?? entry.firstIndex;
41
+ ui.nav.scroll = 0;
42
+ ui.nav.clearSelection();
43
+ };
44
+
45
+ const showHit = (ui, jump) => {
46
+ const state = ui.nav.find;
47
+ if (!state) return;
48
+ const hits = fileMatches(ui.fileList(), state.query);
49
+ if (!hits.length) return;
50
+ const count = hits.length;
51
+ let at = state.at % count;
52
+ if (at < 0) at += count;
53
+ state.at = at;
54
+ focusMatch(ui, hits[at], jump);
55
+ };
56
+
57
+ const moveFind = (ui, delta) => {
58
+ const state = ui.nav.find;
59
+ if (!state.query) return;
60
+ state.at += delta;
61
+ showHit(ui, false);
62
+ };
63
+
64
+ const onFindKey = (ui, key) => {
65
+ if (!ui.nav.find) {
66
+ ui.mode = 'review';
67
+ return;
68
+ }
69
+ if (key === 'escape' || key === 'enter') return void stopFind(ui);
70
+ if (key === 'backspace' && !ui.nav.find.query) return void stopFind(ui);
71
+ const handled = lineKey(ui.nav.find.editor, key, ui, true);
72
+ if (handled === 'text') {
73
+ ui.nav.find.at = 0;
74
+ showHit(ui, true);
75
+ }
76
+ if (handled) return;
77
+ if (key === 'up') return void moveFind(ui, -1);
78
+ if (key === 'down') moveFind(ui, 1);
79
+ };
80
+
81
+ module.exports = { FIND_PANES, startFind, onFindKey };
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const { remoteKind, selectChangeSource } = require('../source/source.js');
4
+ const { lineKey, lineField } = require('../input/line-edit.js');
5
+
6
+ const startImport = (ui) => {
7
+ if (ui.mode !== 'review' || ui.pane !== 'tasks') return;
8
+ ui.composer.tasks.closePlanMenu();
9
+ ui.mode = 'import';
10
+ ui.nav.import = lineField('url');
11
+ ui.status = '';
12
+ };
13
+
14
+ const stopImport = (ui) => {
15
+ ui.mode = 'review';
16
+ ui.nav.import = null;
17
+ ui.status = '';
18
+ };
19
+
20
+ const noteImport = (ui, message) => {
21
+ ui.status = message;
22
+ if (ui.uiOpen) ui.paint();
23
+ };
24
+
25
+ const finishImport = (ui, store, loaded) => {
26
+ const imported = loaded && loaded.imported;
27
+ if (!imported || ui.review.store !== store) {
28
+ noteImport(ui, 'import failed');
29
+ return;
30
+ }
31
+ store.applyImportedNotes(imported);
32
+ ui.flushReview();
33
+ noteImport(ui, 'imported');
34
+ };
35
+
36
+ const runImport = (ui, selected) => {
37
+ const kind = remoteKind(selected);
38
+ if (!kind) return Promise.resolve(null);
39
+ const load = ui[kind.option] ?? kind.load;
40
+ const target = kind.target(selected);
41
+ if (!load || !target) return Promise.resolve(null);
42
+ const fetch = ui.fetchImpl ?? globalThis.fetch;
43
+ const options = { cwd: ui.top || ui.cwd, token: kind.token(), fetch };
44
+ return Promise.resolve().then(() => load(target, options));
45
+ };
46
+
47
+ const submitImport = (ui) => {
48
+ const state = ui.nav.import;
49
+ const url = state ? state.url.trim() : '';
50
+ const store = ui.review.store;
51
+ stopImport(ui);
52
+ if (!url) return;
53
+ const selected = selectChangeSource([url]);
54
+ if (!remoteKind(selected)) {
55
+ ui.status = 'bad url';
56
+ return;
57
+ }
58
+ ui.status = 'importing';
59
+ const pending = runImport(ui, selected);
60
+ ui.importPromise = pending;
61
+ pending.then(
62
+ (loaded) => finishImport(ui, store, loaded),
63
+ (error) => {
64
+ const message = error instanceof Error ? error.message : '';
65
+ noteImport(ui, message || 'import failed');
66
+ },
67
+ );
68
+ };
69
+
70
+ const onImportKey = (ui, key) => {
71
+ const state = ui.nav.import;
72
+ if (!state) {
73
+ ui.mode = 'review';
74
+ return;
75
+ }
76
+ if (key === 'escape') return void stopImport(ui);
77
+ if (key === 'enter') return void submitImport(ui);
78
+ if (key === 'backspace' && !state.url) return void stopImport(ui);
79
+ lineKey(state.editor, key, ui, true);
80
+ };
81
+
82
+ module.exports = { startImport, stopImport, onImportKey };
@@ -1,11 +1,13 @@
1
1
  'use strict';
2
2
 
3
- const files = require('../files.js');
4
- const { itemPath, TASKS_FILE, isTaskItem } = files;
5
- const review = require('../review.js');
6
- const { feedbackKey } = review;
7
- const diff = require('../diff/diff.js');
8
- const { refreshIndexPatches } = diff;
3
+ const {
4
+ itemPath,
5
+ TASKS_FILE,
6
+ isTaskItem,
7
+ groupByPath,
8
+ } = require('../common/files.js');
9
+ const { feedbackKey } = require('../review/review.js');
10
+ const { refreshIndexPatches } = require('../diff/diff.js');
9
11
 
10
12
  const depChangeKey = (item) => {
11
13
  const change = item.dep && item.dep.change;
@@ -72,17 +74,6 @@ const takeBucket = (map, key, used) => {
72
74
  return null;
73
75
  };
74
76
 
75
- const groupByPath = (items) => {
76
- const groups = new Map();
77
- for (const item of items) {
78
- const rel = itemPath(item) || '';
79
- const list = groups.get(rel) ?? [];
80
- list.push(item);
81
- groups.set(rel, list);
82
- }
83
- return groups;
84
- };
85
-
86
77
  const alignGroup = (prev, next) => {
87
78
  if (!prev.length) return next;
88
79
  if (!next.length) return [];
@@ -3,20 +3,16 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
 
6
- const utilities = require('../utilities.js');
7
- const { isAbort } = utilities;
8
- const sessionLoad = require('./load.js');
9
- const { tagLoaded, fetchSnapshot, fetchExtras } = sessionLoad;
10
- const watch = require('./watch.js');
11
- const { DiskWatcher, DEBOUNCE_MS } = watch;
12
- const files = require('../files.js');
13
- const { itemPath, relativeAge } = files;
14
- const deps = require('../deps.js');
15
- const { MANIFEST, LOCKFILE, depFileMeta } = deps;
16
- const review = require('../review.js');
17
- const { hasNotes, loadReview } = review;
18
- const reviewMerge = require('../review-merge.js');
19
- const { mergeReview } = reviewMerge;
6
+ const { isAbort } = require('../common/process.js');
7
+ const { tagLoaded, fetchSnapshot, fetchExtras } = require('./load.js');
8
+ const { DiskWatcher, DEBOUNCE_MS } = require('../dashboard/watch.js');
9
+ const { itemPath, REVIEW_DIR } = require('../common/files.js');
10
+ const { relativeAge } = require('../common/format.js');
11
+ const { MANIFEST, LOCKFILE, depFileMeta } = require('../deps/deps.js');
12
+ const { hasNotes, loadReview } = require('../review/review.js');
13
+ const { setTheme } = require('../term/ansi.js');
14
+ const { readUiPrefs } = require('./prefs.js');
15
+ const { mergeReview } = require('../review/merge.js');
20
16
 
21
17
  const WATCH_IGNORE_MS = 500;
22
18
 
@@ -147,8 +143,9 @@ class Lifecycle {
147
143
  syncUnitEditor() {
148
144
  const ui = this.ui;
149
145
  const rel = ui.nav.reviewPath;
150
- if (ui.nav.pane !== 'unit' || !rel || !ui.repo.fileText) return;
151
- ui.composer.applyDiskText(ui.repo.fileText(this.root, rel, ui.rev));
146
+ if (ui.nav.pane !== 'unit' || !rel) return;
147
+ const text = ui.reads.cached(rel);
148
+ if (text !== null) ui.composer.applyDiskText(text);
152
149
  }
153
150
 
154
151
  syncItemWatches() {
@@ -161,28 +158,49 @@ class Lifecycle {
161
158
  this.diskWatcher.watchFiles(rels);
162
159
  }
163
160
 
161
+ applyPrefs() {
162
+ const ui = this.ui;
163
+ const saved = readUiPrefs(ui.top);
164
+ ui.lineNumbers = saved.lineNumbers;
165
+ ui.layout = saved.layout;
166
+ ui.commits.commitView = saved.commitView;
167
+ setTheme(saved.theme);
168
+ }
169
+
170
+ applySource(loaded) {
171
+ const ui = this.ui;
172
+ if (loaded.rev) ui.rev = loaded.rev;
173
+ if (loaded.revShort) ui.revShort = loaded.revShort;
174
+ else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
175
+ if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
176
+ if (loaded.change) ui.change = loaded.change;
177
+ if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
178
+ const repository = ui.change ? ui.change.repository : '';
179
+ if (!ui.repoName && repository) ui.repoName = repository;
180
+ }
181
+
164
182
  applyLoaded(loaded) {
165
183
  const ui = this.ui;
184
+ if (ui.nav.pane === 'repos') return false;
166
185
  const { loader, collection, nav } = ui;
167
186
  if (loaded.generation !== undefined) {
168
187
  if (!loader.isCurrent(loaded.generation, ui.done)) return false;
169
188
  }
170
189
  const wasTasks = nav.tasksOpen === true;
190
+ const firstLoad = !loader.didLoad;
171
191
  const here = ui.current();
172
192
  ui.top = loaded.top ?? ui.cwd;
193
+ this.applyPrefs();
173
194
  collection.replace(loaded.items);
174
195
  stampItemDates(ui.top, collection.items);
175
- if (loaded.rev) ui.rev = loaded.rev;
176
- if (loaded.revShort) ui.revShort = loaded.revShort;
177
- else if (ui.rev && !ui.revShort) ui.revShort = ui.rev.slice(0, 7);
178
- if (loaded.sourceLabel) ui.sourceLabel = loaded.sourceLabel;
179
- if (loaded.change) ui.change = loaded.change;
180
- if (loaded.branch !== undefined) ui.branch = loaded.branch || '';
181
- const repository = ui.change ? ui.change.repository : '';
182
- if (!ui.repoName && repository) ui.repoName = repository;
196
+ this.applySource(loaded);
183
197
  if (!ui.review.store) ui.initReview();
184
198
  ui.review.applyImported(loaded.imported);
185
199
  this.restoreAfterLoad(here, wasTasks);
200
+ const issue = loaded.change && loaded.change.source === 'issue';
201
+ if (firstLoad && issue && !collection.items.length) {
202
+ ui.composer.tasks.openTasksPage();
203
+ }
186
204
  loader.didLoad = true;
187
205
  ui.updater.maybePrompt();
188
206
  loader.pendingExtras = loaded.pending === true;
@@ -192,10 +210,9 @@ class Lifecycle {
192
210
  return true;
193
211
  }
194
212
 
195
- load() {
196
- const ui = this.ui;
197
- const loaded = ui.repo.load(ui.cwd, ui.paths, this.loadOptions());
198
- this.applyLoaded(loaded);
213
+ async load() {
214
+ await this.openLoad();
215
+ if (this.ui.loadError) throw this.ui.loadError;
199
216
  }
200
217
 
201
218
  npmStamp() {
@@ -245,13 +262,18 @@ class Lifecycle {
245
262
  paths: ui.paths,
246
263
  signal: ui.loader.signal,
247
264
  });
248
- let next;
249
- try {
250
- next = await fetchExtras(ui.repo, loaded, extra);
251
- } catch (error) {
252
- if (ui.loader.isCurrent(gen)) this.failOpen(error);
253
- return;
254
- }
265
+ const takeExtras = async () => {
266
+ try {
267
+ const value = await fetchExtras(ui.repo, loaded, extra);
268
+ return { value };
269
+ } catch (error) {
270
+ if (ui.loader.isCurrent(gen)) this.failOpen(error);
271
+ return null;
272
+ }
273
+ };
274
+ const outcome = await takeExtras();
275
+ if (!outcome) return;
276
+ const next = outcome.value;
255
277
  if (!this.isWanted(gen)) return;
256
278
  if (next) {
257
279
  this.applyLoaded(tagLoaded(next, gen));
@@ -283,7 +305,7 @@ class Lifecycle {
283
305
 
284
306
  afterSnapshot(kind, extra = {}) {
285
307
  const nav = this.ui.nav;
286
- if (nav.pane === 'commits') this.ui.commits.refresh();
308
+ if (nav.pane === 'commits') this.ui.commits.reload();
287
309
  if (kind !== 'open' && kind !== 'watch') {
288
310
  nav.scroll = 0;
289
311
  nav.clearSelection();
@@ -298,15 +320,18 @@ class Lifecycle {
298
320
  ui.ops.setBusy('loading');
299
321
  ui.paint();
300
322
  }
301
- let loaded;
323
+ let loaded = null;
324
+ const side = ui.reads.read();
302
325
  try {
303
326
  const opts = this.snapshotOpts({ signal: ui.loader.signal });
304
327
  loaded = await fetchSnapshot(ui.repo, ui.cwd, ui.paths, opts);
305
328
  } catch (error) {
306
329
  if (ui.loader.isCurrent(gen)) this.failOpen(error);
307
- return;
308
330
  }
331
+ if (!loaded) return;
332
+ const found = await side;
309
333
  if (!this.isWanted(gen)) return;
334
+ ui.reads.apply(found);
310
335
  this.applyLoaded(tagLoaded(loaded, gen));
311
336
  this.afterSnapshot(kind, extra);
312
337
  if (loaded.pending === true || this.npmExtrasStale()) {
@@ -325,42 +350,35 @@ class Lifecycle {
325
350
 
326
351
  openLoad() {
327
352
  const loader = this.ui.loader;
353
+ if (this.ui.nav.pane === 'repos') {
354
+ loader.didLoad = true;
355
+ loader.promise = Promise.resolve();
356
+ return loader.promise;
357
+ }
328
358
  if (loader.promise) return loader.promise;
329
359
  if (loader.didLoad && !loader.pendingExtras) {
330
360
  loader.promise = Promise.resolve();
331
361
  } else {
332
- loader.promise = this.runSnapshot(loader.bump(), 'open');
362
+ const run = this.runSnapshot(loader.bump(), 'open');
363
+ loader.promise = this.ui.ops.track(run);
333
364
  }
334
365
  return loader.promise;
335
366
  }
336
367
 
337
368
  async loadReady() {
338
- this.ui.ensureRepo();
339
- await this.openLoad();
340
- if (this.ui.loadError) throw this.ui.loadError;
369
+ if (this.ui.nav.pane === 'repos') {
370
+ this.ui.loader.didLoad = true;
371
+ return;
372
+ }
373
+ await this.ui.ensureRepo();
374
+ await this.load();
341
375
  }
342
376
 
343
377
  refreshFromRepo(extra = {}) {
344
378
  const ui = this.ui;
345
379
  ui.loader.promise = null;
346
380
  const gen = ui.loader.bump();
347
- const kind = kindFromExtra(extra);
348
- if (ui.uiOpen && ui.repo.loadAsync) {
349
- return void this.runSnapshot(gen, kind, extra);
350
- }
351
- const loaded = ui.repo.load(ui.cwd, ui.paths, this.snapshotOpts(extra));
352
- this.applyLoaded(tagLoaded(loaded, gen));
353
- this.afterSnapshot(kind, extra);
354
- if (loaded.pending === true || this.npmExtrasStale()) {
355
- ui.ops.setBusy('checking npm');
356
- return void this.followExtras(loaded, gen, kind);
357
- }
358
- if (kind === 'keep') return;
359
- this.finishIfEmpty(kind);
360
- if (extra.doneStatus && !ui.done && !ui.loader.pendingExtras) {
361
- ui.status = extra.doneStatus;
362
- }
363
- if (kind === 'watch') ui.paint();
381
+ ui.ops.track(this.runSnapshot(gen, kindFromExtra(extra), extra));
364
382
  }
365
383
 
366
384
  reloadAfterChange(afterLoad) {
@@ -375,11 +393,7 @@ class Lifecycle {
375
393
  ui.syncFileCursor();
376
394
  if (afterLoad) afterLoad();
377
395
  };
378
- if (ui.uiOpen && ui.repo.loadAsync) {
379
- return void this.refreshFromRepo({ afterLoad: restore });
380
- }
381
- this.refreshFromRepo();
382
- restore();
396
+ this.refreshFromRepo({ afterLoad: restore });
383
397
  }
384
398
 
385
399
  onReload() {
@@ -407,6 +421,7 @@ class Lifecycle {
407
421
  }
408
422
 
409
423
  onDiskChange() {
424
+ if (this.ui.nav.pane === 'repos') return;
410
425
  if (Date.now() < this.ignoreWatchUntil) return;
411
426
  if (!this.shouldDeferWatch()) {
412
427
  return void this.refreshFromRepo({ keepView: true, quiet: true });
@@ -444,12 +459,13 @@ class Lifecycle {
444
459
  return;
445
460
  }
446
461
  const store = this.ui.review.store;
447
- let disk;
462
+ let disk = null;
448
463
  try {
449
464
  disk = loadReview(store.reviewPath, store.templates);
450
465
  } catch {
451
- return;
466
+ // unreadable review
452
467
  }
468
+ if (!disk) return;
453
469
  mergeReview(store, disk);
454
470
  this.reviewStamp = stamp;
455
471
  this.ui.paint();
@@ -465,6 +481,20 @@ class Lifecycle {
465
481
 
466
482
  startWatch() {
467
483
  this.stopWatch();
484
+ const ui = this.ui;
485
+ if (ui.nav.pane === 'repos') {
486
+ const root = ui.workspaceRoot;
487
+ if (!root) return;
488
+ const info = ui.workspaceInfo;
489
+ const repos = info ? info.repos : [];
490
+ const paths = ['.'];
491
+ for (const repo of repos) paths.push(path.join(repo.name, '.git'));
492
+ const matchReview = (rel) => rel.includes(`/${REVIEW_DIR}/`);
493
+ const onPaths = (rels) => ui.workspace.notePaths(rels);
494
+ const options = { root, paths, matchReview, onPaths };
495
+ this.diskWatcher = new DiskWatcher(options);
496
+ return;
497
+ }
468
498
  const root = this.root;
469
499
  if (!isWorktree(this.ui) || !root) return;
470
500
  this.diskWatcher = new DiskWatcher({
@@ -40,10 +40,8 @@ class LoadCoordinator {
40
40
 
41
41
  const tagLoaded = (loaded, gen) => ({ ...loaded, generation: gen });
42
42
 
43
- const fetchSnapshot = async (repo, cwd, paths, options) => {
44
- if (repo.loadAsync) return repo.loadAsync(cwd, paths, options);
45
- return repo.load(cwd, paths, options);
46
- };
43
+ const fetchSnapshot = async (repo, cwd, paths, options) =>
44
+ repo.load(cwd, paths, options);
47
45
 
48
46
  const fetchExtras = async (repo, loaded, extra) => {
49
47
  if (!repo.loadExtras) return null;