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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/bin/llm-deep-trace.js +24 -0
  4. package/next.config.ts +8 -0
  5. package/package.json +56 -0
  6. package/postcss.config.mjs +5 -0
  7. package/public/banner-v2.png +0 -0
  8. package/public/file.svg +1 -0
  9. package/public/globe.svg +1 -0
  10. package/public/logo.png +0 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/agent-config/route.ts +31 -0
  15. package/src/app/api/all-sessions/route.ts +9 -0
  16. package/src/app/api/analytics/route.ts +379 -0
  17. package/src/app/api/detect-agents/route.ts +170 -0
  18. package/src/app/api/image/route.ts +73 -0
  19. package/src/app/api/search/route.ts +28 -0
  20. package/src/app/api/session-by-key/route.ts +21 -0
  21. package/src/app/api/sessions/[sessionId]/messages/route.ts +46 -0
  22. package/src/app/api/sse/route.ts +86 -0
  23. package/src/app/favicon.ico +0 -0
  24. package/src/app/globals.css +3518 -0
  25. package/src/app/icon.svg +4 -0
  26. package/src/app/layout.tsx +20 -0
  27. package/src/app/page.tsx +5 -0
  28. package/src/components/AnalyticsDashboard.tsx +393 -0
  29. package/src/components/App.tsx +243 -0
  30. package/src/components/CopyButton.tsx +42 -0
  31. package/src/components/Logo.tsx +20 -0
  32. package/src/components/MainPanel.tsx +1128 -0
  33. package/src/components/MessageRenderer.tsx +983 -0
  34. package/src/components/SessionTree.tsx +505 -0
  35. package/src/components/SettingsPanel.tsx +160 -0
  36. package/src/components/SetupView.tsx +206 -0
  37. package/src/components/Sidebar.tsx +714 -0
  38. package/src/components/ThemeToggle.tsx +54 -0
  39. package/src/lib/client-utils.ts +360 -0
  40. package/src/lib/normalizers.ts +371 -0
  41. package/src/lib/sessions.ts +1223 -0
  42. package/src/lib/store.ts +518 -0
  43. package/src/lib/types.ts +112 -0
  44. package/src/lib/useSSE.ts +81 -0
  45. package/tsconfig.json +34 -0
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { useStore } from "@/lib/store";
4
+
5
+ const SystemIcon = () => (
6
+ <svg width="11" height="11" viewBox="0 0 16 16" fill="none">
7
+ <rect x="1" y="2" width="14" height="10" rx="1.5" stroke="currentColor" strokeWidth="1.4" />
8
+ <path d="M5 14h6M8 12v2" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" />
9
+ </svg>
10
+ );
11
+
12
+ const DarkIcon = () => (
13
+ <svg width="11" height="11" viewBox="0 0 16 16" fill="none">
14
+ <path d="M13.5 10.5A6 6 0 115.5 2.5a5 5 0 008 8z" fill="currentColor" />
15
+ </svg>
16
+ );
17
+
18
+ const LightIcon = () => (
19
+ <svg width="11" height="11" viewBox="0 0 16 16" fill="none">
20
+ <circle cx="8" cy="8" r="3.5" stroke="currentColor" strokeWidth="1.4" />
21
+ <path
22
+ 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"
23
+ stroke="currentColor"
24
+ strokeWidth="1.3"
25
+ strokeLinecap="round"
26
+ />
27
+ </svg>
28
+ );
29
+
30
+ export default function ThemeToggle() {
31
+ const theme = useStore((s) => s.theme);
32
+ const setTheme = useStore((s) => s.setTheme);
33
+
34
+ const buttons = [
35
+ { key: "system", icon: <SystemIcon />, title: "System default" },
36
+ { key: "dark", icon: <DarkIcon />, title: "Dark" },
37
+ { key: "light", icon: <LightIcon />, title: "Light" },
38
+ ];
39
+
40
+ return (
41
+ <div className="theme-toggle" title="Switch theme">
42
+ {buttons.map(({ key, icon, title }) => (
43
+ <button
44
+ key={key}
45
+ onClick={() => setTheme(key)}
46
+ title={title}
47
+ className={`theme-btn ${theme === key ? "active" : "inactive"}`}
48
+ >
49
+ {icon}
50
+ </button>
51
+ ))}
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,360 @@
1
+ import { marked } from "marked";
2
+ import hljs from "highlight.js";
3
+
4
+ // Configure marked
5
+ marked.setOptions({
6
+ breaks: true,
7
+ gfm: true,
8
+ });
9
+
10
+ // Custom renderer for code highlighting
11
+ const renderer = new marked.Renderer();
12
+ renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
13
+ let highlighted = text;
14
+ if (lang && hljs.getLanguage(lang)) {
15
+ try {
16
+ highlighted = hljs.highlight(text, { language: lang }).value;
17
+ } catch {
18
+ // fallback
19
+ }
20
+ } else {
21
+ try {
22
+ highlighted = hljs.highlightAuto(text).value;
23
+ } catch {
24
+ // fallback
25
+ }
26
+ }
27
+ return `<pre><code class="hljs${lang ? " language-" + lang : ""}">${highlighted}</code></pre>`;
28
+ };
29
+ marked.use({ renderer });
30
+
31
+ export function esc(text: string): string {
32
+ if (!text) return "";
33
+ const el = document.createElement("span");
34
+ el.textContent = text;
35
+ return el.innerHTML;
36
+ }
37
+
38
+ export function relativeTime(ts: number): string {
39
+ if (!ts) return "";
40
+ const diff = Date.now() - ts;
41
+ const s = Math.floor(diff / 1000);
42
+ if (s < 60) return "just now";
43
+ const m = Math.floor(s / 60);
44
+ if (m < 60) return m + "m";
45
+ const h = Math.floor(m / 60);
46
+ if (h < 24) return h + "h";
47
+ const d = Math.floor(h / 24);
48
+ if (d === 1) return "yesterday";
49
+ if (d < 30) return d + "d";
50
+ return new Date(ts).toLocaleDateString();
51
+ }
52
+
53
+ export function fmtTime(ts: string | undefined): string {
54
+ if (!ts) return "";
55
+ try {
56
+ return new Date(ts).toLocaleTimeString([], {
57
+ hour: "2-digit",
58
+ minute: "2-digit",
59
+ second: "2-digit",
60
+ });
61
+ } catch {
62
+ return "";
63
+ }
64
+ }
65
+
66
+ export function fileExt(filepath: string): string {
67
+ if (!filepath) return "";
68
+ const m = filepath.match(/\.(\w+)$/);
69
+ return m ? m[1] : "";
70
+ }
71
+
72
+ export function extToLang(ext: string): string {
73
+ const map: Record<string, string> = {
74
+ js: "javascript", ts: "typescript", tsx: "typescript", jsx: "javascript",
75
+ py: "python", rb: "ruby", rs: "rust", go: "go", java: "java",
76
+ c: "c", cpp: "cpp", h: "c", hpp: "cpp", cs: "csharp",
77
+ sh: "bash", bash: "bash", zsh: "bash", fish: "bash",
78
+ json: "json", yaml: "yaml", yml: "yaml", toml: "toml",
79
+ xml: "xml", html: "html", css: "css", scss: "scss",
80
+ md: "markdown", sql: "sql",
81
+ tf: "hcl", hcl: "hcl", lua: "lua",
82
+ swift: "swift", kt: "kotlin", scala: "scala",
83
+ r: "r", php: "php", pl: "perl", ex: "elixir", exs: "elixir",
84
+ };
85
+ return map[(ext || "").toLowerCase()] || ext;
86
+ }
87
+
88
+ export function truncStr(text: string, max: number): { t: string; trunc: boolean } {
89
+ if (!text) return { t: "", trunc: false };
90
+ if (text.length <= max) return { t: text, trunc: false };
91
+ return { t: text.slice(0, max), trunc: true };
92
+ }
93
+
94
+ export function stripConversationMeta(text: string): string {
95
+ if (!text) return text;
96
+ if (!text.startsWith("Conversation info")) return text;
97
+ const idx = text.indexOf("```\n\n");
98
+ if (idx !== -1) return text.slice(idx + 5).trim();
99
+ const idx2 = text.indexOf("```\n");
100
+ if (idx2 !== -1) return text.slice(idx2 + 4).trim();
101
+ return text;
102
+ }
103
+
104
+ export function renderMarkdown(rawText: string): string {
105
+ if (!rawText) return "";
106
+ try {
107
+ return marked.parse(rawText) as string;
108
+ } catch {
109
+ return esc(rawText).replace(/\n/g, "<br>");
110
+ }
111
+ }
112
+
113
+ export function highlightCode(code: string, lang: string): string {
114
+ if (lang && hljs.getLanguage(lang)) {
115
+ try {
116
+ return hljs.highlight(code, { language: lang }).value;
117
+ } catch {
118
+ // fallback
119
+ }
120
+ }
121
+ return esc(code);
122
+ }
123
+
124
+ export function syntaxHighlightJson(json: string): string {
125
+ const escaped = esc(json);
126
+ return escaped.replace(
127
+ /("(?:[^"\\]|\\.)*")(\s*:)|("(?:[^"\\]|\\.)*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b)|(\bnull\b)/g,
128
+ (match, key, colon, str, num, bool, nil) => {
129
+ if (key && colon) return `<span class="jk">${key}</span>${colon}`;
130
+ if (str) return `<span class="js">${str}</span>`;
131
+ if (num !== undefined && num !== "") return `<span class="jn">${num}</span>`;
132
+ if (bool) return `<span class="jb">${bool}</span>`;
133
+ if (nil) return `<span class="jnl">${nil}</span>`;
134
+ return match;
135
+ }
136
+ );
137
+ }
138
+
139
+ export function extractText(content: unknown): string {
140
+ if (!content) return "";
141
+ if (typeof content === "string") return content;
142
+ if (Array.isArray(content)) {
143
+ return content
144
+ .filter((b) => b && b.type === "text")
145
+ .map((b) => b.text || "")
146
+ .join("\n");
147
+ }
148
+ return "";
149
+ }
150
+
151
+ export function extractResultText(content: unknown): string {
152
+ if (!content) return "";
153
+ if (typeof content === "string") return content;
154
+ if (Array.isArray(content)) {
155
+ return content
156
+ .map((b) => {
157
+ if (!b) return "";
158
+ if (typeof b === "string") return b;
159
+ if (b.type === "text") return b.text || "";
160
+ if (b.content)
161
+ return typeof b.content === "string"
162
+ ? b.content
163
+ : JSON.stringify(b.content);
164
+ return JSON.stringify(b);
165
+ })
166
+ .join("\n");
167
+ }
168
+ if (typeof content === "object" && content !== null) {
169
+ const obj = content as Record<string, unknown>;
170
+ return (obj.text as string) || (obj.content as string) || JSON.stringify(content);
171
+ }
172
+ return String(content);
173
+ }
174
+
175
+ export function looksLikeMarkdown(text: string): boolean {
176
+ if (text.length < 200) return false;
177
+ return (
178
+ /^#{1,6} /m.test(text) ||
179
+ /^\|.+\|/m.test(text) ||
180
+ /\*\*.+?\*\*/m.test(text) ||
181
+ /^---/m.test(text)
182
+ );
183
+ }
184
+
185
+ export function cleanPreview(text: string): string {
186
+ if (!text) return "";
187
+ return text
188
+ .replace(/^#{1,6}\s+/gm, "")
189
+ .replace(/\*\*(.+?)\*\*/g, "$1")
190
+ .replace(/\*(.+?)\*/g, "$1")
191
+ .replace(/`{1,3}[^`]*`{1,3}/g, "")
192
+ .replace(/!\[.*?\]\(.*?\)/g, "")
193
+ .replace(/\[(.+?)\]\(.*?\)/g, "$1")
194
+ .replace(/^[-*+]\s+/gm, "")
195
+ .replace(/^\d+\.\s+/gm, "")
196
+ .replace(/^>\s+/gm, "")
197
+ .replace(/[\u{1F000}-\u{1FFFF}]/gu, "")
198
+ .replace(/[\u2600-\u27BF\uFE00-\uFE0F]/g, "")
199
+ .replace(/\s{2,}/g, " ")
200
+ .replace(/\n+/g, " ")
201
+ .trim();
202
+ }
203
+
204
+ // Simple Icons brand SVGs (CC0) — all 24×24 viewBox, rendered at 12×12
205
+ const CHANNEL_SVGS: Record<string, { path: string; color: string }> = {
206
+ telegram: {
207
+ color: "#26A5E4",
208
+ path: "M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z",
209
+ },
210
+ whatsapp: {
211
+ color: "#25D366",
212
+ path: "M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z",
213
+ },
214
+ discord: {
215
+ color: "#5865F2",
216
+ path: "M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z",
217
+ },
218
+ signal: {
219
+ color: "#3A76F0",
220
+ path: "M12 0q-.934 0-1.83.139l.17 1.111a11 11 0 0 1 3.32 0l.172-1.111A12 12 0 0 0 12 0M9.152.34A12 12 0 0 0 5.77 1.742l.584.961a10.8 10.8 0 0 1 3.066-1.27zm5.696 0-.268 1.094a10.8 10.8 0 0 1 3.066 1.27l.584-.962A12 12 0 0 0 14.848.34M12 2.25a9.75 9.75 0 0 0-8.539 14.459c.074.134.1.292.064.441l-1.013 4.338 4.338-1.013a.62.62 0 0 1 .441.064A9.7 9.7 0 0 0 12 21.75c5.385 0 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25m-7.092.068a12 12 0 0 0-2.59 2.59l.909.664a11 11 0 0 1 2.345-2.345zm14.184 0-.664.909a11 11 0 0 1 2.345 2.345l.909-.664a12 12 0 0 0-2.59-2.59M1.742 5.77A12 12 0 0 0 .34 9.152l1.094.268a10.8 10.8 0 0 1 1.269-3.066zm20.516 0-.961.584a10.8 10.8 0 0 1 1.27 3.066l1.093-.268a12 12 0 0 0-1.402-3.383M.138 10.168A12 12 0 0 0 0 12q0 .934.139 1.83l1.111-.17A11 11 0 0 1 1.125 12q0-.848.125-1.66zm23.723.002-1.111.17q.125.812.125 1.66c0 .848-.042 1.12-.125 1.66l1.111.172a12.1 12.1 0 0 0 0-3.662M1.434 14.58l-1.094.268a12 12 0 0 0 .96 2.591l-.265 1.14 1.096.255.36-1.539-.188-.365a10.8 10.8 0 0 1-.87-2.35m21.133 0a10.8 10.8 0 0 1-1.27 3.067l.962.584a12 12 0 0 0 1.402-3.383zm-1.793 3.848a11 11 0 0 1-2.345 2.345l.664.909a12 12 0 0 0 2.59-2.59zm-19.959 1.1L.357 21.48a1.8 1.8 0 0 0 2.162 2.161l1.954-.455-.256-1.095-1.953.455a.675.675 0 0 1-.81-.81l.454-1.954zm16.832 1.769a10.8 10.8 0 0 1-3.066 1.27l.268 1.093a12 12 0 0 0 3.382-1.402zm-10.94.213-1.54.36.256 1.095 1.139-.266c.814.415 1.683.74 2.591.961l.268-1.094a10.8 10.8 0 0 1-2.35-.869zm3.634 1.24-.172 1.111a12.1 12.1 0 0 0 3.662 0l-.17-1.111q-.812.125-1.66.125a11 11 0 0 1-1.66-.125",
221
+ },
222
+ slack: {
223
+ color: "#4A154B",
224
+ path: "M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z",
225
+ },
226
+ imessage: {
227
+ color: "#1FCA41",
228
+ path: "M5.285 0A5.273 5.273 0 0 0 0 5.285v13.43A5.273 5.273 0 0 0 5.285 24h13.43A5.273 5.273 0 0 0 24 18.715V5.285A5.273 5.273 0 0 0 18.715 0ZM12 4.154a8.809 7.337 0 0 1 8.809 7.338A8.809 7.337 0 0 1 12 18.828a8.809 7.337 0 0 1-2.492-.303A8.656 7.337 0 0 1 5.93 19.93a9.929 7.337 0 0 0 1.54-2.155 8.809 7.337 0 0 1-4.279-6.283A8.809 7.337 0 0 1 12 4.154",
229
+ },
230
+ googlechat: {
231
+ color: "#00AC47",
232
+ path: "M1.637 0C.733 0 0 .733 0 1.637v16.5c0 .904.733 1.636 1.637 1.636h3.955v3.323c0 .804.97 1.207 1.539.638l3.963-3.96h11.27c.903 0 1.636-.733 1.636-1.637V5.592L18.408 0Zm3.955 5.592h12.816v8.59H8.455l-2.863 2.863Z",
233
+ },
234
+ };
235
+
236
+ function brandSvg(ch: string): string {
237
+ const icon = CHANNEL_SVGS[ch];
238
+ if (!icon) return "";
239
+ return `<svg width="12" height="12" viewBox="0 0 24 24" fill="${icon.color}" style="vertical-align:-2px;flex-shrink:0"><path d="${icon.path}"/></svg>`;
240
+ }
241
+
242
+ export function channelIcon(ch: string): string {
243
+ if (!ch) return "";
244
+ if (CHANNEL_SVGS[ch]) return brandSvg(ch);
245
+ if (ch.startsWith("codex"))
246
+ return `<svg width="10" height="10" viewBox="0 0 16 16" fill="none" style="vertical-align:-1px"><path d="M5 3l6 5-6 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
247
+ if (ch === "claude-code")
248
+ return `<svg width="10" height="10" viewBox="0 0 16 16" fill="none" style="vertical-align:-1px"><circle cx="8" cy="8" r="5" stroke="currentColor" stroke-width="1.5"/><circle cx="8" cy="8" r="2" fill="currentColor"/></svg>`;
249
+ return "";
250
+ }
251
+
252
+ export function sessionLabel(s: {
253
+ key: string;
254
+ label?: string;
255
+ title?: string;
256
+ sessionId: string;
257
+ source?: string;
258
+ preview?: string;
259
+ cwd?: string;
260
+ isSubagent?: boolean;
261
+ }): string {
262
+ const src = s.source || "kova";
263
+
264
+ // Kova/OpenClaw sessions
265
+ if (src === "kova") {
266
+ if (s.key === "agent:main:main") return "main session";
267
+ if (s.title) return s.title;
268
+ if (s.label) return s.label;
269
+ if (s.preview) return s.preview.slice(0, 60);
270
+ if (s.key) {
271
+ const parts = s.key.split(":");
272
+ if (parts.length > 2) return parts.slice(1).join(":");
273
+ return s.key;
274
+ }
275
+ return s.sessionId.slice(0, 14) + "\u2026";
276
+ }
277
+
278
+ // Claude Code sessions
279
+ if (src === "claude") {
280
+ if (s.isSubagent) {
281
+ return "subagent " + s.sessionId.slice(0, 8);
282
+ }
283
+ const slug = s.label || s.key || "";
284
+ let project = slug;
285
+ if (slug.startsWith("~/")) {
286
+ const parts = slug.split("/");
287
+ project = parts[parts.length - 1] || parts[parts.length - 2] || slug;
288
+ } else if (slug.startsWith("-") || slug.includes("-")) {
289
+ const parts = slug.replace(/^-/, "").split(/[-/]/);
290
+ project = parts[parts.length - 1] || slug;
291
+ }
292
+ if (s.preview && project) {
293
+ const snippet = s.preview.slice(0, 40).replace(/\n/g, " ");
294
+ return `${project}: ${snippet}`;
295
+ }
296
+ return project || s.sessionId.slice(0, 14) + "\u2026";
297
+ }
298
+
299
+ // Codex sessions
300
+ if (src === "codex") {
301
+ if (s.cwd) {
302
+ const base = s.cwd.split("/").pop() || s.cwd;
303
+ if (s.preview) {
304
+ return `${base}: ${s.preview.slice(0, 40).replace(/\n/g, " ")}`;
305
+ }
306
+ return base;
307
+ }
308
+ if (s.label) return s.label;
309
+ return s.sessionId.slice(0, 14) + "\u2026";
310
+ }
311
+
312
+ // Fallback
313
+ if (s.label) return s.label;
314
+ if (s.key) {
315
+ const parts = s.key.split(":");
316
+ if (parts.length > 2) return parts.slice(1).join(":");
317
+ return s.key;
318
+ }
319
+ return s.sessionId.slice(0, 14) + "\u2026";
320
+ }
321
+
322
+ /** Map tool name to its block color category key */
323
+ export function toolColorKey(name: string): string {
324
+ switch (name) {
325
+ case "exec": case "Bash": return "exec";
326
+ case "read": case "Read": case "write": case "Write": case "edit": case "Edit": case "Glob": case "Grep": return "file";
327
+ case "web_search": case "WebSearch": case "web_fetch": case "WebFetch": return "web";
328
+ case "browser": case "Browser": return "browser";
329
+ case "message": case "Message": case "SendMessage": return "msg";
330
+ case "sessions_spawn": case "Task": case "task": return "agent";
331
+ default: return "";
332
+ }
333
+ }
334
+
335
+ export function copyToClipboard(text: string, label?: string): Promise<void> {
336
+ return navigator.clipboard.writeText(text).catch(() => {
337
+ const ta = document.createElement("textarea");
338
+ ta.value = text;
339
+ ta.style.cssText = "position:fixed;opacity:0;top:0;left:0;";
340
+ document.body.appendChild(ta);
341
+ ta.select();
342
+ try {
343
+ document.execCommand("copy");
344
+ } catch {
345
+ // ignore
346
+ }
347
+ document.body.removeChild(ta);
348
+ });
349
+ }
350
+
351
+ export function debounce<T extends (...args: unknown[]) => void>(
352
+ fn: T,
353
+ ms: number
354
+ ): (...args: Parameters<T>) => void {
355
+ let t: ReturnType<typeof setTimeout>;
356
+ return (...args: Parameters<T>) => {
357
+ clearTimeout(t);
358
+ t = setTimeout(() => fn(...args), ms);
359
+ };
360
+ }