diff --git a/CLAUDE.md b/CLAUDE.md index f25ed441..2d62e9d4 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.1511 (must match `package.json` for npm publish) +**Version**: 0.1512 (must match `package.json` for npm publish) ## Project Overview diff --git a/package.json b/package.json index 326433de..02d31b80 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1511", + "version": "0.1512", "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 40a1cdb3..c2e77bc6 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -516,13 +516,22 @@ const KeyboardAccessoryBar = { `; - // Add click handlers + // Add click handlers — preventDefault stops event from reaching terminal this.element.addEventListener('click', (e) => { const btn = e.target.closest('.accessory-btn'); if (!btn) return; + e.preventDefault(); + e.stopPropagation(); const action = btn.dataset.action; - this.handleAction(action); + this.handleAction(action, btn); + + // For double-tap buttons, refocus terminal so keyboard stays open during confirm state + if ((action === 'clear' || action === 'compact') && this._confirmAction) { + if (typeof app !== 'undefined' && app.terminal) { + app.terminal.focus(); + } + } }); // Insert before toolbar @@ -532,20 +541,29 @@ const KeyboardAccessoryBar = { } }, + _confirmTimer: null, + _confirmAction: null, + /** Handle accessory button actions */ - handleAction(action) { + handleAction(action, btn) { if (typeof app === 'undefined' || !app.activeSessionId) return; switch (action) { case 'init': - this.confirmAndSend('/init', 'Run /init command?'); + this.sendCommand('/init'); break; case 'clear': - this.confirmAndSend('/clear', 'Clear conversation history?'); - break; - case 'compact': - this.confirmAndSend('/compact', 'Compact context?'); + case 'compact': { + // Require double-tap: first tap turns amber, second tap within 2s sends + const cmd = action === 'clear' ? '/clear' : '/compact'; + if (this._confirmAction === action && this._confirmTimer) { + this.clearConfirm(); + this.sendCommand(cmd); + } else { + this.setConfirm(action, btn); + } break; + } case 'paste': this.pasteFromClipboard(); break; @@ -556,11 +574,43 @@ const KeyboardAccessoryBar = { } }, - /** Show confirmation dialog before sending command */ - confirmAndSend(command, message) { - if (confirm(message)) { - app.sendInput(command + '\r'); + /** Enter confirm state: button turns amber for 2s waiting for second tap */ + setConfirm(action, btn) { + this.clearConfirm(); + this._confirmAction = action; + if (btn) { + btn.classList.add('confirming'); + btn.dataset.origText = btn.textContent; + btn.textContent = 'Tap again'; } + this._confirmTimer = setTimeout(() => this.clearConfirm(), 2000); + }, + + /** Reset confirm state */ + clearConfirm() { + if (this._confirmTimer) { + clearTimeout(this._confirmTimer); + this._confirmTimer = null; + } + 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.classList.remove('confirming'); + } + this._confirmAction = null; + }, + + /** Send a slash command to the active session. + * Sends text and Enter separately so Ink processes them as distinct events. */ + sendCommand(command) { + if (!app.activeSessionId) return; + // Send command text first (without Enter) + app.sendInput(command); + // Send Enter separately after a brief delay so Ink has time to process the text. + setTimeout(() => app.sendInput('\r'), 120); }, /** Read clipboard and send contents as input */ diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index f16ff680..81607b08 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -504,10 +504,17 @@ border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: #e5e5e5; - font-size: 0.75rem; + font-size: 0.65rem; font-weight: 500; cursor: pointer; - transition: background 0.1s; + transition: background 0.15s, border-color 0.15s; + } + + /* Confirm state — button turns amber and shows "Tap again" */ + .accessory-btn.confirming { + background: #6b4f00; + border-color: #b8860b; + color: #ffd54f; } .accessory-btn:active { diff --git a/src/web/public/styles.css b/src/web/public/styles.css index eec76f23..bb4d81ca 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -1807,8 +1807,9 @@ body { background: var(--bg-card); border-top: 1px solid var(--border); flex-shrink: 0; - /* Performance: isolate from terminal — toolbar updates shouldn't trigger terminal reflow */ - contain: layout style paint; + /* contain: style only — layout/paint containment clips the case-settings popover + that extends above the toolbar (popover uses position:absolute + bottom:100%) */ + contain: style; } .toolbar-left, @@ -8580,10 +8581,17 @@ kbd { border: 1px solid rgba(255, 255, 255, 0.15); border-radius: 6px; color: #e5e5e5; - font-size: 0.75rem; + font-size: 0.65rem; font-weight: 500; cursor: pointer; - transition: background 0.1s; + transition: background 0.15s, border-color 0.15s; + } + + /* Confirm state — button turns amber and shows "Tap again" */ + .accessory-btn.confirming { + background: #6b4f00; + border-color: #b8860b; + color: #ffd54f; } .accessory-btn:active {