mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
fix(beehive): expose provider results at minimum terminal size
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -43,7 +43,7 @@ export class ProviderPanel {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const action = new TextRenderable(renderer, { left: 2, position: 'absolute', height: 1, fg: palette.text, onMouseDown: () => { if (!this.active || this.dialog) return; this.actionIndex = this.actionOffset + i; this.state.focus = 'detail'; void this.run(); } }); this.detail.add(action); this.actions.push(action);
|
||||
}
|
||||
this.unsubscribe = client.subscribe(snapshot => { this.snapshot = snapshot; this.dialog?.updateSecretLength(snapshot.secretLength); this.paint(); });
|
||||
this.unsubscribe = client.subscribe(snapshot => { if (this.snapshot.message !== snapshot.message) this.detailOffset = 0; this.snapshot = snapshot; this.dialog?.updateSecretLength(snapshot.secretLength); this.paint(); });
|
||||
}
|
||||
private items() { return [...this.snapshot.rows.map(row => row.id), 'add', 'reload']; }
|
||||
private row() { return this.snapshot.rows.find(row => row.id === this.selected); }
|
||||
@@ -128,7 +128,7 @@ export class ProviderPanel {
|
||||
if (!this.active || this.dialog) return;
|
||||
if (this.state.focus === 'list') this.select(y - 6 + this.offset);
|
||||
else {
|
||||
const count = Math.min(this.commands().length, this.renderer.height < 28 ? 3 : 7);
|
||||
const count = Math.min(this.commands().length, this.renderer.height < 28 ? 2 : 7);
|
||||
const first = this.renderer.height - 5 - count - 4;
|
||||
const index = y - 3 - first + this.actionOffset;
|
||||
if (index >= 0 && index < this.commands().length) { this.actionIndex = index; void this.run(); }
|
||||
@@ -166,12 +166,12 @@ export class ProviderPanel {
|
||||
const row = this.row();
|
||||
this.detailTitle.content = row ? row.name.toUpperCase() : this.selected === 'add' ? 'ADD PROVIDER' : 'RELOAD PROVIDERS'; this.detailTitle.width = width;
|
||||
const commands = this.commands();
|
||||
const actionCapacity = Math.min(commands.length, this.renderer.height < 28 ? 3 : 7);
|
||||
const actionCapacity = Math.min(commands.length, this.renderer.height < 28 ? 2 : 7);
|
||||
if (this.actionIndex < this.actionOffset) this.actionOffset = this.actionIndex;
|
||||
if (this.actionIndex >= this.actionOffset + actionCapacity) this.actionOffset = this.actionIndex - actionCapacity + 1;
|
||||
const actionStart = height - actionCapacity - 4;
|
||||
const info = row ? [`State ${row.state}`, `Type ${row.type}`, `Connects to ${row.endpoint}`, '', row.detail, ...(this.snapshot.modelProvider === row.id ? ['', 'MODELS', ...this.snapshot.models] : [])] : this.selected === 'add' ? ['Choose OpenAI, Anthropic, OpenAI-compatible, OpenRouter, or Databricks v2.', '', 'Credentials stay in the OS credential store.'] : ['Reload saved providers and the DATABRICKS_HOST workspace. No owner sign-in is required.'];
|
||||
info.push('', 'RESULT', this.snapshot.message);
|
||||
if (this.snapshot.message) info.unshift('RESULT', this.snapshot.message, '');
|
||||
const wrapped = info.flatMap(line => { const result: string[] = []; for (let start = 0; start < Math.max(1, line.length); start += width) result.push(line.slice(start, start + width)); return result; });
|
||||
this.detailOffset = Math.min(this.detailOffset, Math.max(0, wrapped.length - (actionStart - 4)));
|
||||
this.details.width = width; this.details.height = Math.max(1, actionStart - 4); this.details.content = wrapped.slice(this.detailOffset, this.detailOffset + Math.max(1, actionStart - 4)).join('\n');
|
||||
|
||||
28
beehive/test/provider-ipc.test.ts
Normal file
28
beehive/test/provider-ipc.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { fork } from 'node:child_process';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { once } from 'node:events';
|
||||
import { readSettings, saveSettings } from '../src/settings.ts';
|
||||
|
||||
test('real fixture IPC rejects stale Add revision without a public settings mutation', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'provider-tui-fixture-cas-'));
|
||||
const child = fork(new URL('./provider-tui-fixture-child.ts', import.meta.url), [], { execArgv: ['--experimental-transform-types'], silent: true, env: { HOME: home, BEEHIVE_HOME: home, PATH: '/usr/bin:/bin' } });
|
||||
child.stdout?.resume(); child.stderr?.resume();
|
||||
try {
|
||||
await once(child, 'message');
|
||||
const directory = join(home, '.beehive', 'host');
|
||||
saveSettings(directory, readSettings(directory), 0);
|
||||
const completion = new Promise<any>((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(Error('IPC timeout')), 5000);
|
||||
child.on('message', (message: any) => { if (message.type === 'result') { clearTimeout(timer); resolve(message); } });
|
||||
});
|
||||
child.send({ type: 'secret', action: 'append', value: 'fixture-secret' });
|
||||
child.send({ type: 'request', id: 1, request: { action: 'save', revision: 0, values: { type: 'openai', name: 'Stale' } } });
|
||||
assert.equal((await completion).ok, false);
|
||||
assert.equal(readSettings(directory).revision, 1);
|
||||
assert.deepEqual(readSettings(directory).providers, []);
|
||||
} finally { const exited = once(child, 'close'); child.kill(); await exited; rmSync(home, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -9,7 +9,7 @@ const keys = new Map<string, string>();
|
||||
const backend: ProviderCredentials = { read: ref => keys.get(ref.account) ?? null, create: (ref, value) => { keys.set(ref.account, value); } };
|
||||
const service = new ProviderService(home, process.env, {
|
||||
credential: async (input: any, signal) => {
|
||||
if (input.action === 'add-provider') addProvider(directory, input.name, input.secret, backend, input.type, input.endpoint, input.wire);
|
||||
if (input.action === 'add-provider') addProvider(directory, input.name, input.secret, backend, input.type, input.endpoint, input.wire, input.revision);
|
||||
else if (input.action === 'edit-provider') editProvider(directory, input.provider, input.revision, input.name, input.endpoint, input.wire, input.secret ?? '', backend);
|
||||
else if (input.action === 'models') {
|
||||
await new Promise(resolve => setTimeout(resolve, 400)); signal.throwIfAborted();
|
||||
|
||||
@@ -19,7 +19,7 @@ function fixture() {
|
||||
cancel() {}, dispose() {},
|
||||
};
|
||||
const inventory = new HarnessInventoryController({ read: () => ({ version: 1, revision: 0, agents: [], providers: [], runtimes: [] }), save: candidate => candidate }, async () => []);
|
||||
return { client, inventory, requests, secrets };
|
||||
return { client, inventory, requests, secrets, snapshot, emit() { for (const fn of listeners) fn(structuredClone(snapshot)); } };
|
||||
}
|
||||
for (const [width, height] of [[120, 40], [60, 20]]) test(`Providers signed-out keyboard actions and modal at ${width}x${height}`, async () => {
|
||||
const ui = await createTestRenderer({ width, height, exitOnCtrlC: false }), f = fixture();
|
||||
@@ -55,3 +55,21 @@ test('form input preserves q as text and cancellation clears secret without save
|
||||
dialog.cancel(); assert.equal(await dialog.done, undefined); assert.deepEqual(calls, ['clear']);
|
||||
} finally { ui.renderer.destroy(); }
|
||||
});
|
||||
|
||||
test('minimum-size results, recovery text and models remain reachable through real page keys', async () => {
|
||||
const ui = await createTestRenderer({ width: 60, height: 20, exitOnCtrlC: false }), f = fixture();
|
||||
const shell = new OpenTuiShell(ui.renderer, f.inventory, undefined, f.client);
|
||||
try {
|
||||
for (let i = 0; i < 3; i++) ui.mockInput.pressArrow('right');
|
||||
ui.mockInput.pressEnter(); ui.mockInput.pressArrow('up'); ui.mockInput.pressTab();
|
||||
f.snapshot.phase = 'error'; f.snapshot.message = 'Denied. Check provider access. Reload before retrying.'; f.emit();
|
||||
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Denied/); assert.match(ui.captureCharFrame(), /retrying/);
|
||||
ui.mockInput.pressKey('\x1b[6~'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /State/);
|
||||
f.snapshot.phase = 'idle'; f.snapshot.message = 'Models loaded.'; f.snapshot.modelProvider = 'p'; f.snapshot.models = ['gpt-visible']; f.emit();
|
||||
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Models loaded/);
|
||||
for (let i = 0; i < 12; i++) ui.mockInput.pressKey('\x1b[6~');
|
||||
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /gpt-visible/);
|
||||
f.snapshot.message = 'Stopped waiting. Changes may already be saved. Reload before retrying.'; f.emit();
|
||||
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Stopped waiting/);
|
||||
} finally { shell.close(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user