mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
feat(ralph): enhance Ralph Wiggum loop visualization
Add polished, animated Ralph Wiggum Loop panel with:
- Circular progress ring showing task completion percentage
- Task cards with status indicators (pending/in-progress/completed)
- Animated status badge with pulsing indicator when active
- Completion celebration animation with animated checkmark
- Smart time formatting ("1h 23m" instead of "1.38 hours")
Enhanced detection patterns based on official Ralph plugin:
- /ralph-loop command detection
- Iteration patterns: "Iteration 5/50", "[5/50]"
- Max iterations from YAML: "max-iterations: 50"
- TodoWrite tool output detection
Adds maxIterations field to InnerLoopState for tracking loop limits.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,19 @@ const LOOP_START_PATTERN = /Loop started at|Starting.*loop|Ralph loop started|<p
|
||||
const ELAPSED_TIME_PATTERN = /Elapsed:\s*(\d+(?:\.\d+)?)\s*hours?/i;
|
||||
const CYCLE_PATTERN = /cycle\s*#?(\d+)|respawn cycle #(\d+)/i;
|
||||
|
||||
// New patterns for improved Ralph detection (based on official Ralph Wiggum plugin)
|
||||
// Iteration patterns: "Iteration 5/50", "[5/50]", "iteration #5"
|
||||
const ITERATION_PATTERN = /(?:iteration|iter\.?)\s*#?(\d+)(?:\s*[\/of]\s*(\d+))?|\[(\d+)\/(\d+)\]/i;
|
||||
|
||||
// Ralph loop start: "/ralph-loop" command or "Starting Ralph loop"
|
||||
const RALPH_START_PATTERN = /\/ralph-loop|starting ralph(?:\s+wiggum)?\s+loop|ralph loop (?:started|beginning)/i;
|
||||
|
||||
// Max iterations: "max-iterations 50" or "maxIterations: 50" or "max_iterations=50"
|
||||
const MAX_ITERATIONS_PATTERN = /max[_-]?iterations?\s*[=:]\s*(\d+)/i;
|
||||
|
||||
// TodoWrite tool output - detect the tool being used
|
||||
const TODOWRITE_PATTERN = /TodoWrite|todo(?:s)?\s*(?:updated|written|saved)|Todos have been modified/i;
|
||||
|
||||
// ANSI escape code removal for cleaner parsing
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||
|
||||
@@ -143,7 +156,20 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
* Detect loop start and status indicators
|
||||
*/
|
||||
private detectLoopStatus(line: string): void {
|
||||
// Check for loop start
|
||||
// Check for Ralph loop start command (/ralph-loop)
|
||||
if (RALPH_START_PATTERN.test(line)) {
|
||||
if (!this._loopState.active) {
|
||||
this._loopState.active = true;
|
||||
this._loopState.startedAt = Date.now();
|
||||
this._loopState.cycleCount = 0;
|
||||
this._loopState.maxIterations = null;
|
||||
this._loopState.elapsedHours = null;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for generic loop start
|
||||
if (LOOP_START_PATTERN.test(line)) {
|
||||
// Check if this is a promise match (loop ending) vs loop start
|
||||
if (!PROMISE_PATTERN.test(line)) {
|
||||
@@ -152,6 +178,7 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this._loopState.active = true;
|
||||
this._loopState.startedAt = Date.now();
|
||||
this._loopState.cycleCount = 0;
|
||||
this._loopState.maxIterations = null;
|
||||
this._loopState.elapsedHours = null;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
@@ -159,6 +186,40 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for max iterations setting
|
||||
const maxIterMatch = line.match(MAX_ITERATIONS_PATTERN);
|
||||
if (maxIterMatch) {
|
||||
const maxIter = parseInt(maxIterMatch[1]);
|
||||
if (!isNaN(maxIter) && maxIter > 0) {
|
||||
this._loopState.maxIterations = maxIter;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for iteration patterns: "Iteration 5/50", "[5/50]"
|
||||
const iterMatch = line.match(ITERATION_PATTERN);
|
||||
if (iterMatch) {
|
||||
// Pattern captures: group 1&2 for "Iteration X/Y", group 3&4 for "[X/Y]"
|
||||
const currentIter = parseInt(iterMatch[1] || iterMatch[3]);
|
||||
const maxIter = iterMatch[2] || iterMatch[4] ? parseInt(iterMatch[2] || iterMatch[4]) : null;
|
||||
|
||||
if (!isNaN(currentIter)) {
|
||||
// If not already active, start the loop
|
||||
if (!this._loopState.active) {
|
||||
this._loopState.active = true;
|
||||
this._loopState.startedAt = Date.now();
|
||||
}
|
||||
|
||||
this._loopState.cycleCount = currentIter;
|
||||
if (maxIter !== null && !isNaN(maxIter)) {
|
||||
this._loopState.maxIterations = maxIter;
|
||||
}
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for elapsed time
|
||||
const elapsedMatch = line.match(ELAPSED_TIME_PATTERN);
|
||||
if (elapsedMatch) {
|
||||
@@ -167,7 +228,7 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
|
||||
// Check for cycle count
|
||||
// Check for cycle count (legacy pattern)
|
||||
const cycleMatch = line.match(CYCLE_PATTERN);
|
||||
if (cycleMatch) {
|
||||
const cycleNum = parseInt(cycleMatch[1] || cycleMatch[2]);
|
||||
@@ -177,6 +238,12 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for TodoWrite tool usage - indicates active task tracking
|
||||
if (TODOWRITE_PATTERN.test(line)) {
|
||||
this._loopState.lastActivity = Date.now();
|
||||
// Don't emit update just for activity, let todo detection handle it
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -321,10 +388,11 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
/**
|
||||
* Mark the loop as started (can be called externally)
|
||||
*/
|
||||
startLoop(completionPhrase?: string): void {
|
||||
startLoop(completionPhrase?: string, maxIterations?: number): void {
|
||||
this._loopState.active = true;
|
||||
this._loopState.startedAt = Date.now();
|
||||
this._loopState.cycleCount = 0;
|
||||
this._loopState.maxIterations = maxIterations ?? null;
|
||||
this._loopState.elapsedHours = null;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
if (completionPhrase) {
|
||||
@@ -333,6 +401,15 @@ export class InnerLoopTracker extends EventEmitter {
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update max iterations (can be called externally)
|
||||
*/
|
||||
setMaxIterations(maxIterations: number | null): void {
|
||||
this._loopState.maxIterations = maxIterations;
|
||||
this._loopState.lastActivity = Date.now();
|
||||
this.emit('loopUpdate', this.loopState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the loop as stopped
|
||||
*/
|
||||
|
||||
@@ -255,6 +255,7 @@ export interface InnerLoopState {
|
||||
completionPhrase: string | null;
|
||||
startedAt: number | null;
|
||||
cycleCount: number;
|
||||
maxIterations: number | null;
|
||||
lastActivity: number;
|
||||
elapsedHours: number | null;
|
||||
}
|
||||
@@ -283,6 +284,7 @@ export function createInitialInnerLoopState(): InnerLoopState {
|
||||
completionPhrase: null,
|
||||
startedAt: null,
|
||||
cycleCount: 0,
|
||||
maxIterations: null,
|
||||
lastActivity: Date.now(),
|
||||
elapsedHours: null,
|
||||
};
|
||||
|
||||
+207
-83
@@ -505,6 +505,10 @@ class ClaudemanApp {
|
||||
existing.loop.active = false;
|
||||
this.updateInnerState(data.sessionId, existing);
|
||||
}
|
||||
// Show celebration animation if this is the active session
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
this.showRalphCelebration(data.phrase);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1791,7 +1795,7 @@ class ClaudemanApp {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== Inner State (Ralph Loop & Todo List) ==========
|
||||
// ========== Enhanced Ralph Wiggum Loop Panel ==========
|
||||
|
||||
updateInnerState(sessionId, updates) {
|
||||
const existing = this.innerStates.get(sessionId) || { loop: null, todos: [] };
|
||||
@@ -1811,10 +1815,9 @@ class ClaudemanApp {
|
||||
|
||||
renderInnerStatePanel() {
|
||||
const panel = this.$('innerStatePanel');
|
||||
const content = this.$('innerStateContent');
|
||||
const toggle = this.$('innerStateToggle');
|
||||
const toggle = this.$('ralphToggle');
|
||||
|
||||
if (!panel) return; // Panel not in DOM yet
|
||||
if (!panel) return;
|
||||
|
||||
const state = this.innerStates.get(this.activeSessionId);
|
||||
|
||||
@@ -1829,108 +1832,229 @@ class ClaudemanApp {
|
||||
|
||||
panel.style.display = '';
|
||||
|
||||
// Calculate completion percentage
|
||||
const todos = state?.todos || [];
|
||||
const completed = todos.filter(t => t.status === 'completed').length;
|
||||
const total = todos.length;
|
||||
const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
|
||||
// Update progress rings
|
||||
this.updateRalphRing(percent);
|
||||
|
||||
// Update status badge
|
||||
this.updateRalphStatus(state?.loop);
|
||||
|
||||
// Update stats
|
||||
this.updateRalphStats(state?.loop, completed, total);
|
||||
|
||||
// Update phrase preview
|
||||
const phrasePreview = this.$('ralphPhrasePreview');
|
||||
if (phrasePreview && state?.loop?.completionPhrase) {
|
||||
phrasePreview.innerHTML = `<code>${this.escapeHtml(state.loop.completionPhrase)}</code>`;
|
||||
} else if (phrasePreview) {
|
||||
phrasePreview.textContent = '';
|
||||
}
|
||||
|
||||
// Handle collapsed/expanded state
|
||||
if (this.innerStatePanelCollapsed) {
|
||||
panel.classList.add('collapsed');
|
||||
toggle.innerHTML = '▶'; // Right arrow
|
||||
content.innerHTML = this.renderInnerStateSummary(state);
|
||||
if (toggle) toggle.innerHTML = '▶';
|
||||
} else {
|
||||
panel.classList.remove('collapsed');
|
||||
toggle.innerHTML = '▼'; // Down arrow
|
||||
content.innerHTML = this.renderInnerStateExpanded(state);
|
||||
if (toggle) toggle.innerHTML = '▼';
|
||||
|
||||
// Update expanded view content
|
||||
this.updateRalphExpandedView(state);
|
||||
}
|
||||
}
|
||||
|
||||
renderInnerStateSummary(state) {
|
||||
const parts = [];
|
||||
|
||||
// Loop status
|
||||
if (state?.loop) {
|
||||
const loop = state.loop;
|
||||
if (loop.active) {
|
||||
let loopText = '🔄 Loop';
|
||||
if (loop.completionPhrase) {
|
||||
loopText += `: "${this.escapeHtml(loop.completionPhrase)}"`;
|
||||
}
|
||||
if (loop.elapsedHours !== null) {
|
||||
loopText += ` (${loop.elapsedHours.toFixed(1)}h)`;
|
||||
}
|
||||
parts.push(`<span class="inner-loop-status active">${loopText}</span>`);
|
||||
} else if (loop.completionPhrase) {
|
||||
parts.push(`<span class="inner-loop-status completed">✓ ${this.escapeHtml(loop.completionPhrase)}</span>`);
|
||||
}
|
||||
updateRalphRing(percent) {
|
||||
// Mini ring (in summary)
|
||||
const miniProgress = this.$('ralphRingMiniProgress');
|
||||
const miniText = this.$('ralphRingMiniText');
|
||||
if (miniProgress) {
|
||||
// Circumference = 2 * PI * r = 2 * PI * 15.9 ≈ 100
|
||||
const offset = 100 - percent;
|
||||
miniProgress.style.strokeDashoffset = offset;
|
||||
}
|
||||
if (miniText) {
|
||||
miniText.textContent = `${percent}%`;
|
||||
}
|
||||
|
||||
// Todo summary
|
||||
if (state?.todos?.length > 0) {
|
||||
const completed = state.todos.filter(t => t.status === 'completed').length;
|
||||
const total = state.todos.length;
|
||||
const inProgress = state.todos.filter(t => t.status === 'in_progress').length;
|
||||
let todoText = `Tasks: ${completed}/${total}`;
|
||||
if (inProgress > 0) {
|
||||
todoText += ` (${inProgress} in progress)`;
|
||||
}
|
||||
parts.push(`<span class="inner-todo-summary">${todoText}</span>`);
|
||||
// Large ring (in expanded view)
|
||||
const largeProgress = this.$('ralphRingProgress');
|
||||
const largePercent = this.$('ralphRingPercent');
|
||||
if (largeProgress) {
|
||||
// Circumference = 2 * PI * r = 2 * PI * 42 ≈ 264
|
||||
const offset = 264 - (264 * percent / 100);
|
||||
largeProgress.style.strokeDashoffset = offset;
|
||||
}
|
||||
if (largePercent) {
|
||||
largePercent.textContent = `${percent}%`;
|
||||
}
|
||||
|
||||
return parts.join(' | ');
|
||||
}
|
||||
|
||||
renderInnerStateExpanded(state) {
|
||||
let html = '';
|
||||
updateRalphStatus(loop) {
|
||||
const badge = this.$('ralphStatusBadge');
|
||||
const statusText = badge?.querySelector('.ralph-status-text');
|
||||
if (!badge || !statusText) return;
|
||||
|
||||
// Loop details
|
||||
if (state?.loop) {
|
||||
const loop = state.loop;
|
||||
html += '<div class="inner-loop-details">';
|
||||
if (loop.active) {
|
||||
html += `<div class="inner-loop-active">🔄 <strong>Loop Active</strong>`;
|
||||
if (loop.completionPhrase) {
|
||||
html += ` - waiting for: <code>${this.escapeHtml(loop.completionPhrase)}</code>`;
|
||||
}
|
||||
html += '</div>';
|
||||
if (loop.elapsedHours !== null) {
|
||||
html += `<div class="inner-loop-elapsed">Elapsed: ${loop.elapsedHours.toFixed(2)} hours</div>`;
|
||||
}
|
||||
if (loop.cycleCount > 0) {
|
||||
html += `<div class="inner-loop-cycles">Cycle: #${loop.cycleCount}</div>`;
|
||||
}
|
||||
} else if (loop.completionPhrase) {
|
||||
html += `<div class="inner-loop-completed">✓ Completed: <code>${this.escapeHtml(loop.completionPhrase)}</code></div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
badge.classList.remove('active', 'completed');
|
||||
|
||||
if (loop?.active) {
|
||||
badge.classList.add('active');
|
||||
statusText.textContent = 'Running';
|
||||
} else if (loop?.completionPhrase && !loop?.active) {
|
||||
badge.classList.add('completed');
|
||||
statusText.textContent = 'Complete';
|
||||
} else {
|
||||
statusText.textContent = 'Idle';
|
||||
}
|
||||
|
||||
// Todo list
|
||||
if (state?.todos?.length > 0) {
|
||||
const completed = state.todos.filter(t => t.status === 'completed').length;
|
||||
const total = state.todos.length;
|
||||
|
||||
html += '<div class="inner-todo-list">';
|
||||
html += `<div class="inner-todo-header">Tasks (${completed}/${total} complete)</div>`;
|
||||
html += '<ul class="inner-todo-items">';
|
||||
|
||||
for (const todo of state.todos) {
|
||||
const icon = this.getTodoIcon(todo.status);
|
||||
const statusClass = `todo-${todo.status.replace('_', '-')}`;
|
||||
html += `<li class="${statusClass}">${icon} ${this.escapeHtml(todo.content)}</li>`;
|
||||
}
|
||||
|
||||
html += '</ul>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html || '<div class="inner-state-empty">No inner state data</div>';
|
||||
}
|
||||
|
||||
getTodoIcon(status) {
|
||||
updateRalphStats(loop, completed, total) {
|
||||
// Time stat
|
||||
const timeEl = this.$('ralphStatTime');
|
||||
if (timeEl) {
|
||||
if (loop?.elapsedHours !== null && loop?.elapsedHours !== undefined) {
|
||||
timeEl.textContent = this.formatRalphTime(loop.elapsedHours);
|
||||
} else if (loop?.startedAt) {
|
||||
const hours = (Date.now() - loop.startedAt) / (1000 * 60 * 60);
|
||||
timeEl.textContent = this.formatRalphTime(hours);
|
||||
} else {
|
||||
timeEl.textContent = '0m';
|
||||
}
|
||||
}
|
||||
|
||||
// Cycles stat
|
||||
const cyclesEl = this.$('ralphStatCycles');
|
||||
if (cyclesEl) {
|
||||
if (loop?.maxIterations) {
|
||||
cyclesEl.textContent = `${loop.cycleCount || 0}/${loop.maxIterations}`;
|
||||
} else {
|
||||
cyclesEl.textContent = String(loop?.cycleCount || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Tasks stat
|
||||
const tasksEl = this.$('ralphStatTasks');
|
||||
if (tasksEl) {
|
||||
tasksEl.textContent = `${completed}/${total}`;
|
||||
}
|
||||
}
|
||||
|
||||
formatRalphTime(hours) {
|
||||
if (hours < 0.0167) return '0m'; // < 1 minute
|
||||
if (hours < 1) {
|
||||
const minutes = Math.round(hours * 60);
|
||||
return `${minutes}m`;
|
||||
}
|
||||
const h = Math.floor(hours);
|
||||
const m = Math.round((hours - h) * 60);
|
||||
if (m === 0) return `${h}h`;
|
||||
return `${h}h ${m}m`;
|
||||
}
|
||||
|
||||
updateRalphExpandedView(state) {
|
||||
// Update phrase
|
||||
const phraseEl = this.$('ralphPhrase');
|
||||
if (phraseEl) {
|
||||
phraseEl.textContent = state?.loop?.completionPhrase || '--';
|
||||
}
|
||||
|
||||
// Update elapsed
|
||||
const elapsedEl = this.$('ralphElapsed');
|
||||
if (elapsedEl) {
|
||||
if (state?.loop?.elapsedHours !== null && state?.loop?.elapsedHours !== undefined) {
|
||||
elapsedEl.textContent = this.formatRalphTime(state.loop.elapsedHours);
|
||||
} else if (state?.loop?.startedAt) {
|
||||
const hours = (Date.now() - state.loop.startedAt) / (1000 * 60 * 60);
|
||||
elapsedEl.textContent = this.formatRalphTime(hours);
|
||||
} else {
|
||||
elapsedEl.textContent = '0m';
|
||||
}
|
||||
}
|
||||
|
||||
// Update iterations
|
||||
const iterationsEl = this.$('ralphIterations');
|
||||
if (iterationsEl) {
|
||||
if (state?.loop?.maxIterations) {
|
||||
iterationsEl.textContent = `${state.loop.cycleCount || 0} / ${state.loop.maxIterations}`;
|
||||
} else {
|
||||
iterationsEl.textContent = String(state?.loop?.cycleCount || 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Update tasks count
|
||||
const todos = state?.todos || [];
|
||||
const completed = todos.filter(t => t.status === 'completed').length;
|
||||
const tasksCountEl = this.$('ralphTasksCount');
|
||||
if (tasksCountEl) {
|
||||
tasksCountEl.textContent = `${completed}/${todos.length}`;
|
||||
}
|
||||
|
||||
// Render task cards
|
||||
this.renderRalphTasks(todos);
|
||||
}
|
||||
|
||||
renderRalphTasks(todos) {
|
||||
const grid = this.$('ralphTasksGrid');
|
||||
if (!grid) return;
|
||||
|
||||
if (todos.length === 0) {
|
||||
grid.innerHTML = '<div class="inner-state-empty">No tasks detected</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort: in_progress first, then pending, then completed
|
||||
const sorted = [...todos].sort((a, b) => {
|
||||
const order = { in_progress: 0, pending: 1, completed: 2 };
|
||||
return (order[a.status] || 1) - (order[b.status] || 1);
|
||||
});
|
||||
|
||||
grid.innerHTML = sorted.map(todo => {
|
||||
const statusClass = `task-${todo.status.replace('_', '-')}`;
|
||||
const icon = this.getRalphTaskIcon(todo.status);
|
||||
return `
|
||||
<div class="ralph-task-card ${statusClass}">
|
||||
<span class="ralph-task-icon">${icon}</span>
|
||||
<span class="ralph-task-content">${this.escapeHtml(todo.content)}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
getRalphTaskIcon(status) {
|
||||
switch (status) {
|
||||
case 'completed': return '✓';
|
||||
case 'in_progress': return '◐';
|
||||
case 'pending':
|
||||
default: return '☐';
|
||||
default: return '○';
|
||||
}
|
||||
}
|
||||
|
||||
showRalphCelebration(phrase) {
|
||||
const celebration = this.$('ralphCelebration');
|
||||
const text = this.$('ralphCelebrationText');
|
||||
if (!celebration) return;
|
||||
|
||||
if (text) {
|
||||
text.textContent = phrase ? `${phrase}` : 'Complete!';
|
||||
}
|
||||
|
||||
celebration.classList.add('show');
|
||||
|
||||
// Auto-hide after 3 seconds
|
||||
setTimeout(() => {
|
||||
celebration.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// Legacy method for backwards compatibility
|
||||
getTodoIcon(status) {
|
||||
return this.getRalphTaskIcon(status);
|
||||
}
|
||||
|
||||
// ========== Screen Sessions (in Monitor Panel) ==========
|
||||
|
||||
async loadScreens() {
|
||||
|
||||
@@ -72,13 +72,92 @@
|
||||
<button class="btn-icon-only" onclick="app.stopRespawn()" title="Stop Respawn">■</button>
|
||||
</div>
|
||||
|
||||
<!-- Inner State Panel (Ralph Loop + Todo List detection) -->
|
||||
<div class="inner-state-panel collapsed" id="innerStatePanel" style="display: none;">
|
||||
<div class="inner-state-header" onclick="app.toggleInnerStatePanel()">
|
||||
<span class="inner-state-toggle" id="innerStateToggle">▶</span>
|
||||
<span class="inner-state-title">Inner State</span>
|
||||
<!-- Enhanced Ralph Wiggum Loop Panel -->
|
||||
<div class="ralph-panel collapsed" id="innerStatePanel" style="display: none;">
|
||||
<!-- Collapsed Summary Bar -->
|
||||
<div class="ralph-summary" onclick="app.toggleInnerStatePanel()">
|
||||
<div class="ralph-ring-mini" id="ralphRingMini">
|
||||
<svg viewBox="0 0 36 36" class="ralph-ring-svg">
|
||||
<circle class="ralph-ring-bg" cx="18" cy="18" r="15.9" />
|
||||
<circle class="ralph-ring-progress" cx="18" cy="18" r="15.9" id="ralphRingMiniProgress" />
|
||||
</svg>
|
||||
<span class="ralph-ring-text" id="ralphRingMiniText">0%</span>
|
||||
</div>
|
||||
<div class="ralph-summary-info">
|
||||
<span class="ralph-status-badge" id="ralphStatusBadge">
|
||||
<span class="ralph-status-dot"></span>
|
||||
<span class="ralph-status-text">Idle</span>
|
||||
</span>
|
||||
<span class="ralph-phrase-preview" id="ralphPhrasePreview"></span>
|
||||
</div>
|
||||
<div class="ralph-summary-stats">
|
||||
<span class="ralph-stat"><span class="ralph-stat-icon">⏱</span><span id="ralphStatTime">0m</span></span>
|
||||
<span class="ralph-stat"><span class="ralph-stat-icon">↻</span><span id="ralphStatCycles">0</span></span>
|
||||
<span class="ralph-stat"><span class="ralph-stat-icon">✓</span><span id="ralphStatTasks">0/0</span></span>
|
||||
</div>
|
||||
<span class="ralph-toggle" id="ralphToggle">▶</span>
|
||||
</div>
|
||||
|
||||
<!-- Expanded Detail View -->
|
||||
<div class="ralph-detail" id="ralphDetail">
|
||||
<div class="ralph-progress-section">
|
||||
<!-- Large Progress Ring -->
|
||||
<div class="ralph-ring">
|
||||
<svg viewBox="0 0 100 100" class="ralph-ring-svg-large">
|
||||
<defs>
|
||||
<linearGradient id="ralphGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#3b82f6" />
|
||||
<stop offset="100%" stop-color="#22c55e" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle class="ralph-ring-track" cx="50" cy="50" r="42" />
|
||||
<circle class="ralph-ring-fill" cx="50" cy="50" r="42" id="ralphRingProgress" />
|
||||
</svg>
|
||||
<div class="ralph-ring-center">
|
||||
<div class="ralph-ring-percent" id="ralphRingPercent">0%</div>
|
||||
<div class="ralph-ring-label">Complete</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Meta Info -->
|
||||
<div class="ralph-meta">
|
||||
<div class="ralph-meta-item">
|
||||
<span class="ralph-meta-label">Waiting for</span>
|
||||
<code id="ralphPhrase">--</code>
|
||||
</div>
|
||||
<div class="ralph-meta-row">
|
||||
<div class="ralph-meta-item">
|
||||
<span class="ralph-meta-label">Elapsed</span>
|
||||
<span id="ralphElapsed">0m</span>
|
||||
</div>
|
||||
<div class="ralph-meta-item">
|
||||
<span class="ralph-meta-label">Iterations</span>
|
||||
<span id="ralphIterations">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Task Cards -->
|
||||
<div class="ralph-tasks-section">
|
||||
<div class="ralph-tasks-header">
|
||||
<span>Tasks</span>
|
||||
<span class="ralph-tasks-count" id="ralphTasksCount">0/0</span>
|
||||
</div>
|
||||
<div class="ralph-tasks-grid" id="ralphTasksGrid"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completion Celebration -->
|
||||
<div class="ralph-celebration" id="ralphCelebration">
|
||||
<div class="ralph-celebration-content">
|
||||
<div class="ralph-checkmark">
|
||||
<svg viewBox="0 0 52 52">
|
||||
<circle class="ralph-checkmark-circle" cx="26" cy="26" r="25" />
|
||||
<path class="ralph-checkmark-check" d="M14.1 27.2l7.1 7.2 16.7-16.8" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ralph-celebration-text" id="ralphCelebrationText">Complete!</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner-state-content" id="innerStateContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Main Terminal Area -->
|
||||
|
||||
+388
-100
@@ -447,166 +447,454 @@ body {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Inner State Panel (Ralph Loop + Todo List) */
|
||||
.inner-state-panel {
|
||||
/* ===== Enhanced Ralph Wiggum Loop Panel ===== */
|
||||
.ralph-panel {
|
||||
background: var(--bg-card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inner-state-panel.collapsed .inner-state-content {
|
||||
max-height: 28px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inner-state-header {
|
||||
/* Collapsed Summary Bar */
|
||||
.ralph-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
gap: 0.75rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.inner-state-header:hover {
|
||||
.ralph-summary:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.inner-state-toggle {
|
||||
/* Mini Progress Ring (in summary) */
|
||||
.ralph-ring-mini {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ralph-ring-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.ralph-ring-bg {
|
||||
fill: none;
|
||||
stroke: var(--bg-input);
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.ralph-ring-progress {
|
||||
fill: none;
|
||||
stroke: url(#ralphGradient);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 100;
|
||||
stroke-dashoffset: 100;
|
||||
transition: stroke-dashoffset 0.5s ease-out;
|
||||
}
|
||||
|
||||
.ralph-ring-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 0.55rem;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Summary Info */
|
||||
.ralph-summary-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ralph-status-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-input);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ralph-status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.ralph-status-badge.active .ralph-status-dot {
|
||||
background: var(--accent);
|
||||
animation: ralph-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.ralph-status-badge.completed .ralph-status-dot {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
@keyframes ralph-pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.6; transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.ralph-phrase-preview {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.7rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.ralph-phrase-preview code {
|
||||
background: var(--bg-input);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
/* Summary Stats */
|
||||
.ralph-summary-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.ralph-stat-icon {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.ralph-toggle {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.6rem;
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inner-state-title {
|
||||
color: var(--text-dim);
|
||||
font-weight: 500;
|
||||
.ralph-panel:not(.collapsed) .ralph-toggle {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.inner-state-content {
|
||||
padding: 0 0.75rem 0.5rem;
|
||||
transition: max-height 0.2s;
|
||||
/* Hide detail when collapsed */
|
||||
.ralph-panel.collapsed .ralph-detail {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inner-state-panel.collapsed .inner-state-content {
|
||||
padding: 0 0.75rem;
|
||||
/* Expanded Detail View */
|
||||
.ralph-detail {
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.ralph-progress-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Loop status in summary view */
|
||||
.inner-loop-status {
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
/* Large Progress Ring */
|
||||
.ralph-ring {
|
||||
position: relative;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inner-loop-status.active {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: var(--accent-hover);
|
||||
.ralph-ring-svg-large {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.inner-loop-status.completed {
|
||||
background: rgba(34, 197, 94, 0.2);
|
||||
color: var(--green);
|
||||
.ralph-ring-track {
|
||||
fill: none;
|
||||
stroke: var(--bg-input);
|
||||
stroke-width: 6;
|
||||
}
|
||||
|
||||
.inner-todo-summary {
|
||||
color: var(--text-dim);
|
||||
.ralph-ring-fill {
|
||||
fill: none;
|
||||
stroke: url(#ralphGradient);
|
||||
stroke-width: 6;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 264;
|
||||
stroke-dashoffset: 264;
|
||||
transition: stroke-dashoffset 0.5s ease-out;
|
||||
}
|
||||
|
||||
/* Loop details in expanded view */
|
||||
.inner-loop-details {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid var(--accent);
|
||||
.ralph-ring-center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inner-loop-active {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.inner-loop-active code {
|
||||
background: var(--bg-input);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.inner-loop-elapsed,
|
||||
.inner-loop-cycles {
|
||||
color: var(--text-dim);
|
||||
font-size: 0.7rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.inner-loop-completed {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.inner-loop-completed code {
|
||||
background: var(--bg-input);
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
/* Todo list in expanded view */
|
||||
.inner-todo-list {
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-input);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.inner-todo-header {
|
||||
.ralph-ring-percent {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.inner-todo-items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.ralph-ring-label {
|
||||
font-size: 0.6rem;
|
||||
color: var(--text-dim);
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* Meta Info */
|
||||
.ralph-meta {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.ralph-meta-label {
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.ralph-meta code {
|
||||
background: var(--bg-input);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ralph-meta-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.ralph-meta-item span:not(.ralph-meta-label) {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Task Cards Section */
|
||||
.ralph-tasks-section {
|
||||
background: var(--bg-card);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ralph-tasks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--bg-input);
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-tasks-count {
|
||||
color: var(--text-dim);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.ralph-tasks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.inner-todo-items li {
|
||||
padding: 0.25rem 0;
|
||||
color: var(--text-dim);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
/* Task Cards */
|
||||
.ralph-task-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid var(--text-muted);
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
}
|
||||
|
||||
.ralph-task-card.task-pending {
|
||||
border-left-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ralph-task-card.task-in-progress {
|
||||
border-left-color: var(--accent);
|
||||
background: rgba(59, 130, 246, 0.05);
|
||||
}
|
||||
|
||||
.ralph-task-card.task-completed {
|
||||
border-left-color: var(--green);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.ralph-task-icon {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7rem;
|
||||
white-space: nowrap;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.ralph-task-card.task-pending .ralph-task-icon {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.ralph-task-card.task-in-progress .ralph-task-icon {
|
||||
color: var(--accent);
|
||||
animation: ralph-spin 2s linear infinite;
|
||||
}
|
||||
|
||||
.ralph-task-card.task-completed .ralph-task-icon {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
@keyframes ralph-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.ralph-task-content {
|
||||
flex: 1;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-dim);
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.inner-todo-items li.todo-completed {
|
||||
color: var(--green);
|
||||
.ralph-task-card.task-completed .ralph-task-content {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.inner-todo-items li.todo-in-progress {
|
||||
color: var(--accent-hover);
|
||||
/* Completion Celebration */
|
||||
.ralph-celebration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.inner-todo-items li.todo-pending {
|
||||
color: var(--text-dim);
|
||||
.ralph-celebration.show {
|
||||
display: flex;
|
||||
animation: ralph-celebration-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes ralph-celebration-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.ralph-celebration-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ralph-checkmark {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin: 0 auto 0.75rem;
|
||||
}
|
||||
|
||||
.ralph-checkmark svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ralph-checkmark-circle {
|
||||
fill: none;
|
||||
stroke: var(--green);
|
||||
stroke-width: 2;
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
animation: ralph-circle 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.ralph-checkmark-check {
|
||||
fill: none;
|
||||
stroke: var(--green);
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-dasharray: 48;
|
||||
stroke-dashoffset: 48;
|
||||
animation: ralph-check 0.3s ease-out 0.4s forwards;
|
||||
}
|
||||
|
||||
@keyframes ralph-circle {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
@keyframes ralph-check {
|
||||
to { stroke-dashoffset: 0; }
|
||||
}
|
||||
|
||||
.ralph-celebration-text {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
/* Empty state (legacy support) */
|
||||
.inner-state-empty {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Main - Terminal Area */
|
||||
|
||||
Reference in New Issue
Block a user