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 @@
- +