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 <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
d35ff8a2db
commit
bcbf34b1d1
@@ -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 (
|
||||
<ScrollableOverlay outerClassName="h-full" className="w-full">
|
||||
<div className="mx-auto w-full max-w-3xl p-3 sm:p-6 sm:pt-8 space-y-6">
|
||||
<div className="space-y-1">
|
||||
<h2 className="typography-ui-header font-semibold text-foreground">
|
||||
{t('settings.behavior.page.title')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<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.systemPrompt')}
|
||||
</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">
|
||||
<div className="space-y-1">
|
||||
<p className="font-medium text-foreground">
|
||||
{t('settings.behavior.page.warning.title')}
|
||||
</p>
|
||||
<p>
|
||||
{t('settings.behavior.page.warning.description', { path: AGENTS_MD_PATH })}
|
||||
</p>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="px-2 pb-2 pt-0">
|
||||
<Textarea
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
placeholder={t('settings.behavior.page.field.systemPromptPlaceholder')}
|
||||
rows={12}
|
||||
disabled={isLoading}
|
||||
outerClassName="min-h-[160px] max-h-[70vh]"
|
||||
className="w-full font-mono typography-meta bg-transparent"
|
||||
/>
|
||||
</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>
|
||||
</ScrollableOverlay>
|
||||
);
|
||||
};
|
||||
@@ -30,11 +30,13 @@ import {
|
||||
RiRestartLine,
|
||||
RiServerLine,
|
||||
RiSlashCommands2,
|
||||
RiBrainLine,
|
||||
} from '@remixicon/react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
import { AgentsSidebar } from '@/components/sections/agents/AgentsSidebar';
|
||||
import { AgentsPage } from '@/components/sections/agents/AgentsPage';
|
||||
import { BehaviorPage } from '@/components/sections/behavior/BehaviorPage';
|
||||
import { CommandsSidebar } from '@/components/sections/commands/CommandsSidebar';
|
||||
import { CommandsPage } from '@/components/sections/commands/CommandsPage';
|
||||
import { McpSidebar } from '@/components/sections/mcp/McpSidebar';
|
||||
@@ -93,6 +95,7 @@ const pageOrder: SettingsPageSlug[] = [
|
||||
'projects',
|
||||
'remote-instances',
|
||||
'agents',
|
||||
'behavior',
|
||||
'commands',
|
||||
'mcp',
|
||||
'providers',
|
||||
@@ -140,6 +143,8 @@ export function getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<
|
||||
return RiCloudLine;
|
||||
case 'agents':
|
||||
return RiAiAgentLine;
|
||||
case 'behavior':
|
||||
return RiBrainLine;
|
||||
case 'commands':
|
||||
return RiSlashCommands2;
|
||||
case 'mcp':
|
||||
@@ -395,6 +400,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return t('settings.page.usage.title');
|
||||
case 'agents':
|
||||
return t('settings.page.agents.title');
|
||||
case 'behavior':
|
||||
return t('settings.page.behavior.title');
|
||||
case 'commands':
|
||||
return t('settings.page.commands.title');
|
||||
case 'mcp':
|
||||
@@ -478,6 +485,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
|
||||
return <RemoteInstancesPage />;
|
||||
case 'agents':
|
||||
return <AgentsPage />;
|
||||
case 'behavior':
|
||||
return <BehaviorPage />;
|
||||
case 'commands':
|
||||
return <CommandsPage />;
|
||||
case 'mcp':
|
||||
|
||||
@@ -158,6 +158,9 @@ export type DesktopSettings = {
|
||||
skillCatalogs?: SkillCatalogConfig[];
|
||||
// Opt-in to send anonymous usage reports for update checks (default: true)
|
||||
reportUsage?: boolean;
|
||||
|
||||
// Global behavior prompt — synced to ~/.config/opencode/AGENTS.md
|
||||
globalBehaviorPrompt?: string;
|
||||
};
|
||||
|
||||
type TauriGlobal = {
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
'settings.agents.page.permissions.addCustomRule': 'Add Custom Rule',
|
||||
'settings.agents.page.permissions.permissionPlaceholder': 'Permission...',
|
||||
'settings.agents.page.permissions.patternPlaceholder': 'Pattern (e.g. *)',
|
||||
'settings.page.behavior.title': 'Behavior',
|
||||
'settings.behavior.page.title': 'Behavior',
|
||||
'settings.behavior.page.warning.title': 'Global rules are combined with project rules',
|
||||
'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.toast.saved': 'Behavior saved successfully',
|
||||
'settings.behavior.page.toast.saveFailed': 'Failed to save behavior',
|
||||
'settings.commands.sidebar.title': 'Commands',
|
||||
'settings.commands.sidebar.total': 'Total {count}',
|
||||
'settings.commands.sidebar.empty.title': 'No commands configured',
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
"settings.agents.page.permissions.addCustomRule": "Añadir regla personalizada",
|
||||
"settings.agents.page.permissions.permissionPlaceholder": "Permiso...",
|
||||
"settings.agents.page.permissions.patternPlaceholder": "Patrón (ej. *)",
|
||||
"settings.page.behavior.title": "Comportamiento",
|
||||
"settings.behavior.page.title": "Comportamiento",
|
||||
"settings.behavior.page.warning.title": "Las reglas globales se combinan con las reglas del proyecto",
|
||||
"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.toast.saved": "Comportamiento guardado correctamente",
|
||||
"settings.behavior.page.toast.saveFailed": "Error al guardar el comportamiento",
|
||||
"settings.commands.sidebar.title": "Comandos",
|
||||
"settings.commands.sidebar.total": "Total {count}",
|
||||
"settings.commands.sidebar.empty.title": "No hay comandos configurados",
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
'settings.agents.page.permissions.addCustomRule': '사용자 정의 규칙 추가',
|
||||
'settings.agents.page.permissions.permissionPlaceholder': '권한...',
|
||||
'settings.agents.page.permissions.patternPlaceholder': '패턴(예: *)',
|
||||
'settings.page.behavior.title': '동작',
|
||||
'settings.behavior.page.title': '동작',
|
||||
'settings.behavior.page.warning.title': '전역 규칙은 프로젝트 규칙과 함께 사용됩니다',
|
||||
'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.toast.saved': '동작이 성공적으로 저장되었습니다',
|
||||
'settings.behavior.page.toast.saveFailed': '동작 저장에 실패했습니다',
|
||||
'settings.commands.sidebar.title': '명령어',
|
||||
'settings.commands.sidebar.total': '총 {count}개',
|
||||
'settings.commands.sidebar.empty.title': '설정된 명령어가 없습니다',
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
"settings.agents.page.permissions.addCustomRule": "Adicionar regra personalizada",
|
||||
"settings.agents.page.permissions.permissionPlaceholder": "Permissão...",
|
||||
"settings.agents.page.permissions.patternPlaceholder": "Padrão (ex.: *)",
|
||||
"settings.page.behavior.title": "Comportamento",
|
||||
"settings.behavior.page.title": "Comportamento",
|
||||
"settings.behavior.page.warning.title": "Regras globais são combinadas com regras do projeto",
|
||||
"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.toast.saved": "Comportamento salvo com sucesso",
|
||||
"settings.behavior.page.toast.saveFailed": "Falha ao salvar o comportamento",
|
||||
"settings.commands.sidebar.title": "Comandos",
|
||||
"settings.commands.sidebar.total": "Total {count}",
|
||||
"settings.commands.sidebar.empty.title": "Não há comandos configurados",
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
"settings.agents.page.permissions.addCustomRule": "Додати спеціальне правило",
|
||||
"settings.agents.page.permissions.permissionPlaceholder": "Дозвіл...",
|
||||
"settings.agents.page.permissions.patternPlaceholder": "Шаблон (наприклад, *)",
|
||||
"settings.page.behavior.title": "Поведінка",
|
||||
"settings.behavior.page.title": "Поведінка",
|
||||
"settings.behavior.page.warning.title": "Глобальні правила поєднуються з правилами проєкту",
|
||||
"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.toast.saved": "Поведінку збережено",
|
||||
"settings.behavior.page.toast.saveFailed": "Не вдалося зберегти поведінку",
|
||||
"settings.commands.sidebar.title": "Команди",
|
||||
"settings.commands.sidebar.total": "Усього {count}",
|
||||
"settings.commands.sidebar.empty.title": "Команди не налаштовано",
|
||||
|
||||
@@ -337,6 +337,14 @@ export const settingsDict = {
|
||||
'settings.agents.page.permissions.addCustomRule': '添加自定义规则',
|
||||
'settings.agents.page.permissions.permissionPlaceholder': '权限...',
|
||||
'settings.agents.page.permissions.patternPlaceholder': '模式(例如 *)',
|
||||
'settings.page.behavior.title': '行为',
|
||||
'settings.behavior.page.title': '行为',
|
||||
'settings.behavior.page.warning.title': '全局规则会与项目规则合并使用',
|
||||
'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.toast.saved': '行为保存成功',
|
||||
'settings.behavior.page.toast.saveFailed': '行为保存失败',
|
||||
'settings.commands.sidebar.title': '命令',
|
||||
'settings.commands.sidebar.total': '总计 {count}',
|
||||
'settings.commands.sidebar.empty.title': '未配置命令',
|
||||
|
||||
@@ -7,6 +7,7 @@ export type SettingsPageSlug =
|
||||
| 'providers'
|
||||
| 'usage'
|
||||
| 'agents'
|
||||
| 'behavior'
|
||||
| 'commands'
|
||||
| 'mcp'
|
||||
| 'skills.installed'
|
||||
@@ -103,6 +104,13 @@ export const SETTINGS_PAGE_METADATA: readonly SettingsPageMeta[] = [
|
||||
kind: 'split',
|
||||
keywords: ['agent', 'agents', 'prompts', 'tools', 'permissions'],
|
||||
},
|
||||
{
|
||||
slug: 'behavior',
|
||||
title: 'Behavior',
|
||||
group: 'opencode',
|
||||
kind: 'single',
|
||||
keywords: ['behavior', 'agents.md', 'system prompt', 'global rules', 'instructions', 'override'],
|
||||
},
|
||||
{
|
||||
slug: 'commands',
|
||||
title: 'Commands',
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
createAgent,
|
||||
@@ -55,6 +57,9 @@ type ConfigRuntimeDeps = {
|
||||
clientReloadDelayMs: number;
|
||||
};
|
||||
|
||||
const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
|
||||
const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024;
|
||||
|
||||
const resolveWorkingDirectory = (ctx: BridgeContext | undefined, directory?: string): string | undefined => (
|
||||
(typeof directory === 'string' && directory.trim())
|
||||
? directory.trim()
|
||||
@@ -144,6 +149,30 @@ export async function handleConfigBridgeMessage(
|
||||
return { id, type, success: true, data: updated };
|
||||
}
|
||||
|
||||
case 'api:behavior/agents-md:get': {
|
||||
try {
|
||||
const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
|
||||
return { id, type, success: true, data: { content, exists: true } };
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
||||
return { id, type, success: true, data: { content: '', exists: false } };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:behavior/agents-md:save': {
|
||||
const request = (payload || {}) as { content?: unknown };
|
||||
const content = typeof request.content === 'string' ? request.content : '';
|
||||
if (content.length > MAX_BEHAVIOR_PROMPT_SIZE) {
|
||||
return { id, type, success: false, error: `Content exceeds maximum size of ${MAX_BEHAVIOR_PROMPT_SIZE} bytes` };
|
||||
}
|
||||
await fs.promises.mkdir(path.dirname(AGENTS_MD_PATH), { recursive: true });
|
||||
await fs.promises.writeFile(AGENTS_MD_PATH, content, 'utf8');
|
||||
await ctx?.manager?.restart();
|
||||
return { id, type, success: true, data: { success: true } };
|
||||
}
|
||||
|
||||
case 'api:magic-prompts:get': {
|
||||
return { id, type, success: true, data: deps.readMagicPromptOverrides() };
|
||||
}
|
||||
|
||||
@@ -843,6 +843,18 @@ const handleLocalApiRequest = async (url: URL, init?: RequestInit) => {
|
||||
return new Response(JSON.stringify(updated), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
|
||||
if (normalizedPathname === '/api/behavior/agents-md') {
|
||||
if (method === 'GET') {
|
||||
const data = await sendBridgeMessage('api:behavior/agents-md:get');
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
if (method === 'PUT') {
|
||||
const body = init?.body ? JSON.parse(init.body as string) : {};
|
||||
const data = await sendBridgeMessage('api:behavior/agents-md:save', body);
|
||||
return new Response(JSON.stringify(data), { status: 200, headers: { 'Content-Type': 'application/json' } });
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/magic-prompts') {
|
||||
if (method === 'GET') {
|
||||
const data = await sendBridgeMessage('api:magic-prompts:get');
|
||||
|
||||
@@ -454,7 +454,13 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
|
||||
const { express } = dependencies;
|
||||
|
||||
app.use((req, res, next) => {
|
||||
if (
|
||||
if (req.path.startsWith('/api/behavior')) {
|
||||
const contentLength = parseInt(req.headers['content-length'] || '0', 10);
|
||||
if (contentLength > 1024 * 1024) {
|
||||
return res.status(413).json({ error: 'Content exceeds maximum size of 1048576 bytes' });
|
||||
}
|
||||
express.json({ limit: '1mb' })(req, res, next);
|
||||
} else if (
|
||||
req.path.startsWith('/api/config/agents') ||
|
||||
req.path.startsWith('/api/config/commands') ||
|
||||
req.path.startsWith('/api/config/mcp') ||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { createProjectIdFromPath } from '../projects/project-id.js';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
const {
|
||||
@@ -295,4 +298,55 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
|
||||
return res.status(500).json({ error: error.message || 'Failed to update working directory' });
|
||||
}
|
||||
});
|
||||
|
||||
// Behavior / Global AGENTS.md endpoints
|
||||
const AGENTS_MD_PATH = path.join(os.homedir(), '.config', 'opencode', 'AGENTS.md');
|
||||
const MAX_BEHAVIOR_PROMPT_SIZE = 1024 * 1024; // 1 MB
|
||||
|
||||
app.get('/api/behavior/agents-md', async (_req, res) => {
|
||||
try {
|
||||
try {
|
||||
await fs.promises.access(AGENTS_MD_PATH);
|
||||
} catch {
|
||||
return res.json({ content: '', exists: false });
|
||||
}
|
||||
const content = await fs.promises.readFile(AGENTS_MD_PATH, 'utf8');
|
||||
return res.json({ content, exists: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to read AGENTS.md:', error);
|
||||
return res.status(500).json({ error: 'Failed to read AGENTS.md' });
|
||||
}
|
||||
});
|
||||
|
||||
app.put('/api/behavior/agents-md', async (req, res) => {
|
||||
try {
|
||||
const content = typeof req.body?.content === 'string' ? req.body.content : '';
|
||||
|
||||
if (content.length > MAX_BEHAVIOR_PROMPT_SIZE) {
|
||||
return res.status(413).json({ error: `Content exceeds maximum size of ${MAX_BEHAVIOR_PROMPT_SIZE} bytes` });
|
||||
}
|
||||
|
||||
// Ensure parent directory exists
|
||||
const parentDir = path.dirname(AGENTS_MD_PATH);
|
||||
try {
|
||||
await fs.promises.access(parentDir);
|
||||
} catch {
|
||||
await fs.promises.mkdir(parentDir, { recursive: true });
|
||||
}
|
||||
|
||||
await fs.promises.writeFile(AGENTS_MD_PATH, content, 'utf8');
|
||||
|
||||
// Refresh OpenCode so it picks up the new AGENTS.md without a full restart
|
||||
try {
|
||||
await refreshOpenCodeAfterConfigChange('global behavior (AGENTS.md) updated');
|
||||
} catch {
|
||||
// Non-fatal: file was written successfully
|
||||
}
|
||||
|
||||
return res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to write AGENTS.md:', error);
|
||||
return res.status(500).json({ error: error.message || 'Failed to write AGENTS.md' });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -555,6 +555,14 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
result.reportUsage = candidate.reportUsage;
|
||||
}
|
||||
|
||||
// Global behavior prompt — synced to ~/.config/opencode/AGENTS.md
|
||||
if (typeof candidate.globalBehaviorPrompt === 'string') {
|
||||
const value = candidate.globalBehaviorPrompt;
|
||||
if (value.length <= 1024 * 1024) {
|
||||
result.globalBehaviorPrompt = value;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user