fix: mobile keyboard accessory buttons — split text/Enter, double-tap confirm for clear/compact

- Remove confirm() dialog which dismissed keyboard and broke Enter delivery
- Send command text and Enter as separate requests with 120ms delay so Ink processes them as distinct events
- Add double-tap confirmation for /clear and /compact (amber "Tap again" state, 2s timeout)
- /init sends immediately (non-destructive)
- Refocus terminal after first tap on confirm buttons to keep keyboard open
- Match accessory button font size to toolbar buttons (0.65rem)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-16 12:43:18 +01:00
co-authored by Claude Opus 4.6
parent da74ac248b
commit 6b8f863666
5 changed files with 85 additions and 20 deletions
+1 -1
View File
@@ -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`) 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` 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 ## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "claudeman", "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", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
+62 -12
View File
@@ -516,13 +516,22 @@ const KeyboardAccessoryBar = {
</button> </button>
`; `;
// Add click handlers // Add click handlers — preventDefault stops event from reaching terminal
this.element.addEventListener('click', (e) => { this.element.addEventListener('click', (e) => {
const btn = e.target.closest('.accessory-btn'); const btn = e.target.closest('.accessory-btn');
if (!btn) return; if (!btn) return;
e.preventDefault();
e.stopPropagation();
const action = btn.dataset.action; 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 // Insert before toolbar
@@ -532,20 +541,29 @@ const KeyboardAccessoryBar = {
} }
}, },
_confirmTimer: null,
_confirmAction: null,
/** Handle accessory button actions */ /** Handle accessory button actions */
handleAction(action) { handleAction(action, btn) {
if (typeof app === 'undefined' || !app.activeSessionId) return; if (typeof app === 'undefined' || !app.activeSessionId) return;
switch (action) { switch (action) {
case 'init': case 'init':
this.confirmAndSend('/init', 'Run /init command?'); this.sendCommand('/init');
break; break;
case 'clear': case 'clear':
this.confirmAndSend('/clear', 'Clear conversation history?'); case 'compact': {
break; // Require double-tap: first tap turns amber, second tap within 2s sends
case 'compact': const cmd = action === 'clear' ? '/clear' : '/compact';
this.confirmAndSend('/compact', 'Compact context?'); if (this._confirmAction === action && this._confirmTimer) {
this.clearConfirm();
this.sendCommand(cmd);
} else {
this.setConfirm(action, btn);
}
break; break;
}
case 'paste': case 'paste':
this.pasteFromClipboard(); this.pasteFromClipboard();
break; break;
@@ -556,11 +574,43 @@ const KeyboardAccessoryBar = {
} }
}, },
/** Show confirmation dialog before sending command */ /** Enter confirm state: button turns amber for 2s waiting for second tap */
confirmAndSend(command, message) { setConfirm(action, btn) {
if (confirm(message)) { this.clearConfirm();
app.sendInput(command + '\r'); 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 */ /** Read clipboard and send contents as input */
+9 -2
View File
@@ -504,10 +504,17 @@
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px; border-radius: 6px;
color: #e5e5e5; color: #e5e5e5;
font-size: 0.75rem; font-size: 0.65rem;
font-weight: 500; font-weight: 500;
cursor: pointer; 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 { .accessory-btn:active {
+12 -4
View File
@@ -1807,8 +1807,9 @@ body {
background: var(--bg-card); background: var(--bg-card);
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
flex-shrink: 0; flex-shrink: 0;
/* Performance: isolate from terminal — toolbar updates shouldn't trigger terminal reflow */ /* contain: style only — layout/paint containment clips the case-settings popover
contain: layout style paint; that extends above the toolbar (popover uses position:absolute + bottom:100%) */
contain: style;
} }
.toolbar-left, .toolbar-left,
@@ -8580,10 +8581,17 @@ kbd {
border: 1px solid rgba(255, 255, 255, 0.15); border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px; border-radius: 6px;
color: #e5e5e5; color: #e5e5e5;
font-size: 0.75rem; font-size: 0.65rem;
font-weight: 500; font-weight: 500;
cursor: pointer; 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 { .accessory-btn:active {