mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
The CLI already emits memory_saved events and stores Markdown memory files, but the desktop app had no usable surface for seeing or editing those writes. This adds a project memory API, a Settings memory editor, chat memory event cards, and routing from /memory or /context into the memory UI. Constraint: Memory files live under Claude project storage and must remain plain Markdown editable by users. Rejected: Only expose raw filesystem links | users need an in-app review and edit flow from chat. Confidence: high Scope-risk: moderate Directive: Keep memory storage project-scoped and preserve unknown Markdown content when editing. Tested: bun test src/server/__tests__/ws-memory-events.test.ts src/server/__tests__/memory.test.ts Tested: bun run check:server Tested: bun run check:desktop Tested: agent-browser E2E for chat memory card, Open Memory navigation, and responsive Markdown editor layout Not-tested: Live model auto-memory trigger rate with real provider credentials
121 lines
3.3 KiB
TypeScript
121 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 'light'
|
|
}
|
|
|
|
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'
|
|
| 'permissions'
|
|
| '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] ?? '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 }),
|
|
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) })),
|
|
}))
|