chore: bump version to 0.1550

This commit is contained in:
arkon
2026-02-19 08:49:36 +01:00
parent 1833a9ac2d
commit 926e77b1c2
27 changed files with 290 additions and 1067 deletions
+1 -12
View File
@@ -28,7 +28,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { EventEmitter } from 'node:events';
import { getAugmentedPath } from './session.js';
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
import { ANSI_ESCAPE_PATTERN_SIMPLE } from './utils/index.js';
// ========== Security Validation ==========
@@ -95,17 +95,6 @@ export interface AiCheckerStateBase<V extends string> {
disabledReason: string | null;
}
/** Events emitted by AI checkers */
export interface AiCheckerEvents<R> {
checkStarted: () => void;
checkCompleted: (result: R) => void;
checkFailed: (error: string) => void;
cooldownStarted: (endsAt: number) => void;
cooldownEnded: () => void;
disabled: (reason: string) => void;
log: (message: string) => void;
}
// ========== Constants ==========
/** Poll interval for checking temp file completion */
-14
View File
@@ -29,32 +29,18 @@ import {
type AiCheckerConfigBase,
type AiCheckerResultBase,
type AiCheckerStateBase,
type AiCheckerStatus,
} from './ai-checker-base.js';
// ========== Types ==========
export interface AiIdleCheckConfig extends AiCheckerConfigBase {}
// Re-export the status type for backwards compatibility
export type AiCheckStatus = AiCheckerStatus;
export type AiCheckVerdict = 'IDLE' | 'WORKING' | 'ERROR';
export interface AiCheckResult extends AiCheckerResultBase<AiCheckVerdict> {}
export interface AiCheckState extends AiCheckerStateBase<AiCheckVerdict> {}
/** Events emitted by AiIdleChecker */
export interface AiIdleCheckerEvents {
checkStarted: () => void;
checkCompleted: (result: AiCheckResult) => void;
checkFailed: (error: string) => void;
cooldownStarted: (endsAt: number) => void;
cooldownEnded: () => void;
disabled: (reason: string) => void;
log: (message: string) => void;
}
// ========== Constants ==========
const DEFAULT_AI_CHECK_CONFIG: AiIdleCheckConfig = {
-14
View File
@@ -28,32 +28,18 @@ import {
type AiCheckerConfigBase,
type AiCheckerResultBase,
type AiCheckerStateBase,
type AiCheckerStatus,
} from './ai-checker-base.js';
// ========== Types ==========
export interface AiPlanCheckConfig extends AiCheckerConfigBase {}
// Re-export the status type for backwards compatibility
export type AiPlanCheckStatus = AiCheckerStatus;
export type AiPlanCheckVerdict = 'PLAN_MODE' | 'NOT_PLAN_MODE' | 'ERROR';
export interface AiPlanCheckResult extends AiCheckerResultBase<AiPlanCheckVerdict> {}
export interface AiPlanCheckState extends AiCheckerStateBase<AiPlanCheckVerdict> {}
/** Events emitted by AiPlanChecker */
export interface AiPlanCheckerEvents {
checkStarted: () => void;
checkCompleted: (result: AiPlanCheckResult) => void;
checkFailed: (error: string) => void;
cooldownStarted: (endsAt: number) => void;
cooldownEnded: () => void;
disabled: (reason: string) => void;
log: (message: string) => void;
}
// ========== Constants ==========
const DEFAULT_PLAN_CHECK_CONFIG: AiPlanCheckConfig = {
-60
View File
@@ -36,16 +36,6 @@ export const MAX_SUBAGENT_ACTIVITY_PER_AGENT = 100;
*/
export const MAX_TOOL_RESULTS_PER_AGENT = 200;
// ============================================================================
// Hook Event Limits
// ============================================================================
/**
* Maximum pending hook events to queue.
* Prevents unbounded growth if hook processing is slow.
*/
export const MAX_PENDING_HOOKS = 50;
// ============================================================================
// Session Tracking Limits
// ============================================================================
@@ -56,56 +46,6 @@ export const MAX_PENDING_HOOKS = 50;
*/
export const MAX_CONCURRENT_SESSIONS = 50;
/**
* Maximum session history entries to keep (for analytics).
*/
export const MAX_SESSION_HISTORY = 100;
// ============================================================================
// SSE Client Limits
// ============================================================================
/**
* Maximum SSE clients per session.
* Prevents resource exhaustion from many browser tabs.
*/
export const MAX_SSE_CLIENTS_PER_SESSION = 10;
/**
* Maximum total SSE clients across all sessions.
*/
export const MAX_TOTAL_SSE_CLIENTS = 100;
// ============================================================================
// File Watcher Limits
// ============================================================================
/**
* Maximum file watchers (FSWatcher) to allow.
* Linux default max_user_watches is 8192-65536.
* We warn at 80% capacity and evict idle watchers.
*/
export const MAX_FILE_WATCHERS = 500;
/**
* Warning threshold as percentage of max watchers.
*/
export const FILE_WATCHER_WARNING_THRESHOLD = 0.8;
// ============================================================================
// Task Tracking Limits
// ============================================================================
/**
* Maximum tasks to keep in the task queue.
*/
export const MAX_QUEUED_TASKS = 100;
/**
* Maximum completed tasks to keep for history.
*/
export const MAX_COMPLETED_TASKS_HISTORY = 50;
// ============================================================================
// Todo Item Limits (Ralph Tracker)
// ============================================================================
+1 -1
View File
@@ -406,7 +406,7 @@ export class PlanOrchestrator {
this.runningSessions.add(session);
const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription);
const prompt = RESEARCH_AGENT_PROMPT.replace('{TASK}', taskDescription).replace('{WORKING_DIR}', this.workingDir);
// Start progress interval before try block to ensure cleanup in finally
const progressInterval = setInterval(() => {
-46
View File
@@ -1,46 +0,0 @@
/**
* Code Reviewer Prompt
*
* Designs post-implementation review tasks.
* Reserved for future use.
*
* Placeholders: {TASK}
*/
export const CODE_REVIEWER_PROMPT = `You are a Code Review Specialist designing post-implementation review tasks.
## YOUR TASK
Design code review steps for implementations in this task:
## TASK DESCRIPTION
{TASK}
## INSTRUCTIONS
For each implementation identified, create a review task that checks:
1. **Best Practices**: Language-specific conventions and idioms
2. **Security**: OWASP top 10, input validation, authentication
3. **Performance**: Time complexity, memory usage, N+1 queries
4. **Error Handling**: Edge cases covered, meaningful error messages
5. **Code Quality**: DRY, SOLID principles, readability
6. **Type Safety**: Proper typing, no implicit any, null checks
## REVIEW TASK GUIDELINES
- Review tasks run AFTER implementation, BEFORE merge
- Each review should be specific and actionable
- Include what to look for and how to verify
- Reference language-specific linting tools where applicable
## OUTPUT FORMAT
Return ONLY a JSON array:
[
{
"category": "security|performance|quality|error-handling|best-practices|type-safety",
"content": "Review authentication handler for XSS vulnerabilities",
"rationale": "User input flows through auth - must sanitize",
"verificationCriteria": "No unescaped user input, all inputs validated",
"reviewChecklist": ["Check input sanitization", "Verify CSRF tokens", "Review session handling"],
"implToReview": "Implement authentication handler"
}
]
Generate 5-10 review tasks. Code review catches bugs that tests miss.`;
-1
View File
@@ -7,4 +7,3 @@
export { RESEARCH_AGENT_PROMPT } from './research-agent.js';
export { PLANNER_PROMPT } from './planner.js';
export { CODE_REVIEWER_PROMPT } from './code-reviewer.js';
-8
View File
@@ -112,9 +112,6 @@ export class RunSummaryTracker {
this.events.push(event);
this.lastUpdatedAt = event.timestamp;
// Update stats based on event type
this.updateStatsFromEvent(event);
// Trim if needed
if (this.events.length > MAX_EVENTS) {
this.events = this.events.slice(-TRIM_TO_EVENTS);
@@ -394,11 +391,6 @@ export class RunSummaryTracker {
// ========== Private Methods ==========
private updateStatsFromEvent(_event: RunSummaryEvent): void {
// Most stats are updated in the specific record* methods
// This is for any additional cross-cutting concerns
}
private startStateStuckDetection(): void {
this.stateStuckCheckTimer = setInterval(() => {
if (!this.currentState || !this.stateEnteredAt || this.stateStuckWarned) {
-10
View File
@@ -293,13 +293,3 @@ export function getSessionManager(): SessionManager {
return managerInstance;
}
/**
* Resets the singleton SessionManager instance.
* Primarily used for testing to ensure test isolation.
*/
export async function resetSessionManager(): Promise<void> {
if (managerInstance) {
await managerInstance.stopAllSessions();
managerInstance = null;
}
}
+2 -2
View File
@@ -85,8 +85,6 @@ const NEWLINE_SPLIT_PATTERN = /\r?\n/;
// Claude CLI PATH resolution — shared utility
import { getAugmentedPath } from './utils/claude-cli-resolver.js';
// Re-export for backward compatibility (ai-checker-base imports from session)
export { getAugmentedPath } from './utils/claude-cli-resolver.js';
/**
* Wraps a promise with a timeout to prevent indefinite hangs.
@@ -193,6 +191,8 @@ export interface SessionEvents {
bashToolEnd: (tool: ActiveBashTool) => void;
/** Active Bash tools list updated */
bashToolsUpdate: (tools: ActiveBashTool[]) => void;
/** CLI info (version, model, account) updated */
cliInfoUpdated: (info: { version: string | null; model: string | null; accountType: string | null; latestVersion: string | null }) => void;
}
/**
+9 -14
View File
@@ -13,7 +13,6 @@
import { EventEmitter } from 'node:events';
import { Task, CreateTaskOptions } from './task.js';
import { getStore } from './state-store.js';
import { TaskState } from './types.js';
/**
* Events emitted by TaskQueue
@@ -214,16 +213,16 @@ export class TaskQueue extends EventEmitter {
) || null;
}
/** Gets counts of tasks by status. */
/** Gets counts of tasks by status (single-pass). */
getCount(): { total: number; pending: number; running: number; completed: number; failed: number } {
const tasks = this.getAllTasks();
return {
total: tasks.length,
pending: tasks.filter((t) => t.isPending()).length,
running: tasks.filter((t) => t.isRunning()).length,
completed: tasks.filter((t) => t.isCompleted()).length,
failed: tasks.filter((t) => t.isFailed()).length,
};
let pending = 0, running = 0, completed = 0, failed = 0;
for (const task of this.tasks.values()) {
if (task.isPending()) pending++;
else if (task.isRunning()) running++;
else if (task.isCompleted()) completed++;
else if (task.isFailed()) failed++;
}
return { total: this.tasks.size, pending, running, completed, failed };
}
/** Removes all completed tasks. Returns count removed. */
@@ -260,10 +259,6 @@ export class TaskQueue extends EventEmitter {
return count;
}
/** Gets tasks from persistent storage. */
getStoredTasks(): Record<string, TaskState> {
return this.store.getTasks();
}
}
// Singleton instance
+23 -14
View File
@@ -32,6 +32,7 @@ import { dirname, join } from 'node:path';
import { homedir } from 'node:os';
import { ProcessStats, PersistedRespawnConfig, getErrorMessage, NiceConfig, DEFAULT_NICE_CONFIG, type PaneInfo } from './types.js';
import { wrapWithNice } from './utils/nice-wrapper.js';
import { SAFE_PATH_PATTERN } from './utils/regex-patterns.js';
import type { TerminalMultiplexer, MuxSession, MuxSessionWithStats } from './mux-interface.js';
// Claude CLI PATH resolution — shared utility
@@ -77,9 +78,6 @@ const MUX_SESSIONS_FILE = join(homedir(), '.claudeman', 'mux-sessions.json');
/** Regex to validate tmux session names (only allow safe characters) */
const SAFE_MUX_NAME_PATTERN = /^claudeman-[a-f0-9-]+$/;
/** Regex to validate working directory paths (no shell metacharacters) */
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
/** Regex to validate tmux pane targets (e.g., "%0", "%1", "0", "1") */
const SAFE_PANE_TARGET_PATTERN = /^(%\d+|\d+)$/;
@@ -795,7 +793,8 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
/**
* Start periodic mouse mode sync for all tracked sessions.
* Polls pane counts every 5s and toggles mouse on/off as needed.
* Only calls tmux set-option when the pane count actually changes.
* Polls every 5s. On pane count change, toggles mouse on (>1 pane) or off (1 pane).
* If enableMouseMode/disableMouseMode fails, lastPaneCount is NOT updated so it retries next poll.
*/
startMouseModeSync(intervalMs: number = 5000): void {
if (this.mouseSyncInterval) {
@@ -804,18 +803,24 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
this.mouseSyncInterval = setInterval(() => {
if (IS_TEST_MODE) return;
for (const session of this.sessions.values()) {
const panes = this.listPanes(session.muxName);
const count = panes.length;
const prev = this.lastPaneCount.get(session.muxName);
if (count === 0) continue;
// Only toggle when pane count crosses the 1↔N boundary
if (prev !== count && count > 0) {
this.lastPaneCount.set(session.muxName, count);
if (count > 1) {
this.enableMouseMode(session.muxName);
} else {
this.disableMouseMode(session.muxName);
const prev = this.lastPaneCount.get(session.muxName);
if (prev === count) continue;
// Pane count changed — toggle mouse mode
if (count > 1) {
if (this.enableMouseMode(session.muxName)) {
this.lastPaneCount.set(session.muxName, count);
}
// If enableMouseMode fails, DON'T update lastPaneCount — retry next poll
} else {
if (this.disableMouseMode(session.muxName)) {
this.lastPaneCount.set(session.muxName, count);
}
}
}
@@ -950,8 +955,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
`tmux set-option -t "${muxName}" mouse on`,
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
);
console.log(`[TmuxManager] Mouse mode ON for ${muxName}`);
return true;
} catch {
} catch (err) {
console.error(`[TmuxManager] Failed to enable mouse mode for ${muxName}:`, err);
return false;
}
}
@@ -972,8 +979,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
`tmux set-option -t "${muxName}" mouse off`,
{ encoding: 'utf-8', timeout: EXEC_TIMEOUT_MS }
);
console.log(`[TmuxManager] Mouse mode OFF for ${muxName}`);
return true;
} catch {
} catch (err) {
console.error(`[TmuxManager] Failed to disable mouse mode for ${muxName}:`, err);
return false;
}
}
+30 -43
View File
@@ -291,54 +291,41 @@ export class TranscriptWatcher extends EventEmitter {
}
const entries: TranscriptEntry[] = [];
// Read raw buffer to detect actual line endings (LF vs CRLF)
const transcriptPath = this.transcriptPath;
let rawChunks: Buffer[] = [];
const rawStream = createReadStream(transcriptPath, {
const stream = createReadStream(transcriptPath, {
start: this.filePosition,
encoding: 'utf-8',
});
rawStream.on('data', (chunk: Buffer | string) => rawChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
rawStream.on('end', () => {
// Detect if file uses CRLF
const raw = Buffer.concat(rawChunks);
rawChunks = []; // Free memory
const hasCRLF = raw.includes(0x0d); // 0x0d = \r
const lineEndingSize = hasCRLF ? 2 : 1;
const stream = createReadStream(transcriptPath, {
start: this.filePosition,
encoding: 'utf-8',
});
const rl = createInterface({
input: stream,
crlfDelay: Infinity,
});
let bytesRead = this.filePosition;
rl.on('line', (line) => {
bytesRead += Buffer.byteLength(line, 'utf-8') + lineEndingSize;
if (!line.trim()) return;
try {
const entry = JSON.parse(line) as TranscriptEntry;
entries.push(entry);
} catch {
// Skip malformed lines
}
});
rl.on('close', () => {
this.filePosition = bytesRead;
resolve(entries);
});
rl.on('error', reject);
stream.on('error', reject);
const rl = createInterface({
input: stream,
crlfDelay: Infinity, // Handles both LF and CRLF
});
rawStream.on('error', reject);
rl.on('line', (line) => {
if (!line.trim()) return;
try {
const entry = JSON.parse(line) as TranscriptEntry;
entries.push(entry);
} catch {
// Skip malformed lines
}
});
rl.on('close', () => {
// Update position to current file size (accounts for any line ending style)
try {
this.filePosition = statSync(transcriptPath).size;
} catch {
// File may have been deleted between read and stat
}
resolve(entries);
});
rl.on('error', reject);
stream.on('error', reject);
});
}
-145
View File
@@ -37,29 +37,6 @@ export interface BufferConfig {
onTrim?: (trimmedBytes: number) => void;
}
/**
* Memory metrics for monitoring and debugging.
* Extends Node.js process.memoryUsage() with application-specific tracking.
*/
export interface MemoryMetrics {
/** Heap memory used by V8 (bytes) */
heapUsed: number;
/** Total heap size allocated by V8 (bytes) */
heapTotal: number;
/** Memory used by C++ objects bound to JavaScript (bytes) */
external: number;
/** Memory used by ArrayBuffers and SharedArrayBuffers (bytes) */
arrayBuffers: number;
/** Sizes of tracked Maps by name */
mapSizes: Record<string, number>;
/** Number of active timers (setTimeout/setInterval) */
timerCount: number;
/** Number of active file system watchers */
watcherCount: number;
/** Timestamp when metrics were collected */
timestamp: number;
}
/**
* Resource types that can be registered for cleanup.
*/
@@ -621,18 +598,6 @@ export interface SessionOutput {
exitCode: number | null;
}
/**
* Task assignment record
*/
export interface TaskAssignment {
/** Session ID that task is assigned to */
sessionId: string;
/** Task ID being assigned */
taskId: string;
/** Timestamp of assignment */
assignedAt: number;
}
// ========== API Error Handling ==========
/**
@@ -665,101 +630,11 @@ export const ErrorMessages: Record<ApiErrorCode, string> = {
[ApiErrorCode.INTERNAL_ERROR]: 'An internal error occurred',
};
// ========== API Request Types ==========
/**
* Request to create a new session
*/
export interface CreateSessionRequest {
/** Optional working directory path */
workingDir?: string;
}
/**
* Request to run a prompt in a session
*/
export interface RunPromptRequest {
/** Prompt to send to Claude */
prompt: string;
}
/**
* Request to send input to a session
*/
export interface SessionInputRequest {
/** Input string to send */
input: string;
}
/**
* Request to resize terminal
*/
export interface ResizeRequest {
/** Number of columns */
cols: number;
/** Number of rows */
rows: number;
}
/**
* Request to create a new case
*/
export interface CreateCaseRequest {
/** Case name (alphanumeric with hyphens/underscores) */
name: string;
/** Optional case description */
description?: string;
}
/**
* Request for quick start (create case + session)
*/
export interface QuickStartRequest {
/** Optional case name, defaults to 'testcase' */
caseName?: string;
/** Session mode: 'claude' for Claude CLI, 'shell' for bash shell */
mode?: 'claude' | 'shell';
}
/**
* Request to create a scheduled run
*/
export interface CreateScheduledRunRequest {
/** Prompt to run */
prompt: string;
/** Optional working directory */
workingDir?: string;
/** Duration in minutes */
durationMinutes: number;
}
/**
* Request for quick run (one-shot prompt execution)
*/
export interface QuickRunRequest {
/** Prompt to run */
prompt: string;
/** Optional working directory */
workingDir?: string;
}
/**
* Hook event types triggered by Claude Code's hooks system
*/
export type HookEventType = 'idle_prompt' | 'permission_prompt' | 'elicitation_dialog' | 'stop' | 'teammate_idle' | 'task_completed';
/**
* Request body for the hook-event API endpoint
*/
export interface HookEventRequest {
/** Type of hook event that fired */
event: HookEventType;
/** Session ID from CLAUDEMAN_SESSION_ID env var */
sessionId: string;
/** Additional event data (tool name, command, question, etc.) */
data?: Record<string, unknown>;
}
// ========== API Response Types ==========
/**
@@ -784,19 +659,6 @@ export function createErrorResponse(code: ApiErrorCode, details?: string): ApiRe
};
}
/**
* Creates a standardized success response
* @template T Type of the data payload
* @param data Optional response data
* @returns Formatted success response
*/
export function createSuccessResponse<T>(data?: T): ApiResponse<T> {
return {
success: true,
data,
};
}
/**
* Response for session operations
*/
@@ -1074,13 +936,6 @@ export interface RalphSessionState {
lastUpdated: number;
}
/**
* Map of session ID to inner state
*/
export interface RalphStateRecord {
[sessionId: string]: RalphSessionState;
}
// ========== RALPH_STATUS Block Types ==========
/**
-2
View File
@@ -193,5 +193,3 @@ export class BufferAccumulator {
}
}
}
export default BufferAccumulator;
-2
View File
@@ -326,5 +326,3 @@ export class CleanupManager implements Disposable {
}
}
}
export default CleanupManager;
-4
View File
@@ -17,7 +17,6 @@ export {
createAnsiPatternFull,
createAnsiPatternSimple,
stripAnsi,
stripAnsiSimple,
} from './regex-patterns.js';
export {
MAX_SESSION_TOKENS,
@@ -25,10 +24,7 @@ export {
validateTokensAndCost,
} from './token-validation.js';
export {
levenshteinDistance,
stringSimilarity,
isSimilar,
isSimilarByDistance,
normalizePhrase,
fuzzyPhraseMatch,
todoContentHash,
-2
View File
@@ -261,5 +261,3 @@ export class LRUMap<K, V> extends Map<K, V> {
return Math.max(0, this.maxSize - super.size);
}
}
export default LRUMap;
+4 -6
View File
@@ -64,10 +64,8 @@ export function stripAnsi(text: string): string {
}
/**
* Strips simple ANSI CSI codes from text (faster, less comprehensive).
* @param text - Text containing ANSI escape codes
* @returns Text with ANSI CSI codes removed
* Regex to validate safe file paths (no shell metacharacters).
* Shared by schemas.ts and tmux-manager.ts for consistent path validation.
*/
export function stripAnsiSimple(text: string): string {
return text.replace(ANSI_ESCAPE_PATTERN_SIMPLE, '');
}
export const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
-2
View File
@@ -334,5 +334,3 @@ export class StaleExpirationMap<K, V> implements Disposable {
}
}
}
export default StaleExpirationMap;
-26
View File
@@ -4384,11 +4384,6 @@ class ClaudemanApp {
}
}
// Legacy toggle for backwards compat
toggleSubagentDropdown(badgeEl) {
this.pinSubagentDropdown(badgeEl);
}
// Permanently close a minimized subagent (remove from DOM and minimized set)
permanentlyCloseMinimizedSubagent(agentId, sessionId) {
// Remove from minimized set
@@ -7307,27 +7302,6 @@ class ClaudemanApp {
}
}
skipPlanGeneration() {
// Stop any running timers
if (this.planLoadingTimer) {
clearInterval(this.planLoadingTimer);
this.planLoadingTimer = null;
}
if (this.planPhaseTimer) {
clearInterval(this.planPhaseTimer);
this.planPhaseTimer = null;
}
this.ralphWizardConfig.skipPlanGeneration = true;
this.ralphWizardConfig.planGenerated = false;
this.ralphWizardConfig.generatedPlan = null;
// Generate preview and go to step 3
this.updateRalphPromptPreview();
this.ralphWizardStep = 3;
this.updateRalphWizardUI();
}
regeneratePlan() {
this.ralphWizardConfig.generatedPlan = null;
this.ralphWizardConfig.planGenerated = false;
-601
View File
@@ -39,9 +39,6 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
/* Utility classes */
.mt-sm { margin-top: 0.5rem; }
/* ========== Accessibility: Focus Styles ========== */
/* Skip link for keyboard users */
@@ -1319,22 +1316,7 @@ body {
white-space: nowrap;
}
/* Legacy classes for backward compatibility */
.ralph-progress-section {
display: flex;
align-items: flex-start;
gap: 1.5rem;
margin-bottom: 1rem;
}
/* Large Progress Ring */
.ralph-ring {
position: relative;
width: 100px;
height: 100px;
flex-shrink: 0;
}
.ralph-ring-svg-large {
width: 100%;
height: 100%;
@@ -1378,66 +1360,6 @@ body {
margin-top: 0.15rem;
}
/* Meta Info */
.ralph-meta {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.ralph-meta-item {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.ralph-meta-label {
font-size: 0.65rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.ralph-meta code {
background: var(--bg-input);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.75rem;
color: var(--accent);
word-break: break-all;
}
.ralph-meta-row {
display: flex;
gap: 1.5rem;
}
.ralph-meta-item span:not(.ralph-meta-label) {
font-size: 0.9rem;
font-weight: 500;
color: var(--text);
}
/* Task Cards Section */
.ralph-tasks-section {
background: var(--bg-card);
border-radius: 6px;
overflow: hidden;
}
.ralph-tasks-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
background: var(--bg-input);
font-weight: 500;
color: var(--text);
font-size: 0.75rem;
}
.ralph-tasks-count {
color: var(--text-dim);
font-weight: 400;
@@ -1886,13 +1808,6 @@ body {
gap: 0;
}
.toolbar-divider {
width: 1px;
height: 20px;
background: var(--border);
margin: 0 0.5rem;
}
.btn-toolbar {
display: flex;
align-items: center;
@@ -2339,15 +2254,6 @@ body {
background: var(--bg-hover);
}
.btn-danger-sm {
color: #ff6b6b;
}
.btn-danger-sm:hover {
color: #fff;
background: #dc3545;
}
.settings-section {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
@@ -2360,15 +2266,6 @@ body {
border-bottom: none;
}
.settings-section-title {
font-size: 0.75rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.form-row {
margin-bottom: 0.75rem;
}
@@ -2403,17 +2300,6 @@ body {
border-color: var(--accent);
}
.form-row-inline {
display: flex;
gap: 0.5rem;
margin-bottom: 0.75rem;
align-items: center;
}
.form-row-inline input[type="text"] {
flex: 1;
}
.form-col {
flex: 1;
}
@@ -2447,43 +2333,6 @@ body {
font-size: 0.75rem;
}
.checkbox-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
cursor: pointer;
transition: border-color 0.15s;
}
.checkbox-label:hover {
border-color: var(--border-light);
}
.checkbox-label input[type="checkbox"] {
accent-color: var(--accent);
}
.checkbox-label span:first-of-type {
font-size: 0.8rem;
color: var(--text);
}
.checkbox-hint {
font-size: 0.7rem;
color: var(--text-muted);
margin-left: auto;
}
.form-hint {
display: block;
font-size: 0.65rem;
@@ -2667,10 +2516,6 @@ body {
font-size: 0.75rem;
}
.settings-item-full {
grid-column: 1 / -1;
}
.settings-item-label {
color: var(--text);
white-space: nowrap;
@@ -2726,26 +2571,6 @@ body {
color: var(--red);
}
/* CPU limit status indicator */
.cpu-limit-status {
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 3px;
background: var(--bg-hover);
color: var(--text-dim);
margin-left: 6px;
}
.cpu-limit-status.available {
background: rgba(34, 197, 94, 0.2);
color: var(--green);
}
.cpu-limit-status.unavailable {
background: rgba(239, 68, 68, 0.2);
color: var(--red);
}
/* Event Type Notification Grid */
.event-type-grid {
display: grid;
@@ -2824,24 +2649,6 @@ body {
border-top: 1px solid var(--border);
}
.form-actions-stack {
flex-direction: column;
align-items: stretch;
}
.form-actions-stack button {
width: 100%;
}
.btn-warning {
background: var(--yellow);
color: #000;
}
.btn-warning:hover {
background: #d4a107;
}
/* Modal */
.modal {
display: none;
@@ -3251,13 +3058,6 @@ body {
padding: 0.25rem 0.5rem;
}
.modal-footer-actions {
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.auto-refresh-label {
display: flex;
align-items: center;
@@ -3297,32 +3097,6 @@ body {
font-family: 'SF Mono', Monaco, monospace;
}
#runSummaryModal .modal-footer-actions {
gap: 0.5rem;
}
#runSummaryModal .modal-footer-actions .btn-toolbar {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
}
/* Tab summary icon */
.tab-summary {
opacity: 0;
cursor: pointer;
font-size: 0.85rem;
transition: opacity 0.15s;
margin-right: 0.25rem;
}
.session-tab:hover .tab-summary {
opacity: 0.5;
}
.session-tab:hover .tab-summary:hover {
opacity: 1;
}
.modal-warning-text {
color: var(--text-dim);
font-size: 0.85rem;
@@ -3399,35 +3173,12 @@ body {
border: 1px solid var(--border);
}
.session-dir-display {
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-dim);
font-size: 0.8rem;
font-family: 'SF Mono', Monaco, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Session respawn section in options modal */
.session-respawn-section {
padding-top: 0;
margin-top: 0;
}
.section-divider {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-dim);
}
.session-respawn-status {
display: flex;
align-items: center;
@@ -3459,11 +3210,6 @@ body {
}
/* Inline checkbox group */
.checkbox-group-inline {
display: flex;
gap: 1rem;
}
.checkbox-inline {
display: flex;
align-items: center;
@@ -3481,13 +3227,6 @@ body {
color: var(--text);
}
/* Auto-clear row */
.auto-clear-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.input-suffix-sm {
display: flex;
align-items: center;
@@ -3583,11 +3322,6 @@ body {
gap: 0.5rem;
}
.session-respawn-actions {
display: flex;
gap: 0.5rem;
}
/* Modal Tabs */
.modal-tabs {
display: flex;
@@ -4349,59 +4083,6 @@ kbd {
transition: opacity 0.1s, color 0.1s;
}
/* Task Panel */
.task-panel {
position: fixed;
bottom: var(--toolbar-height);
right: 0.5rem;
width: 350px;
max-width: calc(100vw - 1rem);
/* Performance: GPU-accelerated transforms and isolated paint */
contain: layout style;
will-change: transform;
max-height: 50vh;
background: var(--bg-card);
border: 1px solid var(--border);
border-bottom: none;
border-radius: 8px 8px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
z-index: 100;
transform: translateY(100%);
transition: transform 0.2s ease;
display: flex;
flex-direction: column;
}
.task-panel.open {
transform: translateY(0);
}
.task-panel-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
font-weight: 500;
flex-shrink: 0;
}
.task-panel-stats {
margin-left: auto;
font-size: 0.7rem;
color: var(--text-dim);
padding: 0.2rem 0.5rem;
background: var(--bg-input);
border-radius: 10px;
}
.task-panel-body {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.task-empty {
padding: 1rem;
text-align: center;
@@ -4522,53 +4203,6 @@ kbd {
background: var(--border-light);
}
/* Process Monitor Panel */
.process-panel {
position: fixed;
bottom: var(--toolbar-height);
left: 0.5rem;
width: 400px;
max-width: calc(100vw - 1rem);
max-height: 50vh;
background: var(--bg-card);
border: 1px solid var(--border);
border-bottom: none;
border-radius: 8px 8px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
z-index: 100;
transform: translateY(100%);
transition: transform 0.2s ease;
display: flex;
flex-direction: column;
}
.process-panel.open {
transform: translateY(0);
}
.process-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.6rem 1rem;
border-bottom: 1px solid var(--border);
font-size: 0.8rem;
font-weight: 500;
flex-shrink: 0;
}
.process-panel-actions {
display: flex;
align-items: center;
gap: 0.5rem;
}
.process-panel-body {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.process-empty {
padding: 1rem;
text-align: center;
@@ -4714,55 +4348,6 @@ kbd {
color: var(--text);
}
.monitor-tabs {
display: flex;
gap: 0.25rem;
}
.monitor-tab {
background: transparent;
border: none;
color: var(--text-muted);
padding: 0.3rem 0.6rem;
font-size: 0.75rem;
cursor: pointer;
border-radius: 4px;
transition: all 0.15s;
}
.monitor-tab:hover {
color: var(--text);
background: var(--bg-hover);
}
.monitor-tab.active {
color: white;
background: var(--accent);
}
.monitor-tab-badge {
display: inline-block;
background: var(--success);
color: white;
font-size: 0.6rem;
padding: 0.1rem 0.35rem;
border-radius: 8px;
margin-left: 0.25rem;
font-weight: 600;
}
.monitor-tab-badge:empty {
display: none;
}
.monitor-tab-content {
display: none;
}
.monitor-tab-content.active {
display: flex;
}
.monitor-panel-actions {
display: flex;
align-items: center;
@@ -4864,47 +4449,6 @@ kbd {
/* ========== Subagent Panel (Claude Code Background Agents) ========== */
/* Toolbar button for subagents tab */
.toolbar-btn-subagents {
display: flex;
align-items: center;
gap: 0.4rem;
background: var(--bg-hover);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 0.75rem;
padding: 0.4rem 0.75rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s;
}
.toolbar-btn-subagents:hover {
background: var(--bg-card);
border-color: var(--accent);
color: var(--text);
}
.toolbar-btn-subagents.active {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.toolbar-badge {
background: var(--accent);
color: white;
font-size: 0.65rem;
padding: 0.1rem 0.4rem;
border-radius: 8px;
min-width: 1.2em;
text-align: center;
}
.toolbar-badge:empty {
display: none;
}
/* ========== Separate Subagents Panel ========== */
.subagents-panel {
position: fixed;
@@ -6312,25 +5856,6 @@ kbd {
cursor: pointer;
}
/* Custom tooltip for file links */
.file-link-tooltip {
position: fixed;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 4px;
padding: 0.3rem 0.5rem;
font-size: 0.75rem;
color: var(--text);
z-index: 10000;
pointer-events: none;
box-shadow: var(--shadow-md);
}
.file-link-tooltip::before {
content: "📄 Click to stream: ";
color: var(--accent);
}
/* ========== Ralph Loop Wizard ========== */
/* Wizard Progress Indicator */
@@ -6767,28 +6292,11 @@ kbd {
margin: 0 0 1rem 0;
}
.plan-skip-btn {
opacity: 0.6;
}
.plan-skip-btn:hover {
opacity: 1;
}
.plan-error-msg {
color: var(--red);
margin-bottom: 1rem;
}
/* Plan Options (detail level, model) */
.plan-options {
display: flex;
gap: 1rem;
justify-content: center;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.plan-option-group {
display: flex;
align-items: center;
@@ -6987,10 +6495,6 @@ kbd {
color: var(--text);
}
.plan-actions-single {
justify-content: center;
}
.plan-skip-hint {
font-size: 0.75rem;
color: var(--text-muted);
@@ -7446,11 +6950,6 @@ kbd {
font-weight: 500;
}
.mode-changed {
color: var(--accent);
font-weight: 500;
}
/* Plan warnings and gaps display */
#planWarnings {
margin-bottom: 0.75rem;
@@ -7762,55 +7261,6 @@ kbd {
border-color: var(--accent);
}
/* Verification criteria tooltip expansion */
.plan-item-verify-expanded {
position: absolute;
left: 0;
right: 0;
bottom: calc(100% + 4px);
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.5rem;
font-size: 0.75rem;
color: var(--text);
z-index: 10;
box-shadow: 0 -2px 8px rgba(0,0,0,0.2);
max-width: 300px;
}
.plan-item-verify-expanded::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: var(--border);
}
/* Plan toolbar improvements */
.plan-editor-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.plan-toolbar-left {
display: flex;
gap: 0.5rem;
align-items: center;
}
.plan-toolbar-right {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Plan item inline actions */
.plan-item-actions {
display: flex;
@@ -7909,36 +7359,6 @@ kbd {
border-color: var(--accent);
}
/* Plan version indicator */
.plan-version-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-muted);
padding: 0.25rem 0.5rem;
background: var(--bg-input);
border-radius: 4px;
}
.plan-version-badge {
background: var(--accent);
color: white;
padding: 0.1rem 0.3rem;
border-radius: 3px;
font-size: 0.65rem;
font-weight: 500;
}
.plan-version-history {
cursor: pointer;
color: var(--accent);
}
.plan-version-history:hover {
text-decoration: underline;
}
/* Plan history dropdown */
.plan-history-dropdown {
position: absolute;
@@ -8061,27 +7481,6 @@ kbd {
border-color: var(--accent);
}
/* Phrase Input Group */
.phrase-input-group {
position: relative;
}
.phrase-input-group input {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.85rem;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
}
.phrase-input-group input:focus {
outline: none;
border-color: var(--accent);
}
/* Iteration Presets */
.iteration-presets {
display: flex;
+1 -12
View File
@@ -8,12 +8,10 @@
*/
import { z } from 'zod';
import { SAFE_PATH_PATTERN } from '../utils/regex-patterns.js';
// ========== Path Validation ==========
/** Regex to validate working directory paths (no shell metacharacters) — matches tmux-manager.ts */
const SAFE_PATH_PATTERN = /^[a-zA-Z0-9_\/\-. ~]+$/;
/** Validate a path string: no shell metacharacters, no traversal, must be absolute */
export function isValidWorkingDir(p: string): boolean {
if (!p || !p.startsWith('/')) return false;
@@ -80,15 +78,6 @@ export const RunPromptSchema = z.object({
prompt: z.string().min(1).max(100000),
});
/**
* Schema for POST /api/sessions/:id/input
* Sends input to an interactive session.
*/
export const SessionInputSchema = z.object({
input: z.string(),
useMux: z.boolean().optional(),
});
/**
* Schema for POST /api/sessions/:id/resize
* Resizes a session's terminal.
+11 -11
View File
@@ -13,7 +13,7 @@
import Fastify, { FastifyInstance, FastifyReply } from 'fastify';
import fastifyCompress from '@fastify/compress';
import fastifyStatic from '@fastify/static';
import path, { join, dirname, resolve, relative, isAbsolute } from 'node:path';
import { join, dirname, resolve, relative, isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import { existsSync, statSync, mkdirSync, writeFileSync, readdirSync, readFileSync, rmSync } from 'node:fs';
import fs from 'node:fs/promises';
@@ -89,6 +89,7 @@ import {
RespawnEnableSchema,
} from './schemas.js';
import { StaleExpirationMap } from '../utils/index.js';
import { MAX_CONCURRENT_SESSIONS } from '../config/map-limits.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -127,8 +128,6 @@ const SESSIONS_LIST_CACHE_TTL = 1000;
const SCHEDULED_CLEANUP_INTERVAL = 5 * 60 * 1000;
// Completed scheduled runs max age (1 hour)
const SCHEDULED_RUN_MAX_AGE = 60 * 60 * 1000;
// Maximum concurrent sessions to prevent resource exhaustion
const MAX_CONCURRENT_SESSIONS = 50;
// SSE client health check interval (every 30 seconds)
const SSE_HEALTH_CHECK_INTERVAL = 30 * 1000;
// Maximum allowed input length for session write (64KB)
@@ -1461,7 +1460,7 @@ export class WebServer extends EventEmitter {
}
const content = session.ralphTracker.generateFixPlanMarkdown();
const filePath = path.join(workingDir, '@fix_plan.md');
const filePath = join(workingDir, '@fix_plan.md');
try {
await fs.writeFile(filePath, content, 'utf-8');
@@ -1491,7 +1490,7 @@ export class WebServer extends EventEmitter {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory');
}
const filePath = path.join(workingDir, '@fix_plan.md');
const filePath = join(workingDir, '@fix_plan.md');
try {
const content = await fs.readFile(filePath, 'utf-8');
@@ -1534,7 +1533,7 @@ export class WebServer extends EventEmitter {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Session has no working directory');
}
const filePath = path.join(workingDir, '@ralph_prompt.md');
const filePath = join(workingDir, '@ralph_prompt.md');
try {
await fs.writeFile(filePath, content, 'utf-8');
@@ -5335,12 +5334,13 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
// Start stats collection for mux sessions
this.mux.startStatsCollection(STATS_COLLECTION_INTERVAL_MS);
}
// Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count.
// Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts).
if ('startMouseModeSync' in this.mux) {
(this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync();
}
// Start mouse mode sync (tmux only) — toggles mouse on/off based on pane count.
// Mouse off = native xterm.js selection; mouse on = tmux pane clicking (split layouts).
// Always start, even with no sessions — new sessions may be created later.
if ('startMouseModeSync' in this.mux) {
(this.mux as { startMouseModeSync: (ms?: number) => void }).startMouseModeSync();
}
if (dead.length > 0) {