feat(wizard): add minimize button and network retry for Ralph wizard

- Add minimize button to Ralph wizard modal header
- Minimized indicator shows at bottom of screen with elapsed time
- Click indicator to restore wizard
- Add retry logic (3 attempts with exponential backoff) for plan generation
- Shows "Connection lost, retrying..." message during retries

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-28 06:03:48 +01:00
co-authored by Claude Opus 4.5
parent e60ef9a931
commit c17dc57fd9
5 changed files with 145 additions and 9 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ When user says "COM":
1. Increment version in BOTH `package.json` AND `CLAUDE.md`
2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web`
**Version**: 0.1413 (must match `package.json`)
**Version**: 0.1414 (must match `package.json`)
## Project Overview
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claudeman",
"version": "0.1413",
"version": "0.1414",
"description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence",
"type": "module",
"main": "dist/index.js",
+80 -7
View File
@@ -2715,6 +2715,9 @@ class ClaudemanApp {
// Remove agents minimized tab if exists
this.removeAgentsMinimizedTab();
// Hide minimized indicator
this.hideWizardMinimizedIndicator();
// Reset wizard position
this.wizardPosition = null;
this.planAgentsMinimized = false;
@@ -2723,6 +2726,49 @@ class ClaudemanApp {
this.updateConnectionLines();
}
minimizeRalphWizard() {
const modal = document.getElementById('ralphWizardModal');
modal?.classList.remove('active');
// Show the minimized indicator
this.showWizardMinimizedIndicator();
}
restoreRalphWizard() {
// Hide the minimized indicator
this.hideWizardMinimizedIndicator();
// Show the modal again
const modal = document.getElementById('ralphWizardModal');
modal?.classList.add('active');
}
showWizardMinimizedIndicator() {
const indicator = document.getElementById('wizardMinimizedIndicator');
if (!indicator) return;
indicator.classList.add('visible');
// Start time counter if plan generation is active
if (this.planLoadingStartTime) {
this.wizardMinimizedTimer = setInterval(() => {
const elapsed = Math.floor((Date.now() - this.planLoadingStartTime) / 1000);
const timeEl = document.getElementById('wizardMinimizedTime');
if (timeEl) timeEl.textContent = `${elapsed}s`;
}, 1000);
}
}
hideWizardMinimizedIndicator() {
const indicator = document.getElementById('wizardMinimizedIndicator');
if (indicator) indicator.classList.remove('visible');
if (this.wizardMinimizedTimer) {
clearInterval(this.wizardMinimizedTimer);
this.wizardMinimizedTimer = null;
}
}
populateRalphCaseSelector() {
const select = document.getElementById('ralphCaseSelect');
const quickStartSelect = document.getElementById('quickStartCase');
@@ -3296,14 +3342,41 @@ class ClaudemanApp {
? { taskDescription: config.taskDescription, caseName: config.caseName }
: { taskDescription: config.taskDescription, detailLevel: config.planDetailLevel };
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: this.planGenerationAbortController?.signal,
});
// Retry logic for network errors
const maxRetries = 3;
let lastError = null;
let data = null;
const data = await res.json();
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: this.planGenerationAbortController?.signal,
});
data = await res.json();
break; // Success, exit retry loop
} catch (fetchErr) {
lastError = fetchErr;
// Don't retry if aborted
if (fetchErr.name === 'AbortError') throw fetchErr;
// Network error - retry with exponential backoff
console.warn(`[Plan] Fetch attempt ${attempt + 1} failed:`, fetchErr.message);
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
const titleEl = document.getElementById('planLoadingTitle');
const hintEl = document.getElementById('planLoadingHint');
if (titleEl) titleEl.textContent = 'Connection lost, retrying...';
if (hintEl) hintEl.textContent = `Attempt ${attempt + 2} of ${maxRetries} in ${delay / 1000}s`;
await new Promise(r => setTimeout(r, delay));
}
}
}
if (!data) {
throw lastError || new Error('Failed to fetch after retries');
}
// Stop timer
if (this.planLoadingTimer) {
+8
View File
@@ -951,6 +951,7 @@
<span class="icon">🤖</span>
<span class="agent-count" id="wizardAgentCount">0</span>
</button>
<button class="btn-icon-only wizard-minimize-btn" id="wizardMinimizeBtn" onclick="app.minimizeRalphWizard()" title="Minimize wizard">&#x2012;</button>
<button class="modal-close" onclick="app.closeRalphWizard()">&times;</button>
</div>
</div>
@@ -1143,6 +1144,13 @@
</div>
</div>
<!-- Ralph Wizard Minimized Indicator -->
<div class="wizard-minimized-indicator" id="wizardMinimizedIndicator" onclick="app.restoreRalphWizard()">
<span class="wizard-minimized-icon">🔄</span>
<span class="wizard-minimized-text">Ralph Wizard</span>
<span class="wizard-minimized-time" id="wizardMinimizedTime">0s</span>
</div>
<!-- Kill All Confirmation Modal -->
<div class="modal" id="killAllModal">
<div class="modal-backdrop" onclick="app.closeKillAllModal()"></div>
+55
View File
@@ -5635,6 +5635,61 @@ kbd {
font-weight: 600;
}
/* Wizard minimize button */
.wizard-minimize-btn {
margin-right: 0.25rem;
font-size: 1.2rem;
line-height: 1;
}
/* Wizard minimized floating indicator */
.wizard-minimized-indicator {
display: none;
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 8px;
padding: 0.5rem 1rem;
cursor: pointer;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s ease;
gap: 0.5rem;
align-items: center;
}
.wizard-minimized-indicator.visible {
display: flex;
}
.wizard-minimized-indicator:hover {
background: var(--bg-hover);
border-color: var(--accent-bright);
transform: translateX(-50%) scale(1.02);
}
.wizard-minimized-icon {
font-size: 1.1rem;
animation: spin 2s linear infinite;
}
.wizard-minimized-text {
font-size: 0.8rem;
color: var(--text);
font-weight: 500;
}
.wizard-minimized-time {
font-size: 0.75rem;
color: var(--text-dim);
background: var(--bg-input);
padding: 0.15rem 0.4rem;
border-radius: 4px;
}
.modal-wizard .modal-body {
overflow-y: auto;
flex: 1;