Refactor app shell and thread state modules#89
Conversation
Review Summary by QodoRefactor app shell and thread state into feature-rooted deep modules
WalkthroughsDescription• Refactored App.vue by extracting ~1000 lines of app shell orchestration logic into six feature-rooted composables (useAppShellSettings, useAppShellAccounts, useAppShellProjects, useAppShellRouting, useAppShellIntegrations, useAppShellViewport) • Extracted thread-state utilities from useDesktopState into three dedicated feature modules: storage.ts, merge.ts, and messages.ts (~900+ lines moved) • Created new src/features/app-shell/ module with composables for project management, settings, accounts, integrations, routing, and viewport state • Created new src/features/thread-state/ module with utilities for storage persistence, thread/project merging, and message handling • Added barrel exports (index.ts) for both feature modules to provide clean import interfaces • Maintained backward compatibility of useDesktopState API while delegating to new feature modules • Added comprehensive manual verification test cases in tests.md for both app-shell and thread-state refactors Diagramflowchart LR
AppVue["App.vue<br/>~1000 lines removed"]
DesktopState["useDesktopState.ts<br/>~900 lines removed"]
AppShell["src/features/app-shell/"]
ThreadState["src/features/thread-state/"]
AppShellComposables["Composables:<br/>Settings, Accounts,<br/>Projects, Routing,<br/>Integrations, Viewport"]
AppShellTypes["Types & Constants"]
ThreadStateModules["Modules:<br/>storage.ts,<br/>merge.ts,<br/>messages.ts"]
AppVue -- "delegates to" --> AppShell
DesktopState -- "delegates to" --> ThreadState
AppShell --> AppShellComposables
AppShell --> AppShellTypes
ThreadState --> ThreadStateModules
File Changes1. src/composables/useDesktopState.ts
|
Code Review by Qodo
1. Dark mode not applied
|
| const darkMode = ref<'system' | 'light' | 'dark'>(loadDarkModePref()) | ||
| const chatWidth = ref<ChatWidthMode>(loadChatWidthPref()) | ||
| const dictationClickToToggle = ref(loadBoolPref(DICTATION_CLICK_TO_TOGGLE_KEY, false)) | ||
| const dictationAutoSend = ref(loadBoolPref(DICTATION_AUTO_SEND_KEY, true)) | ||
| const dictationLanguage = ref(loadDictationLanguagePref()) | ||
| const freeModeEnabled = ref(false) | ||
| const freeModeLoading = ref(false) | ||
| const freeModeCustomKey = ref('') | ||
| const freeModeHasCustomKey = ref(false) | ||
| const freeModeCustomKeyMasked = ref<string | null>(null) | ||
| const freeModeCustomKeySaving = ref(false) | ||
| const providerError = ref('') | ||
| const selectedProvider = ref<'codex' | 'openrouter' | 'opencode-zen' | 'custom'>('codex') | ||
| const customEndpointUrl = ref('') | ||
| const customEndpointKey = ref('') | ||
| const customEndpointWireApi = ref<'responses' | 'chat'>('responses') | ||
| const openRouterWireApi = ref<'responses' | 'chat'>('responses') | ||
| const opencodeZenKey = ref('') | ||
|
|
||
| const dictationLanguageOptions = computed(() => buildDictationLanguageOptions(dictationLanguage.value, t)) | ||
| const chatWidthLabel = computed(() => t(CHAT_WIDTH_PRESETS[chatWidth.value].label)) | ||
|
|
||
| function setSidebarCollapsed(nextValue: boolean): void { | ||
| if (isSidebarCollapsed.value === nextValue) return | ||
| isSidebarCollapsed.value = nextValue | ||
| if (typeof window !== 'undefined') { | ||
| window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, nextValue ? '1' : '0') | ||
| } | ||
| } | ||
|
|
||
| function toggleSidebarSearch(): void { | ||
| isSidebarSearchVisible.value = !isSidebarSearchVisible.value | ||
| if (isSidebarSearchVisible.value) { | ||
| queueMicrotask(() => sidebarSearchInputRef.value?.focus()) | ||
| } else { | ||
| sidebarSearchQuery.value = '' | ||
| } | ||
| } | ||
|
|
||
| function clearSidebarSearch(): void { | ||
| sidebarSearchQuery.value = '' | ||
| sidebarSearchInputRef.value?.focus() | ||
| } | ||
|
|
||
| function onSidebarSearchKeydown(event: KeyboardEvent): void { | ||
| if (event.key === 'Escape') { | ||
| isSidebarSearchVisible.value = false | ||
| sidebarSearchQuery.value = '' | ||
| } | ||
| } | ||
|
|
||
| function toggleSendWithEnter(): void { | ||
| sendWithEnter.value = !sendWithEnter.value | ||
| window.localStorage.setItem(SEND_WITH_ENTER_KEY, sendWithEnter.value ? '1' : '0') | ||
| } | ||
|
|
||
| function cycleInProgressSendMode(): void { | ||
| inProgressSendMode.value = inProgressSendMode.value === 'steer' ? 'queue' : 'steer' | ||
| window.localStorage.setItem(IN_PROGRESS_SEND_MODE_KEY, inProgressSendMode.value) | ||
| } | ||
|
|
||
| function cycleDarkMode(): void { | ||
| const order: Array<'system' | 'light' | 'dark'> = ['system', 'light', 'dark'] | ||
| const idx = order.indexOf(darkMode.value) | ||
| darkMode.value = order[(idx + 1) % order.length] | ||
| window.localStorage.setItem(DARK_MODE_KEY, darkMode.value) | ||
| applyDarkMode(darkMode.value) | ||
| } |
There was a problem hiding this comment.
1. Dark mode not applied 🐞 Bug ≡ Correctness
useAppShellSettings loads the persisted darkMode but never applies it during initialization, so the HTML dark class is only updated after the user clicks the theme toggle. App.vue no longer applies dark mode on mount either, so the UI can start (and remain) in the wrong theme.
Agent Prompt
## Issue description
Dark mode is persisted and read, but never applied on startup. The only call site for `applyDarkMode(...)` is inside `cycleDarkMode()`, so users won’t see their stored theme reflected until they interact with the setting.
## Issue Context
Before the refactor, `App.vue` applied dark mode on mount. After the refactor, `useAppShellSettings` owns `darkMode`, but it doesn’t call `applyDarkMode(darkMode.value)` during setup/onMounted.
## Fix Focus Areas
- src/features/app-shell/useAppShellSettings.ts[37-115]
- src/App.vue[1358-1370]
## Implementation notes
- In `useAppShellSettings`, call `applyDarkMode(darkMode.value)` once during initialization (ideally in `onMounted`, or guarded by `typeof document/window !== 'undefined'`).
- Optionally add a `watch(darkMode, ...)` to apply whenever the preference changes (even if some other code path updates it).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Summary
Testing