reslop 0.2.7 → 0.3.1

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 +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  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 +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  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} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -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 +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -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/{dash-blocks.js → dashboard/blocks.js} +131 -137
  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 +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  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} +13 -60
  72. package/lib/render/tiles.js +109 -48
  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} +48 -19
  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 +14 -0
  81. package/lib/review/format.js +420 -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 +976 -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 +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -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} +158 -63
  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 +65 -65
  126. package/lib/session/workspace.js +39 -78
  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} +4 -3
  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 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  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,33 +1,30 @@
1
1
  'use strict';
2
2
 
3
- const compose = require('./compose.js');
4
- const { COMPOSE_LEAVE } = compose;
5
- const commit = require('./commit.js');
6
- const { hasStagedChanges, listedCommits } = commit;
7
- const actions = require('./actions.js');
3
+ const { COMPOSE_LEAVE } = require('./compose.js');
4
+ const { listedCommits } = require('./commit.js');
5
+ const actions = require('../input/actions.js');
8
6
  const { LIST_PANES, DASH_BLOCKS, DASH_ALLOWED, CONFIRM_MODE } = actions;
9
7
  const { REPOS_ALLOWED } = actions;
10
8
  const { disabledActions, actionFromKey, confirmKey, lineHasDiff } = actions;
11
- const changeActions = require('./change-actions.js');
12
- const { onCommand } = changeActions;
9
+ const { onCommand } = require('./change-actions.js');
13
10
  const filesPane = require('./files-pane.js');
14
- const pointer = require('./pointer.js');
15
- const { handlePointer } = pointer;
16
- const finder = require('./find.js');
17
- const { FIND_PANES, startFind, onFindKey } = finder;
11
+ const { handlePointer } = require('./pointer.js');
12
+ const { FIND_PANES, startFind, onFindKey } = require('./find.js');
13
+ const { startImport, stopImport, onImportKey } = require('./import.js');
14
+ const { unitBodyLines } = require('./view.js');
18
15
 
19
16
  const currentTarget = (ui) => {
20
17
  if (ui.pane === 'files') return ui.fileCursorEntry();
21
18
  if (ui.pane === 'branches') return ui.branches[ui.branchCursor];
22
19
  if (ui.pane === 'commits') {
23
20
  const entry = listedCommits(ui.commits.commits)[ui.commitCursor];
24
- const canCommit = hasStagedChanges(ui);
21
+ const canCommit = ui.commits.staged;
25
22
  const unstagedCurrent = !ui.rev && !ui.revShort;
26
23
  return { ...entry, canCommit, unstagedCurrent };
27
24
  }
28
25
  if (ui.pane !== 'unit') return ui.current();
29
26
  const item = ui.current();
30
- const lines = ui.view().unitLines ?? [];
27
+ const lines = unitBodyLines(ui);
31
28
  if (!lines.length) return item;
32
29
  const line = lines[ui.nav.unitLine] ?? null;
33
30
  const lineDiff = lineHasDiff(line);
@@ -53,6 +50,7 @@ const HANDLERS = {
53
50
  next: filesPane.onNext,
54
51
  prev: filesPane.onPrev,
55
52
  layout: filesPane.onLayout,
53
+ lines: filesPane.onLineNumbers,
56
54
  theme: filesPane.onTheme,
57
55
  files: filesPane.onFiles,
58
56
  open: (ui) => {
@@ -97,6 +95,10 @@ const HANDLERS = {
97
95
  if (ui.pane === 'tasks') return void ui.composer.tasks.removeFocusedTask();
98
96
  ui.gitBranches.onDropBranch();
99
97
  },
98
+ taskUp: (ui) => ui.composer.tasks.moveIssue(-1),
99
+ taskDown: (ui) => ui.composer.tasks.moveIssue(1),
100
+ plan: (ui) => ui.composer.tasks.togglePlanMenu(),
101
+ import: (ui) => startImport(ui),
100
102
  npm: (ui) => ui.npm.open(),
101
103
  npmEdit: (ui) => ui.npm.edit(),
102
104
  npmNew: (ui) => ui.npm.create(),
@@ -106,9 +108,17 @@ const HANDLERS = {
106
108
  packageWanted: (ui) => ui.packages.updateWanted(),
107
109
  packageLatest: (ui) => ui.packages.updateLatest(),
108
110
  npmLogs: (ui) => ui.npm.askLogs(),
109
- npmStop: (ui) => ui.npm.closeView(),
111
+ npmStop: (ui) => ui.npm.stop(),
110
112
  npmVerbose: (ui) => ui.npm.toggleVerbose(),
111
113
  npmRerun: (ui) => ui.npm.rerun(),
114
+ agentModel: (ui) => ui.agents.openPick('model'),
115
+ agentEffort: (ui) => ui.agents.openPick('effort'),
116
+ agentFast: (ui) => ui.agents.toggleFast(),
117
+ agentContext: (ui) => ui.agents.openPick('context'),
118
+ agentReview: (ui) => ui.agents.openPick('plan'),
119
+ agentContinue: (ui) => ui.agents.continueSession(),
120
+ agentStop: (ui) => ui.agents.stop(),
121
+ agentRerun: (ui) => ui.agents.rerun(),
112
122
  quit: (ui) => ui.onQuit(),
113
123
  repoLeft: (ui) => ui.workspace.move(-1, 0),
114
124
  repoRight: (ui) => ui.workspace.move(1, 0),
@@ -119,10 +129,15 @@ const HANDLERS = {
119
129
 
120
130
  const dispatchAction = (ui, action) => {
121
131
  if (CONFIRM_MODE.includes(ui.mode)) return;
132
+ if (ui.mode === 'import') stopImport(ui);
133
+ if (action === 'lines' && (ui.pane === 'diff' || ui.pane === 'unit')) {
134
+ return void filesPane.onLineNumbers(ui);
135
+ }
122
136
  if (ui.mode === 'compose') {
123
137
  if (!COMPOSE_LEAVE.includes(action)) return;
124
- const result = ui.composer.saveCompose();
125
- if (result && result.kind === 'error') return;
138
+ const after = () => dispatchAction(ui, action);
139
+ const result = ui.composer.saveCompose({ after });
140
+ if (result.kind === 'saving') return;
126
141
  }
127
142
  const pane = ui.pane;
128
143
  if (pane === 'repos' && !REPOS_ALLOWED.includes(action)) return;
@@ -137,8 +152,7 @@ const dispatchAction = (ui, action) => {
137
152
 
138
153
  const CONFIRMATIONS = {
139
154
  confirmQuit: {
140
- ready: (ui) => ui.finishQuit('ready'),
141
- editing: (ui) => ui.finishQuit('editing'),
155
+ confirm: (ui) => ui.confirmQuit(),
142
156
  cancel: (ui) => ui.cancelQuit(),
143
157
  },
144
158
  confirmUpdate: {
@@ -164,6 +178,7 @@ const CONFIRMATIONS = {
164
178
  cancel: (ui) => ui.gitBranches.cancelPush(),
165
179
  },
166
180
  confirmCommit: {
181
+ update: (ui) => ui.commits.chooseCommit('update'),
167
182
  commit: (ui) => ui.commits.chooseCommit('commit'),
168
183
  amend: (ui) => ui.commits.chooseCommit('amend'),
169
184
  fixup: (ui) => ui.commits.chooseCommit('fixup'),
@@ -179,6 +194,7 @@ const confirmChoice = (ui, key) => {
179
194
 
180
195
  const NPM_VIEW_KEYS = {
181
196
  escape: (ui) => ui.npm.closeView(),
197
+ s: (ui) => ui.npm.stop(),
182
198
  r: (ui) => ui.npm.rerun(),
183
199
  v: (ui) => ui.npm.toggleVerbose(),
184
200
  e: () => {},
@@ -190,31 +206,80 @@ const NPM_LIST_KEYS = {
190
206
  'ctrl-down': (ui) => ui.npm.reorder(1),
191
207
  up: (ui) => ui.npm.move(-1),
192
208
  down: (ui) => ui.npm.move(1),
209
+ right: (ui) => ui.npm.focusRuns(),
210
+ left: (ui) => ui.npm.focusCommands(),
211
+ tab: (ui) => ui.npm.toggleFocus(),
212
+ 'shift-tab': (ui) => ui.npm.toggleFocus(),
213
+ };
214
+
215
+ const AGENT_VIEW_KEYS = {
216
+ escape: (ui) => ui.agents.closeView(),
217
+ s: (ui) => ui.agents.stop(),
218
+ r: (ui) => ui.agents.rerun(),
219
+ e: () => {},
220
+ m: () => {},
221
+ a: () => {},
222
+ t: () => {},
223
+ f: () => {},
224
+ };
225
+
226
+ const AGENT_LIST_KEYS = {
227
+ right: (ui) => ui.agents.focusRuns(),
228
+ left: (ui) => ui.agents.focusCli(),
229
+ tab: (ui) => ui.agents.toggleFocus(),
230
+ 'shift-tab': (ui) => ui.agents.toggleFocus(),
193
231
  };
194
232
 
195
233
  const npmKey = (ui, key) => {
196
234
  if (ui.pane !== 'npm') return null;
197
- if (ui.npm.viewing && Object.hasOwn(NPM_VIEW_KEYS, key)) {
198
- return NPM_VIEW_KEYS[key];
235
+ if (ui.npm.viewing) {
236
+ if (Object.hasOwn(NPM_VIEW_KEYS, key)) return NPM_VIEW_KEYS[key];
237
+ if (key === 'up' || key === 'down') return NPM_LIST_KEYS[key];
238
+ return null;
199
239
  }
200
240
  return Object.hasOwn(NPM_LIST_KEYS, key) ? NPM_LIST_KEYS[key] : null;
201
241
  };
202
242
 
243
+ const agentKey = (ui, key) => {
244
+ if (ui.pane !== 'agents') return null;
245
+ if (ui.agents.pick) return () => ui.agents.onPickKey(key);
246
+ const keys = ui.agents.viewing ? AGENT_VIEW_KEYS : AGENT_LIST_KEYS;
247
+ return Object.hasOwn(keys, key) ? keys[key] : null;
248
+ };
249
+
250
+ const planMenuKey = (ui, key) => {
251
+ if (!ui.composer.tasks.planOpen || ui.pane !== 'tasks') return false;
252
+ ui.composer.tasks.onPlanKey(key);
253
+ return true;
254
+ };
255
+
203
256
  const handleKey = (ui, key) => {
204
257
  if (CONFIRM_MODE.includes(ui.mode)) return void confirmChoice(ui, key);
258
+ if (planMenuKey(ui, key)) return;
259
+ if (ui.pane === 'tasks' && (key === 'ctrl-up' || key === 'ctrl-down')) {
260
+ if (ui.mode === 'import') stopImport(ui);
261
+ const delta = key === 'ctrl-up' ? -1 : 1;
262
+ return void ui.composer.tasks.moveIssue(delta);
263
+ }
264
+ if (key === 'ctrl-l' && (ui.pane === 'diff' || ui.pane === 'unit')) {
265
+ return void filesPane.onLineNumbers(ui);
266
+ }
205
267
  if (ui.mode === 'compose') {
206
268
  return void ui.runComposeCommand(ui.composer.handleKey(key));
207
269
  }
208
270
  if (ui.mode === 'find') return void onFindKey(ui, key);
271
+ if (ui.mode === 'import') return void onImportKey(ui, key);
209
272
  if (key === '/' && FIND_PANES.includes(ui.pane)) return void startFind(ui);
210
273
  const onNpmKey = npmKey(ui, key);
211
274
  if (onNpmKey) return void onNpmKey(ui);
275
+ const onAgentKey = agentKey(ui, key);
276
+ if (onAgentKey) return void onAgentKey(ui);
212
277
  if (key === 'escape') return void filesPane.onEscape(ui);
213
278
  const tasks = ui.composer.tasks;
214
279
  const toggle = key === ' ';
215
280
  if (toggle && tasks.toggleFocusedTask()) return;
216
281
  if (tasks.typeIntoTask(key)) return;
217
- const action = actionFromKey(key, ui.pane, ui.fileScope);
282
+ const action = actionFromKey(key, ui.pane);
218
283
  if (action) ui.dispatch(action);
219
284
  };
220
285
 
@@ -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,12 +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' ||
39
- nav.pane === 'packages';
31
+ const listed = nav.pane !== 'files' && LIST_PANES.includes(nav.pane);
32
+ const held = nav.tasksOpen === true || listed;
40
33
  nav.pane = 'files';
41
34
  nav.resetListScroll('files');
42
35
  nav.clearSelection();
@@ -67,15 +60,22 @@ const showDashboard = (ui) => {
67
60
  ui.dashboard.enter();
68
61
  };
69
62
 
70
- const HOME_CHILDREN = ['files', 'branches', 'commits', 'npm', 'packages'];
71
-
72
63
  const returnsHome = (ui) => {
73
64
  if (!ui.dashboardHome) return false;
74
65
  const { pane, tasksOpen } = ui.nav;
75
- return HOME_CHILDREN.includes(pane) || tasksOpen === true;
66
+ return LIST_PANES.includes(pane) || tasksOpen === true;
76
67
  };
77
68
 
78
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
79
  if (ui.nav.pane === 'repos') return void ui.onQuit();
80
80
  if (ui.nav.pane === 'dashboard' && ui.workspaceRoot) {
81
81
  return void ui.workspace.show();
@@ -112,6 +112,7 @@ const onOpenFile = (ui) => {
112
112
  }
113
113
  if (ui.nav.pane === 'commits') return void ui.commits.onShowDiff();
114
114
  if (ui.nav.pane === 'npm') return void ui.npm.run();
115
+ if (ui.nav.pane === 'agents') return void ui.agents.start();
115
116
  if (ui.nav.pane !== 'files') {
116
117
  if (ui.nav.tasksOpen) ui.composer.tasks.editFocusedTask();
117
118
  return;
@@ -144,7 +145,7 @@ const onFileMove = (ui, delta) => {
144
145
  }
145
146
  const last = list.length - 1;
146
147
  const moved = ui.nav.fileCursor + delta;
147
- const next = Math.min(last, Math.max(0, moved));
148
+ const next = clamp(moved, 0, last);
148
149
  if (next === ui.nav.fileCursor) return;
149
150
  ui.nav.fileCursor = next;
150
151
  const entry = list[next];
@@ -191,7 +192,8 @@ const scrollStep = (ui, fraction) => {
191
192
  const fallback = Math.max(1, rows - 4);
192
193
  const bodyH = frame?.bodyH ?? fallback;
193
194
  const viewing = ui.nav.pane === 'npm' && ui.npm.viewing;
194
- const viewRows = viewing ? logViewRows(bodyH) : bodyH;
195
+ const agentLog = ui.nav.pane === 'agents' && ui.agents.viewing;
196
+ const viewRows = viewing || agentLog ? logViewRows(bodyH) : bodyH;
195
197
  const height = Math.max(1, viewRows);
196
198
  const span = Math.floor(height * Math.abs(fraction));
197
199
  const n = Math.max(1, span);
@@ -200,6 +202,10 @@ const scrollStep = (ui, fraction) => {
200
202
  };
201
203
 
202
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
+ }
203
209
  if (ui.nav.pane === 'dashboard') return;
204
210
  if (ui.nav.pane === 'files') return void onFileMove(ui, delta);
205
211
  if (ui.nav.pane === 'branches') {
@@ -209,12 +215,13 @@ const onScroll = (ui, delta) => {
209
215
  return void ui.commits.onCommitMove(delta);
210
216
  }
211
217
  if (ui.nav.pane === 'npm') return void ui.npm.move(delta);
218
+ if (ui.nav.pane === 'agents') return void ui.agents.move(delta);
212
219
  if (ui.nav.pane === 'packages') {
213
220
  const count = ui.dashboard.packageCount();
214
221
  const nav = ui.nav;
215
222
  const next = nav.packagesCursor + delta;
216
223
  const last = Math.max(0, count - 1);
217
- nav.packagesCursor = Math.max(0, Math.min(last, next));
224
+ nav.packagesCursor = clamp(next, 0, last);
218
225
  ui.status = '';
219
226
  return;
220
227
  }
@@ -253,10 +260,22 @@ const onLayout = (ui) => {
253
260
  const i = LAYOUTS.indexOf(ui.layout);
254
261
  const next = (i < 0 ? 0 : i + 1) % LAYOUTS.length;
255
262
  ui.layout = LAYOUTS[next];
256
- const name = LAYOUTS[next] === 'side' ? 'side-by-side' : LAYOUTS[next];
257
- ui.status = name;
263
+ ui.status = LAYOUT_LABEL[ui.layout] || ui.layout;
258
264
  ui.nav.scroll = 0;
259
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();
260
279
  };
261
280
 
262
281
  const onTheme = (ui) => {
@@ -264,12 +283,13 @@ const onTheme = (ui) => {
264
283
  const next = THEME_NAMES[(i + 1) % THEME_NAMES.length];
265
284
  setTheme(next);
266
285
  ui.status = next;
286
+ saveUiPrefs(ui.top || ui.cwd, { theme: next });
267
287
  };
268
288
 
269
289
  const onHome = (ui) => onJump(ui, false);
270
290
  const onEnd = (ui) => onJump(ui, true);
271
- const onFileScope = (ui) => setFileScope(ui, 'file');
272
- 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);
273
293
 
274
294
  module.exports = {
275
295
  fileCursorEntry,
@@ -287,6 +307,7 @@ module.exports = {
287
307
  onHome,
288
308
  onEnd,
289
309
  onLayout,
310
+ onLineNumbers,
290
311
  onTheme,
291
312
  onFileScope,
292
313
  onDiffScope,
@@ -1,20 +1,18 @@
1
1
  'use strict';
2
2
 
3
- const files = require('../files.js');
4
- const { isTasksEntry, TASKS_FILE } = files;
5
- const find = require('../find.js');
6
- const { fileMatches } = find;
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');
7
6
 
8
7
  const FIND_PANES = ['files', 'diff'];
9
8
 
10
- const isTypeKey = (key) =>
11
- Boolean(key) && key.length === 1 && key.charCodeAt(0) >= 32;
12
-
13
9
  const startFind = (ui) => {
14
10
  if (ui.mode !== 'review') return;
15
11
  if (!FIND_PANES.includes(ui.pane)) return;
16
12
  ui.mode = 'find';
17
- ui.nav.find = { query: '', at: 0 };
13
+ const find = lineField('query');
14
+ find.at = 0;
15
+ ui.nav.find = find;
18
16
  ui.status = '';
19
17
  };
20
18
 
@@ -56,21 +54,6 @@ const showHit = (ui, jump) => {
56
54
  focusMatch(ui, hits[at], jump);
57
55
  };
58
56
 
59
- const typeFind = (ui, key) => {
60
- const state = ui.nav.find;
61
- state.query += key;
62
- state.at = 0;
63
- showHit(ui, true);
64
- };
65
-
66
- const eraseFind = (ui) => {
67
- const state = ui.nav.find;
68
- if (!state.query) return void stopFind(ui);
69
- state.query = state.query.slice(0, -1);
70
- state.at = 0;
71
- showHit(ui, true);
72
- };
73
-
74
57
  const moveFind = (ui, delta) => {
75
58
  const state = ui.nav.find;
76
59
  if (!state.query) return;
@@ -83,12 +66,16 @@ const onFindKey = (ui, key) => {
83
66
  ui.mode = 'review';
84
67
  return;
85
68
  }
86
- if (key === 'ctrl-c') return void ui.onQuit();
87
69
  if (key === 'escape' || key === 'enter') return void stopFind(ui);
88
- if (key === 'backspace') return void eraseFind(ui);
89
- if (key === 'up' || key === 'left') return void moveFind(ui, -1);
90
- if (key === 'down' || key === 'right') return void moveFind(ui, 1);
91
- if (isTypeKey(key)) typeFind(ui, key);
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);
92
79
  };
93
80
 
94
- module.exports = { FIND_PANES, startFind, stopFind, onFindKey };
81
+ module.exports = { FIND_PANES, startFind, onFindKey };
@@ -0,0 +1,93 @@
1
+ 'use strict';
2
+
3
+ const { remoteKind, selectChangeSource } = require('../source/source.js');
4
+ const { lineKey, lineField } = require('../input/line-edit.js');
5
+
6
+ const changeRepositoryUrl = (change) => {
7
+ if (!change?.url || !change.repository) return '';
8
+ try {
9
+ const url = new URL(change.url);
10
+ return `${url.origin}/${change.repository}`;
11
+ } catch {
12
+ return '';
13
+ }
14
+ };
15
+
16
+ const startImport = (ui) => {
17
+ if (ui.mode !== 'review' || ui.pane !== 'tasks') return;
18
+ ui.composer.tasks.closePlanMenu();
19
+ ui.mode = 'import';
20
+ const defaultUrl = ui.repositoryUrl || changeRepositoryUrl(ui.change);
21
+ ui.nav.import = lineField('url', defaultUrl);
22
+ ui.status = '';
23
+ };
24
+
25
+ const stopImport = (ui) => {
26
+ ui.mode = 'review';
27
+ ui.nav.import = null;
28
+ ui.status = '';
29
+ };
30
+
31
+ const noteImport = (ui, message) => {
32
+ ui.status = message;
33
+ if (ui.uiOpen) ui.paint();
34
+ };
35
+
36
+ const finishImport = (ui, store, loaded) => {
37
+ const imported = loaded && loaded.imported;
38
+ if (!imported || ui.review.store !== store) {
39
+ noteImport(ui, 'import failed');
40
+ return;
41
+ }
42
+ store.applyImportedNotes(imported);
43
+ ui.flushReview();
44
+ noteImport(ui, 'imported');
45
+ };
46
+
47
+ const runImport = (ui, selected) => {
48
+ const kind = remoteKind(selected);
49
+ if (!kind) return Promise.resolve(null);
50
+ const load = ui[kind.option] ?? kind.load;
51
+ const target = kind.target(selected);
52
+ if (!load || !target) return Promise.resolve(null);
53
+ const fetch = ui.fetchImpl ?? globalThis.fetch;
54
+ const options = { cwd: ui.top || ui.cwd, token: kind.token(), fetch };
55
+ return Promise.resolve().then(() => load(target, options));
56
+ };
57
+
58
+ const submitImport = (ui) => {
59
+ const state = ui.nav.import;
60
+ const url = state ? state.url.trim() : '';
61
+ const store = ui.review.store;
62
+ stopImport(ui);
63
+ if (!url) return;
64
+ const selected = selectChangeSource([url]);
65
+ if (!remoteKind(selected)) {
66
+ ui.status = 'bad url';
67
+ return;
68
+ }
69
+ ui.status = 'importing';
70
+ const pending = runImport(ui, selected);
71
+ ui.importPromise = pending;
72
+ pending.then(
73
+ (loaded) => finishImport(ui, store, loaded),
74
+ (error) => {
75
+ const message = error instanceof Error ? error.message : '';
76
+ noteImport(ui, message || 'import failed');
77
+ },
78
+ );
79
+ };
80
+
81
+ const onImportKey = (ui, key) => {
82
+ const state = ui.nav.import;
83
+ if (!state) {
84
+ ui.mode = 'review';
85
+ return;
86
+ }
87
+ if (key === 'escape') return void stopImport(ui);
88
+ if (key === 'enter') return void submitImport(ui);
89
+ if (key === 'backspace' && !state.url) return void stopImport(ui);
90
+ lineKey(state.editor, key, ui, true);
91
+ };
92
+
93
+ 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 [];