@townco/ui 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  2. package/dist/core/hooks/use-chat-session.js +33 -15
  3. package/dist/core/schemas/chat.d.ts +1 -1
  4. package/dist/core/store/chat-store.js +17 -2
  5. package/dist/gui/components/Button.js +1 -1
  6. package/dist/gui/components/ChatEmptyState.js +1 -1
  7. package/dist/gui/components/ChatHeader.js +2 -2
  8. package/dist/gui/components/ChatInput.js +1 -1
  9. package/dist/gui/components/ChatInputCommandMenu.js +1 -1
  10. package/dist/gui/components/ChatLayout.js +1 -1
  11. package/dist/gui/components/ChatPanelTabContent.d.ts +3 -0
  12. package/dist/gui/components/ChatPanelTabContent.js +24 -5
  13. package/dist/gui/components/ChatSecondaryPanel.js +3 -3
  14. package/dist/gui/components/ChatView.js +28 -9
  15. package/dist/gui/components/Dialog.js +2 -2
  16. package/dist/gui/components/DropdownMenu.js +7 -7
  17. package/dist/gui/components/FileSystemItem.d.ts +17 -0
  18. package/dist/gui/components/FileSystemItem.js +81 -0
  19. package/dist/gui/components/FileSystemView.d.ts +14 -0
  20. package/dist/gui/components/FileSystemView.js +46 -0
  21. package/dist/gui/components/Input.js +1 -1
  22. package/dist/gui/components/Label.js +1 -1
  23. package/dist/gui/components/MarkdownRenderer.js +5 -5
  24. package/dist/gui/components/Message.d.ts +1 -1
  25. package/dist/gui/components/MessageContent.js +8 -8
  26. package/dist/gui/components/PanelTabsHeader.js +1 -1
  27. package/dist/gui/components/Reasoning.js +2 -2
  28. package/dist/gui/components/Response.js +13 -11
  29. package/dist/gui/components/Select.js +3 -3
  30. package/dist/gui/components/SourceListItem.js +1 -1
  31. package/dist/gui/components/Tabs.js +1 -1
  32. package/dist/gui/components/Task.js +2 -2
  33. package/dist/gui/components/Textarea.js +1 -1
  34. package/dist/gui/components/ThinkingBlock.js +2 -2
  35. package/dist/gui/components/TodoList.js +1 -1
  36. package/dist/gui/components/ToolCall.js +67 -70
  37. package/dist/gui/components/ToolCallList.js +1 -1
  38. package/dist/gui/components/index.d.ts +4 -0
  39. package/dist/gui/components/index.js +4 -0
  40. package/dist/gui/data/mockFileSystemData.d.ts +21 -0
  41. package/dist/gui/data/mockFileSystemData.js +127 -0
  42. package/dist/gui/types/filesystem.d.ts +27 -0
  43. package/dist/gui/types/filesystem.js +5 -0
  44. package/dist/sdk/schemas/session.d.ts +12 -12
  45. package/package.json +3 -3
  46. package/src/styles/global.css +108 -0
  47. package/dist/core/lib/logger.d.ts +0 -59
  48. package/dist/core/lib/logger.js +0 -191
  49. package/dist/tui/components/LogsPanel.d.ts +0 -5
  50. package/dist/tui/components/LogsPanel.js +0 -29
@@ -3,7 +3,7 @@ import type { AcpClient } from "../../sdk/client/index.js";
3
3
  * Hook for managing chat session lifecycle
4
4
  */
5
5
  export declare function useChatSession(client: AcpClient | null, initialSessionId?: string | null): {
6
- connectionStatus: "error" | "disconnected" | "connecting" | "connected";
6
+ connectionStatus: "error" | "connecting" | "connected" | "disconnected";
7
7
  sessionId: string | null;
8
8
  connect: () => Promise<void>;
9
9
  loadSession: (sessionIdToLoad: string) => Promise<void>;
@@ -27,21 +27,39 @@ export function useChatSession(client, initialSessionId) {
27
27
  // Convert SDK message to DisplayMessage
28
28
  // Filter out tool messages as they're not displayed in the chat
29
29
  if (update.message.role !== "tool") {
30
- const displayMessage = {
31
- id: update.message.id,
32
- role: update.message.role,
33
- content: update.message.content
34
- .map((c) => {
35
- if (c.type === "text") {
36
- return c.text;
37
- }
38
- return "";
39
- })
40
- .join(""),
41
- timestamp: update.message.timestamp,
42
- isStreaming: false,
43
- };
44
- addMessage(displayMessage);
30
+ const textContent = update.message.content
31
+ .map((c) => {
32
+ if (c.type === "text") {
33
+ return c.text;
34
+ }
35
+ return "";
36
+ })
37
+ .join("");
38
+ // During session replay, chunks for the same message arrive separately
39
+ // Check if we should append to the last assistant message or create a new one
40
+ const messages = useChatStore.getState().messages;
41
+ const lastMessage = messages[messages.length - 1];
42
+ if (update.message.role === "assistant" &&
43
+ lastMessage?.role === "assistant" &&
44
+ lastMessage.content === "" &&
45
+ textContent) {
46
+ // Append text to existing empty assistant message (created by tool call)
47
+ logger.debug("Appending text to existing assistant message");
48
+ useChatStore.getState().updateMessage(lastMessage.id, {
49
+ content: lastMessage.content + textContent,
50
+ });
51
+ }
52
+ else {
53
+ // Create new message
54
+ const displayMessage = {
55
+ id: update.message.id,
56
+ role: update.message.role,
57
+ content: textContent,
58
+ timestamp: update.message.timestamp,
59
+ isStreaming: false,
60
+ };
61
+ addMessage(displayMessage);
62
+ }
45
63
  }
46
64
  }
47
65
  });
@@ -193,8 +193,8 @@ export type ChatSessionState = z.infer<typeof ChatSessionState>;
193
193
  */
194
194
  export declare const ConnectionStatus: z.ZodEnum<{
195
195
  error: "error";
196
- disconnected: "disconnected";
197
196
  connecting: "connecting";
198
197
  connected: "connected";
198
+ disconnected: "disconnected";
199
199
  }>;
200
200
  export type ConnectionStatus = z.infer<typeof ConnectionStatus>;
@@ -155,8 +155,23 @@ export const useChatStore = create((set) => ({
155
155
  // Find the most recent assistant message (which should be streaming)
156
156
  const lastAssistantIndex = state.messages.findLastIndex((msg) => msg.role === "assistant");
157
157
  if (lastAssistantIndex === -1) {
158
- logger.warn("No assistant message found to add tool call to");
159
- return state;
158
+ // No assistant message exists yet - create one with the tool call
159
+ // This happens during session replay when a tool call comes before any text
160
+ logger.debug("No assistant message found, creating one for tool call at position 0");
161
+ const newMessage = {
162
+ id: `msg_${Date.now()}_assistant`,
163
+ role: "assistant",
164
+ content: "",
165
+ timestamp: new Date().toISOString(),
166
+ isStreaming: false,
167
+ toolCalls: [
168
+ {
169
+ ...toolCall,
170
+ contentPosition: 0, // Tool call at the start
171
+ },
172
+ ],
173
+ };
174
+ return { messages: [...state.messages, newMessage] };
160
175
  }
161
176
  const messages = [...state.messages];
162
177
  const lastAssistantMsg = messages[lastAssistantIndex];
@@ -3,7 +3,7 @@ import { Slot } from "@radix-ui/react-slot";
3
3
  import { cva } from "class-variance-authority";
4
4
  import * as React from "react";
5
5
  import { cn } from "../lib/utils.js";
6
- const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer", {
6
+ const buttonVariants = cva("inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-paragraph-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer", {
7
7
  variants: {
8
8
  variant: {
9
9
  default: "bg-primary text-primary-foreground hover:bg-primary-hover",
@@ -17,6 +17,6 @@ export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl,
17
17
  for (let i = 0; i < suggestedPrompts.length; i += 2) {
18
18
  promptRows.push(suggestedPrompts.slice(i, i + 2));
19
19
  }
20
- return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start gap-6", className), ...props, children: [_jsx("h3", { className: "text-heading-3 text-text-primary hidden lg:block", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose", children: description }), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-sm font-medium leading-normal text-text-primary", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-text-primary" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-base font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
20
+ return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start gap-6", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose", children: description }), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "flex items-center gap-2 px-3 py-1.5 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm font-medium leading-normal text-text-primary", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-text-primary" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-paragraph font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
21
21
  });
22
22
  ChatEmptyState.displayName = "ChatEmptyState";
@@ -27,7 +27,7 @@ const ChatHeaderRoot = React.forwardRef(({ defaultExpanded = false, expanded: ex
27
27
  });
28
28
  ChatHeaderRoot.displayName = "ChatHeader.Root";
29
29
  const ChatHeaderTitle = React.forwardRef(({ className, children, ...props }, ref) => {
30
- return (_jsx("h1", { ref: ref, className: cn("m-0 text-xl font-semibold", className), ...props, children: children }));
30
+ return (_jsx("h1", { ref: ref, className: cn("m-0 text-subheading font-semibold", className), ...props, children: children }));
31
31
  });
32
32
  ChatHeaderTitle.displayName = "ChatHeader.Title";
33
33
  const ChatHeaderActions = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -61,7 +61,7 @@ const getDefaultStatusText = (status) => {
61
61
  const ChatHeaderStatusIndicator = React.forwardRef(({ status, statusText, className, ...props }, ref) => {
62
62
  const text = statusText ?? getDefaultStatusText(status);
63
63
  const colorClass = getStatusColor(status);
64
- return (_jsxs("div", { ref: ref, className: cn("flex items-center gap-2", className), ...props, children: [_jsx("div", { className: cn("h-2 w-2 rounded-full", colorClass) }), _jsx("span", { className: "text-sm text-muted-foreground", children: text })] }));
64
+ return (_jsxs("div", { ref: ref, className: cn("flex items-center gap-2", className), ...props, children: [_jsx("div", { className: cn("h-2 w-2 rounded-full", colorClass) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: text })] }));
65
65
  });
66
66
  ChatHeaderStatusIndicator.displayName = "ChatHeader.StatusIndicator";
67
67
  const ChatHeaderToggle = React.forwardRef(({ icon, className, children, onClick, ...props }, ref) => {
@@ -201,7 +201,7 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
201
201
  if (asChild && React.isValidElement(children)) {
202
202
  return React.cloneElement(children, fieldProps);
203
203
  }
204
- return (_jsx("textarea", { ...fieldProps, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 field-sizing-content max-h-[6lh]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", className) }));
204
+ return (_jsx("textarea", { ...fieldProps, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 field-sizing-content max-h-[6lh]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-paragraph-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", className) }));
205
205
  });
206
206
  ChatInputField.displayName = "ChatInput.Field";
207
207
  const ChatInputSubmit = React.forwardRef(({ asChild = false, className, disabled: disabledProp, children, ...props }, ref) => {
@@ -57,6 +57,6 @@ export const ChatInputCommandMenu = React.forwardRef(({ commands = [], showComma
57
57
  if (!showCommandMenu || filteredCommands.length === 0) {
58
58
  return null;
59
59
  }
60
- return (_jsxs("div", { ref: ref, className: cn("absolute bottom-full left-0 z-50 mb-2 w-full max-w-md", "rounded-md border border-border bg-card p-2 shadow-lg", className), ...props, children: [_jsx("div", { className: "text-xs font-semibold text-muted-foreground px-2 py-1", children: "Commands" }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredCommands.map((command, index) => (_jsxs("button", { type: "button", onClick: () => command.onSelect(), className: cn("w-full rounded-sm px-2 py-2 text-left text-sm transition-colors", "flex items-start gap-2", "hover:bg-muted", index === selectedMenuIndex && "bg-muted"), children: [command.icon && (_jsx("span", { className: "shrink-0 mt-0.5", children: command.icon })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium", children: command.label }), command.description && (_jsx("div", { className: "text-xs text-muted-foreground truncate", children: command.description }))] })] }, command.id))) })] }));
60
+ return (_jsxs("div", { ref: ref, className: cn("absolute bottom-full left-0 z-50 mb-2 w-full max-w-md", "rounded-md border border-border bg-card p-2 shadow-lg", className), ...props, children: [_jsx("div", { className: "text-caption font-semibold text-muted-foreground px-2 py-1", children: "Commands" }), _jsx("div", { className: "max-h-64 overflow-y-auto", children: filteredCommands.map((command, index) => (_jsxs("button", { type: "button", onClick: () => command.onSelect(), className: cn("w-full rounded-sm px-2 py-2 text-left text-paragraph-sm transition-colors", "flex items-start gap-2", "hover:bg-muted", index === selectedMenuIndex && "bg-muted"), children: [command.icon && (_jsx("span", { className: "shrink-0 mt-0.5", children: command.icon })), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "font-medium", children: command.label }), command.description && (_jsx("div", { className: "text-caption text-muted-foreground truncate", children: command.description }))] })] }, command.id))) })] }));
61
61
  });
62
62
  ChatInputCommandMenu.displayName = "ChatInputCommandMenu";
@@ -72,7 +72,7 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
72
72
  React.useEffect(() => {
73
73
  checkScrollPosition();
74
74
  }, [checkScrollPosition]);
75
- return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto", className), onScroll: handleScroll, ...props, children: _jsx("div", { className: "mx-auto max-w-chat min-h-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: scrollToBottom, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
75
+ return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: scrollToBottom, className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
76
76
  });
77
77
  ChatLayoutMessages.displayName = "ChatLayout.Messages";
78
78
  const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import type { FileSystemProvider } from "../types/filesystem.js";
2
3
  import { type SourceItem } from "./SourceListItem.js";
3
4
  import type { TodoItem } from "./TodoListItem.js";
4
5
  /**
@@ -11,6 +12,8 @@ export interface TodoTabContentProps extends React.HTMLAttributes<HTMLDivElement
11
12
  export declare const TodoTabContent: React.ForwardRefExoticComponent<TodoTabContentProps & React.RefAttributes<HTMLDivElement>>;
12
13
  export interface FilesTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
13
14
  files?: string[];
15
+ provider?: FileSystemProvider;
16
+ onFileSelect?: (filePath: string) => void;
14
17
  }
15
18
  export declare const FilesTabContent: React.ForwardRefExoticComponent<FilesTabContentProps & React.RefAttributes<HTMLDivElement>>;
16
19
  export interface SourcesTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -1,20 +1,39 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { cn } from "../lib/utils.js";
4
+ import { FileSystemView } from "./FileSystemView.js";
4
5
  import { SourceListItem } from "./SourceListItem.js";
5
6
  export const TodoTabContent = React.forwardRef(({ todos, className, ...props }, ref) => {
6
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: todos.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-[200px]", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No todos yet" }) })) : (todos.map((todo) => (_jsx("div", { className: "text-sm", children: todo.text }, todo.id)))) }));
7
+ return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: todos.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-[200px]", children: _jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No todos yet" }) })) : (todos.map((todo) => (_jsx("div", { className: "text-paragraph-sm", children: todo.text }, todo.id)))) }));
7
8
  });
8
9
  TodoTabContent.displayName = "TodoTabContent";
9
- export const FilesTabContent = React.forwardRef(({ files = [], className, ...props }, ref) => {
10
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: files.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-[200px]", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No files attached" }) })) : (files.map((file) => (_jsx("div", { className: "text-sm", children: file }, file)))) }));
10
+ export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
11
+ // Stub handlers for the overflow menu actions
12
+ // Replace these with real implementations when connecting to actual filesystem
13
+ const handleDownload = React.useCallback((item) => {
14
+ console.log("Download:", item.name);
15
+ // TODO: Implement download logic
16
+ }, []);
17
+ const handleRename = React.useCallback((item) => {
18
+ console.log("Rename:", item.name);
19
+ // TODO: Implement rename logic
20
+ }, []);
21
+ const handleDelete = React.useCallback((item) => {
22
+ console.log("Delete:", item.name);
23
+ // TODO: Implement delete logic
24
+ }, []);
25
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(FileSystemView, { ...(provider && { provider }), onItemSelect: (item) => {
26
+ if (item.type === "file" && onFileSelect) {
27
+ onFileSelect(item.path || item.name);
28
+ }
29
+ }, onDownload: handleDownload, onRename: handleRename, onDelete: handleDelete, className: "h-full" }) }));
11
30
  });
12
31
  FilesTabContent.displayName = "FilesTabContent";
13
32
  export const SourcesTabContent = React.forwardRef(({ sources = [], className, ...props }, ref) => {
14
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: sources.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-[200px]", children: _jsx("p", { className: "text-sm text-muted-foreground", children: "No sources available" }) })) : (sources.map((source) => (_jsx(SourceListItem, { source: source }, source.id)))) }));
33
+ return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: sources.length === 0 ? (_jsx("div", { className: "flex items-center justify-center h-full min-h-[200px]", children: _jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No sources available" }) })) : (sources.map((source) => (_jsx(SourceListItem, { source: source }, source.id)))) }));
15
34
  });
16
35
  SourcesTabContent.displayName = "SourcesTabContent";
17
36
  export const DatabaseTabContent = React.forwardRef(({ data, className, ...props }, ref) => {
18
- return (_jsxs("div", { ref: ref, className: cn("space-y-4", className), ...props, children: [_jsx("h3", { className: "font-semibold text-lg", children: "Database" }), _jsxs("div", { className: "text-sm text-muted-foreground", children: [_jsx("p", { children: "Database viewer - panel automatically expanded to large size" }), _jsxs("div", { className: "mt-4 p-4 border border-border rounded", children: [_jsx("p", { children: "Your large data table would go here" }), data && typeof data === "object" ? (_jsx("pre", { className: "mt-2 text-xs overflow-auto", children: JSON.stringify(data, null, 2) })) : null] })] })] }));
37
+ return (_jsxs("div", { ref: ref, className: cn("space-y-4", className), ...props, children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Database" }), _jsxs("div", { className: "text-paragraph-sm text-muted-foreground", children: [_jsx("p", { children: "Database viewer - panel automatically expanded to large size" }), _jsxs("div", { className: "mt-4 p-4 border border-border rounded", children: [_jsx("p", { children: "Your large data table would go here" }), data && typeof data === "object" ? (_jsx("pre", { className: "mt-2 text-caption overflow-auto", children: JSON.stringify(data, null, 2) })) : null] })] })] }));
19
38
  });
20
39
  DatabaseTabContent.displayName = "DatabaseTabContent";
@@ -60,12 +60,12 @@ export const ChatSecondaryPanel = React.forwardRef(({ client, todos, variant = "
60
60
  // Pills variant - Simple background highlight (Figma design)
61
61
  _jsx(TabsList, { className: cn("w-full justify-start bg-transparent p-0 h-auto", "gap-1"), children: tabs.map((tab) => {
62
62
  const Icon = tab.icon;
63
- return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-sm font-medium", "data-[state=active]:bg-zinc-100 data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
63
+ return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-zinc-100 data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
64
64
  }) })) : (
65
65
  // Animated variant - Clip-path animation (original style)
66
- _jsxs("div", { className: "relative mb-4 border-border", children: [_jsx(TabsList, { className: "bg-transparent p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, className: "px-3 py-1 text-sm font-[var(--font-family)] font-medium rounded-none text-foreground opacity-60 data-[state=active]:opacity-100 data-[state=active]:bg-transparent data-[state=active]:shadow-none", children: tab.label }, tab.id))) }), _jsx("div", { ref: containerRef, className: "absolute top-0 left-0 w-full overflow-hidden z-10 pointer-events-none", style: {
66
+ _jsxs("div", { className: "relative mb-4 border-border", children: [_jsx(TabsList, { className: "bg-transparent p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, className: "px-3 py-1 text-paragraph-sm font-medium rounded-none text-foreground opacity-60 data-[state=active]:opacity-100 data-[state=active]:bg-transparent data-[state=active]:shadow-none", children: tab.label }, tab.id))) }), _jsx("div", { ref: containerRef, className: "absolute top-0 left-0 w-full overflow-hidden z-10 pointer-events-none", style: {
67
67
  clipPath: "inset(0 100% 0 0% round 999px)",
68
68
  transition: "clip-path 0.25s ease-out",
69
- }, children: _jsx(TabsList, { className: "bg-secondary p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, ref: activeTab === tab.id ? activeTabElementRef : null, className: "px-3 py-1 text-sm font-[var(--font-family)] font-medium rounded-none text-primary bg-transparent data-[state=active]:shadow-none shadow-none", tabIndex: -1, children: tab.label }, tab.id))) }) })] })), _jsx(TabsContent, { value: "todo", className: variant === "pills" ? "mt-0" : "", children: variant === "pills" ? (_jsx(TodoTabContent, { todos: todosToDisplay })) : (_jsx(TodoList, { todos: todosToDisplay })) }), _jsx(TabsContent, { value: "files", className: variant === "pills" ? "mt-0" : "", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "database", className: variant === "pills" ? "mt-0" : "", children: _jsx("div", { className: "text-sm text-foreground opacity-60 italic", children: "Database tab coming soon..." }) }), _jsx(TabsContent, { value: "sources", className: variant === "pills" ? "mt-0" : "", children: _jsx(SourcesTabContent, {}) })] }) }));
69
+ }, children: _jsx(TabsList, { className: "bg-secondary p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, ref: activeTab === tab.id ? activeTabElementRef : null, className: "px-3 py-1 text-paragraph-sm font-medium rounded-none text-primary bg-transparent data-[state=active]:shadow-none shadow-none", tabIndex: -1, children: tab.label }, tab.id))) }) })] })), _jsx(TabsContent, { value: "todo", className: variant === "pills" ? "mt-0" : "", children: variant === "pills" ? (_jsx(TodoTabContent, { todos: todosToDisplay })) : (_jsx(TodoList, { todos: todosToDisplay })) }), _jsx(TabsContent, { value: "files", className: variant === "pills" ? "mt-0" : "", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "database", className: variant === "pills" ? "mt-0" : "", children: _jsx("div", { className: "text-paragraph-sm text-foreground opacity-60 italic", children: "Database tab coming soon..." }) }), _jsx(TabsContent, { value: "sources", className: variant === "pills" ? "mt-0" : "", children: _jsx(SourcesTabContent, {}) })] }) }));
70
70
  });
71
71
  ChatSecondaryPanel.displayName = "ChatSecondaryPanel";
@@ -8,21 +8,21 @@ import { cn } from "../lib/utils.js";
8
8
  import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, FilesTabContent, Message, MessageContent, PanelTabsHeader, SourcesTabContent, Tabs, TabsContent, TodoTabContent, } from "./index.js";
9
9
  const logger = createLogger("gui");
10
10
  // Mobile header component that uses ChatHeader context
11
- function MobileHeader({ agentName }) {
11
+ function MobileHeader({ agentName, showHeader, }) {
12
12
  const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
13
- return (_jsxs("div", { className: "flex lg:hidden items-center gap-2 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("h1", { className: "text-[20px] font-semibold leading-[1.2] tracking-[-0.4px] text-foreground", children: agentName }), _jsx("div", { className: "flex items-center justify-center shrink-0", children: _jsx(ChevronUp, { className: "size-4 rotate-180 text-muted-foreground" }) })] }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }));
13
+ return (_jsxs("div", { className: "flex lg:hidden items-center gap-2 flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
14
14
  }
15
15
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
16
16
  function AppChatHeader({ agentName, todos, sources, showHeader, }) {
17
17
  const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
18
- return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center gap-2 w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("h1", { className: "text-[20px] font-semibold leading-[1.2] tracking-[-0.4px] text-foreground", children: agentName }), _jsx("div", { className: "flex items-center justify-center shrink-0", children: _jsx(ChevronUp, { className: "size-4 rotate-180 text-muted-foreground" }) })] })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle sidebar", onClick: () => {
18
+ return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center gap-2 w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle sidebar", onClick: () => {
19
19
  setPanelSize(panelSize === "hidden" ? "small" : "hidden");
20
- }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) })] }) })] }));
20
+ }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) })] }) })] }));
21
21
  }
22
22
  export function ChatView({ client, initialSessionId, error: initError, }) {
23
23
  // Use shared hooks from @townco/ui/core - MUST be called before any early returns
24
24
  const { connectionStatus, connect, sessionId } = useChatSession(client, initialSessionId);
25
- const { messages } = useChatMessages(client);
25
+ const { messages, sendMessage } = useChatMessages(client);
26
26
  useToolCalls(client); // Still need to subscribe to tool call events
27
27
  const error = useChatStore((state) => state.error);
28
28
  const [agentName, setAgentName] = useState("Agent");
@@ -141,13 +141,32 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
141
141
  },
142
142
  },
143
143
  ];
144
- return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { className: messages.length > 0 ? "pt-4" : "", children: messages.length === 0 ? (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: "This agent can help you with your tasks. Start a conversation by typing a message below.", suggestedPrompts: [
145
- "Help me debug this code",
144
+ return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: "This agent can help you with your tasks. Start a conversation by typing a message below.", suggestedPrompts: [
145
+ "Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
146
146
  "Explain how this works",
147
147
  "Create a new feature",
148
148
  "Review my changes",
149
149
  ], onPromptClick: (prompt) => {
150
- // TODO: Implement prompt click handler
150
+ sendMessage(prompt);
151
151
  logger.info("Prompt clicked", { prompt });
152
- } }) })) : (_jsx("div", { className: "flex flex-col gap-4 px-4", children: messages.map((message, index) => (_jsx(Message, { message: message, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id))) })) }), _jsx(ChatLayout.Footer, { children: _jsxs(ChatInputRoot, { client: client, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: "Type a message or / for commands...", autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {})] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsxs(Tabs, { defaultValue: "todo", className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, { sources: sources }) })] }) }))] }));
152
+ } }) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
153
+ // Calculate dynamic spacing based on message sequence
154
+ const isFirst = index === 0;
155
+ const previousMessage = isFirst ? null : messages[index - 1];
156
+ let spacingClass = "mt-2";
157
+ if (isFirst) {
158
+ spacingClass = "mt-2";
159
+ }
160
+ else if (message.role === "user") {
161
+ // User message usually starts a new turn
162
+ spacingClass =
163
+ previousMessage?.role === "user" ? "mt-4" : "mt-4";
164
+ }
165
+ else if (message.role === "assistant") {
166
+ // Assistant message is usually a response
167
+ spacingClass =
168
+ previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
169
+ }
170
+ return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
171
+ }) })) }), _jsx(ChatLayout.Footer, { children: _jsxs(ChatInputRoot, { client: client, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: "Type a message or / for commands...", autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {})] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsxs(Tabs, { defaultValue: "todo", className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, { sources: sources }) })] }) }))] }));
153
172
  }
@@ -15,8 +15,8 @@ const DialogHeader = ({ className, ...props }) => (_jsx("div", { className: cn("
15
15
  DialogHeader.displayName = "DialogHeader";
16
16
  const DialogFooter = ({ className, ...props }) => (_jsx("div", { className: cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className), ...props }));
17
17
  DialogFooter.displayName = "DialogFooter";
18
- const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Title, { ref: ref, className: cn("text-lg font-semibold leading-none tracking-tight", className), ...props })));
18
+ const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Title, { ref: ref, className: cn("text-subheading font-semibold leading-none tracking-tight", className), ...props })));
19
19
  DialogTitle.displayName = DialogPrimitive.Title.displayName;
20
- const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Description, { ref: ref, className: cn("text-sm text-muted-foreground", className), ...props })));
20
+ const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (_jsx(DialogPrimitive.Description, { ref: ref, className: cn("text-paragraph-sm text-muted-foreground", className), ...props })));
21
21
  DialogDescription.displayName = DialogPrimitive.Description.displayName;
22
22
  export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription, };
@@ -15,7 +15,7 @@ const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
15
15
  /* -------------------------------------------------------------------------------------------------
16
16
  * DropdownMenuSubTrigger
17
17
  * -----------------------------------------------------------------------------------------------*/
18
- const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.SubTrigger, { ref: ref, className: cn("flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none", "focus:bg-muted data-[state=open]:bg-muted", inset && "pl-8", className), ...props, children: [children, _jsx(ChevronRight, { className: "ml-auto h-4 w-4" })] })));
18
+ const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.SubTrigger, { ref: ref, className: cn("flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-paragraph-sm outline-none", "focus:bg-muted data-[state=open]:bg-muted", inset && "pl-8", className), ...props, children: [children, _jsx(ChevronRight, { className: "ml-auto h-4 w-4" })] })));
19
19
  DropdownMenuSubTrigger.displayName =
20
20
  DropdownMenuPrimitive.SubTrigger.displayName;
21
21
  /* -------------------------------------------------------------------------------------------------
@@ -27,28 +27,28 @@ DropdownMenuSubContent.displayName =
27
27
  /* -------------------------------------------------------------------------------------------------
28
28
  * DropdownMenuContent
29
29
  * -----------------------------------------------------------------------------------------------*/
30
- const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-card p-1 shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:slide-in-from-top-2", "data-[side=left]:slide-in-from-right-2", "data-[side=right]:slide-in-from-left-2", "data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
30
+ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { ref: ref, sideOffset: sideOffset, className: cn("z-50 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-popover p-0.5 shadow-md", "data-[state=open]:animate-in data-[state=closed]:animate-out", "data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95", "data-[side=bottom]:slide-in-from-top-2", "data-[side=left]:slide-in-from-right-2", "data-[side=right]:slide-in-from-left-2", "data-[side=top]:slide-in-from-bottom-2", className), ...props }) })));
31
31
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
32
32
  /* -------------------------------------------------------------------------------------------------
33
33
  * DropdownMenuItem
34
34
  * -----------------------------------------------------------------------------------------------*/
35
- const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Item, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className), ...props })));
35
+ const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Item, { ref: ref, className: cn("relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-paragraph-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className), ...props })));
36
36
  DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
37
37
  /* -------------------------------------------------------------------------------------------------
38
38
  * DropdownMenuCheckboxItem
39
39
  * -----------------------------------------------------------------------------------------------*/
40
- const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), children] })));
40
+ const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.CheckboxItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-paragraph-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Check, { className: "h-4 w-4" }) }) }), children] })));
41
41
  DropdownMenuCheckboxItem.displayName =
42
42
  DropdownMenuPrimitive.CheckboxItem.displayName;
43
43
  /* -------------------------------------------------------------------------------------------------
44
44
  * DropdownMenuRadioItem
45
45
  * -----------------------------------------------------------------------------------------------*/
46
- const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.RadioItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Circle, { className: "h-2 w-2 fill-current" }) }) }), children] })));
46
+ const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => (_jsxs(DropdownMenuPrimitive.RadioItem, { ref: ref, className: cn("relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-paragraph-sm outline-none transition-colors", "focus:bg-muted focus:text-foreground", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", className), ...props, children: [_jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(Circle, { className: "h-2 w-2 fill-current" }) }) }), children] })));
47
47
  DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
48
48
  /* -------------------------------------------------------------------------------------------------
49
49
  * DropdownMenuLabel
50
50
  * -----------------------------------------------------------------------------------------------*/
51
- const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Label, { ref: ref, className: cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className), ...props })));
51
+ const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => (_jsx(DropdownMenuPrimitive.Label, { ref: ref, className: cn("px-2 py-1.5 text-paragraph-sm font-semibold", inset && "pl-8", className), ...props })));
52
52
  DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
53
53
  /* -------------------------------------------------------------------------------------------------
54
54
  * DropdownMenuSeparator
@@ -59,7 +59,7 @@ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
59
59
  * DropdownMenuShortcut
60
60
  * -----------------------------------------------------------------------------------------------*/
61
61
  const DropdownMenuShortcut = ({ className, ...props }) => {
62
- return (_jsx("span", { className: cn("ml-auto text-xs tracking-widest opacity-60", className), ...props }));
62
+ return (_jsx("span", { className: cn("ml-auto text-caption tracking-widest opacity-60", className), ...props }));
63
63
  };
64
64
  DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
65
65
  /* -------------------------------------------------------------------------------------------------
@@ -0,0 +1,17 @@
1
+ /**
2
+ * FileSystemItem component - represents a single file or folder in the tree
3
+ * Matches the Figma design specifications with all states
4
+ * States: Default, Hover, Focus, Selected (Targeted in Figma)
5
+ */
6
+ import type { FileSystemItem as FileSystemItemType } from "../types/filesystem.js";
7
+ export interface FileSystemItemProps {
8
+ item: FileSystemItemType;
9
+ level?: number;
10
+ onSelect?: (item: FileSystemItemType) => void;
11
+ selectedId?: string;
12
+ isDropTarget?: boolean;
13
+ onDownload?: (item: FileSystemItemType) => void;
14
+ onRename?: (item: FileSystemItemType) => void;
15
+ onDelete?: (item: FileSystemItemType) => void;
16
+ }
17
+ export declare function FileSystemItem({ item, level, onSelect, selectedId, isDropTarget, onDownload, onRename, onDelete, }: FileSystemItemProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * FileSystemItem component - represents a single file or folder in the tree
4
+ * Matches the Figma design specifications with all states
5
+ * States: Default, Hover, Focus, Selected (Targeted in Figma)
6
+ */
7
+ import { ChevronDown, FileCode, Folder, FolderOpen, MoreVertical, } from "lucide-react";
8
+ import * as React from "react";
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, }) {
12
+ const [isExpanded, setIsExpanded] = React.useState(true);
13
+ const [isFocused, setIsFocused] = React.useState(false);
14
+ const isSelected = selectedId === item.id;
15
+ const handleToggle = () => {
16
+ if (item.type === "folder") {
17
+ setIsExpanded(!isExpanded);
18
+ }
19
+ };
20
+ const handleClick = () => {
21
+ onSelect?.(item);
22
+ if (item.type === "folder") {
23
+ handleToggle();
24
+ }
25
+ };
26
+ const handleKeyDown = (e) => {
27
+ if (e.key === "Enter" || e.key === " ") {
28
+ e.preventDefault();
29
+ handleClick();
30
+ }
31
+ else if (e.key === "ArrowRight" &&
32
+ item.type === "folder" &&
33
+ !isExpanded) {
34
+ e.preventDefault();
35
+ setIsExpanded(true);
36
+ }
37
+ else if (e.key === "ArrowLeft" && item.type === "folder" && isExpanded) {
38
+ e.preventDefault();
39
+ setIsExpanded(false);
40
+ }
41
+ };
42
+ // Calculate padding based on level
43
+ const getPaddingClass = () => {
44
+ if (level === 0)
45
+ return "p-2";
46
+ if (level === 1)
47
+ return "pl-8 pr-2 py-2";
48
+ return "pl-16 pr-2 py-2"; // level 2+
49
+ };
50
+ return (_jsxs("div", { className: "flex flex-col w-full", children: [_jsxs("div", { role: "button", tabIndex: 0, "aria-expanded": item.type === "folder" ? isExpanded : undefined, className: cn(
51
+ // Base styles with semantic typography
52
+ "group flex items-center gap-2 rounded-md cursor-pointer transition-colors text-paragraph-sm", getPaddingClass(),
53
+ // State styles using semantic colors
54
+ // Default: transparent background
55
+ // Hover: accent-hover background
56
+ "hover:bg-accent-hover",
57
+ // Focus: ring with border-dark color (3px ring matching Figma)
58
+ "focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-border-dark",
59
+ // Selected: just accent background (no border)
60
+ isSelected && "bg-accent",
61
+ // Drop target state (for future drag & drop) - dashed border
62
+ isDropTarget && [
63
+ "bg-accent",
64
+ "border border-dashed border-border-dark",
65
+ ]), onClick: handleClick, onKeyDown: handleKeyDown, onFocus: () => setIsFocused(true), onBlur: () => setIsFocused(false), children: [_jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-foreground", children: item.type === "folder" ? (isExpanded ? (_jsx(FolderOpen, { className: "size-4" })) : (_jsx(Folder, { className: "size-4" }))) : (_jsx(FileCode, { className: "size-4" })) }), _jsx("p", { className: "flex-1 text-foreground whitespace-nowrap overflow-hidden text-ellipsis", children: item.name }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { className: cn("shrink-0 size-4 transition-opacity text-muted-foreground hover:text-foreground",
66
+ // Hidden by default, shown on group hover
67
+ "opacity-0 group-hover:opacity-100",
68
+ // Always hidden when focused (to show clean state)
69
+ isFocused && "opacity-0"), onClick: (e) => {
70
+ e.stopPropagation();
71
+ }, "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) => {
72
+ e.stopPropagation();
73
+ onDownload(item);
74
+ }, children: "Download" })), onRename && (_jsx(DropdownMenuItem, { onClick: (e) => {
75
+ e.stopPropagation();
76
+ onRename(item);
77
+ }, children: "Rename" })), (onDownload || onRename) && onDelete && _jsx(DropdownMenuSeparator, {}), onDelete && (_jsx(DropdownMenuItem, { className: "text-destructive focus:text-destructive focus:bg-muted", onClick: (e) => {
78
+ e.stopPropagation();
79
+ onDelete(item);
80
+ }, children: _jsx("span", { className: "text-paragraph-sm", children: "Delete" }) }))] })] }), 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))) }))] }));
81
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * FileSystemView component - main file tree/manager view
3
+ * Based on shadcn file manager design and Figma specifications
4
+ */
5
+ import type { FileSystemItem as FileSystemItemType, FileSystemProvider } from "../types/filesystem.js";
6
+ export interface FileSystemViewProps {
7
+ className?: string;
8
+ provider?: FileSystemProvider;
9
+ onItemSelect?: (item: FileSystemItemType) => void;
10
+ onDownload?: (item: FileSystemItemType) => void;
11
+ onRename?: (item: FileSystemItemType) => void;
12
+ onDelete?: (item: FileSystemItemType) => void;
13
+ }
14
+ export declare function FileSystemView({ className, provider, onItemSelect, onDownload, onRename, onDelete, }: FileSystemViewProps): import("react/jsx-runtime").JSX.Element;