From b02712bee1e340faf33a6931c55d253d5b2a94af Mon Sep 17 00:00:00 2001 From: Taylor Beeston <39720479+TaylorBeeston@users.noreply.github.com> Date: Sat, 17 Jan 2026 01:09:21 -0800 Subject: [PATCH] :sparkles: add gitmoji support for commit messages (#165) Add gitmoji toggle in Git settings to enable emoji support Show Add gitmoji button in commit section when feature is enabled Display gitmoji picker dialog with searchable emoji list --- .../src-tauri/src/commands/settings.rs | 3 + .../sections/openchamber/GitSettings.tsx | 56 +++- packages/ui/src/components/views/GitView.tsx | 243 +++++++++++++----- .../components/views/git/CommitSection.tsx | 19 +- packages/ui/src/lib/api/types.ts | 1 + packages/ui/src/lib/desktop.ts | 1 + packages/ui/src/lib/persistence.ts | 10 +- packages/ui/src/stores/useConfigStore.ts | 135 +++++----- packages/web/server/index.js | 3 + 9 files changed, 335 insertions(+), 136 deletions(-) diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs index bed01079..4f93215b 100644 --- a/packages/desktop/src-tauri/src/commands/settings.rs +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -275,6 +275,9 @@ fn sanitize_settings_update(payload: &Value) -> Value { } // Boolean fields + if let Some(Value::Bool(b)) = obj.get("gitmojiEnabled") { + result_obj.insert("gitmojiEnabled".to_string(), json!(b)); + } if let Some(Value::Bool(b)) = obj.get("useSystemTheme") { result_obj.insert("useSystemTheme".to_string(), json!(b)); } diff --git a/packages/ui/src/components/sections/openchamber/GitSettings.tsx b/packages/ui/src/components/sections/openchamber/GitSettings.tsx index 072617ac..0dce615f 100644 --- a/packages/ui/src/components/sections/openchamber/GitSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/GitSettings.tsx @@ -37,6 +37,8 @@ const getDisplayModel = ( export const GitSettings: React.FC = () => { const settingsCommitMessageModel = useConfigStore((state) => state.settingsCommitMessageModel); const setSettingsCommitMessageModel = useConfigStore((state) => state.setSettingsCommitMessageModel); + const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled); + const setSettingsGitmojiEnabled = useConfigStore((state) => state.setSettingsGitmojiEnabled); const providers = useConfigStore((state) => state.providers); const [isLoading, setIsLoading] = React.useState(true); @@ -56,7 +58,7 @@ export const GitSettings: React.FC = () => { React.useEffect(() => { const loadSettings = async () => { try { - let data: { commitMessageModel?: string } | null = null; + let data: { commitMessageModel?: string; gitmojiEnabled?: boolean } | null = null; // 1. Desktop runtime (Tauri) if (isDesktopRuntime()) { @@ -71,6 +73,9 @@ export const GitSettings: React.FC = () => { if (settings) { data = { commitMessageModel: typeof settings.commitMessageModel === 'string' ? settings.commitMessageModel : undefined, + gitmojiEnabled: typeof (settings as Record).gitmojiEnabled === 'boolean' + ? ((settings as Record).gitmojiEnabled as boolean) + : undefined, }; } } catch { @@ -90,12 +95,16 @@ export const GitSettings: React.FC = () => { } } - if (data) { - const model = typeof data.commitMessageModel === 'string' && data.commitMessageModel.trim().length > 0 - ? data.commitMessageModel.trim() - : undefined; - setSettingsCommitMessageModel(model); - } + if (data) { + const model = typeof data.commitMessageModel === 'string' && data.commitMessageModel.trim().length > 0 + ? data.commitMessageModel.trim() + : undefined; + setSettingsCommitMessageModel(model); + if (typeof data.gitmojiEnabled === 'boolean') { + setSettingsGitmojiEnabled(data.gitmojiEnabled); + } + } + } catch (error) { console.warn('Failed to load git settings:', error); } finally { @@ -103,7 +112,7 @@ export const GitSettings: React.FC = () => { } }; loadSettings(); - }, [setSettingsCommitMessageModel]); + }, [setSettingsCommitMessageModel, setSettingsGitmojiEnabled]); const handleModelChange = React.useCallback(async (providerId: string, modelId: string) => { const newValue = providerId && modelId ? `${providerId}/${modelId}` : undefined; @@ -118,6 +127,18 @@ export const GitSettings: React.FC = () => { } }, [setSettingsCommitMessageModel]); + const handleGitmojiChange = React.useCallback(async (event: React.ChangeEvent) => { + const enabled = event.target.checked; + setSettingsGitmojiEnabled(enabled); + try { + await updateDesktopSettings({ + gitmojiEnabled: enabled, + }); + } catch (error) { + console.warn('Failed to save gitmoji setting:', error); + } + }, [setSettingsGitmojiEnabled]); + if (isLoading) { return null; } @@ -139,8 +160,8 @@ export const GitSettings: React.FC = () => {
-
- +
+ Model for generation { This model will be used to analyze diffs and suggest commit messages. {!settingsCommitMessageModel && <> Default: opencode/big-pickle}

+
+ +
+ +

+ Adds a gitmoji selector to the Git commit message input. +

diff --git a/packages/ui/src/components/views/GitView.tsx b/packages/ui/src/components/views/GitView.tsx index 8e4c9f91..775dec39 100644 --- a/packages/ui/src/components/views/GitView.tsx +++ b/packages/ui/src/components/views/GitView.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useSessionStore } from '@/stores/useSessionStore'; +import { useConfigStore } from '@/stores/useConfigStore'; import { useFireworksCelebration } from '@/contexts/FireworksContext'; import type { GitIdentityProfile, CommitFileEntry } from '@/lib/api/types'; import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore'; @@ -15,6 +16,20 @@ import { import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { RiGitBranchLine, RiLoader4Line } from '@remixicon/react'; import { toast } from 'sonner'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; import type { Session } from '@opencode-ai/sdk/v2'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useUIStore } from '@/stores/useUIStore'; @@ -86,9 +101,12 @@ export const GitView: React.FC = () => { return gitViewSnapshot; }, [currentDirectory]); + const settingsGitmojiEnabled = useConfigStore((state) => state.settingsGitmojiEnabled); + const [commitMessage, setCommitMessage] = React.useState( initialSnapshot?.commitMessage ?? '' ); + const [isGitmojiPickerOpen, setIsGitmojiPickerOpen] = React.useState(false); const [syncAction, setSyncAction] = React.useState(null); const [commitAction, setCommitAction] = React.useState(null); const [logMaxCountLocal, setLogMaxCountLocal] = React.useState(25); @@ -124,6 +142,8 @@ export const GitView: React.FC = () => { const [commitFilesMap, setCommitFilesMap] = React.useState>(new Map()); const [loadingCommitHashes, setLoadingCommitHashes] = React.useState>(new Set()); const [remoteUrl, setRemoteUrl] = React.useState(null); + const [gitmojiEmojis, setGitmojiEmojis] = React.useState>([]); + const [gitmojiSearch, setGitmojiSearch] = React.useState(''); const handleCopyCommitHash = React.useCallback((hash: string) => { navigator.clipboard @@ -160,11 +180,13 @@ export const GitView: React.FC = () => { setLoadingCommitHashes((prev) => { const next = new Set(prev); - hashesToLoad.forEach((h) => next.add(h)); + for (const hash of hashesToLoad) { + next.add(hash); + } return next; }); - hashesToLoad.forEach((hash) => { + for (const hash of hashesToLoad) { git .getCommitFiles(currentDirectory, hash) .then((response) => { @@ -181,7 +203,7 @@ export const GitView: React.FC = () => { return next; }); }); - }); + } }, [expandedCommitHashes, currentDirectory, git, commitFilesMap, loadingCommitHashes]); React.useEffect(() => { @@ -213,6 +235,38 @@ export const GitView: React.FC = () => { git.getRemoteUrl(currentDirectory).then(setRemoteUrl).catch(() => setRemoteUrl(null)); }, [currentDirectory, git]); + React.useEffect(() => { + if (!settingsGitmojiEnabled) { + setGitmojiEmojis([]); + return; + } + + let cancelled = false; + + const loadGitmojis = async () => { + try { + const response = await fetch('https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json'); + if (!response.ok) { + throw new Error(`Failed to load gitmojis: ${response.statusText}`); + } + const payload = (await response.json()) as { gitmojis?: Array<{ emoji: string; code: string; description: string }> }; + if (!cancelled) { + setGitmojiEmojis(payload.gitmojis ?? []); + } + } catch (error) { + if (!cancelled) { + console.warn('Failed to load gitmoji list:', error); + } + } + }; + + loadGitmojis(); + + return () => { + cancelled = true; + }; + }, [settingsGitmojiEnabled]); + React.useEffect(() => { if (currentDirectory) { setActiveDirectory(currentDirectory); @@ -269,9 +323,9 @@ export const GitView: React.FC = () => { const run = async () => { try { - const hasLocal = await git.hasLocalIdentity!(currentDirectory); + const hasLocal = await git.hasLocalIdentity?.(currentDirectory); if (cancelled) return; - if (hasLocal) return; + if (hasLocal === true) return; beginIdentityApply(); await git.setGitIdentity(currentDirectory, defaultId); @@ -293,18 +347,19 @@ export const GitView: React.FC = () => { }; }, [beginIdentityApply, currentDirectory, defaultGitIdentityId, endIdentityApply, git, isGitRepo, refreshIdentity]); - const changeEntries = React.useMemo(() => { + const changeEntries = React.useMemo(() => { if (!status) return []; const files = status.files ?? []; const unique = new Map(); - files.forEach((file) => { + for (const file of files) { unique.set(file.path, file); - }); + } return Array.from(unique.values()).sort((a, b) => a.path.localeCompare(b.path)); }, [status]); + React.useEffect(() => { if (!status || changeEntries.length === 0) { setSelectedPaths(new Set()); @@ -316,13 +371,13 @@ export const GitView: React.FC = () => { const next = new Set(); const previousSet = previous ?? new Set(); - changeEntries.forEach((file) => { + for (const file of changeEntries) { if (previousSet.has(file.path)) { next.add(file.path); } else if (!hasUserAdjustedSelection) { next.add(file.path); } - }); + } return next; }); @@ -565,7 +620,7 @@ export const GitView: React.FC = () => { try { let normalized = remoteUrl.trim(); if (normalized.startsWith('git@')) { - normalized = 'https://' + normalized.slice(4).replace(':', '/'); + normalized = `https://${normalized.slice(4).replace(':', '/')}`; } if (normalized.endsWith('.git')) { normalized = normalized.slice(0, -4); @@ -706,6 +761,20 @@ export const GitView: React.FC = () => { }); }, [generatedHighlights, clearGeneratedHighlights]); + const handleSelectGitmoji = React.useCallback((emoji: string, code: string) => { + const token = code || emoji; + setCommitMessage((current) => { + const trimmed = current.trimStart(); + if (trimmed.startsWith(emoji) || (code && trimmed.startsWith(code))) { + return current; + } + const prefix = token.endsWith(' ') ? token : `${token} `; + return `${prefix}${current}`.trimStart(); + }); + setGitmojiSearch(''); + setIsGitmojiPickerOpen(false); + }, []); + const handleLogMaxCountChange = React.useCallback( (count: number) => { setLogMaxCountLocal(count); @@ -773,64 +842,108 @@ export const GitView: React.FC = () => { isWorktreeMode={!!worktreeMetadata} /> - -
- {/* Two-column layout on large screens: Changes + Commit */} -
- {hasChanges ? ( - useUIStore.getState().navigateToDiff(path)} - onRevertFile={handleRevertFile} - /> - ) : ( -
- handleSyncAction('pull')} - isPulling={syncAction === 'pull'} + +
+ {/* Two-column layout on large screens: Changes + Commit */} +
+ {hasChanges ? ( + useUIStore.getState().navigateToDiff(path)} + onRevertFile={handleRevertFile} /> -
- )} + ) : ( +
+ handleSyncAction('pull')} + isPulling={syncAction === 'pull'} + /> +
+ )} - {changeEntries.length > 0 && ( - handleCommit({ pushAfter: false })} - onCommitAndPush={() => handleCommit({ pushAfter: true })} - commitAction={commitAction} - isBusy={isBusy} - /> - )} + {changeEntries.length > 0 && ( + handleCommit({ pushAfter: false })} + onCommitAndPush={() => handleCommit({ pushAfter: true })} + commitAction={commitAction} + isBusy={isBusy} + gitmojiEnabled={settingsGitmojiEnabled} + onOpenGitmojiPicker={() => setIsGitmojiPickerOpen(true)} + /> + )} +
+ + {/* History below, constrained width */} +
+ + + + + + Pick a gitmoji + + + + + No gitmojis found. + + {(gitmojiEmojis.length === 0 + ? [] + : gitmojiEmojis.filter((entry) => { + const term = gitmojiSearch.trim().toLowerCase(); + if (!term) return true; + return ( + entry.emoji.includes(term) || + entry.code.toLowerCase().includes(term) || + entry.description.toLowerCase().includes(term) + ); + }) + ).map((entry) => ( + handleSelectGitmoji(entry.emoji, entry.code)} + > + {entry.emoji} + {entry.code} + {entry.description} + + ))} + + + + + - {/* History below, constrained width */} - -
-
); }; diff --git a/packages/ui/src/components/views/git/CommitSection.tsx b/packages/ui/src/components/views/git/CommitSection.tsx index 78868b8e..22ba80bd 100644 --- a/packages/ui/src/components/views/git/CommitSection.tsx +++ b/packages/ui/src/components/views/git/CommitSection.tsx @@ -1,9 +1,9 @@ -import React from 'react'; import { RiGitCommitLine, RiArrowUpLine, RiAiGenerate2, RiLoader4Line, + RiEmotionHappyLine, } from '@remixicon/react'; import { Collapsible, @@ -30,6 +30,8 @@ interface CommitSectionProps { onCommitAndPush: () => void; commitAction: CommitAction; isBusy: boolean; + gitmojiEnabled: boolean; + onOpenGitmojiPicker: () => void; } export const CommitSection: React.FC = ({ @@ -45,6 +47,8 @@ export const CommitSection: React.FC = ({ onCommitAndPush, commitAction, isBusy, + gitmojiEnabled, + onOpenGitmojiPicker, }) => { const hasSelectedFiles = selectedCount > 0; const canCommit = commitMessage.trim() && hasSelectedFiles && commitAction === null; @@ -79,6 +83,19 @@ export const CommitSection: React.FC = ({ disabled={commitAction !== null} /> + {gitmojiEnabled && ( + + )} +
diff --git a/packages/ui/src/lib/api/types.ts b/packages/ui/src/lib/api/types.ts index 4cc29928..7bea986d 100644 --- a/packages/ui/src/lib/api/types.ts +++ b/packages/ui/src/lib/api/types.ts @@ -385,6 +385,7 @@ export interface SettingsPayload { autoDeleteEnabled?: boolean; autoDeleteAfterDays?: number; queueModeEnabled?: boolean; + gitmojiEnabled?: boolean; [key: string]: unknown; } diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index 8156f79c..b124f970 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -60,6 +60,7 @@ export type DesktopSettings = { autoCreateWorktree?: boolean; queueModeEnabled?: boolean; commitMessageModel?: string; // format: "provider/model" + gitmojiEnabled?: boolean; // User-added skills catalogs (persisted to ~/.config/openchamber/settings.json) skillCatalogs?: SkillCatalogConfig[]; diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 33240001..57d23ec7 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -52,11 +52,16 @@ const persistToLocalStorage = (settings: DesktopSettings) => { } else { localStorage.removeItem('commitMessageModel'); } + if (typeof settings.gitmojiEnabled === 'boolean') { + localStorage.setItem('gitmojiEnabled', String(settings.gitmojiEnabled)); + } else { + localStorage.removeItem('gitmojiEnabled'); + } }; type PersistApi = { hasHydrated?: () => boolean; - onFinishHydration?: (callback: () => void) => (() => void) | void; + onFinishHydration?: (callback: () => void) => (() => void) | undefined; }; const sanitizeSkillCatalogs = (value: unknown): DesktopSettings['skillCatalogs'] | undefined => { @@ -269,6 +274,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { if (typeof candidate.commitMessageModel === 'string' && candidate.commitMessageModel.length > 0) { result.commitMessageModel = candidate.commitMessageModel; } + if (typeof candidate.gitmojiEnabled === 'boolean') { + result.gitmojiEnabled = candidate.gitmojiEnabled; + } if (typeof candidate.queueModeEnabled === 'boolean') { result.queueModeEnabled = candidate.queueModeEnabled; } diff --git a/packages/ui/src/stores/useConfigStore.ts b/packages/ui/src/stores/useConfigStore.ts index 2b73e427..e5fdb2f3 100644 --- a/packages/ui/src/stores/useConfigStore.ts +++ b/packages/ui/src/stores/useConfigStore.ts @@ -26,6 +26,7 @@ interface OpenChamberDefaults { defaultAgent?: string; autoCreateWorktree?: boolean; commitMessageModel?: string; + gitmojiEnabled?: boolean; } const fetchOpenChamberDefaults = async (): Promise => { @@ -39,6 +40,7 @@ const fetchOpenChamberDefaults = async (): Promise => { defaultAgent: settings?.defaultAgent, autoCreateWorktree: settings?.autoCreateWorktree, commitMessageModel: settings?.commitMessageModel, + gitmojiEnabled: settings?.gitmojiEnabled, }; } @@ -53,6 +55,7 @@ const fetchOpenChamberDefaults = async (): Promise => { const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : ''; + const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, @@ -60,6 +63,7 @@ const fetchOpenChamberDefaults = async (): Promise => { defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined, + gitmojiEnabled, }; } } catch { @@ -80,6 +84,7 @@ const fetchOpenChamberDefaults = async (): Promise => { const defaultVariant = typeof data?.defaultVariant === 'string' ? data.defaultVariant.trim() : ''; const defaultAgent = typeof data?.defaultAgent === 'string' ? data.defaultAgent.trim() : ''; const commitMessageModel = typeof data?.commitMessageModel === 'string' ? data.commitMessageModel.trim() : ''; + const gitmojiEnabled = typeof data?.gitmojiEnabled === 'boolean' ? data.gitmojiEnabled : undefined; return { defaultModel: defaultModel.length > 0 ? defaultModel : undefined, @@ -87,6 +92,7 @@ const fetchOpenChamberDefaults = async (): Promise => { defaultAgent: defaultAgent.length > 0 ? defaultAgent : undefined, autoCreateWorktree: typeof data?.autoCreateWorktree === 'boolean' ? data.autoCreateWorktree : undefined, commitMessageModel: commitMessageModel.length > 0 ? commitMessageModel : undefined, + gitmojiEnabled, }; } catch { return {}; @@ -379,6 +385,7 @@ interface ConfigStore { settingsDefaultAgent: string | undefined; settingsAutoCreateWorktree: boolean; settingsCommitMessageModel: string | undefined; // format: "provider/model" + settingsGitmojiEnabled: boolean; activateDirectory: (directory: string | null | undefined) => Promise; @@ -396,6 +403,7 @@ interface ConfigStore { setSettingsDefaultAgent: (agent: string | undefined) => void; setSettingsAutoCreateWorktree: (enabled: boolean) => void; setSettingsCommitMessageModel: (model: string | undefined) => void; + setSettingsGitmojiEnabled: (enabled: boolean) => void; saveAgentModelSelection: (agentName: string, providerId: string, modelId: string) => void; getAgentModelSelection: (agentName: string) => { providerId: string; modelId: string } | null; checkConnection: () => Promise; @@ -440,6 +448,7 @@ export const useConfigStore = create()( settingsDefaultAgent: undefined, settingsAutoCreateWorktree: false, settingsCommitMessageModel: undefined, + settingsGitmojiEnabled: false, activateDirectory: async (directory) => { const directoryKey = toDirectoryKey(directory); @@ -879,13 +888,13 @@ export const useConfigStore = create()( }; const nextState: Partial = { - settingsDefaultModel: openChamberDefaults.defaultModel, - settingsDefaultVariant: openChamberDefaults.defaultVariant, - settingsDefaultAgent: openChamberDefaults.defaultAgent, - settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, - settingsCommitMessageModel: openChamberDefaults.commitMessageModel, - directoryScoped: { - + settingsDefaultModel: openChamberDefaults.defaultModel, + settingsDefaultVariant: openChamberDefaults.defaultVariant, + settingsDefaultAgent: openChamberDefaults.defaultAgent, + settingsAutoCreateWorktree: openChamberDefaults.autoCreateWorktree ?? false, + settingsCommitMessageModel: openChamberDefaults.commitMessageModel, + settingsGitmojiEnabled: openChamberDefaults.gitmojiEnabled ?? false, + directoryScoped: { ...state.directoryScoped, [directoryKey]: nextSnapshot, }, @@ -996,9 +1005,10 @@ export const useConfigStore = create()( // 2. Fall back to agent's preferred model if (!resolvedProviderId && resolvedAgent?.model?.providerID && resolvedAgent?.model?.modelID) { - if (validateModel(resolvedAgent.model.providerID, resolvedAgent.model.modelID)) { - resolvedProviderId = resolvedAgent.model.providerID; - resolvedModelId = resolvedAgent.model.modelID; + const { providerID, modelID } = resolvedAgent.model; + if (validateModel(providerID, modelID)) { + resolvedProviderId = providerID; + resolvedModelId = modelID; } } @@ -1010,9 +1020,10 @@ export const useConfigStore = create()( } else { // Last resort: first provider's first model const firstProvider = providers[0]; - if (firstProvider && firstProvider.models[0]) { + const firstModel = firstProvider?.models[0]; + if (firstProvider && firstModel) { resolvedProviderId = firstProvider.id; - resolvedModelId = firstProvider.models[0].id; + resolvedModelId = firstModel.id; } } } @@ -1034,9 +1045,9 @@ export const useConfigStore = create()( providers, agents: safeAgents, currentAgentName: resolvedAgent.name, - currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId, - currentModelId: resolvedModelId ?? baseSnapshot.currentModelId, - currentVariant: resolvedVariant, + currentProviderId: resolvedProviderId ?? baseSnapshot.currentProviderId, + currentModelId: resolvedModelId ?? baseSnapshot.currentModelId, + currentVariant: resolvedVariant, }; const nextState: Partial = { @@ -1046,14 +1057,14 @@ export const useConfigStore = create()( }, }; - if (state.activeDirectoryKey === directoryKey) { - nextState.currentAgentName = resolvedAgent.name; - if (resolvedProviderId && resolvedModelId) { - nextState.currentProviderId = resolvedProviderId; - nextState.currentModelId = resolvedModelId; - nextState.currentVariant = resolvedVariant; - } - } + if (state.activeDirectoryKey === directoryKey) { + nextState.currentAgentName = resolvedAgent.name; + if (resolvedProviderId && resolvedModelId) { + nextState.currentProviderId = resolvedProviderId; + nextState.currentModelId = resolvedModelId; + nextState.currentVariant = resolvedVariant; + } + } return nextState; }); @@ -1178,7 +1189,7 @@ export const useConfigStore = create()( if (agentName && typeof window !== "undefined") { const sessionStore = window.__zustand_session_store__; - if (sessionStore) { + if (sessionStore?.getState) { const { currentSessionId, getAgentModelForSession } = sessionStore.getState(); if (currentSessionId) { @@ -1245,43 +1256,43 @@ export const useConfigStore = create()( // Fall back to agent's preferred model const agent = agents.find((candidate) => candidate.name === agentName); - if (agent?.model?.providerID && agent?.model?.modelID) { - const agentProvider = providers.find((provider) => provider.id === agent.model!.providerID); - if (agentProvider) { - const agentModel = agentProvider.models.find((model) => model.id === agent.model!.modelID); + const agentModelSelection = agent?.model; + if (agentModelSelection?.providerID && agentModelSelection?.modelID) { + const { providerID, modelID } = agentModelSelection; + const agentProvider = providers.find((provider) => provider.id === providerID); + const agentModel = agentProvider?.models.find((model) => model.id === modelID); - if (agentModel) { - set((state) => { - const directoryKey = state.activeDirectoryKey; - const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { - providers: state.providers, - agents: state.agents, - currentProviderId: state.currentProviderId, - currentModelId: state.currentModelId, - currentAgentName: state.currentAgentName, - selectedProviderId: state.selectedProviderId, - agentModelSelections: state.agentModelSelections, - defaultProviders: state.defaultProviders, - }; + if (agentModel) { + set((state) => { + const directoryKey = state.activeDirectoryKey; + const baseSnapshot: DirectoryScopedConfig = state.directoryScoped[directoryKey] ?? { + providers: state.providers, + agents: state.agents, + currentProviderId: state.currentProviderId, + currentModelId: state.currentModelId, + currentAgentName: state.currentAgentName, + selectedProviderId: state.selectedProviderId, + agentModelSelections: state.agentModelSelections, + defaultProviders: state.defaultProviders, + }; - const nextSnapshot: DirectoryScopedConfig = { - ...baseSnapshot, - currentProviderId: agent.model!.providerID, - currentModelId: agent.model!.modelID, - selectedProviderId: agent.model!.providerID, - }; + const nextSnapshot: DirectoryScopedConfig = { + ...baseSnapshot, + currentProviderId: providerID, + currentModelId: modelID, + selectedProviderId: providerID, + }; - return { - currentProviderId: agent.model!.providerID, - currentModelId: agent.model!.modelID, - selectedProviderId: agent.model!.providerID, - directoryScoped: { - ...state.directoryScoped, - [directoryKey]: nextSnapshot, - }, - }; - }); - } + return { + currentProviderId: providerID, + currentModelId: modelID, + selectedProviderId: providerID, + directoryScoped: { + ...state.directoryScoped, + [directoryKey]: nextSnapshot, + }, + }; + }); } } } @@ -1307,6 +1318,10 @@ export const useConfigStore = create()( set({ settingsCommitMessageModel: model }); }, + setSettingsGitmojiEnabled: (enabled: boolean) => { + set({ settingsGitmojiEnabled: enabled }); + }, + checkConnection: async () => { const maxAttempts = 5; let attempt = 0; @@ -1412,7 +1427,9 @@ export const useConfigStore = create()( settingsDefaultVariant: state.settingsDefaultVariant, settingsDefaultAgent: state.settingsDefaultAgent, settingsAutoCreateWorktree: state.settingsAutoCreateWorktree, - }), + settingsCommitMessageModel: state.settingsCommitMessageModel, + settingsGitmojiEnabled: state.settingsGitmojiEnabled, + }), }, ), ), diff --git a/packages/web/server/index.js b/packages/web/server/index.js index 202c353f..3abf6642 100644 --- a/packages/web/server/index.js +++ b/packages/web/server/index.js @@ -619,6 +619,9 @@ const sanitizeSettingsUpdate = (payload) => { const trimmed = candidate.commitMessageModel.trim(); result.commitMessageModel = trimmed.length > 0 ? trimmed : undefined; } + if (typeof candidate.gitmojiEnabled === 'boolean') { + result.gitmojiEnabled = candidate.gitmojiEnabled; + } const skillCatalogs = sanitizeSkillCatalogs(candidate.skillCatalogs); if (skillCatalogs) {