@addai/node 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-runner.d.ts +3 -0
- package/dist/command-runner.js +26 -7
- package/dist/index.js +13 -4
- package/dist/self-update.d.ts +25 -0
- package/dist/self-update.js +21 -0
- package/dist/tui/dashboard.d.ts +21 -0
- package/dist/tui/dashboard.js +86 -8
- package/dist/tui/run.js +1 -1
- package/package.json +1 -1
package/dist/command-runner.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type RestartHook = (opts: {
|
|
2
2
|
commandId: string;
|
|
3
3
|
version: string;
|
|
4
|
+
/** Bounce this daemon on the version it is already running: no npm
|
|
5
|
+
* install, no version change. See runUpdateRuntime. */
|
|
6
|
+
restartOnly?: boolean;
|
|
4
7
|
}) => Promise<void>;
|
|
5
8
|
export declare function setRestartHook(fn: RestartHook): void;
|
|
6
9
|
/** Called from index.ts when a heartbeat reports pending_commands > 0.
|
package/dist/command-runner.js
CHANGED
|
@@ -479,26 +479,45 @@ async function runLogout(cmd, spec) {
|
|
|
479
479
|
/* ── update_runtime ──────────────────────────────────────────────────────
|
|
480
480
|
* Roll THIS node to a version (default: npm latest) and come back up.
|
|
481
481
|
*
|
|
482
|
+
* Two shapes share the command:
|
|
483
|
+
*
|
|
484
|
+
* {} roll to npm latest
|
|
485
|
+
* {"version": "0.6.0"} roll to a pinned version
|
|
486
|
+
* {"restart_only": true} bounce the daemon on the version it is already
|
|
487
|
+
* running — no npm install, no version change
|
|
488
|
+
*
|
|
489
|
+
* restart_only exists because the reasons to bounce a node are mostly not
|
|
490
|
+
* version reasons: a credential edit the running daemon has cached, a wedged
|
|
491
|
+
* harness, a machine that has been up for a fortnight. Doing that through a
|
|
492
|
+
* version roll meant an unwanted upgrade every time, and could not be done at
|
|
493
|
+
* all on a node running from a source checkout — where a restart is exactly
|
|
494
|
+
* the safe half of the operation.
|
|
495
|
+
*
|
|
496
|
+
* An older daemon that has never heard of restart_only still does the right
|
|
497
|
+
* thing, because the caller also pins `version` to the version it can see the
|
|
498
|
+
* node running: the old code installs the version already installed and
|
|
499
|
+
* restarts. Same destination, one wasted npm call.
|
|
500
|
+
*
|
|
482
501
|
* The command is deliberately left `running` here: this process is about to
|
|
483
502
|
* stop existing, so it cannot honestly report the outcome. index.ts's boot
|
|
484
503
|
* path completes it from the handoff file once the replacement daemon is
|
|
485
504
|
* actually up, with whatever version it actually came up as. */
|
|
486
505
|
async function runUpdateRuntime(cmd) {
|
|
487
|
-
const target = (cmd.input?.version ?? '').trim() || 'latest';
|
|
488
506
|
if (!restartHook) {
|
|
489
507
|
await update(cmd.id, 'failed', {}, 'this daemon is too old to restart itself');
|
|
490
508
|
return;
|
|
491
509
|
}
|
|
492
510
|
const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
await update(cmd.id, 'failed', { mode }, 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply');
|
|
511
|
+
const { target, restartOnly, refusal } = (0, self_update_1.planRoll)(cmd.input, mode, VERSION);
|
|
512
|
+
if (refusal) {
|
|
513
|
+
await update(cmd.id, 'failed', { mode }, refusal);
|
|
497
514
|
return;
|
|
498
515
|
}
|
|
499
|
-
await update(cmd.id, 'running', {
|
|
516
|
+
await update(cmd.id, 'running', {
|
|
517
|
+
step: 'draining', mode, target_version: target, from_version: VERSION, restart_only: restartOnly,
|
|
518
|
+
});
|
|
500
519
|
try {
|
|
501
|
-
await restartHook({ commandId: cmd.id, version: target });
|
|
520
|
+
await restartHook({ commandId: cmd.id, version: target, restartOnly });
|
|
502
521
|
}
|
|
503
522
|
catch (err) {
|
|
504
523
|
// The hook owns the point of no return (it writes the handoff only once
|
package/dist/index.js
CHANGED
|
@@ -248,11 +248,17 @@ async function start() {
|
|
|
248
248
|
// daemon for one node — the failure the lockfile-identity fix was about. So
|
|
249
249
|
// when launchd is holding us up, the roll simply STANDS DOWN and lets
|
|
250
250
|
// KeepAlive do the starting.
|
|
251
|
-
|
|
251
|
+
//
|
|
252
|
+
// A restart-only bounce takes the same path minus the install: `version` is
|
|
253
|
+
// already this process's version, so the respawn plan pins the replacement
|
|
254
|
+
// to the same bits (which matters for npx, whose cache dir is per-version)
|
|
255
|
+
// and npm is never called — nothing to fetch, and nothing that can fail
|
|
256
|
+
// offline.
|
|
257
|
+
(0, command_runner_1.setRestartHook)(async ({ commandId, version, restartOnly }) => {
|
|
252
258
|
const mode = (0, self_update_1.detectLaunchMode)(process.argv[1] ?? '');
|
|
253
259
|
const plan = (0, self_update_1.planRespawn)(mode, process.argv[1] ?? '', version, process.execPath, process.argv.slice(2));
|
|
254
260
|
const supervised = (0, autostart_1.isSupervised)();
|
|
255
|
-
if (plan.installFirst) {
|
|
261
|
+
if (plan.installFirst && !restartOnly) {
|
|
256
262
|
const err = await (0, self_update_1.installGlobal)(version);
|
|
257
263
|
if (err)
|
|
258
264
|
throw new Error(err);
|
|
@@ -276,14 +282,16 @@ async function start() {
|
|
|
276
282
|
at: new Date().toISOString(),
|
|
277
283
|
drained: lastDrain.drained,
|
|
278
284
|
timed_out: lastDrain.timedOut,
|
|
285
|
+
restart_only: restartOnly === true,
|
|
279
286
|
});
|
|
280
287
|
}
|
|
281
288
|
catch (err) {
|
|
282
289
|
console.error('[restart] handoff write failed — the roll still happened, it just cannot self-report:', err.message);
|
|
283
290
|
}
|
|
291
|
+
const what = restartOnly ? `${mode} → restart on ${version}` : `${mode} → ${version}`;
|
|
284
292
|
console.log(supervised
|
|
285
|
-
? `[restart] standing down for launchd to restart us (${
|
|
286
|
-
: `[restart] handed over to ${plan.file} (${
|
|
293
|
+
? `[restart] standing down for launchd to restart us (${what}); exiting`
|
|
294
|
+
: `[restart] handed over to ${plan.file} (${what}); exiting`);
|
|
287
295
|
setTimeout(() => process.exit(0), 250).unref?.();
|
|
288
296
|
});
|
|
289
297
|
process.once('SIGINT', () => { stop().finally(() => process.exit(0)); });
|
|
@@ -395,6 +403,7 @@ async function finalizeRestartHandoff() {
|
|
|
395
403
|
version: VERSION,
|
|
396
404
|
mode: h.mode,
|
|
397
405
|
restarted: true,
|
|
406
|
+
restart_only: h.restart_only === true,
|
|
398
407
|
version_changed: changed,
|
|
399
408
|
drained: h.drained ?? 0,
|
|
400
409
|
// >0 means the drain ceiling expired with work still running — the
|
package/dist/self-update.d.ts
CHANGED
|
@@ -45,6 +45,27 @@ export declare function planRespawn(mode: LaunchMode, entryPath: string, targetV
|
|
|
45
45
|
* slice(2)) — carried across the restart so a node launched as
|
|
46
46
|
* `… cli.js run --foo` doesn't silently come back up without them. */
|
|
47
47
|
userArgs?: string[]): RespawnPlan;
|
|
48
|
+
export interface RollPlan {
|
|
49
|
+
/** Version the replacement must come up as. */
|
|
50
|
+
target: string;
|
|
51
|
+
/** A bounce on the running version: nothing is installed, nothing moves. */
|
|
52
|
+
restartOnly: boolean;
|
|
53
|
+
/** Non-null = refuse before anything drains, with this explanation. */
|
|
54
|
+
refusal: string | null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read an `update_runtime` command's input into what this node should do.
|
|
58
|
+
*
|
|
59
|
+
* Pure so the one rule that is easy to get wrong stays testable: a source
|
|
60
|
+
* checkout may not be version-bumped remotely (its version comes from the
|
|
61
|
+
* working tree, so it would restart and report the same version — a silent
|
|
62
|
+
* no-op dressed up as success) but it MAY be restarted, which is the half of
|
|
63
|
+
* the operation that works there.
|
|
64
|
+
*/
|
|
65
|
+
export declare function planRoll(input: {
|
|
66
|
+
version?: string;
|
|
67
|
+
restart_only?: boolean;
|
|
68
|
+
} | null, mode: LaunchMode, currentVersion: string): RollPlan;
|
|
48
69
|
export interface RestartHandoff {
|
|
49
70
|
command_id: string;
|
|
50
71
|
from_version: string;
|
|
@@ -56,6 +77,10 @@ export interface RestartHandoff {
|
|
|
56
77
|
* work unfinished rather than claiming a clean restart either way. */
|
|
57
78
|
drained?: number;
|
|
58
79
|
timed_out?: number;
|
|
80
|
+
/** True when this was a bounce on the running version rather than a roll,
|
|
81
|
+
* so the completed command says which one actually happened instead of
|
|
82
|
+
* leaving it to be inferred from two versions that match. */
|
|
83
|
+
restart_only?: boolean;
|
|
59
84
|
}
|
|
60
85
|
export declare function handoffPath(runtimeDir: string): string;
|
|
61
86
|
export declare function writeHandoff(runtimeDir: string, h: RestartHandoff): void;
|
package/dist/self-update.js
CHANGED
|
@@ -55,6 +55,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
55
55
|
exports.PACKAGE_NAME = void 0;
|
|
56
56
|
exports.detectLaunchMode = detectLaunchMode;
|
|
57
57
|
exports.planRespawn = planRespawn;
|
|
58
|
+
exports.planRoll = planRoll;
|
|
58
59
|
exports.handoffPath = handoffPath;
|
|
59
60
|
exports.writeHandoff = writeHandoff;
|
|
60
61
|
exports.takeHandoff = takeHandoff;
|
|
@@ -112,6 +113,26 @@ userArgs = []) {
|
|
|
112
113
|
canChangeVersion: mode === 'global',
|
|
113
114
|
};
|
|
114
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Read an `update_runtime` command's input into what this node should do.
|
|
118
|
+
*
|
|
119
|
+
* Pure so the one rule that is easy to get wrong stays testable: a source
|
|
120
|
+
* checkout may not be version-bumped remotely (its version comes from the
|
|
121
|
+
* working tree, so it would restart and report the same version — a silent
|
|
122
|
+
* no-op dressed up as success) but it MAY be restarted, which is the half of
|
|
123
|
+
* the operation that works there.
|
|
124
|
+
*/
|
|
125
|
+
function planRoll(input, mode, currentVersion) {
|
|
126
|
+
const restartOnly = input?.restart_only === true;
|
|
127
|
+
// A restart targets the version already running, which keeps the boot-side
|
|
128
|
+
// "did we come up as what was asked for?" check meaningful instead of
|
|
129
|
+
// special-cased — and pins npx, whose cache dir is per-version.
|
|
130
|
+
const target = restartOnly ? currentVersion : (input?.version ?? '').trim() || 'latest';
|
|
131
|
+
const refusal = !restartOnly && mode === 'source' && target !== 'latest'
|
|
132
|
+
? 'this node runs from a source checkout — pull and rebuild it there; a remote version bump cannot apply'
|
|
133
|
+
: null;
|
|
134
|
+
return { target, restartOnly, refusal };
|
|
135
|
+
}
|
|
115
136
|
function handoffPath(runtimeDir) {
|
|
116
137
|
return path.join(runtimeDir, 'restart.json');
|
|
117
138
|
}
|
package/dist/tui/dashboard.d.ts
CHANGED
|
@@ -7,7 +7,15 @@ export interface DashboardState {
|
|
|
7
7
|
/** Recent requests, newest first — the NOW band reads the live ones off
|
|
8
8
|
* the top and the idle line reads the most recent finish. */
|
|
9
9
|
recent: RequestRow[];
|
|
10
|
+
/** Cursor position in the menu. */
|
|
10
11
|
sel: number;
|
|
12
|
+
/** Id of the selected live run, or null when the cursor is in the menu.
|
|
13
|
+
* Pinned by id rather than by index because the band reorders under the
|
|
14
|
+
* cursor: a new run is prepended the moment it starts, and an index would
|
|
15
|
+
* quietly leave you pointing at a different run than the one you chose. */
|
|
16
|
+
nowSelId: string | null;
|
|
17
|
+
/** Live rows the last render could fit — what bounds the cursor. */
|
|
18
|
+
nowRows: number;
|
|
11
19
|
spin: number;
|
|
12
20
|
pid: number | null;
|
|
13
21
|
startedAt: number | null;
|
|
@@ -35,6 +43,19 @@ export declare const MENU: Array<{
|
|
|
35
43
|
/** Live rows the NOW band shows before it starts counting the rest. */
|
|
36
44
|
export declare const MAX_NOW_ROWS = 6;
|
|
37
45
|
export declare function liveRequests(rows: RequestRow[]): RequestRow[];
|
|
46
|
+
/** The live rows the band actually draws — the only ones the cursor can reach.
|
|
47
|
+
* Everything past the cap lives on the Activity screen. A short terminal
|
|
48
|
+
* draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
|
|
49
|
+
* that are not on screen, so the last render's row count is what bounds it. */
|
|
50
|
+
export declare function selectableNow(st: DashboardState): RequestRow[];
|
|
51
|
+
/**
|
|
52
|
+
* Where the cursor is in the NOW band, or -1 for "in the menu".
|
|
53
|
+
*
|
|
54
|
+
* Derived from the pinned id on every read, so a run that finishes while
|
|
55
|
+
* selected drops the cursor back to the menu rather than silently moving it
|
|
56
|
+
* onto whichever run took that row.
|
|
57
|
+
*/
|
|
58
|
+
export declare function nowIndex(st: DashboardState): number;
|
|
38
59
|
/**
|
|
39
60
|
* The node's own state, as a label.
|
|
40
61
|
*
|
package/dist/tui/dashboard.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.MAX_NOW_ROWS = exports.MENU = void 0;
|
|
10
10
|
exports.liveRequests = liveRequests;
|
|
11
|
+
exports.selectableNow = selectableNow;
|
|
12
|
+
exports.nowIndex = nowIndex;
|
|
11
13
|
exports.stateLabel = stateLabel;
|
|
12
14
|
exports.autostartLine = autostartLine;
|
|
13
15
|
exports.fmtCount = fmtCount;
|
|
@@ -29,6 +31,26 @@ exports.MAX_NOW_ROWS = 6;
|
|
|
29
31
|
function liveRequests(rows) {
|
|
30
32
|
return rows.filter(r => request_row_1.ACTIVE.has(r.status));
|
|
31
33
|
}
|
|
34
|
+
/** The live rows the band actually draws — the only ones the cursor can reach.
|
|
35
|
+
* Everything past the cap lives on the Activity screen. A short terminal
|
|
36
|
+
* draws fewer than MAX_NOW_ROWS, and the cursor must not run off into rows
|
|
37
|
+
* that are not on screen, so the last render's row count is what bounds it. */
|
|
38
|
+
function selectableNow(st) {
|
|
39
|
+
const cap = Math.max(1, Math.min(exports.MAX_NOW_ROWS, st.nowRows || exports.MAX_NOW_ROWS));
|
|
40
|
+
return liveRequests(st.recent).slice(0, cap);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Where the cursor is in the NOW band, or -1 for "in the menu".
|
|
44
|
+
*
|
|
45
|
+
* Derived from the pinned id on every read, so a run that finishes while
|
|
46
|
+
* selected drops the cursor back to the menu rather than silently moving it
|
|
47
|
+
* onto whichever run took that row.
|
|
48
|
+
*/
|
|
49
|
+
function nowIndex(st) {
|
|
50
|
+
if (!st.nowSelId)
|
|
51
|
+
return -1;
|
|
52
|
+
return selectableNow(st).findIndex(r => r.id === st.nowSelId);
|
|
53
|
+
}
|
|
32
54
|
/**
|
|
33
55
|
* The node's own state, as a label.
|
|
34
56
|
*
|
|
@@ -128,7 +150,10 @@ function lastFinished(rows) {
|
|
|
128
150
|
}
|
|
129
151
|
function nowLines(st, width, rows) {
|
|
130
152
|
const live = liveRequests(st.recent);
|
|
131
|
-
const
|
|
153
|
+
const heading = live.length
|
|
154
|
+
? ` ${(0, render_1.bold)('Now')} ${(0, render_1.dim)('↑ to select · ⏎ to watch')}`
|
|
155
|
+
: ` ${(0, render_1.bold)('Now')}`;
|
|
156
|
+
const out = [heading];
|
|
132
157
|
if (live.length === 0) {
|
|
133
158
|
const last = lastFinished(st.recent);
|
|
134
159
|
out.push(last
|
|
@@ -138,7 +163,9 @@ function nowLines(st, width, rows) {
|
|
|
138
163
|
}
|
|
139
164
|
out.push((0, request_row_1.requestHeader)(width));
|
|
140
165
|
const shown = live.slice(0, Math.max(1, rows));
|
|
141
|
-
|
|
166
|
+
st.nowRows = shown.length;
|
|
167
|
+
const cursor = nowIndex(st);
|
|
168
|
+
shown.forEach((r, i) => out.push((0, request_row_1.requestLine)(r, st.now, i === cursor, width, st.spin)));
|
|
142
169
|
if (live.length > shown.length) {
|
|
143
170
|
out.push((0, render_1.dim)(` +${live.length - shown.length} more running`));
|
|
144
171
|
}
|
|
@@ -153,8 +180,11 @@ function renderDashboard(st, width, height) {
|
|
|
153
180
|
// The shortcut letters line up in a column of their own, so the eye can
|
|
154
181
|
// find them without reading the hints.
|
|
155
182
|
const hintWidth = Math.max(...exports.MENU.map(m => m.hint.length)) + 2;
|
|
183
|
+
// One cursor on screen at a time: while it is up in the NOW band the menu
|
|
184
|
+
// shows no selection, or the eye reads two.
|
|
185
|
+
const inNow = nowIndex(st) >= 0;
|
|
156
186
|
const menu = exports.MENU.map((m, i) => {
|
|
157
|
-
const selected = i === st.sel;
|
|
187
|
+
const selected = !inNow && i === st.sel;
|
|
158
188
|
const label = selected ? (0, render_1.bold)(m.label.padEnd(11)) : m.label.padEnd(11);
|
|
159
189
|
const cursor = selected ? (0, render_1.cyan)('❯') : ' ';
|
|
160
190
|
const badge = m.key === 'logs' && st.logCount ? `${st.logCount} lines` : '';
|
|
@@ -162,7 +192,7 @@ function renderDashboard(st, width, height) {
|
|
|
162
192
|
});
|
|
163
193
|
const foot = (0, app_1.footerHint)([
|
|
164
194
|
{ keys: '↑↓', label: 'move' },
|
|
165
|
-
{ keys: '⏎', label: 'open' },
|
|
195
|
+
{ keys: '⏎', label: inNow ? 'watch this run' : 'open' },
|
|
166
196
|
{ keys: 's', label: st.autostart?.enabled ? 'startup off' : 'startup on' },
|
|
167
197
|
{ keys: 'r', label: 'refresh' },
|
|
168
198
|
{ keys: '?', label: 'keys' },
|
|
@@ -202,6 +232,11 @@ function createDashboardScreen(deps) {
|
|
|
202
232
|
if (recent.length)
|
|
203
233
|
st.recent = recent;
|
|
204
234
|
st.offline = deps.data.offline();
|
|
235
|
+
// A selected run that has finished is no longer in the band. nowIndex
|
|
236
|
+
// already reads that as "cursor is in the menu"; drop the id too so the
|
|
237
|
+
// state doesn't keep pointing at a run nobody can see.
|
|
238
|
+
if (st.nowSelId && !selectableNow(st).some(r => r.id === st.nowSelId))
|
|
239
|
+
st.nowSelId = null;
|
|
205
240
|
// Reads a file (and, on Windows, a cached schtasks query) — cheap enough
|
|
206
241
|
// to ride the same poll, so the header can't disagree with reality after
|
|
207
242
|
// someone changes it from Studio or the CLI.
|
|
@@ -242,6 +277,43 @@ function createDashboardScreen(deps) {
|
|
|
242
277
|
// line — a note that never clears becomes furniture.
|
|
243
278
|
setTimeout(() => { st.autostartNote = null; deps.host.redraw(); }, 8000).unref?.();
|
|
244
279
|
};
|
|
280
|
+
/**
|
|
281
|
+
* One cursor over two stacked lists: the NOW band sits above the menu, so
|
|
282
|
+
* ↑ off the top of the menu lands on the LAST live row (the one nearest the
|
|
283
|
+
* menu) and ↓ off the bottom of the band returns to the first menu item.
|
|
284
|
+
* Reading the screen top to bottom, the cursor moves the way the eye does.
|
|
285
|
+
*/
|
|
286
|
+
const moveUp = () => {
|
|
287
|
+
const live = selectableNow(st);
|
|
288
|
+
const i = nowIndex(st);
|
|
289
|
+
if (i < 0) {
|
|
290
|
+
// In the menu. Step up inside it first; only leave from the top row.
|
|
291
|
+
if (st.sel > 0) {
|
|
292
|
+
st.sel -= 1;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (live.length)
|
|
296
|
+
st.nowSelId = live[live.length - 1].id;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (i > 0)
|
|
300
|
+
st.nowSelId = live[i - 1].id;
|
|
301
|
+
};
|
|
302
|
+
const moveDown = () => {
|
|
303
|
+
const live = selectableNow(st);
|
|
304
|
+
const i = nowIndex(st);
|
|
305
|
+
if (i < 0) {
|
|
306
|
+
st.sel = Math.min(exports.MENU.length - 1, st.sel + 1);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Past the last live row is the menu, entered at its first item.
|
|
310
|
+
if (i >= live.length - 1) {
|
|
311
|
+
st.nowSelId = null;
|
|
312
|
+
st.sel = 0;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
st.nowSelId = live[i + 1].id;
|
|
316
|
+
};
|
|
245
317
|
return {
|
|
246
318
|
id: 'dashboard',
|
|
247
319
|
title: '+Ai Node',
|
|
@@ -258,8 +330,8 @@ function createDashboardScreen(deps) {
|
|
|
258
330
|
return true;
|
|
259
331
|
},
|
|
260
332
|
keys: () => [
|
|
261
|
-
{ keys: '↑↓ / jk', label: 'move
|
|
262
|
-
{ keys: '⏎', label: 'open the selected
|
|
333
|
+
{ keys: '↑↓ / jk', label: 'move — up from the menu into the live runs' },
|
|
334
|
+
{ keys: '⏎', label: 'open the destination, or watch the selected run' },
|
|
263
335
|
{ keys: 'a', label: 'activity — full request history' },
|
|
264
336
|
{ keys: 'h', label: 'harnesses — install / log in agent CLIs' },
|
|
265
337
|
{ keys: 'l', label: 'logs — daemon output' },
|
|
@@ -268,12 +340,12 @@ function createDashboardScreen(deps) {
|
|
|
268
340
|
],
|
|
269
341
|
async onKey(key) {
|
|
270
342
|
if (key.name === 'up' || key.name === 'k') {
|
|
271
|
-
|
|
343
|
+
moveUp();
|
|
272
344
|
deps.host.redraw();
|
|
273
345
|
return;
|
|
274
346
|
}
|
|
275
347
|
if (key.name === 'down' || key.name === 'j') {
|
|
276
|
-
|
|
348
|
+
moveDown();
|
|
277
349
|
deps.host.redraw();
|
|
278
350
|
return;
|
|
279
351
|
}
|
|
@@ -298,6 +370,12 @@ function createDashboardScreen(deps) {
|
|
|
298
370
|
return;
|
|
299
371
|
}
|
|
300
372
|
if (key.name === 'return') {
|
|
373
|
+
// A selected run wins over the menu: the cursor is visibly on it.
|
|
374
|
+
const live = selectableNow(st)[nowIndex(st)];
|
|
375
|
+
if (live) {
|
|
376
|
+
deps.openTranscript(live);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
301
379
|
const target = exports.MENU[st.sel]?.key;
|
|
302
380
|
if (target === 'harnesses')
|
|
303
381
|
deps.openHarnesses();
|
package/dist/tui/run.js
CHANGED
|
@@ -192,7 +192,7 @@ async function runDashboard(opts) {
|
|
|
192
192
|
// the daemon log file, so nothing is lost by watching.
|
|
193
193
|
const logs = (0, console_capture_1.captureConsole)({ logFile: paths_1.RUNTIME_LOG_FILE });
|
|
194
194
|
const state = {
|
|
195
|
-
self: null, stats: null, recent: [], sel: 0, spin: 0,
|
|
195
|
+
self: null, stats: null, recent: [], sel: 0, nowSelId: null, nowRows: 0, spin: 0,
|
|
196
196
|
pid: opts.pid, startedAt: opts.startedAt,
|
|
197
197
|
inflight: 0, paired: (0, store_1.isPaired)(), viewerMode: opts.viewerMode,
|
|
198
198
|
offline: false, now: Date.now(), version: opts.version,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@addai/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Daemon that pairs a machine with your +Ai account and runs Claude / Codex / Kimi / Gemini agents on its behalf. Reachable via Supabase from Vault, Entity Studio, or any other +Ai surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|