@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.
- package/dist/cli.js +2 -3
- package/dist/lockfile.d.ts +43 -3
- package/dist/lockfile.js +114 -38
- 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 +12 -12
- package/dist/tui/dashboard.js +114 -111
- package/dist/tui/harnesses.d.ts +50 -13
- package/dist/tui/harnesses.js +176 -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 +29 -0
- package/dist/tui/request-row.js +133 -0
- package/dist/tui/requests.d.ts +10 -2
- package/dist/tui/requests.js +110 -18
- 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
- package/dist/tui.d.ts +0 -1
- package/dist/tui.js +0 -314
|
@@ -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,21 @@ 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
|
+
export declare function nowLines(st: DashboardState, width: number, rows: number): string[];
|
|
33
|
+
export declare function renderDashboard(st: DashboardState, width: number, height: number): string[];
|
|
35
34
|
export declare function createDashboardScreen(deps: {
|
|
36
35
|
data: DataLayer;
|
|
37
36
|
host: AppHost;
|
|
@@ -39,4 +38,5 @@ export declare function createDashboardScreen(deps: {
|
|
|
39
38
|
openTranscript(r: RequestRow): void;
|
|
40
39
|
openRequests(): void;
|
|
41
40
|
openHarnesses(): void;
|
|
41
|
+
openLogs(): void;
|
|
42
42
|
}): Screen;
|
package/dist/tui/dashboard.js
CHANGED
|
@@ -1,87 +1,29 @@
|
|
|
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.nowLines = nowLines;
|
|
12
12
|
exports.renderDashboard = renderDashboard;
|
|
13
13
|
exports.createDashboardScreen = createDashboardScreen;
|
|
14
14
|
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());
|
|
67
|
-
}
|
|
68
|
-
function requestLine(r, now, selected, width) {
|
|
69
|
-
const entity = (0, render_1.padEndVisible)((0, render_1.truncate)(r.entity_name ?? '—', 12), 12);
|
|
70
|
-
const agent = `${r.agent}/${r.mode}`.padEnd(12).slice(0, 12);
|
|
71
|
-
const model = (r.model ?? '—').replace(/^claude-/, '').padEnd(7).slice(0, 7);
|
|
72
|
-
const via = shortVia(r.issued_via).padEnd(9).slice(0, 9);
|
|
73
|
-
const dur = (0, render_1.fmtDuration)(durationOf(r, now)).padStart(7);
|
|
74
|
-
// A run that failed once and then succeeded on retry still carries the
|
|
75
|
-
// old error_code. Showing it on a DONE row reads as "this broke" when
|
|
76
|
-
// the work actually landed — so the code is only for rows that ended badly.
|
|
77
|
-
const ended_badly = r.status !== 'completed' && !ACTIVE.has(r.status);
|
|
78
|
-
const tail = r.error_code && ended_badly
|
|
79
|
-
? (0, render_1.red)(`[${r.error_code}]`)
|
|
80
|
-
: (r.prompt ?? '').replace(/\s+/g, ' ');
|
|
81
|
-
const prefix = `${dot(r.status)} ${statusCell(r.status)} ${entity} ${(0, render_1.dim)(agent)} ${(0, render_1.dim)(model)} ${(0, render_1.dim)(via)} ${dur} `;
|
|
82
|
-
const room = Math.max(8, width - 2 - (0, render_1.visibleWidth)(prefix));
|
|
83
|
-
const line = `${prefix}${(0, render_1.truncate)(tail, room)}`;
|
|
84
|
-
return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
|
|
15
|
+
const request_row_1 = require("./request-row");
|
|
16
|
+
const app_1 = require("./app");
|
|
17
|
+
/** The landing screen's menu. */
|
|
18
|
+
exports.MENU = [
|
|
19
|
+
{ key: 'activity', label: 'Activity', hint: 'every request this node has handled', shortcut: 'a' },
|
|
20
|
+
{ key: 'harnesses', label: 'Harnesses', hint: 'install / log in agent CLIs', shortcut: 'h' },
|
|
21
|
+
{ key: 'logs', label: 'Logs', hint: 'what the daemon is saying', shortcut: 'l' },
|
|
22
|
+
];
|
|
23
|
+
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
24
|
+
exports.MAX_NOW_ROWS = 6;
|
|
25
|
+
function liveRequests(rows) {
|
|
26
|
+
return rows.filter(r => request_row_1.ACTIVE.has(r.status));
|
|
85
27
|
}
|
|
86
28
|
function headerLines(st) {
|
|
87
29
|
if (!st.paired) {
|
|
@@ -121,56 +63,110 @@ function statsLines(st) {
|
|
|
121
63
|
(0, render_1.dim)(`Week ${s.week} Month ${s.month} Total ${s.total}`),
|
|
122
64
|
];
|
|
123
65
|
}
|
|
124
|
-
/** The
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
out
|
|
137
|
-
exports.MENU.forEach((m, i) => {
|
|
138
|
-
const selected = i === st.sel;
|
|
139
|
-
const label = selected ? (0, render_1.bold)(m.label.padEnd(12)) : m.label.padEnd(12);
|
|
140
|
-
const marker = selected ? (0, render_1.cyan)('❯') : ' ';
|
|
141
|
-
out.push(`${marker} ${label} ${(0, render_1.dim)(m.hint)} ${(0, render_1.dim)(m.shortcut)}`);
|
|
142
|
-
});
|
|
66
|
+
/** The most recent request that actually finished, for the idle line. */
|
|
67
|
+
function lastFinished(rows) {
|
|
68
|
+
return rows.find(r => r.finished_at) ?? null;
|
|
69
|
+
}
|
|
70
|
+
function nowLines(st, width, rows) {
|
|
71
|
+
const live = liveRequests(st.recent);
|
|
72
|
+
const out = [` ${(0, render_1.bold)('NOW')}`];
|
|
73
|
+
if (live.length === 0) {
|
|
74
|
+
const last = lastFinished(st.recent);
|
|
75
|
+
out.push(last
|
|
76
|
+
? (0, render_1.dim)(` idle — last run finished ${(0, render_1.fmtRelative)(last.finished_at, st.now)}`)
|
|
77
|
+
: (0, render_1.dim)(' idle'));
|
|
78
|
+
return out;
|
|
143
79
|
}
|
|
144
|
-
out.push(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
80
|
+
out.push((0, request_row_1.requestHeader)(width));
|
|
81
|
+
const shown = live.slice(0, Math.max(1, rows));
|
|
82
|
+
shown.forEach(r => out.push((0, request_row_1.requestLine)(r, st.now, false, width, st.spin)));
|
|
83
|
+
if (live.length > shown.length) {
|
|
84
|
+
out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function renderDashboard(st, width, height) {
|
|
89
|
+
const head = (0, render_1.panel)(`+Ai Node ${(0, render_1.dim)(`ainode v${st.version}`)}`, headerLines(st), width);
|
|
90
|
+
if (!st.paired) {
|
|
91
|
+
return [...head, '', (0, app_1.footerHint)([{ keys: 'q', label: 'quit' }])].slice(0, height);
|
|
149
92
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
93
|
+
const stats = statsLines(st).map(l => ` ${l}`);
|
|
94
|
+
// The shortcut letters line up in a column of their own, so the eye can
|
|
95
|
+
// find them without reading the hints.
|
|
96
|
+
const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
|
|
97
|
+
const menu = exports.MENU.map((m, i) => {
|
|
98
|
+
const selected = i === st.sel;
|
|
99
|
+
const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
|
|
100
|
+
const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
|
|
101
|
+
const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
|
|
102
|
+
return `${cursor} ${label} ${(0, render_1.dim)(m.hint.padEnd(hintWidth))}${(0, render_1.dim)(badge.padEnd(10))}${(0, render_1.dim)(m.shortcut)}`;
|
|
103
|
+
});
|
|
104
|
+
const foot = (0, app_1.footerHint)([
|
|
105
|
+
{ keys: '↑↓', label: 'move' },
|
|
106
|
+
{ keys: '⏎', label: 'open' },
|
|
107
|
+
{ keys: 'r', label: 'refresh' },
|
|
108
|
+
{ keys: '?', label: 'keys' },
|
|
109
|
+
{ keys: 'q', label: 'quit' },
|
|
110
|
+
]);
|
|
111
|
+
// Everything but the NOW band is fixed height, so the band takes what is
|
|
112
|
+
// left: heading + header row + rows + the "+N more" line.
|
|
113
|
+
const fixed = head.length + 1 + stats.length + 1 + 3 + 1 + menu.length + 1 + 1;
|
|
114
|
+
const room = Math.min(exports.MAX_NOW_ROWS, Math.max(1, height - fixed));
|
|
115
|
+
return [
|
|
116
|
+
...head,
|
|
117
|
+
'',
|
|
118
|
+
...stats,
|
|
119
|
+
'',
|
|
120
|
+
...nowLines(st, width, room),
|
|
121
|
+
'',
|
|
122
|
+
...menu,
|
|
123
|
+
'',
|
|
124
|
+
foot,
|
|
125
|
+
].map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
153
126
|
}
|
|
154
127
|
function createDashboardScreen(deps) {
|
|
155
128
|
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
129
|
const refresh = async () => {
|
|
159
130
|
st.now = Date.now();
|
|
160
|
-
const [self, stats] = await Promise.all([
|
|
131
|
+
const [self, stats, recent] = await Promise.all([
|
|
132
|
+
deps.data.self(),
|
|
133
|
+
deps.data.stats(),
|
|
134
|
+
// Enough rows to hold every plausible concurrent run plus something
|
|
135
|
+
// finished for the idle line, and small enough to poll every 2s.
|
|
136
|
+
deps.data.requests({ limit: 20 }),
|
|
137
|
+
]);
|
|
161
138
|
if (self)
|
|
162
139
|
st.self = self;
|
|
163
140
|
if (stats)
|
|
164
141
|
st.stats = stats;
|
|
142
|
+
if (recent.length)
|
|
143
|
+
st.recent = recent;
|
|
165
144
|
st.offline = deps.data.offline();
|
|
166
145
|
deps.host.redraw();
|
|
167
146
|
};
|
|
168
147
|
return {
|
|
169
148
|
id: 'dashboard',
|
|
170
149
|
title: '+Ai Node',
|
|
171
|
-
render: (width) => renderDashboard(st, width),
|
|
172
|
-
|
|
150
|
+
render: (width, height) => renderDashboard(st, width, height),
|
|
151
|
+
// Poll hard while something is moving; an idle node doesn't need traffic.
|
|
152
|
+
pollMs: () => (liveRequests(st.recent).length > 0 || st.inflight > 0 ? 2000 : 5000),
|
|
173
153
|
poll: refresh,
|
|
154
|
+
tick(n) {
|
|
155
|
+
st.spin = n;
|
|
156
|
+
// Only the NOW band animates, and only when something is in it.
|
|
157
|
+
if (liveRequests(st.recent).length === 0)
|
|
158
|
+
return false;
|
|
159
|
+
st.now = Date.now();
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
keys: () => [
|
|
163
|
+
{ keys: '↑↓ / jk', label: 'move between destinations' },
|
|
164
|
+
{ keys: '⏎', label: 'open the selected destination' },
|
|
165
|
+
{ keys: 'a', label: 'activity — full request history' },
|
|
166
|
+
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
167
|
+
{ keys: 'l', label: 'logs — daemon output' },
|
|
168
|
+
{ keys: 'r', label: 'refresh now' },
|
|
169
|
+
],
|
|
174
170
|
async onKey(key) {
|
|
175
171
|
if (key.name === 'up' || key.name === 'k') {
|
|
176
172
|
st.sel = Math.max(0, st.sel - 1);
|
|
@@ -190,13 +186,20 @@ function createDashboardScreen(deps) {
|
|
|
190
186
|
deps.openHarnesses();
|
|
191
187
|
return;
|
|
192
188
|
}
|
|
189
|
+
if (key.name === 'l') {
|
|
190
|
+
deps.openLogs();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
193
|
if (key.name === 'r') {
|
|
194
194
|
await refresh();
|
|
195
195
|
return;
|
|
196
196
|
}
|
|
197
197
|
if (key.name === 'return') {
|
|
198
|
-
|
|
198
|
+
const target = exports.MENU[st.sel]?.key;
|
|
199
|
+
if (target === 'harnesses')
|
|
199
200
|
deps.openHarnesses();
|
|
201
|
+
else if (target === 'logs')
|
|
202
|
+
deps.openLogs();
|
|
200
203
|
else
|
|
201
204
|
deps.openRequests();
|
|
202
205
|
}
|
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>;
|