mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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.
113 lines
2.9 KiB
TypeScript
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) })),
|
|
}))
|