From 2ad0ff86688ba26cf6eb65faf77b5150036bbe4c Mon Sep 17 00:00:00 2001 From: junlong5 Date: Fri, 14 Aug 2026 20:14:28 +0800 Subject: [PATCH 1/6] fix(plugins): repair Windows DingTalk authorization --- .../src/runtime_work/local_connector_auth.rs | 28 +++-- .../local_connector_auth/tools.rs | 11 +- wework/e2e/desktop/modules/plugin-flows.mjs | 9 ++ .../plugins/LocalConnectorAuthDialog.test.tsx | 9 ++ .../plugins/LocalConnectorAuthDialog.tsx | 5 +- .../components/plugins/PluginsWorkspace.tsx | 74 ++++++++----- .../retainMarketplaceInstallHints.test.ts | 100 +++++++++++++++++- .../plugins/retainMarketplaceInstallHints.ts | 43 ++++++++ .../plugins/useLocalConnectorAuthSession.ts | 1 + 9 files changed, 238 insertions(+), 42 deletions(-) diff --git a/executor/src/runtime_work/local_connector_auth.rs b/executor/src/runtime_work/local_connector_auth.rs index 48c896c2cc1..4758eff80f2 100644 --- a/executor/src/runtime_work/local_connector_auth.rs +++ b/executor/src/runtime_work/local_connector_auth.rs @@ -238,12 +238,10 @@ fn resolve_plugin_root_candidates( )); } - let home = env::var_os("HOME") - .map(PathBuf::from) - .ok_or_else(|| AppIpcError::new("internal_error", "HOME is not set"))?; - let executor_home = env::var_os("WEGENT_EXECUTOR_HOME") - .map(PathBuf::from) - .unwrap_or_else(|| home.join(".wegent-executor")); + let executor_home = resolve_executor_home( + env::var_os("WEGENT_EXECUTOR_HOME").map(PathBuf::from), + dirs::home_dir(), + )?; let mut candidates: Vec = Vec::new(); let store_plugins = executor_home.join("capabilities/store/plugins"); @@ -286,6 +284,15 @@ fn resolve_plugin_root_candidates( Ok(candidates) } +fn resolve_executor_home( + configured_executor_home: Option, + platform_home: Option, +) -> Result { + configured_executor_home + .or_else(|| platform_home.map(|home| home.join(".wegent-executor"))) + .ok_or_else(|| AppIpcError::new("internal_error", "Home directory is not available")) +} + fn has_plugin_manifest(path: &Path) -> bool { path.join(".codex-plugin/plugin.json").is_file() || path.join(".claude-plugin/plugin.json").is_file() @@ -921,6 +928,15 @@ mod tests { } } + #[test] + fn resolve_executor_home_uses_runtime_path_without_platform_home() { + let executor_home = PathBuf::from("runtime/executor-home"); + + let resolved = resolve_executor_home(Some(executor_home.clone()), None).unwrap(); + + assert_eq!(resolved, executor_home); + } + #[test] fn resolve_plugin_root_prefers_newest_version_directory() { let _guard = env_lock(); diff --git a/executor/src/runtime_work/local_connector_auth/tools.rs b/executor/src/runtime_work/local_connector_auth/tools.rs index 1e3c5ec88de..59cfb7faba2 100644 --- a/executor/src/runtime_work/local_connector_auth/tools.rs +++ b/executor/src/runtime_work/local_connector_auth/tools.rs @@ -275,13 +275,10 @@ async fn resolve_managed_tool( } fn executor_home() -> Result { - if let Some(path) = env::var_os("WEGENT_EXECUTOR_HOME") { - return Ok(PathBuf::from(path)); - } - env::var_os("HOME") - .map(PathBuf::from) - .map(|home| home.join(".wegent-executor")) - .ok_or_else(|| AppIpcError::new("internal_error", "HOME is not set")) + super::resolve_executor_home( + env::var_os("WEGENT_EXECUTOR_HOME").map(PathBuf::from), + dirs::home_dir(), + ) } fn current_tool_target() -> Result { diff --git a/wework/e2e/desktop/modules/plugin-flows.mjs b/wework/e2e/desktop/modules/plugin-flows.mjs index c3860bfe208..777542d7aa5 100644 --- a/wework/e2e/desktop/modules/plugin-flows.mjs +++ b/wework/e2e/desktop/modules/plugin-flows.mjs @@ -691,6 +691,15 @@ async function verifyMarketplacePluginLifecycle({ await control.command('waitFor', actionsSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) + await waitForSnapshot( + control, + snapshot => + snapshot.testIds.includes(`plugin-marketplace-actions-${pluginId}`) && + snapshot.testIds.some( + testId => testId.startsWith('plugins-installed-strip-item-') && testId.includes(PLUGIN_NAME) + ), + 'The installed marketplace card and installed strip became inconsistent' + ) await control.command('click', actionsSelector) const trySelector = `[data-testid="plugin-marketplace-try-${pluginId}"]` await control.command('waitFor', trySelector, { diff --git a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx index fb417bceeff..e4cffa43501 100644 --- a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx +++ b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx @@ -109,4 +109,13 @@ describe('LocalConnectorAuthDialog browser oauth', () => { expect(onCancel).toHaveBeenCalledTimes(1) await waitFor(() => expect(authMocks.cancel).toHaveBeenCalledWith(executorTarget, 'session-2')) }) + + test('shows a string executor error once', async () => { + authMocks.start.mockRejectedValue('internal_error: HOME is not set') + + render() + + await screen.findByTestId('local-connector-auth-retry') + expect(screen.getAllByText('internal_error: HOME is not set')).toHaveLength(1) + }) }) diff --git a/wework/src/components/plugins/LocalConnectorAuthDialog.tsx b/wework/src/components/plugins/LocalConnectorAuthDialog.tsx index 48e19f962c1..d716568acab 100644 --- a/wework/src/components/plugins/LocalConnectorAuthDialog.tsx +++ b/wework/src/components/plugins/LocalConnectorAuthDialog.tsx @@ -123,8 +123,9 @@ export function LocalConnectorAuthDialog({ )} )} -

{statusText}

- {error ?

{error}

: null} +

+ {statusText} +

diff --git a/wework/src/components/plugins/PluginsWorkspace.tsx b/wework/src/components/plugins/PluginsWorkspace.tsx index 4e4c93d598f..f90b2d2ca79 100644 --- a/wework/src/components/plugins/PluginsWorkspace.tsx +++ b/wework/src/components/plugins/PluginsWorkspace.tsx @@ -90,7 +90,7 @@ import type { } from '@/types/api' import { connectorDisplayName } from './connectorDisplayName' import { holdBackInFlightMarketplaceInstalls } from './holdBackInFlightMarketplaceInstalls' -import { retainMarketplaceInstallHints } from './retainMarketplaceInstallHints' +import { retainMarketplaceInstalledState } from './retainMarketplaceInstallHints' import { type InstalledPluginItem } from './PluginManagementRows' import { PluginCreateMenu } from './PluginCreateMenu' import { PluginImportDialog } from './PluginImportDialog' @@ -3311,22 +3311,28 @@ export function PluginsWorkspace({ ), diskPersonalItemsForMerge ) - const heldBack = holdBackInFlightMarketplaceInstalls({ - items: mergeMarketplaceCatalog( + const previousMarketplaceItems = + pluginMarketplaceStateRef.current.items.length > 0 + ? pluginMarketplaceStateRef.current.items + : (getPluginMarketplaceCache(marketplaceCacheKeyValue)?.marketplaceItems ?? []) + const retained = retainMarketplaceInstalledState({ + previousItems: previousMarketplaceItems, + nextItems: mergeMarketplaceCatalog( cloudItems, localRows, nextInstalledRaw.map(plugin => plugin.raw) ), - installed: nextInstalledRaw, + previousInstalled: installedPluginsRef.current, + nextInstalled: nextInstalledRaw, + }) + const heldBack = holdBackInFlightMarketplaceInstalls({ + items: retained.items, + installed: retained.installed, installingIds: installingMarketplacePluginIdsRef.current, authPluginKey: pendingLocalConnectorAuthRef.current?.target.pluginKey, }) const nextInstalled = heldBack.installed - const previousMarketplaceItems = - pluginMarketplaceStateRef.current.items.length > 0 - ? pluginMarketplaceStateRef.current.items - : (getPluginMarketplaceCache(marketplaceCacheKeyValue)?.marketplaceItems ?? []) - const mergedItems = retainMarketplaceInstallHints(previousMarketplaceItems, heldBack.items) + const mergedItems = heldBack.items setInstalledPlugins(previous => sameInstalledPlugins(previous, nextInstalled) ? previous : nextInstalled ) @@ -3490,21 +3496,24 @@ export function PluginsWorkspace({ // Prefer in-flight cloud rows, then this key's durable cache — never stale // previous-account React state after a cache miss / account switch. const cloudItems = cloudItemsForMerge ?? cachedCloudItems - const heldBack = holdBackInFlightMarketplaceInstalls({ - items: mergeMarketplaceCatalog( + const retained = retainMarketplaceInstalledState({ + previousItems: pluginMarketplaceStateRef.current.items, + nextItems: mergeMarketplaceCatalog( cloudItems, localRows, nextInstalledRaw.map(plugin => plugin.raw) ), - installed: nextInstalledRaw, + previousInstalled: installedPluginsRef.current, + nextInstalled: nextInstalledRaw, + }) + const heldBack = holdBackInFlightMarketplaceInstalls({ + items: retained.items, + installed: retained.installed, installingIds: installingMarketplacePluginIdsRef.current, authPluginKey: pendingLocalConnectorAuthRef.current?.target.pluginKey, }) const nextInstalled = heldBack.installed - const mergedItems = retainMarketplaceInstallHints( - pluginMarketplaceStateRef.current.items, - heldBack.items - ) + const mergedItems = heldBack.items // Publish installed rows even when the catalog is still empty — cloud // listInstalledPlugins often arrives before marketplace rows. if (localStateForMerge || cloudInstalledForMerge) { @@ -3581,13 +3590,19 @@ export function PluginsWorkspace({ localState.installedPlugins, localState.deviceId || currentDeviceIdRef.current || '' ).map(toInstalledPluginItem) - const heldBack = holdBackInFlightMarketplaceInstalls({ - items: mergeMarketplaceCatalog( + const retained = retainMarketplaceInstalledState({ + previousItems: pluginMarketplaceStateRef.current.items, + nextItems: mergeMarketplaceCatalog( pluginMarketplaceStateRef.current.items.filter(item => !isCodexCatalogItem(item)), localRows, nextInstalledRaw.map(plugin => plugin.raw) ), - installed: nextInstalledRaw, + previousInstalled: installedPluginsRef.current, + nextInstalled: nextInstalledRaw, + }) + const heldBack = holdBackInFlightMarketplaceInstalls({ + items: retained.items, + installed: retained.installed, installingIds: installingMarketplacePluginIdsRef.current, authPluginKey: pendingLocalConnectorAuthRef.current?.target.pluginKey, }) @@ -3596,7 +3611,7 @@ export function PluginsWorkspace({ sameInstalledPlugins(previous, nextInstalled) ? previous : nextInstalled ) setPluginMarketplaceState(previous => { - const nextItems = retainMarketplaceInstallHints(previous.items, heldBack.items) + const nextItems = heldBack.items if (sameMarketplaceItems(previous.items, nextItems) && !previous.error) { return previous } @@ -3687,19 +3702,26 @@ export function PluginsWorkspace({ localInstalledForMerge, localStateForMerge?.deviceId || deviceIdHint || currentDeviceIdRef.current || '' ).map(toInstalledPluginItem) - const heldBack = holdBackInFlightMarketplaceInstalls({ - items: mergeMarketplaceCatalog( + const retained = retainMarketplaceInstalledState({ + previousItems: pluginMarketplaceStateRef.current.items, + nextItems: mergeMarketplaceCatalog( cloudItems, localRows, nextInstalledRaw.map(plugin => plugin.raw) ), - installed: nextInstalledRaw, + previousInstalled: installedPluginsRef.current, + nextInstalled: nextInstalledRaw, + }) + const heldBack = holdBackInFlightMarketplaceInstalls({ + items: retained.items, + installed: retained.installed, installingIds: installingMarketplacePluginIdsRef.current, authPluginKey: pendingLocalConnectorAuthRef.current?.target.pluginKey, }) - const mergedItems = retainMarketplaceInstallHints( - pluginMarketplaceStateRef.current.items, - heldBack.items + const nextInstalled = heldBack.installed + const mergedItems = heldBack.items + setInstalledPlugins(previous => + sameInstalledPlugins(previous, nextInstalled) ? previous : nextInstalled ) setPluginMarketplaceState(previous => sameMarketplaceItems(previous.items, mergedItems) diff --git a/wework/src/components/plugins/retainMarketplaceInstallHints.test.ts b/wework/src/components/plugins/retainMarketplaceInstallHints.test.ts index d748ed0fa7a..f591d75a08d 100644 --- a/wework/src/components/plugins/retainMarketplaceInstallHints.test.ts +++ b/wework/src/components/plugins/retainMarketplaceInstallHints.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from 'vitest' import type { PluginMarketplaceItem } from '@/types/api' -import { retainMarketplaceInstallHints } from './retainMarketplaceInstallHints' +import type { InstalledPlugin } from '@/types/api' +import type { InstalledPluginItem } from './PluginManagementRows' +import { + retainMarketplaceInstalledState, + retainMarketplaceInstallHints, +} from './retainMarketplaceInstallHints' function item(overrides: Partial = {}): PluginMarketplaceItem { return { @@ -33,6 +38,60 @@ function item(overrides: Partial = {}): PluginMarketplace } } +function installedItem(): InstalledPluginItem { + const raw: InstalledPlugin = { + apiVersion: 'agent.wecode.io/v1', + kind: 'InstalledPlugin', + metadata: { + name: 'github', + namespace: 'openai-curated-remote', + labels: { id: 'github@openai-curated-remote' }, + }, + spec: { + source: { + type: 'local', + providerKey: 'openai-curated-remote', + pluginKey: 'github', + catalogItemId: 'github@openai-curated-remote', + marketplace: 'openai-curated-remote', + }, + origin: 'market', + installState: 'installed', + enabled: true, + displayName: 'GitHub', + description: '', + componentStates: {}, + components: { + skills: [], + commands: [], + agents: [], + hooks: [], + mcps: [], + lsps: [], + monitors: [], + bins: [], + }, + interface: null, + packageRef: null, + sourcePayload: null, + }, + status: { state: 'enabled' }, + } + return { + id: 'github@openai-curated-remote', + name: 'GitHub', + description: '', + enabled: true, + version: '0.1.8', + origin: 'market', + sourceLabel: 'OpenAI', + distribution: 'official', + updateAvailable: false, + componentCounts: {}, + raw, + } +} + describe('retainMarketplaceInstallHints', () => { test('keeps an optimistic install when the next catalog snapshot lags', () => { const previous = [ @@ -62,4 +121,43 @@ describe('retainMarketplaceInstallHints', () => { const next = [item({ installed: false, installedPluginId: null })] expect(retainMarketplaceInstallHints(previous, next)).toEqual(next) }) + + test('retains the matching installed row with an optimistic marketplace hint', () => { + const previousItem = item({ + installed: true, + installedLocally: true, + installedPluginId: 'github@openai-curated-remote', + enabled: true, + }) + const plugin = installedItem() + + const retained = retainMarketplaceInstalledState({ + previousItems: [previousItem], + nextItems: [item()], + previousInstalled: [plugin], + nextInstalled: [], + }) + + expect(retained.items[0]?.installed).toBe(true) + expect(retained.installed).toEqual([plugin]) + }) + + test('clears a stale installed card when no installed row can be retained', () => { + const retained = retainMarketplaceInstalledState({ + previousItems: [ + item({ + installed: true, + installedLocally: true, + installedPluginId: 'github@openai-curated-remote', + enabled: true, + }), + ], + nextItems: [item()], + previousInstalled: [], + nextInstalled: [], + }) + + expect(retained.items[0]?.installed).toBe(false) + expect(retained.installed).toEqual([]) + }) }) diff --git a/wework/src/components/plugins/retainMarketplaceInstallHints.ts b/wework/src/components/plugins/retainMarketplaceInstallHints.ts index 82b185bc133..b56eff66e27 100644 --- a/wework/src/components/plugins/retainMarketplaceInstallHints.ts +++ b/wework/src/components/plugins/retainMarketplaceInstallHints.ts @@ -1,4 +1,5 @@ import type { PluginMarketplaceItem } from '@/types/api' +import type { InstalledPluginItem } from './PluginManagementRows' /** * Progressive catalog paints can briefly reuse a stale cache snapshot after a @@ -36,3 +37,45 @@ export function retainMarketplaceInstallHints( } }) } + +/** + * Keep the installed strip and marketplace actions on the same snapshot when a + * post-install catalog refresh temporarily lags behind the optimistic update. + */ +export function retainMarketplaceInstalledState(input: { + previousItems: PluginMarketplaceItem[] + nextItems: PluginMarketplaceItem[] + previousInstalled: InstalledPluginItem[] + nextInstalled: InstalledPluginItem[] +}): { items: PluginMarketplaceItem[]; installed: InstalledPluginItem[] } { + const hintedItems = retainMarketplaceInstallHints(input.previousItems, input.nextItems) + const nextItemsById = new Map(input.nextItems.map(item => [String(item.id), item])) + const retainedPluginIds = new Set( + hintedItems.flatMap(item => { + const next = nextItemsById.get(String(item.id)) + const retained = item.installed && !next?.installed && item.installedPluginId != null + return retained ? [String(item.installedPluginId)] : [] + }) + ) + if (retainedPluginIds.size === 0) { + return { items: hintedItems, installed: input.nextInstalled } + } + + const installed = [...input.nextInstalled] + const installedIds = new Set(installed.map(plugin => String(plugin.id))) + for (const plugin of input.previousInstalled) { + const id = String(plugin.id) + if (!retainedPluginIds.has(id) || installedIds.has(id)) continue + installed.push(plugin) + installedIds.add(id) + } + const items = hintedItems.map(item => { + const next = nextItemsById.get(String(item.id)) + const retainedId = + item.installed && !next?.installed && item.installedPluginId != null + ? String(item.installedPluginId) + : null + return retainedId && !installedIds.has(retainedId) && next ? next : item + }) + return { items, installed } +} diff --git a/wework/src/features/plugins/useLocalConnectorAuthSession.ts b/wework/src/features/plugins/useLocalConnectorAuthSession.ts index 13eaf84086f..0711e4f6c2d 100644 --- a/wework/src/features/plugins/useLocalConnectorAuthSession.ts +++ b/wework/src/features/plugins/useLocalConnectorAuthSession.ts @@ -13,6 +13,7 @@ import { export type LocalConnectorAuthTranslate = TFunction function startErrorMessage(startError: unknown, fallback: string): string { + if (typeof startError === 'string' && startError.trim()) return startError if (startError instanceof Error) return startError.message if ( typeof startError === 'object' && From 20f7028f744d93b56ac236e5bf71e91ebffc5e09 Mon Sep 17 00:00:00 2001 From: junlong5 Date: Sat, 15 Aug 2026 10:37:41 +0800 Subject: [PATCH 2/6] fix(plugins): preserve browser auth callbacks --- .../src/runtime_work/local_connector_auth.rs | 56 +++++++++++++++++++ .../plugins/LocalConnectorAuthDialog.test.tsx | 19 +++++++ .../plugins/useLocalConnectorAuthSession.ts | 11 ++-- 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/executor/src/runtime_work/local_connector_auth.rs b/executor/src/runtime_work/local_connector_auth.rs index 4758eff80f2..56129efcee0 100644 --- a/executor/src/runtime_work/local_connector_auth.rs +++ b/executor/src/runtime_work/local_connector_auth.rs @@ -461,6 +461,22 @@ fn browser_auth_sessions() -> &'static Mutex BROWSER_AUTH_SESSIONS.get_or_init(|| Mutex::new(HashMap::new())) } +async fn active_browser_auth_state(plugin_key: &str, connector_slug: &str) -> Option { + let state = { + let sessions = browser_auth_sessions().lock().await; + sessions + .values() + .find(|session| { + session.plugin_key == plugin_key + && session.connector_slug == connector_slug + && !session.task.is_finished() + }) + .map(|session| Arc::clone(&session.state)) + }?; + let response = state.lock().await.clone(); + Some(response) +} + async fn start_browser_auth( payload: Value, plugin_root: PathBuf, @@ -473,6 +489,9 @@ async fn start_browser_auth( .or_else(|| string_field(&payload, "connector_slug")) .or_else(|| string_field(&payload, "slug")) .ok_or_else(|| AppIpcError::new("bad_request", "connectorSlug is required"))?; + if let Some(state) = active_browser_auth_state(&plugin_key, &connector_slug).await { + return Ok(state); + } let session_id = Uuid::new_v4().to_string(); let state = Arc::new(Mutex::new(browser_session_state( "preparing", @@ -867,6 +886,43 @@ mod tests { .any(|pair| pair == ["--wait-seconds", "0"])); } + #[tokio::test] + async fn reuses_active_browser_auth_session_state() { + let plugin_key = format!("plugin-{}", Uuid::new_v4()); + let connector_slug = "browser-auth".to_owned(); + let session_id = Uuid::new_v4().to_string(); + let state = Arc::new(tokio::sync::Mutex::new(browser_session_state( + "waiting_browser", + "Complete authorization in the browser", + &session_id, + ))); + let task = tokio::spawn(async { std::future::pending::<()>().await }); + browser_auth_sessions().lock().await.insert( + session_id.clone(), + BrowserAuthSession { + plugin_key: plugin_key.clone(), + connector_slug: connector_slug.clone(), + state, + task, + }, + ); + + let reused = active_browser_auth_state(&plugin_key, &connector_slug) + .await + .expect("active session"); + + assert_eq!( + reused.get("sessionId").and_then(Value::as_str), + Some(session_id.as_str()) + ); + let session = browser_auth_sessions() + .lock() + .await + .remove(&session_id) + .expect("inserted session"); + session.task.abort(); + } + #[test] fn reject_absolute_and_parent_paths() { assert!(validate_relative_arg("scripts/run.sh").is_ok()); diff --git a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx index e4cffa43501..5eb2ddb4359 100644 --- a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx +++ b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx @@ -110,6 +110,25 @@ describe('LocalConnectorAuthDialog browser oauth', () => { await waitFor(() => expect(authMocks.cancel).toHaveBeenCalledWith(executorTarget, 'session-2')) }) + test('keeps the executor session alive when the dialog is remounted by its host', async () => { + authMocks.start.mockResolvedValue({ + status: 'waiting_browser', + sessionId: 'session-remount', + }) + authMocks.poll.mockImplementation(() => new Promise(() => undefined)) + + const view = render( + + ) + + await waitFor(() => expect(authMocks.start).toHaveBeenCalled()) + view.unmount() + render() + + await waitFor(() => expect(authMocks.start).toHaveBeenCalledTimes(2)) + expect(authMocks.cancel).not.toHaveBeenCalled() + }) + test('shows a string executor error once', async () => { authMocks.start.mockRejectedValue('internal_error: HOME is not set') diff --git a/wework/src/features/plugins/useLocalConnectorAuthSession.ts b/wework/src/features/plugins/useLocalConnectorAuthSession.ts index 0711e4f6c2d..f1b43ec51ca 100644 --- a/wework/src/features/plugins/useLocalConnectorAuthSession.ts +++ b/wework/src/features/plugins/useLocalConnectorAuthSession.ts @@ -81,6 +81,7 @@ export function useLocalConnectorAuthSession({ const tRef = useRef(t) const sessionRef = useRef(0) const activeAuthSessionRef = useRef(null) + const cancelRequestedRef = useRef(false) const cancelledAuthSessionsRef = useRef(new Set()) useEffect(() => { @@ -123,6 +124,7 @@ export function useLocalConnectorAuthSession({ const browserMode = targetUsesBrowser const start = async () => { + cancelRequestedRef.current = false setBusy(true) setError(null) setStatus(null) @@ -131,7 +133,7 @@ export function useLocalConnectorAuthSession({ // executor reading the installed plugin manifest by pluginKey/slug. const started = await localConnectorAuthStart(authTarget) if (!isCurrent()) { - if (started.sessionId) { + if (cancelRequestedRef.current && started.sessionId) { cancelAuthSession(authTarget, started.sessionId) } return @@ -214,12 +216,6 @@ export function useLocalConnectorAuthSession({ } if (startTimer) clearTimeout(startTimer) if (pollTimer) clearTimeout(pollTimer) - if (authSessionId) { - cancelAuthSession(authTarget, authSessionId) - if (activeAuthSessionRef.current === authSessionId) { - activeAuthSessionRef.current = null - } - } } }, [ enabled, @@ -237,6 +233,7 @@ export function useLocalConnectorAuthSession({ }, []) const cancelActiveSession = useCallback(() => { + cancelRequestedRef.current = true const sessionId = activeAuthSessionRef.current activeAuthSessionRef.current = null if (!sessionId) return From 287ce9a8988a00fe462243b8d4e7cddf6fa9501b Mon Sep 17 00:00:00 2001 From: junlong5 Date: Sat, 15 Aug 2026 13:29:48 +0800 Subject: [PATCH 3/6] fix(plugins): stop auth spinner after failure --- .../plugins/LocalConnectorAuthDialog.test.tsx | 2 ++ .../plugins/LocalConnectorAuthDialog.tsx | 22 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx index 5eb2ddb4359..e9d9e770fae 100644 --- a/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx +++ b/wework/src/components/plugins/LocalConnectorAuthDialog.test.tsx @@ -136,5 +136,7 @@ describe('LocalConnectorAuthDialog browser oauth', () => { await screen.findByTestId('local-connector-auth-retry') expect(screen.getAllByText('internal_error: HOME is not set')).toHaveLength(1) + expect(screen.getByTestId('local-connector-auth-browser-error')).toBeInTheDocument() + expect(screen.queryByTestId('local-connector-auth-browser-loading')).not.toBeInTheDocument() }) }) diff --git a/wework/src/components/plugins/LocalConnectorAuthDialog.tsx b/wework/src/components/plugins/LocalConnectorAuthDialog.tsx index d716568acab..b5794ae84d3 100644 --- a/wework/src/components/plugins/LocalConnectorAuthDialog.tsx +++ b/wework/src/components/plugins/LocalConnectorAuthDialog.tsx @@ -1,4 +1,4 @@ -import { ExternalLink, Loader2, QrCode, RefreshCw, X } from 'lucide-react' +import { AlertCircle, ExternalLink, Loader2, QrCode, RefreshCw, X } from 'lucide-react' import { useTranslation } from '@/hooks/useTranslation' import { cn } from '@/lib/utils' import type { @@ -98,10 +98,24 @@ export function LocalConnectorAuthDialog({ className="flex h-56 w-56 flex-col items-center justify-center gap-3 rounded-lg border border-dashed bg-background" data-testid="local-connector-auth-browser" > - {status?.status === 'waiting_browser' ? ( - + {error ? ( +