@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
package/dist/tui/run.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// Terminal wiring for the console UI: raw-mode keys in, frames out.
|
|
3
3
|
//
|
|
4
4
|
// Everything decision-shaped lives in app.ts and the screens; this file
|
|
5
|
-
// owns only the things that need a real TTY
|
|
5
|
+
// owns only the things that need a real TTY — and it owns them once, for
|
|
6
|
+
// every screen, so no screen has to run a terminal of its own.
|
|
6
7
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
8
|
if (k2 === undefined) k2 = k;
|
|
8
9
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
@@ -37,77 +38,54 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
38
|
};
|
|
38
39
|
})();
|
|
39
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.FEED_ROWS = void 0;
|
|
41
41
|
exports.shouldRenderTui = shouldRenderTui;
|
|
42
42
|
exports.runDashboard = runDashboard;
|
|
43
|
+
exports.runHarnessesTui = runHarnessesTui;
|
|
43
44
|
const readline = __importStar(require("readline"));
|
|
44
45
|
const app_1 = require("./app");
|
|
45
46
|
const data_1 = require("./data");
|
|
46
47
|
const dashboard_1 = require("./dashboard");
|
|
47
|
-
Object.defineProperty(exports, "FEED_ROWS", { enumerable: true, get: function () { return dashboard_1.FEED_ROWS; } });
|
|
48
48
|
const requests_1 = require("./requests");
|
|
49
49
|
const transcript_1 = require("./transcript");
|
|
50
50
|
const harnesses_1 = require("./harnesses");
|
|
51
|
+
const logs_1 = require("./logs");
|
|
51
52
|
const render_1 = require("./render");
|
|
52
53
|
const console_capture_1 = require("./console-capture");
|
|
53
54
|
const store_1 = require("../store");
|
|
54
55
|
const request_pump_1 = require("../request-pump");
|
|
56
|
+
const paths_1 = require("../paths");
|
|
55
57
|
function shouldRenderTui(env) {
|
|
56
58
|
return env.isTTY && env.stdinTTY && !env.noTui;
|
|
57
59
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
lastLog: null, logCount: 0,
|
|
71
|
-
};
|
|
60
|
+
/** How often the poll scheduler looks for work. Screens set their own rate. */
|
|
61
|
+
const TICK_MS = 250;
|
|
62
|
+
/** Spinner cadence. Cheap now that frames are painted as a diff. */
|
|
63
|
+
const SPIN_MS = 120;
|
|
64
|
+
/**
|
|
65
|
+
* Own the terminal on behalf of a screen stack.
|
|
66
|
+
*
|
|
67
|
+
* `build` gets the host so it can wire navigation, and the suspend hook so a
|
|
68
|
+
* screen can hand the TTY to a vendor CLI. It returns the root screen.
|
|
69
|
+
*/
|
|
70
|
+
function createConsole(build, hooks = {}) {
|
|
71
|
+
const painter = (0, render_1.createPainter)(s => process.stdout.write(s));
|
|
72
72
|
let quitting = false;
|
|
73
73
|
let inputAttached = false;
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
74
|
+
// Building the root screen redraws, and that happens before run() has
|
|
75
|
+
// opened the alt screen. Painting then would scribble a frame onto the
|
|
76
|
+
// user's real terminal — and worse, leave the painter believing that
|
|
77
|
+
// frame is already on screen, so the first alt-screen paint would be a
|
|
78
|
+
// no-op diff against a screen that is actually blank.
|
|
79
|
+
let started = false;
|
|
80
|
+
// While a child process owns the terminal, this app must go completely
|
|
81
|
+
// silent: its timers keep firing otherwise and repaint straight over the
|
|
82
|
+
// child's output.
|
|
78
83
|
let suspended = false;
|
|
79
84
|
const app = (0, app_1.createApp)({ id: 'boot', title: '+Ai Node', render: () => [] }, { onQuit: () => teardown(), onRedraw: () => draw() });
|
|
80
|
-
const openTranscript = (r) => app.push((0, transcript_1.createTranscriptScreen)({ data, host: app, request: r }));
|
|
81
|
-
app.replace((0, dashboard_1.createDashboardScreen)({
|
|
82
|
-
data, host: app, state,
|
|
83
|
-
openTranscript,
|
|
84
|
-
openRequests: () => app.push((0, requests_1.createRequestsScreen)({ data, host: app, openTranscript })),
|
|
85
|
-
openHarnesses: () => { void openHarnesses(); },
|
|
86
|
-
}));
|
|
87
|
-
// The harness screen owns the whole terminal (it hands the TTY to CLIs
|
|
88
|
-
// for login), so we detach entirely and rebuild our input when it exits.
|
|
89
|
-
async function openHarnesses() {
|
|
90
|
-
suspended = true;
|
|
91
|
-
detachInput();
|
|
92
|
-
(0, render_1.altOff)();
|
|
93
|
-
try {
|
|
94
|
-
await (0, harnesses_1.runHarnessesTui)({ embedded: true });
|
|
95
|
-
}
|
|
96
|
-
finally {
|
|
97
|
-
suspended = false;
|
|
98
|
-
(0, render_1.altOn)();
|
|
99
|
-
attachInput();
|
|
100
|
-
draw();
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
85
|
function draw() {
|
|
104
|
-
if (quitting || suspended)
|
|
86
|
+
if (!started || quitting || suspended)
|
|
105
87
|
return;
|
|
106
|
-
|
|
107
|
-
state.lastLog = latest ? latest.text.replace(/\s+/g, ' ') : null;
|
|
108
|
-
state.logCount = logs.count();
|
|
109
|
-
(0, render_1.home)();
|
|
110
|
-
process.stdout.write(app.frame((0, render_1.termWidth)()).join('\n') + '\n');
|
|
88
|
+
painter.paint(app.frame((0, render_1.termWidth)(), (0, render_1.termHeight)()));
|
|
111
89
|
}
|
|
112
90
|
function onKeypress(_s, key) {
|
|
113
91
|
void app.handleKey(key);
|
|
@@ -131,48 +109,131 @@ async function runDashboard(opts) {
|
|
|
131
109
|
process.stdin.pause();
|
|
132
110
|
inputAttached = false;
|
|
133
111
|
}
|
|
112
|
+
let pollTimer = null;
|
|
113
|
+
let spinTimer = null;
|
|
134
114
|
function teardown() {
|
|
135
115
|
if (quitting)
|
|
136
116
|
return;
|
|
137
117
|
quitting = true;
|
|
138
|
-
|
|
139
|
-
|
|
118
|
+
if (pollTimer)
|
|
119
|
+
clearInterval(pollTimer);
|
|
120
|
+
if (spinTimer)
|
|
121
|
+
clearInterval(spinTimer);
|
|
140
122
|
detachInput();
|
|
141
123
|
(0, render_1.altOff)();
|
|
142
|
-
|
|
143
|
-
// the screen — otherwise running under the dashboard swallows the log.
|
|
144
|
-
logs.restoreAndReplay();
|
|
124
|
+
hooks.onTeardown?.();
|
|
145
125
|
process.exit(0);
|
|
146
126
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Redraw on a spin tick only while something is actually moving, so an
|
|
162
|
-
// idle node isn't repainting the terminal ten times a second.
|
|
163
|
-
const spinTimer = setInterval(() => {
|
|
164
|
-
if (suspended)
|
|
165
|
-
return;
|
|
166
|
-
state.spin++;
|
|
167
|
-
if (state.stats?.active || state.inflight > 0)
|
|
127
|
+
const suspend = async (fn) => {
|
|
128
|
+
suspended = true;
|
|
129
|
+
detachInput();
|
|
130
|
+
(0, render_1.altOff)();
|
|
131
|
+
try {
|
|
132
|
+
await fn();
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
suspended = false;
|
|
136
|
+
(0, render_1.altOn)();
|
|
137
|
+
attachInput();
|
|
138
|
+
// The child scribbled all over the screen; the diff from before it ran
|
|
139
|
+
// is meaningless now.
|
|
140
|
+
painter.invalidate();
|
|
168
141
|
draw();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
app.replace(build(app, suspend));
|
|
145
|
+
return {
|
|
146
|
+
host: app,
|
|
147
|
+
suspend,
|
|
148
|
+
async run() {
|
|
149
|
+
// Each screen declares its own cadence and carries its own clock, so a
|
|
150
|
+
// screen that has just been opened is polled at the next tick rather
|
|
151
|
+
// than waiting out the previous screen's interval.
|
|
152
|
+
pollTimer = setInterval(() => {
|
|
153
|
+
if (suspended)
|
|
154
|
+
return;
|
|
155
|
+
const screen = app.duePoll(Date.now());
|
|
156
|
+
if (!screen)
|
|
157
|
+
return;
|
|
158
|
+
void screen.poll?.().catch(() => { });
|
|
159
|
+
}, TICK_MS);
|
|
160
|
+
let n = 0;
|
|
161
|
+
spinTimer = setInterval(() => {
|
|
162
|
+
if (suspended)
|
|
163
|
+
return;
|
|
164
|
+
n++;
|
|
165
|
+
let wants = false;
|
|
166
|
+
try {
|
|
167
|
+
wants = app.current().tick?.(n) === true;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
wants = false;
|
|
171
|
+
}
|
|
172
|
+
if (wants)
|
|
173
|
+
draw();
|
|
174
|
+
}, SPIN_MS);
|
|
175
|
+
// Resizing must not leave half-width panels behind.
|
|
176
|
+
process.stdout.on('resize', () => { painter.invalidate(); draw(); });
|
|
177
|
+
(0, render_1.altOn)();
|
|
178
|
+
attachInput();
|
|
179
|
+
started = true;
|
|
180
|
+
painter.invalidate();
|
|
181
|
+
draw();
|
|
182
|
+
await new Promise(() => { });
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async function runDashboard(opts) {
|
|
187
|
+
const pairing = (0, store_1.readPairing)();
|
|
188
|
+
const data = (0, data_1.createDataLayer)({ token: pairing?.daemonToken ?? '' });
|
|
189
|
+
// In daemon mode the pump is running in THIS process and logs straight to
|
|
190
|
+
// the stdout we are painting on. Capture it for as long as we own the
|
|
191
|
+
// screen; the Logs screen reads the buffer and every line still reaches
|
|
192
|
+
// the daemon log file, so nothing is lost by watching.
|
|
193
|
+
const logs = (0, console_capture_1.captureConsole)({ logFile: paths_1.RUNTIME_LOG_FILE });
|
|
194
|
+
const state = {
|
|
195
|
+
self: null, stats: null, recent: [], sel: 0, spin: 0,
|
|
196
|
+
pid: opts.pid, startedAt: opts.startedAt,
|
|
197
|
+
inflight: 0, paired: (0, store_1.isPaired)(), viewerMode: opts.viewerMode,
|
|
198
|
+
offline: false, now: Date.now(), version: opts.version,
|
|
199
|
+
logCount: 0,
|
|
200
|
+
};
|
|
201
|
+
const ui = createConsole((host, suspend) => {
|
|
202
|
+
const openTranscript = (r) => host.push((0, transcript_1.createTranscriptScreen)({ data, host, request: r }));
|
|
203
|
+
return (0, dashboard_1.createDashboardScreen)({
|
|
204
|
+
data, host, state,
|
|
205
|
+
openTranscript,
|
|
206
|
+
openRequests: () => host.push((0, requests_1.createRequestsScreen)({ data, host, openTranscript })),
|
|
207
|
+
openHarnesses: () => host.push((0, harnesses_1.createHarnessesScreen)({ host, suspend })),
|
|
208
|
+
openLogs: () => host.push((0, logs_1.createLogsScreen)({
|
|
209
|
+
host,
|
|
210
|
+
logs,
|
|
211
|
+
emptyHint: opts.viewerMode
|
|
212
|
+
? 'the daemon runs in another process — its log is in ~/.ainode/daemon.log'
|
|
213
|
+
: undefined,
|
|
214
|
+
})),
|
|
215
|
+
});
|
|
216
|
+
}, {
|
|
217
|
+
onTeardown: () => {
|
|
218
|
+
logs.restore();
|
|
219
|
+
if (logs.count()) {
|
|
220
|
+
console.log(`${logs.count()} daemon log lines this session — ${paths_1.RUNTIME_LOG_FILE}`);
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
// The daemon's own numbers don't come from an RPC.
|
|
225
|
+
setInterval(() => {
|
|
226
|
+
state.inflight = (0, request_pump_1.inflightCount)();
|
|
227
|
+
state.logCount = logs.count();
|
|
228
|
+
}, 1000).unref();
|
|
229
|
+
await ui.run();
|
|
230
|
+
}
|
|
231
|
+
/** `ainode harnesses` — straight to the harness screen. */
|
|
232
|
+
async function runHarnessesTui() {
|
|
233
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
234
|
+
await (0, harnesses_1.plainStatus)();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const ui = createConsole((host, suspend) => (0, harnesses_1.createHarnessesScreen)({ host, suspend }));
|
|
238
|
+
await ui.run();
|
|
178
239
|
}
|
package/dist/tui/transcript.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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 Message {
|
|
4
4
|
role: 'user' | 'assistant' | 'thinking' | 'tool_use' | 'tool_result';
|
|
5
5
|
text?: string;
|
|
@@ -15,6 +15,7 @@ export interface TranscriptState {
|
|
|
15
15
|
scroll: number;
|
|
16
16
|
follow: boolean;
|
|
17
17
|
loading: boolean;
|
|
18
|
+
/** Rows the last render could fit — what a PgUp/PgDn moves by. */
|
|
18
19
|
viewportRows: number;
|
|
19
20
|
}
|
|
20
21
|
interface RawEvent {
|
|
@@ -30,7 +31,7 @@ export declare function wrapText(s: string, width: number): string[];
|
|
|
30
31
|
*/
|
|
31
32
|
export declare function messagesFromEvents(events: RawEvent[], prompt: string | null): Message[];
|
|
32
33
|
export declare function messageLines(m: Message, width: number): string[];
|
|
33
|
-
export declare function renderTranscript(st: TranscriptState, width: number): string[];
|
|
34
|
+
export declare function renderTranscript(st: TranscriptState, width: number, height: number): string[];
|
|
34
35
|
export declare function createTranscriptScreen(deps: {
|
|
35
36
|
data: DataLayer;
|
|
36
37
|
host: AppHost;
|
package/dist/tui/transcript.js
CHANGED
|
@@ -14,7 +14,8 @@ exports.messageLines = messageLines;
|
|
|
14
14
|
exports.renderTranscript = renderTranscript;
|
|
15
15
|
exports.createTranscriptScreen = createTranscriptScreen;
|
|
16
16
|
const render_1 = require("./render");
|
|
17
|
-
const
|
|
17
|
+
const request_row_1 = require("./request-row");
|
|
18
|
+
const app_1 = require("./app");
|
|
18
19
|
function wrapText(s, width) {
|
|
19
20
|
const out = [];
|
|
20
21
|
for (const paragraph of String(s ?? '').split('\n')) {
|
|
@@ -141,14 +142,18 @@ function messageLines(m, width) {
|
|
|
141
142
|
const wrapped = wrapText(m.text ?? '', body);
|
|
142
143
|
return wrapped.map((l, i) => ` ${i === 0 ? head(tag, colour) : ' '.repeat(label)} ${m.role === 'thinking' ? (0, render_1.grey)(l) : l}`);
|
|
143
144
|
}
|
|
144
|
-
|
|
145
|
+
/** Lines the screen spends on things that are not transcript body. */
|
|
146
|
+
const CHROME = 4;
|
|
147
|
+
function renderTranscript(st, width, height) {
|
|
145
148
|
const r = st.request;
|
|
146
149
|
const started = new Date(r.picked_up_at ?? r.created_at).getTime();
|
|
147
150
|
const dur = r.finished_at
|
|
148
151
|
? (0, render_1.fmtDuration)(new Date(r.finished_at).getTime() - started)
|
|
149
152
|
: (0, render_1.fmtDuration)(Date.now() - started);
|
|
153
|
+
const rows = Math.max(1, height - CHROME);
|
|
154
|
+
st.viewportRows = rows;
|
|
150
155
|
const out = [];
|
|
151
|
-
out.push(` ${(0, render_1.cyan)('▸')} ${(0, render_1.bold)(r.entity_name ?? '—')} ${(0, render_1.dim)('·')} ${(0, render_1.dim)(`${r.agent}/${r.mode}`)} ${(0, render_1.dim)('·')} ${(0,
|
|
156
|
+
out.push(` ${(0, render_1.cyan)('▸')} ${(0, render_1.bold)(r.entity_name ?? '—')} ${(0, render_1.dim)('·')} ${(0, render_1.dim)(`${r.agent}/${r.mode}`)} ${(0, render_1.dim)('·')} ${(0, request_row_1.statusColour)(r.status)((0, request_row_1.statusLabel)(r.status))} ${(0, render_1.dim)(dur)}`);
|
|
152
157
|
out.push('');
|
|
153
158
|
if (st.loading && st.messages.length === 0) {
|
|
154
159
|
out.push((0, render_1.dim)(' loading transcript…'));
|
|
@@ -158,28 +163,40 @@ function renderTranscript(st, width) {
|
|
|
158
163
|
}
|
|
159
164
|
else {
|
|
160
165
|
const all = st.messages.flatMap(m => messageLines(m, width));
|
|
161
|
-
const
|
|
166
|
+
const maxScroll = Math.max(0, all.length - rows);
|
|
162
167
|
const start = st.follow
|
|
163
|
-
?
|
|
164
|
-
: Math.min(st.scroll,
|
|
168
|
+
? maxScroll
|
|
169
|
+
: Math.min(st.scroll, maxScroll);
|
|
170
|
+
st.scroll = start;
|
|
165
171
|
out.push(...all.slice(start, start + rows));
|
|
166
172
|
}
|
|
167
|
-
out.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
173
|
+
while (out.length < height - 1)
|
|
174
|
+
out.push('');
|
|
175
|
+
const live = request_row_1.ACTIVE.has(r.status) && st.follow ? ` ${(0, render_1.cyan)('following')}` : '';
|
|
176
|
+
out.push((0, app_1.footerHint)([
|
|
177
|
+
{ keys: '↑↓ PgUp PgDn', label: 'scroll' },
|
|
178
|
+
{ keys: 'G', label: 'follow' },
|
|
179
|
+
{ keys: 'q', label: 'back' },
|
|
180
|
+
]) + live);
|
|
181
|
+
return out.map(l => (0, render_1.truncate)(l, width)).slice(0, height);
|
|
171
182
|
}
|
|
172
183
|
function createTranscriptScreen(deps) {
|
|
173
184
|
const st = {
|
|
174
185
|
request: deps.request, messages: [], scroll: 0,
|
|
175
186
|
follow: true, loading: true,
|
|
176
|
-
viewportRows:
|
|
187
|
+
viewportRows: 20,
|
|
177
188
|
};
|
|
178
189
|
return {
|
|
179
190
|
id: `transcript:${deps.request.id}`,
|
|
180
191
|
title: 'Transcript',
|
|
181
|
-
render: (width) => renderTranscript(st, width),
|
|
182
|
-
pollMs: () => (ACTIVE.has(st.request.status) ? 1000 : 15_000),
|
|
192
|
+
render: (width, height) => renderTranscript(st, width, height),
|
|
193
|
+
pollMs: () => (request_row_1.ACTIVE.has(st.request.status) ? 1000 : 15_000),
|
|
194
|
+
keys: () => [
|
|
195
|
+
{ keys: '↑↓ / jk', label: 'scroll a line' },
|
|
196
|
+
{ keys: 'PgUp/PgDn', label: 'scroll a screenful' },
|
|
197
|
+
{ keys: 'g', label: 'jump to the start' },
|
|
198
|
+
{ keys: 'G', label: 'jump to the end and follow the tail' },
|
|
199
|
+
],
|
|
183
200
|
async poll() {
|
|
184
201
|
const detail = await deps.data.detail(deps.request.id);
|
|
185
202
|
st.loading = false;
|
|
@@ -187,8 +204,15 @@ function createTranscriptScreen(deps) {
|
|
|
187
204
|
deps.host.redraw();
|
|
188
205
|
return;
|
|
189
206
|
}
|
|
190
|
-
|
|
191
|
-
|
|
207
|
+
// Merge, don't replace: the detail payload's row carries live status and
|
|
208
|
+
// timings but not the joined entity name, so replacing wholesale blanked
|
|
209
|
+
// the name out of the header the moment the transcript loaded.
|
|
210
|
+
if (detail.request) {
|
|
211
|
+
st.request = {
|
|
212
|
+
...st.request,
|
|
213
|
+
...Object.fromEntries(Object.entries(detail.request).filter(([, v]) => v !== null && v !== undefined)),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
192
216
|
st.messages = messagesFromEvents(detail.events ?? [], st.request.prompt);
|
|
193
217
|
deps.host.redraw();
|
|
194
218
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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": [
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Drive the console UI in a real pty and dump the frames a user would see.
|
|
3
|
+
//
|
|
4
|
+
// Unit tests assert on the pure render functions; this drives the actual
|
|
5
|
+
// binary — alt screen, raw keys, live data, the lot. Every defect in the
|
|
6
|
+
// 2026-07-30 console pass was found with this and none of them were visible
|
|
7
|
+
// from the code.
|
|
8
|
+
//
|
|
9
|
+
// node scripts/probe-tui.mjs "w4000,shome,ka,w2500,sactivity"
|
|
10
|
+
//
|
|
11
|
+
// Script steps, comma separated:
|
|
12
|
+
// w<ms> wait
|
|
13
|
+
// k<key> send a key: a literal, or CR UP DOWN LEFT RIGHT ESC TAB
|
|
14
|
+
// s<label> snapshot the screen under that label
|
|
15
|
+
//
|
|
16
|
+
// Frames are painted with cursor addressing, so the raw pty stream is
|
|
17
|
+
// replayed through a tiny terminal model to reconstruct what is on screen.
|
|
18
|
+
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import * as path from 'node:path';
|
|
22
|
+
import * as url from 'node:url';
|
|
23
|
+
|
|
24
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
25
|
+
const root = path.resolve(here, '..');
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
|
|
28
|
+
let pty;
|
|
29
|
+
try {
|
|
30
|
+
pty = require('node-pty');
|
|
31
|
+
} catch {
|
|
32
|
+
console.error('node-pty is not installed — run npm install first');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const COLS = Number(process.env.PROBE_COLS ?? 120);
|
|
37
|
+
const ROWS = Number(process.env.PROBE_ROWS ?? 40);
|
|
38
|
+
const steps = (process.argv[2] ?? 'w4000,shome').split(',').filter(Boolean);
|
|
39
|
+
|
|
40
|
+
const KEYS = {
|
|
41
|
+
CR: '\r', ESC: '\x1b', TAB: '\t',
|
|
42
|
+
UP: '\x1b[A', DOWN: '\x1b[B', RIGHT: '\x1b[C', LEFT: '\x1b[D',
|
|
43
|
+
PGUP: '\x1b[5~', PGDN: '\x1b[6~',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Just enough terminal to reconstruct a screen from cursor-addressed writes. */
|
|
47
|
+
function createScreen(rows, cols) {
|
|
48
|
+
let grid = Array.from({ length: rows }, () => '');
|
|
49
|
+
let row = 0;
|
|
50
|
+
let col = 0;
|
|
51
|
+
|
|
52
|
+
const put = text => {
|
|
53
|
+
if (!text) return;
|
|
54
|
+
const line = grid[row] ?? '';
|
|
55
|
+
grid[row] = (line.padEnd(col, ' ')).slice(0, col) + text;
|
|
56
|
+
col += text.length;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// A pty delivers bytes, not frames: an escape sequence is routinely split
|
|
60
|
+
// across two chunks. Anything that looks like the start of one is held
|
|
61
|
+
// back until the rest arrives, or it lands in the output as literal text.
|
|
62
|
+
let pending = '';
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
write(raw) {
|
|
66
|
+
const chunk = pending + raw;
|
|
67
|
+
pending = '';
|
|
68
|
+
const dangling = chunk.match(/\x1b(\[[0-9;]*|\][^\x07]*|[()]?)?$/);
|
|
69
|
+
const body = dangling ? chunk.slice(0, dangling.index) : chunk;
|
|
70
|
+
if (dangling) pending = dangling[0];
|
|
71
|
+
|
|
72
|
+
// Split into escape sequences and printable runs.
|
|
73
|
+
const re = /\x1b\[([0-9;]*)([A-Za-z])|\x1b\][^\x07]*\x07|\x1b[()][A-Za-z0-9]|[^\x1b]+/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = re.exec(body)) !== null) {
|
|
76
|
+
const [seq, params, cmd] = m;
|
|
77
|
+
if (cmd === undefined) {
|
|
78
|
+
if (seq.startsWith('\x1b')) continue; // OSC / charset — ignore
|
|
79
|
+
for (const part of seq.split(/(\r\n|\n|\r)/)) {
|
|
80
|
+
if (part === '\n' || part === '\r\n') { row = Math.min(rows - 1, row + 1); col = 0; }
|
|
81
|
+
else if (part === '\r') { col = 0; }
|
|
82
|
+
else put(part);
|
|
83
|
+
}
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const n = params.split(';').map(x => Number(x || 0));
|
|
87
|
+
if (cmd === 'H' || cmd === 'f') { row = Math.max(0, (n[0] || 1) - 1); col = Math.max(0, (n[1] || 1) - 1); }
|
|
88
|
+
else if (cmd === 'J') { if ((n[0] ?? 0) === 2) { grid = Array.from({ length: rows }, () => ''); row = 0; col = 0; } }
|
|
89
|
+
else if (cmd === 'K') { grid[row] = (grid[row] ?? '').slice(0, col); }
|
|
90
|
+
else if (cmd === 'A') { row = Math.max(0, row - (n[0] || 1)); }
|
|
91
|
+
else if (cmd === 'B') { row = Math.min(rows - 1, row + (n[0] || 1)); }
|
|
92
|
+
// m (colour), h/l (modes) do not move the cursor or change content
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
snapshot() {
|
|
96
|
+
return grid.map(l => l.replace(/\s+$/, '')).join('\n').replace(/\n{3,}$/, '\n');
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const screen = createScreen(ROWS, COLS);
|
|
102
|
+
const child = pty.spawn(process.execPath, ['dist/cli.js'], {
|
|
103
|
+
name: 'xterm-256color', cols: COLS, rows: ROWS, cwd: root,
|
|
104
|
+
env: { ...process.env, TERM: 'xterm-256color' },
|
|
105
|
+
});
|
|
106
|
+
child.onData(d => screen.write(d));
|
|
107
|
+
|
|
108
|
+
const wait = ms => new Promise(r => setTimeout(r, ms));
|
|
109
|
+
const strip = s => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
110
|
+
|
|
111
|
+
for (const step of steps) {
|
|
112
|
+
const kind = step[0];
|
|
113
|
+
const arg = step.slice(1);
|
|
114
|
+
if (kind === 'w') await wait(Number(arg));
|
|
115
|
+
else if (kind === 'k') child.write(KEYS[arg] ?? arg);
|
|
116
|
+
else if (kind === 's') {
|
|
117
|
+
process.stdout.write(`\n===== ${arg} =====\n${strip(screen.snapshot())}\n`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
child.kill();
|
|
122
|
+
process.exit(0);
|
package/dist/tui.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function runHarnessesTui(): Promise<void>;
|