agent-teams-dashboard 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ :root{--bg-primary: #1a1a2e;--bg-secondary: #16213e;--bg-sidebar: #0f0f1a;--bg-sidebar-2: #111122;--bg-card: #16213e;--bg-hover: #1f2b47;--bg-active: #253350;--border-primary: #2a2a3e;--border-subtle: #222236;--text-primary: #e0e0e0;--text-secondary: #aaa;--text-muted: #777;--accent-blue: #58a6ff;--accent-green: #00ff88;--accent-yellow: #ffd700;--accent-red: #ff4444;--accent-purple: #bc8cff;--accent-cyan: #00d4ff;--font-mono: "JetBrains Mono", "Fira Code", Consolas, monospace;--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;--radius-sm: 4px;--radius-md: 6px;--radius-lg: 8px;--transition-fast: .15s ease}*,*:before,*:after{margin:0;padding:0;box-sizing:border-box}body{font-family:var(--font-mono);font-size:14px;line-height:1.5;color:var(--text-primary);background:var(--bg-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.app-container{display:flex;height:100vh;overflow:hidden}.resize-handle{width:4px;cursor:col-resize;background:transparent;flex-shrink:0;position:relative;z-index:10;transition:background var(--transition-fast)}.resize-handle:hover,.resize-handle:active{background:var(--accent-cyan)}.teams-panel{background:var(--bg-sidebar);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0}.teams-panel__header{padding:14px 12px;border-bottom:1px solid var(--border-primary)}.teams-panel__title{font-size:13px;font-weight:700;color:var(--text-primary);letter-spacing:.04em;text-transform:uppercase}.teams-panel__conn-dot{font-size:10px}.sidebar-mode-toggle{display:flex;background:var(--bg-secondary);border:1px solid var(--border-primary);border-radius:var(--radius-md);padding:2px;gap:2px}.sidebar-mode-toggle__btn{padding:4px 10px;border:none;background:transparent;color:var(--text-muted);font-family:var(--font-mono);font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.04em;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast);line-height:1}.sidebar-mode-toggle__btn:hover{color:var(--text-primary);background:var(--bg-hover)}.sidebar-mode-toggle__btn--active{background:var(--bg-active);color:var(--accent-cyan);box-shadow:0 0 0 1px #00d4ff4d}.teams-panel__nav{flex:1;overflow-y:auto;padding:6px}.teams-panel__nav-item{display:flex;align-items:center;gap:8px;width:100%;padding:8px 10px;border:none;background:transparent;color:var(--text-secondary);font-family:var(--font-mono);font-size:12px;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast);text-align:left}.teams-panel__nav-item:hover{background:var(--bg-hover);color:var(--text-primary)}.teams-panel__nav-item--active{background:var(--bg-active);color:var(--accent-cyan)}.teams-panel__nav-icon{font-size:14px}.teams-panel__divider{height:1px;background:var(--border-subtle);margin:6px 4px}.teams-panel__team{display:flex;flex-direction:column;gap:3px;width:100%;padding:8px 10px;margin-bottom:2px;border:none;background:transparent;color:var(--text-primary);font-family:var(--font-mono);font-size:14px;cursor:pointer;border-radius:var(--radius-sm);border-left:3px solid transparent;transition:all var(--transition-fast);text-align:left}.teams-panel__team:hover{background:var(--bg-hover)}.teams-panel__team--active{background:var(--bg-active);border-left-color:var(--accent-cyan)}.teams-panel__team-row{display:flex;align-items:center;gap:6px}.teams-panel__team-dot{font-size:10px;flex-shrink:0;line-height:1}.teams-panel__team-name{flex:1;min-width:0}.teams-panel__team-progress{display:flex;align-items:center;gap:6px;padding-left:16px}.teams-panel__team-bar{flex:1;height:3px;background:var(--border-primary);border-radius:2px;overflow:hidden}.teams-panel__team-bar-fill{height:100%;background:var(--accent-green);border-radius:2px;transition:width .3s ease}.teams-panel__team-pct{flex-shrink:0;white-space:nowrap}.teams-panel__footer{padding:10px 12px;border-top:1px solid var(--border-primary)}.teams-panel__footer-stats{display:flex;gap:10px}.agents-panel{background:var(--bg-sidebar-2);display:flex;flex-direction:column;overflow:hidden;flex-shrink:0}.agents-panel__header{padding:14px 12px;border-bottom:1px solid var(--border-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}.agents-panel__title{font-size:13px;font-weight:700;color:var(--text-primary);min-width:0}.agents-panel__task-summary{flex-shrink:0;white-space:nowrap}.agents-panel__empty{flex:1;display:flex;align-items:center;justify-content:center}.agents-panel__actions{padding:8px;border-bottom:1px solid var(--border-subtle)}.agents-panel__tasks-btn{display:flex;align-items:center;gap:6px;width:100%;padding:6px 10px;border:1px solid var(--border-primary);background:transparent;color:var(--text-secondary);font-family:var(--font-mono);font-size:12px;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast)}.agents-panel__tasks-btn:hover{background:var(--bg-hover);color:var(--text-primary);border-color:var(--text-muted)}.agents-panel__tasks-btn--active{background:var(--bg-active);color:var(--accent-cyan);border-color:var(--accent-cyan)}.agents-panel__list{flex:1;overflow-y:auto;padding:6px}.agents-panel__agent{margin-bottom:2px}.agents-panel__agent-btn{display:flex;align-items:center;gap:6px;width:100%;padding:7px 10px;border:none;background:transparent;color:var(--text-primary);font-family:var(--font-mono);font-size:14px;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast);text-align:left;border-left:3px solid transparent}.agents-panel__agent-btn:hover{background:var(--bg-hover)}.agents-panel__agent-btn--active{background:var(--bg-active);border-left-color:var(--accent-cyan);color:var(--accent-cyan)}.agents-panel__agent-dot{font-size:10px;flex-shrink:0;line-height:1}.agents-panel__agent-name{flex:1;min-width:0}.agents-panel__agent-type{flex-shrink:0}.agents-panel__agent-meta{display:flex;align-items:center;gap:8px;padding:2px 10px 4px 26px}.agents-panel__session-toggle{border:none;background:transparent;color:var(--text-muted);font-family:var(--font-mono);font-size:13px;cursor:pointer;padding:0;transition:color var(--transition-fast)}.agents-panel__session-toggle:hover{color:var(--text-secondary)}.agents-panel__sessions{padding:2px 10px 6px 26px}.agents-panel__session-id{font-size:13px;color:var(--accent-purple);font-family:var(--font-mono)}.agents-panel__session-time{font-size:13px;color:var(--text-muted)}.agents-panel__session-count{font-size:12px;color:var(--text-muted);background:var(--bg-hover);padding:0 4px;border-radius:3px}.main-panel{flex:1;overflow:hidden;display:flex;flex-direction:column}.main-panel>.overview-grid,.main-panel>.task-board__columns,.main-panel--padded{padding:16px;overflow-y:auto}.placeholder{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:14px}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-primary);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--text-muted)}*{scrollbar-width:thin;scrollbar-color:var(--border-primary) transparent}.text-primary{color:var(--text-primary)}.text-secondary{color:var(--text-secondary)}.text-muted{color:var(--text-muted)}.text-blue{color:var(--accent-blue)}.text-green{color:var(--accent-green)}.text-yellow{color:var(--accent-yellow)}.text-red{color:var(--accent-red)}.text-purple{color:var(--accent-purple)}.text-cyan{color:var(--accent-cyan)}.bg-card{background:var(--bg-card)}.bg-hover{background:var(--bg-hover)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-md{border-radius:var(--radius-md)}.rounded-lg{border-radius:var(--radius-lg)}.flex{display:flex}.flex-col{flex-direction:column}.flex-1{flex:1}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-1{gap:4px}.gap-2{gap:8px}.gap-3{gap:12px}.gap-4{gap:16px}.p-1{padding:4px}.p-2{padding:8px}.p-3{padding:12px}.p-4{padding:16px}.px-2{padding-left:8px;padding-right:8px}.px-3{padding-left:12px;padding-right:12px}.py-1{padding-top:4px;padding-bottom:4px}.py-2{padding-top:8px;padding-bottom:8px}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.font-bold{font-weight:600}.text-sm{font-size:14px}.text-xs{font-size:13px}.border{border:1px solid var(--border-primary)}.border-b{border-bottom:1px solid var(--border-primary)}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.transition{transition:all var(--transition-fast)}.panel-title{font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:16px}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px;text-align:center}.empty-state__icon{font-size:48px}.empty-state__title{font-size:16px;font-weight:600;color:var(--text-secondary)}.empty-state__text{max-width:400px;line-height:1.6}.overview-panel{padding:16px;overflow-y:auto;height:100%}.overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}.overview-card{background:var(--bg-card);border:1px solid var(--border-primary);border-left:3px solid var(--text-muted);border-radius:var(--radius-md);padding:16px;transition:all var(--transition-fast)}.overview-card:hover{border-color:var(--accent-cyan);border-left-color:inherit;background:var(--bg-hover)}.overview-card--active{border-left-color:var(--accent-green)}.overview-card--idle{border-left-color:var(--accent-yellow)}.overview-card--done{border-left-color:var(--accent-cyan)}.overview-card--inactive{border-left-color:var(--text-muted)}.overview-card__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.overview-card__meta{margin-bottom:12px}.overview-card__progress{display:flex;align-items:center;gap:8px;margin-bottom:8px}.progress-bar{flex:1;height:4px;background:var(--border-primary);border-radius:2px;overflow:hidden}.progress-bar__fill{height:100%;background:var(--accent-green);border-radius:2px;transition:width .3s ease}.overview-card__footer{text-align:right}.pulse-dot{display:inline-block;width:8px;height:8px;border-radius:50%;background:var(--accent-green);animation:pulse 2s ease-in-out infinite;flex-shrink:0}@keyframes pulse{0%,to{opacity:1;box-shadow:0 0 #0f86}50%{opacity:.7;box-shadow:0 0 0 6px #0f80}}.task-board{padding:16px;overflow-y:auto;height:100%}.task-board__columns{display:flex;gap:12px}.task-board__column{flex:1;min-width:0}.task-board__column-header{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;margin-bottom:8px;border-bottom:2px solid var(--border-primary);font-weight:600;font-size:12px;color:var(--text-secondary)}.task-card{background:var(--bg-card);border:1px solid var(--border-primary);border-radius:var(--radius-md);padding:12px;margin-bottom:8px;border-left:3px solid var(--text-muted)}.task-card--in-progress{border-left-color:var(--accent-yellow)}.task-card--completed{border-left-color:var(--accent-green)}.task-card--pending{border-left-color:var(--text-muted)}.task-card__subject{font-size:12px;margin-bottom:4px;color:var(--text-primary)}.task-card__desc{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;margin-bottom:8px;line-height:1.4}.task-card__meta{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.task-card__owner{display:inline-block;padding:2px 6px;background:#00d4ff26;color:var(--accent-cyan);border-radius:var(--radius-sm);font-size:11px}.task-card__blocked{color:var(--accent-yellow)}.chat-panel{display:flex;flex-direction:column;height:100%;position:relative}.chat-panel__header{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-bottom:1px solid var(--border-primary);background:var(--bg-secondary);flex-shrink:0}.chat-panel__header-left{display:flex;align-items:center;gap:8px;min-width:0}.chat-panel__header-right{display:flex;align-items:center;gap:12px;flex-shrink:0}.chat-panel__agent-name{font-size:14px;font-weight:700;color:var(--accent-cyan);font-family:var(--font-mono)}.chat-panel__session-tag{font-size:11px;padding:2px 6px;background:#bc8cff26;color:var(--accent-purple);border-radius:var(--radius-sm);font-family:var(--font-mono)}.chat-panel__team{white-space:nowrap}.chat-panel__feed{flex:1;overflow-y:auto;padding:8px 0}.chat-panel__empty{display:flex;align-items:center;justify-content:center;height:100%}.chat-panel__scroll-btn{position:absolute;bottom:12px;right:16px;padding:6px 12px;background:var(--bg-active);border:1px solid var(--accent-cyan);color:var(--accent-cyan);font-family:var(--font-mono);font-size:11px;border-radius:var(--radius-sm);cursor:pointer;transition:all var(--transition-fast);z-index:10}.chat-panel__scroll-btn:hover{background:var(--bg-hover)}.chat-msg{padding:6px 16px;border-left:3px solid transparent;transition:background var(--transition-fast)}.chat-msg:hover{background:#ffffff05}.chat-msg--user{border-left-color:var(--accent-green);background:#00ff8808}.chat-msg--assistant{border-left-color:var(--accent-cyan)}.chat-msg--tool-use{border-left-color:var(--accent-yellow);background:#ffd70008}.chat-msg--tool-result{border-left-color:var(--text-secondary);background:#aaaaaa08}.chat-msg__meta{display:flex;align-items:center;gap:8px;margin-bottom:4px}.chat-msg__role{font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;font-family:var(--font-mono)}.chat-msg--user .chat-msg__role{color:var(--accent-green)}.chat-msg--assistant .chat-msg__role{color:var(--accent-cyan)}.chat-msg--tool-use .chat-msg__role{color:var(--accent-yellow)}.chat-msg--tool-result .chat-msg__role{color:var(--text-secondary)}.chat-msg__time{font-size:12px;color:var(--text-secondary);font-family:var(--font-mono)}.chat-msg__body{font-size:14px;line-height:1.6;word-break:break-word}.msg-prose{color:var(--text-primary);white-space:pre-wrap}.chat-msg--assistant .msg-prose{color:var(--text-primary)}.msg-code{background:var(--bg-secondary);border:1px solid var(--border-subtle);border-radius:var(--radius-sm);padding:8px 10px;font-size:13px;white-space:pre-wrap;overflow-x:auto;color:var(--text-primary);margin-top:2px}.msg-expand-btn{border:none;background:transparent;color:var(--text-muted);font-family:var(--font-mono);font-size:11px;cursor:pointer;padding:2px 0;margin-top:4px;transition:color var(--transition-fast)}.msg-expand-btn:hover{color:var(--text-secondary)}.msg-tool-use{margin-top:2px}.msg-tool-header{display:flex;align-items:center;gap:6px;border:none;background:transparent;cursor:pointer;padding:3px 0;font-family:var(--font-mono)}.msg-tool-name{display:inline-block;padding:2px 8px;background:#ffd7001f;border:1px solid rgba(255,215,0,.3);color:var(--accent-yellow);border-radius:var(--radius-sm);font-size:13px;font-weight:600;font-family:var(--font-mono)}.msg-tool-toggle{font-size:11px;color:var(--text-muted)}.msg-tool-body{background:var(--bg-secondary);border:1px solid rgba(255,215,0,.15);border-radius:var(--radius-sm);padding:8px 10px;font-size:13px;white-space:pre-wrap;overflow-x:auto;color:var(--text-secondary);margin-top:4px;max-height:300px;overflow-y:auto}.msg-tool-result{margin-top:2px}.msg-error-badge{display:inline-block;padding:1px 5px;background:#ff444426;color:var(--accent-red);border-radius:var(--radius-sm);font-size:12px;font-weight:700;margin-bottom:4px;font-family:var(--font-mono)}.msg-result-body{font-size:13px;white-space:pre-wrap;overflow-x:auto;color:var(--text-secondary);line-height:1.5;max-height:400px;overflow-y:auto}.msg-tool-result--error .msg-result-body{color:#ff4444b3}.agents-panel__session{display:flex;align-items:center;gap:8px;padding:3px 8px;border-left:1px solid var(--border-subtle);margin-left:4px;width:100%;border-radius:0 var(--radius-sm) var(--radius-sm) 0;background:transparent;cursor:pointer;font-family:var(--font-mono);transition:background var(--transition-fast);border-top:none;border-right:none;border-bottom:none}.agents-panel__session:hover{background:var(--bg-hover)}.agents-panel__session--active{background:var(--bg-active);border-left-color:var(--accent-purple)}.agent-panel{display:flex;flex-direction:column;height:100%}.agent-panel__header{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid var(--border-primary)}.agent-panel__header .panel-title{margin-bottom:0}.status-badge{display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;font-weight:600}.status-badge--active{background:#00ff8826;color:var(--accent-green)}.status-badge--idle{background:#ffd70026;color:var(--accent-yellow)}.status-badge--done{background:#00d4ff26;color:var(--accent-cyan)}.status-badge--unknown{background:#5555554d;color:var(--text-muted)}.agent-panel__tasks{padding:8px 12px;margin-bottom:12px;background:var(--bg-card);border-radius:var(--radius-md);border:1px solid var(--border-primary)}.agent-panel__task-item{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px}.task-status-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}.task-status-dot--pending{background:var(--text-muted)}.task-status-dot--in_progress{background:var(--accent-yellow)}.task-status-dot--completed{background:var(--accent-green)}.agent-panel__feed{flex:1;overflow-y:auto;padding:4px}.activity-entry{padding:8px 12px;margin-bottom:4px;border-radius:var(--radius-sm);font-size:12px}.activity-entry--user{border-left:3px solid var(--accent-green);background:#00ff880d}.activity-entry--assistant{color:var(--accent-cyan)}.activity-entry--tool-use{background:#ffd7000d}.activity-entry--tool-result{color:var(--text-muted)}.activity-entry__header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}.activity-entry__role{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em}.activity-entry__body{word-break:break-word}.activity-entry__code{background:var(--bg-secondary);padding:8px;border-radius:var(--radius-sm);overflow-x:auto;font-size:11px;white-space:pre-wrap;margin-top:4px}.activity-entry__tool-use{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}.activity-entry__tool-badge{display:inline-block;padding:2px 6px;background:#ffd70026;color:var(--accent-yellow);border-radius:var(--radius-sm);font-size:11px;white-space:nowrap}.activity-entry__tool-input{font-size:11px;word-break:break-all}.activity-entry__tool-result{font-size:11px;line-height:1.4}
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Agent Teams Dashboard</title>
7
- <script type="module" crossorigin src="/assets/index-Ddn7oByj.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-DyZplrwc.css">
7
+ <script type="module" crossorigin src="/assets/index-B8t0Tabx.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-CsK61Xi-.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-teams-dashboard",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Real-time monitoring dashboard for Claude Code agent teams",
5
5
  "type": "module",
6
6
  "author": "pingshian0131",
@@ -13,6 +13,7 @@ const tasks = new Map();
13
13
  const agentEntries = new Map();
14
14
  const agentOffsets = new Map();
15
15
  const teamFileMtimes = new Map(); // team name -> latest mtime (ms)
16
+ const knownProjectDirs = new Set(); // all project dir names under ~/.claude/projects/
16
17
  export const onChange = new EventEmitter();
17
18
  // --- Helpers ---
18
19
  async function safeReaddir(dir) {
@@ -138,24 +139,35 @@ async function refreshAllTasks() {
138
139
  }
139
140
  }
140
141
  // --- Agent JSONL scanning ---
142
+ // Scan both subagent JSONL (agent-*.jsonl) and team session JSONL (UUID.jsonl with teamName)
141
143
  export async function scanAgentJsonl() {
142
144
  const projectDirs = await safeReaddir(PROJECTS_DIR);
145
+ // Track all known project dirs for name resolution
146
+ for (const d of projectDirs) {
147
+ knownProjectDirs.add(d);
148
+ }
143
149
  for (const projDir of projectDirs) {
144
150
  const projPath = join(PROJECTS_DIR, projDir);
145
- const sessionDirs = await safeReaddir(projPath);
146
- for (const sessionDir of sessionDirs) {
147
- const subagentsDir = join(projPath, sessionDir, 'subagents');
151
+ const entries = await safeReaddir(projPath);
152
+ for (const entry of entries) {
153
+ const entryPath = join(projPath, entry);
154
+ // Team session JSONL: UUID.jsonl files at project root level
155
+ if (entry.endsWith('.jsonl')) {
156
+ await readNewEntries(entryPath, true, projDir);
157
+ continue;
158
+ }
159
+ // Subagent JSONL: agent-*.jsonl under session/subagents/
160
+ const subagentsDir = join(entryPath, 'subagents');
148
161
  const files = await safeReaddir(subagentsDir);
149
162
  for (const file of files) {
150
163
  if (!file.startsWith('agent-') || !file.endsWith('.jsonl'))
151
164
  continue;
152
- const filePath = join(subagentsDir, file);
153
- await readNewEntries(filePath);
165
+ await readNewEntries(join(subagentsDir, file), false, projDir);
154
166
  }
155
167
  }
156
168
  }
157
169
  }
158
- async function readNewEntries(filePath) {
170
+ async function readNewEntries(filePath, isSessionFile, projectDir) {
159
171
  const fileStat = await safeFileStat(filePath);
160
172
  if (!fileStat)
161
173
  return;
@@ -166,13 +178,49 @@ async function readNewEntries(filePath) {
166
178
  const raw = await safeReadFile(filePath);
167
179
  if (!raw)
168
180
  return;
169
- // Read only from the offset position
170
181
  const newContent = raw.slice(currentOffset);
171
182
  agentOffsets.set(filePath, fileSize);
172
183
  const lines = newContent.split('\n').filter(Boolean);
173
184
  for (const line of lines) {
174
185
  try {
175
186
  const parsed = JSON.parse(line);
187
+ // For session files, only process entries that belong to a team
188
+ if (isSessionFile) {
189
+ const teamName = parsed.teamName;
190
+ if (!teamName)
191
+ continue;
192
+ const agentName = parsed.agentName || 'team-lead';
193
+ // Use team agentId format: name@team
194
+ const fullAgentId = `${agentName}@${teamName}`;
195
+ const entry = {
196
+ agentId: fullAgentId,
197
+ slug: agentName,
198
+ sessionId: parsed.sessionId ?? '',
199
+ type: parsed.type ?? 'assistant',
200
+ message: {
201
+ role: parsed.message?.role ?? '',
202
+ content: Array.isArray(parsed.message?.content)
203
+ ? parsed.message.content
204
+ : typeof parsed.message?.content === 'string'
205
+ ? [{ type: 'text', text: parsed.message.content }]
206
+ : [],
207
+ model: parsed.message?.model,
208
+ },
209
+ timestamp: parsed.timestamp ?? '',
210
+ projectDir,
211
+ };
212
+ let arr = agentEntries.get(fullAgentId);
213
+ if (!arr) {
214
+ arr = [];
215
+ agentEntries.set(fullAgentId, arr);
216
+ }
217
+ arr.push(entry);
218
+ if (arr.length > MAX_ENTRIES_PER_AGENT) {
219
+ arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
220
+ }
221
+ continue;
222
+ }
223
+ // Subagent JSONL: use agentId from the file
176
224
  const entry = {
177
225
  agentId: parsed.agentId ?? '',
178
226
  slug: parsed.slug ?? '',
@@ -188,19 +236,18 @@ async function readNewEntries(filePath) {
188
236
  model: parsed.message?.model,
189
237
  },
190
238
  timestamp: parsed.timestamp ?? '',
239
+ projectDir,
191
240
  };
192
241
  if (!entry.agentId)
193
242
  continue;
194
- let entries = agentEntries.get(entry.agentId);
195
- if (!entries) {
196
- entries = [];
197
- agentEntries.set(entry.agentId, entries);
243
+ let arr = agentEntries.get(entry.agentId);
244
+ if (!arr) {
245
+ arr = [];
246
+ agentEntries.set(entry.agentId, arr);
198
247
  }
199
- entries.push(entry);
200
- // Trim to max entries
201
- if (entries.length > MAX_ENTRIES_PER_AGENT) {
202
- const excess = entries.length - MAX_ENTRIES_PER_AGENT;
203
- entries.splice(0, excess);
248
+ arr.push(entry);
249
+ if (arr.length > MAX_ENTRIES_PER_AGENT) {
250
+ arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
204
251
  }
205
252
  }
206
253
  catch {
@@ -254,15 +301,136 @@ function buildTeamOverview(teamName) {
254
301
  }
255
302
  return { config, tasks: teamTasks, taskStats, agentSlugs, lastActivity };
256
303
  }
304
+ /**
305
+ * Encode a filesystem path to the Claude project dir format.
306
+ * /Users/ping → -Users-ping
307
+ */
308
+ function encodePathPrefix(fsPath) {
309
+ return '-' + fsPath.replace(/\//g, '-').replace(/^-/, '');
310
+ }
311
+ // The home directory prefix in encoded form, used to strip from project dir names.
312
+ // Uses HOST_HOME env var (set in Docker) or falls back to os.homedir().
313
+ const HOME_PREFIX = encodePathPrefix(process.env.HOST_HOME || homedir());
314
+ /**
315
+ * Use the set of all known project dirs to resolve ambiguous dashes.
316
+ * If "-Users-ping-projects" exists as a project dir, then in
317
+ * "-Users-ping-projects-agent-teams-dashboard", the "projects" portion
318
+ * is a directory (path separator), not part of a directory name.
319
+ *
320
+ * Returns the last path segment (the actual project directory name).
321
+ */
322
+ function resolveProjectName(projectDir, allProjectDirs) {
323
+ if (projectDir === HOME_PREFIX)
324
+ return '~';
325
+ const prefixWithDash = HOME_PREFIX + '-';
326
+ if (!projectDir.startsWith(prefixWithDash))
327
+ return projectDir;
328
+ const remainder = projectDir.slice(prefixWithDash.length);
329
+ // Try to find the longest known parent directory prefix.
330
+ // A known dir is a valid parent only if:
331
+ // 1. It's a strict prefix of projectDir (not equal to it)
332
+ // 2. No OTHER known dir starts with it + the next dash-segment
333
+ // (which would mean the continuation is part of the dir name, not a child)
334
+ let bestSplit = 0;
335
+ const parts = remainder.split('-');
336
+ let accumulated = HOME_PREFIX;
337
+ for (let i = 0; i < parts.length - 1; i++) {
338
+ accumulated += '-' + parts[i];
339
+ if (!allProjectDirs.has(accumulated) || accumulated === projectDir)
340
+ continue;
341
+ // Check: is there another known dir that starts with accumulated + '-' + nextPart?
342
+ // If so, accumulated might not be a true parent — the next segment could be part
343
+ // of a longer directory name at the same level.
344
+ const nextAccumulated = accumulated + '-' + parts[i + 1];
345
+ // When nextAccumulated === projectDir, accumulated could be a real parent
346
+ // (e.g. panamera-python3 → worktree3) or a false parent (e.g. erp-shipment → 2).
347
+ // Heuristic: accumulated is a real parent if other known dirs also have it as prefix.
348
+ if (nextAccumulated === projectDir) {
349
+ const accPrefix = accumulated + '-';
350
+ const hasOtherChildren = Array.from(allProjectDirs).some(d => d !== projectDir && d.startsWith(accPrefix));
351
+ if (hasOtherChildren) {
352
+ bestSplit = i + 1;
353
+ }
354
+ continue;
355
+ }
356
+ const isFalseParent = allProjectDirs.has(nextAccumulated) &&
357
+ projectDir.startsWith(nextAccumulated);
358
+ if (!isFalseParent) {
359
+ bestSplit = i + 1;
360
+ }
361
+ }
362
+ if (bestSplit > 0) {
363
+ return parts.slice(bestSplit).join('-');
364
+ }
365
+ return remainder;
366
+ }
367
+ function buildProjectOverviews() {
368
+ // Group all agent entries by projectDir
369
+ const projectMap = new Map();
370
+ for (const [agentId, entries] of agentEntries) {
371
+ for (const entry of entries) {
372
+ if (!entry.projectDir)
373
+ continue;
374
+ let agentMap = projectMap.get(entry.projectDir);
375
+ if (!agentMap) {
376
+ agentMap = new Map();
377
+ projectMap.set(entry.projectDir, agentMap);
378
+ }
379
+ let agentData = agentMap.get(agentId);
380
+ if (!agentData) {
381
+ agentData = { slug: entry.slug, entries: [] };
382
+ agentMap.set(agentId, agentData);
383
+ }
384
+ agentData.entries.push(entry);
385
+ if (entry.slug)
386
+ agentData.slug = entry.slug;
387
+ }
388
+ }
389
+ const projects = [];
390
+ for (const [projectDir, agentMap] of projectMap) {
391
+ let lastActivity = '';
392
+ const agents = [];
393
+ for (const [agentId, data] of agentMap) {
394
+ const lastTs = data.entries.length > 0
395
+ ? data.entries[data.entries.length - 1].timestamp
396
+ : '';
397
+ agents.push({
398
+ agentId,
399
+ slug: data.slug || agentId,
400
+ entryCount: data.entries.length,
401
+ lastTimestamp: lastTs,
402
+ });
403
+ if (lastTs > lastActivity)
404
+ lastActivity = lastTs;
405
+ }
406
+ agents.sort((a, b) => b.lastTimestamp.localeCompare(a.lastTimestamp));
407
+ projects.push({
408
+ projectDir,
409
+ projectName: resolveProjectName(projectDir, knownProjectDirs),
410
+ agents,
411
+ lastActivity,
412
+ });
413
+ }
414
+ projects.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
415
+ return projects;
416
+ }
257
417
  export function getSnapshot() {
258
418
  const teamOverviews = [];
259
419
  const matchedAgentIds = new Set();
420
+ // Map full agentId (name@team) -> resolved entries
421
+ const activity = {};
260
422
  for (const teamName of teams.keys()) {
261
423
  const overview = buildTeamOverview(teamName);
262
424
  teamOverviews.push(overview);
263
425
  for (const member of overview.config.members) {
264
426
  matchedAgentIds.add(member.agentId);
265
- matchedAgentIds.add(member.agentId.split('@')[0]);
427
+ const shortId = member.agentId.split('@')[0];
428
+ matchedAgentIds.add(shortId);
429
+ // Resolve: try full agentId first, then short hash
430
+ const entries = agentEntries.get(member.agentId) ?? agentEntries.get(shortId);
431
+ if (entries && entries.length > 0) {
432
+ activity[member.agentId] = entries;
433
+ }
266
434
  }
267
435
  }
268
436
  // Find unmatched agents
@@ -275,9 +443,10 @@ export function getSnapshot() {
275
443
  slug: last.slug,
276
444
  sessionId: last.sessionId,
277
445
  });
446
+ activity[agentId] = entries;
278
447
  }
279
448
  }
280
- return { teams: teamOverviews, unmatchedAgents };
449
+ return { teams: teamOverviews, unmatchedAgents, agentActivity: activity, projects: buildProjectOverviews() };
281
450
  }
282
451
  // --- Query ---
283
452
  export function getAgentActivity(agentId) {