@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
package/dist/tui/render.d.ts
CHANGED
|
@@ -4,6 +4,26 @@ export declare function visibleWidth(s: string): number;
|
|
|
4
4
|
/** Cut to `max` VISIBLE columns, ellipsis included in the budget. */
|
|
5
5
|
export declare function truncate(s: string, max: number): string;
|
|
6
6
|
export declare function padEndVisible(s: string, width: number): string;
|
|
7
|
+
export declare function padStartVisible(s: string, width: number): string;
|
|
8
|
+
/** A column that takes what it needs, or grows to absorb what's spare. */
|
|
9
|
+
export interface ColumnSpec {
|
|
10
|
+
/** Columns narrower than this are unreadable, so they are never cut below it. */
|
|
11
|
+
min: number;
|
|
12
|
+
/** Share of the leftover width. 0 (the default) means fixed at `min`. */
|
|
13
|
+
grow?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Split `width` across columns.
|
|
17
|
+
*
|
|
18
|
+
* Fixed columns are served first and always get their `min`. Whatever is left
|
|
19
|
+
* over after the gaps is handed to the growing columns in proportion to their
|
|
20
|
+
* `grow` — so a table clips the prompt, never the version or the account.
|
|
21
|
+
* When the terminal is too narrow for even the minimums, every column shrinks
|
|
22
|
+
* proportionally rather than the last one falling off the edge.
|
|
23
|
+
*/
|
|
24
|
+
export declare function layoutColumns(specs: ColumnSpec[], width: number, gap?: number): number[];
|
|
25
|
+
/** Lay cells into `widths`, truncating and padding each to fit exactly. */
|
|
26
|
+
export declare function tableRow(cells: string[], widths: number[], align?: Array<'left' | 'right'>, gap?: number): string;
|
|
7
27
|
export declare const dim: (s: string) => string;
|
|
8
28
|
export declare const bold: (s: string) => string;
|
|
9
29
|
export declare const green: (s: string) => string;
|
|
@@ -19,3 +39,23 @@ export declare function fmtDuration(ms: number | null): string;
|
|
|
19
39
|
export declare const altOn: () => void;
|
|
20
40
|
export declare const altOff: () => void;
|
|
21
41
|
export declare const home: () => void;
|
|
42
|
+
export declare function termHeight(): number;
|
|
43
|
+
export interface Painter {
|
|
44
|
+
/** Write only what changed since the last frame. */
|
|
45
|
+
paint(lines: string[]): void;
|
|
46
|
+
/** Force the next paint to clear and redraw everything (resize, resume). */
|
|
47
|
+
invalidate(): void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Frames are painted as a diff against the last one.
|
|
51
|
+
*
|
|
52
|
+
* The screen used to be erased (`ESC[2J`) before every frame, and with the
|
|
53
|
+
* spinner redrawing eight times a second while work is in flight, that is a
|
|
54
|
+
* full clear-and-repaint eight times a second — the flicker you can see from
|
|
55
|
+
* across the room. Almost nothing changes between those frames: a spinner
|
|
56
|
+
* glyph and a duration. So keep the last frame, compare line by line, and
|
|
57
|
+
* touch only the rows that actually moved.
|
|
58
|
+
*
|
|
59
|
+
* `write` is injected so the painter can be unit-tested without a TTY.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createPainter(write: (s: string) => void): Painter;
|
package/dist/tui/render.js
CHANGED
|
@@ -11,10 +11,15 @@ exports.stripAnsi = stripAnsi;
|
|
|
11
11
|
exports.visibleWidth = visibleWidth;
|
|
12
12
|
exports.truncate = truncate;
|
|
13
13
|
exports.padEndVisible = padEndVisible;
|
|
14
|
+
exports.padStartVisible = padStartVisible;
|
|
15
|
+
exports.layoutColumns = layoutColumns;
|
|
16
|
+
exports.tableRow = tableRow;
|
|
14
17
|
exports.termWidth = termWidth;
|
|
15
18
|
exports.panel = panel;
|
|
16
19
|
exports.fmtRelative = fmtRelative;
|
|
17
20
|
exports.fmtDuration = fmtDuration;
|
|
21
|
+
exports.termHeight = termHeight;
|
|
22
|
+
exports.createPainter = createPainter;
|
|
18
23
|
const E = '\x1b[';
|
|
19
24
|
exports.SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
20
25
|
/** Not global: a /g regex carries lastIndex between calls and would make
|
|
@@ -59,6 +64,58 @@ function padEndVisible(s, width) {
|
|
|
59
64
|
const pad = Math.max(0, width - visibleWidth(s));
|
|
60
65
|
return s + ' '.repeat(pad);
|
|
61
66
|
}
|
|
67
|
+
function padStartVisible(s, width) {
|
|
68
|
+
const pad = Math.max(0, width - visibleWidth(s));
|
|
69
|
+
return ' '.repeat(pad) + s;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Split `width` across columns.
|
|
73
|
+
*
|
|
74
|
+
* Fixed columns are served first and always get their `min`. Whatever is left
|
|
75
|
+
* over after the gaps is handed to the growing columns in proportion to their
|
|
76
|
+
* `grow` — so a table clips the prompt, never the version or the account.
|
|
77
|
+
* When the terminal is too narrow for even the minimums, every column shrinks
|
|
78
|
+
* proportionally rather than the last one falling off the edge.
|
|
79
|
+
*/
|
|
80
|
+
function layoutColumns(specs, width, gap = 1) {
|
|
81
|
+
if (specs.length === 0)
|
|
82
|
+
return [];
|
|
83
|
+
const gaps = gap * (specs.length - 1);
|
|
84
|
+
const mins = specs.map(s => Math.max(0, s.min));
|
|
85
|
+
const totalMin = mins.reduce((a, b) => a + b, 0);
|
|
86
|
+
const room = Math.max(0, width - gaps);
|
|
87
|
+
if (totalMin > room) {
|
|
88
|
+
const scale = room / (totalMin || 1);
|
|
89
|
+
return mins.map(m => Math.max(1, Math.floor(m * scale)));
|
|
90
|
+
}
|
|
91
|
+
const totalGrow = specs.reduce((a, s) => a + (s.grow ?? 0), 0);
|
|
92
|
+
let spare = room - totalMin;
|
|
93
|
+
if (totalGrow === 0 || spare === 0)
|
|
94
|
+
return mins;
|
|
95
|
+
// The last growing column takes the remainder, so rounding never loses a
|
|
96
|
+
// column of width off the right-hand edge.
|
|
97
|
+
let lastGrowing = -1;
|
|
98
|
+
specs.forEach((s, i) => { if ((s.grow ?? 0) > 0)
|
|
99
|
+
lastGrowing = i; });
|
|
100
|
+
return mins.map((m, i) => {
|
|
101
|
+
const grow = specs[i].grow ?? 0;
|
|
102
|
+
if (grow === 0)
|
|
103
|
+
return m;
|
|
104
|
+
const share = i === lastGrowing ? spare : Math.floor((spare * grow) / totalGrow);
|
|
105
|
+
spare -= share;
|
|
106
|
+
return m + share;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Lay cells into `widths`, truncating and padding each to fit exactly. */
|
|
110
|
+
function tableRow(cells, widths, align = [], gap = 1) {
|
|
111
|
+
return cells
|
|
112
|
+
.map((c, i) => {
|
|
113
|
+
const w = widths[i] ?? 0;
|
|
114
|
+
const cut = truncate(c, w);
|
|
115
|
+
return align[i] === 'right' ? padStartVisible(cut, w) : padEndVisible(cut, w);
|
|
116
|
+
})
|
|
117
|
+
.join(' '.repeat(gap));
|
|
118
|
+
}
|
|
62
119
|
const wrapCode = (on, off) => (s) => `${E}${on}${s}${E}${off}`;
|
|
63
120
|
exports.dim = wrapCode('2m', '22m');
|
|
64
121
|
exports.bold = wrapCode('1m', '22m');
|
|
@@ -114,3 +171,45 @@ const altOff = () => { process.stdout.write(`${E}?1049l${E}?25h`); };
|
|
|
114
171
|
exports.altOff = altOff;
|
|
115
172
|
const home = () => { process.stdout.write(`${E}H${E}2J`); };
|
|
116
173
|
exports.home = home;
|
|
174
|
+
function termHeight() {
|
|
175
|
+
return Math.max(10, process.stdout.rows || 30);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Frames are painted as a diff against the last one.
|
|
179
|
+
*
|
|
180
|
+
* The screen used to be erased (`ESC[2J`) before every frame, and with the
|
|
181
|
+
* spinner redrawing eight times a second while work is in flight, that is a
|
|
182
|
+
* full clear-and-repaint eight times a second — the flicker you can see from
|
|
183
|
+
* across the room. Almost nothing changes between those frames: a spinner
|
|
184
|
+
* glyph and a duration. So keep the last frame, compare line by line, and
|
|
185
|
+
* touch only the rows that actually moved.
|
|
186
|
+
*
|
|
187
|
+
* `write` is injected so the painter can be unit-tested without a TTY.
|
|
188
|
+
*/
|
|
189
|
+
function createPainter(write) {
|
|
190
|
+
let prev = [];
|
|
191
|
+
let full = true;
|
|
192
|
+
return {
|
|
193
|
+
invalidate() { full = true; },
|
|
194
|
+
paint(lines) {
|
|
195
|
+
let out = '';
|
|
196
|
+
if (full) {
|
|
197
|
+
out += `${E}H${E}2J`;
|
|
198
|
+
prev = [];
|
|
199
|
+
full = false;
|
|
200
|
+
}
|
|
201
|
+
const rows = Math.max(lines.length, prev.length);
|
|
202
|
+
for (let i = 0; i < rows; i++) {
|
|
203
|
+
const next = lines[i] ?? '';
|
|
204
|
+
if (prev[i] === next)
|
|
205
|
+
continue;
|
|
206
|
+
// Position, write, and erase whatever the previous, longer line left
|
|
207
|
+
// behind on that row.
|
|
208
|
+
out += `${E}${i + 1};1H${next}${E}K`;
|
|
209
|
+
}
|
|
210
|
+
prev = lines.slice();
|
|
211
|
+
if (out)
|
|
212
|
+
write(out);
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { RequestRow } from './data';
|
|
2
|
+
export declare const ACTIVE: Set<string>;
|
|
3
|
+
export declare const CANCELLED: Set<string>;
|
|
4
|
+
/**
|
|
5
|
+
* Trigger sources are DB-shaped ('self_improve_distil', 'tables_trigger');
|
|
6
|
+
* the column is nine columns wide and 'self_impr' tells you nothing.
|
|
7
|
+
*
|
|
8
|
+
* Anything unmapped is shortened by rule rather than cut mid-word: the
|
|
9
|
+
* 'entity_' prefix and the '_trigger' suffix carry no information a reader of
|
|
10
|
+
* this column needs.
|
|
11
|
+
*/
|
|
12
|
+
export declare function shortVia(via: string | null | undefined): string;
|
|
13
|
+
/**
|
|
14
|
+
* Model ids carry a vendor prefix and a release date the column has no room
|
|
15
|
+
* for: `claude-haiku-4-5-20251001` was rendering as `haiku-4-…`, which loses
|
|
16
|
+
* the one part you were reading it for.
|
|
17
|
+
*/
|
|
18
|
+
export declare function shortModel(model: string | null | undefined): string;
|
|
19
|
+
export declare function statusLabel(status: string): string;
|
|
20
|
+
export declare function statusColour(status: string): (s: string) => string;
|
|
21
|
+
/** Live rows get a moving spinner; settled rows get a dot. */
|
|
22
|
+
export declare function marker(status: string, spin: number): string;
|
|
23
|
+
export declare function durationOf(r: RequestRow, now: number): number | null;
|
|
24
|
+
/**
|
|
25
|
+
* Column widths for the request table.
|
|
26
|
+
*
|
|
27
|
+
* The two columns that carry names — the entity and the prompt — grow into
|
|
28
|
+
* whatever the terminal has spare, and the prompt takes the larger share
|
|
29
|
+
* because it is the only free text on the row. Everything else is fixed at
|
|
30
|
+
* the width its values actually need, so a version or a duration is never
|
|
31
|
+
* the thing that gets cut.
|
|
32
|
+
*/
|
|
33
|
+
export declare function requestColumns(width: number): number[];
|
|
34
|
+
export declare function requestHeader(width: number): string;
|
|
35
|
+
export declare function requestLine(r: RequestRow, now: number, selected: boolean, width: number, spin?: number): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// One request, rendered as a row — shared by the cockpit's NOW band and the
|
|
3
|
+
// Activity screen so the two never drift apart.
|
|
4
|
+
//
|
|
5
|
+
// Status vocabulary comes from the DB, where terminal success is 'completed'
|
|
6
|
+
// and 'canceled' is a deliberate user action — shown apart from failures so a
|
|
7
|
+
// healthy node isn't slandered by its own cancels.
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CANCELLED = exports.ACTIVE = void 0;
|
|
10
|
+
exports.shortVia = shortVia;
|
|
11
|
+
exports.shortModel = shortModel;
|
|
12
|
+
exports.statusLabel = statusLabel;
|
|
13
|
+
exports.statusColour = statusColour;
|
|
14
|
+
exports.marker = marker;
|
|
15
|
+
exports.durationOf = durationOf;
|
|
16
|
+
exports.requestColumns = requestColumns;
|
|
17
|
+
exports.requestHeader = requestHeader;
|
|
18
|
+
exports.requestLine = requestLine;
|
|
19
|
+
const render_1 = require("./render");
|
|
20
|
+
exports.ACTIVE = new Set(['pending', 'starting', 'running']);
|
|
21
|
+
exports.CANCELLED = new Set(['canceled', 'cancelled']);
|
|
22
|
+
/**
|
|
23
|
+
* Trigger sources are DB-shaped ('self_improve_distil', 'tables_trigger');
|
|
24
|
+
* the column is nine columns wide and 'self_impr' tells you nothing.
|
|
25
|
+
*
|
|
26
|
+
* Anything unmapped is shortened by rule rather than cut mid-word: the
|
|
27
|
+
* 'entity_' prefix and the '_trigger' suffix carry no information a reader of
|
|
28
|
+
* this column needs.
|
|
29
|
+
*/
|
|
30
|
+
function shortVia(via) {
|
|
31
|
+
if (!via)
|
|
32
|
+
return '—';
|
|
33
|
+
const map = {
|
|
34
|
+
self_improve_distil: 'distil',
|
|
35
|
+
self_improve: 'improve',
|
|
36
|
+
entity_chat: 'chat',
|
|
37
|
+
chat: 'chat',
|
|
38
|
+
chatflows: 'chatflow',
|
|
39
|
+
entity_schedule: 'schedule',
|
|
40
|
+
schedule: 'schedule',
|
|
41
|
+
tables_trigger: 'tables',
|
|
42
|
+
skillflow: 'skillflow',
|
|
43
|
+
flow: 'flow',
|
|
44
|
+
api: 'api',
|
|
45
|
+
studio: 'studio',
|
|
46
|
+
widget: 'widget',
|
|
47
|
+
};
|
|
48
|
+
if (map[via])
|
|
49
|
+
return map[via];
|
|
50
|
+
return via.replace(/^entity_/, '').replace(/_trigger$/, '');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Model ids carry a vendor prefix and a release date the column has no room
|
|
54
|
+
* for: `claude-haiku-4-5-20251001` was rendering as `haiku-4-…`, which loses
|
|
55
|
+
* the one part you were reading it for.
|
|
56
|
+
*/
|
|
57
|
+
function shortModel(model) {
|
|
58
|
+
if (!model)
|
|
59
|
+
return '—';
|
|
60
|
+
return model.replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
61
|
+
}
|
|
62
|
+
function statusLabel(status) {
|
|
63
|
+
if (status === 'completed')
|
|
64
|
+
return 'Done';
|
|
65
|
+
if (exports.CANCELLED.has(status))
|
|
66
|
+
return 'Cancelled';
|
|
67
|
+
if (status === 'dead_letter')
|
|
68
|
+
return 'Dead';
|
|
69
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
70
|
+
}
|
|
71
|
+
function statusColour(status) {
|
|
72
|
+
if (exports.ACTIVE.has(status))
|
|
73
|
+
return render_1.cyan;
|
|
74
|
+
if (status === 'completed')
|
|
75
|
+
return render_1.green;
|
|
76
|
+
if (exports.CANCELLED.has(status))
|
|
77
|
+
return render_1.grey;
|
|
78
|
+
return render_1.red;
|
|
79
|
+
}
|
|
80
|
+
/** Live rows get a moving spinner; settled rows get a dot. */
|
|
81
|
+
function marker(status, spin) {
|
|
82
|
+
const colour = statusColour(status);
|
|
83
|
+
return colour(exports.ACTIVE.has(status) ? render_1.SPIN[spin % render_1.SPIN.length] : '⏺');
|
|
84
|
+
}
|
|
85
|
+
function durationOf(r, now) {
|
|
86
|
+
const start = r.picked_up_at ?? r.created_at;
|
|
87
|
+
if (!start)
|
|
88
|
+
return null;
|
|
89
|
+
const end = r.finished_at ? new Date(r.finished_at).getTime() : now;
|
|
90
|
+
return Math.max(0, end - new Date(start).getTime());
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Column widths for the request table.
|
|
94
|
+
*
|
|
95
|
+
* The two columns that carry names — the entity and the prompt — grow into
|
|
96
|
+
* whatever the terminal has spare, and the prompt takes the larger share
|
|
97
|
+
* because it is the only free text on the row. Everything else is fixed at
|
|
98
|
+
* the width its values actually need, so a version or a duration is never
|
|
99
|
+
* the thing that gets cut.
|
|
100
|
+
*/
|
|
101
|
+
function requestColumns(width) {
|
|
102
|
+
// marker, status, entity, agent/mode, model, via, duration, prompt
|
|
103
|
+
return (0, render_1.layoutColumns)([
|
|
104
|
+
{ min: 1 },
|
|
105
|
+
// 'Cancelled' is the longest label; anything shorter than it clips the
|
|
106
|
+
// status, which is the one column that must never be ambiguous.
|
|
107
|
+
{ min: 9 },
|
|
108
|
+
{ min: 10, grow: 1 },
|
|
109
|
+
{ min: 12 },
|
|
110
|
+
// 'haiku-4-5' is nine columns once the claude- prefix is stripped.
|
|
111
|
+
{ min: 9 },
|
|
112
|
+
{ min: 8 },
|
|
113
|
+
{ min: 7 },
|
|
114
|
+
{ min: 12, grow: 3 },
|
|
115
|
+
], Math.max(40, width - 2));
|
|
116
|
+
}
|
|
117
|
+
const ALIGN = ['left', 'left', 'left', 'left', 'left', 'left', 'right', 'left'];
|
|
118
|
+
function requestHeader(width) {
|
|
119
|
+
const w = requestColumns(width);
|
|
120
|
+
return (0, render_1.dim)(' ' + (0, render_1.tableRow)(['', 'Status', 'Entity', 'Agent', 'Model', 'Via', 'Dur', 'Prompt'], w, ALIGN));
|
|
121
|
+
}
|
|
122
|
+
function requestLine(r, now, selected, width, spin = 0) {
|
|
123
|
+
const w = requestColumns(width);
|
|
124
|
+
const colour = statusColour(r.status);
|
|
125
|
+
// A run that failed once and then succeeded on retry still carries the
|
|
126
|
+
// old error_code. Showing it on a DONE row reads as "this broke" when
|
|
127
|
+
// the work actually landed — so the code is only for rows that ended badly.
|
|
128
|
+
const endedBadly = r.status !== 'completed' && !exports.ACTIVE.has(r.status);
|
|
129
|
+
const tail = r.error_code && endedBadly
|
|
130
|
+
? (0, render_1.red)(`[${r.error_code}]`)
|
|
131
|
+
: (r.prompt ?? '').replace(/\s+/g, ' ');
|
|
132
|
+
const cells = [
|
|
133
|
+
marker(r.status, spin),
|
|
134
|
+
colour(statusLabel(r.status)),
|
|
135
|
+
r.entity_name ?? '—',
|
|
136
|
+
(0, render_1.dim)(`${r.agent}/${r.mode}`),
|
|
137
|
+
(0, render_1.dim)(shortModel(r.model)),
|
|
138
|
+
(0, render_1.dim)(shortVia(r.issued_via)),
|
|
139
|
+
(0, render_1.dim)((0, render_1.fmtDuration)(durationOf(r, now))),
|
|
140
|
+
tail,
|
|
141
|
+
];
|
|
142
|
+
const line = (0, render_1.tableRow)(cells, w, ALIGN);
|
|
143
|
+
return selected ? `${(0, render_1.cyan)('❯')} ${line}` : ` ${line}`;
|
|
144
|
+
}
|
package/dist/tui/requests.d.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import { type AppHost, type Screen } from './app';
|
|
1
2
|
import type { DataLayer, RequestRow } from './data';
|
|
2
|
-
import type { AppHost, Screen } from './app';
|
|
3
3
|
export interface RequestsState {
|
|
4
4
|
rows: RequestRow[];
|
|
5
5
|
sel: number;
|
|
6
|
+
/** Index of the first visible row. */
|
|
7
|
+
top: number;
|
|
8
|
+
/** Rows the last render could fit — how far a PgDn moves, and what
|
|
9
|
+
* onKey needs to know to keep the cursor inside the window. */
|
|
10
|
+
rowsVisible: number;
|
|
6
11
|
filter: string;
|
|
7
12
|
filtering: boolean;
|
|
8
13
|
loading: boolean;
|
|
9
14
|
exhausted: boolean;
|
|
15
|
+
spin: number;
|
|
10
16
|
now: number;
|
|
11
17
|
}
|
|
12
18
|
export declare function filterRows(rows: RequestRow[], filter: string): RequestRow[];
|
|
13
|
-
|
|
19
|
+
/** Scroll the window the smallest distance that puts the cursor back in it. */
|
|
20
|
+
export declare function windowTop(sel: number, top: number, rows: number, total: number): number;
|
|
21
|
+
export declare function renderRequests(st: RequestsState, width: number, height: number): string[];
|
|
14
22
|
export declare function createRequestsScreen(deps: {
|
|
15
23
|
data: DataLayer;
|
|
16
24
|
host: AppHost;
|
package/dist/tui/requests.js
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Full request history for this node: cursor-paged, filterable.
|
|
2
|
+
// Full request history for this node: windowed, cursor-paged, filterable.
|
|
3
|
+
//
|
|
4
|
+
// The list is windowed rather than fully rendered. Rendering all loaded rows
|
|
5
|
+
// meant a 50-row page painted 50 lines into a 40-row terminal, and the heading
|
|
6
|
+
// and newest rows scrolled off the top of the alt screen.
|
|
3
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
8
|
exports.filterRows = filterRows;
|
|
9
|
+
exports.windowTop = windowTop;
|
|
5
10
|
exports.renderRequests = renderRequests;
|
|
6
11
|
exports.createRequestsScreen = createRequestsScreen;
|
|
7
12
|
const render_1 = require("./render");
|
|
8
|
-
const
|
|
13
|
+
const request_row_1 = require("./request-row");
|
|
14
|
+
const app_1 = require("./app");
|
|
9
15
|
const PAGE = 50;
|
|
16
|
+
/** Lines the screen spends on things that are not rows. */
|
|
17
|
+
const CHROME = 6;
|
|
10
18
|
function filterRows(rows, filter) {
|
|
11
19
|
const f = filter.trim().toLowerCase();
|
|
12
20
|
if (!f)
|
|
@@ -16,33 +24,59 @@ function filterRows(rows, filter) {
|
|
|
16
24
|
(r.agent ?? '').toLowerCase().includes(f) ||
|
|
17
25
|
(r.error_code ?? '').toLowerCase().includes(f));
|
|
18
26
|
}
|
|
19
|
-
|
|
27
|
+
/** Scroll the window the smallest distance that puts the cursor back in it. */
|
|
28
|
+
function windowTop(sel, top, rows, total) {
|
|
29
|
+
const max = Math.max(0, total - rows);
|
|
30
|
+
let next = top;
|
|
31
|
+
if (sel < next)
|
|
32
|
+
next = sel;
|
|
33
|
+
if (sel >= next + rows)
|
|
34
|
+
next = sel - rows + 1;
|
|
35
|
+
return Math.min(Math.max(0, next), max);
|
|
36
|
+
}
|
|
37
|
+
function renderRequests(st, width, height) {
|
|
20
38
|
const shown = filterRows(st.rows, st.filter);
|
|
39
|
+
const rows = Math.max(1, height - CHROME);
|
|
40
|
+
st.rowsVisible = rows;
|
|
41
|
+
st.top = windowTop(st.sel, st.top, rows, shown.length);
|
|
21
42
|
const out = [];
|
|
22
|
-
|
|
43
|
+
const scope = st.filter ? `${shown.length} matching “${st.filter}”` : `${shown.length} loaded`;
|
|
44
|
+
out.push((0, app_1.heading)('Activity', scope));
|
|
23
45
|
out.push('');
|
|
24
46
|
if (shown.length === 0) {
|
|
25
|
-
out.push((0, render_1.dim)(st.filter ? '
|
|
47
|
+
out.push((0, render_1.dim)(st.filter ? ' No requests match that filter' : ' No requests yet'));
|
|
26
48
|
}
|
|
27
49
|
else {
|
|
28
|
-
|
|
50
|
+
out.push((0, request_row_1.requestHeader)(width));
|
|
51
|
+
shown
|
|
52
|
+
.slice(st.top, st.top + rows)
|
|
53
|
+
.forEach((r, i) => out.push((0, request_row_1.requestLine)(r, st.now, st.top + i === st.sel, width, st.spin)));
|
|
29
54
|
}
|
|
30
|
-
|
|
55
|
+
// Pad so the footer sits at the bottom of the screen rather than floating
|
|
56
|
+
// up under a short list.
|
|
57
|
+
while (out.length < height - 2)
|
|
58
|
+
out.push('');
|
|
31
59
|
if (st.loading)
|
|
32
|
-
out.push((0, render_1.dim)('
|
|
60
|
+
out.push((0, render_1.dim)(' Loading more…'));
|
|
33
61
|
else if (st.exhausted)
|
|
34
|
-
out.push((0, render_1.dim)(
|
|
62
|
+
out.push((0, render_1.dim)(` End of history — ${shown.length} rows`));
|
|
35
63
|
else
|
|
36
|
-
out.push((0, render_1.dim)(' ↓
|
|
64
|
+
out.push((0, render_1.dim)(' ↓ past the last row loads more'));
|
|
37
65
|
out.push(st.filtering
|
|
38
66
|
? ` ${(0, render_1.cyan)('/')}${st.filter}${(0, render_1.cyan)('▏')}`
|
|
39
|
-
: (0,
|
|
40
|
-
|
|
67
|
+
: (0, app_1.footerHint)([
|
|
68
|
+
{ keys: '↑↓', label: 'move' },
|
|
69
|
+
{ keys: '⏎', label: 'transcript' },
|
|
70
|
+
{ keys: '/', label: 'filter' },
|
|
71
|
+
{ keys: 'q', label: 'back' },
|
|
72
|
+
]));
|
|
73
|
+
return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
41
74
|
}
|
|
42
75
|
function createRequestsScreen(deps) {
|
|
43
76
|
const st = {
|
|
44
|
-
rows: [], sel: 0,
|
|
45
|
-
|
|
77
|
+
rows: [], sel: 0, top: 0, rowsVisible: 10,
|
|
78
|
+
filter: '', filtering: false,
|
|
79
|
+
loading: false, exhausted: false, spin: 0, now: Date.now(),
|
|
46
80
|
};
|
|
47
81
|
async function loadMore() {
|
|
48
82
|
if (st.loading || st.exhausted)
|
|
@@ -65,12 +99,32 @@ function createRequestsScreen(deps) {
|
|
|
65
99
|
deps.host.redraw();
|
|
66
100
|
}
|
|
67
101
|
}
|
|
102
|
+
const move = (delta, total) => {
|
|
103
|
+
st.sel = Math.min(Math.max(0, total - 1), Math.max(0, st.sel + delta));
|
|
104
|
+
st.top = windowTop(st.sel, st.top, st.rowsVisible, total);
|
|
105
|
+
};
|
|
68
106
|
return {
|
|
69
107
|
id: 'requests',
|
|
70
|
-
title: '
|
|
71
|
-
render: (width) => renderRequests(st, width),
|
|
108
|
+
title: 'Activity',
|
|
109
|
+
render: (width, height) => renderRequests(st, width, height),
|
|
72
110
|
pollMs: () => 5000,
|
|
73
111
|
capturesKeys: () => st.filtering,
|
|
112
|
+
tick(n) {
|
|
113
|
+
st.spin = n;
|
|
114
|
+
// Durations only tick on rows that haven't finished.
|
|
115
|
+
const live = st.rows.slice(st.top, st.top + st.rowsVisible).some(r => request_row_1.ACTIVE.has(r.status));
|
|
116
|
+
if (live)
|
|
117
|
+
st.now = Date.now();
|
|
118
|
+
return live;
|
|
119
|
+
},
|
|
120
|
+
keys: () => [
|
|
121
|
+
{ keys: '↑↓ / jk', label: 'move the cursor' },
|
|
122
|
+
{ keys: 'PgUp/PgDn', label: 'move a screenful' },
|
|
123
|
+
{ keys: 'g / G', label: 'first row / last loaded row' },
|
|
124
|
+
{ keys: '⏎', label: 'open the transcript' },
|
|
125
|
+
{ keys: '/', label: 'filter by entity, status, agent or error code' },
|
|
126
|
+
{ keys: 'esc', label: 'clear the filter' },
|
|
127
|
+
],
|
|
74
128
|
async poll() {
|
|
75
129
|
st.now = Date.now();
|
|
76
130
|
if (st.rows.length === 0)
|
|
@@ -79,13 +133,23 @@ function createRequestsScreen(deps) {
|
|
|
79
133
|
},
|
|
80
134
|
async onKey(key) {
|
|
81
135
|
if (st.filtering) {
|
|
82
|
-
if (key.name === 'return'
|
|
136
|
+
if (key.name === 'return') {
|
|
137
|
+
st.filtering = false;
|
|
138
|
+
deps.host.redraw();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (key.name === 'escape') {
|
|
83
142
|
st.filtering = false;
|
|
143
|
+
st.filter = '';
|
|
144
|
+
st.sel = 0;
|
|
145
|
+
st.top = 0;
|
|
84
146
|
deps.host.redraw();
|
|
85
147
|
return;
|
|
86
148
|
}
|
|
87
149
|
if (key.name === 'backspace') {
|
|
88
150
|
st.filter = st.filter.slice(0, -1);
|
|
151
|
+
st.sel = 0;
|
|
152
|
+
st.top = 0;
|
|
89
153
|
deps.host.redraw();
|
|
90
154
|
return;
|
|
91
155
|
}
|
|
@@ -94,18 +158,38 @@ function createRequestsScreen(deps) {
|
|
|
94
158
|
const ch = key.sequence ?? key.name ?? '';
|
|
95
159
|
if (ch.length === 1 && ch >= ' ') {
|
|
96
160
|
st.filter += ch;
|
|
161
|
+
st.sel = 0;
|
|
162
|
+
st.top = 0;
|
|
97
163
|
deps.host.redraw();
|
|
98
164
|
}
|
|
99
165
|
return;
|
|
100
166
|
}
|
|
101
167
|
const shown = filterRows(st.rows, st.filter);
|
|
168
|
+
const page = Math.max(1, st.rowsVisible - 1);
|
|
102
169
|
if (key.name === 'up' || key.name === 'k') {
|
|
103
|
-
|
|
170
|
+
move(-1, shown.length);
|
|
171
|
+
deps.host.redraw();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (key.name === 'pageup') {
|
|
175
|
+
move(-page, shown.length);
|
|
104
176
|
deps.host.redraw();
|
|
105
177
|
return;
|
|
106
178
|
}
|
|
107
|
-
if (key.name === '
|
|
108
|
-
st.sel =
|
|
179
|
+
if (key.name === 'g' && !key.shift) {
|
|
180
|
+
st.sel = 0;
|
|
181
|
+
st.top = 0;
|
|
182
|
+
deps.host.redraw();
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (key.name === 'g' && key.shift) {
|
|
186
|
+
move(shown.length, shown.length);
|
|
187
|
+
deps.host.redraw();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (key.name === 'down' || key.name === 'j' || key.name === 'pagedown') {
|
|
191
|
+
move(key.name === 'pagedown' ? page : 1, shown.length);
|
|
192
|
+
// Reaching the end of what's loaded is the request for more.
|
|
109
193
|
if (st.sel >= shown.length - 1)
|
|
110
194
|
await loadMore();
|
|
111
195
|
deps.host.redraw();
|
|
@@ -115,6 +199,14 @@ function createRequestsScreen(deps) {
|
|
|
115
199
|
st.filtering = true;
|
|
116
200
|
st.filter = '';
|
|
117
201
|
st.sel = 0;
|
|
202
|
+
st.top = 0;
|
|
203
|
+
deps.host.redraw();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.name === 'escape' && st.filter) {
|
|
207
|
+
st.filter = '';
|
|
208
|
+
st.sel = 0;
|
|
209
|
+
st.top = 0;
|
|
118
210
|
deps.host.redraw();
|
|
119
211
|
return;
|
|
120
212
|
}
|
package/dist/tui/run.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { FEED_ROWS } from './dashboard';
|
|
2
1
|
export declare function shouldRenderTui(env: {
|
|
3
2
|
isTTY: boolean;
|
|
4
3
|
stdinTTY: boolean;
|
|
@@ -10,4 +9,5 @@ export declare function runDashboard(opts: {
|
|
|
10
9
|
startedAt: number | null;
|
|
11
10
|
version: string;
|
|
12
11
|
}): Promise<void>;
|
|
13
|
-
|
|
12
|
+
/** `ainode harnesses` — straight to the harness screen. */
|
|
13
|
+
export declare function runHarnessesTui(): Promise<void>;
|