fix: mobile keyboard handlers killed on SSE reconnect — re-init after cleanup

handleInit() called KeyboardHandler.cleanup() and MobileDetection.cleanup()
on every SSE connect but never re-initialized them, breaking keyboard
scroll-into-view and the /init /clear accessory bar on mobile.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-02-18 11:59:42 +01:00
co-authored by Claude Opus 4.6
parent ee22ef204f
commit c548c2d6ee
9 changed files with 619 additions and 126 deletions
+10 -3
View File
@@ -1543,14 +1543,17 @@ class ClaudemanApp {
KeyboardHandler.init();
SwipeHandler.init();
KeyboardAccessoryBar.init();
this.initTerminal();
this.loadFontSize();
this.applyHeaderVisibilitySettings();
this.applyTabWrapSettings();
this.applyMonitorVisibility();
// Remove mobile-init class now that JS has applied visibility settings.
// The inline <script> in <head> added this to prevent flash-of-content on mobile.
document.documentElement.classList.remove('mobile-init');
// Defer heavy terminal canvas creation to next frame — lets browser paint header/skeleton first
requestAnimationFrame(() => {
this.initTerminal();
this.loadFontSize();
});
this.connectSSE();
// Only fetch state if SSE init event hasn't arrived within 3s (avoids duplicate handleInit)
this._initFallbackTimer = setTimeout(() => {
@@ -1578,6 +1581,8 @@ class ClaudemanApp {
this.applyTabWrapSettings();
this.applyMonitorVisibility();
});
// Hide loading skeleton now that the app shell is ready
document.body.classList.add('app-loaded');
}
initTerminal() {
@@ -3431,9 +3436,11 @@ class ClaudemanApp {
// Clear subagent activity/results maps (prevents leaks if data.subagents is missing)
this.subagentActivity.clear();
this.subagentToolResults.clear();
// Clean up mobile/keyboard handlers before potential re-init
// Clean up mobile/keyboard handlers and re-init (prevents listener accumulation on reconnect)
MobileDetection.cleanup();
KeyboardHandler.cleanup();
MobileDetection.init();
KeyboardHandler.init();
// Clear tab alerts
this.tabAlerts.clear();
// Clear shown completions (used for duplicate notification prevention)
+27 -8
View File
@@ -5,23 +5,42 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Claude Code session manager with web interface">
<meta name="theme-color" content="#0a0a0a">
<meta name="google" content="notranslate">
<title>Claudeman</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2360a5fa'/%3E%3Cstop offset='100%25' stop-color='%233b82f6'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='32' height='32' rx='6' fill='%230a0a0a'/%3E%3Cpath d='M18 4L8 18h6l-2 10 10-14h-6z' fill='url(%23g)'/%3E%3C/svg%3E">
<!-- DNS preconnect for CDN — saves ~100ms on first load -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
<link rel="stylesheet" href="styles.css?v=0.1536">
<link rel="stylesheet" href="mobile.css?v=0.1536" media="(max-width: 1023px)">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css"></noscript>
<!-- Defer CDN scripts — they no longer block initial HTML parsing/paint.
<!-- xterm.css loaded async — terminal won't display until xterm.js runs anyway -->
<link rel="preload" href="vendor/xterm.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="vendor/xterm.css"></noscript>
<!-- Self-hosted xterm.js — eliminates CDN DNS/TLS latency (~100ms).
'defer' preserves execution order (xterm loads before fit addon). -->
<script defer src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.min.js"></script>
<script defer src="vendor/xterm.min.js"></script>
<script defer src="vendor/xterm-addon-fit.min.js"></script>
<!-- Synchronous mobile detection — runs before first paint to prevent panel flash -->
<script>if(window.innerWidth<768||(('ontouchstart' in window||navigator.maxTouchPoints>0)&&window.innerWidth<1024))document.documentElement.classList.add('mobile-init');</script>
<!-- Inline critical CSS for instant skeleton paint (before styles.css loads) -->
<style>
.loading-skeleton{display:flex;flex-direction:column;height:100vh;background:#0a0a0a}
.skeleton-header{height:40px;background:#111;border-bottom:1px solid #1a1a2e;display:flex;align-items:center;padding:0 12px}
.skeleton-brand{color:#60a5fa;font-size:14px;font-weight:600;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;opacity:.7}
.skeleton-tabs{display:flex;gap:4px;margin-left:16px}
.skeleton-tab{width:80px;height:24px;background:#1a1a2e;border-radius:4px}
.skeleton-terminal{flex:1;background:#0d0d0d}
.skeleton-toolbar{height:32px;background:#111;border-top:1px solid #1a1a2e}
.app-loaded .loading-skeleton{display:none}
</style>
</head>
<body>
<!-- Loading skeleton — visible instantly before CSS/JS loads, hidden by app-loaded class -->
<div class="loading-skeleton">
<div class="skeleton-header">
<span class="skeleton-brand">Claudeman</span>
<div class="skeleton-tabs"><div class="skeleton-tab"></div></div>
</div>
<div class="skeleton-terminal"></div>
<div class="skeleton-toolbar"></div>
</div>
<!-- Skip link for keyboard users -->
<a href="#terminalContainer" class="skip-link">Skip to terminal</a>
<div class="app">
+3 -3
View File
@@ -593,12 +593,12 @@ export class WebServer extends EventEmitter {
done();
});
// Serve static files with caching headers for immutable CDN-like assets
// Serve static files — versioned assets (?v=X) are immutable, cache aggressively
await this.app.register(fastifyStatic, {
root: join(__dirname, 'public'),
prefix: '/',
// Cache static assets for 1 hour (they change on deploy, not during session)
maxAge: '1h',
maxAge: '1y',
immutable: true,
});
// SSE endpoint for real-time updates