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,714 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useRef, useEffect, useCallback, useState } from "react";
|
|
4
|
+
import { useStore } from "@/lib/store";
|
|
5
|
+
import { sessionLabel, relativeTime, cleanPreview, copyToClipboard } from "@/lib/client-utils";
|
|
6
|
+
import { SessionInfo } from "@/lib/types";
|
|
7
|
+
import SettingsPanel from "./SettingsPanel";
|
|
8
|
+
import Logo from "./Logo";
|
|
9
|
+
|
|
10
|
+
const ChevronSvg = () => (
|
|
11
|
+
<svg width="9" height="9" viewBox="0 0 16 16" fill="none">
|
|
12
|
+
<path d="M6 3l5 5-5 5" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
|
13
|
+
</svg>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const SearchIcon = () => (
|
|
17
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
18
|
+
<circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1.5" />
|
|
19
|
+
<line x1="11" y1="11" x2="14.5" y2="14.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
20
|
+
</svg>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const GearIcon = () => (
|
|
24
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
|
|
25
|
+
<circle cx="8" cy="8" r="2.5" stroke="currentColor" strokeWidth="1.3" />
|
|
26
|
+
<path d="M8 1v2M8 13v2M1 8h2M13 8h2M3.05 3.05l1.41 1.41M11.54 11.54l1.41 1.41M3.05 12.95l1.41-1.41M11.54 4.46l1.41-1.41" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" />
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const sourceColors: Record<string, string> = {
|
|
31
|
+
kova: "#9B72EF",
|
|
32
|
+
claude: "#3B82F6",
|
|
33
|
+
codex: "#F59E0B",
|
|
34
|
+
kimi: "#06B6D4",
|
|
35
|
+
gemini: "#22C55E",
|
|
36
|
+
copilot: "#52525B",
|
|
37
|
+
factory: "#F97316",
|
|
38
|
+
opencode: "#14B8A6",
|
|
39
|
+
aider: "#EC4899",
|
|
40
|
+
continue: "#8B5CF6",
|
|
41
|
+
cursor: "#6366F1",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const BotSvg = () => (
|
|
45
|
+
<svg viewBox="0 0 14 14" width="12" height="12" fill="none" xmlns="http://www.w3.org/2000/svg" className="bot-icon">
|
|
46
|
+
<rect x="2" y="5" width="10" height="7" rx="1.5" stroke="#22C55E" strokeWidth="1.4"/>
|
|
47
|
+
<rect x="5" y="2" width="4" height="3" rx="1" stroke="#22C55E" strokeWidth="1.4"/>
|
|
48
|
+
<circle cx="5" cy="8.5" r="1" fill="#22C55E"/>
|
|
49
|
+
<circle cx="9" cy="8.5" r="1" fill="#22C55E"/>
|
|
50
|
+
</svg>
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function ContextMenu({
|
|
54
|
+
x,
|
|
55
|
+
y,
|
|
56
|
+
session,
|
|
57
|
+
isArchived,
|
|
58
|
+
onClose,
|
|
59
|
+
}: {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
session: SessionInfo;
|
|
63
|
+
isArchived: boolean;
|
|
64
|
+
onClose: () => void;
|
|
65
|
+
}) {
|
|
66
|
+
const archiveSession = useStore((s) => s.archiveSession);
|
|
67
|
+
const unarchiveSession = useStore((s) => s.unarchiveSession);
|
|
68
|
+
const starredSessionIds = useStore((s) => s.starredSessionIds);
|
|
69
|
+
const toggleStarred = useStore((s) => s.toggleStarred);
|
|
70
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
const handler = (e: MouseEvent) => {
|
|
74
|
+
if (ref.current && !ref.current.contains(e.target as Node)) onClose();
|
|
75
|
+
};
|
|
76
|
+
document.addEventListener("mousedown", handler);
|
|
77
|
+
return () => document.removeEventListener("mousedown", handler);
|
|
78
|
+
}, [onClose]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
ref={ref}
|
|
83
|
+
className="ctx-menu"
|
|
84
|
+
style={{ top: y, left: x }}
|
|
85
|
+
>
|
|
86
|
+
{isArchived ? (
|
|
87
|
+
<button
|
|
88
|
+
className="ctx-menu-item"
|
|
89
|
+
onClick={() => { unarchiveSession(session.sessionId); onClose(); }}
|
|
90
|
+
>
|
|
91
|
+
Unarchive
|
|
92
|
+
</button>
|
|
93
|
+
) : (
|
|
94
|
+
<button
|
|
95
|
+
className="ctx-menu-item"
|
|
96
|
+
onClick={() => { archiveSession(session.sessionId); onClose(); }}
|
|
97
|
+
>
|
|
98
|
+
Archive
|
|
99
|
+
</button>
|
|
100
|
+
)}
|
|
101
|
+
<button
|
|
102
|
+
className="ctx-menu-item"
|
|
103
|
+
onClick={() => { copyToClipboard(session.sessionId, "Session ID copied"); onClose(); }}
|
|
104
|
+
>
|
|
105
|
+
Copy ID
|
|
106
|
+
</button>
|
|
107
|
+
{session.filePath && (
|
|
108
|
+
<button
|
|
109
|
+
className="ctx-menu-item"
|
|
110
|
+
onClick={() => { copyToClipboard(session.filePath!, "Path copied"); onClose(); }}
|
|
111
|
+
>
|
|
112
|
+
Copy path
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function SessionItem({
|
|
120
|
+
session,
|
|
121
|
+
isSubagent,
|
|
122
|
+
childCount,
|
|
123
|
+
hasSubagents,
|
|
124
|
+
isExpanded,
|
|
125
|
+
isSelected,
|
|
126
|
+
isLive,
|
|
127
|
+
isArchived,
|
|
128
|
+
isStarred,
|
|
129
|
+
compact,
|
|
130
|
+
onSelect,
|
|
131
|
+
onToggleExpand,
|
|
132
|
+
onContextMenu,
|
|
133
|
+
onToggleStar,
|
|
134
|
+
}: {
|
|
135
|
+
session: SessionInfo;
|
|
136
|
+
isSubagent: boolean;
|
|
137
|
+
childCount: number;
|
|
138
|
+
hasSubagents: boolean;
|
|
139
|
+
isExpanded: boolean;
|
|
140
|
+
isSelected: boolean;
|
|
141
|
+
isLive: boolean;
|
|
142
|
+
isArchived: boolean;
|
|
143
|
+
isStarred: boolean;
|
|
144
|
+
compact: boolean;
|
|
145
|
+
onSelect: (id: string) => void;
|
|
146
|
+
onToggleExpand: (id: string) => void;
|
|
147
|
+
onContextMenu: (e: React.MouseEvent, session: SessionInfo) => void;
|
|
148
|
+
onToggleStar: (id: string) => void;
|
|
149
|
+
}) {
|
|
150
|
+
const label = sessionLabel(session);
|
|
151
|
+
const src = session.source || "kova";
|
|
152
|
+
const dotCls = isLive
|
|
153
|
+
? "live"
|
|
154
|
+
: isSubagent
|
|
155
|
+
? "subagent"
|
|
156
|
+
: session.isActive && !session.isDeleted
|
|
157
|
+
? "active"
|
|
158
|
+
: "inactive";
|
|
159
|
+
const preview = cleanPreview(session.preview || "");
|
|
160
|
+
|
|
161
|
+
let cls = "session-item";
|
|
162
|
+
if (isSelected) cls += " selected";
|
|
163
|
+
if (isSubagent) cls += " subagent";
|
|
164
|
+
if (compact) cls += " compact";
|
|
165
|
+
if (isArchived) cls += " archived";
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div
|
|
169
|
+
className={cls}
|
|
170
|
+
onClick={() => {
|
|
171
|
+
onSelect(session.sessionId);
|
|
172
|
+
if (childCount > 0) onToggleExpand(session.sessionId);
|
|
173
|
+
}}
|
|
174
|
+
onContextMenu={(e) => onContextMenu(e, session)}
|
|
175
|
+
>
|
|
176
|
+
<div className="session-row">
|
|
177
|
+
{childCount > 0 && (
|
|
178
|
+
<button
|
|
179
|
+
className="expand-btn"
|
|
180
|
+
onClick={(e) => {
|
|
181
|
+
e.stopPropagation();
|
|
182
|
+
onToggleExpand(session.sessionId);
|
|
183
|
+
}}
|
|
184
|
+
title={`${isExpanded ? "Collapse" : "Expand"} ${childCount} subagent${childCount !== 1 ? "s" : ""}`}
|
|
185
|
+
>
|
|
186
|
+
<span className={`expand-chevron ${isExpanded ? "open" : ""}`}>
|
|
187
|
+
<ChevronSvg />
|
|
188
|
+
</span>
|
|
189
|
+
</button>
|
|
190
|
+
)}
|
|
191
|
+
<div className={`session-dot ${dotCls}`} />
|
|
192
|
+
<span className={`session-label ${isSubagent ? "sub" : ""}`}>
|
|
193
|
+
{label}
|
|
194
|
+
</span>
|
|
195
|
+
{isLive && <span className="live-badge-small">live</span>}
|
|
196
|
+
{hasSubagents && <BotSvg />}
|
|
197
|
+
{isSubagent && <span className="subagent-badge">subagent</span>}
|
|
198
|
+
<button
|
|
199
|
+
className={`star-btn ${isStarred ? "starred" : ""}`}
|
|
200
|
+
title={isStarred ? "Unstar" : "Star session"}
|
|
201
|
+
onClick={(e) => { e.stopPropagation(); onToggleStar(session.sessionId); }}
|
|
202
|
+
>
|
|
203
|
+
<svg width="11" height="11" viewBox="0 0 16 16" fill="none">
|
|
204
|
+
{isStarred
|
|
205
|
+
? <path d="M8 1l1.8 3.6 4 .6-2.9 2.8.7 4L8 10l-3.6 1.9.7-4L2.2 5.2l4-.6z" fill="#F59E0B"/>
|
|
206
|
+
: <path d="M8 1l1.8 3.6 4 .6-2.9 2.8.7 4L8 10l-3.6 1.9.7-4L2.2 5.2l4-.6z" stroke="currentColor" strokeWidth="1.2" strokeLinejoin="round"/>
|
|
207
|
+
}
|
|
208
|
+
</svg>
|
|
209
|
+
</button>
|
|
210
|
+
<span className="session-time">
|
|
211
|
+
{relativeTime(session.lastUpdated)}
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
{!compact && (
|
|
215
|
+
<div className="session-meta">
|
|
216
|
+
<span className="session-source" style={{ color: sourceColors[src] || "var(--text-2)" }}>
|
|
217
|
+
{src}
|
|
218
|
+
</span>
|
|
219
|
+
<span className="session-msgs">
|
|
220
|
+
{session.messageCount || 0} msgs
|
|
221
|
+
</span>
|
|
222
|
+
{session.compactionCount > 0 && (
|
|
223
|
+
<span className="session-msgs">
|
|
224
|
+
· {session.compactionCount}×
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
{childCount > 0 && (
|
|
228
|
+
<span className="subagent-count-badge">
|
|
229
|
+
({childCount} subagent{childCount !== 1 ? "s" : ""})
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
{!compact && preview && (
|
|
235
|
+
<div className="session-preview">{preview}</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default function Sidebar() {
|
|
242
|
+
const sessions = useStore((s) => s.sessions);
|
|
243
|
+
const filteredSessions = useStore((s) => s.filteredSessions);
|
|
244
|
+
const currentSessionId = useStore((s) => s.currentSessionId);
|
|
245
|
+
const searchQuery = useStore((s) => s.searchQuery);
|
|
246
|
+
const sourceFilters = useStore((s) => s.sourceFilters);
|
|
247
|
+
const expandedGroups = useStore((s) => s.expandedGroups);
|
|
248
|
+
const expandAllGroups = useStore((s) => s.expandAllGroups);
|
|
249
|
+
const collapseAllGroups = useStore((s) => s.collapseAllGroups);
|
|
250
|
+
const sidebarWidth = useStore((s) => s.sidebarWidth);
|
|
251
|
+
const settingsOpen = useStore((s) => s.settingsOpen);
|
|
252
|
+
const compactSidebar = useStore((s) => s.settings.compactSidebar);
|
|
253
|
+
const sidebarTab = useStore((s) => s.sidebarTab);
|
|
254
|
+
const activeSessions = useStore((s) => s.activeSessions);
|
|
255
|
+
const archivedSessionIds = useStore((s) => s.archivedSessionIds);
|
|
256
|
+
const starredSessionIds = useStore((s) => s.starredSessionIds);
|
|
257
|
+
const toggleStarred = useStore((s) => s.toggleStarred);
|
|
258
|
+
const pinnedMessages = useStore((s) => s.pinnedMessages);
|
|
259
|
+
const setSearchQuery = useStore((s) => s.setSearchQuery);
|
|
260
|
+
const toggleSourceFilter = useStore((s) => s.toggleSourceFilter);
|
|
261
|
+
const toggleGroupExpanded = useStore((s) => s.toggleGroupExpanded);
|
|
262
|
+
const setCurrentSession = useStore((s) => s.setCurrentSession);
|
|
263
|
+
const setSidebarWidth = useStore((s) => s.setSidebarWidth);
|
|
264
|
+
const setSettingsOpen = useStore((s) => s.setSettingsOpen);
|
|
265
|
+
const setSidebarTab = useStore((s) => s.setSidebarTab);
|
|
266
|
+
|
|
267
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
268
|
+
const sidebarRef = useRef<HTMLDivElement>(null);
|
|
269
|
+
const [localSearch, setLocalSearch] = useState(searchQuery);
|
|
270
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
271
|
+
const ftSearchRef = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
272
|
+
const dragging = useRef(false);
|
|
273
|
+
const [ftResults, setFtResults] = useState<{ session: SessionInfo; snippet: string }[]>([]);
|
|
274
|
+
const [ftLoading, setFtLoading] = useState(false);
|
|
275
|
+
const [ftActive, setFtActive] = useState(false);
|
|
276
|
+
const [ctxMenu, setCtxMenu] = useState<{ x: number; y: number; session: SessionInfo } | null>(null);
|
|
277
|
+
|
|
278
|
+
const handleSearchChange = useCallback(
|
|
279
|
+
(value: string) => {
|
|
280
|
+
setLocalSearch(value);
|
|
281
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
282
|
+
debounceRef.current = setTimeout(() => {
|
|
283
|
+
setSearchQuery(value);
|
|
284
|
+
}, 120);
|
|
285
|
+
|
|
286
|
+
// Full-text search for 2+ chars
|
|
287
|
+
if (ftSearchRef.current) clearTimeout(ftSearchRef.current);
|
|
288
|
+
if (value.length >= 2) {
|
|
289
|
+
ftSearchRef.current = setTimeout(() => {
|
|
290
|
+
setFtLoading(true);
|
|
291
|
+
setFtActive(true);
|
|
292
|
+
fetch("/api/search", {
|
|
293
|
+
method: "POST",
|
|
294
|
+
headers: { "Content-Type": "application/json" },
|
|
295
|
+
body: JSON.stringify({ query: value }),
|
|
296
|
+
})
|
|
297
|
+
.then((r) => r.json())
|
|
298
|
+
.then((data) => {
|
|
299
|
+
if (Array.isArray(data)) setFtResults(data);
|
|
300
|
+
setFtLoading(false);
|
|
301
|
+
})
|
|
302
|
+
.catch(() => {
|
|
303
|
+
setFtLoading(false);
|
|
304
|
+
});
|
|
305
|
+
}, 300);
|
|
306
|
+
} else {
|
|
307
|
+
setFtResults([]);
|
|
308
|
+
setFtActive(false);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
[setSearchQuery]
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
// Build parent/child map — use full sessions list for detection
|
|
315
|
+
const childrenOf = new Map<string, SessionInfo[]>();
|
|
316
|
+
const childIds = new Set<string>();
|
|
317
|
+
const parentIds = new Set<string>();
|
|
318
|
+
|
|
319
|
+
// First pass: detect all parent-child relationships from full sessions list
|
|
320
|
+
for (const s of sessions) {
|
|
321
|
+
const isKovaSub = s.key?.startsWith("agent:main:subagent:");
|
|
322
|
+
let parentId = s.parentSessionId;
|
|
323
|
+
if (!parentId && isKovaSub) {
|
|
324
|
+
const main = sessions.find((p) => p.key === "agent:main:main");
|
|
325
|
+
if (main) parentId = main.sessionId;
|
|
326
|
+
}
|
|
327
|
+
if (parentId) {
|
|
328
|
+
parentIds.add(parentId);
|
|
329
|
+
const parentByKey = sessions.find((p) => p.key === parentId);
|
|
330
|
+
if (parentByKey) parentIds.add(parentByKey.sessionId);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Second pass: build children map from filtered sessions only (for display)
|
|
335
|
+
for (const s of filteredSessions) {
|
|
336
|
+
const isKovaSub = s.key?.startsWith("agent:main:subagent:");
|
|
337
|
+
let parentId = s.parentSessionId;
|
|
338
|
+
if (!parentId && isKovaSub) {
|
|
339
|
+
const main = filteredSessions.find((p) => p.key === "agent:main:main");
|
|
340
|
+
if (main) parentId = main.sessionId;
|
|
341
|
+
}
|
|
342
|
+
if (parentId) {
|
|
343
|
+
if (!childrenOf.has(parentId)) childrenOf.set(parentId, []);
|
|
344
|
+
childrenOf.get(parentId)!.push(s);
|
|
345
|
+
childIds.add(s.sessionId);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// hasSubagents: use the direct flag from scanner OR derive from parentSessionId links
|
|
350
|
+
const hasSubagentsSet = new Set<string>();
|
|
351
|
+
for (const s of sessions) {
|
|
352
|
+
if (s.hasSubagents) {
|
|
353
|
+
hasSubagentsSet.add(s.sessionId);
|
|
354
|
+
}
|
|
355
|
+
const pid = s.parentSessionId;
|
|
356
|
+
if (pid) {
|
|
357
|
+
hasSubagentsSet.add(pid);
|
|
358
|
+
for (const p of sessions) {
|
|
359
|
+
if (p.key === pid || p.sessionId === pid) hasSubagentsSet.add(p.sessionId);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (s.key?.startsWith("agent:main:subagent:")) {
|
|
363
|
+
const main = sessions.find((p) => p.key === "agent:main:main");
|
|
364
|
+
if (main) hasSubagentsSet.add(main.sessionId);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Determine which providers have sessions
|
|
369
|
+
const activeSources = new Set<string>();
|
|
370
|
+
for (const s of sessions) {
|
|
371
|
+
activeSources.add(s.source || "kova");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const handleSelect = useCallback(
|
|
375
|
+
(id: string) => {
|
|
376
|
+
setCurrentSession(id);
|
|
377
|
+
},
|
|
378
|
+
[setCurrentSession]
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const handleContextMenu = useCallback(
|
|
382
|
+
(e: React.MouseEvent, session: SessionInfo) => {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
setCtxMenu({ x: e.clientX, y: e.clientY, session });
|
|
385
|
+
},
|
|
386
|
+
[]
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
// Keyboard shortcut
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
392
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
393
|
+
if (mod && e.key === "k") {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
searchRef.current?.focus();
|
|
396
|
+
searchRef.current?.select();
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
document.addEventListener("keydown", handleKey);
|
|
400
|
+
return () => document.removeEventListener("keydown", handleKey);
|
|
401
|
+
}, []);
|
|
402
|
+
|
|
403
|
+
// Drag-to-resize sidebar
|
|
404
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
405
|
+
e.preventDefault();
|
|
406
|
+
dragging.current = true;
|
|
407
|
+
document.body.style.cursor = "col-resize";
|
|
408
|
+
document.body.style.userSelect = "none";
|
|
409
|
+
|
|
410
|
+
const startX = e.clientX;
|
|
411
|
+
const startW = sidebarRef.current?.offsetWidth || 280;
|
|
412
|
+
|
|
413
|
+
const onMove = (ev: MouseEvent) => {
|
|
414
|
+
if (!dragging.current) return;
|
|
415
|
+
const newW = startW + (ev.clientX - startX);
|
|
416
|
+
setSidebarWidth(newW);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const onUp = () => {
|
|
420
|
+
dragging.current = false;
|
|
421
|
+
document.body.style.cursor = "";
|
|
422
|
+
document.body.style.userSelect = "";
|
|
423
|
+
document.removeEventListener("mousemove", onMove);
|
|
424
|
+
document.removeEventListener("mouseup", onUp);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
document.addEventListener("mousemove", onMove);
|
|
428
|
+
document.addEventListener("mouseup", onUp);
|
|
429
|
+
}, [setSidebarWidth]);
|
|
430
|
+
|
|
431
|
+
// Only show filter badges for providers that have sessions
|
|
432
|
+
const allBadges = [
|
|
433
|
+
{ key: "kova", label: "kova" },
|
|
434
|
+
{ key: "claude", label: "claude" },
|
|
435
|
+
{ key: "codex", label: "codex" },
|
|
436
|
+
{ key: "kimi", label: "kimi" },
|
|
437
|
+
{ key: "gemini", label: "gemini" },
|
|
438
|
+
{ key: "copilot", label: "copilot" },
|
|
439
|
+
{ key: "factory", label: "factory" },
|
|
440
|
+
{ key: "opencode", label: "opencode" },
|
|
441
|
+
{ key: "aider", label: "aider" },
|
|
442
|
+
{ key: "continue", label: "continue" },
|
|
443
|
+
{ key: "cursor", label: "cursor" },
|
|
444
|
+
];
|
|
445
|
+
const srcBadges = allBadges.filter(b => activeSources.has(b.key));
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div
|
|
449
|
+
className="sidebar"
|
|
450
|
+
ref={sidebarRef}
|
|
451
|
+
style={{ width: sidebarWidth, minWidth: sidebarWidth }}
|
|
452
|
+
>
|
|
453
|
+
<div className="sidebar-header">
|
|
454
|
+
<div className="sidebar-title-row">
|
|
455
|
+
<Logo />
|
|
456
|
+
<span className="sidebar-count">{sessions.length}</span>
|
|
457
|
+
</div>
|
|
458
|
+
|
|
459
|
+
<div className="search-wrap">
|
|
460
|
+
<span className="search-icon"><SearchIcon /></span>
|
|
461
|
+
<input
|
|
462
|
+
ref={searchRef}
|
|
463
|
+
type="text"
|
|
464
|
+
placeholder="Search sessions (2+ chars: full-text)"
|
|
465
|
+
autoComplete="off"
|
|
466
|
+
spellCheck={false}
|
|
467
|
+
value={localSearch}
|
|
468
|
+
onChange={(e) => handleSearchChange(e.target.value)}
|
|
469
|
+
onKeyDown={(e) => {
|
|
470
|
+
if (e.key === "Escape") {
|
|
471
|
+
setLocalSearch("");
|
|
472
|
+
setSearchQuery("");
|
|
473
|
+
setFtResults([]);
|
|
474
|
+
setFtActive(false);
|
|
475
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
476
|
+
if (ftSearchRef.current) clearTimeout(ftSearchRef.current);
|
|
477
|
+
searchRef.current?.blur();
|
|
478
|
+
}
|
|
479
|
+
}}
|
|
480
|
+
className="search-input"
|
|
481
|
+
/>
|
|
482
|
+
<span className="search-kbd">⌘K</span>
|
|
483
|
+
</div>
|
|
484
|
+
|
|
485
|
+
<div className="source-filters">
|
|
486
|
+
{srcBadges.map(({ key, label }) => (
|
|
487
|
+
<label key={key} className="source-filter">
|
|
488
|
+
<input
|
|
489
|
+
type="checkbox"
|
|
490
|
+
checked={sourceFilters[key] !== false}
|
|
491
|
+
onChange={() => toggleSourceFilter(key)}
|
|
492
|
+
/>
|
|
493
|
+
<span
|
|
494
|
+
className={`source-filter-label ${sourceFilters[key] !== false ? "active" : "inactive"}`}
|
|
495
|
+
data-source={key}
|
|
496
|
+
>
|
|
497
|
+
{label}
|
|
498
|
+
</span>
|
|
499
|
+
</label>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
{/* Browse / Starred / Pinned / Archived / Analytics tabs */}
|
|
504
|
+
<div className="sidebar-tabs">
|
|
505
|
+
<button
|
|
506
|
+
className={`sidebar-tab ${sidebarTab === "browse" ? "active" : ""}`}
|
|
507
|
+
onClick={() => setSidebarTab("browse")}
|
|
508
|
+
>browse</button>
|
|
509
|
+
<button
|
|
510
|
+
className={`sidebar-tab ${sidebarTab === "starred" ? "active" : ""}`}
|
|
511
|
+
onClick={() => setSidebarTab("starred")}
|
|
512
|
+
>
|
|
513
|
+
<svg width="9" height="9" viewBox="0 0 16 16" fill="none" style={{ marginRight: 3, verticalAlign: -1 }}>
|
|
514
|
+
<path d="M8 1l1.8 3.6 4 .6-2.9 2.8.7 4L8 10l-3.6 1.9.7-4L2.2 5.2l4-.6z"
|
|
515
|
+
fill={sidebarTab === "starred" ? "#F59E0B" : "none"}
|
|
516
|
+
stroke={sidebarTab === "starred" ? "#F59E0B" : "currentColor"}
|
|
517
|
+
strokeWidth="1.2" strokeLinejoin="round"/>
|
|
518
|
+
</svg>
|
|
519
|
+
starred
|
|
520
|
+
{starredSessionIds.size > 0 && (
|
|
521
|
+
<span className="sidebar-tab-count">{starredSessionIds.size}</span>
|
|
522
|
+
)}
|
|
523
|
+
</button>
|
|
524
|
+
<button
|
|
525
|
+
className={`sidebar-tab ${sidebarTab === "pinned" ? "active" : ""}`}
|
|
526
|
+
onClick={() => setSidebarTab("pinned")}
|
|
527
|
+
>
|
|
528
|
+
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" style={{ marginRight: 3, verticalAlign: -1 }}>
|
|
529
|
+
<path d="M5 17H19V13L17 5H7L5 13V17Z"
|
|
530
|
+
fill={sidebarTab === "pinned" ? "#9B72EF" : "none"}
|
|
531
|
+
stroke={sidebarTab === "pinned" ? "#9B72EF" : "currentColor"}
|
|
532
|
+
strokeWidth="1.4" strokeLinejoin="round"/>
|
|
533
|
+
<line x1="5" y1="9" x2="19" y2="9" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
|
|
534
|
+
<line x1="12" y1="17" x2="12" y2="22" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
|
|
535
|
+
</svg>
|
|
536
|
+
pinned
|
|
537
|
+
{Object.values(pinnedMessages).filter(v => v.length > 0).length > 0 && (
|
|
538
|
+
<span className="sidebar-tab-count">{Object.values(pinnedMessages).filter(v => v.length > 0).length}</span>
|
|
539
|
+
)}
|
|
540
|
+
</button>
|
|
541
|
+
<button
|
|
542
|
+
className={`sidebar-tab ${sidebarTab === "archived" ? "active" : ""}`}
|
|
543
|
+
onClick={() => setSidebarTab("archived")}
|
|
544
|
+
>
|
|
545
|
+
archived
|
|
546
|
+
{archivedSessionIds.size > 0 && (
|
|
547
|
+
<span className="sidebar-tab-count">{archivedSessionIds.size}</span>
|
|
548
|
+
)}
|
|
549
|
+
</button>
|
|
550
|
+
<button
|
|
551
|
+
className={`sidebar-tab ${sidebarTab === "analytics" ? "active" : ""}`}
|
|
552
|
+
onClick={() => setSidebarTab("analytics")}
|
|
553
|
+
>analytics</button>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
{settingsOpen ? (
|
|
558
|
+
<SettingsPanel />
|
|
559
|
+
) : ftActive ? (
|
|
560
|
+
<div className="session-list scroller">
|
|
561
|
+
{ftLoading ? (
|
|
562
|
+
<div className="session-list-empty">
|
|
563
|
+
<div className="spinner" style={{ width: 12, height: 12 }} /> Searching…
|
|
564
|
+
</div>
|
|
565
|
+
) : ftResults.length === 0 ? (
|
|
566
|
+
<div className="session-list-empty">No content matches</div>
|
|
567
|
+
) : (
|
|
568
|
+
ftResults.map(({ session: s, snippet }) => {
|
|
569
|
+
const src = s.source || "kova";
|
|
570
|
+
return (
|
|
571
|
+
<div
|
|
572
|
+
key={s.sessionId}
|
|
573
|
+
className={`session-item ft-result ${currentSessionId === s.sessionId ? "selected" : ""}`}
|
|
574
|
+
onClick={() => handleSelect(s.sessionId)}
|
|
575
|
+
>
|
|
576
|
+
<div className="session-row">
|
|
577
|
+
<div className={`session-dot ${s.isActive ? "active" : "inactive"}`} />
|
|
578
|
+
<span className="session-label">{sessionLabel(s)}</span>
|
|
579
|
+
<span className="session-source" style={{ color: sourceColors[src] || "var(--text-2)" }}>{src}</span>
|
|
580
|
+
</div>
|
|
581
|
+
<div className="ft-snippet">{snippet}</div>
|
|
582
|
+
</div>
|
|
583
|
+
);
|
|
584
|
+
})
|
|
585
|
+
)}
|
|
586
|
+
</div>
|
|
587
|
+
) : (
|
|
588
|
+
<div className="session-list scroller">
|
|
589
|
+
{filteredSessions.length === 0 ? (
|
|
590
|
+
<div className="session-list-empty">
|
|
591
|
+
{searchQuery ? "No matches" : sidebarTab === "archived" ? "No archived sessions" : sidebarTab === "starred" ? "No starred sessions" : sidebarTab === "pinned" ? "No sessions with pins" : "No sessions"}
|
|
592
|
+
</div>
|
|
593
|
+
) : (
|
|
594
|
+
filteredSessions.map((s) => {
|
|
595
|
+
if (childIds.has(s.sessionId)) return null;
|
|
596
|
+
// Tab-specific filters
|
|
597
|
+
if (sidebarTab === "starred" && !starredSessionIds.has(s.sessionId)) return null;
|
|
598
|
+
if (sidebarTab === "pinned" && !(pinnedMessages[s.sessionId]?.length > 0)) return null;
|
|
599
|
+
const children = childrenOf.get(s.sessionId) || [];
|
|
600
|
+
const isExpanded = expandedGroups.has(s.sessionId);
|
|
601
|
+
const isLive = activeSessions.has(s.sessionId);
|
|
602
|
+
const isArchived = archivedSessionIds.has(s.sessionId);
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
<div key={s.sessionId}>
|
|
606
|
+
<SessionItem
|
|
607
|
+
session={s}
|
|
608
|
+
isSubagent={false}
|
|
609
|
+
childCount={children.length}
|
|
610
|
+
hasSubagents={hasSubagentsSet.has(s.sessionId)}
|
|
611
|
+
isExpanded={isExpanded}
|
|
612
|
+
isSelected={currentSessionId === s.sessionId}
|
|
613
|
+
isLive={isLive}
|
|
614
|
+
isArchived={isArchived}
|
|
615
|
+
isStarred={starredSessionIds.has(s.sessionId)}
|
|
616
|
+
compact={compactSidebar}
|
|
617
|
+
onSelect={handleSelect}
|
|
618
|
+
onToggleExpand={toggleGroupExpanded}
|
|
619
|
+
onContextMenu={handleContextMenu}
|
|
620
|
+
onToggleStar={toggleStarred}
|
|
621
|
+
/>
|
|
622
|
+
{children.length > 0 && isExpanded && (
|
|
623
|
+
<div className="subagent-children">
|
|
624
|
+
{children.map((c) => (
|
|
625
|
+
<SessionItem
|
|
626
|
+
key={c.sessionId}
|
|
627
|
+
session={c}
|
|
628
|
+
isSubagent={true}
|
|
629
|
+
childCount={0}
|
|
630
|
+
hasSubagents={hasSubagentsSet.has(c.sessionId)}
|
|
631
|
+
isExpanded={false}
|
|
632
|
+
isSelected={currentSessionId === c.sessionId}
|
|
633
|
+
isLive={activeSessions.has(c.sessionId)}
|
|
634
|
+
isArchived={archivedSessionIds.has(c.sessionId)}
|
|
635
|
+
isStarred={starredSessionIds.has(c.sessionId)}
|
|
636
|
+
compact={compactSidebar}
|
|
637
|
+
onSelect={handleSelect}
|
|
638
|
+
onToggleExpand={toggleGroupExpanded}
|
|
639
|
+
onContextMenu={handleContextMenu}
|
|
640
|
+
onToggleStar={toggleStarred}
|
|
641
|
+
/>
|
|
642
|
+
))}
|
|
643
|
+
</div>
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
646
|
+
);
|
|
647
|
+
})
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
)}
|
|
651
|
+
|
|
652
|
+
{/* Context menu */}
|
|
653
|
+
{ctxMenu && (
|
|
654
|
+
<ContextMenu
|
|
655
|
+
x={ctxMenu.x}
|
|
656
|
+
y={ctxMenu.y}
|
|
657
|
+
session={ctxMenu.session}
|
|
658
|
+
isArchived={archivedSessionIds.has(ctxMenu.session.sessionId)}
|
|
659
|
+
onClose={() => setCtxMenu(null)}
|
|
660
|
+
/>
|
|
661
|
+
)}
|
|
662
|
+
|
|
663
|
+
{/* Footer: expand/collapse all + settings */}
|
|
664
|
+
<div className="sidebar-footer">
|
|
665
|
+
<div className="sidebar-footer-group">
|
|
666
|
+
<button
|
|
667
|
+
className="sidebar-footer-btn"
|
|
668
|
+
onClick={expandAllGroups}
|
|
669
|
+
title="Expand all subagent groups"
|
|
670
|
+
>
|
|
671
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
672
|
+
<path d="M3 5l5 5 5-5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
|
|
673
|
+
<path d="M3 2h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
|
|
674
|
+
</svg>
|
|
675
|
+
<span>expand all</span>
|
|
676
|
+
</button>
|
|
677
|
+
<button
|
|
678
|
+
className="sidebar-footer-btn"
|
|
679
|
+
onClick={collapseAllGroups}
|
|
680
|
+
title="Collapse all subagent groups"
|
|
681
|
+
>
|
|
682
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
683
|
+
<path d="M3 11l5-5 5 5" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
|
|
684
|
+
<path d="M3 14h10" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
|
|
685
|
+
</svg>
|
|
686
|
+
<span>collapse all</span>
|
|
687
|
+
</button>
|
|
688
|
+
</div>
|
|
689
|
+
<button
|
|
690
|
+
className={`sidebar-settings-btn ${settingsOpen ? "active" : ""}`}
|
|
691
|
+
onClick={() => setSettingsOpen(!settingsOpen)}
|
|
692
|
+
title="Settings"
|
|
693
|
+
>
|
|
694
|
+
<GearIcon />
|
|
695
|
+
<span>settings</span>
|
|
696
|
+
</button>
|
|
697
|
+
<button
|
|
698
|
+
className="sidebar-settings-btn"
|
|
699
|
+
onClick={() => { window.location.href = "/?setup=1"; }}
|
|
700
|
+
title="Configure agent paths"
|
|
701
|
+
>
|
|
702
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="none">
|
|
703
|
+
<circle cx="8" cy="8" r="5.5" stroke="currentColor" strokeWidth="1.3"/>
|
|
704
|
+
<path d="M8 5v3l2 2" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" strokeLinejoin="round"/>
|
|
705
|
+
</svg>
|
|
706
|
+
<span>agents</span>
|
|
707
|
+
</button>
|
|
708
|
+
</div>
|
|
709
|
+
|
|
710
|
+
{/* Drag handle */}
|
|
711
|
+
<div className="sidebar-resize-handle" onMouseDown={handleMouseDown} />
|
|
712
|
+
</div>
|
|
713
|
+
);
|
|
714
|
+
}
|