cc-haha/desktop/src/stores/uiStore.ts
程序员阿江(Relakkes) 3f4d731cc7 fix: scope permission mode to sessions
Permission mode is a per-session runtime choice, while scheduled tasks cannot rely on a human approval loop. This removes the General settings permission surface, persists session permission metadata, and forces scheduled tasks to run with bypass permissions. Runtime permission changes now handle both directions across bypass boundaries, including startup/prewarm races, by persisting first and restarting only when the CLI launch mode must change.

Constraint: Scheduled tasks must be able to execute without a human standing by for authorization.

Constraint: The CLI only honors bypass permissions when launched with the skip-permissions flag, so switching to or from bypass requires a restart.

Rejected: Keep a global General permission default | it can leak across sessions and scheduled runs in ways the user cannot reason about.

Confidence: high

Scope-risk: broad

Directive: Do not reintroduce a global UI permission selector without proving it cannot affect unrelated sessions or automations.

Tested: bun test src/server/__tests__/conversations.test.ts -t "permission" --timeout 30000 (10 pass, 0 fail)

Tested: bun run check:server (858 pass, 0 fail)

Tested: cd desktop && bun run check:desktop (760 tests plus production build passed)

Tested: desktop/scripts/build-macos-arm64.sh and codesign verification passed

Tested: Real DeepSeek smoke validated plan, bypass, and scheduled task permission behavior

Not-tested: Did not repeat the full DeepSeek smoke after the final startup-race hardening; mock WebSocket permission regression and full server gate were rerun after that change.

Fixes #632
2026-05-31 17:40:45 +08:00

120 lines
3.3 KiB
TypeScript

import { create } from 'zustand'
import { isThemeMode, THEME_MODES, type ThemeMode } from '../types/settings'
const THEME_STORAGE_KEY = 'cc-haha-theme'
function getStoredTheme(): ThemeMode {
try {
const stored = localStorage.getItem(THEME_STORAGE_KEY)
if (isThemeMode(stored)) return stored
} catch { /* localStorage unavailable */ }
return 'white'
}
export function applyTheme(theme: ThemeMode) {
if (typeof document === 'undefined') return
document.documentElement.setAttribute('data-theme', theme)
document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light'
}
export function initializeTheme() {
applyTheme(getStoredTheme())
}
export type Toast = {
id: string
type: 'success' | 'error' | 'warning' | 'info'
message: string
duration?: number
}
export type SettingsTab =
| 'providers'
| 'activity'
| 'general'
| 'h5Access'
| 'adapters'
| 'terminal'
| 'mcp'
| 'agents'
| 'skills'
| 'memory'
| 'plugins'
| 'computerUse'
| 'diagnostics'
| 'about'
type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'
type UIStore = {
theme: ThemeMode
sidebarOpen: boolean
activeView: ActiveView
pendingSettingsTab: SettingsTab | null
pendingMemoryPath: string | 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
setPendingMemoryPath: (path: string | 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,
pendingMemoryPath: null,
activeModal: null,
toasts: [],
setTheme: (theme) => {
applyTheme(theme)
try { localStorage.setItem(THEME_STORAGE_KEY, theme) } catch { /* noop */ }
set({ theme })
},
toggleTheme: () => {
set((state) => {
const currentIndex = THEME_MODES.indexOf(state.theme)
const next = THEME_MODES[(currentIndex + 1) % THEME_MODES.length] ?? 'white'
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 }),
setPendingMemoryPath: (path) => set({ pendingMemoryPath: path }),
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) })),
}))