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,153 @@
1
+ 'use strict';
2
+
3
+ const { isHashObject } = require('metautil');
4
+ const { trimText } = require('../../common/utilities.js');
5
+
6
+ const models = require('../../agents/models.js');
7
+ const { catalogOf, fitEffort, withoutFast, contextMenu } = models;
8
+
9
+ const RESTORED_FIELDS = ['model', 'effort', 'context'];
10
+
11
+ const concrete = (name) => {
12
+ const text = trimText(name);
13
+ return text === 'default' ? '' : text;
14
+ };
15
+
16
+ const menuNames = (catalog) => {
17
+ const names = catalog.entries.map((entry) => concrete(entry.model));
18
+ const known = new Set(names);
19
+ const shown = names.filter((name) => {
20
+ const base = withoutFast(name);
21
+ return name && (name === base || !known.has(base));
22
+ });
23
+ return [...new Set(shown)];
24
+ };
25
+
26
+ class AgentOptions {
27
+ constructor(row) {
28
+ this.row = row;
29
+ this.catalog = catalogOf(row.models);
30
+ this.names = menuNames(this.catalog);
31
+ }
32
+
33
+ shown(model) {
34
+ const saved = concrete(model);
35
+ const base = withoutFast(saved);
36
+ if (this.names.includes(base)) return base;
37
+ if (this.names.includes(saved)) return saved;
38
+ return this.names[0] || '';
39
+ }
40
+
41
+ // What the cli lists wins over the levels the spec falls back to
42
+ levels(model) {
43
+ const resolved = this.catalog.resolve({ model, effort: 'default' });
44
+ if (resolved.encoded) return resolved.levels;
45
+ const { modelEfforts, listedEfforts, spec } = this.row;
46
+ const own = modelEfforts ? modelEfforts.get(resolved.model) : null;
47
+ const listed = own ?? listedEfforts;
48
+ return listed ? ['default', ...listed] : spec.efforts;
49
+ }
50
+
51
+ efforts(model) {
52
+ return this.levels(model).filter((level) => concrete(level));
53
+ }
54
+
55
+ contexts(model) {
56
+ return contextMenu(
57
+ model,
58
+ this.row.wide,
59
+ this.catalog,
60
+ this.row.spec.contexts,
61
+ );
62
+ }
63
+
64
+ hasFast(model) {
65
+ const base = withoutFast(concrete(model));
66
+ return Boolean(base) && this.catalog.find(`${base}-fast`) !== null;
67
+ }
68
+
69
+ firstModel(accept) {
70
+ return this.names.find(accept) ?? '';
71
+ }
72
+
73
+ fitContext(model, context) {
74
+ const want = concrete(context);
75
+ if (!want) return '';
76
+ const sizes = this.contexts(model);
77
+ return sizes.includes(want) ? want : sizes[0] || '';
78
+ }
79
+
80
+ align(picked, model = '') {
81
+ const shown = model || this.shown(picked.model);
82
+ const basis = concrete(picked.model) || shown;
83
+ picked.effort = fitEffort(this.levels(basis), picked.effort);
84
+ picked.context = this.fitContext(shown, picked.context);
85
+ const lost = this.catalog.hasFast() && !this.hasFast(shown);
86
+ if (picked.fast && lost) picked.fast = false;
87
+ return shown;
88
+ }
89
+
90
+ normalize(picked, model = '') {
91
+ picked.model = this.catalog.resolve(picked).family;
92
+ return this.align(picked, model || this.shown(picked.model));
93
+ }
94
+
95
+ preferEffort(picked, level) {
96
+ if (concrete(picked.effort)) return;
97
+ if (this.efforts(picked.model).includes(level)) picked.effort = level;
98
+ }
99
+
100
+ pickModel(picked, model) {
101
+ picked.model = model;
102
+ this.align(picked, model);
103
+ }
104
+
105
+ pickContext(picked, size) {
106
+ picked.context = size;
107
+ const offers = (model) => this.contexts(model).includes(size);
108
+ if (!offers(this.shown(picked.model))) {
109
+ picked.model = this.firstModel(offers) || picked.model;
110
+ }
111
+ const model = this.shown(picked.model);
112
+ this.align(picked, model);
113
+ if (offers(model)) picked.context = size;
114
+ }
115
+
116
+ toggleFast(picked) {
117
+ picked.fast = picked.fast !== true;
118
+ const shown = this.shown(picked.model);
119
+ const missing = this.catalog.hasFast() && !this.hasFast(shown);
120
+ if (picked.fast && missing) {
121
+ const next = this.firstModel((name) => this.hasFast(name));
122
+ if (next) picked.model = next;
123
+ else picked.fast = false;
124
+ }
125
+ this.align(picked, this.shown(picked.model));
126
+ }
127
+ }
128
+
129
+ const restoredFields = (spec) =>
130
+ spec.savesExtra ? [...RESTORED_FIELDS, 'extra'] : RESTORED_FIELDS;
131
+
132
+ const restoreChoice = (picked, saved, spec) => {
133
+ if (!isHashObject(saved)) return;
134
+ for (const field of restoredFields(spec)) {
135
+ const value = concrete(saved[field]);
136
+ if (value) picked[field] = value;
137
+ }
138
+ if (saved.fast === true) picked.fast = true;
139
+ };
140
+
141
+ const storedChoice = (picked, spec) => {
142
+ const fields = [
143
+ ['model', concrete(picked.model)],
144
+ ['effort', concrete(picked.effort)],
145
+ ['extra', spec.savesExtra ? concrete(picked.extra) : ''],
146
+ ['fast', picked.fast === true],
147
+ ['context', concrete(picked.context)],
148
+ ];
149
+ const stored = Object.fromEntries(fields.filter((field) => field[1]));
150
+ return Object.keys(stored).length ? stored : null;
151
+ };
152
+
153
+ module.exports = { AgentOptions, concrete, restoreChoice, storedChoice };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { visibleWidth } = require('../../term/ansi.js');
6
+ const { padVisible } = require('../../render/primitives.js');
7
+ const review = require('../../review/review.js');
8
+ const { parseReview, reviewProgress, frontmatterStatus } = review;
9
+ const { trimText } = require('../../common/utilities.js');
10
+
11
+ const planKey = (value) => {
12
+ const text = trimText(value);
13
+ if (!text) return '';
14
+ const gap = text.indexOf(' ');
15
+ return gap > 0 ? text.slice(0, gap) : path.basename(text);
16
+ };
17
+
18
+ const isLive = (store, file) => {
19
+ const live = store ? store.reviewPath : '';
20
+ return Boolean(live) && path.basename(live) === path.basename(file);
21
+ };
22
+
23
+ const planState = (file, store) => {
24
+ if (isLive(store, file)) {
25
+ const status = trimText(store.status) || 'editing';
26
+ return { status, ...reviewProgress(store) };
27
+ }
28
+ try {
29
+ const text = fs.readFileSync(file, 'utf8');
30
+ const parsed = parseReview(text, file);
31
+ const progress = reviewProgress(parsed);
32
+ const fallback = progress.total ? 'editing' : '';
33
+ const status = frontmatterStatus(text) ? parsed.status : fallback;
34
+ return { status, done: progress.done, total: progress.total };
35
+ } catch {
36
+ return null;
37
+ }
38
+ };
39
+
40
+ const planProgress = (file, root, store) => {
41
+ if (!file) return '';
42
+ const abs = path.isAbsolute(file) ? file : path.join(root, file);
43
+ const state = planState(abs, store);
44
+ return state && state.total ? `${state.done}/${state.total}` : '';
45
+ };
46
+
47
+ const planPart = (dir, name, store) => {
48
+ const base = planKey(name);
49
+ if (!base) return null;
50
+ const state = planState(path.join(dir, base), store);
51
+ if (!state || !state.status) return { name: base, status: '', progress: '' };
52
+ const progress = `${state.done}/${state.total}`;
53
+ return { name: base, status: state.status, progress };
54
+ };
55
+
56
+ const planLabel = (part) => {
57
+ if (!part.status) return part.name;
58
+ return `${part.name} ${part.status} ${part.progress}`;
59
+ };
60
+
61
+ const alignPlanLabels = (parts) => {
62
+ let nameW = 0;
63
+ let statusW = 0;
64
+ for (const part of parts) {
65
+ if (!part.status) continue;
66
+ nameW = Math.max(nameW, visibleWidth(part.name));
67
+ statusW = Math.max(statusW, visibleWidth(part.status));
68
+ }
69
+ return parts.map((part) => {
70
+ if (!part.status) return part.name;
71
+ const name = padVisible(part.name, nameW, 'end');
72
+ const status = padVisible(part.status, statusW, 'end');
73
+ return planLabel({ name, status, progress: part.progress });
74
+ });
75
+ };
76
+
77
+ module.exports = {
78
+ planKey,
79
+ planProgress,
80
+ planPart,
81
+ planLabel,
82
+ alignPlanLabels,
83
+ };
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const ops = require('./ops.js');
4
- const { pickRepoWrite } = ops;
3
+ const { clamp } = require('../common/utilities.js');
5
4
 
6
5
  class BranchController {
7
6
  constructor(ui) {
@@ -22,10 +21,10 @@ class BranchController {
22
21
  return this.ui.nav.pane === 'branches' && this.ui.commits.canCommit();
23
22
  }
24
23
 
25
- loadBranches() {
24
+ async loadBranches() {
26
25
  const ui = this.ui;
27
26
  try {
28
- this.branches = ui.repo.listBranches(ui.top);
27
+ this.branches = await ui.repo.listBranches(ui.top);
29
28
  return true;
30
29
  } catch (error) {
31
30
  ui.status = error.message;
@@ -33,14 +32,32 @@ class BranchController {
33
32
  }
34
33
  }
35
34
 
35
+ async refresh() {
36
+ if (!this.ui.repo.listBranches) return;
37
+ const loaded = await this.loadBranches();
38
+ if (!loaded) return;
39
+ const last = this.branches.length - 1;
40
+ const nav = this.ui.nav;
41
+ if (nav.branchCursor > last) nav.branchCursor = Math.max(0, last);
42
+ }
43
+
36
44
  refreshAfterWrite(extra = {}) {
37
- if (this.ui.repo.listBranches && this.loadBranches()) {
38
- const last = this.branches.length - 1;
39
- const nav = this.ui.nav;
40
- if (nav.branchCursor > last) nav.branchCursor = Math.max(0, last);
41
- }
42
- this.ui.commits.refresh();
43
- this.ui.refreshFromRepo({ keepEmpty: true, ...extra });
45
+ const ui = this.ui;
46
+ ui.ops.hold(Promise.all([this.refresh(), ui.commits.refresh()]));
47
+ ui.refreshFromRepo({ keepEmpty: true, ...extra });
48
+ }
49
+
50
+ async open() {
51
+ const ui = this.ui;
52
+ const loaded = await this.loadBranches();
53
+ if (!loaded) return;
54
+ const current = this.branches.findIndex((entry) => entry.current);
55
+ const nav = ui.nav;
56
+ nav.branchCursor = current < 0 ? 0 : current;
57
+ nav.pane = 'branches';
58
+ nav.resetListScroll('branches');
59
+ ui.status = '';
60
+ nav.clearSelection();
44
61
  }
45
62
 
46
63
  runWrite(name, args, kind, done) {
@@ -56,14 +73,7 @@ class BranchController {
56
73
  ui.status = 'read only';
57
74
  return;
58
75
  }
59
- if (!this.loadBranches()) return;
60
- const current = this.branches.findIndex((entry) => entry.current);
61
- const nav = ui.nav;
62
- nav.branchCursor = current < 0 ? 0 : current;
63
- nav.pane = 'branches';
64
- nav.resetListScroll('branches');
65
- ui.status = '';
66
- nav.clearSelection();
76
+ ui.ops.hold(this.open());
67
77
  }
68
78
 
69
79
  onBranchMove(delta) {
@@ -73,7 +83,7 @@ class BranchController {
73
83
  return;
74
84
  }
75
85
  const last = this.branches.length - 1;
76
- const next = Math.min(last, Math.max(0, nav.branchCursor + delta));
86
+ const next = clamp(nav.branchCursor + delta, 0, last);
77
87
  if (next === nav.branchCursor) return;
78
88
  nav.branchCursor = next;
79
89
  this.ui.status = '';
@@ -159,25 +169,9 @@ class BranchController {
159
169
  if (!ui.commits.canCommit()) return;
160
170
  const done = force ? 'force pushed' : 'pushed';
161
171
  const kind = force ? 'force pushing' : 'pushing';
162
- const picked = pickRepoWrite(ui.repo, 'push', ui.uiOpen);
163
- if (picked.async) {
164
- const write = (top) => picked.write(top, force);
165
- const after = () => this.refreshAfterWrite();
166
- return void ui.ops.runBusy(kind, done, write, after);
167
- }
168
- try {
169
- ui.repo.push(ui.top, force);
170
- } catch (error) {
171
- if (error.rejected && !force) {
172
- ui.mode = 'confirmPush';
173
- ui.status = '';
174
- return;
175
- }
176
- ui.status = error.message;
177
- return;
178
- }
179
- this.refreshAfterWrite();
180
- ui.status = done;
172
+ const write = (top) => ui.repo.push(top, force);
173
+ const after = () => this.refreshAfterWrite({ doneStatus: done });
174
+ ui.ops.runBusy(kind, done, write, after);
181
175
  }
182
176
 
183
177
  onPush() {
@@ -2,13 +2,12 @@
2
2
 
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
+ const { withNewline } = require('../common/utilities.js');
5
6
 
6
- const files = require('../files.js');
7
+ const files = require('../common/files.js');
7
8
  const { listPath, isTaskItem, isTasksEntry, isReadOnlyOrigin } = files;
8
- const items = require('./items.js');
9
- const { needsReload, npmBusyKind, sameBlock } = items;
10
- const diff = require('../diff/diff.js');
11
- const { hunkKey } = diff;
9
+ const { needsReload, npmBusyKind, sameBlock } = require('./items.js');
10
+ const { hunkKey } = require('../diff/diff.js');
12
11
 
13
12
  const nextUnstageOrigin = (item) =>
14
13
  item.file.isNew === true ? 'untracked' : 'unstaged';
@@ -39,7 +38,7 @@ const appendIgnoreFile = (top, name, pattern) => {
39
38
  const text = exists ? fs.readFileSync(abs, 'utf8') : '';
40
39
  const lines = text.split('\n');
41
40
  if (lines.some((line) => line.trim() === pattern)) return false;
42
- const base = text && !text.endsWith('\n') ? `${text}\n` : text;
41
+ const base = withNewline(text);
43
42
  fs.writeFileSync(abs, `${base}${pattern}\n`);
44
43
  return true;
45
44
  };
@@ -116,26 +115,17 @@ const selectNextFileAfter = (ui, rel) => {
116
115
  nav.index = entry.firstIndex;
117
116
  };
118
117
 
119
- const liveItem = (ui, item) =>
120
- ui.items.find((entry) => sameBlock(entry, item)) ?? item;
121
-
122
- const applyGit = (ui, label, write, after) => {
123
- try {
124
- write(ui.top);
125
- ui.status = label;
126
- after();
127
- } catch (error) {
128
- ui.status = error.message;
129
- }
118
+ const liveItem = (ui, item) => {
119
+ const match = (entry) => sameBlock(entry, item);
120
+ return ui.items.find(match) ?? item;
130
121
  };
131
122
 
132
- const applyFileGit = (ui, targets, label, write, afterItem = () => {}) => {
123
+ const applyFileGit = async (ui, targets, label, write) => {
133
124
  let written = 0;
134
125
  try {
135
126
  for (const target of targets) {
136
- const current = liveItem(ui, target);
137
- write(ui.top, current);
138
- afterItem(current);
127
+ const item = liveItem(ui, target);
128
+ await ui.ops.exclusive((top) => write(top, item));
139
129
  written++;
140
130
  }
141
131
  ui.status = label;
@@ -146,12 +136,6 @@ const applyFileGit = (ui, targets, label, write, afterItem = () => {}) => {
146
136
  }
147
137
  };
148
138
 
149
- const reconcilePartial = (ui, result) => {
150
- if (result.ok) return false;
151
- if (result.written) ui.reloadAfterChange();
152
- return true;
153
- };
154
-
155
139
  const enqueueOrigins = (ui, targets, spec) => {
156
140
  const writes = targets.map((target) => {
157
141
  const item = liveItem(ui, target);
@@ -164,36 +148,31 @@ const enqueueOrigins = (ui, targets, spec) => {
164
148
  kind: spec.busy || spec.done,
165
149
  done: spec.done,
166
150
  reload: spec.reload === true,
167
- write: async (top) => {
168
- for (const item of writes) await spec.write(top, item);
151
+ write: async () => {
152
+ for (const item of writes) await spec.write(item);
169
153
  },
170
154
  });
171
155
  };
172
156
 
173
157
  const stageLazy = (ui, targets, extra = {}) => {
174
158
  const busy = extra.busy || '';
175
- const repo = ui.repo;
176
159
  enqueueOrigins(ui, targets, {
177
160
  done: 'staged',
178
161
  busy,
179
162
  reload: extra.reload === true,
180
163
  originOf: () => 'staged',
181
164
  afterUi: extra.afterUi,
182
- write: (top, item) => {
183
- if (busy && repo.addAsync) return repo.addAsync(top, item);
184
- return repo.add(top, item);
185
- },
165
+ write: (item) => ui.ops.add(item),
186
166
  });
187
167
  };
188
168
 
189
169
  const unstageLazy = (ui, targets, extra = {}) => {
190
- const repo = ui.repo;
191
170
  enqueueOrigins(ui, targets, {
192
171
  done: 'unstaged',
193
172
  busy: '',
194
173
  originOf: nextUnstageOrigin,
195
174
  afterUi: extra.afterUi,
196
- write: (top, item) => repo.unstage(top, item),
175
+ write: (item) => ui.ops.unstage(item),
197
176
  });
198
177
  };
199
178
 
@@ -205,41 +184,18 @@ const dismissReloadable = (ui, targets) => {
205
184
 
206
185
  const onBlockAdd = (ui, item) => {
207
186
  const reload = needsReload(item);
208
- if (ui.uiOpen) {
209
- return void stageLazy(ui, [item], { busy: npmBusyKind(item), reload });
210
- }
211
- applyGit(
212
- ui,
213
- 'staged',
214
- (top) => ui.repo.add(top, item),
215
- () => {
216
- if (reload) ui.reloadAfterChange();
217
- else keepOrigin(ui, item, 'staged');
218
- },
219
- );
187
+ stageLazy(ui, [item], { busy: npmBusyKind(item), reload });
220
188
  };
221
189
 
222
- const onBlockUnstage = (ui, item) => {
223
- if (ui.uiOpen) return void unstageLazy(ui, [item]);
224
- const origin = nextUnstageOrigin(item);
225
- applyGit(
226
- ui,
227
- 'unstaged',
228
- (top) => ui.repo.unstage(top, item),
229
- () => keepOrigin(ui, item, origin),
230
- );
231
- };
190
+ const onBlockUnstage = (ui, item) => unstageLazy(ui, [item]);
232
191
 
233
192
  const onBlockRevert = (ui, item) => {
234
- applyGit(
235
- ui,
236
- 'dropped',
237
- (top) => ui.repo.revert(top, item),
238
- () => {
239
- dismissReloadable(ui, [item]);
240
- ui.reloadAfterChange();
241
- },
242
- );
193
+ const write = (top) => ui.repo.revert(top, item);
194
+ const after = () => {
195
+ dismissReloadable(ui, [item]);
196
+ ui.reloadAfterChange();
197
+ };
198
+ ui.ops.track(ui.ops.apply('dropped', write, after));
243
199
  };
244
200
 
245
201
  const BLOCK = {
@@ -263,48 +219,26 @@ const onBlockCommand = (ui, name) => {
263
219
  const stageTargets = (ui, targets, after = null) => {
264
220
  const reload = targets.some(needsReload);
265
221
  const busy = targets.map(npmBusyKind).find((kind) => kind) || '';
266
- if (ui.uiOpen) {
267
- return void stageLazy(ui, targets, { busy, reload, afterUi: after });
268
- }
269
- const keepStaged = (entry) => keepOrigin(ui, entry, 'staged');
270
- const result = applyFileGit(ui, targets, 'staged', ui.repo.add, keepStaged);
271
- if (reconcilePartial(ui, result)) return;
272
- if (reload) ui.reloadAfterChange();
273
- if (after) after();
222
+ stageLazy(ui, targets, { busy, reload, afterUi: after });
274
223
  };
275
224
 
276
- const unstageTargets = (ui, targets, after = null) => {
277
- if (ui.uiOpen) return void unstageLazy(ui, targets, { afterUi: after });
278
- const keepUnstaged = (entry) =>
279
- keepOrigin(ui, entry, nextUnstageOrigin(entry));
280
- const result = applyFileGit(
281
- ui,
282
- targets,
283
- 'unstaged',
284
- ui.repo.unstage,
285
- keepUnstaged,
286
- );
287
- if (reconcilePartial(ui, result)) return;
288
- if (after) after();
289
- };
225
+ const unstageTargets = (ui, targets, after = null) =>
226
+ unstageLazy(ui, targets, { afterUi: after });
290
227
 
291
228
  const revertTargets = (ui, targets) => {
292
229
  dismissReloadable(ui, targets);
293
- const result = applyFileGit(ui, targets, 'dropped', ui.repo.revert);
294
- if (!result.ok && !result.written) return;
295
- ui.reloadAfterChange();
230
+ const write = (top, item) => ui.repo.revert(top, item);
231
+ const done = (result) => {
232
+ if (result.ok || result.written) ui.reloadAfterChange();
233
+ };
234
+ ui.ops.track(applyFileGit(ui, targets, 'dropped', write).then(done));
296
235
  };
297
236
 
298
237
  const revertFile = (ui, rel, selected, after) => {
299
238
  dismissReloadable(ui, selected);
300
- try {
301
- ui.repo.revertFile(ui.top, rel, selected);
302
- ui.status = 'dropped';
303
- } catch (error) {
304
- ui.status = error.message;
305
- return void ui.reloadAfterChange();
306
- }
307
- ui.reloadAfterChange(after);
239
+ const write = (top) => ui.repo.revertFile(top, rel, selected);
240
+ const done = (ok) => ui.reloadAfterChange(ok ? after : undefined);
241
+ ui.ops.track(ui.ops.apply('dropped', write).then(done));
308
242
  };
309
243
 
310
244
  const TARGETS = {