From 20e2a6a5fcf93b152962cf04bfb30dbb7234fc03 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 21 Sep 2026 14:33:17 -0400 Subject: [PATCH] fix(beehive): keep compact provider status and recovery complete Signed-off-by: Logan Johnson --- beehive/src/provider-panel.ts | 27 ++++++++++++++++++++++----- beehive/test/shell-providers.test.ts | 24 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/beehive/src/provider-panel.ts b/beehive/src/provider-panel.ts index 03221488cf..16b9e99dc0 100644 --- a/beehive/src/provider-panel.ts +++ b/beehive/src/provider-panel.ts @@ -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)); diff --git a/beehive/test/shell-providers.test.ts b/beehive/test/shell-providers.test.ts index 0972a1d42b..cfedafcced 100644 --- a/beehive/test/shell-providers.test.ts +++ b/beehive/test/shell-providers.test.ts @@ -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(); } +});