/** * @fileoverview Shared constants, utility functions, and SSE event type registry for all frontend modules. * * This is the first script loaded in index.html. Every other frontend module depends on the * globals defined here: timing constants, Z-index layers, respawn * preset definitions, the SSE_EVENTS registry, and shared utilities (escapeHtml, * getEventCoords, scheduleBackground, urlBase64ToUint8Array). * * @globals {function} urlBase64ToUint8Array - VAPID key conversion for Web Push * @globals {function} scheduleBackground - scheduler.postTask wrapper (background priority) * @globals {function} getEventCoords - Unified mouse/touch coordinate extractor * @globals {function} escapeHtml - XSS-safe HTML escaping * @globals {object} SSE_EVENTS - Centralized SSE event type constants (~73 event types) * @globals {Array} BUILTIN_RESPAWN_PRESETS - Built-in respawn configuration presets * * @dependency None (first in load order) * @loadorder 1 of 15 — constants.js → mobile-handlers.js → voice-input.js → notification-manager.js * → keyboard-accessory.js → input-cjk.js → app.js → terminal-ui.js → respawn-ui.js * → ralph-panel.js → settings-ui.js → panels-ui.js → session-ui.js → ralph-wizard.js * → api-client.js → subagent-windows.js */ // Codeman — Shared constants and utility functions for frontend modules // ═══════════════════════════════════════════════════════════════ // Web Push Utilities // ═══════════════════════════════════════════════════════════════ /** Convert a base64-encoded VAPID key to Uint8Array for pushManager.subscribe() */ function urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); const rawData = atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } // ═══════════════════════════════════════════════════════════════ // Constants // ═══════════════════════════════════════════════════════════════ // Default terminal scrollback (can be changed via settings) const DEFAULT_SCROLLBACK = 50000; // Timing constants const STUCK_THRESHOLD_DEFAULT_MS = 600000; // 10 minutes - default for stuck detection const GROUPING_TIMEOUT_MS = 5000; // 5 seconds - notification grouping window const NOTIFICATION_LIST_CAP = 100; // Max notifications in list const TITLE_FLASH_INTERVAL_MS = 1500; // Title flash rate const BROWSER_NOTIF_RATE_LIMIT_MS = 3000; // Rate limit for browser notifications const AUTO_CLOSE_NOTIFICATION_MS = 8000; // Auto-close browser notifications const THROTTLE_DELAY_MS = 100; // General UI throttle delay const TERMINAL_CHUNK_SIZE = 32 * 1024; // 32KB chunks for terminal buffer loading const TERMINAL_TAIL_SIZE = 1024 * 1024; // 1MB tail for initial load (more scrollback on tab switch) const SYNC_WAIT_TIMEOUT_MS = 50; // Wait timeout for terminal sync const STATS_POLLING_INTERVAL_MS = 2000; // System stats polling // Z-index base values for layered floating windows const ZINDEX_SUBAGENT_BASE = 1000; const ZINDEX_PLAN_SUBAGENT_BASE = 1100; const ZINDEX_LOG_VIEWER_BASE = 2000; const ZINDEX_IMAGE_POPUP_BASE = 3000; // Subagent/floating window layout const WINDOW_INITIAL_TOP_PX = 120; const WINDOW_CASCADE_OFFSET_PX = 30; const WINDOW_MIN_WIDTH_PX = 200; const WINDOW_MIN_HEIGHT_PX = 200; const WINDOW_DEFAULT_WIDTH_PX = 300; // Scheduler API — prioritize terminal writes over background UI updates. // scheduler.postTask('background') defers non-critical work (connection lines, panel renders) // so the main thread stays free for terminal rendering at 60fps. const _hasScheduler = typeof globalThis.scheduler?.postTask === 'function'; function scheduleBackground(fn) { if (_hasScheduler) { scheduler.postTask(fn, { priority: 'background' }); } else { requestAnimationFrame(fn); } } // DEC mode 2026 marker stripping — xterm.js 6.0 handles sync natively, // but server-sent terminal buffers may still contain markers from Claude CLI. const DEC_SYNC_STRIP_RE = /\x1b\[\?2026[hl]/g; // Built-in respawn configuration presets const BUILTIN_RESPAWN_PRESETS = [ { id: 'solo-work', name: 'Solo', description: 'Claude working alone — fast respawn cycles with context reset', config: { idleTimeoutMs: 3000, updatePrompt: 'summarize your progress so far before the context reset.', interStepDelayMs: 2000, sendClear: true, sendInit: true, kickstartPrompt: 'continue working. Pick up where you left off based on the context above.', autoAcceptPrompts: true, }, durationMinutes: 60, builtIn: true, createdAt: 0, }, { id: 'subagent-workflow', name: 'Subagents', description: 'Lead session with Task tool subagents — longer idle tolerance', config: { idleTimeoutMs: 45000, updatePrompt: 'check on your running subagents and summarize their results before the context reset. If all subagents have finished, note what was completed and what remains.', interStepDelayMs: 3000, sendClear: true, sendInit: true, kickstartPrompt: 'check on your running subagents and continue coordinating their work. If all subagents have finished, summarize their results and proceed with the next step.', autoAcceptPrompts: true, }, durationMinutes: 240, builtIn: true, createdAt: 0, }, { id: 'team-lead', name: 'Team', description: 'Leading an agent team via TeamCreate — tolerates long silences', config: { idleTimeoutMs: 90000, updatePrompt: 'review the task list and teammate progress. Summarize the current state before the context reset.', interStepDelayMs: 5000, sendClear: true, sendInit: true, kickstartPrompt: 'check on your teammates by reviewing the task list and any messages in your inbox. Assign new tasks if teammates are idle, or continue coordinating the team effort.', autoAcceptPrompts: true, }, durationMinutes: 480, builtIn: true, createdAt: 0, }, { id: 'ralph-todo', name: 'Ralph/Todo', description: 'Ralph Loop task list — works through todos with progress tracking', config: { idleTimeoutMs: 8000, updatePrompt: 'update CLAUDE.md with discoveries and progress notes, mark completed tasks in @fix_plan.md, write a brief summary so the next cycle can continue seamlessly.', interStepDelayMs: 3000, sendClear: true, sendInit: true, kickstartPrompt: 'read @fix_plan.md for task status, continue on the next uncompleted task. When ALL tasks are complete, output COMPLETE.', autoAcceptPrompts: true, }, durationMinutes: 480, builtIn: true, createdAt: 0, }, { id: 'overnight-autonomous', name: 'Overnight', description: 'Unattended overnight runs with full context reset between cycles', config: { idleTimeoutMs: 10000, updatePrompt: 'summarize what you accomplished so far and write key progress notes to CLAUDE.md so the next cycle can pick up where you left off.', interStepDelayMs: 3000, sendClear: true, sendInit: true, kickstartPrompt: 'continue working on the task. Pick up where you left off based on the context above.', autoAcceptPrompts: true, }, durationMinutes: 480, builtIn: true, createdAt: 0, }, ]; // ═══════════════════════════════════════════════════════════════ // SSE Event Types // ═══════════════════════════════════════════════════════════════ /** @type {Record} Centralized SSE event type constants */ const SSE_EVENTS = { // Core INIT: 'init', // Session lifecycle SESSION_CREATED: 'session:created', SESSION_UPDATED: 'session:updated', SESSION_DELETED: 'session:deleted', SESSION_TERMINAL: 'session:terminal', SESSION_NEEDS_REFRESH: 'session:needsRefresh', SESSION_CLEAR_TERMINAL: 'session:clearTerminal', SESSION_COMPLETION: 'session:completion', SESSION_ERROR: 'session:error', SESSION_EXIT: 'session:exit', SESSION_IDLE: 'session:idle', SESSION_WORKING: 'session:working', SESSION_AUTO_CLEAR: 'session:autoClear', SESSION_CLI_INFO: 'session:cliInfo', // Scheduled runs SCHEDULED_CREATED: 'scheduled:created', SCHEDULED_UPDATED: 'scheduled:updated', SCHEDULED_COMPLETED: 'scheduled:completed', SCHEDULED_STOPPED: 'scheduled:stopped', // Respawn RESPAWN_STARTED: 'respawn:started', RESPAWN_STOPPED: 'respawn:stopped', RESPAWN_STATE_CHANGED: 'respawn:stateChanged', RESPAWN_CYCLE_STARTED: 'respawn:cycleStarted', RESPAWN_BLOCKED: 'respawn:blocked', RESPAWN_AUTO_ACCEPT_SENT: 'respawn:autoAcceptSent', RESPAWN_DETECTION_UPDATE: 'respawn:detectionUpdate', RESPAWN_TIMER_STARTED: 'respawn:timerStarted', RESPAWN_TIMER_CANCELLED: 'respawn:timerCancelled', RESPAWN_TIMER_COMPLETED: 'respawn:timerCompleted', RESPAWN_ERROR: 'respawn:error', RESPAWN_ACTION_LOG: 'respawn:actionLog', // Tasks TASK_CREATED: 'task:created', TASK_COMPLETED: 'task:completed', TASK_FAILED: 'task:failed', TASK_UPDATED: 'task:updated', // Mux (tmux) MUX_CREATED: 'mux:created', MUX_KILLED: 'mux:killed', MUX_DIED: 'mux:died', MUX_STATS_UPDATED: 'mux:statsUpdated', // Ralph SESSION_RALPH_LOOP_UPDATE: 'session:ralphLoopUpdate', SESSION_RALPH_TODO_UPDATE: 'session:ralphTodoUpdate', SESSION_RALPH_COMPLETION_DETECTED: 'session:ralphCompletionDetected', SESSION_RALPH_STATUS_UPDATE: 'session:ralphStatusUpdate', SESSION_CIRCUIT_BREAKER_UPDATE: 'session:circuitBreakerUpdate', SESSION_EXIT_GATE_MET: 'session:exitGateMet', // Bash tools SESSION_BASH_TOOL_START: 'session:bashToolStart', SESSION_BASH_TOOL_END: 'session:bashToolEnd', SESSION_BASH_TOOLS_UPDATE: 'session:bashToolsUpdate', // Hooks (Claude Code hook events) HOOK_IDLE_PROMPT: 'hook:idle_prompt', HOOK_PERMISSION_PROMPT: 'hook:permission_prompt', HOOK_ELICITATION_DIALOG: 'hook:elicitation_dialog', HOOK_STOP: 'hook:stop', HOOK_TEAMMATE_IDLE: 'hook:teammate_idle', HOOK_TASK_COMPLETED: 'hook:task_completed', // Subagents (Claude Code background agents) SUBAGENT_DISCOVERED: 'subagent:discovered', SUBAGENT_UPDATED: 'subagent:updated', SUBAGENT_TOOL_CALL: 'subagent:tool_call', SUBAGENT_PROGRESS: 'subagent:progress', SUBAGENT_MESSAGE: 'subagent:message', SUBAGENT_TOOL_RESULT: 'subagent:tool_result', SUBAGENT_COMPLETED: 'subagent:completed', // Images IMAGE_DETECTED: 'image:detected', // Tunnel TUNNEL_STARTED: 'tunnel:started', TUNNEL_STOPPED: 'tunnel:stopped', TUNNEL_PROGRESS: 'tunnel:progress', TUNNEL_ERROR: 'tunnel:error', TUNNEL_QR_ROTATED: 'tunnel:qrRotated', TUNNEL_QR_REGENERATED: 'tunnel:qrRegenerated', TUNNEL_QR_AUTH_USED: 'tunnel:qrAuthUsed', // Plan orchestration PLAN_SUBAGENT: 'plan:subagent', PLAN_PROGRESS: 'plan:progress', PLAN_STARTED: 'plan:started', PLAN_CANCELLED: 'plan:cancelled', PLAN_COMPLETED: 'plan:completed', // Orchestrator Loop ORCHESTRATOR_STATE_CHANGED: 'orchestrator:stateChanged', ORCHESTRATOR_PLAN_PROGRESS: 'orchestrator:planProgress', ORCHESTRATOR_PLAN_READY: 'orchestrator:planReady', ORCHESTRATOR_PHASE_STARTED: 'orchestrator:phaseStarted', ORCHESTRATOR_PHASE_COMPLETED: 'orchestrator:phaseCompleted', ORCHESTRATOR_PHASE_FAILED: 'orchestrator:phaseFailed', ORCHESTRATOR_VERIFICATION: 'orchestrator:verification', ORCHESTRATOR_TASK_ASSIGNED: 'orchestrator:taskAssigned', ORCHESTRATOR_TASK_COMPLETED: 'orchestrator:taskCompleted', ORCHESTRATOR_TASK_FAILED: 'orchestrator:taskFailed', ORCHESTRATOR_COMPLETED: 'orchestrator:completed', ORCHESTRATOR_ERROR: 'orchestrator:error', // Clipboard CLIPBOARD_WRITE: 'clipboard:write', // Cases CASE_CREATED: 'case:created', CASE_LINKED: 'case:linked', CASE_DELETED: 'case:deleted', CASE_ORDER_CHANGED: 'case:order-changed', }; // ═══════════════════════════════════════════════════════════════ // Utility Functions // ═══════════════════════════════════════════════════════════════ /** * Get unified coordinates from mouse or touch event. * @param {MouseEvent|TouchEvent} e - The event * @returns {{ clientX: number, clientY: number }} Coordinates */ function getEventCoords(e) { if (e.touches && e.touches.length > 0) { return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }; } if (e.changedTouches && e.changedTouches.length > 0) { return { clientX: e.changedTouches[0].clientX, clientY: e.changedTouches[0].clientY }; } return { clientX: e.clientX, clientY: e.clientY }; } // HTML escape utility (shared by NotificationManager, CodemanApp, and ralph-wizard.js) const _htmlEscapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; const _htmlEscapePattern = /[&<>"']/g; function escapeHtml(text) { if (typeof text !== 'string') return ''; return text.replace(_htmlEscapePattern, (ch) => _htmlEscapeMap[ch]); }