|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Claude Code Settings</title> |
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/editor/editor.main.css"> |
|
<style> |
|
:root { |
|
--bg-primary: #1e1e1e; |
|
--bg-secondary: #252526; |
|
--bg-tertiary: #2d2d2d; |
|
--text-primary: #cccccc; |
|
--text-secondary: #969696; |
|
--accent: #3b82f6; |
|
--accent-hover: #2563eb; |
|
--border: #3c3c3c; |
|
--success: #10b981; |
|
--error: #ef4444; |
|
} |
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; } |
|
|
|
body { |
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
|
background: var(--bg-primary); |
|
color: var(--text-primary); |
|
height: 100vh; |
|
overflow: hidden; |
|
} |
|
|
|
.container { |
|
display: flex; |
|
flex-direction: column; |
|
height: 100vh; |
|
} |
|
|
|
/* Header */ |
|
header { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
padding: 12px 20px; |
|
background: var(--bg-secondary); |
|
border-bottom: 1px solid var(--border); |
|
} |
|
|
|
.logo { |
|
display: flex; |
|
align-items: center; |
|
gap: 10px; |
|
font-weight: 600; |
|
font-size: 14px; |
|
} |
|
|
|
.logo svg { width: 24px; height: 24px; } |
|
|
|
/* Tabs */ |
|
.tabs { |
|
display: flex; |
|
gap: 2px; |
|
background: var(--bg-primary); |
|
} |
|
|
|
.tab { |
|
padding: 10px 20px; |
|
background: var(--bg-tertiary); |
|
border: none; |
|
color: var(--text-secondary); |
|
cursor: pointer; |
|
font-size: 13px; |
|
transition: all 0.15s; |
|
} |
|
|
|
.tab:hover { color: var(--text-primary); } |
|
.tab.active { |
|
background: var(--bg-secondary); |
|
color: var(--text-primary); |
|
border-top: 2px solid var(--accent); |
|
} |
|
|
|
/* Mode toggle */ |
|
.mode-toggle { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
font-size: 12px; |
|
color: var(--text-secondary); |
|
} |
|
|
|
.toggle-btn { |
|
padding: 6px 12px; |
|
background: var(--bg-tertiary); |
|
border: 1px solid var(--border); |
|
border-radius: 4px; |
|
color: var(--text-secondary); |
|
cursor: pointer; |
|
font-size: 12px; |
|
transition: all 0.15s; |
|
} |
|
|
|
.toggle-btn:hover { border-color: var(--accent); color: var(--text-primary); } |
|
.toggle-btn.active { background: var(--accent); border-color: var(--accent); color: white; } |
|
|
|
/* Main content */ |
|
main { |
|
flex: 1; |
|
display: flex; |
|
overflow: hidden; |
|
} |
|
|
|
.panel { |
|
flex: 1; |
|
display: none; |
|
overflow: hidden; |
|
} |
|
|
|
.panel.active { display: flex; flex-direction: column; } |
|
|
|
/* Settings form mode */ |
|
.settings-form { |
|
flex: 1; |
|
overflow-y: auto; |
|
padding: 20px; |
|
} |
|
|
|
.setting-group { |
|
margin-bottom: 24px; |
|
padding-bottom: 24px; |
|
border-bottom: 1px solid var(--border); |
|
} |
|
|
|
.setting-group:last-child { border-bottom: none; } |
|
|
|
.setting-group h3 { |
|
font-size: 11px; |
|
text-transform: uppercase; |
|
letter-spacing: 0.5px; |
|
color: var(--text-secondary); |
|
margin-bottom: 16px; |
|
} |
|
|
|
.setting-item { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: flex-start; |
|
margin-bottom: 16px; |
|
gap: 20px; |
|
} |
|
|
|
.setting-info { flex: 1; } |
|
|
|
.setting-label { |
|
font-size: 13px; |
|
color: var(--text-primary); |
|
margin-bottom: 4px; |
|
} |
|
|
|
.setting-description { |
|
font-size: 12px; |
|
color: var(--text-secondary); |
|
line-height: 1.4; |
|
} |
|
|
|
.setting-control { min-width: 200px; } |
|
|
|
/* Form controls */ |
|
input[type="text"], select { |
|
width: 100%; |
|
padding: 8px 12px; |
|
background: var(--bg-tertiary); |
|
border: 1px solid var(--border); |
|
border-radius: 4px; |
|
color: var(--text-primary); |
|
font-size: 13px; |
|
} |
|
|
|
input[type="text"]:focus, select:focus { |
|
outline: none; |
|
border-color: var(--accent); |
|
} |
|
|
|
/* Toggle switch */ |
|
.toggle-switch { |
|
position: relative; |
|
width: 40px; |
|
height: 20px; |
|
} |
|
|
|
.toggle-switch input { display: none; } |
|
|
|
.toggle-slider { |
|
position: absolute; |
|
inset: 0; |
|
background: var(--bg-tertiary); |
|
border: 1px solid var(--border); |
|
border-radius: 20px; |
|
cursor: pointer; |
|
transition: all 0.2s; |
|
} |
|
|
|
.toggle-slider::before { |
|
content: ''; |
|
position: absolute; |
|
width: 14px; |
|
height: 14px; |
|
left: 2px; |
|
top: 2px; |
|
background: var(--text-secondary); |
|
border-radius: 50%; |
|
transition: all 0.2s; |
|
} |
|
|
|
.toggle-switch input:checked + .toggle-slider { |
|
background: var(--accent); |
|
border-color: var(--accent); |
|
} |
|
|
|
.toggle-switch input:checked + .toggle-slider::before { |
|
transform: translateX(20px); |
|
background: white; |
|
} |
|
|
|
/* Editor container */ |
|
.editor-container { |
|
flex: 1; |
|
position: relative; |
|
} |
|
|
|
#settings-editor, #claude-md-editor { |
|
width: 100%; |
|
height: 100%; |
|
} |
|
|
|
/* Markdown preview */ |
|
.md-split { |
|
display: flex; |
|
flex: 1; |
|
} |
|
|
|
.md-split > div { flex: 1; } |
|
|
|
.md-preview { |
|
padding: 20px; |
|
overflow-y: auto; |
|
background: var(--bg-secondary); |
|
border-left: 1px solid var(--border); |
|
} |
|
|
|
.md-preview h1, .md-preview h2, .md-preview h3 { margin: 16px 0 8px; } |
|
.md-preview h1 { font-size: 24px; border-bottom: 1px solid var(--border); padding-bottom: 8px; } |
|
.md-preview h2 { font-size: 18px; } |
|
.md-preview h3 { font-size: 14px; } |
|
.md-preview p { margin: 8px 0; line-height: 1.6; } |
|
.md-preview code { |
|
background: var(--bg-tertiary); |
|
padding: 2px 6px; |
|
border-radius: 3px; |
|
font-size: 12px; |
|
} |
|
.md-preview pre { |
|
background: var(--bg-tertiary); |
|
padding: 12px; |
|
border-radius: 4px; |
|
overflow-x: auto; |
|
margin: 12px 0; |
|
} |
|
.md-preview pre code { background: none; padding: 0; } |
|
.md-preview ul, .md-preview ol { margin: 8px 0; padding-left: 24px; } |
|
.md-preview li { margin: 4px 0; } |
|
.md-preview blockquote { |
|
border-left: 3px solid var(--accent); |
|
padding-left: 12px; |
|
margin: 12px 0; |
|
color: var(--text-secondary); |
|
} |
|
|
|
/* Status bar */ |
|
.status-bar { |
|
display: flex; |
|
justify-content: space-between; |
|
padding: 4px 12px; |
|
background: var(--accent); |
|
font-size: 12px; |
|
color: white; |
|
} |
|
|
|
.status-bar.error { background: var(--error); } |
|
.status-bar.success { background: var(--success); } |
|
|
|
/* Hooks editor */ |
|
.hooks-editor { |
|
background: var(--bg-tertiary); |
|
border: 1px solid var(--border); |
|
border-radius: 4px; |
|
padding: 12px; |
|
} |
|
|
|
.hook-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 8px; |
|
margin-bottom: 8px; |
|
padding: 8px; |
|
background: var(--bg-secondary); |
|
border-radius: 4px; |
|
} |
|
|
|
.hook-item input { |
|
flex: 1; |
|
} |
|
|
|
.hook-type { |
|
font-size: 11px; |
|
padding: 2px 8px; |
|
background: var(--accent); |
|
border-radius: 3px; |
|
color: white; |
|
} |
|
|
|
.btn-small { |
|
padding: 4px 8px; |
|
background: transparent; |
|
border: 1px solid var(--border); |
|
border-radius: 3px; |
|
color: var(--text-secondary); |
|
cursor: pointer; |
|
font-size: 11px; |
|
} |
|
|
|
.btn-small:hover { border-color: var(--accent); color: var(--text-primary); } |
|
.btn-small.danger:hover { border-color: var(--error); color: var(--error); } |
|
|
|
/* Permissions editor */ |
|
.perm-section { |
|
margin-bottom: 16px; |
|
} |
|
|
|
.perm-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 8px; |
|
} |
|
|
|
.perm-badge { |
|
font-size: 11px; |
|
padding: 3px 10px; |
|
border-radius: 12px; |
|
font-weight: 500; |
|
} |
|
|
|
.perm-badge.allow { background: var(--success); color: white; } |
|
.perm-badge.ask { background: #f59e0b; color: white; } |
|
.perm-badge.deny { background: var(--error); color: white; } |
|
|
|
.perm-list { |
|
background: var(--bg-tertiary); |
|
border: 1px solid var(--border); |
|
border-radius: 4px; |
|
max-height: 200px; |
|
overflow-y: auto; |
|
} |
|
|
|
.perm-item { |
|
display: flex; |
|
align-items: center; |
|
justify-content: space-between; |
|
padding: 6px 10px; |
|
border-bottom: 1px solid var(--border); |
|
font-family: monospace; |
|
font-size: 12px; |
|
} |
|
|
|
.perm-item:last-child { border-bottom: none; } |
|
.perm-item:hover { background: var(--bg-secondary); } |
|
|
|
.perm-input { |
|
display: flex; |
|
gap: 8px; |
|
margin-top: 8px; |
|
} |
|
|
|
.perm-input input { |
|
flex: 1; |
|
font-family: monospace; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<header> |
|
<div class="logo"> |
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/> |
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1Z"/> |
|
</svg> |
|
Claude Code Settings |
|
</div> |
|
<div class="tabs"> |
|
<button class="tab active" data-panel="settings">settings.json</button> |
|
<button class="tab" data-panel="claude-md">CLAUDE.md</button> |
|
</div> |
|
<div class="mode-toggle" id="settings-mode-toggle"> |
|
<button class="toggle-btn active" data-mode="form">UI</button> |
|
<button class="toggle-btn" data-mode="json">JSON</button> |
|
</div> |
|
</header> |
|
|
|
<main> |
|
<!-- Settings Panel --> |
|
<div class="panel active" id="settings-panel"> |
|
<!-- Form Mode --> |
|
<div class="settings-form" id="settings-form"> |
|
<div class="setting-group"> |
|
<h3>General</h3> |
|
<div class="setting-item"> |
|
<div class="setting-info"> |
|
<div class="setting-label">Model</div> |
|
<div class="setting-description">Default model for Claude Code sessions</div> |
|
</div> |
|
<div class="setting-control"> |
|
<select id="setting-model"> |
|
<option value="">Not set (use default)</option> |
|
<option value="claude-sonnet-4-20250514">Sonnet 4</option> |
|
<option value="claude-opus-4-20250115">Opus 4</option> |
|
<option value="claude-opus-4-5-20251101">Opus 4.5</option> |
|
</select> |
|
</div> |
|
</div> |
|
<div class="setting-item"> |
|
<div class="setting-info"> |
|
<div class="setting-label">Always Enable Thinking</div> |
|
<div class="setting-description">Enable extended thinking mode by default</div> |
|
</div> |
|
<div class="setting-control"> |
|
<label class="toggle-switch"> |
|
<input type="checkbox" id="setting-thinking"> |
|
<span class="toggle-slider"></span> |
|
</label> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="setting-group"> |
|
<h3>Status Line</h3> |
|
<div class="setting-item"> |
|
<div class="setting-info"> |
|
<div class="setting-label">Command</div> |
|
<div class="setting-description">Shell command to generate status line content</div> |
|
</div> |
|
<div class="setting-control"> |
|
<input type="text" id="setting-statusline-cmd" placeholder="e.g., ~/.claude/scripts/statusline.sh"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="setting-group"> |
|
<h3>Hooks</h3> |
|
<div id="hooks-container"></div> |
|
</div> |
|
|
|
<div class="setting-group"> |
|
<h3>Permissions</h3> |
|
<div class="setting-description" style="margin-bottom: 12px;"> |
|
Control which commands Claude can run. Format: <code>Bash(command:*)</code> |
|
</div> |
|
<div id="permissions-container"></div> |
|
</div> |
|
</div> |
|
|
|
<!-- JSON Mode --> |
|
<div class="editor-container" id="settings-editor-container" style="display: none;"> |
|
<div id="settings-editor"></div> |
|
</div> |
|
</div> |
|
|
|
<!-- CLAUDE.md Panel --> |
|
<div class="panel" id="claude-md-panel"> |
|
<div class="md-split"> |
|
<div class="editor-container"> |
|
<div id="claude-md-editor"></div> |
|
</div> |
|
<div class="md-preview" id="md-preview"></div> |
|
</div> |
|
</div> |
|
</main> |
|
|
|
<div class="status-bar" id="status-bar"> |
|
<span id="status-message">Ready</span> |
|
<span id="status-file">~/.claude/settings.json</span> |
|
</div> |
|
</div> |
|
|
|
<script src="https://cdn.jsdelivr.net/npm/marked@9.1.6/marked.min.js"></script> |
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script> |
|
<script> |
|
let settingsEditor, claudeMdEditor; |
|
let currentSettings = {}; |
|
let isDirty = false; |
|
|
|
// Monaco setup |
|
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } }); |
|
|
|
require(['vs/editor/editor.main'], async function () { |
|
// Define VS Code dark theme |
|
monaco.editor.defineTheme('claude-dark', { |
|
base: 'vs-dark', |
|
inherit: true, |
|
rules: [], |
|
colors: { |
|
'editor.background': '#1e1e1e', |
|
'editor.foreground': '#cccccc', |
|
} |
|
}); |
|
|
|
// Settings JSON editor |
|
settingsEditor = monaco.editor.create(document.getElementById('settings-editor'), { |
|
language: 'json', |
|
theme: 'claude-dark', |
|
minimap: { enabled: false }, |
|
fontSize: 13, |
|
lineNumbers: 'on', |
|
scrollBeyondLastLine: false, |
|
automaticLayout: true, |
|
tabSize: 2, |
|
}); |
|
|
|
// CLAUDE.md editor |
|
claudeMdEditor = monaco.editor.create(document.getElementById('claude-md-editor'), { |
|
language: 'markdown', |
|
theme: 'claude-dark', |
|
minimap: { enabled: false }, |
|
fontSize: 13, |
|
lineNumbers: 'on', |
|
scrollBeyondLastLine: false, |
|
automaticLayout: true, |
|
wordWrap: 'on', |
|
}); |
|
|
|
// Load data |
|
await loadSettings(); |
|
await loadClaudeMd(); |
|
|
|
// Auto-save on change |
|
settingsEditor.onDidChangeModelContent(debounce(saveSettings, 1000)); |
|
claudeMdEditor.onDidChangeModelContent(debounce(saveClaudeMd, 1000)); |
|
claudeMdEditor.onDidChangeModelContent(updatePreview); |
|
}); |
|
|
|
// Tab switching |
|
document.querySelectorAll('.tab').forEach(tab => { |
|
tab.addEventListener('click', () => { |
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
|
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); |
|
tab.classList.add('active'); |
|
document.getElementById(`${tab.dataset.panel}-panel`).classList.add('active'); |
|
|
|
// Update status bar |
|
const file = tab.dataset.panel === 'settings' ? '~/.claude/settings.json' : '~/.claude/CLAUDE.md'; |
|
document.getElementById('status-file').textContent = file; |
|
|
|
// Show/hide mode toggle |
|
document.getElementById('settings-mode-toggle').style.display = |
|
tab.dataset.panel === 'settings' ? 'flex' : 'none'; |
|
}); |
|
}); |
|
|
|
// Mode toggle (form/json) |
|
document.querySelectorAll('.mode-toggle .toggle-btn').forEach(btn => { |
|
btn.addEventListener('click', () => { |
|
document.querySelectorAll('.mode-toggle .toggle-btn').forEach(b => b.classList.remove('active')); |
|
btn.classList.add('active'); |
|
|
|
const isForm = btn.dataset.mode === 'form'; |
|
document.getElementById('settings-form').style.display = isForm ? 'block' : 'none'; |
|
document.getElementById('settings-editor-container').style.display = isForm ? 'none' : 'block'; |
|
|
|
if (!isForm) { |
|
// Sync form to JSON |
|
settingsEditor.setValue(JSON.stringify(currentSettings, null, 2)); |
|
} else { |
|
// Sync JSON to form |
|
try { |
|
currentSettings = JSON.parse(settingsEditor.getValue()); |
|
populateForm(); |
|
} catch (e) {} |
|
} |
|
}); |
|
}); |
|
|
|
async function loadSettings() { |
|
try { |
|
const res = await fetch('/api/settings'); |
|
const data = await res.json(); |
|
currentSettings = data.parsed; |
|
settingsEditor.setValue(data.content); |
|
populateForm(); |
|
setStatus('Loaded settings.json', 'success'); |
|
} catch (e) { |
|
setStatus('Error loading settings: ' + e.message, 'error'); |
|
} |
|
} |
|
|
|
async function saveSettings() { |
|
try { |
|
const content = settingsEditor.getValue(); |
|
JSON.parse(content); // Validate |
|
await fetch('/api/settings', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ content }) |
|
}); |
|
setStatus('Saved settings.json', 'success'); |
|
} catch (e) { |
|
setStatus('Error: ' + e.message, 'error'); |
|
} |
|
} |
|
|
|
async function loadClaudeMd() { |
|
try { |
|
const res = await fetch('/api/claude-md'); |
|
const data = await res.json(); |
|
claudeMdEditor.setValue(data.content); |
|
updatePreview(); |
|
setStatus('Loaded CLAUDE.md', 'success'); |
|
} catch (e) { |
|
setStatus('Error loading CLAUDE.md: ' + e.message, 'error'); |
|
} |
|
} |
|
|
|
async function saveClaudeMd() { |
|
try { |
|
const content = claudeMdEditor.getValue(); |
|
await fetch('/api/claude-md', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ content }) |
|
}); |
|
setStatus('Saved CLAUDE.md', 'success'); |
|
} catch (e) { |
|
setStatus('Error: ' + e.message, 'error'); |
|
} |
|
} |
|
|
|
function populateForm() { |
|
// Model |
|
document.getElementById('setting-model').value = currentSettings.model || ''; |
|
|
|
// Thinking |
|
document.getElementById('setting-thinking').checked = currentSettings.alwaysThinkingEnabled || false; |
|
|
|
// Status line |
|
document.getElementById('setting-statusline-cmd').value = currentSettings.statusLine?.command || ''; |
|
|
|
// Hooks |
|
renderHooks(); |
|
|
|
// Permissions |
|
renderPermissions(); |
|
} |
|
|
|
// Permission presets (inspired by bcherny's config) |
|
const PERMISSION_PRESETS = { |
|
'boris-safe': { |
|
name: "Boris's Safe Defaults", |
|
desc: "Based on 19k tool uses - safe git, npm, file ops", |
|
permissions: { |
|
allow: [ |
|
"Bash(git remote:*)", "Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", |
|
"Bash(git show:*)", "Bash(git rev-parse:*)", "Bash(git describe:*)", "Bash(git branch:*)", |
|
"Bash(git tag:*)", "Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)", |
|
"Bash(npm run build:*)", "Bash(npm test:*)", "Bash(npm run test:*)", "Bash(npm run dev:*)", |
|
"Bash(npm ci:*)", "Bash(npm install:*)", |
|
"Bash(curl -s http://localhost:*)", "Bash(curl http://localhost:*)", |
|
"Bash(cd:*)", "Bash(ls:*)", "Bash(pwd:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(find:*)", |
|
"Bash(echo:*)", "Bash(date:*)", "Bash(wc:*)", "Bash(head:*)", "Bash(tail:*)", "Bash(jq:*)", |
|
"Bash(sort:*)", "Bash(uniq:*)", "Bash(cut:*)", "Bash(tr:*)", "Bash(ps:*)", "Bash(lsof:*)", |
|
"Bash(mkdir:*)", "Bash(chmod +x:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(touch:*)" |
|
], |
|
ask: [ |
|
"Bash(rm -rf:*)", "Bash(bash -c:*)", "Bash(ssh:*)" |
|
], |
|
deny: [ |
|
"Bash(rm -rf /:*)", "Bash(rm -rf ~:*)", "Bash(sudo rm:*)", |
|
"Bash(sudo shutdown:*)", "Bash(sudo reboot:*)", |
|
"Bash(chmod 777:*)", "Bash(chmod -R 777:*)" |
|
] |
|
} |
|
}, |
|
'minimal': { |
|
name: "Minimal Safe", |
|
desc: "Read-only commands only", |
|
permissions: { |
|
allow: [ |
|
"Bash(git status:*)", "Bash(git diff:*)", "Bash(git log:*)", |
|
"Bash(ls:*)", "Bash(pwd:*)", "Bash(cat:*)", "Bash(head:*)", "Bash(tail:*)" |
|
], |
|
ask: [ |
|
"Bash(git add:*)", "Bash(git commit:*)", "Bash(git push:*)", |
|
"Bash(npm install:*)", "Bash(mkdir:*)", "Bash(cp:*)", "Bash(mv:*)" |
|
], |
|
deny: [ |
|
"Bash(rm -rf:*)", "Bash(sudo:*)", "Bash(chmod 777:*)" |
|
] |
|
} |
|
}, |
|
'yolo': { |
|
name: "YOLO Mode", |
|
desc: "Allow almost everything (dangerous!)", |
|
permissions: { |
|
allow: [ |
|
"Bash(git:*)", "Bash(npm:*)", "Bash(node:*)", "Bash(bun:*)", |
|
"Bash(ls:*)", "Bash(cd:*)", "Bash(cat:*)", "Bash(grep:*)", "Bash(find:*)", |
|
"Bash(mkdir:*)", "Bash(cp:*)", "Bash(mv:*)", "Bash(rm:*)", "Bash(touch:*)", |
|
"Bash(curl:*)", "Bash(wget:*)", "Bash(chmod:*)" |
|
], |
|
ask: ["Bash(sudo:*)"], |
|
deny: ["Bash(rm -rf /:*)", "Bash(rm -rf ~:*)"] |
|
} |
|
} |
|
}; |
|
|
|
function renderPermissions() { |
|
const container = document.getElementById('permissions-container'); |
|
const perms = currentSettings.permissions || { allow: [], ask: [], deny: [] }; |
|
|
|
const sections = [ |
|
{ key: 'allow', label: 'Allow', desc: 'Auto-approved commands' }, |
|
{ key: 'ask', label: 'Ask', desc: 'Requires confirmation' }, |
|
{ key: 'deny', label: 'Deny', desc: 'Always blocked' } |
|
]; |
|
|
|
// Preset selector |
|
const presetHtml = ` |
|
<div style="margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 4px;"> |
|
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> |
|
<span style="font-size: 12px; color: var(--text-secondary);">Load preset:</span> |
|
${Object.entries(PERMISSION_PRESETS).map(([key, preset]) => ` |
|
<button class="btn-small" onclick="loadPreset('${key}')" title="${preset.desc}"> |
|
${preset.name} |
|
</button> |
|
`).join('')} |
|
<button class="btn-small danger" onclick="clearPermissions()" title="Remove all rules"> |
|
Clear All |
|
</button> |
|
</div> |
|
</div> |
|
`; |
|
|
|
container.innerHTML = presetHtml + sections.map(({ key, label, desc }) => { |
|
const items = perms[key] || []; |
|
return ` |
|
<div class="perm-section"> |
|
<div class="perm-header"> |
|
<span> |
|
<span class="perm-badge ${key}">${label}</span> |
|
<span style="color: var(--text-secondary); font-size: 11px; margin-left: 8px;">${desc}</span> |
|
</span> |
|
<span style="color: var(--text-secondary); font-size: 11px;">${items.length} rules</span> |
|
</div> |
|
<div class="perm-list"> |
|
${items.length === 0 ? '<div class="perm-item" style="color: var(--text-secondary);">No rules</div>' : ''} |
|
${items.map((item, i) => ` |
|
<div class="perm-item"> |
|
<span>${item}</span> |
|
<button class="btn-small danger" onclick="removePerm('${key}', ${i})">×</button> |
|
</div> |
|
`).join('')} |
|
</div> |
|
<div class="perm-input"> |
|
<input type="text" id="perm-input-${key}" placeholder="Bash(command:*)"> |
|
<button class="btn-small" onclick="addPerm('${key}')">+ Add</button> |
|
</div> |
|
</div> |
|
`; |
|
}).join(''); |
|
} |
|
|
|
function addPerm(type) { |
|
const input = document.getElementById(`perm-input-${type}`); |
|
const value = input.value.trim(); |
|
if (!value) return; |
|
|
|
if (!currentSettings.permissions) currentSettings.permissions = { allow: [], ask: [], deny: [] }; |
|
if (!currentSettings.permissions[type]) currentSettings.permissions[type] = []; |
|
currentSettings.permissions[type].push(value); |
|
input.value = ''; |
|
renderPermissions(); |
|
syncFormToJson(); |
|
} |
|
|
|
function removePerm(type, index) { |
|
currentSettings.permissions[type].splice(index, 1); |
|
renderPermissions(); |
|
syncFormToJson(); |
|
} |
|
|
|
function loadPreset(presetKey) { |
|
const preset = PERMISSION_PRESETS[presetKey]; |
|
if (!preset) return; |
|
|
|
if (!confirm(`Load "${preset.name}"?\n\n${preset.desc}\n\nThis will replace your current permissions.`)) { |
|
return; |
|
} |
|
|
|
currentSettings.permissions = JSON.parse(JSON.stringify(preset.permissions)); |
|
renderPermissions(); |
|
syncFormToJson(); |
|
setStatus(`Loaded preset: ${preset.name}`, 'success'); |
|
} |
|
|
|
function clearPermissions() { |
|
if (!confirm('Clear all permission rules?')) return; |
|
currentSettings.permissions = { allow: [], ask: [], deny: [] }; |
|
renderPermissions(); |
|
syncFormToJson(); |
|
setStatus('Cleared all permissions', 'success'); |
|
} |
|
|
|
function renderHooks() { |
|
const container = document.getElementById('hooks-container'); |
|
const hookTypes = ['SessionStart', 'SessionEnd', 'PreToolUse', 'PostToolUse']; |
|
|
|
container.innerHTML = hookTypes.map(type => { |
|
const hooks = currentSettings.hooks?.[type]?.[0]?.hooks || []; |
|
return ` |
|
<div class="hooks-editor" style="margin-bottom: 12px;"> |
|
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> |
|
<strong style="font-size: 12px;">${type}</strong> |
|
<button class="btn-small" onclick="addHook('${type}')">+ Add</button> |
|
</div> |
|
${hooks.map((hook, i) => ` |
|
<div class="hook-item"> |
|
<span class="hook-type">${hook.type}</span> |
|
<input type="text" value="${hook.command || ''}" |
|
onchange="updateHook('${type}', ${i}, this.value)"> |
|
<button class="btn-small danger" onclick="removeHook('${type}', ${i})">×</button> |
|
</div> |
|
`).join('')} |
|
${hooks.length === 0 ? '<div style="color: var(--text-secondary); font-size: 12px;">No hooks configured</div>' : ''} |
|
</div> |
|
`; |
|
}).join(''); |
|
} |
|
|
|
function updateHook(type, index, value) { |
|
if (!currentSettings.hooks) currentSettings.hooks = {}; |
|
if (!currentSettings.hooks[type]) currentSettings.hooks[type] = [{ hooks: [] }]; |
|
currentSettings.hooks[type][0].hooks[index].command = value; |
|
syncFormToJson(); |
|
} |
|
|
|
function addHook(type) { |
|
if (!currentSettings.hooks) currentSettings.hooks = {}; |
|
if (!currentSettings.hooks[type]) currentSettings.hooks[type] = [{ hooks: [] }]; |
|
currentSettings.hooks[type][0].hooks.push({ type: 'command', command: '' }); |
|
renderHooks(); |
|
syncFormToJson(); |
|
} |
|
|
|
function removeHook(type, index) { |
|
currentSettings.hooks[type][0].hooks.splice(index, 1); |
|
renderHooks(); |
|
syncFormToJson(); |
|
} |
|
|
|
// Form change handlers |
|
document.getElementById('setting-model').addEventListener('change', (e) => { |
|
if (e.target.value) currentSettings.model = e.target.value; |
|
else delete currentSettings.model; |
|
syncFormToJson(); |
|
}); |
|
|
|
document.getElementById('setting-thinking').addEventListener('change', (e) => { |
|
currentSettings.alwaysThinkingEnabled = e.target.checked; |
|
syncFormToJson(); |
|
}); |
|
|
|
document.getElementById('setting-statusline-cmd').addEventListener('change', (e) => { |
|
if (e.target.value) { |
|
currentSettings.statusLine = { type: 'command', command: e.target.value, padding: 0 }; |
|
} else { |
|
delete currentSettings.statusLine; |
|
} |
|
syncFormToJson(); |
|
}); |
|
|
|
function syncFormToJson() { |
|
settingsEditor.setValue(JSON.stringify(currentSettings, null, 2)); |
|
} |
|
|
|
function updatePreview() { |
|
const md = claudeMdEditor.getValue(); |
|
document.getElementById('md-preview').innerHTML = marked.parse(md); |
|
} |
|
|
|
function setStatus(message, type = '') { |
|
const bar = document.getElementById('status-bar'); |
|
const msg = document.getElementById('status-message'); |
|
bar.className = 'status-bar ' + type; |
|
msg.textContent = message; |
|
if (type) setTimeout(() => { bar.className = 'status-bar'; msg.textContent = 'Ready'; }, 3000); |
|
} |
|
|
|
function debounce(fn, ms) { |
|
let timeout; |
|
return (...args) => { |
|
clearTimeout(timeout); |
|
timeout = setTimeout(() => fn(...args), ms); |
|
}; |
|
} |
|
</script> |
|
</body> |
|
</html> |