Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/apps/desktop/tauri.dev.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "BitFun",
"identifier": "com.bitfun.desktop",
"identifier": "com.bitfun.desktop.dev",
"build": {
"beforeDevCommand": "pnpm run dev:web",
"devUrl": "http://localhost:1422",
Expand Down
4 changes: 3 additions & 1 deletion src/web-ui/src/app/scenes/settings/settingsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [
'model',
'temperature',
'token',
'session title',
'auto title',
'subagent',
],
},
{
Expand Down Expand Up @@ -135,7 +138,6 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [
descriptionKey: 'configCenter.tabDescriptions.sessionPersonalization',
keywords: [
'session',
'title',
'companion',
'agent',
'pixel',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const SETTINGS_TAB_SEARCH_CONTENT: Record<ConfigTab, readonly SettingsTab
{ ns: 'settings/default-model', key: 'subtitle' },
{ ns: 'settings/default-model', key: 'tabs.models' },
{ ns: 'settings/ai-model', key: 'subtitle' },
{ ns: 'settings/ai-model', key: 'subagentModels.title' },
{ ns: 'settings/ai-model', key: 'sessionTitle.title' },
{ ns: 'settings/ai-model', key: 'sessionTitle.subtitle' },
{ ns: 'settings/default-model', key: 'tabs.proxy' },
{ ns: 'settings/ai-model', key: 'proxy.enableHint' },
],
Expand All @@ -56,11 +59,8 @@ export const SETTINGS_TAB_SEARCH_CONTENT: Record<ConfigTab, readonly SettingsTab
'session-personalization': [
{ ns: 'settings/session-config', key: 'personalizationPage.title' },
{ ns: 'settings/session-config', key: 'personalizationPage.subtitle' },
{ ns: 'settings/session-config', key: 'features.sessionTitle.title' },
{ ns: 'settings/session-config', key: 'features.sessionTitle.subtitle' },
{ ns: 'settings/session-config', key: 'features.agentCompanion.title' },
{ ns: 'settings/session-config', key: 'features.agentCompanion.subtitle' },
{ ns: 'settings/session-config', key: 'models.empty' },
],

'session-permissions': [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useNotification } from '@/shared/notification-system';
import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow, ConfigCollectionItem } from './common';
import DefaultModelConfig from './DefaultModelConfig';
import SubagentModelConfig from './SubagentModelConfig';
import SessionTitleConfig from './SessionTitleConfig';
import { createLogger } from '@/shared/utils/logger';
import { translateConnectionTestMessage } from '@/shared/utils/aiConnectionTestMessages';
import { i18nService } from '@/infrastructure/i18n';
Expand Down Expand Up @@ -2585,6 +2586,8 @@ const AIModelConfig: React.FC = () => {
<SubagentModelConfig />
</ConfigPageSection>

<SessionTitleConfig />

<ConfigPageSection
title={t('cliAuth.sectionTitle')}
description={t('cliAuth.sectionDescription')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@ import {
import { configManager } from '../services/ConfigManager';
import { systemAPI } from '@/infrastructure/api/service-api/SystemAPI';
import { useNotification, notificationService } from '@/shared/notification-system';
import type { AIModelConfig, DebugModeConfig, LanguageDebugTemplate } from '../types';
import type { DebugModeConfig, LanguageDebugTemplate } from '../types';
import {
LANGUAGE_TEMPLATE_LABELS,
DEFAULT_DEBUG_MODE_CONFIG,
ALL_LANGUAGES,
DEFAULT_LANGUAGE_TEMPLATES,
} from '../types';
import { ModelSelectionRadio } from './ModelSelectionRadio';
import { ChatInputPixelPet } from '@/flow_chat/components/ChatInputPixelPet';
import { ask, open } from '@tauri-apps/plugin-dialog';
import { createLogger } from '@/shared/utils/logger';
Expand All @@ -47,8 +46,6 @@ const log = createLogger('SessionSettingsPanels');

const IS_TAURI_DESKTOP = typeof window !== 'undefined' && '__TAURI__' in window;

const AGENT_SESSION_TITLE = 'session-title-func-agent';

type ComputerUseStatusPayload = {
computerUseEnabled: boolean;
accessibilityGranted: boolean;
Expand Down Expand Up @@ -103,8 +100,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }
const [companionPetImporting, setCompanionPetImporting] = useState(false);
const [companionPetDeletingPath, setCompanionPetDeletingPath] = useState<string | null>(null);
const [companionPetListExpanded, setCompanionPetListExpanded] = useState(false);
const [models, setModels] = useState<AIModelConfig[]>([]);
const [funcAgentModels, setFuncAgentModels] = useState<Record<string, string>>({});
const [skipToolConfirmation, setSkipToolConfirmation] = useState(true);
const [enableDeferredToolLoading, setEnableDeferredToolLoading] = useState(true);
const [subagentMaxConcurrency, setSubagentMaxConcurrency] = useState(DEFAULT_SUBAGENT_MAX_CONCURRENCY);
Expand Down Expand Up @@ -209,8 +204,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }
try {
const [
loadedSettings,
allModels,
funcAgentModelsData,
skipConfirm,
deferredToolLoadingEnabled,
loadedSubagentMaxConcurrency,
Expand All @@ -223,8 +216,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }
loadedCompanionPets,
] = await Promise.all([
aiExperienceConfigService.getSettingsAsync(),
configManager.getConfig<AIModelConfig[]>('ai.models') || [],
configManager.getConfig<Record<string, string>>('ai.func_agent_models') || {},
configManager.getConfig<boolean>('ai.skip_tool_confirmation'),
configManager.getConfig<boolean>('ai.enable_deferred_tool_loading'),
configManager.getConfig<number | null>('ai.subagent_max_concurrency'),
Expand All @@ -239,8 +230,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }

setSettings(loadedSettings);
setCompanionPets(loadedCompanionPets);
setModels(allModels as AIModelConfig[]);
setFuncAgentModels(funcAgentModelsData as Record<string, string>);
setSkipToolConfirmation(skipConfirm ?? true);
setEnableDeferredToolLoading(deferredToolLoadingEnabled ?? true);
setSubagentMaxConcurrency(loadedSubagentMaxConcurrency != null
Expand Down Expand Up @@ -431,37 +420,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }
setCompanionPetListExpanded(false);
};

const getModelName = useCallback((modelId: string | null | undefined): string | undefined => {
if (!modelId) return undefined;
return models.find(m => m.id === modelId)?.name;
}, [models]);

const handleAgentModelChange = async (agentKey: string, featureTitleKey: string, modelId: string) => {
try {
const current = await configManager.getConfig<Record<string, string>>('ai.func_agent_models') || {};
const updated = { ...current, [agentKey]: modelId };
await configManager.setConfig('ai.func_agent_models', updated);
setFuncAgentModels(updated);

let modelDesc = '';
if (modelId === 'primary') {
modelDesc = t('model.primary');
} else if (modelId === 'fast') {
modelDesc = t('model.fast');
} else {
modelDesc = getModelName(modelId) || modelId || '';
}

notificationService.success(
t('models.updateSuccess', { agentName: t(featureTitleKey), modelName: modelDesc }),
{ duration: 2000 }
);
} catch (error) {
log.error('Failed to update agent model', { agentKey, modelId, error });
notificationService.error(t('messages.updateFailed'), { duration: 3000 });
}
};

const handleSkipToolConfirmationChange = async (checked: boolean) => {
setSkipToolConfirmation(checked);
setToolExecConfigLoading(true);
Expand Down Expand Up @@ -812,8 +770,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }

// ── Derived values ───────────────────────────────────────────────────────

const enabledModels = models.filter((m: AIModelConfig) => m.enabled);
const sessionTitleModelId = funcAgentModels[AGENT_SESSION_TITLE] || 'fast';
const templateEntries = getTemplateEntries();
const computerUseAccessLabel = computerUseStatusLoading
? t('loading.text')
Expand Down Expand Up @@ -857,38 +813,6 @@ const SessionSettingsPanels: React.FC<SessionSettingsPanelsProps> = ({ variant }
{variant === 'personalization' ? (
<>

{/* ── Auto session title ─────────────────────────────────── */}
<ConfigPageSection
title={t('features.sessionTitle.title')}
description={t('features.sessionTitle.subtitle')}
>
<ConfigPageRow label={t('common.enable')} align="center">
<div className="bitfun-func-agent-config__row-control">
<Switch
checked={settings.enable_session_title_generation}
onChange={(e) => updateSetting('enable_session_title_generation', e.target.checked)}
size="small"
/>
</div>
</ConfigPageRow>
<ConfigPageRow
className="bitfun-func-agent-config__model-row"
label={t('model.label')}
description={enabledModels.length === 0 ? t('models.empty') : undefined}
align="center"
>
<div className="bitfun-func-agent-config__row-control bitfun-func-agent-config__row-control--model">
<ModelSelectionRadio
value={sessionTitleModelId}
models={enabledModels}
onChange={(modelId) => handleAgentModelChange(AGENT_SESSION_TITLE, 'features.sessionTitle.title', modelId)}
layout="horizontal"
size="small"
/>
</div>
</ConfigPageRow>
</ConfigPageSection>

{/* ── Agent companion (collapsed input) ─────────────────── */}
<ConfigPageSection
title={t('features.agentCompanion.title')}
Expand Down
147 changes: 147 additions & 0 deletions src/web-ui/src/infrastructure/config/components/SessionTitleConfig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Switch } from '@/component-library';
import { useNotification, notificationService } from '@/shared/notification-system';
import { createLogger } from '@/shared/utils/logger';
import { aiExperienceConfigService, type AIExperienceSettings } from '../services/AIExperienceConfigService';
import { configManager } from '../services/ConfigManager';
import type { AIModelConfig } from '../types';
import { ConfigPageRow, ConfigPageSection } from './common';
import { ModelSelectionRadio } from './ModelSelectionRadio';
import './AIFeaturesConfig.scss';

const log = createLogger('SessionTitleConfig');

const AGENT_SESSION_TITLE = 'session-title-func-agent';

export const SessionTitleConfig: React.FC = () => {
const { t } = useTranslation('settings/ai-model');
const notification = useNotification();
const [isLoading, setIsLoading] = useState(true);
const [settings, setSettings] = useState<AIExperienceSettings | null>(null);
const [models, setModels] = useState<AIModelConfig[]>([]);
const [funcAgentModels, setFuncAgentModels] = useState<Record<string, string>>({});

const loadData = useCallback(async () => {
setIsLoading(true);
try {
const [loadedSettings, allModels, funcAgentModelsData] = await Promise.all([
aiExperienceConfigService.getSettingsAsync(),
configManager.getConfig<AIModelConfig[]>('ai.models') || [],
configManager.getConfig<Record<string, string>>('ai.func_agent_models') || {},
]);
setSettings(loadedSettings);
setModels(allModels ?? []);
setFuncAgentModels(funcAgentModelsData ?? {});
} catch (error) {
log.error('Failed to load session title config', error);
notification.error(t('sessionTitle.loadFailed'));
} finally {
setIsLoading(false);
}
}, [notification, t]);

useEffect(() => {
void loadData();
const unwatchModels = configManager.watch('ai.models', () => void loadData());
const unwatchFuncAgentModels = configManager.watch('ai.func_agent_models', () => void loadData());
const unwatchSettings = aiExperienceConfigService.addChangeListener((next) => {
setSettings(next);
});
return () => {
unwatchModels();
unwatchFuncAgentModels();
unwatchSettings();
};
}, [loadData]);

const enabledModels = models.filter((model) => model.enabled);
const sessionTitleModelId = funcAgentModels[AGENT_SESSION_TITLE] || 'fast';

const updateEnabled = async (checked: boolean) => {
if (!settings) return;
const previous = settings;
const next = { ...settings, enable_session_title_generation: checked };
setSettings(next);
try {
await aiExperienceConfigService.saveSettings(next);
notification.success(t('sessionTitle.messages.saveSuccess'));
} catch (error) {
log.error('Failed to save session title enable setting', error);
notification.error(t('sessionTitle.messages.saveFailed'));
setSettings(previous);
}
};

const getModelName = useCallback((modelId: string | null | undefined): string | undefined => {
if (!modelId) return undefined;
return models.find((model) => model.id === modelId)?.name;
}, [models]);

const handleModelChange = async (modelId: string) => {
try {
const current = await configManager.getConfig<Record<string, string>>('ai.func_agent_models') || {};
const updated = { ...current, [AGENT_SESSION_TITLE]: modelId };
await configManager.setConfig('ai.func_agent_models', updated);
setFuncAgentModels(updated);

let modelDesc = '';
if (modelId === 'primary') {
modelDesc = t('sessionTitle.model.primary');
} else if (modelId === 'fast') {
modelDesc = t('sessionTitle.model.fast');
} else {
modelDesc = getModelName(modelId) || modelId || '';
}

notificationService.success(
t('sessionTitle.models.updateSuccess', {
agentName: t('sessionTitle.title'),
modelName: modelDesc,
}),
{ duration: 2000 },
);
} catch (error) {
log.error('Failed to update session title model', { modelId, error });
notificationService.error(t('sessionTitle.messages.updateFailed'), { duration: 3000 });
}
};

return (
<ConfigPageSection
className="bitfun-func-agent-config"
title={t('sessionTitle.title')}
description={t('sessionTitle.subtitle')}
>
<ConfigPageRow label={t('sessionTitle.enable')} align="center">
<div className="bitfun-func-agent-config__row-control">
<Switch
checked={settings?.enable_session_title_generation ?? false}
onChange={(e) => void updateEnabled(e.target.checked)}
size="small"
disabled={isLoading || !settings}
/>
</div>
</ConfigPageRow>
<ConfigPageRow
className="bitfun-func-agent-config__model-row"
label={t('sessionTitle.model.label')}
description={enabledModels.length === 0 ? t('sessionTitle.models.empty') : undefined}
align="center"
>
<div className="bitfun-func-agent-config__row-control bitfun-func-agent-config__row-control--model">
<ModelSelectionRadio
value={sessionTitleModelId}
models={enabledModels}
onChange={(modelId) => void handleModelChange(modelId)}
layout="horizontal"
size="small"
disabled={isLoading}
/>
</div>
</ConfigPageRow>
</ConfigPageSection>
);
};

export default SessionTitleConfig;
4 changes: 2 additions & 2 deletions src/web-ui/src/locales/en-US/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"tabDescriptions": {
"basics": "Logging, terminal shell, notifications, and launch at login.",
"appearance": "Language, theme, and UI font size.",
"models": "AI models, API keys, providers, and proxy.",
"sessionPersonalization": "Session title and Agent companion.",
"models": "AI models, API keys, providers, proxy, and session title.",
"sessionPersonalization": "Agent companion.",
"sessionPermissions": "Accelerated workspace search, tool confirmation, Computer use, browser, and debug.",
"quickActions": "One-click AI actions after coding. Built-in and customizable prompts.",
"review": "Review strategy, coverage depth, capacity, cost, and latency controls.",
Expand Down
20 changes: 20 additions & 0 deletions src/web-ui/src/locales/en-US/settings/ai-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@
},
"loadFailed": "Failed to load Subagent model settings"
},
"sessionTitle": {
"title": "Auto Session Title",
"subtitle": "AI automatically generates concise titles for new conversations",
"enable": "Enable",
"loadFailed": "Failed to load session title settings",
"model": {
"label": "Model",
"primary": "Primary Model",
"fast": "Fast Model"
},
"models": {
"empty": "No available models. Please add models in \"Models\" first.",
"updateSuccess": "{{agentName}} will use {{modelName}}"
},
"messages": {
"saveSuccess": "Settings saved",
"saveFailed": "Save failed",
"updateFailed": "Update failed"
}
},
"cliAuth": {
"sectionTitle": "Local CLI accounts",
"sectionDescription": "Import Codex CLI / Gemini CLI accounts already signed in on this machine and use them as models",
Expand Down
Loading
Loading