@meetsmore-oss/use-ai-client 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -353,9 +353,8 @@ declare class UseAIClient {
353
353
  private serverUrl;
354
354
  private socket;
355
355
  private eventHandlers;
356
- private reconnectAttempts;
357
- private maxReconnectAttempts;
358
356
  private reconnectDelay;
357
+ private reconnectDelayMax;
359
358
  private _threadId;
360
359
  private _tools;
361
360
  private _messages;
@@ -737,11 +736,22 @@ interface PersistedFileContent {
737
736
  type: 'file';
738
737
  file: PersistedFileMetadata;
739
738
  }
739
+ /**
740
+ * Transformed file content part for persisted messages.
741
+ * Stores the text produced by a client-side FileTransformer (e.g. OCR result)
742
+ * so the full context survives a page reload / Socket.IO reconnect.
743
+ */
744
+ interface PersistedTransformedFileContent {
745
+ type: 'transformed_file';
746
+ /** The transformed text representation (e.g. OCR'd markdown). */
747
+ text: string;
748
+ originalFile: PersistedFileMetadata;
749
+ }
740
750
  /**
741
751
  * Content part for persisted messages.
742
- * Can be text or file metadata.
752
+ * Can be text, file metadata, or transformed file content.
743
753
  */
744
- type PersistedContentPart = PersistedTextContent | PersistedFileContent;
754
+ type PersistedContentPart = PersistedTextContent | PersistedFileContent | PersistedTransformedFileContent;
745
755
  /**
746
756
  * Content that can be persisted.
747
757
  * Simple string for text-only messages, or array for multimodal content.
@@ -1011,6 +1021,19 @@ interface UseToolSystemReturn {
1011
1021
  */
1012
1022
  declare function useToolSystem({ clientRef, buildState, }: UseToolSystemOptions): UseToolSystemReturn;
1013
1023
 
1024
+ /**
1025
+ * How the chat textarea should treat the Enter key.
1026
+ *
1027
+ * - `'enter'` — Enter submits the message, Shift+Enter inserts a newline.
1028
+ * Typical desktop behavior.
1029
+ * - `'mod-enter'` — Enter inserts a newline. Cmd/Ctrl+Enter submits the message.
1030
+ * Recommended on mobile, where soft keyboards lack modifier
1031
+ * keys and the user is expected to tap the send button.
1032
+ * ("mod" follows the CodeMirror/ProseMirror convention of
1033
+ * meaning Cmd on macOS and Ctrl elsewhere.)
1034
+ */
1035
+ type SubmitMode = 'enter' | 'mod-enter';
1036
+
1014
1037
  /**
1015
1038
  * Options for programmatically sending a message via sendMessage().
1016
1039
  */
@@ -1147,6 +1170,8 @@ declare const defaultStrings: {
1147
1170
  API_OVERLOADED: string;
1148
1171
  /** Error when rate limited */
1149
1172
  RATE_LIMITED: string;
1173
+ /** Error when the connection was lost while a response was being generated */
1174
+ CONNECTION_LOST: string;
1150
1175
  /** Error for unknown/unexpected errors */
1151
1176
  UNKNOWN_ERROR: string;
1152
1177
  };
@@ -1547,6 +1572,32 @@ interface UseAIProviderProps extends UseAIConfig {
1547
1572
  * ```
1548
1573
  */
1549
1574
  onOpenChange?: (isOpen: boolean) => void;
1575
+ /**
1576
+ * How the built-in chat panel should treat the Enter key.
1577
+ *
1578
+ * - `'enter'` (default): Enter submits, Shift+Enter inserts a newline.
1579
+ * Typical desktop behavior.
1580
+ * - `'mod-enter'`: Enter inserts a newline. Cmd/Ctrl+Enter submits.
1581
+ * Recommended for mobile/touch devices where soft keyboards lack modifier
1582
+ * keys and the user is expected to tap the send button.
1583
+ *
1584
+ * Can be overridden per-instance via the `submitMode` prop on `<UseAIChat>`.
1585
+ *
1586
+ * @default 'enter'
1587
+ *
1588
+ * @example
1589
+ * ```tsx
1590
+ * import { isMobileApp } from '@/lib/is-mobile';
1591
+ *
1592
+ * <UseAIProvider
1593
+ * serverUrl="wss://your-server.com"
1594
+ * submitMode={isMobileApp() ? 'mod-enter' : 'enter'}
1595
+ * >
1596
+ * <App />
1597
+ * </UseAIProvider>
1598
+ * ```
1599
+ */
1600
+ submitMode?: SubmitMode;
1550
1601
  }
1551
1602
  /**
1552
1603
  * Provider component that manages AI client connection and tool registration.
@@ -1576,7 +1627,7 @@ interface UseAIProviderProps extends UseAIConfig {
1576
1627
  * }
1577
1628
  * ```
1578
1629
  */
1579
- declare function UseAIProvider({ serverUrl, children, systemPrompt, CustomButton, CustomChat, chatRepository, forwardedPropsProvider, fileUploadConfig: fileUploadConfigProp, commandRepository, renderChat, theme: customTheme, strings: customStrings, visibleAgentIds, onOpenChange, }: UseAIProviderProps): react_jsx_runtime.JSX.Element;
1630
+ declare function UseAIProvider({ serverUrl, children, systemPrompt, CustomButton, CustomChat, chatRepository, forwardedPropsProvider, fileUploadConfig: fileUploadConfigProp, commandRepository, renderChat, theme: customTheme, strings: customStrings, visibleAgentIds, onOpenChange, submitMode, }: UseAIProviderProps): react_jsx_runtime.JSX.Element;
1580
1631
  /**
1581
1632
  * Hook to access the UseAI context.
1582
1633
  * When used outside a UseAIProvider, returns a no-op context and logs a warning.
@@ -1641,12 +1692,26 @@ interface UseAIChatPanelProps {
1641
1692
  onApproveToolCall?: () => void;
1642
1693
  /** Callback to reject all pending tool calls */
1643
1694
  onRejectToolCall?: (reason?: string) => void;
1695
+ /**
1696
+ * How the textarea should treat the Enter key.
1697
+ *
1698
+ * - `'enter'` (default): Enter submits, Shift+Enter inserts a newline. Suitable
1699
+ * for desktop.
1700
+ * - `'mod-enter'`: Enter inserts a newline. Cmd/Ctrl+Enter submits. Recommended
1701
+ * for mobile, where soft keyboards lack modifier keys and the user is
1702
+ * expected to tap the send button.
1703
+ *
1704
+ * IME composition is always respected regardless of mode.
1705
+ *
1706
+ * @default 'enter'
1707
+ */
1708
+ submitMode?: SubmitMode;
1644
1709
  }
1645
1710
  /**
1646
1711
  * Chat panel content - fills its container.
1647
1712
  * Use directly for embedded mode, or wrap with UseAIFloatingChatWrapper for floating mode.
1648
1713
  */
1649
- declare function UseAIChatPanel({ onSendMessage, messages, loading, connected, streamingText, streamingReasoning, currentChatId, onNewChat, onLoadChat, onDeleteChat, onListChats, onGetChat, suggestions, availableAgents, defaultAgent, selectedAgent, onAgentChange, fileUploadConfig, fileProcessing, commands, onSaveCommand, onRenameCommand, onDeleteCommand, closeButton, executingTool, feedbackEnabled, onFeedback, pendingApprovals, onApproveToolCall, onRejectToolCall, }: UseAIChatPanelProps): react_jsx_runtime.JSX.Element;
1714
+ declare function UseAIChatPanel({ onSendMessage, messages, loading, connected, streamingText, streamingReasoning, currentChatId, onNewChat, onLoadChat, onDeleteChat, onListChats, onGetChat, suggestions, availableAgents, defaultAgent, selectedAgent, onAgentChange, fileUploadConfig, fileProcessing, commands, onSaveCommand, onRenameCommand, onDeleteCommand, closeButton, executingTool, feedbackEnabled, onFeedback, pendingApprovals, onApproveToolCall, onRejectToolCall, submitMode, }: UseAIChatPanelProps): react_jsx_runtime.JSX.Element;
1650
1715
 
1651
1716
  /**
1652
1717
  * Props for the floating chat wrapper.
@@ -1685,6 +1750,14 @@ interface UseAIChatProps {
1685
1750
  * When false (default), renders inline filling its container.
1686
1751
  */
1687
1752
  floating?: boolean;
1753
+ /**
1754
+ * How the chat textarea should treat the Enter key. Overrides the value
1755
+ * provided on `UseAIProvider` for this instance.
1756
+ *
1757
+ * - `'enter'` (default): Enter submits, Shift+Enter inserts a newline.
1758
+ * - `'mod-enter'`: Enter inserts a newline. Cmd/Ctrl+Enter submits.
1759
+ */
1760
+ submitMode?: SubmitMode;
1688
1761
  }
1689
1762
  /**
1690
1763
  * Standalone chat component that can be placed anywhere within UseAIProvider.
@@ -1711,7 +1784,7 @@ interface UseAIChatProps {
1711
1784
  * </UseAIProvider>
1712
1785
  * ```
1713
1786
  */
1714
- declare function UseAIChat({ floating }: UseAIChatProps): react_jsx_runtime.JSX.Element;
1787
+ declare function UseAIChat({ floating, submitMode }: UseAIChatProps): react_jsx_runtime.JSX.Element;
1715
1788
 
1716
1789
  /**
1717
1790
  * LocalStorage-based implementation of ChatRepository.
@@ -2323,6 +2396,12 @@ interface UseServerEventsReturn {
2323
2396
  * (currentToolCalls, currentMessageContent).
2324
2397
  */
2325
2398
  handleServerEvent: (client: UseAIClient, event: AGUIEvent) => Promise<void>;
2399
+ /**
2400
+ * Handles a connection loss. When a disconnect occurs mid-run the server
2401
+ * session is destroyed and the run cannot resume, so we surface an error
2402
+ * message and clear in-flight UI state. No-op when no run is active.
2403
+ */
2404
+ handleDisconnect: () => void;
2326
2405
  }
2327
2406
  /**
2328
2407
  * Hook that owns all server event handling state and logic.
@@ -2458,4 +2537,4 @@ interface UseDropdownStateOptions {
2458
2537
  */
2459
2538
  declare function useDropdownState(options?: UseDropdownStateOptions): UseDropdownStateReturn;
2460
2539
 
2461
- export { type AgentContextValue, type Chat, type ChatContextValue, type ChatMetadata, type ChatPanelProps, type ChatRepository, CloseButton, type CommandContextValue, type CommandRepository, type CreateChatOptions, type CreateCommandOptions, DEFAULT_MAX_FILE_SIZE, type DefinedTool, type DropZoneProps, EmbedFileUploadBackend, type ExecutingToolDisplay, type FileAttachment, type FileProcessingState, type FileProcessingStatus, type FileTransformer, type FileTransformerContext, type FileTransformerMap, type FileUploadBackend, type FileUploadConfig, type FloatingButtonProps, type InlineSaveProps, type ListChatsOptions, type ListCommandsOptions, LocalStorageChatRepository, LocalStorageCommandRepository, type Message, type PendingToolApproval, type PersistedContentPart, type PersistedFileContent, type PersistedFileMetadata, type PersistedMessage, type PersistedMessageContent, type PersistedTextContent, type ProcessAttachmentsConfig, type PromptsContextValue, type RegisterToolsOptions, type SavedCommand, type SendMessageOptions, type ToolExecutionContext, type ToolOptions, type ToolRegistryContextValue, type ToolsDefinition, type TriggerWorkflowOptions, UseAIChat, UseAIChatPanel, type UseAIChatPanelProps, type UseAIChatPanelStrings, type UseAIChatPanelTheme, type UseAIChatProps, UseAIClient, type UseAIConfig, type UseAIContextValue, UseAIFloatingButton, UseAIFloatingChatWrapper, type UseAIOptions, UseAIProvider, type UseAIProviderProps, type UseAIResult, type UseAIStrings, type UseAITheme, type UseAIWorkflowResult, type UseAgentSelectionOptions, type UseAgentSelectionReturn, type UseChatManagementOptions, type UseChatManagementReturn, type UseCommandManagementOptions, type UseCommandManagementReturn, type UseDropdownStateOptions, type UseDropdownStateReturn, type UseFeedbackOptions, type UseFeedbackReturn, type UseFileUploadOptions, type UseFileUploadReturn, type UseMessageQueueOptions, type UseMessageQueueReturn, type UsePromptStateOptions, type UsePromptStateReturn, type UseServerEventsOptions, type UseServerEventsReturn, type UseSlashCommandsOptions, type UseSlashCommandsReturn, type UseToolSystemOptions, type UseToolSystemReturn, type WorkflowProgress, clearTransformationCache, convertToolsToDefinitions, defaultStrings, defaultTheme, defineTool, executeDefinedTool, findTransformerPattern, generateChatId, generateCommandId, generateMessageId, matchesMimeType, processAttachments, useAI, useAIContext, useAIWorkflow, useAgentSelection, useChatManagement, useCommandManagement, useDropdownState, useFeedback, useFileUpload, useMessageQueue, usePromptState, useServerEvents, useSlashCommands, useStableTools, useStrings, useTheme, useToolSystem, validateCommandName };
2540
+ export { type AgentContextValue, type Chat, type ChatContextValue, type ChatMetadata, type ChatPanelProps, type ChatRepository, CloseButton, type CommandContextValue, type CommandRepository, type CreateChatOptions, type CreateCommandOptions, DEFAULT_MAX_FILE_SIZE, type DefinedTool, type DropZoneProps, EmbedFileUploadBackend, type ExecutingToolDisplay, type FileAttachment, type FileProcessingState, type FileProcessingStatus, type FileTransformer, type FileTransformerContext, type FileTransformerMap, type FileUploadBackend, type FileUploadConfig, type FloatingButtonProps, type InlineSaveProps, type ListChatsOptions, type ListCommandsOptions, LocalStorageChatRepository, LocalStorageCommandRepository, type Message, type PendingToolApproval, type PersistedContentPart, type PersistedFileContent, type PersistedFileMetadata, type PersistedMessage, type PersistedMessageContent, type PersistedTextContent, type ProcessAttachmentsConfig, type PromptsContextValue, type RegisterToolsOptions, type SavedCommand, type SendMessageOptions, type SubmitMode, type ToolExecutionContext, type ToolOptions, type ToolRegistryContextValue, type ToolsDefinition, type TriggerWorkflowOptions, UseAIChat, UseAIChatPanel, type UseAIChatPanelProps, type UseAIChatPanelStrings, type UseAIChatPanelTheme, type UseAIChatProps, UseAIClient, type UseAIConfig, type UseAIContextValue, UseAIFloatingButton, UseAIFloatingChatWrapper, type UseAIOptions, UseAIProvider, type UseAIProviderProps, type UseAIResult, type UseAIStrings, type UseAITheme, type UseAIWorkflowResult, type UseAgentSelectionOptions, type UseAgentSelectionReturn, type UseChatManagementOptions, type UseChatManagementReturn, type UseCommandManagementOptions, type UseCommandManagementReturn, type UseDropdownStateOptions, type UseDropdownStateReturn, type UseFeedbackOptions, type UseFeedbackReturn, type UseFileUploadOptions, type UseFileUploadReturn, type UseMessageQueueOptions, type UseMessageQueueReturn, type UsePromptStateOptions, type UsePromptStateReturn, type UseServerEventsOptions, type UseServerEventsReturn, type UseSlashCommandsOptions, type UseSlashCommandsReturn, type UseToolSystemOptions, type UseToolSystemReturn, type WorkflowProgress, clearTransformationCache, convertToolsToDefinitions, defaultStrings, defaultTheme, defineTool, executeDefinedTool, findTransformerPattern, generateChatId, generateCommandId, generateMessageId, matchesMimeType, processAttachments, useAI, useAIContext, useAIWorkflow, useAgentSelection, useChatManagement, useCommandManagement, useDropdownState, useFeedback, useFileUpload, useMessageQueue, usePromptState, useServerEvents, useSlashCommands, useStableTools, useStrings, useTheme, useToolSystem, validateCommandName };
package/dist/index.js CHANGED
@@ -94,6 +94,8 @@ var defaultStrings = {
94
94
  API_OVERLOADED: "The AI service is currently experiencing high demand. Please try again in a moment.",
95
95
  /** Error when rate limited */
96
96
  RATE_LIMITED: "Too many requests. Please wait a moment before trying again.",
97
+ /** Error when the connection was lost while a response was being generated */
98
+ CONNECTION_LOST: "The connection was lost. Please send your message again.",
97
99
  /** Error for unknown/unexpected errors */
98
100
  UNKNOWN_ERROR: "An unexpected error occurred. Please try again."
99
101
  },
@@ -280,7 +282,17 @@ function getTextFromContent(content) {
280
282
  if (typeof content === "string") {
281
283
  return content;
282
284
  }
283
- return content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
285
+ return content.flatMap((part) => {
286
+ if (part.type === "text") return [part.text];
287
+ if (part.type === "transformed_file") return [part.text];
288
+ return [];
289
+ }).join("\n");
290
+ }
291
+ function getDisplayTextFromContent(content) {
292
+ if (typeof content === "string") {
293
+ return content;
294
+ }
295
+ return content.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
284
296
  }
285
297
 
286
298
  // src/utils/mergeAssistantMessages.ts
@@ -344,6 +356,17 @@ function mergeAssistantMessagesForDisplay(messages) {
344
356
  return result;
345
357
  }
346
358
 
359
+ // src/utils/keyboard.ts
360
+ function shouldSubmitOnEnter(e, mode) {
361
+ if (e.key !== "Enter" || e.nativeEvent.isComposing || e.keyCode === 229) {
362
+ return false;
363
+ }
364
+ if (mode === "enter") {
365
+ return !e.shiftKey;
366
+ }
367
+ return e.metaKey || e.ctrlKey;
368
+ }
369
+
347
370
  // src/components/MarkdownContent.tsx
348
371
  import ReactMarkdown from "react-markdown";
349
372
  import remarkGfm from "remark-gfm";
@@ -2166,7 +2189,8 @@ function UseAIChatPanel({
2166
2189
  onFeedback,
2167
2190
  pendingApprovals = [],
2168
2191
  onApproveToolCall,
2169
- onRejectToolCall
2192
+ onRejectToolCall,
2193
+ submitMode = "enter"
2170
2194
  }) {
2171
2195
  const strings = useStrings();
2172
2196
  const theme = useTheme();
@@ -2241,7 +2265,7 @@ function UseAIChatPanel({
2241
2265
  if (slashCommands.handleKeyDown(e)) {
2242
2266
  return;
2243
2267
  }
2244
- if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && !(e.keyCode === 229)) {
2268
+ if (shouldSubmitOnEnter(e, submitMode)) {
2245
2269
  e.preventDefault();
2246
2270
  handleSend();
2247
2271
  }
@@ -2339,7 +2363,7 @@ function UseAIChatPanel({
2339
2363
  if (displayMessages.length > 0) {
2340
2364
  const firstUserMsg = displayMessages.find((m) => m.role === "user");
2341
2365
  if (firstUserMsg) {
2342
- const textContent = getTextFromContent(firstUserMsg.content);
2366
+ const textContent = getDisplayTextFromContent(firstUserMsg.content);
2343
2367
  const maxLength = 30;
2344
2368
  return textContent.length > maxLength ? textContent.substring(0, maxLength) + "..." : textContent || strings.header.newChat;
2345
2369
  }
@@ -2645,7 +2669,7 @@ function UseAIChatPanel({
2645
2669
  {
2646
2670
  style: {
2647
2671
  display: "grid",
2648
- gridTemplateColumns: "repeat(2, 1fr)",
2672
+ gridTemplateColumns: "1fr",
2649
2673
  gap: "8px",
2650
2674
  width: "100%",
2651
2675
  maxWidth: "320px"
@@ -2721,7 +2745,7 @@ function UseAIChatPanel({
2721
2745
  "data-testid": "save-command-button",
2722
2746
  onClick: (e) => {
2723
2747
  e.stopPropagation();
2724
- const messageText = getTextFromContent(message.content);
2748
+ const messageText = getDisplayTextFromContent(message.content);
2725
2749
  slashCommands.startSavingCommand(message.id, messageText);
2726
2750
  },
2727
2751
  title: "Save as slash command",
@@ -2791,13 +2815,17 @@ function UseAIChatPanel({
2791
2815
  }
2792
2816
  ),
2793
2817
  /* @__PURE__ */ jsx12(MarkdownContent, { content: getTextFromContent(message.content) })
2794
- ] }) : getTextFromContent(message.content)
2818
+ ] }) : (
2819
+ // User/tool bubbles: display-only text so transformed_file
2820
+ // (e.g. OCR body) isn't dumped into the chat bubble.
2821
+ getDisplayTextFromContent(message.content)
2822
+ )
2795
2823
  ]
2796
2824
  }
2797
2825
  ),
2798
2826
  slashCommands.renderInlineSaveUI({
2799
2827
  messageId: message.id,
2800
- messageText: getTextFromContent(message.content)
2828
+ messageText: getDisplayTextFromContent(message.content)
2801
2829
  })
2802
2830
  ]
2803
2831
  }
@@ -3257,9 +3285,10 @@ function useChatUIContext() {
3257
3285
  }
3258
3286
  return context;
3259
3287
  }
3260
- function UseAIChat({ floating = false }) {
3288
+ function UseAIChat({ floating = false, submitMode }) {
3261
3289
  const ctx = useChatUIContext();
3262
3290
  const chatPanelProps = {
3291
+ submitMode: submitMode ?? ctx.submitMode,
3263
3292
  onSendMessage: ctx.sendMessage,
3264
3293
  messages: ctx.messages,
3265
3294
  loading: ctx.loading,
@@ -3324,9 +3353,12 @@ var UseAIClient = class {
3324
3353
  }
3325
3354
  socket = null;
3326
3355
  eventHandlers = /* @__PURE__ */ new Map();
3327
- reconnectAttempts = 0;
3328
- maxReconnectAttempts = 5;
3356
+ // Reconnect indefinitely so clients recover after extended outages (mobile
3357
+ // app backgrounded long enough for server pingTimeout, airplane mode, etc.).
3358
+ // Socket.IO applies exponential backoff capped at reconnectionDelayMax,
3359
+ // so steady-state retry frequency is ~one attempt per 10s.
3329
3360
  reconnectDelay = 1e3;
3361
+ reconnectDelayMax = 1e4;
3330
3362
  // Session state
3331
3363
  _threadId = null;
3332
3364
  _tools = [];
@@ -3364,14 +3396,14 @@ var UseAIClient = class {
3364
3396
  this.socket = io(this.serverUrl, {
3365
3397
  transports: ["polling", "websocket"],
3366
3398
  reconnection: true,
3367
- reconnectionAttempts: this.maxReconnectAttempts,
3399
+ reconnectionAttempts: Infinity,
3368
3400
  reconnectionDelay: this.reconnectDelay,
3401
+ reconnectionDelayMax: this.reconnectDelayMax,
3369
3402
  withCredentials: true
3370
3403
  });
3371
3404
  this.socket.on("connect", () => {
3372
3405
  console.log("[UseAI] Connected to server");
3373
3406
  console.log("[UseAI] Transport:", this.socket?.io?.engine?.transport?.name);
3374
- this.reconnectAttempts = 0;
3375
3407
  const engine = this.socket?.io?.engine;
3376
3408
  if (engine) {
3377
3409
  engine.on("upgrade", (transport) => {
@@ -4176,35 +4208,89 @@ var LocalStorageChatRepository = class {
4176
4208
  }
4177
4209
  };
4178
4210
 
4211
+ // src/fileUpload/buildPersistedParts.ts
4212
+ function buildPersistedParts(message, attachments, fileContent) {
4213
+ const parts = [];
4214
+ if (message.trim()) {
4215
+ parts.push({ type: "text", text: message });
4216
+ }
4217
+ const transformedByKey = /* @__PURE__ */ new Map();
4218
+ for (const part of fileContent) {
4219
+ if (part.type === "transformed_file") {
4220
+ const key = `${part.originalFile.name}:${part.originalFile.size}:${part.originalFile.mimeType}`;
4221
+ const list = transformedByKey.get(key);
4222
+ if (list) {
4223
+ list.push(part);
4224
+ } else {
4225
+ transformedByKey.set(key, [part]);
4226
+ }
4227
+ }
4228
+ }
4229
+ for (const attachment of attachments) {
4230
+ const key = `${attachment.file.name}:${attachment.file.size}:${attachment.file.type}`;
4231
+ const transformed = transformedByKey.get(key)?.shift();
4232
+ if (transformed) {
4233
+ parts.push({
4234
+ type: "transformed_file",
4235
+ text: transformed.text,
4236
+ originalFile: transformed.originalFile
4237
+ });
4238
+ } else {
4239
+ parts.push({
4240
+ type: "file",
4241
+ file: {
4242
+ name: attachment.file.name,
4243
+ size: attachment.file.size,
4244
+ mimeType: attachment.file.type
4245
+ }
4246
+ });
4247
+ }
4248
+ }
4249
+ return parts;
4250
+ }
4251
+
4179
4252
  // src/hooks/useChatManagement.ts
4180
4253
  import { useState as useState7, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect5 } from "react";
4181
4254
 
4182
4255
  // src/utils/messageConversion.ts
4183
4256
  function transformMessagesToClientFormat(persistedMessages) {
4184
4257
  return persistedMessages.map((msg) => {
4185
- const textContent = getTextFromContent(msg.content);
4186
4258
  switch (msg.role) {
4187
4259
  case "tool":
4188
4260
  return {
4189
4261
  id: msg.id,
4190
4262
  role: "tool",
4191
- content: textContent,
4263
+ content: getTextFromContent(msg.content),
4192
4264
  toolCallId: msg.toolCallId || ""
4193
4265
  };
4194
4266
  case "assistant":
4195
4267
  return {
4196
4268
  id: msg.id,
4197
4269
  role: "assistant",
4198
- content: textContent,
4270
+ content: getTextFromContent(msg.content),
4199
4271
  ...msg.toolCalls && msg.toolCalls.length > 0 ? { toolCalls: msg.toolCalls } : {},
4200
4272
  ...msg.reasoningParts && msg.reasoningParts.length > 0 ? { reasoningParts: msg.reasoningParts } : {}
4201
4273
  };
4202
- case "user":
4203
- return {
4204
- id: msg.id,
4205
- role: "user",
4206
- content: textContent
4207
- };
4274
+ case "user": {
4275
+ if (typeof msg.content === "string") {
4276
+ return { id: msg.id, role: "user", content: msg.content };
4277
+ }
4278
+ const parts = msg.content.flatMap((p) => {
4279
+ if (p.type === "text") {
4280
+ return [{ type: "text", text: p.text }];
4281
+ }
4282
+ if (p.type === "transformed_file") {
4283
+ return [{
4284
+ type: "text",
4285
+ text: `[Content of file "${p.originalFile.name}" (${p.originalFile.mimeType})]:
4286
+
4287
+ ${p.text}`
4288
+ }];
4289
+ }
4290
+ return [];
4291
+ });
4292
+ return { id: msg.id, role: "user", content: parts };
4293
+ }
4208
4294
  }
4209
4295
  });
4210
4296
  }
@@ -4377,7 +4463,7 @@ function useChatManagement({
4377
4463
  };
4378
4464
  chat.messages.push(newMessage);
4379
4465
  if (!chat.title) {
4380
- const text = getTextFromContent(content);
4466
+ const text = getDisplayTextFromContent(content);
4381
4467
  if (text) {
4382
4468
  chat.title = generateChatTitle(text);
4383
4469
  }
@@ -4421,7 +4507,7 @@ function useChatManagement({
4421
4507
  if (!chat.title) {
4422
4508
  const firstUserMessage = chat.messages.find((msg) => msg.role === "user");
4423
4509
  if (firstUserMessage) {
4424
- const textContent = getTextFromContent(firstUserMessage.content);
4510
+ const textContent = getDisplayTextFromContent(firstUserMessage.content);
4425
4511
  if (textContent) {
4426
4512
  chat.title = generateChatTitle(textContent);
4427
4513
  }
@@ -5143,6 +5229,8 @@ function useServerEvents({
5143
5229
  const [streamingText, setStreamingText] = useState13("");
5144
5230
  const [streamingReasoning, setStreamingReasoning] = useState13("");
5145
5231
  const streamingChatIdRef = useRef10(null);
5232
+ const loadingRef = useRef10(loading);
5233
+ loadingRef.current = loading;
5146
5234
  const messageCountAtRunStartRef = useRef10(0);
5147
5235
  const hasTextFromPriorStepRef = useRef10(false);
5148
5236
  const [executingToolRaw, setExecutingTool] = useState13(null);
@@ -5238,6 +5326,17 @@ function useServerEvents({
5238
5326
  setLoading(false);
5239
5327
  }
5240
5328
  }, []);
5329
+ const handleDisconnect = useCallback11(() => {
5330
+ if (!loadingRef.current) return;
5331
+ const strs = stringsRef.current;
5332
+ const message = strs.errors[ErrorCode.CONNECTION_LOST] || strs.errors[ErrorCode.UNKNOWN_ERROR];
5333
+ saveAIResponseRef.current(message, "error");
5334
+ setStreamingText("");
5335
+ setStreamingReasoning("");
5336
+ streamingChatIdRef.current = null;
5337
+ setExecutingTool(null);
5338
+ setLoading(false);
5339
+ }, []);
5241
5340
  const executingTool = executingToolRaw ? {
5242
5341
  displayText: executingToolRaw.title ?? executingToolFallbackRef.current ?? strings.toolExecution.fallbackMessages[0]
5243
5342
  } : null;
@@ -5249,7 +5348,8 @@ function useServerEvents({
5249
5348
  executingTool,
5250
5349
  streamingChatIdRef,
5251
5350
  streamingReasoning,
5252
- handleServerEvent
5351
+ handleServerEvent,
5352
+ handleDisconnect
5253
5353
  };
5254
5354
  }
5255
5355
 
@@ -5415,7 +5515,8 @@ function UseAIProvider({
5415
5515
  theme: customTheme,
5416
5516
  strings: customStrings,
5417
5517
  visibleAgentIds,
5418
- onOpenChange
5518
+ onOpenChange,
5519
+ submitMode = "enter"
5419
5520
  }) {
5420
5521
  const fileUploadConfig = fileUploadConfigProp === false ? void 0 : fileUploadConfigProp ?? DEFAULT_FILE_UPLOAD_CONFIG;
5421
5522
  const theme = { ...defaultTheme, ...customTheme };
@@ -5474,12 +5575,17 @@ function UseAIProvider({
5474
5575
  } = useCommandManagement({ repository: commandRepository });
5475
5576
  const handleServerEventRef = useRef12(serverEvents.handleServerEvent);
5476
5577
  handleServerEventRef.current = serverEvents.handleServerEvent;
5578
+ const handleDisconnectRef = useRef12(serverEvents.handleDisconnect);
5579
+ handleDisconnectRef.current = serverEvents.handleDisconnect;
5477
5580
  useEffect11(() => {
5478
5581
  console.log("[UseAIProvider] Initializing client with serverUrl:", serverUrl);
5479
5582
  const client = new UseAIClient(serverUrl);
5480
5583
  const unsubscribeConnection = client.onConnectionStateChange((isConnected) => {
5481
5584
  console.log("[UseAIProvider] Connection state changed:", isConnected);
5482
5585
  setConnected(isConnected);
5586
+ if (!isConnected) {
5587
+ handleDisconnectRef.current();
5588
+ }
5483
5589
  });
5484
5590
  console.log("[UseAIProvider] Connecting...");
5485
5591
  client.connect();
@@ -5529,27 +5635,10 @@ function UseAIProvider({
5529
5635
  let persistedContent = message;
5530
5636
  let multimodalContent;
5531
5637
  if (attachments && attachments.length > 0) {
5532
- const persistedParts = [];
5533
- if (message.trim()) {
5534
- persistedParts.push({ type: "text", text: message });
5535
- }
5536
- for (const attachment of attachments) {
5537
- persistedParts.push({
5538
- type: "file",
5539
- file: {
5540
- name: attachment.file.name,
5541
- size: attachment.file.size,
5542
- mimeType: attachment.file.type
5543
- }
5544
- });
5545
- }
5546
- persistedContent = persistedParts;
5547
- if (activeChatId) {
5548
- await chatManagement.saveUserMessage(activeChatId, persistedContent);
5549
- }
5550
5638
  serverEvents.setLoading(true);
5639
+ let fileContent;
5551
5640
  try {
5552
- const fileContent = await processAttachments(attachments, {
5641
+ fileContent = await processAttachments(attachments, {
5553
5642
  getCurrentChat: chatManagement.getCurrentChat,
5554
5643
  backend: fileUploadConfig?.backend,
5555
5644
  transformers: fileUploadConfig?.transformers,
@@ -5557,17 +5646,21 @@ function UseAIProvider({
5557
5646
  setFileProcessingState(state);
5558
5647
  }
5559
5648
  });
5560
- multimodalContent = [];
5561
- if (message.trim()) {
5562
- multimodalContent.push({ type: "text", text: message });
5563
- }
5564
- multimodalContent.push(...fileContent);
5565
5649
  } catch (error) {
5566
5650
  serverEvents.setLoading(false);
5567
5651
  throw error;
5568
5652
  } finally {
5569
5653
  setFileProcessingState(null);
5570
5654
  }
5655
+ persistedContent = buildPersistedParts(message, attachments, fileContent);
5656
+ if (activeChatId) {
5657
+ await chatManagement.saveUserMessage(activeChatId, persistedContent);
5658
+ }
5659
+ multimodalContent = [];
5660
+ if (message.trim()) {
5661
+ multimodalContent.push({ type: "text", text: message });
5662
+ }
5663
+ multimodalContent.push(...fileContent);
5571
5664
  } else {
5572
5665
  if (activeChatId) {
5573
5666
  await chatManagement.saveUserMessage(activeChatId, persistedContent);
@@ -5680,7 +5773,8 @@ function UseAIProvider({
5680
5773
  feedback: {
5681
5774
  enabled: feedback.enabled,
5682
5775
  submit: feedback.submitFeedback
5683
- }
5776
+ },
5777
+ submitMode
5684
5778
  };
5685
5779
  const isUIDisabled = CustomButton === null || CustomChat === null;
5686
5780
  const ButtonComponent = isUIDisabled ? null : CustomButton || UseAIFloatingButton;
@@ -5713,7 +5807,8 @@ function UseAIProvider({
5713
5807
  onFeedback: feedback.submitFeedback,
5714
5808
  pendingApprovals: toolSystem.pendingApprovals,
5715
5809
  onApproveToolCall: toolSystem.pendingApprovals.length > 0 ? toolSystem.approveAll : void 0,
5716
- onRejectToolCall: toolSystem.pendingApprovals.length > 0 ? toolSystem.rejectAll : void 0
5810
+ onRejectToolCall: toolSystem.pendingApprovals.length > 0 ? toolSystem.rejectAll : void 0,
5811
+ submitMode
5717
5812
  };
5718
5813
  const renderDefaultChat = () => {
5719
5814
  if (isUIDisabled) return null;