mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 15:09:42 +02:00
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:
+225
-115
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user