@addai/node 0.4.0 → 0.5.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.
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
- // `entities-runtime harnesses` interactive harness manager in the
3
- // Claude Code terminal idiom: alt-screen, rounded panels, dim chrome,
4
- // ❯ selection, braille spinner, ⏺ status dots. Hand-rolled ANSI — the
5
- // daemon deliberately has no TUI framework dependency.
2
+ // Which agent CLIs this box has, and who they are logged in as.
6
3
  //
7
- // Local actions hand the real terminal to the harness CLI itself (there
8
- // is nothing to bridge when you're already at the machine): install
9
- // streams npm output into a panel; login leaves the alt screen, runs the
10
- // CLI's own login flow attached to your TTY, then re-probes on return.
4
+ // This used to be a second, parallel TUI with its own ANSI helpers, its own
5
+ // panel, its own key loop and its own alt screen; entering it from the
6
+ // dashboard suspended the whole app and the two fought over the terminal.
7
+ // It is now a screen on the stack like any other, so it inherits the diff
8
+ // painter, the footer grammar and the `?` help for free.
9
+ //
10
+ // One thing genuinely does need the raw terminal: a harness login runs the
11
+ // vendor's own CLI flow, which draws its own prompts. That still leaves the
12
+ // alt screen and hands the TTY over — but as a scoped suspend, not as a
13
+ // parallel application.
11
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
15
  if (k2 === undefined) k2 = k;
13
16
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -42,48 +45,28 @@ var __importStar = (this && this.__importStar) || (function () {
42
45
  };
43
46
  })();
44
47
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.runHarnessesTui = runHarnessesTui;
48
+ exports.actionFor = actionFor;
49
+ exports.formatVersion = formatVersion;
50
+ exports.accountLabel = accountLabel;
51
+ exports.harnessColumns = harnessColumns;
52
+ exports.harnessRow = harnessRow;
53
+ exports.renderHarnesses = renderHarnesses;
54
+ exports.logoutSelected = logoutSelected;
55
+ exports.createHarnessesScreen = createHarnessesScreen;
56
+ exports.plainStatus = plainStatus;
46
57
  const child_process_1 = require("child_process");
47
- const readline = __importStar(require("readline"));
48
- const capabilities_1 = require("../capabilities");
49
- const win_1 = require("../win");
50
58
  const fs = __importStar(require("fs"));
51
59
  const os = __importStar(require("os"));
52
60
  const path = __importStar(require("path"));
61
+ const capabilities_1 = require("../capabilities");
62
+ const win_1 = require("../win");
53
63
  const harness_registry_1 = require("../harness-registry");
54
64
  const render_1 = require("./render");
55
- /* ── ansi helpers ────────────────────────────────────────────────────── */
56
- const ESC = '\x1b[';
57
- const dim = (s) => `${ESC}2m${s}${ESC}22m`;
58
- const bold = (s) => `${ESC}1m${s}${ESC}22m`;
59
- const fg = (n, s) => `${ESC}38;5;${n}m${s}${ESC}39m`;
60
- const green = (s) => fg(114, s);
61
- const yellow = (s) => fg(179, s);
62
- const grey = (s) => fg(244, s);
63
- const cyan = (s) => fg(80, s);
64
- const altOn = () => process.stdout.write(`${ESC}?1049h${ESC}?25l`);
65
- const altOff = () => process.stdout.write(`${ESC}?1049l${ESC}?25h`);
66
- const home = () => process.stdout.write(`${ESC}H${ESC}2J`);
67
- const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
68
- const width = () => Math.min(process.stdout.columns || 100, 110);
69
- function panel(title, lines) {
70
- const w = width() - 2;
71
- const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
72
- const top = `${dim('╭─')} ${bold(title)} ${dim('─'.repeat(Math.max(1, w - strip(title).length - 4)) + '╮')}`;
73
- const body = lines.map(raw => {
74
- // Rows are wider than a narrow terminal: without a cut they run past
75
- // the right border and the panel frame comes apart.
76
- const l = (0, render_1.truncate)(raw, w - 2);
77
- const pad = Math.max(0, w - 2 - strip(l).length);
78
- return `${dim('│')} ${l}${' '.repeat(pad)} ${dim('│')}`;
79
- });
80
- const bottom = dim(`╰${'─'.repeat(w)}╯`);
81
- return [top, ...body, bottom].join('\n');
82
- }
65
+ const app_1 = require("./app");
83
66
  function dot(p) {
84
67
  if (!p?.available)
85
- return grey('⏺');
86
- return p.authed ? green('⏺') : yellow('⏺');
68
+ return (0, render_1.grey)('⏺');
69
+ return p.authed ? (0, render_1.green)('⏺') : (0, render_1.yellow)('⏺');
87
70
  }
88
71
  function actionFor(id, p) {
89
72
  if (!p?.available)
@@ -92,49 +75,102 @@ function actionFor(id, p) {
92
75
  return 'log in';
93
76
  return harness_registry_1.HARNESSES[id].logout ? 'log out' : 'ok';
94
77
  }
95
- function row(id, p, selected) {
78
+ /**
79
+ * Harness CLIs report their version in whatever shape they feel like:
80
+ * `2.1.220 (Claude Code)`, `codex-cli 0.139.0`, `0.52.0`. Prefixing all of
81
+ * those with a `v` produced `vcodex-cli 0.139.0`. Pull out the version
82
+ * itself and leave the branding to the label column.
83
+ */
84
+ function formatVersion(v) {
85
+ if (!v)
86
+ return '—';
87
+ const m = v.match(/\d+\.\d+(\.\d+)?\S*/);
88
+ return m ? `v${m[0]}` : v;
89
+ }
90
+ function accountLabel(p) {
91
+ if (!p?.available)
92
+ return 'not installed';
93
+ if (!p.authed)
94
+ return 'not logged in';
95
+ const who = p.account ?? p.accountKind ?? 'logged in';
96
+ return p.plan ? `${who} · ${p.plan}` : who;
97
+ }
98
+ /**
99
+ * Column widths.
100
+ *
101
+ * The account and the effort ladder vary most, so they take the spare width;
102
+ * version and action are fixed at what their values actually need. The old
103
+ * fixed widths cut `codex-cli` down to `vcodex-cli` and
104
+ * `low/medium/high/xhigh` to `low/medium/high/x`.
105
+ */
106
+ function harnessColumns(width) {
107
+ // dot, name, version, account, models, efforts, action
108
+ return (0, render_1.layoutColumns)([
109
+ { min: 1 },
110
+ { min: 12 },
111
+ { min: 9, grow: 1 },
112
+ { min: 16, grow: 2 },
113
+ { min: 8 },
114
+ // claude's ladder is low/medium/high/xhigh/max — 25 columns of it.
115
+ { min: 14, grow: 3 },
116
+ { min: 14 },
117
+ ], Math.max(40, width - 4));
118
+ }
119
+ function harnessRow(id, p, selected, width) {
96
120
  const spec = harness_registry_1.HARNESSES[id];
97
- // Budget: the action hint on the right is the point of the row, so the
98
- // wide informational columns give up space before it does. At 100 cols
99
- // the old widths pushed the action past the border and it was cut.
100
- const name = spec.label.padEnd(13);
101
- const ver = (p?.version ? `v${p.version}` : '—').padEnd(11).slice(0, 11);
102
- const who = (p?.authed
103
- ? `${p.account ?? p.accountKind ?? 'logged in'}${p.plan ? ` · ${p.plan}` : ''}`
104
- : p?.available ? 'not logged in' : 'not installed').padEnd(24).slice(0, 24);
105
- const models = `${p?.models?.length ?? 0} models`.padEnd(9);
106
- const efforts = spec.efforts.length ? spec.efforts.join('/') : '—';
121
+ const w = harnessColumns(width);
107
122
  const act = actionFor(id, p);
108
- const actTxt = act === 'ok' ? green('✓') : act === 'log out' ? dim('↵ log out') : cyan(`↵ ${act}`);
109
- const line = `${dot(p)} ${name} ${dim(ver)} ${who} ${dim(models)} ${dim(efforts.padEnd(17).slice(0, 17))} ${actTxt}`;
110
- return selected ? `${cyan('❯')} ${bold(line)}` : ` ${line}`;
123
+ const action = act === 'ok' ? (0, render_1.green)('✓ ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${act}`) : (0, render_1.cyan)(`⏎ ${act}`);
124
+ const line = (0, render_1.tableRow)([
125
+ dot(p),
126
+ selected ? (0, render_1.bold)(spec.label) : spec.label,
127
+ (0, render_1.dim)(formatVersion(p?.version)),
128
+ accountLabel(p),
129
+ (0, render_1.dim)(`${p?.models?.length ?? 0} models`),
130
+ (0, render_1.dim)(spec.efforts.length ? spec.efforts.join('/') : '—'),
131
+ action,
132
+ ], w);
133
+ return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
111
134
  }
112
- function render(st) {
113
- home();
114
- const lines = [];
135
+ function renderHarnesses(st, width, height) {
136
+ const out = [];
137
+ const authed = st.caps ? harness_registry_1.HARNESS_IDS.filter(id => st.caps?.[id]?.authed).length : 0;
138
+ out.push((0, app_1.heading)('HARNESSES', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'probing…'));
139
+ out.push('');
115
140
  if (!st.caps) {
116
- lines.push(`${cyan(SPIN[st.spin % SPIN.length])} probing installed harnesses…`);
141
+ out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('probing installed harnesses…')}`);
117
142
  }
118
143
  else {
119
- lines.push(dim(' harness version account models efforts action'));
120
- harness_registry_1.HARNESS_IDS.forEach((id, i) => lines.push(row(id, st.caps[id], i === st.sel)));
144
+ const w = harnessColumns(width);
145
+ out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'HARNESS', 'VERSION', 'ACCOUNT', 'MODELS', 'EFFORTS', ''], w)));
146
+ harness_registry_1.HARNESS_IDS.forEach((id, i) => out.push(harnessRow(id, st.caps?.[id], i === st.sel, width)));
121
147
  }
122
148
  if (st.busy)
123
- lines.push('', `${cyan(SPIN[st.spin % SPIN.length])} ${st.busy}`);
149
+ out.push('', ` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${st.busy}`);
124
150
  if (st.log.length) {
125
- lines.push('');
126
- st.log.slice(-8).forEach(l => lines.push(dim(l.slice(0, width() - 8))));
151
+ out.push('');
152
+ // Whatever room is left after the chrome, so an install's output fills
153
+ // the screen rather than a fixed eight lines.
154
+ const room = Math.max(3, height - out.length - 3);
155
+ st.log.slice(-room).forEach(l => out.push((0, render_1.dim)(` ${l}`)));
127
156
  }
128
157
  if (st.note)
129
- lines.push('', st.note);
130
- process.stdout.write(panel('Harnesses', lines) + '\n');
131
- process.stdout.write(dim(' ↑/↓ move · ↵ install / log in · r refresh · q quit\n'));
158
+ out.push('', ` ${st.note}`);
159
+ while (out.length < height - 1)
160
+ out.push('');
161
+ out.push((0, app_1.footerHint)([
162
+ { keys: '↑↓', label: 'move' },
163
+ { keys: '⏎', label: 'install / log in / log out' },
164
+ { keys: 'r', label: 'refresh' },
165
+ { keys: 'q', label: 'back' },
166
+ ]));
167
+ return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
132
168
  }
133
169
  /* ── actions ─────────────────────────────────────────────────────────── */
134
170
  async function installSelected(st, id, redraw) {
135
171
  const spec = harness_registry_1.HARNESSES[id];
136
172
  if (!(0, harness_registry_1.isInstallable)(id)) {
137
- st.note = yellow(`${spec.label}: ${spec.login.instructions}`);
173
+ st.note = (0, render_1.yellow)(`${spec.label}: ${spec.login.instructions}`);
138
174
  return;
139
175
  }
140
176
  st.busy = `installing ${spec.label} (npm i -g ${spec.npmPackage})`;
@@ -151,10 +187,10 @@ async function installSelected(st, id, redraw) {
151
187
  child.stderr?.on('data', onChunk);
152
188
  child.on('exit', code => {
153
189
  st.busy = null;
154
- st.note = code === 0 ? green(`✓ ${spec.label} installed`) : fg(203, `✗ npm exited ${code}`);
190
+ st.note = code === 0 ? (0, render_1.green)(`✓ ${spec.label} installed`) : (0, render_1.red)(`✗ npm exited ${code}`);
155
191
  resolve();
156
192
  });
157
- child.on('error', err => { st.busy = null; st.note = fg(203, `✗ ${err.message}`); resolve(); });
193
+ child.on('error', err => { st.busy = null; st.note = (0, render_1.red)(`✗ ${err.message}`); resolve(); });
158
194
  });
159
195
  }
160
196
  function logoutSelected(id) {
@@ -186,93 +222,70 @@ function logoutSelected(id) {
186
222
  }
187
223
  return null;
188
224
  }
225
+ /** Runs with the real terminal handed over — see `suspend` below. */
189
226
  function loginSelected(id) {
190
227
  const spec = harness_registry_1.HARNESSES[id];
191
228
  const bin = spec.findBinary();
192
229
  if (!bin)
193
230
  return `${spec.label} is not installed`;
194
- // Hand the real terminal to the CLI leave the alt screen first.
195
- altOff();
196
- console.log(cyan(`\n— ${spec.label} login —`));
197
- console.log(dim(spec.login.instructions) + '\n');
231
+ console.log((0, render_1.cyan)(`\n${spec.label} login —`));
232
+ console.log((0, render_1.dim)(spec.login.instructions) + '\n');
198
233
  const args = spec.login.strategy === 'pty-url-code' ? (spec.login.loginArgs ?? []) : [];
199
234
  const inv = (0, win_1.resolveCliInvocation)(bin, args);
200
235
  const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'inherit', env: process.env });
201
- altOn();
202
236
  return res.status === 0 ? null : `login exited with code ${res.status ?? '?'}`;
203
237
  }
204
- /* ── entry ───────────────────────────────────────────────────────────── */
205
- async function plainStatus() {
206
- const caps = await (0, capabilities_1.probeCapabilities)();
207
- for (const id of harness_registry_1.HARNESS_IDS) {
208
- const p = caps[id];
209
- const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
210
- console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
211
- }
212
- }
213
- /**
214
- * Run the harness manager.
215
- *
216
- * The returned promise resolves when the USER LEAVES the screen — not when
217
- * the first capability probe finishes. Resolving early is what made the
218
- * embedded case flicker: the dashboard re-attached its own keypress handler
219
- * and repainted while this screen was still live, and the two fought over
220
- * the terminal.
221
- *
222
- * `embedded` distinguishes `ainode harnesses` (q exits the process) from
223
- * the dashboard's `h` (q hands control back to the caller). Ctrl-C always
224
- * ends the process.
225
- */
226
- async function runHarnessesTui(opts = {}) {
227
- if (!process.stdout.isTTY || !process.stdin.isTTY) {
228
- await plainStatus();
229
- return;
230
- }
231
- const st = { caps: null, sel: 0, spin: 0, busy: null, log: [], note: null, confirmLogout: null };
232
- const redraw = () => render(st);
233
- altOn();
234
- readline.emitKeypressEvents(process.stdin);
235
- process.stdin.setRawMode(true);
236
- process.stdin.resume();
237
- let done = null;
238
- const spinTimer = setInterval(() => { st.spin++; if (st.busy || !st.caps)
239
- redraw(); }, 90);
240
- const detach = () => {
241
- clearInterval(spinTimer);
242
- process.stdin.off('keypress', onKeypress);
243
- };
244
- const quit = (hard) => {
245
- detach();
246
- if (opts.embedded && !hard) {
247
- done?.();
248
- return;
249
- }
250
- process.stdin.setRawMode(false);
251
- process.stdin.pause();
252
- altOff();
253
- process.exit(0);
238
+ /* ── screen ──────────────────────────────────────────────────────────── */
239
+ function createHarnessesScreen(deps) {
240
+ const st = {
241
+ caps: null, sel: 0, spin: 0, busy: null, confirmLogout: null, log: [], note: null,
254
242
  };
243
+ const probe = deps.probe ?? (() => (0, capabilities_1.probeCapabilities)());
244
+ const redraw = () => deps.host.redraw();
245
+ let acting = false;
255
246
  const reprobe = async () => {
256
247
  st.caps = null;
257
248
  redraw();
258
- st.caps = await (0, capabilities_1.probeCapabilities)();
249
+ st.caps = await probe();
259
250
  redraw();
260
251
  };
261
- let acting = false;
262
- function onKeypress(_str, key) {
263
- void (async () => {
264
- if (!key || acting)
252
+ return {
253
+ id: 'harnesses',
254
+ title: 'Harnesses',
255
+ render: (width, height) => renderHarnesses(st, width, height),
256
+ // Probing spawns five CLIs; do it on open and then only on request.
257
+ pollMs: () => 120_000,
258
+ poll: reprobe,
259
+ tick(n) {
260
+ st.spin = n;
261
+ // The only moving parts are the probe and install spinners.
262
+ return st.busy !== null || st.caps === null;
263
+ },
264
+ keys: () => [
265
+ { keys: '↑↓ / jk', label: 'move between harnesses' },
266
+ { keys: '⏎', label: 'install, log in, or arm a log out' },
267
+ { keys: 'y', label: 'confirm a log out' },
268
+ { keys: 'r', label: 'probe again' },
269
+ ],
270
+ async onKey(key) {
271
+ if (acting || !key)
265
272
  return;
266
- if (key.ctrl && key.name === 'c') {
267
- quit(true);
273
+ if (key.name === 'up' || key.name === 'k') {
274
+ st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
275
+ redraw();
268
276
  return;
269
277
  }
270
- if (key.name === 'q') {
271
- quit(false);
278
+ if (key.name === 'down' || key.name === 'j') {
279
+ st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
280
+ redraw();
272
281
  return;
273
282
  }
274
283
  if (!st.caps)
275
284
  return;
285
+ if (key.name === 'r') {
286
+ await reprobe();
287
+ return;
288
+ }
276
289
  if (st.confirmLogout) {
277
290
  const target = st.confirmLogout;
278
291
  st.confirmLogout = null;
@@ -280,7 +293,7 @@ async function runHarnessesTui(opts = {}) {
280
293
  acting = true;
281
294
  try {
282
295
  const err = logoutSelected(target);
283
- st.note = err ? fg(203, `✗ ${err}`) : green(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
296
+ st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
284
297
  await reprobe();
285
298
  }
286
299
  finally {
@@ -288,25 +301,11 @@ async function runHarnessesTui(opts = {}) {
288
301
  }
289
302
  }
290
303
  else {
291
- st.note = dim('logout cancelled');
304
+ st.note = (0, render_1.dim)('logout cancelled');
292
305
  redraw();
293
306
  }
294
307
  return;
295
308
  }
296
- if (key.name === 'up' || key.name === 'k') {
297
- st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
298
- redraw();
299
- return;
300
- }
301
- if (key.name === 'down' || key.name === 'j') {
302
- st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
303
- redraw();
304
- return;
305
- }
306
- if (key.name === 'r') {
307
- await reprobe();
308
- return;
309
- }
310
309
  if (key.name !== 'return')
311
310
  return;
312
311
  const id = harness_registry_1.HARNESS_IDS[st.sel];
@@ -316,39 +315,45 @@ async function runHarnessesTui(opts = {}) {
316
315
  if (!p?.available) {
317
316
  await installSelected(st, id, redraw);
318
317
  await reprobe();
318
+ return;
319
319
  }
320
- else if (!p.authed) {
321
- const err = loginSelected(id);
322
- st.note = err ? fg(203, `✗ ${err}`) : green('✓ login flow finished — verifying…');
320
+ if (!p.authed) {
321
+ let err = null;
322
+ // The vendor CLI draws its own prompts, so it needs the real
323
+ // terminal — and it must have it back before we repaint.
324
+ await deps.suspend(() => { err = loginSelected(id); });
325
+ st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)('✓ login flow finished — verifying…');
326
+ redraw();
323
327
  await reprobe();
324
328
  const now = st.caps?.[id];
325
329
  if (now?.authed)
326
- st.note = green(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
330
+ st.note = (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
327
331
  else if (!err)
328
- st.note = yellow(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
332
+ st.note = (0, render_1.yellow)(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
329
333
  redraw();
334
+ return;
330
335
  }
331
- else if (harness_registry_1.HARNESSES[id].logout) {
336
+ if (harness_registry_1.HARNESSES[id].logout) {
332
337
  st.confirmLogout = id;
333
- st.note = yellow(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
334
- redraw();
335
- }
336
- else {
337
- st.note = green(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
338
+ st.note = (0, render_1.yellow)(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
338
339
  redraw();
340
+ return;
339
341
  }
342
+ st.note = (0, render_1.green)(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
343
+ redraw();
340
344
  }
341
345
  finally {
342
346
  acting = false;
343
347
  }
344
- })();
348
+ },
349
+ };
350
+ }
351
+ /** `ainode harnesses` on a pipe: no screen, just the facts. */
352
+ async function plainStatus() {
353
+ const caps = await (0, capabilities_1.probeCapabilities)();
354
+ for (const id of harness_registry_1.HARNESS_IDS) {
355
+ const p = caps[id];
356
+ const state = !p?.available ? 'not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'not logged in';
357
+ console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
345
358
  }
346
- // The exit promise is created BEFORE the first probe, not after it.
347
- // Probing five CLIs takes seconds; if `q` arrives during that window and
348
- // `done` isn't wired yet, quit() detaches the key listener and resolves
349
- // nothing — the screen wedges with no way out.
350
- const exited = new Promise(resolve => { done = resolve; });
351
- process.stdin.on('keypress', onKeypress);
352
- void reprobe();
353
- await exited;
354
359
  }
@@ -0,0 +1,18 @@
1
+ import { type AppHost, type Screen } from './app';
2
+ import type { CapturedLog, ConsoleCapture } from './console-capture';
3
+ export interface LogsState {
4
+ scroll: number;
5
+ follow: boolean;
6
+ filter: string;
7
+ filtering: boolean;
8
+ viewportRows: number;
9
+ }
10
+ export declare function logLine(l: CapturedLog, width: number): string;
11
+ export declare function filterLogs(lines: CapturedLog[], filter: string): CapturedLog[];
12
+ export declare function renderLogs(st: LogsState, lines: CapturedLog[], width: number, height: number, emptyHint?: string): string[];
13
+ export declare function createLogsScreen(deps: {
14
+ host: AppHost;
15
+ logs: ConsoleCapture;
16
+ /** Shown instead of "nothing yet" when this process isn't the daemon. */
17
+ emptyHint?: string;
18
+ }): Screen;
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ // What the daemon is saying.
3
+ //
4
+ // The console UI has to take stdout away from the daemon to paint at all, so
5
+ // without this screen the pump's own account of itself — backoffs, wake
6
+ // detection, session chatter — is invisible for as long as the UI is open.
7
+ // Here it is, tailing live, with the same scrollback grammar as the
8
+ // transcript screen.
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.logLine = logLine;
11
+ exports.filterLogs = filterLogs;
12
+ exports.renderLogs = renderLogs;
13
+ exports.createLogsScreen = createLogsScreen;
14
+ const render_1 = require("./render");
15
+ const app_1 = require("./app");
16
+ /** Lines the screen spends on things that are not log lines. */
17
+ const CHROME = 4;
18
+ function stamp(at) {
19
+ const d = new Date(at);
20
+ const p = (n) => String(n).padStart(2, '0');
21
+ return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
22
+ }
23
+ function logLine(l, width) {
24
+ const colour = l.level === 'error' ? render_1.red : l.level === 'warn' ? render_1.yellow : render_1.dim;
25
+ const text = l.text.replace(/\s+/g, ' ');
26
+ const prefix = ` ${(0, render_1.grey)(stamp(l.at))} `;
27
+ return (0, render_1.truncate)(prefix + colour(text), width);
28
+ }
29
+ function filterLogs(lines, filter) {
30
+ const f = filter.trim().toLowerCase();
31
+ if (!f)
32
+ return lines;
33
+ return lines.filter(l => l.text.toLowerCase().includes(f));
34
+ }
35
+ function renderLogs(st, lines, width, height, emptyHint) {
36
+ const shown = filterLogs(lines, st.filter);
37
+ const rows = Math.max(1, height - CHROME);
38
+ st.viewportRows = rows;
39
+ const out = [];
40
+ const scope = st.filter
41
+ ? `${shown.length} matching “${st.filter}”`
42
+ : `${shown.length} lines this session`;
43
+ out.push((0, app_1.heading)('LOGS', scope));
44
+ out.push('');
45
+ if (shown.length === 0) {
46
+ if (st.filter)
47
+ out.push((0, render_1.dim)(' nothing matches that filter'));
48
+ else
49
+ out.push((0, render_1.dim)(` ${emptyHint ?? 'the daemon has said nothing yet'}`));
50
+ }
51
+ else {
52
+ const maxScroll = Math.max(0, shown.length - rows);
53
+ const start = st.follow ? maxScroll : Math.min(st.scroll, maxScroll);
54
+ st.scroll = start;
55
+ shown.slice(start, start + rows).forEach(l => out.push(logLine(l, width)));
56
+ }
57
+ while (out.length < height - 1)
58
+ out.push('');
59
+ const tail = st.follow ? ` ${(0, render_1.dim)('tailing')}` : '';
60
+ out.push(st.filtering
61
+ ? ` /${st.filter}▏`
62
+ : (0, app_1.footerHint)([
63
+ { keys: '↑↓ PgUp PgDn', label: 'scroll' },
64
+ { keys: 'G', label: 'tail' },
65
+ { keys: '/', label: 'filter' },
66
+ { keys: 'q', label: 'back' },
67
+ ]) + tail);
68
+ return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
69
+ }
70
+ function createLogsScreen(deps) {
71
+ const st = { scroll: 0, follow: true, filter: '', filtering: false, viewportRows: 20 };
72
+ return {
73
+ id: 'logs',
74
+ title: 'Logs',
75
+ render: (width, height) => renderLogs(st, deps.logs.lines(), width, height, deps.emptyHint),
76
+ // Nothing to fetch — new lines arrive by callback. The poll only exists
77
+ // so a tailing view repaints while the daemon talks.
78
+ pollMs: () => 1000,
79
+ capturesKeys: () => st.filtering,
80
+ async poll() { if (st.follow)
81
+ deps.host.redraw(); },
82
+ keys: () => [
83
+ { keys: '↑↓ / jk', label: 'scroll a line — stops tailing' },
84
+ { keys: 'PgUp/PgDn', label: 'scroll a screenful' },
85
+ { keys: 'g / G', label: 'first line / tail the newest' },
86
+ { keys: '/', label: 'filter lines by text' },
87
+ ],
88
+ onKey(key) {
89
+ if (st.filtering) {
90
+ if (key.name === 'return') {
91
+ st.filtering = false;
92
+ deps.host.redraw();
93
+ return;
94
+ }
95
+ if (key.name === 'escape') {
96
+ st.filtering = false;
97
+ st.filter = '';
98
+ deps.host.redraw();
99
+ return;
100
+ }
101
+ if (key.name === 'backspace') {
102
+ st.filter = st.filter.slice(0, -1);
103
+ deps.host.redraw();
104
+ return;
105
+ }
106
+ const ch = key.sequence ?? key.name ?? '';
107
+ if (ch.length === 1 && ch >= ' ') {
108
+ st.filter += ch;
109
+ deps.host.redraw();
110
+ }
111
+ return;
112
+ }
113
+ const page = Math.max(1, st.viewportRows - 1);
114
+ if (key.name === 'up' || key.name === 'k') {
115
+ st.follow = false;
116
+ st.scroll = Math.max(0, st.scroll - 1);
117
+ deps.host.redraw();
118
+ return;
119
+ }
120
+ if (key.name === 'down' || key.name === 'j') {
121
+ st.follow = false;
122
+ st.scroll += 1;
123
+ deps.host.redraw();
124
+ return;
125
+ }
126
+ if (key.name === 'pageup') {
127
+ st.follow = false;
128
+ st.scroll = Math.max(0, st.scroll - page);
129
+ deps.host.redraw();
130
+ return;
131
+ }
132
+ if (key.name === 'pagedown') {
133
+ st.follow = false;
134
+ st.scroll += page;
135
+ deps.host.redraw();
136
+ return;
137
+ }
138
+ if (key.name === 'g' && !key.shift) {
139
+ st.follow = false;
140
+ st.scroll = 0;
141
+ deps.host.redraw();
142
+ return;
143
+ }
144
+ if (key.name === 'g' && key.shift) {
145
+ st.follow = true;
146
+ deps.host.redraw();
147
+ return;
148
+ }
149
+ if (key.name === '/' || key.sequence === '/') {
150
+ st.filtering = true;
151
+ st.filter = '';
152
+ deps.host.redraw();
153
+ return;
154
+ }
155
+ },
156
+ };
157
+ }