// 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;