feat(settings): add response style presets

This commit is contained in:
Bohdan Triapitsyn
2026-05-01 01:27:31 +03:00
parent bcbf34b1d1
commit c924e85a29
12 changed files with 407 additions and 21 deletions
@@ -72,6 +72,8 @@ import { buildSessionTargetOptions } from '@/sync/session-worktree-contract';
import { usePermissionStore } from '@/stores/permissionStore';
import { extractGitChangedFiles } from './changedFiles';
import { useI18n } from '@/lib/i18n';
import { fetchResponseStyleInstruction } from '@/lib/responseStyle';
import { getSyncMessages } from '@/sync/sync-refs';
const MAX_VISIBLE_TEXTAREA_LINES = 8;
const EMPTY_QUEUE: QueuedMessage[] = [];
@@ -86,6 +88,10 @@ const VS_CODE_DROP_DATA_TYPES = [
'text/plain',
];
const hasUserMessages = (sessionId: string, directory?: string) => {
return getSyncMessages(sessionId, directory).some((message) => message.role === 'user');
};
const FILE_URI_PREFIX = 'file://';
const encodeFilePath = (filepath: string): string => {
@@ -1547,6 +1553,20 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({ onOpenSettings, scrollTo
}
}
const currentSessionDirectory = currentSessionId
? useSessionUIStore.getState().getDirectoryForSession(currentSessionId) || currentDirectory
: currentDirectory;
const shouldAddResponseStyle = newSessionDraftOpen || (currentSessionId ? !hasUserMessages(currentSessionId, currentSessionDirectory) : false);
if (shouldAddResponseStyle) {
const responseStyleInstruction = await fetchResponseStyleInstruction().catch(() => null);
if (responseStyleInstruction) {
additionalParts.push({
text: responseStyleInstruction,
synthetic: true,
});
}
}
// Collect all attachments for error recovery
const allAttachments = [
...primaryAttachments,
@@ -3,9 +3,24 @@ import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
import { toast } from '@/components/ui';
import { useI18n } from '@/lib/i18n';
import { useI18n, type I18nKey } from '@/lib/i18n';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Checkbox } from '@/components/ui/checkbox';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { RiInformationLine } from '@remixicon/react';
import {
getResponseStylePresetInstructions,
isResponseStylePreset,
RESPONSE_STYLE_PRESETS,
type ResponseStylePreset,
} from '@/lib/responseStyle';
import type { DesktopSettings } from '@/lib/desktop';
const AGENTS_MD_PATH = '~/.config/opencode/AGENTS.md';
@@ -18,14 +33,49 @@ const normalizeAgentsMdContent = (content: string) => {
return content.length > 0 && !content.endsWith('\n') ? `${content}\n` : content;
};
const saveBehaviorSetting = async (globalBehaviorPrompt: string, fallbackError: string) => {
type ResponseStyleValue = ResponseStylePreset | 'custom';
type BehaviorSettingsState = {
prompt: string;
responseStyleEnabled: boolean;
responseStylePreset: ResponseStyleValue;
responseStyleCustomInstructions: string;
};
const DEFAULT_BEHAVIOR_SETTINGS: BehaviorSettingsState = {
prompt: '',
responseStyleEnabled: false,
responseStylePreset: 'concise',
responseStyleCustomInstructions: '',
};
const getResponseStylePreview = (preset: ResponseStyleValue, customInstructions: string) => {
return preset === 'custom' ? customInstructions : getResponseStylePresetInstructions(preset);
};
const sanitizeResponseStylePreset = (value: unknown): ResponseStyleValue => {
if (value === 'custom') return 'custom';
return isResponseStylePreset(value) ? value : 'concise';
};
const RESPONSE_STYLE_OPTION_LABEL_KEYS: Record<ResponseStylePreset, I18nKey> = {
concise: 'settings.behavior.page.responseStyle.option.concise',
detailed: 'settings.behavior.page.responseStyle.option.detailed',
mentor: 'settings.behavior.page.responseStyle.option.mentor',
pushback: 'settings.behavior.page.responseStyle.option.pushback',
noFiller: 'settings.behavior.page.responseStyle.option.noFiller',
matchEnergy: 'settings.behavior.page.responseStyle.option.matchEnergy',
warmPeer: 'settings.behavior.page.responseStyle.option.warmPeer',
};
const saveBehaviorSetting = async (settings: Partial<DesktopSettings>, fallbackError: string) => {
const response = await fetch('/api/config/settings', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ globalBehaviorPrompt }),
body: JSON.stringify(settings),
});
if (!response.ok) {
@@ -36,9 +86,17 @@ const saveBehaviorSetting = async (globalBehaviorPrompt: string, fallbackError:
export const BehaviorPage: React.FC = () => {
const { t } = useI18n();
const [prompt, setPrompt] = React.useState('');
const [responseStyleEnabled, setResponseStyleEnabled] = React.useState(DEFAULT_BEHAVIOR_SETTINGS.responseStyleEnabled);
const [responseStylePreset, setResponseStylePreset] = React.useState<ResponseStyleValue>(DEFAULT_BEHAVIOR_SETTINGS.responseStylePreset);
const [responseStyleCustomInstructions, setResponseStyleCustomInstructions] = React.useState(DEFAULT_BEHAVIOR_SETTINGS.responseStyleCustomInstructions);
const [isLoading, setIsLoading] = React.useState(true);
const [isSaving, setIsSaving] = React.useState(false);
const [initialPrompt, setInitialPrompt] = React.useState('');
const lastSavedResponseStyleRef = React.useRef<{
enabled: boolean;
preset: ResponseStyleValue;
custom: string;
} | null>(null);
React.useEffect(() => {
const abort = new AbortController();
@@ -58,23 +116,39 @@ export const BehaviorPage: React.FC = () => {
}),
]);
let settingsPrompt = '';
let nextSettings: BehaviorSettingsState = DEFAULT_BEHAVIOR_SETTINGS;
if (settingsRes.ok) {
const data = await settingsRes.json();
nextSettings = {
...nextSettings,
responseStyleEnabled: data.responseStyleEnabled === true,
responseStylePreset: sanitizeResponseStylePreset(data.responseStylePreset),
responseStyleCustomInstructions: typeof data.responseStyleCustomInstructions === 'string'
? data.responseStyleCustomInstructions
: '',
};
if (typeof data.globalBehaviorPrompt === 'string') {
settingsPrompt = data.globalBehaviorPrompt;
nextSettings = { ...nextSettings, prompt: data.globalBehaviorPrompt };
}
}
if (!settingsPrompt.trim() && agentsMdRes.ok) {
if (!nextSettings.prompt.trim() && agentsMdRes.ok) {
const agentsData = await agentsMdRes.json();
if (typeof agentsData.content === 'string') {
settingsPrompt = agentsData.content;
nextSettings = { ...nextSettings, prompt: agentsData.content };
}
}
setPrompt(settingsPrompt);
setInitialPrompt(settingsPrompt);
setPrompt(nextSettings.prompt);
setResponseStyleEnabled(nextSettings.responseStyleEnabled);
setResponseStylePreset(nextSettings.responseStylePreset);
setResponseStyleCustomInstructions(nextSettings.responseStyleCustomInstructions);
setInitialPrompt(nextSettings.prompt);
lastSavedResponseStyleRef.current = {
enabled: nextSettings.responseStyleEnabled,
preset: nextSettings.responseStylePreset,
custom: nextSettings.responseStyleCustomInstructions,
};
} catch (error) {
if ((error as Error).name !== 'AbortError') {
console.warn('Failed to load behavior settings:', error);
@@ -88,7 +162,43 @@ export const BehaviorPage: React.FC = () => {
return () => abort.abort();
}, []);
const isDirty = prompt !== initialPrompt;
React.useEffect(() => {
if (isLoading) return;
const last = lastSavedResponseStyleRef.current;
if (
last &&
last.enabled === responseStyleEnabled &&
last.preset === responseStylePreset &&
last.custom === responseStyleCustomInstructions
) {
return;
}
const next = {
enabled: responseStyleEnabled,
preset: responseStylePreset,
custom: responseStyleCustomInstructions,
};
const timer = setTimeout(async () => {
try {
await saveBehaviorSetting({
responseStyleEnabled: next.enabled,
responseStylePreset: next.preset,
responseStyleCustomInstructions: next.custom,
}, t('settings.behavior.page.toast.saveFailed'));
lastSavedResponseStyleRef.current = next;
} catch (error) {
const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed');
toast.error(message);
}
}, 400);
return () => clearTimeout(timer);
}, [responseStyleEnabled, responseStylePreset, responseStyleCustomInstructions, isLoading, t]);
const responseStylePreview = getResponseStylePreview(responseStylePreset, responseStyleCustomInstructions);
const isPromptDirty = prompt !== initialPrompt;
const handleSave = async () => {
setIsSaving(true);
@@ -107,7 +217,9 @@ export const BehaviorPage: React.FC = () => {
throw new Error(await readApiError(response, t('settings.behavior.page.toast.saveFailed')));
}
await saveBehaviorSetting(content, t('settings.behavior.page.toast.saveFailed'));
await saveBehaviorSetting({
globalBehaviorPrompt: content,
}, t('settings.behavior.page.toast.saveFailed'));
setPrompt(content);
setInitialPrompt(content);
@@ -154,7 +266,7 @@ export const BehaviorPage: React.FC = () => {
</div>
</div>
<section className="px-2 pb-2 pt-0">
<section className="px-2 pb-2 pt-0 space-y-3">
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
@@ -164,19 +276,83 @@ export const BehaviorPage: React.FC = () => {
outerClassName="min-h-[160px] max-h-[70vh]"
className="w-full font-mono typography-meta bg-transparent"
/>
<Button
onClick={handleSave}
disabled={isSaving || !isPromptDirty || isLoading}
size="xs"
className="!font-normal"
>
{isSaving ? t('settings.common.actions.saving') : t('settings.common.actions.saveChanges')}
</Button>
</section>
</div>
<div className="px-2 py-1">
<Button
onClick={handleSave}
disabled={isSaving || !isDirty || isLoading}
size="xs"
className="!font-normal"
>
{isSaving ? t('settings.common.actions.saving') : t('settings.common.actions.saveChanges')}
</Button>
<div>
<div className="mb-1 px-1">
<div className="flex items-center gap-1.5">
<h3 className="typography-ui-header font-medium text-foreground">
{t('settings.behavior.page.section.responseStyle')}
</h3>
<Tooltip>
<TooltipTrigger asChild>
<RiInformationLine className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent sideOffset={8} className="max-w-xs">
{t('settings.behavior.page.responseStyle.tooltip')}
</TooltipContent>
</Tooltip>
</div>
</div>
<section className="px-2 pb-2 pt-0 space-y-3">
<label className="flex items-center gap-2 typography-ui-label text-foreground">
<Checkbox
checked={responseStyleEnabled}
onChange={setResponseStyleEnabled}
disabled={isLoading}
ariaLabel={t('settings.behavior.page.responseStyle.enableAria')}
/>
{t('settings.behavior.page.responseStyle.enable')}
</label>
<Select<ResponseStyleValue>
value={responseStylePreset}
onValueChange={(value) => setResponseStylePreset(value)}
disabled={isLoading || !responseStyleEnabled}
>
<SelectTrigger className="w-full sm:w-56" size="lg">
<SelectValue>
{(value) => {
if (value === 'custom') return t('settings.behavior.page.responseStyle.option.custom');
if (isResponseStylePreset(value)) return t(RESPONSE_STYLE_OPTION_LABEL_KEYS[value]);
return null;
}}
</SelectValue>
</SelectTrigger>
<SelectContent>
{RESPONSE_STYLE_PRESETS.map((preset) => (
<SelectItem key={preset} value={preset}>
{t(RESPONSE_STYLE_OPTION_LABEL_KEYS[preset])}
</SelectItem>
))}
<SelectItem value="custom">
{t('settings.behavior.page.responseStyle.option.custom')}
</SelectItem>
</SelectContent>
</Select>
<Textarea
value={responseStylePreview}
onChange={(event) => setResponseStyleCustomInstructions(event.target.value)}
placeholder={t('settings.behavior.page.responseStyle.customPlaceholder')}
rows={5}
disabled={isLoading || !responseStyleEnabled || responseStylePreset !== 'custom'}
outerClassName="min-h-[120px]"
className="w-full font-mono typography-meta bg-transparent"
/>
</section>
</div>
</div>
</ScrollableOverlay>
);
+3
View File
@@ -161,6 +161,9 @@ export type DesktopSettings = {
// Global behavior prompt — synced to ~/.config/opencode/AGENTS.md
globalBehaviorPrompt?: string;
responseStyleEnabled?: boolean;
responseStylePreset?: 'concise' | 'detailed' | 'mentor' | 'pushback' | 'noFiller' | 'matchEnergy' | 'warmPeer' | 'custom';
responseStyleCustomInstructions?: string;
};
type TauriGlobal = {
@@ -343,6 +343,20 @@ export const settingsDict = {
'settings.behavior.page.warning.description': 'Changes made here update {path}. OpenCode also includes any project-level AGENTS.md rules when they exist.',
'settings.behavior.page.section.systemPrompt': 'Global AGENTS.md',
'settings.behavior.page.field.systemPromptPlaceholder': 'You are a helpful AI assistant...\n\nUse this space to define absolute rules for how the AI should behave across all sessions and providers.',
'settings.behavior.page.section.responseStyle': 'Response style',
'settings.behavior.page.responseStyle.tooltip': 'When enabled, these instructions guide how the assistant responds in each new conversation. They are sent with your first message and do not change your global AGENTS.md rules.',
'settings.behavior.page.responseStyle.enableAria': 'Enable response style instructions',
'settings.behavior.page.responseStyle.enable': 'Add response style instructions to new conversations',
'settings.behavior.page.responseStyle.preset': 'Preset',
'settings.behavior.page.responseStyle.option.concise': 'Concise',
'settings.behavior.page.responseStyle.option.detailed': 'Detailed',
'settings.behavior.page.responseStyle.option.mentor': 'Mentor',
'settings.behavior.page.responseStyle.option.pushback': 'Pushback',
'settings.behavior.page.responseStyle.option.noFiller': 'No filler',
'settings.behavior.page.responseStyle.option.matchEnergy': 'Match my energy',
'settings.behavior.page.responseStyle.option.warmPeer': 'Warm peer',
'settings.behavior.page.responseStyle.option.custom': 'Custom',
'settings.behavior.page.responseStyle.customPlaceholder': 'Write the instruction to add to the first message...',
'settings.behavior.page.toast.saved': 'Behavior saved successfully',
'settings.behavior.page.toast.saveFailed': 'Failed to save behavior',
'settings.commands.sidebar.title': 'Commands',
@@ -343,6 +343,20 @@ export const settingsDict = {
"settings.behavior.page.warning.description": "Los cambios realizados aquí actualizan {path}. OpenCode también incluye cualquier AGENTS.md a nivel de proyecto cuando existe.",
"settings.behavior.page.section.systemPrompt": "AGENTS.md global",
"settings.behavior.page.field.systemPromptPlaceholder": "Eres un asistente de IA útil...\n\nUsa este espacio para definir reglas absolutas sobre cómo la IA debe comportarse en todas las sesiones y proveedores.",
"settings.behavior.page.section.responseStyle": "Estilo de respuesta",
"settings.behavior.page.responseStyle.tooltip": "Cuando está habilitado, estas instrucciones guían cómo responde el asistente en cada conversación nueva. Se envían con tu primer mensaje y no cambian tus reglas globales de AGENTS.md.",
"settings.behavior.page.responseStyle.enableAria": "Habilitar instrucciones de estilo de respuesta",
"settings.behavior.page.responseStyle.enable": "Agregar instrucciones de estilo de respuesta a conversaciones nuevas",
"settings.behavior.page.responseStyle.preset": "Preset",
"settings.behavior.page.responseStyle.option.concise": "Conciso",
"settings.behavior.page.responseStyle.option.detailed": "Detallado",
"settings.behavior.page.responseStyle.option.mentor": "Mentor",
"settings.behavior.page.responseStyle.option.pushback": "Réplica",
"settings.behavior.page.responseStyle.option.noFiller": "Sin relleno",
"settings.behavior.page.responseStyle.option.matchEnergy": "Igualar mi tono",
"settings.behavior.page.responseStyle.option.warmPeer": "Colega cercano",
"settings.behavior.page.responseStyle.option.custom": "Personalizado",
"settings.behavior.page.responseStyle.customPlaceholder": "Escribe la instrucción que se agregará al primer mensaje...",
"settings.behavior.page.toast.saved": "Comportamiento guardado correctamente",
"settings.behavior.page.toast.saveFailed": "Error al guardar el comportamiento",
"settings.commands.sidebar.title": "Comandos",
@@ -343,6 +343,20 @@ export const settingsDict = {
'settings.behavior.page.warning.description': '여기에서 변경한 내용은 {path}를 업데이트합니다. 프로젝트 수준의 AGENTS.md가 있으면 OpenCode도 함께 포함합니다.',
'settings.behavior.page.section.systemPrompt': '전역 AGENTS.md',
'settings.behavior.page.field.systemPromptPlaceholder': '당신은 유용한 AI 어시스턴트입니다...\n\n모든 세션과 공급자에서 AI가 어떻게 행동해야 하는지에 대한 절대적인 규칙을 정의할 수 있습니다.',
'settings.behavior.page.section.responseStyle': '응답 스타일',
'settings.behavior.page.responseStyle.tooltip': '활성화하면 이 지침이 새 대화에서 어시스턴트의 응답 방식을 안내합니다. 첫 메시지와 함께 전송되며 전역 AGENTS.md 규칙은 변경하지 않습니다.',
'settings.behavior.page.responseStyle.enableAria': '응답 스타일 지침 활성화',
'settings.behavior.page.responseStyle.enable': '새 대화에 응답 스타일 지침 추가',
'settings.behavior.page.responseStyle.preset': '프리셋',
'settings.behavior.page.responseStyle.option.concise': '간결하게',
'settings.behavior.page.responseStyle.option.detailed': '자세하게',
'settings.behavior.page.responseStyle.option.mentor': '멘토',
'settings.behavior.page.responseStyle.option.pushback': '반박',
'settings.behavior.page.responseStyle.option.noFiller': '군더더기 없이',
'settings.behavior.page.responseStyle.option.matchEnergy': '내 톤에 맞추기',
'settings.behavior.page.responseStyle.option.warmPeer': '친근한 동료',
'settings.behavior.page.responseStyle.option.custom': '사용자 정의',
'settings.behavior.page.responseStyle.customPlaceholder': '첫 메시지에 추가할 지침을 작성하세요...',
'settings.behavior.page.toast.saved': '동작이 성공적으로 저장되었습니다',
'settings.behavior.page.toast.saveFailed': '동작 저장에 실패했습니다',
'settings.commands.sidebar.title': '명령어',
@@ -343,6 +343,20 @@ export const settingsDict = {
"settings.behavior.page.warning.description": "As alterações feitas aqui atualizam {path}. O OpenCode também inclui qualquer AGENTS.md a nível de projeto quando existir.",
"settings.behavior.page.section.systemPrompt": "AGENTS.md global",
"settings.behavior.page.field.systemPromptPlaceholder": "Você é um assistente de IA útil...\n\nUse este espaço para definir regras absolutas sobre como a IA deve se comportar em todas as sessões e provedores.",
"settings.behavior.page.section.responseStyle": "Estilo de resposta",
"settings.behavior.page.responseStyle.tooltip": "Quando habilitado, estas instruções orientam como o assistente responde em cada nova conversa. Elas são enviadas com sua primeira mensagem e não alteram suas regras globais de AGENTS.md.",
"settings.behavior.page.responseStyle.enableAria": "Habilitar instruções de estilo de resposta",
"settings.behavior.page.responseStyle.enable": "Adicionar instruções de estilo de resposta a novas conversas",
"settings.behavior.page.responseStyle.preset": "Predefinição",
"settings.behavior.page.responseStyle.option.concise": "Conciso",
"settings.behavior.page.responseStyle.option.detailed": "Detalhado",
"settings.behavior.page.responseStyle.option.mentor": "Mentor",
"settings.behavior.page.responseStyle.option.pushback": "Contraponto",
"settings.behavior.page.responseStyle.option.noFiller": "Sem enrolação",
"settings.behavior.page.responseStyle.option.matchEnergy": "Acompanhar meu tom",
"settings.behavior.page.responseStyle.option.warmPeer": "Colega próximo",
"settings.behavior.page.responseStyle.option.custom": "Personalizado",
"settings.behavior.page.responseStyle.customPlaceholder": "Escreva a instrução para adicionar à primeira mensagem...",
"settings.behavior.page.toast.saved": "Comportamento salvo com sucesso",
"settings.behavior.page.toast.saveFailed": "Falha ao salvar o comportamento",
"settings.commands.sidebar.title": "Comandos",
@@ -343,6 +343,20 @@ export const settingsDict = {
"settings.behavior.page.warning.description": "Зміни тут оновлюють {path}. OpenCode також додає правила з AGENTS.md на рівні проєкту, якщо вони існують.",
"settings.behavior.page.section.systemPrompt": "Глобальний AGENTS.md",
"settings.behavior.page.field.systemPromptPlaceholder": "Ви — корисний AI-асистент...\n\nВикористовуйте цей простір для визначення абсолютних правил поведінки AI для всіх сесій та провайдерів.",
"settings.behavior.page.section.responseStyle": "Стиль відповіді",
"settings.behavior.page.responseStyle.tooltip": "Якщо увімкнено, ці інструкції задають стиль відповідей асистента в кожній новій розмові. Вони надсилаються разом із вашим першим повідомленням і не змінюють глобальні правила AGENTS.md.",
"settings.behavior.page.responseStyle.enableAria": "Увімкнути інструкції стилю відповіді",
"settings.behavior.page.responseStyle.enable": "Додавати інструкції стилю відповіді до нових розмов",
"settings.behavior.page.responseStyle.preset": "Пресет",
"settings.behavior.page.responseStyle.option.concise": "Коротко",
"settings.behavior.page.responseStyle.option.detailed": "Детально",
"settings.behavior.page.responseStyle.option.mentor": "Ментор",
"settings.behavior.page.responseStyle.option.pushback": "Опір",
"settings.behavior.page.responseStyle.option.noFiller": "Без води",
"settings.behavior.page.responseStyle.option.matchEnergy": "У тон мені",
"settings.behavior.page.responseStyle.option.warmPeer": "Близький колега",
"settings.behavior.page.responseStyle.option.custom": "Власний",
"settings.behavior.page.responseStyle.customPlaceholder": "Напишіть інструкцію, яку треба додати до першого повідомлення...",
"settings.behavior.page.toast.saved": "Поведінку збережено",
"settings.behavior.page.toast.saveFailed": "Не вдалося зберегти поведінку",
"settings.commands.sidebar.title": "Команди",
@@ -343,6 +343,20 @@ export const settingsDict = {
'settings.behavior.page.warning.description': '此处所做的更改会更新 {path}。当项目级 AGENTS.md 存在时,OpenCode 也会包含它。',
'settings.behavior.page.section.systemPrompt': '全局 AGENTS.md',
'settings.behavior.page.field.systemPromptPlaceholder': '你是一个有帮助的 AI 助手...\n\n使用此空间定义 AI 在所有会话和提供商中的绝对行为规则。',
'settings.behavior.page.section.responseStyle': '回复风格',
'settings.behavior.page.responseStyle.tooltip': '启用后,这些说明会指导助手在每个新对话中的回复方式。它们会随你的第一条消息一起发送,不会更改全局 AGENTS.md 规则。',
'settings.behavior.page.responseStyle.enableAria': '启用回复风格说明',
'settings.behavior.page.responseStyle.enable': '向新对话添加回复风格说明',
'settings.behavior.page.responseStyle.preset': '预设',
'settings.behavior.page.responseStyle.option.concise': '简洁',
'settings.behavior.page.responseStyle.option.detailed': '详细',
'settings.behavior.page.responseStyle.option.mentor': '导师',
'settings.behavior.page.responseStyle.option.pushback': '反驳',
'settings.behavior.page.responseStyle.option.noFiller': '无废话',
'settings.behavior.page.responseStyle.option.matchEnergy': '匹配我的语气',
'settings.behavior.page.responseStyle.option.warmPeer': '亲近的同事',
'settings.behavior.page.responseStyle.option.custom': '自定义',
'settings.behavior.page.responseStyle.customPlaceholder': '编写要添加到第一条消息的说明...',
'settings.behavior.page.toast.saved': '行为保存成功',
'settings.behavior.page.toast.saveFailed': '行为保存失败',
'settings.commands.sidebar.title': '命令',
+23
View File
@@ -942,6 +942,29 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
result.reportUsage = candidate.reportUsage;
}
if (typeof candidate.globalBehaviorPrompt === 'string') {
result.globalBehaviorPrompt = candidate.globalBehaviorPrompt;
}
if (typeof candidate.responseStyleEnabled === 'boolean') {
result.responseStyleEnabled = candidate.responseStyleEnabled;
}
if (
typeof candidate.responseStylePreset === 'string'
&& (candidate.responseStylePreset === 'concise'
|| candidate.responseStylePreset === 'detailed'
|| candidate.responseStylePreset === 'mentor'
|| candidate.responseStylePreset === 'pushback'
|| candidate.responseStylePreset === 'noFiller'
|| candidate.responseStylePreset === 'matchEnergy'
|| candidate.responseStylePreset === 'warmPeer'
|| candidate.responseStylePreset === 'custom')
) {
result.responseStylePreset = candidate.responseStylePreset;
}
if (typeof candidate.responseStyleCustomInstructions === 'string') {
result.responseStyleCustomInstructions = candidate.responseStyleCustomInstructions;
}
return result;
};
+62
View File
@@ -0,0 +1,62 @@
export const RESPONSE_STYLE_PRESETS = ['concise', 'detailed', 'mentor', 'pushback', 'noFiller', 'matchEnergy', 'warmPeer'] as const;
export type ResponseStylePreset = typeof RESPONSE_STYLE_PRESETS[number];
export const isResponseStylePreset = (value: unknown): value is ResponseStylePreset => (
typeof value === 'string' && RESPONSE_STYLE_PRESETS.includes(value as ResponseStylePreset)
);
export const getResponseStylePresetInstructions = (preset: ResponseStylePreset): string => {
switch (preset) {
case 'concise':
return "Keep replies short. Answer first, no preamble or recap of the question. Write like you're texting a colleague who already has the context — plain sentences, not headings or bullets. Reach for a list only when the content is genuinely a list; never use one to look organised.";
case 'detailed':
return "Take the space you need to actually explain things. Walk through what's going on, why it matters, and where the real tradeoffs are. Prefer flowing prose over bullet points and headings — structure the answer with paragraphs and let the reasoning carry it. Lists are fine when something really is enumerable, but don't fragment a normal explanation into bullets.";
case 'mentor':
return "Talk like a patient senior engineer pairing with someone less experienced. Explain the underlying idea before the answer, think out loud about how you'd approach it, and drop in a small concrete example when it actually helps. Keep it conversational — no lecture format, no checklists, no numbered steps unless the task literally is a sequence.";
case 'pushback':
return "Don't agree automatically. If something I say sounds off — a wrong assumption, a flawed approach, a request that won't actually do what I think it will — push back first. Explain what you disagree with and why, and only proceed once I've responded. Disagreement is welcome; sycophancy is not. Don't soften it with 'you might want to consider' — just say it.";
case 'noFiller':
return "Cut the filler. No 'Great question', no 'Certainly', no 'I'll help you with that', no restating what I just asked. No closing summary of what you did when the diff or output already shows it. No trailing 'let me know if you need anything else'. Open with the actual content and stop when you're done.";
case 'matchEnergy':
return "Mirror the size and register of my message. A one-line question gets a one-line answer. A casual aside gets a casual reply, not a structured breakdown. If I write three words, don't respond with three paragraphs. Match the tone too — informal stays informal, technical stays technical. Don't inflate small asks into full essays.";
case 'warmPeer':
return "Talk like a colleague, not an assistant. First person is fine and encouraged — 'I'd do this', 'I don't love that approach', 'that was sloppy of me'. Have actual opinions and share them. Push back when you disagree. Admit when you screwed up without grovelling. Skip the corporate helpfulness and performative politeness — just be a person.";
}
};
export const buildResponseStyleInstruction = ({
enabled,
preset,
customInstructions,
}: {
enabled?: boolean;
preset?: unknown;
customInstructions?: unknown;
}): string | null => {
if (!enabled) return null;
if (preset === 'custom') {
const custom = typeof customInstructions === 'string' ? customInstructions.trim() : '';
return custom || null;
}
if (!isResponseStylePreset(preset)) return null;
return getResponseStylePresetInstructions(preset);
};
export const fetchResponseStyleInstruction = async (): Promise<string | null> => {
const response = await fetch('/api/config/settings', {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (!response.ok) return null;
const settings = await response.json().catch(() => null) as {
responseStyleEnabled?: unknown;
responseStylePreset?: unknown;
responseStyleCustomInstructions?: unknown;
} | null;
if (!settings) return null;
return buildResponseStyleInstruction({
enabled: settings.responseStyleEnabled === true,
preset: settings.responseStylePreset,
customInstructions: settings.responseStyleCustomInstructions,
});
};
@@ -563,6 +563,24 @@ export const createSettingsHelpers = (dependencies) => {
}
}
if (typeof candidate.responseStyleEnabled === 'boolean') {
result.responseStyleEnabled = candidate.responseStyleEnabled;
}
if (
typeof candidate.responseStylePreset === 'string' &&
['concise', 'detailed', 'mentor', 'pushback', 'noFiller', 'matchEnergy', 'warmPeer', 'custom'].includes(candidate.responseStylePreset)
) {
result.responseStylePreset = candidate.responseStylePreset;
}
if (typeof candidate.responseStyleCustomInstructions === 'string') {
const value = candidate.responseStyleCustomInstructions;
if (value.length <= 50_000) {
result.responseStyleCustomInstructions = value;
}
}
return result;
};