@townco/ui 0.1.99 → 0.1.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/hooks/use-chat-messages.js +5 -4
- package/dist/core/schemas/chat.d.ts +6 -0
- package/dist/core/schemas/chat.js +21 -0
- package/dist/core/store/chat-store.d.ts +1 -1
- package/dist/core/store/chat-store.js +4 -3
- package/dist/gui/components/ChatPanelTabContent.d.ts +4 -0
- package/dist/gui/components/ChatPanelTabContent.js +60 -16
- package/dist/gui/components/ChatView.js +3 -3
- package/dist/gui/components/FileSystemItem.d.ts +1 -3
- package/dist/gui/components/FileSystemItem.js +7 -13
- package/dist/gui/components/FileSystemView.d.ts +1 -3
- package/dist/gui/components/FileSystemView.js +17 -3
- package/dist/gui/components/HookNotification.js +23 -6
- package/dist/gui/components/SourceListItem.js +2 -2
- package/dist/gui/components/TodoListItem.js +2 -2
- package/dist/gui/components/ToolOperation.js +173 -15
- package/dist/gui/providers/SandboxFileSystemProvider.d.ts +18 -0
- package/dist/gui/providers/SandboxFileSystemProvider.js +76 -0
- package/dist/sdk/client/acp-client.d.ts +18 -1
- package/dist/sdk/client/acp-client.js +23 -4
- package/dist/sdk/transports/http.d.ts +7 -0
- package/dist/sdk/transports/http.js +45 -2
- package/package.json +3 -3
- package/src/styles/global.css +25 -0
|
@@ -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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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,
|
|
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
|
|
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,
|
|
11
|
-
export function FileSystemItem({ item, level = 0, onSelect, selectedId, isDropTarget = false, onDownload,
|
|
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" }) }) }),
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
] })),
|
|
163
|
-
|
|
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-
|
|
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
|
|
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:
|
|
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 &&
|
|
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.
|
|
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
|
-
//
|
|
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.
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
3
|
+
"version": "0.1.101",
|
|
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.
|
|
52
|
+
"@townco/core": "0.0.79",
|
|
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.
|
|
70
|
+
"@townco/tsconfig": "0.1.98",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"@types/unist": "^3.0.3",
|
package/src/styles/global.css
CHANGED
|
@@ -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 */
|