Show Linear's tools and the Knowledge choice on one screen after signing in

An MCP preset that also syncs asked about Knowledge first and showed its
tools on a separate summary. After signing in it now shows the tools and
their permissions, then Sync into Knowledge underneath; Done or Add to
Knowledge closes the wizard, with a toast saying what syncs.
This commit is contained in:
arc53-machine committed 2026-09-29 13:32:12 +01:00
1 parent 85175fa8c1
commit 7daa3eb972
2 files changed
+94 -34

No files matched your search

+37 -28
View File
@@ -100,6 +100,9 @@ vi.mock('./LinearPicker', async (importOriginal) => {
};
});
import actionToastReducer, {
selectActionToast,
} from '../notifications/actionToastSlice';
import notificationsReducer, {
sseEventReceived,
} from '../notifications/notificationsSlice';
@@ -231,6 +234,7 @@ describe('ConnectWizard', () => {
reducer: {
connectors: connectorsReducer,
notifications: notificationsReducer,
actionToast: actionToastReducer,
preference: (state = { token: null, selectedDocs: [] }) => state,
conversation: (state = {}) => state,
},
@@ -881,7 +885,7 @@ describe('ConnectWizard', () => {
publisher: 'preset',
};
const signIn = async (props = {}) => {
const signIn = async (props = {}, onClose = vi.fn()) => {
const popup = { closed: false, close: vi.fn(), location: { href: '' } };
const open = vi.spyOn(window, 'open').mockReturnValue(popup as never);
mcpApi.testMCPConnection.mockResolvedValue({
@@ -905,7 +909,7 @@ describe('ConnectWizard', () => {
success: true,
connection: { tools: [{ ...TELEGRAM_TOOL, display_name: 'Linear' }] },
});
const store = await render(linear, vi.fn(), props);
const store = await render(linear, onClose, props);
await click('settings.connectors.wizard.signIn');
await act(async () => {
store.dispatch(
@@ -918,19 +922,28 @@ describe('ConnectWizard', () => {
);
});
open.mockRestore();
return store;
};
it('signs in once, then picks teams to sync with the same connection', async () => {
// The tools come first on the one screen after signing in.
const toolsFirst = () => {
const text = document.body.textContent ?? '';
const tools = text.indexOf('settings.connectors.wizard.toolsHeading:1');
const knowledge = text.indexOf(
'settings.connectors.wizard.syncToKnowledge',
);
return tools >= 0 && (knowledge < 0 || tools < knowledge);
};
it('signs in once, then shows its tools and picks teams to sync on one screen', async () => {
service.setup.mockResolvedValue({
success: true,
tools: [],
sources: [{ id: 'src-1', name: 'Linear · Engineering' }],
});
await signIn({ purpose: 'knowledge' });
// The tools came with the sign-in; syncing is the next question.
expect(document.body.textContent).toContain(
'settings.connectors.wizard.chooseWhatToSync',
);
const onClose = vi.fn();
const store = await signIn({ purpose: 'knowledge' }, onClose);
expect(toolsFirst()).toBe(true);
await click('pick-team');
await click('modals.uploadDoc.train');
const [id, body] = service.setup.mock.calls[0];
@@ -949,14 +962,18 @@ describe('ConnectWizard', () => {
config: optionsToConfig(DEFAULT_RETRIEVAL_OPTIONS),
},
});
// The summary still lists the tools from the sign-in.
expect(document.body.textContent).toContain(
'settings.connectors.wizard.toolsHeading:1',
);
// Nothing more to show: the modal closes and says what syncs.
expect(onClose).toHaveBeenCalled();
expect(
selectActionToast(
store.getState() as Parameters<typeof selectActionToast>[0],
)?.variant,
).toBe('success');
});
it('can skip syncing and keep only the tools', async () => {
await signIn({ purpose: 'knowledge' });
const onClose = vi.fn();
await signIn({ purpose: 'knowledge' }, onClose);
const train = Array.from(
document.body.querySelectorAll<HTMLButtonElement>('button'),
).find((b) => b.textContent?.trim() === 'modals.uploadDoc.train');
@@ -964,26 +981,18 @@ describe('ConnectWizard', () => {
expect(train?.disabled).toBe(true);
await click('settings.connectors.wizard.skip');
expect(service.setup).not.toHaveBeenCalled();
expect(document.body.textContent).toContain(
'settings.connectors.wizard.doneTitle',
);
expect(document.body.textContent).toContain(
'settings.connectors.wizard.toolsHeading:1',
);
expect(onClose).toHaveBeenCalled();
});
it('asks before syncing on a plain connect, and keeps the tools', async () => {
await signIn();
it('asks about Knowledge under its tools on a plain connect', async () => {
const onClose = vi.fn();
await signIn({}, onClose);
expect(toolsFirst()).toBe(true);
expect(knowledgeSwitch()!.getAttribute('aria-checked')).toBe('false');
expect(document.body.textContent).not.toContain('pick-team');
await click('settings.connectors.wizard.continue');
await click('settings.connectors.wizard.done');
expect(service.setup).not.toHaveBeenCalled();
expect(document.body.textContent).toContain(
'settings.connectors.wizard.toolsHeading:1',
);
expect(document.body.textContent).toContain(
'settings.connectors.wizard.syncLater',
);
expect(onClose).toHaveBeenCalled();
});
it('goes straight to the summary after signing in again', async () => {
+57 -6
View File
@@ -23,6 +23,7 @@ import { Button } from '../components/ui/button';
import { FormField } from '../components/ui/form-field';
import { Input } from '../components/ui/input';
import { Modal, ModalActions } from '../components/ui/modal';
import { SectionHeader } from '../components/ui/section-header';
import {
Select,
SelectContent,
@@ -49,6 +50,7 @@ import RetrievalOptions, {
type RetrievalOptionsValue,
} from '../settings/components/RetrievalOptions';
import useRetrievalAvailability from '../settings/components/useRetrievalAvailability';
import { showActionToast } from '../notifications/actionToastSlice';
import type { AppDispatch } from '../store';
import { formatCount } from '../utils/dateTimeUtils';
import { ACCOUNT_NAME_MAX } from './accounts';
@@ -233,6 +235,10 @@ export default function ConnectWizard({
// done step shows what it can do.
const isMcpPreset =
connector.auth_kind === 'mcp_oauth' && !!connector.mcp_url;
// An MCP preset that also syncs (Linear) has its tools from the sign-in:
// one screen shows them first, then asks about Knowledge, and finishing
// it closes the wizard with no separate summary.
const toolsFirst = isMcpPreset && mode === 'connect' && tools.length > 0;
const mcp = useMcpOAuth();
const mcpConfig = (): McpOAuthConfig => ({
server_url: connector.mcp_url ?? '',
@@ -387,7 +393,8 @@ export default function ConnectWizard({
if (!connectionId) return;
if (!wantsTools && !syncReady) {
// Connected, nothing more to set up.
setStep('done');
if (toolsFirst) finish();
else setStep('done');
return;
}
setPending(true);
@@ -426,6 +433,22 @@ export default function ConnectWizard({
if (wantsTools) setTools(data.tools ?? []);
setSources(data.sources ?? []);
refresh();
if (toolsFirst) {
const count = (data.sources ?? []).length;
dispatch(
showActionToast({
variant: 'success',
message: t('settings.connectors.wizard.doneSources', {
sources: t('settings.connectors.wizard.sourcesCount', {
count,
formatted: formatCount(count),
}),
}),
}),
);
finish();
return;
}
setStep('done');
} catch {
setError(t('settings.connectors.wizard.syncFailed'));
@@ -615,6 +638,25 @@ export default function ConnectWizard({
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{toolsFirst && connectionId && (
<section className="flex flex-col gap-3">
<SectionHeader
as="h3"
size="xs"
title={t('settings.connectors.wizard.toolsHeading', {
count: toolCount,
formatted: formatCount(toolCount),
})}
/>
{tools.map((tool) => (
<ToolPermissions
key={tool.id}
connectionId={connectionId}
tool={tool}
/>
))}
</section>
)}
{(offerTools || askSync) && (
<SettingRows>
{offerTools && (
@@ -810,9 +852,14 @@ export default function ConnectWizard({
interpolation: { escapeValue: false },
})
: step === 'setup'
? offerTools || !syncing
? t('settings.connectors.wizard.chooseWhatToSetUp')
: t('settings.connectors.wizard.chooseWhatToSync')
? toolsFirst
? t('settings.connectors.wizard.doneTitle', {
name,
interpolation: { escapeValue: false },
})
: offerTools || !syncing
? t('settings.connectors.wizard.chooseWhatToSetUp')
: t('settings.connectors.wizard.chooseWhatToSync')
: t('settings.connectors.wizard.doneTitle', {
name,
interpolation: { escapeValue: false },
@@ -853,12 +900,16 @@ export default function ConnectWizard({
) : step === 'setup' && !offerTools && !syncing ? (
// Only the Knowledge question, answered no: Skip would do the same.
<Button type="button" size="lg" shape="pill" onClick={addSource}>
{t('settings.connectors.wizard.continue')}
{toolsFirst
? t('settings.connectors.wizard.done')
: t('settings.connectors.wizard.continue')}
</Button>
) : step === 'setup' ? (
<ModalActions
cancelLabel={t('settings.connectors.wizard.skip')}
onCancel={() => (mode === 'sync' ? onClose() : setStep('done'))}
onCancel={() =>
mode === 'sync' ? onClose() : toolsFirst ? finish() : setStep('done')
}
submitLabel={
offerTools
? t('settings.connectors.wizard.continue')