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
This commit is contained in:
Taylor Beeston
2026-01-17 11:09:21 +02:00
committed by GitHub
parent 71857eab99
commit b02712bee1
9 changed files with 335 additions and 136 deletions
@@ -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));
}
@@ -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<string, unknown>).gitmojiEnabled === 'boolean'
? ((settings as Record<string, unknown>).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<HTMLInputElement>) => {
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 = () => {
</div>
<div className="space-y-3">
<div className="flex flex-col gap-1.5">
<label className="typography-ui-label text-muted-foreground">Model for generation</label>
<fieldset className="flex flex-col gap-1.5">
<legend className="typography-ui-label text-muted-foreground">Model for generation</legend>
<ModelSelector
providerId={parsedModel.providerId}
modelId={parsedModel.modelId}
@@ -151,6 +172,21 @@ export const GitSettings: React.FC = () => {
This model will be used to analyze diffs and suggest commit messages.
{!settingsCommitMessageModel && <> Default: <span className="text-foreground">opencode/big-pickle</span></>}
</p>
</fieldset>
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
className="h-3.5 w-3.5 accent-primary"
checked={settingsGitmojiEnabled}
onChange={handleGitmojiChange}
/>
<span className="typography-ui-label text-foreground">Enable gitmoji picker</span>
</label>
<p className="typography-meta text-muted-foreground pl-5.5">
Adds a gitmoji selector to the Git commit message input.
</p>
</div>
</div>
</div>
+178 -65
View File
@@ -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<SyncAction>(null);
const [commitAction, setCommitAction] = React.useState<CommitAction>(null);
const [logMaxCountLocal, setLogMaxCountLocal] = React.useState<number>(25);
@@ -124,6 +142,8 @@ export const GitView: React.FC = () => {
const [commitFilesMap, setCommitFilesMap] = React.useState<Map<string, CommitFileEntry[]>>(new Map());
const [loadingCommitHashes, setLoadingCommitHashes] = React.useState<Set<string>>(new Set());
const [remoteUrl, setRemoteUrl] = React.useState<string | null>(null);
const [gitmojiEmojis, setGitmojiEmojis] = React.useState<Array<{ emoji: string; code: string; description: string }>>([]);
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<string, (typeof files)[number]>();
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<string>();
const previousSet = previous ?? new Set<string>();
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}
/>
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="p-3">
<div className="flex flex-col gap-3">
{/* Two-column layout on large screens: Changes + Commit */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{hasChanges ? (
<ChangesSection
changeEntries={changeEntries}
selectedPaths={selectedPaths}
diffStats={status?.diffStats}
revertingPaths={revertingPaths}
onToggleFile={toggleFileSelection}
onSelectAll={selectAll}
onClearSelection={clearSelection}
onViewDiff={(path) => useUIStore.getState().navigateToDiff(path)}
onRevertFile={handleRevertFile}
/>
) : (
<div className="lg:col-span-2 flex justify-center">
<GitEmptyState
behind={status?.behind ?? 0}
onPull={() => handleSyncAction('pull')}
isPulling={syncAction === 'pull'}
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="p-3">
<div className="flex flex-col gap-3">
{/* Two-column layout on large screens: Changes + Commit */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{hasChanges ? (
<ChangesSection
changeEntries={changeEntries}
selectedPaths={selectedPaths}
diffStats={status?.diffStats}
revertingPaths={revertingPaths}
onToggleFile={toggleFileSelection}
onSelectAll={selectAll}
onClearSelection={clearSelection}
onViewDiff={(path) => useUIStore.getState().navigateToDiff(path)}
onRevertFile={handleRevertFile}
/>
</div>
)}
) : (
<div className="lg:col-span-2 flex justify-center">
<GitEmptyState
behind={status?.behind ?? 0}
onPull={() => handleSyncAction('pull')}
isPulling={syncAction === 'pull'}
/>
</div>
)}
{changeEntries.length > 0 && (
<CommitSection
selectedCount={selectedCount}
commitMessage={commitMessage}
onCommitMessageChange={setCommitMessage}
generatedHighlights={generatedHighlights}
onInsertHighlights={handleInsertHighlights}
onClearHighlights={clearGeneratedHighlights}
onGenerateMessage={handleGenerateCommitMessage}
isGeneratingMessage={isGeneratingMessage}
onCommit={() => handleCommit({ pushAfter: false })}
onCommitAndPush={() => handleCommit({ pushAfter: true })}
commitAction={commitAction}
isBusy={isBusy}
/>
)}
{changeEntries.length > 0 && (
<CommitSection
selectedCount={selectedCount}
commitMessage={commitMessage}
onCommitMessageChange={setCommitMessage}
generatedHighlights={generatedHighlights}
onInsertHighlights={handleInsertHighlights}
onClearHighlights={clearGeneratedHighlights}
onGenerateMessage={handleGenerateCommitMessage}
isGeneratingMessage={isGeneratingMessage}
onCommit={() => handleCommit({ pushAfter: false })}
onCommitAndPush={() => handleCommit({ pushAfter: true })}
commitAction={commitAction}
isBusy={isBusy}
gitmojiEnabled={settingsGitmojiEnabled}
onOpenGitmojiPicker={() => setIsGitmojiPickerOpen(true)}
/>
)}
</div>
{/* History below, constrained width */}
<HistorySection
log={log}
isLogLoading={isLogLoading}
logMaxCount={logMaxCountLocal}
onLogMaxCountChange={handleLogMaxCountChange}
expandedCommitHashes={expandedCommitHashes}
onToggleCommit={handleToggleCommit}
commitFilesMap={commitFilesMap}
loadingCommitHashes={loadingCommitHashes}
onCopyHash={handleCopyCommitHash}
/>
</div>
</ScrollableOverlay>
<Dialog open={isGitmojiPickerOpen} onOpenChange={setIsGitmojiPickerOpen}>
<DialogContent className="max-w-md p-0 overflow-hidden">
<DialogHeader className="px-4 pt-4">
<DialogTitle>Pick a gitmoji</DialogTitle>
</DialogHeader>
<Command className="h-[420px]">
<CommandInput
placeholder="Search gitmojis..."
value={gitmojiSearch}
onValueChange={setGitmojiSearch}
/>
<CommandList>
<CommandEmpty>No gitmojis found.</CommandEmpty>
<CommandGroup>
{(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) => (
<CommandItem
key={entry.code}
onSelect={() => handleSelectGitmoji(entry.emoji, entry.code)}
>
<span className="text-lg">{entry.emoji}</span>
<span className="typography-ui-label text-foreground">{entry.code}</span>
<span className="typography-meta text-muted-foreground">{entry.description}</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DialogContent>
</Dialog>
{/* History below, constrained width */}
<HistorySection
log={log}
isLogLoading={isLogLoading}
logMaxCount={logMaxCountLocal}
onLogMaxCountChange={handleLogMaxCountChange}
expandedCommitHashes={expandedCommitHashes}
onToggleCommit={handleToggleCommit}
commitFilesMap={commitFilesMap}
loadingCommitHashes={loadingCommitHashes}
onCopyHash={handleCopyCommitHash}
/>
</div>
</ScrollableOverlay>
</div>
);
};
@@ -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<CommitSectionProps> = ({
@@ -45,6 +47,8 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
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<CommitSectionProps> = ({
disabled={commitAction !== null}
/>
{gitmojiEnabled && (
<Button
variant="outline"
size="sm"
onClick={onOpenGitmojiPicker}
className="w-fit"
type="button"
>
<RiEmotionHappyLine className="size-4" />
Add gitmoji
</Button>
)}
<div className="flex items-center gap-2">
<Tooltip delayDuration={1000}>
<TooltipTrigger asChild>
+1
View File
@@ -385,6 +385,7 @@ export interface SettingsPayload {
autoDeleteEnabled?: boolean;
autoDeleteAfterDays?: number;
queueModeEnabled?: boolean;
gitmojiEnabled?: boolean;
[key: string]: unknown;
}
+1
View File
@@ -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[];
+9 -1
View File
@@ -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;
}
+76 -59
View File
@@ -26,6 +26,7 @@ interface OpenChamberDefaults {
defaultAgent?: string;
autoCreateWorktree?: boolean;
commitMessageModel?: string;
gitmojiEnabled?: boolean;
}
const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
@@ -39,6 +40,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
defaultAgent: settings?.defaultAgent,
autoCreateWorktree: settings?.autoCreateWorktree,
commitMessageModel: settings?.commitMessageModel,
gitmojiEnabled: settings?.gitmojiEnabled,
};
}
@@ -53,6 +55,7 @@ const fetchOpenChamberDefaults = async (): Promise<OpenChamberDefaults> => {
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<OpenChamberDefaults> => {
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<OpenChamberDefaults> => {
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<OpenChamberDefaults> => {
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<void>;
@@ -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<boolean>;
@@ -440,6 +448,7 @@ export const useConfigStore = create<ConfigStore>()(
settingsDefaultAgent: undefined,
settingsAutoCreateWorktree: false,
settingsCommitMessageModel: undefined,
settingsGitmojiEnabled: false,
activateDirectory: async (directory) => {
const directoryKey = toDirectoryKey(directory);
@@ -879,13 +888,13 @@ export const useConfigStore = create<ConfigStore>()(
};
const nextState: Partial<ConfigStore> = {
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<ConfigStore>()(
// 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<ConfigStore>()(
} 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<ConfigStore>()(
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<ConfigStore> = {
@@ -1046,14 +1057,14 @@ export const useConfigStore = create<ConfigStore>()(
},
};
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<ConfigStore>()(
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<ConfigStore>()(
// 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<ConfigStore>()(
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<ConfigStore>()(
settingsDefaultVariant: state.settingsDefaultVariant,
settingsDefaultAgent: state.settingsDefaultAgent,
settingsAutoCreateWorktree: state.settingsAutoCreateWorktree,
}),
settingsCommitMessageModel: state.settingsCommitMessageModel,
settingsGitmojiEnabled: state.settingsGitmojiEnabled,
}),
},
),
),
+3
View File
@@ -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) {