@townco/ui 0.1.47 → 0.1.49

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 (172) hide show
  1. package/dist/core/hooks/index.d.ts +1 -0
  2. package/dist/core/hooks/index.js +1 -0
  3. package/dist/core/hooks/use-chat-input.d.ts +2 -0
  4. package/dist/core/hooks/use-chat-input.js +11 -1
  5. package/dist/core/hooks/use-chat-messages.d.ts +22 -1
  6. package/dist/core/hooks/use-chat-messages.js +19 -4
  7. package/dist/core/hooks/use-chat-session.js +22 -0
  8. package/dist/core/hooks/use-message-history.d.ts +12 -0
  9. package/dist/core/hooks/use-message-history.js +113 -0
  10. package/dist/core/hooks/use-tool-calls.d.ts +11 -0
  11. package/dist/core/schemas/chat.d.ts +40 -0
  12. package/dist/core/schemas/chat.js +9 -0
  13. package/dist/core/schemas/tool-call.d.ts +34 -0
  14. package/dist/core/schemas/tool-call.js +27 -0
  15. package/dist/core/store/chat-store.d.ts +7 -0
  16. package/dist/core/store/chat-store.js +46 -0
  17. package/dist/gui/components/ChatEmptyState.d.ts +4 -0
  18. package/dist/gui/components/ChatEmptyState.js +2 -2
  19. package/dist/gui/components/ChatInput.d.ts +21 -1
  20. package/dist/gui/components/ChatInput.js +184 -7
  21. package/dist/gui/components/ChatLayout.d.ts +3 -2
  22. package/dist/gui/components/ChatLayout.js +7 -7
  23. package/dist/gui/components/ChatPanelTabContent.d.ts +17 -0
  24. package/dist/gui/components/ChatPanelTabContent.js +6 -5
  25. package/dist/gui/components/ChatView.d.ts +3 -1
  26. package/dist/gui/components/ChatView.js +81 -49
  27. package/dist/gui/components/ContextUsageButton.d.ts +2 -0
  28. package/dist/gui/components/ContextUsageButton.js +3 -1
  29. package/dist/gui/components/InvokingGroup.d.ts +9 -0
  30. package/dist/gui/components/InvokingGroup.js +16 -0
  31. package/dist/gui/components/MessageContent.js +122 -6
  32. package/dist/gui/components/PanelTabsHeader.d.ts +1 -1
  33. package/dist/gui/components/PanelTabsHeader.js +6 -1
  34. package/dist/gui/components/Response.js +2 -0
  35. package/dist/gui/components/Task.js +3 -3
  36. package/dist/gui/components/TodoListItem.js +1 -1
  37. package/dist/gui/components/ToolCall.js +57 -5
  38. package/dist/gui/components/ToolCallGroup.d.ts +8 -0
  39. package/dist/gui/components/ToolCallGroup.js +29 -0
  40. package/dist/gui/components/index.d.ts +1 -2
  41. package/dist/gui/components/index.js +1 -2
  42. package/dist/sdk/client/acp-client.d.ts +24 -1
  43. package/dist/sdk/client/acp-client.js +28 -7
  44. package/dist/sdk/schemas/message.d.ts +41 -9
  45. package/dist/sdk/schemas/message.js +15 -3
  46. package/dist/sdk/schemas/session.d.ts +75 -10
  47. package/dist/sdk/transports/http.d.ts +14 -0
  48. package/dist/sdk/transports/http.js +130 -36
  49. package/dist/sdk/transports/stdio.d.ts +14 -0
  50. package/dist/sdk/transports/stdio.js +27 -10
  51. package/dist/sdk/transports/types.d.ts +29 -0
  52. package/package.json +3 -3
  53. package/dist/core/hooks/index.d.ts.map +0 -1
  54. package/dist/core/hooks/index.js.map +0 -1
  55. package/dist/core/hooks/use-chat-input.d.ts.map +0 -1
  56. package/dist/core/hooks/use-chat-input.js.map +0 -1
  57. package/dist/core/hooks/use-chat-messages.d.ts.map +0 -1
  58. package/dist/core/hooks/use-chat-messages.js.map +0 -1
  59. package/dist/core/hooks/use-chat-session.d.ts.map +0 -1
  60. package/dist/core/hooks/use-chat-session.js.map +0 -1
  61. package/dist/core/index.d.ts.map +0 -1
  62. package/dist/core/index.js.map +0 -1
  63. package/dist/core/lib/logger.d.ts +0 -24
  64. package/dist/core/lib/logger.js +0 -108
  65. package/dist/core/schemas/chat.d.ts.map +0 -1
  66. package/dist/core/schemas/chat.js.map +0 -1
  67. package/dist/core/schemas/index.d.ts.map +0 -1
  68. package/dist/core/schemas/index.js.map +0 -1
  69. package/dist/core/store/chat-store.d.ts.map +0 -1
  70. package/dist/core/store/chat-store.js.map +0 -1
  71. package/dist/gui/components/Button.d.ts.map +0 -1
  72. package/dist/gui/components/Button.js.map +0 -1
  73. package/dist/gui/components/Card.d.ts.map +0 -1
  74. package/dist/gui/components/Card.js.map +0 -1
  75. package/dist/gui/components/ChatInput.d.ts.map +0 -1
  76. package/dist/gui/components/ChatInput.js.map +0 -1
  77. package/dist/gui/components/ChatSecondaryPanel.d.ts.map +0 -1
  78. package/dist/gui/components/ChatSecondaryPanel.js.map +0 -1
  79. package/dist/gui/components/ChatStatus.d.ts.map +0 -1
  80. package/dist/gui/components/ChatStatus.js.map +0 -1
  81. package/dist/gui/components/Conversation.d.ts.map +0 -1
  82. package/dist/gui/components/Conversation.js.map +0 -1
  83. package/dist/gui/components/Dialog.d.ts.map +0 -1
  84. package/dist/gui/components/Dialog.js.map +0 -1
  85. package/dist/gui/components/HeightTransition.d.ts.map +0 -1
  86. package/dist/gui/components/HeightTransition.js.map +0 -1
  87. package/dist/gui/components/Input.d.ts.map +0 -1
  88. package/dist/gui/components/Input.js.map +0 -1
  89. package/dist/gui/components/Label.d.ts.map +0 -1
  90. package/dist/gui/components/Label.js.map +0 -1
  91. package/dist/gui/components/MarkdownRenderer.d.ts.map +0 -1
  92. package/dist/gui/components/MarkdownRenderer.js.map +0 -1
  93. package/dist/gui/components/Message.d.ts.map +0 -1
  94. package/dist/gui/components/Message.js.map +0 -1
  95. package/dist/gui/components/MessageContent.d.ts.map +0 -1
  96. package/dist/gui/components/MessageContent.js.map +0 -1
  97. package/dist/gui/components/MessageList.d.ts.map +0 -1
  98. package/dist/gui/components/MessageList.js.map +0 -1
  99. package/dist/gui/components/Reasoning.d.ts.map +0 -1
  100. package/dist/gui/components/Reasoning.js.map +0 -1
  101. package/dist/gui/components/Response.d.ts.map +0 -1
  102. package/dist/gui/components/Response.js.map +0 -1
  103. package/dist/gui/components/Select.d.ts.map +0 -1
  104. package/dist/gui/components/Select.js.map +0 -1
  105. package/dist/gui/components/Tabs.d.ts.map +0 -1
  106. package/dist/gui/components/Tabs.js.map +0 -1
  107. package/dist/gui/components/Task.d.ts.map +0 -1
  108. package/dist/gui/components/Task.js.map +0 -1
  109. package/dist/gui/components/Textarea.d.ts.map +0 -1
  110. package/dist/gui/components/Textarea.js.map +0 -1
  111. package/dist/gui/components/ThinkingBlock.d.ts.map +0 -1
  112. package/dist/gui/components/ThinkingBlock.js.map +0 -1
  113. package/dist/gui/components/TodoList.d.ts.map +0 -1
  114. package/dist/gui/components/TodoList.js.map +0 -1
  115. package/dist/gui/components/TodoListItem.d.ts.map +0 -1
  116. package/dist/gui/components/TodoListItem.js.map +0 -1
  117. package/dist/gui/components/index.d.ts.map +0 -1
  118. package/dist/gui/components/index.js.map +0 -1
  119. package/dist/gui/index.d.ts.map +0 -1
  120. package/dist/gui/index.js.map +0 -1
  121. package/dist/gui/lib/utils.d.ts.map +0 -1
  122. package/dist/gui/lib/utils.js.map +0 -1
  123. package/dist/index.d.ts.map +0 -1
  124. package/dist/index.js.map +0 -1
  125. package/dist/sdk/client/acp-client.d.ts.map +0 -1
  126. package/dist/sdk/client/acp-client.js.map +0 -1
  127. package/dist/sdk/client/index.d.ts.map +0 -1
  128. package/dist/sdk/client/index.js.map +0 -1
  129. package/dist/sdk/index.d.ts.map +0 -1
  130. package/dist/sdk/index.js.map +0 -1
  131. package/dist/sdk/schemas/agent.d.ts.map +0 -1
  132. package/dist/sdk/schemas/agent.js.map +0 -1
  133. package/dist/sdk/schemas/index.d.ts.map +0 -1
  134. package/dist/sdk/schemas/index.js.map +0 -1
  135. package/dist/sdk/schemas/message.d.ts.map +0 -1
  136. package/dist/sdk/schemas/message.js.map +0 -1
  137. package/dist/sdk/schemas/session.d.ts.map +0 -1
  138. package/dist/sdk/schemas/session.js.map +0 -1
  139. package/dist/sdk/transports/http.d.ts.map +0 -1
  140. package/dist/sdk/transports/http.js.map +0 -1
  141. package/dist/sdk/transports/index.d.ts.map +0 -1
  142. package/dist/sdk/transports/index.js.map +0 -1
  143. package/dist/sdk/transports/stdio.d.ts.map +0 -1
  144. package/dist/sdk/transports/stdio.js.map +0 -1
  145. package/dist/sdk/transports/types.d.ts.map +0 -1
  146. package/dist/sdk/transports/types.js.map +0 -1
  147. package/dist/sdk/transports/websocket.d.ts.map +0 -1
  148. package/dist/sdk/transports/websocket.js.map +0 -1
  149. package/dist/test-http-client.d.ts +0 -11
  150. package/dist/test-http-client.d.ts.map +0 -1
  151. package/dist/test-http-client.js +0 -147
  152. package/dist/test-http-client.js.map +0 -1
  153. package/dist/test-http-transport.d.ts +0 -11
  154. package/dist/test-http-transport.d.ts.map +0 -1
  155. package/dist/test-http-transport.js +0 -127
  156. package/dist/test-http-transport.js.map +0 -1
  157. package/dist/tui/components/ChatView.d.ts.map +0 -1
  158. package/dist/tui/components/ChatView.js.map +0 -1
  159. package/dist/tui/components/GameOfLife.d.ts.map +0 -1
  160. package/dist/tui/components/GameOfLife.js.map +0 -1
  161. package/dist/tui/components/InputBox.d.ts.map +0 -1
  162. package/dist/tui/components/InputBox.js.map +0 -1
  163. package/dist/tui/components/MessageList.d.ts.map +0 -1
  164. package/dist/tui/components/MessageList.js.map +0 -1
  165. package/dist/tui/components/ReadlineInput.d.ts.map +0 -1
  166. package/dist/tui/components/ReadlineInput.js.map +0 -1
  167. package/dist/tui/components/StatusBar.d.ts.map +0 -1
  168. package/dist/tui/components/StatusBar.js.map +0 -1
  169. package/dist/tui/components/index.d.ts.map +0 -1
  170. package/dist/tui/components/index.js.map +0 -1
  171. package/dist/tui/index.d.ts.map +0 -1
  172. package/dist/tui/index.js.map +0 -1
@@ -2,6 +2,52 @@ import { createLogger } from "@townco/core";
2
2
  import { create } from "zustand";
3
3
  import { mergeToolCallUpdate } from "../schemas/tool-call.js";
4
4
  const logger = createLogger("chat-store", "debug");
5
+ // Constants to avoid creating new empty arrays
6
+ const EMPTY_TODOS = [];
7
+ // Cache for memoized todos to prevent infinite re-render loops
8
+ let cachedTodos = {
9
+ sessionId: null,
10
+ toolCallId: null,
11
+ todos: EMPTY_TODOS,
12
+ };
13
+ /**
14
+ * Selector to get todos for the current session (memoized to prevent infinite loops)
15
+ */
16
+ export const selectTodosForCurrentSession = (state) => {
17
+ const sessionId = state.sessionId;
18
+ if (!sessionId)
19
+ return EMPTY_TODOS;
20
+ const sessionToolCalls = state.toolCalls[sessionId] || [];
21
+ // Find most recent TodoWrite tool call
22
+ const todoToolCalls = sessionToolCalls
23
+ .filter((tc) => tc.title === "todo_write")
24
+ .sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
25
+ if (todoToolCalls.length === 0)
26
+ return EMPTY_TODOS;
27
+ const latestTodoCall = todoToolCalls[0];
28
+ if (!latestTodoCall?.rawInput?.todos ||
29
+ !Array.isArray(latestTodoCall.rawInput.todos)) {
30
+ return EMPTY_TODOS;
31
+ }
32
+ // Return cached result if the tool call hasn't changed
33
+ if (cachedTodos.sessionId === sessionId &&
34
+ cachedTodos.toolCallId === latestTodoCall.id) {
35
+ return cachedTodos.todos;
36
+ }
37
+ // Map tool schema to UI schema
38
+ const newTodos = latestTodoCall.rawInput.todos.map((todo, index) => ({
39
+ id: `${latestTodoCall.id}-${index}`,
40
+ text: todo.status === "in_progress" ? todo.activeForm : todo.content,
41
+ status: todo.status,
42
+ }));
43
+ // Update cache
44
+ cachedTodos = {
45
+ sessionId,
46
+ toolCallId: latestTodoCall.id,
47
+ todos: newTodos,
48
+ };
49
+ return newTodos;
50
+ };
5
51
  /**
6
52
  * Create chat store
7
53
  */
@@ -16,6 +16,10 @@ export interface ChatEmptyStateProps extends React.HTMLAttributes<HTMLDivElement
16
16
  onGuideClick?: () => void;
17
17
  /** Callback when "Open Files" is clicked */
18
18
  onOpenFiles?: () => void;
19
+ /** Callback when "View Tools & MCPs" is clicked */
20
+ onOpenSettings?: () => void;
21
+ /** Count of tools and MCPs to display */
22
+ toolsAndMcpsCount?: number;
19
23
  /** Callback when hovering over a prompt */
20
24
  onPromptHover?: (prompt: string) => void;
21
25
  /** Callback when mouse leaves a prompt */
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ChevronRight } from "lucide-react";
3
3
  import * as React from "react";
4
4
  import { cn } from "../lib/utils.js";
5
- export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, onOpenFiles, onPromptHover, onPromptLeave, className, ...props }, ref) => {
5
+ export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, onOpenFiles, onOpenSettings, toolsAndMcpsCount, onPromptHover, onPromptLeave, className, ...props }, ref) => {
6
6
  const handlePromptClick = (prompt) => {
7
7
  onPromptClick?.(prompt);
8
8
  };
@@ -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", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary mb-6", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose mb-6", children: description }), onOpenFiles && (_jsxs("button", { type: "button", onClick: onOpenFiles, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors mb-6", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: "View Files" }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), 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), onMouseEnter: () => onPromptHover?.(prompt), onMouseLeave: () => onPromptLeave?.(), 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("-")))) })] }))] }));
20
+ return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary mb-6", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose mb-6", children: description }), (onOpenFiles || onOpenSettings) && (_jsxs("div", { className: "flex items-center gap-1 -ml-3 mb-6", children: [onOpenFiles && (_jsxs("button", { type: "button", onClick: onOpenFiles, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: "View Files" }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), onOpenSettings && (_jsxs("button", { type: "button", onClick: onOpenSettings, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsxs("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: ["View Tools & MCPs", toolsAndMcpsCount !== undefined && toolsAndMcpsCount > 0 && (_jsxs("span", { className: "ml-1", children: ["(", toolsAndMcpsCount, ")"] }))] }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] }))] })), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), 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), onMouseEnter: () => onPromptHover?.(prompt), onMouseLeave: () => onPromptLeave?.(), 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";
@@ -35,6 +35,16 @@ export interface ChatInputRootProps extends Omit<React.FormHTMLAttributes<HTMLFo
35
35
  declare const ChatInputRoot: React.ForwardRefExoticComponent<ChatInputRootProps & React.RefAttributes<HTMLFormElement>>;
36
36
  export interface ChatInputFieldProps extends Omit<React.ComponentPropsWithoutRef<typeof Textarea>, "value" | "onChange"> {
37
37
  asChild?: boolean;
38
+ /**
39
+ * Callback when files are dropped
40
+ */
41
+ onFilesDropped?: (files: Array<{
42
+ name: string;
43
+ path: string;
44
+ size: number;
45
+ mimeType: string;
46
+ data: string;
47
+ }>) => void;
38
48
  }
39
49
  declare const ChatInputField: React.ForwardRefExoticComponent<ChatInputFieldProps & React.RefAttributes<HTMLTextAreaElement>>;
40
50
  export interface ChatInputSubmitProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -48,8 +58,18 @@ export interface ChatInputActionsProps extends React.ButtonHTMLAttributes<HTMLBu
48
58
  asChild?: boolean;
49
59
  }
50
60
  declare const ChatInputActions: React.ForwardRefExoticComponent<ChatInputActionsProps & React.RefAttributes<HTMLButtonElement>>;
51
- export interface ChatInputAttachmentProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
61
+ export interface ChatInputAttachmentProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "onChange"> {
52
62
  asChild?: boolean;
63
+ /**
64
+ * Callback when files are selected
65
+ */
66
+ onFilesSelected?: (files: Array<{
67
+ name: string;
68
+ path: string;
69
+ size: number;
70
+ mimeType: string;
71
+ data: string;
72
+ }>) => void;
53
73
  }
54
74
  declare const ChatInputAttachment: React.ForwardRefExoticComponent<ChatInputAttachmentProps & React.RefAttributes<HTMLButtonElement>>;
55
75
  export interface ChatInputVoiceInputProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -1,8 +1,9 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Slot } from "@radix-ui/react-slot";
3
3
  import { Mic, Paperclip, SquareSlash } from "lucide-react";
4
4
  import * as React from "react";
5
5
  import { useChatInput as useCoreChatInput } from "../../core/hooks/use-chat-input.js";
6
+ import { useMessageHistory } from "../../core/hooks/use-message-history.js";
6
7
  import { useChatStore } from "../../core/store/chat-store.js";
7
8
  import { cn } from "../lib/utils.js";
8
9
  import { Button } from "./Button.js";
@@ -22,18 +23,28 @@ const ChatInputRoot = React.forwardRef(({ client, startSession, value: valueProp
22
23
  // Always call hooks unconditionally (React rules)
23
24
  const hookData = useCoreChatInput(client ?? null, startSession ?? dummyStartSession);
24
25
  const storeIsStreaming = useChatStore((state) => state.isStreaming);
26
+ // Message history hook
27
+ const { addToHistory, navigatePrevious, navigateNext, resetNavigation } = useMessageHistory();
25
28
  // Choose data source based on whether client is provided
26
29
  const useHookData = client && startSession;
27
30
  const value = useHookData ? hookData.value : valueProp || "";
28
31
  const onChange = useHookData
29
32
  ? hookData.onChange
30
33
  : onChangeProp || (() => { });
31
- const onSubmit = useHookData
34
+ const baseOnSubmit = useHookData
32
35
  ? hookData.onSubmit
33
36
  : onSubmitProp || (async () => { });
34
37
  const isSubmitting = useHookData
35
38
  ? hookData.isSubmitting || storeIsStreaming
36
39
  : isSubmittingProp || false;
40
+ // Wrap onSubmit to add messages to history
41
+ const onSubmit = React.useCallback(async () => {
42
+ const messageToSave = value.trim();
43
+ if (messageToSave) {
44
+ addToHistory(messageToSave);
45
+ }
46
+ await baseOnSubmit();
47
+ }, [value, addToHistory, baseOnSubmit]);
37
48
  // Command menu state
38
49
  const [showCommandMenu, setShowCommandMenu] = React.useState(false);
39
50
  const [commandMenuQuery, setCommandMenuQuery] = React.useState("");
@@ -112,12 +123,16 @@ const ChatInputRoot = React.forwardRef(({ client, startSession, value: valueProp
112
123
  setMenuItemCount,
113
124
  triggerMenuSelect,
114
125
  triggerCounter,
126
+ navigateHistoryPrevious: navigatePrevious,
127
+ navigateHistoryNext: navigateNext,
128
+ resetHistoryNavigation: resetNavigation,
115
129
  }, children: _jsx("form", { ref: ref, onSubmit: handleSubmit, onClick: handleFormClick, onKeyDown: handleFormKeyDown, className: cn("relative w-full divide-y rounded-xl border bg-background shadow-md", className), ...props, children: children }) }));
116
130
  });
117
131
  ChatInputRoot.displayName = "ChatInput.Root";
118
- const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown, children, ...props }, ref) => {
119
- const { value, onChange, onSubmit, disabled, isSubmitting, submitOnEnter, showCommandMenu, setShowCommandMenu, setCommandMenuQuery, setSelectedMenuIndex, menuItemCount, triggerMenuSelect, } = useChatInputContext();
132
+ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown, onFilesDropped, children, ...props }, ref) => {
133
+ const { value, onChange, onSubmit, disabled, isSubmitting, submitOnEnter, showCommandMenu, setShowCommandMenu, setCommandMenuQuery, setSelectedMenuIndex, menuItemCount, triggerMenuSelect, navigateHistoryPrevious, navigateHistoryNext, resetHistoryNavigation, } = useChatInputContext();
120
134
  const textareaRef = React.useRef(null);
135
+ const [isDragging, setIsDragging] = React.useState(false);
121
136
  const handleRef = React.useCallback((node) => {
122
137
  textareaRef.current = node;
123
138
  if (typeof ref === "function") {
@@ -127,6 +142,65 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
127
142
  ref.current = node;
128
143
  }
129
144
  }, [ref]);
145
+ /**
146
+ * Convert file to base64
147
+ */
148
+ const fileToBase64 = (file) => {
149
+ return new Promise((resolve, reject) => {
150
+ const reader = new FileReader();
151
+ reader.readAsDataURL(file);
152
+ reader.onload = () => {
153
+ const base64 = reader.result.split(",")[1];
154
+ resolve(base64 || "");
155
+ };
156
+ reader.onerror = (error) => reject(error);
157
+ });
158
+ };
159
+ /**
160
+ * Handle file drop
161
+ */
162
+ const handleDrop = async (e) => {
163
+ e.preventDefault();
164
+ setIsDragging(false);
165
+ const files = e.dataTransfer.files;
166
+ if (!files || files.length === 0 || !onFilesDropped)
167
+ return;
168
+ const processedFiles = [];
169
+ for (const file of Array.from(files)) {
170
+ if (!file.type.startsWith("image/"))
171
+ continue;
172
+ try {
173
+ const base64Data = await fileToBase64(file);
174
+ processedFiles.push({
175
+ name: file.name,
176
+ path: file.name,
177
+ size: file.size,
178
+ mimeType: file.type,
179
+ data: base64Data,
180
+ });
181
+ }
182
+ catch (error) {
183
+ console.error("Failed to process dropped file:", file.name, error);
184
+ }
185
+ }
186
+ if (processedFiles.length > 0) {
187
+ onFilesDropped(processedFiles);
188
+ }
189
+ };
190
+ /**
191
+ * Handle drag over (required to enable drop)
192
+ */
193
+ const handleDragOver = (e) => {
194
+ e.preventDefault();
195
+ setIsDragging(true);
196
+ };
197
+ /**
198
+ * Handle drag leave
199
+ */
200
+ const handleDragLeave = (e) => {
201
+ e.preventDefault();
202
+ setIsDragging(false);
203
+ };
130
204
  const handleKeyDown = (e) => {
131
205
  // Handle arrow keys and Enter when command menu is open
132
206
  if (showCommandMenu && menuItemCount > 0) {
@@ -152,6 +226,46 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
152
226
  return;
153
227
  }
154
228
  }
229
+ // Handle history navigation with up/down arrows when command menu is not open
230
+ // Only navigate history when cursor is at start/end of input or input is empty
231
+ if (!showCommandMenu) {
232
+ const textarea = textareaRef.current;
233
+ const cursorAtStart = textarea?.selectionStart === 0 && textarea?.selectionEnd === 0;
234
+ const cursorAtEnd = textarea?.selectionStart === value.length &&
235
+ textarea?.selectionEnd === value.length;
236
+ const isMultiline = value.includes("\n");
237
+ if (e.key === "ArrowUp" && (cursorAtStart || !value || !isMultiline)) {
238
+ const previousMessage = navigateHistoryPrevious(value);
239
+ if (previousMessage !== null) {
240
+ e.preventDefault();
241
+ onChange(previousMessage);
242
+ // Move cursor to end after a tick
243
+ setTimeout(() => {
244
+ if (textareaRef.current) {
245
+ textareaRef.current.selectionStart = previousMessage.length;
246
+ textareaRef.current.selectionEnd = previousMessage.length;
247
+ }
248
+ }, 0);
249
+ return;
250
+ }
251
+ }
252
+ else if (e.key === "ArrowDown" &&
253
+ (cursorAtEnd || !value || !isMultiline)) {
254
+ const nextMessage = navigateHistoryNext();
255
+ if (nextMessage !== null) {
256
+ e.preventDefault();
257
+ onChange(nextMessage);
258
+ // Move cursor to end after a tick
259
+ setTimeout(() => {
260
+ if (textareaRef.current) {
261
+ textareaRef.current.selectionStart = nextMessage.length;
262
+ textareaRef.current.selectionEnd = nextMessage.length;
263
+ }
264
+ }, 0);
265
+ return;
266
+ }
267
+ }
268
+ }
155
269
  // Handle Enter without Shift - only submit if not already submitting
156
270
  if (submitOnEnter && e.key === "Enter" && !e.shiftKey) {
157
271
  // Only prevent default and submit if conditions are met
@@ -169,6 +283,8 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
169
283
  const handleChange = (e) => {
170
284
  const newValue = e.target.value;
171
285
  onChange(newValue);
286
+ // Reset history navigation when user types
287
+ resetHistoryNavigation();
172
288
  // Check if user typed "/" at the start to trigger command menu
173
289
  if (newValue.startsWith("/") && !newValue.includes("\n")) {
174
290
  setShowCommandMenu(true);
@@ -200,13 +316,16 @@ const ChatInputField = React.forwardRef(({ asChild = false, className, onKeyDown
200
316
  value,
201
317
  onChange: handleChange,
202
318
  onKeyDown: handleKeyDown,
319
+ onDrop: handleDrop,
320
+ onDragOver: handleDragOver,
321
+ onDragLeave: handleDragLeave,
203
322
  disabled: disabled,
204
323
  ...props,
205
324
  };
206
325
  if (asChild && React.isValidElement(children)) {
207
326
  return React.cloneElement(children, fieldProps);
208
327
  }
209
- return (_jsx("textarea", { ...fieldProps, rows: 1, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 max-h-[6lh] min-h-[44px]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-paragraph-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", className) }));
328
+ return (_jsx("textarea", { ...fieldProps, rows: 1, className: cn("w-full resize-none rounded-none border-none p-4 shadow-none", "outline-none ring-0 max-h-[6lh] min-h-[44px]", "bg-transparent dark:bg-transparent focus-visible:ring-0", "text-paragraph-sm placeholder:text-muted-foreground", "disabled:cursor-not-allowed disabled:opacity-50", isDragging && "ring-2 ring-primary ring-offset-2", className) }));
210
329
  });
211
330
  ChatInputField.displayName = "ChatInput.Field";
212
331
  const ChatInputSubmit = React.forwardRef(({ asChild = false, className, disabled: disabledProp, children, ...props }, ref) => {
@@ -235,9 +354,67 @@ const ChatInputActions = React.forwardRef(({ asChild = false, className, childre
235
354
  return (_jsx(Comp, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full", className), onClick: handleClick, ...props, children: children || _jsx(SquareSlash, { className: "size-4" }) }));
236
355
  });
237
356
  ChatInputActions.displayName = "ChatInput.Actions";
238
- const ChatInputAttachment = React.forwardRef(({ asChild = false, className, children, ...props }, ref) => {
357
+ const ChatInputAttachment = React.forwardRef(({ asChild = false, className, children, onFilesSelected, ...props }, ref) => {
358
+ const fileInputRef = React.useRef(null);
359
+ /**
360
+ * Convert file to base64
361
+ */
362
+ const fileToBase64 = (file) => {
363
+ return new Promise((resolve, reject) => {
364
+ const reader = new FileReader();
365
+ reader.readAsDataURL(file);
366
+ reader.onload = () => {
367
+ // Remove the data URL prefix (e.g., "data:image/png;base64,")
368
+ const base64 = reader.result.split(",")[1];
369
+ resolve(base64 || "");
370
+ };
371
+ reader.onerror = (error) => reject(error);
372
+ });
373
+ };
374
+ /**
375
+ * Handle file selection
376
+ */
377
+ const handleFileChange = async (e) => {
378
+ const files = e.target.files;
379
+ if (!files || files.length === 0)
380
+ return;
381
+ const processedFiles = [];
382
+ for (const file of Array.from(files)) {
383
+ // Only process image files
384
+ if (!file.type.startsWith("image/")) {
385
+ continue;
386
+ }
387
+ try {
388
+ const base64Data = await fileToBase64(file);
389
+ processedFiles.push({
390
+ name: file.name,
391
+ path: file.name, // For web, we don't have a real path
392
+ size: file.size,
393
+ mimeType: file.type,
394
+ data: base64Data,
395
+ });
396
+ }
397
+ catch (error) {
398
+ console.error("Failed to process file:", file.name, error);
399
+ }
400
+ }
401
+ if (processedFiles.length > 0 && onFilesSelected) {
402
+ onFilesSelected(processedFiles);
403
+ }
404
+ // Reset input so the same file can be selected again
405
+ if (fileInputRef.current) {
406
+ fileInputRef.current.value = "";
407
+ }
408
+ };
409
+ /**
410
+ * Handle button click to trigger file input
411
+ */
412
+ const handleClick = (e) => {
413
+ e.preventDefault();
414
+ fileInputRef.current?.click();
415
+ };
239
416
  const Comp = asChild ? Slot : Button;
240
- return (_jsx(Comp, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full", className), ...props, children: children || _jsx(Paperclip, { className: "size-4" }) }));
417
+ return (_jsxs(_Fragment, { children: [_jsx("input", { ref: fileInputRef, type: "file", accept: "image/*", multiple: true, style: { display: "none" }, onChange: handleFileChange }), _jsx(Comp, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full", className), onClick: handleClick, ...props, children: children || _jsx(Paperclip, { className: "size-4" }) })] }));
241
418
  });
242
419
  ChatInputAttachment.displayName = "ChatInput.Attachment";
243
420
  const ChatInputVoiceInput = React.forwardRef(({ asChild = false, className, children, ...props }, ref) => {
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  type PanelSize = "hidden" | "small" | "large";
3
- type PanelTabType = "todo" | "files" | "database";
3
+ type PanelTabType = "todo" | "files" | "database" | "sources" | "settings";
4
4
  interface ChatLayoutContextValue {
5
5
  sidebarOpen: boolean;
6
6
  setSidebarOpen: (open: boolean) => void;
@@ -9,6 +9,7 @@ interface ChatLayoutContextValue {
9
9
  activeTab: PanelTabType;
10
10
  setActiveTab: (tab: PanelTabType) => void;
11
11
  }
12
+ declare const ChatLayoutContext: React.Context<ChatLayoutContextValue | undefined>;
12
13
  declare const useChatLayoutContext: () => ChatLayoutContextValue;
13
14
  export interface ChatLayoutRootProps extends React.HTMLAttributes<HTMLDivElement> {
14
15
  /** Initial sidebar open state */
@@ -48,5 +49,5 @@ export interface ChatLayoutAsideProps extends React.HTMLAttributes<HTMLDivElemen
48
49
  breakpoint?: "md" | "lg" | "xl" | "2xl";
49
50
  }
50
51
  declare const ChatLayoutAside: React.ForwardRefExoticComponent<ChatLayoutAsideProps & React.RefAttributes<HTMLDivElement>>;
51
- export { ChatLayoutRoot as Root, ChatLayoutHeader as Header, ChatLayoutMain as Main, ChatLayoutBody as Body, ChatLayoutMessages as Messages, ChatLayoutFooter as Footer, ChatLayoutSidebar as Sidebar, ChatLayoutAside as Aside, useChatLayoutContext, };
52
+ export { ChatLayoutRoot as Root, ChatLayoutHeader as Header, ChatLayoutMain as Main, ChatLayoutBody as Body, ChatLayoutMessages as Messages, ChatLayoutFooter as Footer, ChatLayoutSidebar as Sidebar, ChatLayoutAside as Aside, ChatLayoutContext as Context, useChatLayoutContext, };
52
53
  export type { PanelSize, PanelTabType };
@@ -31,7 +31,7 @@ const ChatLayoutHeader = React.forwardRef(({ className, children, ...props }, re
31
31
  });
32
32
  ChatLayoutHeader.displayName = "ChatLayout.Header";
33
33
  const ChatLayoutMain = React.forwardRef(({ className, children, ...props }, ref) => {
34
- return (_jsx(ResizablePanel, { defaultSize: 75, minSize: 50, children: _jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden h-full", className), ...props, children: children }) }));
34
+ return (_jsx(ResizablePanel, { defaultSize: 70, minSize: 50, children: _jsx("div", { ref: ref, className: cn("flex flex-1 flex-col overflow-hidden h-full", className), ...props, children: children }) }));
35
35
  });
36
36
  ChatLayoutMain.displayName = "ChatLayout.Main";
37
37
  const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, children, ...props }, ref) => {
@@ -168,13 +168,13 @@ const ChatLayoutSidebar = React.forwardRef(({ className, children, ...props }, r
168
168
  ChatLayoutSidebar.displayName = "ChatLayout.Sidebar";
169
169
  const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, children, ...props }, ref) => {
170
170
  const { panelSize } = useChatLayoutContext();
171
- const [minSizePercent, setMinSizePercent] = React.useState(25);
172
- // Convert 400px minimum to percentage based on window width
171
+ const [minSizePercent, setMinSizePercent] = React.useState(28);
172
+ // Convert 450px minimum to percentage based on window width
173
173
  React.useEffect(() => {
174
174
  const updateMinSize = () => {
175
- const minPixels = 400;
175
+ const minPixels = 450;
176
176
  const minPercent = (minPixels / window.innerWidth) * 100;
177
- setMinSizePercent(Math.max(minPercent, 25)); // Never less than 25% or 400px
177
+ setMinSizePercent(Math.max(minPercent, 28)); // Never less than 28% or 450px
178
178
  };
179
179
  updateMinSize();
180
180
  window.addEventListener("resize", updateMinSize);
@@ -185,7 +185,7 @@ const ChatLayoutAside = React.forwardRef(({ breakpoint = "lg", className, childr
185
185
  // Hidden state - don't render
186
186
  if (panelSize === "hidden")
187
187
  return null;
188
- return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true, className: "group-hover:opacity-100 opacity-0 transition-opacity" }), _jsx(ResizablePanel, { defaultSize: 25, minSize: minSizePercent, maxSize: 35, className: "group", children: _jsx("div", { ref: ref, className: cn(
188
+ return (_jsxs(_Fragment, { children: [_jsx(ResizableHandle, { withHandle: true, className: "group-hover:opacity-100 opacity-0 transition-opacity" }), _jsx(ResizablePanel, { defaultSize: 30, minSize: minSizePercent, maxSize: 40, className: "group", children: _jsx("div", { ref: ref, className: cn(
189
189
  // Hidden by default, visible at breakpoint
190
190
  "hidden h-full border-l border-border bg-card overflow-y-auto transition-all duration-300",
191
191
  // Breakpoint visibility
@@ -197,4 +197,4 @@ ChatLayoutAside.displayName = "ChatLayout.Aside";
197
197
  /* -------------------------------------------------------------------------------------------------
198
198
  * Exports
199
199
  * -----------------------------------------------------------------------------------------------*/
200
- export { ChatLayoutRoot as Root, ChatLayoutHeader as Header, ChatLayoutMain as Main, ChatLayoutBody as Body, ChatLayoutMessages as Messages, ChatLayoutFooter as Footer, ChatLayoutSidebar as Sidebar, ChatLayoutAside as Aside, useChatLayoutContext, };
200
+ export { ChatLayoutRoot as Root, ChatLayoutHeader as Header, ChatLayoutMain as Main, ChatLayoutBody as Body, ChatLayoutMessages as Messages, ChatLayoutFooter as Footer, ChatLayoutSidebar as Sidebar, ChatLayoutAside as Aside, ChatLayoutContext as Context, useChatLayoutContext, };
@@ -24,3 +24,20 @@ export interface DatabaseTabContentProps extends React.HTMLAttributes<HTMLDivEle
24
24
  data?: unknown;
25
25
  }
26
26
  export declare const DatabaseTabContent: React.ForwardRefExoticComponent<DatabaseTabContentProps & React.RefAttributes<HTMLDivElement>>;
27
+ export interface SettingsTabContentProps extends React.HTMLAttributes<HTMLDivElement> {
28
+ tools?: Array<{
29
+ name: string;
30
+ description?: string;
31
+ prettyName?: string;
32
+ icon?: string;
33
+ }>;
34
+ mcps?: Array<{
35
+ name: string;
36
+ transport: string;
37
+ }>;
38
+ subagents?: Array<{
39
+ name: string;
40
+ description: string;
41
+ }>;
42
+ }
43
+ export declare const SettingsTabContent: React.ForwardRefExoticComponent<SettingsTabContentProps & React.RefAttributes<HTMLDivElement>>;
@@ -1,15 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { mockSourceData } from "../data/mockSourceData.js";
4
- import { mockTodoData } from "../data/mockTodoData.js";
5
4
  import { cn } from "../lib/utils.js";
6
5
  import { FileSystemView } from "./FileSystemView.js";
7
6
  import { SourceListItem } from "./SourceListItem.js";
8
7
  import { TodoList } from "./TodoList.js";
9
- export const TodoTabContent = React.forwardRef(({ todos, className, ...props }, ref) => {
10
- // Use mock data if no todos provided or if empty array
11
- const displayTodos = todos && todos.length > 0 ? todos : mockTodoData;
12
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: _jsx(TodoList, { todos: displayTodos }) }));
8
+ export const TodoTabContent = React.forwardRef(({ todos = [], className, ...props }, ref) => {
9
+ return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: _jsx(TodoList, { todos: todos }) }));
13
10
  });
14
11
  TodoTabContent.displayName = "TodoTabContent";
15
12
  export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
@@ -44,3 +41,7 @@ export const DatabaseTabContent = React.forwardRef(({ data, className, ...props
44
41
  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] })] })] }));
45
42
  });
46
43
  DatabaseTabContent.displayName = "DatabaseTabContent";
44
+ export const SettingsTabContent = React.forwardRef(({ tools = [], mcps = [], subagents = [], className, ...props }, ref) => {
45
+ return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [_jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Subagents" }), subagents.length > 0 ? (_jsx("div", { className: "space-y-2", children: subagents.map((subagent) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: subagent.name }), _jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-2", children: subagent.description })] }, subagent.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No subagents available" }))] })] }));
46
+ });
47
+ SettingsTabContent.displayName = "SettingsTabContent";
@@ -3,5 +3,7 @@ export interface ChatViewProps {
3
3
  client: AcpClient | null;
4
4
  initialSessionId?: string | null;
5
5
  error?: string | null;
6
+ /** Optional debugger URL for viewing sessions */
7
+ debuggerUrl?: string;
6
8
  }
7
- export declare function ChatView({ client, initialSessionId, error: initError, }: ChatViewProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }: ChatViewProps): import("react/jsx-runtime").JSX.Element;