mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 22:28:48 +03:00
claude-code
This commit is contained in:
397
web/lib/store.ts
Normal file
397
web/lib/store.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
import { nanoid } from "nanoid";
|
||||
import type { Conversation, Message, AppSettings, ConversationTag } from "./types";
|
||||
import { DEFAULT_MODEL } from "./constants";
|
||||
|
||||
const DEFAULT_SETTINGS: AppSettings = {
|
||||
// General
|
||||
theme: "dark",
|
||||
fontSize: { chat: 14, code: 13 },
|
||||
sendOnEnter: true,
|
||||
showTimestamps: false,
|
||||
compactMode: false,
|
||||
|
||||
// Model
|
||||
model: DEFAULT_MODEL,
|
||||
maxTokens: 8096,
|
||||
temperature: 1.0,
|
||||
systemPrompt: "",
|
||||
|
||||
// API
|
||||
apiUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001",
|
||||
apiKey: "",
|
||||
streamingEnabled: true,
|
||||
|
||||
// Permissions
|
||||
permissions: {
|
||||
autoApprove: {
|
||||
file_read: false,
|
||||
file_write: false,
|
||||
bash: false,
|
||||
web_search: false,
|
||||
},
|
||||
restrictedDirs: [],
|
||||
},
|
||||
|
||||
// MCP
|
||||
mcpServers: [],
|
||||
|
||||
// Keybindings
|
||||
keybindings: {
|
||||
"new-conversation": "Ctrl+Shift+N",
|
||||
"send-message": "Enter",
|
||||
"focus-input": "Ctrl+L",
|
||||
"toggle-sidebar": "Ctrl+B",
|
||||
"open-settings": "Ctrl+,",
|
||||
"command-palette": "Ctrl+K",
|
||||
},
|
||||
|
||||
// Privacy
|
||||
telemetryEnabled: false,
|
||||
};
|
||||
|
||||
interface ChatState {
|
||||
conversations: Conversation[];
|
||||
activeConversationId: string | null;
|
||||
settings: AppSettings;
|
||||
settingsOpen: boolean;
|
||||
|
||||
// Sidebar state
|
||||
sidebarOpen: boolean;
|
||||
sidebarWidth: number;
|
||||
sidebarTab: "chats" | "history" | "files" | "settings";
|
||||
pinnedIds: string[];
|
||||
searchQuery: string;
|
||||
|
||||
// Search & selection state (not persisted)
|
||||
isSearchOpen: boolean;
|
||||
selectedConversationIds: string[];
|
||||
|
||||
// Persisted search/tag state
|
||||
recentSearches: string[];
|
||||
tags: ConversationTag[];
|
||||
|
||||
// Conversation actions
|
||||
createConversation: () => string;
|
||||
setActiveConversation: (id: string) => void;
|
||||
deleteConversation: (id: string) => void;
|
||||
renameConversation: (id: string, title: string) => void;
|
||||
updateConversation: (id: string, updates: Partial<Pick<Conversation, "title" | "model">>) => void;
|
||||
addMessage: (conversationId: string, message: Omit<Message, "id" | "createdAt">) => string;
|
||||
updateMessage: (conversationId: string, messageId: string, updates: Partial<Message>) => void;
|
||||
/** Keep only the first `keepCount` messages in the conversation. */
|
||||
truncateMessages: (conversationId: string, keepCount: number) => void;
|
||||
getActiveConversation: () => Conversation | null;
|
||||
|
||||
// Bulk actions
|
||||
toggleSelectConversation: (id: string) => void;
|
||||
clearSelection: () => void;
|
||||
bulkDeleteConversations: (ids: string[]) => void;
|
||||
|
||||
// Tag actions
|
||||
createTag: (label: string, color: string) => string;
|
||||
deleteTag: (id: string) => void;
|
||||
updateTag: (id: string, updates: Partial<Pick<ConversationTag, "label" | "color">>) => void;
|
||||
tagConversation: (conversationId: string, tagId: string) => void;
|
||||
untagConversation: (conversationId: string, tagId: string) => void;
|
||||
|
||||
// Search actions
|
||||
openSearch: () => void;
|
||||
closeSearch: () => void;
|
||||
addRecentSearch: (query: string) => void;
|
||||
clearRecentSearches: () => void;
|
||||
|
||||
// Settings actions
|
||||
updateSettings: (settings: Partial<AppSettings>) => void;
|
||||
resetSettings: (section?: string) => void;
|
||||
openSettings: () => void;
|
||||
closeSettings: () => void;
|
||||
|
||||
// Sidebar actions
|
||||
toggleSidebar: () => void;
|
||||
setSidebarWidth: (w: number) => void;
|
||||
setSidebarTab: (tab: "chats" | "history" | "files" | "settings") => void;
|
||||
pinConversation: (id: string) => void;
|
||||
setSearchQuery: (q: string) => void;
|
||||
}
|
||||
|
||||
export const useChatStore = create<ChatState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
conversations: [],
|
||||
activeConversationId: null,
|
||||
settings: DEFAULT_SETTINGS,
|
||||
settingsOpen: false,
|
||||
|
||||
sidebarOpen: true,
|
||||
sidebarWidth: 280,
|
||||
sidebarTab: "chats",
|
||||
pinnedIds: [],
|
||||
searchQuery: "",
|
||||
|
||||
isSearchOpen: false,
|
||||
selectedConversationIds: [],
|
||||
|
||||
recentSearches: [],
|
||||
tags: [],
|
||||
|
||||
createConversation: () => {
|
||||
const id = nanoid();
|
||||
const now = Date.now();
|
||||
const conversation: Conversation = {
|
||||
id,
|
||||
title: "New conversation",
|
||||
messages: [],
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
model: get().settings.model,
|
||||
tags: [],
|
||||
};
|
||||
set((state) => ({
|
||||
conversations: [conversation, ...state.conversations],
|
||||
activeConversationId: id,
|
||||
sidebarTab: "chats",
|
||||
}));
|
||||
return id;
|
||||
},
|
||||
|
||||
setActiveConversation: (id) => {
|
||||
set({ activeConversationId: id, sidebarTab: "chats" });
|
||||
},
|
||||
|
||||
deleteConversation: (id) => {
|
||||
set((state) => {
|
||||
const remaining = state.conversations.filter((c) => c.id !== id);
|
||||
const nextActive =
|
||||
state.activeConversationId === id
|
||||
? (remaining[0]?.id ?? null)
|
||||
: state.activeConversationId;
|
||||
return {
|
||||
conversations: remaining,
|
||||
activeConversationId: nextActive,
|
||||
pinnedIds: state.pinnedIds.filter((pid) => pid !== id),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
renameConversation: (id, title) => {
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === id ? { ...c, title, updatedAt: Date.now() } : c
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
updateConversation: (id, updates) => {
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === id ? { ...c, ...updates, updatedAt: Date.now() } : c
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
addMessage: (conversationId, message) => {
|
||||
const id = nanoid();
|
||||
const now = Date.now();
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === conversationId
|
||||
? {
|
||||
...c,
|
||||
messages: [...c.messages, { ...message, id, createdAt: now }],
|
||||
updatedAt: now,
|
||||
}
|
||||
: c
|
||||
),
|
||||
}));
|
||||
return id;
|
||||
},
|
||||
|
||||
updateMessage: (conversationId, messageId, updates) => {
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === conversationId
|
||||
? {
|
||||
...c,
|
||||
messages: c.messages.map((m) =>
|
||||
m.id === messageId ? { ...m, ...updates } : m
|
||||
),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
: c
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
toggleSelectConversation: (id) => {
|
||||
set((state) => ({
|
||||
selectedConversationIds: state.selectedConversationIds.includes(id)
|
||||
? state.selectedConversationIds.filter((sid) => sid !== id)
|
||||
: [...state.selectedConversationIds, id],
|
||||
}));
|
||||
},
|
||||
|
||||
clearSelection: () => set({ selectedConversationIds: [] }),
|
||||
|
||||
bulkDeleteConversations: (ids) => {
|
||||
set((state) => {
|
||||
const idSet = new Set(ids);
|
||||
const remaining = state.conversations.filter((c) => !idSet.has(c.id));
|
||||
const nextActive =
|
||||
state.activeConversationId && idSet.has(state.activeConversationId)
|
||||
? (remaining[0]?.id ?? null)
|
||||
: state.activeConversationId;
|
||||
return {
|
||||
conversations: remaining,
|
||||
activeConversationId: nextActive,
|
||||
pinnedIds: state.pinnedIds.filter((pid) => !idSet.has(pid)),
|
||||
selectedConversationIds: [],
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
createTag: (label, color) => {
|
||||
const id = nanoid();
|
||||
set((state) => ({ tags: [...state.tags, { id, label, color }] }));
|
||||
return id;
|
||||
},
|
||||
|
||||
deleteTag: (id) => {
|
||||
set((state) => ({
|
||||
tags: state.tags.filter((t) => t.id !== id),
|
||||
conversations: state.conversations.map((c) => ({
|
||||
...c,
|
||||
tags: c.tags?.filter((tid) => tid !== id),
|
||||
})),
|
||||
}));
|
||||
},
|
||||
|
||||
updateTag: (id, updates) => {
|
||||
set((state) => ({
|
||||
tags: state.tags.map((t) => (t.id === id ? { ...t, ...updates } : t)),
|
||||
}));
|
||||
},
|
||||
|
||||
tagConversation: (conversationId, tagId) => {
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === conversationId
|
||||
? { ...c, tags: [...new Set([...(c.tags ?? []), tagId])] }
|
||||
: c
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
untagConversation: (conversationId, tagId) => {
|
||||
set((state) => ({
|
||||
conversations: state.conversations.map((c) =>
|
||||
c.id === conversationId
|
||||
? { ...c, tags: c.tags?.filter((tid) => tid !== tagId) }
|
||||
: c
|
||||
),
|
||||
}));
|
||||
},
|
||||
|
||||
openSearch: () => set({ isSearchOpen: true }),
|
||||
closeSearch: () => set({ isSearchOpen: false }),
|
||||
|
||||
addRecentSearch: (query) => {
|
||||
if (!query.trim()) return;
|
||||
set((state) => ({
|
||||
recentSearches: [
|
||||
query,
|
||||
...state.recentSearches.filter((s) => s !== query),
|
||||
].slice(0, 10),
|
||||
}));
|
||||
},
|
||||
|
||||
clearRecentSearches: () => set({ recentSearches: [] }),
|
||||
|
||||
updateSettings: (settings) => {
|
||||
set((state) => ({
|
||||
settings: { ...state.settings, ...settings },
|
||||
}));
|
||||
},
|
||||
|
||||
resetSettings: (section) => {
|
||||
if (!section) {
|
||||
set({ settings: DEFAULT_SETTINGS });
|
||||
return;
|
||||
}
|
||||
const sectionDefaults: Record<string, Partial<AppSettings>> = {
|
||||
general: {
|
||||
theme: DEFAULT_SETTINGS.theme,
|
||||
fontSize: DEFAULT_SETTINGS.fontSize,
|
||||
sendOnEnter: DEFAULT_SETTINGS.sendOnEnter,
|
||||
showTimestamps: DEFAULT_SETTINGS.showTimestamps,
|
||||
compactMode: DEFAULT_SETTINGS.compactMode,
|
||||
},
|
||||
model: {
|
||||
model: DEFAULT_SETTINGS.model,
|
||||
maxTokens: DEFAULT_SETTINGS.maxTokens,
|
||||
temperature: DEFAULT_SETTINGS.temperature,
|
||||
systemPrompt: DEFAULT_SETTINGS.systemPrompt,
|
||||
},
|
||||
api: {
|
||||
apiUrl: DEFAULT_SETTINGS.apiUrl,
|
||||
streamingEnabled: DEFAULT_SETTINGS.streamingEnabled,
|
||||
},
|
||||
permissions: { permissions: DEFAULT_SETTINGS.permissions },
|
||||
keybindings: { keybindings: DEFAULT_SETTINGS.keybindings },
|
||||
data: { telemetryEnabled: DEFAULT_SETTINGS.telemetryEnabled },
|
||||
};
|
||||
const defaults = sectionDefaults[section];
|
||||
if (defaults) {
|
||||
set((state) => ({ settings: { ...state.settings, ...defaults } }));
|
||||
}
|
||||
},
|
||||
|
||||
openSettings: () => set({ settingsOpen: true }),
|
||||
closeSettings: () => set({ settingsOpen: false }),
|
||||
|
||||
toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })),
|
||||
setSidebarWidth: (w) => set({ sidebarWidth: w }),
|
||||
setSidebarTab: (tab) => set({ sidebarTab: tab }),
|
||||
pinConversation: (id) =>
|
||||
set((state) => ({
|
||||
pinnedIds: state.pinnedIds.includes(id)
|
||||
? state.pinnedIds.filter((pid) => pid !== id)
|
||||
: [id, ...state.pinnedIds],
|
||||
})),
|
||||
setSearchQuery: (q) => set({ searchQuery: q }),
|
||||
|
||||
getActiveConversation: () => {
|
||||
const state = get();
|
||||
return (
|
||||
state.conversations.find((c) => c.id === state.activeConversationId) ??
|
||||
null
|
||||
);
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "claude-code-chat",
|
||||
partialize: (state) => ({
|
||||
conversations: state.conversations,
|
||||
activeConversationId: state.activeConversationId,
|
||||
settings: state.settings,
|
||||
pinnedIds: state.pinnedIds,
|
||||
recentSearches: state.recentSearches,
|
||||
tags: state.tags,
|
||||
}),
|
||||
merge: (persisted, current) => ({
|
||||
...current,
|
||||
...(persisted as object),
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
...((persisted as { settings?: Partial<AppSettings> }).settings ?? {}),
|
||||
},
|
||||
// Never persist UI state
|
||||
settingsOpen: false,
|
||||
isSearchOpen: false,
|
||||
sidebarTab: "chats",
|
||||
selectedConversationIds: [],
|
||||
}),
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user