@brainpilot/web 0.0.3 → 0.0.5

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.
Files changed (97) hide show
  1. package/dist/assets/index-C-8G4D4j.js +448 -0
  2. package/dist/assets/index-C501m5OS.css +1 -0
  3. package/dist/index.html +2 -2
  4. package/index.html +13 -0
  5. package/package.json +9 -3
  6. package/src/App.tsx +10 -0
  7. package/src/__tests__/api.test.ts +103 -0
  8. package/src/__tests__/messageGroups.test.ts +80 -0
  9. package/src/__tests__/newUiComponents.test.tsx +101 -0
  10. package/src/__tests__/newUiEvents.test.ts +236 -0
  11. package/src/components/chat/AskUserCard.tsx +123 -0
  12. package/src/components/chat/AutoRetryIndicator.tsx +71 -0
  13. package/src/components/chat/ComposerInput.tsx +73 -0
  14. package/src/components/chat/ComposerSendButton.tsx +26 -0
  15. package/src/components/chat/MarkdownMessage.tsx +24 -0
  16. package/src/components/chat/MessageStream.tsx +464 -0
  17. package/src/components/chat/PromptComposer.tsx +398 -0
  18. package/src/components/chat/SystemMessageBubble.tsx +46 -0
  19. package/src/components/demo/DemoFileTree.tsx +146 -0
  20. package/src/components/demo/DemoView.tsx +668 -0
  21. package/src/components/demo/TraceNodeModal.tsx +76 -0
  22. package/src/components/demo/demoBundle.ts +218 -0
  23. package/src/components/demo/demoCache.ts +42 -0
  24. package/src/components/files/FilePreviewView.tsx +153 -0
  25. package/src/components/files/FileSidebar.tsx +664 -0
  26. package/src/components/files/filePreview.ts +113 -0
  27. package/src/components/primitives/CustomSelect.tsx +200 -0
  28. package/src/components/primitives/IconButton.tsx +27 -0
  29. package/src/components/quota/DiskQuotaCriticalDialog.tsx +56 -0
  30. package/src/components/quota/DiskQuotaWarningDialog.tsx +65 -0
  31. package/src/components/quota/QuotaFileManager.tsx +197 -0
  32. package/src/components/search/SearchDialog.tsx +101 -0
  33. package/src/components/session/AgentNetwork.tsx +1240 -0
  34. package/src/components/session/AgentTraceViews.tsx +381 -0
  35. package/src/components/session/AnalyticsTab.tsx +386 -0
  36. package/src/components/session/GlobalOverview.tsx +108 -0
  37. package/src/components/session/NodeTooltip.tsx +127 -0
  38. package/src/components/session/TimelineTab.tsx +320 -0
  39. package/src/components/session/TraceGraphView.tsx +301 -0
  40. package/src/components/session/TraceNodeDetail.tsx +142 -0
  41. package/src/components/session/agentAnalytics.ts +397 -0
  42. package/src/components/session/agentNetworkShared.ts +329 -0
  43. package/src/components/session/traceLayout.ts +150 -0
  44. package/src/components/settings/SettingsDialog.tsx +719 -0
  45. package/src/components/shell/DesktopShell.tsx +236 -0
  46. package/src/components/shell/SandboxBuildingOverlay.tsx +73 -0
  47. package/src/components/shell/SandboxStatus.tsx +287 -0
  48. package/src/components/shell/TerminalDrawer.tsx +387 -0
  49. package/src/components/sidebar/Sidebar.tsx +187 -0
  50. package/src/config.ts +10 -0
  51. package/src/contexts/AppProviders.tsx +20 -0
  52. package/src/contexts/AuthContext.tsx +61 -0
  53. package/src/contexts/PreferencesContext.tsx +125 -0
  54. package/src/contexts/SSEContext.tsx +175 -0
  55. package/src/contexts/SandboxContext.tsx +310 -0
  56. package/src/contexts/SessionContext.tsx +608 -0
  57. package/src/contexts/draftStore.ts +103 -0
  58. package/src/contexts/messageFilters.ts +29 -0
  59. package/src/contexts/messageGroups.ts +77 -0
  60. package/src/contexts/messageReducer.ts +401 -0
  61. package/src/contexts/newUiEvents.ts +190 -0
  62. package/src/contracts/backend.ts +846 -0
  63. package/src/contracts/demoBundle.ts +83 -0
  64. package/src/i18n/messages/analytics.ts +96 -0
  65. package/src/i18n/messages/chat.ts +108 -0
  66. package/src/i18n/messages/contexts.ts +40 -0
  67. package/src/i18n/messages/demo.ts +80 -0
  68. package/src/i18n/messages/files.ts +82 -0
  69. package/src/i18n/messages/network.ts +186 -0
  70. package/src/i18n/messages/profile.ts +40 -0
  71. package/src/i18n/messages/quota.ts +36 -0
  72. package/src/i18n/messages/sandbox.ts +116 -0
  73. package/src/i18n/messages/search.ts +16 -0
  74. package/src/i18n/messages/settings.ts +184 -0
  75. package/src/i18n/messages/shell.ts +38 -0
  76. package/src/i18n/messages/sidebar.ts +52 -0
  77. package/src/i18n/messages/terminal.ts +22 -0
  78. package/src/i18n/messages/trace.ts +84 -0
  79. package/src/i18n/messages.ts +32 -0
  80. package/src/i18n/translate.ts +46 -0
  81. package/src/i18n/types.ts +15 -0
  82. package/src/i18n/useT.ts +15 -0
  83. package/src/main.tsx +13 -0
  84. package/src/mocks/backend.ts +722 -0
  85. package/src/styles/global.css +7429 -0
  86. package/src/styles/tokens.css +161 -0
  87. package/src/utils/api.ts +627 -0
  88. package/src/utils/download.ts +18 -0
  89. package/src/utils/format.ts +7 -0
  90. package/src/utils/zip.ts +119 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tsconfig.app.json +22 -0
  93. package/tsconfig.json +7 -0
  94. package/tsconfig.node.json +13 -0
  95. package/vite.config.ts +13 -0
  96. package/dist/assets/index-Cd0Mi_WU.css +0 -1
  97. package/dist/assets/index-FGg-DeYR.js +0 -448
@@ -0,0 +1,142 @@
1
+ import { AlertTriangle, ArrowRight, Box, Clock3, FileText, GitBranch, Timer, Wrench } from "lucide-react";
2
+ import { TraceNode } from "../../contracts/backend";
3
+ import { TranslateVars } from "../../i18n/translate";
4
+ import {
5
+ artifactLabels,
6
+ formatDuration,
7
+ formatTime,
8
+ getNodeKind,
9
+ getStatusLabelKey,
10
+ normalizeStatus,
11
+ relationLabels,
12
+ } from "./traceLayout";
13
+
14
+ interface TraceNodeDetailProps {
15
+ node: TraceNode | null;
16
+ onSelectNode: (id: string) => void;
17
+ /** When provided, artifact rows become buttons that focus that file. */
18
+ onSelectArtifact?: (path: string) => void;
19
+ /** Currently focused artifact path (for highlight). */
20
+ activeArtifactPath?: string | null;
21
+ t: (key: string, vars?: TranslateVars) => string;
22
+ }
23
+
24
+ /**
25
+ * Presentational detail pane for a single reasoning-trace node. Extracted from
26
+ * TracePanel so the live trace view and the demo replay share it. In the demo
27
+ * an `onSelectArtifact` handler wires artifact rows to the file preview.
28
+ */
29
+ export function TraceNodeDetail({ node, onSelectNode, onSelectArtifact, activeArtifactPath, t }: TraceNodeDetailProps) {
30
+ if (!node) {
31
+ return <p>No trace node selected.</p>;
32
+ }
33
+ const statusKey = getStatusLabelKey(node.status);
34
+ return (
35
+ <>
36
+ <div className="trace-detail__title">
37
+ <GitBranch size={17} />
38
+ <h3>{node.title}</h3>
39
+ <span className={`trace-detail__status trace-detail__status--${normalizeStatus(node.status)}`}>
40
+ {statusKey ? t(statusKey) : node.status}
41
+ </span>
42
+ </div>
43
+ <div className="trace-detail__badges">
44
+ <span>{node.id}</span>
45
+ <span>{getNodeKind(node)}</span>
46
+ <span>{node.agent || "agent unknown"}</span>
47
+ </div>
48
+ <p>{node.summary || node.description || node.content || "No summary recorded."}</p>
49
+ {node.reason ? (
50
+ <section className="trace-detail__section">
51
+ <h4><ArrowRight size={13} /> Reason</h4>
52
+ <p>{node.reason}</p>
53
+ </section>
54
+ ) : null}
55
+ {node.context ? (
56
+ <section className="trace-detail__section">
57
+ <h4><FileText size={13} /> Context</h4>
58
+ <p>{node.context}</p>
59
+ </section>
60
+ ) : null}
61
+ <div className="trace-detail__metrics">
62
+ <span><Timer size={13} /> {formatDuration(node.durationMs)}</span>
63
+ <span><Wrench size={13} /> {node.toolCalls.length} tools</span>
64
+ <span><Box size={13} /> {node.artifacts.length} artifacts</span>
65
+ </div>
66
+ {node.parents.length > 0 ? (
67
+ <section className="trace-detail__section">
68
+ <h4><GitBranch size={13} /> Dependencies</h4>
69
+ <div className="trace-relation-list">
70
+ {node.parents.map((parent) => (
71
+ <button key={parent.id} onClick={() => onSelectNode(parent.id)} type="button">
72
+ <strong>{parent.id}</strong>
73
+ <span>{relationLabels[parent.relation || ""] || parent.relation || "parent"}{parent.edgeType ? ` · ${parent.edgeType}` : ""}</span>
74
+ {parent.explanation ? <small>{parent.explanation}</small> : null}
75
+ </button>
76
+ ))}
77
+ </div>
78
+ </section>
79
+ ) : null}
80
+ {node.toolCalls.length > 0 ? (
81
+ <section className="trace-detail__section">
82
+ <h4><Wrench size={13} /> Tool Calls</h4>
83
+ <div className="trace-chip-list">
84
+ {node.toolCalls.map((tool) => <span key={tool}>{tool}</span>)}
85
+ </div>
86
+ </section>
87
+ ) : null}
88
+ {node.errorMessage ? (
89
+ <section className="trace-detail__section trace-detail__section--error">
90
+ <h4><AlertTriangle size={13} /> Error</h4>
91
+ <p>{node.errorMessage}</p>
92
+ </section>
93
+ ) : null}
94
+ {node.artifacts.length > 0 ? (
95
+ <section className="trace-detail__section">
96
+ <h4><Box size={13} /> Artifacts</h4>
97
+ <div className="trace-artifact-list">
98
+ {node.artifacts.map((artifact) => {
99
+ const label = artifactLabels[artifact.type || ""] || artifact.type || "file";
100
+ const name = artifact.path.split("/").pop() || artifact.path;
101
+ if (onSelectArtifact) {
102
+ return (
103
+ <button
104
+ key={`${artifact.path}-${artifact.type || ""}`}
105
+ type="button"
106
+ className={`trace-artifact-row ${activeArtifactPath === artifact.path ? "is-active" : ""}`}
107
+ title={artifact.path}
108
+ onClick={() => onSelectArtifact(artifact.path)}
109
+ >
110
+ <FileText size={13} />
111
+ <span>{name}</span>
112
+ <small>{label}</small>
113
+ </button>
114
+ );
115
+ }
116
+ return (
117
+ <div key={`${artifact.path}-${artifact.type || ""}`} title={artifact.path}>
118
+ <FileText size={13} />
119
+ <span>{name}</span>
120
+ <small>{label}</small>
121
+ </div>
122
+ );
123
+ })}
124
+ </div>
125
+ </section>
126
+ ) : null}
127
+ <section className="trace-detail__section">
128
+ <h4><Clock3 size={13} /> Timeline</h4>
129
+ <dl>
130
+ <div>
131
+ <dt>Created</dt>
132
+ <dd>{formatTime(node.timestamp?.createdAt || node.createdAt)}</dd>
133
+ </div>
134
+ <div>
135
+ <dt>Children</dt>
136
+ <dd>{node.childIds.join(", ") || "-"}</dd>
137
+ </div>
138
+ </dl>
139
+ </section>
140
+ </>
141
+ );
142
+ }
@@ -0,0 +1,397 @@
1
+ /* --------------------------------------------------------------------------
2
+ * agentAnalytics — pure derivation functions over the session's ChatMessage[]
3
+ * and the derived AgentEdge[]. No React, no DOM: easy to unit-test and reuse
4
+ * across GlobalOverview / AnalyticsTab / TimelineTab.
5
+ *
6
+ * All inter-agent semantics go through the SAME helpers used to draw the
7
+ * network graph (`getMessageEdge` / `msgTypeKind`) so analytics never drifts
8
+ * from what the graph shows.
9
+ * ------------------------------------------------------------------------ */
10
+ import { ChatMessage } from "../../contracts/backend";
11
+ import { AgentEdge, getMessageEdge, msgTypeKind } from "./agentNetworkShared";
12
+
13
+ export interface TrendPoint {
14
+ time: number; // bucket start, ms epoch
15
+ count: number;
16
+ }
17
+
18
+ export interface AgentLoad {
19
+ name: string;
20
+ sent: number;
21
+ received: number;
22
+ total: number;
23
+ }
24
+
25
+ export interface TypeDistribution {
26
+ delegate: number;
27
+ result: number;
28
+ other: number;
29
+ }
30
+
31
+ export interface LatencyStats {
32
+ count: number;
33
+ min: number;
34
+ q1: number;
35
+ median: number;
36
+ q3: number;
37
+ max: number;
38
+ mean: number;
39
+ }
40
+
41
+ export interface TokenRow {
42
+ name: string;
43
+ sentMsgs: number;
44
+ avgLen: number;
45
+ tokens: number;
46
+ }
47
+
48
+ export interface Heatmap {
49
+ agents: string[];
50
+ buckets: number;
51
+ bucketMs: number;
52
+ startMs: number;
53
+ /** counts[agentIndex][bucketIndex] */
54
+ counts: number[][];
55
+ max: number;
56
+ }
57
+
58
+ const tsOf = (iso: string): number => {
59
+ const t = new Date(iso).getTime();
60
+ return Number.isFinite(t) ? t : 0;
61
+ };
62
+
63
+ /** Inter-agent messages only (the same set the graph draws edges from). */
64
+ function interAgentMessages(messages: ChatMessage[]) {
65
+ return messages
66
+ .map((m) => getMessageEdge(m))
67
+ .filter((e): e is NonNullable<ReturnType<typeof getMessageEdge>> => e !== null);
68
+ }
69
+
70
+ /** Message volume bucketed over [now - windowMs, now]. */
71
+ export function computeMessageTrend(
72
+ messages: ChatMessage[],
73
+ nowMs: number,
74
+ windowMs = 3_600_000,
75
+ buckets = 30,
76
+ ): TrendPoint[] {
77
+ const start = nowMs - windowMs;
78
+ const bucketMs = windowMs / buckets;
79
+ const counts = new Array(buckets).fill(0);
80
+ for (const e of interAgentMessages(messages)) {
81
+ const ts = tsOf(e.timestamp);
82
+ if (ts < start || ts > nowMs) continue;
83
+ const idx = Math.min(buckets - 1, Math.floor((ts - start) / bucketMs));
84
+ counts[idx] += 1;
85
+ }
86
+ return counts.map((count, i) => ({ time: start + i * bucketMs, count }));
87
+ }
88
+
89
+ export function computeAgentLoad(edges: AgentEdge[]): AgentLoad[] {
90
+ const map = new Map<string, AgentLoad>();
91
+ const ensure = (name: string) => {
92
+ let row = map.get(name);
93
+ if (!row) {
94
+ row = { name, sent: 0, received: 0, total: 0 };
95
+ map.set(name, row);
96
+ }
97
+ return row;
98
+ };
99
+ for (const edge of edges) {
100
+ ensure(edge.from).sent += edge.messages.length;
101
+ ensure(edge.to).received += edge.messages.length;
102
+ }
103
+ const rows = Array.from(map.values());
104
+ rows.forEach((r) => (r.total = r.sent + r.received));
105
+ rows.sort((a, b) => b.total - a.total);
106
+ return rows;
107
+ }
108
+
109
+ export function computeTypeDistribution(messages: ChatMessage[]): TypeDistribution {
110
+ const dist: TypeDistribution = { delegate: 0, result: 0, other: 0 };
111
+ for (const e of interAgentMessages(messages)) {
112
+ const kind = msgTypeKind(e.msgType);
113
+ if (kind === "delegate") dist.delegate += 1;
114
+ else if (kind === "result") dist.result += 1;
115
+ else dist.other += 1;
116
+ }
117
+ return dist;
118
+ }
119
+
120
+ /**
121
+ * Pair each delegate with the next result that the delegated-to agent sends
122
+ * back (after the delegate's timestamp). Returns raw latencies in ms.
123
+ */
124
+ export function computeResponseLatencies(messages: ChatMessage[]): number[] {
125
+ const inter = interAgentMessages(messages).sort(
126
+ (a, b) => tsOf(a.timestamp) - tsOf(b.timestamp),
127
+ );
128
+ const latencies: number[] = [];
129
+ const usedResultIdx = new Set<number>();
130
+
131
+ inter.forEach((msg) => {
132
+ if (msgTypeKind(msg.msgType) !== "delegate") return;
133
+ const delegateTs = tsOf(msg.timestamp);
134
+ // Find earliest unused result FROM the delegated-to agent, after this time.
135
+ for (let i = 0; i < inter.length; i++) {
136
+ if (usedResultIdx.has(i)) continue;
137
+ const cand = inter[i];
138
+ if (msgTypeKind(cand.msgType) !== "result") continue;
139
+ if (cand.from !== msg.to) continue;
140
+ const candTs = tsOf(cand.timestamp);
141
+ if (candTs <= delegateTs) continue;
142
+ usedResultIdx.add(i);
143
+ latencies.push(candTs - delegateTs);
144
+ break;
145
+ }
146
+ });
147
+ return latencies;
148
+ }
149
+
150
+ export function summarizeLatencies(latencies: number[]): LatencyStats | null {
151
+ if (latencies.length === 0) return null;
152
+ const sorted = [...latencies].sort((a, b) => a - b);
153
+ const q = (p: number) => {
154
+ const idx = (sorted.length - 1) * p;
155
+ const lo = Math.floor(idx);
156
+ const hi = Math.ceil(idx);
157
+ if (lo === hi) return sorted[lo];
158
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
159
+ };
160
+ const mean = sorted.reduce((s, v) => s + v, 0) / sorted.length;
161
+ return {
162
+ count: sorted.length,
163
+ min: sorted[0],
164
+ q1: q(0.25),
165
+ median: q(0.5),
166
+ q3: q(0.75),
167
+ max: sorted[sorted.length - 1],
168
+ mean,
169
+ };
170
+ }
171
+
172
+ /** Rough per-agent token estimate from the content the agent SENT. */
173
+ export function estimateTokens(messages: ChatMessage[]): TokenRow[] {
174
+ const map = new Map<string, { chars: number; msgs: number }>();
175
+ for (const e of interAgentMessages(messages)) {
176
+ const row = map.get(e.from) ?? { chars: 0, msgs: 0 };
177
+ row.chars += e.content.length;
178
+ row.msgs += 1;
179
+ map.set(e.from, row);
180
+ }
181
+ const rows: TokenRow[] = Array.from(map.entries()).map(([name, { chars, msgs }]) => ({
182
+ name,
183
+ sentMsgs: msgs,
184
+ avgLen: msgs ? Math.round(chars / msgs) : 0,
185
+ tokens: Math.ceil(chars / 4),
186
+ }));
187
+ rows.sort((a, b) => b.tokens - a.tokens);
188
+ return rows;
189
+ }
190
+
191
+ export function computeLifecycleHeatmap(
192
+ messages: ChatMessage[],
193
+ agentNames: string[],
194
+ nowMs: number,
195
+ buckets = 20,
196
+ ): Heatmap {
197
+ const inter = interAgentMessages(messages);
198
+ const times = inter.map((e) => tsOf(e.timestamp)).filter((t) => t > 0);
199
+ const startMs = times.length ? Math.min(...times) : nowMs;
200
+ const span = Math.max(1, nowMs - startMs);
201
+ const bucketMs = span / buckets;
202
+
203
+ const agents = agentNames.filter((name) =>
204
+ inter.some((e) => e.from === name || e.to === name),
205
+ );
206
+ const indexOf = new Map(agents.map((a, i) => [a, i]));
207
+ const counts = agents.map(() => new Array(buckets).fill(0));
208
+ let max = 0;
209
+
210
+ for (const e of inter) {
211
+ const ts = tsOf(e.timestamp);
212
+ const bIdx = Math.min(buckets - 1, Math.max(0, Math.floor((ts - startMs) / bucketMs)));
213
+ for (const who of [e.from, e.to]) {
214
+ const aIdx = indexOf.get(who);
215
+ if (aIdx === undefined) continue;
216
+ counts[aIdx][bIdx] += 1;
217
+ if (counts[aIdx][bIdx] > max) max = counts[aIdx][bIdx];
218
+ }
219
+ }
220
+
221
+ return { agents, buckets, bucketMs, startMs, counts, max };
222
+ }
223
+
224
+ export function computeErrorCount(messages: ChatMessage[]): number {
225
+ return messages.filter((m) => m.kind === "error").length;
226
+ }
227
+
228
+ /** Human-friendly duration formatter for latency values. */
229
+ export function formatDuration(ms: number): string {
230
+ if (!Number.isFinite(ms)) return "—";
231
+ if (ms < 1000) return `${Math.round(ms)}ms`;
232
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
233
+ return `${(ms / 60_000).toFixed(1)}m`;
234
+ }
235
+
236
+ /* --------------------------------------------------------------------------
237
+ * Per-agent activity statistics (all messages, not just send_message)
238
+ * ------------------------------------------------------------------------ */
239
+
240
+ export interface AgentActivity {
241
+ name: string;
242
+ // Message output by role
243
+ assistantMessages: number;
244
+ reasoningMessages: number;
245
+ toolMessages: number;
246
+ systemMessages: number;
247
+ totalMessages: number;
248
+ // Content volume
249
+ totalChars: number;
250
+ estimatedTokens: number;
251
+ // Tool usage
252
+ toolCalls: number;
253
+ toolCallsByName: Record<string, number>;
254
+ topTools: Array<{ name: string; count: number }>;
255
+ // Communication (send_message only)
256
+ sentMessages: number;
257
+ receivedMessages: number;
258
+ communicationPartners: string[];
259
+ }
260
+
261
+ /**
262
+ * Compute comprehensive activity stats for a specific agent.
263
+ * Includes all message types, tool calls, and communication patterns.
264
+ */
265
+ export function computeAgentActivity(
266
+ agentName: string,
267
+ messages: ChatMessage[],
268
+ edges: AgentEdge[],
269
+ ): AgentActivity {
270
+ // Filter messages from this agent
271
+ const agentMessages = messages.filter((m) => m.agent === agentName);
272
+
273
+ // Count by role
274
+ let assistantMessages = 0;
275
+ let reasoningMessages = 0;
276
+ let toolMessages = 0;
277
+ let systemMessages = 0;
278
+ let totalChars = 0;
279
+
280
+ for (const msg of agentMessages) {
281
+ if (msg.role === "assistant") assistantMessages++;
282
+ else if (msg.role === "system") systemMessages++;
283
+ // Tool messages are identified by kind, not role
284
+ if (msg.kind === "thinking") reasoningMessages++;
285
+ else if (msg.kind === "tool") toolMessages++;
286
+
287
+ totalChars += (msg.content || "").length;
288
+ }
289
+
290
+ // Tool call statistics
291
+ const toolCallsByName: Record<string, number> = {};
292
+ let toolCalls = 0;
293
+
294
+ for (const msg of agentMessages) {
295
+ if (msg.kind === "tool" && msg.toolName) {
296
+ toolCalls++;
297
+ toolCallsByName[msg.toolName] = (toolCallsByName[msg.toolName] || 0) + 1;
298
+ }
299
+ }
300
+
301
+ const topTools = Object.entries(toolCallsByName)
302
+ .map(([name, count]) => ({ name, count }))
303
+ .sort((a, b) => b.count - a.count)
304
+ .slice(0, 3);
305
+
306
+ // Communication statistics (from edges)
307
+ let sentMessages = 0;
308
+ let receivedMessages = 0;
309
+ const partners = new Set<string>();
310
+
311
+ for (const edge of edges) {
312
+ if (edge.from === agentName) {
313
+ sentMessages += edge.messages.length;
314
+ partners.add(edge.to);
315
+ }
316
+ if (edge.to === agentName) {
317
+ receivedMessages += edge.messages.length;
318
+ partners.add(edge.from);
319
+ }
320
+ }
321
+
322
+ return {
323
+ name: agentName,
324
+ assistantMessages,
325
+ reasoningMessages,
326
+ toolMessages,
327
+ systemMessages,
328
+ totalMessages: agentMessages.length,
329
+ totalChars,
330
+ estimatedTokens: Math.ceil(totalChars / 4),
331
+ toolCalls,
332
+ toolCallsByName,
333
+ topTools,
334
+ sentMessages,
335
+ receivedMessages,
336
+ communicationPartners: Array.from(partners).sort(),
337
+ };
338
+ }
339
+
340
+ /**
341
+ * Compute activity stats for all agents in the session.
342
+ * Returns a map of agent name -> activity stats.
343
+ */
344
+ export function computeAllAgentActivities(
345
+ messages: ChatMessage[],
346
+ edges: AgentEdge[],
347
+ ): Map<string, AgentActivity> {
348
+ const agentNames = new Set<string>();
349
+
350
+ // Collect all agent names from messages
351
+ for (const msg of messages) {
352
+ if (msg.agent) agentNames.add(msg.agent);
353
+ }
354
+
355
+ // Also collect from edges
356
+ for (const edge of edges) {
357
+ agentNames.add(edge.from);
358
+ agentNames.add(edge.to);
359
+ }
360
+
361
+ const activities = new Map<string, AgentActivity>();
362
+ for (const name of agentNames) {
363
+ activities.set(name, computeAgentActivity(name, messages, edges));
364
+ }
365
+
366
+ return activities;
367
+ }
368
+
369
+ /**
370
+ * Compute global activity percentages for an agent.
371
+ */
372
+ export interface AgentActivityPercentages {
373
+ messagePercent: number; // This agent's messages / total messages
374
+ toolCallPercent: number; // This agent's tool calls / total tool calls
375
+ tokenPercent: number; // This agent's tokens / total tokens
376
+ }
377
+
378
+ export function computeAgentActivityPercentages(
379
+ activity: AgentActivity,
380
+ allActivities: Map<string, AgentActivity>,
381
+ ): AgentActivityPercentages {
382
+ let totalMessages = 0;
383
+ let totalToolCalls = 0;
384
+ let totalTokens = 0;
385
+
386
+ for (const a of allActivities.values()) {
387
+ totalMessages += a.totalMessages;
388
+ totalToolCalls += a.toolCalls;
389
+ totalTokens += a.estimatedTokens;
390
+ }
391
+
392
+ return {
393
+ messagePercent: totalMessages > 0 ? (activity.totalMessages / totalMessages) * 100 : 0,
394
+ toolCallPercent: totalToolCalls > 0 ? (activity.toolCalls / totalToolCalls) * 100 : 0,
395
+ tokenPercent: totalTokens > 0 ? (activity.estimatedTokens / totalTokens) * 100 : 0,
396
+ };
397
+ }