diff --git a/CLAUDE.md b/CLAUDE.md
index 6db40cfc..e8750f08 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,6 +80,11 @@ src/
- Terminal buffer persisted for client reconnections
- Works with RespawnController for autonomous cycling
+**Shell Mode** (`startShell()`):
+- Plain bash/zsh terminal without Claude
+- Useful for running commands alongside Claude sessions
+- Same PTY features (resize, buffer persistence)
+
## Code Patterns
### Claude Message Parsing
@@ -241,8 +246,9 @@ All events are broadcast to clients connected to `/api/events`. Event format: `{
```
GET /api/sessions # List all sessions (includes buffer stats)
-POST /api/sessions # Create session { workingDir }
+POST /api/sessions # Create session { workingDir, mode?, name? }
GET /api/sessions/:id # Get single session details
+PUT /api/sessions/:id/name # Rename session { name }
DELETE /api/sessions/:id # Stop and remove a session (kills process + children)
DELETE /api/sessions # Kill all sessions at once
GET /api/sessions/:id/output # Get session output buffer
@@ -253,7 +259,8 @@ GET /api/sessions/:id/terminal # Get terminal buffer (raw ANSI)
```
POST /api/sessions/:id/run # Run prompt { prompt } (one-shot mode)
-POST /api/sessions/:id/interactive # Start interactive terminal mode
+POST /api/sessions/:id/interactive # Start interactive Claude terminal mode
+POST /api/sessions/:id/shell # Start plain shell (bash/zsh, no Claude)
POST /api/sessions/:id/input # Send input to interactive session { input }
POST /api/sessions/:id/resize # Resize terminal { cols, rows }
POST /api/sessions/:id/interactive-respawn # Start interactive + respawn controller
@@ -307,3 +314,9 @@ GET /api/events # SSE stream (real-time events)
GET /api/status # Full state snapshot (sessions + scheduled + respawn)
```
+## Pending Tasks
+
+- [ ] Remove the "New Session" tab and add a gear icon (⚙️) in the top right corner for app settings
+- [ ] Add confirmation dialog when clicking "x" on a session tab - warn user that the screen session behind will be closed
+- [ ] In settings: add option to configure a default CLAUDE.md file path (e.g. /home/arkon/default/CLAUDE.md) that will be used for new sessions/cases
+- [ ] Wrap each session in a GNU screen session so they persist even if webinterface stops. Track active screen sessions in a file (~/.claudeman/screens.json), allow killing them after webserver restart, and show them in the background tasks tab for monitoring. Include a detailed task manager view: show memory usage (MB), CPU usage, child processes spawned per session - make it very responsive with real-time updates
diff --git a/src/web/public/app.js b/src/web/public/app.js
index b4ff5f97..ed63cb0c 100644
--- a/src/web/public/app.js
+++ b/src/web/public/app.js
@@ -108,7 +108,7 @@ class ClaudemanApp {
showWelcome() {
this.terminal.writeln('\x1b[1;36m Claudeman Terminal\x1b[0m');
- this.terminal.writeln('\x1b[90m Click "Quick Start" or press Ctrl+Enter to begin\x1b[0m');
+ this.terminal.writeln('\x1b[90m Click "Run Claude" or press Ctrl+Enter to begin\x1b[0m');
this.terminal.writeln('');
}
@@ -127,6 +127,7 @@ class ClaudemanApp {
}
setupEventListeners() {
+ // Use capture to handle before terminal
document.addEventListener('keydown', (e) => {
// Escape - close panels and modals
if (e.key === 'Escape') {
@@ -185,7 +186,7 @@ class ClaudemanApp {
e.preventDefault();
this.decreaseFontSize();
}
- });
+ }, true); // Use capture phase to handle before terminal
}
// ========== SSE Connection ==========
@@ -465,10 +466,10 @@ class ClaudemanApp {
const hasRunningTasks = taskStats.running > 0;
html += `
-
+
${mode === 'shell' ? 'sh' : ''}
- ${this.escapeHtml(name)}
+ ${this.escapeHtml(name)}
${hasRunningTasks ? `${taskStats.running}` : ''}
⚙
×
@@ -597,11 +598,36 @@ class ClaudemanApp {
select.innerHTML = options;
newSessionSelect.innerHTML = options;
+
+ // Auto-select first case and update directory display
+ if (cases.length > 0) {
+ const firstCase = cases.find(c => c.name === 'testcase') || cases[0];
+ select.value = firstCase.name;
+ this.updateDirDisplayForCase(firstCase.name);
+ }
+
+ // Update directory when case selection changes
+ select.addEventListener('change', () => {
+ this.updateDirDisplayForCase(select.value);
+ });
} catch (err) {
console.error('Failed to load cases:', err);
}
}
+ async updateDirDisplayForCase(caseName) {
+ try {
+ const res = await fetch(`/api/cases/${caseName}`);
+ const data = await res.json();
+ if (data.path) {
+ document.getElementById('dirDisplay').textContent = data.path;
+ document.getElementById('dirInput').value = data.path;
+ }
+ } catch (err) {
+ document.getElementById('dirDisplay').textContent = caseName;
+ }
+ }
+
async quickStart() {
// Alias for backward compatibility
return this.runClaude();
@@ -1175,6 +1201,58 @@ class ClaudemanApp {
}
}
+ // Inline rename on right-click
+ startInlineRename(sessionId) {
+ const session = this.sessions.get(sessionId);
+ if (!session) return;
+
+ const tabName = document.querySelector(`.tab-name[data-session-id="${sessionId}"]`);
+ if (!tabName) return;
+
+ const currentName = this.getSessionName(session);
+ const input = document.createElement('input');
+ input.type = 'text';
+ input.value = session.name || '';
+ input.placeholder = currentName;
+ input.className = 'tab-rename-input';
+ input.style.cssText = 'width: 80px; font-size: 0.75rem; padding: 2px 4px; background: var(--bg-input); border: 1px solid var(--accent); border-radius: 3px; color: var(--text); outline: none;';
+
+ const originalContent = tabName.textContent;
+ tabName.textContent = '';
+ tabName.appendChild(input);
+ input.focus();
+ input.select();
+
+ const finishRename = async () => {
+ const newName = input.value.trim();
+ tabName.textContent = newName || originalContent;
+
+ if (newName && newName !== session.name) {
+ try {
+ await fetch(`/api/sessions/${sessionId}/name`, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ name: newName })
+ });
+ } catch (err) {
+ tabName.textContent = originalContent;
+ this.showToast('Failed to rename', 'error');
+ }
+ }
+ };
+
+ input.addEventListener('blur', finishRename);
+ input.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ input.blur();
+ } else if (e.key === 'Escape') {
+ input.value = '';
+ input.blur();
+ }
+ });
+ }
+
// ========== Help Modal ==========
showHelp() {
diff --git a/src/web/public/index.html b/src/web/public/index.html
index 3c7dd325..de1f94bf 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -217,7 +217,7 @@
-
Ctrl+Enter
Quick Start
+
Ctrl+Enter
Run Claude
Ctrl+N
New Session
Ctrl+W
Close Session
Ctrl+Tab
Next Session
diff --git a/src/web/public/styles.css b/src/web/public/styles.css
index d00f1908..118c6c26 100644
--- a/src/web/public/styles.css
+++ b/src/web/public/styles.css
@@ -190,12 +190,12 @@ body {
/* Session tab gear icon */
.session-tab .tab-gear {
opacity: 0;
- padding: 0 0.2rem;
- font-size: 0.75rem;
+ padding: 0 0.3rem;
+ font-size: 1rem;
line-height: 1;
color: var(--text-muted);
cursor: pointer;
- transition: opacity 0.15s;
+ transition: opacity 0.15s, transform 0.2s;
}
.session-tab:hover .tab-gear {
@@ -204,6 +204,7 @@ body {
.session-tab .tab-gear:hover {
color: var(--accent-hover);
+ transform: rotate(45deg);
}
/* Mode indicator on session tab */