From eb8724bbf2167f72d1c203450f74067191be0a54 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 27 Jul 2026 03:01:14 +0200 Subject: [PATCH] feat(mobile): replace the phone Shell button with Enter, move Shell into Run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On phones the toolbar slot held "Shell", which starts a rarely-needed session type. Sending Enter is a constant need on a touch keyboard, so the slot now holds a dark blue Enter button and shell launching moves into the expandable Run dropdown (Terminal / Shell, label "Run SH"). Desktop and tablet are unchanged: the green Run Shell button stays exactly where it was. Enter goes through xterm's own input path: coreService.triggerDataEvent('\r', true) NOT through sendInput() or a direct POST to /input. localEchoEnabled defaults to MobileDetection.isTouchDevice(), so on a phone the characters you type are buffered client-side in the LocalEchoOverlay and have never reached the PTY. The onData Enter branch in terminal-ui.js is what flushes that buffer before sending \r. A bare \r submits an empty line and leaves the typed text stranded on screen, which presents as "the Enter button does nothing". Replaying the keypress reuses the overlay flush, the flushed-offset cleanup and the 80ms text-before-CR ordering instead of reimplementing them. Verified with local echo forced on: before the fix the overlay still held "echo OLD_WAY" after Enter; after it, pendingText is empty and the command executes in the pane. The !important on the Enter button's colors is required, not habit: styles.css nests its skin overrides inside `html:not([data-skin="og"]) { … }`, so a plain .btn-toolbar there resolves to (0,2,1) and outranks .btn-toolbar.btn-enter at (0,2,0). Without it the button renders in generic toolbar grey. Co-Authored-By: Claude Opus 5 (1M context) --- src/web/public/index.html | 10 ++++++ src/web/public/mobile.css | 62 +++++++++++++++++++++--------------- src/web/public/session-ui.js | 27 +++++++++++++++- src/web/public/styles.css | 9 ++++++ 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/web/public/index.html b/src/web/public/index.html index b51c96d1..557d1a00 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -460,6 +460,10 @@ Gemini
+ +
Recent Sessions
@@ -475,6 +479,12 @@ + +
diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css index 67cf181d..68339cc8 100644 --- a/src/web/public/mobile.css +++ b/src/web/public/mobile.css @@ -875,19 +875,44 @@ html.mobile-init .file-browser-panel { margin-right: 0; } - /* Secondary action - Run Shell - right side */ + /* Shell is NOT a toolbar button on phones — it moved into the Run dropdown + (Terminal / Shell), freeing this slot for Enter. Starting a shell is a rare, + deliberate act; sending Enter is a constant one, so the scarce phone real + estate goes to Enter. */ .btn-toolbar.btn-shell { - flex: 0 0 auto; - background: transparent; - border: 1px solid rgba(255, 255, 255, 0.2); - color: #9ca3af; - order: 4; /* Right position */ + display: none !important; } - .btn-toolbar.btn-shell:hover, - .btn-toolbar.btn-shell:active { - background: rgba(255, 255, 255, 0.1); - color: #fff; + /* Secondary action - Enter - right side. Takes the slot (and the order) the + Shell button used to hold, so the toolbar rhythm is unchanged. */ + .btn-toolbar.btn-enter { + display: flex !important; + flex: 0 0 auto; + align-items: center; + justify-content: center; + min-width: 54px; + width: 54px; + white-space: nowrap; + padding: 0 8px !important; + overflow: hidden; + font-size: 0.65rem; + font-weight: 600; + letter-spacing: 0.01em; + /* !important is REQUIRED here, not defensive habit: styles.css nests its skin + overrides inside `html:not([data-skin="og"]) { … }`, so a plain `.btn-toolbar` + in that block resolves to (0,2,1) and outranks this (0,2,0) rule. Without + !important the button silently renders in generic toolbar grey. */ + background: rgba(30, 58, 95, 0.85) !important; + border: 1px solid rgba(59, 130, 246, 0.45) !important; + color: #dbeafe !important; + order: 4; /* Right position — same slot Shell used to occupy */ + } + + .btn-toolbar.btn-enter:hover, + .btn-toolbar.btn-enter:active { + background: rgba(37, 74, 122, 0.95) !important; + border-color: rgba(59, 130, 246, 0.7) !important; + color: #fff !important; } /* Hide case selector on mobile - simplified toolbar */ @@ -895,27 +920,12 @@ html.mobile-init .file-browser-panel { display: none !important; } - /* Simplified toolbar layout — Run, Shell, and Case */ + /* Simplified toolbar layout — Run, Enter, and Case */ .toolbar-left .toolbar-group:first-child { width: 100%; gap: 8px; } - .btn-toolbar.btn-shell { - flex: 0 0 auto; - min-width: 54px; - width: 54px; - white-space: nowrap; - padding: 0 8px !important; - overflow: hidden; - font-size: 0 !important; - } - - .btn-toolbar.btn-shell::after { - content: "Shell"; - font-size: 0.65rem; - } - /* Mobile case button - visible on mobile */ .btn-toolbar.btn-case-mobile { display: flex !important; diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index a3e01b71..75ef9513 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -394,6 +394,9 @@ Object.assign(CodemanApp.prototype, { if (mode === 'gemini') { return await this.runGemini(); } + if (mode === 'shell') { + return await this.runShell(); + } return await this.runClaude(); } finally { const remaining = minLockMs - (Date.now() - startedAt); @@ -500,10 +503,32 @@ Object.assign(CodemanApp.prototype, { gearBtn.className = `btn-toolbar btn-run-gear mode-${mode}`; } if (label) { - label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : 'Run'; + label.textContent = mode === 'opencode' ? 'Run OC' : mode === 'codex' ? 'Run CX' : mode === 'gemini' ? 'Run GM' : mode === 'shell' ? 'Run SH' : 'Run'; } }, + /** Send Enter to the active session (phone toolbar button). + * + * MUST go through xterm's onData path, NOT straight to sendInput()/the API. + * With local echo on (the mobile default) the characters you typed are still + * buffered in the LocalEchoOverlay and have NEVER reached the PTY. The onData + * Enter branch (terminal-ui.js) is what flushes that pending text and only + * then sends \r. Send a bare \r instead and you submit an empty line while the + * typed text stays stranded on screen — which reads as "the button does + * nothing". triggerDataEvent replays it exactly as if the key were pressed, + * so overlay flush, flushed-offset cleanup and ordering are all reused. */ + sendEnterKey() { + if (!this.activeSessionId) return; + const coreService = this.terminal?._core?.coreService; + if (coreService && typeof coreService.triggerDataEvent === 'function') { + coreService.triggerDataEvent('\r', true); + return; + } + // Fallback only if xterm's private core API moves: correct when local echo + // is off, and still better than doing nothing. + this.sendInput('\r'); + }, + _initRunMode() { try { this._runMode = localStorage.getItem('codeman_runMode') || 'claude'; } catch { this._runMode = 'claude'; } this._applyRunMode(); diff --git a/src/web/public/styles.css b/src/web/public/styles.css index a29a8606..a3ae5659 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -3370,6 +3370,14 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea { .run-mode-dot.opencode { background: #10b981; } .run-mode-dot.codex { background: #a855f7; } .run-mode-dot.gemini { background: #8ab4f8; } +.run-mode-dot.shell { background: #94a3b8; } + +/* Phone-only Enter button (see index.html). Hidden by default at every width; + mobile.css turns it on inside @media (max-width: 430px), where it takes over + the slot the Shell button occupies on wider screens. */ +.btn-toolbar.btn-enter { + display: none; +} .run-mode-sep { height: 1px; @@ -11618,6 +11626,7 @@ html:not([data-skin="og"]) { .run-mode-dot.claude { background: var(--accent); } .run-mode-dot.opencode { background: var(--accent-soft); } .run-mode-dot.codex { background: var(--accent-grad-b); } +.run-mode-dot.shell { background: var(--text-dim); } /* ---- Shell button: quiet neutral with a calm green tint ---- */ .btn-toolbar.btn-shell {