feat(mobile): replace the phone Shell button with Enter, move Shell into Run

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) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-27 03:01:14 +02:00
parent cb6c25220f
commit eb8724bbf2
4 changed files with 81 additions and 27 deletions
+10
View File
@@ -460,6 +460,10 @@
<span class="run-mode-dot gemini"></span>Gemini
</button>
<div class="run-mode-sep"></div>
<button class="run-mode-option" data-mode="shell" onclick="app.setRunMode('shell')">
<span class="run-mode-dot shell"></span>Terminal / Shell
</button>
<div class="run-mode-sep"></div>
<div class="run-mode-header">Recent Sessions</div>
<div class="run-mode-history" id="runModeHistory"></div>
</div>
@@ -475,6 +479,12 @@
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
Run Shell
</button>
<!-- Phone-only: replaces the Shell button on ≤430px (Shell moves into the Run
dropdown there). Sends a bare Enter to the active session, the complement
to the accessory bar's Esc. Hidden everywhere else — see styles.css. -->
<button class="btn-toolbar btn-enter" onclick="app.sendEnterKey()" title="Send Enter">
Enter
</button>
<div class="tab-count-group" title="Instance count">
<button class="tab-count-btn" onclick="app.decrementShellCount()">−</button>
<input type="number" id="shellCount" class="tab-count-input" value="1" min="1" max="20" readonly>
+36 -26
View File
@@ -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;
+26 -1
View File
@@ -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();
+9
View File
@@ -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 {