mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix: restore mobile terminal scrollback — use JS scrollLines() instead of broken native scroll
xterm.js DOM renderer doesn't populate .xterm-viewport's scroll area (the div is empty, scrollHeight === clientHeight), so native CSS scrolling via touch-action:pan-y and overflow-y:scroll had nothing to scroll. Desktop worked only because the wheel handler called terminal.scrollLines() directly. - Replace split mobile/desktop touch handlers with unified JS-driven handler that converts touch deltas to terminal.scrollLines() calls (with pixel accumulation for slow swipes and momentum scrolling) - Change touch-action from pan-y to none on terminal elements so browser doesn't fight the JS handler - Remove now-unnecessary xterm-viewport position/overflow/z-index overrides and iOS -webkit-overflow-scrolling rules - Fix _shrinkPaddingToFit() arithmetic (was adding gap instead of subtracting) - Minor: add route-helpers.ts to CLAUDE.md, fix sse-events.ts comment count Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,7 @@ Codeman is a Claude Code session manager with web interface and autonomous Ralph
|
||||
| **State** | `src/state-store.ts`, `src/run-summary.ts`, `src/session-lifecycle-log.ts` | |
|
||||
| **Infra** | `src/hooks-config.ts`, `src/push-store.ts`, `src/tunnel-manager.ts`, `src/image-watcher.ts`, `src/file-stream-manager.ts` | |
|
||||
| **Plan** | `src/plan-orchestrator.ts`, `src/prompts/*.ts`, `src/templates/claude-md.ts` | |
|
||||
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 route modules incl. `ws-routes.ts` + barrel), `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | |
|
||||
| **Web** | `src/web/server.ts`, `src/web/sse-events.ts`, `src/web/routes/*.ts` (13 route modules incl. `ws-routes.ts` + barrel), `src/web/route-helpers.ts`, `src/web/ports/*.ts`, `src/web/middleware/auth.ts`, `src/web/schemas.ts` | |
|
||||
| **Frontend** | `src/web/public/app.js` (~2.6K lines, core) + 5 infra modules (`constants.js`, `mobile-handlers.js`, `voice-input.js`, `notification-manager.js`, `keyboard-accessory.js`) + 6 domain modules (`terminal-ui.js`, `respawn-ui.js`, `ralph-panel.js`, `settings-ui.js`, `panels-ui.js`, `session-ui.js`) + 4 feature modules (`ralph-wizard.js`, `api-client.js`, `subagent-windows.js`, `input-cjk.js`) + `sw.js` | |
|
||||
| **Types** | `src/types/index.ts` → 13 domain files | See `@fileoverview` in index.ts |
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ const KeyboardHandler = {
|
||||
const gap = container.clientHeight - app.terminal.rows * cellH;
|
||||
if (gap > 0 && gap < cellH) {
|
||||
const currentPadding = parseInt(main.style.paddingBottom) || 0;
|
||||
main.style.paddingBottom = currentPadding + gap + 'px';
|
||||
main.style.paddingBottom = Math.max(0, currentPadding - gap) + 'px';
|
||||
if (app.fitAddon)
|
||||
try {
|
||||
app.fitAddon.fit();
|
||||
|
||||
+10
-31
@@ -1128,36 +1128,21 @@ html.mobile-init .file-browser-panel {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Mobile terminal - native touch scrolling via xterm-viewport */
|
||||
/* Mobile terminal — JS touch handler (terminal.scrollLines()) drives
|
||||
scrollback because xterm.js DOM renderer doesn't populate xterm-viewport's
|
||||
scroll area. touch-action:none lets our JS handler own the gesture. */
|
||||
.terminal-container {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
overflow: visible; /* Must not be hidden — blocks touch scroll on viewport */
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
.terminal-container .xterm {
|
||||
touch-action: pan-y;
|
||||
}
|
||||
|
||||
/* xterm-viewport is the scrollable element inside xterm.js.
|
||||
It must sit above xterm-screen to receive touch events.
|
||||
See: https://github.com/xtermjs/xterm.js/issues/5377 */
|
||||
.terminal-container .xterm-viewport {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
bottom: 0 !important;
|
||||
z-index: 10 !important;
|
||||
touch-action: pan-y;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overflow-y: scroll !important;
|
||||
overflow: visible;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.terminal-container .xterm,
|
||||
.terminal-container .xterm-viewport,
|
||||
.terminal-container .xterm-screen {
|
||||
touch-action: pan-y;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* Compact welcome overlay for mobile */
|
||||
@@ -2061,14 +2046,8 @@ html.mobile-init .file-browser-panel {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.ios-device.safari-browser .terminal-container {
|
||||
/* Keep momentum scrolling but prevent page bounce */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ios-device.safari-browser .terminal-container .xterm-viewport {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
/* JS touch handler now drives terminal scrollback — no native scroll needed.
|
||||
-webkit-overflow-scrolling and overflow-y:scroll on xterm-viewport removed. */
|
||||
|
||||
/* ============================================================================
|
||||
Hover State Fallbacks for Touch
|
||||
|
||||
@@ -139,80 +139,82 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.terminal.scrollLines(lines);
|
||||
}, { passive: false });
|
||||
|
||||
// Touch scrolling - only use custom JS scrolling on desktop
|
||||
// Mobile uses native browser scrolling via CSS touch-action: pan-y
|
||||
const isMobileDevice = MobileDetection.isTouchDevice() && window.innerWidth < 1024;
|
||||
|
||||
if (!isMobileDevice) {
|
||||
// Desktop touch scrolling with custom momentum
|
||||
// Touch scrolling — use terminal.scrollLines() for all devices.
|
||||
// xterm.js DOM renderer doesn't populate xterm-viewport's scroll area,
|
||||
// so native CSS scrolling (overflow-y: scroll + touch-action: pan-y)
|
||||
// has nothing to scroll. Instead, convert touch deltas into scrollLines()
|
||||
// calls, matching the wheel handler above.
|
||||
{
|
||||
const cellHeight = () =>
|
||||
this.terminal._core?._renderService?.dimensions?.css?.cell?.height || 13;
|
||||
let touchLastY = 0;
|
||||
let pendingDelta = 0;
|
||||
let velocity = 0;
|
||||
let lastTime = 0;
|
||||
let scrollFrame = null;
|
||||
let isTouching = false;
|
||||
|
||||
const viewport = container.querySelector('.xterm-viewport');
|
||||
|
||||
// Single RAF loop handles both touch and momentum
|
||||
const scrollLoop = (timestamp) => {
|
||||
if (!viewport) return;
|
||||
|
||||
const dt = lastTime ? (timestamp - lastTime) / 16.67 : 1; // Normalize to 60fps
|
||||
const dt = lastTime ? (timestamp - lastTime) / 16.67 : 1;
|
||||
lastTime = timestamp;
|
||||
|
||||
if (isTouching) {
|
||||
// During touch: apply pending delta
|
||||
if (pendingDelta !== 0) {
|
||||
viewport.scrollTop += pendingDelta;
|
||||
pendingDelta = 0;
|
||||
}
|
||||
if (!isTouching && Math.abs(velocity) > 0.3) {
|
||||
// Momentum phase — convert pixel velocity to lines
|
||||
const lines = Math.round(velocity / cellHeight());
|
||||
if (lines !== 0) this.terminal.scrollLines(lines);
|
||||
velocity *= 0.92;
|
||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||
} else if (Math.abs(velocity) > 0.1) {
|
||||
// Momentum phase
|
||||
viewport.scrollTop += velocity * dt;
|
||||
velocity *= 0.94; // Smooth deceleration
|
||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||
} else {
|
||||
} else if (!isTouching) {
|
||||
scrollFrame = null;
|
||||
velocity = 0;
|
||||
} else {
|
||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||
}
|
||||
};
|
||||
|
||||
// Accumulate sub-line pixel deltas so slow swipes still scroll
|
||||
let pixelAccum = 0;
|
||||
|
||||
container.addEventListener('touchstart', (ev) => {
|
||||
if (ev.touches.length === 1) {
|
||||
touchLastY = ev.touches[0].clientY;
|
||||
pendingDelta = 0;
|
||||
velocity = 0;
|
||||
pixelAccum = 0;
|
||||
isTouching = true;
|
||||
lastTime = 0;
|
||||
if (!scrollFrame) {
|
||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||
}
|
||||
if (scrollFrame) { cancelAnimationFrame(scrollFrame); scrollFrame = null; }
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
container.addEventListener('touchmove', (ev) => {
|
||||
if (ev.touches.length === 1 && isTouching) {
|
||||
const touchY = ev.touches[0].clientY;
|
||||
const delta = touchLastY - touchY;
|
||||
pendingDelta += delta;
|
||||
velocity = delta * 1.2; // Track for momentum
|
||||
const delta = touchLastY - touchY; // positive = scroll down
|
||||
pixelAccum += delta;
|
||||
velocity = delta * 1.2;
|
||||
touchLastY = touchY;
|
||||
// Convert accumulated pixels to whole lines
|
||||
const ch = cellHeight();
|
||||
const lines = Math.trunc(pixelAccum / ch);
|
||||
if (lines !== 0) {
|
||||
this.terminal.scrollLines(lines);
|
||||
pixelAccum -= lines * ch;
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
container.addEventListener('touchend', () => {
|
||||
isTouching = false;
|
||||
// Momentum continues in scrollLoop
|
||||
if (!scrollFrame && Math.abs(velocity) > 0.3) {
|
||||
scrollFrame = requestAnimationFrame(scrollLoop);
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
container.addEventListener('touchcancel', () => {
|
||||
isTouching = false;
|
||||
velocity = 0;
|
||||
pixelAccum = 0;
|
||||
}, { passive: true });
|
||||
}
|
||||
// Mobile: native scrolling handles touch via CSS
|
||||
|
||||
// Welcome message
|
||||
this.showWelcome();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* and referenced by the frontend (`SSE_EVENTS` in `constants.js`).
|
||||
* Both files MUST be kept in sync.
|
||||
*
|
||||
* ~90 event constants organized by category:
|
||||
* ~106 event constants organized by category:
|
||||
* - **Core** (1): init
|
||||
* - **Session lifecycle** (17): created, updated, deleted, terminal, idle, working, ...
|
||||
* - **Session: Ralph** (6): ralphLoopUpdate, todoUpdate, completionDetected, ...
|
||||
|
||||
Reference in New Issue
Block a user