mirror of
https://github.com/block/buzz.git
synced 2026-11-09 10:51:42 +00:00
fix(beehive): bind provider results to their selected account
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -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(); }
|
||||
|
||||
@@ -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<string, string> };
|
||||
export interface ProviderClient {
|
||||
snapshot(): ProviderSnapshot;
|
||||
|
||||
@@ -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 ?? {};
|
||||
|
||||
@@ -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(); }
|
||||
});
|
||||
|
||||
@@ -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(); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user