fix(beehive): keep compact provider status and recovery complete

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-21 14:33:17 -04:00
parent f130790ea2
commit 20e2a6a5fc
2 changed files with 45 additions and 6 deletions

View File

@@ -112,7 +112,7 @@ export class ProviderPanel {
if (type === 'openai-compat') fields.push({ label: 'Endpoint', value: row?.endpoint ?? 'https://' }, { label: 'Wire', value: row?.wire ?? 'auto', choices: ['auto', 'chat', 'responses'] });
fields.push({ label: row ? 'API key (blank keeps saved)' : 'API key', secret: true });
}
const result = await this.form(row ? 'EDIT PROVIDER' : 'ADD PROVIDER', fields);
const result = await this.form(row ? 'EDIT PROVIDER' : 'CONFIGURE PROVIDER', fields);
if (!result || generation !== this.generation) return;
await this.client.request({ action: 'save', provider: row?.id ?? `type:${type}`, revision, values: { type, name: result.Name!, endpoint: result['Workspace (DATABRICKS_HOST)'] ?? result.Endpoint ?? '', wire: result.Wire ?? 'auto' } });
}
@@ -165,14 +165,27 @@ export class ProviderPanel {
const commands = this.commands();
const compact = this.renderer.height < 28;
const ownsResult = this.snapshot.resultTarget === this.selected;
const info = row ? [`State ${row.state}${row.modelCount === undefined ? '' : ` · ${row.modelCount} models`}`, `Type ${row.type}`, ...(row.endpoint ? [`Connects to ${row.endpoint}`] : []), '', row.detail] : ['Reload saved providers and check their model catalogs. No owner sign-in is required.'];
const wrap = (lines: string[]) => lines.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; });
const modelCount = row?.modelCount === undefined ? '' : `${row.modelCount} ${row.modelCount === 1 ? 'model' : 'models'}`;
const info = row ? [`State ${row.state}${modelCount ? ` · ${modelCount}` : ''}`, `Type ${row.type}`, ...(row.endpoint ? [`Connects to ${row.endpoint}`] : []), '', row.detail] : ['Reload saved providers and check their model catalogs. No owner sign-in is required.'];
const wrap = (lines: string[]) => lines.flatMap(line => {
const result: string[] = [];
while (line.length > width) {
const space = line.lastIndexOf(' ', width);
const end = space > 0 ? space : width;
result.push(line.slice(0, end)); line = line.slice(end).trimStart();
}
result.push(line); return result;
});
const fullDetails = wrap(info);
// Reserve every action and a result viewport before allocating detail rows.
this.detailTitle.top = 1;
const detailTop = compact ? 2 : 3;
const detailCapacity = compact ? 2 : Math.max(1, height - commands.length - 10);
const summary = compact ? wrap(row ? [`${row.state}${row.modelCount === undefined ? '' : ` · ${row.modelCount} models`}`, row.detail] : info).slice(0, detailCapacity) : fullDetails.slice(0, detailCapacity);
const compactSummary = row ? [
`${row.state}${modelCount ? ` · ${modelCount}` : ''}`,
row.state === 'Connected' ? 'Execution not tested' : row.state === 'Failed' ? 'Catalog unavailable' : row.state === 'Checking…' ? 'Checking model access' : row.id.startsWith('type:') ? 'Select Configure to get started' : 'Model access not checked',
] : ['Reload saved accounts', 'Check model access'];
const summary = compact ? compactSummary : fullDetails.slice(0, detailCapacity);
this.details.top = detailTop; this.details.width = width; this.details.height = summary.length; this.details.content = summary.join('\n');
this.actionTitle.top = detailTop + summary.length + (compact ? 0 : 1); this.actionTitle.width = width;
this.actionTitle.content = 'AVAILABLE ACTIONS ' + '─'.repeat(Math.max(1, width - 18));
@@ -186,9 +199,13 @@ export class ProviderPanel {
const resultTop = this.actionStart + commands.length;
const resultCapacity = Math.max(1, height - 2 - resultTop);
this.resultPageSize = resultCapacity;
const recovery = row?.state === 'Failed' ? row.type === 'databricks_v2'
? 'Check workspace and network. Sign in to Databricks, then Refresh models.'
: 'Check API key, endpoint, network and model access. Then Refresh models.' : undefined;
const output = [
...(recovery ? [recovery] : []),
...(ownsResult && this.snapshot.message ? [`${this.snapshot.phase === 'error' ? 'FAILED' : this.snapshot.phase === 'busy' ? 'WORKING' : 'RESULT'}: ${this.snapshot.message}`] : []),
...(compact || fullDetails.length > detailCapacity ? ['DETAILS', ...info] : []),
...(compact ? row ? [`Type ${row.type}`, ...(row.endpoint ? [`Connects to ${row.endpoint}`] : [])] : [] : fullDetails.length > detailCapacity ? ['DETAILS', ...info] : []),
];
const wrapped = wrap(output);
this.detailOffset = Math.min(this.detailOffset, Math.max(0, wrapped.length - resultCapacity));

View File

@@ -64,7 +64,7 @@ test('minimum-size results and recovery text remain reachable; only the model co
ui.mockInput.pressEnter(); ui.mockInput.pressTab();
f.snapshot.resultTarget = 'p'; 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/);
ui.mockInput.pressKey('\x1b[6~'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Type/);
f.snapshot.phase = 'idle'; f.snapshot.message = 'Models loaded.'; f.snapshot.rows[0]!.state = 'Connected'; f.snapshot.rows[0]!.modelCount = 2; f.emit();
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Models loaded/);
for (let i = 0; i < 12; i++) ui.mockInput.pressKey('\x1b[6~');
@@ -248,3 +248,25 @@ for (const [width, height] of [[120, 40], [60, 20]]) test(`provider inventory ha
assert.deepEqual(f.snapshot, before);
} finally { shell.close(); }
});
for (const [width, height] of [[120, 40], [60, 20]]) test(`provider status is complete before scrolling at ${width}x${height}`, async () => {
const ui = await createTestRenderer({ width, height, exitOnCtrlC: false }), f = fixture();
f.snapshot.rows[0]!.state = 'Connected'; f.snapshot.rows[0]!.modelCount = 1;
f.snapshot.rows[0]!.detail = 'Model catalog reachable. Model execution has not been tested.';
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(); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /Connected · 1 model/);
assert.doesNotMatch(ui.captureCharFrame(), /1 models/);
if (height === 20) assert.match(ui.captureCharFrame(), /Execution not tested/);
f.snapshot.rows[0]!.state = 'Failed'; delete f.snapshot.rows[0]!.modelCount;
f.snapshot.rows[0]!.detail = 'Could not load models. Check the API key, endpoint, network and model access, then retry Refresh models.';
f.snapshot.resultTarget = 'p'; f.snapshot.phase = 'error'; f.snapshot.message = 'Provider saved. Existing runs are unchanged. Model catalog failed.'; f.emit();
await ui.renderOnce(); const frame = ui.captureCharFrame();
assert.match(frame, /Check API key, endpoint/); assert.match(frame.replace(/[│\s]+/g, ' '), /Then Refresh models/);
if (height === 20) { assert.match(frame, /Catalog unavailable/); assert.doesNotMatch(frame, /Check th|DETAILS/); }
ui.mockInput.pressArrow('down'); ui.mockInput.pressTab(); ui.mockInput.pressEnter(); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /CONFIGURE PROVIDER/); assert.doesNotMatch(ui.captureCharFrame(), /ADD PROVIDER/);
} finally { shell.close(); }
});