reslop 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +16 -1
  2. package/README.md +21 -3
  3. package/lib/agents/acp.js +309 -0
  4. package/lib/agents/agents.js +163 -436
  5. package/lib/agents/allow.js +141 -52
  6. package/lib/agents/allow.json +16 -14
  7. package/lib/agents/models.js +250 -383
  8. package/lib/agents/registry.js +437 -0
  9. package/lib/agents/run.js +234 -0
  10. package/lib/agents/transcript.js +143 -0
  11. package/lib/cli.js +2 -1
  12. package/lib/common/process.js +26 -1
  13. package/lib/dashboard/git.js +11 -1
  14. package/lib/dashboard/reports.js +80 -0
  15. package/lib/dashboard/watch.js +43 -8
  16. package/lib/dashboard/workspace.js +19 -20
  17. package/lib/dashboard/worktrees.js +145 -0
  18. package/lib/git/dir.js +50 -0
  19. package/lib/git/linked.js +152 -0
  20. package/lib/git/local.js +179 -0
  21. package/lib/git/status.js +126 -0
  22. package/lib/git/worktree.js +113 -36
  23. package/lib/input/actions.js +83 -5
  24. package/lib/input/keys.js +30 -1
  25. package/lib/input/line-edit.js +5 -1
  26. package/lib/render/agents.js +50 -24
  27. package/lib/render/branches.js +55 -27
  28. package/lib/render/field.js +19 -5
  29. package/lib/render/footer.js +14 -6
  30. package/lib/render/frame.js +16 -2
  31. package/lib/render/menu.js +103 -6
  32. package/lib/render/name-edit.js +31 -0
  33. package/lib/render/repos.js +52 -8
  34. package/lib/render/status.js +22 -4
  35. package/lib/report/run.js +16 -23
  36. package/lib/review/agent-instructions.md +2 -2
  37. package/lib/runs/process.js +3 -3
  38. package/lib/runs/runs.js +27 -4
  39. package/lib/session/agents/agents.js +220 -205
  40. package/lib/session/agents/away.js +48 -0
  41. package/lib/session/agents/job.js +23 -86
  42. package/lib/session/agents/jobs.js +14 -27
  43. package/lib/session/agents/options.js +45 -101
  44. package/lib/session/agents/plans.js +53 -0
  45. package/lib/session/agents/view.js +34 -0
  46. package/lib/session/branches.js +18 -1
  47. package/lib/session/compose.js +13 -5
  48. package/lib/session/dispatch.js +43 -4
  49. package/lib/session/editor-pointer.js +98 -0
  50. package/lib/session/files-pane.js +3 -0
  51. package/lib/session/lifecycle.js +67 -10
  52. package/lib/session/navigation.js +5 -0
  53. package/lib/session/npm/history.js +2 -1
  54. package/lib/session/npm/npm.js +89 -12
  55. package/lib/session/ops.js +3 -0
  56. package/lib/session/pointer.js +304 -46
  57. package/lib/session/session.js +15 -2
  58. package/lib/session/terminal.js +2 -2
  59. package/lib/session/text-menu.js +50 -0
  60. package/lib/session/view.js +19 -2
  61. package/lib/session/workspace.js +179 -38
  62. package/lib/session/worktrees.js +280 -0
  63. package/lib/term/themes/dark.json +4 -0
  64. package/lib/term/themes/light.json +4 -0
  65. package/package.json +2 -2
@@ -4,19 +4,8 @@ const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { clamp } = require('../../common/utilities.js');
6
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;
7
+ const { detectAgents, listModels, startAgent, buildLaunch } = agents;
8
+ const { emptyChoice, catalogOf, buildInstall } = agents;
20
9
  const review = require('../../review/review.js');
21
10
  const { latestReviewName, listReviewNames, planFileNames } = review;
22
11
  const { REVIEW_DIR } = require('../../common/files.js');
@@ -24,16 +13,18 @@ const render = require('../../render/log.js');
24
13
  const { logRowCount } = render;
25
14
  const { RunPanel } = require('../run-panel.js');
26
15
  const options = require('./options.js');
27
- const { AgentOptions, concrete, restoreChoice, storedChoice } = options;
16
+ const { AgentOptions, restoreChoice, storedChoice } = options;
28
17
  const jobs = require('./jobs.js');
18
+ const { AwayRuns } = require('./away.js');
29
19
  const { readPrefs, writePrefs } = require('../prefs.js');
30
20
  const { elapsedLabel, launchJob, assignLog, stemOf } = jobs;
31
21
  const { restoreJobs, readRunList, saveRunFiles } = jobs;
32
22
  const { panelSessions, orderedSessions, runBlock } = jobs;
33
23
  const { runningJobs, withLiveRuns } = jobs;
34
- const { replayLaunch } = jobs;
35
24
  const plans = require('./plans.js');
36
25
  const { planKey, planProgress, planPart, planLabel, alignPlanLabels } = plans;
26
+ const { planCaption } = plans;
27
+ const { NO_PICK, NO_LIST, NO_JOB } = require('./view.js');
37
28
  const { MenuPick, menuRows, pickDelta } = require('../../input/menu-pick.js');
38
29
  const { lineKey } = require('../../input/line-edit.js');
39
30
 
@@ -41,7 +32,9 @@ const optionsOf = (row) => new AgentOptions(row);
41
32
 
42
33
  const APPLY_FIELD = {
43
34
  model: (choices, picked, value) => choices.pickModel(picked, value),
44
- context: (choices, picked, value) => choices.pickContext(picked, value),
35
+ context: (choices, picked, value) => {
36
+ picked.context = value;
37
+ },
45
38
  effort: (choices, picked, value) => {
46
39
  picked.effort = value;
47
40
  },
@@ -61,9 +54,9 @@ const continuedId = (session) => {
61
54
 
62
55
  const cacheKey = (row) => `${row.id}:${row.bin}`;
63
56
 
64
- const listQuietly = async (list, row, env) => {
57
+ const listQuietly = async (list, row, options) => {
65
58
  try {
66
- return await list(row.bin, row.spec, { env });
59
+ return await list(row, options);
67
60
  } catch {
68
61
  return null;
69
62
  }
@@ -75,42 +68,12 @@ const firstEnabled = (items, at, isOff) => {
75
68
  return free < 0 ? at : free;
76
69
  };
77
70
 
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
71
  class AgentsController extends RunPanel {
109
72
  constructor(ui) {
110
73
  super(ui);
111
74
  this.spawn = startAgent;
112
75
  this.listModels = listModels;
113
- this.authProbe = probeAuth;
76
+ this.probes = null;
114
77
  this.reset();
115
78
  }
116
79
 
@@ -123,8 +86,12 @@ class AgentsController extends RunPanel {
123
86
  this.list = [];
124
87
  this.choices = new Map();
125
88
  this.loaded = new Map();
89
+ this.asking = new Set();
126
90
  this.modelsEpoch = 0;
127
91
  this.jobs = [];
92
+ this.away = new AwayRuns();
93
+ this.installs = new Map();
94
+ this.installTarget = '';
128
95
  this.nextId = 1;
129
96
  this.viewing = false;
130
97
  this.viewId = 0;
@@ -140,31 +107,34 @@ class AgentsController extends RunPanel {
140
107
  this.viewSession = '';
141
108
  this.historyText = '';
142
109
  this.historyAt = 0;
143
- this.pendingStart = null;
144
- this.authYes = new Set();
145
- this.authWait = '';
146
110
  this.env = process.env;
147
111
  this.refresh();
148
112
  this.loadRuns();
149
113
  }
150
114
 
151
115
  killAll() {
152
- for (const job of this.jobs ?? []) {
116
+ if (this.probes) this.probes.abort();
117
+ this.probes = null;
118
+ for (const job of this.everyJob()) {
153
119
  if (job.child) job.child.kill();
154
120
  }
155
121
  }
156
122
 
123
+ // The install of an adapter is watched like a run but is no session
124
+ everyJob() {
125
+ const installs = this.installs ? this.installs.values() : [];
126
+ const runs = this.jobs ?? [];
127
+ const away = this.away ? this.away.jobs() : [];
128
+ return [...runs, ...away, ...installs];
129
+ }
130
+
157
131
  reviewStore() {
158
132
  return this.ui.review ? this.ui.review.store : null;
159
133
  }
160
134
 
161
135
  applyCached(row) {
162
136
  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;
137
+ if (cached) row.catalog = cached;
168
138
  }
169
139
 
170
140
  refresh(env = this.env) {
@@ -174,7 +144,7 @@ class AgentsController extends RunPanel {
174
144
  this.choices = new Map();
175
145
  for (const row of this.list) {
176
146
  this.applyCached(row);
177
- this.choices.set(row.id, prev.get(row.id) ?? emptyChoice(row));
147
+ this.choices.set(row.id, prev.get(row.id) ?? emptyChoice());
178
148
  }
179
149
  this.loadChoices();
180
150
  this.clampCursor();
@@ -185,7 +155,7 @@ class AgentsController extends RunPanel {
185
155
  const saved = readPrefs(this.root).agents;
186
156
  for (const row of this.list) {
187
157
  const picked = this.choice(row.id);
188
- restoreChoice(picked, saved[row.id], row.spec);
158
+ restoreChoice(picked, saved[row.id]);
189
159
  optionsOf(row).preferEffort(picked, 'medium');
190
160
  }
191
161
  }
@@ -196,7 +166,7 @@ class AgentsController extends RunPanel {
196
166
  if (!this.root || !picked || !row) return;
197
167
  const data = readPrefs(this.root);
198
168
  const saved = { ...data.agents };
199
- const stored = storedChoice(picked, row.spec);
169
+ const stored = storedChoice(picked);
200
170
  if (stored) saved[id] = stored;
201
171
  else delete saved[id];
202
172
  writePrefs(this.root, { ...data, agents: saved });
@@ -211,22 +181,36 @@ class AgentsController extends RunPanel {
211
181
  }
212
182
 
213
183
  // The repository can change after construction: its runs and its saved
214
- // sessions must come from the same directory
184
+ // sessions must come from the same directory. A run that goes on in the
185
+ // previous one stays with it
215
186
  syncRuns() {
216
187
  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
- }
188
+ this.away.park(this.runsRoot, this.jobs);
189
+ const parked = this.away.take(this.root);
190
+ if (parked) this.jobs = parked;
191
+ else this.loadRuns();
192
+ this.runsRoot = this.root;
193
+ const last = this.jobs.reduce((max, job) => Math.max(max, job.id), 0);
194
+ this.nextId = Math.max(this.nextId, last + 1);
224
195
  this.viewId = 0;
225
196
  }
226
197
 
198
+ retarget() {
199
+ this.viewing = false;
200
+ this.viewId = 0;
201
+ this.pick = null;
202
+ this.viewSession = '';
203
+ }
204
+
205
+ // The runs in work by directory, for the screen of worktrees
206
+ liveByRoot() {
207
+ return this.away.counts(this.runsRoot, this.jobs);
208
+ }
209
+
227
210
  saveRuns() {
228
211
  if (!this.root) return;
229
- saveRunFiles(this.root, this.jobs);
212
+ saveRunFiles(this.runsRoot || this.root, this.jobs);
213
+ this.away.save();
230
214
  }
231
215
 
232
216
  clampCursor() {
@@ -244,8 +228,8 @@ class AgentsController extends RunPanel {
244
228
 
245
229
  choice(id) {
246
230
  const row = this.cliById(id);
247
- if (!row) return emptyChoice(null);
248
- if (!this.choices.has(id)) this.choices.set(id, emptyChoice(row));
231
+ if (!row) return emptyChoice();
232
+ if (!this.choices.has(id)) this.choices.set(id, emptyChoice());
249
233
  return this.choices.get(id);
250
234
  }
251
235
 
@@ -256,13 +240,14 @@ class AgentsController extends RunPanel {
256
240
  const picked = this.choice(row.id);
257
241
  const choices = optionsOf(row);
258
242
  const model = choices.normalize(picked);
243
+ const { effort, fast, context } = choices.settings(picked);
259
244
  return {
260
245
  ...row,
261
246
  model,
262
- effort: concrete(picked.effort),
263
- efforts: choices.efforts(picked.model),
264
- fast: picked.fast ? 'on' : 'off',
265
- context: concrete(picked.context),
247
+ effort,
248
+ efforts: choices.efforts(model),
249
+ fast,
250
+ context,
266
251
  contexts: choices.contexts(model),
267
252
  plan: row.bin ? plan : '',
268
253
  planBusy: Boolean(row.bin && planBusy),
@@ -317,25 +302,21 @@ class AgentsController extends RunPanel {
317
302
  }
318
303
 
319
304
  jobById(id) {
320
- return this.jobs.find((job) => job.id === id) ?? null;
305
+ return this.everyJob().find((job) => job.id === id) ?? null;
321
306
  }
322
307
 
323
308
  viewedJob() {
324
309
  return this.jobById(this.viewId);
325
310
  }
326
311
 
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
- };
312
+ runningJob(id) {
313
+ const live = (job) => job.cliId === id && job.status === 'running';
334
314
  return this.jobs.findLast(live) ?? null;
335
315
  }
336
316
 
337
317
  runningCount() {
338
- return this.jobs.filter((job) => job.status === 'running').length;
318
+ const live = (job) => job.status === 'running';
319
+ return this.everyJob().filter(live).length;
339
320
  }
340
321
 
341
322
  summary() {
@@ -381,17 +362,18 @@ class AgentsController extends RunPanel {
381
362
  if (!onAgents) return NO_LIST;
382
363
  const agentRuns = this.sessionRows();
383
364
  const selected = agentRuns[this.runCursor];
384
- const agentCanContinue =
385
- this.focus === 'runs' &&
386
- Boolean(continuedId(selected)) &&
387
- selected.status !== 'running';
365
+ const running = Boolean(selected && selected.status === 'running');
366
+ const fresh = Boolean(selected && selected.fresh);
367
+ const resume = Boolean(continuedId(selected));
368
+ const agentCanStart = !running && (fresh || resume);
388
369
  return {
389
370
  agents: this.rows(),
371
+ agentInstallPackage: this.installPackage(),
390
372
  agentRuns,
391
373
  agentRunCursor: this.runCursor,
392
374
  agentRunScroll: this.runScroll,
393
375
  agentFocus: this.focus,
394
- agentCanContinue,
376
+ agentCanStart,
395
377
  agentPlanCount: this.reviewChoices().length,
396
378
  agentView: this.viewing,
397
379
  };
@@ -409,7 +391,7 @@ class AgentsController extends RunPanel {
409
391
  if (!job) return NO_JOB;
410
392
  const running = job.status === 'running';
411
393
  const name = stemOf(job.logName);
412
- const agentCommand = name || job.command;
394
+ const agentCommand = job.title || name || job.command;
413
395
  return {
414
396
  agentOutput: job.output,
415
397
  agentCommand,
@@ -465,25 +447,52 @@ class AgentsController extends RunPanel {
465
447
  async loadModels() {
466
448
  this.modelsEpoch += 1;
467
449
  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);
450
+ const waiting = this.list.filter(
451
+ (row) => row.bin && !this.loaded.has(cacheKey(row)),
452
+ );
453
+ if (!waiting.length) return;
454
+ await Promise.all(waiting.map((row) => this.fetchModels(row, epoch)));
473
455
  if (epoch === this.modelsEpoch) this.ui.paint();
474
456
  }
475
457
 
458
+ probeOptions(model) {
459
+ this.probes ??= new AbortController();
460
+ const { signal } = this.probes;
461
+ const models = model ? [model] : [];
462
+ return { env: this.env, cwd: this.root, signal, models };
463
+ }
464
+
465
+ // The agent lists its models only to a session, so one is opened to ask
476
466
  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);
467
+ const options = this.probeOptions(this.choice(row.id).model);
468
+ const catalog = await listQuietly(this.listModels, row, options);
469
+ if (epoch !== this.modelsEpoch || !catalog) return;
470
+ this.loaded.set(cacheKey(row), catalog);
471
+ const current = this.cliById(row.id);
472
+ if (!current || current.bin !== row.bin) return;
473
+ this.applyCached(current);
474
+ optionsOf(current).preferEffort(this.choice(current.id), 'medium');
475
+ }
476
+
477
+ // The settings of a model come from asking the agent about that model
478
+ async describeModel(row, model) {
479
+ const key = cacheKey(row);
480
+ const mark = `${key}:${model}`;
481
+ const known = this.loaded.get(key);
482
+ if (!known || !catalogOf(known).unknown(model)) return;
483
+ if (this.asking.has(mark)) return;
484
+ this.asking.add(mark);
485
+ const options = this.probeOptions(model);
486
+ const found = await listQuietly(this.listModels, row, options);
487
+ this.asking.delete(mark);
488
+ const cached = this.loaded.get(key);
489
+ if (!cached || !found || !(found.byModel instanceof Map)) return;
490
+ const prior = cached.byModel ?? [];
491
+ const byModel = new Map([...prior, ...found.byModel]);
492
+ this.loaded.set(key, { ...cached, byModel });
485
493
  const current = this.cliById(row.id);
486
494
  if (current && current.bin === row.bin) this.applyCached(current);
495
+ this.ui.paint();
487
496
  }
488
497
 
489
498
  logWidth() {
@@ -560,9 +569,7 @@ class AgentsController extends RunPanel {
560
569
  const choices = optionsOf(row);
561
570
  const picked = this.choice(row.id);
562
571
  if (field === 'model') return choices.names;
563
- if (field === 'context') {
564
- return choices.contexts(choices.shown(picked.model));
565
- }
572
+ if (field === 'context') return choices.contexts(picked.model);
566
573
  return choices.efforts(picked.model);
567
574
  }
568
575
 
@@ -573,9 +580,10 @@ class AgentsController extends RunPanel {
573
580
  return firstEnabled(items, Math.max(0, at), this.busyCheck());
574
581
  }
575
582
  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));
583
+ const choices = optionsOf(row);
584
+ const shown = { ...choices.settings(picked) };
585
+ shown.model = choices.shown(picked.model);
586
+ return Math.max(0, items.indexOf(shown[field]));
579
587
  }
580
588
 
581
589
  menuItems() {
@@ -621,7 +629,7 @@ class AgentsController extends RunPanel {
621
629
  if (!row || !row.bin) return;
622
630
  optionsOf(row).normalize(this.choice(row.id));
623
631
  const items = this.fieldItems(field, row);
624
- if (items.length < (field === 'context' ? 1 : 2)) return;
632
+ if (items.length < 2) return;
625
633
  this.pick = new MenuPick(field, this.fieldStart(field, items, row));
626
634
  this.ui.status = '';
627
635
  this.ui.paint();
@@ -654,6 +662,7 @@ class AgentsController extends RunPanel {
654
662
  const apply = APPLY_FIELD[field] ?? APPLY_FIELD.effort;
655
663
  apply(optionsOf(row), picked, value);
656
664
  this.saveChoice(row.id);
665
+ if (field === 'model') void this.describeModel(row, picked.model);
657
666
  return `${field} ${picked[field]}`;
658
667
  }
659
668
 
@@ -680,9 +689,9 @@ class AgentsController extends RunPanel {
680
689
  return;
681
690
  }
682
691
  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';
692
+ const choices = optionsOf(row);
693
+ if (choices.toggleFast(picked)) this.saveChoice(row.id);
694
+ this.ui.status = `fast ${choices.settings(picked).fast}`;
686
695
  this.ui.paint();
687
696
  }
688
697
 
@@ -718,7 +727,7 @@ class AgentsController extends RunPanel {
718
727
  busyPlans() {
719
728
  const busy = new Set();
720
729
  for (const job of this.jobs) {
721
- if (job.status !== 'running' || job.action === 'login') continue;
730
+ if (job.status !== 'running') continue;
722
731
  const name = path.basename(`${job.plan ?? ''}`);
723
732
  if (name) busy.add(name);
724
733
  }
@@ -766,12 +775,10 @@ class AgentsController extends RunPanel {
766
775
  }
767
776
 
768
777
  closeView() {
769
- const job = this.viewedJob();
770
778
  this.viewing = false;
771
779
  this.viewSession = '';
772
780
  this.historyText = '';
773
- const waiting = job && job.action === 'login' && job.status === 'running';
774
- this.ui.status = waiting ? 'logging in' : '';
781
+ this.ui.status = '';
775
782
  this.ui.paint();
776
783
  }
777
784
 
@@ -790,6 +797,7 @@ class AgentsController extends RunPanel {
790
797
  showOutput(job, text) {
791
798
  job.output = text;
792
799
  job.dirty = true;
800
+ job.captureStats();
793
801
  this.keepLog(job);
794
802
  const watched = this.viewing && this.viewId === job.id;
795
803
  if (watched || this.ui.nav.pane === 'agents') this.ui.paint();
@@ -808,77 +816,13 @@ class AgentsController extends RunPanel {
808
816
  if (job.status !== 'running') return;
809
817
  const stopped = job.stopping;
810
818
  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;
819
+ if (stopped && this.viewing && this.viewId === job.id) {
820
+ this.ui.status = 'stopped';
829
821
  }
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
822
  this.ui.paint();
875
823
  }
876
824
 
877
- launchReady(row, sessionId = '') {
878
- this.startJob(row, sessionId);
879
- }
880
-
881
- startLaunch(launch, model, showLog = true) {
825
+ startLaunch(launch, model) {
882
826
  if (!launch.ok) {
883
827
  this.ui.status = launch.error;
884
828
  return;
@@ -893,14 +837,15 @@ class AgentsController extends RunPanel {
893
837
  if (job.status === 'running') job.child = child;
894
838
  this.saveRuns();
895
839
  this.syncProgress();
896
- if (showLog && this.jobs.at(-1) === job) this.openView(job.id);
840
+ if (this.jobs.at(-1) === job) this.openView(job.id);
897
841
  }
898
842
 
899
843
  startJob(row, sessionId = '') {
900
844
  const picked = this.choice(row.id);
901
845
  const plan = this.ensurePlan();
902
846
  const launch = buildLaunch(row, picked, plan, this.root, sessionId);
903
- this.startLaunch(launch, picked.model);
847
+ const title = planCaption(this.root, launch.plan);
848
+ this.startLaunch({ ...launch, title }, picked.model);
904
849
  }
905
850
 
906
851
  prepareStart() {
@@ -922,19 +867,90 @@ class AgentsController extends RunPanel {
922
867
  return row;
923
868
  }
924
869
 
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
870
  rerun() {
934
871
  if (!this.viewing) return;
935
872
  const job = this.viewedJob();
936
- if (!job || job.status === 'running' || !job.cmd) return;
937
- this.startLaunch(replayLaunch(job), job.model);
873
+ if (!job || job.status === 'running') return;
874
+ if (job.action === 'install') {
875
+ this.closeView();
876
+ return void this.install(job.cliId);
877
+ }
878
+ const row = this.cliById(job.cliId);
879
+ const plan = job.plan ? path.resolve(this.root, job.plan) : '';
880
+ const launch = buildLaunch(row, job.choice, plan, this.root, job.resume);
881
+ const title = planCaption(this.root, launch.plan) || job.title;
882
+ this.startLaunch({ ...launch, title }, job.model);
883
+ }
884
+
885
+ installPackage() {
886
+ const row = this.cliById(this.installTarget);
887
+ return row ? row.package || row.installLabel : '';
888
+ }
889
+
890
+ proposeInstall() {
891
+ if (this.viewing || this.pick) return;
892
+ const row = this.selected();
893
+ const live = row ? this.installs.get(row.id) : null;
894
+ if (live && live.status === 'running') return void this.openView(live.id);
895
+ const launch = buildInstall(row);
896
+ if (!launch.ok) {
897
+ this.ui.status = launch.error;
898
+ return void this.ui.paint();
899
+ }
900
+ this.installTarget = row.id;
901
+ this.ui.status = '';
902
+ this.ui.mode = 'confirmAgentInstall';
903
+ this.ui.paint();
904
+ }
905
+
906
+ cancelInstall() {
907
+ this.installTarget = '';
908
+ this.ui.mode = 'review';
909
+ this.ui.paint();
910
+ }
911
+
912
+ confirmInstall() {
913
+ const id = this.installTarget;
914
+ this.installTarget = '';
915
+ this.ui.mode = 'review';
916
+ this.install(id);
917
+ }
918
+
919
+ install(id = '') {
920
+ if (this.viewing || this.pick) return;
921
+ const row = id ? this.cliById(id) : this.selected();
922
+ const launch = buildInstall(row);
923
+ if (!launch.ok) {
924
+ this.ui.status = launch.error;
925
+ return void this.ui.paint();
926
+ }
927
+ const job = launchJob(this.nextId, launch, '');
928
+ this.nextId += 1;
929
+ this.installs.set(row.id, job);
930
+ const onData = (text) => this.showOutput(job, text);
931
+ const onClose = (result) => this.finishInstall(job, result);
932
+ const child = this.spawn(this.root, launch, onData, onClose);
933
+ if (job.status === 'running') job.child = child;
934
+ this.syncProgress();
935
+ this.openView(job.id);
936
+ }
937
+
938
+ // Refresh PATH and managed registry locations after the installer exits.
939
+ finishInstall(job, result) {
940
+ if (job.status !== 'running') return;
941
+ const stopped = job.stopping;
942
+ this.settleJob(job, stopped ? null : result);
943
+ this.ui.status = stopped ? 'stopped' : this.installOutcome(job, result);
944
+ this.ui.paint();
945
+ }
946
+
947
+ installOutcome(job, result) {
948
+ if (result.status !== 0) return 'install failed';
949
+ this.refresh();
950
+ const row = this.cliById(job.cliId);
951
+ if (!row || !row.bin) return 'installed, but not found';
952
+ void this.loadModels();
953
+ return `installed ${job.name}`;
938
954
  }
939
955
 
940
956
  start() {
@@ -942,18 +958,17 @@ class AgentsController extends RunPanel {
942
958
  const session = this.selectedSession();
943
959
  if (this.focus === 'runs' && this.openRunRow(session)) return;
944
960
  const row = this.prepareStart();
945
- if (!row) return;
946
- this.beginStart(row, continuedId(session));
961
+ if (row) this.startJob(row, continuedId(session));
947
962
  }
948
963
 
949
- continueSession() {
950
- if (this.viewing || this.pick || this.focus !== 'runs') return;
964
+ startSelected() {
965
+ if (this.viewing || this.pick) return;
951
966
  const session = this.selectedSession();
952
- const id = continuedId(session);
953
- if (!id || session.status === 'running') return;
967
+ if (!session || session.status === 'running') return;
968
+ const resume = session.fresh ? '' : continuedId(session);
969
+ if (!session.fresh && !resume) return;
954
970
  const row = this.prepareStart();
955
- if (!row) return;
956
- this.beginStart(row, id);
971
+ if (row) this.startJob(row, resume);
957
972
  }
958
973
 
959
974
  selectedJob() {