reslop 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +16 -14
  3. package/bin/reslop.js +8 -5
  4. package/lib/agents/agents.js +573 -0
  5. package/lib/agents/allow.js +71 -0
  6. package/lib/agents/allow.json +17 -0
  7. package/lib/agents/models.js +467 -0
  8. package/lib/api.js +15 -12
  9. package/lib/cli.js +43 -68
  10. package/lib/{files.js → common/files.js} +48 -55
  11. package/lib/{find.js → common/find.js} +1 -1
  12. package/lib/common/format.js +101 -0
  13. package/lib/{utilities.js → common/process.js} +7 -95
  14. package/lib/common/tasks.json +27 -0
  15. package/lib/common/utilities.js +112 -0
  16. package/lib/dashboard/git.js +9 -16
  17. package/lib/dashboard/model.js +90 -49
  18. package/lib/dashboard/npm.js +17 -13
  19. package/lib/dashboard/tree.js +27 -30
  20. package/lib/{session → dashboard}/watch.js +36 -31
  21. package/lib/{workspace.js → dashboard/workspace.js} +23 -38
  22. package/lib/{deps.js → deps/deps.js} +31 -45
  23. package/lib/{deps-diff.js → deps/diff.js} +35 -25
  24. package/lib/{manifest.js → deps/manifest.js} +47 -30
  25. package/lib/diff/align.js +1 -2
  26. package/lib/diff/diff.js +2 -2
  27. package/lib/diff/display.js +29 -5
  28. package/lib/diff/inline.js +10 -14
  29. package/lib/diff/items.js +6 -4
  30. package/lib/diff/patch.js +39 -3
  31. package/lib/diff/unit.js +45 -16
  32. package/lib/{git-branches.js → git/branches.js} +53 -48
  33. package/lib/{git-deps.js → git/deps.js} +151 -95
  34. package/lib/git/git.js +71 -0
  35. package/lib/git/worktree.js +504 -0
  36. package/lib/highlight/bash.js +1 -2
  37. package/lib/highlight/csv.js +1 -2
  38. package/lib/highlight/dot.js +1 -2
  39. package/lib/{highlight.js → highlight/highlight.js} +42 -11
  40. package/lib/highlight/html.js +20 -18
  41. package/lib/highlight/json.js +1 -2
  42. package/lib/highlight/log.js +1 -2
  43. package/lib/highlight/txt.js +1 -2
  44. package/lib/{session → input}/actions.js +114 -48
  45. package/lib/{editor.js → input/editor.js} +16 -8
  46. package/lib/{keys.js → input/keys.js} +24 -45
  47. package/lib/input/line-edit.js +78 -0
  48. package/lib/input/menu-pick.js +70 -0
  49. package/lib/render/agents.js +642 -0
  50. package/lib/render/branches.js +4 -5
  51. package/lib/render/commits.js +54 -64
  52. package/lib/render/{dash-activity.js → dashboard/activity.js} +25 -40
  53. package/lib/render/{dash-blocks.js → dashboard/blocks.js} +131 -137
  54. package/lib/render/{dashboard.js → dashboard/dashboard.js} +25 -22
  55. package/lib/render/diff.js +166 -45
  56. package/lib/render/field.js +131 -0
  57. package/lib/render/files.js +9 -14
  58. package/lib/render/footer.js +385 -0
  59. package/lib/render/frame.js +123 -25
  60. package/lib/render/header.js +176 -0
  61. package/lib/render/log.js +320 -0
  62. package/lib/render/menu.js +60 -0
  63. package/lib/render/notes.js +16 -11
  64. package/lib/render/npm.js +322 -196
  65. package/lib/render/packages.js +9 -13
  66. package/lib/render/primitives.js +101 -15
  67. package/lib/render/render.js +7 -11
  68. package/lib/render/repos.js +13 -25
  69. package/lib/render/status.js +370 -0
  70. package/lib/render/status.json +85 -0
  71. package/lib/render/{dash-table.js → table.js} +13 -60
  72. package/lib/render/tiles.js +109 -48
  73. package/lib/render/unit.js +32 -13
  74. package/lib/{report-model.js → report/model.js} +4 -6
  75. package/lib/{report-parse.js → report/parse.js} +9 -14
  76. package/lib/{report-render.js → report/render.js} +48 -19
  77. package/lib/{report-run.js → report/run.js} +42 -30
  78. package/lib/{report-test.js → report/test.js} +56 -55
  79. package/lib/{report-tools.js → report/tools.js} +24 -33
  80. package/lib/review/agent-instructions.md +14 -0
  81. package/lib/review/format.js +420 -0
  82. package/lib/{review-merge.js → review/merge.js} +9 -8
  83. package/lib/review/review.js +190 -0
  84. package/lib/review/store.js +281 -0
  85. package/lib/review/templates.js +83 -0
  86. package/lib/runs/commands.js +14 -0
  87. package/lib/runs/logs.js +299 -0
  88. package/lib/runs/output.js +174 -0
  89. package/lib/runs/process.js +113 -0
  90. package/lib/{runs.js → runs/runs.js} +84 -36
  91. package/lib/runs/scripts.js +150 -0
  92. package/lib/session/agents/agents.js +976 -0
  93. package/lib/session/agents/job.js +320 -0
  94. package/lib/session/agents/jobs.js +355 -0
  95. package/lib/session/agents/options.js +153 -0
  96. package/lib/session/agents/plans.js +83 -0
  97. package/lib/session/branches.js +33 -40
  98. package/lib/session/change-actions.js +34 -100
  99. package/lib/session/commit.js +91 -52
  100. package/lib/session/compose.js +189 -147
  101. package/lib/session/dashboard.js +23 -22
  102. package/lib/session/dispatch.js +86 -21
  103. package/lib/session/files-pane.js +47 -26
  104. package/lib/session/find.js +16 -29
  105. package/lib/session/import.js +93 -0
  106. package/lib/session/items.js +8 -17
  107. package/lib/session/lifecycle.js +78 -72
  108. package/lib/session/load.js +2 -4
  109. package/lib/session/navigation.js +6 -3
  110. package/lib/session/npm/history.js +250 -0
  111. package/lib/session/npm/npm.js +836 -0
  112. package/lib/session/{packages.js → npm/packages.js} +21 -14
  113. package/lib/session/ops.js +125 -34
  114. package/lib/session/pointer.js +202 -10
  115. package/lib/session/prefs.js +112 -0
  116. package/lib/session/reads.js +109 -0
  117. package/lib/session/review.js +28 -10
  118. package/lib/session/run-panel.js +40 -0
  119. package/lib/{session.js → session/session.js} +158 -63
  120. package/lib/session/tasks.js +321 -66
  121. package/lib/session/templates.js +1 -1
  122. package/lib/session/terminal.js +94 -12
  123. package/lib/session/unit.js +35 -30
  124. package/lib/session/update.js +194 -6
  125. package/lib/session/view.js +65 -65
  126. package/lib/session/workspace.js +39 -78
  127. package/lib/{capabilities.js → source/capabilities.js} +3 -12
  128. package/lib/{github.js → source/github.js} +102 -32
  129. package/lib/{gitlab.js → source/gitlab.js} +87 -10
  130. package/lib/{remote.js → source/remote.js} +144 -40
  131. package/lib/source/review-threads.graphql +20 -0
  132. package/lib/source/source.js +74 -0
  133. package/lib/term/ansi.js +396 -0
  134. package/lib/{clipboard.js → term/clipboard.js} +3 -6
  135. package/lib/{select.js → term/select.js} +4 -3
  136. package/lib/term/themes/dark.json +122 -0
  137. package/lib/term/themes/light.json +122 -0
  138. package/lib/term/wide.txt +21 -0
  139. package/lib/{wrap.js → term/wrap.js} +4 -4
  140. package/package.json +2 -2
  141. package/lib/ansi.js +0 -545
  142. package/lib/detect.js +0 -34
  143. package/lib/diff/edit.js +0 -33
  144. package/lib/git-worktree.js +0 -455
  145. package/lib/git.js +0 -79
  146. package/lib/npm-commands.js +0 -522
  147. package/lib/render/chrome.js +0 -806
  148. package/lib/render/present.js +0 -19
  149. package/lib/render/report.js +0 -43
  150. package/lib/render/search.js +0 -25
  151. package/lib/review.js +0 -745
  152. package/lib/session/accessors.js +0 -26
  153. package/lib/session/edit-click.js +0 -57
  154. package/lib/session/npm.js +0 -452
  155. package/lib/session/progress.js +0 -41
  156. package/lib/source.js +0 -36
  157. package/lib/update.js +0 -192
  158. /package/lib/{git-reword-todo.js → git/reword-todo.js} +0 -0
@@ -0,0 +1,642 @@
1
+ 'use strict';
2
+
3
+ const { clamp, trimText } = require('../common/utilities.js');
4
+ const ansi = require('../term/ansi.js');
5
+ const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
6
+ const { stampVisible } = ansi;
7
+ const primitives = require('./primitives.js');
8
+ const { scrollMark } = primitives;
9
+ const { placeHits } = primitives;
10
+ const { LIST_PAD, paintBodyFill, fill } = primitives;
11
+ const { paintCursorList, paneResult } = primitives;
12
+ const { seg } = require('./tiles.js');
13
+ const { menuBoxWidth, dropBox, menuFaces } = require('./menu.js');
14
+ const table = require('./table.js');
15
+ const { waiting, notice, cell, cellSegs } = table;
16
+ const { flexCell, tableLines, titleAside } = table;
17
+ const logPaint = require('./log.js');
18
+ const { LOG_PAD_X } = logPaint;
19
+ const { paintNpmLog, withLogScroller } = logPaint;
20
+ const { paintField } = require('./field.js');
21
+
22
+ const BUBBLE = '●';
23
+ const BUBBLE_GAP = 1;
24
+ const BUBBLE_PHASE = ['key', 'add', 'sha', 'warn'];
25
+ const PAD_W = 2;
26
+ const TAIL = 2;
27
+ const RUN_TAIL = 2;
28
+ const HEAD_GAP = 1;
29
+ const NAME_MAX = 10;
30
+ const MODEL_MAX = 32;
31
+ const EFFORT_MAX = 10;
32
+ const FAST_MAX = 4;
33
+ const CONTEXT_MAX = 8;
34
+ const PLAN_MAX = 48;
35
+ const COL_KEYS = ['name', 'model', 'effort', 'fast', 'context', 'plan'];
36
+ const VALUE_FIELDS = COL_KEYS.slice(1);
37
+ const COL_TITLES = {
38
+ name: 'agent',
39
+ model: 'model',
40
+ effort: 'effort',
41
+ fast: 'fast',
42
+ context: 'context',
43
+ plan: 'plan',
44
+ };
45
+
46
+ const agentFields = (row) => {
47
+ if (!row.bin) {
48
+ return {
49
+ name: row.name,
50
+ model: row.model || '',
51
+ effort: '',
52
+ fast: '',
53
+ context: '',
54
+ plan: 'not installed',
55
+ };
56
+ }
57
+ const levels = row.efforts ?? [];
58
+ return {
59
+ name: row.name,
60
+ model: row.model || '',
61
+ effort: levels.length ? row.effort || '' : '',
62
+ fast: row.fast || 'off',
63
+ context: row.context || '',
64
+ plan: row.plan || '',
65
+ };
66
+ };
67
+
68
+ const colWidth = (rows, pick, max) => {
69
+ let width = 0;
70
+ for (const row of rows) {
71
+ width = Math.max(width, visibleWidth(pick(row)));
72
+ }
73
+ return Math.min(max, width);
74
+ };
75
+
76
+ const nameWidth = (rows) =>
77
+ colWidth(rows, (row) => agentFields(row).name, NAME_MAX);
78
+
79
+ const modelWidth = (rows) => {
80
+ let width = 0;
81
+ for (const row of rows) {
82
+ if (!row.bin) continue;
83
+ const shown = visibleWidth(agentFields(row).model);
84
+ width = Math.max(width, shown, visibleWidth('model'));
85
+ }
86
+ if (width) return Math.min(MODEL_MAX, width);
87
+ for (const row of rows) {
88
+ if (agentFields(row).model) return visibleWidth('model');
89
+ }
90
+ return 0;
91
+ };
92
+
93
+ const effortWidth = (rows) => {
94
+ let width = 0;
95
+ for (const row of rows) {
96
+ if (!row.bin) continue;
97
+ width = Math.max(width, visibleWidth(agentFields(row).effort));
98
+ for (const level of row.efforts ?? []) {
99
+ const label = trimText(level);
100
+ if (!label || label === 'default') continue;
101
+ width = Math.max(width, visibleWidth(label));
102
+ }
103
+ }
104
+ return Math.min(EFFORT_MAX, width);
105
+ };
106
+
107
+ const fastWidth = (rows) => {
108
+ for (const row of rows) {
109
+ if (row.bin) return FAST_MAX;
110
+ }
111
+ return 0;
112
+ };
113
+
114
+ const contextWidth = (rows) => {
115
+ let width = 0;
116
+ for (const row of rows) {
117
+ if (!row.bin) continue;
118
+ const choices = row.contexts ?? [];
119
+ if (!choices.length) continue;
120
+ width = Math.max(width, visibleWidth('context'));
121
+ width = Math.max(width, visibleWidth(agentFields(row).context));
122
+ for (const size of choices) {
123
+ width = Math.max(width, visibleWidth(`${size ?? ''}`));
124
+ }
125
+ }
126
+ return Math.min(CONTEXT_MAX, width);
127
+ };
128
+
129
+ const planWidth = (rows) =>
130
+ colWidth(rows, (row) => agentFields(row).plan, PLAN_MAX);
131
+
132
+ const colW = (cols, field) => cols[`${field}W`] || 0;
133
+
134
+ const walkFields = (cols, gap, visit) => {
135
+ let x = PAD_W + cols.nameW;
136
+ for (const field of VALUE_FIELDS) {
137
+ const width = colW(cols, field);
138
+ if (!width) continue;
139
+ x += gap.length;
140
+ visit(field, x, width);
141
+ x += width;
142
+ }
143
+ };
144
+
145
+ const columnSpans = (row, fields, cols, gap) => {
146
+ if (!row.bin) return [];
147
+ const sizes = row.contexts ?? [];
148
+ const spans = [];
149
+ walkFields(cols, gap, (field, x, width) => {
150
+ if (field === 'context' && !sizes.length) return;
151
+ if (field === 'plan' && !fields.plan) return;
152
+ spans.push({ x0: x, x1: x + width, field });
153
+ });
154
+ return spans;
155
+ };
156
+
157
+ const typedQuery = (search, field) => {
158
+ if (!search || search.field !== field) return '';
159
+ return `${search.query ?? ''}`;
160
+ };
161
+
162
+ const cellFace = (text, width, query, end = false) => {
163
+ const body = query || text;
164
+ const shown = trimVisible(body, width);
165
+ const pad = ' '.repeat(Math.max(0, width - visibleWidth(shown)));
166
+ return end && !query ? `${pad}${shown}` : `${shown}${pad}`;
167
+ };
168
+
169
+ const paintAgentRow = (row, width, color, selected, cols, search) => {
170
+ const fields = agentFields(row);
171
+ const prefix = ' '.repeat(PAD_W);
172
+ const rest = Math.max(1, width - PAD_W - TAIL);
173
+ const gap = cols.gap || ' ';
174
+ const name = cellFace(fields.name, cols.nameW, '');
175
+ const cells = {
176
+ model: cellFace(fields.model, cols.modelW, typedQuery(search, 'model')),
177
+ effort: cellFace(fields.effort, cols.effortW, typedQuery(search, 'effort')),
178
+ fast: cellFace(fields.fast, cols.fastW, ''),
179
+ context: cellFace(
180
+ fields.context,
181
+ cols.contextW,
182
+ typedQuery(search, 'context'),
183
+ ),
184
+ plan: cellFace(fields.plan, cols.planW, typedQuery(search, 'plan'), true),
185
+ };
186
+ let mid = name;
187
+ for (const field of VALUE_FIELDS) {
188
+ if (colW(cols, field)) mid += `${gap}${cells[field]}`;
189
+ }
190
+ const pad = ' '.repeat(Math.max(0, rest - visibleWidth(mid)));
191
+ const tail = ' '.repeat(TAIL);
192
+ const spans = columnSpans(row, fields, cols, gap);
193
+ const fgRgb = selected ? THEME.chromeFg : THEME.mutedFg;
194
+ const bgRgb = selected ? THEME.buttonBg : THEME.ctxBg;
195
+ if (!color) return { row: `${prefix}${mid}${pad}${tail}`, spans };
196
+ const nameFg = row.bin ? THEME.buttonHotFg : THEME.mutedFg;
197
+ const modelFg = row.bin ? THEME.shaFg : THEME.shaDarkFg;
198
+ const valueFg = row.bin ? THEME.buttonHotFg : fgRgb;
199
+ const tones = {
200
+ model: { fg: modelFg, bold: Boolean(row.bin) },
201
+ effort: { fg: valueFg, bold: false },
202
+ fast: { fg: valueFg, bold: false, glue: true },
203
+ context: { fg: valueFg, bold: false },
204
+ plan: { fg: row.planBusy ? THEME.mutedFg : fgRgb, bold: false },
205
+ };
206
+ let out = `${seq(fgRgb, bgRgb)}${EL}`;
207
+ out += paint(prefix, fgRgb, bgRgb, color);
208
+ out += paint(name, nameFg, bgRgb, color, Boolean(row.bin));
209
+ for (const field of VALUE_FIELDS) {
210
+ const size = colW(cols, field);
211
+ if (!size) continue;
212
+ const face = cells[field];
213
+ const tone = tones[field];
214
+ const query = typedQuery(search, field);
215
+ if (tone.glue) {
216
+ out += paint(`${gap}${face}`, tone.fg, bgRgb, color);
217
+ continue;
218
+ }
219
+ out += paint(gap, valueFg, bgRgb, color);
220
+ if (query && search.editor) {
221
+ search.editor.reveal(size);
222
+ const queryTones = {
223
+ plain: { fg: THEME.searchFg, bg: THEME.searchBg },
224
+ select: { fg: THEME.chromeFg, bg: THEME.checkBg },
225
+ caret: { fg: THEME.searchOnFg, bg: THEME.searchOnBg },
226
+ };
227
+ out += paintField(query, search.editor, size, color, queryTones);
228
+ } else if (query) {
229
+ out += paint(face, THEME.searchFg, THEME.searchBg, color, true);
230
+ } else {
231
+ out += paint(face, tone.fg, bgRgb, color, tone.bold);
232
+ }
233
+ }
234
+ out += paint(`${pad}${tail}`, fgRgb, bgRgb, color);
235
+ return { row: out, spans };
236
+ };
237
+
238
+ const fieldX = (cols, gap, field) => {
239
+ let x = PAD_W + cols.nameW;
240
+ for (const name of VALUE_FIELDS) {
241
+ const width = colW(cols, name);
242
+ if (name === field) return x + (width ? gap.length : 0);
243
+ if (width) x += gap.length + width;
244
+ }
245
+ return x;
246
+ };
247
+
248
+ const overlayMenu = (painted, view, cols, gap, width, color, headerLines) => {
249
+ const options = view.agentMenu ?? [];
250
+ const selected = view.agentCursor ?? 0;
251
+ const rowHit = painted.fileHits.find((hit) => {
252
+ const onRow = hit.cursor === selected;
253
+ const plain = hit.field === undefined && hit.side !== 'runs';
254
+ return onRow && plain;
255
+ });
256
+ if (!rowHit) return painted;
257
+ const rowAt = rowHit.y - headerLines - 1;
258
+ const below = painted.body.length - rowAt - 1;
259
+ const above = rowAt;
260
+ const down = below >= above;
261
+ const room = Math.max(1, down ? below : above);
262
+ const box = dropBox(
263
+ options,
264
+ view.agentPickCursor,
265
+ view.agentMenuScroll,
266
+ room,
267
+ );
268
+ const menus = new Set(['model', 'effort', 'context', 'plan']);
269
+ const field = menus.has(view.agentPick) ? view.agentPick : 'model';
270
+ const off = new Set(view.agentMenuOff ?? []);
271
+ const columnW = cols[`${field}W`];
272
+ let x = Math.max(0, fieldX(cols, gap, field) - 1);
273
+ const boxW = menuBoxWidth(options, columnW, width - x);
274
+ if (x + boxW > width) x = Math.max(0, width - boxW);
275
+ const origin = down ? rowAt + 1 : Math.max(0, rowAt - box.span);
276
+ const faces = menuFaces(options, box, boxW, color, off);
277
+ const body = painted.body.slice();
278
+ const fileHits = painted.fileHits.slice();
279
+ for (let i = 0; i < faces.length && origin + i < body.length; i++) {
280
+ const at = origin + i;
281
+ body[at] = stampVisible(body[at], x, faces[i].face);
282
+ const option = box.start + i;
283
+ if (options[option] === undefined) continue;
284
+ fileHits.unshift({
285
+ y: headerLines + at + 1,
286
+ cursor: option,
287
+ x0: x,
288
+ x1: x + boxW,
289
+ field: 'menu',
290
+ });
291
+ }
292
+ return { ...painted, body, fileHits, menuScroll: box.start };
293
+ };
294
+
295
+ const chooserSplit = (width) => {
296
+ let rightW = clamp(Math.floor(width * 0.3), 16, 36);
297
+ let leftW = Math.max(0, width - rightW);
298
+ const leftNeed = PAD_W + TAIL + 58;
299
+ if (leftW < leftNeed && rightW > 12) {
300
+ const give = Math.min(rightW - 12, leftNeed - leftW);
301
+ rightW -= give;
302
+ leftW += give;
303
+ }
304
+ return { leftW, rightW };
305
+ };
306
+
307
+ const shareWidths = (weights, room) => {
308
+ const count = weights.length;
309
+ const out = new Array(count).fill(0);
310
+ const total = weights.reduce((sum, n) => sum + n, 0);
311
+ if (!total || room <= 0) return out;
312
+ let used = 0;
313
+ for (let i = 0; i < count; i++) {
314
+ if (!weights[i]) continue;
315
+ out[i] = Math.floor((weights[i] * room) / total);
316
+ used += out[i];
317
+ }
318
+ let left = room - used;
319
+ const rank = [];
320
+ for (let i = 0; i < count; i++) {
321
+ if (!weights[i]) continue;
322
+ const exact = (weights[i] * room) / total;
323
+ rank.push({ i, frac: exact - out[i] });
324
+ }
325
+ rank.sort((a, b) => b.frac - a.frac || weights[b.i] - weights[a.i]);
326
+ for (const item of rank) {
327
+ if (left <= 0) break;
328
+ out[item.i] += 1;
329
+ left -= 1;
330
+ }
331
+ return out;
332
+ };
333
+
334
+ const fitCols = (width, cols) => {
335
+ const rest = Math.max(0, width - PAD_W - TAIL);
336
+ const wants = COL_KEYS.map((key) => Math.max(0, cols[`${key}W`] ?? 0));
337
+ let gaps = 0;
338
+ let seen = false;
339
+ for (const want of wants) {
340
+ if (want <= 0) continue;
341
+ if (seen) gaps += 1;
342
+ seen = true;
343
+ }
344
+ const room = Math.max(0, rest - gaps);
345
+ const widths = shareWidths(wants, room);
346
+ const fitted = {};
347
+ for (let i = 0; i < COL_KEYS.length; i++) {
348
+ fitted[`${COL_KEYS[i]}W`] = widths[i];
349
+ }
350
+ return fitted;
351
+ };
352
+
353
+ const statusTone = (status) => {
354
+ if (status === 'running') return THEME.warnFg;
355
+ if (status === 'exit 0') return THEME.pkgDepFg;
356
+ if (status === 'stopped' || `${status ?? ''}`.startsWith('exit')) {
357
+ return THEME.errorFg;
358
+ }
359
+ return THEME.mutedFg;
360
+ };
361
+
362
+ const statusWidth = (rows) => {
363
+ let width = 7;
364
+ for (const row of rows) {
365
+ width = Math.max(width, visibleWidth(row.status || ''));
366
+ }
367
+ return Math.min(10, width);
368
+ };
369
+
370
+ const buttonFill = (width, color) =>
371
+ fill(width, THEME.chromeFg, THEME.buttonBg, color);
372
+
373
+ const paintRunRow = (row, width, color, selected, statusW) => {
374
+ const bgRgb = selected ? THEME.checkBg : THEME.buttonBg;
375
+ const fgRgb = THEME.chromeFg;
376
+ const prefix = ' '.repeat(PAD_W);
377
+ const tail = ' '.repeat(RUN_TAIL);
378
+ if (row.empty || row.fresh || row.session) {
379
+ const rest = Math.max(0, width - PAD_W - RUN_TAIL);
380
+ const label = trimVisible(row.empty ? 'none' : row.name || '', rest);
381
+ const pad = ' '.repeat(Math.max(0, rest - visibleWidth(label)));
382
+ const line = `${prefix}${label}${pad}${tail}`;
383
+ if (!color) {
384
+ if (row.empty) return { row: line, hit: false };
385
+ return { row: line };
386
+ }
387
+ const face = paint(line, fgRgb, bgRgb, color);
388
+ if (row.empty) return { row: face, hit: false };
389
+ return { row: face };
390
+ }
391
+ const budget = Math.max(0, width - PAD_W - RUN_TAIL);
392
+ const statusRoom = Math.min(statusW, budget);
393
+ const status = trimVisible(row.status || '', statusRoom);
394
+ const statusPad = ' '.repeat(statusRoom - visibleWidth(status));
395
+ let gap = '';
396
+ let command = '';
397
+ if (budget > statusRoom) {
398
+ gap = ' ';
399
+ const cmdW = budget - statusRoom - gap.length;
400
+ const detail = [row.elapsed, row.progress, row.command]
401
+ .filter(Boolean)
402
+ .join(' ');
403
+ const shown = trimVisible(detail, cmdW);
404
+ command = `${shown}${' '.repeat(Math.max(0, cmdW - visibleWidth(shown)))}`;
405
+ }
406
+ const line = `${prefix}${status}${statusPad}${gap}${command}`;
407
+ const pad = ' '.repeat(Math.max(0, width - RUN_TAIL - visibleWidth(line)));
408
+ const text = `${line}${pad}${tail}`;
409
+ if (!color) return { row: text };
410
+ const tone = statusTone(row.status);
411
+ let out = paint(prefix, fgRgb, bgRgb, color);
412
+ out += paint(`${status}${statusPad}`, tone, bgRgb, color);
413
+ out += paint(`${gap}${command}${pad}${tail}`, fgRgb, bgRgb, color);
414
+ return { row: out };
415
+ };
416
+
417
+ const joinPanes = (left, right, width, color, bodyH) => {
418
+ const { leftW, rightW } = chooserSplit(width);
419
+ const blankL = paintBodyFill(leftW, color, 'files');
420
+ const blankR = buttonFill(rightW, color);
421
+ const count = Math.max(left.body.length, right.body.length, bodyH);
422
+ const body = [];
423
+ for (let i = 0; i < count; i++) {
424
+ const sideL = left.body[i] ?? blankL;
425
+ const sideR = right.body[i] ?? blankR;
426
+ body.push(`${sideL}${sideR}`);
427
+ }
428
+ const fileHits = [
429
+ ...placeHits(left.fileHits, 0, leftW, 'cli'),
430
+ ...placeHits(right.fileHits, leftW, rightW, 'runs'),
431
+ ];
432
+ return paneResult({
433
+ body,
434
+ fileHits,
435
+ editHits: left.editHits,
436
+ cursor: left.cursor,
437
+ splitBody: true,
438
+ listScroll: left.listScroll,
439
+ });
440
+ };
441
+
442
+ const titleCell = (title, width, end) => {
443
+ const shown = trimVisible(title, width);
444
+ const pad = ' '.repeat(Math.max(0, width - visibleWidth(shown)));
445
+ return end ? `${pad}${shown}` : `${shown}${pad}`;
446
+ };
447
+
448
+ const paintTitles = (width, cols, color) => {
449
+ const gap = cols.gap || ' ';
450
+ const prefix = ' '.repeat(PAD_W);
451
+ let mid = titleCell(COL_TITLES.name, cols.nameW);
452
+ for (const field of VALUE_FIELDS) {
453
+ const size = colW(cols, field);
454
+ if (!size) continue;
455
+ mid += `${gap}${titleCell(COL_TITLES[field], size, field === 'plan')}`;
456
+ }
457
+ const room = Math.max(0, width - PAD_W - TAIL - visibleWidth(mid));
458
+ const text = `${prefix}${mid}${' '.repeat(room)}${' '.repeat(TAIL)}`;
459
+ if (!color) return text;
460
+ return paint(text, THEME.mutedFg, THEME.ctxBg, color);
461
+ };
462
+
463
+ const logBanner = (text, width, color, fgRgb) => {
464
+ const inner = Math.max(1, width - LOG_PAD_X * 2);
465
+ const shown = trimVisible(text, inner);
466
+ const lead = ' '.repeat(clamp(width, 0, LOG_PAD_X));
467
+ const pad = Math.max(0, width - lead.length - visibleWidth(shown));
468
+ const line = `${lead}${shown}${' '.repeat(pad)}`;
469
+ if (!color) return line;
470
+ return paint(line, fgRgb, THEME.ctxBg, color);
471
+ };
472
+
473
+ const paintAgentLog = (view, width, color, bodyH) => {
474
+ const head = [fill(width, THEME.ctxFg, THEME.ctxBg, color)];
475
+ const command = trimText(view.agentCommand);
476
+ if (command) head.push(logBanner(command, width, color, THEME.chromeFg));
477
+ const summary = [view.agentStatus, view.agentElapsed, view.agentProgress]
478
+ .filter(Boolean)
479
+ .join(' ');
480
+ if (summary) {
481
+ head.push(logBanner(summary, width, color, statusTone(view.agentStatus)));
482
+ }
483
+ const logH = Math.max(0, bodyH - head.length);
484
+ if (!logH) return paneResult({ body: head.slice(0, bodyH) });
485
+ const log = paintNpmLog(
486
+ {
487
+ npmOutput: view.agentOutput,
488
+ npmRunning: view.agentRunning,
489
+ npmFollow: view.agentFollow,
490
+ npmScroll: view.agentScroll,
491
+ progressFrame: view.progressFrame,
492
+ logScroller: true,
493
+ },
494
+ width,
495
+ color,
496
+ logH,
497
+ );
498
+ const bar = log.scrollBar;
499
+ if (bar) log.scrollBar = { ...bar, row: bar.row + head.length };
500
+ log.body.unshift(...head);
501
+ return log;
502
+ };
503
+
504
+ const withRunScroller = (
505
+ painted,
506
+ count,
507
+ start,
508
+ bodyH,
509
+ width,
510
+ color,
511
+ runCursor,
512
+ ) => {
513
+ const pad = bodyH >= LIST_PAD * 2 + 1 ? LIST_PAD : 0;
514
+ const rows = Math.max(1, bodyH - pad * 2);
515
+ const overflow = count > rows;
516
+ const body = painted.body.map((line, index) => {
517
+ const at = index - pad;
518
+ const onText = at >= 0 && at < rows;
519
+ let mark = '';
520
+ if (overflow && onText) mark = scrollMark(at, count, rows, start);
521
+ if (overflow && !onText) mark = 'track';
522
+ const selected = onText && runCursor === start + at;
523
+ const rowBg = selected ? THEME.checkBg : null;
524
+ return withLogScroller(line, mark, width, color, rowBg);
525
+ });
526
+ const fileHits = painted.fileHits;
527
+ if (!overflow) return { ...painted, body, fileHits };
528
+ const scrollBar = {
529
+ col: width,
530
+ row: 0,
531
+ height: body.length,
532
+ track: pad,
533
+ rows,
534
+ count,
535
+ start,
536
+ };
537
+ return { ...painted, body, fileHits, scrollBar };
538
+ };
539
+
540
+ const paintBodyAgents = (view, width, color, bodyH, headerLines) => {
541
+ if (view.agentView) return paintAgentLog(view, width, color, bodyH);
542
+ const { leftW, rightW } = chooserSplit(width);
543
+ const rows = view.agents ?? [];
544
+ const cursor = view.agentCursor ?? 0;
545
+ const fitted = fitCols(leftW, {
546
+ nameW: nameWidth(rows),
547
+ modelW: modelWidth(rows),
548
+ effortW: effortWidth(rows),
549
+ fastW: fastWidth(rows),
550
+ contextW: contextWidth(rows),
551
+ planW: planWidth(rows),
552
+ });
553
+ const cols = { ...fitted, gap: ' ' };
554
+ const listH = Math.max(0, bodyH - 1 - HEAD_GAP);
555
+ const pane = {
556
+ width: leftW,
557
+ color,
558
+ bodyH: listH,
559
+ headerLines: headerLines + 1 + HEAD_GAP,
560
+ offset: view.listScroll,
561
+ };
562
+ let search = null;
563
+ if (view.agentPick && view.agentQuery) {
564
+ const field = view.agentPick;
565
+ const query = view.agentQuery;
566
+ const editor = view.agentEditor;
567
+ search = { field, query, editor };
568
+ }
569
+ const paintRow = (entry, selected) =>
570
+ paintAgentRow(entry, leftW, color, selected, cols, selected && search);
571
+ const left = paintCursorList(rows, cursor, pane, paintRow);
572
+ left.body.unshift(paintTitles(leftW, cols, color));
573
+ left.body.unshift(paintBodyFill(leftW, color, 'files'));
574
+ const runs = view.agentRuns ?? [];
575
+ const fresh = { fresh: true, name: '<start new session>' };
576
+ const runRows = runs.length ? runs : [fresh];
577
+ const runCursor = view.agentRunCursor ?? 0;
578
+ const runW = statusWidth(runs);
579
+ const paintRun = (entry, selected) =>
580
+ paintRunRow(entry, rightW, color, selected, runW);
581
+ const runPane = {
582
+ width: rightW,
583
+ color,
584
+ bodyH,
585
+ headerLines,
586
+ offset: view.agentRunScroll ?? 0,
587
+ blank: buttonFill(rightW, color),
588
+ };
589
+ const right = paintCursorList(runRows, runCursor, runPane, paintRun);
590
+ const joined = joinPanes(left, right, width, color, bodyH);
591
+ joined.runScroll = right.listScroll;
592
+ let menu = joined;
593
+ if (view.agentMenu) {
594
+ menu = overlayMenu(joined, view, cols, cols.gap, width, color, headerLines);
595
+ }
596
+ return withRunScroller(
597
+ menu,
598
+ runRows.length,
599
+ right.listScroll,
600
+ bodyH,
601
+ width,
602
+ color,
603
+ runCursor,
604
+ );
605
+ };
606
+
607
+ const bubbleTone = (running, frame) => {
608
+ if (!running) return 'muted';
609
+ const index = Math.abs(frame ?? 0) % BUBBLE_PHASE.length;
610
+ return BUBBLE_PHASE[index];
611
+ };
612
+
613
+ const agentLead = (item, frame) => {
614
+ const segs = [seg(BUBBLE, bubbleTone(item.running, frame))];
615
+ if (item.running) segs.push(seg(` ${item.running}`, 'key', true));
616
+ return cellSegs(segs);
617
+ };
618
+
619
+ const agentName = (item) => {
620
+ const tone = item.bin ? 'text' : 'muted';
621
+ const name = flexCell(item.name, tone);
622
+ name.gap = BUBBLE_GAP;
623
+ return name;
624
+ };
625
+
626
+ const agentsBlock = (model, width, height, ctx, tile) => {
627
+ const data = model.agents;
628
+ if (!data || !data.ready) return waiting('detecting clis…');
629
+ const aside = [seg(`${data.running || 0}`, 'key', true)];
630
+ const titleLine = titleAside(tile, aside, width);
631
+ const items = data.items ?? [];
632
+ if (!items.length) return notice('no agents', titleLine);
633
+ const shown = items.slice(0, Math.max(0, height));
634
+ const rows = shown.map((item) => [
635
+ agentLead(item, ctx.frame),
636
+ agentName(item),
637
+ cell(item.bin ? item.model : 'missing', item.bin ? 'sha' : 'muted', 'r'),
638
+ ]);
639
+ return { titleLine, lines: tableLines(rows, width) };
640
+ };
641
+
642
+ module.exports = { paintBodyAgents, agentsBlock };
@@ -1,10 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const ansi = require('../ansi.js');
4
- const files = require('../files.js');
3
+ const ansi = require('../term/ansi.js');
4
+ const { shortAge } = require('../common/format.js');
5
5
  const primitives = require('./primitives.js');
6
6
 
7
- const { shortAge } = files;
8
7
  const { THEME, paint, visibleWidth, trimVisible, seq, EL } = ansi;
9
8
  const { MARK_W, markPrefix, branchLabel, editSpan } = primitives;
10
9
  const { paintCursorList, fieldWindow } = primitives;
@@ -124,8 +123,8 @@ const paintBodyBranches = (view, width, color, bodyH, headerLines) => {
124
123
  cols.name = Math.min(BRANCH_NAME_MAX, cols.name);
125
124
  const cursor = view.branchCursor ?? 0;
126
125
  const { compose } = view;
127
- const edit =
128
- compose?.kind === 'branch' ? paintBranchEdit(compose, width, color) : null;
126
+ const branchEdit = compose?.kind === 'branch';
127
+ const edit = branchEdit ? paintBranchEdit(compose, width, color) : null;
129
128
  const pane = { width, color, bodyH, headerLines, offset: view.listScroll };
130
129
  const paintRow = (entry, selected) =>
131
130
  paintBranchRow(entry, width, color, selected, cols);