feat: UI improvements - buttons, monitor panel, system stats

- Redesign Run Claude button with purple gradient
- Redesign Run Shell button with dark green gradient
- Add + button to create new cases from toolbar
- Add Create Case modal for quick case creation
- Add Kill All modal with two options (tabs only vs full kill)
- Add CPU/Memory progress bars with color gradients
- Make monitor panel detachable as floating draggable window
- Monitor panel resizable when detached (min 350x250)
- Default CLAUDE.md template now uses /home/arkon/default/CLAUDE.md
- Streamline CLAUDE.md documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-19 10:36:30 +01:00
co-authored by Claude Opus 4.5
parent 6a5beb3afa
commit 23cafd2556
5 changed files with 499 additions and 272 deletions
+190 -21
View File
@@ -1470,6 +1470,92 @@ class ClaudemanApp {
this.toggleMonitorPanel();
}
// ========== Monitor Panel Detach & Drag ==========
toggleMonitorDetach() {
const panel = document.getElementById('monitorPanel');
const detachBtn = document.getElementById('monitorDetachBtn');
if (panel.classList.contains('detached')) {
// Re-attach to bottom
panel.classList.remove('detached');
panel.style.top = '';
panel.style.left = '';
panel.style.width = '';
panel.style.height = '';
if (detachBtn) {
detachBtn.innerHTML = '&#x2197;'; // Detach icon
detachBtn.title = 'Detach panel';
}
} else {
// Detach as floating window
panel.classList.add('detached');
panel.classList.add('open'); // Ensure it's visible
if (detachBtn) {
detachBtn.innerHTML = '&#x2199;'; // Attach icon
detachBtn.title = 'Attach panel';
}
// Setup drag functionality
this.setupMonitorDrag();
}
}
setupMonitorDrag() {
const panel = document.getElementById('monitorPanel');
const header = document.getElementById('monitorPanelHeader');
if (!panel || !header) return;
let isDragging = false;
let startX, startY, startLeft, startTop;
const onMouseDown = (e) => {
// Only drag from header, not from buttons
if (e.target.closest('button')) return;
if (!panel.classList.contains('detached')) return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// Keep within viewport bounds
const rect = panel.getBoundingClientRect();
newLeft = Math.max(0, Math.min(window.innerWidth - rect.width, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - rect.height, newTop));
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
};
const onMouseUp = () => {
isDragging = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
// Remove existing listeners before adding new ones
header.removeEventListener('mousedown', header._dragHandler);
header._dragHandler = onMouseDown;
header.addEventListener('mousedown', onMouseDown);
}
renderTaskPanel() {
const session = this.sessions.get(this.activeSessionId);
const body = document.getElementById('backgroundTasksBody');
@@ -1561,32 +1647,103 @@ class ClaudemanApp {
}
}
async killAllSessions() {
killAllSessions() {
const count = this.screenSessions?.length || 0;
if (count === 0) {
alert('No sessions to kill');
return;
}
const confirmed = confirm(`Kill all ${count} session(s)? This cannot be undone.`);
if (!confirmed) return;
// Show the kill all modal
document.getElementById('killAllCount').textContent = count;
document.getElementById('killAllModal').classList.add('active');
}
closeKillAllModal() {
document.getElementById('killAllModal').classList.remove('active');
}
async confirmKillAll(killScreens) {
this.closeKillAllModal();
try {
const res = await fetch('/api/sessions', { method: 'DELETE' });
const data = await res.json();
if (data.success) {
// Clear local state
if (killScreens) {
// Kill everything including screens
const res = await fetch('/api/sessions', { method: 'DELETE' });
const data = await res.json();
if (data.success) {
this.sessions.clear();
this.screenSessions = [];
this.activeSessionId = null;
this.renderSessionTabs();
this.renderScreenSessions();
this.terminal.clear();
this.terminal.reset();
this.toast('All sessions and screens killed', 'success');
}
} else {
// Just remove tabs, keep screens running
this.sessions.clear();
this.screenSessions = [];
this.activeSessionId = null;
this.renderSessionTabs();
this.renderScreenSessions();
this.terminal.clear();
this.terminal.reset();
this.toast('All tabs removed, screens still running', 'info');
}
} catch (err) {
console.error('Failed to kill all sessions:', err);
alert('Failed to kill sessions: ' + err.message);
console.error('Failed to kill sessions:', err);
this.toast('Failed to kill sessions: ' + err.message, 'error');
}
}
// ========== Create Case Modal ==========
showCreateCaseModal() {
document.getElementById('newCaseName').value = '';
document.getElementById('newCaseDescription').value = '';
document.getElementById('createCaseModal').classList.add('active');
document.getElementById('newCaseName').focus();
}
closeCreateCaseModal() {
document.getElementById('createCaseModal').classList.remove('active');
}
async createCase() {
const name = document.getElementById('newCaseName').value.trim();
const description = document.getElementById('newCaseDescription').value.trim();
if (!name) {
this.toast('Please enter a case name', 'error');
return;
}
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
this.toast('Invalid name. Use only letters, numbers, hyphens, underscores.', 'error');
return;
}
try {
const res = await fetch('/api/cases', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description })
});
const data = await res.json();
if (data.success) {
this.closeCreateCaseModal();
this.toast(`Case "${name}" created`, 'success');
// Reload cases and select the new one
await this.loadQuickStartCases();
document.getElementById('quickStartCase').value = name;
this.updateDirDisplayForCase(name);
} else {
this.toast(data.error || 'Failed to create case', 'error');
}
} catch (err) {
console.error('Failed to create case:', err);
this.toast('Failed to create case: ' + err.message, 'error');
}
}
@@ -1701,26 +1858,38 @@ class ClaudemanApp {
updateSystemStatsDisplay(stats) {
const cpuEl = document.getElementById('statCpu');
const cpuBar = document.getElementById('statCpuBar');
const memEl = document.getElementById('statMem');
const memBar = document.getElementById('statMemBar');
if (cpuEl) {
cpuEl.textContent = `CPU: ${stats.cpu}%`;
// Color based on usage
if (cpuEl && cpuBar) {
cpuEl.textContent = `${stats.cpu}%`;
cpuBar.style.width = `${Math.min(100, stats.cpu)}%`;
// Color classes based on usage
cpuBar.classList.remove('medium', 'high');
cpuEl.classList.remove('high');
if (stats.cpu > 80) {
cpuBar.classList.add('high');
cpuEl.classList.add('high');
} else {
cpuEl.classList.remove('high');
} else if (stats.cpu > 50) {
cpuBar.classList.add('medium');
}
}
if (memEl) {
if (memEl && memBar) {
const memGB = (stats.memory.usedMB / 1024).toFixed(1);
memEl.textContent = `Mem: ${memGB}GB`;
// Color based on usage
memEl.textContent = `${memGB}G`;
memBar.style.width = `${Math.min(100, stats.memory.percent)}%`;
// Color classes based on usage
memBar.classList.remove('medium', 'high');
memEl.classList.remove('high');
if (stats.memory.percent > 80) {
memBar.classList.add('high');
memEl.classList.add('high');
} else {
memEl.classList.remove('high');
} else if (stats.memory.percent > 50) {
memBar.classList.add('medium');
}
}
}
+81 -11
View File
@@ -36,8 +36,20 @@
</div>
<div class="header-tokens" id="headerTokens" title="Total tokens across all sessions">0 tokens</div>
<div class="header-system-stats" id="headerSystemStats" title="System resource usage">
<span class="stat-cpu" id="statCpu">CPU: --</span>
<span class="stat-mem" id="statMem">Mem: --</span>
<div class="stat-item">
<span class="stat-label">CPU</span>
<div class="stat-bar">
<div class="stat-bar-fill stat-bar-cpu" id="statCpuBar"></div>
</div>
<span class="stat-value" id="statCpu">--%</span>
</div>
<div class="stat-item">
<span class="stat-label">MEM</span>
<div class="stat-bar">
<div class="stat-bar-fill stat-bar-mem" id="statMemBar"></div>
</div>
<span class="stat-value" id="statMem">--</span>
</div>
</div>
<button class="btn-icon-header" onclick="app.openAppSettings()" title="App Settings">&#x2699;</button>
</div>
@@ -74,7 +86,7 @@
<div class="toolbar-left">
<!-- Run Claude -->
<div class="toolbar-group">
<button class="btn-toolbar btn-primary" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)">
<button class="btn-toolbar btn-claude" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)">
Run Claude
</button>
<div class="tab-count-group" title="Number of tabs to open (1-10)">
@@ -82,12 +94,15 @@
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="10" readonly>
<button class="tab-count-btn" onclick="app.incrementTabCount()">+</button>
</div>
<button class="btn-toolbar" onclick="app.runShell()" title="Run Shell">
Shell
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
Run Shell
</button>
<select id="quickStartCase" class="toolbar-select" title="Select case">
<option value="testcase">testcase</option>
</select>
<div class="case-select-group">
<select id="quickStartCase" class="toolbar-select" title="Select case">
<option value="testcase">testcase</option>
</select>
<button class="btn-case-add" onclick="app.showCreateCaseModal()" title="Create new case">+</button>
</div>
</div>
<!-- Directory -->
@@ -117,7 +132,7 @@
<div><kbd>Ctrl</kbd>+<kbd>Enter</kbd></div><div>Run Claude</div>
<div><kbd>Ctrl</kbd>+<kbd>W</kbd></div><div>Close Session</div>
<div><kbd>Ctrl</kbd>+<kbd>Tab</kbd></div><div>Next Session</div>
<div><kbd>Ctrl</kbd>+<kbd>K</kbd></div><div>Kill All Sessions</div>
<div><kbd>Ctrl</kbd>+<kbd>K</kbd></div><div>Kill All Sessions + Screens</div>
<div><kbd>Ctrl</kbd>+<kbd>L</kbd></div><div>Clear Terminal</div>
<div><kbd>Ctrl</kbd>+<kbd>+</kbd></div><div>Increase Font</div>
<div><kbd>Ctrl</kbd>+<kbd>-</kbd></div><div>Decrease Font</div>
@@ -130,14 +145,16 @@
<!-- Monitor Panel (combined Process Monitor + Background Tasks) -->
<div class="monitor-panel" id="monitorPanel">
<div class="monitor-panel-header">
<div class="monitor-panel-header" id="monitorPanelHeader">
<span>Monitor</span>
<div class="monitor-panel-actions">
<button class="btn-icon-sm btn-danger-sm" onclick="app.killAllSessions()" title="Kill all sessions">&#x2715;</button>
<button class="btn-icon-sm btn-danger-sm" onclick="app.killAllSessions()" title="Kill all sessions and their screen processes">&#x2715;</button>
<button class="btn-icon-sm" onclick="app.reconcileScreens()" title="Refresh screens">&#x21BB;</button>
<button class="btn-icon-sm" onclick="app.toggleMonitorDetach()" title="Detach/Attach panel" id="monitorDetachBtn">&#x2197;</button>
<button class="btn-icon-sm" onclick="app.toggleMonitorPanel()" title="Toggle panel" id="monitorToggleBtn">&#x25BC;</button>
</div>
</div>
<div class="monitor-resize-handle" id="monitorResizeHandle"></div>
<div class="monitor-panel-body">
<div class="monitor-section">
<div class="monitor-section-header">
@@ -312,6 +329,59 @@
</div>
</div>
<!-- Create Case Modal -->
<div class="modal" id="createCaseModal">
<div class="modal-backdrop" onclick="app.closeCreateCaseModal()"></div>
<div class="modal-content modal-sm">
<div class="modal-header">
<h3>Create New Case</h3>
<button class="modal-close" onclick="app.closeCreateCaseModal()">&times;</button>
</div>
<div class="modal-body">
<div class="form-row">
<label>Case Name</label>
<input type="text" id="newCaseName" placeholder="my-project" pattern="[a-zA-Z0-9_-]+">
<span class="form-hint">Letters, numbers, hyphens, underscores only</span>
</div>
<div class="form-row">
<label>Description (optional)</label>
<input type="text" id="newCaseDescription" placeholder="A brief description...">
</div>
</div>
<div class="form-actions">
<button class="btn-toolbar" onclick="app.closeCreateCaseModal()">Cancel</button>
<button class="btn-toolbar btn-primary" onclick="app.createCase()">Create</button>
</div>
</div>
</div>
<!-- Kill All Confirmation Modal -->
<div class="modal" id="killAllModal">
<div class="modal-backdrop" onclick="app.closeKillAllModal()"></div>
<div class="modal-content modal-sm">
<div class="modal-header">
<h3>Kill All Sessions</h3>
<button class="modal-close" onclick="app.closeKillAllModal()">&times;</button>
</div>
<div class="modal-body">
<p class="modal-warning-text">Choose how to close <span id="killAllCount">0</span> session(s):</p>
</div>
<div class="close-options">
<button class="close-option" onclick="app.confirmKillAll(false)">
<span class="close-option-title">Remove All Tabs</span>
<span class="close-option-desc">Screen sessions keep running in background</span>
</button>
<button class="close-option close-option-danger" onclick="app.confirmKillAll(true)">
<span class="close-option-title">Kill All Claude + Screens</span>
<span class="close-option-desc">Terminate everything completely</span>
</button>
</div>
<div class="modal-footer-cancel">
<button class="btn-toolbar btn-sm" onclick="app.closeKillAllModal()">Cancel</button>
</div>
</div>
</div>
<!-- Help Button -->
<button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button>
</div>
+185 -6
View File
@@ -190,7 +190,7 @@ body {
.header-system-stats {
display: flex;
gap: 0.75rem;
font-size: 0.7rem;
font-size: 0.65rem;
font-family: 'SF Mono', Monaco, monospace;
color: var(--text-dim);
padding: 0 0.5rem;
@@ -198,11 +198,64 @@ body {
margin-left: 0.5rem;
}
.header-system-stats span {
white-space: nowrap;
.stat-item {
display: flex;
align-items: center;
gap: 0.35rem;
}
.header-system-stats .high {
.stat-label {
color: var(--text-muted);
font-size: 0.6rem;
text-transform: uppercase;
min-width: 24px;
}
.stat-bar {
width: 40px;
height: 6px;
background: var(--bg-input);
border-radius: 3px;
overflow: hidden;
}
.stat-bar-fill {
height: 100%;
border-radius: 3px;
transition: width 0.3s ease, background 0.3s ease;
}
.stat-bar-cpu {
background: linear-gradient(90deg, var(--green) 0%, var(--green) 100%);
}
.stat-bar-cpu.medium {
background: linear-gradient(90deg, var(--green) 0%, var(--yellow) 100%);
}
.stat-bar-cpu.high {
background: linear-gradient(90deg, var(--yellow) 0%, var(--red) 100%);
}
.stat-bar-mem {
background: linear-gradient(90deg, var(--accent) 0%, var(--accent-hover) 100%);
}
.stat-bar-mem.medium {
background: linear-gradient(90deg, var(--accent) 0%, var(--yellow) 100%);
}
.stat-bar-mem.high {
background: linear-gradient(90deg, var(--yellow) 0%, var(--red) 100%);
}
.stat-value {
min-width: 32px;
text-align: right;
color: var(--text-dim);
}
.stat-value.high {
color: var(--red);
font-weight: 500;
}
@@ -482,6 +535,47 @@ body {
background: var(--accent-hover);
}
/* Claude Button - Special styling */
.btn-toolbar.btn-claude {
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
border-color: #3b82f6;
color: white;
font-weight: 500;
padding: 0.4rem 0.85rem;
}
.btn-toolbar.btn-claude:hover {
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
border-color: #60a5fa;
box-shadow: 0 0 12px rgba(139, 92, 246, 0.4);
}
.btn-toolbar.btn-claude .btn-icon {
font-size: 0.9rem;
}
/* Shell Button - Special styling */
.btn-toolbar.btn-shell {
background: linear-gradient(135deg, #1a2e1a 0%, #234023 100%);
border-color: #2d5a2d;
color: #8fbc8f;
font-weight: 500;
}
.btn-toolbar.btn-shell:hover {
background: linear-gradient(135deg, #234023 0%, #2d5a2d 100%);
border-color: #3d7a3d;
box-shadow: 0 0 8px rgba(34, 139, 34, 0.4);
color: #a8d8a8;
}
/* Button icon styling */
.btn-toolbar .btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-toolbar.btn-success {
background: var(--green);
border-color: var(--green);
@@ -523,12 +617,17 @@ body {
color: var(--text);
}
/* Case select group with + button */
.case-select-group {
display: flex;
align-items: center;
}
.toolbar-select {
padding: 0.4rem 1.5rem 0.4rem 0.5rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-left: none;
border-radius: 0 4px 4px 0;
border-radius: 4px 0 0 4px;
color: var(--text-dim);
font-size: 0.75rem;
cursor: pointer;
@@ -548,6 +647,29 @@ body {
border-color: var(--accent);
}
.btn-case-add {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--bg-input);
border: 1px solid var(--border);
border-left: none;
border-radius: 0 4px 4px 0;
color: var(--green);
font-size: 1.1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
}
.btn-case-add:hover {
background: var(--green);
color: white;
border-color: var(--green);
}
.toolbar-input {
padding: 0.4rem 0.5rem;
background: var(--bg-input);
@@ -1528,6 +1650,39 @@ kbd {
transform: translateY(0);
}
/* Detached floating state */
.monitor-panel.detached {
bottom: auto;
right: auto;
top: 100px;
left: calc(50% - 300px);
width: 600px;
height: 500px;
max-height: none;
border-radius: 8px;
border-bottom: 1px solid var(--border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05);
transform: none;
resize: both;
overflow: hidden;
min-width: 350px;
min-height: 250px;
}
.monitor-panel.detached .monitor-panel-header {
cursor: move;
background: linear-gradient(180deg, var(--bg-hover) 0%, var(--bg-card) 100%);
user-select: none;
}
.monitor-panel.detached .monitor-resize-handle {
display: block;
}
.monitor-panel.detached .monitor-section-body {
max-height: none;
}
.monitor-panel-header {
display: flex;
align-items: center;
@@ -1551,6 +1706,30 @@ kbd {
padding: 0;
}
/* Resize handle for detached panel */
.monitor-resize-handle {
display: none;
position: absolute;
bottom: 0;
right: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, var(--border-light) 50%);
border-radius: 0 0 8px 0;
}
.monitor-resize-handle::before {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 8px;
height: 8px;
border-right: 2px solid var(--text-muted);
border-bottom: 2px solid var(--text-muted);
}
.monitor-section {
border-bottom: 1px solid var(--border);
}
+10 -1
View File
@@ -936,11 +936,20 @@ export class WebServer extends EventEmitter {
// Helper to get custom CLAUDE.md template path from settings
private getDefaultClaudeMdPath(): string | undefined {
const settingsPath = join(homedir(), '.claudeman', 'settings.json');
// Default template path (can be overridden in settings)
const defaultPath = '/home/arkon/default/CLAUDE.md';
try {
if (existsSync(settingsPath)) {
const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
return settings.defaultClaudeMdPath || undefined;
if (settings.defaultClaudeMdPath) {
return settings.defaultClaudeMdPath;
}
}
// Use default path if it exists
if (existsSync(defaultPath)) {
return defaultPath;
}
} catch (err) {
console.error('Failed to read settings:', err);