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
package/src/lib/store.ts
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { create } from "zustand";
|
|
4
|
+
import { SessionInfo, RawEntry, NormalizedMessage, BlockColors, AppSettings, DEFAULT_BLOCK_COLORS, DEFAULT_SETTINGS } from "./types";
|
|
5
|
+
import { normalizeEntries } from "./normalizers";
|
|
6
|
+
|
|
7
|
+
export type BlockCategory = "thinking" | "exec" | "file" | "web" | "browser" | "msg" | "agent" | "user-msg" | "asst-text";
|
|
8
|
+
|
|
9
|
+
export type BlockExpansion = Record<BlockCategory, boolean>;
|
|
10
|
+
|
|
11
|
+
interface AppState {
|
|
12
|
+
sessions: SessionInfo[];
|
|
13
|
+
filteredSessions: SessionInfo[];
|
|
14
|
+
currentSessionId: string | null;
|
|
15
|
+
currentMessages: NormalizedMessage[];
|
|
16
|
+
rawEntries: RawEntry[];
|
|
17
|
+
loading: boolean;
|
|
18
|
+
sseConnected: boolean;
|
|
19
|
+
searchQuery: string;
|
|
20
|
+
sourceFilters: Record<string, boolean>;
|
|
21
|
+
expandedGroups: Set<string>;
|
|
22
|
+
allThinkingExpanded: boolean;
|
|
23
|
+
blockExpansion: BlockExpansion;
|
|
24
|
+
treePanelOpen: boolean;
|
|
25
|
+
treePanelManualClose: boolean;
|
|
26
|
+
theme: string;
|
|
27
|
+
sidebarWidth: number;
|
|
28
|
+
treePanelWidth: number;
|
|
29
|
+
settingsOpen: boolean;
|
|
30
|
+
blockColors: BlockColors;
|
|
31
|
+
settings: AppSettings;
|
|
32
|
+
scrollTargetIndex: number | null;
|
|
33
|
+
archivedSessionIds: Set<string>;
|
|
34
|
+
sidebarTab: "browse" | "starred" | "pinned" | "archived" | "analytics";
|
|
35
|
+
activeSessions: Set<string>;
|
|
36
|
+
hiddenBlockTypes: Set<BlockCategory>;
|
|
37
|
+
starredSessionIds: Set<string>;
|
|
38
|
+
pinnedMessages: Record<string, number[]>;
|
|
39
|
+
|
|
40
|
+
setSessions: (sessions: SessionInfo[]) => void;
|
|
41
|
+
setCurrentSession: (id: string | null) => void;
|
|
42
|
+
setMessages: (entries: RawEntry[]) => void;
|
|
43
|
+
setLoading: (loading: boolean) => void;
|
|
44
|
+
setSseConnected: (connected: boolean) => void;
|
|
45
|
+
setSearchQuery: (query: string) => void;
|
|
46
|
+
toggleSourceFilter: (source: string) => void;
|
|
47
|
+
toggleGroupExpanded: (sessionId: string) => void;
|
|
48
|
+
expandAllGroups: () => void;
|
|
49
|
+
collapseAllGroups: () => void;
|
|
50
|
+
toggleAllThinking: () => void;
|
|
51
|
+
toggleBlockExpansion: (category: BlockCategory) => void;
|
|
52
|
+
setTreePanelOpen: (open: boolean) => void;
|
|
53
|
+
setTreePanelManualClose: (val: boolean) => void;
|
|
54
|
+
setTheme: (theme: string) => void;
|
|
55
|
+
setSidebarWidth: (w: number) => void;
|
|
56
|
+
setTreePanelWidth: (w: number) => void;
|
|
57
|
+
setSettingsOpen: (open: boolean) => void;
|
|
58
|
+
setBlockColor: (key: keyof BlockColors, color: string) => void;
|
|
59
|
+
resetBlockColor: (key: keyof BlockColors) => void;
|
|
60
|
+
setSetting: <K extends keyof AppSettings>(key: K, value: AppSettings[K]) => void;
|
|
61
|
+
setScrollTargetIndex: (idx: number | null) => void;
|
|
62
|
+
archiveSession: (sessionId: string) => void;
|
|
63
|
+
unarchiveSession: (sessionId: string) => void;
|
|
64
|
+
setSidebarTab: (tab: "browse" | "starred" | "pinned" | "archived" | "analytics") => void;
|
|
65
|
+
setActiveSessions: (ids: Set<string>) => void;
|
|
66
|
+
toggleHiddenBlockType: (cat: BlockCategory) => void;
|
|
67
|
+
toggleStarred: (sessionId: string) => void;
|
|
68
|
+
togglePinMessage: (sessionId: string, msgIndex: number) => void;
|
|
69
|
+
initFromLocalStorage: () => void;
|
|
70
|
+
applyFilter: () => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadExpandedGroups(): Set<string> {
|
|
74
|
+
try {
|
|
75
|
+
const raw = localStorage.getItem("llm-deep-trace-expanded");
|
|
76
|
+
if (raw) return new Set(JSON.parse(raw));
|
|
77
|
+
} catch { /* ignore */ }
|
|
78
|
+
return new Set();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function saveExpandedGroups(groups: Set<string>) {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem("llm-deep-trace-expanded", JSON.stringify([...groups]));
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadBlockColors(): BlockColors {
|
|
88
|
+
try {
|
|
89
|
+
const raw = localStorage.getItem("llm-deep-trace-block-colors");
|
|
90
|
+
if (raw) return { ...DEFAULT_BLOCK_COLORS, ...JSON.parse(raw) };
|
|
91
|
+
} catch { /* ignore */ }
|
|
92
|
+
return { ...DEFAULT_BLOCK_COLORS };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function saveBlockColors(colors: BlockColors) {
|
|
96
|
+
try {
|
|
97
|
+
localStorage.setItem("llm-deep-trace-block-colors", JSON.stringify(colors));
|
|
98
|
+
} catch { /* ignore */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadSettings(): AppSettings {
|
|
102
|
+
try {
|
|
103
|
+
const raw = localStorage.getItem("llm-deep-trace-settings");
|
|
104
|
+
if (raw) return { ...DEFAULT_SETTINGS, ...JSON.parse(raw) };
|
|
105
|
+
} catch { /* ignore */ }
|
|
106
|
+
return { ...DEFAULT_SETTINGS };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function saveSettings(settings: AppSettings) {
|
|
110
|
+
try {
|
|
111
|
+
localStorage.setItem("llm-deep-trace-settings", JSON.stringify(settings));
|
|
112
|
+
} catch { /* ignore */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadSidebarWidth(): number {
|
|
116
|
+
try {
|
|
117
|
+
const raw = localStorage.getItem("llm-deep-trace-sidebar-w");
|
|
118
|
+
if (raw) {
|
|
119
|
+
const w = parseInt(raw, 10);
|
|
120
|
+
if (w >= 200 && w <= 480) return w;
|
|
121
|
+
}
|
|
122
|
+
} catch { /* ignore */ }
|
|
123
|
+
return 280;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function loadTreePanelWidth(): number {
|
|
127
|
+
try {
|
|
128
|
+
const raw = localStorage.getItem("llm-deep-trace-tree-w");
|
|
129
|
+
if (raw) {
|
|
130
|
+
const w = parseInt(raw, 10);
|
|
131
|
+
if (w >= 240 && w <= 600) return w;
|
|
132
|
+
}
|
|
133
|
+
} catch { /* ignore */ }
|
|
134
|
+
return 380;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadArchivedIds(): Set<string> {
|
|
138
|
+
try {
|
|
139
|
+
const raw = localStorage.getItem("llm-deep-trace-archived");
|
|
140
|
+
if (raw) return new Set(JSON.parse(raw));
|
|
141
|
+
} catch { /* ignore */ }
|
|
142
|
+
return new Set();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function saveArchivedIds(ids: Set<string>) {
|
|
146
|
+
try {
|
|
147
|
+
localStorage.setItem("llm-deep-trace-archived", JSON.stringify([...ids]));
|
|
148
|
+
} catch { /* ignore */ }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function loadHiddenBlockTypes(): Set<BlockCategory> {
|
|
152
|
+
try {
|
|
153
|
+
const raw = localStorage.getItem("llm-deep-trace-hidden-blocks");
|
|
154
|
+
if (raw) return new Set(JSON.parse(raw) as BlockCategory[]);
|
|
155
|
+
} catch { /* ignore */ }
|
|
156
|
+
return new Set();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function saveHiddenBlockTypes(types: Set<BlockCategory>) {
|
|
160
|
+
try {
|
|
161
|
+
localStorage.setItem("llm-deep-trace-hidden-blocks", JSON.stringify([...types]));
|
|
162
|
+
} catch { /* ignore */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function loadStarred(): Set<string> {
|
|
166
|
+
try {
|
|
167
|
+
const raw = localStorage.getItem("llm-deep-trace-starred");
|
|
168
|
+
if (raw) return new Set(JSON.parse(raw));
|
|
169
|
+
} catch { /* ignore */ }
|
|
170
|
+
return new Set();
|
|
171
|
+
}
|
|
172
|
+
function saveStarred(ids: Set<string>) {
|
|
173
|
+
try { localStorage.setItem("llm-deep-trace-starred", JSON.stringify([...ids])); } catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function loadPinnedMessages(): Record<string, number[]> {
|
|
177
|
+
try {
|
|
178
|
+
const raw = localStorage.getItem("llm-deep-trace-pinned");
|
|
179
|
+
if (raw) return JSON.parse(raw);
|
|
180
|
+
} catch { /* ignore */ }
|
|
181
|
+
return {};
|
|
182
|
+
}
|
|
183
|
+
function savePinnedMessages(pins: Record<string, number[]>) {
|
|
184
|
+
try { localStorage.setItem("llm-deep-trace-pinned", JSON.stringify(pins)); } catch { /* ignore */ }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function buildGroupedSessions(list: SessionInfo[]): SessionInfo[] {
|
|
188
|
+
const childrenOf = new Map<string, SessionInfo[]>();
|
|
189
|
+
const childIds = new Set<string>();
|
|
190
|
+
|
|
191
|
+
for (const s of list) {
|
|
192
|
+
const isKovaSubagent = s.key?.startsWith("agent:main:subagent:");
|
|
193
|
+
const parentId = s.parentSessionId || (isKovaSubagent ? "__main__" : null);
|
|
194
|
+
if (parentId) {
|
|
195
|
+
if (!childrenOf.has(parentId)) childrenOf.set(parentId, []);
|
|
196
|
+
childrenOf.get(parentId)!.push(s);
|
|
197
|
+
childIds.add(s.sessionId);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const mainSess = list.find((s) => s.key === "agent:main:main");
|
|
202
|
+
if (mainSess && childrenOf.has("__main__")) {
|
|
203
|
+
childrenOf.set(mainSess.sessionId, [
|
|
204
|
+
...(childrenOf.get(mainSess.sessionId) || []),
|
|
205
|
+
...childrenOf.get("__main__")!,
|
|
206
|
+
]);
|
|
207
|
+
childrenOf.delete("__main__");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const result: SessionInfo[] = [];
|
|
211
|
+
const added = new Set<string>();
|
|
212
|
+
|
|
213
|
+
for (const s of list) {
|
|
214
|
+
if (added.has(s.sessionId) || childIds.has(s.sessionId)) continue;
|
|
215
|
+
result.push(s);
|
|
216
|
+
added.add(s.sessionId);
|
|
217
|
+
const children = childrenOf.get(s.sessionId) || [];
|
|
218
|
+
children.sort((a, b) => (b.lastUpdated || 0) - (a.lastUpdated || 0));
|
|
219
|
+
for (const c of children) {
|
|
220
|
+
if (!added.has(c.sessionId)) {
|
|
221
|
+
result.push(c);
|
|
222
|
+
added.add(c.sessionId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (const s of list) {
|
|
228
|
+
if (!added.has(s.sessionId)) {
|
|
229
|
+
result.push(s);
|
|
230
|
+
added.add(s.sessionId);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const DEFAULT_SOURCE_FILTERS: Record<string, boolean> = {
|
|
237
|
+
kova: true,
|
|
238
|
+
claude: true,
|
|
239
|
+
codex: true,
|
|
240
|
+
kimi: true,
|
|
241
|
+
gemini: true,
|
|
242
|
+
copilot: true,
|
|
243
|
+
factory: true,
|
|
244
|
+
opencode: true,
|
|
245
|
+
aider: true,
|
|
246
|
+
continue: true,
|
|
247
|
+
cursor: true,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export const useStore = create<AppState>((set, get) => ({
|
|
251
|
+
sessions: [],
|
|
252
|
+
filteredSessions: [],
|
|
253
|
+
currentSessionId: null,
|
|
254
|
+
currentMessages: [],
|
|
255
|
+
rawEntries: [],
|
|
256
|
+
loading: false,
|
|
257
|
+
sseConnected: false,
|
|
258
|
+
searchQuery: "",
|
|
259
|
+
sourceFilters: { ...DEFAULT_SOURCE_FILTERS },
|
|
260
|
+
expandedGroups: new Set<string>(),
|
|
261
|
+
allThinkingExpanded: false,
|
|
262
|
+
blockExpansion: { thinking: false, exec: false, file: false, web: false, browser: false, msg: false, agent: false, "user-msg": false, "asst-text": false },
|
|
263
|
+
treePanelOpen: false,
|
|
264
|
+
treePanelManualClose: false,
|
|
265
|
+
theme: "system",
|
|
266
|
+
sidebarWidth: 280,
|
|
267
|
+
treePanelWidth: 380,
|
|
268
|
+
settingsOpen: false,
|
|
269
|
+
blockColors: { ...DEFAULT_BLOCK_COLORS },
|
|
270
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
271
|
+
scrollTargetIndex: null,
|
|
272
|
+
archivedSessionIds: new Set<string>(),
|
|
273
|
+
sidebarTab: "browse",
|
|
274
|
+
activeSessions: new Set<string>(),
|
|
275
|
+
hiddenBlockTypes: new Set<BlockCategory>(),
|
|
276
|
+
starredSessionIds: new Set<string>(),
|
|
277
|
+
pinnedMessages: {},
|
|
278
|
+
|
|
279
|
+
setSessions: (sessions) => {
|
|
280
|
+
sessions.sort((a, b) => {
|
|
281
|
+
if (a.isActive && !a.isDeleted && !(b.isActive && !b.isDeleted)) return -1;
|
|
282
|
+
if (b.isActive && !b.isDeleted && !(a.isActive && !a.isDeleted)) return 1;
|
|
283
|
+
return (b.lastUpdated || 0) - (a.lastUpdated || 0);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
// Detect active sessions (modified in last 60s)
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
const active = new Set<string>();
|
|
289
|
+
for (const s of sessions) {
|
|
290
|
+
if (s.lastUpdated && now - s.lastUpdated < 60000) {
|
|
291
|
+
active.add(s.sessionId);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
set({ sessions, activeSessions: active });
|
|
296
|
+
get().applyFilter();
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
setCurrentSession: (id) => set({ currentSessionId: id }),
|
|
300
|
+
|
|
301
|
+
setMessages: (entries) => {
|
|
302
|
+
entries.sort((a, b) => {
|
|
303
|
+
const ta = a.timestamp ? new Date(a.timestamp).getTime() : 0;
|
|
304
|
+
const tb = b.timestamp ? new Date(b.timestamp).getTime() : 0;
|
|
305
|
+
return ta - tb;
|
|
306
|
+
});
|
|
307
|
+
const normalized = normalizeEntries(entries);
|
|
308
|
+
set({ rawEntries: entries, currentMessages: normalized });
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
setLoading: (loading) => set({ loading }),
|
|
312
|
+
setSseConnected: (connected) => set({ sseConnected: connected }),
|
|
313
|
+
|
|
314
|
+
setSearchQuery: (query) => {
|
|
315
|
+
set({ searchQuery: query });
|
|
316
|
+
get().applyFilter();
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
toggleSourceFilter: (source) => {
|
|
320
|
+
const filters = { ...get().sourceFilters };
|
|
321
|
+
filters[source] = !filters[source];
|
|
322
|
+
set({ sourceFilters: filters });
|
|
323
|
+
get().applyFilter();
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
toggleGroupExpanded: (sessionId) => {
|
|
327
|
+
const expanded = new Set(get().expandedGroups);
|
|
328
|
+
if (expanded.has(sessionId)) expanded.delete(sessionId);
|
|
329
|
+
else expanded.add(sessionId);
|
|
330
|
+
set({ expandedGroups: expanded });
|
|
331
|
+
// Not persisted — groups always start collapsed on load
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
expandAllGroups: () => {
|
|
335
|
+
const parentIds = new Set<string>();
|
|
336
|
+
for (const s of get().sessions) {
|
|
337
|
+
if (s.parentSessionId) parentIds.add(s.parentSessionId);
|
|
338
|
+
if (s.hasSubagents) parentIds.add(s.sessionId);
|
|
339
|
+
if (s.key?.startsWith("agent:main:subagent:")) {
|
|
340
|
+
const main = get().sessions.find((p) => p.key === "agent:main:main");
|
|
341
|
+
if (main) parentIds.add(main.sessionId);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
set({ expandedGroups: parentIds });
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
collapseAllGroups: () => {
|
|
348
|
+
set({ expandedGroups: new Set() });
|
|
349
|
+
},
|
|
350
|
+
|
|
351
|
+
toggleAllThinking: () => {
|
|
352
|
+
const newVal = !get().allThinkingExpanded;
|
|
353
|
+
const expansion = { ...get().blockExpansion, thinking: newVal };
|
|
354
|
+
set({ allThinkingExpanded: newVal, blockExpansion: expansion });
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
toggleBlockExpansion: (category) => {
|
|
358
|
+
const expansion = { ...get().blockExpansion };
|
|
359
|
+
expansion[category] = !expansion[category];
|
|
360
|
+
if (category === "thinking") {
|
|
361
|
+
set({ blockExpansion: expansion, allThinkingExpanded: expansion.thinking });
|
|
362
|
+
} else {
|
|
363
|
+
set({ blockExpansion: expansion });
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
|
|
367
|
+
setTreePanelOpen: (open) => set({ treePanelOpen: open }),
|
|
368
|
+
setTreePanelManualClose: (val) => set({ treePanelManualClose: val }),
|
|
369
|
+
|
|
370
|
+
setTheme: (theme) => {
|
|
371
|
+
set({ theme });
|
|
372
|
+
const root = document.documentElement;
|
|
373
|
+
root.classList.remove("theme-dim", "theme-light");
|
|
374
|
+
let effective = theme;
|
|
375
|
+
if (theme === "system") {
|
|
376
|
+
effective = window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
377
|
+
? "dark"
|
|
378
|
+
: "light";
|
|
379
|
+
}
|
|
380
|
+
if (effective === "light") root.classList.add("theme-light");
|
|
381
|
+
try {
|
|
382
|
+
localStorage.setItem("deep-trace-theme", theme);
|
|
383
|
+
} catch {
|
|
384
|
+
// ignore
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
setSidebarWidth: (w) => {
|
|
389
|
+
const clamped = Math.max(200, Math.min(480, w));
|
|
390
|
+
set({ sidebarWidth: clamped });
|
|
391
|
+
try {
|
|
392
|
+
localStorage.setItem("llm-deep-trace-sidebar-w", String(clamped));
|
|
393
|
+
} catch { /* ignore */ }
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
setTreePanelWidth: (w) => {
|
|
397
|
+
const clamped = Math.max(240, Math.min(600, w));
|
|
398
|
+
set({ treePanelWidth: clamped });
|
|
399
|
+
try {
|
|
400
|
+
localStorage.setItem("llm-deep-trace-tree-w", String(clamped));
|
|
401
|
+
} catch { /* ignore */ }
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
setSettingsOpen: (open) => set({ settingsOpen: open }),
|
|
405
|
+
|
|
406
|
+
setScrollTargetIndex: (idx) => set({ scrollTargetIndex: idx }),
|
|
407
|
+
|
|
408
|
+
setBlockColor: (key, color) => {
|
|
409
|
+
const colors = { ...get().blockColors, [key]: color };
|
|
410
|
+
set({ blockColors: colors });
|
|
411
|
+
saveBlockColors(colors);
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
resetBlockColor: (key) => {
|
|
415
|
+
const colors = { ...get().blockColors, [key]: DEFAULT_BLOCK_COLORS[key] };
|
|
416
|
+
set({ blockColors: colors });
|
|
417
|
+
saveBlockColors(colors);
|
|
418
|
+
},
|
|
419
|
+
|
|
420
|
+
setSetting: (key, value) => {
|
|
421
|
+
const settings = { ...get().settings, [key]: value };
|
|
422
|
+
set({ settings });
|
|
423
|
+
saveSettings(settings);
|
|
424
|
+
},
|
|
425
|
+
|
|
426
|
+
archiveSession: (sessionId) => {
|
|
427
|
+
const ids = new Set(get().archivedSessionIds);
|
|
428
|
+
ids.add(sessionId);
|
|
429
|
+
const update: Partial<ReturnType<typeof get>> = { archivedSessionIds: ids };
|
|
430
|
+
if (get().currentSessionId === sessionId) update.currentSessionId = null;
|
|
431
|
+
set(update);
|
|
432
|
+
saveArchivedIds(ids);
|
|
433
|
+
get().applyFilter();
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
unarchiveSession: (sessionId) => {
|
|
437
|
+
const ids = new Set(get().archivedSessionIds);
|
|
438
|
+
ids.delete(sessionId);
|
|
439
|
+
set({ archivedSessionIds: ids });
|
|
440
|
+
saveArchivedIds(ids);
|
|
441
|
+
get().applyFilter();
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
setSidebarTab: (tab) => {
|
|
445
|
+
const { currentSessionId, archivedSessionIds } = get();
|
|
446
|
+
// Clear the panel when switching tabs if the active session doesn't belong there
|
|
447
|
+
let clearSession = false;
|
|
448
|
+
if (tab === "archived" && currentSessionId && !archivedSessionIds.has(currentSessionId)) {
|
|
449
|
+
clearSession = true;
|
|
450
|
+
} else if (tab === "browse" && currentSessionId && archivedSessionIds.has(currentSessionId)) {
|
|
451
|
+
clearSession = true;
|
|
452
|
+
}
|
|
453
|
+
set({ sidebarTab: tab, ...(clearSession ? { currentSessionId: null, currentMessages: [], rawEntries: [] } : {}) });
|
|
454
|
+
get().applyFilter();
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
setActiveSessions: (ids) => set({ activeSessions: ids }),
|
|
458
|
+
toggleHiddenBlockType: (cat) => {
|
|
459
|
+
const next = new Set(get().hiddenBlockTypes);
|
|
460
|
+
if (next.has(cat)) next.delete(cat); else next.add(cat);
|
|
461
|
+
set({ hiddenBlockTypes: next });
|
|
462
|
+
saveHiddenBlockTypes(next);
|
|
463
|
+
},
|
|
464
|
+
toggleStarred: (sessionId) => {
|
|
465
|
+
const next = new Set(get().starredSessionIds);
|
|
466
|
+
if (next.has(sessionId)) next.delete(sessionId); else next.add(sessionId);
|
|
467
|
+
set({ starredSessionIds: next });
|
|
468
|
+
saveStarred(next);
|
|
469
|
+
get().applyFilter();
|
|
470
|
+
},
|
|
471
|
+
togglePinMessage: (sessionId, msgIndex) => {
|
|
472
|
+
const current = get().pinnedMessages;
|
|
473
|
+
const arr = [...(current[sessionId] || [])];
|
|
474
|
+
const idx = arr.indexOf(msgIndex);
|
|
475
|
+
if (idx >= 0) arr.splice(idx, 1); else arr.push(msgIndex);
|
|
476
|
+
const next = { ...current, [sessionId]: arr };
|
|
477
|
+
set({ pinnedMessages: next });
|
|
478
|
+
savePinnedMessages(next);
|
|
479
|
+
get().applyFilter();
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
initFromLocalStorage: () => {
|
|
483
|
+
set({
|
|
484
|
+
expandedGroups: new Set(), // always start collapsed
|
|
485
|
+
settings: { ...loadSettings(), autoExpandToolCalls: false }, // blocks always collapsed by default
|
|
486
|
+
blockColors: loadBlockColors(),
|
|
487
|
+
sidebarWidth: loadSidebarWidth(),
|
|
488
|
+
treePanelWidth: loadTreePanelWidth(),
|
|
489
|
+
archivedSessionIds: loadArchivedIds(),
|
|
490
|
+
hiddenBlockTypes: loadHiddenBlockTypes(),
|
|
491
|
+
starredSessionIds: loadStarred(),
|
|
492
|
+
pinnedMessages: loadPinnedMessages(),
|
|
493
|
+
});
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
applyFilter: () => {
|
|
497
|
+
const { sessions, searchQuery, sourceFilters, archivedSessionIds, sidebarTab, starredSessionIds, pinnedMessages } = get();
|
|
498
|
+
const q = searchQuery.toLowerCase().trim();
|
|
499
|
+
const filtered = sessions.filter((s) => {
|
|
500
|
+
const src = s.source || "kova";
|
|
501
|
+
if (sourceFilters[src] === false) return false;
|
|
502
|
+
const isArchived = archivedSessionIds.has(s.sessionId);
|
|
503
|
+
// Tab-specific visibility rules
|
|
504
|
+
if (sidebarTab === "archived") { if (!isArchived) return false; }
|
|
505
|
+
else if (sidebarTab === "starred") { if (!starredSessionIds.has(s.sessionId)) return false; }
|
|
506
|
+
else if (sidebarTab === "pinned") { if (!(pinnedMessages[s.sessionId]?.length > 0)) return false; }
|
|
507
|
+
else { if (isArchived) return false; } // browse
|
|
508
|
+
if (!q) return true;
|
|
509
|
+
const label = (s.label || s.title || s.key || "").toLowerCase();
|
|
510
|
+
const preview = (s.preview || "").toLowerCase();
|
|
511
|
+
const key = (s.key || "").toLowerCase();
|
|
512
|
+
const id = (s.sessionId || "").toLowerCase();
|
|
513
|
+
return label.includes(q) || preview.includes(q) || key.includes(q) || id.includes(q);
|
|
514
|
+
});
|
|
515
|
+
const grouped = q ? filtered : buildGroupedSessions(filtered);
|
|
516
|
+
set({ filteredSessions: grouped });
|
|
517
|
+
},
|
|
518
|
+
}));
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export interface SessionInfo {
|
|
2
|
+
sessionId: string;
|
|
3
|
+
key: string;
|
|
4
|
+
label?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
lastUpdated: number;
|
|
7
|
+
channel: string;
|
|
8
|
+
chatType: string;
|
|
9
|
+
messageCount: number;
|
|
10
|
+
preview: string;
|
|
11
|
+
isActive: boolean;
|
|
12
|
+
isDeleted: boolean;
|
|
13
|
+
isSubagent: boolean;
|
|
14
|
+
parentSessionId?: string;
|
|
15
|
+
hasSubagents?: boolean;
|
|
16
|
+
compactionCount: number;
|
|
17
|
+
source?: string;
|
|
18
|
+
model?: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
filePath?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BlockColors {
|
|
24
|
+
exec: string;
|
|
25
|
+
file: string;
|
|
26
|
+
web: string;
|
|
27
|
+
browser: string;
|
|
28
|
+
msg: string;
|
|
29
|
+
agent: string;
|
|
30
|
+
thinking: string;
|
|
31
|
+
"user-msg": string;
|
|
32
|
+
"asst-text": string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface AppSettings {
|
|
36
|
+
showTimestamps: boolean;
|
|
37
|
+
autoExpandToolCalls: boolean;
|
|
38
|
+
compactSidebar: boolean;
|
|
39
|
+
skipPreamble: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TokenStats {
|
|
43
|
+
inputTokens: number;
|
|
44
|
+
outputTokens: number;
|
|
45
|
+
cacheReadTokens: number;
|
|
46
|
+
cacheWriteTokens: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_BLOCK_COLORS: BlockColors = {
|
|
50
|
+
exec: "#22C55E",
|
|
51
|
+
file: "#3B82F6",
|
|
52
|
+
web: "#8B5CF6",
|
|
53
|
+
browser: "#06B6D4",
|
|
54
|
+
msg: "#F59E0B",
|
|
55
|
+
agent: "#9B72EF",
|
|
56
|
+
thinking: "#71717A",
|
|
57
|
+
"user-msg": "#9B72EF",
|
|
58
|
+
"asst-text": "#E8E8F0",
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const DEFAULT_SETTINGS: AppSettings = {
|
|
62
|
+
showTimestamps: true,
|
|
63
|
+
autoExpandToolCalls: false,
|
|
64
|
+
compactSidebar: false,
|
|
65
|
+
skipPreamble: false,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface NormalizedMessage {
|
|
69
|
+
type: string;
|
|
70
|
+
timestamp?: string;
|
|
71
|
+
message?: {
|
|
72
|
+
role: string;
|
|
73
|
+
content: ContentBlock[] | string;
|
|
74
|
+
toolCallId?: string;
|
|
75
|
+
toolName?: string;
|
|
76
|
+
isError?: boolean;
|
|
77
|
+
};
|
|
78
|
+
// For special event types
|
|
79
|
+
summary?: string;
|
|
80
|
+
modelId?: string;
|
|
81
|
+
provider?: string;
|
|
82
|
+
thinkingLevel?: string;
|
|
83
|
+
customType?: string;
|
|
84
|
+
data?: Record<string, unknown>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ContentBlock {
|
|
88
|
+
type: string;
|
|
89
|
+
text?: string;
|
|
90
|
+
thinking?: string;
|
|
91
|
+
id?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
input?: Record<string, unknown>;
|
|
94
|
+
arguments?: Record<string, unknown>;
|
|
95
|
+
tool_use_id?: string;
|
|
96
|
+
content?: ContentBlock[] | string;
|
|
97
|
+
is_error?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface RawEntry {
|
|
101
|
+
type: string;
|
|
102
|
+
timestamp?: string;
|
|
103
|
+
message?: Record<string, unknown>;
|
|
104
|
+
payload?: Record<string, unknown>;
|
|
105
|
+
summary?: string;
|
|
106
|
+
modelId?: string;
|
|
107
|
+
provider?: string;
|
|
108
|
+
thinkingLevel?: string;
|
|
109
|
+
customType?: string;
|
|
110
|
+
data?: Record<string, unknown>;
|
|
111
|
+
[key: string]: unknown;
|
|
112
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
import { useStore } from "./store";
|
|
5
|
+
|
|
6
|
+
export function useSSE() {
|
|
7
|
+
const setSseConnected = useStore((s) => s.setSseConnected);
|
|
8
|
+
const setSessions = useStore((s) => s.setSessions);
|
|
9
|
+
const setMessages = useStore((s) => s.setMessages);
|
|
10
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
11
|
+
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
function connect() {
|
|
15
|
+
const es = new EventSource("/api/sse");
|
|
16
|
+
eventSourceRef.current = es;
|
|
17
|
+
|
|
18
|
+
es.onopen = () => {
|
|
19
|
+
setSseConnected(true);
|
|
20
|
+
if (reconnectTimer.current) {
|
|
21
|
+
clearTimeout(reconnectTimer.current);
|
|
22
|
+
reconnectTimer.current = null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
es.onmessage = (event) => {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(event.data);
|
|
29
|
+
if (data.event === "session_updated") {
|
|
30
|
+
// Reload sessions list
|
|
31
|
+
fetch("/api/all-sessions")
|
|
32
|
+
.then((r) => r.json())
|
|
33
|
+
.then((sessions) => setSessions(sessions))
|
|
34
|
+
.catch(() => {});
|
|
35
|
+
|
|
36
|
+
// Reload current session messages if it's the one that updated
|
|
37
|
+
const state = useStore.getState();
|
|
38
|
+
if (data.sessionId === state.currentSessionId) {
|
|
39
|
+
const sess = state.sessions.find(
|
|
40
|
+
(s) => s.sessionId === data.sessionId
|
|
41
|
+
);
|
|
42
|
+
const source = sess?.source || "kova";
|
|
43
|
+
fetch(
|
|
44
|
+
`/api/sessions/${data.sessionId}/messages?source=${source}`
|
|
45
|
+
)
|
|
46
|
+
.then((r) => r.json())
|
|
47
|
+
.then((entries) => {
|
|
48
|
+
if (Array.isArray(entries)) setMessages(entries);
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
} else if (data.event === "sessions_index_updated") {
|
|
53
|
+
fetch("/api/all-sessions")
|
|
54
|
+
.then((r) => r.json())
|
|
55
|
+
.then((sessions) => setSessions(sessions))
|
|
56
|
+
.catch(() => {});
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// ignore parse errors
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
es.onerror = () => {
|
|
64
|
+
setSseConnected(false);
|
|
65
|
+
es.close();
|
|
66
|
+
reconnectTimer.current = setTimeout(connect, 3000);
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
connect();
|
|
71
|
+
|
|
72
|
+
return () => {
|
|
73
|
+
if (eventSourceRef.current) {
|
|
74
|
+
eventSourceRef.current.close();
|
|
75
|
+
}
|
|
76
|
+
if (reconnectTimer.current) {
|
|
77
|
+
clearTimeout(reconnectTimer.current);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}, [setSseConnected, setSessions, setMessages]);
|
|
81
|
+
}
|