diff --git a/CLAUDE.md b/CLAUDE.md index eda8fefa..5c11bca6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `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.1450 (must match `package.json`) +**Version**: 0.1451 (must match `package.json`) ## Project Overview @@ -24,7 +24,7 @@ Claudeman is a Claude Code session manager with web interface and autonomous Ral **Tech Stack**: TypeScript (ES2022/NodeNext, strict mode), Node.js, Fastify, node-pty, xterm.js -**TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false` +**TypeScript Strictness** (see `tsconfig.json`): `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `allowUnreachableCode: false`, `allowUnusedLabels: false`. Note: `src/tui` is excluded from compilation (legacy/deprecated code path). **Requirements**: Node.js 18+, Claude CLI, GNU Screen diff --git a/package.json b/package.json index fb6951c3..797f8d63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1450", + "version": "0.1451", "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", diff --git a/src/subagent-watcher.ts b/src/subagent-watcher.ts index 45fe5d04..cd0ce5e0 100644 --- a/src/subagent-watcher.ts +++ b/src/subagent-watcher.ts @@ -133,8 +133,9 @@ const MAX_TRACKED_AGENTS = 500; // Maximum agents to track (LRU eviction when ex const INTERNAL_AGENT_PATTERNS = [ /^\[?SUGGESTION MODE/i, // Claude Code's internal suggestion mode /^Suggest what user might/i, // Suggestion mode prompt variant - /^aprompt$/i, // Internal prompt agent + /aprompt/i, // Internal prompt agent (anywhere in string) /^a\s?prompt/i, // Variants of internal prompt agent + /^prompt$/i, // Just "prompt" ]; // Minimum description length - very short descriptions are likely internal or malformed diff --git a/src/web/public/app.js b/src/web/public/app.js index 26a0a247..54525a5a 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -178,6 +178,156 @@ function getEventCoords(e) { return { clientX: e.clientX, clientY: e.clientY }; } +// ============================================================================ +// Mobile Keyboard Handler +// ============================================================================ + +/** + * KeyboardHandler - Detects virtual keyboard visibility and scrolls inputs into view. + * Uses visualViewport API (works on iOS Safari and Android Chrome). + * Sets CSS custom property --keyboard-height and toggles .keyboard-visible class. + */ +const KeyboardHandler = { + initialViewportHeight: null, + isKeyboardVisible: false, + keyboardHeight: 0, + resizeHandler: null, + scrollHandler: null, + + /** Initialize keyboard detection */ + init() { + // Only initialize on touch devices + if (!MobileDetection.isTouchDevice()) return; + + const vv = window.visualViewport; + if (!vv) return; + + // Store initial height (update on orientation change) + this.initialViewportHeight = vv.height; + + // Create bound handlers for cleanup + this.resizeHandler = this.handleResize.bind(this); + this.scrollHandler = this.handleVisualViewportScroll.bind(this); + + // Listen for visualViewport resize (keyboard open/close) + vv.addEventListener('resize', this.resizeHandler); + vv.addEventListener('scroll', this.scrollHandler); + + // Update initial height on orientation change + window.addEventListener('orientationchange', () => { + setTimeout(() => { + if (window.visualViewport) { + this.initialViewportHeight = window.visualViewport.height; + } + }, 200); + }); + + // Focus event handling for scrolling inputs into view + document.addEventListener('focusin', this.handleFocusIn.bind(this)); + }, + + /** Handle visualViewport resize - detect keyboard */ + handleResize() { + const vv = window.visualViewport; + if (!vv) return; + + const heightDiff = this.initialViewportHeight - vv.height; + const threshold = 150; // Minimum height difference to consider keyboard open + + if (heightDiff > threshold) { + this.updateKeyboardState(true, heightDiff); + } else { + this.updateKeyboardState(false, 0); + } + }, + + /** Handle visualViewport scroll - keep fixed elements positioned on iOS */ + handleVisualViewportScroll() { + const vv = window.visualViewport; + if (!vv || !this.isKeyboardVisible) return; + + // Update CSS property for any elements that need to track viewport offset + document.documentElement.style.setProperty('--viewport-offset-top', `${vv.offsetTop}px`); + }, + + /** Update keyboard visibility state */ + updateKeyboardState(visible, height) { + if (this.isKeyboardVisible === visible && this.keyboardHeight === height) return; + + this.isKeyboardVisible = visible; + this.keyboardHeight = height; + + // Update CSS custom property + document.documentElement.style.setProperty('--keyboard-height', `${height}px`); + + // Toggle class for CSS targeting + document.body.classList.toggle('keyboard-visible', visible); + + // Dispatch custom event for any listeners + window.dispatchEvent(new CustomEvent('keyboardchange', { + detail: { visible, height } + })); + }, + + /** Handle focus on input elements - scroll into view after keyboard appears */ + handleFocusIn(e) { + const target = e.target; + if (!this.isInputElement(target)) return; + + // Wait for keyboard to appear before scrolling + setTimeout(() => { + this.scrollInputIntoView(target); + }, 350); + }, + + /** Check if element is an input that triggers keyboard */ + isInputElement(el) { + if (!el) return false; + const tagName = el.tagName?.toLowerCase(); + return ( + tagName === 'input' || + tagName === 'textarea' || + tagName === 'select' || + el.isContentEditable + ); + }, + + /** Scroll input into view above the keyboard */ + scrollInputIntoView(input) { + const vv = window.visualViewport; + + // Find scrollable container (modal-body or main viewport) + const modalBody = input.closest('.modal-body'); + const scrollContainer = modalBody || document.scrollingElement || document.body; + + if (vv) { + const rect = input.getBoundingClientRect(); + const visibleHeight = vv.height; + const inputBottom = rect.bottom; + const inputTop = rect.top; + + // If input is in bottom 40% of visible area or below it, scroll up + if (inputBottom > visibleHeight * 0.6 || inputTop > visibleHeight * 0.5) { + // For modal body, scroll within the modal + if (modalBody) { + const inputOffsetTop = input.offsetTop; + const targetScroll = inputOffsetTop - (visibleHeight * 0.3); + modalBody.scrollTo({ + top: Math.max(0, targetScroll), + behavior: 'smooth' + }); + } else { + // For page-level scroll + input.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + } + } else { + // Fallback for browsers without visualViewport + input.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + } +}; + // ============================================================================ // Accessibility: Focus Trap for Modals // ============================================================================ @@ -884,6 +1034,8 @@ class ClaudemanApp { init() { // Initialize mobile detection first (adds device classes to body) MobileDetection.init(); + // Initialize keyboard handler for mobile input visibility + KeyboardHandler.init(); this.initTerminal(); this.loadFontSize(); this.applyHeaderVisibilitySettings(); diff --git a/src/web/public/index.html b/src/web/public/index.html index 49b5e213..ddcc210a 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -2,7 +2,7 @@ - + Claudeman diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 083deecb..9563fc62 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -7879,6 +7879,17 @@ kbd { .project-insights-panel { display: none; } + + /* Tablet Keyboard Input Visibility */ + input, textarea, select, [contenteditable] { + scroll-margin-bottom: 200px; + scroll-margin-top: 80px; + } + + .keyboard-visible .modal-body { + max-height: calc(60vh - var(--keyboard-height) / 2); + overflow-y: auto; + } } /* ============================================================================ @@ -7957,6 +7968,7 @@ kbd { /* App container - prevent scrolling, strict layout */ .app { overflow: hidden; + position: relative; } /* Ultra-compact session tabs */ @@ -8015,21 +8027,25 @@ kbd { height: 14px; } - /* Clean dark toolbar - simple black/gray with white text */ + /* Clean dark toolbar - overlaps terminal to hide Claude's status/input */ .toolbar { + position: absolute; + bottom: calc(30px + var(--safe-area-bottom)); + left: 0; + right: 0; height: 40px; min-height: 40px; max-height: 40px; padding: 0.3rem 0.4rem; padding-left: calc(0.4rem + var(--safe-area-left)); padding-right: calc(0.4rem + var(--safe-area-right)); - padding-bottom: calc(0.3rem + var(--safe-area-bottom)); flex-wrap: nowrap; gap: 0.3rem; align-items: center; justify-content: space-between; background: #111111; border-top: 1px solid rgba(255, 255, 255, 0.1); + z-index: 50; } /* Show case selector in center */ @@ -8389,11 +8405,55 @@ kbd { padding-right: var(--safe-area-right); padding-bottom: var(--safe-area-bottom); } + + /* ------------------------------------------------------------------------- + Mobile Keyboard Input Visibility + ------------------------------------------------------------------------- */ + + /* Scroll margin for inputs - ensures they scroll above keyboard */ + input, textarea, select, [contenteditable] { + scroll-margin-bottom: 250px; + scroll-margin-top: 100px; + } + + /* Modal adjustments when keyboard is visible */ + .keyboard-visible .modal-content { + /* Don't take full height - resize with keyboard */ + height: auto; + max-height: calc(100dvh - var(--keyboard-height) - var(--safe-area-top)); + /* Anchor to top */ + top: var(--safe-area-top); + bottom: auto; + } + + .keyboard-visible .modal-body { + /* Limit modal body height when keyboard up - make room for header/footer */ + max-height: calc(50dvh - var(--keyboard-height) / 3); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + /* Ensure modal footer stays visible above keyboard */ + .keyboard-visible .modal-footer, + .keyboard-visible .wizard-footer, + .keyboard-visible .form-actions { + padding-bottom: 0.5rem; + } + + /* Smooth transition for keyboard appearance */ + .modal-content { + transition: max-height 0.2s ease-out, top 0.2s ease-out; + } } /* ============================================================================ Virtual Keyboard Handling ============================================================================ */ +:root { + --keyboard-height: 0px; + --viewport-offset-top: 0px; +} + .keyboard-visible .toolbar { display: none; }