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-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
@@ -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
|
|
146
|
-
for (const
|
|
147
|
-
const
|
|
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
|
-
|
|
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
|
|
195
|
-
if (!
|
|
196
|
-
|
|
197
|
-
agentEntries.set(entry.agentId,
|
|
243
|
+
let arr = agentEntries.get(entry.agentId);
|
|
244
|
+
if (!arr) {
|
|
245
|
+
arr = [];
|
|
246
|
+
agentEntries.set(entry.agentId, arr);
|
|
198
247
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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) {
|