# Phase 5: Frontend Modularization — Implementation Plan **Date**: 2026-03-01 **Prerequisites**: Phase 1-4 complete (utils cleanup, CleanupManager/Debouncer, route extraction, domain splitting) **Goal**: Split the 15,196-line `app.js` monolith into focused, independently-loadable modules while preserving the proven `Object.assign(CodemanApp.prototype, ...)` pattern from `ralph-wizard.js`. --- ## Table of Contents 1. [Current State Analysis](#1-current-state-analysis) 2. [Architectural Decisions](#2-architectural-decisions) 3. [Module Dependency Graph](#3-module-dependency-graph) 4. [Step 1: Extract Constants & Utilities](#step-1-extract-constants--utilities) 5. [Step 2: Extract Mobile Handlers](#step-2-extract-mobile-handlers) 6. [Step 3: Extract Voice Input](#step-3-extract-voice-input) 7. [Step 4: Extract NotificationManager](#step-4-extract-notificationmanager) 8. [Step 5: Extract FocusTrap & KeyboardAccessoryBar](#step-5-extract-focustrap--keyboardaccessorybar) 9. [Step 6: Replace Inlined xterm-zerolag-input](#step-6-replace-inlined-xterm-zerolag-input) 10. [Step 7: Create ApiClient Wrapper](#step-7-create-apiclient-wrapper) 11. [Step 8: Extract Subagent Window Manager](#step-8-extract-subagent-window-manager) 12. [Execution Order & Dependencies](#execution-order--dependencies) 13. [Validation Checklist](#validation-checklist) 14. [Risk Assessment](#risk-assessment) --- ## Safety Constraints Before starting ANY work, read and follow these rules: 1. **Never run `npx vitest run`** (full suite) — it kills tmux sessions. You are running inside a Codeman-managed tmux session. 2. **Never test on port 3000** — the live dev server runs there. Tests use ports 3150+. 3. **After TypeScript changes**: Run `tsc --noEmit` to verify type checking passes. 4. **Before considering done**: Run `npm run lint` and `npm run format:check` to ensure CI passes. 5. **Never kill tmux sessions** — check `echo $CODEMAN_MUX` first. 6. **Frontend changes require browser verification**: After each step, use Playwright to load the page and verify the UI renders. Use `waitUntil: 'domcontentloaded'` (not `networkidle` — SSE keeps the connection open). Wait 3-4s for polling/async data to populate, then check element visibility, text content, and CSS values. 7. **Cache busting**: Update the `?v=` query strings in `index.html` for any modified/new JS files after each step. 8. **Verify dev server**: `npx tsx src/index.ts web --port 3099 &` on a non-production port, confirm `curl -s http://localhost:3099/api/status | jq .status` returns `"ok"`, then kill the background process. --- ## 1. Current State Analysis ### File Structure (Before) ``` src/web/public/ ├── app.js (15,196 lines — EVERYTHING) ├── ralph-wizard.js (1,037 lines — already extracted) ├── index.html (1,040 lines) ├── styles.css (main styling) ├── mobile.css (responsive overrides) ├── sw.js (service worker) ├── manifest.json (PWA manifest) ├── upload.html (screenshot upload) └── vendor/ (xterm.js + addons) ``` ### app.js Section Map | Section | Lines | Size | Standalone? | |---------|-------|------|-------------| | Web Push utils (`urlBase64ToUint8Array`), `scheduleBackground`, constants | 1-159 | 159 | Yes | | `MobileDetection` object | 168-271 | ~104 | Yes | | `getEventCoords()` function | 273-285 | 13 | Yes | | `KeyboardHandler` object | 292-567 | ~276 | No (refs `app`, `KeyboardAccessoryBar`, `MobileDetection`) | | `SwipeHandler` object | 569-628 | ~60 | No (refs `app`) | | `DeepgramProvider` object | 631-841 | 211 | Yes (WebSocket-only) | | `VoiceInput` object | 842-1479 | 638 | No (refs `app`, `DeepgramProvider`) | | `KeyboardAccessoryBar` object | 1480-1688 | ~209 | No (refs `app`, `MobileDetection`) | | `FocusTrap` class | 1689-1748 | 60 | Yes | | xterm-zerolag-input (inlined copy) | 1756-2153 | ~398 | No (refs xterm internals) | | `extractSyncSegments()` | 2173-2215 | 43 | Yes | | `NotificationManager` class | 2218-2663 | ~446 | No (refs `app`, `MobileDetection`; has own `escapeHtml` method) | | **`CodemanApp` class | 2665-15176 | **12,512** | No (depends on all above) | | Initialization + export | 15178-15196 | 19 | — | ### Cross-Reference Pattern All top-level objects use `typeof X !== 'undefined'` guards for optional dependencies. This pattern enables **graceful degradation** if a module fails to load and must be preserved in extracted files. ```javascript // Pattern used everywhere — must be preserved: if (typeof app !== 'undefined') app.someMethod(); if (typeof KeyboardAccessoryBar !== 'undefined') KeyboardAccessoryBar.show(); if (typeof MobileDetection !== 'undefined' && MobileDetection.isTouchDevice()) { ... } ``` ### escapeHtml — Two Independent Copies There are **two** `escapeHtml` methods in app.js: 1. **`NotificationManager.escapeHtml()`** (line 2659) — instance method, used internally by `NotificationManager` at lines 2461-2467 2. **`CodemanApp.escapeHtml()`** (line 15172) — instance method with static lookup maps (`_htmlEscapeMap`, `_htmlEscapePattern`) for performance. Used ~70 times throughout CodemanApp, and by `ralph-wizard.js` (3 calls via `this.escapeHtml`) These are independent. Each class owns its own copy. The plan extracts a shared global `escapeHtml()` in `constants.js` and updates both classes + `ralph-wizard.js` to use it. ### Proven Extraction Pattern (ralph-wizard.js) The `ralph-wizard.js` extraction established the pattern all further extractions will follow: ```javascript // File loaded after app.js via ``` ### Verification 1. All constant names are already globals — no reference changes needed in app.js for constants 2. `escapeHtml` usage changes from `this.escapeHtml()` → `escapeHtml()` (global) in CodemanApp, NotificationManager, and ralph-wizard.js 3. `extractSyncSegments` is already called as a global function — no change needed 4. Browser test: load page, verify terminal renders, tabs work, notifications fire --- ## Step 2: Extract Mobile Handlers **New file**: `src/web/public/mobile-handlers.js` **Lines moved from app.js**: 168-628 (`MobileDetection`, `KeyboardHandler`, `SwipeHandler`) **Estimated size**: ~460 lines **Time**: ~2 hours ### What Moves Three related mobile objects that form a cohesive unit: ```javascript // mobile-handlers.js — Device detection, keyboard handling, and swipe navigation const MobileDetection = { isTouchDevice() { ... }, isIOS() { ... }, isSafari() { ... }, isSmallScreen() { ... }, isMediumScreen() { ... }, getDeviceType() { ... }, updateBodyClass() { ... }, init() { ... }, cleanup() { ... } }; const KeyboardHandler = { lastViewportHeight: 0, keyboardVisible: false, initialViewportHeight: 0, init() { ... }, cleanup() { ... }, handleViewportResize() { ... }, updateLayoutForKeyboard() { ... }, resetLayout() { ... }, onKeyboardShow() { ... }, onKeyboardHide() { ... }, _sendTerminalResize() { ... }, isInputElement(el) { ... }, scrollInputIntoView(input) { ... } }; const SwipeHandler = { startX: 0, startY: 0, startTime: 0, minSwipeDistance: 80, maxSwipeTime: 300, maxVerticalDrift: 100, init() { ... }, onTouchStart(e) { ... }, onTouchEnd(e) { ... } }; ``` ### Cross-Reference Analysis | Object | References to... | How to handle | |--------|-----------------|---------------| | `MobileDetection` | None (standalone) | Works as-is | | `KeyboardHandler` | `MobileDetection` (direct calls) | Guaranteed loaded before (same file) | | `KeyboardHandler` | `typeof app !== 'undefined'` (6 guards) | Preserved as-is — `app` defined later | | `KeyboardHandler` | `typeof KeyboardAccessoryBar !== 'undefined'` (2 guards) | Preserved — `KeyboardAccessoryBar` defined in later file, guards handle deferral | | `SwipeHandler` | `MobileDetection.isTouchDevice()` (1 call) | Same file, no issue | | `SwipeHandler` | `typeof app !== 'undefined'` (2 guards) | Preserved as-is | ### What Changes in app.js 1. **Delete lines 160-628** (MobileDetection section header through end of SwipeHandler) 2. `getEventCoords()` (lines 273-285) already moved to `constants.js` in Step 1 — verify deleted 3. No reference changes needed — all three objects are already globals accessed by name ### What Changes in index.html ```html ``` ### CodemanApp.init() Update The `init()` method initializes mobile handlers. Verify this code path still works: ```javascript // In app.js CodemanApp.init(): MobileDetection.init(); // Now global from mobile-handlers.js KeyboardHandler.init(); // Now global from mobile-handlers.js SwipeHandler.init(); // Now global from mobile-handlers.js ``` These are already called as globals, so no changes needed. ### Verification 1. Load page on desktop — verify MobileDetection sets correct body classes 2. Load page on mobile viewport (DevTools device mode, ≤768px) — verify: - Touch detection works - Keyboard show/hide repositions toolbar - Swipe left/right switches sessions 3. Check that `window.MobileDetection` is still set (line 15196 of app.js). `MobileDetection` is now a global from `mobile-handlers.js` — the assignment still works. --- ## Step 3: Extract Voice Input **New file**: `src/web/public/voice-input.js` **Lines moved from app.js**: 631-1479 (`DeepgramProvider`, `VoiceInput`) **Estimated size**: ~850 lines **Time**: ~3 hours ### What Moves ```javascript // voice-input.js — Multi-provider voice input (Deepgram Nova-3 + Web Speech API) const DeepgramProvider = { _ws: null, _mediaRecorder: null, _stream: null, // ... 200+ lines async start(opts) { ... }, stop() { ... }, cleanup() { ... } }; const VoiceInput = { recognition: null, isRecording: false, supported: false, _activeProvider: null, // ... 630+ lines init() { ... }, toggle() { ... }, start() { ... }, stop() { ... }, _startDeepgram() { ... }, _startWebSpeech() { ... }, // Audio level meter, config persistence, UI updates cleanup() { ... } }; ``` ### Cross-Reference Analysis | Object | References to... | How to handle | |--------|-----------------|---------------| | `DeepgramProvider` | None (standalone WebSocket) | Works as-is | | `VoiceInput` | `DeepgramProvider` (direct calls) | Same file — guaranteed available | | `VoiceInput` | `typeof app !== 'undefined'` (~15 guards) | Preserved — `app` defined later | **Note**: VoiceInput does NOT reference `MobileDetection`. Its only external dependency is `app` (via `typeof` guards) and `DeepgramProvider` (same file). ### What Changes in app.js 1. **Delete lines 631-1479** (DeepgramProvider + VoiceInput sections) 2. No reference changes — `VoiceInput` and `DeepgramProvider` are accessed as globals throughout app.js ### What Changes in index.html ```html ``` ### Important: VoiceInput ↔ CodemanApp References VoiceInput accesses `app` through `typeof app !== 'undefined'` guards in these cases: - `app.activeSessionId` — to know which session receives voice input - `app.terminal` — to focus terminal after voice input - `app._sendInputAsync()` — to send transcribed text - `app.notificationManager` — to show voice-related notifications - `app.sessions.get()` — to check session state All of these use the existing guard pattern and will continue to work because `app` is initialized in `app.js` which loads after `voice-input.js`. The guards handle the case where `VoiceInput.init()` is called before `app` exists (which doesn't happen in practice, but is safe regardless). ### Verification 1. Load page — verify no console errors related to voice/Deepgram 2. If Deepgram API key is configured in settings, test voice toggle 3. Verify `typeof VoiceInput !== 'undefined'` checks in app.js still resolve correctly --- ## Step 4: Extract NotificationManager **New file**: `src/web/public/notification-manager.js` **Lines moved from app.js**: 2218-2663 (`NotificationManager` class) **Estimated size**: ~450 lines **Time**: ~2 hours ### What Moves ```javascript // notification-manager.js — Multi-layer notification system // 5 layers: in-app drawer, tab flash, browser Notification API, Web Push, audio beep class NotificationManager { constructor(app) { this.app = app; this.notifications = []; this.unreadCount = 0; this.isTabVisible = !document.hidden; this.isDrawerOpen = false; this.originalTitle = document.title; this.preferences = this.loadPreferences(); // Visibility change listeners... } loadPreferences() { ... } // localStorage with v1→v4 migration savePreferences() { ... } notify({ urgency, category, sessionId, title, message }) { ... } _addNotification() { ... } _broadcastNotification() { ... } _flashTab() { ... } _showBrowserNotification() { ... } _playAudio() { ... } _sendPushNotification() { ... } onTabVisible() { ... } markAllRead() { ... } clearAll() { ... } removeNotification(id) { ... } getUnreadCount() { ... } relativeTime(ts) { ... } cleanup() { ... } // Note: escapeHtml() already removed in Step 1 — uses global escapeHtml() } ``` ### Cross-Reference Analysis | Reference | Direction | How to handle | |-----------|-----------|---------------| | `NotificationManager` → `this.app` | Constructor injection | Already uses `this.app` — works unchanged | | `NotificationManager` → `MobileDetection` | Direct calls (2 places: lines 2269, 2324) | `mobile-handlers.js` loaded first | | `CodemanApp` → `NotificationManager` | `this.notificationManager = new NotificationManager(this)` | Constructor creates instance — needs class available | | `NotificationManager` → `escapeHtml()` | Global function (4 calls) | Already migrated in Step 1 to `constants.js` | **Note**: NotificationManager does NOT reference VoiceInput. ### What Changes in app.js 1. **Delete lines 2218-2663** (NotificationManager class, including the `relativeTime` and old `escapeHtml` utilities at lines 2650-2662) 2. The CodemanApp constructor already creates the instance: ```javascript this.notificationManager = new NotificationManager(this); ``` This works because `notification-manager.js` loads before `app.js`. ### What Changes in index.html ```html ``` ### Verification 1. Load page — verify notification badge appears in header 2. Trigger a notification event (e.g., create a session, send input) — verify: - In-app notification appears in drawer - Tab title flashes when not focused - Browser notification appears if enabled 3. Open Settings → Notifications — verify preferences load/save correctly --- ## Step 5: Extract FocusTrap & KeyboardAccessoryBar **New file**: `src/web/public/keyboard-accessory.js` **Lines moved from app.js**: 1480-1748 (`KeyboardAccessoryBar`, `FocusTrap`) **Estimated size**: ~270 lines **Time**: ~1 hour ### Why Combine These Two? `FocusTrap` (60 lines) is too small for its own file but doesn't naturally belong in any other module. `KeyboardAccessoryBar` (200 lines) is mobile-focused but depends on `MobileDetection` (which is in `mobile-handlers.js`). Combining them creates a cohesive "keyboard interaction" module at a reasonable size. ### What Moves ```javascript // keyboard-accessory.js — Mobile keyboard accessory bar + modal focus trapping class FocusTrap { constructor(element) { ... } activate() { ... } deactivate() { ... } getFocusableElements() { ... } handleKeydown(e) { ... } } const KeyboardAccessoryBar = { element: null, _confirmTimer: null, _confirmAction: null, init() { ... }, // Creates DOM, mobile-only handleAction(action, btn) { ... }, setConfirm(action, btn) { ... }, clearConfirm() { ... }, sendCommand(command) { ... }, sendKey(escapeSequence) { ... }, pasteFromClipboard() { ... }, show() { ... }, hide() { ... } }; ``` ### Cross-Reference Analysis | Reference | Direction | How to handle | |-----------|-----------|---------------| | `FocusTrap` | Standalone | No dependencies | | `KeyboardAccessoryBar` → `MobileDetection` | Direct call in `init()` | `mobile-handlers.js` loaded first | | `KeyboardAccessoryBar` → `typeof app` | 8+ guards for sending input | `app` defined later | | `CodemanApp` → `FocusTrap` | `this.activeFocusTrap = new FocusTrap(el)` | Needs class available | | `CodemanApp` → `KeyboardAccessoryBar` | `KeyboardAccessoryBar.init()` in init() | Needs object available | | `KeyboardHandler` → `typeof KeyboardAccessoryBar` | 2 guards for show/hide | `keyboard-accessory.js` loaded after `mobile-handlers.js` — **LOAD ORDER MATTERS** | | `ralph-wizard.js` → `FocusTrap` | `new FocusTrap(modal)` (direct usage) | `keyboard-accessory.js` loaded before `ralph-wizard.js` | ### Critical: Load Order for KeyboardHandler ↔ KeyboardAccessoryBar `KeyboardHandler` (in `mobile-handlers.js`) references `typeof KeyboardAccessoryBar !== 'undefined'`. For this to resolve as `true`, `keyboard-accessory.js` must load BEFORE `mobile-handlers.js`... but `KeyboardAccessoryBar` depends on `MobileDetection` (in `mobile-handlers.js`). **This is a circular dependency!** **Resolution**: The `typeof` guard in `KeyboardHandler` already handles the case where `KeyboardAccessoryBar` doesn't exist yet. The guard runs at **runtime** (when `onKeyboardShow/Hide()` fires), not at **load time**. By the time the keyboard actually shows/hides (user interaction), all scripts have loaded. So the current load order works: ``` mobile-handlers.js → defines MobileDetection, KeyboardHandler (with typeof guards) keyboard-accessory.js → defines KeyboardAccessoryBar (MobileDetection available) app.js → calls KeyboardHandler.init(), KeyboardAccessoryBar.init() ``` When `KeyboardHandler.onKeyboardShow()` fires at runtime, `KeyboardAccessoryBar` is already defined. ### What Changes in app.js 1. **Delete lines 1480-1748** (KeyboardAccessoryBar + FocusTrap) ### What Changes in index.html ```html ``` ### Verification 1. Load page on mobile viewport — verify accessory bar appears when keyboard opens 2. Test arrow keys, /init, /clear, /compact, paste buttons in accessory bar 3. Open a modal (e.g., Settings) — verify Tab key cycles within modal (FocusTrap) 4. Open Ralph Wizard — verify focus trap works there too --- ## Step 6: Replace Inlined xterm-zerolag-input **Lines removed from app.js**: 1756-2153 (~398 lines of inlined overlay code) **New script tag**: Load built file from `packages/xterm-zerolag-input/` **Time**: ~1.5 hours ### Current Problem `app.js` contains a full copy of the `xterm-zerolag-input` package (7 helper functions + `ZerolagInputAddon` class + `LocalEchoOverlay` subclass). This code is also maintained in `packages/xterm-zerolag-input/src/`. Changes must be applied to both copies. ### Strategy Build the package and serve its output as a separate script, then remove the inlined copy from `app.js`. ### Steps 1. **Build the package**: ```bash cd packages/xterm-zerolag-input npm run build ``` This produces `dist/` output via tsup. 2. **Check build output format**: The package's `tsup.config.ts` builds for ESM/CJS. We need an IIFE or global build that exposes `LocalEchoOverlay` as a global. Options: - **Option A**: Add an IIFE build target to tsup config - **Option B**: Create a small wrapper script that imports from the built package - **Option C (simplest)**: Keep a thin wrapper in app.js that references the package's globals **Recommended: Option A** — Add IIFE build to `packages/xterm-zerolag-input/tsup.config.ts`: ```typescript export default defineConfig({ // ... existing config format: ['esm', 'cjs', 'iife'], globalName: 'XtermZerolagInput', }); ``` Then in the built IIFE file, `LocalEchoOverlay` would be available as `XtermZerolagInput.LocalEchoOverlay`. 3. **Copy built IIFE to vendor/**: ```bash cp packages/xterm-zerolag-input/dist/index.global.js src/web/public/vendor/xterm-zerolag-input.js ``` 4. **Add to index.html** (after xterm.js, before app.js): ```html ``` 5. **Update app.js**: Delete lines 1756-2153 (inlined copy). In CodemanApp where `LocalEchoOverlay` is instantiated, update the reference: ```javascript // Old: this._localEchoOverlay = new LocalEchoOverlay(this.terminal); // New (if IIFE exposes as global): this._localEchoOverlay = new (XtermZerolagInput?.LocalEchoOverlay || LocalEchoOverlay)(this.terminal); ``` Or simpler: ensure the IIFE wrapper also exposes `LocalEchoOverlay` as a direct global: ```javascript // At end of IIFE wrapper: window.LocalEchoOverlay = XtermZerolagInput.LocalEchoOverlay; window.ZerolagInputAddon = XtermZerolagInput.ZerolagInputAddon; ``` ### Important: Build Pipeline Integration After this step, the workflow for overlay changes becomes: 1. Edit source in `packages/xterm-zerolag-input/src/` 2. Run `npm run build` in the package directory 3. Copy built output to `src/web/public/vendor/` 4. Bump version query string in `index.html` Consider adding a `scripts/build-overlay.sh` script to automate steps 2-3. ### Alternative: Simpler Approach (Skip Build) If the IIFE build adds too much complexity, a simpler alternative is to keep the inlined code but add a comment marking it as generated: ```javascript // ============================================================================ // xterm-zerolag-input — AUTO-GENERATED from packages/xterm-zerolag-input/ // Do NOT edit directly. Modify packages/xterm-zerolag-input/src/ and copy. // ============================================================================ ``` This documents the duplication without adding build complexity. The actual code reduction (398 lines) is the same either way since the copy in `app.js` is removed. **Recommendation**: Go with the IIFE build approach. The 398-line reduction is worth the small build step addition. ### Verification 1. Load page — verify terminal renders 2. Type text — verify local echo overlay shows instant keystroke feedback 3. Type on mobile — verify overlay appears above keyboard 4. Verify no ghost artifacts when Claude is processing (overlay should hide) --- ## Step 7: Create ApiClient Wrapper **New file**: `src/web/public/api-client.js` **Pattern**: `Object.assign(CodemanApp.prototype, { ... })` **Estimated size**: ~150 lines **Time**: ~2 hours ### Problem ~98 `fetch()` calls scattered through CodemanApp with identical boilerplate: ```javascript // Repeated pattern: fetch(`/api/sessions/${sessionId}/something`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: value }) }).catch(() => {}); ``` ### What Moves Extract a centralized API helper and replace repetitive patterns. This doesn't move existing methods — it adds new helper methods and gradually migrates call sites. ```javascript // api-client.js — Centralized API fetch helper for CodemanApp Object.assign(CodemanApp.prototype, { /** * Send a JSON API request. Handles Content-Type, JSON serialization, and error swallowing. * @param {string} path - API path (e.g., '/api/sessions/123/input') * @param {object} [opts] - { method, body, signal } * @returns {Promise} Response or null on error */ async _api(path, opts = {}) { const { method = 'GET', body, signal } = opts; const fetchOpts = { method, signal }; if (body !== undefined) { fetchOpts.headers = { 'Content-Type': 'application/json' }; fetchOpts.body = JSON.stringify(body); } try { const res = await fetch(path, fetchOpts); return res; } catch { return null; } }, /** * Send a JSON API request and parse the response as JSON. * @param {string} path - API path * @param {object} [opts] - { method, body, signal } * @returns {Promise} Parsed JSON or null on error */ async _apiJson(path, opts = {}) { const res = await this._api(path, opts); if (!res || !res.ok) return null; try { return await res.json(); } catch { return null; } }, /** * POST JSON to an API endpoint (most common pattern). * @param {string} path - API path * @param {object} body - JSON body * @returns {Promise} */ async _apiPost(path, body) { return this._api(path, { method: 'POST', body }); }, /** * DELETE an API resource. * @param {string} path - API path * @returns {Promise} */ async _apiDelete(path) { return this._api(path, { method: 'DELETE' }); }, }); ``` ### Migration Strategy Do NOT replace all 98 fetch calls in this step. Instead: 1. Add the `_api`/`_apiJson`/`_apiPost`/`_apiDelete` helpers via `Object.assign` 2. Migrate **10 of the most repeated patterns** as proof of concept: - Session input: `_sendInputAsync()` - Session resize: in `KeyboardHandler._sendTerminalResize()` and `setupEventListeners()` - Session create: `quickStartSession()` - Session delete: `deleteSession()` - Respawn start/stop: `startRespawn()` / `stopRespawn()` - Settings save: `saveSettings()` - Ralph state: `sendRalphInput()` - Subagent kill: `killSubagent()` - File browse: `browseFiles()` - Hook response: `respondToHook()` 3. Leave remaining fetch calls for incremental migration in future PRs ### What Changes in index.html ```html ``` ### Why After app.js? `api-client.js` uses `Object.assign(CodemanApp.prototype, ...)`, which requires `CodemanApp` to be defined. Since `app.js` defines the class and immediately instantiates `const app = new CodemanApp()`, the prototype methods will be available on the `app` instance because JavaScript prototype lookup is dynamic. **However**: Methods added via `Object.assign` AFTER instantiation are still available because JS reads the prototype at call time, not at creation time. So this works: ```javascript // app.js: class CodemanApp { ... } const app = new CodemanApp(); // api-client.js (loaded after): Object.assign(CodemanApp.prototype, { _api() { ... } }); // Later: app._api('/api/status'); // ✓ Works — looks up prototype chain at call time ``` ### Verification 1. Load page — verify no console errors 2. Create a session — verify `quickStartSession()` works 3. Send input — verify text is delivered 4. Delete a session — verify it's removed 5. Open/close subagent windows — verify kill works --- ## Step 8: Extract Subagent Window Manager **New file**: `src/web/public/subagent-windows.js` **Pattern**: `Object.assign(CodemanApp.prototype, { ... })` **Estimated size**: ~700 lines **Time**: ~3 hours ### Problem Subagent window management is the largest self-contained system within CodemanApp. It handles: - Floating terminal windows with drag/resize - Parent-child connection lines (canvas/SVG) - Minimized state + badge indicators - Window Z-ordering and cascading layout - Mobile-specific stacking ### What Moves Extract all subagent window methods from CodemanApp. The actual method names and their locations in app.js: ```javascript // subagent-windows.js — Floating subagent terminal window management Object.assign(CodemanApp.prototype, { // Tab badge (minimized agents indicator) renderSubagentTabBadge(sessionId, minimizedAgents) { ... }, // line 5642 restoreMinimizedSubagent(agentId, sessionId) { ... }, // line 5800 permanentlyCloseMinimizedSubagent(agentId, sessionId) { ... }, // line 5908 // State persistence async saveSubagentWindowStates() { ... }, // line 9760 async restoreSubagentWindowStates() { ... }, // line 9834 // Connection lines (parent ↔ child visual links) updateConnectionLines() { ... }, // line 11998 _updateConnectionLinesImmediate() { ... }, // line 12010 // Window lifecycle openSubagentWindow(agentId) { ... }, // line 12274 (~280 LOC) closeSubagentWindow(agentId) { ... }, // line 12551 (~40 LOC) relayoutMobileSubagentWindows() { ... }, // line 12590 (~90 LOC) cleanupAllFloatingWindows() { ... }, // line 12680 (~170 LOC) // Shared drag utility (used by subagent + teammate + log viewer windows) makeWindowDraggable(win, handle) { ... }, // line 12849 (~50 LOC) }); ``` ### What Stays in app.js - The `subagentWindows`, `minimizedSubagents`, `subagentWindowZIndex` **properties** stay in the CodemanApp constructor (they are state, not methods) - SSE event handlers for `subagent:discovered`, `subagent:updated`, `subagent:completed` stay in `handleSSEEvent()` — they call into the extracted methods - The subagent panel rendering stays (it's part of the main UI, not the floating windows) ### Important: Non-contiguous Methods Unlike Steps 1-6 (which moved contiguous blocks), the subagent window methods are scattered across CodemanApp: - Tab badge methods: lines ~5642-5926 - State persistence: lines ~9760-9906 - Connection lines + window lifecycle: lines ~11998-12900 The extraction requires cutting from **3 separate regions** of app.js. Be careful to: 1. Cut each method completely (including any JSDoc comments above it) 2. Leave surrounding methods intact 3. Verify no shared local variables between cut and remaining methods ### `makeWindowDraggable` Dependency `makeWindowDraggable()` is used by subagent windows, teammate terminal windows (line 13298), and log viewer windows. If it moves to `subagent-windows.js`, it's still accessible via prototype — teammate/log-viewer code in `app.js` can call `this.makeWindowDraggable()` without issue. ### What Changes in index.html ```html ``` ### Verification 1. Start a session that spawns subagents — verify floating windows appear 2. Drag a window — verify smooth movement 3. Resize a window — verify constraints (min width/height) 4. Minimize a window — verify badge appears on session tab 5. Click badge — verify dropdown, click to restore 6. Switch sessions — verify windows for other session hide 7. Mobile viewport — verify windows stack from bottom when keyboard visible 8. Open a teammate terminal window — verify `makeWindowDraggable` still works from app.js --- ## Execution Order & Dependencies Execute steps in this order. Each step is independently deployable and verifiable. ``` Step 1: constants.js (self-contained, no deps) ↓ Step 2: mobile-handlers.js (depends on constants.js for getEventCoords) ↓ Step 3: voice-input.js (standalone; only needs `app` at runtime via typeof guard) ↓ Step 4: notification-manager.js (depends on mobile-handlers.js for MobileDetection) ↓ Step 5: keyboard-accessory.js (depends on mobile-handlers.js for MobileDetection) ↓ Step 6: xterm-zerolag-input (depends on vendor/xterm.js) ↓ Step 7: api-client.js (depends on CodemanApp — loads after app.js) ↓ Step 8: subagent-windows.js (depends on CodemanApp — loads after app.js) ``` **Parallelization opportunities**: - Steps 3 + 5 can run in parallel (no cross-dependencies) - Steps 4 + 5 can run in parallel (both depend on mobile-handlers but not each other) - Steps 7 + 8 can run in parallel (both extend CodemanApp prototype independently) ### index.html Final Script Order ```html ``` --- ## Validation Checklist After EACH step, verify: - [ ] Page loads without console errors - [ ] All `