chore: bump version to 0.1402

This commit is contained in:
arkon
2026-01-27 23:15:25 +01:00
parent 9fc3d2eda3
commit 46434ee6d0
11 changed files with 2253 additions and 870 deletions
+195 -14
View File
@@ -124,14 +124,29 @@ export interface DetailedPlanResult {
export type ProgressCallback = (phase: string, detail: string) => void;
/** Event types for plan subagent visibility */
export interface PlanSubagentEvent {
type: 'started' | 'progress' | 'completed' | 'failed';
agentId: string;
agentType: 'requirements' | 'architecture' | 'testing' | 'risks' | 'verification';
model: string;
status: string;
detail?: string;
itemCount?: number;
durationMs?: number;
error?: string;
}
export type SubagentCallback = (event: PlanSubagentEvent) => void;
// ============================================================================
// Constants
// ============================================================================
const SUBAGENT_TIMEOUT_MS = 45000; // 45 seconds per subagent
const VERIFICATION_TIMEOUT_MS = 60000; // 60 seconds for verification
const MODEL_ANALYSIS = 'haiku'; // Fast model for parallel analysis
const MODEL_VERIFICATION = 'sonnet'; // Better reasoning for verification
const SUBAGENT_TIMEOUT_MS = 300000; // 5 minutes per subagent (Opus needs time for complex analysis)
const VERIFICATION_TIMEOUT_MS = 480000; // 8 minutes for verification (Opus + large plans)
const MODEL_ANALYSIS = 'opus'; // Best model for thorough analysis
const MODEL_VERIFICATION = 'opus'; // Best model for verification
// ============================================================================
// Subagent Prompts
@@ -375,12 +390,35 @@ Be critical but constructive. A thorough review catches issues that tests miss.`
export class PlanOrchestrator {
private screenManager: ScreenManager;
private workingDir: string;
private runningSessions: Set<Session> = new Set();
private cancelled = false;
constructor(screenManager: ScreenManager, workingDir: string = process.cwd()) {
this.screenManager = screenManager;
this.workingDir = workingDir;
}
/**
* Cancel all running subagent sessions.
* Call this when the client disconnects or user clicks Stop.
*/
async cancel(): Promise<void> {
this.cancelled = true;
console.log(`[PlanOrchestrator] Cancelling ${this.runningSessions.size} running sessions...`);
const stopPromises = Array.from(this.runningSessions).map(async (session) => {
try {
await session.stop();
} catch {
// Ignore cleanup errors
}
});
await Promise.all(stopPromises);
this.runningSessions.clear();
console.log('[PlanOrchestrator] All sessions cancelled');
}
/**
* Generate a detailed implementation plan using subagent orchestration.
*
@@ -391,7 +429,8 @@ export class PlanOrchestrator {
*/
async generateDetailedPlan(
taskDescription: string,
onProgress?: ProgressCallback
onProgress?: ProgressCallback,
onSubagent?: SubagentCallback
): Promise<DetailedPlanResult> {
const startTime = Date.now();
let totalCost = 0;
@@ -399,7 +438,7 @@ export class PlanOrchestrator {
try {
// Phase 1: Parallel Analysis
onProgress?.('parallel-analysis', 'Spawning analysis subagents...');
const subagentResults = await this.runParallelAnalysis(taskDescription, onProgress);
const subagentResults = await this.runParallelAnalysis(taskDescription, onProgress, onSubagent);
totalCost += subagentResults.reduce((sum, r) => sum + (r.success ? 0.002 : 0), 0); // Estimate
@@ -421,7 +460,8 @@ export class PlanOrchestrator {
const verificationResult = await this.runVerification(
taskDescription,
synthesisResult.items,
onProgress
onProgress,
onSubagent
);
totalCost += 0.01; // Verification cost estimate
@@ -462,7 +502,8 @@ export class PlanOrchestrator {
*/
private async runParallelAnalysis(
taskDescription: string,
onProgress?: ProgressCallback
onProgress?: ProgressCallback,
onSubagent?: SubagentCallback
): Promise<SubagentResult[]> {
const subagents: Array<{
type: SubagentResult['agentType'];
@@ -476,7 +517,7 @@ export class PlanOrchestrator {
// Run all subagents in parallel
const promises = subagents.map(({ type, prompt }) =>
this.runSubagent(type, prompt, onProgress)
this.runSubagent(type, prompt, onProgress, onSubagent)
);
return Promise.all(promises);
@@ -488,10 +529,34 @@ export class PlanOrchestrator {
private async runSubagent(
agentType: SubagentResult['agentType'],
prompt: string,
onProgress?: ProgressCallback
onProgress?: ProgressCallback,
onSubagent?: SubagentCallback
): Promise<SubagentResult> {
const agentId = `plan-${agentType}-${Date.now()}`;
// Check if already cancelled
if (this.cancelled) {
return {
agentType,
items: [],
success: false,
error: 'Cancelled',
durationMs: 0,
};
}
const startTime = Date.now();
// Emit started event
onSubagent?.({
type: 'started',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'running',
detail: `Analyzing ${agentType}...`,
});
const session = new Session({
workingDir: this.workingDir,
screenManager: this.screenManager,
@@ -499,6 +564,9 @@ export class PlanOrchestrator {
mode: 'claude',
});
// Track this session for cancellation
this.runningSessions.add(session);
try {
onProgress?.('subagent', `Running ${agentType} analysis...`);
@@ -507,9 +575,38 @@ export class PlanOrchestrator {
this.timeout(SUBAGENT_TIMEOUT_MS),
]);
// Check if cancelled during execution
if (this.cancelled) {
onSubagent?.({
type: 'failed',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'cancelled',
error: 'Cancelled',
durationMs: Date.now() - startTime,
});
return {
agentType,
items: [],
success: false,
error: 'Cancelled',
durationMs: Date.now() - startTime,
};
}
// Parse JSON from result
const jsonMatch = result.match(/\[[\s\S]*\]/);
if (!jsonMatch) {
onSubagent?.({
type: 'failed',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'failed',
error: 'No JSON array found in response',
durationMs: Date.now() - startTime,
});
return {
agentType,
items: [],
@@ -521,6 +618,15 @@ export class PlanOrchestrator {
const parsed = JSON.parse(jsonMatch[0]);
if (!Array.isArray(parsed)) {
onSubagent?.({
type: 'failed',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'failed',
error: 'Response is not an array',
durationMs: Date.now() - startTime,
});
return {
agentType,
items: [],
@@ -544,6 +650,17 @@ export class PlanOrchestrator {
onProgress?.('subagent', `${agentType} complete (${items.length} items)`);
// Emit completed event
onSubagent?.({
type: 'completed',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'completed',
itemCount: items.length,
durationMs: Date.now() - startTime,
});
return {
agentType,
items,
@@ -551,14 +668,26 @@ export class PlanOrchestrator {
durationMs: Date.now() - startTime,
};
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
onSubagent?.({
type: 'failed',
agentId,
agentType,
model: MODEL_ANALYSIS,
status: 'failed',
error: errorMsg,
durationMs: Date.now() - startTime,
});
return {
agentType,
items: [],
success: false,
error: err instanceof Error ? err.message : String(err),
error: errorMsg,
durationMs: Date.now() - startTime,
};
} finally {
// Remove from tracking and clean up
this.runningSessions.delete(session);
try {
await session.stop();
} catch {
@@ -702,8 +831,28 @@ export class PlanOrchestrator {
private async runVerification(
taskDescription: string,
synthesizedItems: PlanItem[],
onProgress?: ProgressCallback
onProgress?: ProgressCallback,
onSubagent?: SubagentCallback
): Promise<VerificationResult> {
const agentId = `plan-verification-${Date.now()}`;
// Check if already cancelled
if (this.cancelled) {
return this.fallbackVerification(synthesizedItems);
}
// Emit started event
onSubagent?.({
type: 'started',
agentId,
agentType: 'verification',
model: MODEL_VERIFICATION,
status: 'running',
detail: 'Validating and prioritizing plan...',
});
const startTime = Date.now();
const session = new Session({
workingDir: this.workingDir,
screenManager: this.screenManager,
@@ -711,6 +860,9 @@ export class PlanOrchestrator {
mode: 'claude',
});
// Track this session for cancellation
this.runningSessions.add(session);
try {
// Format plan for verification
const planText = synthesizedItems
@@ -728,6 +880,11 @@ export class PlanOrchestrator {
this.timeout(VERIFICATION_TIMEOUT_MS),
]);
// Check if cancelled during execution
if (this.cancelled) {
return this.fallbackVerification(synthesizedItems);
}
// Parse JSON from result
const jsonMatch = result.match(/\{[\s\S]*\}/);
if (!jsonMatch) {
@@ -787,6 +944,17 @@ export class PlanOrchestrator {
onProgress?.('verification', `Verification complete (quality: ${Math.round((parsed.qualityScore || 0.8) * 100)}%)`);
// Emit completed event
onSubagent?.({
type: 'completed',
agentId,
agentType: 'verification',
model: MODEL_VERIFICATION,
status: 'completed',
itemCount: validatedPlan.length,
durationMs: Date.now() - startTime,
});
return {
validatedPlan,
gaps: Array.isArray(parsed.gaps) ? parsed.gaps.map(String) : [],
@@ -795,8 +963,19 @@ export class PlanOrchestrator {
};
} catch (err) {
console.error('[PlanOrchestrator] Verification failed:', err);
onSubagent?.({
type: 'failed',
agentId,
agentType: 'verification',
model: MODEL_VERIFICATION,
status: 'failed',
error: err instanceof Error ? err.message : String(err),
durationMs: Date.now() - startTime,
});
return this.fallbackVerification(synthesizedItems);
} finally {
// Remove from tracking and clean up
this.runningSessions.delete(session);
try {
await session.stop();
} catch {
@@ -808,8 +987,10 @@ export class PlanOrchestrator {
/**
* Fallback verification when the verification subagent fails.
* Assigns heuristic priorities and adds basic verification criteria.
* Note: This is a silent fallback - no warning shown to user since the result is still useful.
*/
private fallbackVerification(items: PlanItem[]): VerificationResult {
console.log('[PlanOrchestrator] Using heuristic priorities (verification subagent timed out or failed)');
return {
validatedPlan: items.map((item, idx) => ({
...item,
@@ -824,8 +1005,8 @@ export class PlanOrchestrator {
version: 1,
})),
gaps: [],
warnings: ['Verification subagent failed - using heuristic priorities'],
qualityScore: 0.7,
warnings: [], // Silent fallback - heuristics are good enough, no need to alarm user
qualityScore: 0.75, // Slightly higher since fallback still produces useful results
};
}
+416 -1
View File
@@ -31,6 +31,72 @@ import {
createInitialCircuitBreakerStatus,
} from './types.js';
// ========== Enhanced Plan Task Interface ==========
/** Task execution status for plan tracking */
export type PlanTaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked';
/** TDD phase categories */
export type TddPhase = 'setup' | 'test' | 'impl' | 'verify' | 'review';
/**
* Enhanced plan task with verification criteria, dependencies, and execution tracking.
* Supports TDD workflow, failure tracking, and plan versioning.
*/
export interface EnhancedPlanTask {
/** Unique identifier (e.g., "P0-001") */
id: string;
/** Task description */
content: string;
/** Criticality level */
priority: 'P0' | 'P1' | 'P2' | null;
/** How to verify completion */
verificationCriteria?: string;
/** Command to run for verification */
testCommand?: string;
/** IDs of tasks that must complete first */
dependencies: string[];
/** Current execution status */
status: PlanTaskStatus;
/** How many times attempted */
attempts: number;
/** Most recent failure reason */
lastError?: string;
/** Timestamp of completion */
completedAt?: number;
/** Plan version this belongs to */
version: number;
/** TDD phase category */
tddPhase?: TddPhase;
/** ID of paired test/impl task */
pairedWith?: string;
/** Estimated complexity */
complexity?: 'low' | 'medium' | 'high';
/** Checklist items for review tasks (tddPhase: 'review') */
reviewChecklist?: string[];
}
/** Checkpoint review data */
export interface CheckpointReview {
iteration: number;
timestamp: number;
summary: {
total: number;
completed: number;
failed: number;
blocked: number;
pending: number;
inProgress: number;
};
stuckTasks: Array<{
id: string;
content: string;
attempts: number;
lastError?: string;
}>;
recommendations: string[];
}
// ========== Configuration Constants ==========
/**
@@ -444,6 +510,28 @@ export class RalphTracker extends EventEmitter {
/** Path to the @fix_plan.md file being watched */
private _fixPlanPath: string | null = null;
// ========== Enhanced Plan Management ==========
/** Current version of the plan (incremented on changes) */
private _planVersion: number = 1;
/** History of plan versions for rollback support */
private _planHistory: Array<{
version: number;
timestamp: number;
tasks: Map<string, EnhancedPlanTask>;
summary: string;
}> = [];
/** Enhanced plan tasks with execution tracking */
private _planTasks: Map<string, EnhancedPlanTask> = new Map();
/** Checkpoint intervals (iterations at which to trigger review) */
private _checkpointIterations: number[] = [5, 10, 20, 30, 50, 75, 100];
/** Last checkpoint iteration */
private _lastCheckpointIteration: number = 0;
/**
* Creates a new RalphTracker instance.
* Starts in disabled state until Ralph patterns are detected.
@@ -778,7 +866,11 @@ export class RalphTracker extends EventEmitter {
* @returns Shallow copy of loop state (safe to modify)
*/
get loopState(): RalphTrackerState {
return { ...this._loopState };
return {
...this._loopState,
planVersion: this._planVersion,
planHistoryLength: this._planHistory.length,
};
}
/**
@@ -2338,4 +2430,327 @@ export class RalphTracker extends EventEmitter {
return newTodos.length;
}
// ========== Enhanced Plan Management Methods ==========
/**
* Initialize plan tasks from generated plan items.
* Called when wizard generates a new plan.
*/
initializePlanTasks(items: Array<{
id?: string;
content: string;
priority?: 'P0' | 'P1' | 'P2' | null;
verificationCriteria?: string;
testCommand?: string;
dependencies?: string[];
tddPhase?: TddPhase;
pairedWith?: string;
complexity?: 'low' | 'medium' | 'high';
}>): void {
// Save current plan to history before replacing
if (this._planTasks.size > 0) {
this._savePlanToHistory('Plan replaced with new generation');
}
// Clear and rebuild
this._planTasks.clear();
this._planVersion++;
items.forEach((item, idx) => {
const id = item.id || `task-${idx}`;
const task: EnhancedPlanTask = {
id,
content: item.content,
priority: item.priority || null,
verificationCriteria: item.verificationCriteria,
testCommand: item.testCommand,
dependencies: item.dependencies || [],
status: 'pending',
attempts: 0,
version: this._planVersion,
tddPhase: item.tddPhase,
pairedWith: item.pairedWith,
complexity: item.complexity,
};
this._planTasks.set(id, task);
});
this.emit('planInitialized', { version: this._planVersion, taskCount: this._planTasks.size });
}
/**
* Update a specific plan task's status, attempts, or error.
*/
updatePlanTask(taskId: string, update: {
status?: PlanTaskStatus;
error?: string;
incrementAttempts?: boolean;
}): { success: boolean; task?: EnhancedPlanTask; error?: string } {
const task = this._planTasks.get(taskId);
if (!task) {
return { success: false, error: 'Task not found' };
}
if (update.status) {
task.status = update.status;
if (update.status === 'completed') {
task.completedAt = Date.now();
}
}
if (update.error) {
task.lastError = update.error;
}
if (update.incrementAttempts) {
task.attempts++;
// After 3 failed attempts, mark as blocked and emit warning
if (task.attempts >= 3 && task.status === 'failed') {
task.status = 'blocked';
this.emit('taskBlocked', {
taskId,
content: task.content,
attempts: task.attempts,
lastError: task.lastError,
});
}
}
// Update blocked tasks when a dependency completes
if (update.status === 'completed') {
this._unblockDependentTasks(taskId);
}
// Check for checkpoint
this._checkForCheckpoint();
this.emit('planTaskUpdate', { taskId, task });
return { success: true, task };
}
/**
* Unblock tasks that were waiting on a completed dependency.
*/
private _unblockDependentTasks(completedTaskId: string): void {
for (const [_, task] of this._planTasks) {
if (task.dependencies.includes(completedTaskId)) {
// Check if all dependencies are now complete
const allDepsComplete = task.dependencies.every(depId => {
const dep = this._planTasks.get(depId);
return dep && dep.status === 'completed';
});
if (allDepsComplete && task.status === 'blocked') {
task.status = 'pending';
this.emit('taskUnblocked', { taskId: task.id });
}
}
}
}
/**
* Check if current iteration is a checkpoint and emit review if so.
*/
private _checkForCheckpoint(): void {
const currentIteration = this._loopState.cycleCount;
if (this._checkpointIterations.includes(currentIteration) &&
currentIteration > this._lastCheckpointIteration) {
this._lastCheckpointIteration = currentIteration;
const checkpoint = this.generateCheckpointReview();
this.emit('planCheckpoint', checkpoint);
}
}
/**
* Generate a checkpoint review summarizing plan progress and stuck tasks.
*/
generateCheckpointReview(): CheckpointReview {
const tasks = Array.from(this._planTasks.values());
const summary = {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
blocked: tasks.filter(t => t.status === 'blocked').length,
pending: tasks.filter(t => t.status === 'pending').length,
inProgress: tasks.filter(t => t.status === 'in_progress').length,
};
// Find stuck tasks (3+ attempts or blocked)
const stuckTasks = tasks
.filter(t => t.attempts >= 3 || t.status === 'blocked')
.map(t => ({
id: t.id,
content: t.content,
attempts: t.attempts,
lastError: t.lastError,
}));
// Generate recommendations
const recommendations: string[] = [];
if (stuckTasks.length > 0) {
recommendations.push(`${stuckTasks.length} task(s) are stuck. Consider breaking them into smaller steps.`);
}
if (summary.failed > summary.completed && summary.total > 5) {
recommendations.push('More tasks have failed than completed. Review approach and consider plan adjustment.');
}
const progressPercent = Math.round((summary.completed / summary.total) * 100);
if (progressPercent < 20 && this._loopState.cycleCount > 10) {
recommendations.push('Progress is slow. Consider simplifying tasks or reviewing dependencies.');
}
if (summary.blocked > summary.total / 3) {
recommendations.push('Many tasks are blocked. Review dependency chain for bottlenecks.');
}
return {
iteration: this._loopState.cycleCount,
timestamp: Date.now(),
summary,
stuckTasks,
recommendations,
};
}
/**
* Save current plan state to history.
*/
private _savePlanToHistory(summary: string): void {
// Clone current tasks
const tasksCopy = new Map<string, EnhancedPlanTask>();
for (const [id, task] of this._planTasks) {
tasksCopy.set(id, { ...task });
}
this._planHistory.push({
version: this._planVersion,
timestamp: Date.now(),
tasks: tasksCopy,
summary,
});
// Limit history size
if (this._planHistory.length > 10) {
this._planHistory.shift();
}
}
/**
* Get plan version history.
*/
getPlanHistory(): Array<{
version: number;
timestamp: number;
summary: string;
stats: { total: number; completed: number; failed: number };
}> {
return this._planHistory.map(h => {
const tasks = Array.from(h.tasks.values());
return {
version: h.version,
timestamp: h.timestamp,
summary: h.summary,
stats: {
total: tasks.length,
completed: tasks.filter(t => t.status === 'completed').length,
failed: tasks.filter(t => t.status === 'failed').length,
},
};
});
}
/**
* Rollback to a previous plan version.
*/
rollbackToVersion(version: number): { success: boolean; plan?: EnhancedPlanTask[]; error?: string } {
const historyEntry = this._planHistory.find(h => h.version === version);
if (!historyEntry) {
return { success: false, error: `Version ${version} not found in history` };
}
// Save current state first
this._savePlanToHistory(`Rolled back from v${this._planVersion} to v${version}`);
// Restore the historical version
this._planTasks.clear();
for (const [id, task] of historyEntry.tasks) {
// Reset execution state for retry
this._planTasks.set(id, {
...task,
status: task.status === 'completed' ? 'completed' : 'pending',
attempts: task.status === 'completed' ? task.attempts : 0,
lastError: undefined,
});
}
this._planVersion++;
this.emit('planRollback', { version, newVersion: this._planVersion });
return { success: true, plan: Array.from(this._planTasks.values()) };
}
/**
* Add a new task to the plan (for runtime adaptation).
*/
addPlanTask(task: {
content: string;
priority?: 'P0' | 'P1' | 'P2';
verificationCriteria?: string;
dependencies?: string[];
insertAfter?: string;
}): { task: EnhancedPlanTask } {
// Generate unique ID
const existingIds = Array.from(this._planTasks.keys());
const prefix = task.priority || 'P1';
let counter = existingIds.filter(id => id.startsWith(prefix)).length + 1;
let id = `${prefix}-${String(counter).padStart(3, '0')}`;
while (this._planTasks.has(id)) {
counter++;
id = `${prefix}-${String(counter).padStart(3, '0')}`;
}
const newTask: EnhancedPlanTask = {
id,
content: task.content,
priority: task.priority || null,
verificationCriteria: task.verificationCriteria || 'Task completed successfully',
dependencies: task.dependencies || [],
status: 'pending',
attempts: 0,
version: this._planVersion,
};
this._planTasks.set(id, newTask);
this.emit('planTaskAdded', { task: newTask });
return { task: newTask };
}
/**
* Get all plan tasks.
*/
getPlanTasks(): EnhancedPlanTask[] {
return Array.from(this._planTasks.values());
}
/**
* Get current plan version.
*/
get planVersion(): number {
return this._planVersion;
}
/**
* Check if checkpoint review is due for current iteration.
*/
isCheckpointDue(): boolean {
const currentIteration = this._loopState.cycleCount;
return this._checkpointIterations.includes(currentIteration) &&
currentIteration > this._lastCheckpointIteration;
}
}
+4
View File
@@ -738,6 +738,10 @@ export interface RalphTrackerState {
lastActivity: number;
/** Elapsed hours if detected */
elapsedHours: number | null;
/** Current plan version (for versioning UI) */
planVersion?: number;
/** Number of versions in history (for versioning UI) */
planHistoryLength?: number;
}
/**
+349 -26
View File
@@ -464,6 +464,11 @@ class ClaudemanApp {
this._subagentHideTimeout = null; // Timeout for hover-based dropdown hide
this.ralphStatePanelCollapsed = true; // Default to collapsed
// Plan subagent windows (visible Opus agents during plan generation)
this.planSubagents = new Map(); // Map<agentId, { type, model, status, startTime, element }>
this.planSubagentWindowZIndex = 1100;
this.planGenerationStopped = false; // Flag to ignore SSE events after Stop
// Project Insights tracking (active Bash tools with clickable file paths)
this.projectInsights = new Map(); // Map<sessionId, ActiveBashTool[]>
this.logViewerWindows = new Map(); // Map<windowId, { element, eventSource, filePath }>
@@ -1907,6 +1912,13 @@ class ClaudemanApp {
}, 5 * 60 * 1000); // 5 minutes
});
// Plan subagent visibility events (show Opus agents during plan generation)
addListener('plan:subagent', (e) => {
const data = JSON.parse(e.data);
console.log('[Plan Subagent]', data);
this.handlePlanSubagentEvent(data);
});
// Plan generation progress events (for detailed mode with subagents)
addListener('plan:progress', (e) => {
const data = JSON.parse(e.data);
@@ -2578,7 +2590,7 @@ class ClaudemanApp {
generatedPlan: null, // [{content, priority, enabled, id}] or null
planGenerated: false,
skipPlanGeneration: false,
planDetailLevel: 'standard', // 'brief', 'standard', 'detailed'
planDetailLevel: 'detailed', // 'brief', 'standard', 'detailed'
};
planLoadingTimer = null;
planLoadingStartTime = null;
@@ -2596,7 +2608,7 @@ class ClaudemanApp {
generatedPlan: null,
planGenerated: false,
skipPlanGeneration: false,
planDetailLevel: 'standard',
planDetailLevel: 'detailed',
// Existing plan detection
existingPlan: null, // { todos, stats, content } from @fix_plan.md
useExistingPlan: false, // User chose to use existing plan
@@ -2620,10 +2632,16 @@ class ClaudemanApp {
this.updateRalphWizardUI();
document.getElementById('ralphWizardModal').classList.add('active');
document.getElementById('ralphTaskDescription').focus();
// Update connection lines to show wizard connections
this.updateConnectionLines();
}
closeRalphWizard() {
document.getElementById('ralphWizardModal')?.classList.remove('active');
// Update connection lines (wizard closed, revert to normal)
this.updateConnectionLines();
}
populateRalphCaseSelector() {
@@ -2925,6 +2943,10 @@ class ClaudemanApp {
const spinnerEl = document.querySelector('.plan-spinner');
if (spinnerEl) spinnerEl.style.display = '';
// Reset stopped indicator
const stoppedIndicator = document.getElementById('planStoppedIndicator');
if (stoppedIndicator) stoppedIndicator.style.display = 'none';
// Reset existing plan badge
const badge = document.getElementById('existingPlanBadge');
if (badge) badge.style.display = 'none';
@@ -2937,6 +2959,9 @@ class ClaudemanApp {
// Stop any existing generation first
this.stopPlanGeneration();
// Reset stopped flag to allow new SSE events
this.planGenerationStopped = false;
// Create abort controller for this generation
this.planGenerationAbortController = new AbortController();
@@ -3164,6 +3189,10 @@ class ClaudemanApp {
const errorEl = document.getElementById('planGenerationError');
const msgEl = document.getElementById('planErrorMsg');
const stoppedIndicator = document.getElementById('planStoppedIndicator');
// Hide the stopped indicator for real errors, show error message
if (stoppedIndicator) stoppedIndicator.style.display = 'none';
if (msgEl) msgEl.textContent = message;
errorEl?.classList.remove('hidden');
}
@@ -3176,6 +3205,9 @@ class ClaudemanApp {
document.getElementById('planGenerationError')?.classList.add('hidden');
document.getElementById('planEditor')?.classList.remove('hidden');
// Close plan subagent windows since generation is complete
this.closePlanSubagentWindows();
const list = document.getElementById('planItemsList');
if (!list) return;
@@ -3565,13 +3597,183 @@ class ClaudemanApp {
cancelPlanGeneration() {
this.stopPlanGeneration();
this.showToast('Plan generation cancelled', 'info');
this.planGenerationStopped = true; // Ignore future SSE events
this.showToast('Plan generation stopped', 'info');
// Allow user to proceed without a plan by clicking Next
this.ralphWizardConfig.skipPlanGeneration = true;
// Show stopped state with clear visual indicator
const errorEl = document.getElementById('planGenerationError');
const msgEl = document.getElementById('planErrorMsg');
if (msgEl) msgEl.textContent = 'Plan generation was cancelled.';
errorEl?.classList.remove('hidden');
const stoppedIndicator = document.getElementById('planStoppedIndicator');
// Show the stopped indicator, hide error message since this was intentional
if (stoppedIndicator) stoppedIndicator.style.display = 'flex';
if (msgEl) msgEl.textContent = '';
// Hide spinner immediately and show stopped state
document.getElementById('planGenerationLoading')?.classList.add('hidden');
errorEl?.classList.remove('hidden');
// Close all plan subagent windows
this.closePlanSubagentWindows();
}
// ========== Plan Subagent Windows (Visible Opus Agents) ==========
handlePlanSubagentEvent(event) {
// Ignore events if user has stopped plan generation
if (this.planGenerationStopped) return;
const { type, agentId, agentType, model, status, detail, itemCount, durationMs, error } = event;
if (type === 'started') {
// Create new plan subagent window
this.createPlanSubagentWindow(agentId, agentType, model, detail);
} else if (type === 'completed' || type === 'failed') {
// Update window to show completion/failure
this.updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error);
} else if (type === 'progress') {
// Update progress detail
const windowData = this.planSubagents.get(agentId);
if (windowData?.element) {
const detailEl = windowData.element.querySelector('.plan-subagent-detail');
if (detailEl) detailEl.textContent = detail || '';
}
}
}
createPlanSubagentWindow(agentId, agentType, model, detail) {
if (this.planSubagents.has(agentId)) return;
const wizardModal = document.getElementById('ralphWizardModal');
const wizardContent = wizardModal?.querySelector('.modal-content');
const wizardRect = wizardContent?.getBoundingClientRect();
// Calculate position - alternate left/right of wizard
const windowWidth = 280;
const windowHeight = 100;
const windowCount = this.planSubagents.size;
const viewportWidth = window.innerWidth;
let x, y;
if (wizardRect) {
const centerX = wizardRect.left + wizardRect.width / 2;
const gap = 30;
// Stack windows vertically on each side
const side = windowCount % 2 === 0 ? 'right' : 'left';
const sideIndex = Math.floor(windowCount / 2);
if (side === 'right') {
x = wizardRect.right + gap;
} else {
x = wizardRect.left - windowWidth - gap;
}
y = wizardRect.top + sideIndex * (windowHeight + 15);
// Clamp to viewport
x = Math.max(10, Math.min(x, viewportWidth - windowWidth - 10));
y = Math.max(60, Math.min(y, window.innerHeight - windowHeight - 10));
} else {
// Fallback positioning
x = 50 + windowCount * 30;
y = 120 + windowCount * 30;
}
// Create window element
const win = document.createElement('div');
win.className = 'plan-subagent-window';
win.id = `plan-subagent-${agentId}`;
win.style.left = `${x}px`;
win.style.top = `${y}px`;
win.style.zIndex = ++this.planSubagentWindowZIndex;
const typeLabels = {
requirements: 'Requirements Analyst',
architecture: 'Architecture Planner',
testing: 'TDD Specialist',
risks: 'Risk Analyst',
verification: 'Verification Expert',
};
const typeIcons = {
requirements: '📋',
architecture: '🏗️',
testing: '🧪',
risks: '⚠️',
verification: '✓',
};
win.innerHTML = `
<div class="plan-subagent-header">
<span class="plan-subagent-icon">${typeIcons[agentType] || '🤖'}</span>
<span class="plan-subagent-title">${typeLabels[agentType] || agentType}</span>
<span class="plan-subagent-model">${model}</span>
</div>
<div class="plan-subagent-body">
<div class="plan-subagent-status running">
<span class="plan-subagent-spinner"></span>
<span class="plan-subagent-status-text">Running...</span>
</div>
<div class="plan-subagent-detail">${detail || ''}</div>
</div>
`;
document.body.appendChild(win);
// Store reference
this.planSubagents.set(agentId, {
type: agentType,
model,
status: 'running',
startTime: Date.now(),
element: win,
});
// Update connection lines
this.updateConnectionLines();
}
updatePlanSubagentWindow(agentId, status, itemCount, durationMs, error) {
const windowData = this.planSubagents.get(agentId);
if (!windowData?.element) return;
const win = windowData.element;
const statusEl = win.querySelector('.plan-subagent-status');
const statusTextEl = win.querySelector('.plan-subagent-status-text');
const spinnerEl = win.querySelector('.plan-subagent-spinner');
const detailEl = win.querySelector('.plan-subagent-detail');
windowData.status = status;
if (status === 'completed') {
statusEl?.classList.remove('running');
statusEl?.classList.add('completed');
if (spinnerEl) spinnerEl.style.display = 'none';
if (statusTextEl) statusTextEl.textContent = `Done (${itemCount || 0} items)`;
if (detailEl && durationMs) detailEl.textContent = `${(durationMs / 1000).toFixed(1)}s`;
} else if (status === 'failed' || status === 'cancelled') {
statusEl?.classList.remove('running');
statusEl?.classList.add('failed');
if (spinnerEl) spinnerEl.style.display = 'none';
if (statusTextEl) statusTextEl.textContent = status === 'cancelled' ? 'Cancelled' : 'Failed';
if (detailEl) detailEl.textContent = error || '';
}
// Update connection lines
this.updateConnectionLines();
}
closePlanSubagentWindows() {
for (const [agentId, windowData] of this.planSubagents) {
if (windowData.element) {
windowData.element.remove();
}
}
this.planSubagents.clear();
this.updateConnectionLines();
}
skipPlanGeneration() {
@@ -7768,34 +7970,118 @@ class ClaudemanApp {
svg.innerHTML = '';
// Check if Ralph wizard modal is open
const wizardModal = document.getElementById('ralphWizardModal');
const wizardOpen = wizardModal && !wizardModal.classList.contains('hidden') &&
wizardModal.style.display !== 'none';
const wizardContent = wizardOpen ? wizardModal.querySelector('.modal-content') : null;
for (const [agentId, windowInfo] of this.subagentWindows) {
if (windowInfo.minimized || windowInfo.hidden) continue;
const agent = this.subagents.get(agentId);
if (!agent?.parentSessionId) continue;
const tab = document.querySelector(`.session-tab[data-id="${agent.parentSessionId}"]`);
const win = windowInfo.element;
if (!tab || !win) continue;
if (!win) continue;
const tabRect = tab.getBoundingClientRect();
const winRect = win.getBoundingClientRect();
// Draw curved line from tab bottom-center to window top-center
const x1 = tabRect.left + tabRect.width / 2;
const y1 = tabRect.bottom;
const x2 = winRect.left + winRect.width / 2;
const y2 = winRect.top;
// If wizard is open, draw lines from wizard to subagent windows
if (wizardOpen && wizardContent) {
const wizardRect = wizardContent.getBoundingClientRect();
// Bezier curve control points for smooth curve
const midY = (y1 + y2) / 2;
const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
// Determine which side of wizard the window is on
const winCenterX = winRect.left + winRect.width / 2;
const wizardCenterX = wizardRect.left + wizardRect.width / 2;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
line.setAttribute('class', 'connection-line');
line.setAttribute('data-agent-id', agentId);
svg.appendChild(line);
let x1, y1, x2, y2;
if (winCenterX < wizardCenterX) {
// Window is on the left - connect from wizard left edge
x1 = wizardRect.left;
y1 = wizardRect.top + wizardRect.height / 2;
x2 = winRect.right;
y2 = winRect.top + winRect.height / 2;
} else {
// Window is on the right - connect from wizard right edge
x1 = wizardRect.right;
y1 = wizardRect.top + wizardRect.height / 2;
x2 = winRect.left;
y2 = winRect.top + winRect.height / 2;
}
// Bezier curve for smooth horizontal connection
const midX = (x1 + x2) / 2;
const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
line.setAttribute('class', 'connection-line wizard-connection');
line.setAttribute('data-agent-id', agentId);
svg.appendChild(line);
} else if (agent?.parentSessionId) {
// Normal mode - connect to parent session tab
const tab = document.querySelector(`.session-tab[data-id="${agent.parentSessionId}"]`);
if (!tab) continue;
const tabRect = tab.getBoundingClientRect();
// Draw curved line from tab bottom-center to window top-center
const x1 = tabRect.left + tabRect.width / 2;
const y1 = tabRect.bottom;
const x2 = winRect.left + winRect.width / 2;
const y2 = winRect.top;
// Bezier curve control points for smooth curve
const midY = (y1 + y2) / 2;
const path = `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
line.setAttribute('class', 'connection-line');
line.setAttribute('data-agent-id', agentId);
svg.appendChild(line);
}
}
// Draw lines from wizard to plan subagent windows (Opus agents during plan generation)
if (wizardOpen && wizardContent && this.planSubagents.size > 0) {
const wizardRect = wizardContent.getBoundingClientRect();
for (const [agentId, windowData] of this.planSubagents) {
const win = windowData.element;
if (!win) continue;
const winRect = win.getBoundingClientRect();
// Determine which side of wizard the window is on
const winCenterX = winRect.left + winRect.width / 2;
const wizardCenterX = wizardRect.left + wizardRect.width / 2;
let x1, y1, x2, y2;
if (winCenterX < wizardCenterX) {
// Window is on the left
x1 = wizardRect.left;
y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50);
x2 = winRect.right;
y2 = winRect.top + winRect.height / 2;
} else {
// Window is on the right
x1 = wizardRect.right;
y1 = wizardRect.top + wizardRect.height / 3 + (this.planSubagents.size > 3 ? 0 : 50);
x2 = winRect.left;
y2 = winRect.top + winRect.height / 2;
}
const midX = (x1 + x2) / 2;
const path = `M ${x1} ${y1} C ${midX} ${y1}, ${midX} ${y2}, ${x2} ${y2}`;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'path');
line.setAttribute('d', path);
line.setAttribute('class', 'connection-line wizard-connection plan-subagent-line');
line.setAttribute('data-plan-agent-id', agentId);
svg.appendChild(line);
}
}
}
@@ -7874,11 +8160,48 @@ class ClaudemanApp {
const windowWidth = 420;
const windowHeight = 350;
const gap = 20;
const startX = 50;
const startY = 120;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
// Check if Ralph wizard modal is open - if so, position windows on the sides
const wizardModal = document.getElementById('ralphWizardModal');
const wizardOpen = wizardModal && !wizardModal.classList.contains('hidden') &&
wizardModal.style.display !== 'none';
let startX, startY, maxCols;
if (wizardOpen) {
// Wizard is ~720px wide, centered. Position windows on left/right sides
const wizardWidth = 720;
const centerX = viewportWidth / 2;
const wizardLeft = centerX - wizardWidth / 2;
const wizardRight = centerX + wizardWidth / 2;
// Alternate between left and right sides of the wizard
const leftSideSpace = wizardLeft - 20;
const rightSideSpace = viewportWidth - wizardRight - 20;
if (windowCount % 2 === 0 && rightSideSpace >= windowWidth) {
// Even windows go to the right
startX = wizardRight + 20;
maxCols = Math.floor(rightSideSpace / (windowWidth + gap)) || 1;
} else if (leftSideSpace >= windowWidth) {
// Odd windows go to the left
startX = Math.max(10, wizardLeft - windowWidth - 20);
maxCols = 1; // Usually only room for 1 column on left
} else {
// Not enough side space, use right side
startX = wizardRight + 20;
maxCols = 1;
}
startY = 80; // Start higher when wizard is open
} else {
// Normal positioning
startX = 50;
startY = 120;
maxCols = Math.floor((viewportWidth - startX - 50) / (windowWidth + gap)) || 1;
}
const maxRows = Math.floor((viewportHeight - startY - 50) / (windowHeight + gap)) || 1;
const col = windowCount % maxCols;
const row = Math.floor(windowCount / maxCols) % maxRows; // Wrap rows to stay in viewport
+24 -6
View File
@@ -172,6 +172,13 @@
<span class="ralph-info-label">Elapsed</span>
<span id="ralphElapsed">0m</span>
</div>
<div class="ralph-info-row" id="ralphVersionRow" style="display: none;">
<span class="ralph-info-label">Plan</span>
<div class="ralph-version-info">
<span class="version-badge" id="ralphPlanVersion">v1</span>
<button class="rollback-btn" id="ralphRollbackBtn" onclick="app.showPlanHistory()" title="View history">history</button>
</div>
</div>
</div>
<div class="ralph-tasks-grid" id="ralphTasksGrid"></div>
</div>
@@ -1013,16 +1020,23 @@
</div>
</div>
<p class="plan-loading-hint" id="planLoadingHint">Initializing deep reasoning model</p>
<button class="btn-toolbar btn-sm plan-skip-btn" onclick="app.skipPlanGeneration()">Skip</button>
<div class="plan-loading-actions">
<button class="btn-toolbar btn-sm plan-cancel-btn" onclick="app.cancelPlanGeneration()">Stop</button>
<button class="btn-toolbar btn-sm" onclick="app.regeneratePlan()">Regenerate</button>
</div>
</div>
<!-- Error state -->
<!-- Stopped/Error state -->
<div id="planGenerationError" class="plan-section hidden">
<div class="plan-stopped-indicator" id="planStoppedIndicator">
<span class="plan-stopped-icon">&#x25A0;</span>
<span class="plan-stopped-text">Stopped</span>
</div>
<p class="plan-error-msg" id="planErrorMsg"></p>
<div class="plan-actions">
<button class="btn-toolbar btn-primary" onclick="app.generatePlan()">Retry</button>
<button class="btn-toolbar" onclick="app.skipPlanGeneration()">Skip</button>
<button class="btn-toolbar btn-primary" onclick="app.generatePlan()">Regenerate Plan</button>
</div>
<p class="plan-skip-hint">Use <strong>Back</strong> to edit your task, or <strong>Next</strong> to continue without a plan</p>
</div>
<!-- Editor state -->
@@ -1034,8 +1048,8 @@
<label>Detail:</label>
<div class="plan-detail-btns">
<button type="button" class="plan-detail-btn" data-detail="brief" onclick="app.setPlanDetail('brief')" title="High-level milestones only">Brief</button>
<button type="button" class="plan-detail-btn active" data-detail="standard" onclick="app.setPlanDetail('standard')" title="Single-pass generation with Opus 4.5">Standard</button>
<button type="button" class="plan-detail-btn" data-detail="detailed" onclick="app.setPlanDetail('detailed')" title="Enhanced: 4 parallel subagents + verification (slower but more thorough)">Enhanced</button>
<button type="button" class="plan-detail-btn" data-detail="standard" onclick="app.setPlanDetail('standard')" title="Single-pass generation with Opus 4.5">Standard</button>
<button type="button" class="plan-detail-btn active" data-detail="detailed" onclick="app.setPlanDetail('detailed')" title="Enhanced: 4 parallel subagents + verification (slower but more thorough)">Enhanced</button>
</div>
</div>
<button class="btn-toolbar btn-sm" onclick="app.regeneratePlan()">Regenerate</button>
@@ -1045,6 +1059,10 @@
<div id="planItemsList" class="plan-items-list"></div>
<div class="plan-editor-footer">
<button class="btn-toolbar btn-sm" onclick="app.addPlanItem()">+ Add Step</button>
<div class="plan-view-toggle">
<button type="button" class="plan-view-btn active" data-view="list" onclick="app.setPlanViewMode('list')" title="Flat list">&#x2630;</button>
<button type="button" class="plan-view-btn" data-view="grouped" onclick="app.setPlanViewMode('grouped')" title="Group by TDD phase">&#x229E;</button>
</div>
</div>
</div>
</div>
+738 -7
View File
@@ -4692,6 +4692,19 @@ kbd {
stroke-dashoffset: 1000;
}
/* Wizard connection lines - different color to show plan generation link */
.connection-line.wizard-connection {
stroke: #60a5fa;
filter: drop-shadow(0 0 4px #60a5fa);
stroke-dasharray: 8 4;
animation: wizard-pulse 1.5s ease-in-out infinite;
}
@keyframes wizard-pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 0.9; }
}
/* ========== Project Insights Panel (Bash File Viewers) ========== */
.project-insights-panel {
@@ -5366,10 +5379,23 @@ kbd {
gap: 0.5rem;
}
/* Wizard Modal - wider layout */
/* Wizard Modal - wider layout with height constraints */
.modal-wizard {
width: 720px;
max-width: 95vw;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.modal-wizard .modal-body {
overflow-y: auto;
flex: 1;
min-height: 0;
}
.modal-wizard .wizard-footer {
flex-shrink: 0;
}
.wizard-page-compact {
@@ -5641,20 +5667,158 @@ kbd {
display: flex;
flex-direction: column;
gap: 0.35rem;
max-height: 300px;
max-height: 200px;
overflow-y: auto;
margin-bottom: 0.5rem;
}
.plan-item {
.plan-loading-actions {
display: flex;
gap: 0.5rem;
justify-content: center;
}
.plan-cancel-btn {
color: var(--red);
}
/* Plan Stopped State */
.plan-stopped-indicator {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
background: var(--bg-input);
border-radius: 4px;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
padding: 0.75rem;
background: var(--surface);
border-radius: 6px;
border: 1px solid var(--border);
}
.plan-stopped-icon {
color: var(--red);
font-size: 1rem;
}
.plan-stopped-text {
font-weight: 500;
color: var(--text);
}
.plan-actions-single {
justify-content: center;
}
.plan-skip-hint {
font-size: 0.75rem;
color: var(--text-muted);
text-align: center;
margin-top: 0.75rem;
}
.plan-skip-hint strong {
color: var(--text-dim);
}
/* ========== Plan Subagent Windows (Visible Opus Agents) ========== */
.plan-subagent-window {
position: fixed;
width: 280px;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 0 20px rgba(96, 165, 250, 0.2);
overflow: hidden;
animation: planSubagentSpawn 0.3s ease-out;
}
@keyframes planSubagentSpawn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.plan-subagent-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: linear-gradient(135deg, rgba(96, 165, 250, 0.2), rgba(96, 165, 250, 0.1));
border-bottom: 1px solid var(--border);
}
.plan-subagent-icon {
font-size: 1rem;
}
.plan-subagent-title {
flex: 1;
font-weight: 500;
font-size: 0.8rem;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.plan-subagent-model {
font-size: 0.65rem;
padding: 0.15rem 0.4rem;
background: #7c3aed;
color: white;
border-radius: 3px;
text-transform: uppercase;
font-weight: 600;
}
.plan-subagent-body {
padding: 0.75rem;
}
.plan-subagent-status {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.35rem;
}
.plan-subagent-status.running .plan-subagent-spinner {
width: 14px;
height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.plan-subagent-status.completed .plan-subagent-status-text {
color: var(--green);
}
.plan-subagent-status.failed .plan-subagent-status-text {
color: var(--red);
}
.plan-subagent-status-text {
font-size: 0.8rem;
font-weight: 500;
}
.plan-subagent-detail {
font-size: 0.7rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Note: main .plan-item styles are in "Enhanced Plan Item Styling" section below */
.plan-item-checkbox {
flex-shrink: 0;
width: 14px;
@@ -5701,6 +5865,8 @@ kbd {
color: var(--red);
}
/* Note: .plan-item-phase and .plan-item-verify enhanced styles are in section below */
.plan-item:has(.plan-item-checkbox:not(:checked)) {
opacity: 0.5;
}
@@ -5761,6 +5927,571 @@ kbd {
color: #fbbf24;
}
/* Enhanced Plan Item Styling */
.plan-item {
display: flex;
align-items: center;
gap: 0.4rem;
padding: 0.35rem 0.5rem;
background: var(--bg-input);
border-radius: 4px;
border-left: 3px solid transparent;
transition: border-color 0.15s, background-color 0.15s, transform 0.15s;
cursor: default;
}
.plan-item.dragging {
opacity: 0.5;
transform: scale(0.98);
}
.plan-item.drag-over {
background: var(--accent-dim);
border-left-color: var(--accent);
}
/* Priority color borders */
.plan-item.priority-p0 {
border-left-color: #ef4444;
}
.plan-item.priority-p1 {
border-left-color: #f59e0b;
}
.plan-item.priority-p2 {
border-left-color: #3b82f6;
}
/* Drag handle */
.plan-item-drag {
cursor: grab;
color: var(--text-muted);
font-size: 0.9rem;
padding: 0 0.2rem;
opacity: 0.5;
user-select: none;
}
.plan-item-drag:hover {
opacity: 1;
}
.plan-item-drag:active {
cursor: grabbing;
}
/* TDD Phase badge - enhanced */
.plan-item-phase {
flex-shrink: 0;
min-width: 22px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
border-radius: 3px;
background: var(--bg-card);
}
.plan-item-phase.phase-setup {
background: #6366f1;
color: white;
}
.plan-item-phase.phase-test {
background: #8b5cf6;
color: white;
}
.plan-item-phase.phase-impl {
background: #06b6d4;
color: white;
}
.plan-item-phase.phase-review {
background: #f59e0b;
color: white;
}
.plan-item-phase.phase-verify {
background: #22c55e;
color: white;
}
/* Enhanced verification indicator */
.plan-item-verify {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.15rem;
color: var(--green);
font-size: 0.65rem;
cursor: help;
padding: 0.1rem 0.25rem;
background: rgba(34, 197, 94, 0.1);
border-radius: 3px;
opacity: 0.85;
}
.plan-item-verify:hover {
opacity: 1;
background: rgba(34, 197, 94, 0.2);
}
/* Dependencies indicator */
.plan-item-deps {
flex-shrink: 0;
font-size: 0.65rem;
color: var(--text-muted);
padding: 0.1rem 0.25rem;
background: var(--bg-card);
border-radius: 3px;
cursor: help;
}
.plan-item-deps:hover {
color: var(--accent);
}
/* Review checklist indicator */
.plan-item-checklist {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.15rem;
color: #f59e0b;
font-size: 0.65rem;
cursor: help;
padding: 0.1rem 0.25rem;
background: rgba(245, 158, 11, 0.1);
border-radius: 3px;
opacity: 0.85;
}
.plan-item-checklist:hover {
opacity: 1;
background: rgba(245, 158, 11, 0.2);
}
/* Checklist modal */
.checklist-modal-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.checklist-modal {
padding: 1rem;
}
.checklist-modal h3 {
margin: 0 0 0.5rem 0;
color: var(--text);
}
.checklist-task {
color: var(--text-muted);
font-size: 0.85rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border);
}
.checklist-list {
list-style: none;
padding: 0;
margin: 0 0 1rem 0;
max-height: 300px;
overflow-y: auto;
}
.checklist-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.35rem 0;
}
.checklist-checkbox {
flex-shrink: 0;
margin-top: 0.15rem;
}
.checklist-item label {
color: var(--text);
font-size: 0.85rem;
cursor: pointer;
}
.checklist-footer {
text-align: right;
}
/* Group headers for TDD phases */
.plan-group-header {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0.5rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
border-bottom: 1px solid var(--border);
margin-top: 0.5rem;
cursor: pointer;
user-select: none;
}
.plan-group-header:first-child {
margin-top: 0;
}
.plan-group-header:hover {
color: var(--text);
}
.plan-group-header .group-icon {
font-size: 0.8rem;
}
.plan-group-header .group-count {
margin-left: auto;
color: var(--text-dim);
font-weight: normal;
}
.plan-group-header .group-chevron {
transition: transform 0.15s;
}
.plan-group-header.collapsed .group-chevron {
transform: rotate(-90deg);
}
.plan-group-items {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.25rem 0;
}
.plan-group-items.collapsed {
display: none;
}
/* View mode toggle */
.plan-view-toggle {
display: flex;
gap: 0.25rem;
}
.plan-view-btn {
padding: 0.2rem 0.4rem;
font-size: 0.7rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text-muted);
cursor: pointer;
transition: all 0.15s;
}
.plan-view-btn:hover {
color: var(--text);
border-color: var(--accent);
}
.plan-view-btn.active {
background: var(--accent);
color: white;
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;
gap: 0.1rem;
opacity: 0;
transition: opacity 0.15s;
}
.plan-item:hover .plan-item-actions {
opacity: 1;
}
.plan-item-action {
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
font-size: 0.75rem;
padding: 0.1rem 0.2rem;
border-radius: 2px;
}
.plan-item-action:hover {
color: var(--text);
background: var(--bg-card);
}
/* Runtime task card enhancements */
.ralph-task-card {
position: relative;
}
.ralph-task-attempts {
position: absolute;
top: 2px;
right: 2px;
font-size: 0.6rem;
color: var(--text-muted);
background: var(--bg-card);
padding: 0 3px;
border-radius: 2px;
}
.ralph-task-attempts.has-errors {
color: var(--red);
background: rgba(239, 68, 68, 0.1);
}
.ralph-task-verify-badge {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 0.6rem;
color: var(--green);
opacity: 0.7;
}
.ralph-task-deps-indicator {
position: absolute;
top: 2px;
left: 2px;
font-size: 0.55rem;
color: var(--accent);
}
/* Quick actions on task hover */
.ralph-task-actions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: none;
gap: 0.25rem;
background: rgba(0,0,0,0.85);
padding: 0.25rem;
border-radius: 4px;
}
.ralph-task-card:hover .ralph-task-actions {
display: flex;
}
.ralph-task-action-btn {
padding: 0.2rem 0.35rem;
font-size: 0.65rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
cursor: pointer;
}
.ralph-task-action-btn:hover {
background: var(--accent);
color: white;
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;
top: 100%;
right: 0;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100;
min-width: 200px;
max-height: 200px;
overflow-y: auto;
}
.plan-history-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: 4px;
}
.plan-history-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--border);
cursor: pointer;
font-size: 0.8rem;
transition: background-color 0.15s;
}
.plan-history-item:last-child {
border-bottom: none;
}
.plan-history-item:hover {
background: var(--bg-input);
}
.plan-history-item.current {
background: var(--accent-dim);
cursor: default;
}
.plan-history-item.current::after {
content: '(current)';
font-size: 0.65rem;
color: var(--accent);
margin-left: 0.5rem;
}
.plan-history-version {
font-weight: 500;
color: var(--text);
}
.plan-history-tasks {
font-size: 0.7rem;
color: var(--text-muted);
margin-left: 0.5rem;
}
.plan-history-time {
color: var(--text-muted);
font-size: 0.7rem;
}
/* Ralph panel version info */
.ralph-version-info {
display: flex;
align-items: center;
gap: 0.35rem;
font-size: 0.7rem;
color: var(--text-muted);
margin-left: auto;
}
.ralph-version-info .version-badge {
background: var(--bg-card);
padding: 0.1rem 0.25rem;
border-radius: 2px;
font-family: 'SF Mono', Monaco, monospace;
}
.ralph-version-info .rollback-btn {
background: none;
border: none;
color: var(--accent);
cursor: pointer;
font-size: 0.65rem;
padding: 0.1rem 0.25rem;
}
.ralph-version-info .rollback-btn:hover {
text-decoration: underline;
}
/* Textarea with AI Assist button */
.textarea-with-assist {
position: relative;