chore: bump version to 0.1427

- Remove TUI from git (moved to .gitignore for local development)
- Remove React/Ink dependencies (unused in published version)
- Remove tui command from CLI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-29 12:47:23 +01:00
co-authored by Claude Opus 4.5
parent a73f7c3a4f
commit db20f4f63d
22 changed files with 414 additions and 3311 deletions
+225 -115
View File
@@ -431,6 +431,8 @@ class NotificationManager {
class ClaudemanApp {
constructor() {
this.sessions = new Map();
this.sessionOrder = []; // Track tab order for drag-and-drop reordering
this.draggedTabId = null; // Currently dragged tab session ID
this.cases = [];
this.currentRun = null;
this.totalTokens = 0;
@@ -842,14 +844,17 @@ class ClaudemanApp {
}
showWelcome() {
this.terminal.writeln('\x1b[1;36m Claudeman Terminal\x1b[0m');
this.terminal.writeln('');
this.terminal.writeln('\x1b[90m Each instance opens a persistent GNU Screen session running Claude or Shell.\x1b[0m');
this.terminal.writeln('\x1b[90m Sessions stay alive for autonomous work, even if you close this browser.\x1b[0m');
this.terminal.writeln('\x1b[90m Use the +/- controls to set how many instances to launch at once.\x1b[0m');
this.terminal.writeln('');
this.terminal.writeln('\x1b[90m Press \x1b[1;37mCtrl+Enter\x1b[0m\x1b[90m to start Claude\x1b[0m');
this.terminal.writeln('');
const overlay = document.getElementById('welcomeOverlay');
if (overlay) {
overlay.classList.add('visible');
}
}
hideWelcome() {
const overlay = document.getElementById('welcomeOverlay');
if (overlay) {
overlay.classList.remove('visible');
}
}
/**
@@ -1113,6 +1118,11 @@ class ClaudemanApp {
addListener('session:created', (e) => {
const data = JSON.parse(e.data);
this.sessions.set(data.id, data);
// Add new session to end of tab order
if (!this.sessionOrder.includes(data.id)) {
this.sessionOrder.push(data.id);
this.saveSessionOrder();
}
this.renderSessionTabs();
this.updateCost();
});
@@ -1120,6 +1130,8 @@ class ClaudemanApp {
addListener('session:updated', (e) => {
const data = JSON.parse(e.data);
const session = data.session || data;
const oldSession = this.sessions.get(session.id);
const claudeSessionIdJustSet = session.claudeSessionId && (!oldSession || !oldSession.claudeSessionId);
this.sessions.set(session.id, session);
this.renderSessionTabs();
this.updateCost();
@@ -1130,11 +1142,22 @@ class ClaudemanApp {
// Update parentSessionName for any subagents belonging to this session
// (fixes stale name display after session rename)
this.updateSubagentParentNames(session.id);
// If claudeSessionId was just set, re-check orphan subagents
// This connects subagents that were waiting for the session to identify itself
if (claudeSessionIdJustSet) {
this.recheckOrphanSubagents();
}
});
addListener('session:deleted', (e) => {
const data = JSON.parse(e.data);
this.sessions.delete(data.id);
// Remove from tab order
const orderIndex = this.sessionOrder.indexOf(data.id);
if (orderIndex !== -1) {
this.sessionOrder.splice(orderIndex, 1);
this.saveSessionOrder();
}
this.terminalBuffers.delete(data.id);
this.ralphStates.delete(data.id); // Clean up ralph state for this session
this.projectInsights.delete(data.id); // Clean up project insights for this session
@@ -1790,7 +1813,7 @@ class ClaudemanApp {
// ========== Subagent Events (Claude Code Background Agents) ==========
addListener('subagent:discovered', async (e) => {
addListener('subagent:discovered', (e) => {
const data = JSON.parse(e.data);
// Clear all old data for this agentId (in case of ID reuse)
this.subagents.set(data.agentId, data);
@@ -1802,8 +1825,8 @@ class ClaudemanApp {
}
this.renderSubagentPanel();
// Find which Claudeman session owns this subagent (by working directory)
await this.findParentSessionForSubagent(data.agentId);
// Find which Claudeman session owns this subagent (direct claudeSessionId match only)
this.findParentSessionForSubagent(data.agentId);
// Auto-open window for new active agents
if (data.status === 'active') {
@@ -2065,6 +2088,9 @@ class ClaudemanApp {
}
});
// Sync sessionOrder with current sessions (preserve order, add new, remove stale)
this.syncSessionOrder();
if (data.respawnStatus) {
this.respawnStatus = data.respawnStatus;
} else {
@@ -2111,10 +2137,9 @@ class ClaudemanApp {
this.restoreSubagentWindowStates();
}
// Auto-select first session if any
if (this.sessions.size > 0 && !this.activeSessionId) {
const firstSession = this.sessions.values().next().value;
this.selectSession(firstSession.id);
// Auto-select first session if any (use sessionOrder for user's preferred order)
if (this.sessionOrder.length > 0 && !this.activeSessionId) {
this.selectSession(this.sessionOrder[0]);
}
}
@@ -2225,11 +2250,6 @@ class ClaudemanApp {
this._fullRenderSessionTabs();
}
// Auto-focus: if there's exactly one session and none is active, select it
if (this.sessions.size === 1 && !this.activeSessionId) {
const [sessionId] = this.sessions.keys();
this.selectSession(sessionId);
}
}
_fullRenderSessionTabs() {
@@ -2240,8 +2260,12 @@ class ClaudemanApp {
this.cancelHideSubagentDropdown();
// Build tabs HTML using array for better string concatenation performance
// Iterate in sessionOrder to respect user's custom tab arrangement
const parts = [];
for (const [id, session] of this.sessions) {
for (const id of this.sessionOrder) {
const session = this.sessions.get(id);
if (!session) continue; // Skip if session was removed
const isActive = id === this.activeSessionId;
const status = session.status || 'idle';
const name = this.getSessionName(session);
@@ -2268,6 +2292,10 @@ class ClaudemanApp {
}
container.innerHTML = parts.join('');
// Set up drag-and-drop handlers for tab reordering
this.setupTabDragHandlers();
// Update connection lines after tabs change (positions may have shifted)
this.updateConnectionLines();
}
@@ -2307,6 +2335,129 @@ class ClaudemanApp {
`;
}
// ========== Tab Order and Drag-and-Drop ==========
// Sync sessionOrder with current sessions (preserve order for existing, add new at end)
syncSessionOrder() {
const currentIds = new Set(this.sessions.keys());
// Load saved order from localStorage
const savedOrder = this.loadSessionOrder();
// Start with saved order, keeping only sessions that still exist
const preserved = savedOrder.filter(id => currentIds.has(id));
const preservedSet = new Set(preserved);
// Add any new sessions at the end
const newSessions = [...currentIds].filter(id => !preservedSet.has(id));
this.sessionOrder = [...preserved, ...newSessions];
}
// Load session order from localStorage
loadSessionOrder() {
try {
const saved = localStorage.getItem('claudeman-session-order');
return saved ? JSON.parse(saved) : [];
} catch {
return [];
}
}
// Save session order to localStorage
saveSessionOrder() {
try {
localStorage.setItem('claudeman-session-order', JSON.stringify(this.sessionOrder));
} catch {
// Ignore storage errors
}
}
// Set up drag-and-drop handlers on tab elements
setupTabDragHandlers() {
const container = this.$('sessionTabs');
const tabs = container.querySelectorAll('.session-tab[data-id]');
tabs.forEach(tab => {
tab.setAttribute('draggable', 'true');
tab.addEventListener('dragstart', (e) => {
this.draggedTabId = tab.dataset.id;
tab.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', tab.dataset.id);
});
tab.addEventListener('dragend', () => {
tab.classList.remove('dragging');
this.draggedTabId = null;
// Remove all drag-over indicators
container.querySelectorAll('.session-tab').forEach(t => {
t.classList.remove('drag-over-left', 'drag-over-right');
});
});
tab.addEventListener('dragover', (e) => {
e.preventDefault();
if (!this.draggedTabId || this.draggedTabId === tab.dataset.id) return;
e.dataTransfer.dropEffect = 'move';
// Determine drop position based on mouse position
const rect = tab.getBoundingClientRect();
const midpoint = rect.left + rect.width / 2;
const isLeftHalf = e.clientX < midpoint;
// Update visual indicator
tab.classList.toggle('drag-over-left', isLeftHalf);
tab.classList.toggle('drag-over-right', !isLeftHalf);
});
tab.addEventListener('dragleave', () => {
tab.classList.remove('drag-over-left', 'drag-over-right');
});
tab.addEventListener('drop', (e) => {
e.preventDefault();
tab.classList.remove('drag-over-left', 'drag-over-right');
if (!this.draggedTabId || this.draggedTabId === tab.dataset.id) return;
const targetId = tab.dataset.id;
const draggedId = this.draggedTabId;
// Determine insertion position
const rect = tab.getBoundingClientRect();
const midpoint = rect.left + rect.width / 2;
const insertBefore = e.clientX < midpoint;
// Reorder sessionOrder array
const fromIndex = this.sessionOrder.indexOf(draggedId);
let toIndex = this.sessionOrder.indexOf(targetId);
if (fromIndex === -1 || toIndex === -1) return;
// Remove dragged item
this.sessionOrder.splice(fromIndex, 1);
// Recalculate target index after removal
toIndex = this.sessionOrder.indexOf(targetId);
if (toIndex === -1) return;
// Insert at correct position
if (insertBefore) {
this.sessionOrder.splice(toIndex, 0, draggedId);
} else {
this.sessionOrder.splice(toIndex + 1, 0, draggedId);
}
// Save and re-render
this.saveSessionOrder();
this._fullRenderSessionTabs();
});
});
}
// Restore a minimized subagent window
restoreMinimizedSubagent(agentId, sessionId) {
// Remove from minimized set
@@ -2443,6 +2594,7 @@ class ClaudemanApp {
if (this.activeSessionId === sessionId) return;
this.activeSessionId = sessionId;
this.hideWelcome();
// Clear idle hooks on view, but keep action hooks until user interacts
this.clearPendingHooks(sessionId, 'idle_prompt');
this.renderSessionTabs();
@@ -2552,16 +2704,22 @@ class ClaudemanApp {
try {
await fetch(`/api/sessions/${sessionId}?killScreen=${killScreen}`, { method: 'DELETE' });
this.sessions.delete(sessionId);
// Remove from tab order
const orderIndex = this.sessionOrder.indexOf(sessionId);
if (orderIndex !== -1) {
this.sessionOrder.splice(orderIndex, 1);
this.saveSessionOrder();
}
this.terminalBuffers.delete(sessionId);
this.ralphStates.delete(sessionId);
this.clearCountdownTimers(sessionId);
if (this.activeSessionId === sessionId) {
this.activeSessionId = null;
// Select another session or show welcome
if (this.sessions.size > 0) {
const nextSession = this.sessions.values().next().value;
this.selectSession(nextSession.id);
// Select another session or show welcome (use sessionOrder for consistent ordering)
if (this.sessionOrder.length > 0 && this.sessions.size > 0) {
const nextSessionId = this.sessionOrder[0];
this.selectSession(nextSessionId);
} else {
this.terminal.clear();
this.showWelcome();
@@ -2611,12 +2769,11 @@ class ClaudemanApp {
}
nextSession() {
const ids = Array.from(this.sessions.keys());
if (ids.length <= 1) return;
if (this.sessionOrder.length <= 1) return;
const currentIndex = ids.indexOf(this.activeSessionId);
const nextIndex = (currentIndex + 1) % ids.length;
this.selectSession(ids[nextIndex]);
const currentIndex = this.sessionOrder.indexOf(this.activeSessionId);
const nextIndex = (currentIndex + 1) % this.sessionOrder.length;
this.selectSession(this.sessionOrder[nextIndex]);
}
// ========== Navigation ==========
@@ -7442,15 +7599,11 @@ class ClaudemanApp {
// 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));
this.findParentSessionForSubagent(agentId);
}
}
if (parentDiscoveryPromises.length > 0) {
await Promise.all(parentDiscoveryPromises);
}
// Restore minimized state, but verify session mapping is correct
// Skip old agents from previous runs to avoid confusion
@@ -9239,92 +9392,50 @@ class ClaudemanApp {
// ========== Subagent Parent Session Tracking ==========
async findParentSessionForSubagent(agentId) {
/**
* Find and assign the parent Claudeman session for a subagent.
*
* ROCK-SOLID MATCHING: Only uses claudeSessionId.
* - agent.sessionId = Claude session ID that spawned this subagent (from file path)
* - session.claudeSessionId = Claude session ID running in this Claudeman session
*
* If they match, the subagent belongs to that tab. No fallbacks, no heuristics.
* If no match is found, the subagent waits - when a session's claudeSessionId gets
* set (via session:updated), we re-check all orphan subagents.
*/
findParentSessionForSubagent(agentId) {
const agent = this.subagents.get(agentId);
if (!agent) return;
// Strategy 1: Check if another subagent with the same Claude sessionId already has a parent
// This ensures subagents from the same Claude session go to the same Claudeman session
if (agent.sessionId) {
for (const [otherAgentId, otherAgent] of this.subagents) {
if (otherAgentId !== agentId &&
otherAgent.sessionId === agent.sessionId &&
otherAgent.parentSessionId &&
this.sessions.has(otherAgent.parentSessionId)) {
// Found a sibling subagent with an assigned parent
agent.parentSessionId = otherAgent.parentSessionId;
agent.parentSessionName = otherAgent.parentSessionName;
this.subagents.set(agentId, agent);
this.updateSubagentWindowParent(agentId);
this.updateSubagentWindowVisibility();
return;
}
}
}
// Can't match without a sessionId
if (!agent.sessionId) return;
// Strategy 2: Match by claudeSessionId (most reliable)
// The subagent's sessionId is the Claude session that spawned it
// Match this to the Claudeman session's claudeSessionId
if (agent.sessionId) {
for (const [sessionId, session] of this.sessions) {
if (session.claudeSessionId === agent.sessionId) {
agent.parentSessionId = sessionId;
agent.parentSessionName = this.getSessionName(session);
this.subagents.set(agentId, agent);
this.updateSubagentWindowParent(agentId);
this.updateSubagentWindowVisibility();
return;
}
}
}
// Strategy 3: Fall back to workingDir matching (for sessions without claudeSessionId yet)
const matchingSessions = [];
// Direct match: subagent.sessionId === session.claudeSessionId
for (const [sessionId, session] of this.sessions) {
try {
const resp = await fetch(`/api/sessions/${sessionId}/subagents`);
if (!resp.ok) continue;
const result = await resp.json();
const subagents = result.data || result.subagents || [];
if (subagents.some(s => s.agentId === agentId)) {
matchingSessions.push({ sessionId, session });
}
} catch (err) {
// Ignore errors
if (session.claudeSessionId === agent.sessionId) {
agent.parentSessionId = sessionId;
agent.parentSessionName = this.getSessionName(session);
this.subagents.set(agentId, agent);
this.updateSubagentWindowParent(agentId);
this.updateSubagentWindowVisibility();
return;
}
}
if (matchingSessions.length === 0) {
// No matching session found
return;
}
// No match found - the session's claudeSessionId might not be set yet.
// When session:updated fires with claudeSessionId, we'll re-check orphans.
}
// If only one session matches, use it
let chosen = matchingSessions[0];
if (matchingSessions.length > 1) {
// Multiple sessions with same workingDir - prefer session with matching claudeSessionId
// If no match, fall back to oldest session (first created is more likely the original)
const byClaudeSession = matchingSessions.find(m => m.session.claudeSessionId === agent.sessionId);
if (byClaudeSession) {
chosen = byClaudeSession;
} else {
// Fall back to oldest session (not newest - oldest is more likely the original workspace)
matchingSessions.sort((a, b) => {
const aTime = new Date(a.session.createdAt || 0).getTime();
const bTime = new Date(b.session.createdAt || 0).getTime();
return aTime - bTime; // Oldest first
});
chosen = matchingSessions[0];
/**
* Re-check all orphan subagents (those without a parentSessionId) when a session updates.
* Called when session:updated fires, in case claudeSessionId was just set.
*/
recheckOrphanSubagents() {
for (const [agentId, agent] of this.subagents) {
if (!agent.parentSessionId && agent.sessionId) {
this.findParentSessionForSubagent(agentId);
}
}
// Assign the parent
agent.parentSessionId = chosen.sessionId;
agent.parentSessionName = this.getSessionName(chosen.session);
this.subagents.set(agentId, agent);
this.updateSubagentWindowParent(agentId);
this.updateSubagentWindowVisibility();
}
/**
@@ -9821,7 +9932,7 @@ class ClaudemanApp {
this.saveSubagentWindowStates();
}
async closeSubagentWindow(agentId) {
closeSubagentWindow(agentId) {
const windowData = this.subagentWindows.get(agentId);
if (!windowData) return;
@@ -9830,12 +9941,11 @@ class ClaudemanApp {
// 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);
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;
// Get the parent session (use active session if no parent found)
const parentSessionId = agent?.parentSessionId || this.activeSessionId;
// Always minimize to tab (use active session if no parent found)
windowData.element.style.display = 'none';
+27 -12
View File
@@ -190,6 +190,26 @@
<!-- Main Terminal Area -->
<main class="main">
<div class="terminal-container" id="terminalContainer"></div>
<!-- Welcome Overlay (shown when no session active) -->
<div class="welcome-overlay" id="welcomeOverlay">
<div class="welcome-content">
<h1 class="welcome-title">Claudeman Terminal</h1>
<p class="welcome-desc">Each instance opens a persistent GNU Screen session running Claude or Shell.</p>
<p class="welcome-desc">Sessions stay alive for autonomous work, even if you close this browser.</p>
<div class="welcome-actions">
<button class="welcome-btn welcome-btn-primary" onclick="app.runClaude()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run Claude
</button>
<button class="welcome-btn welcome-btn-ralph" onclick="app.showRalphWizard()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
Start Ralph Loop Wizard
</button>
</div>
<p class="welcome-hint">Or press <kbd>Ctrl</kbd>+<kbd>Enter</kbd> to start Claude</p>
</div>
</div>
</main>
<!-- Project Insights Panel (shows file-viewing Bash commands) -->
@@ -242,24 +262,22 @@
<!-- Run Claude -->
<div class="toolbar-group">
<button class="btn-toolbar btn-claude" onclick="app.runClaude()" title="Run Claude (Ctrl+Enter)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Run Claude
</button>
<div class="tab-count-group" title="Instance count">
<button class="tab-count-btn" onclick="app.decrementTabCount()" title="Instance count">−</button>
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="20" readonly title="Instance count">
<button class="tab-count-btn" onclick="app.incrementTabCount()" title="Instance count">+</button>
<button class="tab-count-btn" onclick="app.decrementTabCount()">−</button>
<input type="number" id="tabCount" class="tab-count-input" value="1" min="1" max="20" readonly>
<button class="tab-count-btn" onclick="app.incrementTabCount()">+</button>
</div>
<button class="btn-toolbar btn-shell" onclick="app.runShell()" title="Run Shell">
Run Shell
</button>
<div class="tab-count-group" title="Instance count">
<button class="tab-count-btn" onclick="app.decrementShellCount()" title="Instance count">−</button>
<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>
<button class="tab-count-btn" onclick="app.decrementShellCount()">−</button>
<input type="number" id="shellCount" class="tab-count-input" value="1" min="1" max="20" readonly>
<button class="tab-count-btn" onclick="app.incrementShellCount()">+</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>
@@ -1295,9 +1313,6 @@
<div class="notif-drawer-empty" id="notifEmpty">No notifications</div>
</div>
<!-- Help Button -->
<button class="help-btn" onclick="app.showHelp()" title="Keyboard shortcuts (Ctrl+?)">?</button>
<!-- Token Stats Modal -->
<div class="modal" id="tokenStatsModal">
<div class="modal-backdrop" onclick="app.closeTokenStats()"></div>
+144 -57
View File
@@ -215,6 +215,24 @@ body {
50% { opacity: 0.4; }
}
/* Tab drag-and-drop reordering */
.session-tab[draggable="true"] {
cursor: grab;
}
.session-tab.dragging {
opacity: 0.5;
cursor: grabbing;
}
.session-tab.drag-over-left {
box-shadow: -2px 0 0 0 var(--accent);
}
.session-tab.drag-over-right {
box-shadow: 2px 0 0 0 var(--accent);
}
/* Header Right */
.header-right {
display: flex;
@@ -1502,6 +1520,7 @@ body {
display: flex;
overflow: hidden;
background: var(--bg-dark);
position: relative;
}
.terminal-container {
@@ -1522,6 +1541,111 @@ body {
background: transparent !important;
}
/* Welcome Overlay */
.welcome-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-dark);
z-index: 10;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
.welcome-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.welcome-content {
text-align: center;
max-width: 500px;
padding: 2rem;
}
.welcome-title {
font-size: 1.75rem;
font-weight: 600;
color: var(--accent-hover);
margin-bottom: 1rem;
letter-spacing: -0.02em;
}
.welcome-desc {
color: var(--text-dim);
font-size: 0.9rem;
line-height: 1.5;
margin-bottom: 0.5rem;
}
.welcome-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 2rem;
margin-bottom: 1.5rem;
}
.welcome-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
font-size: 0.9rem;
font-weight: 500;
border-radius: 6px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.15s ease;
}
.welcome-btn-primary {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
border-color: #3b82f6;
color: #e0e7ff;
}
.welcome-btn-primary:hover {
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
color: #fff;
}
.welcome-btn-ralph {
background: linear-gradient(135deg, #3d3415 0%, #4a3f1a 100%);
border-color: #6b5a1e;
color: #eab308;
}
.welcome-btn-ralph:hover {
background: linear-gradient(135deg, #4a3f1a 0%, #5a4d20 100%);
border-color: #8b7a2e;
box-shadow: 0 0 12px rgba(234, 179, 8, 0.4);
color: #fcd34d;
}
.welcome-hint {
color: var(--text-muted);
font-size: 0.8rem;
}
.welcome-hint kbd {
display: inline-block;
padding: 0.15rem 0.4rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 3px;
font-family: inherit;
font-size: 0.75rem;
color: var(--text-dim);
}
/* Bottom Toolbar */
.toolbar {
display: flex;
@@ -1539,7 +1663,7 @@ body {
.toolbar-right {
display: flex;
align-items: center;
gap: 0.25rem;
gap: 0.5rem;
}
.toolbar-center {
@@ -1608,23 +1732,25 @@ body {
background: var(--accent-hover);
}
/* Claude Button - Special styling */
/* Claude Button - Special styling (matches welcome-btn-primary) */
.btn-toolbar.btn-claude {
background: linear-gradient(135deg, #1e3a5f 0%, #3d2a5c 100%);
border-color: #2d4a6f;
color: var(--accent-hover);
font-weight: 700;
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
border-color: #3b82f6;
color: #e0e7ff;
font-weight: 500;
padding: 0.4rem 0.85rem;
gap: 0.4rem;
}
.btn-toolbar.btn-claude:hover {
background: linear-gradient(135deg, #2a4d7a 0%, #4d3a6c 100%);
border-color: #3d5a7f;
box-shadow: 0 0 12px rgba(61, 90, 127, 0.3);
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
border-color: #3b82f6;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
color: #fff;
}
.btn-toolbar.btn-claude .btn-icon {
font-size: 0.9rem;
.btn-toolbar.btn-claude svg {
flex-shrink: 0;
}
/* Shell Button - Special styling */
@@ -1642,21 +1768,6 @@ 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;
@@ -1779,18 +1890,19 @@ body {
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
margin: 0 0.25rem;
}
.tab-count-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 28px;
width: 22px;
height: 26px;
background: transparent;
border: none;
color: var(--text-dim);
font-size: 0.9rem;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
@@ -1807,14 +1919,14 @@ body {
}
.tab-count-input {
width: 28px;
height: 28px;
width: 24px;
height: 26px;
background: transparent;
border: none;
border-left: 1px solid var(--border);
border-right: 1px solid var(--border);
color: var(--text);
font-size: 0.8rem;
font-size: 0.75rem;
font-weight: 600;
text-align: center;
font-family: 'SF Mono', Monaco, monospace;
@@ -3188,31 +3300,6 @@ kbd {
color: var(--text);
}
/* Help Button */
.help-btn {
position: fixed;
bottom: calc(var(--toolbar-height) + 0.5rem);
right: 0.5rem;
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bg-card);
border: 1px solid var(--border);
color: var(--text-muted);
font-size: 0.85rem;
cursor: pointer;
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
}
.help-btn:hover {
background: var(--bg-hover);
color: var(--text);
}
/* Toast Notifications */
.toast-container {
position: fixed;
+12 -3
View File
@@ -4942,19 +4942,28 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
const screens = this.screenManager.getScreens();
for (const screen of screens) {
if (!this.sessions.has(screen.sessionId)) {
// Restore session settings from state.json (single source of truth)
const savedState = this.store.getSession(screen.sessionId);
// Determine the correct session name (priority: savedState > screen > screenName)
// This ensures renamed sessions keep their name after server restart
const sessionName = savedState?.name || screen.name || screen.screenName;
// Create a session object for this screen with the existing screenSession
const session = new Session({
id: screen.sessionId, // Preserve the original session ID
workingDir: screen.workingDir,
mode: screen.mode,
name: screen.name || screen.screenName,
name: sessionName,
screenManager: this.screenManager,
useScreen: true,
screenSession: screen // Pass the existing screen so startInteractive() can attach to it
});
// Restore ALL session settings from state.json (single source of truth)
const savedState = this.store.getSession(screen.sessionId);
// Update screen name if it was a "Restored:" placeholder or doesn't match saved name
if (savedState?.name && screen.name !== savedState.name) {
this.screenManager.updateScreenName(screen.sessionId, savedState.name);
}
if (savedState) {
// Auto-compact
if (savedState.autoCompactEnabled !== undefined || savedState.autoCompactThreshold !== undefined) {