@townco/ui 0.1.41 → 0.1.42
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/dist/gui/components/Button.d.ts +1 -1
- package/dist/gui/components/ChatView.js +3 -3
- package/dist/gui/components/IconButton.d.ts +5 -0
- package/dist/gui/components/IconButton.js +8 -0
- package/dist/gui/components/PanelTabsHeader.js +1 -1
- package/dist/gui/components/ThemeProvider.d.ts +14 -0
- package/dist/gui/components/ThemeProvider.js +42 -0
- package/dist/gui/components/ThemeToggle.d.ts +1 -0
- package/dist/gui/components/ThemeToggle.js +9 -0
- package/dist/gui/components/ToolCall.js +42 -17
- package/dist/gui/components/ToolCallList.js +1 -1
- package/dist/gui/components/index.d.ts +3 -0
- package/dist/gui/components/index.js +3 -0
- package/package.json +3 -3
- package/dist/core/lib/logger.d.ts +0 -59
- package/dist/core/lib/logger.js +0 -191
- package/dist/tui/components/LogsPanel.d.ts +0 -5
- package/dist/tui/components/LogsPanel.js +0 -29
|
@@ -2,7 +2,7 @@ import { type VariantProps } from "class-variance-authority";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
4
|
variant?: "default" | "link" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
5
|
-
size?: "default" | "
|
|
5
|
+
size?: "default" | "icon" | "sm" | "lg" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
8
8
|
asChild?: boolean;
|
|
@@ -6,7 +6,7 @@ import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks
|
|
|
6
6
|
import { useChatStore } from "../../core/store/chat-store.js";
|
|
7
7
|
import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
|
|
8
8
|
import { cn } from "../lib/utils.js";
|
|
9
|
-
import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, Message, MessageContent, PanelTabsHeader, SourcesTabContent, Tabs, TabsContent, TodoTabContent, } from "./index.js";
|
|
9
|
+
import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, } from "./index.js";
|
|
10
10
|
const logger = createLogger("gui");
|
|
11
11
|
// Helper component to provide openFiles callback
|
|
12
12
|
function OpenFilesButton({ children, }) {
|
|
@@ -25,12 +25,12 @@ function AsideTabs() {
|
|
|
25
25
|
// Mobile header component that uses ChatHeader context
|
|
26
26
|
function MobileHeader({ agentName, showHeader, }) {
|
|
27
27
|
const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
|
|
28
|
-
return (_jsxs("div", { className: "flex lg:hidden items-center
|
|
28
|
+
return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
|
|
29
29
|
}
|
|
30
30
|
// Header component that uses ChatLayout context (must be inside ChatLayout.Root)
|
|
31
31
|
function AppChatHeader({ agentName, todos, sources, showHeader, }) {
|
|
32
32
|
const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
|
|
33
|
-
return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center
|
|
33
|
+
return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
|
|
34
34
|
setPanelSize(panelSize === "hidden" ? "small" : "hidden");
|
|
35
35
|
}, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) })] }) })] }));
|
|
36
36
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/utils.js";
|
|
4
|
+
import { Button } from "./Button.js";
|
|
5
|
+
export const IconButton = React.forwardRef(({ className, ...props }, ref) => {
|
|
6
|
+
return (_jsx(Button, { ref: ref, variant: "ghost", size: "icon", className: cn("rounded-full", className), ...props }));
|
|
7
|
+
});
|
|
8
|
+
IconButton.displayName = "IconButton";
|
|
@@ -30,7 +30,7 @@ export const PanelTabsHeader = React.forwardRef(({ showIcons = true, visibleTabs
|
|
|
30
30
|
const gap = variant === "compact" ? "gap-[4px]" : "gap-3";
|
|
31
31
|
return (_jsx(TabsList, { ref: ref, className: cn("w-full justify-start bg-transparent p-0 h-auto", gap, className), ...props, children: tabs.map((tab) => {
|
|
32
32
|
const Icon = tab.icon;
|
|
33
|
-
return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-
|
|
33
|
+
return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-muted data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground hover:text-foreground transition-colors"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
|
|
34
34
|
}) }));
|
|
35
35
|
});
|
|
36
36
|
PanelTabsHeader.displayName = "PanelTabsHeader";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type Theme = "dark" | "light" | "system";
|
|
2
|
+
type ThemeProviderProps = {
|
|
3
|
+
children: React.ReactNode;
|
|
4
|
+
defaultTheme?: Theme;
|
|
5
|
+
storageKey?: string;
|
|
6
|
+
};
|
|
7
|
+
type ThemeProviderState = {
|
|
8
|
+
theme: Theme;
|
|
9
|
+
setTheme: (theme: Theme) => void;
|
|
10
|
+
resolvedTheme: "dark" | "light";
|
|
11
|
+
};
|
|
12
|
+
export declare function ThemeProvider({ children, defaultTheme, storageKey, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare const useTheme: () => ThemeProviderState;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useState } from "react";
|
|
3
|
+
const initialState = {
|
|
4
|
+
theme: "system",
|
|
5
|
+
setTheme: () => null,
|
|
6
|
+
resolvedTheme: "light",
|
|
7
|
+
};
|
|
8
|
+
const ThemeProviderContext = createContext(initialState);
|
|
9
|
+
export function ThemeProvider({ children, defaultTheme = "system", storageKey = "vite-ui-theme", }) {
|
|
10
|
+
const [theme, setTheme] = useState(() => localStorage.getItem(storageKey) || defaultTheme);
|
|
11
|
+
const [resolvedTheme, setResolvedTheme] = useState("light");
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const root = window.document.documentElement;
|
|
14
|
+
root.classList.remove("light", "dark");
|
|
15
|
+
if (theme === "system") {
|
|
16
|
+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
|
17
|
+
.matches
|
|
18
|
+
? "dark"
|
|
19
|
+
: "light";
|
|
20
|
+
root.classList.add(systemTheme);
|
|
21
|
+
setResolvedTheme(systemTheme);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
root.classList.add(theme);
|
|
25
|
+
setResolvedTheme(theme);
|
|
26
|
+
}, [theme]);
|
|
27
|
+
const value = {
|
|
28
|
+
theme,
|
|
29
|
+
setTheme: (theme) => {
|
|
30
|
+
localStorage.setItem(storageKey, theme);
|
|
31
|
+
setTheme(theme);
|
|
32
|
+
},
|
|
33
|
+
resolvedTheme,
|
|
34
|
+
};
|
|
35
|
+
return (_jsx(ThemeProviderContext.Provider, { value: value, children: children }));
|
|
36
|
+
}
|
|
37
|
+
export const useTheme = () => {
|
|
38
|
+
const context = useContext(ThemeProviderContext);
|
|
39
|
+
if (context === undefined)
|
|
40
|
+
throw new Error("useTheme must be used within a ThemeProvider");
|
|
41
|
+
return context;
|
|
42
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ThemeToggle(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Moon, Sun } from "lucide-react";
|
|
3
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./DropdownMenu.js";
|
|
4
|
+
import { IconButton } from "./IconButton.js";
|
|
5
|
+
import { useTheme } from "./ThemeProvider.js";
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const { setTheme } = useTheme();
|
|
8
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(IconButton, { children: [_jsx(Sun, { className: "size-4 text-muted-foreground rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" }), _jsx(Moon, { className: "absolute size-4 text-muted-foreground rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" }), _jsx("span", { className: "sr-only", children: "Toggle theme" })] }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsx(DropdownMenuItem, { onClick: () => setTheme("light"), children: "Light" }), _jsx(DropdownMenuItem, { onClick: () => setTheme("dark"), children: "Dark" }), _jsx(DropdownMenuItem, { onClick: () => setTheme("system"), children: "System" })] })] }));
|
|
9
|
+
}
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import JsonView from "@uiw/react-json-view";
|
|
3
3
|
import { CheckSquare, ChevronDown, Cloud, Edit, FileText, Globe, Link, Search, Wrench, } from "lucide-react";
|
|
4
4
|
import { useState } from "react";
|
|
5
|
+
import { useTheme } from "./ThemeProvider.js";
|
|
5
6
|
/**
|
|
6
7
|
* Map of icon names to Lucide components
|
|
7
8
|
*/
|
|
@@ -35,20 +36,48 @@ const _kindIcons = {
|
|
|
35
36
|
*/
|
|
36
37
|
export function ToolCall({ toolCall }) {
|
|
37
38
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
39
|
+
const { resolvedTheme } = useTheme();
|
|
38
40
|
// Determine which icon to show
|
|
39
41
|
const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
|
|
40
42
|
? ICON_MAP[toolCall.icon]
|
|
41
43
|
: Wrench;
|
|
42
44
|
// Determine display name
|
|
43
45
|
const displayName = toolCall.prettyName || toolCall.title;
|
|
44
|
-
|
|
46
|
+
// JSON View style based on theme
|
|
47
|
+
const jsonStyle = {
|
|
48
|
+
fontSize: "11px",
|
|
49
|
+
backgroundColor: "transparent",
|
|
50
|
+
fontFamily: "inherit",
|
|
51
|
+
"--w-rjv-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
52
|
+
"--w-rjv-key-string": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
53
|
+
"--w-rjv-background-color": "transparent",
|
|
54
|
+
"--w-rjv-line-color": resolvedTheme === "dark" ? "#27272a" : "#e4e4e7",
|
|
55
|
+
"--w-rjv-arrow-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
56
|
+
"--w-rjv-edit-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
57
|
+
"--w-rjv-info-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
58
|
+
"--w-rjv-update-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
59
|
+
"--w-rjv-copied-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
60
|
+
"--w-rjv-copied-success-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
|
|
61
|
+
"--w-rjv-curlybraces-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
62
|
+
"--w-rjv-colon-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
63
|
+
"--w-rjv-brackets-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
64
|
+
"--w-rjv-quotes-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
65
|
+
"--w-rjv-quotes-string-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
66
|
+
"--w-rjv-type-string-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
|
|
67
|
+
"--w-rjv-type-int-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
68
|
+
"--w-rjv-type-float-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
69
|
+
"--w-rjv-type-bigint-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
70
|
+
"--w-rjv-type-boolean-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
|
|
71
|
+
"--w-rjv-type-date-color": resolvedTheme === "dark" ? "#ec4899" : "#db2777",
|
|
72
|
+
"--w-rjv-type-url-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
|
|
73
|
+
"--w-rjv-type-null-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
74
|
+
"--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
75
|
+
"--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
76
|
+
};
|
|
77
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsx("button", { type: "button", className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: _jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: "text-muted-foreground", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }) }), isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
|
|
45
78
|
loc.line !== undefined &&
|
|
46
|
-
`:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-
|
|
47
|
-
|
|
48
|
-
backgroundColor: "transparent",
|
|
49
|
-
fontFamily: "inherit",
|
|
50
|
-
} }) })] })), (toolCall.content && toolCall.content.length > 0) ||
|
|
51
|
-
toolCall.error ? (_jsxs("div", { className: "p-3 border-b border-zinc-200 last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-zinc-400 uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-zinc-700", children: [toolCall.content?.map((block, idx) => {
|
|
79
|
+
`:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), (toolCall.content && toolCall.content.length > 0) ||
|
|
80
|
+
toolCall.error ? (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-foreground", children: [toolCall.content?.map((block, idx) => {
|
|
52
81
|
// Generate a stable key based on content
|
|
53
82
|
const getBlockKey = () => {
|
|
54
83
|
if (block.type === "diff" && "path" in block) {
|
|
@@ -73,18 +102,14 @@ export function ToolCall({ toolCall }) {
|
|
|
73
102
|
const parsed = JSON.parse(text);
|
|
74
103
|
// If it's an object or array, render with JsonView
|
|
75
104
|
if (typeof parsed === "object" && parsed !== null) {
|
|
76
|
-
return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style:
|
|
77
|
-
fontSize: "11px",
|
|
78
|
-
backgroundColor: "transparent",
|
|
79
|
-
fontFamily: "inherit",
|
|
80
|
-
} }) }, key));
|
|
105
|
+
return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) }, key));
|
|
81
106
|
}
|
|
82
107
|
}
|
|
83
108
|
catch {
|
|
84
109
|
// Not valid JSON, render as plain text
|
|
85
110
|
}
|
|
86
111
|
// Render as plain text
|
|
87
|
-
return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-
|
|
112
|
+
return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-foreground overflow-x-auto", children: text }, key));
|
|
88
113
|
};
|
|
89
114
|
// Handle nested content blocks (ACP format)
|
|
90
115
|
if (block.type === "content" && "content" in block) {
|
|
@@ -102,15 +127,15 @@ export function ToolCall({ toolCall }) {
|
|
|
102
127
|
"path" in block &&
|
|
103
128
|
"oldText" in block &&
|
|
104
129
|
"newText" in block) {
|
|
105
|
-
return (_jsxs("div", { className: "border border-
|
|
130
|
+
return (_jsxs("div", { className: "border border-border rounded bg-card", children: [_jsxs("div", { className: "bg-muted px-2 py-1 text-[10px] font-mono text-muted-foreground border-b border-border", children: [block.path, "line" in block &&
|
|
106
131
|
block.line !== null &&
|
|
107
132
|
block.line !== undefined &&
|
|
108
|
-
`:${block.line}`] }), _jsxs("div", { className: "p-2 font-mono text-[11px]", children: [_jsxs("div", { className: "text-red-
|
|
133
|
+
`:${block.line}`] }), _jsxs("div", { className: "p-2 font-mono text-[11px]", children: [_jsxs("div", { className: "text-red-500 dark:text-red-400", children: ["- ", block.oldText] }), _jsxs("div", { className: "text-green-500 dark:text-green-400", children: ["+ ", block.newText] })] })] }, getBlockKey()));
|
|
109
134
|
}
|
|
110
135
|
// Handle terminal blocks
|
|
111
136
|
if (block.type === "terminal" && "terminalId" in block) {
|
|
112
|
-
return (_jsxs("div", { className: "bg-
|
|
137
|
+
return (_jsxs("div", { className: "bg-neutral-900 text-neutral-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
|
|
113
138
|
}
|
|
114
139
|
return null;
|
|
115
|
-
}), toolCall.error && (_jsxs("div", { className: "text-
|
|
140
|
+
}), toolCall.error && (_jsxs("div", { className: "text-destructive font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })] })) : null, (toolCall.tokenUsage || toolCall.startedAt) && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border flex flex-wrap gap-4 text-[10px] text-muted-foreground font-sans", children: [toolCall.tokenUsage && (_jsxs("div", { className: "flex gap-3", children: [toolCall.tokenUsage.inputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Input:" }), toolCall.tokenUsage.inputTokens.toLocaleString()] })), toolCall.tokenUsage.outputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Output:" }), toolCall.tokenUsage.outputTokens.toLocaleString()] })), toolCall.tokenUsage.totalTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Total:" }), toolCall.tokenUsage.totalTokens.toLocaleString()] }))] })), toolCall.startedAt && (_jsxs("div", { className: "flex gap-3 ml-auto", children: [_jsxs("span", { children: ["Started: ", new Date(toolCall.startedAt).toLocaleTimeString()] }), toolCall.completedAt && (_jsxs("span", { children: ["Completed:", " ", new Date(toolCall.completedAt).toLocaleTimeString(), " (", Math.round((toolCall.completedAt - toolCall.startedAt) / 1000), "s)"] }))] }))] }))] }))] }));
|
|
116
141
|
}
|
|
@@ -15,7 +15,7 @@ export function ToolCallList({ toolCalls, groupBy = "chronological", }) {
|
|
|
15
15
|
completed: toolCalls.filter((tc) => tc.status === "completed"),
|
|
16
16
|
failed: toolCalls.filter((tc) => tc.status === "failed"),
|
|
17
17
|
};
|
|
18
|
-
return (_jsxs("div", { className: "space-y-4", children: [grouped.in_progress.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-[10px] font-bold text-
|
|
18
|
+
return (_jsxs("div", { className: "space-y-4", children: [grouped.in_progress.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-2 pl-1", children: "In Progress" }), grouped.in_progress.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] })), grouped.pending.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-2 pl-1", children: "Pending" }), grouped.pending.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] })), grouped.completed.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-2 pl-1", children: "Completed" }), grouped.completed.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] })), grouped.failed.length > 0 && (_jsxs("div", { children: [_jsx("h4", { className: "text-[10px] font-bold text-zinc-400 uppercase tracking-wider mb-2 pl-1", children: "Failed" }), grouped.failed.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id)))] }))] }));
|
|
19
19
|
}
|
|
20
20
|
// Default: chronological order
|
|
21
21
|
return (_jsx("div", { className: "space-y-2", children: toolCalls.map((tc) => (_jsx(ToolCall, { toolCall: tc }, tc.id))) }));
|
|
@@ -22,6 +22,7 @@ export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMe
|
|
|
22
22
|
export { FileSystemItem, type FileSystemItemProps, } from "./FileSystemItem.js";
|
|
23
23
|
export { FileSystemView, type FileSystemViewProps, } from "./FileSystemView.js";
|
|
24
24
|
export { HeightTransition } from "./HeightTransition.js";
|
|
25
|
+
export { IconButton, type IconButtonProps } from "./IconButton.js";
|
|
25
26
|
export { Input, type InputProps, inputVariants } from "./Input.js";
|
|
26
27
|
export { Label } from "./Label.js";
|
|
27
28
|
export { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
@@ -37,6 +38,8 @@ export { type SourceItem, SourceListItem, type SourceListItemProps, } from "./So
|
|
|
37
38
|
export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
|
|
38
39
|
export { Task, type TaskItem, TaskList, type TaskListProps, type TaskProps, } from "./Task.js";
|
|
39
40
|
export { Textarea, type TextareaProps, textareaVariants } from "./Textarea.js";
|
|
41
|
+
export { ThemeProvider, useTheme } from "./ThemeProvider.js";
|
|
42
|
+
export { ThemeToggle } from "./ThemeToggle.js";
|
|
40
43
|
export { ThinkingBlock, type ThinkingBlockProps, } from "./ThinkingBlock.js";
|
|
41
44
|
export { TodoList, type TodoListProps } from "./TodoList.js";
|
|
42
45
|
export { type TodoItem, TodoListItem, type TodoListItemProps, } from "./TodoListItem.js";
|
|
@@ -28,6 +28,7 @@ export { FileSystemItem, } from "./FileSystemItem.js";
|
|
|
28
28
|
export { FileSystemView, } from "./FileSystemView.js";
|
|
29
29
|
// Utility components
|
|
30
30
|
export { HeightTransition } from "./HeightTransition.js";
|
|
31
|
+
export { IconButton } from "./IconButton.js";
|
|
31
32
|
export { Input, inputVariants } from "./Input.js";
|
|
32
33
|
export { Label } from "./Label.js";
|
|
33
34
|
export { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
@@ -44,6 +45,8 @@ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
|
|
|
44
45
|
// Task/Todo components
|
|
45
46
|
export { Task, TaskList, } from "./Task.js";
|
|
46
47
|
export { Textarea, textareaVariants } from "./Textarea.js";
|
|
48
|
+
export { ThemeProvider, useTheme } from "./ThemeProvider.js";
|
|
49
|
+
export { ThemeToggle } from "./ThemeToggle.js";
|
|
47
50
|
export { ThinkingBlock, } from "./ThinkingBlock.js";
|
|
48
51
|
export { TodoList } from "./TodoList.js";
|
|
49
52
|
export { TodoListItem, } from "./TodoListItem.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@agentclientprotocol/sdk": "^0.5.1",
|
|
43
|
-
"@townco/core": "0.0.
|
|
43
|
+
"@townco/core": "0.0.20",
|
|
44
44
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
45
45
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
46
46
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
|
65
65
|
"@tailwindcss/postcss": "^4.1.17",
|
|
66
|
-
"@townco/tsconfig": "0.1.
|
|
66
|
+
"@townco/tsconfig": "0.1.39",
|
|
67
67
|
"@types/node": "^24.10.0",
|
|
68
68
|
"@types/react": "^19.2.2",
|
|
69
69
|
"ink": "^6.4.0",
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-compatible logger
|
|
3
|
-
* Outputs structured JSON logs to console with color-coding
|
|
4
|
-
* Also captures logs to a global store for in-app viewing
|
|
5
|
-
* In Node.js environment with logsDir option, also writes to .logs/ directory
|
|
6
|
-
*/
|
|
7
|
-
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
|
8
|
-
export interface LogEntry {
|
|
9
|
-
id: string;
|
|
10
|
-
timestamp: string;
|
|
11
|
-
level: LogLevel;
|
|
12
|
-
service: string;
|
|
13
|
-
message: string;
|
|
14
|
-
metadata?: Record<string, unknown>;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Get all captured logs
|
|
18
|
-
*/
|
|
19
|
-
export declare function getCapturedLogs(): LogEntry[];
|
|
20
|
-
/**
|
|
21
|
-
* Clear all captured logs
|
|
22
|
-
*/
|
|
23
|
-
export declare function clearCapturedLogs(): void;
|
|
24
|
-
/**
|
|
25
|
-
* Subscribe to log updates
|
|
26
|
-
*/
|
|
27
|
-
type LogSubscriber = (entry: LogEntry) => void;
|
|
28
|
-
export declare function subscribeToLogs(callback: LogSubscriber): () => void;
|
|
29
|
-
/**
|
|
30
|
-
* Configure global logs directory for file writing
|
|
31
|
-
* Must be called before creating any loggers (typically at TUI startup)
|
|
32
|
-
*/
|
|
33
|
-
export declare function configureLogsDir(logsDir: string): void;
|
|
34
|
-
export declare class Logger {
|
|
35
|
-
private service;
|
|
36
|
-
private minLevel;
|
|
37
|
-
private logFilePath?;
|
|
38
|
-
private logsDir?;
|
|
39
|
-
private writeQueue;
|
|
40
|
-
private isWriting;
|
|
41
|
-
constructor(service: string, minLevel?: LogLevel);
|
|
42
|
-
private setupFileLogging;
|
|
43
|
-
private writeToFile;
|
|
44
|
-
private shouldLog;
|
|
45
|
-
private log;
|
|
46
|
-
trace(message: string, metadata?: Record<string, unknown>): void;
|
|
47
|
-
debug(message: string, metadata?: Record<string, unknown>): void;
|
|
48
|
-
info(message: string, metadata?: Record<string, unknown>): void;
|
|
49
|
-
warn(message: string, metadata?: Record<string, unknown>): void;
|
|
50
|
-
error(message: string, metadata?: Record<string, unknown>): void;
|
|
51
|
-
fatal(message: string, metadata?: Record<string, unknown>): void;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Create a logger instance for a service
|
|
55
|
-
* @param service - Service name (e.g., "gui", "http-agent", "tui")
|
|
56
|
-
* @param minLevel - Minimum log level to display (default: "debug")
|
|
57
|
-
*/
|
|
58
|
-
export declare function createLogger(service: string, minLevel?: LogLevel): Logger;
|
|
59
|
-
export {};
|
package/dist/core/lib/logger.js
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-compatible logger
|
|
3
|
-
* Outputs structured JSON logs to console with color-coding
|
|
4
|
-
* Also captures logs to a global store for in-app viewing
|
|
5
|
-
* In Node.js environment with logsDir option, also writes to .logs/ directory
|
|
6
|
-
*/
|
|
7
|
-
// Check if running in Node.js
|
|
8
|
-
const isNode = typeof process !== "undefined" && process.versions?.node;
|
|
9
|
-
// Global logs directory configuration (set once at app startup for TUI)
|
|
10
|
-
let globalLogsDir;
|
|
11
|
-
// Global log store
|
|
12
|
-
const globalLogStore = [];
|
|
13
|
-
let logIdCounter = 0;
|
|
14
|
-
/**
|
|
15
|
-
* Get all captured logs
|
|
16
|
-
*/
|
|
17
|
-
export function getCapturedLogs() {
|
|
18
|
-
return [...globalLogStore];
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Clear all captured logs
|
|
22
|
-
*/
|
|
23
|
-
export function clearCapturedLogs() {
|
|
24
|
-
globalLogStore.length = 0;
|
|
25
|
-
}
|
|
26
|
-
const logSubscribers = new Set();
|
|
27
|
-
export function subscribeToLogs(callback) {
|
|
28
|
-
logSubscribers.add(callback);
|
|
29
|
-
return () => logSubscribers.delete(callback);
|
|
30
|
-
}
|
|
31
|
-
function notifyLogSubscribers(entry) {
|
|
32
|
-
for (const callback of logSubscribers) {
|
|
33
|
-
callback(entry);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Configure global logs directory for file writing
|
|
38
|
-
* Must be called before creating any loggers (typically at TUI startup)
|
|
39
|
-
*/
|
|
40
|
-
export function configureLogsDir(logsDir) {
|
|
41
|
-
globalLogsDir = logsDir;
|
|
42
|
-
}
|
|
43
|
-
const LOG_LEVELS = {
|
|
44
|
-
trace: 0,
|
|
45
|
-
debug: 1,
|
|
46
|
-
info: 2,
|
|
47
|
-
warn: 3,
|
|
48
|
-
error: 4,
|
|
49
|
-
fatal: 5,
|
|
50
|
-
};
|
|
51
|
-
const _LOG_COLORS = {
|
|
52
|
-
trace: "#6B7280", // gray
|
|
53
|
-
debug: "#3B82F6", // blue
|
|
54
|
-
info: "#10B981", // green
|
|
55
|
-
warn: "#F59E0B", // orange
|
|
56
|
-
error: "#EF4444", // red
|
|
57
|
-
fatal: "#DC2626", // dark red
|
|
58
|
-
};
|
|
59
|
-
const _LOG_STYLES = {
|
|
60
|
-
trace: "color: #6B7280",
|
|
61
|
-
debug: "color: #3B82F6; font-weight: bold",
|
|
62
|
-
info: "color: #10B981; font-weight: bold",
|
|
63
|
-
warn: "color: #F59E0B; font-weight: bold",
|
|
64
|
-
error: "color: #EF4444; font-weight: bold",
|
|
65
|
-
fatal: "color: #DC2626; font-weight: bold; background: #FEE2E2",
|
|
66
|
-
};
|
|
67
|
-
export class Logger {
|
|
68
|
-
service;
|
|
69
|
-
minLevel;
|
|
70
|
-
logFilePath;
|
|
71
|
-
logsDir;
|
|
72
|
-
writeQueue = [];
|
|
73
|
-
isWriting = false;
|
|
74
|
-
constructor(service, minLevel = "debug") {
|
|
75
|
-
this.service = service;
|
|
76
|
-
this.minLevel = minLevel;
|
|
77
|
-
// In production, suppress trace and debug logs
|
|
78
|
-
if (typeof process !== "undefined" &&
|
|
79
|
-
process.env?.NODE_ENV === "production") {
|
|
80
|
-
this.minLevel = "info";
|
|
81
|
-
}
|
|
82
|
-
// Note: File logging setup is done lazily in log() method
|
|
83
|
-
// This allows loggers created before configureLogsDir() to still write to files
|
|
84
|
-
}
|
|
85
|
-
setupFileLogging() {
|
|
86
|
-
if (!isNode || !globalLogsDir)
|
|
87
|
-
return;
|
|
88
|
-
try {
|
|
89
|
-
// Dynamic import for Node.js modules
|
|
90
|
-
const path = require("node:path");
|
|
91
|
-
const fs = require("node:fs");
|
|
92
|
-
this.logsDir = globalLogsDir;
|
|
93
|
-
this.logFilePath = path.join(this.logsDir, `${this.service}.log`);
|
|
94
|
-
// Create logs directory if it doesn't exist
|
|
95
|
-
if (!fs.existsSync(this.logsDir)) {
|
|
96
|
-
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
catch (_error) {
|
|
100
|
-
// Silently fail if we can't set up file logging
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
async writeToFile(content) {
|
|
104
|
-
if (!this.logFilePath || !isNode)
|
|
105
|
-
return;
|
|
106
|
-
this.writeQueue.push(content);
|
|
107
|
-
if (this.isWriting) {
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
this.isWriting = true;
|
|
111
|
-
while (this.writeQueue.length > 0) {
|
|
112
|
-
const batch = this.writeQueue.splice(0, this.writeQueue.length);
|
|
113
|
-
const data = `${batch.join("\n")}\n`;
|
|
114
|
-
try {
|
|
115
|
-
// Dynamic import for Node.js modules
|
|
116
|
-
const fs = require("node:fs");
|
|
117
|
-
await fs.promises.appendFile(this.logFilePath, data, "utf-8");
|
|
118
|
-
}
|
|
119
|
-
catch (_error) {
|
|
120
|
-
// Silently fail
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
this.isWriting = false;
|
|
124
|
-
}
|
|
125
|
-
shouldLog(level) {
|
|
126
|
-
return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
|
|
127
|
-
}
|
|
128
|
-
log(level, message, metadata) {
|
|
129
|
-
if (!this.shouldLog(level)) {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
const entry = {
|
|
133
|
-
id: `log_${++logIdCounter}`,
|
|
134
|
-
timestamp: new Date().toISOString(),
|
|
135
|
-
level,
|
|
136
|
-
service: this.service,
|
|
137
|
-
message,
|
|
138
|
-
...(metadata && { metadata }),
|
|
139
|
-
};
|
|
140
|
-
// Store in global log store
|
|
141
|
-
globalLogStore.push(entry);
|
|
142
|
-
// Notify subscribers
|
|
143
|
-
notifyLogSubscribers(entry);
|
|
144
|
-
// Write to file in Node.js (for logs tab to read)
|
|
145
|
-
// Lazily set up file logging if globalLogsDir was configured after this logger was created
|
|
146
|
-
if (isNode && !this.logFilePath && globalLogsDir) {
|
|
147
|
-
this.setupFileLogging();
|
|
148
|
-
}
|
|
149
|
-
if (isNode && this.logFilePath) {
|
|
150
|
-
// Write as JSON without the id field (to match expected format)
|
|
151
|
-
const fileEntry = {
|
|
152
|
-
timestamp: entry.timestamp,
|
|
153
|
-
level: entry.level,
|
|
154
|
-
service: entry.service,
|
|
155
|
-
message: entry.message,
|
|
156
|
-
...(entry.metadata && { metadata: entry.metadata }),
|
|
157
|
-
};
|
|
158
|
-
this.writeToFile(JSON.stringify(fileEntry)).catch(() => {
|
|
159
|
-
// Silently fail
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
// No console output - logs are only captured and displayed in UI
|
|
163
|
-
// This prevents logs from polluting stdout/stderr in TUI mode
|
|
164
|
-
}
|
|
165
|
-
trace(message, metadata) {
|
|
166
|
-
this.log("trace", message, metadata);
|
|
167
|
-
}
|
|
168
|
-
debug(message, metadata) {
|
|
169
|
-
this.log("debug", message, metadata);
|
|
170
|
-
}
|
|
171
|
-
info(message, metadata) {
|
|
172
|
-
this.log("info", message, metadata);
|
|
173
|
-
}
|
|
174
|
-
warn(message, metadata) {
|
|
175
|
-
this.log("warn", message, metadata);
|
|
176
|
-
}
|
|
177
|
-
error(message, metadata) {
|
|
178
|
-
this.log("error", message, metadata);
|
|
179
|
-
}
|
|
180
|
-
fatal(message, metadata) {
|
|
181
|
-
this.log("fatal", message, metadata);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Create a logger instance for a service
|
|
186
|
-
* @param service - Service name (e.g., "gui", "http-agent", "tui")
|
|
187
|
-
* @param minLevel - Minimum log level to display (default: "debug")
|
|
188
|
-
*/
|
|
189
|
-
export function createLogger(service, minLevel = "debug") {
|
|
190
|
-
return new Logger(service, minLevel);
|
|
191
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { subscribeToLogs } from "../../core/lib/logger.js";
|
|
5
|
-
// Color mapping for log levels
|
|
6
|
-
const LOG_LEVEL_COLORS = {
|
|
7
|
-
trace: "gray",
|
|
8
|
-
debug: "blue",
|
|
9
|
-
info: "green",
|
|
10
|
-
warn: "yellow",
|
|
11
|
-
error: "red",
|
|
12
|
-
fatal: "red",
|
|
13
|
-
};
|
|
14
|
-
export function LogsPanel({ logs: initialLogs }) {
|
|
15
|
-
const [logs, setLogs] = useState(initialLogs);
|
|
16
|
-
// Subscribe to new logs
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
const unsubscribe = subscribeToLogs((entry) => {
|
|
19
|
-
setLogs((prev) => [...prev, entry]);
|
|
20
|
-
});
|
|
21
|
-
return unsubscribe;
|
|
22
|
-
}, []);
|
|
23
|
-
if (logs.length === 0) {
|
|
24
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: _jsx(Text, { dimColor: true, children: "No logs yet..." }) }));
|
|
25
|
-
}
|
|
26
|
-
// Show last 100 logs
|
|
27
|
-
const displayLogs = logs.slice(-100);
|
|
28
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: 1, children: displayLogs.map((log) => (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: new Date(log.timestamp).toLocaleTimeString() }), _jsxs(Text, { color: LOG_LEVEL_COLORS[log.level], bold: true, children: ["[", log.level.toUpperCase(), "]"] }), _jsxs(Text, { dimColor: true, children: ["[", log.service, "]"] }), _jsx(Text, { children: log.message }), log.metadata && (_jsx(Text, { dimColor: true, children: JSON.stringify(log.metadata) }))] }, log.id))) }));
|
|
29
|
-
}
|