fix: subagent tabs render to correct parent session after restart

- restoreSubagentWindowStates now discovers parent sessions before restoring
- closeSubagentWindow discovers parent before minimizing to prevent wrong tab
- Fixed async handling in subagent:completed event handler

chore: bump version to 0.1389

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-26 23:37:14 +01:00
co-authored by Claude Opus 4.5
parent a730a295b1
commit b896478ca6
12 changed files with 4250 additions and 31 deletions
+282 -11
View File
@@ -1764,7 +1764,7 @@ class ClaudemanApp {
}
});
this.eventSource.addEventListener('subagent:completed', (e) => {
this.eventSource.addEventListener('subagent:completed', async (e) => {
const data = JSON.parse(e.data);
const existing = this.subagents.get(data.agentId);
if (existing) {
@@ -1778,7 +1778,7 @@ class ClaudemanApp {
if (this.subagentWindows.has(data.agentId)) {
const windowData = this.subagentWindows.get(data.agentId);
if (windowData && !windowData.minimized) {
this.closeSubagentWindow(data.agentId); // This minimizes to tab
await this.closeSubagentWindow(data.agentId); // This minimizes to tab
this.saveSubagentWindowStates(); // Persist the minimized state
}
}
@@ -2346,6 +2346,245 @@ class ClaudemanApp {
this.selectSession(ids[nextIndex]);
}
// ========== Navigation ==========
goHome() {
// Deselect active session and show welcome screen
this.activeSessionId = null;
this.terminal.clear();
this.showWelcome();
this.renderSessionTabs();
this.renderRalphStatePanel();
}
// ========== Ralph Loop Wizard ==========
// Wizard state
ralphWizardStep = 1;
ralphWizardConfig = {
taskDescription: '',
completionPhrase: 'COMPLETE',
maxIterations: 10,
caseName: 'testcase',
enableRespawn: true,
};
showRalphWizard() {
// Reset wizard state
this.ralphWizardStep = 1;
this.ralphWizardConfig = {
taskDescription: '',
completionPhrase: 'COMPLETE',
maxIterations: 10,
caseName: document.getElementById('quickStartCase')?.value || 'testcase',
enableRespawn: true,
};
// Reset UI
document.getElementById('ralphTaskDescription').value = '';
document.getElementById('ralphCompletionPhrase').value = 'COMPLETE';
this.selectIterationPreset(10);
// Populate case selector
this.populateRalphCaseSelector();
// Show wizard modal
this.updateRalphWizardUI();
document.getElementById('ralphWizardModal').classList.add('active');
document.getElementById('ralphTaskDescription').focus();
}
closeRalphWizard() {
document.getElementById('ralphWizardModal')?.classList.remove('active');
}
populateRalphCaseSelector() {
const select = document.getElementById('ralphCaseSelect');
const quickStartSelect = document.getElementById('quickStartCase');
if (quickStartSelect && select) {
select.innerHTML = quickStartSelect.innerHTML;
select.value = this.ralphWizardConfig.caseName;
}
}
selectIterationPreset(iterations) {
this.ralphWizardConfig.maxIterations = iterations;
// Update button states
document.querySelectorAll('.iteration-preset-btn').forEach(btn => {
const btnIterations = parseInt(btn.dataset.iterations);
btn.classList.toggle('active', btnIterations === iterations);
});
}
ralphWizardNext() {
if (this.ralphWizardStep === 1) {
// Validate step 1
const taskDescription = document.getElementById('ralphTaskDescription').value.trim();
const completionPhrase = document.getElementById('ralphCompletionPhrase').value.trim() || 'COMPLETE';
const caseName = document.getElementById('ralphCaseSelect').value;
if (!taskDescription) {
this.showToast('Please enter a task description', 'error');
document.getElementById('ralphTaskDescription').focus();
return;
}
// Save config
this.ralphWizardConfig.taskDescription = taskDescription;
this.ralphWizardConfig.completionPhrase = completionPhrase.toUpperCase();
this.ralphWizardConfig.caseName = caseName;
// Generate preview
this.updateRalphPromptPreview();
// Move to step 2
this.ralphWizardStep = 2;
this.updateRalphWizardUI();
}
}
ralphWizardBack() {
if (this.ralphWizardStep === 2) {
this.ralphWizardStep = 1;
this.updateRalphWizardUI();
}
}
updateRalphWizardUI() {
const step = this.ralphWizardStep;
// Update progress indicators
document.querySelectorAll('.wizard-step').forEach(el => {
const stepNum = parseInt(el.dataset.step);
el.classList.toggle('active', stepNum === step);
el.classList.toggle('completed', stepNum < step);
});
// Show/hide pages
document.getElementById('ralphWizardStep1').classList.toggle('hidden', step !== 1);
document.getElementById('ralphWizardStep2').classList.toggle('hidden', step !== 2);
// Show/hide buttons
document.getElementById('ralphBackBtn').style.display = step === 1 ? 'none' : 'block';
document.getElementById('ralphNextBtn').style.display = step === 2 ? 'none' : 'block';
document.getElementById('ralphStartBtn').style.display = step === 2 ? 'block' : 'none';
}
updateRalphPromptPreview() {
const config = this.ralphWizardConfig;
const preview = document.getElementById('ralphPromptPreview');
// Build the formatted prompt
let prompt = config.taskDescription;
// Add completion instructions
prompt += '\n\n---\n\n';
prompt += '## Completion Criteria\n\n';
prompt += 'When ALL requirements above are complete:\n';
prompt += '1. Run all tests to verify\n';
prompt += '2. Commit your changes\n';
prompt += `3. Output: <promise>${config.completionPhrase}</promise>\n`;
// Show preview with highlighting
const highlightedPrompt = prompt
.replace(/<promise>/g, '<span class="preview-highlight">&lt;promise&gt;')
.replace(/<\/promise>/g, '&lt;/promise&gt;</span>');
preview.innerHTML = highlightedPrompt;
// Update summary
document.getElementById('summaryPhrase').textContent = config.completionPhrase;
document.getElementById('summaryIterations').textContent =
config.maxIterations === 0 ? 'Unlimited' : config.maxIterations;
document.getElementById('summaryCase').textContent = config.caseName;
}
async startRalphLoop() {
const config = this.ralphWizardConfig;
// Read advanced options
config.enableRespawn = document.getElementById('ralphEnableRespawn')?.checked ?? true;
// Close wizard
this.closeRalphWizard();
// Build the full prompt
let fullPrompt = config.taskDescription;
fullPrompt += '\n\n---\n\n';
fullPrompt += '## Completion Criteria\n\n';
fullPrompt += 'When ALL requirements above are complete:\n';
fullPrompt += '1. Run all tests to verify\n';
fullPrompt += '2. Commit your changes\n';
fullPrompt += `3. Output: <promise>${config.completionPhrase}</promise>\n`;
try {
// Step 1: Create/verify case exists and start session
this.terminal.clear();
this.terminal.writeln(`\x1b[1;33m Starting Ralph Loop in ${config.caseName}...\x1b[0m`);
this.terminal.writeln('');
// Create session via quick-start
const res = await fetch('/api/quick-start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ caseName: config.caseName, mode: 'claude' })
});
const data = await res.json();
if (!data.success) {
this.showToast(data.error || 'Failed to start session', 'error');
return;
}
const sessionId = data.sessionId;
// Step 2: Configure Ralph tracker for this session
await fetch(`/api/sessions/${sessionId}/ralph-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled: true,
completionPhrase: config.completionPhrase,
maxIterations: config.maxIterations || null,
})
});
// Step 3: Enable respawn if requested
if (config.enableRespawn) {
await fetch(`/api/sessions/${sessionId}/respawn/enable`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: 'continue',
sendClear: true,
sendInit: true,
})
});
}
// Step 4: Switch to session and wait for it to be ready
await this.selectSession(sessionId);
// Wait a moment for session to be ready, then send the prompt
await new Promise(r => setTimeout(r, 2000));
// Send the prompt to start the loop
await fetch(`/api/sessions/${sessionId}/input`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: fullPrompt + '\n' })
});
this.showToast(`Ralph Loop started in ${config.caseName}`, 'success');
} catch (err) {
console.error('Failed to start Ralph loop:', err);
this.showToast('Failed to start Ralph loop: ' + err.message, 'error');
}
}
// ========== Quick Start ==========
async loadQuickStartCases(selectCaseName = null) {
@@ -4287,13 +4526,35 @@ class ClaudemanApp {
async restoreSubagentWindowStates() {
const states = await this.loadSubagentWindowStates();
// Restore minimized state
for (const [sessionId, agentIds] of Object.entries(states.minimized || {})) {
// First, discover parent sessions for all subagents to establish correct mapping
// This is important after server restart when parentSessionId isn't set
const parentDiscoveryPromises = [];
for (const [agentId, agent] of this.subagents) {
if (!agent.parentSessionId) {
parentDiscoveryPromises.push(this.findParentSessionForSubagent(agentId));
}
}
if (parentDiscoveryPromises.length > 0) {
await Promise.all(parentDiscoveryPromises);
}
// Restore minimized state, but verify session mapping is correct
for (const [savedSessionId, agentIds] of Object.entries(states.minimized || {})) {
if (Array.isArray(agentIds) && agentIds.length > 0) {
// Verify agents still exist
const validAgents = agentIds.filter(id => this.subagents.has(id));
if (validAgents.length > 0) {
this.minimizedSubagents.set(sessionId, new Set(validAgents));
for (const agentId of agentIds) {
const agent = this.subagents.get(agentId);
if (!agent) continue; // Agent no longer exists
// Use discovered parentSessionId, or validate saved sessionId exists
const correctSessionId = agent.parentSessionId ||
(this.sessions.has(savedSessionId) ? savedSessionId : null);
if (correctSessionId) {
if (!this.minimizedSubagents.has(correctSessionId)) {
this.minimizedSubagents.set(correctSessionId, new Set());
}
this.minimizedSubagents.get(correctSessionId).add(agentId);
}
}
}
}
@@ -4326,6 +4587,7 @@ class ClaudemanApp {
}
this.renderSessionTabs(); // Update tab badges
this.saveSubagentWindowStates(); // Persist corrected mappings
}
// ========== Help Modal ==========
@@ -5804,14 +6066,23 @@ class ClaudemanApp {
this.saveSubagentWindowStates();
}
closeSubagentWindow(agentId) {
async closeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (!windowData) return;
const agent = this.subagents.get(agentId);
const parentSessionId = agent?.parentSessionId || this.activeSessionId;
// Always minimize to tab (use active session if no parent)
// Try to discover parent session if not already known
// This ensures we minimize to the correct tab
if (agent && !agent.parentSessionId) {
await this.findParentSessionForSubagent(agentId);
}
// Now get the correct parent session (re-read agent after discovery)
const updatedAgent = this.subagents.get(agentId);
const parentSessionId = updatedAgent?.parentSessionId || this.activeSessionId;
// Always minimize to tab (use active session if no parent found)
windowData.element.style.display = 'none';
windowData.minimized = true;
+133 -1
View File
@@ -17,7 +17,7 @@
<!-- Compact Header with Session Tabs -->
<header class="header">
<div class="header-brand">
<span class="logo">Claudeman</span>
<span class="logo" onclick="app.goHome()" title="Go to main page">Claudeman</span>
</div>
<!-- Session Tabs -->
@@ -206,6 +206,9 @@
<input type="number" id="shellCount" class="tab-count-input" value="1" min="1" max="20" readonly title="Instance count">
<button class="tab-count-btn" onclick="app.incrementShellCount()" title="Instance count">+</button>
</div>
<button class="btn-toolbar btn-ralph" onclick="app.showRalphWizard()" title="Start Ralph Loop">
Ralph Loop
</button>
<div class="case-select-group">
<select id="quickStartCase" class="toolbar-select" title="Select case">
<option value="testcase">testcase</option>
@@ -854,6 +857,135 @@
</div>
</div>
<!-- Ralph Loop Wizard Modal -->
<div class="modal" id="ralphWizardModal">
<div class="modal-backdrop" onclick="app.closeRalphWizard()"></div>
<div class="modal-content modal-lg">
<div class="modal-header">
<h3>Start Ralph Loop</h3>
<button class="modal-close" onclick="app.closeRalphWizard()">&times;</button>
</div>
<!-- Wizard Progress Steps -->
<div class="wizard-progress">
<div class="wizard-step active" data-step="1">
<span class="wizard-step-number">1</span>
<span class="wizard-step-label">Task Setup</span>
</div>
<div class="wizard-step" data-step="2">
<span class="wizard-step-number">2</span>
<span class="wizard-step-label">Launch</span>
</div>
</div>
<div class="modal-body">
<!-- Step 1: Task & Completion Setup -->
<div class="wizard-page" id="ralphWizardStep1">
<div class="form-row">
<label>Task Description</label>
<div class="textarea-with-assist">
<textarea
id="ralphTaskDescription"
rows="6"
placeholder="Describe what you want Claude to accomplish...
Example:
Build a REST API for user authentication:
- JWT token generation and validation
- Login/logout endpoints
- Password reset flow
- Tests for all endpoints"></textarea>
</div>
<span class="form-hint">Be specific about requirements and success criteria</span>
</div>
<div class="form-row">
<label>Completion Phrase</label>
<div class="phrase-input-group">
<input type="text" id="ralphCompletionPhrase" value="COMPLETE" placeholder="COMPLETE" list="phrasesSuggestions">
<datalist id="phrasesSuggestions">
<option value="COMPLETE">
<option value="DONE">
<option value="FINISHED">
<option value="TESTS_PASS">
<option value="ALL_TASKS_COMPLETE">
</datalist>
</div>
<span class="form-hint">Claude outputs <code>&lt;promise&gt;PHRASE&lt;/promise&gt;</code> when done</span>
</div>
<div class="form-row">
<label>Max Iterations</label>
<div class="iteration-presets">
<button type="button" class="iteration-preset-btn active" data-iterations="10" onclick="app.selectIterationPreset(10)">10</button>
<button type="button" class="iteration-preset-btn" data-iterations="25" onclick="app.selectIterationPreset(25)">25</button>
<button type="button" class="iteration-preset-btn" data-iterations="50" onclick="app.selectIterationPreset(50)">50</button>
<button type="button" class="iteration-preset-btn" data-iterations="100" onclick="app.selectIterationPreset(100)">100</button>
<button type="button" class="iteration-preset-btn" data-iterations="0" onclick="app.selectIterationPreset(0)">&infin;</button>
</div>
<span class="form-hint">Safety limit - loop stops after this many iterations</span>
</div>
<div class="form-row">
<label>Working Directory</label>
<div class="case-select-inline">
<select id="ralphCaseSelect" class="form-select"></select>
<button class="btn-toolbar btn-sm" onclick="app.showCreateCaseModal()">+ New</button>
</div>
</div>
</div>
<!-- Step 2: Preview & Launch -->
<div class="wizard-page hidden" id="ralphWizardStep2">
<div class="ralph-preview-section">
<label>Prompt Preview</label>
<div class="ralph-prompt-preview" id="ralphPromptPreview"></div>
</div>
<div class="ralph-config-summary">
<div class="config-summary-row">
<span class="config-label">Completion Phrase:</span>
<code id="summaryPhrase">COMPLETE</code>
</div>
<div class="config-summary-row">
<span class="config-label">Max Iterations:</span>
<span id="summaryIterations">10</span>
</div>
<div class="config-summary-row">
<span class="config-label">Case:</span>
<span id="summaryCase">testcase</span>
</div>
</div>
<!-- Advanced Options (Collapsible) -->
<details class="advanced-options">
<summary>Advanced Options</summary>
<div class="advanced-options-content">
<div class="form-row form-row-switch">
<label>Enable Respawn</label>
<label class="switch">
<input type="checkbox" id="ralphEnableRespawn" checked>
<span class="slider"></span>
</label>
<span class="form-hint">Auto-restart sessions when context fills up</span>
</div>
</div>
</details>
</div>
</div>
<!-- Wizard Footer -->
<div class="wizard-footer">
<button class="btn-toolbar" id="ralphBackBtn" onclick="app.ralphWizardBack()" style="display: none;">Back</button>
<div class="wizard-footer-right">
<button class="btn-toolbar" onclick="app.closeRalphWizard()">Cancel</button>
<button class="btn-toolbar btn-primary" id="ralphNextBtn" onclick="app.ralphWizardNext()">Next</button>
<button class="btn-toolbar btn-success" id="ralphStartBtn" onclick="app.startRalphLoop()" style="display: none;">Start Loop</button>
</div>
</div>
</div>
</div>
<!-- Kill All Confirmation Modal -->
<div class="modal" id="killAllModal">
<div class="modal-backdrop" onclick="app.closeKillAllModal()"></div>
+278
View File
@@ -67,6 +67,13 @@ body {
font-weight: 600;
color: var(--accent-hover);
letter-spacing: -0.02em;
cursor: pointer;
transition: color 0.15s, text-shadow 0.15s;
}
.logo:hover {
color: #93c5fd;
text-shadow: 0 0 8px rgba(96, 165, 250, 0.5);
}
/* Session Tabs */
@@ -1435,6 +1442,21 @@ body {
color: #a8d8a8;
}
/* Ralph Loop button - golden/yellow theme */
.btn-toolbar.btn-ralph {
background: linear-gradient(135deg, #3d3415 0%, #4a3f1a 100%);
border-color: #6b5a1e;
color: #eab308;
font-weight: 500;
}
.btn-toolbar.btn-ralph:hover {
background: linear-gradient(135deg, #4a3f1a 0%, #5a4d20 100%);
border-color: #8b7a2e;
box-shadow: 0 0 8px rgba(234, 179, 8, 0.4);
color: #fcd34d;
}
/* Button icon styling */
.btn-toolbar .btn-icon {
display: inline-flex;
@@ -4600,3 +4622,259 @@ kbd {
content: "📄 Click to stream: ";
color: var(--accent);
}
/* ========== Ralph Loop Wizard ========== */
/* Wizard Progress Indicator */
.wizard-progress {
display: flex;
justify-content: center;
gap: 2rem;
padding: 0.75rem 1rem;
background: var(--bg-dark);
border-bottom: 1px solid var(--border);
}
.wizard-step {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-muted);
font-size: 0.8rem;
transition: color 0.2s;
}
.wizard-step.active {
color: var(--accent);
}
.wizard-step.completed {
color: var(--green);
}
.wizard-step-number {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 50%;
font-size: 0.75rem;
font-weight: 600;
}
.wizard-step.active .wizard-step-number {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.wizard-step.completed .wizard-step-number {
background: var(--green);
border-color: var(--green);
color: #fff;
}
.wizard-step-label {
font-weight: 500;
}
/* Wizard Pages */
.wizard-page {
min-height: 280px;
}
.wizard-page.hidden {
display: none;
}
/* Wizard Footer */
.wizard-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
border-top: 1px solid var(--border);
background: var(--bg-card);
}
.wizard-footer-right {
display: flex;
gap: 0.5rem;
}
/* Textarea with AI Assist button */
.textarea-with-assist {
position: relative;
}
.textarea-with-assist textarea {
width: 100%;
padding: 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.85rem;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
resize: vertical;
min-height: 140px;
}
.textarea-with-assist textarea:focus {
outline: none;
border-color: var(--accent);
}
/* Phrase Input Group */
.phrase-input-group {
position: relative;
}
.phrase-input-group input {
width: 100%;
padding: 0.5rem 0.75rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text);
font-size: 0.85rem;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
}
.phrase-input-group input:focus {
outline: none;
border-color: var(--accent);
}
/* Iteration Presets */
.iteration-presets {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
}
.iteration-preset-btn {
padding: 0.5rem 1rem;
font-size: 0.8rem;
font-weight: 500;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-dim);
cursor: pointer;
transition: all 0.15s;
min-width: 50px;
}
.iteration-preset-btn:hover {
border-color: var(--border-light);
color: var(--text);
}
.iteration-preset-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
/* Case Select Inline */
.case-select-inline {
display: flex;
gap: 0.5rem;
}
.case-select-inline select {
flex: 1;
}
/* Ralph Prompt Preview */
.ralph-preview-section {
margin-bottom: 1rem;
}
.ralph-preview-section > label {
display: block;
font-size: 0.7rem;
color: var(--text-dim);
margin-bottom: 0.35rem;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.ralph-prompt-preview {
background: var(--bg-dark);
border: 1px solid var(--border);
border-radius: 4px;
padding: 0.75rem;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.75rem;
color: var(--text-dim);
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-word;
}
.ralph-prompt-preview .preview-highlight {
color: var(--accent);
font-weight: 500;
}
/* Config Summary */
.ralph-config-summary {
background: var(--bg-input);
border-radius: 4px;
padding: 0.75rem;
margin-bottom: 1rem;
}
.config-summary-row {
display: flex;
justify-content: space-between;
padding: 0.35rem 0;
font-size: 0.8rem;
}
.config-summary-row:not(:last-child) {
border-bottom: 1px solid var(--border);
}
.config-label {
color: var(--text-dim);
}
.config-summary-row code {
background: var(--bg-dark);
padding: 0.15rem 0.4rem;
border-radius: 3px;
font-size: 0.75rem;
color: var(--green);
}
/* Advanced Options */
.advanced-options {
margin-top: 1rem;
}
.advanced-options summary {
cursor: pointer;
font-size: 0.8rem;
color: var(--text-dim);
padding: 0.5rem 0;
border-top: 1px solid var(--border);
}
.advanced-options summary:hover {
color: var(--text);
}
.advanced-options[open] summary {
margin-bottom: 0.75rem;
}
.advanced-options-content {
padding-left: 0.5rem;
}