diff --git a/beehive/src/provider-panel.ts b/beehive/src/provider-panel.ts index 681bf25fa9..2e64248310 100644 --- a/beehive/src/provider-panel.ts +++ b/beehive/src/provider-panel.ts @@ -171,7 +171,8 @@ export class ProviderPanel { 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.']; - if (this.snapshot.message) info.unshift('RESULT', this.snapshot.message, ''); + const ownsResult = this.snapshot.resultTarget === this.selected; + if (ownsResult && 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'); @@ -183,8 +184,8 @@ export class ProviderPanel { action.bg = this.state.focus === 'detail' && this.actionIndex === commandIndex ? palette.selected : palette.surface; }); this.status.top = height - 3; this.status.width = width; this.status.height = 2; - this.status.content = this.snapshot.phase === 'busy' ? 'Working… Esc stops waiting' : `${this.snapshot.phase === 'error' ? 'FAILED · ' : ''}PgUp/PgDn details\n↑↓ actions · Enter run`; - this.status.fg = this.snapshot.phase === 'error' ? palette.failure : palette.muted; + this.status.content = ownsResult && this.snapshot.phase === 'busy' ? 'Working… Esc stops waiting' : `${ownsResult && this.snapshot.phase === 'error' ? 'FAILED · ' : ''}PgUp/PgDn details\n↑↓ actions · Enter run`; + this.status.fg = ownsResult && this.snapshot.phase === 'error' ? palette.failure : palette.muted; this.dialog?.paint(); } dispose() { this.generation++; this.dialog?.cancel(); this.unsubscribe(); this.client.dispose(); } diff --git a/beehive/src/provider-protocol.ts b/beehive/src/provider-protocol.ts index 8172ef10fe..82f715b23f 100644 --- a/beehive/src/provider-protocol.ts +++ b/beehive/src/provider-protocol.ts @@ -1,7 +1,7 @@ /** Public renderer/controller protocol. Snapshots contain no credential references or secrets. */ export type ProviderType = 'openai' | 'anthropic' | 'openai-compat' | 'openrouter' | 'databricks_v2'; export type ProviderRow = { id: string; name: string; type: ProviderType; endpoint: string; wire?: string; state: string; detail: string }; -export type ProviderSnapshot = { revision: number; rows: ProviderRow[]; phase: 'idle' | 'busy' | 'error'; message: string; secretLength: number; models: string[]; modelProvider?: string; databricksHost?: string; setup?: { id: string; label: string; state: string; reason: string; providers: string[] }[] }; +export type ProviderSnapshot = { revision: number; rows: ProviderRow[]; phase: 'idle' | 'busy' | 'error'; message: string; resultTarget?: string; secretLength: number; models: string[]; modelProvider?: string; databricksHost?: string; setup?: { id: string; label: string; state: string; reason: string; providers: string[] }[] }; export type ProviderRequest = { action: 'reload' | 'save' | 'test' | 'models' | 'login' | 'discover' | 'setup'; provider?: string; revision?: number; values?: Record }; export interface ProviderClient { snapshot(): ProviderSnapshot; diff --git a/beehive/src/provider-service.ts b/beehive/src/provider-service.ts index 98c90b890f..197b6b3bd2 100644 --- a/beehive/src/provider-service.ts +++ b/beehive/src/provider-service.ts @@ -45,7 +45,7 @@ export class ProviderService implements ProviderClient { async request(request: ProviderRequest) { if (this.active || this.disposed) return false; const abort = new AbortController(), generation = ++this.generation; - this.active = abort; this.current.phase = 'busy'; this.current.message = 'Working… Esc stops waiting; changes may already be saved.'; this.publish(); + this.active = abort; this.current.resultTarget = request.provider ?? (request.action === 'save' ? 'add' : 'reload'); this.current.phase = 'busy'; this.current.message = 'Working… Esc stops waiting; changes may already be saved.'; this.publish(); const check = () => { abort.signal.throwIfAborted(); if (generation !== this.generation || this.disposed) throw Error('Cancelled'); }; try { const before = readSettings(this.directory), values = request.values ?? {}; diff --git a/beehive/test/provider-service.test.ts b/beehive/test/provider-service.test.ts index 5bf94f7bcc..0d6b99a502 100644 --- a/beehive/test/provider-service.test.ts +++ b/beehive/test/provider-service.test.ts @@ -35,6 +35,7 @@ test('signed-out add, edit, models and Codex setup retain active credential refe const oldKey = provider.key; assert.equal(await f.service.request({ action: 'models', provider: provider.id, revision: settings.revision }), true); assert.deepEqual(f.service.snapshot().models, ['gpt-fixture']); + assert.equal(f.service.snapshot().resultTarget, provider.id); assert.equal(await f.service.request({ action: 'setup', provider: provider.id, revision: settings.revision, values: { harness: 'codex', model: 'gpt-fixture' } }), true); settings = readSettings(f.directory); f.service.secret('append', 'second-private-key'); @@ -90,5 +91,6 @@ test('cancel fences late model result and duplicate operation; secrets clear', a f.service.cancel(); finish({ models: ['late'] }); assert.equal(await pending, false); assert.deepEqual(f.service.snapshot().models, []); assert.match(f.service.snapshot().message, /may already be saved/); + assert.equal(f.service.snapshot().resultTarget, id); } finally { f.close(); } }); diff --git a/beehive/test/shell-providers.test.ts b/beehive/test/shell-providers.test.ts index f6d371f8c9..4ea79acfed 100644 --- a/beehive/test/shell-providers.test.ts +++ b/beehive/test/shell-providers.test.ts @@ -62,7 +62,7 @@ test('minimum-size results, recovery text and models remain reachable through re 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(); + 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/); f.snapshot.phase = 'idle'; f.snapshot.message = 'Models loaded.'; f.snapshot.modelProvider = 'p'; f.snapshot.models = ['gpt-visible']; f.emit(); @@ -73,3 +73,19 @@ test('minimum-size results, recovery text and models remain reachable through re await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Stopped waiting/); } finally { shell.close(); } }); + +test('success and failure reports belong only to their selected provider', async () => { + const ui = await createTestRenderer({ width: 60, height: 20, exitOnCtrlC: false }), f = fixture(); + f.snapshot.rows.push({ id: 'other', name: 'Other account', type: 'anthropic', endpoint: 'https://api.anthropic.com', state: 'SAVED', detail: 'Untested' }); + f.snapshot.resultTarget = 'p'; f.snapshot.message = 'OpenAI result only'; + 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'); await ui.renderOnce(); + assert.doesNotMatch(ui.captureCharFrame(), /OpenAI result only/); + ui.mockInput.pressArrow('up'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /OpenAI result only/); + f.snapshot.phase = 'error'; f.snapshot.message = 'OpenAI denied only'; f.emit(); + ui.mockInput.pressArrow('down'); await ui.renderOnce(); assert.doesNotMatch(ui.captureCharFrame(), /OpenAI denied only|FAILED/); + ui.mockInput.pressArrow('up'); await ui.renderOnce(); assert.match(ui.captureCharFrame(), /OpenAI denied only/); + } finally { shell.close(); } +});