agent-teams-dashboard 0.1.0 → 0.3.0

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.
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-DEOAkXJf.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BwUrrF1R.css">
7
+ <script type="module" crossorigin src="/assets/index-C2fnhLSE.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-BqtV8ETR.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.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Real-time monitoring dashboard for Claude Code agent teams",
5
5
  "type": "module",
6
6
  "author": "pingshian0131",
@@ -48,6 +48,23 @@ export async function handleTeamsApi(req, res) {
48
48
  json(res, team);
49
49
  return true;
50
50
  }
51
+ // GET /api/agents/:agentId/sessions/:sessionId
52
+ const sessionDetailMatch = path.match(/^\/api\/agents\/([^/]+)\/sessions\/([^/]+)$/);
53
+ if (sessionDetailMatch) {
54
+ const agentId = decodeURIComponent(sessionDetailMatch[1]);
55
+ const sessionId = decodeURIComponent(sessionDetailMatch[2]);
56
+ const entries = cache.getSessionEntries(agentId, sessionId);
57
+ json(res, entries);
58
+ return true;
59
+ }
60
+ // GET /api/agents/:agentId/sessions
61
+ const sessionsMatch = path.match(/^\/api\/agents\/([^/]+)\/sessions$/);
62
+ if (sessionsMatch) {
63
+ const agentId = decodeURIComponent(sessionsMatch[1]);
64
+ const sessions = cache.getAgentSessions(agentId);
65
+ json(res, sessions);
66
+ return true;
67
+ }
51
68
  // GET /api/agents/:agentId/activity
52
69
  const agentMatch = path.match(/^\/api\/agents\/([^/]+)\/activity$/);
53
70
  if (agentMatch) {
@@ -138,24 +138,31 @@ async function refreshAllTasks() {
138
138
  }
139
139
  }
140
140
  // --- Agent JSONL scanning ---
141
+ // Scan both subagent JSONL (agent-*.jsonl) and team session JSONL (UUID.jsonl with teamName)
141
142
  export async function scanAgentJsonl() {
142
143
  const projectDirs = await safeReaddir(PROJECTS_DIR);
143
144
  for (const projDir of projectDirs) {
144
145
  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');
146
+ const entries = await safeReaddir(projPath);
147
+ for (const entry of entries) {
148
+ const entryPath = join(projPath, entry);
149
+ // Team session JSONL: UUID.jsonl files at project root level
150
+ if (entry.endsWith('.jsonl')) {
151
+ await readNewEntries(entryPath, true);
152
+ continue;
153
+ }
154
+ // Subagent JSONL: agent-*.jsonl under session/subagents/
155
+ const subagentsDir = join(entryPath, 'subagents');
148
156
  const files = await safeReaddir(subagentsDir);
149
157
  for (const file of files) {
150
158
  if (!file.startsWith('agent-') || !file.endsWith('.jsonl'))
151
159
  continue;
152
- const filePath = join(subagentsDir, file);
153
- await readNewEntries(filePath);
160
+ await readNewEntries(join(subagentsDir, file), false);
154
161
  }
155
162
  }
156
163
  }
157
164
  }
158
- async function readNewEntries(filePath) {
165
+ async function readNewEntries(filePath, isSessionFile) {
159
166
  const fileStat = await safeFileStat(filePath);
160
167
  if (!fileStat)
161
168
  return;
@@ -166,13 +173,48 @@ async function readNewEntries(filePath) {
166
173
  const raw = await safeReadFile(filePath);
167
174
  if (!raw)
168
175
  return;
169
- // Read only from the offset position
170
176
  const newContent = raw.slice(currentOffset);
171
177
  agentOffsets.set(filePath, fileSize);
172
178
  const lines = newContent.split('\n').filter(Boolean);
173
179
  for (const line of lines) {
174
180
  try {
175
181
  const parsed = JSON.parse(line);
182
+ // For session files, only process entries that belong to a team agent
183
+ if (isSessionFile) {
184
+ const teamName = parsed.teamName;
185
+ const agentName = parsed.agentName;
186
+ if (!teamName || !agentName)
187
+ continue;
188
+ // Use team agentId format: name@team
189
+ const fullAgentId = `${agentName}@${teamName}`;
190
+ const entry = {
191
+ agentId: fullAgentId,
192
+ slug: agentName,
193
+ sessionId: parsed.sessionId ?? '',
194
+ type: parsed.type ?? 'assistant',
195
+ message: {
196
+ role: parsed.message?.role ?? '',
197
+ content: Array.isArray(parsed.message?.content)
198
+ ? parsed.message.content
199
+ : typeof parsed.message?.content === 'string'
200
+ ? [{ type: 'text', text: parsed.message.content }]
201
+ : [],
202
+ model: parsed.message?.model,
203
+ },
204
+ timestamp: parsed.timestamp ?? '',
205
+ };
206
+ let arr = agentEntries.get(fullAgentId);
207
+ if (!arr) {
208
+ arr = [];
209
+ agentEntries.set(fullAgentId, arr);
210
+ }
211
+ arr.push(entry);
212
+ if (arr.length > MAX_ENTRIES_PER_AGENT) {
213
+ arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
214
+ }
215
+ continue;
216
+ }
217
+ // Subagent JSONL: use agentId from the file
176
218
  const entry = {
177
219
  agentId: parsed.agentId ?? '',
178
220
  slug: parsed.slug ?? '',
@@ -191,16 +233,14 @@ async function readNewEntries(filePath) {
191
233
  };
192
234
  if (!entry.agentId)
193
235
  continue;
194
- let entries = agentEntries.get(entry.agentId);
195
- if (!entries) {
196
- entries = [];
197
- agentEntries.set(entry.agentId, entries);
236
+ let arr = agentEntries.get(entry.agentId);
237
+ if (!arr) {
238
+ arr = [];
239
+ agentEntries.set(entry.agentId, arr);
198
240
  }
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);
241
+ arr.push(entry);
242
+ if (arr.length > MAX_ENTRIES_PER_AGENT) {
243
+ arr.splice(0, arr.length - MAX_ENTRIES_PER_AGENT);
204
244
  }
205
245
  }
206
246
  catch {
@@ -257,12 +297,20 @@ function buildTeamOverview(teamName) {
257
297
  export function getSnapshot() {
258
298
  const teamOverviews = [];
259
299
  const matchedAgentIds = new Set();
300
+ // Map full agentId (name@team) -> resolved entries
301
+ const activity = {};
260
302
  for (const teamName of teams.keys()) {
261
303
  const overview = buildTeamOverview(teamName);
262
304
  teamOverviews.push(overview);
263
305
  for (const member of overview.config.members) {
264
306
  matchedAgentIds.add(member.agentId);
265
- matchedAgentIds.add(member.agentId.split('@')[0]);
307
+ const shortId = member.agentId.split('@')[0];
308
+ matchedAgentIds.add(shortId);
309
+ // Resolve: try full agentId first, then short hash
310
+ const entries = agentEntries.get(member.agentId) ?? agentEntries.get(shortId);
311
+ if (entries && entries.length > 0) {
312
+ activity[member.agentId] = entries;
313
+ }
266
314
  }
267
315
  }
268
316
  // Find unmatched agents
@@ -275,14 +323,52 @@ export function getSnapshot() {
275
323
  slug: last.slug,
276
324
  sessionId: last.sessionId,
277
325
  });
326
+ activity[agentId] = entries;
278
327
  }
279
328
  }
280
- return { teams: teamOverviews, unmatchedAgents };
329
+ return { teams: teamOverviews, unmatchedAgents, agentActivity: activity };
281
330
  }
282
331
  // --- Query ---
283
332
  export function getAgentActivity(agentId) {
284
333
  return agentEntries.get(agentId) ?? [];
285
334
  }
335
+ export function getAgentSessions(agentId) {
336
+ const entries = agentEntries.get(agentId);
337
+ if (!entries || entries.length === 0)
338
+ return [];
339
+ const sessionMap = new Map();
340
+ for (const entry of entries) {
341
+ const sid = entry.sessionId || 'unknown';
342
+ let group = sessionMap.get(sid);
343
+ if (!group) {
344
+ group = { entries: [] };
345
+ sessionMap.set(sid, group);
346
+ }
347
+ group.entries.push(entry);
348
+ }
349
+ const sessions = [];
350
+ for (const [sessionId, group] of sessionMap) {
351
+ const first = group.entries[0];
352
+ const last = group.entries[group.entries.length - 1];
353
+ sessions.push({
354
+ sessionId,
355
+ agentId,
356
+ slug: last.slug,
357
+ entryCount: group.entries.length,
358
+ firstTimestamp: first.timestamp,
359
+ lastTimestamp: last.timestamp,
360
+ });
361
+ }
362
+ // Sort by lastTimestamp descending (most recent first)
363
+ sessions.sort((a, b) => (b.lastTimestamp > a.lastTimestamp ? 1 : -1));
364
+ return sessions;
365
+ }
366
+ export function getSessionEntries(agentId, sessionId) {
367
+ const entries = agentEntries.get(agentId);
368
+ if (!entries)
369
+ return [];
370
+ return entries.filter(e => (e.sessionId || 'unknown') === sessionId);
371
+ }
286
372
  // --- Full refresh ---
287
373
  export async function refreshAll() {
288
374
  await refreshTeams();
@@ -8,10 +8,12 @@ const TASKS_DIR = join(CLAUDE_DIR, 'tasks');
8
8
  const DEBOUNCE_MS = 200;
9
9
  const JSONL_POLL_MS = 2000;
10
10
  const DIR_CHECK_MS = 5000;
11
+ const FULL_REFRESH_MS = 5000; // Fallback polling for environments where fs.watch doesn't work (e.g. Docker bind mounts)
11
12
  let tasksWatcher = null;
12
13
  let teamsWatcher = null;
13
14
  let jsonlTimer = null;
14
15
  let dirCheckTimer = null;
16
+ let fullRefreshTimer = null;
15
17
  // --- Debounce helper ---
16
18
  function debounce(fn, ms) {
17
19
  let timer = null;
@@ -95,11 +97,31 @@ function startDirCheckPoller() {
95
97
  }
96
98
  }, DIR_CHECK_MS);
97
99
  }
100
+ // --- Full refresh poller (fallback for Docker / environments without fs.watch) ---
101
+ function startFullRefreshPoller() {
102
+ let lastSnapshot = '';
103
+ fullRefreshTimer = setInterval(async () => {
104
+ await cache.refreshTeams();
105
+ for (const team of cache.getSnapshot().teams) {
106
+ await cache.refreshTasks(team.config.name);
107
+ }
108
+ const snapshot = JSON.stringify(cache.getSnapshot());
109
+ if (snapshot !== lastSnapshot) {
110
+ lastSnapshot = snapshot;
111
+ cache.onChange.emit('change');
112
+ }
113
+ }, FULL_REFRESH_MS);
114
+ }
98
115
  // --- Agent JSONL poller ---
99
116
  function startJsonlPoller() {
117
+ let lastSnapshot = '';
100
118
  jsonlTimer = setInterval(async () => {
101
119
  await cache.scanAgentJsonl();
102
- cache.onChange.emit('change');
120
+ const snapshot = JSON.stringify(cache.getSnapshot());
121
+ if (snapshot !== lastSnapshot) {
122
+ lastSnapshot = snapshot;
123
+ cache.onChange.emit('change');
124
+ }
103
125
  }, JSONL_POLL_MS);
104
126
  }
105
127
  // --- Public API ---
@@ -107,6 +129,7 @@ export function startWatching() {
107
129
  startTasksWatcher();
108
130
  startTeamsWatcher();
109
131
  startJsonlPoller();
132
+ startFullRefreshPoller();
110
133
  // If either dir doesn't exist yet, poll for them
111
134
  if (!tasksWatcher || !teamsWatcher) {
112
135
  startDirCheckPoller();
@@ -125,4 +148,8 @@ export function stopWatching() {
125
148
  clearInterval(dirCheckTimer);
126
149
  dirCheckTimer = null;
127
150
  }
151
+ if (fullRefreshTimer) {
152
+ clearInterval(fullRefreshTimer);
153
+ fullRefreshTimer = null;
154
+ }
128
155
  }
@@ -1 +0,0 @@
1
- :root{--bg-primary: #1a1a2e;--bg-secondary: #16213e;--bg-sidebar: #0f0f1a;--bg-card: #16213e;--bg-hover: #1f2b47;--bg-active: #253350;--border-primary: #2a2a3e;--border-subtle: #222236;--text-primary: #e0e0e0;--text-secondary: #888;--text-muted: #555;--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:13px;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}.sidebar{width:250px;min-width:250px;background:var(--bg-sidebar);border-right:1px solid var(--border-primary);overflow-y:auto;display:flex;flex-direction:column}.sidebar-header{padding:16px;border-bottom:1px solid var(--border-primary)}.sidebar-title{font-size:14px;font-weight:600;color:var(--text-primary);letter-spacing:.02em}.sidebar-nav{flex:1;padding:8px}.main-panel{flex:1;overflow-y:auto;padding:16px}.placeholder{display:flex;align-items:center;justify-content:center;height:100%;color:var(--text-muted);font-size:14px}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--border-primary);border-radius:4px}::-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:12px}.text-xs{font-size:11px}.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)}.sidebar-nav-item{display:flex;align-items:center;width:100%;padding:8px 12px;border:none;background:transparent;color:var(--text-secondary);font-family:var(--font-mono);font-size:13px;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast);text-align:left}.sidebar-nav-item:hover{background:var(--bg-hover);color:var(--text-primary)}.sidebar-nav-item--active{background:var(--bg-active);color:var(--accent-cyan)}.sidebar-team{margin-bottom:2px}.sidebar-team-header{display:flex;align-items:center;gap:6px;width:100%;padding:8px 12px;border:none;background:transparent;color:var(--text-primary);font-family:var(--font-mono);font-size:13px;cursor:pointer;border-radius:var(--radius-sm);transition:all var(--transition-fast);text-align:left}.sidebar-team-header:hover{background:var(--bg-hover)}.sidebar-team-arrow{font-size:10px;color:var(--text-muted);width:12px}.sidebar-footer{padding:12px 16px;border-top:1px solid var(--border-primary)}.sidebar-stats{display:flex;gap:12px}.agent-item{display:flex;align-items:center;gap:6px;width:100%;padding:6px 12px 6px 28px;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}.agent-item:hover{background:var(--bg-hover);color:var(--text-primary)}.agent-item--selected{background:var(--bg-active);color:var(--accent-cyan)}.agent-item__dot{font-size:8px;flex-shrink:0}.agent-item__name{flex:1;min-width:0}.agent-item__type{flex-shrink:0}.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-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__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)}.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}