From 5b2091bd86d9caa20a5199d94dc85cc29ba70c42 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 02:55:10 +0100 Subject: [PATCH] feat: add multi-tab feature to open 1-10 Claude sessions at once - Add number input (1-10) next to "Run Claude" button in toolbar - When tabCount > 1, create sessions named "1-projectname", "2-projectname", etc. - All sessions share the same working directory (case folder) - First session is auto-selected after creation - Add CSS for small number input in toolbar Co-Authored-By: Claude Opus 4.5 --- src/web/public/app.js | 73 ++++++++++++++++++++++++++++++--------- src/web/public/index.html | 1 + src/web/public/styles.css | 22 ++++++++++++ 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/src/web/public/app.js b/src/web/public/app.js index 3a642011..bcf2b65e 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -678,32 +678,71 @@ class ClaudemanApp { async runClaude() { const caseName = document.getElementById('quickStartCase').value || 'testcase'; + const tabCount = Math.min(10, Math.max(1, parseInt(document.getElementById('tabCount').value) || 1)); this.terminal.clear(); - this.terminal.writeln(`\x1b[1;32m Starting Claude in ${caseName}...\x1b[0m`); + this.terminal.writeln(`\x1b[1;32m Starting ${tabCount} Claude session(s) in ${caseName}...\x1b[0m`); this.terminal.writeln(''); try { - const res = await fetch('/api/quick-start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ caseName }) - }); - const data = await res.json(); + // Get case path first + const caseRes = await fetch(`/api/cases/${caseName}`); + const caseData = await caseRes.json(); - if (!data.success) throw new Error(data.error); - - this.activeSessionId = data.sessionId; - this.loadQuickStartCases(); - - // Send resize - const dims = this.fitAddon.proposeDimensions(); - if (dims) { - await fetch(`/api/sessions/${data.sessionId}/resize`, { + // Create the case if it doesn't exist + if (!caseData.path) { + const createCaseRes = await fetch('/api/cases', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) + body: JSON.stringify({ name: caseName, description: '' }) }); + const createCaseData = await createCaseRes.json(); + if (!createCaseData.success) throw new Error(createCaseData.error || 'Failed to create case'); + } + + const workingDir = caseData.path || `${process.env.HOME}/claudeman-cases/${caseName}`; + let firstSessionId = null; + + // Create multiple sessions + for (let i = 1; i <= tabCount; i++) { + const sessionName = tabCount > 1 ? `${i}-${caseName}` : caseName; + + // Create session + const createRes = await fetch('/api/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workingDir, name: sessionName }) + }); + const createData = await createRes.json(); + if (!createData.success) throw new Error(createData.error); + + // Start interactive mode + await fetch(`/api/sessions/${createData.session.id}/interactive`, { + method: 'POST' + }); + + // Track first session + if (i === 1) { + firstSessionId = createData.session.id; + } + + this.terminal.writeln(`\x1b[90m Created session ${i}/${tabCount}: ${sessionName}\x1b[0m`); + } + + // Select the first session + if (firstSessionId) { + this.activeSessionId = firstSessionId; + this.loadQuickStartCases(); + + // Send resize + const dims = this.fitAddon.proposeDimensions(); + if (dims) { + await fetch(`/api/sessions/${firstSessionId}/resize`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ cols: dims.cols, rows: dims.rows }) + }); + } } this.terminal.focus(); diff --git a/src/web/public/index.html b/src/web/public/index.html index 7115e8f4..ff05268e 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -73,6 +73,7 @@ + diff --git a/src/web/public/styles.css b/src/web/public/styles.css index f9eab3d7..cb2715ed 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -539,6 +539,28 @@ body { max-width: 50vw; } +.toolbar-input-sm { + padding: 0.3rem 0.4rem; + background: var(--bg-input); + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text); + font-size: 0.75rem; + width: 40px; + text-align: center; +} + +.toolbar-input-sm:focus { + outline: none; + border-color: var(--accent); +} + +.toolbar-input-sm::-webkit-inner-spin-button, +.toolbar-input-sm::-webkit-outer-spin-button { + opacity: 1; + height: 16px; +} + #dirDisplay { max-width: 300px; overflow: hidden;