reslop 0.2.6 → 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 +27 -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 +61 -66
  10. package/lib/{files.js → common/files.js} +51 -52
  11. package/lib/common/find.js +61 -0
  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 +117 -51
  18. package/lib/dashboard/npm.js +133 -32
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +41 -31
  21. package/lib/dashboard/workspace.js +151 -0
  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 -95
  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 +240 -71
  45. package/lib/{editor.js → input/editor.js} +7 -8
  46. package/lib/{keys.js → input/keys.js} +36 -39
  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/dashboard/blocks.js +490 -0
  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 +22 -8
  58. package/lib/render/footer.js +383 -0
  59. package/lib/render/frame.js +127 -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 +29 -13
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +89 -0
  66. package/lib/render/primitives.js +109 -16
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +214 -0
  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} +19 -61
  72. package/lib/render/tiles.js +203 -47
  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 +36 -44
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +122 -56
  100. package/lib/session/compose.js +197 -146
  101. package/lib/session/dashboard.js +183 -37
  102. package/lib/session/dispatch.js +120 -22
  103. package/lib/session/files-pane.js +60 -24
  104. package/lib/session/find.js +81 -0
  105. package/lib/session/import.js +82 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +96 -66
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +8 -3
  110. package/lib/session/npm/history.js +249 -0
  111. package/lib/session/npm/npm.js +815 -0
  112. package/lib/session/npm/packages.js +150 -0
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +216 -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} +166 -59
  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 +72 -59
  126. package/lib/session/workspace.js +416 -0
  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 -505
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -437
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -628
  148. package/lib/render/dash-blocks.js +0 -245
  149. package/lib/render/present.js +0 -19
  150. package/lib/render/report.js +0 -43
  151. package/lib/review.js +0 -744
  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,112 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { isHashObject } = require('metautil');
6
+ const { THEME_NAMES } = require('../term/ansi.js');
7
+ const { REVIEW_DIR } = require('../common/files.js');
8
+ const { readJson, writeJson } = require('../common/utilities.js');
9
+
10
+ const PREFS = '.reslop';
11
+
12
+ const LAYOUT_LABEL = {
13
+ unified: 'unified',
14
+ mixed: 'mixed',
15
+ side: 'side-by-side',
16
+ };
17
+
18
+ const LAYOUT_VALUE = {
19
+ unified: 'unified',
20
+ mixed: 'mixed',
21
+ 'side-by-side': 'side',
22
+ };
23
+
24
+ const prefsFile = (root) => path.join(root, PREFS);
25
+
26
+ const planPath = (root, name) => path.join(root, REVIEW_DIR, name);
27
+
28
+ const readPrefs = (root) => {
29
+ const data = readJson(prefsFile(root));
30
+ if (!isHashObject(data)) return { agents: {} };
31
+ const agents = isHashObject(data.agents) ? data.agents : {};
32
+ return { ...data, agents };
33
+ };
34
+
35
+ const writePrefs = (root, data) => {
36
+ const stored = { ...data };
37
+ delete stored.runs;
38
+ try {
39
+ fs.writeFileSync(prefsFile(root), `${JSON.stringify(stored, null, 2)}\n`);
40
+ } catch {
41
+ // The repo root can be read-only.
42
+ }
43
+ };
44
+
45
+ const section = (data, name) => {
46
+ const value = data[name];
47
+ if (!isHashObject(value)) return {};
48
+ return { ...value };
49
+ };
50
+
51
+ const readUiPrefs = (root) => {
52
+ const data = readPrefs(root);
53
+ const editor = section(data, 'editor');
54
+ const general = section(data, 'general');
55
+ const commits = section(data, 'commits');
56
+ let lineNumbers = false;
57
+ if (typeof editor.lineNumbers === 'boolean') {
58
+ lineNumbers = editor.lineNumbers;
59
+ } else if (typeof data.lineNumbers === 'boolean') {
60
+ lineNumbers = data.lineNumbers;
61
+ }
62
+ const layout = LAYOUT_VALUE[editor.mode] || 'unified';
63
+ const theme = THEME_NAMES.includes(general.theme) ? general.theme : 'dark';
64
+ const commitView = commits.view === 'full' ? 'full' : 'brief';
65
+ return { lineNumbers, layout, theme, commitView };
66
+ };
67
+
68
+ const saveUiPrefs = (root, patch) => {
69
+ if (!root) return;
70
+ const data = readPrefs(root);
71
+ const editor = section(data, 'editor');
72
+ const legacy = typeof data.lineNumbers === 'boolean';
73
+ if (legacy && editor.lineNumbers === undefined) {
74
+ editor.lineNumbers = data.lineNumbers;
75
+ }
76
+ delete data.lineNumbers;
77
+ if (patch.lineNumbers !== undefined) {
78
+ editor.lineNumbers = patch.lineNumbers === true;
79
+ }
80
+ if (patch.layout) editor.mode = LAYOUT_LABEL[patch.layout] || 'unified';
81
+ const next = { ...data, editor };
82
+ if (patch.theme) {
83
+ const general = section(data, 'general');
84
+ general.theme = patch.theme;
85
+ next.general = general;
86
+ }
87
+ if (patch.commitView) {
88
+ const commits = section(data, 'commits');
89
+ const full = patch.commitView === 'full';
90
+ commits.view = full ? 'full' : 'brief';
91
+ next.commits = commits;
92
+ }
93
+ writePrefs(root, next);
94
+ };
95
+
96
+ const writeStored = (file, data) => {
97
+ try {
98
+ writeJson(file, data);
99
+ } catch {
100
+ // The plan directory can be read-only.
101
+ }
102
+ };
103
+
104
+ module.exports = {
105
+ LAYOUT_LABEL,
106
+ planPath,
107
+ readPrefs,
108
+ writePrefs,
109
+ readUiPrefs,
110
+ saveUiPrefs,
111
+ writeStored,
112
+ };
@@ -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,53 +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 accessors = require('./session/accessors.js');
40
- const { bindAccessors, bindGetters } = accessors;
41
- const view = require('./session/view.js');
42
- const filesPane = require('./session/files-pane.js');
43
- const lifecycle = require('./session/lifecycle.js');
44
- const { Lifecycle } = lifecycle;
45
- 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');
46
27
  const { dispatchAction, confirmChoice, handleEvent } = dispatch;
47
- const dashboard = require('./session/dashboard.js');
48
- const { Dashboard } = dashboard;
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
+ };
49
53
 
50
54
  const PROGRESS_MS = 160;
55
+ const QUIT_WAIT_MS = 3000;
51
56
 
52
57
  const NAV_FIELDS = [
53
58
  'pane',
@@ -56,6 +61,8 @@ const NAV_FIELDS = [
56
61
  'branchCursor',
57
62
  'commitCursor',
58
63
  'npmCursor',
64
+ 'packagesCursor',
65
+ 'agentCursor',
59
66
  'scroll',
60
67
  'listScroll',
61
68
  'reviewPath',
@@ -98,6 +105,13 @@ const assignOptions = (session, options) => {
98
105
  session.auditMap = options.auditMap;
99
106
  session.updateOpts = options.update;
100
107
  session.dashboardHome = options.startPane === 'dashboard';
108
+ session.workspaceRoot = options.workspace ? options.workspace.root : '';
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;
101
115
  };
102
116
 
103
117
  const assignParts = (session, options) => {
@@ -112,10 +126,14 @@ const assignParts = (session, options) => {
112
126
  session.updater = new UpdateCoordinator(session);
113
127
  session.composer = new Composer(session);
114
128
  session.ops = new OpsRunner(session);
129
+ session.reads = new FileReads(session);
115
130
  session.commits = new CommitController(session);
116
131
  session.gitBranches = new BranchController(session);
117
132
  session.npm = new NpmController(session);
133
+ session.packages = new PackageController(session);
134
+ session.agents = new AgentsController(session);
118
135
  session.dashboard = new Dashboard(session);
136
+ session.workspace = new Workspace(session);
119
137
  session.lifecycle = new Lifecycle(session);
120
138
  };
121
139
 
@@ -156,17 +174,22 @@ class Session {
156
174
  this.review.reset();
157
175
  this.composer.reset();
158
176
  this.ops.reset();
177
+ this.reads.reset();
159
178
  this.gitBranches.reset();
160
179
  this.commits.reset();
161
180
  this.npm.reset();
181
+ this.agents.reset();
162
182
  this.dashboard.reset();
183
+ this.workspace.reset();
163
184
  this.top = this.cwd;
164
185
  this.branch = '';
165
186
  this.status = '';
166
187
  this.layout = 'unified';
188
+ this.lineNumbers = false;
167
189
  this.done = false;
168
190
  this.exitCode = 0;
169
191
  this.mode = 'review';
192
+ this.quitWarning = '';
170
193
  this.tasksItem = makeTaskItem();
171
194
  this.progressFrame = 0;
172
195
  this.emptyReview = false;
@@ -181,13 +204,13 @@ class Session {
181
204
  this.progress.clear();
182
205
  }
183
206
 
184
- ensureRepo() {
207
+ async ensureRepo() {
185
208
  if (!this.repo.toplevel) return;
186
- this.top = this.repo.toplevel(this.cwd);
209
+ this.top = await this.repo.toplevel(this.cwd);
187
210
  }
188
211
 
189
212
  load() {
190
- this.lifecycle.load();
213
+ return this.lifecycle.load();
191
214
  }
192
215
 
193
216
  loadReady() {
@@ -225,6 +248,21 @@ class Session {
225
248
  if (LIST_PANES.includes(this.pane)) {
226
249
  this.nav.listScroll = frame.listScroll;
227
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
+ }
228
266
  if (this.nav.tasksOpen) this.nav.scroll = frame.scroll;
229
267
  const editScroll = frame.cursor && frame.cursor.scroll;
230
268
  if (this.editor && typeof editScroll === 'number') {
@@ -234,6 +272,7 @@ class Session {
234
272
  this.term.paint(frame, size, composing ? frame.cursor : null);
235
273
  this.scheduleAutosave();
236
274
  this.dashboard.afterDraw();
275
+ this.workspace.afterDraw();
237
276
  }
238
277
 
239
278
  scheduleAutosave() {
@@ -284,7 +323,11 @@ class Session {
284
323
  }
285
324
 
286
325
  handleEvent(event) {
287
- handleEvent(this, event);
326
+ if (!this.ops.defer(event)) handleEvent(this, event);
327
+ }
328
+
329
+ finishInput() {
330
+ if (this.finishReadLoop) this.finishReadLoop();
288
331
  }
289
332
 
290
333
  pushInput(text) {
@@ -321,6 +364,7 @@ class Session {
321
364
  if (result.kind === 'commit') this.commits.finishGitCommit();
322
365
  if (result.kind === 'branch') this.gitBranches.finishCreateBranch();
323
366
  if (result.kind === 'npm') this.npm.finishEdit();
367
+ if (result.kind === 'package') this.packages.finishAdd();
324
368
  }
325
369
 
326
370
  selectTemplate(index) {
@@ -331,28 +375,83 @@ class Session {
331
375
  this.composer.autosave();
332
376
  }
333
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
+
334
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
+ }
335
434
  if (this.mode === 'compose') {
336
- this.composer.commitCompose();
337
- this.composer.closeCompose();
435
+ composer.commitCompose();
436
+ composer.closeCompose();
338
437
  }
339
- if (hasNotes(this.notes)) {
438
+ const warning = this.childWarning();
439
+ if (warning) {
440
+ this.quitWarning = warning;
340
441
  this.mode = 'confirmQuit';
341
442
  this.status = '';
342
443
  return;
343
444
  }
344
- this.done = true;
345
- this.exitCode = 0;
445
+ this.exitNow();
346
446
  }
347
447
 
348
- finishQuit(status) {
349
- const result = this.review.finishQuit(status);
350
- if (!result.ok) this.status = result.error.message;
351
- this.done = true;
352
- this.exitCode = 0;
448
+ confirmQuit() {
449
+ this.stopChildren();
450
+ this.exitNow();
353
451
  }
354
452
 
355
453
  cancelQuit() {
454
+ this.quitWarning = '';
356
455
  this.mode = 'review';
357
456
  this.status = '';
358
457
  }
@@ -367,6 +466,7 @@ class Session {
367
466
 
368
467
  tickProgress() {
369
468
  this.progressFrame += 1;
469
+ if (this.npm.running) this.npm.refreshRecorded();
370
470
  this.paint();
371
471
  }
372
472
 
@@ -385,7 +485,12 @@ class Session {
385
485
  this.uiOpen = true;
386
486
  this.term.enter();
387
487
  try {
488
+ if (this.nav.pane === 'repos') this.workspace.start();
388
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();
389
494
  this.openUpdate();
390
495
  this.draw();
391
496
  await this.readLoop();
@@ -412,6 +517,7 @@ class Session {
412
517
  settled = true;
413
518
  this.lifecycle.stopWatch();
414
519
  this.dashboard.stop();
520
+ this.workspace.stop();
415
521
  this.term.stopListening();
416
522
  this.progress.clear();
417
523
  this.term.clearTimers();
@@ -431,6 +537,7 @@ class Session {
431
537
  this.term.startListening({ onData, onResize: () => this.draw() });
432
538
  this.lifecycle.startWatch();
433
539
  if (this.dashboardHome) this.dashboard.start();
540
+ if (this.nav.pane === 'repos') this.workspace.start();
434
541
  });
435
542
  }
436
543
  }