From c667e0baac08dddc0ded3d59acf487342db059c9 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 29 Jan 2026 11:39:12 +0100 Subject: [PATCH] fix: match subagents to sessions by claudeSessionId instead of newest heuristic When multiple Claudeman sessions share the same workingDir, subagents were incorrectly attaching to the newest session instead of the actual parent. Now uses direct claudeSessionId matching for reliable parent detection. Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 2 +- package.json | 2 +- src/web/public/app.js | 40 +++++++++--- src/web/public/styles.css | 125 +++++++++++++++++++++++++++++--------- 4 files changed, 128 insertions(+), 41 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 316d565d..9b19f3ed 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ When user says "COM": 1. Increment version in BOTH `package.json` AND `CLAUDE.md` 2. Run: `git add -A && git commit -m "chore: bump version to X.XXXX" && git push && npm run build && systemctl --user restart claudeman-web` -**Version**: 0.1422 (must match `package.json`) +**Version**: 0.1423 (must match `package.json`) ## Project Overview diff --git a/package.json b/package.json index 0df593e8..bbd4b01a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claudeman", - "version": "0.1422", + "version": "0.1423", "description": "The missing control plane for Claude Code - run 20 autonomous agents with real-time monitoring and session persistence", "type": "module", "main": "dist/index.js", diff --git a/src/web/public/app.js b/src/web/public/app.js index dd5bb9e6..c6fcf4cb 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -9247,7 +9247,23 @@ class ClaudemanApp { } } - // Strategy 2: Find all sessions that match by workingDir + // 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 = []; for (const [sessionId, session] of this.sessions) { try { @@ -9272,14 +9288,20 @@ class ClaudemanApp { let chosen = matchingSessions[0]; if (matchingSessions.length > 1) { - // Multiple sessions with same workingDir - use heuristics to pick the right one - // Prefer the most recently created session (newest session likely spawned the subagent) - matchingSessions.sort((a, b) => { - const aTime = new Date(a.session.createdAt || 0).getTime(); - const bTime = new Date(b.session.createdAt || 0).getTime(); - return bTime - aTime; // Newest first - }); - chosen = matchingSessions[0]; + // 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]; + } } // Assign the parent diff --git a/src/web/public/styles.css b/src/web/public/styles.css index 7284be82..73c1d020 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -2368,53 +2368,91 @@ body { } /* Run Summary Modal */ + +/* Summary tab scrollable container */ +#summary-tab { + display: flex; + flex-direction: column; + max-height: calc(70vh - 120px); + overflow: hidden; +} + +#summary-tab .run-summary-stats { + flex-shrink: 0; +} + +#summary-tab .run-summary-filters { + flex-shrink: 0; +} + +#summary-tab .run-summary-timeline { + flex: 1; + min-height: 0; + max-height: none; +} + +#summary-tab .run-summary-footer { + flex-shrink: 0; +} + .run-summary-stats { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 0.75rem; - margin-bottom: 1rem; + gap: 0.5rem; + margin-bottom: 0.75rem; } .run-summary-stats .stat-card { - background: var(--bg-hover); - border: 1px solid var(--border); - border-radius: 6px; - padding: 0.75rem; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.04) 100%); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + padding: 0.5rem 0.625rem; text-align: center; + transition: border-color 0.15s, background 0.15s; +} + +.run-summary-stats .stat-card:hover { + border-color: rgba(255, 255, 255, 0.12); + background: linear-gradient(135deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.05) 100%); } .run-summary-stats .stat-value { - font-size: 1.5rem; + font-size: 1rem; font-weight: 600; - color: var(--accent); + color: var(--text); + font-family: 'SF Mono', Monaco, monospace; + letter-spacing: -0.02em; } .run-summary-stats .stat-label { - font-size: 0.75rem; - color: var(--text-dim); - margin-top: 0.25rem; + font-size: 0.65rem; + color: var(--text-muted); + margin-top: 0.125rem; + text-transform: uppercase; + letter-spacing: 0.03em; } .run-summary-filters { display: flex; - gap: 0.5rem; - margin-bottom: 1rem; + gap: 0.375rem; + margin-bottom: 0.75rem; flex-wrap: wrap; } .filter-btn { - padding: 0.375rem 0.75rem; - background: var(--bg-hover); - border: 1px solid var(--border); - border-radius: 4px; + padding: 0.25rem 0.625rem; + background: transparent; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 100px; color: var(--text-dim); cursor: pointer; - font-size: 0.8rem; + font-size: 0.7rem; transition: all 0.15s; } .filter-btn:hover { - background: var(--bg-card); + background: rgba(255, 255, 255, 0.04); + border-color: rgba(255, 255, 255, 0.15); color: var(--text); } @@ -2427,17 +2465,41 @@ body { .run-summary-timeline { max-height: 400px; overflow-y: auto; - padding-right: 0.5rem; + padding-right: 0.25rem; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.1) transparent; +} + +.run-summary-timeline::-webkit-scrollbar { + width: 4px; +} + +.run-summary-timeline::-webkit-scrollbar-track { + background: transparent; +} + +.run-summary-timeline::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + +.run-summary-timeline::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.2); } .timeline-event { display: flex; - gap: 0.75rem; - padding: 0.75rem; - margin-bottom: 0.5rem; - background: var(--bg-hover); + gap: 0.5rem; + padding: 0.5rem 0.625rem; + margin-bottom: 0.375rem; + background: rgba(255, 255, 255, 0.02); border-radius: 6px; - border-left: 3px solid var(--border); + border-left: 2px solid var(--border); + transition: background 0.15s; +} + +.timeline-event:hover { + background: rgba(255, 255, 255, 0.04); } .timeline-event.event-success { @@ -2457,8 +2519,9 @@ body { } .event-icon { - font-size: 1.1rem; + font-size: 0.9rem; flex-shrink: 0; + opacity: 0.8; } .event-content { @@ -2475,19 +2538,21 @@ body { .event-title { font-weight: 500; + font-size: 0.8rem; color: var(--text); } .event-time { - font-size: 0.75rem; - color: var(--text-dim); + font-size: 0.65rem; + color: var(--text-muted); flex-shrink: 0; + font-family: 'SF Mono', Monaco, monospace; } .event-details { - font-size: 0.8rem; + font-size: 0.7rem; color: var(--text-dim); - margin-top: 0.25rem; + margin-top: 0.125rem; } .run-summary-session-info {