From bcbf34b1d19d88936c3499464d9de58382d09022 Mon Sep 17 00:00:00 2001 From: Shyamalan Kannan <78594762+Yabuku-xD@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:39:55 -0700 Subject: [PATCH] feat: add Behavior settings page for global AGENTS.md (#1079) * feat: add Behavior settings page for global AGENTS.md - Add new 'Behavior' settings page to manage global system prompt - Sync global behavior prompt to ~/.config/opencode/AGENTS.md - Add GET/PUT /api/behavior/agents-md endpoints with 1MB size limit - Add AbortController guards and parallel fetches in BehaviorPage - Add i18n translations for en, es, ko, pt-BR, uk, zh-CN - Add globalBehaviorPrompt to DesktopSettings type and sanitizer - Add /api/behavior to express.json() body-parser whitelist - Ensure atomic save: AGENTS.md written before settings updated * fix: address Greptile review feedback - Rename misleading 'trimmed' variable to 'value' in settings-helpers.js - Add Content-Length guard for /api/behavior before 50mb JSON parser - Use express.json({ limit: '1mb' }) for behavior endpoints - Add actual translations for es, ko, pt-BR, uk, zh-CN locales * fix(vscode): support behavior settings endpoint * fix(behavior): wait for settings persistence * fix(behavior): end agents file with newline * fix(behavior): avoid duplicate textarea resize handles * fix(behavior): clarify global rules copy * fix(behavior): move rules note into tooltip --------- Co-authored-by: Bohdan Triapitsyn --- .../sections/behavior/BehaviorPage.tsx | 183 ++++++++++++++++++ .../ui/src/components/views/SettingsView.tsx | 9 + packages/ui/src/lib/desktop.ts | 3 + .../ui/src/lib/i18n/messages/en.settings.ts | 8 + .../ui/src/lib/i18n/messages/es.settings.ts | 8 + .../ui/src/lib/i18n/messages/ko.settings.ts | 8 + .../src/lib/i18n/messages/pt-BR.settings.ts | 8 + .../ui/src/lib/i18n/messages/uk.settings.ts | 8 + .../src/lib/i18n/messages/zh-CN.settings.ts | 8 + packages/ui/src/lib/settings/metadata.ts | 8 + packages/vscode/src/bridge-config-runtime.ts | 29 +++ packages/vscode/webview/main.tsx | 12 ++ .../web/server/lib/opencode/core-routes.js | 8 +- packages/web/server/lib/opencode/routes.js | 54 ++++++ .../server/lib/opencode/settings-helpers.js | 8 + 15 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/components/sections/behavior/BehaviorPage.tsx diff --git a/packages/ui/src/components/sections/behavior/BehaviorPage.tsx b/packages/ui/src/components/sections/behavior/BehaviorPage.tsx new file mode 100644 index 00000000..bdeae59d --- /dev/null +++ b/packages/ui/src/components/sections/behavior/BehaviorPage.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +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 { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; +import { RiInformationLine } from '@remixicon/react'; + +const AGENTS_MD_PATH = '~/.config/opencode/AGENTS.md'; + +const readApiError = async (response: Response, fallback: string) => { + const data = await response.json().catch(() => null) as { error?: unknown } | null; + return typeof data?.error === 'string' && data.error.trim() ? data.error : fallback; +}; + +const normalizeAgentsMdContent = (content: string) => { + return content.length > 0 && !content.endsWith('\n') ? `${content}\n` : content; +}; + +const saveBehaviorSetting = async (globalBehaviorPrompt: string, fallbackError: string) => { + const response = await fetch('/api/config/settings', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ globalBehaviorPrompt }), + }); + + if (!response.ok) { + throw new Error(await readApiError(response, fallbackError)); + } +}; + +export const BehaviorPage: React.FC = () => { + const { t } = useI18n(); + const [prompt, setPrompt] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(true); + const [isSaving, setIsSaving] = React.useState(false); + const [initialPrompt, setInitialPrompt] = React.useState(''); + + React.useEffect(() => { + const abort = new AbortController(); + + const load = async () => { + try { + const [settingsRes, agentsMdRes] = await Promise.all([ + fetch('/api/config/settings', { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: abort.signal, + }), + fetch('/api/behavior/agents-md', { + method: 'GET', + headers: { Accept: 'application/json' }, + signal: abort.signal, + }), + ]); + + let settingsPrompt = ''; + if (settingsRes.ok) { + const data = await settingsRes.json(); + if (typeof data.globalBehaviorPrompt === 'string') { + settingsPrompt = data.globalBehaviorPrompt; + } + } + + if (!settingsPrompt.trim() && agentsMdRes.ok) { + const agentsData = await agentsMdRes.json(); + if (typeof agentsData.content === 'string') { + settingsPrompt = agentsData.content; + } + } + + setPrompt(settingsPrompt); + setInitialPrompt(settingsPrompt); + } catch (error) { + if ((error as Error).name !== 'AbortError') { + console.warn('Failed to load behavior settings:', error); + } + } finally { + setIsLoading(false); + } + }; + + void load(); + return () => abort.abort(); + }, []); + + const isDirty = prompt !== initialPrompt; + + const handleSave = async () => { + setIsSaving(true); + try { + const content = normalizeAgentsMdContent(prompt); + const response = await fetch('/api/behavior/agents-md', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ content }), + }); + + if (!response.ok) { + throw new Error(await readApiError(response, t('settings.behavior.page.toast.saveFailed'))); + } + + await saveBehaviorSetting(content, t('settings.behavior.page.toast.saveFailed')); + + setPrompt(content); + setInitialPrompt(content); + toast.success(t('settings.behavior.page.toast.saved')); + } catch (error) { + console.error('Failed to save behavior:', error); + const message = error instanceof Error ? error.message : t('settings.behavior.page.toast.saveFailed'); + toast.error(message); + } finally { + setIsSaving(false); + } + }; + + return ( + +
+
+

+ {t('settings.behavior.page.title')} +

+
+ +
+
+
+

+ {t('settings.behavior.page.section.systemPrompt')} +

+ + + + + +
+

+ {t('settings.behavior.page.warning.title')} +

+

+ {t('settings.behavior.page.warning.description', { path: AGENTS_MD_PATH })} +

+
+
+
+
+
+ +
+