Skip to content

Refactor app shell and thread state modules#89

Open
friuns2 wants to merge 1 commit into
mainfrom
codex/thread-state-deep-modules
Open

Refactor app shell and thread state modules#89
friuns2 wants to merge 1 commit into
mainfrom
codex/thread-state-deep-modules

Conversation

@friuns2
Copy link
Copy Markdown
Owner

@friuns2 friuns2 commented Apr 27, 2026

Summary

  • move app shell orchestration into feature-rooted modules under src/features/app-shell
  • extract thread-state storage, merge, and message helpers under src/features/thread-state while keeping useDesktopState stable
  • update tests.md with manual verification coverage for both refactor slices

Testing

  • pnpm run build:frontend
  • pnpm run test:unit

@friuns2 friuns2 marked this pull request as draft May 12, 2026 12:59
@friuns2 friuns2 marked this pull request as ready for review May 12, 2026 12:59
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Refactor app shell and thread state into feature-rooted deep modules

✨ Enhancement

Grey Divider

Walkthroughs

Description
• 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
Diagram
flowchart 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
Loading

Grey Divider

File Changes

1. src/composables/useDesktopState.ts Refactoring +73/-958

Extract thread-state utilities into feature modules

• Removed 900+ lines of helper functions and constants that were moved to feature modules
• Added imports from three new feature modules: thread-state/storage, thread-state/merge, and
 thread-state/messages
• Kept core composable logic intact while delegating storage, merging, and message utilities to
 dedicated modules
• Maintained backward compatibility of the useDesktopState API

src/composables/useDesktopState.ts


2. src/features/app-shell/useAppShellProjects.ts ✨ Enhancement +758/-0

New app-shell projects management composable

• New 758-line composable for managing project-related state and operations
• Handles project folder selection, worktree creation, and git branch management
• Provides UI state for folder browsing, creation dialogs, and project initialization
• Includes watchers for reactive updates to folder options, runtime selection, and route changes

src/features/app-shell/useAppShellProjects.ts


3. src/features/app-shell/useAppShellSettings.ts ✨ Enhancement +454/-0

New app-shell settings management composable

• New 454-line composable for application settings management
• Handles UI preferences (sidebar, chat width, dark mode, dictation settings)
• Manages provider configuration (Codex, OpenRouter, OpenCode Zen, custom endpoints)
• Provides localStorage persistence and settings UI state

src/features/app-shell/useAppShellSettings.ts


View more (13)
4. src/features/thread-state/storage.ts ✨ Enhancement +440/-0

Extract thread-state storage utilities module

• New 440-line module extracting all thread-state storage utilities from useDesktopState
• Exports constants like GLOBAL_SERVER_REQUEST_SCOPE, MODEL_FALLBACK_ID,
 REASONING_EFFORT_OPTIONS
• Provides functions for loading/saving thread metadata (scroll state, token usage, terminal state,
 selected thread)
• Includes model and collaboration mode selection persistence with legacy migration support

src/features/thread-state/storage.ts


5. src/features/app-shell/useAppShellAccounts.ts ✨ Enhancement +291/-0

New app-shell accounts management composable

• New 291-line composable for account management functionality
• Handles account switching, removal, and quota display
• Provides account state polling and error handling
• Includes UI helpers for account card interactions and status formatting

src/features/app-shell/useAppShellAccounts.ts


6. src/features/app-shell/useAppShellIntegrations.ts ✨ Enhancement +132/-0

New app-shell integrations management composable

• New 132-line composable for managing integrations (Telegram bot configuration)
• Handles Telegram bot token and allowed user IDs configuration
• Provides status tracking and first-launch plugins card preference
• Includes validation and error handling for integration setup

src/features/app-shell/useAppShellIntegrations.ts


7. src/features/app-shell/types.ts ✨ Enhancement +16/-0

New app-shell shared type definitions

• New 16-line file defining shared types for app-shell feature module
• Exports ChatWidthMode, DirectoryTryItemPayload, and ChatWidthPreset types
• Provides type definitions for UI configuration and directory browsing

src/features/app-shell/types.ts


8. src/features/app-shell/index.ts ✨ Enhancement +8/-0

New app-shell feature module barrel export

• New 8-line barrel export file for app-shell feature module
• Re-exports all public APIs from constants, types, and composables
• Provides clean import interface for app-shell functionality

src/features/app-shell/index.ts


9. src/features/thread-state/index.ts ✨ Enhancement +3/-0

New thread-state feature module barrel export

• New 3-line barrel export file for thread-state feature module
• Re-exports storage, merge, and messages submodules
• Provides unified import interface for thread-state utilities

src/features/thread-state/index.ts


10. src/features/thread-state/messages.ts ✨ Enhancement +284/-0

Message handling and merging utilities for thread state

• Extracts message handling utilities including parsing, formatting, and comparison functions for
 turn summaries, activities, and errors
• Implements message merging logic with field-level equality checks and redundant message removal
 for live agent updates
• Provides helper functions for message array operations, upsert, and text normalization
• Defines TypeScript types for turn state tracking (TurnSummaryState, TurnActivityState,
 TurnErrorState, TurnStartedInfo, TurnCompletedInfo)

src/features/thread-state/messages.ts


11. src/features/thread-state/merge.ts ✨ Enhancement +205/-0

Thread and project group merging and ordering utilities

• Extracts thread and project group merging logic with field-level equality checks to preserve
 object references
• Implements thread group ordering by project name with support for reordering and omitting keys
 from records
• Provides utilities for flattening thread groups, pruning state maps, and handling in-progress
 threads during merge
• Includes helpers for thread title generation (optimistic and forked variants) and project name
 conversion

src/features/thread-state/merge.ts


12. src/features/app-shell/useAppShellViewport.ts ✨ Enhancement +188/-0

App shell viewport and terminal state management composable

• Composable for managing viewport state including visual/layout viewport dimensions and virtual
 keyboard detection on mobile
• Handles terminal focus state, keyboard fallback timing, and terminal visibility for both home and
 thread composers
• Implements mobile resume synchronization logic with configurable hidden duration threshold
• Manages event listeners for resize, focus, visibility, and pageshow events with proper cleanup on
 unmount

src/features/app-shell/useAppShellViewport.ts


13. src/features/app-shell/useAppShellRouting.ts ✨ Enhancement +139/-0

App shell routing and thread selection synchronization composable

• Composable for managing route synchronization between router state and selected thread/route
 context
• Implements thread search with debounced server queries and sidebar search binding
• Handles initialization flow including router readiness, thread priming, and polling startup
• Manages route watchers to sync thread selection and prevent race conditions with pending sync
 flags

src/features/app-shell/useAppShellRouting.ts


14. src/features/app-shell/constants.ts ⚙️ Configuration changes +133/-0

App shell configuration constants and storage keys

• Defines storage keys for local preferences (sidebar collapse, send mode, dark mode, dictation,
 chat width)
• Exports chat width presets configuration with column and card max widths for standard, wide, and
 extra-wide modes
• Provides comprehensive mapping of language codes to Whisper speech-recognition language names
• Includes mobile resume reload timing constant (MOBILE_RESUME_RELOAD_MIN_HIDDEN_MS)

src/features/app-shell/constants.ts


15. tests.md 🧪 Tests +75/-0

Manual verification test coverage for deep-module refactors

• Adds comprehensive manual verification test case for app-shell deep-module refactor covering
 settings, routing, accounts, projects, and viewport behavior
• Adds manual verification test case for thread-state deep-module refactor covering persistence,
 message merging, and state reconciliation
• Both test cases verify that internal module extraction does not introduce user-visible behavior
 changes
• Includes prerequisites, step-by-step procedures, expected results, and cleanup instructions for
 both refactors

tests.md


16. src/App.vue ✨ Enhancement +304/-1952

Refactor App.vue to use feature-rooted composables

• Extracted app shell orchestration logic into feature-rooted composables (useAppShellSettings,
 useAppShellAccounts, useAppShellProjects, useAppShellRouting, useAppShellIntegrations,
 useAppShellViewport)
• Removed ~1000 lines of inline state management, helper functions, and event handlers from the main
 component
• Simplified imports by consolidating related functionality into dedicated modules under
 src/features/app-shell
• Refactored lifecycle hooks and watchers to delegate to composable-level implementations

src/App.vue


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 12, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0)

Grey Divider


Action required

1. Dark mode not applied 🐞 Bug ≡ Correctness
Description
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.
Code

src/features/app-shell/useAppShellSettings.ts[R48-115]

+  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)
+  }
Evidence
The settings composable initializes darkMode, but the only invocation of applyDarkMode is inside
cycleDarkMode(). App.vue’s onMounted sequence no longer applies dark mode, so there is no
startup path that sets document.documentElement’s dark class based on the stored preference.

src/features/app-shell/useAppShellSettings.ts[36-115]
src/App.vue[1356-1370]
src/features/app-shell/useAppShellSettings.ts[400-410]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## 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



Remediation recommended

2. System theme changes ignored 🐞 Bug ≡ Correctness
Description
When darkMode is 'system', applyDarkMode reads matchMedia('(prefers-color-scheme: dark)')
once but no 'change' listener is registered, so OS theme changes won’t update the UI while the app
is open. This is a regression from the previous behavior where system theme changes were listened
to.
Code

src/features/app-shell/useAppShellSettings.ts[R400-410]

+function applyDarkMode(darkMode: 'system' | 'light' | 'dark'): void {
+  const root = document.documentElement
+  if (darkMode === 'dark') {
+    root.classList.add('dark')
+  } else if (darkMode === 'light') {
+    root.classList.remove('dark')
+  } else {
+    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
+    root.classList.toggle('dark', prefersDark)
+  }
+}
Evidence
applyDarkMode computes the system preference via matchMedia(...).matches, but there is no code
to subscribe to changes, so subsequent OS theme flips won’t trigger reapplication of the dark
class.

src/features/app-shell/useAppShellSettings.ts[400-410]
src/features/app-shell/useAppShellSettings.ts[36-115]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In system theme mode, the UI should react to OS light/dark changes. Current code checks the media query but doesn’t subscribe to changes, so the app can get stuck in the wrong theme until refresh or manual toggle.

## Issue Context
`applyDarkMode('system')` relies on `window.matchMedia('(prefers-color-scheme: dark)')`. To remain correct over time, a `MediaQueryList` `'change'` listener must be registered when `darkMode.value === 'system'` and removed otherwise.

## Fix Focus Areas
- src/features/app-shell/useAppShellSettings.ts[48-115]
- src/features/app-shell/useAppShellSettings.ts[400-410]

## Implementation notes
- Create a `MediaQueryList` via `window.matchMedia('(prefers-color-scheme: dark)')`.
- Add a listener that calls `applyDarkMode('system')` (or `applyDarkMode(darkMode.value)` after verifying it’s still `'system'`).
- Use `watch(darkMode, ...)` to (a) apply immediately and (b) attach/detach the listener when entering/leaving `'system'` mode.
- Ensure cleanup on unmount to avoid leaked listeners.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +48 to +115
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants