Fix desktop preview interactions and connector auth feedback (#864)

* Fix desktop preview modal interactions

* Fix connector auth failures surfacing
This commit is contained in:
shangxinyu1 2026-05-08 11:05:41 +08:00 committed by GitHub
parent 915c041545
commit 32df17b87b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 9 deletions

View file

@ -107,6 +107,12 @@ const MAC_WINDOW_CHROME_CSS = `
.entry-header [role="button"],
.entry-tabs,
.entry-tabs *,
.ds-modal-header,
.ds-modal-header *,
.ds-modal-actions,
.ds-modal-actions *,
.share-menu-popover,
.share-menu-popover *,
.entry-side-resizer,
.avatar-popover,
.avatar-popover * {
@ -181,6 +187,15 @@ function isHttpUrl(url: string): boolean {
}
}
function isAllowedChildWindowUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === "blob:";
} catch {
return false;
}
}
function installWindowChromeCssHook(window: BrowserWindow): void {
window.webContents.on("did-finish-load", () => {
void applyWindowChromeCss(window).catch((error: unknown) => {
@ -269,6 +284,7 @@ export async function createDesktopRuntime(options: DesktopRuntimeOptions): Prom
window.on("blur", () => showWindowButtons(window));
window.webContents.setWindowOpenHandler(({ url }) => {
if (isAllowedChildWindowUrl(url)) return { action: "allow" };
if (isHttpUrl(url)) void shell.openExternal(url);
return { action: "deny" };
});

View file

@ -646,7 +646,11 @@ export function EntryView({
toolsLoaded={connectorDiscoveryLoaded}
composioConfigured={Boolean(config.composio?.apiKeyConfigured)}
onOpenSettings={onOpenSettings}
onConnect={async (connectorId) => updateConnector(await connectConnector(connectorId))}
onConnect={async (connectorId) => {
const result = await connectConnector(connectorId);
updateConnector(result.connector);
return result;
}}
onDisconnect={async (connectorId) => updateConnector(await disconnectConnector(connectorId))}
/>
) : null}
@ -710,7 +714,7 @@ function ConnectorsTab({
toolsLoaded: boolean;
composioConfigured: boolean;
onOpenSettings: (section?: 'execution' | 'media' | 'composio' | 'language' | 'about') => void;
onConnect: (connectorId: string) => Promise<void> | void;
onConnect: (connectorId: string) => Promise<{ error?: string } | void> | { error?: string } | void;
onDisconnect: (connectorId: string) => Promise<void> | void;
}) {
const t = useT();
@ -720,6 +724,7 @@ function ConnectorsTab({
} | null>(null);
const [detailConnectorId, setDetailConnectorId] = useState<string | null>(null);
const [filter, setFilter] = useState('');
const [actionError, setActionError] = useState<string | null>(null);
const searchInputRef = useRef<HTMLInputElement | null>(null);
// Mask the grid whenever no Composio-backed connector has its auth
@ -746,10 +751,14 @@ function ConnectorsTab({
async function runConnectorAction(connectorId: string, action: 'connect' | 'disconnect') {
if (pendingConnectorAction) return;
setActionError(null);
setPendingConnectorAction({ connectorId, action });
try {
if (action === 'connect') {
await onConnect(connectorId);
const result = await onConnect(connectorId);
if (result && typeof result === 'object' && 'error' in result && result.error) {
setActionError(result.error);
}
} else {
await onDisconnect(connectorId);
}
@ -811,6 +820,11 @@ function ConnectorsTab({
</div>
</div>
</div>
{actionError ? (
<p className="connector-inline-error" role="alert" data-testid="connectors-action-error">
{actionError}
</p>
) : null}
{loading ? (
<CenteredLoader label={t('common.loading')} />
) : (

View file

@ -3173,6 +3173,17 @@ code {
font-size: 13px;
}
.connector-inline-error {
margin: 0 0 12px;
padding: 10px 12px;
border: 1px solid color-mix(in oklab, #ff6b6b 45%, var(--line) 55%);
border-radius: 12px;
background: color-mix(in oklab, #ff6b6b 10%, var(--panel) 90%);
color: color-mix(in oklab, #ff6b6b 70%, var(--fg) 30%);
font-size: 13px;
line-height: 1.45;
}
.connectors-heading h2 {
margin: 0;
font-size: 18px;

View file

@ -265,7 +265,25 @@ export async function fetchConnectorDiscovery(options: { refresh?: boolean } = {
return promise;
}
export async function connectConnector(connectorId: string): Promise<ConnectorDetail | null> {
export interface ConnectorActionResult {
connector: ConnectorDetail | null;
error?: string;
}
function popupBlockedMessage(): string {
return 'Popup blocked. Allow popups for Open Design and try again.';
}
async function decodeConnectorError(resp: Response): Promise<string> {
try {
const payload = (await resp.json()) as { error?: { message?: string } } | null;
return payload?.error?.message?.trim() || `Connector request failed (${resp.status})`;
} catch {
return `Connector request failed (${resp.status})`;
}
}
export async function connectConnector(connectorId: string): Promise<ConnectorActionResult> {
let authWindow: Window | null = null;
try {
authWindow = window.open('about:blank', '_blank');
@ -275,22 +293,28 @@ export async function connectConnector(connectorId: string): Promise<ConnectorDe
});
if (!resp.ok) {
authWindow?.close();
return null;
return { connector: null, error: await decodeConnectorError(resp) };
}
const json = (await resp.json()) as ConnectorConnectResponse;
if (json.auth?.kind === 'redirect_required' && json.auth.redirectUrl) {
if (authWindow) {
authWindow.location.href = json.auth.redirectUrl;
} else {
window.open(json.auth.redirectUrl, '_blank');
const redirected = window.open(json.auth.redirectUrl, '_blank');
if (!redirected) {
return { connector: json.connector ?? null, error: popupBlockedMessage() };
}
}
} else {
authWindow?.close();
}
return json.connector ?? null;
} catch {
return { connector: json.connector ?? null };
} catch (err) {
authWindow?.close();
return null;
return {
connector: null,
error: err instanceof Error && err.message ? err.message : 'Could not start connector authentication.',
};
}
}

View file

@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import {
CLOUDFLARE_PAGES_PROVIDER_ID,
connectConnector,
DEFAULT_DEPLOY_PROVIDER_ID,
deployProjectFile,
fetchDeployConfig,
@ -132,6 +133,31 @@ describe('fetchConnectorDiscovery', () => {
});
});
describe('connectConnector', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
it('returns a user-facing error when the OAuth popup is blocked', async () => {
const open = vi.fn(() => null);
vi.stubGlobal('window', { open } as unknown as Window & typeof globalThis);
vi.stubGlobal(
'fetch',
vi.fn(async () => new Response(JSON.stringify({
connector: { id: 'github', name: 'GitHub', status: 'available', tools: [] },
auth: { kind: 'redirect_required', redirectUrl: 'https://example.com/oauth' },
}), { status: 200 })),
);
await expect(connectConnector('github')).resolves.toEqual({
connector: { id: 'github', name: 'GitHub', status: 'available', tools: [] },
error: 'Popup blocked. Allow popups for Open Design and try again.',
});
expect(open).toHaveBeenCalledTimes(2);
});
});
describe('uploadProjectFiles', () => {
afterEach(() => {
vi.restoreAllMocks();