mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 05:59:43 +02:00
fix: compact respawn controller UI with filtered action log
- Restructure banner to 2-column layout (status left, log right) - Row 1: state, confidence, cycles, timer, tokens - Row 2: hook/AI check status, countdown timers (auto-hidden when empty) - Remove ALL timer-cancel messages from action log - Only log interesting events: commands, hooks, AI/plan verdicts - Compact text: "✓ Idle (30s)" instead of verbose messages - Smaller fonts and padding throughout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+499
-232
@@ -441,6 +441,7 @@ class ClaudemanApp {
|
||||
this.subagentPanelVisible = false;
|
||||
this.subagentWindows = new Map(); // Map<agentId, { element, position }>
|
||||
this.subagentWindowZIndex = 1000;
|
||||
this.minimizedSubagents = new Map(); // Map<sessionId, Set<agentId>> - minimized to tab
|
||||
this.ralphStatePanelCollapsed = true; // Default to collapsed
|
||||
|
||||
// Project Insights tracking (active Bash tools with clickable file paths)
|
||||
@@ -448,6 +449,7 @@ class ClaudemanApp {
|
||||
this.logViewerWindows = new Map(); // Map<windowId, { element, eventSource, filePath }>
|
||||
this.logViewerWindowZIndex = 2000;
|
||||
this.projectInsightsPanelVisible = false;
|
||||
this.currentSessionWorkingDir = null; // Track current session's working dir for path normalization
|
||||
|
||||
// Tab alert states: Map<sessionId, 'action' | 'idle'>
|
||||
this.tabAlerts = new Map();
|
||||
@@ -660,172 +662,12 @@ class ClaudemanApp {
|
||||
* in Bash tool output and makes them clickable.
|
||||
* When clicked, opens a floating log viewer window with live streaming.
|
||||
*/
|
||||
/**
|
||||
* Placeholder for file path link provider.
|
||||
* Currently disabled - xterm.js link provider doesn't render decorations reliably.
|
||||
*/
|
||||
registerFilePathLinkProvider() {
|
||||
const app = this;
|
||||
|
||||
// Create tooltip element
|
||||
let tooltip = document.getElementById('file-link-tooltip');
|
||||
if (!tooltip) {
|
||||
tooltip = document.createElement('div');
|
||||
tooltip.id = 'file-link-tooltip';
|
||||
tooltip.className = 'file-link-tooltip';
|
||||
tooltip.style.display = 'none';
|
||||
document.body.appendChild(tooltip);
|
||||
}
|
||||
|
||||
this.terminal.registerLinkProvider({
|
||||
provideLinks(bufferLineNumber, callback) {
|
||||
const line = app.terminal.buffer.active.getLine(bufferLineNumber);
|
||||
if (!line) {
|
||||
callback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the line text
|
||||
let lineText = '';
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
lineText += line.getCell(i)?.getChars() || ' ';
|
||||
}
|
||||
|
||||
const links = [];
|
||||
|
||||
// Skip lines that are unlikely to contain file paths (performance optimization)
|
||||
// Only skip if line has NO forward slash at all
|
||||
if (!lineText.includes('/')) {
|
||||
callback(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pattern 1: File paths after tail/cat/head/less/grep/watch commands
|
||||
// Matches: tail -f /path/to/file, cat /path, head -n 10 /path, grep "foo" /path, etc.
|
||||
// The (?:[^\s/]+\s+)* handles any arguments before the path (flags, values, quoted strings)
|
||||
const cmdPattern = /(tail|cat|head|less|grep|watch|multitail)\s+(?:[^\/\n]+?\s+)?(\/[^\s"'<>|;&\n\)]+)/g;
|
||||
|
||||
// Pattern 2: Any absolute path inside Bash() parentheses
|
||||
const bashPathPattern = /Bash\([^)]*?(\/(?:var|tmp|home|usr|etc|opt|log|logs)[^\s"'<>|;&\)\n]+)/g;
|
||||
|
||||
// Pattern 3: General absolute paths with common file extensions (for Claude output)
|
||||
// Matches paths like /home/user/file.log, /tmp/test.txt, etc.
|
||||
const generalPathPattern = /(\/(?:home|tmp|var|etc|usr|opt)[^\s"'<>|;&\n\)\]]+\.(?:log|txt|json|md|ts|js|sh|py|yaml|yml|csv|xml|html|css))/g;
|
||||
|
||||
// Pattern 3b: Common log file paths without extensions
|
||||
// Matches /var/log/syslog, /var/log/auth.log, etc.
|
||||
const logPathPattern = /(\/var\/log\/[^\s"'<>|;&\n\)\]]+)/g;
|
||||
|
||||
// Pattern 4: Paths in Claude output phrases like "Created file:", "Writing to", "Saved:", etc.
|
||||
const claudeOutputPattern = /(?:Created|Writing|Saved|File|Output|Streaming|Monitoring|Log)(?:\s+(?:file|to|at))?[:\s]+([\/~][^\s"'<>|;&\n\)]+)/gi;
|
||||
|
||||
let match;
|
||||
|
||||
// Match command patterns
|
||||
cmdPattern.lastIndex = 0;
|
||||
while ((match = cmdPattern.exec(lineText)) !== null) {
|
||||
const filePath = match[2].replace(/[,;:]+$/, ''); // Remove trailing punctuation
|
||||
const startIndex = lineText.indexOf(filePath, match.index);
|
||||
|
||||
if (startIndex >= 0 && !app.isDuplicateLink(links, startIndex, filePath)) {
|
||||
links.push(app.createFileLink(bufferLineNumber, startIndex, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Match Bash() paths
|
||||
bashPathPattern.lastIndex = 0;
|
||||
while ((match = bashPathPattern.exec(lineText)) !== null) {
|
||||
const filePath = match[1].replace(/[,;:]+$/, '');
|
||||
const startIndex = lineText.indexOf(filePath, match.index);
|
||||
|
||||
if (startIndex >= 0 && !app.isDuplicateLink(links, startIndex, filePath)) {
|
||||
links.push(app.createFileLink(bufferLineNumber, startIndex, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Match general absolute paths with file extensions
|
||||
generalPathPattern.lastIndex = 0;
|
||||
while ((match = generalPathPattern.exec(lineText)) !== null) {
|
||||
const filePath = match[1].replace(/[,;:]+$/, '');
|
||||
const startIndex = lineText.indexOf(filePath, match.index);
|
||||
|
||||
if (startIndex >= 0 && !app.isDuplicateLink(links, startIndex, filePath)) {
|
||||
links.push(app.createFileLink(bufferLineNumber, startIndex, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Match log paths (no extension required)
|
||||
logPathPattern.lastIndex = 0;
|
||||
while ((match = logPathPattern.exec(lineText)) !== null) {
|
||||
const filePath = match[1].replace(/[,;:]+$/, '');
|
||||
const startIndex = lineText.indexOf(filePath, match.index);
|
||||
|
||||
if (startIndex >= 0 && !app.isDuplicateLink(links, startIndex, filePath)) {
|
||||
links.push(app.createFileLink(bufferLineNumber, startIndex, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Match Claude output phrases (Created file:, Writing to, etc.)
|
||||
claudeOutputPattern.lastIndex = 0;
|
||||
while ((match = claudeOutputPattern.exec(lineText)) !== null) {
|
||||
let filePath = match[1].replace(/[,;:]+$/, '');
|
||||
// Expand ~ to /home/username (approximate)
|
||||
if (filePath.startsWith('~')) {
|
||||
filePath = '/home' + filePath.substring(1);
|
||||
}
|
||||
const startIndex = lineText.indexOf(match[1], match.index);
|
||||
|
||||
if (startIndex >= 0 && filePath.startsWith('/') && !app.isDuplicateLink(links, startIndex, filePath)) {
|
||||
links.push(app.createFileLink(bufferLineNumber, startIndex, filePath));
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging (can be removed later)
|
||||
if (links.length > 0) {
|
||||
console.log('[LinkProvider] Found', links.length, 'link(s) on line', bufferLineNumber, ':', links.map(l => l.text));
|
||||
}
|
||||
|
||||
callback(links.length > 0 ? links : undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isDuplicateLink(links, startIndex, filePath) {
|
||||
return links.some(l => l.range.start.x === startIndex + 1 && l.text === filePath);
|
||||
}
|
||||
|
||||
createFileLink(bufferLineNumber, startIndex, filePath) {
|
||||
const app = this;
|
||||
return {
|
||||
range: {
|
||||
start: { x: startIndex + 1, y: bufferLineNumber + 1 },
|
||||
end: { x: startIndex + filePath.length + 1, y: bufferLineNumber + 1 }
|
||||
},
|
||||
text: filePath,
|
||||
activate: () => {
|
||||
app.openLogViewerWindow(filePath, app.activeSessionId);
|
||||
},
|
||||
hover: (event, text) => {
|
||||
app.showFileLinkTooltip(event, text);
|
||||
},
|
||||
leave: () => {
|
||||
app.hideFileLinkTooltip();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
showFileLinkTooltip(event, filePath) {
|
||||
const tooltip = document.getElementById('file-link-tooltip');
|
||||
if (!tooltip) return;
|
||||
|
||||
const fileName = filePath.split('/').pop();
|
||||
tooltip.textContent = fileName;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = `${event.clientX + 10}px`;
|
||||
tooltip.style.top = `${event.clientY + 10}px`;
|
||||
}
|
||||
|
||||
hideFileLinkTooltip() {
|
||||
const tooltip = document.getElementById('file-link-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
// Disabled for now
|
||||
}
|
||||
|
||||
showWelcome() {
|
||||
@@ -1363,14 +1205,12 @@ class ClaudemanApp {
|
||||
|
||||
this.eventSource.addEventListener('respawn:timerCancelled', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
const { sessionId, timerName, reason } = data;
|
||||
const { sessionId, timerName } = data;
|
||||
if (this.respawnCountdownTimers[sessionId]) {
|
||||
delete this.respawnCountdownTimers[sessionId][timerName];
|
||||
}
|
||||
if (sessionId === this.activeSessionId) {
|
||||
this.updateCountdownTimerDisplay();
|
||||
// Add to action log
|
||||
this.addActionLogEntry(sessionId, { type: 'timer-cancel', detail: `${timerName}${reason ? ': ' + reason : ''}`, timestamp: Date.now() });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1674,9 +1514,10 @@ class ClaudemanApp {
|
||||
this.subagents.set(data.agentId, data);
|
||||
}
|
||||
this.renderSubagentPanel();
|
||||
// Update floating window if open
|
||||
// Update floating window if open (content + header/title)
|
||||
if (this.subagentWindows.has(data.agentId)) {
|
||||
this.renderSubagentWindowContent(data.agentId);
|
||||
this.updateSubagentWindowHeader(data.agentId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1895,6 +1736,17 @@ class ClaudemanApp {
|
||||
this._fullRenderSessionTabs();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update subagent badge - check if count changed
|
||||
const subagentBadgeEl = tab.querySelector('.tab-subagent-badge');
|
||||
const minimizedAgents = this.minimizedSubagents.get(id);
|
||||
const minimizedCount = minimizedAgents?.size || 0;
|
||||
const currentCount = subagentBadgeEl ? parseInt(subagentBadgeEl.querySelector('.subagent-count')?.textContent || '0') : 0;
|
||||
if (minimizedCount !== currentCount) {
|
||||
// Count changed - need full rebuild for dropdown update
|
||||
this._fullRenderSessionTabs();
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Full rebuild needed (sessions added/removed)
|
||||
@@ -1923,11 +1775,17 @@ class ClaudemanApp {
|
||||
const alertType = this.tabAlerts.get(id);
|
||||
const alertClass = alertType === 'action' ? ' tab-alert-action' : alertType === 'idle' ? ' tab-alert-idle' : '';
|
||||
|
||||
// Get minimized subagents for this session
|
||||
const minimizedAgents = this.minimizedSubagents.get(id);
|
||||
const minimizedCount = minimizedAgents?.size || 0;
|
||||
const subagentBadge = minimizedCount > 0 ? this.renderSubagentTabBadge(id, minimizedAgents) : '';
|
||||
|
||||
parts.push(`<div class="session-tab ${isActive ? 'active' : ''}${alertClass}" data-id="${id}" onclick="app.selectSession('${id}')" oncontextmenu="event.preventDefault(); app.startInlineRename('${id}')">
|
||||
<span class="tab-status ${status}"></span>
|
||||
${mode === 'shell' ? '<span class="tab-mode shell">sh</span>' : ''}
|
||||
<span class="tab-name" data-session-id="${id}">${this.escapeHtml(name)}</span>
|
||||
${hasRunningTasks ? `<span class="tab-badge" onclick="event.stopPropagation(); app.toggleTaskPanel()">${taskStats.running}</span>` : ''}
|
||||
${subagentBadge}
|
||||
<span class="tab-summary" onclick="event.stopPropagation(); app.openRunSummary('${id}')" title="Run Summary" data-session-id="${id}">📊</span>
|
||||
<span class="tab-gear" onclick="event.stopPropagation(); app.openSessionOptions('${id}')" title="Session options">⚙</span>
|
||||
<span class="tab-close" onclick="event.stopPropagation(); app.requestCloseSession('${id}')">×</span>
|
||||
@@ -1939,6 +1797,98 @@ class ClaudemanApp {
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
// Render subagent badge with dropdown for minimized agents on a tab
|
||||
renderSubagentTabBadge(sessionId, minimizedAgents) {
|
||||
if (!minimizedAgents || minimizedAgents.size === 0) return '';
|
||||
|
||||
const agentItems = [];
|
||||
for (const agentId of minimizedAgents) {
|
||||
const agent = this.subagents.get(agentId);
|
||||
const displayName = agent?.description || agentId.substring(0, 12);
|
||||
const truncatedName = displayName.length > 30 ? displayName.substring(0, 30) + '...' : displayName;
|
||||
const statusClass = agent?.status || 'idle';
|
||||
agentItems.push(`
|
||||
<div class="subagent-dropdown-item">
|
||||
<span class="subagent-dropdown-icon">🤖</span>
|
||||
<span class="subagent-dropdown-name" onclick="event.stopPropagation(); app.restoreMinimizedSubagent('${agentId}', '${sessionId}')" title="Click to restore">${this.escapeHtml(truncatedName)}</span>
|
||||
<span class="subagent-dropdown-status ${statusClass}">${statusClass}</span>
|
||||
<span class="subagent-dropdown-close" onclick="event.stopPropagation(); app.permanentlyCloseMinimizedSubagent('${agentId}', '${sessionId}')" title="Close permanently">×</span>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
|
||||
return `
|
||||
<span class="tab-subagent-badge" onclick="event.stopPropagation(); app.toggleSubagentDropdown(this);" title="${minimizedAgents.size} minimized subagent${minimizedAgents.size > 1 ? 's' : ''}">
|
||||
🤖<span class="subagent-count">${minimizedAgents.size}</span>
|
||||
<div class="subagent-dropdown">
|
||||
<div class="subagent-dropdown-header">Minimized Agents</div>
|
||||
${agentItems.join('')}
|
||||
</div>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// Restore a minimized subagent window
|
||||
restoreMinimizedSubagent(agentId, sessionId) {
|
||||
// Remove from minimized set
|
||||
const minimizedAgents = this.minimizedSubagents.get(sessionId);
|
||||
if (minimizedAgents) {
|
||||
minimizedAgents.delete(agentId);
|
||||
if (minimizedAgents.size === 0) {
|
||||
this.minimizedSubagents.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the window
|
||||
this.restoreSubagentWindow(agentId);
|
||||
|
||||
// Re-render tabs to update badge
|
||||
this.renderSessionTabs();
|
||||
}
|
||||
|
||||
// Toggle subagent dropdown visibility on click
|
||||
toggleSubagentDropdown(badgeEl) {
|
||||
const dropdown = badgeEl.querySelector('.subagent-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
const isOpen = dropdown.classList.contains('open');
|
||||
|
||||
// Close all other dropdowns first
|
||||
document.querySelectorAll('.subagent-dropdown.open').forEach(d => d.classList.remove('open'));
|
||||
|
||||
if (!isOpen) {
|
||||
dropdown.classList.add('open');
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
const closeHandler = (e) => {
|
||||
if (!badgeEl.contains(e.target)) {
|
||||
dropdown.classList.remove('open');
|
||||
document.removeEventListener('click', closeHandler);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeHandler), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Permanently close a minimized subagent (remove from DOM and minimized set)
|
||||
permanentlyCloseMinimizedSubagent(agentId, sessionId) {
|
||||
// Remove from minimized set
|
||||
const minimizedAgents = this.minimizedSubagents.get(sessionId);
|
||||
if (minimizedAgents) {
|
||||
minimizedAgents.delete(agentId);
|
||||
if (minimizedAgents.size === 0) {
|
||||
this.minimizedSubagents.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Force close the window (removes from DOM)
|
||||
this.forceCloseSubagentWindow(agentId);
|
||||
|
||||
// Re-render tabs to update badge
|
||||
this.renderSessionTabs();
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
getSessionName(session) {
|
||||
// Use custom name if set
|
||||
if (session.name) {
|
||||
@@ -1960,6 +1910,9 @@ class ClaudemanApp {
|
||||
|
||||
// Check if this is a restored session that needs to be attached
|
||||
const session = this.sessions.get(sessionId);
|
||||
|
||||
// Track working directory for path normalization in Project Insights
|
||||
this.currentSessionWorkingDir = session?.workingDir || null;
|
||||
if (session && session.pid === null && session.status === 'idle') {
|
||||
// This is a restored session - attach to the existing screen
|
||||
try {
|
||||
@@ -2495,8 +2448,8 @@ class ClaudemanApp {
|
||||
// Hook-based detection indicator (highest priority signals)
|
||||
if (hookEl) {
|
||||
if (detection.stopHookReceived || detection.idlePromptReceived) {
|
||||
const hookType = detection.idlePromptReceived ? 'idle_prompt' : 'Stop';
|
||||
hookEl.textContent = `🎯 ${hookType} hook received`;
|
||||
const hookType = detection.idlePromptReceived ? 'idle' : 'stop';
|
||||
hookEl.textContent = `🎯 ${hookType} hook`;
|
||||
hookEl.className = 'detection-hook hook-active';
|
||||
hookEl.style.display = '';
|
||||
} else {
|
||||
@@ -2533,35 +2486,35 @@ class ClaudemanApp {
|
||||
confidenceEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// AI check display - informative but clear
|
||||
// AI check display - compact format
|
||||
if (aiCheckEl && detection.aiCheck) {
|
||||
const ai = detection.aiCheck;
|
||||
let aiText = '';
|
||||
let aiClass = 'detection-ai-check';
|
||||
|
||||
if (ai.status === 'checking') {
|
||||
aiText = '🔍 AI: Analyzing terminal output...';
|
||||
aiText = '🔍 AI checking...';
|
||||
aiClass += ' ai-checking';
|
||||
} else if (ai.status === 'cooldown' && ai.cooldownEndsAt) {
|
||||
const remaining = Math.ceil((ai.cooldownEndsAt - Date.now()) / 1000);
|
||||
if (remaining > 0) {
|
||||
if (ai.lastVerdict === 'WORKING') {
|
||||
aiText = `⏳ AI: Working detected, retry in ${remaining}s`;
|
||||
aiText = `⏳ Working, retry ${remaining}s`;
|
||||
aiClass += ' ai-working';
|
||||
} else {
|
||||
aiText = `✓ AI: Idle confirmed, cooldown ${remaining}s`;
|
||||
aiText = `✓ Idle, wait ${remaining}s`;
|
||||
aiClass += ' ai-idle';
|
||||
}
|
||||
}
|
||||
} else if (ai.status === 'disabled') {
|
||||
aiText = `⚠ AI: Disabled (${ai.disabledReason || 'errors'})`;
|
||||
aiText = '⚠ AI disabled';
|
||||
aiClass += ' ai-disabled';
|
||||
} else if (ai.lastVerdict && ai.lastCheckTime) {
|
||||
const ago = Math.round((Date.now() - ai.lastCheckTime) / 1000);
|
||||
if (ago < 120) {
|
||||
aiText = ai.lastVerdict === 'IDLE'
|
||||
? `✓ AI: Idle (${ago}s ago)`
|
||||
: `⏳ AI: Working (${ago}s ago)`;
|
||||
? `✓ Idle (${ago}s)`
|
||||
: `⏳ Working (${ago}s)`;
|
||||
aiClass += ai.lastVerdict === 'IDLE' ? ' ai-idle' : ' ai-working';
|
||||
}
|
||||
}
|
||||
@@ -2572,6 +2525,18 @@ class ClaudemanApp {
|
||||
} else if (aiCheckEl) {
|
||||
aiCheckEl.style.display = 'none';
|
||||
}
|
||||
|
||||
// Manage row2 visibility - hide if nothing visible
|
||||
const row2 = this.$('respawnStatusRow2');
|
||||
if (row2) {
|
||||
const hasVisibleContent =
|
||||
(hookEl && hookEl.style.display !== 'none') ||
|
||||
(aiCheckEl && aiCheckEl.style.display !== 'none') ||
|
||||
(statusEl && statusEl.style.display !== 'none') ||
|
||||
(this.respawnCountdownTimers[this.activeSessionId] &&
|
||||
Object.keys(this.respawnCountdownTimers[this.activeSessionId]).length > 0);
|
||||
row2.style.display = hasVisibleContent ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
showRespawnTimer() {
|
||||
@@ -2638,24 +2603,39 @@ class ClaudemanApp {
|
||||
// ========== Countdown Timer Display Methods ==========
|
||||
|
||||
addActionLogEntry(sessionId, action) {
|
||||
// Filter to important actions - keep more for visibility
|
||||
// Skip: timer starts (but keep cancels with reason), routine detection spam
|
||||
if (action.type === 'timer') return;
|
||||
// Keep timer-cancel only if it has a meaningful reason
|
||||
if (action.type === 'timer-cancel' && !action.detail) return;
|
||||
// Keep detection only for completion/silence confirmed events
|
||||
if (action.type === 'detection' && !action.detail.includes('confirmed') && !action.detail.includes('Completion')) return;
|
||||
// Keep ai-check only for verdicts (IDLE, WORKING) and errors
|
||||
if (action.type === 'ai-check' && action.detail.includes('Spawning')) return;
|
||||
// Keep plan-check for verdicts and actions
|
||||
if (action.type === 'plan-check' && action.detail.includes('Spawning')) return;
|
||||
// Only keep truly interesting events - no spam
|
||||
// KEEP: command (inputs), hook events, AI verdicts, plan verdicts
|
||||
// SKIP: timer, timer-cancel, state changes, routine detection, step confirmations
|
||||
|
||||
const interestingTypes = ['command', 'hook'];
|
||||
|
||||
// Always keep commands and hooks
|
||||
if (interestingTypes.includes(action.type)) {
|
||||
// ok, keep it
|
||||
}
|
||||
// AI check: only verdicts (IDLE, WORKING) and errors, not "Spawning"
|
||||
else if (action.type === 'ai-check') {
|
||||
if (action.detail.includes('Spawning')) return;
|
||||
}
|
||||
// Plan check: only verdicts, not "Spawning"
|
||||
else if (action.type === 'plan-check') {
|
||||
if (action.detail.includes('Spawning')) return;
|
||||
}
|
||||
// Transcript: keep completion/plan detection
|
||||
else if (action.type === 'transcript') {
|
||||
// keep it
|
||||
}
|
||||
// Skip everything else (timer, timer-cancel, state, detection, step)
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.respawnActionLogs[sessionId]) {
|
||||
this.respawnActionLogs[sessionId] = [];
|
||||
}
|
||||
this.respawnActionLogs[sessionId].unshift(action);
|
||||
// Keep more history (50 entries instead of 20)
|
||||
if (this.respawnActionLogs[sessionId].length > 50) {
|
||||
// Keep reasonable history
|
||||
if (this.respawnActionLogs[sessionId].length > 30) {
|
||||
this.respawnActionLogs[sessionId].pop();
|
||||
}
|
||||
}
|
||||
@@ -2677,42 +2657,50 @@ class ClaudemanApp {
|
||||
}
|
||||
|
||||
updateCountdownTimerDisplay() {
|
||||
const timersRow = this.$('respawnTimersRow');
|
||||
const timersContainer = this.$('respawnCountdownTimers');
|
||||
if (!timersRow || !timersContainer) return;
|
||||
const row2 = this.$('respawnStatusRow2');
|
||||
if (!timersContainer) return;
|
||||
|
||||
const timers = this.respawnCountdownTimers[this.activeSessionId];
|
||||
const actions = this.respawnActionLogs[this.activeSessionId];
|
||||
const hasTimers = timers && Object.keys(timers).length > 0;
|
||||
const hasActions = actions && actions.length > 0;
|
||||
|
||||
// Show row if we have timers OR action log entries
|
||||
if (!hasTimers && !hasActions) {
|
||||
timersRow.style.display = 'none';
|
||||
if (!hasTimers) {
|
||||
timersContainer.innerHTML = '';
|
||||
// Update row2 visibility
|
||||
if (row2) {
|
||||
const hookEl = document.getElementById('detectionHook');
|
||||
const aiCheckEl = document.getElementById('detectionAiCheck');
|
||||
const statusEl = this.$('detectionStatus');
|
||||
const hasVisibleContent =
|
||||
(hookEl && hookEl.style.display !== 'none') ||
|
||||
(aiCheckEl && aiCheckEl.style.display !== 'none') ||
|
||||
(statusEl && statusEl.style.display !== 'none');
|
||||
row2.style.display = hasVisibleContent ? '' : 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
timersRow.style.display = '';
|
||||
// Show row2 since we have timers
|
||||
if (row2) row2.style.display = '';
|
||||
|
||||
const now = Date.now();
|
||||
let html = '';
|
||||
|
||||
if (hasTimers) {
|
||||
for (const [name, timer] of Object.entries(timers)) {
|
||||
const remainingMs = Math.max(0, timer.endsAt - now);
|
||||
const remainingSec = (remainingMs / 1000).toFixed(1);
|
||||
const percent = Math.max(0, Math.min(100, (remainingMs / timer.totalMs) * 100));
|
||||
for (const [name, timer] of Object.entries(timers)) {
|
||||
const remainingMs = Math.max(0, timer.endsAt - now);
|
||||
const remainingSec = (remainingMs / 1000).toFixed(1);
|
||||
const percent = Math.max(0, Math.min(100, (remainingMs / timer.totalMs) * 100));
|
||||
|
||||
// Format timer name for display (replace hyphens with spaces, capitalize first letter)
|
||||
const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||
// Shorter timer name display
|
||||
const displayName = name.replace(/-/g, ' ').replace(/^\w/, c => c.toUpperCase());
|
||||
|
||||
html += `<div class="respawn-countdown-timer" title="${timer.reason || ''}">
|
||||
<span class="timer-name">${displayName}</span>
|
||||
<span class="timer-value">${remainingSec}s</span>
|
||||
<div class="respawn-timer-bar">
|
||||
<div class="respawn-timer-progress" style="width: ${percent}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
html += `<div class="respawn-countdown-timer" title="${timer.reason || ''}">
|
||||
<span class="timer-name">${displayName}</span>
|
||||
<span class="timer-value">${remainingSec}s</span>
|
||||
<div class="respawn-timer-bar">
|
||||
<div class="respawn-timer-progress" style="width: ${percent}%"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
timersContainer.innerHTML = html;
|
||||
@@ -2724,12 +2712,13 @@ class ClaudemanApp {
|
||||
|
||||
const actions = this.respawnActionLogs[this.activeSessionId];
|
||||
if (!actions || actions.length === 0) {
|
||||
logContainer.innerHTML = '<span class="action-empty">No recent actions</span>';
|
||||
logContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const action of actions.slice(0, 10)) {
|
||||
// Show fewer entries for compact view
|
||||
for (const action of actions.slice(0, 5)) {
|
||||
const time = new Date(action.timestamp).toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
@@ -2738,6 +2727,7 @@ class ClaudemanApp {
|
||||
});
|
||||
const isCommand = action.type === 'command';
|
||||
const extraClass = isCommand ? ' action-command' : '';
|
||||
// Compact format: time [type] detail
|
||||
html += `<div class="respawn-action-entry${extraClass}">
|
||||
<span class="action-time">${time}</span>
|
||||
<span class="action-type">[${action.type}]</span>
|
||||
@@ -5220,8 +5210,7 @@ class ClaudemanApp {
|
||||
<span class="status ${agent.status}">${agent.status}</span>
|
||||
</div>
|
||||
<div class="subagent-window-actions">
|
||||
<button onclick="app.minimizeSubagentWindow('${agentId}')" title="Minimize">─</button>
|
||||
<button onclick="app.closeSubagentWindow('${agentId}')" title="Close">×</button>
|
||||
<button onclick="app.closeSubagentWindow('${agentId}')" title="Minimize to tab">─</button>
|
||||
</div>
|
||||
</div>
|
||||
${parentHeader}
|
||||
@@ -5279,6 +5268,15 @@ class ClaudemanApp {
|
||||
win.style.zIndex = ++this.subagentWindowZIndex;
|
||||
});
|
||||
|
||||
// Update connection lines when window is resized
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
this.updateConnectionLines();
|
||||
});
|
||||
resizeObserver.observe(win);
|
||||
|
||||
// Store observer for cleanup
|
||||
this.subagentWindows.get(agentId).resizeObserver = resizeObserver;
|
||||
|
||||
// Animate to final position if spawning from tab
|
||||
if (parentTab) {
|
||||
requestAnimationFrame(() => {
|
||||
@@ -5303,14 +5301,34 @@ class ClaudemanApp {
|
||||
|
||||
closeSubagentWindow(agentId) {
|
||||
const windowData = this.subagentWindows.get(agentId);
|
||||
if (windowData) {
|
||||
if (!windowData) return;
|
||||
|
||||
const agent = this.subagents.get(agentId);
|
||||
const parentSessionId = agent?.parentSessionId;
|
||||
|
||||
if (parentSessionId) {
|
||||
// Minimize to tab instead of removing
|
||||
windowData.element.style.display = 'none';
|
||||
windowData.minimized = true;
|
||||
|
||||
// Track minimized agent for the parent session
|
||||
if (!this.minimizedSubagents.has(parentSessionId)) {
|
||||
this.minimizedSubagents.set(parentSessionId, new Set());
|
||||
}
|
||||
this.minimizedSubagents.get(parentSessionId).add(agentId);
|
||||
|
||||
// Update tab badge to show minimized agents
|
||||
this.renderSessionTabs();
|
||||
} else {
|
||||
// No parent session - fully remove the window
|
||||
windowData.element.remove();
|
||||
this.subagentWindows.delete(agentId);
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
this.updateConnectionLines();
|
||||
}
|
||||
|
||||
// Close all subagent windows for a session
|
||||
// Close all subagent windows for a session (fully removes them, not minimize)
|
||||
closeSessionSubagentWindows(sessionId) {
|
||||
const toClose = [];
|
||||
for (const [agentId, _windowData] of this.subagentWindows) {
|
||||
@@ -5320,7 +5338,23 @@ class ClaudemanApp {
|
||||
}
|
||||
}
|
||||
for (const agentId of toClose) {
|
||||
this.closeSubagentWindow(agentId);
|
||||
this.forceCloseSubagentWindow(agentId);
|
||||
}
|
||||
// Also clean up minimized agents for this session
|
||||
this.minimizedSubagents.delete(sessionId);
|
||||
this.renderSessionTabs();
|
||||
}
|
||||
|
||||
// Fully close a subagent window (removes from DOM, not minimize)
|
||||
forceCloseSubagentWindow(agentId) {
|
||||
const windowData = this.subagentWindows.get(agentId);
|
||||
if (windowData) {
|
||||
// Clean up resize observer
|
||||
if (windowData.resizeObserver) {
|
||||
windowData.resizeObserver.disconnect();
|
||||
}
|
||||
windowData.element.remove();
|
||||
this.subagentWindows.delete(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5437,16 +5471,37 @@ class ClaudemanApp {
|
||||
updateSubagentWindows() {
|
||||
for (const agentId of this.subagentWindows.keys()) {
|
||||
this.renderSubagentWindowContent(agentId);
|
||||
this.updateSubagentWindowHeader(agentId);
|
||||
}
|
||||
}
|
||||
|
||||
// Update status in header
|
||||
const agent = this.subagents.get(agentId);
|
||||
if (agent) {
|
||||
const statusEl = document.querySelector(`#subagent-window-${agentId} .status`);
|
||||
if (statusEl) {
|
||||
statusEl.className = `status ${agent.status}`;
|
||||
statusEl.textContent = agent.status;
|
||||
}
|
||||
}
|
||||
// Update subagent window header (title and status)
|
||||
updateSubagentWindowHeader(agentId) {
|
||||
const agent = this.subagents.get(agentId);
|
||||
if (!agent) return;
|
||||
|
||||
const win = document.getElementById(`subagent-window-${agentId}`);
|
||||
if (!win) return;
|
||||
|
||||
// Update title/id element with description if available
|
||||
const idEl = win.querySelector('.subagent-window-title .id');
|
||||
if (idEl) {
|
||||
const windowTitle = agent.description || agentId.substring(0, 7);
|
||||
const truncatedTitle = windowTitle.length > 50 ? windowTitle.substring(0, 50) + '...' : windowTitle;
|
||||
idEl.textContent = truncatedTitle;
|
||||
}
|
||||
|
||||
// Update full tooltip
|
||||
const titleContainer = win.querySelector('.subagent-window-title');
|
||||
if (titleContainer) {
|
||||
titleContainer.title = agent.description || agentId;
|
||||
}
|
||||
|
||||
// Update status
|
||||
const statusEl = win.querySelector('.subagent-window-title .status');
|
||||
if (statusEl) {
|
||||
statusEl.className = `status ${agent.status}`;
|
||||
statusEl.textContent = agent.status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5461,6 +5516,201 @@ class ClaudemanApp {
|
||||
|
||||
// ========== Project Insights Panel (Bash Tools with Clickable File Paths) ==========
|
||||
|
||||
/**
|
||||
* Normalize a file path to its canonical form for comparison.
|
||||
* - Expands ~ to home directory approximation
|
||||
* - Resolves relative paths against working directory (case folder)
|
||||
* - Normalizes . and .. components
|
||||
*/
|
||||
normalizeFilePath(path, workingDir) {
|
||||
if (!path) return '';
|
||||
|
||||
let normalized = path.trim();
|
||||
const homeDir = '/home/' + (window.USER || 'user'); // Approximation
|
||||
|
||||
// Expand ~ to home directory
|
||||
if (normalized.startsWith('~/')) {
|
||||
normalized = homeDir + normalized.slice(1);
|
||||
} else if (normalized === '~') {
|
||||
normalized = homeDir;
|
||||
}
|
||||
|
||||
// If not absolute, resolve against working directory (case folder)
|
||||
if (!normalized.startsWith('/') && workingDir) {
|
||||
normalized = workingDir + '/' + normalized;
|
||||
}
|
||||
|
||||
// Normalize path components (resolve . and ..)
|
||||
const parts = normalized.split('/');
|
||||
const stack = [];
|
||||
|
||||
for (const part of parts) {
|
||||
if (part === '' || part === '.') {
|
||||
continue;
|
||||
} else if (part === '..') {
|
||||
if (stack.length > 1) {
|
||||
stack.pop();
|
||||
}
|
||||
} else {
|
||||
stack.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
return '/' + stack.join('/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract just the filename from a path.
|
||||
*/
|
||||
getFilename(path) {
|
||||
const parts = path.split('/');
|
||||
return parts[parts.length - 1] || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is a "shallow root path" - an absolute path with only one
|
||||
* component after root (e.g., /test.txt, /file.log).
|
||||
* These are often typos where the user meant a relative path in the case folder.
|
||||
*/
|
||||
isShallowRootPath(path) {
|
||||
if (!path.startsWith('/')) return false;
|
||||
const parts = path.split('/').filter(p => p !== '');
|
||||
return parts.length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path is inside (or is) the working directory (case folder).
|
||||
*/
|
||||
isPathInWorkingDir(path, workingDir) {
|
||||
if (!workingDir) return false;
|
||||
const normalized = this.normalizeFilePath(path, workingDir);
|
||||
return normalized.startsWith(workingDir + '/') || normalized === workingDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart path equivalence check.
|
||||
* Two paths are considered equivalent if:
|
||||
* 1. They normalize to the same path (standard case)
|
||||
* 2. One is a "shallow root path" (e.g., /test.txt) and the other is the
|
||||
* same filename inside the case folder - the shallow root path
|
||||
* is likely a typo and they probably meant the same file.
|
||||
*/
|
||||
pathsAreEquivalent(path1, path2, workingDir) {
|
||||
const norm1 = this.normalizeFilePath(path1, workingDir);
|
||||
const norm2 = this.normalizeFilePath(path2, workingDir);
|
||||
|
||||
// Standard check: exact normalized match
|
||||
if (norm1 === norm2) return true;
|
||||
|
||||
// Smart check: shallow root path vs case folder path with same filename
|
||||
const file1 = this.getFilename(norm1);
|
||||
const file2 = this.getFilename(norm2);
|
||||
|
||||
if (file1 !== file2) return false; // Different filenames, can't be equivalent
|
||||
|
||||
const shallow1 = this.isShallowRootPath(path1);
|
||||
const shallow2 = this.isShallowRootPath(path2);
|
||||
const inWorkDir1 = this.isPathInWorkingDir(norm1, workingDir);
|
||||
const inWorkDir2 = this.isPathInWorkingDir(norm2, workingDir);
|
||||
|
||||
// If one is shallow root (e.g., /test.txt) and other is in case folder
|
||||
// with same filename, treat as equivalent (user likely made a typo)
|
||||
if (shallow1 && inWorkDir2) return true;
|
||||
if (shallow2 && inWorkDir1) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the "better" of two paths that resolve to the same file.
|
||||
* Prefers paths inside the case folder, longer/more explicit paths, and absolute paths.
|
||||
*/
|
||||
pickBetterPath(path1, path2, workingDir) {
|
||||
// Prefer paths inside the case folder (working directory)
|
||||
if (workingDir) {
|
||||
const inWorkDir1 = this.isPathInWorkingDir(path1, workingDir);
|
||||
const inWorkDir2 = this.isPathInWorkingDir(path2, workingDir);
|
||||
if (inWorkDir1 && !inWorkDir2) return path1;
|
||||
if (inWorkDir2 && !inWorkDir1) return path2;
|
||||
}
|
||||
|
||||
// Prefer absolute paths
|
||||
const abs1 = path1.startsWith('/');
|
||||
const abs2 = path2.startsWith('/');
|
||||
if (abs1 && !abs2) return path1;
|
||||
if (abs2 && !abs1) return path2;
|
||||
|
||||
// Both absolute or both relative - prefer longer (more explicit)
|
||||
if (path1.length !== path2.length) {
|
||||
return path1.length > path2.length ? path1 : path2;
|
||||
}
|
||||
|
||||
// Prefer paths without ~
|
||||
if (!path1.includes('~') && path2.includes('~')) return path1;
|
||||
if (!path2.includes('~') && path1.includes('~')) return path2;
|
||||
|
||||
return path1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deduplicate file paths across all tools, keeping the "best" version.
|
||||
* Uses smart equivalence checking:
|
||||
* - Standard normalization for relative vs absolute paths
|
||||
* - Detects likely typos (e.g., /file.txt when caseFolder/file.txt exists)
|
||||
* - Prefers paths inside the case folder (working directory)
|
||||
* - Prefers longer, more explicit paths
|
||||
* Returns a Map of normalized path -> best raw path.
|
||||
*/
|
||||
deduplicateProjectInsightPaths(tools, workingDir) {
|
||||
// Collect all paths with their tool IDs
|
||||
const allPaths = [];
|
||||
for (const tool of tools) {
|
||||
for (const rawPath of tool.filePaths) {
|
||||
allPaths.push({ rawPath, toolId: tool.id });
|
||||
}
|
||||
}
|
||||
|
||||
if (allPaths.length <= 1) {
|
||||
const pathMap = new Map();
|
||||
for (const p of allPaths) {
|
||||
pathMap.set(this.normalizeFilePath(p.rawPath, workingDir), p);
|
||||
}
|
||||
return pathMap;
|
||||
}
|
||||
|
||||
// Sort paths: prefer paths in case folder first, then by length (longer first)
|
||||
allPaths.sort((a, b) => {
|
||||
const aInWorkDir = this.isPathInWorkingDir(a.rawPath, workingDir);
|
||||
const bInWorkDir = this.isPathInWorkingDir(b.rawPath, workingDir);
|
||||
if (aInWorkDir && !bInWorkDir) return -1;
|
||||
if (bInWorkDir && !aInWorkDir) return 1;
|
||||
return b.rawPath.length - a.rawPath.length; // Longer paths first
|
||||
});
|
||||
|
||||
const result = new Map(); // normalized -> { rawPath, toolId }
|
||||
const seenNormalized = new Set();
|
||||
|
||||
for (const { rawPath, toolId } of allPaths) {
|
||||
const normalized = this.normalizeFilePath(rawPath, workingDir);
|
||||
|
||||
// Check if we've already seen an equivalent path
|
||||
let isDuplicate = false;
|
||||
for (const [, existing] of result) {
|
||||
if (this.pathsAreEquivalent(rawPath, existing.rawPath, workingDir)) {
|
||||
isDuplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDuplicate && !seenNormalized.has(normalized)) {
|
||||
result.set(normalized, { rawPath, toolId });
|
||||
seenNormalized.add(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
handleBashToolStart(sessionId, tool) {
|
||||
let tools = this.projectInsights.get(sessionId) || [];
|
||||
// Add new tool
|
||||
@@ -5517,8 +5767,25 @@ class ClaudemanApp {
|
||||
panel.classList.add('visible');
|
||||
this.projectInsightsPanelVisible = true;
|
||||
|
||||
// Get working directory for path normalization
|
||||
const session = this.sessions.get(this.activeSessionId);
|
||||
const workingDir = session?.workingDir || this.currentSessionWorkingDir;
|
||||
|
||||
// Smart deduplication: collect all unique paths across all tools
|
||||
// Paths that resolve to the same file are deduplicated, keeping the most complete version
|
||||
const deduplicatedPaths = this.deduplicateProjectInsightPaths(runningTools, workingDir);
|
||||
|
||||
// Build a set of paths to show (only the best version of each unique file)
|
||||
const pathsToShow = new Set(Array.from(deduplicatedPaths.values()).map(p => p.rawPath));
|
||||
|
||||
const html = [];
|
||||
for (const tool of runningTools) {
|
||||
// Filter this tool's paths to only include those that weren't deduplicated away
|
||||
const filteredPaths = tool.filePaths.filter(p => pathsToShow.has(p));
|
||||
|
||||
// Skip tools with no paths to show (all were duplicates of better paths elsewhere)
|
||||
if (filteredPaths.length === 0) continue;
|
||||
|
||||
const cmdDisplay = tool.command.length > 50
|
||||
? tool.command.substring(0, 50) + '...'
|
||||
: tool.command;
|
||||
@@ -5534,7 +5801,7 @@ class ClaudemanApp {
|
||||
<div class="project-insight-paths">
|
||||
`);
|
||||
|
||||
for (const path of tool.filePaths) {
|
||||
for (const path of filteredPaths) {
|
||||
const fileName = path.split('/').pop();
|
||||
html.push(`
|
||||
<span class="project-insight-filepath"
|
||||
|
||||
+20
-17
@@ -65,24 +65,27 @@
|
||||
<button class="btn-icon-only" onclick="app.stopCurrentRun()" title="Stop">■</button>
|
||||
</div>
|
||||
|
||||
<!-- Respawn Banner -->
|
||||
<!-- Respawn Banner - Compact 2-column layout -->
|
||||
<div class="respawn-banner" id="respawnBanner" style="display: none;">
|
||||
<div class="respawn-banner-row">
|
||||
<span class="respawn-indicator">↻</span>
|
||||
<span class="respawn-state" id="respawnState">Watching</span>
|
||||
<span class="detection-status" id="detectionStatus"></span>
|
||||
<span class="detection-waiting" id="detectionWaiting" style="display: none;"></span>
|
||||
<span class="detection-confidence" id="detectionConfidence"></span>
|
||||
<span class="detection-hook" id="detectionHook" style="display: none;"></span>
|
||||
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
|
||||
<span class="respawn-spacer"></span>
|
||||
<span class="respawn-cycles">#<span id="respawnCycleCount">0</span></span>
|
||||
<span class="respawn-timer" id="respawnTimer" style="display: none;"></span>
|
||||
<span class="respawn-tokens" id="respawnTokens" style="display: none;"></span>
|
||||
<button class="btn-icon-only" onclick="app.stopRespawn()" title="Stop Respawn">■</button>
|
||||
</div>
|
||||
<div class="respawn-timers-row" id="respawnTimersRow" style="display: none;">
|
||||
<div class="respawn-countdown-timers" id="respawnCountdownTimers"></div>
|
||||
<div class="respawn-compact-layout">
|
||||
<div class="respawn-status-col">
|
||||
<div class="respawn-status-row1">
|
||||
<span class="respawn-indicator">↻</span>
|
||||
<span class="respawn-state" id="respawnState">Watching</span>
|
||||
<span class="detection-confidence" id="detectionConfidence"></span>
|
||||
<span class="respawn-cycles">#<span id="respawnCycleCount">0</span></span>
|
||||
<span class="respawn-timer" id="respawnTimer" style="display: none;"></span>
|
||||
<span class="respawn-tokens" id="respawnTokens" style="display: none;"></span>
|
||||
<button class="btn-icon-only" onclick="app.stopRespawn()" title="Stop Respawn">■</button>
|
||||
</div>
|
||||
<div class="respawn-status-row2" id="respawnStatusRow2">
|
||||
<span class="detection-hook" id="detectionHook" style="display: none;"></span>
|
||||
<span class="detection-ai-check" id="detectionAiCheck" style="display: none;"></span>
|
||||
<span class="detection-status" id="detectionStatus"></span>
|
||||
<span class="detection-waiting" id="detectionWaiting" style="display: none;"></span>
|
||||
<div class="respawn-countdown-timers" id="respawnCountdownTimers"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="respawn-action-log" id="respawnActionLog"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+237
-58
@@ -451,47 +451,64 @@ body {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
/* Respawn Banner - Compact */
|
||||
/* Respawn Banner - Compact 2-column layout */
|
||||
.respawn-banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0.35rem 1rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: linear-gradient(90deg, #1e3a1e, #132513);
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.3);
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.respawn-banner-row {
|
||||
.respawn-compact-layout {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.respawn-timers-row {
|
||||
.respawn-status-col {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid rgba(34, 197, 94, 0.15);
|
||||
margin-top: 0.35rem;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.respawn-status-row1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.respawn-status-row2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 1.2em;
|
||||
font-size: 0.65rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.respawn-status-row2:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.respawn-countdown-timers {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
gap: 0.3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.respawn-countdown-timer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: 0.25rem;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 3px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.respawn-countdown-timer .timer-name {
|
||||
@@ -501,15 +518,15 @@ body {
|
||||
.respawn-countdown-timer .timer-value {
|
||||
color: var(--green);
|
||||
font-weight: 500;
|
||||
min-width: 2.5em;
|
||||
min-width: 2em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.respawn-timer-bar {
|
||||
width: 40px;
|
||||
height: 3px;
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 2px;
|
||||
border-radius: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -521,16 +538,17 @@ body {
|
||||
|
||||
.respawn-action-log {
|
||||
flex: 1;
|
||||
max-height: 150px;
|
||||
max-height: 3.2em;
|
||||
overflow-y: auto;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.9;
|
||||
font-size: 0.65rem;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
color: var(--text-dim);
|
||||
border-left: 1px solid rgba(34, 197, 94, 0.2);
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.respawn-action-log::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
width: 3px;
|
||||
}
|
||||
|
||||
.respawn-action-log::-webkit-scrollbar-thumb {
|
||||
@@ -539,8 +557,7 @@ body {
|
||||
}
|
||||
|
||||
.respawn-action-entry {
|
||||
padding: 0.1rem 0;
|
||||
border-bottom: 1px solid rgba(34, 197, 94, 0.1);
|
||||
padding: 0.05rem 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -548,12 +565,12 @@ body {
|
||||
|
||||
.respawn-action-entry .action-time {
|
||||
color: var(--text-muted);
|
||||
margin-right: 0.4rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.respawn-action-entry .action-type {
|
||||
color: var(--green);
|
||||
margin-right: 0.4rem;
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
.respawn-action-entry .action-detail {
|
||||
@@ -562,10 +579,11 @@ body {
|
||||
|
||||
/* Command entries are highlighted */
|
||||
.respawn-action-entry.action-command {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-left: 2px solid var(--accent);
|
||||
padding-left: 0.3rem;
|
||||
margin: 0.15rem 0;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.respawn-action-entry.action-command .action-detail {
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
.respawn-action-entry.action-command .action-type {
|
||||
@@ -581,54 +599,50 @@ body {
|
||||
.respawn-indicator {
|
||||
color: var(--green);
|
||||
animation: spin 2s linear infinite;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.respawn-state {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
background: rgba(34, 197, 94, 0.3);
|
||||
padding: 0.15rem 0.5rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.respawn-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
/* Respawn spacer removed - using flex layout */
|
||||
|
||||
.respawn-cycles {
|
||||
color: var(--text-muted);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.respawn-timer {
|
||||
color: var(--accent-hover);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-weight: 500;
|
||||
padding: 0 0.5rem;
|
||||
font-size: 0.65rem;
|
||||
padding: 0 0.4rem;
|
||||
border-left: 1px solid rgba(34, 197, 94, 0.3);
|
||||
border-right: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
|
||||
.respawn-tokens {
|
||||
color: var(--text-dim);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.7rem;
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.detection-status {
|
||||
color: var(--text);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.1rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 3px;
|
||||
color: var(--text-dim);
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.detection-ai-check {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@@ -655,10 +669,10 @@ body {
|
||||
.detection-confidence {
|
||||
color: var(--text-dim);
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
font-size: 0.6rem;
|
||||
padding: 0.05rem 0.25rem;
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
border-radius: 3px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.detection-confidence.high {
|
||||
@@ -677,9 +691,9 @@ body {
|
||||
}
|
||||
|
||||
.detection-hook {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.65rem;
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 2px;
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: var(--purple, #a78bfa);
|
||||
}
|
||||
@@ -688,7 +702,6 @@ body {
|
||||
background: rgba(139, 92, 246, 0.25);
|
||||
color: var(--purple, #a78bfa);
|
||||
font-weight: 500;
|
||||
animation: pulse-hook 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-hook {
|
||||
@@ -2922,6 +2935,136 @@ kbd {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Subagent Badge on Tabs */
|
||||
.session-tab .tab-subagent-badge {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.7rem;
|
||||
cursor: pointer;
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
border: 1px solid var(--accent);
|
||||
}
|
||||
|
||||
.session-tab .tab-subagent-badge:hover {
|
||||
background: rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.session-tab .tab-subagent-badge .subagent-count {
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
/* Subagent Dropdown */
|
||||
.subagent-dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 220px;
|
||||
max-width: 320px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-subagent-badge:hover .subagent-dropdown,
|
||||
.subagent-dropdown.open {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.subagent-dropdown-header {
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
background: var(--bg-input);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.subagent-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.subagent-dropdown-item:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.subagent-dropdown-icon {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.subagent-dropdown-name {
|
||||
flex: 1;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.subagent-dropdown-status {
|
||||
font-size: 0.6rem;
|
||||
padding: 0.1rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.subagent-dropdown-status.active {
|
||||
background: var(--green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subagent-dropdown-status.idle {
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.subagent-dropdown-status.completed {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.subagent-dropdown-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.subagent-dropdown-name:hover {
|
||||
color: var(--accent-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.subagent-dropdown-close {
|
||||
font-size: 1rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
padding: 0 0.25rem;
|
||||
margin-left: auto;
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.subagent-dropdown-close:hover {
|
||||
opacity: 1;
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
/* Task Panel */
|
||||
.task-panel {
|
||||
position: fixed;
|
||||
@@ -3823,6 +3966,8 @@ kbd {
|
||||
height: 350px;
|
||||
min-width: 280px;
|
||||
min-height: 180px;
|
||||
max-width: 90vw;
|
||||
max-height: 80vh;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
@@ -3834,6 +3979,19 @@ kbd {
|
||||
resize: both;
|
||||
}
|
||||
|
||||
/* Resize handle indicator in bottom-right corner */
|
||||
.subagent-window::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 4px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: linear-gradient(135deg, transparent 50%, var(--border-light) 50%, var(--border-light) 60%, transparent 60%, transparent 70%, var(--border-light) 70%, var(--border-light) 80%, transparent 80%);
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.subagent-window-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -3892,11 +4050,32 @@ kbd {
|
||||
.subagent-window-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0.5rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.75rem;
|
||||
background: #111;
|
||||
color: #c8c8c8;
|
||||
min-height: 0; /* Allow flex child to shrink below content size */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for subagent window body */
|
||||
.subagent-window-body::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.subagent-window-body::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.subagent-window-body::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.subagent-window-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--border-light);
|
||||
}
|
||||
|
||||
.subagent-window-body .activity-line {
|
||||
|
||||
Reference in New Issue
Block a user