cc-haha/desktop/src/stores/uiStore.ts
程序员阿江(Relakkes) 071975e0cc Improve desktop failure diagnosis with exportable logs
Desktop failures were previously hard to debug from issue reports because server-side and CLI startup details were only partially visible in the UI. This adds a dedicated cc-haha diagnostics store with sanitized structured events, runtime error summaries, a Settings diagnostics view, and an exportable bundle that users can attach to reports.

Constraint: Diagnostic exports must not include chat content, file contents, full environment variables, API keys, bearer tokens, cookies, or OAuth tokens.
Rejected: Export raw server logs | easier to debug but too likely to leak secrets and private workspace data.
Rejected: Keep diagnostics only in transient UI errors | still leaves maintainers unable to diagnose later GitHub issues.
Confidence: high
Scope-risk: moderate
Directive: Do not add raw transcript, prompt, attachment, or environment dumps to diagnostics without a separate privacy review.
Tested: ANTHROPIC_API_KEY=test-key bun test src/server/__tests__/diagnostics-service.test.ts
Tested: ANTHROPIC_API_KEY=test-key bun test src/server/__tests__/conversation-service.test.ts
Tested: ANTHROPIC_API_KEY=test-key bun test src/server/__tests__/conversations.test.ts
Tested: cd desktop && bun run test src/__tests__/diagnosticsSettings.test.tsx --run
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: agent-browser E2E on local server/dev UI for CLI startup failure, provider test failure, diagnostics tab, copy summary, export bundle, and tar/secret scan
Not-tested: Destructive clear-logs button in browser E2E; local deletion was intentionally not clicked.
2026-05-02 12:41:31 +08:00

113 lines
2.9 KiB
TypeScript

import { create } from 'zustand'
import type { ThemeMode } from '../types/settings'
const THEME_STORAGE_KEY = 'cc-haha-theme'
function getStoredTheme(): ThemeMode {
try {
const stored = localStorage.getItem(THEME_STORAGE_KEY)
if (stored === 'light' || stored === 'dark') return stored
} catch { /* localStorage unavailable */ }
return 'light'
}
export function applyTheme(theme: ThemeMode) {
if (typeof document === 'undefined') return
document.documentElement.setAttribute('data-theme', theme)
document.documentElement.style.colorScheme = theme
}
export function initializeTheme() {
applyTheme(getStoredTheme())
}
export type Toast = {
id: string
type: 'success' | 'error' | 'warning' | 'info'
message: string
duration?: number
}
export type SettingsTab =
| 'providers'
| 'permissions'
| 'general'
| 'adapters'
| 'terminal'
| 'mcp'
| 'agents'
| 'skills'
| 'plugins'
| 'computerUse'
| 'diagnostics'
| 'about'
type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'
type UIStore = {
theme: ThemeMode
sidebarOpen: boolean
activeView: ActiveView
pendingSettingsTab: SettingsTab | null
activeModal: string | null
toasts: Toast[]
setTheme: (theme: ThemeMode) => void
toggleTheme: () => void
toggleSidebar: () => void
setSidebarOpen: (open: boolean) => void
setActiveView: (view: ActiveView) => void
setPendingSettingsTab: (tab: SettingsTab | null) => void
openModal: (id: string) => void
closeModal: () => void
addToast: (toast: Omit<Toast, 'id'>) => void
removeToast: (id: string) => void
}
let toastCounter = 0
export const useUIStore = create<UIStore>((set) => ({
theme: getStoredTheme(),
sidebarOpen: true,
activeView: 'code',
pendingSettingsTab: null,
activeModal: null,
toasts: [],
setTheme: (theme) => {
applyTheme(theme)
try { localStorage.setItem(THEME_STORAGE_KEY, theme) } catch { /* noop */ }
set({ theme })
},
toggleTheme: () => {
set((state) => {
const next = state.theme === 'light' ? 'dark' : 'light'
applyTheme(next)
try { localStorage.setItem(THEME_STORAGE_KEY, next) } catch { /* noop */ }
return { theme: next }
})
},
toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open }),
setActiveView: (view) => set({ activeView: view }),
setPendingSettingsTab: (tab) => set({ pendingSettingsTab: tab }),
openModal: (id) => set({ activeModal: id }),
closeModal: () => set({ activeModal: null }),
addToast: (toast) => {
const id = `toast-${++toastCounter}`
set((s) => ({ toasts: [...s.toasts, { ...toast, id }] }))
// Auto-remove after duration
const duration = toast.duration ?? 4000
if (duration > 0) {
setTimeout(() => {
set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) }))
}, duration)
}
},
removeToast: (id) => set((s) => ({ toasts: s.toasts.filter((t) => t.id !== id) })),
}))