@townco/ui 0.1.99 → 0.1.100

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.
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from "@townco/core";
2
2
  import { useCallback, useRef } from "react";
3
+ import { generateMessageId } from "../schemas/index.js";
3
4
  import { useChatStore } from "../store/chat-store.js";
4
5
  const logger = createLogger("use-chat-messages", "debug");
5
6
  /**
@@ -61,7 +62,7 @@ export function useChatMessages(client, startSession) {
61
62
  });
62
63
  }
63
64
  // Create assistant message ID outside try block so it's accessible in catch
64
- const assistantMessageId = `msg_${Date.now()}_assistant`;
65
+ const assistantMessageId = generateMessageId("assistant");
65
66
  try {
66
67
  // Start streaming and track time immediately
67
68
  const startTime = Date.now();
@@ -69,7 +70,7 @@ export function useChatMessages(client, startSession) {
69
70
  setStreamingStartTime(startTime);
70
71
  // Add user message to UI with images
71
72
  const userMessage = {
72
- id: `msg_${Date.now()}_user`,
73
+ id: generateMessageId("user"),
73
74
  role: "user",
74
75
  content: content,
75
76
  timestamp: new Date().toISOString(),
@@ -333,7 +334,7 @@ export function useChatMessages(client, startSession) {
333
334
  messageRole: targetMessage?.role,
334
335
  });
335
336
  // Create assistant message ID outside try block so it's accessible in catch
336
- const assistantMessageId = `msg_${Date.now()}_assistant`;
337
+ const assistantMessageId = generateMessageId("assistant");
337
338
  try {
338
339
  // Start streaming and track time
339
340
  const startTime = Date.now();
@@ -343,7 +344,7 @@ export function useChatMessages(client, startSession) {
343
344
  truncateMessagesFrom(targetArrayIndex);
344
345
  // Add the new user message to UI
345
346
  const userMessage = {
346
- id: `msg_${Date.now()}_user`,
347
+ id: generateMessageId("user"),
347
348
  role: "user",
348
349
  content: newContent,
349
350
  timestamp: new Date().toISOString(),
@@ -578,3 +578,9 @@ export declare const ConnectionStatus: z.ZodEnum<{
578
578
  error: "error";
579
579
  }>;
580
580
  export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
581
+ /**
582
+ * Generate a unique message ID.
583
+ * Uses timestamp + counter + random suffix to ensure uniqueness even when
584
+ * multiple messages are created in the same millisecond (e.g., user + assistant).
585
+ */
586
+ export declare function generateMessageId(role: "user" | "assistant" | "system"): string;
@@ -103,3 +103,24 @@ export const ConnectionStatus = z.enum([
103
103
  "connected",
104
104
  "error",
105
105
  ]);
106
+ /**
107
+ * Counter for generating unique message IDs within the same millisecond
108
+ */
109
+ let messageIdCounter = 0;
110
+ let lastMessageIdTimestamp = 0;
111
+ /**
112
+ * Generate a unique message ID.
113
+ * Uses timestamp + counter + random suffix to ensure uniqueness even when
114
+ * multiple messages are created in the same millisecond (e.g., user + assistant).
115
+ */
116
+ export function generateMessageId(role) {
117
+ const now = Date.now();
118
+ // Reset counter if we're in a new millisecond
119
+ if (now !== lastMessageIdTimestamp) {
120
+ messageIdCounter = 0;
121
+ lastMessageIdTimestamp = now;
122
+ }
123
+ const counter = messageIdCounter++;
124
+ const random = Math.random().toString(36).substring(2, 7);
125
+ return `msg_${now}_${counter}_${random}_${role}`;
126
+ }
@@ -1,7 +1,7 @@
1
1
  import { type LogEntry } from "@townco/core";
2
2
  import type { TodoItem } from "../../gui/components/TodoListItem.js";
3
3
  import type { HookNotification } from "../../sdk/schemas/message.js";
4
- import type { ConnectionStatus, DisplayMessage, InputState } from "../schemas/index.js";
4
+ import { type ConnectionStatus, type DisplayMessage, type InputState } from "../schemas/index.js";
5
5
  import type { Source } from "../schemas/source.js";
6
6
  import type { ToolCall, ToolCallUpdate } from "../schemas/tool-call.js";
7
7
  /**
@@ -1,5 +1,6 @@
1
1
  import { createLogger } from "@townco/core";
2
2
  import { create } from "zustand";
3
+ import { generateMessageId, } from "../schemas/index.js";
3
4
  import { mergeToolCallUpdate } from "../schemas/tool-call.js";
4
5
  const logger = createLogger("chat-store", "debug");
5
6
  // Constants to avoid creating new empty arrays
@@ -245,7 +246,7 @@ export const useChatStore = create((set) => ({
245
246
  // This happens during session replay when a tool call comes before any text
246
247
  logger.debug("No assistant message found, creating one for tool call at position 0");
247
248
  const newMessage = {
248
- id: `msg_${Date.now()}_assistant`,
249
+ id: generateMessageId("assistant"),
249
250
  role: "assistant",
250
251
  content: "",
251
252
  timestamp: new Date().toISOString(),
@@ -317,7 +318,7 @@ export const useChatStore = create((set) => ({
317
318
  contentPosition: 0, // Hook at the start of the message
318
319
  };
319
320
  const newMessage = {
320
- id: `msg_${Date.now()}_assistant`,
321
+ id: generateMessageId("assistant"),
321
322
  role: "assistant",
322
323
  content: "",
323
324
  timestamp: new Date().toISOString(),
@@ -410,7 +411,7 @@ export const useChatStore = create((set) => ({
410
411
  // No assistant message exists yet - create one with the sources
411
412
  logger.debug("No assistant message found, creating one for sources");
412
413
  const newMessage = {
413
- id: `msg_${Date.now()}_assistant`,
414
+ id: generateMessageId("assistant"),
414
415
  role: "assistant",
415
416
  content: "",
416
417
  timestamp: new Date().toISOString(),
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import type { AcpClient } from "../../sdk/client/index.js";
2
3
  import type { FileSystemProvider } from "../types/filesystem.js";
3
4
  import { type SourceItem } from "./SourceListItem.js";
4
5
  import type { TodoItem } from "./TodoListItem.js";
@@ -13,6 +14,9 @@ export declare const TodoTabContent: React.ForwardRefExoticComponent<TodoTabCont
13
14
  export interface FilesTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
14
15
  files?: string[];
15
16
  provider?: FileSystemProvider;
17
+ sessionId?: string | null | undefined;
18
+ agentApiUrl?: string | undefined;
19
+ client?: AcpClient | null | undefined;
16
20
  onFileSelect?: (filePath: string) => void;
17
21
  }
18
22
  export declare const FilesTabContent: React.ForwardRefExoticComponent<FilesTabContentProps & React.RefAttributes<HTMLDivElement>>;
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Globe } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils.js";
5
+ import { SandboxFileSystemProvider } from "../providers/SandboxFileSystemProvider.js";
5
6
  import { FileSystemView } from "./FileSystemView.js";
6
7
  import { SourceListItem } from "./SourceListItem.js";
7
8
  import { TodoList } from "./TodoList.js";
@@ -9,26 +10,69 @@ export const TodoTabContent = React.forwardRef(({ todos = [], className, ...prop
9
10
  return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(TodoList, { todos: todos, className: "h-full" }) }));
10
11
  });
11
12
  TodoTabContent.displayName = "TodoTabContent";
12
- export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
13
- // Stub handlers for the overflow menu actions
14
- // Replace these with real implementations when connecting to actual filesystem
15
- const handleDownload = React.useCallback((item) => {
16
- console.log("Download:", item.name);
17
- // TODO: Implement download logic
13
+ export const FilesTabContent = React.forwardRef(({ files = [], provider, sessionId, agentApiUrl, client, onFileSelect, className, ...props }, ref) => {
14
+ const [refreshKey, setRefreshKey] = React.useState(0);
15
+ const refreshDebounceRef = React.useRef(undefined);
16
+ // Debounced refresh to coalesce rapid file changes
17
+ const scheduleRefresh = React.useCallback(() => {
18
+ if (refreshDebounceRef.current) {
19
+ clearTimeout(refreshDebounceRef.current);
20
+ }
21
+ refreshDebounceRef.current = setTimeout(() => {
22
+ setRefreshKey((prev) => prev + 1);
23
+ }, 300); // 300ms debounce
18
24
  }, []);
19
- const handleRename = React.useCallback((item) => {
20
- console.log("Rename:", item.name);
21
- // TODO: Implement rename logic
22
- }, []);
23
- const handleDelete = React.useCallback((item) => {
24
- console.log("Delete:", item.name);
25
- // TODO: Implement delete logic
26
- }, []);
27
- return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(FileSystemView, { ...(provider && { provider }), onItemSelect: (item) => {
25
+ // Subscribe to file system change events from SSE
26
+ React.useEffect(() => {
27
+ if (!sessionId || !client)
28
+ return;
29
+ const unsubscribe = client.onFileSystemChange(sessionId, (event) => {
30
+ console.log("Files changed:", event);
31
+ scheduleRefresh();
32
+ });
33
+ return unsubscribe;
34
+ }, [sessionId, client, scheduleRefresh]);
35
+ // Create sandbox provider if sessionId and agentApiUrl are provided
36
+ const sandboxProvider = React.useMemo(() => {
37
+ if (provider)
38
+ return provider;
39
+ if (sessionId && agentApiUrl) {
40
+ return new SandboxFileSystemProvider({
41
+ sessionId,
42
+ apiUrl: agentApiUrl,
43
+ });
44
+ }
45
+ return undefined;
46
+ }, [provider, sessionId, agentApiUrl]); // Add refreshKey to trigger provider recreation
47
+ // Handler for downloading files from the sandbox
48
+ const handleDownload = React.useCallback(async (item) => {
49
+ if (item.type === "folder") {
50
+ console.warn("Cannot download folders");
51
+ return;
52
+ }
53
+ if (!sessionId || !agentApiUrl) {
54
+ console.error("Cannot download: missing sessionId or agentApiUrl");
55
+ return;
56
+ }
57
+ try {
58
+ const url = `${agentApiUrl}/sandbox/download?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(item.path || item.name)}`;
59
+ // Create a temporary anchor element to trigger the download
60
+ const a = document.createElement("a");
61
+ a.href = url;
62
+ a.download = item.name;
63
+ document.body.appendChild(a);
64
+ a.click();
65
+ document.body.removeChild(a);
66
+ }
67
+ catch (error) {
68
+ console.error("Failed to download file:", error);
69
+ }
70
+ }, [sessionId, agentApiUrl]);
71
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(FileSystemView, { ...(sandboxProvider && { provider: sandboxProvider }), onItemSelect: (item) => {
28
72
  if (item.type === "file" && onFileSelect) {
29
73
  onFileSelect(item.path || item.name);
30
74
  }
31
- }, onDownload: handleDownload, onRename: handleRename, onDelete: handleDelete, className: "h-full" }) }));
75
+ }, onDownload: handleDownload, className: "h-full" }, refreshKey) }));
32
76
  });
33
77
  FilesTabContent.displayName = "FilesTabContent";
34
78
  export const SourcesTabContent = React.forwardRef(({ sources = [], highlightedSourceId, className, ...props }, ref) => {
@@ -56,10 +56,10 @@ function ChatInputWithAttachments({ client, startSession, placeholder, commandMe
56
56
  ] }));
57
57
  }
58
58
  // Controlled Tabs component for the aside panel
59
- function AsideTabs({ todos, sources, tools, mcps, subagents, }) {
59
+ function AsideTabs({ todos, sources, tools, mcps, subagents, sessionId, agentApiUrl, client, }) {
60
60
  const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
61
61
  return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [
62
- _jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center", "sticky top-0 z-10 shrink-0"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })
62
+ _jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center", "sticky top-0 z-10 shrink-0"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(FilesTabContent, { sessionId: sessionId, agentApiUrl: agentApiUrl, client: client }) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })
63
63
  ] }));
64
64
  }
65
65
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
@@ -350,7 +350,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
350
350
  ] })) }) }, message.id));
351
351
  }) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, commandMenuItems: commandMenuItems, onCancel: cancel, promptParameters: agentPromptParameters }) })
352
352
  ] })
353
- ] }), _jsx(ChatLayout.Aside, { breakpoint: "md", children: _jsx(AsideTabs, { todos: todos, sources: allSources, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) })
353
+ ] }), _jsx(ChatLayout.Aside, { breakpoint: "md", children: _jsx(AsideTabs, { todos: todos, sources: allSources, tools: agentTools, mcps: agentMcps, subagents: agentSubagents, sessionId: sessionId, agentApiUrl: client?.getBaseUrl(), client: client }) })
354
354
  ] }) })
355
355
  ] }));
356
356
  }
@@ -11,7 +11,5 @@ export interface FileSystemItemProps {
11
11
  selectedId?: string;
12
12
  isDropTarget?: boolean;
13
13
  onDownload?: (item: FileSystemItemType) => void;
14
- onRename?: (item: FileSystemItemType) => void;
15
- onDelete?: (item: FileSystemItemType) => void;
16
14
  }
17
- export declare function FileSystemItem({ item, level, onSelect, selectedId, isDropTarget, onDownload, onRename, onDelete }: FileSystemItemProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function FileSystemItem({ item, level, onSelect, selectedId, isDropTarget, onDownload }: FileSystemItemProps): import("react/jsx-runtime").JSX.Element;
@@ -7,8 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
7
7
  import { ChevronDown, FileCode, Folder, FolderOpen, MoreVertical, } from "lucide-react";
8
8
  import * as React from "react";
9
9
  import { cn } from "../lib/utils.js";
10
- import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "./DropdownMenu.js";
11
- export function FileSystemItem({ item, level = 0, onSelect, selectedId, isDropTarget = false, onDownload, onRename, onDelete, }) {
10
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./DropdownMenu.js";
11
+ export function FileSystemItem({ item, level = 0, onSelect, selectedId, isDropTarget = false, onDownload, }) {
12
12
  const [isExpanded, setIsExpanded] = React.useState(true);
13
13
  const isSelected = selectedId === item.id;
14
14
  const handleToggle = () => {
@@ -70,15 +70,9 @@ export function FileSystemItem({ item, level = 0, onSelect, selectedId, isDropTa
70
70
  // Force visible when menu is open
71
71
  "data-[state=open]:opacity-100"), onClick: (e) => {
72
72
  e.stopPropagation();
73
- }, "aria-label": "More options", type: "button", tabIndex: -1, children: _jsx(MoreVertical, { className: "size-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", side: "bottom", sideOffset: 5, alignOffset: 0, collisionPadding: 8, className: "w-40 z-[100]", onClick: (e) => e.stopPropagation(), children: [onDownload && (_jsx(DropdownMenuItem, { onClick: (e) => {
74
- e.stopPropagation();
75
- onDownload(item);
76
- }, children: "Download" })), onRename && (_jsx(DropdownMenuItem, { onClick: (e) => {
77
- e.stopPropagation();
78
- onRename(item);
79
- }, children: "Rename" })), (onDownload || onRename) && onDelete && _jsx(DropdownMenuSeparator, {}), onDelete && (_jsx(DropdownMenuItem, { className: "text-destructive focus:text-destructive focus:bg-muted", onClick: (e) => {
80
- e.stopPropagation();
81
- onDelete(item);
82
- }, children: _jsx("span", { className: "text-paragraph-sm", children: "Delete" }) }))] })
83
- ] }), item.type === "folder" && (_jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-muted-foreground", children: _jsx(ChevronDown, { className: cn("size-4 transition-transform", !isExpanded && "-rotate-90") }) }))] }), item.type === "folder" && isExpanded && item.children && (_jsx("div", { className: "flex flex-col", children: item.children.map((child) => (_jsx(FileSystemItem, { item: child, level: level + 1, ...(onSelect && { onSelect }), ...(selectedId && { selectedId }), ...(onDownload && { onDownload }), ...(onRename && { onRename }), ...(onDelete && { onDelete }) }, child.id))) }))] }));
73
+ }, "aria-label": "More options", type: "button", tabIndex: -1, children: _jsx(MoreVertical, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "end", side: "bottom", sideOffset: 5, alignOffset: 0, collisionPadding: 8, className: "w-40 z-[100]", onClick: (e) => e.stopPropagation(), children: onDownload && (_jsx(DropdownMenuItem, { onClick: (e) => {
74
+ e.stopPropagation();
75
+ onDownload(item);
76
+ }, children: "Download" })) })
77
+ ] }), item.type === "folder" && (_jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-muted-foreground", children: _jsx(ChevronDown, { className: cn("size-4 transition-transform", !isExpanded && "-rotate-90") }) }))] }), item.type === "folder" && isExpanded && item.children && (_jsx("div", { className: "flex flex-col", children: item.children.map((child) => (_jsx(FileSystemItem, { item: child, level: level + 1, ...(onSelect && { onSelect }), ...(selectedId && { selectedId }), ...(onDownload && { onDownload }) }, child.id))) }))] }));
84
78
  }
@@ -8,7 +8,5 @@ export interface FileSystemViewProps {
8
8
  provider?: FileSystemProvider;
9
9
  onItemSelect?: (item: FileSystemItemType) => void;
10
10
  onDownload?: (item: FileSystemItemType) => void;
11
- onRename?: (item: FileSystemItemType) => void;
12
- onDelete?: (item: FileSystemItemType) => void;
13
11
  }
14
- export declare function FileSystemView({ className, provider, onItemSelect, onDownload, onRename, onDelete }: FileSystemViewProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function FileSystemView({ className, provider, onItemSelect, onDownload }: FileSystemViewProps): import("react/jsx-runtime").JSX.Element;
@@ -3,18 +3,27 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  * FileSystemView component - main file tree/manager view
4
4
  * Based on shadcn file manager design and Figma specifications
5
5
  */
6
+ import { FileText } from "lucide-react";
6
7
  import * as React from "react";
7
8
  import { MockFileSystemProvider } from "../data/mockFileSystemData.js";
8
9
  import { cn } from "../lib/utils.js";
9
10
  import { FileSystemItem } from "./FileSystemItem.js";
10
11
  // Create a default provider instance outside the component to avoid infinite loops
11
12
  const defaultProvider = new MockFileSystemProvider();
12
- export function FileSystemView({ className, provider = defaultProvider, onItemSelect, onDownload, onRename, onDelete, }) {
13
+ export function FileSystemView({ className, provider = defaultProvider, onItemSelect, onDownload, }) {
13
14
  const [items, setItems] = React.useState([]);
14
15
  const [selectedId, setSelectedId] = React.useState();
15
16
  const [isLoading, setIsLoading] = React.useState(true);
16
17
  const [error, setError] = React.useState();
17
- // Load root items on mount
18
+ const [_refreshTrigger, setRefreshTrigger] = React.useState(0);
19
+ // 60-second polling to refresh file list
20
+ React.useEffect(() => {
21
+ const intervalId = setInterval(() => {
22
+ setRefreshTrigger((prev) => prev + 1);
23
+ }, 60000); // 60 seconds
24
+ return () => clearInterval(intervalId);
25
+ }, []); // Run once on mount
26
+ // Load root items on mount and when refresh is triggered
18
27
  React.useEffect(() => {
19
28
  const loadItems = async () => {
20
29
  try {
@@ -42,5 +51,10 @@ export function FileSystemView({ className, provider = defaultProvider, onItemSe
42
51
  if (error) {
43
52
  return (_jsx("div", { className: cn("", className), children: _jsxs("p", { className: "text-sm text-destructive", children: ["Error: ", error] }) }));
44
53
  }
45
- return (_jsx("div", { className: cn("flex flex-col", className), children: items.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: "No items found" })) : (items.map((item) => (_jsx(FileSystemItem, { item: item, onSelect: handleItemSelect, ...(selectedId && { selectedId }), ...(onDownload && { onDownload }), ...(onRename && { onRename }), ...(onDelete && { onDelete }) }, item.id)))) }));
54
+ if (items.length === 0) {
55
+ return (_jsxs("div", { className: cn("flex flex-col items-center justify-center h-full text-center py-8 max-w-sm mx-auto", className), children: [
56
+ _jsx(FileText, { className: "size-8 text-muted-foreground opacity-50 mb-3" }), _jsx("p", { className: "text-paragraph text-muted-foreground", children: "No files yet" }), _jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 mt-1", children: "Files will appear when your agent writes to the sandbox." })
57
+ ] }));
58
+ }
59
+ return (_jsx("div", { className: cn("flex flex-col", className), children: items.map((item) => (_jsx(FileSystemItem, { item: item, onSelect: handleItemSelect, ...(selectedId && { selectedId }), ...(onDownload && { onDownload }) }, item.id))) }));
46
60
  }
@@ -21,13 +21,11 @@ function getHookDisplayInfo(hookType, callback, status, action, midTurn) {
21
21
  const isMidTurnCompaction = callback === "mid_turn_compaction" || midTurn === true;
22
22
  if (isMidTurnCompaction) {
23
23
  if (isTriggered) {
24
- return { icon: FoldVertical, title: "Compacting Context Mid-Turn..." };
24
+ return { icon: FoldVertical, title: "Compacting Context..." };
25
25
  }
26
26
  return {
27
27
  icon: FoldVertical,
28
- title: noActionNeeded
29
- ? "Context Check"
30
- : "Context Compacted (Mid-Turn)",
28
+ title: noActionNeeded ? "Context Check" : "Context Compacted",
31
29
  };
32
30
  }
33
31
  // Regular tool response compaction
@@ -51,6 +49,22 @@ function getHookDisplayInfo(hookType, callback, status, action, midTurn) {
51
49
  function formatNumber(num) {
52
50
  return num.toLocaleString();
53
51
  }
52
+ /**
53
+ * Format duration for display
54
+ * Shows seconds with 1 decimal place, then minutes and seconds, then hours
55
+ */
56
+ function formatDuration(seconds) {
57
+ if (seconds < 60) {
58
+ return `${seconds.toFixed(1)}s`;
59
+ }
60
+ const hours = Math.floor(seconds / 3600);
61
+ const mins = Math.floor((seconds % 3600) / 60);
62
+ const secs = Math.floor(seconds % 60);
63
+ if (hours > 0) {
64
+ return `${hours}h ${mins}m ${secs}s`;
65
+ }
66
+ return `${mins}m ${secs}s`;
67
+ }
54
68
  /**
55
69
  * HookNotification component - displays a hook notification inline with messages
56
70
  * Shows triggered (loading), completed, or error states
@@ -159,7 +173,10 @@ export function HookNotification({ notification }) {
159
173
  "Warning"] }), _jsx("div", { className: "text-[11px] text-yellow-700 dark:text-yellow-400", children: truncationWarning })
160
174
  ] })), notification.error && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [
161
175
  _jsx("div", { className: "text-[10px] font-bold text-destructive uppercase tracking-wider mb-1.5 font-sans", children: "Error" }), _jsx("div", { className: "text-[11px] text-destructive font-mono", children: notification.error })
162
- ] })), _jsxs("div", { className: "p-2 bg-muted/50 border-t border-border text-[10px] text-muted-foreground font-sans space-y-0.5", children: [notification.triggeredAt && (_jsxs("div", { children: ["Started:", " ", new Date(notification.triggeredAt).toLocaleTimeString()] })), notification.completedAt && (_jsxs("div", { children: ["Completed:", " ", new Date(notification.completedAt).toLocaleTimeString()] })), notification.triggeredAt && notification.completedAt && (_jsxs("div", { children: ["Duration:", " ", ((notification.completedAt - notification.triggeredAt) /
163
- 1000).toFixed(1), "s"] }))] })
176
+ ] })), _jsx("div", { className: "p-2 bg-muted/50 border-t border-border text-[10px] text-muted-foreground font-sans", children: notification.triggeredAt && (_jsxs("div", { className: "flex gap-3 justify-end", children: [
177
+ _jsxs("span", { children: ["Started:", " ", new Date(notification.triggeredAt).toLocaleTimeString()] }), notification.completedAt && (_jsxs(_Fragment, { children: [
178
+ _jsx("span", { children: "-" }), _jsxs("span", { children: ["Completed:", " ", new Date(notification.completedAt).toLocaleTimeString()] }), _jsx("span", { children: "-" }), _jsxs("span", { children: ["Duration:", " ", formatDuration((notification.completedAt - notification.triggeredAt) /
179
+ 1000)] })
180
+ ] }))] })) })
164
181
  ] }))] }));
165
182
  }
@@ -12,9 +12,9 @@ export const SourceListItem = React.forwardRef(({ source, isSelected, className,
12
12
  // Selected state - matching FileSystemItem
13
13
  isSelected && "bg-accent", className), onClick: () => window.open(source.url, "_blank"), ...props, children: [
14
14
  _jsx("div", { className: "shrink-0 flex items-center h-5", children: _jsx("div", { className: "relative rounded-[3px] size-4 overflow-hidden bg-muted", children: source.favicon ? (_jsx("img", { alt: source.sourceName, className: "size-full object-cover", src: source.favicon })) : (_jsx("div", { className: "size-full bg-muted" })) }) }), _jsxs("div", { className: "flex flex-1 flex-col gap-1 min-w-0", children: [
15
- _jsxs("div", { className: "text-paragraph-sm text-foreground", children: [
15
+ _jsxs("div", { className: "text-paragraph-sm text-foreground truncate", children: [
16
16
  _jsx("span", { className: "font-medium", children: source.sourceName }), _jsxs("span", { className: "text-muted-foreground", children: [" \u00B7 ", source.title] })
17
- ] }), _jsx("p", { className: "text-paragraph-sm text-muted-foreground line-clamp-3", children: source.snippet })
17
+ ] }), _jsx("p", { className: "text-paragraph-sm text-muted-foreground line-clamp-3 break-all", children: source.snippet })
18
18
  ] })
19
19
  ] }));
20
20
  });
@@ -9,14 +9,14 @@ export const TodoListItem = React.forwardRef(({ todo, className, ...props }, ref
9
9
  /* biome-ignore lint/a11y/useSemanticElements: Keeping div for consistency with FileSystemItem pattern */
10
10
  _jsxs("div", { ref: ref, className: cn(
11
11
  // Base styles matching FileSystemItem
12
- "group flex items-center gap-2 p-2 rounded-md cursor-pointer transition-colors text-paragraph-sm",
12
+ "group flex items-start gap-2 p-2 rounded-md cursor-pointer transition-colors text-paragraph-sm",
13
13
  // Hover state - matching FileSystemItem
14
14
  "hover:bg-accent-hover",
15
15
  // Focus state - matching FileSystemItem
16
16
  "focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-border-dark",
17
17
  // Selected state (if needed later)
18
18
  isSelected && "bg-accent", className), role: "button", tabIndex: 0, ...props, children: [
19
- _jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-foreground", children: todo.status === "completed" ? (_jsx(CircleCheck, { className: "size-4 text-muted-foreground" })) : todo.status === "in_progress" ? (_jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" })) : (_jsx(Circle, { className: "size-4 text-foreground" })) }), _jsx("p", { className: cn("flex-1 text-foreground",
19
+ _jsx("div", { className: "shrink-0 flex items-center justify-center w-4 h-5 mt-0.5", children: todo.status === "completed" ? (_jsx(CircleCheck, { className: "size-4 text-muted-foreground" })) : todo.status === "in_progress" ? (_jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" })) : (_jsx(Circle, { className: "size-4 text-foreground" })) }), _jsx("p", { className: cn("flex-1 text-foreground",
20
20
  // Completed state: strikethrough + muted color
21
21
  isCompleted && "line-through text-muted-foreground",
22
22
  // In-progress state: medium weight for emphasis
@@ -1,7 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import JsonView from "@uiw/react-json-view";
3
- import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, FoldVertical, Globe, Image, Link, ListVideo, ScissorsLineDashed, Search, Wrench, } from "lucide-react";
4
- import React, { useEffect, useState } from "react";
3
+ import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, FoldVertical, Globe, Image, Link, ListVideo, ScissorsLineDashed, Search, Terminal, Wrench, } from "lucide-react";
4
+ import React, { useEffect, useRef, useState } from "react";
5
5
  import { getGroupDisplayState, getToolCallDisplayState, getToolCallStateVerbiage, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
6
6
  import { generateSmartSummary } from "../../core/utils/tool-summary.js";
7
7
  import * as ChatLayout from "./ChatLayout.js";
@@ -25,6 +25,56 @@ const ICON_MAP = {
25
25
  BrainCircuit: BrainCircuit,
26
26
  CircleDot: CircleDot,
27
27
  };
28
+ /**
29
+ * Hook to track elapsed time since a given start timestamp
30
+ * Returns elapsed time in seconds, updating every 100ms
31
+ */
32
+ function useElapsedTime(startTime, isRunning) {
33
+ const [elapsed, setElapsed] = useState(0);
34
+ const intervalRef = useRef(null);
35
+ useEffect(() => {
36
+ if (!isRunning || !startTime) {
37
+ setElapsed(0);
38
+ return;
39
+ }
40
+ const updateElapsed = () => {
41
+ setElapsed((Date.now() - startTime) / 1000);
42
+ };
43
+ // Update immediately
44
+ updateElapsed();
45
+ // Update every 100ms for smooth display
46
+ intervalRef.current = setInterval(updateElapsed, 100);
47
+ return () => {
48
+ if (intervalRef.current) {
49
+ clearInterval(intervalRef.current);
50
+ }
51
+ };
52
+ }, [startTime, isRunning]);
53
+ return elapsed;
54
+ }
55
+ /**
56
+ * Format elapsed time for display
57
+ * Shows seconds with 1 decimal place, then minutes and seconds, then hours
58
+ */
59
+ function formatElapsedTime(seconds) {
60
+ if (seconds < 60) {
61
+ return `${seconds.toFixed(1)}s`;
62
+ }
63
+ const hours = Math.floor(seconds / 3600);
64
+ const mins = Math.floor((seconds % 3600) / 60);
65
+ const secs = Math.floor(seconds % 60);
66
+ if (hours > 0) {
67
+ return `${hours}h ${mins}m ${secs}s`;
68
+ }
69
+ return `${mins}m ${secs}s`;
70
+ }
71
+ /**
72
+ * Component to display running duration for a tool call
73
+ */
74
+ function RunningDuration({ startTime }) {
75
+ const elapsed = useElapsedTime(startTime, true);
76
+ return (_jsx("span", { className: "text-xs text-text-secondary/70 tabular-nums ml-1", children: formatElapsedTime(elapsed) }));
77
+ }
28
78
  /**
29
79
  * CompactionDetails component - shows detailed stats when tool response was compacted
30
80
  */
@@ -179,13 +229,24 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
179
229
  const displayText = getDisplayText();
180
230
  // For preliminary/selecting states, show simple non-expandable text
181
231
  if (isSelecting && !isGrouped) {
182
- return (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/50", children: displayText }) }));
232
+ return (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [
233
+ _jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt }))] })
234
+ ] }) }));
183
235
  }
184
236
  // If it's a grouped preliminary (selecting) state
185
237
  if (isSelecting && isGrouped) {
238
+ // Find earliest start time among selecting tools
239
+ const selectingStartTime = (() => {
240
+ const startTimes = toolCalls
241
+ .map((tc) => tc.startedAt)
242
+ .filter((t) => t != null);
243
+ if (startTimes.length === 0)
244
+ return null;
245
+ return Math.min(...startTimes);
246
+ })();
186
247
  return (_jsxs("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: [
187
248
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
188
- _jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
249
+ _jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
189
250
  ] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })
190
251
  ] }));
191
252
  }
@@ -193,7 +254,19 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
193
254
  return (_jsxs("div", { className: "flex flex-col my-4", children: [
194
255
  _jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit rounded-md px-1 -mx-1", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [
195
256
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
196
- _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), isGrouped && (_jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })), isFailed && (_jsx("span", { title: isGrouped
257
+ _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), !isGrouped &&
258
+ singleToolCall?.startedAt &&
259
+ displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt })), isGrouped &&
260
+ displayState === "executing" &&
261
+ (() => {
262
+ const startTimes = toolCalls
263
+ .map((tc) => tc.startedAt)
264
+ .filter((t) => t != null);
265
+ if (startTimes.length === 0)
266
+ return null;
267
+ const earliestStart = Math.min(...startTimes);
268
+ return _jsx(RunningDuration, { startTime: earliestStart });
269
+ })(), isGrouped && (_jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })), isFailed && (_jsx("span", { title: isGrouped
197
270
  ? `${toolCalls.filter((tc) => tc.status === "failed").length} of ${toolCalls.length} operations failed`
198
271
  : singleToolCall?.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), isGrouped && groupHasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
199
272
  _jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: groupHasTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: groupHasTruncation
@@ -271,11 +344,12 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
271
344
  hookNotification?.metadata?.action === "compacted_then_truncated" ||
272
345
  toolCall._meta?.compactionAction === "truncated");
273
346
  const isFailed = toolCall.status === "failed";
347
+ const isRunning = toolCall.status === "pending" || toolCall.status === "in_progress";
274
348
  if (isSubagentCall) {
275
349
  // Render subagent with clickable header and SubAgentDetails component
276
350
  return (_jsxs("div", { className: "flex flex-col ml-5", children: [
277
351
  _jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [
278
- _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.rawInput?.agentName || "Subagent" }), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
352
+ _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.rawInput?.agentName || "Subagent" }), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
279
353
  _jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
280
354
  const meta = toolCall._meta;
281
355
  const percentage = meta?.originalTokens && meta?.finalTokens
@@ -297,7 +371,7 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
297
371
  // Regular tool call - collapsible with clickable header
298
372
  return (_jsxs("div", { className: "flex flex-col ml-5", children: [
299
373
  _jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [
300
- _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.prettyName || toolCall.title }), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
374
+ _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.prettyName || toolCall.title }), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
301
375
  _jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
302
376
  const meta = toolCall._meta;
303
377
  const percentage = meta?.originalTokens && meta?.finalTokens
@@ -315,11 +389,54 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
315
389
  ] }) })), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })
316
390
  ] }), isExpanded && (_jsx("div", { className: "mt-1", children: _jsx(ToolOperationDetails, { toolCall: toolCall, ...(hookNotification ? { hookNotification } : {}) }) }))] }));
317
391
  }
392
+ /**
393
+ * Helper to parse E2B sandbox output into structured parts
394
+ */
395
+ function parseSandboxOutput(text) {
396
+ let stdout = "";
397
+ let stderr = "";
398
+ let error = "";
399
+ // Split by [stderr] and [error] markers
400
+ const stderrMatch = text.match(/\[stderr\]\n?([\s\S]*?)(?=\[error\]|$)/);
401
+ const errorMatch = text.match(/\[error\]\s*([\s\S]*?)$/);
402
+ if (stderrMatch?.[1]) {
403
+ stderr = stderrMatch[1].trim();
404
+ }
405
+ if (errorMatch?.[1]) {
406
+ error = errorMatch[1].trim();
407
+ }
408
+ // stdout is everything before [stderr] or [error]
409
+ const firstMarkerIndex = Math.min(text.includes("[stderr]") ? text.indexOf("[stderr]") : text.length, text.includes("[error]") ? text.indexOf("[error]") : text.length);
410
+ stdout = text.substring(0, firstMarkerIndex).trim();
411
+ return { stdout, stderr, error };
412
+ }
413
+ /**
414
+ * Check if tool is an E2B sandbox code execution tool
415
+ */
416
+ function isSandboxCodeTool(toolTitle) {
417
+ return toolTitle === "Sandbox_RunCode" || toolTitle === "Sandbox_RunBash";
418
+ }
419
+ /**
420
+ * Console tabs component for sandbox output - mimics browser console
421
+ */
422
+ function SandboxConsoleTabs({ stdout, stderr, }) {
423
+ const [activeTab, setActiveTab] = useState(stderr ? "stderr" : "stdout");
424
+ return (_jsxs("div", { className: "mx-3 mb-3 border border-border rounded-md overflow-hidden bg-zinc-950 dark:bg-zinc-950", children: [
425
+ _jsxs("div", { className: "flex border-b border-zinc-800 bg-zinc-900", children: [
426
+ _jsx("button", { type: "button", onClick: () => setActiveTab("stdout"), className: `px-3 py-1.5 text-[11px] font-mono transition-colors border-b-2 ${activeTab === "stdout"
427
+ ? "text-zinc-100 border-zinc-100 bg-zinc-800"
428
+ : "text-zinc-500 border-transparent hover:text-zinc-300"}`, children: "stdout" }), _jsxs("button", { type: "button", onClick: () => setActiveTab("stderr"), className: `px-3 py-1.5 text-[11px] font-mono transition-colors border-b-2 flex items-center gap-1.5 ${activeTab === "stderr"
429
+ ? "text-zinc-100 border-zinc-100 bg-zinc-800"
430
+ : "text-zinc-500 border-transparent hover:text-zinc-300"}`, children: ["stderr", stderr && _jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500" })] })
431
+ ] }), _jsxs("div", { className: "p-3 max-h-[300px] overflow-auto", children: [activeTab === "stdout" && (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-zinc-300", children: stdout || (_jsx("span", { className: "text-zinc-600 italic", children: "(no output)" })) })), activeTab === "stderr" && (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-red-400", children: stderr || (_jsx("span", { className: "text-zinc-600 italic", children: "(no errors)" })) }))] })
432
+ ] }));
433
+ }
318
434
  /**
319
435
  * Component to display detailed tool call information
320
436
  */
321
437
  function ToolOperationDetails({ toolCall, hookNotification, }) {
322
438
  const { resolvedTheme } = useTheme();
439
+ const [showLogs, setShowLogs] = useState(false);
323
440
  // Don't show details for preliminary tool calls
324
441
  if (isPreliminaryToolCall(toolCall)) {
325
442
  return (_jsx("div", { className: "text-paragraph-sm text-text-secondary/70 ml-2", children: getToolCallStateVerbiage(toolCall) }));
@@ -359,8 +476,8 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
359
476
  _jsx("div", { className: "text-[10px] font-bold text-text-secondary 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 && loc.line !== undefined && `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })
360
477
  ] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [
361
478
  _jsx("div", { className: "text-[10px] font-bold text-text-secondary 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, shortenTextAfterLength: 80, style: jsonStyle }) })
362
- ] })), toolCall._meta?.compactionAction && (_jsx(CompactionDetails, { compactionAction: toolCall._meta.compactionAction, originalTokens: toolCall._meta.originalTokens, finalTokens: toolCall._meta.finalTokens, originalContentPath: toolCall._meta.originalContentPath })), ((toolCall.content && toolCall.content.length > 0) ||
363
- toolCall.error) && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [
479
+ ] })), toolCall._meta?.compactionAction && (_jsx(CompactionDetails, { compactionAction: toolCall._meta.compactionAction, originalTokens: toolCall._meta.originalTokens, finalTokens: toolCall._meta.finalTokens, originalContentPath: toolCall._meta.originalContentPath })), ((toolCall.content && toolCall.content.length > 0) || toolCall.error) &&
480
+ !isSandboxCodeTool(toolCall.title) && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [
364
481
  _jsx("div", { className: "text-[10px] font-bold text-text-secondary 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) => {
365
482
  // Generate a stable key based on content
366
483
  const getBlockKey = () => {
@@ -379,7 +496,7 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
379
496
  }
380
497
  return `block-${idx}`;
381
498
  };
382
- // Helper to render text content
499
+ // Helper to render text content (for non-sandbox tools or non-log content)
383
500
  const renderTextContent = (text, key) => {
384
501
  try {
385
502
  const parsed = JSON.parse(text);
@@ -397,14 +514,20 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
397
514
  }
398
515
  return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-foreground overflow-x-auto", children: text }, key));
399
516
  };
400
- // Handle nested content blocks
517
+ // Skip text content for sandbox tools - it's shown in Console tabs
518
+ const isSandbox = isSandboxCodeTool(toolCall.title);
519
+ if (isSandbox &&
520
+ (block.type === "text" || block.type === "content")) {
521
+ return null;
522
+ }
523
+ // Handle nested content blocks (non-sandbox tools)
401
524
  if (block.type === "content" && "content" in block) {
402
525
  const innerContent = block.content;
403
526
  if (innerContent.type === "text" && innerContent.text) {
404
527
  return renderTextContent(innerContent.text, getBlockKey());
405
528
  }
406
529
  }
407
- // Handle direct text blocks
530
+ // Handle direct text blocks (non-sandbox tools)
408
531
  if (block.type === "text" && "text" in block) {
409
532
  return renderTextContent(block.text, getBlockKey());
410
533
  }
@@ -440,7 +563,40 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
440
563
  }
441
564
  return null;
442
565
  }), toolCall.error && (_jsxs("div", { className: "text-destructive font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })
443
- ] })), toolCall._meta?.truncationWarning && (_jsxs("div", { className: "mx-3 mt-3 mb-0 flex items-center gap-2 rounded-md bg-yellow-50 dark:bg-yellow-950/20 px-3 py-2 text-[11px] text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-900", children: [
566
+ ] })), isSandboxCodeTool(toolCall.title) &&
567
+ (() => {
568
+ // Find text content from blocks (handles both direct text and nested content)
569
+ const findBlockText = () => {
570
+ for (const block of toolCall.content || []) {
571
+ if (block.type === "text" && "text" in block) {
572
+ return block.text;
573
+ }
574
+ if (block.type === "content" && "content" in block) {
575
+ const innerContent = block.content;
576
+ if (innerContent.type === "text" && innerContent.text) {
577
+ return innerContent.text;
578
+ }
579
+ }
580
+ }
581
+ return null;
582
+ };
583
+ const blockText = findBlockText();
584
+ if (!blockText)
585
+ return null;
586
+ const parsed = parseSandboxOutput(blockText);
587
+ // Combine stderr and error into one stderr output (errors go to stderr)
588
+ const stderrContent = [parsed.stderr, parsed.error]
589
+ .filter(Boolean)
590
+ .join("\n");
591
+ const hasOutput = parsed.stdout || stderrContent;
592
+ if (!hasOutput)
593
+ return null;
594
+ return (_jsxs("div", { className: "border-b border-border last:border-0", children: [
595
+ _jsxs("button", { type: "button", onClick: () => setShowLogs(!showLogs), className: "w-full p-3 flex items-center justify-between cursor-pointer bg-transparent border-none text-left hover:bg-muted/50 transition-colors", children: [
596
+ _jsxs("div", { className: "flex items-center gap-2", children: [
597
+ _jsx(Terminal, { className: "h-3.5 w-3.5 text-text-secondary" }), _jsx("span", { className: "text-[10px] font-bold text-text-secondary uppercase tracking-wider font-sans", children: "Console" }), parsed.error && (_jsx("span", { className: "px-1.5 py-0.5 bg-destructive/10 text-destructive rounded text-[9px] font-medium", children: "Error" }))] }), _jsx(ChevronDown, { className: `h-3.5 w-3.5 text-text-secondary transition-transform duration-200 ${showLogs ? "rotate-180" : ""}` })
598
+ ] }), showLogs && (_jsx(SandboxConsoleTabs, { stdout: parsed.stdout, stderr: stderrContent }))] }));
599
+ })(), toolCall._meta?.truncationWarning && (_jsxs("div", { className: "mx-3 mt-3 mb-0 flex items-center gap-2 rounded-md bg-yellow-50 dark:bg-yellow-950/20 px-3 py-2 text-[11px] text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-900", children: [
444
600
  _jsx("span", { className: "text-yellow-600 dark:text-yellow-500", children: "\u26A0\uFE0F" }), _jsx("span", { children: toolCall._meta.truncationWarning })
445
601
  ] })), hookNotification &&
446
602
  hookNotification.status === "completed" &&
@@ -459,5 +615,7 @@ function ToolOperationDetails({ toolCall, hookNotification, }) {
459
615
  _jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Input:" }), toolCall.tokenUsage.inputTokens.toLocaleString()] })), toolCall.tokenUsage.outputTokens !== undefined && (_jsxs("div", { children: [
460
616
  _jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Output:" }), toolCall.tokenUsage.outputTokens.toLocaleString()] })), toolCall.tokenUsage.totalTokens !== undefined && (_jsxs("div", { children: [
461
617
  _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: [
462
- _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)"] }))] }))] }))] }));
618
+ _jsxs("span", { children: ["Started: ", new Date(toolCall.startedAt).toLocaleTimeString()] }), toolCall.completedAt && (_jsxs(_Fragment, { children: [
619
+ _jsx("span", { children: "-" }), _jsxs("span", { children: ["Completed:", " ", new Date(toolCall.completedAt).toLocaleTimeString()] }), _jsx("span", { children: "-" }), _jsxs("span", { children: ["Duration:", " ", formatElapsedTime((toolCall.completedAt - toolCall.startedAt) / 1000)] })
620
+ ] }))] }))] }))] }));
463
621
  }
@@ -0,0 +1,18 @@
1
+ import type { FileSystemItem, FileSystemProvider } from "../types/filesystem.js";
2
+ interface SandboxConnection {
3
+ sessionId: string;
4
+ apiUrl: string;
5
+ }
6
+ /**
7
+ * FileSystemProvider implementation that connects to E2B sandbox filesystem
8
+ * via the agent's HTTP API endpoints.
9
+ */
10
+ export declare class SandboxFileSystemProvider implements FileSystemProvider {
11
+ private connection;
12
+ constructor(connection: SandboxConnection);
13
+ getRootItems(): Promise<FileSystemItem[]>;
14
+ getItemChildren(itemId: string): Promise<FileSystemItem[]>;
15
+ getItemDetails(itemId: string): Promise<FileSystemItem>;
16
+ private toFileSystemItem;
17
+ }
18
+ export {};
@@ -0,0 +1,76 @@
1
+ /**
2
+ * FileSystemProvider implementation that connects to E2B sandbox filesystem
3
+ * via the agent's HTTP API endpoints.
4
+ */
5
+ export class SandboxFileSystemProvider {
6
+ connection;
7
+ constructor(connection) {
8
+ this.connection = connection;
9
+ }
10
+ async getRootItems() {
11
+ try {
12
+ const response = await fetch(`${this.connection.apiUrl}/sandbox/files?sessionId=${this.connection.sessionId}&path=/home/user`);
13
+ if (!response.ok) {
14
+ console.error("Failed to fetch sandbox files:", await response.text());
15
+ return [];
16
+ }
17
+ const { files } = (await response.json());
18
+ return files.map(this.toFileSystemItem);
19
+ }
20
+ catch (error) {
21
+ console.error("Error fetching sandbox root items:", error);
22
+ return [];
23
+ }
24
+ }
25
+ async getItemChildren(itemId) {
26
+ try {
27
+ // itemId is the full path in the sandbox
28
+ const response = await fetch(`${this.connection.apiUrl}/sandbox/files?sessionId=${this.connection.sessionId}&path=${encodeURIComponent(itemId)}`);
29
+ if (!response.ok) {
30
+ console.error("Failed to fetch sandbox directory:", await response.text());
31
+ return [];
32
+ }
33
+ const { files } = (await response.json());
34
+ return files.map(this.toFileSystemItem);
35
+ }
36
+ catch (error) {
37
+ console.error("Error fetching sandbox item children:", error);
38
+ return [];
39
+ }
40
+ }
41
+ async getItemDetails(itemId) {
42
+ try {
43
+ // For now, we'll fetch the parent directory and find the item
44
+ // In a more sophisticated implementation, this could call a dedicated endpoint
45
+ const parentPath = itemId.substring(0, itemId.lastIndexOf("/"));
46
+ const response = await fetch(`${this.connection.apiUrl}/sandbox/files?sessionId=${this.connection.sessionId}&path=${encodeURIComponent(parentPath || "/")}`);
47
+ if (!response.ok) {
48
+ throw new Error(`Failed to fetch file details: ${await response.text()}`);
49
+ }
50
+ const { files } = (await response.json());
51
+ const file = files.find((f) => f.path === itemId);
52
+ if (!file) {
53
+ throw new Error(`File not found: ${itemId}`);
54
+ }
55
+ return this.toFileSystemItem(file);
56
+ }
57
+ catch (error) {
58
+ console.error("Error fetching sandbox item details:", error);
59
+ throw error;
60
+ }
61
+ }
62
+ toFileSystemItem = (file) => {
63
+ const extension = file.type === "file" && file.name.includes(".")
64
+ ? file.name.split(".").pop()
65
+ : undefined;
66
+ return {
67
+ id: file.path,
68
+ name: file.name,
69
+ type: file.type === "dir" ? "folder" : "file",
70
+ path: file.path,
71
+ size: file.size,
72
+ lastModified: new Date(file.lastModified),
73
+ ...(extension !== undefined && { extension }),
74
+ };
75
+ };
76
+ }
@@ -112,6 +112,19 @@ export declare class AcpClient {
112
112
  * Subscribe to errors
113
113
  */
114
114
  onError(handler: (error: Error) => void): () => void;
115
+ /**
116
+ * Subscribe to file system change events for a specific session
117
+ * Returns unsubscribe function
118
+ */
119
+ onFileSystemChange(sessionId: string, callback: (event: {
120
+ sessionId: string;
121
+ paths?: string[];
122
+ _meta?: {
123
+ source?: "tool" | "poll";
124
+ toolName?: string;
125
+ isReplay?: boolean;
126
+ };
127
+ }) => void): () => void;
115
128
  /**
116
129
  * Get agent information
117
130
  * - displayName: Human-readable name for UI (preferred)
@@ -156,6 +169,11 @@ export declare class AcpClient {
156
169
  defaultOptionId?: string;
157
170
  }>;
158
171
  };
172
+ /**
173
+ * Get the base URL for HTTP transport
174
+ * Returns undefined for non-HTTP transports
175
+ */
176
+ getBaseUrl(): string | undefined;
159
177
  /**
160
178
  * Create transport based on explicit configuration
161
179
  */
@@ -165,5 +183,4 @@ export declare class AcpClient {
165
183
  private handleError;
166
184
  private updateSessionStatus;
167
185
  private generateSessionId;
168
- private generateMessageId;
169
186
  }
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "@townco/core";
2
+ import { generateMessageId } from "../../core/schemas/chat.js";
2
3
  import { HttpTransport } from "../transports/http.js";
3
4
  import { StdioTransport } from "../transports/stdio.js";
4
5
  import { WebSocketTransport } from "../transports/websocket.js";
@@ -184,7 +185,7 @@ export class AcpClient {
184
185
  });
185
186
  // Create message
186
187
  const message = {
187
- id: this.generateMessageId(),
188
+ id: generateMessageId("user"),
188
189
  role: "user",
189
190
  content: contentBlocks,
190
191
  timestamp: new Date().toISOString(),
@@ -301,6 +302,17 @@ export class AcpClient {
301
302
  this.errorHandlers.delete(handler);
302
303
  };
303
304
  }
305
+ /**
306
+ * Subscribe to file system change events for a specific session
307
+ * Returns unsubscribe function
308
+ */
309
+ onFileSystemChange(sessionId, callback) {
310
+ if (this.transport && "onFileSystemChange" in this.transport) {
311
+ return this.transport.onFileSystemChange(sessionId, callback);
312
+ }
313
+ // Return no-op unsubscribe for transports that don't support file system changes
314
+ return () => { };
315
+ }
304
316
  /**
305
317
  * Get agent information
306
318
  * - displayName: Human-readable name for UI (preferred)
@@ -314,6 +326,16 @@ export class AcpClient {
314
326
  getAgentInfo() {
315
327
  return this.transport.getAgentInfo?.() || {};
316
328
  }
329
+ /**
330
+ * Get the base URL for HTTP transport
331
+ * Returns undefined for non-HTTP transports
332
+ */
333
+ getBaseUrl() {
334
+ if (this.config.type === "http") {
335
+ return this.config.options.baseUrl;
336
+ }
337
+ return undefined;
338
+ }
317
339
  /**
318
340
  * Create transport based on explicit configuration
319
341
  */
@@ -394,7 +416,4 @@ export class AcpClient {
394
416
  generateSessionId() {
395
417
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
396
418
  }
397
- generateMessageId() {
398
- return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
399
- }
400
419
  }
@@ -8,6 +8,7 @@ export declare class HttpTransport implements Transport {
8
8
  private connected;
9
9
  private sessionUpdateCallbacks;
10
10
  private errorCallbacks;
11
+ private fileSystemChangeCallbacks;
11
12
  private messageQueue;
12
13
  private currentSessionId;
13
14
  private chunkResolvers;
@@ -51,6 +52,12 @@ export declare class HttpTransport implements Transport {
51
52
  isConnected(): boolean;
52
53
  onSessionUpdate(callback: (update: SessionUpdate) => void): () => void;
53
54
  onError(callback: (error: Error) => void): () => void;
55
+ onFileSystemChange(sessionId: string, callback: (event: {
56
+ sessionId: string;
57
+ paths?: string[];
58
+ source?: string;
59
+ }) => void): () => void;
60
+ private notifyFileSystemChange;
54
61
  getAgentInfo(): {
55
62
  name?: string;
56
63
  displayName?: string;
@@ -1,5 +1,6 @@
1
1
  import * as acp from "@agentclientprotocol/sdk";
2
2
  import { createLogger } from "@townco/core";
3
+ import { generateMessageId } from "../../core/schemas/chat.js";
3
4
  import { httpTransportOptionsSchema, } from "./types.js";
4
5
  const logger = createLogger("http-transport");
5
6
  /**
@@ -10,6 +11,7 @@ export class HttpTransport {
10
11
  connected = false;
11
12
  sessionUpdateCallbacks = new Set();
12
13
  errorCallbacks = new Set();
14
+ fileSystemChangeCallbacks = new Map();
13
15
  messageQueue = [];
14
16
  currentSessionId = null;
15
17
  chunkResolvers = [];
@@ -573,6 +575,25 @@ export class HttpTransport {
573
575
  this.errorCallbacks.delete(callback);
574
576
  };
575
577
  }
578
+ onFileSystemChange(sessionId, callback) {
579
+ logger.debug("Registering file system change callback", { sessionId });
580
+ this.fileSystemChangeCallbacks.set(sessionId, callback);
581
+ return () => {
582
+ logger.debug("Unregistering file system change callback", { sessionId });
583
+ this.fileSystemChangeCallbacks.delete(sessionId);
584
+ };
585
+ }
586
+ notifyFileSystemChange(event) {
587
+ const callback = this.fileSystemChangeCallbacks.get(event.sessionId);
588
+ logger.debug("notifyFileSystemChange", {
589
+ sessionId: event.sessionId,
590
+ hasCallback: !!callback,
591
+ callbackCount: this.fileSystemChangeCallbacks.size,
592
+ });
593
+ if (callback) {
594
+ callback(event);
595
+ }
596
+ }
576
597
  getAgentInfo() {
577
598
  return this.agentInfo || {};
578
599
  }
@@ -835,6 +856,28 @@ export class HttpTransport {
835
856
  logger.debug("Update session type", {
836
857
  sessionUpdate: update?.sessionUpdate,
837
858
  });
859
+ // Handle sandbox file changes
860
+ // Type assertion needed because TypeScript doesn't recognize this as a valid session update type
861
+ const sessionUpdateValue = update?.sessionUpdate;
862
+ if (sessionUpdateValue === "sandbox_files_changed") {
863
+ const sandboxUpdate = update;
864
+ const isReplay = sandboxUpdate._meta?.isReplay === true;
865
+ if (!isReplay) {
866
+ logger.debug("Sandbox files changed", {
867
+ paths: sandboxUpdate.paths,
868
+ source: sandboxUpdate._meta?.source,
869
+ });
870
+ // Notify callbacks
871
+ this.notifyFileSystemChange({
872
+ sessionId,
873
+ ...(sandboxUpdate.paths && { paths: sandboxUpdate.paths }),
874
+ ...(sandboxUpdate._meta?.source && {
875
+ source: sandboxUpdate._meta.source,
876
+ }),
877
+ });
878
+ }
879
+ return;
880
+ }
838
881
  // Handle ACP tool call notifications
839
882
  if (update?.sessionUpdate === "tool_call") {
840
883
  logger.debug("Tool call notification", {
@@ -1472,7 +1515,7 @@ export class HttpTransport {
1472
1515
  sessionId,
1473
1516
  status: "active",
1474
1517
  message: {
1475
- id: `msg_${Date.now()}_assistant`,
1518
+ id: generateMessageId("assistant"),
1476
1519
  role: "assistant",
1477
1520
  content: [
1478
1521
  {
@@ -1508,7 +1551,7 @@ export class HttpTransport {
1508
1551
  sessionId,
1509
1552
  status: "active",
1510
1553
  message: {
1511
- id: `msg_${Date.now()}_user`,
1554
+ id: generateMessageId("user"),
1512
1555
  role: "user",
1513
1556
  content: [
1514
1557
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.99",
3
+ "version": "0.1.100",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@radix-ui/react-slot": "^1.2.4",
50
50
  "@radix-ui/react-tabs": "^1.1.13",
51
51
  "@radix-ui/react-tooltip": "^1.2.8",
52
- "@townco/core": "0.0.77",
52
+ "@townco/core": "0.0.78",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.96",
70
+ "@townco/tsconfig": "0.1.97",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",
@@ -231,6 +231,31 @@
231
231
  button {
232
232
  cursor: pointer;
233
233
  }
234
+
235
+ /* Custom scrollbar styling - overlay style without gutter */
236
+ ::-webkit-scrollbar {
237
+ width: 8px;
238
+ height: 8px;
239
+ }
240
+
241
+ ::-webkit-scrollbar-track {
242
+ background: transparent;
243
+ }
244
+
245
+ ::-webkit-scrollbar-thumb {
246
+ background: var(--muted-foreground);
247
+ border-radius: 4px;
248
+ }
249
+
250
+ ::-webkit-scrollbar-thumb:hover {
251
+ background: var(--foreground);
252
+ }
253
+
254
+ /* Firefox scrollbar styling */
255
+ * {
256
+ scrollbar-width: thin;
257
+ scrollbar-color: var(--muted-foreground) transparent;
258
+ }
234
259
  }
235
260
 
236
261
  /* Streamdown list styling */