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`)
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
+1 -1
View File
@@ -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",
+62 -12
View File
@@ -516,13 +516,22 @@ const KeyboardAccessoryBar = {
</button>
`;
// 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 */
+9 -2
View File
@@ -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 {
+12 -4
View File
@@ -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 {