// SettingsPage — sidebar groups + scrollable form (matches existing SettingsSidebar structure)
const SETTINGS_GROUPS = [
{
label: '基本設定',
sections: [
{ id: 'general', label: 'General', desc: 'タイムゾーン・言語' },
{ id: 'provider', label: 'Provider', desc: 'LLM API キー・デフォルトモデル' },
{ id: 'workers', label: 'Workers', desc: '並列数・タイムアウト・リトライ' },
{ id: 'workspace', label: 'Workspace', desc: '作業ディレクトリ・クリーンアップ' },
{ id: 'progress', label: 'Progress', desc: '進捗報告の頻度' },
],
},
{
label: 'セキュリティ・アクセス制御',
sections: [
{ id: 'repos', label: 'Repos', desc: 'Gitea 接続・許可リポジトリ' },
{ id: 'access-control', label: 'Access Control', desc: 'ロール・権限マトリクス' },
{ id: 'search-filter', label: 'Search Filter', desc: 'NGワード・ドメイン制限' },
],
},
{
label: 'ツール設定',
sections: [
{ id: 'tools', label: 'Tools', desc: '利用可能なツールの有効化' },
{ id: 'browser-settings', label: 'Browser', desc: 'noVNC・セッション' },
],
},
{
label: 'エージェント制御',
sections: [
{ id: 'ask-subtasks', label: 'Ask / Subtasks', desc: 'ASK・サブタスクの挙動' },
{ id: 'context', label: 'Context', desc: 'コンテキスト長・注入ルール' },
{ id: 'memory-safety', label: 'Memory / Safety', desc: 'メモリ制限・安全装置' },
],
},
];
const PIECES = ['auto', 'chat', 'research', 'general', 'x-ai-digest', 'brainstorming', 'data-process'];
function SettingsSidebar({ section, onSelect, piece, onSelectPiece }) {
const itemStyle = (active) => ({
display: 'block', width: '100%', textAlign: 'left',
padding: '6px 10px', borderRadius: 8, border: 'none', cursor: 'pointer',
fontSize: 12, fontFamily: 'inherit', marginBottom: 1,
background: active ? '#eff6ff' : 'transparent',
color: active ? '#1d4ed8' : '#475569',
fontWeight: active ? 700 : 500,
});
return (
{SETTINGS_GROUPS.map(g => (
{g.label}
{g.sections.map(s => (
))}
))}
Pieces
{PIECES.map(p => (
))}
);
}
function SaveBar({ onDiscard }) {
// 3 states: idle / saving / saved / error — demonstrated via toggle
const [state, setState] = React.useState('idle');
const [dirty, setDirty] = React.useState(false);
// mark dirty when any descendant input changes
const onInput = React.useCallback(() => setDirty(true), []);
React.useEffect(() => {
const h = () => setDirty(true);
document.addEventListener('input', h);
return () => document.removeEventListener('input', h);
}, []);
const save = async () => {
setState('saving');
// mock latency; in 1/5 odds surface an error to showcase failure UI
await new Promise(r => setTimeout(r, 700));
if (Math.random() < 0.2) {
setState('error');
return;
}
setState('saved'); setDirty(false);
setTimeout(() => setState('idle'), 1500);
};
const discard = () => { setDirty(false); setState('idle'); onDiscard?.(); };
return (
{state === 'saved' && (
保存しました
)}
{state === 'error' && (
⚠ 保存に失敗
)}
);
}
// Simple mock form surface — shows that the detail pane follows the exact same "card with form" rhythm as Schedules/Users.
function SettingsForm({ section, piece }) {
const meta = (() => {
for (const g of SETTINGS_GROUPS) for (const s of g.sections) if (s.id === section) return s;
return null;
})();
const title = piece ? `Piece: ${piece}` : (meta?.label || section);
const desc = piece ? `${piece} ピースの定義・ムーブメント・ツール設定` : (meta?.desc || '');
// Sample fields per-section (placeholder — the real forms are in gitea-agent-orchestrator/ui)
const fields = piece ? [
{ label: 'Description', kind: 'text', value: piece === 'auto' ? '入力内容から最適なピースを自動選択' : `${piece} 用の定義` },
{ label: 'Max movements', kind: 'number', value: 25 },
{ label: 'Initial movement', kind: 'select', value: 'execute', options: ['execute', 'plan', 'research'] },
] : ({
provider: [
{ label: 'Default provider', kind: 'select', value: 'anthropic', options: ['anthropic', 'openai', 'google', 'bedrock'] },
{ label: 'Model', kind: 'select', value: 'claude-sonnet-4.5', options: ['claude-sonnet-4.5', 'claude-opus-4', 'gpt-4.1'] },
{ label: 'API key', kind: 'password', value: 'sk-ant-•••••••••••••••••••••••', env: true },
{ label: 'Max tokens', kind: 'number', value: 8192 },
{ label: 'Temperature', kind: 'number', value: 0.7, step: 0.1 },
],
workers: [
{ label: 'Parallel workers', kind: 'number', value: 6 },
{ label: 'Per-task timeout (sec)', kind: 'number', value: 900 },
{ label: 'Max retries', kind: 'number', value: 3 },
{ label: 'Retry backoff (sec)', kind: 'number', value: 60 },
],
general: [
{ label: 'System timezone', kind: 'select', value: 'Asia/Tokyo', options: ['Asia/Tokyo', 'UTC', 'America/Los_Angeles'] },
{ label: 'Language', kind: 'select', value: 'ja', options: ['ja', 'en'] },
{ label: 'Allow anonymous task creation', kind: 'toggle', value: false },
],
repos: [
{ label: 'Gitea URL', kind: 'text', value: 'https://gitea.internal' },
{ label: 'Access token', kind: 'password', value: 'gitea_•••••••••••', env: true },
{ label: 'Allowed repos', kind: 'text', value: 'daichi/*, corp/*', help: 'カンマ区切り・グロブ可' },
],
'access-control': [
{ label: 'Admin ロール allow', kind: 'text', value: '*' },
{ label: 'Operator ロール allow', kind: 'text', value: 'tasks.*, schedules.*' },
{ label: 'Viewer ロール allow', kind: 'text', value: 'tasks.read, schedules.read' },
],
tools: [
{ label: 'Read / Write / Edit', kind: 'toggle', value: true },
{ label: 'Bash', kind: 'toggle', value: true },
{ label: 'Browser (noVNC)', kind: 'toggle', value: true },
{ label: 'WebSearch', kind: 'toggle', value: false },
],
'browser-settings': [
{ label: 'noVNC endpoint', kind: 'text', value: 'https://novnc.internal' },
{ label: 'Session timeout (min)', kind: 'number', value: 30 },
{ label: 'Allow CAPTCHA fallback to human', kind: 'toggle', value: true },
],
'ask-subtasks': [
{ label: 'Max ASK depth', kind: 'number', value: 3 },
{ label: 'Auto-resume after ASK timeout (min)', kind: 'number', value: 60 },
{ label: 'Allow parallel subtasks', kind: 'toggle', value: true },
],
context: [
{ label: 'Context window (tokens)', kind: 'number', value: 200000 },
{ label: 'Auto-compact threshold', kind: 'number', value: 80, help: '% で指定' },
],
'memory-safety': [
{ label: 'Memory limit per worker (MB)', kind: 'number', value: 2048 },
{ label: 'Kill on OOM', kind: 'toggle', value: true },
{ label: 'Safe-mode Bash commands only', kind: 'toggle', value: false },
],
progress: [
{ label: 'Progress update interval (sec)', kind: 'number', value: 15 },
{ label: 'Show subtask progress', kind: 'toggle', value: true },
],
workspace: [
{ label: 'Workspace root', kind: 'text', value: '/var/lib/agent/workspace' },
{ label: 'Clean after task completion', kind: 'toggle', value: false },
],
'search-filter': [
{ label: 'Blocked domains', kind: 'text', value: 'example-bad.com, *.malicious.example' },
{ label: 'NG words', kind: 'text', value: '', help: 'カンマ区切り' },
],
}[section] || []);
return (
{/* Header */}
{piece ? 'PIECE' : 'SETTINGS'}
{title}
{desc &&
{desc}
}
{/* Body */}
{fields.map((f, i) => (
{f.label}
{f.env && ENV}
} help={f.help}>
{f.kind === 'toggle' ? (
) : f.kind === 'select' ? (
{}}>
{f.options.map(o => )}
) : f.kind === 'password' ? (
) : f.kind === 'number' ? (
{}} />
) : (
{}} />
)}
))}
{fields.length === 0 && (
}
title="このセクションは未実装です"
hint="`gitea-agent-orchestrator/ui` 側で設定項目を追加するとここに表示されます。"
/>
)}
);
}
function SettingsPage({ section, setSection, piece, setPiece }) {
return (
{ setSection(s); setPiece(null); }}
piece={piece} onSelectPiece={(p) => setPiece(p)}
/>
);
}
window.SettingsPage = SettingsPage;