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
@@ -0,0 +1,976 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { clamp } = require('../../common/utilities.js');
6
+ const agents = require('../../agents/agents.js');
7
+ const {
8
+ detectAgents,
9
+ canLogin,
10
+ listedOf,
11
+ listModels,
12
+ startAgent,
13
+ buildLaunch,
14
+ buildLogin,
15
+ needsAuth,
16
+ probeAuth,
17
+ emptyChoice,
18
+ mergeModels,
19
+ } = agents;
20
+ const review = require('../../review/review.js');
21
+ const { latestReviewName, listReviewNames, planFileNames } = review;
22
+ const { REVIEW_DIR } = require('../../common/files.js');
23
+ const render = require('../../render/log.js');
24
+ const { logRowCount } = render;
25
+ const { RunPanel } = require('../run-panel.js');
26
+ const options = require('./options.js');
27
+ const { AgentOptions, concrete, restoreChoice, storedChoice } = options;
28
+ const jobs = require('./jobs.js');
29
+ const { readPrefs, writePrefs } = require('../prefs.js');
30
+ const { elapsedLabel, launchJob, assignLog, stemOf } = jobs;
31
+ const { restoreJobs, readRunList, saveRunFiles } = jobs;
32
+ const { panelSessions, orderedSessions, runBlock } = jobs;
33
+ const { runningJobs, withLiveRuns } = jobs;
34
+ const { replayLaunch } = jobs;
35
+ const plans = require('./plans.js');
36
+ const { planKey, planProgress, planPart, planLabel, alignPlanLabels } = plans;
37
+ const { MenuPick, menuRows, pickDelta } = require('../../input/menu-pick.js');
38
+ const { lineKey } = require('../../input/line-edit.js');
39
+
40
+ const optionsOf = (row) => new AgentOptions(row);
41
+
42
+ const APPLY_FIELD = {
43
+ model: (choices, picked, value) => choices.pickModel(picked, value),
44
+ context: (choices, picked, value) => choices.pickContext(picked, value),
45
+ effort: (choices, picked, value) => {
46
+ picked.effort = value;
47
+ },
48
+ };
49
+
50
+ const NEVER = () => false;
51
+
52
+ const lastIndex = (list) => Math.max(0, list.length - 1);
53
+
54
+ const STEM_ID = /-agent-\d+$/;
55
+
56
+ const continuedId = (session) => {
57
+ const id = session && session.id ? `${session.id}` : '';
58
+ if (STEM_ID.test(id)) return '';
59
+ return id;
60
+ };
61
+
62
+ const cacheKey = (row) => `${row.id}:${row.bin}`;
63
+
64
+ const listQuietly = async (list, row, env) => {
65
+ try {
66
+ return await list(row.bin, row.spec, { env });
67
+ } catch {
68
+ return null;
69
+ }
70
+ };
71
+
72
+ const firstEnabled = (items, at, isOff) => {
73
+ if (!isOff(items[at])) return at;
74
+ const free = items.findIndex((item) => !isOff(item));
75
+ return free < 0 ? at : free;
76
+ };
77
+
78
+ const NO_PICK = {
79
+ agentPick: '',
80
+ agentMenu: null,
81
+ agentQuery: '',
82
+ agentEditor: null,
83
+ agentMenuOff: [],
84
+ agentPickCursor: 0,
85
+ agentMenuScroll: 0,
86
+ };
87
+
88
+ const NO_LIST = {
89
+ agents: null,
90
+ agentRuns: [],
91
+ agentRunCursor: 0,
92
+ agentRunScroll: 0,
93
+ agentFocus: 'cli',
94
+ agentCanContinue: false,
95
+ agentPlanCount: 0,
96
+ agentView: false,
97
+ };
98
+
99
+ const NO_JOB = {
100
+ agentOutput: '',
101
+ agentCommand: '',
102
+ agentElapsed: '',
103
+ agentProgress: '',
104
+ agentRunning: false,
105
+ agentStatus: '',
106
+ };
107
+
108
+ class AgentsController extends RunPanel {
109
+ constructor(ui) {
110
+ super(ui);
111
+ this.spawn = startAgent;
112
+ this.listModels = listModels;
113
+ this.authProbe = probeAuth;
114
+ this.reset();
115
+ }
116
+
117
+ get root() {
118
+ return this.ui.top || this.ui.cwd;
119
+ }
120
+
121
+ reset() {
122
+ this.killAll();
123
+ this.list = [];
124
+ this.choices = new Map();
125
+ this.loaded = new Map();
126
+ this.modelsEpoch = 0;
127
+ this.jobs = [];
128
+ this.nextId = 1;
129
+ this.viewing = false;
130
+ this.viewId = 0;
131
+ this.opened = false;
132
+ this.runsRoot = '';
133
+ this.pick = null;
134
+ this.planPath = '';
135
+ this.focus = 'cli';
136
+ this.runCursor = 0;
137
+ this.runScroll = 0;
138
+ this.followEnd = true;
139
+ this.logScroll = 0;
140
+ this.viewSession = '';
141
+ this.historyText = '';
142
+ this.historyAt = 0;
143
+ this.pendingStart = null;
144
+ this.authYes = new Set();
145
+ this.authWait = '';
146
+ this.env = process.env;
147
+ this.refresh();
148
+ this.loadRuns();
149
+ }
150
+
151
+ killAll() {
152
+ for (const job of this.jobs ?? []) {
153
+ if (job.child) job.child.kill();
154
+ }
155
+ }
156
+
157
+ reviewStore() {
158
+ return this.ui.review ? this.ui.review.store : null;
159
+ }
160
+
161
+ applyCached(row) {
162
+ const cached = this.loaded.get(cacheKey(row));
163
+ if (!cached) return;
164
+ row.models = cached.models;
165
+ row.wide = cached.wide;
166
+ row.listedEfforts = cached.efforts;
167
+ row.modelEfforts = cached.modelEfforts;
168
+ }
169
+
170
+ refresh(env = this.env) {
171
+ this.env = env;
172
+ const prev = this.choices;
173
+ this.list = detectAgents(env);
174
+ this.choices = new Map();
175
+ for (const row of this.list) {
176
+ this.applyCached(row);
177
+ this.choices.set(row.id, prev.get(row.id) ?? emptyChoice(row));
178
+ }
179
+ this.loadChoices();
180
+ this.clampCursor();
181
+ }
182
+
183
+ loadChoices() {
184
+ if (!this.root) return;
185
+ const saved = readPrefs(this.root).agents;
186
+ for (const row of this.list) {
187
+ const picked = this.choice(row.id);
188
+ restoreChoice(picked, saved[row.id], row.spec);
189
+ optionsOf(row).preferEffort(picked, 'medium');
190
+ }
191
+ }
192
+
193
+ saveChoice(id) {
194
+ const picked = this.choices.get(id);
195
+ const row = this.cliById(id);
196
+ if (!this.root || !picked || !row) return;
197
+ const data = readPrefs(this.root);
198
+ const saved = { ...data.agents };
199
+ const stored = storedChoice(picked, row.spec);
200
+ if (stored) saved[id] = stored;
201
+ else delete saved[id];
202
+ writePrefs(this.root, { ...data, agents: saved });
203
+ }
204
+
205
+ loadRuns() {
206
+ if (!this.root) return;
207
+ const restored = restoreJobs(readRunList(this.root));
208
+ this.jobs = restored.jobs;
209
+ this.nextId = restored.nextId;
210
+ this.runsRoot = this.root;
211
+ }
212
+
213
+ // The repository can change after construction: its runs and its saved
214
+ // sessions must come from the same directory
215
+ syncRuns() {
216
+ if (!this.root || this.root === this.runsRoot) return;
217
+ const live = this.jobs.filter((job) => job.status === 'running');
218
+ this.loadRuns();
219
+ for (const job of live) {
220
+ job.id = this.nextId;
221
+ this.nextId += 1;
222
+ this.jobs.push(job);
223
+ }
224
+ this.viewId = 0;
225
+ }
226
+
227
+ saveRuns() {
228
+ if (!this.root) return;
229
+ saveRunFiles(this.root, this.jobs);
230
+ }
231
+
232
+ clampCursor() {
233
+ const nav = this.ui.nav;
234
+ nav.agentCursor = clamp(nav.agentCursor ?? 0, 0, lastIndex(this.list));
235
+ }
236
+
237
+ selected() {
238
+ return this.list[this.ui.nav.agentCursor] ?? null;
239
+ }
240
+
241
+ cliById(id) {
242
+ return this.list.find((row) => row.id === id) ?? null;
243
+ }
244
+
245
+ choice(id) {
246
+ const row = this.cliById(id);
247
+ if (!row) return emptyChoice(null);
248
+ if (!this.choices.has(id)) this.choices.set(id, emptyChoice(row));
249
+ return this.choices.get(id);
250
+ }
251
+
252
+ rows() {
253
+ const plan = this.planFace();
254
+ const planBusy = this.planBusy(this.planFile());
255
+ return this.list.map((row) => {
256
+ const picked = this.choice(row.id);
257
+ const choices = optionsOf(row);
258
+ const model = choices.normalize(picked);
259
+ return {
260
+ ...row,
261
+ model,
262
+ effort: concrete(picked.effort),
263
+ efforts: choices.efforts(picked.model),
264
+ fast: picked.fast ? 'on' : 'off',
265
+ context: concrete(picked.context),
266
+ contexts: choices.contexts(model),
267
+ plan: row.bin ? plan : '',
268
+ planBusy: Boolean(row.bin && planBusy),
269
+ };
270
+ });
271
+ }
272
+
273
+ sessionRows() {
274
+ const cli = this.selected();
275
+ const id = cli && cli.bin ? cli.id : '';
276
+ const saved = panelSessions(this.root, this.jobs, id);
277
+ const running = runningJobs(this.jobs, id);
278
+ const progressOf = (job) =>
279
+ planProgress(job.plan, this.root, this.reviewStore());
280
+ const rows = withLiveRuns(saved, running, progressOf);
281
+ this.runCursor = clamp(this.runCursor, 0, lastIndex(rows));
282
+ return rows;
283
+ }
284
+
285
+ openRunRow(row) {
286
+ if (!row) return false;
287
+ if (row.jobId) {
288
+ const job = this.jobById(row.jobId);
289
+ if (job && job.status === 'running') {
290
+ this.openView(job.id);
291
+ return true;
292
+ }
293
+ }
294
+ if (!row.id) return false;
295
+ this.openSession(row.id);
296
+ return true;
297
+ }
298
+
299
+ selectedSession() {
300
+ return this.sessionRows()[this.runCursor] || null;
301
+ }
302
+
303
+ focusRuns() {
304
+ if (this.viewing || this.pick) return;
305
+ this.focus = 'runs';
306
+ this.sessionRows();
307
+ }
308
+
309
+ focusCli() {
310
+ if (this.viewing || this.pick) return;
311
+ this.focus = 'cli';
312
+ }
313
+
314
+ toggleFocus() {
315
+ if (this.focus === 'runs') this.focusCli();
316
+ else this.focusRuns();
317
+ }
318
+
319
+ jobById(id) {
320
+ return this.jobs.find((job) => job.id === id) ?? null;
321
+ }
322
+
323
+ viewedJob() {
324
+ return this.jobById(this.viewId);
325
+ }
326
+
327
+ runningJob(id, action = '') {
328
+ const live = (job) => {
329
+ const sameCli = job.cliId === id;
330
+ const running = job.status === 'running';
331
+ const sameAction = !action || job.action === action;
332
+ return sameCli && running && sameAction;
333
+ };
334
+ return this.jobs.findLast(live) ?? null;
335
+ }
336
+
337
+ runningCount() {
338
+ return this.jobs.filter((job) => job.status === 'running').length;
339
+ }
340
+
341
+ summary() {
342
+ const items = this.rows().map((row) => {
343
+ let running = 0;
344
+ for (const job of this.jobs) {
345
+ if (job.cliId === row.id && job.status === 'running') running += 1;
346
+ }
347
+ return { ...row, running };
348
+ });
349
+ return {
350
+ ready: true,
351
+ installed: items.filter((row) => row.bin).length,
352
+ total: items.length,
353
+ items,
354
+ running: this.runningCount(),
355
+ };
356
+ }
357
+
358
+ visibleText() {
359
+ if (this.viewSession) return this.historyText;
360
+ const job = this.viewedJob();
361
+ return job ? job.output : '';
362
+ }
363
+
364
+ pickView(onAgents) {
365
+ const pick = onAgents ? this.pick : null;
366
+ if (!pick) return NO_PICK;
367
+ const menu = this.menuItems();
368
+ const onPlan = pick.field === 'plan';
369
+ return {
370
+ agentPick: pick.field,
371
+ agentMenu: menu,
372
+ agentQuery: pick.query,
373
+ agentEditor: pick.editor,
374
+ agentMenuOff: onPlan ? menu.filter(this.busyCheck()) : [],
375
+ agentPickCursor: pick.cursor,
376
+ agentMenuScroll: pick.scroll,
377
+ };
378
+ }
379
+
380
+ listView(onAgents) {
381
+ if (!onAgents) return NO_LIST;
382
+ const agentRuns = this.sessionRows();
383
+ const selected = agentRuns[this.runCursor];
384
+ const agentCanContinue =
385
+ this.focus === 'runs' &&
386
+ Boolean(continuedId(selected)) &&
387
+ selected.status !== 'running';
388
+ return {
389
+ agents: this.rows(),
390
+ agentRuns,
391
+ agentRunCursor: this.runCursor,
392
+ agentRunScroll: this.runScroll,
393
+ agentFocus: this.focus,
394
+ agentCanContinue,
395
+ agentPlanCount: this.reviewChoices().length,
396
+ agentView: this.viewing,
397
+ };
398
+ }
399
+
400
+ sessionView(session) {
401
+ const named = this.sessionRows().find((item) => item.id === session);
402
+ const agentCommand = named ? named.name : session;
403
+ return { ...NO_JOB, agentOutput: this.historyText, agentCommand };
404
+ }
405
+
406
+ jobView() {
407
+ if (this.viewSession) return this.sessionView(this.viewSession);
408
+ const job = this.viewedJob();
409
+ if (!job) return NO_JOB;
410
+ const running = job.status === 'running';
411
+ const name = stemOf(job.logName);
412
+ const agentCommand = name || job.command;
413
+ return {
414
+ agentOutput: job.output,
415
+ agentCommand,
416
+ agentElapsed: elapsedLabel(job),
417
+ agentProgress: planProgress(job.plan, this.root, this.reviewStore()),
418
+ agentRunning: running,
419
+ agentStatus: job.status,
420
+ };
421
+ }
422
+
423
+ view(onAgents) {
424
+ return {
425
+ agentCursor: this.ui.nav.agentCursor,
426
+ agentFollow: this.followEnd,
427
+ agentScroll: this.logScroll,
428
+ ...this.listView(onAgents),
429
+ ...this.pickView(onAgents),
430
+ ...this.jobView(),
431
+ };
432
+ }
433
+
434
+ firstInstalled() {
435
+ return Math.max(
436
+ 0,
437
+ this.list.findIndex((row) => row.bin),
438
+ );
439
+ }
440
+
441
+ restoreSelection() {
442
+ const nav = this.ui.nav;
443
+ const row = this.selected();
444
+ if (this.opened && row && row.bin) return;
445
+ nav.resetListScroll('agents');
446
+ nav.agentCursor = this.firstInstalled();
447
+ this.focus = 'cli';
448
+ this.runCursor = 0;
449
+ this.runScroll = 0;
450
+ }
451
+
452
+ open() {
453
+ const nav = this.ui.nav;
454
+ this.syncRuns();
455
+ this.refresh();
456
+ this.viewing = false;
457
+ nav.pane = 'agents';
458
+ nav.clearSelection();
459
+ this.restoreSelection();
460
+ this.opened = true;
461
+ this.ui.status = '';
462
+ return this.loadModels();
463
+ }
464
+
465
+ async loadModels() {
466
+ this.modelsEpoch += 1;
467
+ const epoch = this.modelsEpoch;
468
+ const pending = this.list
469
+ .filter((row) => row.bin && !this.loaded.has(cacheKey(row)))
470
+ .map((row) => this.fetchModels(row, epoch));
471
+ if (!pending.length) return;
472
+ await Promise.all(pending);
473
+ if (epoch === this.modelsEpoch) this.ui.paint();
474
+ }
475
+
476
+ async fetchModels(row, epoch) {
477
+ const listed = listedOf(await listQuietly(this.listModels, row, this.env));
478
+ if (epoch !== this.modelsEpoch) return;
479
+ const { spec } = row;
480
+ const models = mergeModels(spec.models, listed.names, spec.partialList);
481
+ const wide = listed.wide ? new Set(listed.wide) : null;
482
+ const { efforts, modelEfforts } = listed;
483
+ const cached = { models, wide, efforts, modelEfforts };
484
+ this.loaded.set(cacheKey(row), cached);
485
+ const current = this.cliById(row.id);
486
+ if (current && current.bin === row.bin) this.applyCached(current);
487
+ }
488
+
489
+ logWidth() {
490
+ const size = this.ui.lastSize;
491
+ return size && size.width ? size.width : 0;
492
+ }
493
+
494
+ prependOlder() {
495
+ if (!this.viewSession || this.historyAt <= 0) return 0;
496
+ const runs = orderedSessions(this.jobs, this.viewSession);
497
+ const job = runs[this.historyAt - 1];
498
+ if (!job) return 0;
499
+ const block = runBlock(this.root, job);
500
+ const added = logRowCount(`${block}\n`, this.logWidth(), false);
501
+ this.historyAt -= 1;
502
+ const prev = this.historyText;
503
+ this.historyText = prev ? `${block}\n${prev}` : block;
504
+ return added;
505
+ }
506
+
507
+ scrollLog(delta) {
508
+ const atTop = !this.followEnd && this.logScroll === 0;
509
+ if (delta < 0 && atTop) {
510
+ const added = this.prependOlder();
511
+ if (added) this.logScroll = added;
512
+ }
513
+ const job = this.viewSession ? null : this.viewedJob();
514
+ const running = Boolean(job && job.status === 'running');
515
+ this.scrollLogBy(delta, this.visibleText(), running);
516
+ }
517
+
518
+ move(delta) {
519
+ if (this.pick) return void this.movePick(delta);
520
+ if (this.viewing) return void this.scrollLog(delta);
521
+ this.ui.status = '';
522
+ if (this.focus === 'runs') {
523
+ const last = lastIndex(this.sessionRows());
524
+ this.runCursor = clamp(this.runCursor + delta, 0, last);
525
+ return;
526
+ }
527
+ const nav = this.ui.nav;
528
+ nav.agentCursor = clamp(nav.agentCursor + delta, 0, lastIndex(this.list));
529
+ this.runCursor = 0;
530
+ this.runScroll = 0;
531
+ }
532
+
533
+ reviewChoices() {
534
+ const names = planFileNames(this.root);
535
+ const file = this.planFile();
536
+ const current = file ? path.basename(file) : '';
537
+ if (current && !names.includes(current)) names.unshift(current);
538
+ return names;
539
+ }
540
+
541
+ planPartOf(name) {
542
+ const dir = path.join(this.root, REVIEW_DIR);
543
+ return planPart(dir, name, this.reviewStore());
544
+ }
545
+
546
+ planFace() {
547
+ const file = this.planFile();
548
+ const part = file ? this.planPartOf(file) : null;
549
+ return part ? planLabel(part) : '';
550
+ }
551
+
552
+ planMenu() {
553
+ const parts = this.reviewChoices().map((name) => this.planPartOf(name));
554
+ return alignPlanLabels(parts.filter(Boolean));
555
+ }
556
+
557
+ fieldItems(field, row) {
558
+ if (field === 'plan') return this.planMenu();
559
+ if (!row) return [];
560
+ const choices = optionsOf(row);
561
+ const picked = this.choice(row.id);
562
+ if (field === 'model') return choices.names;
563
+ if (field === 'context') {
564
+ return choices.contexts(choices.shown(picked.model));
565
+ }
566
+ return choices.efforts(picked.model);
567
+ }
568
+
569
+ fieldStart(field, items, row) {
570
+ if (field === 'plan') {
571
+ const current = planKey(this.planFile());
572
+ const at = items.findIndex((item) => planKey(item) === current);
573
+ return firstEnabled(items, Math.max(0, at), this.busyCheck());
574
+ }
575
+ const picked = this.choice(row.id);
576
+ const model = field === 'model';
577
+ const current = model ? optionsOf(row).shown(picked.model) : picked[field];
578
+ return Math.max(0, items.indexOf(current));
579
+ }
580
+
581
+ menuItems() {
582
+ if (!this.pick) return [];
583
+ return this.pick.filter(this.fieldItems(this.pick.field, this.selected()));
584
+ }
585
+
586
+ menuPage(fraction = 1) {
587
+ const body = this.ui.lastFrame && this.ui.lastFrame.bodyH;
588
+ return menuRows(body, fraction);
589
+ }
590
+
591
+ movePick(delta) {
592
+ const items = this.menuItems();
593
+ if (!items.length || !this.pick) return;
594
+ const isOff = this.pick.field === 'plan' ? this.busyCheck() : NEVER;
595
+ this.pick.move(delta, items, isOff);
596
+ }
597
+
598
+ dismissPick() {
599
+ this.pick = null;
600
+ }
601
+
602
+ closePick() {
603
+ if (!this.pick) return;
604
+ this.dismissPick();
605
+ this.ui.status = '';
606
+ this.ui.paint();
607
+ }
608
+
609
+ noteQueryEdit(changed) {
610
+ if (changed) {
611
+ this.pick.touch();
612
+ this.ui.status = '';
613
+ }
614
+ this.ui.paint();
615
+ }
616
+
617
+ openPick(field) {
618
+ if (this.viewing) return;
619
+ if (this.pick && this.pick.field === field) return void this.closePick();
620
+ const row = this.selected();
621
+ if (!row || !row.bin) return;
622
+ optionsOf(row).normalize(this.choice(row.id));
623
+ const items = this.fieldItems(field, row);
624
+ if (items.length < (field === 'context' ? 1 : 2)) return;
625
+ this.pick = new MenuPick(field, this.fieldStart(field, items, row));
626
+ this.ui.status = '';
627
+ this.ui.paint();
628
+ }
629
+
630
+ acceptPick(at = this.pick ? this.pick.cursor : 0) {
631
+ const pick = this.pick;
632
+ if (!pick) return;
633
+ const items = this.menuItems();
634
+ const value = items[Math.min(at, lastIndex(items))];
635
+ if (pick.field === 'plan' && this.planBusy(value)) {
636
+ this.ui.status = `busy ${planKey(value)}`;
637
+ this.ui.paint();
638
+ return;
639
+ }
640
+ const row = this.selected();
641
+ this.dismissPick();
642
+ const done = row && value ? this.applyPick(pick.field, row, value) : '';
643
+ this.ui.status = done;
644
+ this.ui.paint();
645
+ }
646
+
647
+ applyPick(field, row, value) {
648
+ if (field === 'plan') {
649
+ const name = planKey(value);
650
+ this.planPath = path.join(this.root, REVIEW_DIR, name);
651
+ return `plan ${name}`;
652
+ }
653
+ const picked = this.choice(row.id);
654
+ const apply = APPLY_FIELD[field] ?? APPLY_FIELD.effort;
655
+ apply(optionsOf(row), picked, value);
656
+ this.saveChoice(row.id);
657
+ return `${field} ${picked[field]}`;
658
+ }
659
+
660
+ pickStep(key) {
661
+ return pickDelta(key, this.menuPage(), this.menuPage(0.5));
662
+ }
663
+
664
+ onPickKey(key) {
665
+ if (!this.pick) return;
666
+ if (key === 'escape') return void this.closePick();
667
+ if (key === 'enter') return void this.acceptPick();
668
+ if (key === ' ') return;
669
+ const handled = lineKey(this.pick.editor, key, this.ui, false);
670
+ if (handled) return void this.noteQueryEdit(handled === 'text');
671
+ const step = this.pickStep(key);
672
+ if (step) this.movePick(step);
673
+ }
674
+
675
+ toggleFast() {
676
+ if (this.viewing || this.pick) return;
677
+ const row = this.selected();
678
+ if (!row || !row.bin) {
679
+ this.ui.status = 'not installed';
680
+ return;
681
+ }
682
+ const picked = this.choice(row.id);
683
+ optionsOf(row).toggleFast(picked);
684
+ this.saveChoice(row.id);
685
+ this.ui.status = picked.fast ? 'fast on' : 'fast off';
686
+ this.ui.paint();
687
+ }
688
+
689
+ latestPlan() {
690
+ const latest = latestReviewName(listReviewNames(this.root));
691
+ return latest ? path.join(this.root, REVIEW_DIR, latest) : '';
692
+ }
693
+
694
+ planFile() {
695
+ if (this.planPath && fs.existsSync(this.planPath)) return this.planPath;
696
+ const store = this.reviewStore();
697
+ if (store && store.reviewPath) return store.reviewPath;
698
+ return this.latestPlan();
699
+ }
700
+
701
+ savePlan(store) {
702
+ if (store.dirty) this.ui.flushReview(true);
703
+ if (fs.existsSync(store.reviewPath)) return true;
704
+ store.dirty = true;
705
+ this.ui.flushReview(true);
706
+ return fs.existsSync(store.reviewPath);
707
+ }
708
+
709
+ ensurePlan() {
710
+ const store = this.reviewStore();
711
+ const live = store ? store.reviewPath : '';
712
+ const chosen = this.planPath;
713
+ if (chosen && chosen !== live && fs.existsSync(chosen)) return chosen;
714
+ if (live && this.savePlan(store)) return live;
715
+ return this.latestPlan();
716
+ }
717
+
718
+ busyPlans() {
719
+ const busy = new Set();
720
+ for (const job of this.jobs) {
721
+ if (job.status !== 'running' || job.action === 'login') continue;
722
+ const name = path.basename(`${job.plan ?? ''}`);
723
+ if (name) busy.add(name);
724
+ }
725
+ return busy;
726
+ }
727
+
728
+ busyCheck() {
729
+ const busy = this.busyPlans();
730
+ return (file) => busy.has(planKey(file));
731
+ }
732
+
733
+ planBusy(file) {
734
+ return this.busyPlans().has(planKey(file));
735
+ }
736
+
737
+ openSession(id) {
738
+ const runs = orderedSessions(this.jobs, id);
739
+ if (!runs.length) {
740
+ this.ui.status = 'no log for this session';
741
+ this.ui.paint();
742
+ return;
743
+ }
744
+ const last = runs.at(-1);
745
+ this.viewing = true;
746
+ this.viewSession = id;
747
+ this.viewId = 0;
748
+ this.historyAt = runs.length - 1;
749
+ this.historyText = runBlock(this.root, last);
750
+ this.followEnd = true;
751
+ this.logScroll = 0;
752
+ this.ui.status = '';
753
+ this.ui.paint();
754
+ }
755
+
756
+ openView(id) {
757
+ if (!this.jobById(id)) return;
758
+ this.viewing = true;
759
+ this.viewSession = '';
760
+ this.historyText = '';
761
+ this.viewId = id;
762
+ this.followEnd = true;
763
+ this.logScroll = 0;
764
+ this.ui.status = '';
765
+ this.ui.paint();
766
+ }
767
+
768
+ closeView() {
769
+ const job = this.viewedJob();
770
+ this.viewing = false;
771
+ this.viewSession = '';
772
+ this.historyText = '';
773
+ const waiting = job && job.action === 'login' && job.status === 'running';
774
+ this.ui.status = waiting ? 'logging in' : '';
775
+ this.ui.paint();
776
+ }
777
+
778
+ syncProgress() {
779
+ if (this.runningCount()) this.ui.progress.start('agents');
780
+ else this.ui.progress.stop('agents');
781
+ }
782
+
783
+ keepLog(job) {
784
+ const now = Date.now();
785
+ if (now - job.loggedAt < 150) return;
786
+ job.loggedAt = now;
787
+ this.saveRuns();
788
+ }
789
+
790
+ showOutput(job, text) {
791
+ job.output = text;
792
+ job.dirty = true;
793
+ this.keepLog(job);
794
+ const watched = this.viewing && this.viewId === job.id;
795
+ if (watched || this.ui.nav.pane === 'agents') this.ui.paint();
796
+ }
797
+
798
+ settleJob(job, result) {
799
+ job.syncRaw();
800
+ job.dirty = true;
801
+ if (result) job.finish(result);
802
+ else job.stop();
803
+ this.saveRuns();
804
+ this.syncProgress();
805
+ }
806
+
807
+ finishJob(job, result) {
808
+ if (job.status !== 'running') return;
809
+ const stopped = job.stopping;
810
+ this.settleJob(job, stopped ? null : result);
811
+ const isLogin = job.action === 'login';
812
+ const watched = this.viewing && this.viewId === job.id;
813
+ const signedIn = !stopped && isLogin && result.status === 0;
814
+ if (stopped && watched) this.ui.status = 'stopped';
815
+ else if (signedIn) this.ui.status = 'logged in';
816
+ this.ui.paint();
817
+ if (isLogin) return void this.finishLogin(job, result, stopped, signedIn);
818
+ if (stopped || !needsAuth(job.output)) return;
819
+ const cli = this.cliById(job.cliId);
820
+ if (canLogin(cli)) this.startLogin(cli);
821
+ }
822
+
823
+ finishLogin(job, result, stopped, signedIn) {
824
+ if (!signedIn) {
825
+ this.pendingStart = null;
826
+ if (!stopped && result) this.ui.status = 'not logged in';
827
+ this.ui.paint();
828
+ return;
829
+ }
830
+ this.authYes.add(job.cliId);
831
+ const pending = this.pendingStart;
832
+ this.pendingStart = null;
833
+ if (!pending || pending.id !== job.cliId) return;
834
+ const cli = this.cliById(pending.id);
835
+ if (cli) this.launchReady(cli, pending.session || '');
836
+ }
837
+
838
+ authAnswer(row) {
839
+ if (!canLogin(row)) return true;
840
+ const args = row.spec && row.spec.status;
841
+ if (!args || !args.length) return true;
842
+ if (this.authYes.has(row.id)) return true;
843
+ return this.authProbe(row);
844
+ }
845
+
846
+ beginStart(row, sessionId = '') {
847
+ const id = row.id;
848
+ if (this.authWait === id) return;
849
+ const answer = this.authAnswer(row);
850
+ const go = (ok) => {
851
+ if (this.authWait === id) this.authWait = '';
852
+ const current = this.cliById(id);
853
+ if (!current) return;
854
+ this.afterAuth(current, sessionId, ok);
855
+ };
856
+ if (answer && typeof answer.then === 'function') {
857
+ this.authWait = id;
858
+ answer.then(go, () => go(null));
859
+ return;
860
+ }
861
+ this.afterAuth(row, sessionId, answer);
862
+ }
863
+
864
+ afterAuth(row, sessionId, ok) {
865
+ if (ok === true) this.authYes.add(row.id);
866
+ if (ok === false) return void this.holdForLogin(row, sessionId);
867
+ this.launchReady(row, sessionId);
868
+ }
869
+
870
+ holdForLogin(row, sessionId) {
871
+ this.pendingStart = { id: row.id, session: sessionId };
872
+ this.startLogin(row);
873
+ this.ui.status = 'not logged in';
874
+ this.ui.paint();
875
+ }
876
+
877
+ launchReady(row, sessionId = '') {
878
+ this.startJob(row, sessionId);
879
+ }
880
+
881
+ startLaunch(launch, model, showLog = true) {
882
+ if (!launch.ok) {
883
+ this.ui.status = launch.error;
884
+ return;
885
+ }
886
+ const job = launchJob(this.nextId, launch, model);
887
+ this.nextId += 1;
888
+ this.jobs.push(job);
889
+ assignLog(this.root, job);
890
+ const onData = (text) => this.showOutput(job, text);
891
+ const onClose = (result) => this.finishJob(job, result);
892
+ const child = this.spawn(this.root, launch, onData, onClose);
893
+ if (job.status === 'running') job.child = child;
894
+ this.saveRuns();
895
+ this.syncProgress();
896
+ if (showLog && this.jobs.at(-1) === job) this.openView(job.id);
897
+ }
898
+
899
+ startJob(row, sessionId = '') {
900
+ const picked = this.choice(row.id);
901
+ const plan = this.ensurePlan();
902
+ const launch = buildLaunch(row, picked, plan, this.root, sessionId);
903
+ this.startLaunch(launch, picked.model);
904
+ }
905
+
906
+ prepareStart() {
907
+ const row = this.selected();
908
+ const live = row ? this.runningJob(row.id) : null;
909
+ if (live) {
910
+ this.openView(live.id);
911
+ return null;
912
+ }
913
+ if (!row || !row.bin) {
914
+ this.ui.status = 'not installed';
915
+ return null;
916
+ }
917
+ const file = this.planFile();
918
+ if (file && this.planBusy(file)) {
919
+ this.ui.status = `busy ${path.basename(file)}`;
920
+ return null;
921
+ }
922
+ return row;
923
+ }
924
+
925
+ startLogin(row) {
926
+ const live = this.runningJob(row.id, 'login');
927
+ if (!live) this.startLaunch(buildLogin(row), '', false);
928
+ if (this.viewing) this.closeView();
929
+ this.ui.status = 'logging in';
930
+ this.ui.paint();
931
+ }
932
+
933
+ rerun() {
934
+ if (!this.viewing) return;
935
+ const job = this.viewedJob();
936
+ if (!job || job.status === 'running' || !job.cmd) return;
937
+ this.startLaunch(replayLaunch(job), job.model);
938
+ }
939
+
940
+ start() {
941
+ if (this.pick) return void this.acceptPick();
942
+ const session = this.selectedSession();
943
+ if (this.focus === 'runs' && this.openRunRow(session)) return;
944
+ const row = this.prepareStart();
945
+ if (!row) return;
946
+ this.beginStart(row, continuedId(session));
947
+ }
948
+
949
+ continueSession() {
950
+ if (this.viewing || this.pick || this.focus !== 'runs') return;
951
+ const session = this.selectedSession();
952
+ const id = continuedId(session);
953
+ if (!id || session.status === 'running') return;
954
+ const row = this.prepareStart();
955
+ if (!row) return;
956
+ this.beginStart(row, id);
957
+ }
958
+
959
+ selectedJob() {
960
+ const row = this.selected();
961
+ return row ? this.runningJob(row.id) : null;
962
+ }
963
+
964
+ stop() {
965
+ const job = this.viewing ? this.viewedJob() : this.selectedJob();
966
+ if (!job || job.status !== 'running' || !job.child) return;
967
+ job.stopping = true;
968
+ job.child.kill();
969
+ if (job.status !== 'running') return;
970
+ this.settleJob(job, null);
971
+ this.ui.status = 'stopped';
972
+ this.ui.paint();
973
+ }
974
+ }
975
+
976
+ module.exports = { AgentsController };