From 5f29455bbbc3c2dc8c46bf9837938bf1f87ae710 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 02:08:58 +0100 Subject: [PATCH] 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 --- src/web/public/app.js | 71 ++++++++++++++++++++++++++++++++++++++- src/web/public/index.html | 28 +++++++++++++++ src/web/public/styles.css | 21 ++++++++++++ src/web/server.ts | 32 +++++++++++++++++- 4 files changed, 150 insertions(+), 2 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 7592d311..4952d29d 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -477,7 +477,7 @@ class ClaudemanApp { `; } - // Add the "+" button + // Add the "+" button at the end of session tabs html += `
+ @@ -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'); diff --git a/src/web/public/index.html b/src/web/public/index.html index 8b34b9a5..9b2066a7 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -33,6 +33,7 @@ Connecting
0 tokens
+ @@ -272,6 +273,33 @@ + + + diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 80d62719..20c2d4f5 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -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; diff --git a/src/web/server.ts b/src/web/server.ts index d3f76efc..58f068b0 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -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 {