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;