diff --git a/desktop/index.html b/desktop/index.html index 05d212eb..c03e7509 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -6,8 +6,8 @@ Claude Code Companion diff --git a/desktop/src/__tests__/generalSettings.test.tsx b/desktop/src/__tests__/generalSettings.test.tsx index f06f23ca..604a1a37 100644 --- a/desktop/src/__tests__/generalSettings.test.tsx +++ b/desktop/src/__tests__/generalSettings.test.tsx @@ -8,6 +8,7 @@ import { useUIStore } from '../stores/uiStore' import { useUpdateStore } from '../stores/updateStore' import type { SavedProvider } from '../types/provider' import type { ProviderPreset } from '../types/providerPreset' +import type { ThemeMode } from '../types/settings' const MOCK_DELETE_PROVIDER = vi.fn() const MOCK_GET_SETTINGS = vi.fn() @@ -138,6 +139,7 @@ describe('Settings > General tab', () => { useSettingsStore.setState({ locale: 'en', + theme: 'light', thinkingEnabled: true, skipWebFetchPreflight: true, desktopNotificationsEnabled: true, @@ -153,6 +155,9 @@ describe('Settings > General tab', () => { setThinkingEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ thinkingEnabled: enabled }) }), + setTheme: vi.fn().mockImplementation(async (theme: ThemeMode) => { + useSettingsStore.setState({ theme }) + }), setSkipWebFetchPreflight: vi.fn().mockImplementation(async (enabled: boolean) => { useSettingsStore.setState({ skipWebFetchPreflight: enabled }) }), @@ -227,6 +232,25 @@ describe('Settings > General tab', () => { expect(toggle).toBeChecked() }) + it('offers the pure white appearance theme', () => { + render() + + fireEvent.click(screen.getByText('General')) + fireEvent.click(screen.getByRole('button', { name: 'Pure White' })) + + expect(useSettingsStore.getState().setTheme).toHaveBeenCalledWith('white') + }) + + it('marks the pure white appearance theme as selected', () => { + useSettingsStore.setState({ theme: 'white' }) + render() + + fireEvent.click(screen.getByText('General')) + + expect(screen.getByRole('button', { name: 'Pure White' })).toHaveAttribute('aria-pressed', 'true') + expect(screen.getByRole('button', { name: 'Warm Classic' })).toHaveAttribute('aria-pressed', 'false') + }) + it('opens the Token usage tab from Settings navigation above Diagnostics', () => { render() diff --git a/desktop/src/components/chat/LocalSlashCommandPanel.tsx b/desktop/src/components/chat/LocalSlashCommandPanel.tsx index 65612bb8..ef0aa179 100644 --- a/desktop/src/components/chat/LocalSlashCommandPanel.tsx +++ b/desktop/src/components/chat/LocalSlashCommandPanel.tsx @@ -32,11 +32,11 @@ type Translate = ReturnType function toneForStatus(status: McpServerRecord['status']) { switch (status) { case 'connected': - return 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' + return 'bg-[var(--color-inspector-success-bg)] text-[var(--color-inspector-success)] border-[var(--color-inspector-border)]' case 'needs-auth': - return 'bg-amber-500/10 text-amber-600 border-amber-500/20' + return 'bg-[var(--color-surface-container-low)] text-[var(--color-warning)] border-[var(--color-border)]' case 'failed': - return 'bg-rose-500/10 text-rose-600 border-rose-500/20' + return 'bg-[var(--color-inspector-danger-bg)] text-[var(--color-inspector-danger)] border-[var(--color-inspector-border)]' case 'disabled': return 'bg-[var(--color-surface-hover)] text-[var(--color-text-secondary)] border-[var(--color-border)]' default: @@ -114,7 +114,7 @@ function EmptyState({ title, body }: { title: string; body: string }) { function ErrorState({ message }: { message: string }) { return ( -
+
{message}
) @@ -270,7 +270,7 @@ function UsageTab({
)} {usage.hasUnknownModelCost && ( -
+
{t('slash.inspector.usage.unknownCost')}
)} @@ -593,9 +593,7 @@ function StatusTab({ {mcpServers.map((server) => (
diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 46de2bb2..7589cc80 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -677,9 +677,10 @@ export const en = { // Settings > General 'settings.general.appearanceTitle': 'Appearance', - 'settings.general.appearanceDescription': 'Switch between the original light workspace and the new dark workspace.', - 'settings.general.appearance.light': 'Light', + 'settings.general.appearanceDescription': 'Switch between the warm classic workspace, dark workspace, and a pure white workspace.', + 'settings.general.appearance.light': 'Warm Classic', 'settings.general.appearance.dark': 'Dark', + 'settings.general.appearance.white': 'Pure White', 'settings.general.languageTitle': 'Language', 'settings.general.languageDescription': 'Choose the display language for the application.', 'settings.general.responseLangTitle': 'Response Language', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index c2e46a9e..d8ec51f9 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -679,9 +679,10 @@ export const zh: Record = { // Settings > General 'settings.general.appearanceTitle': '配色主题', - 'settings.general.appearanceDescription': '在亮色与暗色工作区之间切换,不影响原有亮色主题。', - 'settings.general.appearance.light': '亮色', + 'settings.general.appearanceDescription': '在经典暖色、暗色与纯白工作区之间切换。', + 'settings.general.appearance.light': '经典暖色', 'settings.general.appearance.dark': '暗色', + 'settings.general.appearance.white': '纯白', 'settings.general.languageTitle': '语言', 'settings.general.languageDescription': '选择应用程序的显示语言。', 'settings.general.responseLangTitle': '回复语言', diff --git a/desktop/src/lib/desktopRuntime.test.ts b/desktop/src/lib/desktopRuntime.test.ts index 867d68d3..0e39c865 100644 --- a/desktop/src/lib/desktopRuntime.test.ts +++ b/desktop/src/lib/desktopRuntime.test.ts @@ -158,6 +158,26 @@ describe('desktopRuntime browser H5 bootstrap', () => { }) }) + it('prefers an explicit Vite desktop server URL over a remembered H5 server URL', async () => { + clientMocks.defaultBaseUrl = 'http://127.0.0.1:55189' + clientMocks.explicitDefaultBaseUrl = true + window.history.pushState({}, '', '/') + window.localStorage.setItem(H5_SERVER_URL_STORAGE_KEY, 'http://192.168.0.102:3456') + window.localStorage.setItem(H5_TOKEN_STORAGE_KEY, 'stale-h5-token') + globalThis.fetch = vi.fn().mockResolvedValue( + healthOkResponse(), + ) as typeof fetch + + await expect(initializeDesktopServerUrl()).resolves.toBe('http://127.0.0.1:55189') + + expect(clientMocks.setBaseUrl).toHaveBeenLastCalledWith('http://127.0.0.1:55189') + expect(clientMocks.setAuthToken).toHaveBeenLastCalledWith(null) + expect(clientMocks.postVerify).not.toHaveBeenCalled() + expect(globalThis.fetch).toHaveBeenCalledWith('http://127.0.0.1:55189/api/status', { + cache: 'no-store', + }) + }) + it('normalizes unreachable remote browser startup into a recoverable H5 error', async () => { vi.useFakeTimers() globalThis.fetch = vi.fn().mockRejectedValue(new TypeError('Failed to fetch')) as typeof fetch diff --git a/desktop/src/lib/desktopRuntime.ts b/desktop/src/lib/desktopRuntime.ts index 07d850ae..f39ff09b 100644 --- a/desktop/src/lib/desktopRuntime.ts +++ b/desktop/src/lib/desktopRuntime.ts @@ -137,10 +137,11 @@ async function initializeBrowserServerUrl(fallbackUrl: string) { const queryUrl = query?.get('serverUrl') ?? null const queryToken = normalizeToken(query?.get('h5Token') ?? query?.get('token')) const stored = readStoredH5Connection() + const configuredUrl = getConfiguredBrowserServerUrl(fallbackUrl) const requestedUrl = normalizeServerUrl(queryUrl) ?? + configuredUrl ?? stored.serverUrl ?? - getConfiguredBrowserServerUrl(fallbackUrl) ?? fallbackUrl const token = queryToken ?? stored.token const browserH5Runtime = requiresH5AuthForServerUrl(requestedUrl) diff --git a/desktop/src/lib/persistenceMigrations.test.ts b/desktop/src/lib/persistenceMigrations.test.ts index 107ca10e..a461ed91 100644 --- a/desktop/src/lib/persistenceMigrations.test.ts +++ b/desktop/src/lib/persistenceMigrations.test.ts @@ -56,6 +56,15 @@ describe('desktop persistence migrations', () => { expect(window.localStorage.getItem('cc-haha-theme')).toBeNull() }) + test('preserves the pure white theme as a valid persisted theme', () => { + window.localStorage.setItem('cc-haha-theme', 'white') + + const report = runDesktopPersistenceMigrations() + + expect(report.migratedKeys).not.toContain('cc-haha-theme') + expect(window.localStorage.getItem('cc-haha-theme')).toBe('white') + }) + test('does not throw if schema version persistence is blocked', () => { const storage = { getItem: window.localStorage.getItem.bind(window.localStorage), diff --git a/desktop/src/lib/persistenceMigrations.ts b/desktop/src/lib/persistenceMigrations.ts index a8c4cf7d..5c889ebd 100644 --- a/desktop/src/lib/persistenceMigrations.ts +++ b/desktop/src/lib/persistenceMigrations.ts @@ -1,3 +1,5 @@ +import { THEME_MODES } from '../types/settings' + export const CURRENT_DESKTOP_PERSISTENCE_SCHEMA_VERSION = 1 export const DESKTOP_PERSISTENCE_VERSION_KEY = 'cc-haha.persistence.schemaVersion' @@ -138,7 +140,7 @@ export function runDesktopPersistenceMigrations(storage: StorageLike | null = ge runMigrationStep(report, TAB_STORAGE_KEY, () => migrateTabs(storage, report)) runMigrationStep(report, SESSION_RUNTIME_STORAGE_KEY, () => migrateSessionRuntime(storage, report)) - runMigrationStep(report, THEME_STORAGE_KEY, () => normalizeEnumKey(storage, THEME_STORAGE_KEY, ['light', 'dark'], report)) + runMigrationStep(report, THEME_STORAGE_KEY, () => normalizeEnumKey(storage, THEME_STORAGE_KEY, [...THEME_MODES], report)) runMigrationStep(report, LOCALE_STORAGE_KEY, () => normalizeEnumKey(storage, LOCALE_STORAGE_KEY, ['zh', 'en'], report)) try { storage.setItem(DESKTOP_PERSISTENCE_VERSION_KEY, String(CURRENT_DESKTOP_PERSISTENCE_SCHEMA_VERSION)) diff --git a/desktop/src/pages/ActivitySettings.tsx b/desktop/src/pages/ActivitySettings.tsx index 3cbd71e3..88d0c594 100644 --- a/desktop/src/pages/ActivitySettings.tsx +++ b/desktop/src/pages/ActivitySettings.tsx @@ -31,10 +31,10 @@ const HEAT_CELL_MAX = 22 const TOOLTIP_WIDTH = 172 const HEAT_COLORS = [ 'var(--color-surface-container)', - '#f5d4c4', - '#eea47f', - '#df6b47', - '#bd432c', + 'var(--color-primary-fixed)', + 'var(--color-primary-fixed-dim)', + 'var(--color-primary-container)', + 'var(--color-primary)', ] const DATE_LOCALES: Record = { en: 'en-US', @@ -401,11 +401,11 @@ export function ActivitySettings() {