llm-deep-trace 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/bin/llm-deep-trace.js +24 -0
- package/next.config.ts +8 -0
- package/package.json +56 -0
- package/postcss.config.mjs +5 -0
- package/public/banner-v2.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.png +0 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agent-config/route.ts +31 -0
- package/src/app/api/all-sessions/route.ts +9 -0
- package/src/app/api/analytics/route.ts +379 -0
- package/src/app/api/detect-agents/route.ts +170 -0
- package/src/app/api/image/route.ts +73 -0
- package/src/app/api/search/route.ts +28 -0
- package/src/app/api/session-by-key/route.ts +21 -0
- package/src/app/api/sessions/[sessionId]/messages/route.ts +46 -0
- package/src/app/api/sse/route.ts +86 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +3518 -0
- package/src/app/icon.svg +4 -0
- package/src/app/layout.tsx +20 -0
- package/src/app/page.tsx +5 -0
- package/src/components/AnalyticsDashboard.tsx +393 -0
- package/src/components/App.tsx +243 -0
- package/src/components/CopyButton.tsx +42 -0
- package/src/components/Logo.tsx +20 -0
- package/src/components/MainPanel.tsx +1128 -0
- package/src/components/MessageRenderer.tsx +983 -0
- package/src/components/SessionTree.tsx +505 -0
- package/src/components/SettingsPanel.tsx +160 -0
- package/src/components/SetupView.tsx +206 -0
- package/src/components/Sidebar.tsx +714 -0
- package/src/components/ThemeToggle.tsx +54 -0
- package/src/lib/client-utils.ts +360 -0
- package/src/lib/normalizers.ts +371 -0
- package/src/lib/sessions.ts +1223 -0
- package/src/lib/store.ts +518 -0
- package/src/lib/types.ts +112 -0
- package/src/lib/useSSE.ts +81 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo, useCallback, useEffect, useRef } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ReactFlow,
|
|
6
|
+
Node,
|
|
7
|
+
Edge,
|
|
8
|
+
useReactFlow,
|
|
9
|
+
ReactFlowProvider,
|
|
10
|
+
NodeProps,
|
|
11
|
+
Handle,
|
|
12
|
+
Position,
|
|
13
|
+
} from "@xyflow/react";
|
|
14
|
+
import "@xyflow/react/dist/style.css";
|
|
15
|
+
import { NormalizedMessage, ContentBlock } from "@/lib/types";
|
|
16
|
+
import { extractText, cleanPreview } from "@/lib/client-utils";
|
|
17
|
+
|
|
18
|
+
// ── Types ──
|
|
19
|
+
|
|
20
|
+
interface TurnInfo {
|
|
21
|
+
turnIndex: number;
|
|
22
|
+
messageIndex: number;
|
|
23
|
+
preview: string;
|
|
24
|
+
subagents: SubagentInfo[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SubagentInfo {
|
|
28
|
+
label: string;
|
|
29
|
+
fullLabel: string;
|
|
30
|
+
agentKey: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface RootNodeData {
|
|
34
|
+
label: string;
|
|
35
|
+
fullLabel: string;
|
|
36
|
+
[key: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface TurnNodeData {
|
|
40
|
+
turnIndex: number;
|
|
41
|
+
messageIndex: number;
|
|
42
|
+
preview: string;
|
|
43
|
+
hasSubagents: boolean;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface SubagentNodeData {
|
|
48
|
+
label: string;
|
|
49
|
+
fullLabel: string;
|
|
50
|
+
agentKey: string;
|
|
51
|
+
[key: string]: unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface SessionTreeProps {
|
|
55
|
+
messages: NormalizedMessage[];
|
|
56
|
+
sessionId: string;
|
|
57
|
+
sessionLabel: string;
|
|
58
|
+
allSessions: import("@/lib/types").SessionInfo[];
|
|
59
|
+
onScrollToMessage: (messageIndex: number) => void;
|
|
60
|
+
onNavigateSession: (sessionKey: string) => void;
|
|
61
|
+
onClose: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Extract turn structure from messages ──
|
|
65
|
+
|
|
66
|
+
function buildTurns(messages: NormalizedMessage[]): TurnInfo[] {
|
|
67
|
+
const turns: TurnInfo[] = [];
|
|
68
|
+
let turnIndex = 0;
|
|
69
|
+
|
|
70
|
+
// Build a map of toolCallId → result text for extracting agentId from results
|
|
71
|
+
const toolResultMap = new Map<string, string>();
|
|
72
|
+
for (const msg of messages) {
|
|
73
|
+
if (msg.message?.role === "toolResult" && msg.message.toolCallId) {
|
|
74
|
+
const text = extractText(msg.message.content);
|
|
75
|
+
toolResultMap.set(msg.message.toolCallId, text);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < messages.length; i++) {
|
|
80
|
+
const msg = messages[i];
|
|
81
|
+
if (msg.message?.role !== "user") continue;
|
|
82
|
+
if (msg.type === "tool_result" || msg.message.toolCallId) continue;
|
|
83
|
+
|
|
84
|
+
turnIndex++;
|
|
85
|
+
const userText = extractText(msg.message.content);
|
|
86
|
+
const preview = cleanPreview(userText).slice(0, 45) || `Turn ${turnIndex}`;
|
|
87
|
+
|
|
88
|
+
const subagents: SubagentInfo[] = [];
|
|
89
|
+
|
|
90
|
+
// Scan forward for assistant responses until next user turn
|
|
91
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
92
|
+
const next = messages[j];
|
|
93
|
+
if (
|
|
94
|
+
next.message?.role === "user" &&
|
|
95
|
+
!next.message.toolCallId &&
|
|
96
|
+
next.type !== "tool_result"
|
|
97
|
+
)
|
|
98
|
+
break;
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
next.message?.role === "assistant" &&
|
|
102
|
+
Array.isArray(next.message.content)
|
|
103
|
+
) {
|
|
104
|
+
for (const block of next.message.content as ContentBlock[]) {
|
|
105
|
+
if (
|
|
106
|
+
block.type === "tool_use" &&
|
|
107
|
+
(block.name === "sessions_spawn" ||
|
|
108
|
+
block.name === "Task" ||
|
|
109
|
+
block.name === "task")
|
|
110
|
+
) {
|
|
111
|
+
const input = block.input || {};
|
|
112
|
+
const desc =
|
|
113
|
+
(input.description as string) ||
|
|
114
|
+
(input.name as string) ||
|
|
115
|
+
(input.prompt as string) ||
|
|
116
|
+
"subagent";
|
|
117
|
+
const fullDesc = desc.trim();
|
|
118
|
+
const label = fullDesc.slice(0, 30);
|
|
119
|
+
|
|
120
|
+
// Try to extract agentId from the tool result
|
|
121
|
+
let agentKey = "";
|
|
122
|
+
const resultText = block.id ? toolResultMap.get(block.id) || "" : "";
|
|
123
|
+
// Claude Code results contain "agentId: XXXXX"
|
|
124
|
+
const agentIdMatch = resultText.match(/agentId:\s*([a-zA-Z0-9_-]+)/);
|
|
125
|
+
if (agentIdMatch) {
|
|
126
|
+
agentKey = agentIdMatch[1];
|
|
127
|
+
}
|
|
128
|
+
// For sessions_spawn, try parsing result as JSON
|
|
129
|
+
if (!agentKey && resultText) {
|
|
130
|
+
try {
|
|
131
|
+
const parsed = JSON.parse(resultText);
|
|
132
|
+
agentKey = parsed.childSessionId || parsed.childSessionKey || parsed.agentId || "";
|
|
133
|
+
} catch { /* not JSON */ }
|
|
134
|
+
}
|
|
135
|
+
// Fallback to input fields
|
|
136
|
+
if (!agentKey) {
|
|
137
|
+
agentKey =
|
|
138
|
+
(input.agentId as string) ||
|
|
139
|
+
(input.name as string) ||
|
|
140
|
+
(input.sessionId as string) ||
|
|
141
|
+
block.id ||
|
|
142
|
+
"";
|
|
143
|
+
}
|
|
144
|
+
subagents.push({ label, fullLabel: fullDesc, agentKey });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
turns.push({ turnIndex, messageIndex: i, preview, subagents });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return turns;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Filter turns for display: if >50, show every 5th + turns with subagent spawns ──
|
|
157
|
+
|
|
158
|
+
function filterTurns(turns: TurnInfo[]): TurnInfo[] {
|
|
159
|
+
if (turns.length <= 50) return turns;
|
|
160
|
+
return turns.filter(
|
|
161
|
+
(t) => t.turnIndex % 5 === 0 || t.subagents.length > 0
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Custom Nodes ──
|
|
166
|
+
|
|
167
|
+
function RootNodeComponent({ data }: NodeProps<Node<RootNodeData>>) {
|
|
168
|
+
return (
|
|
169
|
+
<>
|
|
170
|
+
<div className="tree-node-card tree-node-root" title={data.fullLabel || data.label}>
|
|
171
|
+
<div className="tree-node-top">
|
|
172
|
+
<span className="tree-node-title">{data.label}</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<Handle
|
|
176
|
+
type="source"
|
|
177
|
+
position={Position.Bottom}
|
|
178
|
+
className="tree-handle"
|
|
179
|
+
/>
|
|
180
|
+
</>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function TurnNodeComponent({ data }: NodeProps<Node<TurnNodeData>>) {
|
|
185
|
+
return (
|
|
186
|
+
<>
|
|
187
|
+
<Handle type="target" position={Position.Top} className="tree-handle" />
|
|
188
|
+
<div className="tree-node-card tree-node-turn" title={data.preview}>
|
|
189
|
+
<div className="tree-node-top">
|
|
190
|
+
<span className="tree-turn-label">Turn {data.turnIndex}</span>
|
|
191
|
+
</div>
|
|
192
|
+
<div className="tree-node-preview">{data.preview}</div>
|
|
193
|
+
</div>
|
|
194
|
+
{data.hasSubagents && (
|
|
195
|
+
<Handle
|
|
196
|
+
type="source"
|
|
197
|
+
position={Position.Right}
|
|
198
|
+
id="right"
|
|
199
|
+
className="tree-handle"
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
202
|
+
<Handle
|
|
203
|
+
type="source"
|
|
204
|
+
position={Position.Bottom}
|
|
205
|
+
id="bottom"
|
|
206
|
+
className="tree-handle"
|
|
207
|
+
/>
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function SubagentNodeComponent({ data }: NodeProps<Node<SubagentNodeData>>) {
|
|
213
|
+
return (
|
|
214
|
+
<>
|
|
215
|
+
<Handle type="target" position={Position.Left} className="tree-handle" />
|
|
216
|
+
<div className="tree-node-card tree-node-subagent" title={data.fullLabel || data.label}>
|
|
217
|
+
<div className="tree-node-top">
|
|
218
|
+
<span className="tree-node-title">{data.label}</span>
|
|
219
|
+
<span className="tree-node-badge">subagent</span>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const nodeTypes = {
|
|
227
|
+
rootNode: RootNodeComponent,
|
|
228
|
+
turnNode: TurnNodeComponent,
|
|
229
|
+
subagentNode: SubagentNodeComponent,
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ── Layout ──
|
|
233
|
+
|
|
234
|
+
function layoutTurns(
|
|
235
|
+
turns: TurnInfo[],
|
|
236
|
+
label: string
|
|
237
|
+
): { nodes: Node[]; edges: Edge[] } {
|
|
238
|
+
const nodes: Node[] = [];
|
|
239
|
+
const edges: Edge[] = [];
|
|
240
|
+
|
|
241
|
+
const TURN_X = 0;
|
|
242
|
+
const SUBAGENT_X = 280;
|
|
243
|
+
const ROW_HEIGHT = 100;
|
|
244
|
+
const SUB_ROW_HEIGHT = 56;
|
|
245
|
+
const ROOT_Y = 0;
|
|
246
|
+
|
|
247
|
+
// Root session node
|
|
248
|
+
nodes.push({
|
|
249
|
+
id: "root",
|
|
250
|
+
type: "rootNode",
|
|
251
|
+
position: { x: TURN_X, y: ROOT_Y },
|
|
252
|
+
data: { label: label.slice(0, 30), fullLabel: label },
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
let y = 70;
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < turns.length; i++) {
|
|
258
|
+
const turn = turns[i];
|
|
259
|
+
const turnId = `turn-${turn.turnIndex}`;
|
|
260
|
+
|
|
261
|
+
nodes.push({
|
|
262
|
+
id: turnId,
|
|
263
|
+
type: "turnNode",
|
|
264
|
+
position: { x: TURN_X, y },
|
|
265
|
+
data: {
|
|
266
|
+
turnIndex: turn.turnIndex,
|
|
267
|
+
messageIndex: turn.messageIndex,
|
|
268
|
+
preview: turn.preview,
|
|
269
|
+
hasSubagents: turn.subagents.length > 0,
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Edge from previous
|
|
274
|
+
const prevId = i === 0 ? "root" : `turn-${turns[i - 1].turnIndex}`;
|
|
275
|
+
edges.push({
|
|
276
|
+
id: `${prevId}->${turnId}`,
|
|
277
|
+
source: prevId,
|
|
278
|
+
target: turnId,
|
|
279
|
+
sourceHandle: prevId === "root" ? undefined : "bottom",
|
|
280
|
+
type: "straight",
|
|
281
|
+
style: { stroke: "var(--border)", strokeWidth: 1 },
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Subagent branches to the right
|
|
285
|
+
for (let j = 0; j < turn.subagents.length; j++) {
|
|
286
|
+
const sub = turn.subagents[j];
|
|
287
|
+
const subId = `${turnId}-sub-${j}`;
|
|
288
|
+
const subY = y + j * SUB_ROW_HEIGHT;
|
|
289
|
+
|
|
290
|
+
nodes.push({
|
|
291
|
+
id: subId,
|
|
292
|
+
type: "subagentNode",
|
|
293
|
+
position: { x: SUBAGENT_X, y: subY },
|
|
294
|
+
data: { label: sub.label, fullLabel: sub.fullLabel || sub.label, agentKey: sub.agentKey },
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
edges.push({
|
|
298
|
+
id: `${turnId}->${subId}`,
|
|
299
|
+
source: turnId,
|
|
300
|
+
target: subId,
|
|
301
|
+
sourceHandle: "right",
|
|
302
|
+
type: "straight",
|
|
303
|
+
style: { stroke: "var(--border)", strokeWidth: 1 },
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Advance Y, accounting for subagent stack height
|
|
308
|
+
const subHeight =
|
|
309
|
+
turn.subagents.length > 1
|
|
310
|
+
? (turn.subagents.length - 1) * SUB_ROW_HEIGHT
|
|
311
|
+
: 0;
|
|
312
|
+
y += Math.max(ROW_HEIGHT, subHeight + ROW_HEIGHT);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { nodes, edges };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Inner Flow (needs ReactFlowProvider) ──
|
|
319
|
+
|
|
320
|
+
function InnerFlow({
|
|
321
|
+
messages,
|
|
322
|
+
sessionId: currentSessionId,
|
|
323
|
+
sessionLabel: label,
|
|
324
|
+
allSessions,
|
|
325
|
+
onScrollToMessage,
|
|
326
|
+
onNavigateSession,
|
|
327
|
+
onResetView,
|
|
328
|
+
}: {
|
|
329
|
+
messages: NormalizedMessage[];
|
|
330
|
+
sessionId: string;
|
|
331
|
+
sessionLabel: string;
|
|
332
|
+
allSessions: import("@/lib/types").SessionInfo[];
|
|
333
|
+
onScrollToMessage: (messageIndex: number) => void;
|
|
334
|
+
onNavigateSession: (sessionKey: string) => void;
|
|
335
|
+
onResetView?: (fn: () => void) => void;
|
|
336
|
+
}) {
|
|
337
|
+
const { fitView } = useReactFlow();
|
|
338
|
+
|
|
339
|
+
// Real child sessions from the session index (works for both Task and TaskCreate/Agent Teams)
|
|
340
|
+
const childSessions = useMemo(
|
|
341
|
+
() => allSessions.filter(
|
|
342
|
+
(s) => s.parentSessionId === currentSessionId || s.parentSessionId === currentSessionId
|
|
343
|
+
),
|
|
344
|
+
[allSessions, currentSessionId]
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
// Build turns from messages, then enrich subagent nodes with real session IDs
|
|
348
|
+
const turns = useMemo(() => {
|
|
349
|
+
const raw = filterTurns(buildTurns(messages));
|
|
350
|
+
|
|
351
|
+
// For each turn's subagents, try to resolve agentKey → real sessionId
|
|
352
|
+
for (const turn of raw) {
|
|
353
|
+
for (const sub of turn.subagents) {
|
|
354
|
+
if (!sub.agentKey) continue;
|
|
355
|
+
// Try to find a matching child session
|
|
356
|
+
const match = childSessions.find(
|
|
357
|
+
(s) =>
|
|
358
|
+
s.sessionId === sub.agentKey ||
|
|
359
|
+
s.sessionId.includes(sub.agentKey) ||
|
|
360
|
+
("agent-" + sub.agentKey) === s.sessionId ||
|
|
361
|
+
s.sessionId.endsWith(sub.agentKey)
|
|
362
|
+
);
|
|
363
|
+
if (match) sub.agentKey = match.sessionId;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Add any child sessions not already covered by turns (Agent Teams: TaskCreate-based)
|
|
368
|
+
const coveredIds = new Set(raw.flatMap((t) => t.subagents.map((s) => s.agentKey)));
|
|
369
|
+
const uncovered = childSessions.filter((s) => !coveredIds.has(s.sessionId));
|
|
370
|
+
if (uncovered.length > 0 && raw.length > 0) {
|
|
371
|
+
// Attach uncovered children to the last turn as a group
|
|
372
|
+
const lastTurn = raw[raw.length - 1];
|
|
373
|
+
for (const s of uncovered) {
|
|
374
|
+
const fullSub = s.label || s.sessionId;
|
|
375
|
+
lastTurn.subagents.push({
|
|
376
|
+
label: fullSub.slice(0, 30),
|
|
377
|
+
fullLabel: fullSub,
|
|
378
|
+
agentKey: s.sessionId,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return raw;
|
|
384
|
+
}, [messages, childSessions]);
|
|
385
|
+
|
|
386
|
+
const { nodes, edges } = useMemo(
|
|
387
|
+
() => layoutTurns(turns, label),
|
|
388
|
+
[turns, label]
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Fit once on mount only — never re-fit while user is browsing
|
|
392
|
+
const hasFitted = useRef(false);
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
if (hasFitted.current || nodes.length === 0) return;
|
|
395
|
+
hasFitted.current = true;
|
|
396
|
+
const timer = setTimeout(() => {
|
|
397
|
+
// More padding for small graphs (few turns), less for large ones — prevents over-zooming
|
|
398
|
+
const padding = nodes.length <= 4 ? 0.35 : nodes.length <= 10 ? 0.2 : 0.12;
|
|
399
|
+
fitView({ padding, minZoom: 0.15, maxZoom: 0.75, duration: 300 });
|
|
400
|
+
}, 80);
|
|
401
|
+
return () => clearTimeout(timer);
|
|
402
|
+
}, [nodes.length > 0, fitView]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
403
|
+
|
|
404
|
+
// Expose reset-view callback to parent
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
if (onResetView) {
|
|
407
|
+
onResetView(() => {
|
|
408
|
+
fitView({ padding: 0.12, duration: 300 });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}, [fitView, onResetView]);
|
|
412
|
+
|
|
413
|
+
const onNodeClick = useCallback(
|
|
414
|
+
(_: React.MouseEvent, node: Node) => {
|
|
415
|
+
if (node.type === "turnNode") {
|
|
416
|
+
const d = node.data as TurnNodeData;
|
|
417
|
+
onScrollToMessage(d.messageIndex);
|
|
418
|
+
} else if (node.type === "subagentNode") {
|
|
419
|
+
const d = node.data as SubagentNodeData;
|
|
420
|
+
if (d.agentKey) onNavigateSession(d.agentKey);
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
[onScrollToMessage, onNavigateSession]
|
|
424
|
+
);
|
|
425
|
+
|
|
426
|
+
return (
|
|
427
|
+
<ReactFlow
|
|
428
|
+
nodes={nodes}
|
|
429
|
+
edges={edges}
|
|
430
|
+
nodeTypes={nodeTypes}
|
|
431
|
+
onNodeClick={onNodeClick}
|
|
432
|
+
minZoom={0.15}
|
|
433
|
+
maxZoom={1.5}
|
|
434
|
+
proOptions={{ hideAttribution: true }}
|
|
435
|
+
className="session-tree-flow"
|
|
436
|
+
/>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ── Main Export ──
|
|
441
|
+
|
|
442
|
+
export default function SessionTree({
|
|
443
|
+
messages,
|
|
444
|
+
sessionId,
|
|
445
|
+
sessionLabel: label,
|
|
446
|
+
allSessions,
|
|
447
|
+
onScrollToMessage,
|
|
448
|
+
onNavigateSession,
|
|
449
|
+
onClose,
|
|
450
|
+
}: SessionTreeProps) {
|
|
451
|
+
const resetViewRef = useRef<(() => void) | null>(null);
|
|
452
|
+
|
|
453
|
+
const handleResetView = useCallback((fn: () => void) => {
|
|
454
|
+
resetViewRef.current = fn;
|
|
455
|
+
}, []);
|
|
456
|
+
|
|
457
|
+
return (
|
|
458
|
+
<div className="tree-panel">
|
|
459
|
+
<div className="tree-panel-header">
|
|
460
|
+
<span className="tree-panel-title">Conversation Map</span>
|
|
461
|
+
<div className="tree-panel-actions">
|
|
462
|
+
<button
|
|
463
|
+
className="tree-panel-reset"
|
|
464
|
+
onClick={() => resetViewRef.current?.()}
|
|
465
|
+
title="Reset view"
|
|
466
|
+
>
|
|
467
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
468
|
+
<path
|
|
469
|
+
d="M2 8a6 6 0 0110.47-4M14 8a6 6 0 01-10.47 4"
|
|
470
|
+
stroke="currentColor"
|
|
471
|
+
strokeWidth="1.5"
|
|
472
|
+
strokeLinecap="round"
|
|
473
|
+
/>
|
|
474
|
+
<path d="M12 1v3h-3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
475
|
+
<path d="M4 15v-3h3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
476
|
+
</svg>
|
|
477
|
+
</button>
|
|
478
|
+
<button className="tree-panel-close" onClick={onClose} title="Close">
|
|
479
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
480
|
+
<path
|
|
481
|
+
d="M3 3l10 10M13 3L3 13"
|
|
482
|
+
stroke="currentColor"
|
|
483
|
+
strokeWidth="2"
|
|
484
|
+
strokeLinecap="round"
|
|
485
|
+
/>
|
|
486
|
+
</svg>
|
|
487
|
+
</button>
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
<div className="tree-panel-body">
|
|
491
|
+
<ReactFlowProvider key={sessionId}>
|
|
492
|
+
<InnerFlow
|
|
493
|
+
messages={messages}
|
|
494
|
+
sessionId={sessionId}
|
|
495
|
+
sessionLabel={label}
|
|
496
|
+
allSessions={allSessions}
|
|
497
|
+
onScrollToMessage={onScrollToMessage}
|
|
498
|
+
onNavigateSession={onNavigateSession}
|
|
499
|
+
onResetView={handleResetView}
|
|
500
|
+
/>
|
|
501
|
+
</ReactFlowProvider>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useStore } from "@/lib/store";
|
|
4
|
+
import { BlockColors, DEFAULT_BLOCK_COLORS } from "@/lib/types";
|
|
5
|
+
|
|
6
|
+
const BLOCK_COLOR_ENTRIES: { key: keyof BlockColors; label: string }[] = [
|
|
7
|
+
{ key: "exec", label: "Exec / Bash" },
|
|
8
|
+
{ key: "file", label: "File ops" },
|
|
9
|
+
{ key: "web", label: "Web search/fetch" },
|
|
10
|
+
{ key: "browser", label: "Browser" },
|
|
11
|
+
{ key: "msg", label: "Message" },
|
|
12
|
+
{ key: "agent", label: "Agent / Task" },
|
|
13
|
+
{ key: "thinking", label: "Thinking" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const SystemIcon = () => (
|
|
17
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
|
|
18
|
+
<rect x="1" y="2" width="14" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.4" />
|
|
19
|
+
<path d="M5 14h6M8 12v2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const DarkIcon = () => (
|
|
24
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
|
|
25
|
+
<path d="M13.5 10.5A6 6 0 115.5 2.5a5 5 0 008 8z" fill="currentColor" />
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const LightIcon = () => (
|
|
30
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
|
|
31
|
+
<circle cx="8" cy="8" r="3.5" stroke="currentColor" strokeWidth="1.4" />
|
|
32
|
+
<path
|
|
33
|
+
d="M8 1.5v2M8 12.5v2M1.5 8h2M12.5 8h2M3.7 3.7l1.4 1.4M10.9 10.9l1.4 1.4M3.7 12.3l1.4-1.4M10.9 5.1l1.4-1.4"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth="1.3"
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const CloseIcon = () => (
|
|
42
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
43
|
+
<path d="M3 3l10 10M13 3L3 13" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
|
44
|
+
</svg>
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
export default function SettingsPanel() {
|
|
48
|
+
const theme = useStore((s) => s.theme);
|
|
49
|
+
const setTheme = useStore((s) => s.setTheme);
|
|
50
|
+
const blockColors = useStore((s) => s.blockColors);
|
|
51
|
+
const setBlockColor = useStore((s) => s.setBlockColor);
|
|
52
|
+
const resetBlockColor = useStore((s) => s.resetBlockColor);
|
|
53
|
+
const settings = useStore((s) => s.settings);
|
|
54
|
+
const setSetting = useStore((s) => s.setSetting);
|
|
55
|
+
const setSettingsOpen = useStore((s) => s.setSettingsOpen);
|
|
56
|
+
|
|
57
|
+
const themeButtons = [
|
|
58
|
+
{ key: "system", icon: <SystemIcon />, label: "System" },
|
|
59
|
+
{ key: "dark", icon: <DarkIcon />, label: "Dark" },
|
|
60
|
+
{ key: "light", icon: <LightIcon />, label: "Light" },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="settings-panel">
|
|
65
|
+
<div className="settings-header">
|
|
66
|
+
<span className="settings-title">Settings</span>
|
|
67
|
+
<button className="settings-close" onClick={() => setSettingsOpen(false)} title="Close">
|
|
68
|
+
<CloseIcon />
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="settings-body scroller">
|
|
73
|
+
{/* Theme section */}
|
|
74
|
+
<div className="settings-section">
|
|
75
|
+
<div className="settings-section-title">Theme</div>
|
|
76
|
+
<div className="settings-theme-row">
|
|
77
|
+
{themeButtons.map(({ key, icon, label }) => (
|
|
78
|
+
<button
|
|
79
|
+
key={key}
|
|
80
|
+
onClick={() => setTheme(key)}
|
|
81
|
+
className={`settings-theme-btn ${theme === key ? "active" : ""}`}
|
|
82
|
+
>
|
|
83
|
+
{icon}
|
|
84
|
+
<span>{label}</span>
|
|
85
|
+
</button>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Block colors section */}
|
|
91
|
+
<div className="settings-section">
|
|
92
|
+
<div className="settings-section-title">Block colors</div>
|
|
93
|
+
<div className="settings-colors-list">
|
|
94
|
+
{BLOCK_COLOR_ENTRIES.map(({ key, label }) => (
|
|
95
|
+
<div key={key} className="settings-color-row">
|
|
96
|
+
<span className="settings-color-label">{label}</span>
|
|
97
|
+
<div
|
|
98
|
+
className="settings-color-swatch"
|
|
99
|
+
style={{ background: blockColors[key] }}
|
|
100
|
+
/>
|
|
101
|
+
<input
|
|
102
|
+
type="color"
|
|
103
|
+
value={blockColors[key]}
|
|
104
|
+
onChange={(e) => setBlockColor(key, e.target.value)}
|
|
105
|
+
className="settings-color-input"
|
|
106
|
+
/>
|
|
107
|
+
{blockColors[key] !== DEFAULT_BLOCK_COLORS[key] && (
|
|
108
|
+
<button
|
|
109
|
+
className="settings-color-reset"
|
|
110
|
+
onClick={() => resetBlockColor(key)}
|
|
111
|
+
title="Reset to default"
|
|
112
|
+
>
|
|
113
|
+
reset
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
))}
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
{/* Display section */}
|
|
122
|
+
<div className="settings-section">
|
|
123
|
+
<div className="settings-section-title">Display</div>
|
|
124
|
+
<label className="settings-toggle-row">
|
|
125
|
+
<input
|
|
126
|
+
type="checkbox"
|
|
127
|
+
checked={settings.showTimestamps}
|
|
128
|
+
onChange={(e) => setSetting("showTimestamps", e.target.checked)}
|
|
129
|
+
/>
|
|
130
|
+
<span className="settings-toggle-label">Show timestamps</span>
|
|
131
|
+
</label>
|
|
132
|
+
<label className="settings-toggle-row">
|
|
133
|
+
<input
|
|
134
|
+
type="checkbox"
|
|
135
|
+
checked={settings.autoExpandToolCalls}
|
|
136
|
+
onChange={(e) => setSetting("autoExpandToolCalls", e.target.checked)}
|
|
137
|
+
/>
|
|
138
|
+
<span className="settings-toggle-label">Auto-expand tool calls</span>
|
|
139
|
+
</label>
|
|
140
|
+
<label className="settings-toggle-row">
|
|
141
|
+
<input
|
|
142
|
+
type="checkbox"
|
|
143
|
+
checked={settings.compactSidebar}
|
|
144
|
+
onChange={(e) => setSetting("compactSidebar", e.target.checked)}
|
|
145
|
+
/>
|
|
146
|
+
<span className="settings-toggle-label">Compact sidebar</span>
|
|
147
|
+
</label>
|
|
148
|
+
<label className="settings-toggle-row">
|
|
149
|
+
<input
|
|
150
|
+
type="checkbox"
|
|
151
|
+
checked={settings.skipPreamble}
|
|
152
|
+
onChange={(e) => setSetting("skipPreamble", e.target.checked)}
|
|
153
|
+
/>
|
|
154
|
+
<span className="settings-toggle-label">Skip preamble (hide system messages before first user message)</span>
|
|
155
|
+
</label>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|