mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
chore: bump version to 0.1402
This commit is contained in:
+195
-14
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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">■</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">☰</button>
|
||||
<button type="button" class="plan-view-btn" data-view="grouped" onclick="app.setPlanViewMode('grouped')" title="Group by TDD phase">⊞</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+738
-7
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user