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
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const actions = require('../session/actions.js');
3
+ const { clamp, trimText } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { LIST_PANES } = require('../input/actions.js');
5
6
 
6
7
  const { THEME, paint, visibleWidth, graphemes, graphemeWidth } = ansi;
7
- const { LIST_PANES } = actions;
8
8
 
9
9
  const GUTTER_W = 2;
10
10
  const FILE_MARK = '▶';
@@ -17,12 +17,14 @@ const fill = (width, fgRgb, bgRgb, color) =>
17
17
 
18
18
  const markPrefix = (selected) => ` ${selected ? FILE_MARK : ' '} `;
19
19
 
20
- const isListPane = (pane) =>
21
- LIST_PANES.includes(pane) || pane === 'dashboard' || pane === 'repos';
20
+ const isListPane = (pane) => {
21
+ if (LIST_PANES.includes(pane)) return true;
22
+ return pane === 'dashboard' || pane === 'repos';
23
+ };
22
24
 
23
25
  const isTaskView = (view) => view.item?.origin === 'task';
24
26
 
25
- const branchLabel = (name) => `${name ?? ''}`.trim();
27
+ const branchLabel = (name) => trimText(name);
26
28
 
27
29
  const padVisible = (text, width, align) => {
28
30
  const gap = ' '.repeat(Math.max(0, width - visibleWidth(text)));
@@ -46,6 +48,11 @@ const paintBodyFill = (width, color, kind) => {
46
48
  return ctx(leftW) + paintSplitRule(color) + ctx(rightW);
47
49
  };
48
50
 
51
+ const lazyRows = (length, paint) => ({ length, paint });
52
+
53
+ const bodyRow = (body, index) =>
54
+ Array.isArray(body) ? body[index] : body.paint(index);
55
+
49
56
  const paneResult = (fields = {}) => ({
50
57
  body: fields.body ?? [],
51
58
  fileHits: fields.fileHits ?? [],
@@ -55,6 +62,7 @@ const paneResult = (fields = {}) => ({
55
62
  splitBody: fields.splitBody === true,
56
63
  listScroll: fields.listScroll ?? 0,
57
64
  editHits: fields.editHits ?? [],
65
+ scrollBar: fields.scrollBar ?? null,
58
66
  });
59
67
 
60
68
  const editSpan = (x0, textX, x1, start, text) => ({
@@ -79,7 +87,7 @@ const listWindowStart = (focus, offset, listH, count) => {
79
87
  let start = offset ?? 0;
80
88
  if (focus < start) start = focus;
81
89
  if (focus >= start + listH) start = focus - listH + 1;
82
- return Math.max(0, Math.min(start, count - listH));
90
+ return clamp(start, 0, count - listH);
83
91
  };
84
92
 
85
93
  const rowCursor = (cursor, row) => {
@@ -90,10 +98,11 @@ const rowCursor = (cursor, row) => {
90
98
 
91
99
  const paintListWindow = (count, focus, pane, lineAt) => {
92
100
  const { width, color, bodyH, headerLines, offset } = pane;
93
- const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
101
+ const padded = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
102
+ const pad = pane.pad === 0 ? 0 : padded;
94
103
  const listH = bodyH - pad * 2;
95
104
  const start = listWindowStart(focus, offset, listH, count);
96
- const blank = paintBodyFill(width, color, 'files');
105
+ const blank = pane.blank ?? paintBodyFill(width, color, 'files');
97
106
  const body = pad ? [blank] : [];
98
107
  const fileHits = [];
99
108
  const editHits = [];
@@ -105,7 +114,11 @@ const paintListWindow = (count, focus, pane, lineAt) => {
105
114
  if (line.cursor) cursor = rowCursor(line.cursor, row);
106
115
  for (const edit of line.edits) editHits.push({ ...edit, row });
107
116
  if (line.hit) {
108
- fileHits.push({ y: headerLines + pad + i + 1, cursor: line.index });
117
+ const y = headerLines + pad + i + 1;
118
+ for (const span of line.spans ?? []) {
119
+ fileHits.push({ y, cursor: line.index, ...span });
120
+ }
121
+ fileHits.push({ y, cursor: line.index });
109
122
  }
110
123
  }
111
124
  if (pad) body.push(blank);
@@ -116,7 +129,15 @@ const insertedAt = (extra, count) => {
116
129
  if (extra.at === 'start') return 0;
117
130
  if (extra.at !== 'index') return count;
118
131
  const index = extra.index ?? count;
119
- return Math.max(0, Math.min(index, count));
132
+ return clamp(index, 0, count);
133
+ };
134
+
135
+ const lineFromPaint = (painted, index) => {
136
+ if (typeof painted === 'string') return listLine(painted, index);
137
+ const line = listLine(painted.row, index);
138
+ if (painted.hit === false) line.hit = false;
139
+ if (painted.spans) line.spans = painted.spans;
140
+ return line;
120
141
  };
121
142
 
122
143
  const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
@@ -124,7 +145,7 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
124
145
  const entry = items[index];
125
146
  if (!entry) return null;
126
147
  const selected = !extra && index === cursor;
127
- return listLine(paintRow(entry, selected, index), index);
148
+ return lineFromPaint(paintRow(entry, selected, index), index);
128
149
  };
129
150
  if (!extra) return paintListWindow(items.length, cursor, pane, lineAt);
130
151
  const edit = listLine(extra.row, -1, extra.cursor, extra.edits);
@@ -134,7 +155,11 @@ const paintCursorList = (items, cursor, pane, paintRow, extra = null) => {
134
155
  return paintListWindow(items.length, at, pane, replaced);
135
156
  }
136
157
  const at = insertedAt(extra, items.length);
137
- const inserted = (i) => (i === at ? edit : lineAt(i < at ? i : i - 1));
158
+ const inserted = (i) => {
159
+ if (i === at) return edit;
160
+ const index = i < at ? i : i - 1;
161
+ return lineAt(index);
162
+ };
138
163
  return paintListWindow(items.length + 1, at, pane, inserted);
139
164
  };
140
165
 
@@ -163,7 +188,7 @@ const sliceVisible = (text, skip, width) => {
163
188
 
164
189
  const fieldWindow = (text, cursor, scroll, width) => {
165
190
  if (width <= 0) return { text: '', col: 0, scroll: 0 };
166
- const at = Math.max(0, Math.min(cursor ?? 0, text.length));
191
+ const at = clamp(cursor ?? 0, 0, text.length);
167
192
  const col = visibleWidth(text.slice(0, at));
168
193
  let next = Math.max(0, scroll ?? 0);
169
194
  if (col < next) next = col;
@@ -213,10 +238,67 @@ const paintColumns = (parts, widths, fgRgb, bgRgb, color, tones) => {
213
238
  return text;
214
239
  };
215
240
 
241
+ const paintPathText = (text, fg, bg, color, bold, spans) => {
242
+ if (!spans.length) return paint(text, fg, bg, color, bold);
243
+ let out = '';
244
+ let at = 0;
245
+ for (const span of spans) {
246
+ if (span.start > at) {
247
+ out += paint(text.slice(at, span.start), fg, bg, color, bold);
248
+ }
249
+ const on = span.on === true;
250
+ const markFg = on ? THEME.searchOnFg : THEME.searchFg;
251
+ const markBg = on ? THEME.searchOnBg : THEME.searchBg;
252
+ const slice = text.slice(span.start, span.end);
253
+ out += paint(slice, markFg, markBg, color, true);
254
+ at = span.end;
255
+ }
256
+ if (at < text.length) out += paint(text.slice(at), fg, bg, color, bold);
257
+ return out;
258
+ };
259
+
260
+ const scrollThumb = (count, inner, start) => {
261
+ if (count <= inner || inner < 1) return null;
262
+ let size = Math.max(1, Math.round((inner * inner) / count));
263
+ if (inner > 1) size = Math.min(size, inner - 1);
264
+ const travel = inner - size;
265
+ const maxStart = count - inner;
266
+ const at = maxStart <= 0 ? 0 : Math.round((start * travel) / maxStart);
267
+ return { size, travel, maxStart, at };
268
+ };
269
+
270
+ const scrollMark = (index, count, inner, start) => {
271
+ const thumb = scrollThumb(count, inner, start);
272
+ if (!thumb) return '';
273
+ const end = thumb.at + thumb.size;
274
+ if (index >= thumb.at && index < end) return 'thumb';
275
+ return 'track';
276
+ };
277
+
278
+ const scrollAtThumb = (top, thumb) => {
279
+ if (!thumb || thumb.travel <= 0) return 0;
280
+ const at = clamp(top, 0, thumb.travel);
281
+ return Math.round((at * thumb.maxStart) / thumb.travel);
282
+ };
283
+
284
+ const placeHits = (hits, origin, width, side) => {
285
+ const placed = [];
286
+ for (const hit of hits) {
287
+ const x0 = (hit.x0 ?? 0) + origin;
288
+ const x1 = (hit.x1 ?? width) + origin;
289
+ placed.push({ ...hit, x0, x1, side });
290
+ }
291
+ return placed;
292
+ };
293
+
216
294
  module.exports = {
295
+ scrollThumb,
296
+ scrollMark,
297
+ scrollAtThumb,
298
+ placeHits,
217
299
  GUTTER_W,
218
- FILE_MARK,
219
300
  MARK_W,
301
+ LIST_PAD,
220
302
  fill,
221
303
  markPrefix,
222
304
  isListPane,
@@ -227,8 +309,11 @@ module.exports = {
227
309
  paintSplitRule,
228
310
  paintBodyFill,
229
311
  paneResult,
312
+ lazyRows,
313
+ bodyRow,
230
314
  editSpan,
231
315
  listLine,
316
+ listWindowStart,
232
317
  paintListWindow,
233
318
  paintCursorList,
234
319
  sliceVisible,
@@ -237,4 +322,5 @@ module.exports = {
237
322
  padColumns,
238
323
  joinColumns,
239
324
  paintColumns,
325
+ paintPathText,
240
326
  };
@@ -1,16 +1,11 @@
1
1
  'use strict';
2
2
 
3
- const chrome = require('./chrome.js');
4
- const notes = require('./notes.js');
5
- const diff = require('./diff.js');
6
- const present = require('./present.js');
7
- const frame = require('./frame.js');
8
-
9
- const { formatBusyStatus, headerText, layoutButtons } = chrome;
10
- const { todoBodyWidth, noteInnerWidth } = notes;
11
- const { paintDiffLine, codeInnerWidth } = diff;
12
- const { presentCursor, presentRows } = present;
13
- const { renderFrame } = frame;
3
+ const { formatBusyStatus } = require('./status.js');
4
+ const { headerText } = require('./header.js');
5
+ const { layoutButtons } = require('./footer.js');
6
+ const { todoBodyWidth, noteInnerWidth } = require('./notes.js');
7
+ const { paintDiffLine, codeInnerWidth, activeDigits } = require('./diff.js');
8
+ const { renderFrame, presentCursor, presentRows } = require('./frame.js');
14
9
 
15
10
  module.exports = {
16
11
  formatBusyStatus,
@@ -20,6 +15,7 @@ module.exports = {
20
15
  noteInnerWidth,
21
16
  paintDiffLine,
22
17
  codeInnerWidth,
18
+ activeDigits,
23
19
  renderFrame,
24
20
  presentCursor,
25
21
  presentRows,
@@ -1,15 +1,14 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const { THEME } = ansi;
5
- const primitives = require('./primitives.js');
6
- const { paintBodyFill, paneResult } = primitives;
3
+ const { THEME } = require('../term/ansi.js');
4
+ const { paintBodyFill, paneResult } = require('./primitives.js');
7
5
  const tiles = require('./tiles.js');
6
+ const { RUN_ICON } = tiles;
8
7
  const { seg, spinner, equalGrid, pageWindow, placeEqualTiles } = tiles;
9
8
  const { paintTile, composeBands, tileHits, pageFill } = tiles;
10
- const { segmentsWidth, clipSegments } = tiles;
11
- const table = require('./dash-table.js');
12
- const { size, isHot } = table;
9
+ const { segmentsWidth, clipSegments, blinkTone } = tiles;
10
+ const { size } = require('../common/format.js');
11
+ const { isHot } = require('./table.js');
13
12
 
14
13
  const REPO_MIN_H = 6;
15
14
  const MARGIN_MIN_H = 12;
@@ -19,29 +18,19 @@ const DIM_BACKS = {
19
18
  tile: THEME.dashDimTileBg,
20
19
  row: THEME.dashDimRowBg,
21
20
  };
22
- const RUN_ICON = {
23
- passed: ['✔', 'add'],
24
- failed: ['✖', 'error'],
25
- aborted: ['⊘', 'muted'],
26
- };
27
-
28
21
  const extName = (key) => {
29
22
  const text = `${key ?? ''}`;
30
23
  if (!text || text === '(none)') return 'none';
31
24
  return text.startsWith('.') ? text.slice(1) : text;
32
25
  };
33
26
 
34
- const blinkTone = (frame) => (Math.abs(frame) % 2 ? 'hot' : 'warn');
35
-
36
27
  const spin = (frame) => seg(spinner(frame), 'warn', true);
37
28
 
38
- const rowPending = (row) =>
39
- !row.filesReady ||
40
- !row.commitsReady ||
41
- !row.diffReady ||
42
- !row.tasksReady ||
43
- !row.runsReady ||
44
- !row.npmReady;
29
+ const rowPending = (row) => {
30
+ if (!row.filesReady || !row.commitsReady) return true;
31
+ if (!row.diffReady || !row.tasksReady) return true;
32
+ return !row.runsReady || !row.npmReady;
33
+ };
45
34
 
46
35
  const fitAside = (left, right, width) => {
47
36
  const rightW = segmentsWidth(right);
@@ -214,13 +203,12 @@ const paintBodyRepos = (view, width, color, bodyH, headerLines) => {
214
203
  const page = pageWindow(model.tiles.length, model.cursor, grid.cap);
215
204
  const shown = model.tiles.slice(page.start, page.end);
216
205
  const bands = placeEqualTiles(shown, grid);
217
- model.cols = grid.cols;
218
206
  const painted = paintRepos(model, bands, color);
219
207
  const rows = composeBands(bands, painted, width, height, color);
220
208
  const pad = new Array(margin).fill(pageFill(width, color));
221
209
  const body = [...pad, ...rows, ...pad];
222
210
  const fileHits = tileHits(bands, headerLines + margin);
223
- return paneResult({ body, fileHits });
211
+ return { ...paneResult({ body, fileHits }), gridCols: grid.cols };
224
212
  };
225
213
 
226
- module.exports = { REPO_MIN_H, extName, paintBodyRepos };
214
+ module.exports = { paintBodyRepos };
@@ -0,0 +1,370 @@
1
+ 'use strict';
2
+
3
+ const ansi = require('../term/ansi.js');
4
+ const actions = require('../input/actions.js');
5
+ const files = require('../common/files.js');
6
+ const primitives = require('./primitives.js');
7
+ const { fill, branchLabel } = primitives;
8
+ const { npmStatus } = require('./dashboard/blocks.js');
9
+ const { menuBoxWidth, dropBox, menuFaces } = require('./menu.js');
10
+ const field = require('./field.js');
11
+ const { paintPlanFace, paintImportStatus, paintFindStatus } = field;
12
+ const { fileTotals } = files;
13
+ const { CONFIRM, confirmPrefix, promptFromChoices } = actions;
14
+ const { THEME, paint, visibleWidth } = ansi;
15
+ const { truncateVisible } = ansi;
16
+ const { paintMarkedWord, QUIT_MARK } = require('./footer.js');
17
+
18
+ const WAVE = '·•●•·';
19
+
20
+ const BUSY_BAR = Array.from(WAVE, (_, frame) => {
21
+ const start = (WAVE.length - frame) % WAVE.length;
22
+ return WAVE.slice(start) + WAVE.slice(0, start);
23
+ });
24
+
25
+ const {
26
+ busy: BUSY_PROGRESS,
27
+ info: INFO_STATUS,
28
+ infoPrefix: INFO_PREFIX,
29
+ } = require('./status.json');
30
+
31
+ const formatBusyStatus = (status, frame = 0) => {
32
+ const kind = `${status ?? ''}`;
33
+ if (!BUSY_PROGRESS.includes(kind)) return kind;
34
+ const i = Math.abs(frame) % BUSY_BAR.length;
35
+ return `${kind} ${BUSY_BAR[i]}`;
36
+ };
37
+
38
+ const STATUS_MARK = /(\+[0-9]+(?:\/[0-9]+)?|-[0-9]+(?:\/[0-9]+)?)/g;
39
+
40
+ const OLD_LOGS = /old logs \S+/;
41
+
42
+ const isInfoStatus = (status) => {
43
+ const busy = BUSY_PROGRESS.includes(status);
44
+ const known = INFO_STATUS.includes(status);
45
+ const prefixed = INFO_PREFIX.some((prefix) => status.startsWith(prefix));
46
+ return busy || known || prefixed;
47
+ };
48
+
49
+ const joinStatus = (parts) => parts.filter(Boolean).join(' ');
50
+
51
+ const countsKind = (sourceKind, revShort) => {
52
+ const remote = sourceKind === 'pr' || sourceKind === 'mr';
53
+ if (remote || sourceKind === 'issue') return sourceKind;
54
+ return revShort ? 'commit' : 'worktree';
55
+ };
56
+
57
+ const isWorktree = (view) =>
58
+ countsKind(view.sourceKind, view.revShort) === 'worktree';
59
+
60
+ const notesLine = (counts) => {
61
+ const done = counts.tasksDone ?? 0;
62
+ const total = counts.tasks ?? 0;
63
+ return `tasks ${done}/${total}`;
64
+ };
65
+
66
+ const PLAN_LEAD = 'Plan file: ';
67
+
68
+ const closedPlan = (view) => `${view.planName ?? ''}`;
69
+
70
+ const planLabel = (view) => {
71
+ const closed = closedPlan(view);
72
+ if (!closed) return '';
73
+ const query = view.planOpen ? `${view.planQuery ?? ''}` : '';
74
+ if (!query) return closed;
75
+ const room = Math.max(visibleWidth(closed), visibleWidth(query));
76
+ const shown = truncateVisible(query, room);
77
+ const pad = Math.max(0, room - visibleWidth(shown));
78
+ return `${shown}${' '.repeat(pad)}`;
79
+ };
80
+
81
+ const reposLead = (view) => {
82
+ const tiles = view.repos && view.repos.tiles ? view.repos.tiles : [];
83
+ const count = tiles.length;
84
+ if (!count) return 'repos';
85
+ const at = (view.repos.cursor ?? 0) + 1;
86
+ return `${at}/${count}`;
87
+ };
88
+
89
+ const agentsLead = (view) => {
90
+ if (view.agentView) return view.agentStatus || 'log';
91
+ const list = view.agents ?? [];
92
+ const found = list.filter((row) => row.bin).length;
93
+ return `${found}/${list.length}`;
94
+ };
95
+
96
+ const statusLead = (view) => {
97
+ if (view.pane === 'repos') return reposLead(view);
98
+ if (view.pane === 'agents') return agentsLead(view);
99
+ const counts = view.counts ?? {};
100
+ const kind = countsKind(view.sourceKind, view.revShort);
101
+ if (kind === 'worktree') return branchLabel(view.branch);
102
+ if (kind === 'commit') {
103
+ return `commit ${view.revShort} ${counts.commit ?? 0}`;
104
+ }
105
+ const label = view.sourceLabel || kind;
106
+ return `${kind} ${label} ${counts.pr ?? 0}`;
107
+ };
108
+
109
+ const STATUS_QUIET = ['npm', 'packages', 'branches', 'agents'];
110
+
111
+ const planStatus = (view) => {
112
+ const combo = planLabel(view);
113
+ if (!combo) return '';
114
+ return `${PLAN_LEAD}${combo}`;
115
+ };
116
+
117
+ const statusLeft = (view) => {
118
+ if (view.pane === 'repos') return ` ${statusLead(view)}`;
119
+ if (view.pane === 'tasks') {
120
+ const lead = joinStatus([statusLead(view), planStatus(view)]);
121
+ return ` ${lead}`;
122
+ }
123
+ const quiet = STATUS_QUIET.includes(view.pane);
124
+ const notes = quiet ? '' : notesLine(view.counts ?? {});
125
+ return ` ${joinStatus([statusLead(view), notes])}`;
126
+ };
127
+
128
+ const planAt = (view, text) => {
129
+ const combo = planLabel(view);
130
+ if (!combo) return -1;
131
+ const lead = statusLead(view);
132
+ const head = lead ? ` ${lead} ${PLAN_LEAD}` : ` ${PLAN_LEAD}`;
133
+ const line = `${text ?? ''}`;
134
+ if (!line.startsWith(head)) return -1;
135
+ const rest = line.slice(head.length);
136
+ if (rest.startsWith(combo) || combo.startsWith(rest)) return head.length;
137
+ return -1;
138
+ };
139
+
140
+ const planFace = (view, text) => {
141
+ const at = planAt(view, text);
142
+ if (at < 0) return null;
143
+ const combo = planLabel(view);
144
+ const rest = text.slice(at);
145
+ const face = rest.startsWith(combo) ? combo : rest;
146
+ return { at, face, x: visibleWidth(text.slice(0, at)) };
147
+ };
148
+
149
+ const statusDelta = (total) => {
150
+ const stagedAdd = total.stagedAdded ?? 0;
151
+ const unstagedAdd = total.unstagedAdded ?? 0;
152
+ const stagedDel = total.stagedRemoved ?? 0;
153
+ const unstagedDel = total.unstagedRemoved ?? 0;
154
+ const hasDelta =
155
+ stagedAdd || unstagedAdd || stagedDel || unstagedDel || total.added;
156
+ if (!hasDelta && !total.removed) return '';
157
+ const plus = `+${stagedAdd}/${stagedAdd + unstagedAdd}`;
158
+ const minus = `-${stagedDel}/${stagedDel + unstagedDel}`;
159
+ return `${plus} ${minus}`;
160
+ };
161
+
162
+ const DIFF_QUIET = ['npm', 'packages', 'tasks', 'branches', 'agents'];
163
+
164
+ const statusStats = (view) => {
165
+ if (view.pane === 'tasks') return notesLine(view.counts ?? {});
166
+ const counts = view.counts ?? {};
167
+ const staged = counts.staged ?? 0;
168
+ const total = fileTotals(view.files ?? []);
169
+ const { remaining, staged: listed, added, removed } = total;
170
+ const hasGit = remaining || listed || added || removed;
171
+ const unstaged = counts.unstaged ?? 0;
172
+ const n = hasGit ? listed : staged;
173
+ const m = hasGit ? remaining : staged + unstaged;
174
+ const delta = DIFF_QUIET.includes(view.pane) ? '' : statusDelta(total);
175
+ const ratio = STATUS_QUIET.includes(view.pane) ? '' : `${n}/${m}`;
176
+ const packages = view.pane === 'packages' ? npmStatus(view.packages) : '';
177
+ return joinStatus([view.npmLogs, delta, ratio, packages]);
178
+ };
179
+
180
+ const statusLayout = (view, status, width) => {
181
+ const msg = formatBusyStatus(status, view.progressFrame ?? 0);
182
+ const isError = msg && !isInfoStatus(status);
183
+ const msgFg = isError ? THEME.errorFg : THEME.chromeFg;
184
+ const stats = statusStats(view);
185
+ const right = stats ? `${stats} ` : '';
186
+ const rightW = visibleWidth(right);
187
+ const msgW = visibleWidth(msg);
188
+ const side = msg ? 1 : 0;
189
+ const room = Math.max(0, width - rightW - msgW - side * 2);
190
+ const clipped = truncateVisible(statusLeft(view), room);
191
+ const leftW = visibleWidth(clipped);
192
+ const extra = Math.max(0, width - leftW - rightW - msgW);
193
+ const padLeft = msg ? Math.floor(extra / 2) : extra;
194
+ const padRight = msg ? extra - padLeft : 0;
195
+ return { clipped, msg, msgFg, right, padLeft, padRight };
196
+ };
197
+
198
+ const choiceHits = (choices, prefix, y) => {
199
+ let x = visibleWidth(prefix);
200
+ return choices.map((choice) => {
201
+ const w = visibleWidth(`${choice.lead}${choice.label}`);
202
+ const hit = { id: choice.letter, y, x0: x, x1: x + w };
203
+ x += w;
204
+ return hit;
205
+ });
206
+ };
207
+
208
+ const paintChoicePrompt = (choices, width, color, prefix) => {
209
+ const bgRgb = THEME.chromeBg;
210
+ const body = `${prefix}${promptFromChoices(choices)}`;
211
+ const pad = Math.max(0, width - visibleWidth(body));
212
+ if (!color) return body + ' '.repeat(pad);
213
+ let out = paint(prefix, THEME.mutedFg, bgRgb, color);
214
+ for (const choice of choices) {
215
+ const label = `${choice.lead}${choice.label}`;
216
+ out += paintMarkedWord(label, choice.letter, color, QUIT_MARK);
217
+ }
218
+ return out + fill(pad, THEME.mutedFg, bgRgb, color);
219
+ };
220
+
221
+ const paintStatusLeft = (text, color) => {
222
+ if (!color) return text;
223
+ const bgRgb = THEME.chromeBg;
224
+ const found = text.match(OLD_LOGS);
225
+ if (found) {
226
+ const token = found[0];
227
+ const before = text.slice(0, found.index);
228
+ const after = text.slice(found.index + token.length);
229
+ return (
230
+ paintStatusLeft(before, color) +
231
+ paint(token, THEME.shaFg, bgRgb, color) +
232
+ paintStatusLeft(after, color)
233
+ );
234
+ }
235
+ let out = '';
236
+ let last = 0;
237
+ for (const match of text.matchAll(STATUS_MARK)) {
238
+ const token = match[0];
239
+ const tokenFg = token.startsWith('+') ? THEME.addLineFg : THEME.delLineFg;
240
+ out += paint(text.slice(last, match.index), THEME.mutedFg, bgRgb, color);
241
+ out += paint(token, tokenFg, bgRgb, color);
242
+ last = match.index + token.length;
243
+ }
244
+ return out + paint(text.slice(last), THEME.mutedFg, bgRgb, color);
245
+ };
246
+
247
+ const paintBranchHead = (text, branch, color) => {
248
+ const name = branchLabel(branch);
249
+ if (!name || !text.startsWith(' ')) return null;
250
+ const shown = text.startsWith(` ${name}`) ? name : text.slice(1);
251
+ if (!shown || !name.startsWith(shown)) return null;
252
+ const rest = text.slice(1 + shown.length);
253
+ const bgRgb = THEME.chromeBg;
254
+ return (
255
+ paint(' ', THEME.mutedFg, bgRgb, color) +
256
+ paint(shown, THEME.buttonHotFg, bgRgb, color, true) +
257
+ paintStatusLeft(rest, color)
258
+ );
259
+ };
260
+
261
+ const paintTasksLeft = (view, text, color) => {
262
+ const place = planFace(view, text);
263
+ if (!place) {
264
+ const head = paintBranchHead(text, view.branch, color);
265
+ return head ?? paintStatusLeft(text, color);
266
+ }
267
+ const before = text.slice(0, place.at);
268
+ const after = text.slice(place.at + place.face.length);
269
+ const searching = Boolean(view.planOpen && view.planQuery);
270
+ const comboFg = searching ? THEME.searchFg : THEME.shaFg;
271
+ const comboBg = searching ? THEME.searchBg : THEME.chromeBg;
272
+ const branch = paintBranchHead(before, view.branch, color);
273
+ const head = branch ?? paintStatusLeft(before, color);
274
+ const face = paintPlanFace(view, place.face, comboFg, comboBg, color);
275
+ return head + face + paintStatusLeft(after, color);
276
+ };
277
+
278
+ const statusHead = (view, clipped, color) => {
279
+ if (view.pane === 'tasks') return paintTasksLeft(view, clipped, color);
280
+ if (isWorktree(view)) return paintBranchHead(clipped, view.branch, color);
281
+ return null;
282
+ };
283
+
284
+ const paintStatusLine = (view, status, width, color) => {
285
+ if (view.import) return paintImportStatus(view, width, color);
286
+ if (view.find) return paintFindStatus(view, width, color);
287
+ const confirm = CONFIRM[view.mode];
288
+ if (confirm) {
289
+ const prefix = confirmPrefix(confirm, view);
290
+ return paintChoicePrompt(confirm.choices, width, color, prefix);
291
+ }
292
+ const layout = statusLayout(view, status, width);
293
+ const leftGap = ' '.repeat(layout.padLeft);
294
+ const rightGap = ' '.repeat(layout.padRight);
295
+ if (!color) {
296
+ return layout.clipped + leftGap + layout.msg + rightGap + layout.right;
297
+ }
298
+ const bgRgb = THEME.chromeBg;
299
+ const head = statusHead(view, layout.clipped, color);
300
+ const left = head ?? paintStatusLeft(layout.clipped, color);
301
+ return (
302
+ left +
303
+ paint(leftGap, THEME.mutedFg, bgRgb, color) +
304
+ paint(layout.msg, layout.msgFg, bgRgb, color) +
305
+ paint(rightGap, THEME.mutedFg, bgRgb, color) +
306
+ paintStatusLeft(layout.right, color)
307
+ );
308
+ };
309
+
310
+ const statusChoiceHits = (view, y) => {
311
+ const spec = CONFIRM[view.mode];
312
+ if (!spec) return [];
313
+ return choiceHits(spec.choices, confirmPrefix(spec, view), y);
314
+ };
315
+
316
+ const statusPlanHit = (view, width, y) => {
317
+ if (view.pane !== 'tasks' || view.find || view.import) return null;
318
+ if (CONFIRM[view.mode]) return null;
319
+ const layout = statusLayout(view, view.status ?? '', width);
320
+ const place = planFace(view, layout.clipped);
321
+ if (!place) return null;
322
+ const x0 = place.x;
323
+ return { id: 'plan', y, x0, x1: x0 + visibleWidth(place.face) };
324
+ };
325
+
326
+ const planMenu = (view, width, color, statusY) => {
327
+ if (view.pane !== 'tasks' || !view.planOpen || statusY < 3) return null;
328
+ const names = view.planNames ?? [];
329
+ const layout = statusLayout(view, view.status ?? '', width);
330
+ const place = planFace(view, layout.clipped);
331
+ if (!place) return null;
332
+ const x = Math.max(0, place.x - 1);
333
+ const room = Math.max(1, statusY - 2);
334
+ const box = dropBox(names, view.planCursor, view.planScroll, room);
335
+ const boxW = menuBoxWidth(names, visibleWidth(place.face), width - x);
336
+ const faces = menuFaces(names, box, boxW, color);
337
+ const lines = [];
338
+ const hits = [];
339
+ for (let i = 0; i < faces.length; i++) {
340
+ const y = statusY - box.inner + i;
341
+ lines.push({ y, text: faces[i].face });
342
+ if (names[faces[i].index] === undefined) continue;
343
+ const id = 'plan-item';
344
+ const x0 = x;
345
+ const x1 = x + boxW;
346
+ const cursor = faces[i].index;
347
+ const hit = { id, y, x0, x1, cursor };
348
+ hits.push(hit);
349
+ }
350
+ return { x, lines, hits, scroll: box.start };
351
+ };
352
+
353
+ const statusBranchHit = (view, width, y) => {
354
+ if (view.find || view.import || CONFIRM[view.mode]) return null;
355
+ const name = branchLabel(view.branch);
356
+ if (!name || !isWorktree(view)) return null;
357
+ const layout = statusLayout(view, view.status ?? '', width);
358
+ const at = layout.clipped.indexOf(name);
359
+ if (at < 0) return null;
360
+ return { id: 'branch', y, x0: at, x1: at + visibleWidth(name) };
361
+ };
362
+
363
+ module.exports = {
364
+ formatBusyStatus,
365
+ paintStatusLine,
366
+ statusChoiceHits,
367
+ statusBranchHit,
368
+ statusPlanHit,
369
+ planMenu,
370
+ };