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,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,23 @@ 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 = '';
186
+ this.repositoryUrl = '';
174
187
  this.status = '';
175
188
  this.layout = 'unified';
189
+ this.lineNumbers = false;
176
190
  this.done = false;
177
191
  this.exitCode = 0;
178
192
  this.mode = 'review';
193
+ this.quitWarning = '';
179
194
  this.tasksItem = makeTaskItem();
180
195
  this.progressFrame = 0;
181
196
  this.emptyReview = false;
@@ -190,13 +205,13 @@ class Session {
190
205
  this.progress.clear();
191
206
  }
192
207
 
193
- ensureRepo() {
208
+ async ensureRepo() {
194
209
  if (!this.repo.toplevel) return;
195
- this.top = this.repo.toplevel(this.cwd);
210
+ this.top = await this.repo.toplevel(this.cwd);
196
211
  }
197
212
 
198
213
  load() {
199
- this.lifecycle.load();
214
+ return this.lifecycle.load();
200
215
  }
201
216
 
202
217
  loadReady() {
@@ -234,6 +249,21 @@ class Session {
234
249
  if (LIST_PANES.includes(this.pane)) {
235
250
  this.nav.listScroll = frame.listScroll;
236
251
  }
252
+ const menuScroll = frame.menuScroll;
253
+ if (this.agents.pick && typeof menuScroll === 'number') {
254
+ this.agents.pick.scroll = menuScroll;
255
+ }
256
+ const planPick = this.composer.tasks.planPick;
257
+ if (planPick && typeof menuScroll === 'number') {
258
+ planPick.scroll = menuScroll;
259
+ }
260
+ if (typeof frame.runScroll === 'number') {
261
+ const panel = this.pane === 'npm' ? this.npm : this.agents;
262
+ panel.runScroll = frame.runScroll;
263
+ }
264
+ if (typeof frame.gridCols === 'number') {
265
+ this.workspace.cols = frame.gridCols;
266
+ }
237
267
  if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
238
268
  const editScroll = frame.cursor && frame.cursor.scroll;
239
269
  if (this.editor && typeof editScroll === 'number') {
@@ -294,7 +324,11 @@ class Session {
294
324
  }
295
325
 
296
326
  handleEvent(event) {
297
- handleEvent(this, event);
327
+ if (!this.ops.defer(event)) handleEvent(this, event);
328
+ }
329
+
330
+ finishInput() {
331
+ if (this.finishReadLoop) this.finishReadLoop();
298
332
  }
299
333
 
300
334
  pushInput(text) {
@@ -342,28 +376,84 @@ class Session {
342
376
  this.composer.autosave();
343
377
  }
344
378
 
379
+ retarget({ cwd, top, repoName, pane }) {
380
+ this.lifecycle.stopWatch();
381
+ this.dashboard.stop();
382
+ this.loader.reset();
383
+ this.cwd = cwd;
384
+ if (top) this.top = top;
385
+ this.paths = [];
386
+ this.repoName = repoName;
387
+ this.branch = '';
388
+ this.repositoryUrl = '';
389
+ this.rev = null;
390
+ this.revShort = '';
391
+ this.change = null;
392
+ this.dashboardHome = pane === 'dashboard';
393
+ this.collection.reset();
394
+ this.reads.reset();
395
+ this.review.reset();
396
+ this.nav.pane = pane;
397
+ this.nav.tasksOpen = false;
398
+ this.nav.scroll = 0;
399
+ this.nav.clearSelection();
400
+ this.status = '';
401
+ }
402
+
403
+ childWarning() {
404
+ const tests = this.npm && this.npm.running === true;
405
+ const agents = this.agents && this.agents.runningCount() > 0;
406
+ if (tests && agents) return ' exit will terminate tests and agents';
407
+ if (agents) return ' exit will terminate agents';
408
+ if (tests) return ' exit will terminate tests';
409
+ return '';
410
+ }
411
+
412
+ stopChildren() {
413
+ if (this.npm) this.npm.stopAll();
414
+ if (this.agents) this.agents.killAll();
415
+ }
416
+
417
+ // A quick repo write still running lands first, so it is not left half done
418
+ exitNow() {
419
+ if (this.done || !this.ops.unlanded()) return void this.finishExit();
420
+ const exit = () => this.finishExit();
421
+ this.ops.hold(this.ops.landed(QUIT_WAIT_MS).then(exit));
422
+ }
423
+
424
+ finishExit() {
425
+ this.flushReview();
426
+ this.done = true;
427
+ this.exitCode = 0;
428
+ }
429
+
345
430
  onQuit() {
431
+ const composer = this.composer;
432
+ if (this.mode === 'compose' && composer.writesDisk()) {
433
+ const quit = () => this.onQuit();
434
+ return void this.ops.hold(composer.writeAndClose().then(quit));
435
+ }
346
436
  if (this.mode === 'compose') {
347
- this.composer.commitCompose();
348
- this.composer.closeCompose();
437
+ composer.commitCompose();
438
+ composer.closeCompose();
349
439
  }
350
- if (hasNotes(this.notes)) {
440
+ const warning = this.childWarning();
441
+ if (warning) {
442
+ this.quitWarning = warning;
351
443
  this.mode = 'confirmQuit';
352
444
  this.status = '';
353
445
  return;
354
446
  }
355
- this.done = true;
356
- this.exitCode = 0;
447
+ this.exitNow();
357
448
  }
358
449
 
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;
450
+ confirmQuit() {
451
+ this.stopChildren();
452
+ this.exitNow();
364
453
  }
365
454
 
366
455
  cancelQuit() {
456
+ this.quitWarning = '';
367
457
  this.mode = 'review';
368
458
  this.status = '';
369
459
  }
@@ -378,6 +468,7 @@ class Session {
378
468
 
379
469
  tickProgress() {
380
470
  this.progressFrame += 1;
471
+ if (this.npm.running) this.npm.refreshRecorded();
381
472
  this.paint();
382
473
  }
383
474
 
@@ -398,6 +489,10 @@ class Session {
398
489
  try {
399
490
  if (this.nav.pane === 'repos') this.workspace.start();
400
491
  this.openLoad();
492
+ // Populate the agent model cache while the initial screen is loading.
493
+ // Opening the Agents pane can then use the cache instead of being the
494
+ // first place that starts its CLI probes.
495
+ void this.agents.loadModels();
401
496
  this.openUpdate();
402
497
  this.draw();
403
498
  await this.readLoop();