@addai/node 0.7.0 → 0.8.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.
@@ -7,7 +7,15 @@ export interface DashboardState {
7
7
  /** Recent requests, newest first — the NOW band reads the live ones off
8
8
  * the top and the idle line reads the most recent finish. */
9
9
  recent: RequestRow[];
10
+ /** Cursor position in the menu. */
10
11
  sel: number;
12
+ /** Id of the selected live run, or null when the cursor is in the menu.
13
+ * Pinned by id rather than by index because the band reorders under the
14
+ * cursor: a new run is prepended the moment it starts, and an index would
15
+ * quietly leave you pointing at a different run than the one you chose. */
16
+ nowSelId: string | null;
17
+ /** Live rows the last render could fit — what bounds the cursor. */
18
+ nowRows: number;
11
19
  spin: number;
12
20
  pid: number | null;
13
21
  startedAt: number | null;
@@ -35,6 +43,19 @@ export declare const MENU: Array<{
35
43
  /** Live rows the NOW band shows before it starts counting the rest. */
36
44
  export declare const MAX_NOW_ROWS = 6;
37
45
  export declare function liveRequests(rows: RequestRow[]): RequestRow[];
46
+ /** The live rows the band actually draws — the only ones the cursor can reach.
47
+ * Everything past the cap lives on the Activity screen. A short terminal
48
+ * draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
49
+ * that are not on screen, so the last render's row count is what bounds it. */
50
+ export declare function selectableNow(st: DashboardState): RequestRow[];
51
+ /**
52
+ * Where the cursor is in the NOW band, or -1 for "in the menu".
53
+ *
54
+ * Derived from the pinned id on every read, so a run that finishes while
55
+ * selected drops the cursor back to the menu rather than silently moving it
56
+ * onto whichever run took that row.
57
+ */
58
+ export declare function nowIndex(st: DashboardState): number;
38
59
  /**
39
60
  * The node's own state, as a label.
40
61
  *
@@ -8,6 +8,8 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.MAX_NOW_ROWS = exports.MENU = void 0;
10
10
  exports.liveRequests = liveRequests;
11
+ exports.selectableNow = selectableNow;
12
+ exports.nowIndex = nowIndex;
11
13
  exports.stateLabel = stateLabel;
12
14
  exports.autostartLine = autostartLine;
13
15
  exports.fmtCount = fmtCount;
@@ -29,6 +31,26 @@ exports.MAX_NOW_ROWS = 6;
29
31
  function liveRequests(rows) {
30
32
  return rows.filter(r => request_row_1.ACTIVE.has(r.status));
31
33
  }
34
+ /** The live rows the band actually draws — the only ones the cursor can reach.
35
+ * Everything past the cap lives on the Activity screen. A short terminal
36
+ * draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
37
+ * that are not on screen, so the last render's row count is what bounds it. */
38
+ function selectableNow(st) {
39
+ const cap = Math.max(1, Math.min(exports.MAX_NOW_ROWS, st.nowRows || exports.MAX_NOW_ROWS));
40
+ return liveRequests(st.recent).slice(0, cap);
41
+ }
42
+ /**
43
+ * Where the cursor is in the NOW band, or -1 for "in the menu".
44
+ *
45
+ * Derived from the pinned id on every read, so a run that finishes while
46
+ * selected drops the cursor back to the menu rather than silently moving it
47
+ * onto whichever run took that row.
48
+ */
49
+ function nowIndex(st) {
50
+ if (!st.nowSelId)
51
+ return -1;
52
+ return selectableNow(st).findIndex(r => r.id === st.nowSelId);
53
+ }
32
54
  /**
33
55
  * The node's own state, as a label.
34
56
  *
@@ -128,7 +150,10 @@ function lastFinished(rows) {
128
150
  }
129
151
  function nowLines(st, width, rows) {
130
152
  const live = liveRequests(st.recent);
131
- const out = [` ${(0, render_1.bold)('Now')}`];
153
+ const heading = live.length
154
+ ? ` ${(0, render_1.bold)('Now')} ${(0, render_1.dim)('↑ to select · ⏎ to watch')}`
155
+ : ` ${(0, render_1.bold)('Now')}`;
156
+ const out = [heading];
132
157
  if (live.length === 0) {
133
158
  const last = lastFinished(st.recent);
134
159
  out.push(last
@@ -138,7 +163,9 @@ function nowLines(st, width, rows) {
138
163
  }
139
164
  out.push((0, request_row_1.requestHeader)(width));
140
165
  const shown = live.slice(0, Math.max(1, rows));
141
- shown.forEach(r => out.push((0, request_row_1.requestLine)(r, st.now, false, width, st.spin)));
166
+ st.nowRows = shown.length;
167
+ const cursor = nowIndex(st);
168
+ shown.forEach((r, i) => out.push((0, request_row_1.requestLine)(r, st.now, i === cursor, width, st.spin)));
142
169
  if (live.length > shown.length) {
143
170
  out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
144
171
  }
@@ -153,8 +180,11 @@ function renderDashboard(st, width, height) {
153
180
  // The shortcut letters line up in a column of their own, so the eye can
154
181
  // find them without reading the hints.
155
182
  const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
183
+ // One cursor on screen at a time: while it is up in the NOW band the menu
184
+ // shows no selection, or the eye reads two.
185
+ const inNow = nowIndex(st) >= 0;
156
186
  const menu = exports.MENU.map((m, i) => {
157
- const selected = i === st.sel;
187
+ const selected = !inNow && i === st.sel;
158
188
  const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
159
189
  const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
160
190
  const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
@@ -162,7 +192,7 @@ function renderDashboard(st, width, height) {
162
192
  });
163
193
  const foot = (0, app_1.footerHint)([
164
194
  { keys: '↑↓', label: 'move' },
165
- { keys: '⏎', label: 'open' },
195
+ { keys: '⏎', label: inNow ? 'watch this run' : 'open' },
166
196
  { keys: 's', label: st.autostart?.enabled ? 'startup off' : 'startup on' },
167
197
  { keys: 'r', label: 'refresh' },
168
198
  { keys: '?', label: 'keys' },
@@ -202,6 +232,11 @@ function createDashboardScreen(deps) {
202
232
  if (recent.length)
203
233
  st.recent = recent;
204
234
  st.offline = deps.data.offline();
235
+ // A selected run that has finished is no longer in the band. nowIndex
236
+ // already reads that as "cursor is in the menu"; drop the id too so the
237
+ // state doesn't keep pointing at a run nobody can see.
238
+ if (st.nowSelId && !selectableNow(st).some(r => r.id === st.nowSelId))
239
+ st.nowSelId = null;
205
240
  // Reads a file (and, on Windows, a cached schtasks query) — cheap enough
206
241
  // to ride the same poll, so the header can't disagree with reality after
207
242
  // someone changes it from Studio or the CLI.
@@ -242,6 +277,43 @@ function createDashboardScreen(deps) {
242
277
  // line — a note that never clears becomes furniture.
243
278
  setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
244
279
  };
280
+ /**
281
+ * One cursor over two stacked lists: the NOW band sits above the menu, so
282
+ * ↑ off the top of the menu lands on the LAST live row (the one nearest the
283
+ * menu) and ↓ off the bottom of the band returns to the first menu item.
284
+ * Reading the screen top to bottom, the cursor moves the way the eye does.
285
+ */
286
+ const moveUp = () => {
287
+ const live = selectableNow(st);
288
+ const i = nowIndex(st);
289
+ if (i < 0) {
290
+ // In the menu. Step up inside it first; only leave from the top row.
291
+ if (st.sel > 0) {
292
+ st.sel -= 1;
293
+ return;
294
+ }
295
+ if (live.length)
296
+ st.nowSelId = live[live.length - 1].id;
297
+ return;
298
+ }
299
+ if (i > 0)
300
+ st.nowSelId = live[i - 1].id;
301
+ };
302
+ const moveDown = () => {
303
+ const live = selectableNow(st);
304
+ const i = nowIndex(st);
305
+ if (i < 0) {
306
+ st.sel = Math.min(exports.MENU.length - 1, st.sel + 1);
307
+ return;
308
+ }
309
+ // Past the last live row is the menu, entered at its first item.
310
+ if (i >= live.length - 1) {
311
+ st.nowSelId = null;
312
+ st.sel = 0;
313
+ return;
314
+ }
315
+ st.nowSelId = live[i + 1].id;
316
+ };
245
317
  return {
246
318
  id: 'dashboard',
247
319
  title: '+Ai Node',
@@ -258,8 +330,8 @@ function createDashboardScreen(deps) {
258
330
  return true;
259
331
  },
260
332
  keys: () => [
261
- { keys: '↑↓ / jk', label: 'move between destinations' },
262
- { keys: '⏎', label: 'open the selected destination' },
333
+ { keys: '↑↓ / jk', label: 'move up from the menu into the live runs' },
334
+ { keys: '⏎', label: 'open the destination, or watch the selected run' },
263
335
  { keys: 'a', label: 'activity — full request history' },
264
336
  { keys: 'h', label: 'harnesses — install / log in agent CLIs' },
265
337
  { keys: 'l', label: 'logs — daemon output' },
@@ -268,12 +340,12 @@ function createDashboardScreen(deps) {
268
340
  ],
269
341
  async onKey(key) {
270
342
  if (key.name === 'up' || key.name === 'k') {
271
- st.sel = Math.max(0, st.sel - 1);
343
+ moveUp();
272
344
  deps.host.redraw();
273
345
  return;
274
346
  }
275
347
  if (key.name === 'down' || key.name === 'j') {
276
- st.sel = Math.min(exports.MENU.length - 1, st.sel + 1);
348
+ moveDown();
277
349
  deps.host.redraw();
278
350
  return;
279
351
  }
@@ -298,6 +370,12 @@ function createDashboardScreen(deps) {
298
370
  return;
299
371
  }
300
372
  if (key.name === 'return') {
373
+ // A selected run wins over the menu: the cursor is visibly on it.
374
+ const live = selectableNow(st)[nowIndex(st)];
375
+ if (live) {
376
+ deps.openTranscript(live);
377
+ return;
378
+ }
301
379
  const target = exports.MENU[st.sel]?.key;
302
380
  if (target === 'harnesses')
303
381
  deps.openHarnesses();
package/dist/tui/run.js CHANGED
@@ -192,7 +192,7 @@ async function runDashboard(opts) {
192
192
  // the daemon log file, so nothing is lost by watching.
193
193
  const logs = (0, console_capture_1.captureConsole)({ logFile: paths_1.RUNTIME_LOG_FILE });
194
194
  const state = {
195
- self: null, stats: null, recent: [], sel: 0, spin: 0,
195
+ self: null, stats: null, recent: [], sel: 0, nowSelId: null, nowRows: 0, spin: 0,
196
196
  pid: opts.pid, startedAt: opts.startedAt,
197
197
  inflight: 0, paired: (0, store_1.isPaired)(), viewerMode: opts.viewerMode,
198
198
  offline: false, now: Date.now(), version: opts.version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addai/node",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
5
5
  "license": "MIT",
6
6
  "keywords": [