@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.
- package/dist/cli.js +2 -3
- package/dist/lockfile.d.ts +43 -3
- package/dist/lockfile.js +114 -38
- package/dist/memory-capture.js +10 -1
- package/dist/paths.d.ts +3 -0
- package/dist/paths.js +4 -1
- package/dist/tui/app.d.ts +24 -2
- package/dist/tui/app.js +85 -16
- package/dist/tui/console-capture.d.ts +8 -3
- package/dist/tui/console-capture.js +73 -15
- package/dist/tui/dashboard.d.ts +23 -12
- package/dist/tui/dashboard.js +169 -129
- package/dist/tui/harnesses.d.ts +50 -13
- package/dist/tui/harnesses.js +179 -171
- package/dist/tui/logs.d.ts +18 -0
- package/dist/tui/logs.js +157 -0
- package/dist/tui/render.d.ts +40 -0
- package/dist/tui/render.js +99 -0
- package/dist/tui/request-row.d.ts +35 -0
- package/dist/tui/request-row.js +144 -0
- package/dist/tui/requests.d.ts +10 -2
- package/dist/tui/requests.js +112 -20
- package/dist/tui/run.d.ts +2 -2
- package/dist/tui/run.js +146 -85
- package/dist/tui/transcript.d.ts +3 -2
- package/dist/tui/transcript.js +39 -15
- package/package.json +1 -1
- package/scripts/probe-tui.mjs +122 -0
|
@@ -7,20 +7,65 @@
|
|
|
7
7
|
// every one of those lines lands in the middle of a frame, so the screen
|
|
8
8
|
// appears to flicker between the UI and log output.
|
|
9
9
|
//
|
|
10
|
-
// Capturing rather than silencing: the lines are kept in a ring buffer
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
10
|
+
// Capturing rather than silencing: the lines are kept in a ring buffer that
|
|
11
|
+
// the Logs screen reads, and appended to the daemon log file as they arrive.
|
|
12
|
+
// Nothing is lost by running the daemon under the console — and because the
|
|
13
|
+
// file is written live, nothing has to be dumped into your shell on the way
|
|
14
|
+
// out either.
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
14
48
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
49
|
exports.captureConsole = captureConsole;
|
|
16
|
-
const
|
|
17
|
-
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const MAX_LINES = 2000;
|
|
52
|
+
function captureConsole(opts = {}) {
|
|
18
53
|
const ring = [];
|
|
54
|
+
const listeners = [];
|
|
19
55
|
const original = {
|
|
20
56
|
log: console.log.bind(console),
|
|
21
57
|
warn: console.warn.bind(console),
|
|
22
58
|
error: console.error.bind(console),
|
|
23
59
|
};
|
|
60
|
+
let sink = null;
|
|
61
|
+
if (opts.logFile) {
|
|
62
|
+
try {
|
|
63
|
+
sink = fs.openSync(opts.logFile, 'a');
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
sink = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
24
69
|
const format = (args) => args.map(a => {
|
|
25
70
|
if (typeof a === 'string')
|
|
26
71
|
return a;
|
|
@@ -34,9 +79,22 @@ function captureConsole() {
|
|
|
34
79
|
}
|
|
35
80
|
}).join(' ');
|
|
36
81
|
const push = (level) => (...args) => {
|
|
37
|
-
|
|
82
|
+
const line = { level, text: format(args), at: Date.now() };
|
|
83
|
+
ring.push(line);
|
|
38
84
|
if (ring.length > MAX_LINES)
|
|
39
85
|
ring.shift();
|
|
86
|
+
if (sink !== null) {
|
|
87
|
+
try {
|
|
88
|
+
fs.writeSync(sink, `${line.text}\n`);
|
|
89
|
+
}
|
|
90
|
+
catch { /* disk full, keep the UI alive */ }
|
|
91
|
+
}
|
|
92
|
+
for (const fn of listeners) {
|
|
93
|
+
try {
|
|
94
|
+
fn(line);
|
|
95
|
+
}
|
|
96
|
+
catch { /* a listener must never break logging */ }
|
|
97
|
+
}
|
|
40
98
|
};
|
|
41
99
|
console.log = push('log');
|
|
42
100
|
console.warn = push('warn');
|
|
@@ -45,18 +103,18 @@ function captureConsole() {
|
|
|
45
103
|
lines: () => [...ring],
|
|
46
104
|
last: () => (ring.length ? ring[ring.length - 1] : null),
|
|
47
105
|
count: () => ring.length,
|
|
48
|
-
|
|
106
|
+
onLine(fn) { listeners.push(fn); },
|
|
107
|
+
restore() {
|
|
49
108
|
console.log = original.log;
|
|
50
109
|
console.warn = original.warn;
|
|
51
110
|
console.error = original.error;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
original.log(l.text);
|
|
111
|
+
if (sink !== null) {
|
|
112
|
+
try {
|
|
113
|
+
fs.closeSync(sink);
|
|
114
|
+
}
|
|
115
|
+
catch { /* already gone */ }
|
|
59
116
|
}
|
|
117
|
+
sink = null;
|
|
60
118
|
},
|
|
61
119
|
};
|
|
62
120
|
}
|
package/dist/tui/dashboard.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { type AppHost, type Screen } from './app';
|
|
1
2
|
import type { DataLayer, NodeSelf, NodeStats, RequestRow } from './data';
|
|
2
|
-
import type { AppHost, Screen } from './app';
|
|
3
3
|
export interface DashboardState {
|
|
4
4
|
self: NodeSelf | null;
|
|
5
5
|
stats: NodeStats | null;
|
|
6
|
-
requests
|
|
6
|
+
/** Recent requests, newest first — the NOW band reads the live ones off
|
|
7
|
+
* the top and the idle line reads the most recent finish. */
|
|
8
|
+
recent: RequestRow[];
|
|
7
9
|
sel: number;
|
|
8
10
|
spin: number;
|
|
9
11
|
pid: number | null;
|
|
@@ -14,24 +16,32 @@ export interface DashboardState {
|
|
|
14
16
|
offline: boolean;
|
|
15
17
|
now: number;
|
|
16
18
|
version: string;
|
|
17
|
-
/**
|
|
18
|
-
lastLog: string | null;
|
|
19
|
+
/** How many daemon log lines have been captured this session. */
|
|
19
20
|
logCount: number;
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
/** Trigger sources are DB-shaped ('self_improve_distil'); the column is
|
|
23
|
-
* nine columns wide and "self_impr" tells you nothing. */
|
|
24
|
-
export declare function shortVia(via: string | null | undefined): string;
|
|
25
|
-
export declare function requestLine(r: RequestRow, now: number, selected: boolean, width: number): string;
|
|
26
|
-
/** The landing screen's menu. Activity and Harnesses are places you go,
|
|
27
|
-
* not things that crowd the overview. */
|
|
22
|
+
/** The landing screen's menu. */
|
|
28
23
|
export declare const MENU: Array<{
|
|
29
24
|
key: string;
|
|
30
25
|
label: string;
|
|
31
26
|
hint: string;
|
|
32
27
|
shortcut: string;
|
|
33
28
|
}>;
|
|
34
|
-
|
|
29
|
+
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
30
|
+
export declare const MAX_NOW_ROWS = 6;
|
|
31
|
+
export declare function liveRequests(rows: RequestRow[]): RequestRow[];
|
|
32
|
+
/**
|
|
33
|
+
* The node's own state, as a label.
|
|
34
|
+
*
|
|
35
|
+
* A daemon is running — we either started it or the lockfile proved it — so
|
|
36
|
+
* the node IS online, whatever the server has got round to saying. Waiting
|
|
37
|
+
* for `runtime_self` to come back before admitting that meant the first
|
|
38
|
+
* second of every session claimed the node was in an unknown state.
|
|
39
|
+
*/
|
|
40
|
+
export declare function stateLabel(st: DashboardState): string;
|
|
41
|
+
/** 1204 reads as a year; 1,204 reads as a count. */
|
|
42
|
+
export declare function fmtCount(n: number | null | undefined): string;
|
|
43
|
+
export declare function nowLines(st: DashboardState, width: number, rows: number): string[];
|
|
44
|
+
export declare function renderDashboard(st: DashboardState, width: number, height: number): string[];
|
|
35
45
|
export declare function createDashboardScreen(deps: {
|
|
36
46
|
data: DataLayer;
|
|
37
47
|
host: AppHost;
|
|
@@ -39,4 +49,5 @@ export declare function createDashboardScreen(deps: {
|
|
|
39
49
|
openTranscript(r: RequestRow): void;
|
|
40
50
|
openRequests(): void;
|
|
41
51
|
openHarnesses(): void;
|
|
52
|
+
openLogs(): void;
|
|
42
53
|
}): Screen;
|
package/dist/tui/dashboard.js
CHANGED
|
@@ -1,176 +1,209 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// The cockpit: is this node alive, what is it doing right now, and what has
|
|
3
|
+
// it done today.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// The landing screen answers "is the box earning its keep" without a
|
|
6
|
+
// keystroke. Everything that needs scrolling — history, harnesses, logs — is
|
|
7
|
+
// a place you go, not something that crowds the overview.
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.
|
|
10
|
-
exports.
|
|
11
|
-
exports.
|
|
9
|
+
exports.MAX_NOW_ROWS = exports.MENU = void 0;
|
|
10
|
+
exports.liveRequests = liveRequests;
|
|
11
|
+
exports.stateLabel = stateLabel;
|
|
12
|
+
exports.fmtCount = fmtCount;
|
|
13
|
+
exports.nowLines = nowLines;
|
|
12
14
|
exports.renderDashboard = renderDashboard;
|
|
13
15
|
exports.createDashboardScreen = createDashboardScreen;
|
|
14
16
|
const render_1 = require("./render");
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
chat: 'chat',
|
|
28
|
-
schedule: 'schedule',
|
|
29
|
-
flow: 'flow',
|
|
30
|
-
api: 'api',
|
|
31
|
-
studio: 'studio',
|
|
32
|
-
};
|
|
33
|
-
return map[via] ?? via;
|
|
34
|
-
}
|
|
35
|
-
function statusLabel(status) {
|
|
36
|
-
if (status === 'completed')
|
|
37
|
-
return 'DONE';
|
|
38
|
-
if (CANCELLED.has(status))
|
|
39
|
-
return 'CANCEL';
|
|
40
|
-
return status.toUpperCase();
|
|
41
|
-
}
|
|
42
|
-
function statusCell(status) {
|
|
43
|
-
const label = statusLabel(status).padEnd(8).slice(0, 8);
|
|
44
|
-
if (ACTIVE.has(status))
|
|
45
|
-
return (0, render_1.cyan)(label);
|
|
46
|
-
if (status === 'completed')
|
|
47
|
-
return (0, render_1.green)(label);
|
|
48
|
-
if (CANCELLED.has(status))
|
|
49
|
-
return (0, render_1.grey)(label);
|
|
50
|
-
return (0, render_1.red)(label);
|
|
51
|
-
}
|
|
52
|
-
function dot(status) {
|
|
53
|
-
if (ACTIVE.has(status))
|
|
54
|
-
return (0, render_1.cyan)('⏺');
|
|
55
|
-
if (status === 'completed')
|
|
56
|
-
return (0, render_1.green)('⏺');
|
|
57
|
-
if (CANCELLED.has(status))
|
|
58
|
-
return (0, render_1.grey)('⏺');
|
|
59
|
-
return (0, render_1.red)('⏺');
|
|
60
|
-
}
|
|
61
|
-
function durationOf(r, now) {
|
|
62
|
-
const start = r.picked_up_at ?? r.created_at;
|
|
63
|
-
if (!start)
|
|
64
|
-
return null;
|
|
65
|
-
const end = r.finished_at ? new Date(r.finished_at).getTime() : now;
|
|
66
|
-
return Math.max(0, end - new Date(start).getTime());
|
|
17
|
+
const request_row_1 = require("./request-row");
|
|
18
|
+
const app_1 = require("./app");
|
|
19
|
+
/** The landing screen's menu. */
|
|
20
|
+
exports.MENU = [
|
|
21
|
+
{ key: 'activity', label: 'Activity', hint: 'Every request this node has handled', shortcut: 'a' },
|
|
22
|
+
{ key: 'harnesses', label: 'Harnesses', hint: 'Install / log in agent CLIs', shortcut: 'h' },
|
|
23
|
+
{ key: 'logs', label: 'Logs', hint: 'What the daemon is saying', shortcut: 'l' },
|
|
24
|
+
];
|
|
25
|
+
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
26
|
+
exports.MAX_NOW_ROWS = 6;
|
|
27
|
+
function liveRequests(rows) {
|
|
28
|
+
return rows.filter(r => request_row_1.ACTIVE.has(r.status));
|
|
67
29
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const line = `${prefix}${(0, render_1.truncate)(tail, room)}`;
|
|
84
|
-
return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
|
|
30
|
+
/**
|
|
31
|
+
* The node's own state, as a label.
|
|
32
|
+
*
|
|
33
|
+
* A daemon is running — we either started it or the lockfile proved it — so
|
|
34
|
+
* the node IS online, whatever the server has got round to saying. Waiting
|
|
35
|
+
* for `runtime_self` to come back before admitting that meant the first
|
|
36
|
+
* second of every session claimed the node was in an unknown state.
|
|
37
|
+
*/
|
|
38
|
+
function stateLabel(st) {
|
|
39
|
+
const reported = st.self?.effective_status;
|
|
40
|
+
if (!reported)
|
|
41
|
+
return (0, render_1.green)('Online');
|
|
42
|
+
if (reported === 'online')
|
|
43
|
+
return (0, render_1.green)('Online');
|
|
44
|
+
return (0, render_1.yellow)(reported.charAt(0).toUpperCase() + reported.slice(1));
|
|
85
45
|
}
|
|
86
46
|
function headerLines(st) {
|
|
87
47
|
if (!st.paired) {
|
|
88
48
|
return [
|
|
89
|
-
(0, render_1.yellow)('
|
|
49
|
+
(0, render_1.yellow)('Not paired'),
|
|
90
50
|
(0, render_1.dim)('run `ainode` and follow the prompt to vault.add.ai/entity/connect/<CODE>'),
|
|
91
51
|
];
|
|
92
52
|
}
|
|
93
53
|
const self = st.self;
|
|
94
|
-
const name = self?.name ?? self?.hostname ?? '
|
|
95
|
-
const state = self?.effective_status === 'online'
|
|
96
|
-
? (0, render_1.green)('online')
|
|
97
|
-
: (0, render_1.yellow)(self?.effective_status ?? 'unknown');
|
|
54
|
+
const name = self?.name ?? self?.hostname ?? 'This node';
|
|
98
55
|
const paired = self?.created_at ? `paired ${(0, render_1.fmtRelative)(self.created_at, st.now)}` : '';
|
|
99
|
-
const first = `${(0, render_1.bold)(name)} ${(0, render_1.dim)('·')} ${
|
|
56
|
+
const first = `${(0, render_1.bold)(name)} ${(0, render_1.dim)('·')} ${stateLabel(st)}${paired ? ` ${(0, render_1.dim)('·')} ${(0, render_1.dim)(paired)}` : ''}`;
|
|
100
57
|
const up = st.startedAt ? (0, render_1.fmtDuration)(st.now - st.startedAt) : '—';
|
|
101
58
|
const daemon = st.viewerMode
|
|
102
|
-
? `${(0, render_1.cyan)('⏺')}
|
|
103
|
-
: `${(0, render_1.green)('⏺')}
|
|
104
|
-
const chip = st.offline ? ` ${(0, render_1.yellow)('⚠
|
|
59
|
+
? `${(0, render_1.cyan)('⏺')} Viewer ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)('· another process owns this node')}`
|
|
60
|
+
: `${(0, render_1.green)('⏺')} Running ${(0, render_1.dim)(`pid ${st.pid ?? '?'}`)} ${(0, render_1.dim)(`up ${up}`)} ${(0, render_1.dim)(`${st.inflight} in flight`)}`;
|
|
61
|
+
const chip = st.offline ? ` ${(0, render_1.yellow)('⚠ Offline · retrying')}` : '';
|
|
105
62
|
return [first, daemon + chip];
|
|
106
63
|
}
|
|
64
|
+
/** 1204 reads as a year; 1,204 reads as a count. */
|
|
65
|
+
function fmtCount(n) {
|
|
66
|
+
if (n == null)
|
|
67
|
+
return '—';
|
|
68
|
+
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
69
|
+
}
|
|
70
|
+
/** A labelled figure, padded so the labels line up down the screen. */
|
|
71
|
+
function stat(label, value, width = 14) {
|
|
72
|
+
return (0, render_1.padEndVisible)(`${(0, render_1.dim)(label)} ${value}`, width);
|
|
73
|
+
}
|
|
107
74
|
function statsLines(st) {
|
|
108
75
|
const s = st.stats;
|
|
109
|
-
|
|
110
|
-
|
|
76
|
+
// Keep the shape while the first fetch is in flight, so the band doesn't
|
|
77
|
+
// pop into place a second after the header.
|
|
78
|
+
if (!s) {
|
|
79
|
+
return [
|
|
80
|
+
`${stat('Today', (0, render_1.dim)('—'))}${stat('24h', (0, render_1.dim)('—'), 26)}${stat('Avg', (0, render_1.dim)('—'))}`,
|
|
81
|
+
`${stat('Week', (0, render_1.dim)('—'))}${stat('Month', (0, render_1.dim)('—'), 26)}${stat('Total', (0, render_1.dim)('—'))}`,
|
|
82
|
+
];
|
|
83
|
+
}
|
|
111
84
|
const agents = Object.entries(s.by_agent)
|
|
112
85
|
.sort((a, b) => b[1] - a[1])
|
|
113
|
-
.map(([k, v]) => `${k} ${v}`)
|
|
114
|
-
.join(' · ');
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
86
|
+
.map(([k, v]) => `${k.charAt(0).toUpperCase()}${k.slice(1)} ${fmtCount(v)}`)
|
|
87
|
+
.join((0, render_1.dim)(' · '));
|
|
88
|
+
// The ✓/✗ figures are a rolling 24h window while "Today" is the calendar
|
|
89
|
+
// day, so the window is labelled — otherwise ✓3 next to Today 2 looks
|
|
90
|
+
// like an arithmetic bug.
|
|
91
|
+
const outcomes = [
|
|
92
|
+
(0, render_1.green)(`✓ ${fmtCount(s.succeeded_24h)}`),
|
|
93
|
+
s.failed_24h ? (0, render_1.red)(`✗ ${fmtCount(s.failed_24h)}`) : (0, render_1.dim)('✗ 0'),
|
|
94
|
+
s.canceled_24h ? (0, render_1.grey)(`⊘ ${fmtCount(s.canceled_24h)}`) : '',
|
|
95
|
+
].filter(Boolean).join(' ');
|
|
96
|
+
// Both rows share one three-column grid, so Month sits under 24h and
|
|
97
|
+
// Total under Avg rather than the two rows drifting apart.
|
|
119
98
|
return [
|
|
120
|
-
`${(0, render_1.bold)(
|
|
121
|
-
(
|
|
99
|
+
`${stat('Today', (0, render_1.bold)(fmtCount(s.today)))}${stat('24h', outcomes, 26)}${stat('Avg', (0, render_1.fmtDuration)(s.avg_duration_ms))}`,
|
|
100
|
+
`${stat('Week', fmtCount(s.week))}${stat('Month', fmtCount(s.month), 26)}${stat('Total', fmtCount(s.total))}${(0, render_1.dim)(agents)}`,
|
|
122
101
|
];
|
|
123
102
|
}
|
|
124
|
-
/** The
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
out
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
103
|
+
/** The most recent request that actually finished, for the idle line. */
|
|
104
|
+
function lastFinished(rows) {
|
|
105
|
+
return rows.find(r => r.finished_at) ?? null;
|
|
106
|
+
}
|
|
107
|
+
function nowLines(st, width, rows) {
|
|
108
|
+
const live = liveRequests(st.recent);
|
|
109
|
+
const out = [` ${(0, render_1.bold)('Now')}`];
|
|
110
|
+
if (live.length === 0) {
|
|
111
|
+
const last = lastFinished(st.recent);
|
|
112
|
+
out.push(last
|
|
113
|
+
? (0, render_1.dim)(` Idle — last run finished ${(0, render_1.fmtRelative)(last.finished_at, st.now)}`)
|
|
114
|
+
: (0, render_1.dim)(' Idle'));
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
out.push((0, request_row_1.requestHeader)(width));
|
|
118
|
+
const shown = live.slice(0, Math.max(1, rows));
|
|
119
|
+
shown.forEach(r => out.push((0, request_row_1.requestLine)(r, st.now, false, width, st.spin)));
|
|
120
|
+
if (live.length > shown.length) {
|
|
121
|
+
out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
|
|
143
122
|
}
|
|
144
|
-
out
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
function renderDashboard(st, width, height) {
|
|
126
|
+
const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`@addai/node v${st.version}`)}`, headerLines(st), width);
|
|
127
|
+
if (!st.paired) {
|
|
128
|
+
return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
|
|
149
129
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
130
|
+
const stats = statsLines(st).map(l => ` ${l}`);
|
|
131
|
+
// The shortcut letters line up in a column of their own, so the eye can
|
|
132
|
+
// find them without reading the hints.
|
|
133
|
+
const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
|
|
134
|
+
const menu = exports.MENU.map((m, i) => {
|
|
135
|
+
const selected = i === st.sel;
|
|
136
|
+
const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
|
|
137
|
+
const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
|
|
138
|
+
const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
|
|
139
|
+
return `${cursor} ${label} ${(0, render_1.dim)(m.hint.padEnd(hintWidth))}${(0, render_1.dim)(badge.padEnd(10))}${(0, render_1.dim)(m.shortcut)}`;
|
|
140
|
+
});
|
|
141
|
+
const foot = (0, app_1.footerHint)([
|
|
142
|
+
{ keys: '↑↓', label: 'move' },
|
|
143
|
+
{ keys: '⏎', label: 'open' },
|
|
144
|
+
{ keys: 'r', label: 'refresh' },
|
|
145
|
+
{ keys: '?', label: 'keys' },
|
|
146
|
+
{ keys: 'q', label: 'quit' },
|
|
147
|
+
]);
|
|
148
|
+
// Everything but the NOW band is fixed height, so the band takes what is
|
|
149
|
+
// left: heading + header row + rows + the "+N more" line.
|
|
150
|
+
const fixed = head.length + 1 + stats.length + 1 + 3 + 1 + menu.length + 1 + 1;
|
|
151
|
+
const room = Math.min(exports.MAX_NOW_ROWS, Math.max(1, height - fixed));
|
|
152
|
+
return [
|
|
153
|
+
...head,
|
|
154
|
+
'',
|
|
155
|
+
...stats,
|
|
156
|
+
'',
|
|
157
|
+
...nowLines(st, width, room),
|
|
158
|
+
'',
|
|
159
|
+
...menu,
|
|
160
|
+
'',
|
|
161
|
+
foot,
|
|
162
|
+
].map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
153
163
|
}
|
|
154
164
|
function createDashboardScreen(deps) {
|
|
155
165
|
const st = deps.state;
|
|
156
|
-
// The landing screen shows identity and numbers only, so it fetches two
|
|
157
|
-
// rows of counters — not the request feed. Activity pages that in itself.
|
|
158
166
|
const refresh = async () => {
|
|
159
167
|
st.now = Date.now();
|
|
160
|
-
const [self, stats] = await Promise.all([
|
|
168
|
+
const [self, stats, recent] = await Promise.all([
|
|
169
|
+
deps.data.self(),
|
|
170
|
+
deps.data.stats(),
|
|
171
|
+
// Enough rows to hold every plausible concurrent run plus something
|
|
172
|
+
// finished for the idle line, and small enough to poll every 2s.
|
|
173
|
+
deps.data.requests({ limit: 20 }),
|
|
174
|
+
]);
|
|
161
175
|
if (self)
|
|
162
176
|
st.self = self;
|
|
163
177
|
if (stats)
|
|
164
178
|
st.stats = stats;
|
|
179
|
+
if (recent.length)
|
|
180
|
+
st.recent = recent;
|
|
165
181
|
st.offline = deps.data.offline();
|
|
166
182
|
deps.host.redraw();
|
|
167
183
|
};
|
|
168
184
|
return {
|
|
169
185
|
id: 'dashboard',
|
|
170
186
|
title: '+Ai Node',
|
|
171
|
-
render: (width) => renderDashboard(st, width),
|
|
172
|
-
|
|
187
|
+
render: (width, height) => renderDashboard(st, width, height),
|
|
188
|
+
// Poll hard while something is moving; an idle node doesn't need traffic.
|
|
189
|
+
pollMs: () => (liveRequests(st.recent).length > 0 || st.inflight > 0 ? 2000 : 5000),
|
|
173
190
|
poll: refresh,
|
|
191
|
+
tick(n) {
|
|
192
|
+
st.spin = n;
|
|
193
|
+
// Only the NOW band animates, and only when something is in it.
|
|
194
|
+
if (liveRequests(st.recent).length === 0)
|
|
195
|
+
return false;
|
|
196
|
+
st.now = Date.now();
|
|
197
|
+
return true;
|
|
198
|
+
},
|
|
199
|
+
keys: () => [
|
|
200
|
+
{ keys: '↑↓ / jk', label: 'move between destinations' },
|
|
201
|
+
{ keys: '⏎', label: 'open the selected destination' },
|
|
202
|
+
{ keys: 'a', label: 'activity — full request history' },
|
|
203
|
+
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
204
|
+
{ keys: 'l', label: 'logs — daemon output' },
|
|
205
|
+
{ keys: 'r', label: 'refresh now' },
|
|
206
|
+
],
|
|
174
207
|
async onKey(key) {
|
|
175
208
|
if (key.name === 'up' || key.name === 'k') {
|
|
176
209
|
st.sel = Math.max(0, st.sel - 1);
|
|
@@ -190,13 +223,20 @@ function createDashboardScreen(deps) {
|
|
|
190
223
|
deps.openHarnesses();
|
|
191
224
|
return;
|
|
192
225
|
}
|
|
226
|
+
if (key.name === 'l') {
|
|
227
|
+
deps.openLogs();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
193
230
|
if (key.name === 'r') {
|
|
194
231
|
await refresh();
|
|
195
232
|
return;
|
|
196
233
|
}
|
|
197
234
|
if (key.name === 'return') {
|
|
198
|
-
|
|
235
|
+
const target = exports.MENU[st.sel]?.key;
|
|
236
|
+
if (target === 'harnesses')
|
|
199
237
|
deps.openHarnesses();
|
|
238
|
+
else if (target === 'logs')
|
|
239
|
+
deps.openLogs();
|
|
200
240
|
else
|
|
201
241
|
deps.openRequests();
|
|
202
242
|
}
|
package/dist/tui/harnesses.d.ts
CHANGED
|
@@ -1,16 +1,53 @@
|
|
|
1
|
+
import { type HarnessId } from '../harness-registry';
|
|
2
|
+
import { type AppHost, type Screen } from './app';
|
|
3
|
+
export type Probe = {
|
|
4
|
+
available?: boolean;
|
|
5
|
+
authed?: boolean;
|
|
6
|
+
version?: string;
|
|
7
|
+
account?: string;
|
|
8
|
+
plan?: string;
|
|
9
|
+
accountKind?: string;
|
|
10
|
+
models?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type Caps = Record<string, Probe>;
|
|
13
|
+
export interface HarnessesState {
|
|
14
|
+
caps: Caps | null;
|
|
15
|
+
sel: number;
|
|
16
|
+
spin: number;
|
|
17
|
+
/** Spinner label while an action runs; null when idle. */
|
|
18
|
+
busy: string | null;
|
|
19
|
+
/** Enter on an authed row arms this; y fires it. */
|
|
20
|
+
confirmLogout: HarnessId | null;
|
|
21
|
+
/** Tail of an install's npm output. */
|
|
22
|
+
log: string[];
|
|
23
|
+
note: string | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function actionFor(id: HarnessId, p: Probe | undefined): string;
|
|
1
26
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
27
|
+
* Harness CLIs report their version in whatever shape they feel like:
|
|
28
|
+
* `2.1.220 (Claude Code)`, `codex-cli 0.139.0`, `0.52.0`. Prefixing all of
|
|
29
|
+
* those with a `v` produced `vcodex-cli 0.139.0`. Pull out the version
|
|
30
|
+
* itself and leave the branding to the label column.
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatVersion(v: string | undefined): string;
|
|
33
|
+
export declare function accountLabel(p: Probe | undefined): string;
|
|
34
|
+
/**
|
|
35
|
+
* Column widths.
|
|
9
36
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
37
|
+
* The account and the effort ladder vary most, so they take the spare width;
|
|
38
|
+
* version and action are fixed at what their values actually need. The old
|
|
39
|
+
* fixed widths cut `codex-cli` down to `vcodex-cli` and
|
|
40
|
+
* `low/medium/high/xhigh` to `low/medium/high/x`.
|
|
13
41
|
*/
|
|
14
|
-
export declare function
|
|
15
|
-
|
|
16
|
-
|
|
42
|
+
export declare function harnessColumns(width: number): number[];
|
|
43
|
+
export declare function harnessRow(id: HarnessId, p: Probe | undefined, selected: boolean, width: number): string;
|
|
44
|
+
export declare function renderHarnesses(st: HarnessesState, width: number, height: number): string[];
|
|
45
|
+
export declare function logoutSelected(id: HarnessId): string | null;
|
|
46
|
+
export declare function createHarnessesScreen(deps: {
|
|
47
|
+
host: AppHost;
|
|
48
|
+
/** Hand the raw terminal to a vendor CLI, then take it back. */
|
|
49
|
+
suspend(fn: () => void | Promise<void>): Promise<void>;
|
|
50
|
+
probe?: () => Promise<Caps>;
|
|
51
|
+
}): Screen;
|
|
52
|
+
/** `ainode harnesses` on a pipe: no screen, just the facts. */
|
|
53
|
+
export declare function plainStatus(): Promise<void>;
|