feat: add settings gear icon with app settings modal

- Add gear icon button in header-right for app settings
- Create App Settings modal with fields for:
  - Default CLAUDE.md template path
  - Default working directory
- Settings persist to ~/.claudeman/settings.json
- Add GET/PUT /api/settings endpoints
- Keep "+" tab for creating new sessions (useful UX)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 02:08:58 +01:00
co-authored by Claude Opus 4.5
parent 0876c2d4e9
commit 5f29455bbb
4 changed files with 150 additions and 2 deletions
+70 -1
View File
@@ -477,7 +477,7 @@ class ClaudemanApp {
`;
}
// Add the "+" button
// Add the "+" button at the end of session tabs
html += `
<div class="session-tab new-tab" onclick="app.createNewSession()" title="New Session (Ctrl+N)">
<span>+</span>
@@ -1298,6 +1298,74 @@ class ClaudemanApp {
});
}
// ========== App Settings Modal ==========
openAppSettings() {
// Load current settings
const settings = this.loadAppSettingsFromStorage();
document.getElementById('appSettingsClaudeMdPath').value = settings.defaultClaudeMdPath || '';
document.getElementById('appSettingsDefaultDir').value = settings.defaultWorkingDir || '';
document.getElementById('appSettingsModal').classList.add('active');
}
closeAppSettings() {
document.getElementById('appSettingsModal').classList.remove('active');
}
async saveAppSettings() {
const settings = {
defaultClaudeMdPath: document.getElementById('appSettingsClaudeMdPath').value.trim(),
defaultWorkingDir: document.getElementById('appSettingsDefaultDir').value.trim(),
};
// Save to localStorage
localStorage.setItem('claudeman-app-settings', JSON.stringify(settings));
// Also save to server
try {
await fetch('/api/settings', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
this.showToast('Settings saved', 'success');
} catch (err) {
// Server save failed but localStorage succeeded
this.showToast('Settings saved locally', 'warning');
}
this.closeAppSettings();
}
loadAppSettingsFromStorage() {
try {
const saved = localStorage.getItem('claudeman-app-settings');
if (saved) {
return JSON.parse(saved);
}
} catch (err) {
console.error('Failed to load app settings:', err);
}
return {};
}
async loadAppSettingsFromServer() {
try {
const res = await fetch('/api/settings');
if (res.ok) {
const settings = await res.json();
// Merge with localStorage (server takes precedence)
const localSettings = this.loadAppSettingsFromStorage();
const merged = { ...localSettings, ...settings };
localStorage.setItem('claudeman-app-settings', JSON.stringify(merged));
return merged;
}
} catch (err) {
console.error('Failed to load settings from server:', err);
}
return this.loadAppSettingsFromStorage();
}
// ========== Help Modal ==========
showHelp() {
@@ -1310,6 +1378,7 @@ class ClaudemanApp {
closeAllPanels() {
this.closeSessionOptions();
this.closeAppSettings();
document.getElementById('respawnPanel').classList.remove('open');
document.getElementById('newSessionPanel').classList.remove('open');
document.getElementById('taskPanel').classList.remove('open');
+28
View File
@@ -33,6 +33,7 @@
<span class="status-text">Connecting</span>
</div>
<div class="header-tokens" id="headerTokens" title="Total tokens across all sessions">0 tokens</div>
<button class="btn-icon-header" onclick="app.openAppSettings()" title="App Settings">&#x2699;</button>
</div>
</header>
@@ -272,6 +273,33 @@
</div>
</div>
<!-- App Settings Modal -->
<div class="modal" id="appSettingsModal">
<div class="modal-backdrop" onclick="app.closeAppSettings()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>App Settings</h3>
<button class="modal-close" onclick="app.closeAppSettings()">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<label>Default CLAUDE.md Template Path</label>
<input type="text" id="appSettingsClaudeMdPath" placeholder="/path/to/CLAUDE.md template">
<span class="form-hint">Used when creating new cases. Leave empty for built-in template.</span>
</div>
<div class="form-row">
<label>Default Working Directory</label>
<input type="text" id="appSettingsDefaultDir" placeholder="~/projects">
<span class="form-hint">Default directory for new sessions.</span>
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeAppSettings()">Cancel</button>
<button class="btn-toolbar btn-primary" onclick="app.saveAppSettings()">Save</button>
</div>
</div>
</div>
<!-- Help Button -->
<button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button>
</div>
+21
View File
@@ -187,6 +187,27 @@ body {
font-family: 'SF Mono', Monaco, monospace;
}
.btn-icon-header {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
color: var(--text-dim);
font-size: 1rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.btn-icon-header:hover {
background: var(--bg-hover);
color: var(--text);
transform: rotate(45deg);
}
/* Session tab gear icon */
.session-tab .tab-gear {
opacity: 0;
+31 -1
View File
@@ -2,7 +2,7 @@ import Fastify, { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
import fastifyStatic from '@fastify/static';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'node:fs';
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { EventEmitter } from 'node:events';
import { Session, ClaudeMessage, type BackgroundTask } from '../session.js';
@@ -650,6 +650,36 @@ export class WebServer extends EventEmitter {
return { success: false, error: (err as Error).message };
}
});
// ============ App Settings Endpoints ============
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
this.app.get('/api/settings', async () => {
try {
if (existsSync(settingsPath)) {
const content = readFileSync(settingsPath, 'utf-8');
return JSON.parse(content);
}
} catch (err) {
console.error('Failed to read settings:', err);
}
return {};
});
this.app.put('/api/settings', async (req) => {
const settings = req.body as { defaultClaudeMdPath?: string; defaultWorkingDir?: string };
try {
const dir = dirname(settingsPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
return { success: true };
} catch (err) {
return { error: (err as Error).message };
}
});
}
private setupSessionListeners(session: Session): void {