@mmmbuto/nexuscrew 0.8.40 → 0.8.41
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/CHANGELOG.md +24 -1
- package/README.md +0 -9
- package/frontend/dist/assets/index-CrZBnpKn.css +32 -0
- package/frontend/dist/assets/index-D0MBXlAl.js +93 -0
- package/frontend/dist/assets/index-hq-2ORFQ.js +93 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/version.json +1 -1
- package/lib/cli/doctor.js +37 -0
- package/lib/config.js +7 -0
- package/lib/fleet/builtin.js +2 -1
- package/lib/fleet/launch.js +16 -0
- package/lib/fleet/runtime.js +14 -1
- package/lib/server.js +4 -1
- package/lib/settings/routes.js +19 -4
- package/lib/tmux/lifecycle.js +24 -3
- package/package.json +1 -1
- package/skills/nexuscrew-agent/SKILL.md +30 -1
package/frontend/dist/index.html
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-title" content="NexusCrew" />
|
|
12
12
|
<link rel="manifest" href="/manifest.json" />
|
|
13
13
|
<title>NexusCrew</title>
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-hq-2ORFQ.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CrZBnpKn.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.8.
|
|
1
|
+
{"version":"0.8.41"}
|
package/lib/cli/doctor.js
CHANGED
|
@@ -15,6 +15,7 @@ const { fleetInstallPath } = require('./fleet-service.js');
|
|
|
15
15
|
const { resolvePaths } = require('./url.js');
|
|
16
16
|
const { commandExists } = require('./path.js');
|
|
17
17
|
const { loadDefinitions } = require('../fleet/definitions.js');
|
|
18
|
+
const { loadConfig } = require('../config.js');
|
|
18
19
|
const {
|
|
19
20
|
termuxRuntimePaths, trustedTermuxPreload, TERMUX_EXEC_BASENAME_RE,
|
|
20
21
|
} = require('../runtime/env.js');
|
|
@@ -426,6 +427,39 @@ function checkFleetDefinitions(home, fleetDefsPath, enabled = true) {
|
|
|
426
427
|
};
|
|
427
428
|
}
|
|
428
429
|
|
|
430
|
+
// Read-only check: alternate-screen is applied per managed session, while the
|
|
431
|
+
// history limit remains an operator-owned tmux setting. Do not write ~/.tmux.conf
|
|
432
|
+
// or change a live server from doctor; only recommend a sufficient value.
|
|
433
|
+
function checkAlternateScreenHistory(alternateScreen, execImpl, tmuxBin) {
|
|
434
|
+
if (alternateScreen === true) {
|
|
435
|
+
return { name: 'Fleet alternate-screen history', ok: true, detail: 'alternate-screen standard attivo' };
|
|
436
|
+
}
|
|
437
|
+
let raw = '';
|
|
438
|
+
try {
|
|
439
|
+
raw = String(execImpl(tmuxBin || 'tmux', ['show-options', '-g', 'history-limit'], { encoding: 'utf8' }) || '');
|
|
440
|
+
} catch (_) {
|
|
441
|
+
return {
|
|
442
|
+
name: 'Fleet alternate-screen history', ok: true, warn: true,
|
|
443
|
+
detail: 'history-limit non verificabile; con alternateScreen off configura set -g history-limit 100000 in ~/.tmux.conf',
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const match = raw.match(/(?:^|\n)history-limit\s+(\d+)\b/);
|
|
447
|
+
if (!match) {
|
|
448
|
+
return {
|
|
449
|
+
name: 'Fleet alternate-screen history', ok: true, warn: true,
|
|
450
|
+
detail: 'history-limit non leggibile; con alternateScreen off configura set -g history-limit 100000 in ~/.tmux.conf',
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const limit = Number(match[1]);
|
|
454
|
+
if (limit < 10000) {
|
|
455
|
+
return {
|
|
456
|
+
name: 'Fleet alternate-screen history', ok: true, warn: true,
|
|
457
|
+
detail: `history-limit ${limit} (<10000) con alternateScreen off; configura set -g history-limit 100000 in ~/.tmux.conf`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return { name: 'Fleet alternate-screen history', ok: true, detail: `history-limit ${limit}` };
|
|
461
|
+
}
|
|
462
|
+
|
|
429
463
|
// Esegue tutti i check. Seam iniettabili per test (platform, home, execImpl, ptyLoad).
|
|
430
464
|
function doctor(opts = {}) {
|
|
431
465
|
const platform = opts.platform || detectPlatform();
|
|
@@ -454,6 +488,8 @@ function doctor(opts = {}) {
|
|
|
454
488
|
checkTmuxSurvival(platform, execImpl),
|
|
455
489
|
checkTokenPerms(tokenPath),
|
|
456
490
|
checkFleetDefinitions(home, opts.fleetDefsPath, fleetEnabled),
|
|
491
|
+
checkAlternateScreenHistory(opts.alternateScreen !== undefined
|
|
492
|
+
? opts.alternateScreen : loadConfig().alternateScreen, execImpl, opts.tmuxBin),
|
|
457
493
|
checkTermuxExec(opts.env, { platform, home }),
|
|
458
494
|
checkSshClient(existsImpl),
|
|
459
495
|
checkAutossh(existsImpl),
|
|
@@ -477,6 +513,7 @@ module.exports = {
|
|
|
477
513
|
checkNode, checkTmux, checkPty, checkService, checkBoot, checkTokenPerms,
|
|
478
514
|
checkMacServiceWorkingDirectory,
|
|
479
515
|
checkFleetDefinitions,
|
|
516
|
+
checkAlternateScreenHistory,
|
|
480
517
|
checkServiceWorkingDirectory,
|
|
481
518
|
checkFleetServiceWorkingDirectory,
|
|
482
519
|
checkTmuxSurvival, checkUserLinger,
|
package/lib/config.js
CHANGED
|
@@ -22,6 +22,9 @@ function baseDefaults() {
|
|
|
22
22
|
// Shared-server safety is operational hardening, not a same-UID security
|
|
23
23
|
// boundary. Disable only when the operator deliberately owns tmux policy.
|
|
24
24
|
protectSharedTmuxServer: true,
|
|
25
|
+
// Fleet sessions stay on the normal screen by default so their transcript
|
|
26
|
+
// enters tmux history and remains scrollable from the web terminal.
|
|
27
|
+
alternateScreen: false,
|
|
25
28
|
readonlyDefault: false,
|
|
26
29
|
// Etichetta neutra usata nel prefisso delle risposte ask incollate in TUI.
|
|
27
30
|
replyLabel: 'human',
|
|
@@ -72,6 +75,10 @@ function envOverrides() {
|
|
|
72
75
|
e.protectSharedTmuxServer = !['', '0', 'false', 'no', 'off']
|
|
73
76
|
.includes(String(process.env.NEXUSCREW_PROTECT_SHARED_TMUX_SERVER).toLowerCase());
|
|
74
77
|
}
|
|
78
|
+
if (process.env.NEXUSCREW_ALTERNATE_SCREEN !== undefined) {
|
|
79
|
+
e.alternateScreen = !['', '0', 'false', 'no', 'off']
|
|
80
|
+
.includes(String(process.env.NEXUSCREW_ALTERNATE_SCREEN).toLowerCase());
|
|
81
|
+
}
|
|
75
82
|
if (process.env.NEXUSCREW_READONLY) e.readonlyDefault = process.env.NEXUSCREW_READONLY === '1';
|
|
76
83
|
if (process.env.NEXUSCREW_REPLY_LABEL) e.replyLabel = process.env.NEXUSCREW_REPLY_LABEL;
|
|
77
84
|
if (process.env.NEXUSCREW_FILES_ROOT) e.filesRoot = process.env.NEXUSCREW_FILES_ROOT;
|
package/lib/fleet/builtin.js
CHANGED
|
@@ -47,7 +47,7 @@ const { requireSharedTmuxProtection } = require('../tmux/shared-server.js');
|
|
|
47
47
|
// da builtin.js; httpError e' usato anche dal CRUD del facade.
|
|
48
48
|
const { createBuiltinRuntime } = require('./runtime.js');
|
|
49
49
|
const {
|
|
50
|
-
composeLaunchArgv, composeClientInvocation, minimalEnv, promptCharsOk,
|
|
50
|
+
composeLaunchArgv, composeClientInvocation, alternateScreenArgs, minimalEnv, promptCharsOk,
|
|
51
51
|
redactSecrets, sanitizeEarlyDiagnostic, waitStablePane, httpError, migrateLegacyTmuxSessions,
|
|
52
52
|
} = require('./launch.js');
|
|
53
53
|
|
|
@@ -876,6 +876,7 @@ module.exports = {
|
|
|
876
876
|
resolveCellCwd,
|
|
877
877
|
composeLaunchArgv,
|
|
878
878
|
composeClientInvocation,
|
|
879
|
+
alternateScreenArgs,
|
|
879
880
|
minimalEnv,
|
|
880
881
|
promptCharsOk,
|
|
881
882
|
redactSecrets,
|
package/lib/fleet/launch.js
CHANGED
|
@@ -309,6 +309,21 @@ function composeLaunchArgv({ tmuxSession, realCwd, engine, cell }) {
|
|
|
309
309
|
return ['new-session', '-d', '-s', tmuxSession, '-c', realCwd, child.command, ...child.args];
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// Pure: applica alternate-screen soltanto alla sessione Fleet appena creata.
|
|
313
|
+
// Il target exact-match protegge da nomi che condividono un prefisso; `-w`
|
|
314
|
+
// mantiene l'opzione window-local e non muta mai il server tmux globale.
|
|
315
|
+
// La hook e' necessaria perche' le finestre create dopo new-session non
|
|
316
|
+
// ereditano l'opzione della prima finestra.
|
|
317
|
+
function alternateScreenArgs(session, alternateScreen = false) {
|
|
318
|
+
if (!isTmuxSafeName(session)) return null;
|
|
319
|
+
if (alternateScreen !== false) return [];
|
|
320
|
+
const target = `=${session}:`;
|
|
321
|
+
return [
|
|
322
|
+
['set-option', '-t', target, '-w', 'alternate-screen', 'off'],
|
|
323
|
+
['set-hook', '-t', target, 'after-new-window', 'set-option -w alternate-screen off'],
|
|
324
|
+
];
|
|
325
|
+
}
|
|
326
|
+
|
|
312
327
|
// Poll has-session entro readyMs (no delay fisso cieco). Ritorna true se la sessione
|
|
313
328
|
// e' viva entro la deadline, false altrimenti (command uscito / mai partita).
|
|
314
329
|
async function waitAlive(tmuxBin, session, { env, readyMs }) {
|
|
@@ -380,6 +395,7 @@ module.exports = {
|
|
|
380
395
|
promptCharsOk,
|
|
381
396
|
composeClientInvocation,
|
|
382
397
|
composeLaunchArgv,
|
|
398
|
+
alternateScreenArgs,
|
|
383
399
|
migrateLegacyTmuxSessions,
|
|
384
400
|
waitAlive,
|
|
385
401
|
waitStablePane,
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -21,7 +21,7 @@ const {
|
|
|
21
21
|
} = require('./managed.js');
|
|
22
22
|
const {
|
|
23
23
|
httpError, minimalEnv, tmuxExec,
|
|
24
|
-
composeClientInvocation,
|
|
24
|
+
composeClientInvocation, alternateScreenArgs,
|
|
25
25
|
waitAlive, waitStablePane, injectPrompt,
|
|
26
26
|
redactSecrets, sanitizeEarlyDiagnostic,
|
|
27
27
|
} = require('./launch.js');
|
|
@@ -246,6 +246,19 @@ function createBuiltinRuntime(ctx) {
|
|
|
246
246
|
throw httpError(dup ? 409 : 500, why, null,
|
|
247
247
|
{ phase: 'new-session', code: dup ? 'SESSION_DUPLICATE' : 'NEW_SESSION_FAILED' });
|
|
248
248
|
}
|
|
249
|
+
// Best effort per la sola sessione appena creata: non tocchiamo mai
|
|
250
|
+
// ~/.tmux.conf, opzioni globali o sessioni preesistenti. Un tmux che non
|
|
251
|
+
// accetta una delle opzioni non deve impedire l'avvio della cella.
|
|
252
|
+
const alternateSteps = alternateScreenArgs(cell.tmuxSession, cfg.alternateScreen);
|
|
253
|
+
if (alternateSteps) {
|
|
254
|
+
for (const args of alternateSteps) {
|
|
255
|
+
const configured = await tmuxExec(tmuxBin, args, { env: minimalEnv(), timeoutMs: 2000 });
|
|
256
|
+
if (configured.err) {
|
|
257
|
+
const log = typeof cfg.log === 'function' ? cfg.log : console.warn;
|
|
258
|
+
log(`fleet alternate-screen setup failed for ${cell.id}; continuing`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
249
262
|
const createdIds = create.stdout.trim().split('\n')[0].split('\t');
|
|
250
263
|
const sessionId = /^\$[0-9]+$/.test(createdIds[0] || '') ? createdIds[0] : '';
|
|
251
264
|
const windowId = /^@[0-9]+$/.test(createdIds[1] || '') ? createdIds[1] : '';
|
package/lib/server.js
CHANGED
|
@@ -273,7 +273,10 @@ function createServer(opts = {}) {
|
|
|
273
273
|
try {
|
|
274
274
|
const { name, cwd, preset } = req.body || {};
|
|
275
275
|
await createSession(cfg.tmuxBin, { name, cwd, preset },
|
|
276
|
-
{
|
|
276
|
+
{
|
|
277
|
+
home: os.homedir(), presets: cfg.sessionPresets, ensureProtection: ensureTmuxProtection,
|
|
278
|
+
alternateScreen: cfg.alternateScreen, log: cfg.log,
|
|
279
|
+
});
|
|
277
280
|
res.status(201).json({ created: true, name });
|
|
278
281
|
} catch (e) { res.status(e.status || 500).json({ error: String(e.message || e) }); }
|
|
279
282
|
});
|
package/lib/settings/routes.js
CHANGED
|
@@ -67,7 +67,7 @@ const { createPairHandler } = require('./pairing-coordinator.js');
|
|
|
67
67
|
const { publicPeeringRoutes } = require('./public-peering-routes.js');
|
|
68
68
|
|
|
69
69
|
// Whitelist chiavi scrivibili di config.json via API (lista chiusa dal task B2).
|
|
70
|
-
const CONFIG_KEYS = new Set(['roles', 'port', 'wizardDone', 'autoUpdate']);
|
|
70
|
+
const CONFIG_KEYS = new Set(['roles', 'port', 'wizardDone', 'autoUpdate', 'alternateScreen']);
|
|
71
71
|
const ROLE_KEYS = new Set(['client', 'node']);
|
|
72
72
|
const ADD_KEYS = new Set(['name', 'ssh', 'sshPort', 'remotePort', 'localPort', 'keyPath', 'label']);
|
|
73
73
|
const EDIT_KEYS = new Set(['label', 'ssh', 'sshPort', 'autostart', 'visibility', 'selected']);
|
|
@@ -242,6 +242,9 @@ function settingsRoutes(deps = {}) {
|
|
|
242
242
|
service,
|
|
243
243
|
version: VERSION,
|
|
244
244
|
autoUpdate: updateStatus ? updateStatus.enabled : fileCfg.autoUpdate !== false,
|
|
245
|
+
// Valore effettivo: l'env mantiene la precedenza sul file e il runtime
|
|
246
|
+
// Fleet legge lo stesso oggetto cfg alla prossima creazione di cella.
|
|
247
|
+
alternateScreen: cfg.alternateScreen === true,
|
|
245
248
|
// Nome dispositivo proposto per i form di pairing (etichetta umana, non
|
|
246
249
|
// lo slug). La UI lo precompila e lascia editing libero.
|
|
247
250
|
deviceName,
|
|
@@ -263,10 +266,10 @@ function settingsRoutes(deps = {}) {
|
|
|
263
266
|
return send(res, 400, { error: 'body deve essere un oggetto JSON' });
|
|
264
267
|
}
|
|
265
268
|
for (const k of Object.keys(b)) {
|
|
266
|
-
if (!CONFIG_KEYS.has(k)) return send(res, 400, { error: `chiave non ammessa: "${k}" (whitelist: roles, port, wizardDone, autoUpdate)` });
|
|
269
|
+
if (!CONFIG_KEYS.has(k)) return send(res, 400, { error: `chiave non ammessa: "${k}" (whitelist: roles, port, wizardDone, autoUpdate, alternateScreen)` });
|
|
267
270
|
}
|
|
268
271
|
if (Object.keys(b).length === 0) {
|
|
269
|
-
return send(res, 400, { error: 'nessuna chiave da scrivere (whitelist: roles, port, wizardDone, autoUpdate)' });
|
|
272
|
+
return send(res, 400, { error: 'nessuna chiave da scrivere (whitelist: roles, port, wizardDone, autoUpdate, alternateScreen)' });
|
|
270
273
|
}
|
|
271
274
|
if (b.roles !== undefined) {
|
|
272
275
|
if (!b.roles || typeof b.roles !== 'object' || Array.isArray(b.roles)) {
|
|
@@ -296,6 +299,9 @@ function settingsRoutes(deps = {}) {
|
|
|
296
299
|
if (b.autoUpdate !== undefined && typeof b.autoUpdate !== 'boolean') {
|
|
297
300
|
return send(res, 400, { error: 'autoUpdate deve essere boolean' });
|
|
298
301
|
}
|
|
302
|
+
if (b.alternateScreen !== undefined && typeof b.alternateScreen !== 'boolean') {
|
|
303
|
+
return send(res, 400, { error: 'alternateScreen deve essere boolean' });
|
|
304
|
+
}
|
|
299
305
|
|
|
300
306
|
// merge sul config esistente (preserva le chiavi non gestite qui)
|
|
301
307
|
const { cfg: current } = readConfigFile(configPath);
|
|
@@ -307,14 +313,23 @@ function settingsRoutes(deps = {}) {
|
|
|
307
313
|
if (b.port !== undefined) next.port = b.port;
|
|
308
314
|
if (b.wizardDone !== undefined) next.wizardDone = b.wizardDone;
|
|
309
315
|
if (b.autoUpdate !== undefined) next.autoUpdate = b.autoUpdate;
|
|
316
|
+
if (b.alternateScreen !== undefined) next.alternateScreen = b.alternateScreen;
|
|
310
317
|
atomicWriteConfig(configPath, next);
|
|
311
318
|
if (b.autoUpdate !== undefined && updater && typeof updater.setEnabled === 'function') {
|
|
312
319
|
updater.setEnabled(b.autoUpdate);
|
|
313
320
|
}
|
|
321
|
+
// Vale dalla prossima fleet up, mai retroattivamente; rispetta l'env che
|
|
322
|
+
// conserva la precedenza sul file per tutta la vita del processo.
|
|
323
|
+
if (b.alternateScreen !== undefined && process.env.NEXUSCREW_ALTERNATE_SCREEN === undefined) {
|
|
324
|
+
cfg.alternateScreen = b.alternateScreen;
|
|
325
|
+
}
|
|
314
326
|
|
|
315
327
|
const out = {
|
|
316
328
|
saved: true,
|
|
317
|
-
config: {
|
|
329
|
+
config: {
|
|
330
|
+
roles: next.roles, port: next.port, wizardDone: next.wizardDone,
|
|
331
|
+
autoUpdate: next.autoUpdate !== false, alternateScreen: next.alternateScreen === true,
|
|
332
|
+
},
|
|
318
333
|
};
|
|
319
334
|
// Il server legge la porta SOLO allo startup: il cambio vale al prossimo
|
|
320
335
|
// restart (contratto dichiarato nella risposta, la UI avvisa).
|
package/lib/tmux/lifecycle.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const fs = require('node:fs');
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { execFile } = require('node:child_process');
|
|
5
|
+
const { alternateScreenArgs, minimalEnv, tmuxExec } = require('../fleet/launch.js');
|
|
5
6
|
|
|
6
7
|
// Preset allowlistati (audit F1): il client sceglie un NOME, mai un comando.
|
|
7
8
|
// Estendibili da config.json `sessionPresets` (name -> array argv di stringhe).
|
|
@@ -52,9 +53,25 @@ function buildCreateArgs(name, realCwd, preset, extraPresets) {
|
|
|
52
53
|
|
|
53
54
|
function httpError(status, msg) { const e = new Error(msg); e.status = status; return e; }
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
// Best effort per la sola sessione PWA appena creata. Riusa il builder Fleet:
|
|
57
|
+
// target exact-match, opzione window-local e hook per le finestre successive.
|
|
58
|
+
// Un tmux che rifiuta l'opzione non deve mai trasformare una creazione riuscita
|
|
59
|
+
// in un errore HTTP; nessuna opzione globale viene emessa.
|
|
60
|
+
async function configureAlternateScreen(tmuxBin, name, alternateScreen, log) {
|
|
61
|
+
const steps = alternateScreenArgs(name, alternateScreen);
|
|
62
|
+
if (!steps) return;
|
|
63
|
+
const warn = typeof log === 'function' ? log : console.warn;
|
|
64
|
+
for (const args of steps) {
|
|
65
|
+
const configured = await tmuxExec(tmuxBin, args, { env: minimalEnv(), timeoutMs: 2000 });
|
|
66
|
+
if (configured.err) warn(`session alternate-screen setup failed for ${name}; continuing`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function createSession(tmuxBin, { name, cwd, preset }, {
|
|
71
|
+
home, presets, ensureProtection, alternateScreen, log,
|
|
72
|
+
} = {}) {
|
|
56
73
|
if (typeof ensureProtection === 'function') await ensureProtection();
|
|
57
|
-
|
|
74
|
+
await new Promise((resolve, reject) => {
|
|
58
75
|
if (!validSessionName(name)) return reject(httpError(400, 'nome sessione non valido'));
|
|
59
76
|
// Il namespace cloud-* e' delle celle fleet (audit finale #1): una generica
|
|
60
77
|
// con quel prefisso occuperebbe il binding per-nome e sarebbe poi 409 al kill.
|
|
@@ -71,6 +88,7 @@ async function createSession(tmuxBin, { name, cwd, preset }, { home, presets, en
|
|
|
71
88
|
resolve();
|
|
72
89
|
});
|
|
73
90
|
});
|
|
91
|
+
await configureAlternateScreen(tmuxBin, name, alternateScreen, log);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
async function killSession(tmuxBin, name, { ensureProtection } = {}) {
|
|
@@ -86,4 +104,7 @@ async function killSession(tmuxBin, name, { ensureProtection } = {}) {
|
|
|
86
104
|
});
|
|
87
105
|
}
|
|
88
106
|
|
|
89
|
-
module.exports = {
|
|
107
|
+
module.exports = {
|
|
108
|
+
PRESETS, validSessionName, resolveCwd, isProtectedSession, buildCreateArgs,
|
|
109
|
+
configureAlternateScreen, createSession, killSession,
|
|
110
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nexuscrew-agent
|
|
3
|
-
description: Use when an AI agent connected to NexusCrew must notify or ask the human, inspect runtime or deck context, list authorized Fleet cells, message an exact active cell, read its inbox, deliver a file,
|
|
3
|
+
description: Use when an AI agent connected to NexusCrew must notify or ask the human, inspect runtime or deck context, list authorized Fleet cells, message an exact active cell, read its inbox, deliver a file, recover from local tmux messages that remain unsubmitted or garbled, or when a drag in the web terminal does not scroll a full-screen TUI. Prefer nc_notify, nc_ask, nc_status, nc_deck, nc_cells, nc_send_cell, nc_inbox, and nc_send_file; use bundled tmux/file helpers only as a declared compatibility fallback.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# NexusCrew Agent I/O
|
|
@@ -91,6 +91,33 @@ It does: `load-buffer` → `paste-buffer -p` (bracketed paste) → burst-flush (
|
|
|
91
91
|
tmux capture-pane -t <session> -p | tail -8 # see the text / a running state
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
## Terminal scrolling (NexusCrew-managed tmux)
|
|
95
|
+
|
|
96
|
+
A drag or wheel in the NexusCrew web terminal browses the tmux history through
|
|
97
|
+
copy-mode. That works only while the pane is on the normal screen. A full-screen
|
|
98
|
+
TUI — Claude Code, Codex, Agy, `vim`, `less`, `htop` — renders in the terminal's
|
|
99
|
+
**alternate buffer**, which has no scrollback: copy-mode has nothing to scroll, so
|
|
100
|
+
the gesture does nothing. On a phone, where the drag is the only scroll gesture,
|
|
101
|
+
the terminal looks frozen.
|
|
102
|
+
|
|
103
|
+
NexusCrew applies `alternate-screen off` **per session** when it creates a
|
|
104
|
+
managed Fleet session or a PWA session. No global tmux option or `~/.tmux.conf` edit is needed:
|
|
105
|
+
tmux then ignores `smcup`/`rmcup` for that session, TUI output stays on the
|
|
106
|
+
normal screen, the transcript flows into tmux history, and drag/wheel scrolling
|
|
107
|
+
work. The setting is also applied to windows created later in that session.
|
|
108
|
+
|
|
109
|
+
- It applies only to future NexusCrew-created sessions. A running or unmanaged
|
|
110
|
+
pane remains unchanged; never restart a Fleet cell just to change it.
|
|
111
|
+
- To opt out, set `alternateScreen: true` in NexusCrew's local `config.json` or
|
|
112
|
+
start the service with `NEXUSCREW_ALTERNATE_SCREEN=1`.
|
|
113
|
+
- Verify on a pane that is running a TUI:
|
|
114
|
+
`tmux display-message -p -t '=<session>:' '#{alternate_on}'` must print `0`.
|
|
115
|
+
- Trade-off: after quitting `vim`/`less`/`htop` the last screen stays on display
|
|
116
|
+
instead of being restored.
|
|
117
|
+
- Full-screen redraws consume history. Keep a generous user-owned
|
|
118
|
+
`history-limit` (100000 is a good default); `nexuscrew doctor` warns below
|
|
119
|
+
10000 and suggests `set -g history-limit 100000`, but never writes it.
|
|
120
|
+
|
|
94
121
|
## Quick reference
|
|
95
122
|
|
|
96
123
|
| Goal | Do this |
|
|
@@ -106,6 +133,7 @@ tmux capture-pane -t <session> -p | tail -8 # see the text / a running state
|
|
|
106
133
|
| Send a prompt/command to a session | `nc-send <session> "text"` |
|
|
107
134
|
| Queue text without running it | `nc-send <session> --no-submit "text"` |
|
|
108
135
|
| Confirm a send worked | `tmux capture-pane -t <session> -p | tail` |
|
|
136
|
+
| Make a new managed terminal scrollable on a full-screen TUI | NexusCrew default; verify `#{alternate_on}` is `0` |
|
|
109
137
|
|
|
110
138
|
## Common mistakes
|
|
111
139
|
|
|
@@ -116,3 +144,4 @@ tmux capture-pane -t <session> -p | tail -8 # see the text / a running state
|
|
|
116
144
|
- **Delivering to a guessed session name** → file lands in an orphan folder with no badge. Use `nc-deliver` (it reads the real session).
|
|
117
145
|
- **Sending to an ambiguous cell name** → call `nc_cells` and use the full owner-qualified ID.
|
|
118
146
|
- **Calling `submitted` a completed task** → it is only a transport receipt; require an explicit result callback.
|
|
147
|
+
- **Treating a dead scroll gesture as a web-terminal bug** → the pane is in the alternate buffer. Check whether it predates the NexusCrew setting or opted out with `alternateScreen:true`; never send raw page keys to a TUI to work around it.
|