reslop 0.2.7 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +504 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +16 -0
  7. package/lib/agents/models.js +460 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +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} +123 -96
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +475 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +111 -48
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +15 -43
  47. package/lib/input/line-edit.js +77 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +640 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{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 +383 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +319 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +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} +46 -14
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +9 -0
  81. package/lib/review/format.js +426 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +958 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -39
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +181 -145
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +85 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +75 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -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} +156 -63
  120. package/lib/session/tasks.js +320 -65
  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} +2 -2
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -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,109 @@
1
+ 'use strict';
2
+
3
+ // Repo reads the screen needs on every frame: loaded ahead and kept here
4
+ class FileReads {
5
+ constructor(ui) {
6
+ this.ui = ui;
7
+ this.reset();
8
+ }
9
+
10
+ reset() {
11
+ this.names = [];
12
+ this.textKey = null;
13
+ this.text = '';
14
+ this.stamp = 0;
15
+ this.wanted = null;
16
+ }
17
+
18
+ keyOf(rel) {
19
+ const ui = this.ui;
20
+ return `${ui.top}\0${ui.rev ?? ''}\0${rel}`;
21
+ }
22
+
23
+ async readNames() {
24
+ const ui = this.ui;
25
+ if (!ui.repo.listFiles) return [];
26
+ const options = { commit: ui.rev };
27
+ try {
28
+ return await ui.repo.listFiles(ui.top || ui.cwd, ui.paths ?? [], options);
29
+ } catch {
30
+ return [];
31
+ }
32
+ }
33
+
34
+ async readText(rel) {
35
+ const ui = this.ui;
36
+ const key = this.keyOf(rel);
37
+ const repo = ui.repo;
38
+ const readable = rel && repo && typeof repo.fileText === 'function';
39
+ if (!readable) return { key, text: '' };
40
+ try {
41
+ return { key, text: await repo.fileText(ui.top, rel, ui.rev) };
42
+ } catch {
43
+ return { key, text: '' };
44
+ }
45
+ }
46
+
47
+ // What a reload needs besides the snapshot, applied together with it
48
+ async read() {
49
+ const nav = this.ui.nav;
50
+ const listed = nav.fileScope === 'file';
51
+ const rel = nav.pane === 'unit' ? nav.reviewPath : null;
52
+ const reads = [
53
+ listed ? this.readNames() : null,
54
+ rel ? this.readText(rel) : null,
55
+ ];
56
+ const stamp = this.stamp;
57
+ const [names, viewed] = await Promise.all(reads);
58
+ return { names, viewed, stamp };
59
+ }
60
+
61
+ // Text kept since the read began is newer than what the read found
62
+ apply(found) {
63
+ if (found.names) this.names = found.names;
64
+ if (found.viewed && found.stamp === this.stamp) this.keep(found.viewed);
65
+ }
66
+
67
+ keep(viewed) {
68
+ this.textKey = viewed.key;
69
+ this.text = viewed.text;
70
+ this.stamp += 1;
71
+ }
72
+
73
+ keepText(rel, text) {
74
+ this.keep({ key: this.keyOf(rel), text });
75
+ }
76
+
77
+ async loadNames() {
78
+ this.names = await this.readNames();
79
+ }
80
+
81
+ async loadText(rel) {
82
+ const viewed = await this.readText(rel);
83
+ this.keep(viewed);
84
+ return viewed.text;
85
+ }
86
+
87
+ cached(rel) {
88
+ if (!rel || this.textKey !== this.keyOf(rel)) return null;
89
+ return this.text;
90
+ }
91
+
92
+ // A path nobody loaded ahead is read now and painted when it arrives
93
+ textOf(rel) {
94
+ const text = this.cached(rel);
95
+ if (text !== null || !rel) return text ?? '';
96
+ const key = this.keyOf(rel);
97
+ if (this.wanted === key) return '';
98
+ this.wanted = key;
99
+ const ui = this.ui;
100
+ const done = () => {
101
+ this.wanted = null;
102
+ ui.paint();
103
+ };
104
+ ui.ops.track(this.loadText(rel).then(done, done));
105
+ return '';
106
+ }
107
+ }
108
+
109
+ module.exports = { FileReads };
@@ -1,10 +1,19 @@
1
1
  'use strict';
2
2
 
3
- const review = require('../review.js');
4
- const { createStore, applyImportedNotes, setStatus } = review;
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { REVIEW_DIR, ISSUE_FILE } = require('../common/files.js');
7
+ const review = require('../review/review.js');
8
+ const { ReviewStore } = review;
5
9
  const { listReviewNames, loadTemplates, resolveReviewPath } = review;
6
10
  const { loadReview, flushReview } = review;
7
11
 
12
+ const issueTasks = (imported) => {
13
+ const todos = imported.tasks ?? imported.todos ?? [];
14
+ return todos.filter((todo) => (todo.file || '') === ISSUE_FILE);
15
+ };
16
+
8
17
  class ReviewController {
9
18
  constructor() {
10
19
  this.reset();
@@ -21,16 +30,17 @@ class ReviewController {
21
30
  const templates = loadTemplates(dir);
22
31
  const resolved = resolveReviewPath(dir, date, names, extra);
23
32
  this.didResume = resolved.resume === true;
24
- this.store = this.didResume
25
- ? loadReview(resolved.reviewPath, templates)
26
- : createStore(resolved.reviewPath, templates);
33
+ const reviewPath = resolved.reviewPath;
34
+ if (this.didResume) this.store = loadReview(reviewPath, templates);
35
+ else this.store = new ReviewStore(reviewPath, templates);
27
36
  return this.store;
28
37
  }
29
38
 
30
39
  applyImported(imported) {
31
40
  if (!imported || !this.store) return false;
32
- if (this.didResume || this.importedRemote) return false;
33
- applyImportedNotes(this.store, imported);
41
+ if (this.importedRemote) return false;
42
+ const notes = this.didResume ? { tasks: issueTasks(imported) } : imported;
43
+ this.store.applyImportedNotes(notes);
34
44
  this.importedRemote = true;
35
45
  return true;
36
46
  }
@@ -45,9 +55,17 @@ class ReviewController {
45
55
  }
46
56
  }
47
57
 
48
- finishQuit(status) {
49
- setStatus(this.store, status);
50
- return this.flush();
58
+ usePlan(dir, name) {
59
+ const store = this.store;
60
+ if (!store || !name) return false;
61
+ const next = path.join(dir, REVIEW_DIR, name);
62
+ if (next === store.reviewPath) return false;
63
+ this.flush();
64
+ const templates = store.templates;
65
+ const loaded = fs.existsSync(next);
66
+ if (loaded) this.store = loadReview(next, templates);
67
+ else this.store = new ReviewStore(next, templates);
68
+ return true;
51
69
  }
52
70
  }
53
71
 
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { clamp } = require('../common/utilities.js');
4
+ const { logScrollMax } = require('../render/log.js');
5
+
6
+ class RunPanel {
7
+ constructor(ui) {
8
+ this.ui = ui;
9
+ }
10
+
11
+ barMax() {
12
+ const bar = this.ui.lastFrame && this.ui.lastFrame.scrollBar;
13
+ return bar ? Math.max(0, bar.count - bar.rows) : -1;
14
+ }
15
+
16
+ setLog(start, max) {
17
+ const next = clamp(start, 0, max);
18
+ this.logScroll = next;
19
+ this.followEnd = next === max;
20
+ }
21
+
22
+ placeLog(start) {
23
+ const max = this.barMax();
24
+ if (max >= 0) this.setLog(start, max);
25
+ }
26
+
27
+ placeRuns(start) {
28
+ const max = this.barMax();
29
+ if (max >= 0) this.runScroll = clamp(start, 0, max);
30
+ }
31
+
32
+ scrollLogBy(delta, text, running) {
33
+ const { lastSize, lastFrame } = this.ui;
34
+ const max = logScrollMax(text, lastSize, lastFrame, running);
35
+ const from = this.followEnd ? max : this.logScroll;
36
+ this.setLog(from + delta, max);
37
+ }
38
+ }
39
+
40
+ module.exports = { RunPanel };
@@ -1,57 +1,58 @@
1
1
  'use strict';
2
2
 
3
- const render = require('./render/render.js');
4
- const { renderFrame } = render;
5
- const keys = require('./keys.js');
6
- const { decodeChunk } = keys;
7
- const review = require('./review.js');
8
- const { AUTOSAVE_MS, hasNotes } = review;
9
- const files = require('./files.js');
10
- const { makeTaskItem } = files;
11
- const capabilities = require('./capabilities.js');
12
- const { sessionCapabilities } = capabilities;
13
- const actions = require('./session/actions.js');
14
- const { LIST_PANES } = actions;
15
- const terminal = require('./session/terminal.js');
16
- const { Terminal, LEAVE_TERM } = terminal;
17
- const load = require('./session/load.js');
18
- const { LoadCoordinator } = load;
19
- const items = require('./session/items.js');
20
- const { ItemCollection } = items;
21
- const navigation = require('./session/navigation.js');
22
- const { Navigation } = navigation;
23
- const sessionReview = require('./session/review.js');
24
- const { ReviewController } = sessionReview;
25
- const update = require('./session/update.js');
26
- const { UpdateCoordinator } = update;
27
- const progress = require('./session/progress.js');
28
- const { Progress } = progress;
29
- const compose = require('./session/compose.js');
30
- const { Composer } = compose;
31
- const ops = require('./session/ops.js');
32
- const { OpsRunner } = ops;
33
- const commit = require('./session/commit.js');
34
- const { CommitController } = commit;
35
- const branches = require('./session/branches.js');
36
- const { BranchController } = branches;
37
- const npm = require('./session/npm.js');
38
- const { NpmController } = npm;
39
- const packages = require('./session/packages.js');
40
- const { PackageController } = packages;
41
- const accessors = require('./session/accessors.js');
42
- const { bindAccessors, bindGetters } = accessors;
43
- const view = require('./session/view.js');
44
- const filesPane = require('./session/files-pane.js');
45
- const lifecycle = require('./session/lifecycle.js');
46
- const { Lifecycle } = lifecycle;
47
- const dispatch = require('./session/dispatch.js');
3
+ const { renderFrame } = require('../render/render.js');
4
+ const { decodeChunk } = require('../input/keys.js');
5
+ const { AUTOSAVE_MS, hasNotes } = require('../review/review.js');
6
+ const { makeTaskItem } = require('../common/files.js');
7
+ const { sessionCapabilities } = require('../source/capabilities.js');
8
+ const { LIST_PANES } = require('../input/actions.js');
9
+ const { Terminal, Progress, LEAVE_TERM } = require('./terminal.js');
10
+ const { LoadCoordinator } = require('./load.js');
11
+ const { ItemCollection } = require('./items.js');
12
+ const { Navigation } = require('./navigation.js');
13
+ const { ReviewController } = require('./review.js');
14
+ const { UpdateCoordinator } = require('./update.js');
15
+ const { Composer } = require('./compose.js');
16
+ const { OpsRunner } = require('./ops.js');
17
+ const { FileReads } = require('./reads.js');
18
+ const { CommitController } = require('./commit.js');
19
+ const { BranchController } = require('./branches.js');
20
+ const { NpmController } = require('./npm/npm.js');
21
+ const { PackageController } = require('./npm/packages.js');
22
+ const { AgentsController } = require('./agents/agents.js');
23
+ const view = require('./view.js');
24
+ const filesPane = require('./files-pane.js');
25
+ const { Lifecycle } = require('./lifecycle.js');
26
+ const dispatch = require('./dispatch.js');
48
27
  const { dispatchAction, confirmChoice, handleEvent } = dispatch;
49
- const dashboard = require('./session/dashboard.js');
50
- const { Dashboard } = dashboard;
51
- const workspace = require('./session/workspace.js');
52
- const { Workspace } = workspace;
28
+ const { Dashboard } = require('./dashboard.js');
29
+ const { Workspace } = require('./workspace.js');
30
+
31
+ const bindAccessors = (target, owner, names) => {
32
+ for (const name of names) {
33
+ Object.defineProperty(target, name, {
34
+ configurable: true,
35
+ enumerable: true,
36
+ get: () => owner[name],
37
+ set: (value) => {
38
+ owner[name] = value;
39
+ },
40
+ });
41
+ }
42
+ };
43
+
44
+ const bindGetters = (target, owner, names) => {
45
+ for (const name of names) {
46
+ Object.defineProperty(target, name, {
47
+ configurable: true,
48
+ enumerable: true,
49
+ get: () => owner[name],
50
+ });
51
+ }
52
+ };
53
53
 
54
54
  const PROGRESS_MS = 160;
55
+ const QUIT_WAIT_MS = 3000;
55
56
 
56
57
  const NAV_FIELDS = [
57
58
  'pane',
@@ -60,6 +61,8 @@ const NAV_FIELDS = [
60
61
  'branchCursor',
61
62
  'commitCursor',
62
63
  'npmCursor',
64
+ 'packagesCursor',
65
+ 'agentCursor',
63
66
  'scroll',
64
67
  'listScroll',
65
68
  'reviewPath',
@@ -104,6 +107,11 @@ const assignOptions = (session, options) => {
104
107
  session.dashboardHome = options.startPane === 'dashboard';
105
108
  session.workspaceRoot = options.workspace ? options.workspace.root : '';
106
109
  session.workspaceInfo = options.workspace ?? null;
110
+ session.loadPullRequest = options.loadPullRequest ?? null;
111
+ session.loadMergeRequest = options.loadMergeRequest ?? null;
112
+ session.loadGithubIssue = options.loadGithubIssue ?? null;
113
+ session.loadGitlabIssue = options.loadGitlabIssue ?? null;
114
+ session.fetchImpl = options.fetch ?? null;
107
115
  };
108
116
 
109
117
  const assignParts = (session, options) => {
@@ -118,10 +126,12 @@ const assignParts = (session, options) => {
118
126
  session.updater = new UpdateCoordinator(session);
119
127
  session.composer = new Composer(session);
120
128
  session.ops = new OpsRunner(session);
129
+ session.reads = new FileReads(session);
121
130
  session.commits = new CommitController(session);
122
131
  session.gitBranches = new BranchController(session);
123
132
  session.npm = new NpmController(session);
124
133
  session.packages = new PackageController(session);
134
+ session.agents = new AgentsController(session);
125
135
  session.dashboard = new Dashboard(session);
126
136
  session.workspace = new Workspace(session);
127
137
  session.lifecycle = new Lifecycle(session);
@@ -164,18 +174,22 @@ class Session {
164
174
  this.review.reset();
165
175
  this.composer.reset();
166
176
  this.ops.reset();
177
+ this.reads.reset();
167
178
  this.gitBranches.reset();
168
179
  this.commits.reset();
169
180
  this.npm.reset();
181
+ this.agents.reset();
170
182
  this.dashboard.reset();
171
183
  this.workspace.reset();
172
184
  this.top = this.cwd;
173
185
  this.branch = '';
174
186
  this.status = '';
175
187
  this.layout = 'unified';
188
+ this.lineNumbers = false;
176
189
  this.done = false;
177
190
  this.exitCode = 0;
178
191
  this.mode = 'review';
192
+ this.quitWarning = '';
179
193
  this.tasksItem = makeTaskItem();
180
194
  this.progressFrame = 0;
181
195
  this.emptyReview = false;
@@ -190,13 +204,13 @@ class Session {
190
204
  this.progress.clear();
191
205
  }
192
206
 
193
- ensureRepo() {
207
+ async ensureRepo() {
194
208
  if (!this.repo.toplevel) return;
195
- this.top = this.repo.toplevel(this.cwd);
209
+ this.top = await this.repo.toplevel(this.cwd);
196
210
  }
197
211
 
198
212
  load() {
199
- this.lifecycle.load();
213
+ return this.lifecycle.load();
200
214
  }
201
215
 
202
216
  loadReady() {
@@ -234,6 +248,21 @@ class Session {
234
248
  if (LIST_PANES.includes(this.pane)) {
235
249
  this.nav.listScroll = frame.listScroll;
236
250
  }
251
+ const menuScroll = frame.menuScroll;
252
+ if (this.agents.pick && typeof menuScroll === 'number') {
253
+ this.agents.pick.scroll = menuScroll;
254
+ }
255
+ const planPick = this.composer.tasks.planPick;
256
+ if (planPick && typeof menuScroll === 'number') {
257
+ planPick.scroll = menuScroll;
258
+ }
259
+ if (typeof frame.runScroll === 'number') {
260
+ const panel = this.pane === 'npm' ? this.npm : this.agents;
261
+ panel.runScroll = frame.runScroll;
262
+ }
263
+ if (typeof frame.gridCols === 'number') {
264
+ this.workspace.cols = frame.gridCols;
265
+ }
237
266
  if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
238
267
  const editScroll = frame.cursor && frame.cursor.scroll;
239
268
  if (this.editor && typeof editScroll === 'number') {
@@ -294,7 +323,11 @@ class Session {
294
323
  }
295
324
 
296
325
  handleEvent(event) {
297
- handleEvent(this, event);
326
+ if (!this.ops.defer(event)) handleEvent(this, event);
327
+ }
328
+
329
+ finishInput() {
330
+ if (this.finishReadLoop) this.finishReadLoop();
298
331
  }
299
332
 
300
333
  pushInput(text) {
@@ -342,28 +375,83 @@ class Session {
342
375
  this.composer.autosave();
343
376
  }
344
377
 
378
+ retarget({ cwd, top, repoName, pane }) {
379
+ this.lifecycle.stopWatch();
380
+ this.dashboard.stop();
381
+ this.loader.reset();
382
+ this.cwd = cwd;
383
+ if (top) this.top = top;
384
+ this.paths = [];
385
+ this.repoName = repoName;
386
+ this.branch = '';
387
+ this.rev = null;
388
+ this.revShort = '';
389
+ this.change = null;
390
+ this.dashboardHome = pane === 'dashboard';
391
+ this.collection.reset();
392
+ this.reads.reset();
393
+ this.review.reset();
394
+ this.nav.pane = pane;
395
+ this.nav.tasksOpen = false;
396
+ this.nav.scroll = 0;
397
+ this.nav.clearSelection();
398
+ this.status = '';
399
+ }
400
+
401
+ childWarning() {
402
+ const tests = this.npm && this.npm.running === true;
403
+ const agents = this.agents && this.agents.runningCount() > 0;
404
+ if (tests && agents) return ' exit will terminate tests and agents';
405
+ if (agents) return ' exit will terminate agents';
406
+ if (tests) return ' exit will terminate tests';
407
+ return '';
408
+ }
409
+
410
+ stopChildren() {
411
+ if (this.npm) this.npm.stopAll();
412
+ if (this.agents) this.agents.killAll();
413
+ }
414
+
415
+ // A quick repo write still running lands first, so it is not left half done
416
+ exitNow() {
417
+ if (this.done || !this.ops.unlanded()) return void this.finishExit();
418
+ const exit = () => this.finishExit();
419
+ this.ops.hold(this.ops.landed(QUIT_WAIT_MS).then(exit));
420
+ }
421
+
422
+ finishExit() {
423
+ this.flushReview();
424
+ this.done = true;
425
+ this.exitCode = 0;
426
+ }
427
+
345
428
  onQuit() {
429
+ const composer = this.composer;
430
+ if (this.mode === 'compose' && composer.writesDisk()) {
431
+ const quit = () => this.onQuit();
432
+ return void this.ops.hold(composer.writeAndClose().then(quit));
433
+ }
346
434
  if (this.mode === 'compose') {
347
- this.composer.commitCompose();
348
- this.composer.closeCompose();
435
+ composer.commitCompose();
436
+ composer.closeCompose();
349
437
  }
350
- if (hasNotes(this.notes)) {
438
+ const warning = this.childWarning();
439
+ if (warning) {
440
+ this.quitWarning = warning;
351
441
  this.mode = 'confirmQuit';
352
442
  this.status = '';
353
443
  return;
354
444
  }
355
- this.done = true;
356
- this.exitCode = 0;
445
+ this.exitNow();
357
446
  }
358
447
 
359
- finishQuit(status) {
360
- const result = this.review.finishQuit(status);
361
- if (!result.ok) this.status = result.error.message;
362
- this.done = true;
363
- this.exitCode = 0;
448
+ confirmQuit() {
449
+ this.stopChildren();
450
+ this.exitNow();
364
451
  }
365
452
 
366
453
  cancelQuit() {
454
+ this.quitWarning = '';
367
455
  this.mode = 'review';
368
456
  this.status = '';
369
457
  }
@@ -378,6 +466,7 @@ class Session {
378
466
 
379
467
  tickProgress() {
380
468
  this.progressFrame += 1;
469
+ if (this.npm.running) this.npm.refreshRecorded();
381
470
  this.paint();
382
471
  }
383
472
 
@@ -398,6 +487,10 @@ class Session {
398
487
  try {
399
488
  if (this.nav.pane === 'repos') this.workspace.start();
400
489
  this.openLoad();
490
+ // Populate the agent model cache while the initial screen is loading.
491
+ // Opening the Agents pane can then use the cache instead of being the
492
+ // first place that starts its CLI probes.
493
+ void this.agents.loadModels();
401
494
  this.openUpdate();
402
495
  this.draw();
403
496
  await this.readLoop();