Polish Beehive manager summaries, contextual actions and compact forms

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-11 14:48:00 -04:00
parent 6390e5947c
commit cb1b341ccb
7 changed files with 303 additions and 70 deletions

View File

@@ -117,3 +117,19 @@ with pnpm 11.4.0 `install --ignore-workspace --ignore-scripts --frozen-lockfile`
Node 24.15.0 and Bun 1.4.2 executables at `beehive/runtime/node` and
`beehive/runtime/bun`. Link `~/.local/bin/beehive` to the exported package's
`bin/beehive`, not to a worktree. See the immutable delivery recipe for the exact pin.
## Local visual-polish candidate (not installed)
The subsequent presentation candidate replaces default JSON walls with Node-built
public summaries and F3 technical evidence; uses ≥88×24 split / narrow Enter-to-detail;
puts contextual guarded commands in F2; retains permanent keys with F4 scrollable
outcomes; and uses compact validated inputs plus a named configuration chooser.
The installed `6390e594` exports are deliberately unchanged pending independent
implementation-vs-design review.
Evidence and the corrected external design are under
`/Users/loganj/.buzz/artifacts/beehive-opentui-polish-6390e594/implementation/`.
Read `IMPLEMENTATION.md` there for the eight-point self-review, scoped deviations,
actual PTY comparisons versus offscreen fixtures, exact runtime commands and limits.
This candidate does not claim exhaustive design conformance, real relay inventory,
light-terminal visual proof, or safe scrolled-action mouse activation.

View File

@@ -12,7 +12,15 @@ import { message, type Message } from './protocol.ts';
import { profileDrafts, editProfileDraft } from './profile-drafts.ts';
export type ManagerRequest = { id: number; action: string; values?: Record<string, string>; target?: string; revision?: number };
export type ManagerSnapshot = { local: { id: string; label: string; detail: string }[]; agents: { id: string; label: string; detail: string; revision: number; configurations: string[] }[]; owner?: string; status: string };
export type ManagerItem = { id: string; label: string; detail: string; evidence?: string; disabled?: Record<string, string> };
export type ManagerSnapshot = { local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[] })[]; routing?: { owner: string; relay: string }; owner?: string; status: string };
const short = (s: string) => s.length > 22 ? `${s.slice(0,8)}…${s.slice(-6)}` : s;
const describe = (v: unknown): string => {
if (v === undefined || v === null) return 'Not reported';
if (typeof v !== 'object') return typeof v === 'string' && /^[0-9a-f]{32,}$/i.test(v) ? short(v) : String(v);
if (!Object.keys(v).length) return 'Not reported';
return Object.entries(v).map(([k, value]) => `${k}: ${describe(value)}`).join('\n');
};
/** One bounded Node helper per explicit credential operation. Completion waits for exit.
* Cancellation cannot undo OS persistence; callers must inspect before retrying. */
@@ -66,15 +74,61 @@ export class ManagerController {
try {
if (existsSync(join(this.hostDirectory, 'host-identity.json'))) {
const identity = readHostIdentityPublic(this.hostDirectory);
local.push({ id: 'host', label: identity.pairing.label, detail: `THIS COMPUTER — configuration, not service liveness\n${JSON.stringify(identity.pairing, null, 2)}\nStart foreground service separately: beehive host --owner-present\nLocal provisioning: beehive local-setup ${this.hostDirectory}\nProvision from a binding file: beehive provision-agent (existing CLI).\nService launch and binding authoring forms: Not available in this build. Existing binding/genesis provisioning is available below.` });
local.push({ id: 'host', label: identity.pairing.label, evidence: JSON.stringify(identity.pairing, null, 2), detail: `${identity.pairing.label}
Configuration: saved
Relay: ${identity.pairing.relay}
Owner: ${short(identity.pairing.owner)}
Service: not checked by this view
Registration is not Start authority.
Next steps
F2: provision from prepared binding/genesis files.
Quit manager before foreground CLI use:
beehive host --owner-present
Service launch and binding authoring forms are unavailable.` });
if (existsSync(join(this.hostDirectory, 'setup.json'))) for (const slot of installationPublicSlots(this.hostDirectory)) local.push({ id: slot.agent, label: slot.agent, detail: `Local public slot (not current run status)\nAgent ${slot.agent}\nBindings: ${Object.keys(slot.bindings).join(', ')}` });
} else local.push({ id: 'missing', label: 'Configure this computer', detail: 'No local host configuration. Save owner PUBLIC npub and relay; host identity is created in OS credentials. No owner sign-in, agent or service Start.' });
} catch (error) { local.push({ id: 'error', label: 'Retained configuration needs attention', detail: String(error) + '\nPreserved; no automatic reset.' }); }
const agents: ManagerSnapshot['agents'] = [...this.inventory].map(([id, m]) => ({ id, label: `${m.agent.slice(0, 12)} · ${m.host.slice(0, 12)} · ${this.fresh(m) ? m.body.phase : 'UNKNOWN/stale'}`, revision: m.revision, configurations: Object.keys((m.body.configurations ?? {}) as object), detail: `HOST OBSERVATION — not an independent agent catalog\n${this.fresh(m) ? 'Recent report' : 'UNKNOWN / historical, not stopped'}\nActual run and selected-next are separate.\n${JSON.stringify(m, null, 2)}` }));
for (const [id, m] of this.offers) agents.push({ id: `host:${id}`, label: `Host ${id.slice(0, 12)}`, revision: m.revision, configurations: [], detail: `Infrastructure availability is not agent authorization or global liveness.\n${JSON.stringify(m, null, 2)}` });
for (const operation of this.client?.status() ?? []) agents.push({ id: `operation:${operation.request.id}`, label: `${operation.request.type} · ${operation.state} · ${operation.request.id.slice(0,8)}`, revision: operation.request.revision, configurations: [], detail: `OPERATION RECORD — not current host state\n${JSON.stringify(operation, null, 2)}\nUnknown is not stopped. Reconcile queries receipts without resubmitting. Policy retry remains available in existing CLI.` });
return { local, agents, owner: this.owner, status: this.status };
const agents: ManagerSnapshot['agents'] = [...this.inventory].map(([id, m]) => {
const fresh = this.fresh(m);
const blocked = !fresh ? 'Host stale or unreachable. Refresh before acting.' : this.client?.status().some(o => o.request.host === m.host && o.request.agent === m.agent && !['completed','failed'].includes(o.state)) ? 'Unresolved operation: inspect receipts before acting.' : '';
return { id, label: `Agent ${short(m.agent)} · ${fresh ? m.body.phase : 'UNKNOWN'}`, revision: m.revision, configurations: Object.keys((m.body.configurations ?? {}) as object),
disabled: { 'select-config': blocked, stop: blocked, start: blocked || (m.body.phase !== 'stopped' || m.body.actualRun || m.body.assignedHost !== m.host ? 'Requires fresh assigned stopped report without an actual run.' : '') },
evidence: JSON.stringify(m, null, 2), detail: `Agent ${short(m.agent)}
Host ${short(m.host)}
${fresh ? 'Recent host report' : 'UNKNOWN — stale or unreachable; last reported below'}
Reported phase: ${m.body.phase}
Start authority: ${m.body.assignedHost === m.host ? 'Assigned to this host' : 'Not assigned to this host'}
Actual run (reported)
${m.body.actualRun ? describe(m.body.actualRun) : 'No actual run in this report'}
Selected for next Start
${describe(m.body.selectedNext)}
Choosing next does not change the actual run.` };
});
for (const [id, m] of this.offers) agents.push({ id: `host:${id}`, label: `Host ${short(id)}`, revision: m.revision, configurations: [], evidence: JSON.stringify(m, null, 2), detail: `Host availability report
Host ${short(id)}
${this.fresh(m) ? 'Recent host report' : 'UNKNOWN — stale or unreachable'}
${describe(m.body)}
Availability is not agent Start authority.` });
for (const operation of this.client?.status() ?? []) agents.push({ id: `operation:${operation.request.id}`, label: `${operation.request.type} · ${operation.state} · ${operation.request.id.slice(0,8)}`, revision: operation.request.revision, configurations: [], evidence: JSON.stringify(operation, null, 2), detail: `Operation: ${operation.request.type}
State: ${operation.state}
Host: ${short(operation.request.host)}
Agent: ${short(operation.request.agent)}
Captured revision: ${operation.request.revision}
Publication: ${describe(operation.publication)}
Receipt/result: ${describe(operation.result)}
Publication is not acceptance.
Outcome unknown? Check receipts; do not submit again.
A Stop receipt is not a fresh stopped inventory report.` });
let routing: ReturnType<typeof readControllerConfig>;
try { routing = readControllerConfig(this.ownerDirectory); } catch { /* Invalid retained routing is refused by sign-in; never reset here. */ }
return { local, agents, routing: routing ? { owner: routing.owner, relay: routing.relay } : undefined, owner: this.owner, status: this.status };
}
private fresh(m: Message) { return Boolean(this.client?.connected && Date.now() - Number(m.body.observedAt) <= 6000); }
refresh() { if (!this.closed) this.changed(this.snapshot()); }
cancel() { this.generation++; this.active?.abort(); }
@@ -91,7 +145,7 @@ export class ManagerController {
const owner = ownerPublicInput(v.owner ?? '');
if (!/^(wss|ws):\/\//.test(v.relay ?? '')) throw Error('Management relay URL required');
await this.credential({ action: 'configure', directory: this.hostDirectory, label: hostname().slice(0,128), owner, relay: v.relay }, abort.signal);
check(); this.status = 'Host configuration saved. Not serving; no agents started.';
check(); this.status = 'Configuration saved. This action did not start a service or agent.';
} else if (request.action === 'provision') {
await this.credential({ action: 'provision', directory: this.hostDirectory, binding: v.binding, genesis: v.genesis, secret: v.secret }, abort.signal);
check(); this.status = 'Local agent provisioned stopped using existing binding/genesis APIs. No Start or relay publication.';

View File

@@ -8,37 +8,55 @@ const renderer = await createCliRenderer({ exitOnCtrlC: false });
const screen = new OpenTuiScreen(renderer);
const output = createWriteStream('', { fd: 4 });
let snapshot: ManagerSnapshot = { local: [], agents: [], status: 'Connecting Node controller…' };
let scope = 0, selected = '', sequence = 0;
let scope = 0, selected = '', sequence = 0, lastStatus = '';
let pending: { id: number; resolve: () => void } | undefined;
function request(action: string, values?: Record<string,string>, target?: string, revision?: number) {
if (pending) return Promise.resolve();
const id = ++sequence;
const completion = new Promise<void>(resolve => { pending = { id, resolve }; });
const message: ManagerRequest = { id, action, values, target, revision };
screen.setPending(true); screen.notice(`Working: ${action}… Esc cancels waiting, not submitted remote work or committed OS writes. Inspect before retry.`);
output.write(JSON.stringify(message) + '\n');
return completion;
}
async function routing(action: string, secret = false) {
const owner = await screen.input('Owner PUBLIC npub/hex (blank uses retained controller owner)');
if (owner === undefined) return;
const relay = await screen.input('Management relay URL (blank uses retained controller relay)');
if (relay === undefined) return;
const retained = action !== 'configure' ? snapshot.routing : undefined;
let owner = retained?.owner ?? '', relay = retained?.relay ?? '';
if (!retained) {
let step = 0;
while (step < 2) {
const label = step === 0 ? `${action === 'configure' ? 'Configure this computer' : 'Owner sign-in'} · 1 of 3\nOwner PUBLIC npub/hex (required)` : 'Configure routing · 2 of 3\nManagement relay URL (ws:// or wss://)';
const answer = await screen.input(label, step === 0 ? owner : relay, false, false, value => step === 0 ? /^(?:[0-9a-fA-F]{64}|npub1[023456789acdefghjklmnpqrstuvwxyz]{58})$/.test(value.trim()) ? '' : 'Enter a public 64-hex key or npub.' : /^wss?:\/\//.test(value) ? '' : 'Enter a ws:// or wss:// relay URL.', step === 1 ? value => { relay = value; } : undefined);
if (answer === undefined) return;
if (answer === '\0back') { step = 0; continue; }
if (step === 0) owner = answer; else relay = answer;
step++;
}
}
const values: Record<string,string> = { owner, relay };
if (secret) {
const key = await screen.input('Matching owner private key, 64 hex. Saved only in this computer’s OS credentials. Never sent to a host.', '', true);
if (key === undefined) return;
values.secret = key;
}
if (!await screen.confirm(action === 'configure' ? 'Create this host identity in OS credentials? No owner key, agent or Start.' : 'Access this computer’s owner OS credential? Permission may prompt. Import is verified against configured owner.')) { delete values.secret; return; }
if (!await screen.confirm(action === 'configure' ? `Configure this computer · 3 of 3\nOwner: ${owner}\nRelay: ${relay}\nCreate host identity in OS credentials? No agent or Start.` : `Owner: ${owner}\nRelay: ${relay}\n${retained ? 'Retained routing (read-only). ' : ''}Access owner OS credential? Permission may prompt. Matching import only.`)) { delete values.secret; return; }
await request(action, values); delete values.secret;
}
function eligibility(action: string) {
const row = snapshot.agents.find(r => r.id === selected);
if (!snapshot.owner) return 'Owner sign-in required.';
if (!row?.disabled) return 'Select an agent observation.';
return row.disabled[action] || (action === 'select-config' && !row.configurations.length ? 'No existing configurations reported.' : undefined);
}
function render() {
screen.setOwner(snapshot.owner?.slice(-12));
const rows = scope === 0 ? snapshot.local : snapshot.agents;
if (!rows.some(row => row.id === selected)) selected = rows[0]?.id ?? '';
if (selected === 'empty') selected = '';
if (!selected || (scope === 0 && selected === 'missing' && rows.some(r => r.id === 'host'))) selected = rows[0]?.id ?? '';
const vanished = !!selected && !rows.some(row => row.id === selected);
const actions: ManagerAction[] = scope === 0 ? [
{ label: 'Configure missing Local Host', run: () => routing('configure') },
{ label: 'Provision agent from existing binding/genesis', run: async () => {
snapshot.local.some(r => r.id === 'host') ? { label: 'Host configuration saved — inspect', run: () => screen.inspect() } : { label: 'Configure this computer', disabled: snapshot.local.some(r => r.id === 'error') ? 'Retained configuration needs attention — preserved, not reset.' : undefined, run: () => routing('configure') },
{ label: 'Provision from prepared binding/genesis files', disabled: snapshot.local.some(r => r.id === 'host') ? undefined : 'Configure this computer first.', run: async () => {
const binding = await screen.input('Absolute existing local binding JSON path (no identity fields)'); if (binding === undefined) return;
const genesis = await screen.input('Absolute owner-authorized agent genesis JSON path'); if (genesis === undefined) return;
const secret = await screen.input('Matching AGENT private key, 64 hex. Stored in host OS credentials, not the owner key.', '', true); if (secret === undefined) return;
@@ -47,19 +65,19 @@ function render() {
} },
{ label: 'Refresh local inspection', run: () => request('refresh') },
{ label: 'Save private instructions draft', run: async () => {
const name = await screen.input('Profile name'); if (name === undefined) return;
const instructions = await screen.input('Private behavior instructions (not credentials)', '', false, true); if (instructions === undefined) return;
const name = await screen.input('New private instruction draft · 1 of 2\nProfile name', '', false, false, v => v.trim() ? '' : 'A name is required.'); if (name === undefined) return;
const instructions = await screen.input(`Draft: ${name}\nLocal only — not published or applied. Private instructions (not credentials)`, '', false, true, v => v.trim() ? '' : 'Instructions are required.'); if (instructions === undefined) return;
await request('draft', { name, instructions });
} },
{ label: 'Provisioning / service commands', run: () => screen.notice('Forms: Not available in this build. Existing CLI: beehive local-setup ~/.beehive/host; beehive provision-agent; beehive host --owner-present. Quit this manager before foreground CLI use.') },
] : snapshot.owner ? [
{ label: 'Select existing next configuration', run: async () => {
{ label: 'Choose next configuration…', disabled: eligibility('select-config'), run: async () => {
const row = snapshot.agents.find(r => r.id === selected); if (!row) return;
const name = await screen.input(`Existing configurations: ${row.configurations.join(', ')}\nExact name`); if (name === undefined) return;
if (!await screen.confirm(`Select ${name} for ${row.label}? Actual run unchanged.`)) return;
const name = await screen.choose('Choose existing next configuration', row.configurations); if (name === undefined) return;
if (!await screen.confirm(`Select ${name} for exact host/agent ${row.id} at revision ${row.revision}? Actual run unchanged.`)) return;
await request('select-config', { name }, row.id, row.revision);
} },
...(['start','stop','restart'] as const).map(action => ({ label: action === 'restart' ? 'Restart — not available (binding safety)' : `${action === 'start' ? 'Start' : 'Stop'} selected agent`, run: async () => {
...(['start','stop','restart'] as const).map(action => ({ label: action === 'restart' ? 'Restart — not available (binding safety)' : `${action === 'start' ? 'Start' : 'Stop'} selected agent`, disabled: action === 'restart' ? 'Backend Restart may switch bindings. Use Stop, inspect accepted stopped report, then Start. No Restart request is sent.' : eligibility(action), run: async () => {
const row = snapshot.agents.find(r => r.id === selected); if (!row) return;
if (!await screen.confirm(`${action.toUpperCase()} exact agent/host ${row.id} at revision ${row.revision}?`)) return;
await request(action, undefined, row.id, row.revision);
@@ -72,23 +90,23 @@ function render() {
{ label: 'Sign in with saved owner key', run: () => routing('signin') },
{ label: 'Import matching owner key and sign in', run: () => routing('signin', true) },
];
screen.show(scope === 0 ? snapshot.local : snapshot.agents.length ? snapshot.agents : [{ id: 'empty', label: snapshot.owner ? 'No host observations yet' : 'Owner sign-in required', detail: 'Agents management is owner-authorized. Inventory comes from privately advertising hosts, NOT an independent relay agent catalog. Empty/offline observations do not prove agents stopped. Local Host and offline drafts do not require sign-in.' }], actions, selected);
screen.notice(snapshot.status);
if (scope === 1 && !snapshot.owner) actions.push({ label: 'New private instruction draft…', run: async () => { const name = await screen.input('New private instruction draft · 1 of 2\nName', '', false, false, v => v.trim() ? '' : 'Name required.'); if (name === undefined) return; const instructions = await screen.input(`Draft: ${name}\nLocal only — not published or applied`, '', false, true, v => v.trim() ? '' : 'Instructions required.'); if (instructions !== undefined) await request('draft', { name, instructions }); } });
actions.push({ label: 'Quit manager (not Stop)', run: () => screen.close() });
screen.show(vanished ? [{ id: selected, label: 'Selection no longer available', detail: 'Selection no longer available. Choose a current observation; no operation is targeted at a replacement row.' }, ...rows] : scope === 0 ? snapshot.local : snapshot.agents.length ? snapshot.agents : [{ id: 'empty', label: snapshot.owner ? 'No host observations yet' : 'Owner sign-in required', detail: 'Agents management is owner-authorized. Inventory comes from privately advertising hosts, NOT an independent relay agent catalog. Empty/offline observations do not prove agents stopped. Local Host and offline drafts do not require sign-in.' }], actions, selected);
if (lastStatus !== snapshot.status) { lastStatus = snapshot.status; screen.notice(snapshot.status); }
}
screen.onScope = value => { scope = value; selected = ''; render(); };
screen.onSelect = id => { selected = id; };
screen.onSelect = id => { if (selected !== id) { selected = id; render(); } };
const input = createInterface({ input: createReadStream('', { fd: 3 }) });
input.on('line', line => {
try {
const value = JSON.parse(line);
if (value.snapshot) { snapshot = value.snapshot; render(); }
if (value.complete === pending?.id) { const old = pending; pending = undefined; old?.resolve(); }
if (value.complete === pending?.id) { const old = pending; pending = undefined; screen.setPending(false); old?.resolve(); }
} catch { screen.notice('Controller response unavailable. Quit and inspect retained state.'); }
});
input.on('close', () => screen.close());
renderer.keyInput.on('keypress', key => {
if (key.name === 'escape' && pending) output.write(JSON.stringify({ cancel: pending.id }) + '\n');
});
screen.onCancel = () => { if (pending) output.write(JSON.stringify({ cancel: pending.id }) + '\n'); };
await screen.done;
output.end(JSON.stringify({ quit: true }) + '\n'); input.close();
process.exit(0);

View File

@@ -1,8 +1,9 @@
import { BoxRenderable, InputRenderable, ScrollBoxRenderable, SelectRenderable, TextRenderable, TextareaRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
import { RGBA, BoxRenderable, InputRenderable, ScrollBoxRenderable, SelectRenderable, TextRenderable, TextareaRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
import { stripVTControlCharacters } from 'node:util';
export type ManagerRow = { id: string; label: string; detail: string };
export type ManagerAction = { label: string; run: () => void | Promise<void> };
export type ManagerRow = { id: string; label: string; detail: string; evidence?: string };
export type ManagerAction = { label: string; disabled?: string; run: () => void | Promise<void> };
const fg = RGBA.defaultForeground(), bg = RGBA.defaultBackground();
const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
/** Presentation only: never imports credentials, configuration, transport or host code.
@@ -11,6 +12,7 @@ export class OpenTuiScreen {
private readonly root: BoxRenderable;
private readonly header: TextRenderable;
private readonly list: SelectRenderable;
private readonly listFrame: BoxRenderable;
private readonly detail: TextRenderable;
private readonly scroll: ScrollBoxRenderable;
private readonly actions: SelectRenderable;
@@ -23,6 +25,8 @@ export class OpenTuiScreen {
private owner = 'signed out';
private busy = false;
private closed = false;
private overlay?: BoxRenderable;
private compactOverlay = false;
private modal?: { cancel: () => void; key: (key: KeyEvent) => void; paste: (text: string) => void };
private finish!: () => void;
readonly done = new Promise<void>(resolve => { this.finish = resolve; });
@@ -30,13 +34,14 @@ export class OpenTuiScreen {
onSelect: (id: string) => void = () => {};
constructor(readonly renderer: CliRenderer) {
this.root = new BoxRenderable(renderer, { width: '100%', height: '100%', flexDirection: 'column', backgroundColor: '#101820' });
this.root = new BoxRenderable(renderer, { width: '100%', height: '100%', flexDirection: 'column', backgroundColor: bg });
renderer.root.add(this.root);
this.header = new TextRenderable(renderer, { height: 2, fg: '#72d9ef', onMouseDown: event => this.switchScope(event.x < renderer.width / 2 ? 0 : 1) });
this.header = new TextRenderable(renderer, { fg, height: 2, onMouseDown: event => { if (event.y !== 0) return; const end = this.scope === 0 ? 20 : 18; if (event.x >= 8 && event.x < end) this.switchScope(0); else if (event.x >= end + 3 && event.x < end + 3 + (this.scope === 1 ? 8 : 6)) this.switchScope(1); } });
this.root.add(this.header);
const body = new BoxRenderable(renderer, { flexGrow: 1, flexDirection: 'row', minHeight: 1 });
this.root.add(body);
this.list = new SelectRenderable(renderer, { width: '32%', height: '100%', showDescription: false, showScrollIndicator: true, selectedBackgroundColor: '#245272', focusedBackgroundColor: '#172d3b', onMouseDown: event => {
this.listFrame = new BoxRenderable(renderer, { width: 28, height: '100%', border: true, title: '[List]' }); body.add(this.listFrame);
this.list = new SelectRenderable(renderer, { backgroundColor: bg, textColor: fg, focusedBackgroundColor: bg, focusedTextColor: fg, selectedBackgroundColor: fg, selectedTextColor: bg, width: '100%', height: '100%', showDescription: false, showScrollIndicator: true, showSelectionIndicator: true, onMouseScroll: event => { if (event.scroll?.direction === 'up') this.list.moveUp(); else if (event.scroll?.direction === 'down') this.list.moveDown(); }, onMouseDown: event => {
this.focusIndex = 0; this.list.focus();
const delta = event.y - this.list.y;
// Pinned OpenTUI does not expose its scrolling offset. Never map a click
@@ -44,29 +49,24 @@ export class OpenTuiScreen {
if (this.rows.length <= this.list.height && delta >= 0) this.list.setSelectedIndex(Math.min(this.rows.length - 1, delta));
else this.notice('Scrolled list: use arrows to select the exact row. Mouse selection is not available in this build.');
} });
body.add(this.list);
this.listFrame.add(this.list);
this.scroll = new ScrollBoxRenderable(renderer, { flexGrow: 1, height: '100%', border: true, title: 'Details', scrollY: true, onMouseDown: () => { this.focusIndex = 1; this.scroll.focus(); } });
body.add(this.scroll);
this.detail = new TextRenderable(renderer, { width: '100%', content: 'No items.', selectable: false });
this.detail = new TextRenderable(renderer, { fg, width: '100%', content: 'No items.', selectable: false });
this.scroll.add(this.detail);
this.actions = new SelectRenderable(renderer, { height: 5, showDescription: false, showScrollIndicator: true, selectedBackgroundColor: '#245272', onMouseDown: event => {
this.focusIndex = 2; this.actions.focus();
const delta = event.y - this.actions.y;
if (this.commands.length <= this.actions.height && delta >= 0) {
this.actions.setSelectedIndex(Math.min(this.commands.length - 1, delta));
this.actions.selectCurrent();
} else this.notice('Scrolled actions: use arrows then Enter. Mouse activation is not available in this build.');
} });
this.actions = new SelectRenderable(renderer, { backgroundColor: bg, textColor: fg, focusedBackgroundColor: bg, focusedTextColor: fg, selectedBackgroundColor: fg, selectedTextColor: bg, height: 2, showDescription: false, showSelectionIndicator: true, options: [{name: 'Actions (F2)', description: ''}, {name: 'Inspect (F3)', description: ''}], onMouseDown: event => { this.focusIndex = 2; this.actions.focus(); this.actions.setSelectedIndex(Math.max(0, Math.min(1, event.y - this.actions.y))); this.actions.selectCurrent(); } });
this.root.add(this.actions);
this.status = new TextRenderable(renderer, { height: 2, fg: '#ffce75', content: 'Tab pane · arrows select · Enter action · F1 help · Ctrl-Q quit (not Stop)' });
this.status = new TextRenderable(renderer, { fg, height: 2, onMouseDown: () => this.outcome() });
this.root.add(this.status);
this.small = new TextRenderable(renderer, { position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', bg: '#101820', content: 'Terminal too small. Resize to 40 × 16. Nothing submitted. Ctrl-Q quits.', visible: false });
this.root.add(new TextRenderable(renderer, { fg, height: 1, content: 'F1 Keys F2 Actions F3 Inspect ^Q Quit' }));
this.small = new TextRenderable(renderer, { fg, position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', bg, content: 'Terminal too small. Resize to 40 × 16. Nothing submitted. Ctrl-Q quits.', visible: false });
renderer.root.add(this.small);
this.list.on('selectionChanged', (index: number) => {
const row = this.rows[index]; this.detail.content = clean(row?.detail ?? 'No items.'); this.scroll.scrollTo(0);
if (row) this.onSelect(row.id);
this.focusStyle(); if (row) this.onSelect(row.id);
});
this.actions.on('itemSelected', (index: number) => { void this.run(index); });
this.actions.on('itemSelected', (index: number) => { if (index === 0) this.drawer(); else this.inspect(); });
this.list.on('itemSelected', () => { if (this.narrow) { this.drilled = true; this.resize(); this.focusIndex = 1; this.scroll.focus(); } });
renderer.keyInput.on('keypress', key => this.key(key));
renderer.keyInput.on('paste', event => {
if (!this.modal) return;
@@ -75,27 +75,54 @@ export class OpenTuiScreen {
});
renderer.on('resize', () => this.resize());
renderer.on('destroy', () => this.close());
for (const widget of [this.list, this.scroll, this.actions]) { widget.on('focused', () => this.focusStyle()); widget.on('blurred', () => this.focusStyle()); }
this.list.focus(); this.heading(); this.resize();
}
private resize() { this.small.visible = this.renderer.width < 40 || this.renderer.height < 16; }
private heading() { this.header.content = `BEEHIVE ${this.scope === 0 ? '[Local Host]' : 'Local Host'} | ${this.scope === 1 ? '[Agents]' : 'Agents'}\nOwner · ${clean(this.owner)}`; }
private focusStyle() {
this.listFrame.title = `${this.list.focused ? '[List]' : 'List'} · ${this.list.getSelectedIndex() + 1}/${this.rows.length}`;
this.scroll.title = `${this.scroll.focused ? '[Details]' : 'Details'}${this.narrow ? ' · Esc back' : ''}`;
for (const widget of [this.list, this.actions]) { widget.selectedBackgroundColor = widget.focused ? fg : bg; widget.selectedTextColor = widget.focused ? bg : fg; }
}
private narrow = false;
private drilled = false;
private pending = false;
private lastNotice = '';
onCancel: () => void = () => {};
setPending(value: boolean) { this.pending = value; }
private resize() {
if (this.overlay && this.modal) { this.overlay.width = Math.min(72, this.renderer.width - 4); this.overlay.left = Math.floor((this.renderer.width - Math.min(72, this.renderer.width - 4)) / 2); this.overlay.height = this.compactOverlay ? Math.min(14, this.renderer.height - 4) : this.renderer.height - 4; }
this.small.visible = this.renderer.width < 40 || this.renderer.height < 16;
this.narrow = this.renderer.width < 88 || this.renderer.height < 24;
this.listFrame.visible = !this.narrow || !this.drilled;
this.scroll.visible = !this.narrow || this.drilled;
this.listFrame.width = this.narrow ? '100%' : Math.min(32, Math.max(24, Math.floor(this.renderer.width * .28)));
this.scroll.title = this.narrow ? '[Details] · Esc back' : 'Details';
}
private heading() { this.header.content = `BEEHIVE ${this.scope === 0 ? '[Local Host]' : 'Local Host'} | ${this.scope === 1 ? '[Agents]' : 'Agents'}\n${this.scope === 0 ? 'This computer · no owner sign-in needed' : 'Owner · ' + clean(this.owner)}`; }
setOwner(publicSuffix?: string) { this.owner = publicSuffix ? `${publicSuffix} · signed in · key saved here` : 'signed out'; this.heading(); }
private switchScope(scope: number) {
if (this.modal || this.busy || this.small.visible || this.closed) return;
this.scope = scope; this.heading(); this.onScope(scope);
this.scope = scope; this.drilled = false; this.resize(); this.heading(); this.onScope(scope);
}
private key(key: KeyEvent) {
if (key.ctrl && (key.name === 'q' || key.name === 'c')) { key.preventDefault(); this.close(); return; }
if (this.small.visible) { key.preventDefault(); return; }
if (key.name === 'escape' && this.pending) { key.preventDefault(); this.onCancel(); return; }
if (this.modal) { this.modal.key(key); return; }
if (key.name === 'escape' && this.drilled) { key.preventDefault(); this.drilled = false; this.resize(); this.focusIndex = 0; this.list.focus(); }
if (key.name === 'f2') { key.preventDefault(); this.drawer(); return; }
if (key.name === 'f3') { key.preventDefault(); this.inspect(); return; }
if (key.name === 'f4') { key.preventDefault(); this.outcome(); return; }
if (this.focusIndex === 0 && ['home','end','pageup','pagedown'].includes(key.name)) { key.preventDefault(); this.navigate(this.list, key.name); }
if (key.name === 'left' || key.name === 'right') { key.preventDefault(); this.switchScope(1 - this.scope); }
if (key.name === 'tab') { key.preventDefault(); this.focusIndex = (this.focusIndex + (key.shift ? 2 : 1)) % 3; [this.list, this.scroll, this.actions][this.focusIndex]!.focus(); }
if (key.name === 'f1') { key.preventDefault(); this.notice('Left/Right: Local Host / Agents · Tab: panes · arrows/PgDn/wheel: scroll · Enter: action · Esc: cancel form · Ctrl-Q: quit, never Stop'); }
if (key.name === 'tab') { key.preventDefault(); this.focusIndex = (this.focusIndex + (key.shift ? 2 : 1)) % 3; if (this.narrow && this.focusIndex === (this.drilled ? 0 : 1)) this.focusIndex = (this.focusIndex + (key.shift ? 2 : 1)) % 3; [this.list, this.scroll, this.actions][this.focusIndex]!.focus(); this.focusStyle(); }
if (key.name === 'f1') { key.preventDefault(); this.read('Help', 'F2 Actions · F3 public Inspect · F4 full Outcome · Left/Right: scope · Tab/Shift-Tab: panes · arrows/PgDn/Home/End/wheel: navigate · Enter: open detail/action · Esc: cancel pending first, then dismiss/back · Ctrl-Q: quit, never Stop. Scrolled list/action mouse selection unavailable: use keyboard. No new provider, catalog, publication or Restart capability. Quit before foreground CLI use.'); }
}
private async run(index: number) {
const action = this.commands[index];
if (!action || this.busy || this.modal || this.small.visible || this.closed) return;
private async run(index: number, captured?: ManagerAction) {
const action = captured ?? this.commands[index];
if (!action || this.pending || this.busy || this.modal || this.small.visible || this.closed) return;
if (action.disabled) { this.notice(action.disabled); return; }
this.busy = true; this.notice('Working… Esc cancels an open form. Quit is not Stop.');
try { await action.run(); }
catch { this.notice('Action failed. State may be unchanged or outcome unknown; inspect the operation before retrying.'); }
@@ -105,29 +132,73 @@ export class OpenTuiScreen {
if (this.closed) return;
const prior = selected ?? this.rows[this.list.getSelectedIndex()]?.id;
this.rows = rows; this.commands = actions;
this.list.options = rows.map(row => ({ name: clean(row.label), description: '', value: row.id }));
const options = rows.map(row => ({ name: clean(row.label), description: '', value: row.id }));
if (JSON.stringify(this.list.options) !== JSON.stringify(options)) this.list.options = options;
const index = Math.max(0, rows.findIndex(row => row.id === prior));
this.list.setSelectedIndex(index); this.detail.content = clean(rows[index]?.detail ?? 'No items.');
this.actions.options = actions.map(action => ({ name: clean(action.label), description: '' }));
this.list.setSelectedIndex(index); this.detail.content = clean(rows[index]?.detail ?? 'No items.'); this.focusStyle();
}
notice(text: string) { if (!this.closed) this.status.content = clean(text); }
notice(text: string) { if (!this.closed) { this.lastNotice = clean(text); this.status.content = this.lastNotice; } }
unavailable(feature: string) { this.notice(`${feature}: Not available in this build. No request made. Local Host and existing host observations remain separate.`); }
choose(title: string, names: string[]): Promise<string | undefined> {
if (this.modal || !names.length) return Promise.resolve(undefined);
return new Promise(resolve => {
const box = new BoxRenderable(this.renderer, { position: 'absolute', top: 2, left: Math.floor((this.renderer.width - Math.min(72, this.renderer.width - 4)) / 2), width: Math.min(72, this.renderer.width - 4), height: this.renderer.height - 4, border: true, title: title + ' · Esc cancel', backgroundColor: bg });
const list = new SelectRenderable(this.renderer, { backgroundColor: bg, textColor: fg, focusedBackgroundColor: bg, focusedTextColor: fg, selectedBackgroundColor: fg, selectedTextColor: bg, width: '100%', height: '100%', showDescription: false, showSelectionIndicator: true, options: names.map(name => ({ name: clean(name), description: '' })) });
this.overlay = box; this.compactOverlay = false; this.renderer.root.add(box); box.add(list); list.focus();
const finish = (name?: string) => { this.modal = undefined; box.destroyRecursively(); this.actions.focus(); resolve(name); };
list.on('itemSelected', (i: number) => finish(names[i]));
this.modal = { cancel: () => finish(), paste: () => {}, key: key => { if (key.name === 'escape') { key.preventDefault(); finish(); } else if (['home','end','pageup','pagedown'].includes(key.name)) { key.preventDefault(); this.navigate(list, key.name); } } };
});
}
private navigate(list: SelectRenderable, key: string) {
const n = list.options.length, current = list.getSelectedIndex();
list.setSelectedIndex(Math.max(0, Math.min(n - 1, key === 'home' ? 0 : key === 'end' ? n - 1 : current + (key === 'pageup' ? -1 : 1) * Math.max(1, list.height))));
}
inspect() { const row = this.rows[this.list.getSelectedIndex()]; this.read('Technical details · public evidence', row?.evidence ?? 'Inspect unavailable: no technical evidence for this row.'); }
private outcome() { this.read('Outcome · current attempt', this.lastNotice || 'No outcome yet.'); }
private read(title: string, content: string) {
if (this.modal || this.closed) return;
const box = new ScrollBoxRenderable(this.renderer, { position: 'absolute', top: 2, left: Math.floor((this.renderer.width - Math.min(72, this.renderer.width - 4)) / 2), width: Math.min(72, this.renderer.width - 4), height: this.renderer.height - 4, border: true, title: title + ' · Esc close', backgroundColor: bg, scrollY: true });
this.overlay = box; this.compactOverlay = false; this.renderer.root.add(box); box.add(new TextRenderable(this.renderer, { fg, content: clean(content), width: '100%', selectable: true })); box.focus();
const cancel = () => { this.modal = undefined; box.destroyRecursively(); [this.list, this.scroll, this.actions][this.focusIndex]!.focus(); };
this.modal = { cancel, paste: () => {}, key: key => { if (key.name === 'escape') { key.preventDefault(); cancel(); } } };
}
private drawer() {
if (this.modal || this.closed) return;
const commands = this.commands.slice();
const box = new BoxRenderable(this.renderer, { position: 'absolute', top: 2, left: Math.floor((this.renderer.width - Math.min(72, this.renderer.width - 4)) / 2), width: Math.min(72, this.renderer.width - 4), height: this.renderer.height - 4, border: true, title: '[Actions] · Esc close', backgroundColor: bg, flexDirection: 'column', padding: 1 });
this.overlay = box; this.compactOverlay = false; this.renderer.root.add(box);
box.add(new TextRenderable(this.renderer, { fg, content: clean(this.rows[this.list.getSelectedIndex()]?.label ?? 'No selection'), height: 2 }));
const reason = new TextRenderable(this.renderer, { fg, height: 4 });
const list = new SelectRenderable(this.renderer, { backgroundColor: bg, textColor: fg, focusedBackgroundColor: bg, focusedTextColor: fg, selectedBackgroundColor: fg, selectedTextColor: bg, flexGrow: 1, showDescription: false, showSelectionIndicator: true, showScrollIndicator: true, options: commands.map(a => ({ name: clean((a.disabled || this.pending ? '(unavailable) ' : '') + a.label), description: '' })), onMouseScroll: event => { if (event.scroll?.direction === 'up') list.moveUp(); else if (event.scroll?.direction === 'down') list.moveDown(); }, onMouseDown: () => this.notice('Actions: use arrows then Enter. Mouse activation is unavailable; no request sent.') });
box.add(list); box.add(reason); list.focus();
const explain = () => { reason.content = this.pending ? 'Working… Esc cancels waiting. Submitted remote work or OS writes may already have completed; inspect before retry.' : clean(commands[list.getSelectedIndex()]?.disabled || 'Enter opens action · Esc closes · arrows / PgDn scroll'); };
explain(); list.on('selectionChanged', explain);
const cancel = () => { this.modal = undefined; box.destroyRecursively(); [this.list, this.scroll, this.actions][this.focusIndex]!.focus(); };
list.on('itemSelected', (i: number) => { if (this.pending || commands[i]?.disabled) { explain(); return; } cancel(); void this.run(i, commands[i]); });
this.modal = { cancel, paste: () => {}, key: key => { if (key.name === 'escape') { key.preventDefault(); cancel(); } else if (['home','end','pageup','pagedown'].includes(key.name)) { key.preventDefault(); this.navigate(list, key.name); } } };
}
/** Secret bytes never enter an Input/Textarea, history, selection or text buffer.
* Single-use modal storage is cleared on every settlement, including quit. */
input(label: string, value = '', secret = false, multiline = false): Promise<string | undefined> {
input(label: string, value = '', secret = false, multiline = false, validate?: (value: string) => string, back?: (value: string) => void): Promise<string | undefined> {
if (this.closed || this.modal) return Promise.resolve(undefined);
return new Promise(resolve => {
const box = new BoxRenderable(this.renderer, { position: 'absolute', top: 2, left: 0, width: '100%', height: '80%', border: true, backgroundColor: '#172d3b', flexDirection: 'column', padding: 1, title: secret ? 'Hidden key input' : 'Edit' });
this.renderer.root.add(box);
box.add(new TextRenderable(this.renderer, { content: clean(label), flexShrink: 0 }));
const box = new BoxRenderable(this.renderer, { position: 'absolute', top: 2, left: Math.floor((this.renderer.width - Math.min(72, this.renderer.width - 4)) / 2), width: Math.min(72, this.renderer.width - 4), height: multiline ? this.renderer.height - 4 : Math.min(this.renderer.height - 4, 14), border: true, backgroundColor: bg, flexDirection: 'column', padding: 1, title: secret ? 'Hidden key input' : multiline ? 'Private draft · Ctrl-S saves' : clean(label.split('\n')[0]!).slice(0, 45) });
this.overlay = box; this.compactOverlay = false; this.renderer.root.add(box);
this.compactOverlay = !multiline;
const prompt = new ScrollBoxRenderable(this.renderer, { width: '100%', flexGrow: multiline ? 0 : 1, height: multiline ? 3 : undefined, scrollY: true });
prompt.add(new TextRenderable(this.renderer, { fg, content: clean(label), width: '100%' })); box.add(prompt);
let hidden = secret ? value : '';
const entry = secret ? undefined : multiline
? new TextareaRenderable(this.renderer, { initialValue: value, flexGrow: 1, width: '100%' })
: new InputRenderable(this.renderer, { value, maxLength: 8192, width: '100%' });
if (entry) { box.add(entry); entry.focus(); }
else { this.list.blur(); this.actions.blur(); this.scroll.blur(); box.add(new TextRenderable(this.renderer, { content: 'Input hidden — no characters or length displayed.', height: 2 })); }
box.add(new TextRenderable(this.renderer, { content: multiline ? 'Ctrl-S save · Esc cancel (unsaved text discarded)' : 'Enter accept · Esc cancel', height: 2 }));
: new InputRenderable(this.renderer, { value, maxLength: 8192, width: '100%', backgroundColor: fg, textColor: bg, focusedBackgroundColor: fg, focusedTextColor: bg });
if (entry) { const field = new BoxRenderable(this.renderer, { border: !multiline, height: multiline ? undefined : 3, flexShrink: 0, flexGrow: multiline ? 1 : 0, width: '100%' }); field.add(entry); box.add(field); entry.focus(); }
else { this.list.blur(); this.actions.blur(); this.scroll.blur(); box.add(new TextRenderable(this.renderer, { fg, content: 'Input hidden — no characters or length displayed.', height: 2 })); }
box.add(new TextRenderable(this.renderer, { fg, content: multiline ? 'Ctrl-S save · Esc cancel (unsaved text discarded)' : back ? 'Enter next · Shift-Tab back · Esc cancel' : 'Enter accept · Esc cancel · Ctrl-↑/↓ text', height: 2 }));
const error = new TextRenderable(this.renderer, { fg, height: 2, flexShrink: 0 }); box.add(error);
let settled = false;
const finish = (answer?: string) => {
if (settled) return; settled = true; hidden = '';
@@ -141,9 +212,11 @@ export class OpenTuiScreen {
cancel: () => finish(),
paste: text => { if (secret && hidden.length + text.length <= 4096) hidden += text.replace(/[\r\n]/g, ''); },
key: key => {
if (key.ctrl && ['up','down'].includes(key.name)) { key.preventDefault(); prompt.scrollBy(key.name === 'up' ? -3 : 3); return; }
if (back && !secret && key.name === 'tab' && key.shift) { key.preventDefault(); back(entry instanceof InputRenderable ? entry.value : entry?.plainText ?? ''); finish('\0back'); return; }
if (key.name === 'escape') { key.preventDefault(); finish(); return; }
if ((!multiline && key.name === 'return') || (multiline && key.ctrl && key.name === 's')) {
key.preventDefault(); finish(secret ? hidden : entry instanceof InputRenderable ? entry.value : entry?.plainText); return;
key.preventDefault(); const answer = secret ? hidden : entry instanceof InputRenderable ? entry.value : entry?.plainText ?? ''; const invalid = validate?.(answer); if (invalid) { error.content = secret ? 'Invalid key. Re-enter.' : clean(invalid); if (secret) hidden = ''; return; } finish(answer); return;
}
if (!secret) return;
key.preventDefault();

View File

@@ -39,6 +39,9 @@ test('manager config save is real/keyless owner routing; retained configuration
await f.controller.request({ id: 1, action: 'configure', values: { owner, relay: 'wss://example.invalid' } });
assert.ok(existsSync(join(f.home,'.beehive','host','host-identity.json')));
assert.match(f.snapshot.status,/saved/);
assert.match(f.snapshot.local[0]!.detail, /Service: not checked/);
assert.ok(!f.snapshot.local[0]!.detail.includes('{'));
assert.match(f.snapshot.local[0]!.evidence!, /nonce/);
assert.equal(f.backend.read(credentialReference('owner',owner)),null);
await f.controller.request({ id: 2, action: 'configure', values: { owner, relay: 'wss://other.invalid' } });
assert.match(f.snapshot.status,/already exists/);
@@ -58,6 +61,10 @@ test('manager gates owner, exact selection/revision/freshness, unresolved operat
await request('signin',{ values: { owner, relay: 'wss://example.invalid', secret } }); assert.equal(f.snapshot.owner,owner);
const m = message('inventory','host-a','agent-a',3,{ observedAt: Date.now(), phase: 'stopped', actualRun: null, assignedHost: 'host-a', configurations: { default: {} }, selectedNext: {} });
f.receive(m);
assert.match(f.snapshot.agents[0]!.detail, /Actual run \(reported\)/);
assert.match(f.snapshot.agents[0]!.detail, /Selected for next Start/);
assert.ok(!f.snapshot.agents[0]!.detail.includes('{'));
assert.equal(f.snapshot.agents[0]!.disabled!.start, '');
const target = JSON.stringify(['host-a','agent-a']);
await request('start',{ target, revision: 2 }); assert.match(f.snapshot.status,/Selection changed/);
await request('start',{ target, revision: 3 }); assert.equal(f.submitted.length,1); assert.equal(f.submitted[0]!.type,'start');

View File

@@ -0,0 +1,16 @@
import { createTestRenderer } from '@opentui/core/testing';
import { OpenTuiScreen } from '../src/opentui-screen.ts';
import { writeFileSync } from 'node:fs';
const output = process.argv[2];
if (!output) throw Error('Pass existing output directory with trailing slash');
const ui = await createTestRenderer({ width: 100, height: 30, exitOnCtrlC: false });
const view = new OpenTuiScreen(ui.renderer);
const save = async (name: string) => { await ui.renderOnce(); writeFileSync(output + name + '.txt', ui.captureCharFrame()); writeFileSync(output + name + '-spans.json', JSON.stringify(ui.captureSpans())); };
try {
ui.mockInput.pressArrow('right');
view.show([{ id: 'synthetic-agent', label: 'Agent 12345678…abcdef', detail: 'Agent 12345678…abcdef\nHost abcdef12…123456\nUNKNOWN — stale or unreachable; last reported below\nReported phase: running\n\nActual run (reported)\nConfiguration: daily\nModel: Not reported\n\nSelected for next Start\nConfiguration: careful\nChoosing next does not change the actual run.', evidence: '{"syntheticPublicEvidence":true}' }], [{ label: 'Start agent', disabled: 'Host stale or unreachable. Refresh before acting.', run: () => { throw Error('Must never run'); } }, { label: 'Restart', disabled: 'Backend Restart may switch bindings. Use Stop, inspect accepted stopped report, then Start. No Restart request is sent.', run: () => { throw Error('Must never run'); } }]);
await save('fixture-agent');
view.setPending(true); view.notice('Working: synthetic request… Esc cancels waiting, not committed OS writes or remote work.');
ui.mockInput.pressKey('F2'); await save('fixture-pending');
ui.mockInput.pressKey('q', { ctrl: true });
} finally { view.close(); }

View File

@@ -15,6 +15,8 @@ test('actual OpenTUI selection, hidden entry, multiline save, resize and quit',
ui.mockInput.pressArrow('down');
await ui.renderOnce();
assert.equal(selected, 'b');
assert.ok(!ui.captureCharFrame().includes('Second detail'));
ui.mockInput.pressEnter(); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /Second detail/);
const secret = 'synthetic-secret-not-a-real-key';
const entered = view.input('Fixture secret', '', true);
@@ -41,3 +43,50 @@ test('actual OpenTUI selection, hidden entry, multiline save, resize and quit',
assert.equal(await quitting, undefined);
} finally { view.close(); }
});
test('drawer guards, persistent keys, pending Esc priority, list navigation and validated input', async () => {
const ui = await createTestRenderer({ width: 100, height: 30, exitOnCtrlC: false });
const view = new OpenTuiScreen(ui.renderer);
try {
let calls = 0, cancelled = 0, selected = '';
view.onSelect = id => { selected = id; };
view.onCancel = () => { cancelled++; };
view.show(Array.from({length: 25}, (_, i) => ({id: String(i), label: `Agent ${i}`, detail: `Summary ${i}`, evidence: `PUBLIC ${i}`})), [
{label: 'Start', disabled: 'Requires fresh stopped report.', run: () => { calls++; }},
...Array.from({length: 10}, (_, i) => ({label: `Action ${i}`, run: () => { calls++; }})),
]);
ui.mockInput.pressKey('HOME'); await ui.renderOnce();
await ui.mockMouse.scroll(4, 5, 'down'); await ui.renderOnce();
assert.equal(selected, '1');
ui.mockInput.pressKey('END'); await ui.renderOnce();
assert.equal(selected, '24');
await ui.mockMouse.click(4, 5); await ui.renderOnce();
assert.equal(selected, '24', 'scrolled click cannot select wrong row');
ui.mockInput.pressEnter(); assert.equal(calls, 0);
ui.mockInput.pressKey('F2'); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /\(unavailable\) Start/);
assert.match(ui.captureCharFrame(), /Requires fresh stopped report/);
ui.mockInput.pressEnter(); assert.equal(calls, 0);
ui.mockInput.pressKey('END'); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /Action 9/);
ui.mockInput.pressEscape(); await new Promise(resolve => setTimeout(resolve, 50));
view.setPending(true); view.notice('Working: synthetic pending request');
ui.mockInput.pressKey('F2'); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /Esc cancels waiting/);
assert.match(ui.captureCharFrame(), /F1 Keys F2 Actions F3 Inspect/);
ui.mockInput.pressEnter(); assert.equal(calls, 0);
ui.mockInput.pressEscape(); await new Promise(resolve => setTimeout(resolve, 50)); assert.equal(cancelled, 1);
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /\[Actions\]/);
view.setPending(false); ui.mockInput.pressEscape(); await new Promise(resolve => setTimeout(resolve, 50));
ui.mockInput.pressKey('F3'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /PUBLIC 24/);
ui.mockInput.pressEscape(); await new Promise(resolve => setTimeout(resolve, 50));
const input = view.input('Required public field', '', false, false, value => value ? '' : 'Required; enter a value.');
ui.mockInput.pressEnter(); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Required; enter a value/);
await ui.mockInput.typeText('retained public input');
ui.resize(60,24); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /retained public input/);
ui.mockInput.pressEnter(); assert.equal(await input, 'retained public input');
view.notice('FAILED: a long outcome\n' + 'Details\n'.repeat(40));
ui.mockInput.pressKey('F4'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /FAILED: a long outcome/);
assert.match(ui.captureCharFrame(), /F1 Keys F2 Actions F3 Inspect/);
} finally { view.close(); }
});