fix(onboarding): align MemoryStep layout with Settings MemoryBackendSection (#1445)

* fix(onboarding): align MemoryStep layout with Settings MemoryBackendSection

* fix(onboarding): address PR feedback - restore ollama installer, fix i18n, add persistence

* refactor(onboarding): cleanup unused ollama state and error UI

* fix(onboarding): restore i18n support and jsdoc

* fix(pr): resolve code duplication and add missing ollama config

* fix(pr): resolve code rabbit findings (unused imports, i18n, state init)

* fix(onboarding): initialize ollama settings from saved values

Initialize ollamaEmbeddingModel and ollamaEmbeddingDim from settings
instead of hardcoding defaults. This prevents overwriting user's
saved configuration when re-running the onboarding wizard.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Andy <119136210+AndyMik90@users.noreply.github.com>
This commit is contained in:
Michael Ludlow
2026-01-24 08:47:25 -05:00
committed by GitHub
parent 426d56571c
commit e9de26d598
6 changed files with 239 additions and 240 deletions
@@ -4,9 +4,7 @@ import {
Database,
Info,
Loader2,
Eye,
EyeOff,
ExternalLink,
ExternalLink
} from 'lucide-react';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
@@ -20,9 +18,11 @@ import {
SelectTrigger,
SelectValue
} from '../ui/select';
import { OllamaModelSelector } from './OllamaModelSelector';
import { InfrastructureStatus } from '../project-settings/InfrastructureStatus';
import { PasswordInput } from '../project-settings/PasswordInput';
import { useSettingsStore } from '../../stores/settings-store';
import type { GraphitiEmbeddingProvider, AppSettings } from '../../../shared/types';
import type { GraphitiEmbeddingProvider, AppSettings, InfrastructureStatus as InfrastructureStatusType } from '../../../shared/types';
import { OllamaModelSelector } from './OllamaModelSelector';
interface MemoryStepProps {
onNext: () => void;
@@ -42,23 +42,26 @@ interface MemoryConfig {
azureOpenaiEmbeddingDeployment: string;
// Voyage
voyageApiKey: string;
voyageEmbeddingModel: string;
// Google
googleApiKey: string;
// Ollama
ollamaBaseUrl: string;
ollamaEmbeddingModel: string;
ollamaEmbeddingDim: number;
}
/**
* Memory configuration step for the onboarding wizard.
*
* Matches the settings page Memory section structure:
* Matches the settings page MemoryBackendSection structure:
* - Enable Memory toggle (enabled by default)
* - Infrastructure Status
* - Enable Agent Memory Access toggle
* - Embedding Provider selection (Ollama default)
* - Provider-specific configuration
*
* Note: LLM provider is not configurable - Claude SDK is used throughout.
*/
export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
const { t } = useTranslation('onboarding');
@@ -75,24 +78,31 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
azureOpenaiBaseUrl: '',
azureOpenaiEmbeddingDeployment: '',
voyageApiKey: '',
voyageEmbeddingModel: settings.memoryVoyageEmbeddingModel || '',
googleApiKey: settings.globalGoogleApiKey || '',
ollamaEmbeddingModel: 'qwen3-embedding:4b',
ollamaEmbeddingDim: 2560,
ollamaBaseUrl: settings.ollamaBaseUrl || 'http://localhost:11434',
ollamaEmbeddingModel: settings.memoryOllamaEmbeddingModel || 'qwen3-embedding:4b',
ollamaEmbeddingDim: settings.memoryOllamaEmbeddingDim ?? 2560,
});
const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({});
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [infrastructureStatus, setInfrastructureStatus] = useState<InfrastructureStatusType | null>(null);
const [isCheckingInfra, setIsCheckingInfra] = useState(true);
// Check LadybugDB/Kuzu availability on mount
useEffect(() => {
const checkInfrastructure = async () => {
setIsCheckingInfra(true);
try {
await window.electronAPI.getMemoryInfrastructureStatus();
} catch {
// Infrastructure will be created automatically when needed
const result = await window.electronAPI.getMemoryInfrastructureStatus();
if (result.success && result.data) {
setInfrastructureStatus(result.data);
}
} catch (err) {
console.error('Failed to check infrastructure:', err);
} finally {
setIsCheckingInfra(false);
}
@@ -101,9 +111,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
checkInfrastructure();
}, []);
const toggleShowApiKey = (key: string) => {
setShowApiKey(prev => ({ ...prev, [key]: !prev[key] }));
};
// Check if we have valid configuration
const isConfigValid = (): boolean => {
@@ -140,6 +148,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
// Core memory settings
memoryEnabled: config.enabled,
memoryEmbeddingProvider: config.embeddingProvider,
ollamaBaseUrl: config.ollamaBaseUrl || undefined,
memoryOllamaEmbeddingModel: config.ollamaEmbeddingModel || undefined,
memoryOllamaEmbeddingDim: config.ollamaEmbeddingDim || undefined,
// Agent memory access (MCP)
@@ -150,6 +159,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
globalGoogleApiKey: config.googleApiKey.trim() || undefined,
// Provider-specific keys for memory
memoryVoyageApiKey: config.voyageApiKey.trim() || undefined,
memoryVoyageEmbeddingModel: config.voyageEmbeddingModel.trim() || undefined,
memoryAzureApiKey: config.azureOpenaiApiKey.trim() || undefined,
memoryAzureBaseUrl: config.azureOpenaiBaseUrl.trim() || undefined,
memoryAzureEmbeddingDeployment: config.azureOpenaiEmbeddingDeployment.trim() || undefined,
@@ -162,6 +172,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
const storeUpdate: Partial<AppSettings> = {
memoryEnabled: config.enabled,
memoryEmbeddingProvider: config.embeddingProvider,
ollamaBaseUrl: config.ollamaBaseUrl || undefined,
memoryOllamaEmbeddingModel: config.ollamaEmbeddingModel || undefined,
memoryOllamaEmbeddingDim: config.ollamaEmbeddingDim || undefined,
graphitiMcpEnabled: config.agentMemoryEnabled,
@@ -169,6 +180,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
globalOpenAIApiKey: config.openaiApiKey.trim() || undefined,
globalGoogleApiKey: config.googleApiKey.trim() || undefined,
memoryVoyageApiKey: config.voyageApiKey.trim() || undefined,
memoryVoyageEmbeddingModel: config.voyageEmbeddingModel.trim() || undefined,
memoryAzureApiKey: config.azureOpenaiApiKey.trim() || undefined,
memoryAzureBaseUrl: config.azureOpenaiBaseUrl.trim() || undefined,
memoryAzureEmbeddingDeployment: config.azureOpenaiEmbeddingDeployment.trim() || undefined,
@@ -185,178 +197,6 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
}
};
const handleOllamaModelSelect = (modelName: string, dim: number) => {
setConfig(prev => ({
...prev,
ollamaEmbeddingModel: modelName,
ollamaEmbeddingDim: dim,
}));
};
// Render provider-specific configuration fields
const renderProviderFields = () => {
const { embeddingProvider } = config;
if (embeddingProvider === 'ollama') {
return (
<div className="space-y-3">
<Label className="text-sm font-medium text-foreground">{t('memory.selectEmbeddingModel')}</Label>
<OllamaModelSelector
selectedModel={config.ollamaEmbeddingModel}
onModelSelect={handleOllamaModelSelect}
disabled={isSaving}
/>
</div>
);
}
if (embeddingProvider === 'openai') {
return (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.openaiApiKey')}</Label>
<p className="text-xs text-muted-foreground">{t('memory.openaiApiKeyDescription')}</p>
<div className="relative">
<Input
type={showApiKey['openai'] ? 'text' : 'password'}
value={config.openaiApiKey}
onChange={(e) => setConfig(prev => ({ ...prev, openaiApiKey: e.target.value }))}
placeholder="sk-..."
className="pr-10 font-mono text-sm"
disabled={isSaving}
/>
<button
type="button"
onClick={() => toggleShowApiKey('openai')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey['openai'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
{t('memory.openaiGetKey')}{' '}
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
OpenAI
</a>
</p>
</div>
);
}
if (embeddingProvider === 'voyage') {
return (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.voyageApiKey')}</Label>
<p className="text-xs text-muted-foreground">{t('memory.voyageApiKeyDescription')}</p>
<div className="relative">
<Input
type={showApiKey['voyage'] ? 'text' : 'password'}
value={config.voyageApiKey}
onChange={(e) => setConfig(prev => ({ ...prev, voyageApiKey: e.target.value }))}
placeholder="pa-..."
className="pr-10 font-mono text-sm"
disabled={isSaving}
/>
<button
type="button"
onClick={() => toggleShowApiKey('voyage')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey['voyage'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
{t('memory.openaiGetKey')}{' '}
<a href="https://dash.voyageai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
Voyage AI
</a>
</p>
</div>
);
}
if (embeddingProvider === 'google') {
return (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.googleApiKey')}</Label>
<p className="text-xs text-muted-foreground">{t('memory.googleApiKeyDescription')}</p>
<div className="relative">
<Input
type={showApiKey['google'] ? 'text' : 'password'}
value={config.googleApiKey}
onChange={(e) => setConfig(prev => ({ ...prev, googleApiKey: e.target.value }))}
placeholder="AIza..."
className="pr-10 font-mono text-sm"
disabled={isSaving}
/>
<button
type="button"
onClick={() => toggleShowApiKey('google')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey['google'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
{t('memory.openaiGetKey')}{' '}
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
Google AI Studio
</a>
</p>
</div>
);
}
if (embeddingProvider === 'azure_openai') {
return (
<div className="space-y-3 p-3 rounded-md bg-muted/50">
<Label className="text-sm font-medium text-foreground">{t('memory.azureConfig')}</Label>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">{t('memory.azureApiKey')}</Label>
<div className="relative">
<Input
type={showApiKey['azure'] ? 'text' : 'password'}
value={config.azureOpenaiApiKey}
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiApiKey: e.target.value }))}
placeholder="Azure API Key"
className="pr-10 font-mono text-sm"
disabled={isSaving}
/>
<button
type="button"
onClick={() => toggleShowApiKey('azure')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showApiKey['azure'] ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{t('memory.azureBaseUrl')}</Label>
<Input
placeholder="https://your-resource.openai.azure.com"
value={config.azureOpenaiBaseUrl}
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))}
className="font-mono text-sm"
disabled={isSaving}
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{t('memory.azureEmbeddingDeployment')}</Label>
<Input
placeholder="text-embedding-ada-002"
value={config.azureOpenaiEmbeddingDeployment}
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))}
className="font-mono text-sm"
disabled={isSaving}
/>
</div>
</div>
);
}
return null;
};
return (
<div className="flex h-full flex-col items-center justify-center px-8 py-6">
<div className="w-full max-w-2xl">
@@ -425,6 +265,12 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
{/* Memory Enabled Configuration */}
{config.enabled && (
<>
{/* Infrastructure Status */}
<InfrastructureStatus
infrastructureStatus={infrastructureStatus}
isCheckingInfrastructure={isCheckingInfra}
/>
{/* Agent Memory Access Toggle */}
<div className="flex items-center justify-between">
<div className="space-y-0.5">
@@ -473,7 +319,7 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
disabled={isSaving}
>
<SelectTrigger>
<SelectValue placeholder={t('memory.embeddingProvider')} />
<SelectValue placeholder={t('memory.selectEmbeddingModel')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="ollama">{t('memory.providers.ollama')}</SelectItem>
@@ -486,7 +332,146 @@ export function MemoryStep({ onNext, onBack }: MemoryStepProps) {
</div>
{/* Provider-specific fields */}
{renderProviderFields()}
{/* OpenAI */}
{config.embeddingProvider === 'openai' && (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.openaiApiKey')}</Label>
<p className="text-xs text-muted-foreground">
{t('memory.openaiApiKeyDescription')}
</p>
<PasswordInput
value={config.openaiApiKey}
onChange={(value) => setConfig(prev => ({ ...prev, openaiApiKey: value }))}
placeholder="sk-..."
/>
<p className="text-xs text-muted-foreground">
{t('memory.openaiGetKey')}{' '}
<a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
OpenAI
</a>
</p>
</div>
)}
{/* Voyage AI */}
{config.embeddingProvider === 'voyage' && (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.voyageApiKey')}</Label>
<p className="text-xs text-muted-foreground">
{t('memory.voyageApiKeyDescription')}
</p>
<PasswordInput
value={config.voyageApiKey}
onChange={(value) => setConfig(prev => ({ ...prev, voyageApiKey: value }))}
placeholder="pa-..."
/>
<div className="space-y-1 mt-2">
<Label className="text-xs text-muted-foreground">{t('memory.embeddingModel')}</Label>
<Input
placeholder="voyage-3"
value={config.voyageEmbeddingModel}
onChange={(e) => setConfig(prev => ({ ...prev, voyageEmbeddingModel: e.target.value }))}
/>
</div>
<p className="text-xs text-muted-foreground mt-1">
{t('memory.openaiGetKey')}{' '}
<a href="https://dash.voyageai.com/api-keys" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
Voyage AI
</a>
</p>
</div>
)}
{/* Google AI */}
{config.embeddingProvider === 'google' && (
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">{t('memory.googleApiKey')}</Label>
<p className="text-xs text-muted-foreground">
{t('memory.googleApiKeyDescription')}
</p>
<PasswordInput
value={config.googleApiKey}
onChange={(value) => setConfig(prev => ({ ...prev, googleApiKey: value }))}
placeholder="AIza..."
/>
<p className="text-xs text-muted-foreground">
{t('memory.openaiGetKey')}{' '}
<a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener noreferrer" className="text-primary hover:text-primary/80">
Google AI Studio
</a>
</p>
</div>
)}
{/* Azure OpenAI */}
{config.embeddingProvider === 'azure_openai' && (
<div className="space-y-3">
<Label className="text-sm font-medium text-foreground">{t('memory.azureConfig')}</Label>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">{t('memory.azureApiKey')}</Label>
<PasswordInput
value={config.azureOpenaiApiKey}
onChange={(value) => setConfig(prev => ({ ...prev, azureOpenaiApiKey: value }))}
placeholder="Azure API Key"
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{t('memory.azureBaseUrl')}</Label>
<Input
placeholder="https://your-resource.openai.azure.com"
value={config.azureOpenaiBaseUrl}
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiBaseUrl: e.target.value }))}
className="font-mono text-sm"
disabled={isSaving}
/>
</div>
<div className="space-y-1">
<Label className="text-xs text-muted-foreground">{t('memory.azureEmbeddingDeployment')}</Label>
<Input
placeholder="text-embedding-ada-002"
value={config.azureOpenaiEmbeddingDeployment}
onChange={(e) => setConfig(prev => ({ ...prev, azureOpenaiEmbeddingDeployment: e.target.value }))}
className="font-mono text-sm"
disabled={isSaving}
/>
</div>
</div>
)}
{/* Ollama (Local) */}
{/* Ollama (Local) */}
{config.embeddingProvider === 'ollama' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-foreground">{t('memory.ollamaConfig')}</Label>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">{t('memory.baseUrl')}</Label>
<Input
placeholder="http://localhost:11434"
value={config.ollamaBaseUrl}
onChange={(e) => setConfig(prev => ({ ...prev, ollamaBaseUrl: e.target.value }))}
/>
</div>
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">{t('memory.embeddingModel')}</Label>
<OllamaModelSelector
selectedModel={config.ollamaEmbeddingModel}
baseUrl={config.ollamaBaseUrl}
onModelSelect={(model, dim) => {
setConfig(prev => ({
...prev,
ollamaEmbeddingModel: model,
ollamaEmbeddingDim: dim
}));
}}
disabled={isSaving}
/>
</div>
</div>
)}
{/* Info about Learn More */}
<div className="rounded-lg border border-info/30 bg-info/10 p-4">
@@ -28,6 +28,7 @@ interface OllamaModelSelectorProps {
onModelSelect: (model: string, dim: number) => void;
disabled?: boolean;
className?: string;
baseUrl?: string;
}
// Recommended embedding models for Auto Claude Memory
@@ -73,7 +74,6 @@ const RECOMMENDED_MODELS: OllamaModel[] = [
},
];
/**
* OllamaModelSelector Component
*
@@ -107,6 +107,7 @@ export function OllamaModelSelector({
onModelSelect,
disabled = false,
className,
baseUrl,
}: OllamaModelSelectorProps) {
const { t } = useTranslation('onboarding');
const [models, setModels] = useState<OllamaModel[]>(RECOMMENDED_MODELS);
@@ -149,7 +150,7 @@ export function OllamaModelSelector({
}
// Ollama is installed, now check if it's running
const statusResult = await window.electronAPI.checkOllamaStatus();
const statusResult = await window.electronAPI.checkOllamaStatus(baseUrl);
if (abortSignal?.aborted) return;
if (!statusResult?.success || !statusResult?.data?.running) {
@@ -161,7 +162,7 @@ export function OllamaModelSelector({
setOllamaState('available');
// Get list of installed embedding models
const result = await window.electronAPI.listOllamaEmbeddingModels();
const result = await window.electronAPI.listOllamaEmbeddingModels(baseUrl);
if (abortSignal?.aborted) return;
if (result?.success && result?.data?.embedding_models) {
@@ -258,7 +259,7 @@ export function OllamaModelSelector({
clearTimeout(installCheckTimeoutRef.current);
}
};
}, []);
}, [baseUrl]);
// Progress is now handled globally by the download store listener initialized in App.tsx
@@ -17,7 +17,7 @@ import { OAuthStep } from './OAuthStep';
import { ClaudeCodeStep } from './ClaudeCodeStep';
import { DevToolsStep } from './DevToolsStep';
import { PrivacyStep } from './PrivacyStep';
import { GraphitiStep } from './GraphitiStep';
import { MemoryStep } from './MemoryStep';
import { CompletionStep } from './CompletionStep';
import { useSettingsStore } from '../../stores/settings-store';
@@ -29,7 +29,7 @@ interface OnboardingWizardProps {
}
// Wizard step identifiers
type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'graphiti' | 'completion';
type WizardStepId = 'welcome' | 'auth-choice' | 'oauth' | 'claude-code' | 'devtools' | 'privacy' | 'memory' | 'completion';
// Step configuration with translation keys
const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [
@@ -39,7 +39,7 @@ const WIZARD_STEPS: { id: WizardStepId; labelKey: string }[] = [
{ id: 'claude-code', labelKey: 'steps.claudeCode' },
{ id: 'devtools', labelKey: 'steps.devtools' },
{ id: 'privacy', labelKey: 'steps.privacy' },
{ id: 'graphiti', labelKey: 'steps.memory' },
{ id: 'memory', labelKey: 'steps.memory' },
{ id: 'completion', labelKey: 'steps.done' }
];
@@ -93,8 +93,8 @@ export function OnboardingWizard({
}, [currentStepIndex, currentStepId]);
const goToPreviousStep = useCallback(() => {
// If going back from graphiti and oauth was bypassed, go back to auth-choice (skip oauth)
if (currentStepId === 'graphiti' && oauthBypassed) {
// If going back from memory and oauth was bypassed, go back to auth-choice (skip oauth)
if (currentStepId === 'memory' && oauthBypassed) {
// Find index of auth-choice step
const authChoiceIndex = WIZARD_STEPS.findIndex(step => step.id === 'auth-choice');
setCurrentStepIndex(authChoiceIndex);
@@ -108,13 +108,13 @@ export function OnboardingWizard({
}, [currentStepIndex, currentStepId, oauthBypassed]);
// Handler for when API key path is chosen - skips oauth step
const handleSkipToGraphiti = useCallback(() => {
const handleSkipToMemory = useCallback(() => {
setOauthBypassed(true);
setCompletedSteps(prev => new Set(prev).add('auth-choice'));
// Find index of graphiti step
const graphitiIndex = WIZARD_STEPS.findIndex(step => step.id === 'graphiti');
setCurrentStepIndex(graphitiIndex);
// Find index of memory step
const memoryIndex = WIZARD_STEPS.findIndex(step => step.id === 'memory');
setCurrentStepIndex(memoryIndex);
}, []);
// Reset wizard state (for re-running) - defined before skipWizard/finishWizard that use it
@@ -124,7 +124,7 @@ export function OnboardingWizard({
setOauthBypassed(false);
}, []);
const skipWizard = useCallback(async () => {
const completeWizard = useCallback(async () => {
// Mark onboarding as completed and close - save to disk AND update local state
try {
const result = await window.electronAPI.saveSettings({ onboardingCompleted: true });
@@ -139,21 +139,6 @@ export function OnboardingWizard({
resetWizard();
}, [updateSettings, onOpenChange, resetWizard]);
const finishWizard = useCallback(async () => {
// Mark onboarding as completed - save to disk AND update local state
try {
const result = await window.electronAPI.saveSettings({ onboardingCompleted: true });
if (!result?.success) {
console.error('Failed to save onboarding completion:', result?.error);
}
} catch (err) {
console.error('Error saving onboarding completion:', err);
}
updateSettings({ onboardingCompleted: true });
onOpenChange(false);
resetWizard();
}, [updateSettings, onOpenChange, resetWizard]);
// Handle opening task creator from within wizard
const handleOpenTaskCreator = useCallback(() => {
if (onOpenTaskCreator) {
@@ -167,10 +152,10 @@ export function OnboardingWizard({
const handleOpenSettings = useCallback(() => {
if (onOpenSettings) {
// Finish wizard first, then open settings
finishWizard();
completeWizard();
onOpenSettings();
}
}, [onOpenSettings, finishWizard]);
}, [onOpenSettings, completeWizard]);
// Render current step content
const renderStepContent = () => {
@@ -179,7 +164,7 @@ export function OnboardingWizard({
return (
<WelcomeStep
onGetStarted={goToNextStep}
onSkip={skipWizard}
onSkip={completeWizard}
/>
);
case 'auth-choice':
@@ -187,8 +172,8 @@ export function OnboardingWizard({
<AuthChoiceStep
onNext={goToNextStep}
onBack={goToPreviousStep}
onSkip={skipWizard}
onAPIKeyPathComplete={handleSkipToGraphiti}
onSkip={completeWizard}
onAPIKeyPathComplete={handleSkipToMemory}
/>
);
case 'oauth':
@@ -196,7 +181,7 @@ export function OnboardingWizard({
<OAuthStep
onNext={goToNextStep}
onBack={goToPreviousStep}
onSkip={skipWizard}
onSkip={completeWizard}
/>
);
case 'claude-code':
@@ -204,7 +189,7 @@ export function OnboardingWizard({
<ClaudeCodeStep
onNext={goToNextStep}
onBack={goToPreviousStep}
onSkip={skipWizard}
onSkip={completeWizard}
/>
);
case 'devtools':
@@ -221,18 +206,17 @@ export function OnboardingWizard({
onBack={goToPreviousStep}
/>
);
case 'graphiti':
case 'memory':
return (
<GraphitiStep
<MemoryStep
onNext={goToNextStep}
onBack={goToPreviousStep}
onSkip={skipWizard}
/>
);
case 'completion':
return (
<CompletionStep
onFinish={finishWizard}
onFinish={completeWizard}
onOpenTaskCreator={handleOpenTaskCreator}
onOpenSettings={handleOpenSettings}
/>
@@ -246,11 +230,11 @@ export function OnboardingWizard({
const handleOpenChange = useCallback((newOpen: boolean) => {
if (!newOpen) {
// If closing before completion, skip the wizard
skipWizard();
completeWizard();
} else {
onOpenChange(newOpen);
}
}, [skipWizard, onOpenChange]);
}, [completeWizard, onOpenChange]);
return (
<FullScreenDialog open={open} onOpenChange={handleOpenChange}>
@@ -300,12 +300,30 @@ export function SecuritySettings({
// Ollama (Local) - uses OllamaModelSelector component
if (embeddingProvider === 'ollama') {
return (
<div className="space-y-3">
<Label className="text-sm font-medium text-foreground">Select Embedding Model</Label>
<OllamaModelSelector
selectedModel={envConfig.graphitiProviderConfig?.ollamaEmbeddingModel || ''}
onModelSelect={handleOllamaModelSelect}
/>
<div className="space-y-4">
<div className="space-y-2">
<Label className="text-xs text-muted-foreground">Base URL</Label>
<Input
placeholder="http://localhost:11434"
value={envConfig.graphitiProviderConfig?.ollamaBaseUrl || 'http://localhost:11434'}
onChange={(e) => updateEnvConfig({
graphitiProviderConfig: {
...envConfig.graphitiProviderConfig,
embeddingProvider: 'ollama',
ollamaBaseUrl: e.target.value,
}
})}
/>
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">Select Embedding Model</Label>
<OllamaModelSelector
selectedModel={envConfig.graphitiProviderConfig?.ollamaEmbeddingModel || ''}
baseUrl={envConfig.graphitiProviderConfig?.ollamaBaseUrl}
onModelSelect={handleOllamaModelSelect}
/>
</div>
</div>
);
}
@@ -100,6 +100,7 @@
"openaiGetKey": "Get your key from",
"voyageApiKey": "Voyage AI API Key",
"voyageApiKeyDescription": "Required for Voyage AI embeddings",
"voyageEmbeddingModel": "Embedding Model",
"googleApiKey": "Google AI API Key",
"googleApiKeyDescription": "Required for Google AI embeddings",
"azureConfig": "Azure OpenAI Configuration",
@@ -118,7 +119,16 @@
"voyage": "Voyage AI",
"google": "Google AI",
"azure": "Azure OpenAI"
}
},
"ollamaConfig": "Ollama Configuration",
"checking": "Checking...",
"connected": "Connected",
"notRunning": "Not running",
"baseUrl": "Base URL",
"embeddingModel": "Embedding Model",
"embeddingDim": "Embedding Dimension",
"embeddingDimDescription": "Required for Ollama embeddings (e.g., 768 for nomic-embed-text)",
"modelRecommendation": "Recommended: qwen3-embedding:4b (balanced), :8b (quality), :0.6b (fast)"
},
"completion": {
"title": "You're All Set!",
@@ -245,6 +245,7 @@ export interface AppSettings {
memoryOllamaEmbeddingModel?: string;
memoryOllamaEmbeddingDim?: number;
memoryVoyageApiKey?: string;
memoryVoyageEmbeddingModel?: string;
memoryAzureApiKey?: string;
memoryAzureBaseUrl?: string;
memoryAzureEmbeddingDeployment?: string;