From dc9cb4ed80ede200961760911519a7e0c11edb4b Mon Sep 17 00:00:00 2001 From: Wibus <62133302+wibus-wee@users.noreply.github.com> Date: Sat, 29 Aug 2026 14:36:48 +0000 Subject: [PATCH] fix: reapply sidebar project selection Model: gpt-5 --- .../chat/chat-landing-project-selection.ts | 35 +++++++++++ .../src/components/chat/chat-landing.tsx | 62 ++++++++++++------- .../src/components/loro-app-sidebar.tsx | 17 ++++- .../chat-landing-project-selection.test.ts | 32 ++++++++++ 4 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 packages/components/src/components/chat/chat-landing-project-selection.ts create mode 100644 packages/components/tests/chat-landing-project-selection.test.ts diff --git a/packages/components/src/components/chat/chat-landing-project-selection.ts b/packages/components/src/components/chat/chat-landing-project-selection.ts new file mode 100644 index 000000000..321c84ea0 --- /dev/null +++ b/packages/components/src/components/chat/chat-landing-project-selection.ts @@ -0,0 +1,35 @@ +export type ChatLandingProjectSelection = + | { kind: 'none' } + | { kind: 'github'; repoFullName: string } + | { kind: 'local'; machineId: string; localProjectId: string }; + +export type ChatLandingProjectSelectionRequest = { + workspaceSlug: string; + selection: ChatLandingProjectSelection; +}; + +type ChatLandingProjectSelectionListener = (request: ChatLandingProjectSelectionRequest) => void; + +const projectSelectionListeners = new Set(); + +/** + * This request is deliberately transient. A mounted desktop landing consumes + * it; otherwise the accompanying route navigation applies the URL selection + * when the landing mounts. Do not persist it or mirror it into URL state. + */ +export function requestChatLandingProjectSelection( + request: ChatLandingProjectSelectionRequest +): void { + for (const listener of projectSelectionListeners) { + listener(request); + } +} + +export function subscribeToChatLandingProjectSelection( + listener: ChatLandingProjectSelectionListener +): () => void { + projectSelectionListeners.add(listener); + return () => { + projectSelectionListeners.delete(listener); + }; +} diff --git a/packages/components/src/components/chat/chat-landing.tsx b/packages/components/src/components/chat/chat-landing.tsx index 0bfb71e64..97abfe397 100644 --- a/packages/components/src/components/chat/chat-landing.tsx +++ b/packages/components/src/components/chat/chat-landing.tsx @@ -363,6 +363,10 @@ import { getChatLandingVisibleComposerStatus, isChatLandingMachineReachable, } from './chat-landing-derived'; +import { + subscribeToChatLandingProjectSelection, + type ChatLandingProjectSelection, +} from './chat-landing-project-selection'; interface ChatLandingProps { workspaceSlug: string; @@ -1238,6 +1242,32 @@ function WorkspaceChatLanding({ }, [] ); + const applyProjectSelection = useCallback( + (selection: ChatLandingProjectSelection) => { + if (selection.kind === 'none') { + setContextType('chat'); + return; + } + if (selection.kind === 'github') { + setSelectedRepo(selection.repoFullName); + setContextType('github'); + return; + } + handleSelectedLocalProjectChange({ + machineId: selection.machineId as MachineId, + localProjectId: selection.localProjectId as LocalProjectId, + }); + setContextType('local'); + }, + [handleSelectedLocalProjectChange] + ); + useLayoutEffect(() => { + if (isMobile) return; + return subscribeToChatLandingProjectSelection((request) => { + if (request.workspaceSlug !== workspaceSlug) return; + applyProjectSelection(request.selection); + }); + }, [applyProjectSelection, isMobile, workspaceSlug]); const getFirstVisibleLocalProjectForMachine = useCallback( (machineId: MachineId): LocalProjectSelection | null => { for (const entry of visibleLocalProjectMap.values()) { @@ -1413,24 +1443,23 @@ function WorkspaceChatLanding({ preSelectionAppliedRef.current = preSelectionKey; if (preSelectedContext === 'chat') { - setContextType('chat'); + applyProjectSelection({ kind: 'none' }); } else if (preSelectedContext === 'local' && preSelectedMachine && preSelectedProject) { - setContextType('local'); - handleSelectedLocalProjectChange({ - machineId: preSelectedMachine as MachineId, - localProjectId: preSelectedProject as LocalProjectId, + applyProjectSelection({ + kind: 'local', + machineId: preSelectedMachine, + localProjectId: preSelectedProject, }); } else if (preSelectedRepo) { - setContextType('github'); - setSelectedRepo(preSelectedRepo); + applyProjectSelection({ kind: 'github', repoFullName: preSelectedRepo }); } }, [ + applyProjectSelection, preSelectionKey, preSelectedContext, preSelectedMachine, preSelectedProject, preSelectedRepo, - handleSelectedLocalProjectChange, ]); // ── Machine-owner authorization check for local projects ── @@ -3380,22 +3409,9 @@ function WorkspaceChatLanding({ }, [contextType, selectedLocalProject, selectedRepo]); const handleDesktopProjectChange = useCallback( (selection: UnifiedProjectSelection) => { - if (selection.kind === 'none') { - setContextType('chat'); - return; - } - if (selection.kind === 'github') { - setSelectedRepo(selection.repoFullName); - setContextType('github'); - return; - } - handleSelectedLocalProjectChange({ - machineId: selection.machineId, - localProjectId: selection.localProjectId, - }); - setContextType('local'); + applyProjectSelection(selection); }, - [handleSelectedLocalProjectChange] + [applyProjectSelection] ); const desktopAgentMachineIds = useMemo( () => (scopedMachineId ? [scopedMachineId] : []), diff --git a/packages/components/src/components/loro-app-sidebar.tsx b/packages/components/src/components/loro-app-sidebar.tsx index da5350819..86ac8ff02 100644 --- a/packages/components/src/components/loro-app-sidebar.tsx +++ b/packages/components/src/components/loro-app-sidebar.tsx @@ -58,6 +58,7 @@ import { lodyConnectionUiStateAtom } from '@/atoms/control-connection'; import { localMachineIdAtom } from '@/atoms/local-probe'; import { selectAndWriteLocalProject } from '@/lib/local-project-import'; import { importSidebarLocalProject } from '@/components/sidebar-local-project-import'; +import { requestChatLandingProjectSelection } from '@/components/chat/chat-landing-project-selection'; import { lodyPresenceNowMsAtom, lodyPresenceStatesAtom } from '@/atoms/presence'; import { chatScopeAtom, @@ -1672,13 +1673,19 @@ export function LoroAppSidebar({ className }: LoroAppSidebarProps) { (machineId: MachineId, localProjectId: string) => { if (!workspaceSlug) return; closeMobileDrawer(); + if (!isMobile) { + requestChatLandingProjectSelection({ + workspaceSlug, + selection: { kind: 'local', machineId, localProjectId }, + }); + } void router.navigate({ to: '/$workspaceName/chat', params: { workspaceName: workspaceSlug }, search: { context: 'local' as const, machine: machineId, project: localProjectId }, }); }, - [closeMobileDrawer, router, workspaceSlug] + [closeMobileDrawer, isMobile, router, workspaceSlug] ); const handleImportLocalProject = useCallback(async () => { @@ -1826,6 +1833,12 @@ export function LoroAppSidebar({ className }: LoroAppSidebarProps) { (repoFullName?: string) => { if (!workspaceSlug) return; closeMobileDrawer(); + if (!isMobile) { + requestChatLandingProjectSelection({ + workspaceSlug, + selection: repoFullName ? { kind: 'github', repoFullName } : { kind: 'none' }, + }); + } void router.navigate({ to: '/$workspaceName/chat', params: { workspaceName: workspaceSlug }, @@ -1834,7 +1847,7 @@ export function LoroAppSidebar({ className }: LoroAppSidebarProps) { : { context: 'chat' as const }, }); }, - [closeMobileDrawer, router, workspaceSlug] + [closeMobileDrawer, isMobile, router, workspaceSlug] ); const handleRequestRemoval = useCallback( diff --git a/packages/components/tests/chat-landing-project-selection.test.ts b/packages/components/tests/chat-landing-project-selection.test.ts new file mode 100644 index 000000000..9f9742539 --- /dev/null +++ b/packages/components/tests/chat-landing-project-selection.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { + requestChatLandingProjectSelection, + subscribeToChatLandingProjectSelection, +} from '../src/components/chat/chat-landing-project-selection'; + +describe('chat landing project selection requests', () => { + it('delivers repeated requests for the same project without URL state', () => { + const listener = vi.fn(); + const unsubscribe = subscribeToChatLandingProjectSelection(listener); + const request = { + workspaceSlug: 'lody', + selection: { + kind: 'local' as const, + machineId: 'machine-a', + localProjectId: 'project-a', + }, + }; + + requestChatLandingProjectSelection(request); + requestChatLandingProjectSelection(request); + + expect(listener).toHaveBeenCalledTimes(2); + expect(listener).toHaveBeenNthCalledWith(1, request); + expect(listener).toHaveBeenNthCalledWith(2, request); + + unsubscribe(); + requestChatLandingProjectSelection(request); + expect(listener).toHaveBeenCalledTimes(2); + }); +});