fix(ui): share Agent scope between selection and recovery

Generated-by: Codex
This commit is contained in:
AstroHan committed 2026-10-19 14:46:59 +11:00
1 parent f231aeb0fb
commit 68a62a5300
3 files changed
+33 -21

No files matched your search

@@ -58,22 +58,28 @@ function actions(dom: ReturnType<typeof installTranscriptDom>) {
};
}
test('unselected Agents retain sign-in and discovery retry without committing a selection', async () => {
test('new tasks can recover another Agent without changing their selection', async () => {
const dom = installTranscriptDom();
const setup: string[] = [];
let retries = 0;
let selectionWrites = 0;
try {
for (const readiness of ['authentication_required', 'unavailable'] as const) {
await dom.render(<LocaleProvider locale="en"><ComposerModelPicker label="Native" choices={choices}
executor={{ catalog: [{ ...catalog[0]!, readiness }],
onSelect() { assert.fail('recovery must not select an unavailable Agent'); },
onSetup: id => { setup.push(id!); }, onRetry: () => { retries++; }, onNewTask() {} }}
/></LocaleProvider>);
const { click } = actions(dom);
await click([...dom.document.querySelectorAll('button')].find(el => el.textContent === (readiness === 'unavailable' ? 'Retry' : 'Go to sign in')));
for (const selected of [false, true]) {
await dom.render(<LocaleProvider locale="en"><ComposerModelPicker label="Native" choices={choices}
executor={{ catalog: [{ ...catalog[0]!, readiness }, { ...catalog[0]!, id: 'ready', displayName: 'Ready Agent' }],
selection: selected ? { executorId: 'ready', configuration: { model: 'opaque-high' } } : undefined,
onSelect() { selectionWrites++; },
onSetup: id => { setup.push(id!); }, onRetry: () => { retries++; }, onNewTask() {} }}
/></LocaleProvider>);
const { click, trigger } = actions(dom);
await click([...dom.document.querySelectorAll('button')].find(el => el.textContent === (readiness === 'unavailable' ? 'Retry' : 'Go to sign in')));
assert.equal(trigger()?.textContent, selected ? 'Ready Agent · Family' : 'Native');
}
}
assert.deepEqual(setup, ['agent']);
assert.equal(retries, 1);
assert.deepEqual(setup, ['agent', 'agent']);
assert.equal(retries, 2);
assert.equal(selectionWrites, 0);
} finally { await dom.cleanup(); }
});
@@ -266,12 +272,13 @@ test('fixed external sessions show only their executor and failed writes retain
const dom = installTranscriptDom();
try {
await dom.render(<LocaleProvider locale="en"><ComposerModelPicker label="Native" choices={choices}
executor={{ catalog, selection: { executorId: 'agent', configuration: { model: 'opaque-high' } }, fixed: true,
executor={{ catalog: [...catalog, { ...catalog[0]!, id: 'other', displayName: 'Other Agent', readiness: 'authentication_required' }], selection: { executorId: 'agent', configuration: { model: 'opaque-high' } }, fixed: true,
onSelect: async () => { throw Error('offline'); }, onSetup() {}, onRetry() {}, onNewTask() {} }}
/></LocaleProvider>);
const { click, option, trigger } = actions(dom);
await click(trigger());
assert.equal(dom.document.querySelector('[role="group"][aria-label="OpenAI"]'), null);
assert.deepEqual([...dom.document.querySelectorAll('[role="group"]')].map(element => element.getAttribute('aria-label')), ['Agent']);
assert.equal([...dom.document.querySelectorAll('button')].some(element => element.textContent === 'Go to sign in'), false);
await click(option('Other'));
assert.equal(trigger()?.textContent, 'Family');
assert.match(dom.document.querySelector('[role="status"]')?.textContent ?? '', /failed/);
+4 -7
View File
@@ -220,9 +220,10 @@ export function ComposerModelPicker(props: {
const copy = executorCopy(locale);
const executor = props.executor;
const contribution = props.contribution;
const catalog = executor?.catalog.filter(entry => entry.id !== contribution?.id && (!executor.fixed || entry.id === executor.selection?.executorId)) ?? [];
const contributed = contribution?.id === props.executorTarget?.executorId ? contribution : undefined;
const contributedModel = contributed?.models.find(model => model.id === props.executorTarget?.model);
const selected = !contributed ? executor?.catalog.find(entry => entry.id === executor.selection?.executorId) : undefined;
const selected = catalog.find(entry => entry.id === executor?.selection?.executorId);
const selectedModel = executor?.selection?.configuration.model ?? selected?.currentModel;
const group = selected && executorModelGroup(selected, selectedModel);
const modelLabel = contributed ? contributedModel?.label ?? props.executorTarget?.model ?? copy.default : executor?.selection
@@ -266,9 +267,7 @@ export function ComposerModelPicker(props: {
return { value, label: model.label, icon: providerMarkIcon(model.providerType, props.renderProviderMark) };
}) });
}
for (const entry of executor?.catalog ?? []) {
if (entry.id === contribution?.id) continue;
if (executor?.fixed && entry.id !== executor.selection?.executorId) continue;
for (const entry of catalog) {
options.push({
type: 'section',
title: entry.displayName,
@@ -308,9 +307,7 @@ export function ComposerModelPicker(props: {
executor?.onSelect({ executorId: executor.selection!.executorId, configuration: executor.fixed || (configuration.model !== undefined && configuration.model !== selectedModel)
? configuration : { ...executor.selection!.configuration, ...configuration } });
const mode = executor?.selection?.configuration.mode ?? selected?.currentMode;
const recoveryEntries = executor?.selection
? selected && selected.readiness !== 'ready' ? [selected] : []
: executor?.catalog.filter(entry => entry.readiness === 'authentication_required' || entry.readiness === 'unavailable') ?? [];
const recoveryEntries = catalog.filter(entry => entry.readiness !== 'ready');
return <>
{/* Keep portaled search clicks from focusing the composer editor. */}
<Stack direction="horizontal" className="maka-model-picker-root"
+10 -2
View File
@@ -173,14 +173,22 @@ export const CodexAndAcp: Story = {
},
};
// Real path: a new task discovers an ACP Agent that requires sign-in, before any selection.
// Real path: a new task selects a ready ACP Agent while another configured Agent requires sign-in.
export const UnselectedUnavailableAgentNarrow: Story = {
render: () => <PickerFrame catalog={[{ ...CATALOG[0]!, readiness: 'authentication_required' }]} />,
render: () => <PickerFrame catalog={[CATALOG[0]!, {
id: 'claude', displayName: 'Claude Agent', readiness: 'authentication_required',
models: [], supportsModelChange: false, supportsAttachments: false,
}]} />,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByRole('button', { name: /Go to sign in|前往登录/ })).toBeVisible();
const label = canvasElement.querySelector<HTMLElement>('.maka-composer-model-label')!;
await waitFor(() => expect(label.scrollWidth).toBeLessThanOrEqual(label.clientWidth + 1));
const page = await openModels(canvasElement);
await userEvent.click(page.getByRole('option', { name: /Gemini 3.8 Flash/ }));
await waitFor(() => expect(canvasElement.querySelector('.maka-model-switcher-trigger')).toHaveTextContent('Antigravity · Gemini 3.8 Flash'));
await expect(canvas.getByRole('button', { name: /Go to sign in|前往登录/ })).toBeVisible();
await expect(canvasElement.querySelector('.maka-executor-notice')).toHaveTextContent('Claude Agent');
await openModels(canvasElement);
},
};