@townco/ui 0.1.40 → 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.
@@ -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" | "sm" | "lg" | "icon" | null | undefined;
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 gap-2 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("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
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 gap-2 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("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle sidebar", onClick: () => {
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,5 @@
1
+ import * as React from "react";
2
+ import { type ButtonProps } from "./Button.js";
3
+ export interface IconButtonProps extends ButtonProps {
4
+ }
5
+ export declare const IconButton: React.ForwardRefExoticComponent<IconButtonProps & React.RefAttributes<HTMLButtonElement>>;
@@ -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-zinc-100 data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
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
- 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-zinc-500", children: [_jsx("div", { className: "text-zinc-500", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-zinc-500", children: displayName }), _jsx(ChevronDown, { className: `h-3 w-3 text-zinc-400 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }) }), isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-zinc-200 rounded-lg bg-zinc-50 overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-zinc-200", children: [_jsx("div", { className: "text-[10px] font-bold text-zinc-400 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-zinc-700 bg-zinc-200/50 px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
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-zinc-200", children: [_jsx("div", { className: "text-[10px] font-bold text-zinc-400 uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-zinc-700", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: {
47
- fontSize: "11px",
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-zinc-700 overflow-x-auto", children: text }, key));
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-zinc-200 rounded bg-white", children: [_jsxs("div", { className: "bg-zinc-50 px-2 py-1 text-[10px] font-mono text-zinc-500 border-b border-zinc-200", children: [block.path, "line" in block &&
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-600", children: ["- ", block.oldText] }), _jsxs("div", { className: "text-green-600", children: ["+ ", block.newText] })] })] }, getBlockKey()));
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-zinc-900 text-zinc-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
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-red-600 font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })] })) : null, (toolCall.tokenUsage || toolCall.startedAt) && (_jsxs("div", { className: "p-2 bg-zinc-100/50 border-t border-zinc-200 flex flex-wrap gap-4 text-[10px] text-zinc-500 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)"] }))] }))] }))] }))] }));
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-zinc-400 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-zinc-400 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-zinc-400 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)))] }))] }));
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.40",
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.18",
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.37",
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 {};
@@ -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,5 +0,0 @@
1
- import type { LogEntry } from "../../core/lib/logger.js";
2
- export interface LogsPanelProps {
3
- logs: LogEntry[];
4
- }
5
- export declare function LogsPanel({ logs: initialLogs }: LogsPanelProps): import("react/jsx-runtime").JSX.Element;
@@ -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
- }