mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 13:39:41 +02:00
Add a "Manage" tab to the create-case modal with up/down reorder buttons and delete for each case. Linked cases are unlinked (folder preserved); CASES_DIR cases are permanently deleted. Backend: - DELETE /api/cases/:name — unlink or delete - PUT /api/cases/order — persist ordering to settings.json - GET /api/cases now respects saved caseOrder Frontend: - Third "Manage" tab in createCaseModal with case list - Delete button in mobile case picker bottom sheet - SSE events: case:deleted, case:order-changed
328 lines
14 KiB
JavaScript
328 lines
14 KiB
JavaScript
/**
|
|
* @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 = 20000;
|
|
|
|
// 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 = 128 * 1024; // 128KB tail for initial load
|
|
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 <promise>COMPLETE</promise>.',
|
|
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<string, string>} 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',
|
|
|
|
// 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]);
|
|
}
|