@addai/node 0.4.0 → 0.5.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.
@@ -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,105 @@ 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
+ // The verb is Title Case in the row the same way every other label is; the
124
+ // value actionFor returns stays lowercase because the key handler reads it.
125
+ const verb = act.charAt(0).toUpperCase() + act.slice(1);
126
+ const action = act === 'ok' ? (0, render_1.green)('✓ Ready') : act === 'log out' ? (0, render_1.dim)(`⏎ ${verb}`) : (0, render_1.cyan)(`⏎ ${verb}`);
127
+ const line = (0, render_1.tableRow)([
128
+ dot(p),
129
+ selected ? (0, render_1.bold)(spec.label) : spec.label,
130
+ (0, render_1.dim)(formatVersion(p?.version)),
131
+ accountLabel(p),
132
+ (0, render_1.dim)(`${p?.models?.length ?? 0} models`),
133
+ (0, render_1.dim)(spec.efforts.length ? spec.efforts.join('/') : '—'),
134
+ action,
135
+ ], w);
136
+ return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
111
137
  }
112
- function render(st) {
113
- home();
114
- const lines = [];
138
+ function renderHarnesses(st, width, height) {
139
+ const out = [];
140
+ const authed = st.caps ? harness_registry_1.HARNESS_IDS.filter(id => st.caps?.[id]?.authed).length : 0;
141
+ out.push((0, app_1.heading)('Harnesses', st.caps ? `${authed}/${harness_registry_1.HARNESS_IDS.length} ready` : 'Probing…'));
142
+ out.push('');
115
143
  if (!st.caps) {
116
- lines.push(`${cyan(SPIN[st.spin % SPIN.length])} probing installed harnesses…`);
144
+ out.push(` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${(0, render_1.dim)('Probing installed harnesses…')}`);
117
145
  }
118
146
  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)));
147
+ const w = harnessColumns(width);
148
+ out.push((0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'Harness', 'Version', 'Account', 'Models', 'Efforts', ''], w)));
149
+ harness_registry_1.HARNESS_IDS.forEach((id, i) => out.push(harnessRow(id, st.caps?.[id], i === st.sel, width)));
121
150
  }
122
151
  if (st.busy)
123
- lines.push('', `${cyan(SPIN[st.spin % SPIN.length])} ${st.busy}`);
152
+ out.push('', ` ${(0, render_1.cyan)(render_1.SPIN[st.spin % render_1.SPIN.length])} ${st.busy}`);
124
153
  if (st.log.length) {
125
- lines.push('');
126
- st.log.slice(-8).forEach(l => lines.push(dim(l.slice(0, width() - 8))));
154
+ out.push('');
155
+ // Whatever room is left after the chrome, so an install's output fills
156
+ // the screen rather than a fixed eight lines.
157
+ const room = Math.max(3, height - out.length - 3);
158
+ st.log.slice(-room).forEach(l => out.push((0, render_1.dim)(` ${l}`)));
127
159
  }
128
160
  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'));
161
+ out.push('', ` ${st.note}`);
162
+ while (out.length < height - 1)
163
+ out.push('');
164
+ out.push((0, app_1.footerHint)([
165
+ { keys: '↑↓', label: 'move' },
166
+ { keys: '⏎', label: 'install / log in / log out' },
167
+ { keys: 'r', label: 'refresh' },
168
+ { keys: 'q', label: 'back' },
169
+ ]));
170
+ return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
132
171
  }
133
172
  /* ── actions ─────────────────────────────────────────────────────────── */
134
173
  async function installSelected(st, id, redraw) {
135
174
  const spec = harness_registry_1.HARNESSES[id];
136
175
  if (!(0, harness_registry_1.isInstallable)(id)) {
137
- st.note = yellow(`${spec.label}: ${spec.login.instructions}`);
176
+ st.note = (0, render_1.yellow)(`${spec.label}: ${spec.login.instructions}`);
138
177
  return;
139
178
  }
140
179
  st.busy = `installing ${spec.label} (npm i -g ${spec.npmPackage})`;
@@ -151,10 +190,10 @@ async function installSelected(st, id, redraw) {
151
190
  child.stderr?.on('data', onChunk);
152
191
  child.on('exit', code => {
153
192
  st.busy = null;
154
- st.note = code === 0 ? green(`✓ ${spec.label} installed`) : fg(203, `✗ npm exited ${code}`);
193
+ st.note = code === 0 ? (0, render_1.green)(`✓ ${spec.label} installed`) : (0, render_1.red)(`✗ npm exited ${code}`);
155
194
  resolve();
156
195
  });
157
- child.on('error', err => { st.busy = null; st.note = fg(203, `✗ ${err.message}`); resolve(); });
196
+ child.on('error', err => { st.busy = null; st.note = (0, render_1.red)(`✗ ${err.message}`); resolve(); });
158
197
  });
159
198
  }
160
199
  function logoutSelected(id) {
@@ -186,93 +225,70 @@ function logoutSelected(id) {
186
225
  }
187
226
  return null;
188
227
  }
228
+ /** Runs with the real terminal handed over — see `suspend` below. */
189
229
  function loginSelected(id) {
190
230
  const spec = harness_registry_1.HARNESSES[id];
191
231
  const bin = spec.findBinary();
192
232
  if (!bin)
193
233
  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');
234
+ console.log((0, render_1.cyan)(`\n${spec.label} login —`));
235
+ console.log((0, render_1.dim)(spec.login.instructions) + '\n');
198
236
  const args = spec.login.strategy === 'pty-url-code' ? (spec.login.loginArgs ?? []) : [];
199
237
  const inv = (0, win_1.resolveCliInvocation)(bin, args);
200
238
  const res = (0, child_process_1.spawnSync)(inv.file, inv.args, { stdio: 'inherit', env: process.env });
201
- altOn();
202
239
  return res.status === 0 ? null : `login exited with code ${res.status ?? '?'}`;
203
240
  }
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);
241
+ /* ── screen ──────────────────────────────────────────────────────────── */
242
+ function createHarnessesScreen(deps) {
243
+ const st = {
244
+ caps: null, sel: 0, spin: 0, busy: null, confirmLogout: null, log: [], note: null,
254
245
  };
246
+ const probe = deps.probe ?? (() => (0, capabilities_1.probeCapabilities)());
247
+ const redraw = () => deps.host.redraw();
248
+ let acting = false;
255
249
  const reprobe = async () => {
256
250
  st.caps = null;
257
251
  redraw();
258
- st.caps = await (0, capabilities_1.probeCapabilities)();
252
+ st.caps = await probe();
259
253
  redraw();
260
254
  };
261
- let acting = false;
262
- function onKeypress(_str, key) {
263
- void (async () => {
264
- if (!key || acting)
255
+ return {
256
+ id: 'harnesses',
257
+ title: 'Harnesses',
258
+ render: (width, height) => renderHarnesses(st, width, height),
259
+ // Probing spawns five CLIs; do it on open and then only on request.
260
+ pollMs: () => 120_000,
261
+ poll: reprobe,
262
+ tick(n) {
263
+ st.spin = n;
264
+ // The only moving parts are the probe and install spinners.
265
+ return st.busy !== null || st.caps === null;
266
+ },
267
+ keys: () => [
268
+ { keys: '↑↓ / jk', label: 'move between harnesses' },
269
+ { keys: '⏎', label: 'install, log in, or arm a log out' },
270
+ { keys: 'y', label: 'confirm a log out' },
271
+ { keys: 'r', label: 'probe again' },
272
+ ],
273
+ async onKey(key) {
274
+ if (acting || !key)
265
275
  return;
266
- if (key.ctrl && key.name === 'c') {
267
- quit(true);
276
+ if (key.name === 'up' || key.name === 'k') {
277
+ st.sel = (st.sel + harness_registry_1.HARNESS_IDS.length - 1) % harness_registry_1.HARNESS_IDS.length;
278
+ redraw();
268
279
  return;
269
280
  }
270
- if (key.name === 'q') {
271
- quit(false);
281
+ if (key.name === 'down' || key.name === 'j') {
282
+ st.sel = (st.sel + 1) % harness_registry_1.HARNESS_IDS.length;
283
+ redraw();
272
284
  return;
273
285
  }
274
286
  if (!st.caps)
275
287
  return;
288
+ if (key.name === 'r') {
289
+ await reprobe();
290
+ return;
291
+ }
276
292
  if (st.confirmLogout) {
277
293
  const target = st.confirmLogout;
278
294
  st.confirmLogout = null;
@@ -280,7 +296,7 @@ async function runHarnessesTui(opts = {}) {
280
296
  acting = true;
281
297
  try {
282
298
  const err = logoutSelected(target);
283
- st.note = err ? fg(203, `✗ ${err}`) : green(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
299
+ st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[target].label} logged out`);
284
300
  await reprobe();
285
301
  }
286
302
  finally {
@@ -288,25 +304,11 @@ async function runHarnessesTui(opts = {}) {
288
304
  }
289
305
  }
290
306
  else {
291
- st.note = dim('logout cancelled');
307
+ st.note = (0, render_1.dim)('logout cancelled');
292
308
  redraw();
293
309
  }
294
310
  return;
295
311
  }
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
312
  if (key.name !== 'return')
311
313
  return;
312
314
  const id = harness_registry_1.HARNESS_IDS[st.sel];
@@ -316,39 +318,45 @@ async function runHarnessesTui(opts = {}) {
316
318
  if (!p?.available) {
317
319
  await installSelected(st, id, redraw);
318
320
  await reprobe();
321
+ return;
319
322
  }
320
- else if (!p.authed) {
321
- const err = loginSelected(id);
322
- st.note = err ? fg(203, `✗ ${err}`) : green('✓ login flow finished — verifying…');
323
+ if (!p.authed) {
324
+ let err = null;
325
+ // The vendor CLI draws its own prompts, so it needs the real
326
+ // terminal — and it must have it back before we repaint.
327
+ await deps.suspend(() => { err = loginSelected(id); });
328
+ st.note = err ? (0, render_1.red)(`✗ ${err}`) : (0, render_1.green)('✓ login flow finished — verifying…');
329
+ redraw();
323
330
  await reprobe();
324
331
  const now = st.caps?.[id];
325
332
  if (now?.authed)
326
- st.note = green(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
333
+ st.note = (0, render_1.green)(`✓ ${harness_registry_1.HARNESSES[id].label} logged in as ${now.account ?? now.accountKind ?? '?'}`);
327
334
  else if (!err)
328
- st.note = yellow(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
335
+ st.note = (0, render_1.yellow)(`${harness_registry_1.HARNESSES[id].label} still reports unauthenticated`);
329
336
  redraw();
337
+ return;
330
338
  }
331
- else if (harness_registry_1.HARNESSES[id].logout) {
339
+ if (harness_registry_1.HARNESSES[id].logout) {
332
340
  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.`);
341
+ st.note = (0, render_1.yellow)(`log out of ${harness_registry_1.HARNESSES[id].label}? press y to confirm`);
338
342
  redraw();
343
+ return;
339
344
  }
345
+ st.note = (0, render_1.green)(`${harness_registry_1.HARNESSES[id].label} is installed and logged in.`);
346
+ redraw();
340
347
  }
341
348
  finally {
342
349
  acting = false;
343
350
  }
344
- })();
351
+ },
352
+ };
353
+ }
354
+ /** `ainode harnesses` on a pipe: no screen, just the facts. */
355
+ async function plainStatus() {
356
+ const caps = await (0, capabilities_1.probeCapabilities)();
357
+ for (const id of harness_registry_1.HARNESS_IDS) {
358
+ const p = caps[id];
359
+ const state = !p?.available ? 'Not installed' : p.authed ? `authed (${p.account ?? p.accountKind ?? '?'})` : 'Not logged in';
360
+ console.log(`${id.padEnd(8)} ${(p?.version ?? '—').padEnd(12)} ${state}`);
345
361
  }
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
362
  }
@@ -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
+ }