diff --git a/CLAUDE.md b/CLAUDE.md index 66576b3d..41407a44 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` (verify they match with `grep version package.json && grep Version CLAUDE.md`) 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1524 (must match `package.json` for npm publish) +**Version**: 0.1525 (must match `package.json` for npm publish) ## Project Overview diff --git a/package.json b/package.json index 25e871df..a97b33fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1524", + "version": "0.1525", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/web/public/app.js b/src/web/public/app.js index b0d9ad48..0e52a645 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -642,7 +642,7 @@ const KeyboardAccessoryBar = { this._confirmAction = action; if (btn) { btn.classList.add('confirming'); - btn.dataset.origText = btn.textContent; + btn.dataset.origHtml = btn.innerHTML; btn.textContent = 'Tap again'; } this._confirmTimer = setTimeout(() => this.clearConfirm(), 2000); @@ -656,9 +656,9 @@ const KeyboardAccessoryBar = { } if (this._confirmAction && this.element) { const btn = this.element.querySelector(`[data-action="${this._confirmAction}"]`); - if (btn && btn.dataset.origText) { - btn.textContent = btn.dataset.origText; - delete btn.dataset.origText; + if (btn && btn.dataset.origHtml) { + btn.innerHTML = btn.dataset.origHtml; + delete btn.dataset.origHtml; } if (btn) btn.classList.remove('confirming'); } @@ -7438,6 +7438,41 @@ class ClaudemanApp { } } + /** Send Ctrl+C to the active session to stop the current operation. + * Requires double-tap: first tap turns button amber, second tap within 2s sends Ctrl+C. */ + stopClaude() { + if (!this.activeSessionId) return; + const btn = document.querySelector('.btn-toolbar.btn-stop'); + if (!btn) return; + + if (this._stopConfirmTimer) { + // Second tap — send Ctrl+C + clearTimeout(this._stopConfirmTimer); + this._stopConfirmTimer = null; + btn.innerHTML = btn.dataset.origHtml; + delete btn.dataset.origHtml; + btn.classList.remove('confirming'); + fetch(`/api/sessions/${this.activeSessionId}/input`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input: '\x03' }) + }); + } else { + // First tap — enter confirm state + btn.dataset.origHtml = btn.innerHTML; + btn.textContent = 'Tap again'; + btn.classList.add('confirming'); + this._stopConfirmTimer = setTimeout(() => { + this._stopConfirmTimer = null; + if (btn.dataset.origHtml) { + btn.innerHTML = btn.dataset.origHtml; + delete btn.dataset.origHtml; + } + btn.classList.remove('confirming'); + }, 2000); + } + } + async runShell() { const caseName = document.getElementById('quickStartCase').value || 'testcase'; const shellCount = Math.min(20, Math.max(1, parseInt(document.getElementById('shellCount').value) || 1)); diff --git a/src/web/public/index.html b/src/web/public/index.html index feb52a27..7750c560 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -283,6 +283,9 @@ + diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index ac5066a0..a9200575 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -324,7 +324,7 @@ html.mobile-init .file-browser-panel { font-size: 0.85rem; cursor: pointer; flex-shrink: 0; - order: 4; /* Far right */ + order: 5; /* Far right */ position: relative; } @@ -502,20 +502,20 @@ html.mobile-init .file-browser-panel { color: #fff; } - /* Primary action button - Run Claude */ + /* Primary action button - Run Claude (matches arrow button blue) */ .btn-toolbar.btn-claude { flex: 0 0 auto; padding: 0 0.75rem !important; - background: var(--bg-input); - border: 1px solid var(--border-light); - color: var(--text); + background: #1e3a5f; + border: 1px solid rgba(59, 130, 246, 0.3); + color: #93c5fd; font-weight: 500; } .btn-toolbar.btn-claude:hover, .btn-toolbar.btn-claude:active { - background: var(--bg-hover); - border-color: var(--accent); + background: #2563eb; + border-color: rgba(59, 130, 246, 0.5); } .btn-toolbar.btn-claude svg { @@ -524,13 +524,28 @@ html.mobile-init .file-browser-panel { margin-right: 4px; } + /* Stop button - visible on mobile, icon-only */ + .btn-toolbar.btn-stop { + display: flex !important; + flex: 0 0 auto; + padding: 0 8px !important; + min-width: unset; + order: 3; /* Left of Run Shell */ + } + + .btn-toolbar.btn-stop svg { + width: 10px; + height: 10px; + margin-right: 0; + } + /* Secondary action - Run Shell - right side */ .btn-toolbar.btn-shell { flex: 0 0 auto; background: transparent; border: 1px solid rgba(255, 255, 255, 0.2); color: #9ca3af; - order: 3; /* Right position */ + order: 4; /* Right position */ } .btn-toolbar.btn-shell:hover, @@ -707,7 +722,6 @@ html.mobile-init .file-browser-panel { } .accessory-btn-dismiss { - margin-left: auto; padding: 8px 14px; background: #2a2a2a; border: 1.5px solid rgba(255, 255, 255, 0.25); diff --git a/src/web/public/styles.css b/src/web/public/styles.css index e28614fe..ce036347 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -1932,6 +1932,36 @@ body { color: #a8d8a8; } +/* Stop button - hidden on desktop, shown on mobile */ +.btn-toolbar.btn-stop { + display: none; + background: linear-gradient(135deg, #5a1a1a 0%, #7f1d1d 100%); + border-color: #dc2626; + color: #fca5a5; + font-weight: 500; +} + +.btn-toolbar.btn-stop:hover { + background: linear-gradient(135deg, #7f1d1d 0%, #991b1b 100%); + border-color: #ef4444; + box-shadow: 0 0 8px rgba(220, 38, 38, 0.4); + color: #fff; +} + +.btn-toolbar.btn-stop svg { + flex-shrink: 0; + width: 12px; + height: 12px; +} + +.btn-toolbar.btn-stop.confirming { + background: linear-gradient(135deg, #6b4f00 0%, #856200 100%); + border-color: #b8860b; + color: #ffd54f; + font-size: 0.65rem; + white-space: nowrap; +} + /* Mobile app settings gear - hidden on desktop, shown on mobile */ .btn-settings-mobile { display: none;