@marimo-team/islands 0.15.2 → 0.15.4

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 (211) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-B68gXlbY.js} +323 -328
  2. package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-Cw1oA8Tn.js} +13 -13
  3. package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-CQrhBg_9.js} +67 -67
  4. package/dist/any-language-editor-pzUl6lxp.js +27 -0
  5. package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
  6. package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-DdYf2VnU.js} +144 -144
  7. package/dist/assets/{worker-COGufAQn.js → worker-BcG8m3h5.js} +33 -29
  8. package/dist/asterisk-DS281yxp.js +271 -0
  9. package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DmmYotkP.js} +10 -10
  10. package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-Dyz4zMHJ.js} +8 -8
  11. package/dist/{channel-CeuXqUAU.js → channel-CCL8jXAe.js} +1 -1
  12. package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-BfKwWLfJ.js} +1 -1
  13. package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-CFQU7zSp.js} +1 -1
  14. package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-DDjZzUcl.js} +1 -1
  15. package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-nBRjBU1H.js} +117 -117
  16. package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-B-g8sJzl.js} +1 -1
  17. package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-B7QSJS3J.js} +1 -1
  18. package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-pGXll4d1.js} +7 -7
  19. package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Dx9h-1mv.js} +1 -1
  20. package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-BXryI7DY.js} +2 -2
  21. package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-BXryI7DY.js} +2 -2
  22. package/dist/{clone-78au0tn1.js → clone-DqwV7ges.js} +1 -1
  23. package/dist/cose-bilkent-S5V4N54A-Di6FNMXz.js +2609 -0
  24. package/dist/{cytoscape.esm-BYnVVhJX.js → cytoscape.esm-DfdJODL8.js} +34 -34
  25. package/dist/{dagre-2BBEFEWP-BfEn3ZUV.js → dagre-5GWH7T2D-BTZPMTey.js} +6 -6
  26. package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-ryatXXby.js} +11 -11
  27. package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-D79_zdOu.js} +59 -60
  28. package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-DX2A_SD0.js} +15 -15
  29. package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-DM6VMTrJ.js} +10 -10
  30. package/dist/dockerfile-BoowzQlp.js +194 -0
  31. package/dist/ebnf-DUPDuY4r.js +78 -0
  32. package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-BBirxtlI.js} +8 -8
  33. package/dist/fcl-CPC2WYrI.js +103 -0
  34. package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DyVweEMs.js} +9 -9
  35. package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-DTB7FX7r.js} +34 -34
  36. package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-BrbIb5pD.js} +4 -4
  37. package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-DhMX4nmM.js} +33 -33
  38. package/dist/{graph-77W6heli.js → graph-CuLSrclI.js} +3 -3
  39. package/dist/http-D9LttvKF.js +44 -0
  40. package/dist/{index-BOojn38D.js → index-BNgdUQ2e.js} +7711 -7711
  41. package/dist/index-DIy6LHLJ.js +98 -0
  42. package/dist/{index-CmozKMxx.js → index-Df2dsx1t.js} +6 -6
  43. package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
  44. package/dist/{index-Bfk9dnyS.js → index-cz_xaKvT.js} +33090 -32892
  45. package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-CCBHc7-K.js} +2 -2
  46. package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-LhGSj54j.js} +24 -26
  47. package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-aegTMFS6.js} +14 -14
  48. package/dist/{layout-XySVHJgD.js → layout-BEARWMhl.js} +81 -81
  49. package/dist/{linear-PbooOqg7.js → linear-fbJq6cdO.js} +35 -35
  50. package/dist/{main-B5yML0bw.js → main-HerZgEhd.js} +76533 -69945
  51. package/dist/main.js +1 -1
  52. package/dist/{mermaid-Cg5IX6Nv.js → mermaid-DxPYK0KX.js} +6160 -7493
  53. package/dist/min-DBJkhObB.js +80 -0
  54. package/dist/mindmap-definition-Q6HEUPPD-A3Fh5XDZ.js +785 -0
  55. package/dist/nginx-zDPm3Z74.js +89 -0
  56. package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-USMrY6k3.js} +19 -19
  57. package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Q9uFlCV0.js} +17 -17
  58. package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BuPh-qpK.js} +6 -6
  59. package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-HSqJPRfa.js} +3577 -3577
  60. package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-CHROYNU_.js} +7 -7
  61. package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-DkUqHP2d.js} +5 -5
  62. package/dist/sequenceDiagram-C3RYC4MD-YoPTMplP.js +2519 -0
  63. package/dist/{slides-component-BJLlPJSr.js → slides-component-D7CHSR00.js} +66 -66
  64. package/dist/solr-BNlsLglM.js +41 -0
  65. package/dist/spreadsheet-C-cy4P5N.js +49 -0
  66. package/dist/{stateDiagram-UUKSUZ4H-CYXbjaom.js → stateDiagram-KXAO66HF-DEN00mVU.js} +5 -5
  67. package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-DlQqSUAa.js} +2 -2
  68. package/dist/style.css +1 -1
  69. package/dist/tiddlywiki-5wqsXtSk.js +155 -0
  70. package/dist/tiki-__Kn3CeS.js +181 -0
  71. package/dist/{time-B9SZnSen.js → time-BtVcKqeD.js} +58 -58
  72. package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DEteLt8D.js} +10 -12
  73. package/dist/{timer-BYwnU4DF.js → timer-B0-z63CM.js} +16 -16
  74. package/dist/{treemap-75Q7IDZK-CKP4vV_0.js → treemap-75Q7IDZK-8S6podme.js} +14 -14
  75. package/dist/{vega-component-CpgdqX2d.js → vega-component-D35L45kI.js} +30 -30
  76. package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DKwGThyy.js} +7 -7
  77. package/package.json +44 -41
  78. package/src/__mocks__/notebook.ts +3 -0
  79. package/src/__mocks__/requests.ts +3 -0
  80. package/src/__tests__/__snapshots__/CellStatus.test.tsx.snap +12 -12
  81. package/src/__tests__/chat-utils.test.ts +26 -211
  82. package/src/components/ai/ai-model-dropdown.tsx +25 -9
  83. package/src/components/ai/ai-provider-icon.tsx +5 -1
  84. package/src/components/app-config/ai-config.tsx +7 -0
  85. package/src/components/chat/acp/__tests__/__snapshots__/prompt.test.ts.snap +304 -0
  86. package/src/components/chat/acp/__tests__/atoms.test.ts +56 -0
  87. package/src/components/chat/acp/__tests__/prompt.test.ts +12 -0
  88. package/src/components/chat/acp/__tests__/state.test.ts +621 -0
  89. package/src/components/chat/acp/agent-docs.tsx +78 -0
  90. package/src/components/chat/acp/agent-panel.css +23 -0
  91. package/src/components/chat/acp/agent-panel.tsx +715 -0
  92. package/src/components/chat/acp/agent-selector.tsx +138 -0
  93. package/src/components/chat/acp/blocks.tsx +664 -0
  94. package/src/components/chat/acp/common.tsx +198 -0
  95. package/src/components/chat/acp/prompt.ts +284 -0
  96. package/src/components/chat/acp/scroll-to-bottom-button.tsx +50 -0
  97. package/src/components/chat/acp/session-tabs.tsx +138 -0
  98. package/src/components/chat/acp/state.ts +263 -0
  99. package/src/components/chat/acp/thread.tsx +121 -0
  100. package/src/components/chat/acp/types.ts +63 -0
  101. package/src/components/chat/acp/utils.ts +45 -0
  102. package/src/components/chat/chat-components.tsx +71 -0
  103. package/src/components/chat/chat-panel.tsx +481 -291
  104. package/src/components/chat/chat-utils.ts +50 -0
  105. package/src/components/chat/markdown-renderer.tsx +3 -7
  106. package/src/components/chat/tool-call-accordion.tsx +6 -6
  107. package/src/components/datasources/__tests__/utils.test.ts +6 -0
  108. package/src/components/datasources/column-preview.tsx +1 -3
  109. package/src/components/editor/actions/useNotebookActions.tsx +1 -1
  110. package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
  111. package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
  112. package/src/components/editor/ai/completion-handlers.tsx +2 -4
  113. package/src/components/editor/ai/completion-utils.ts +85 -11
  114. package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
  115. package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
  116. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
  117. package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
  118. package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
  119. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
  120. package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
  121. package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
  122. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
  123. package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
  124. package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
  125. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
  126. package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
  127. package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
  128. package/src/components/editor/chrome/types.ts +10 -0
  129. package/src/components/editor/chrome/wrapper/app-chrome.tsx +55 -31
  130. package/src/components/editor/controls/command-palette-button.tsx +1 -1
  131. package/src/components/editor/controls/command-palette.tsx +5 -4
  132. package/src/components/editor/controls/state.ts +4 -0
  133. package/src/components/editor/package-alert.tsx +108 -58
  134. package/src/components/editor/renderers/CellArray.tsx +2 -0
  135. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
  136. package/src/components/pages/edit-page.tsx +7 -3
  137. package/src/core/ai/chat-utils.ts +26 -43
  138. package/src/core/ai/config.ts +1 -1
  139. package/src/core/ai/context/__tests__/registry.test.ts +277 -3
  140. package/src/core/ai/context/context.ts +11 -1
  141. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
  142. package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
  143. package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
  144. package/src/core/ai/context/providers/cell-output.ts +349 -0
  145. package/src/core/ai/context/providers/common.ts +5 -1
  146. package/src/core/ai/context/providers/file.ts +287 -0
  147. package/src/core/ai/context/registry.ts +79 -0
  148. package/src/core/ai/state.ts +22 -5
  149. package/src/core/alerts/state.ts +71 -3
  150. package/src/core/cells/cell.ts +2 -2
  151. package/src/core/cells/cells.ts +1 -1
  152. package/src/core/cells/logs.ts +1 -1
  153. package/src/core/cells/runs.ts +6 -5
  154. package/src/core/codemirror/ai/resources.ts +47 -5
  155. package/src/core/codemirror/ai/state.ts +12 -0
  156. package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
  157. package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
  158. package/src/core/codemirror/theme/dark.ts +1 -1
  159. package/src/core/config/capabilities.ts +1 -1
  160. package/src/core/config/feature-flag.tsx +2 -0
  161. package/src/core/datasets/__tests__/data-source.test.ts +24 -0
  162. package/src/core/errors/__tests__/errors.test.ts +2 -0
  163. package/src/core/islands/bridge.ts +1 -0
  164. package/src/core/islands/main.ts +1 -0
  165. package/src/core/kernel/messages.ts +12 -6
  166. package/src/core/layout/layout.ts +3 -3
  167. package/src/core/network/requests-network.ts +8 -0
  168. package/src/core/network/requests-static.ts +1 -0
  169. package/src/core/network/requests-toasting.ts +1 -0
  170. package/src/core/network/types.ts +4 -1
  171. package/src/core/wasm/bridge.ts +18 -2
  172. package/src/core/wasm/worker/bootstrap.ts +3 -1
  173. package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
  174. package/src/core/wasm/worker/types.ts +3 -0
  175. package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
  176. package/src/css/app/Cell.css +42 -21
  177. package/src/css/app/codemirror.css +5 -1
  178. package/src/css/globals.css +3 -0
  179. package/src/css/md.css +1 -1
  180. package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
  181. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
  182. package/src/plugins/impl/chat/chat-ui.tsx +129 -110
  183. package/src/plugins/impl/chat/types.ts +5 -8
  184. package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
  185. package/src/plugins/impl/code/any-language-editor.tsx +11 -8
  186. package/src/plugins/impl/vega/vega.css +121 -0
  187. package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
  188. package/src/stories/cell.stories.tsx +6 -0
  189. package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
  190. package/src/theme/useTheme.ts +11 -6
  191. package/src/utils/Logger.ts +5 -6
  192. package/src/utils/__tests__/blob.test.ts +37 -0
  193. package/src/utils/arrays.ts +13 -0
  194. package/src/utils/fileToBase64.ts +21 -6
  195. package/src/utils/json/base64.ts +5 -2
  196. package/src/utils/numbers.ts +9 -7
  197. package/dist/any-language-editor-DC5170DQ.js +0 -45
  198. package/dist/asn1-jKiBa2Ya.js +0 -95
  199. package/dist/clojure-CCKyeQKf.js +0 -800
  200. package/dist/css-BkF-NPzE.js +0 -1553
  201. package/dist/index-5ZH_qS8j.js +0 -288
  202. package/dist/index-U4yn89qO.js +0 -341
  203. package/dist/javascript-C2yteZeJ.js +0 -691
  204. package/dist/min-DS5Jz-hg.js +0 -80
  205. package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
  206. package/dist/mllike-BSnXJBGA.js +0 -272
  207. package/dist/pug-CwAQJzGR.js +0 -248
  208. package/dist/python-BkR3uSy8.js +0 -313
  209. package/dist/rpm-IznJm2Xc.js +0 -57
  210. package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
  211. package/dist/ttcn-cfg-Bac_acMi.js +0 -88
@@ -1,28 +1,23 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
+ import type { UIMessage } from "@ai-sdk/react";
3
4
  import { useChat } from "@ai-sdk/react";
4
5
  import { storePrompt } from "@marimo-team/codemirror-ai";
5
6
  import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
6
- import type { Message } from "ai/react";
7
- import { useAtom, useAtomValue } from "jotai";
7
+ import { DefaultChatTransport, type ToolUIPart } from "ai";
8
+ import { useAtom, useAtomValue, useSetAtom } from "jotai";
8
9
  import {
10
+ AtSignIcon,
9
11
  BotMessageSquareIcon,
10
12
  ClockIcon,
11
13
  Loader2,
14
+ PaperclipIcon,
12
15
  PlusIcon,
13
16
  SendIcon,
14
17
  SettingsIcon,
15
18
  SquareIcon,
16
19
  } from "lucide-react";
17
- import {
18
- type Dispatch,
19
- memo,
20
- type SetStateAction,
21
- useEffect,
22
- useMemo,
23
- useRef,
24
- useState,
25
- } from "react";
20
+ import { memo, useEffect, useMemo, useRef, useState } from "react";
26
21
  import useEvent from "react-use-event-hook";
27
22
  import { Button } from "@/components/ui/button";
28
23
  import {
@@ -39,51 +34,79 @@ import {
39
34
  SelectLabel,
40
35
  SelectTrigger,
41
36
  } from "@/components/ui/select";
42
- import { addMessageToChat } from "@/core/ai/chat-utils";
37
+ import { replaceMessagesInChat } from "@/core/ai/chat-utils";
43
38
  import { useModelChange } from "@/core/ai/config";
39
+ import { AiModelId, type ProviderId } from "@/core/ai/ids/ids";
44
40
  import {
45
41
  activeChatAtom,
46
42
  type Chat,
47
43
  type ChatId,
48
- type ChatState,
49
44
  chatStateAtom,
50
45
  } from "@/core/ai/state";
51
- import { getCodes } from "@/core/codemirror/copilot/getCodes";
52
46
  import { aiAtom, aiEnabledAtom } from "@/core/config/config";
53
47
  import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
54
48
  import { FeatureFlagged } from "@/core/config/feature-flag";
55
49
  import { useRequestClient } from "@/core/network/requests";
56
50
  import { useRuntimeManager } from "@/core/runtime/config";
51
+ import type { ChatMessage } from "@/plugins/impl/chat/types";
57
52
  import { ErrorBanner } from "@/plugins/impl/common/error-banner";
58
53
  import { cn } from "@/utils/cn";
59
54
  import { timeAgo } from "@/utils/dates";
60
55
  import { Logger } from "@/utils/Logger";
61
- import { generateUUID } from "@/utils/uuid";
62
56
  import { AIModelDropdown } from "../ai/ai-model-dropdown";
63
57
  import { useOpenSettingsToTab } from "../app-config/state";
64
58
  import { PromptInput } from "../editor/ai/add-cell-with-ai";
65
- import { getAICompletionBody } from "../editor/ai/completion-utils";
59
+ import {
60
+ addContextCompletion,
61
+ CONTEXT_TRIGGER,
62
+ getAICompletionBodyWithAttachments,
63
+ } from "../editor/ai/completion-utils";
66
64
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
67
65
  import { CopyClipboardIcon } from "../icons/copy-icon";
66
+ import { Input } from "../ui/input";
68
67
  import { Tooltip, TooltipProvider } from "../ui/tooltip";
68
+ import { toast } from "../ui/use-toast";
69
+ import { AttachmentRenderer, FileAttachmentPill } from "./chat-components";
70
+ import {
71
+ convertToFileUIPart,
72
+ generateChatTitle,
73
+ isLastMessageReasoning,
74
+ } from "./chat-utils";
69
75
  import { MarkdownRenderer } from "./markdown-renderer";
70
76
  import { ReasoningAccordion } from "./reasoning-accordion";
71
77
  import { ToolCallAccordion } from "./tool-call-accordion";
72
78
 
79
+ // Default mode for the AI
80
+ const DEFAULT_MODE = "manual";
81
+
82
+ // We need to modify the backend to support attachments for other providers
83
+ // And other types
84
+ const PROVIDERS_THAT_SUPPORT_ATTACHMENTS = new Set<ProviderId>([
85
+ "openai",
86
+ "google",
87
+ "anthropic",
88
+ ]);
89
+ const SUPPORTED_ATTACHMENT_TYPES = ["image/*", "text/*"];
90
+ const MAX_ATTACHMENT_SIZE = 1024 * 1024 * 50; // 50MB
91
+
73
92
  interface ChatHeaderProps {
74
93
  onNewChat: () => void;
75
94
  activeChatId: ChatId | undefined;
76
95
  setActiveChat: (id: ChatId | null) => void;
77
- chats: Chat[];
78
96
  }
79
97
 
80
98
  const ChatHeader: React.FC<ChatHeaderProps> = ({
81
99
  onNewChat,
82
100
  activeChatId,
83
101
  setActiveChat,
84
- chats,
85
102
  }) => {
86
103
  const { handleClick } = useOpenSettingsToTab();
104
+ const chatState = useAtomValue(chatStateAtom);
105
+ const chats = useMemo(() => {
106
+ return [...chatState.chats.values()].sort(
107
+ (a, b) => b.updatedAt - a.updatedAt,
108
+ );
109
+ }, [chatState.chats]);
87
110
 
88
111
  return (
89
112
  <div className="flex border-b px-2 py-1 justify-between shrink-0 items-center">
@@ -149,28 +172,32 @@ const ChatHeader: React.FC<ChatHeaderProps> = ({
149
172
  };
150
173
 
151
174
  interface ChatMessageProps {
152
- message: Message;
175
+ message: UIMessage;
153
176
  index: number;
154
177
  onEdit: (index: number, newValue: string) => void;
155
- setChatState: Dispatch<SetStateAction<ChatState>>;
156
- chatState: ChatState;
157
178
  isStreamingReasoning: boolean;
158
179
  isLast: boolean;
159
180
  }
160
181
 
161
- const ChatMessage: React.FC<ChatMessageProps> = memo(
162
- ({ message, index, onEdit, isStreamingReasoning, isLast }) => (
163
- <div
164
- className={cn(
165
- "flex group relative",
166
- message.role === "user" ? "justify-end" : "justify-start",
167
- )}
168
- >
169
- {message.role === "user" ? (
182
+ function isToolPart(part: UIMessage["parts"][number]): part is ToolUIPart {
183
+ return part.type.startsWith("tool-");
184
+ }
185
+
186
+ const ChatMessageDisplay: React.FC<ChatMessageProps> = memo(
187
+ ({ message, index, onEdit, isStreamingReasoning, isLast }) => {
188
+ const renderUserMessage = (message: UIMessage) => {
189
+ const textParts = message.parts?.filter((p) => p.type === "text");
190
+ const content = textParts?.map((p) => p.text).join("\n");
191
+ const fileParts = message.parts?.filter((p) => p.type === "file");
192
+
193
+ return (
170
194
  <div className="w-[95%] bg-background border p-1 rounded-sm">
195
+ {fileParts?.map((filePart, idx) => (
196
+ <AttachmentRenderer attachment={filePart} key={idx} />
197
+ ))}
171
198
  <PromptInput
172
199
  key={message.id}
173
- value={message.content}
200
+ value={content}
174
201
  placeholder="Type your message..."
175
202
  onChange={() => {
176
203
  // noop
@@ -186,12 +213,31 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
186
213
  }}
187
214
  />
188
215
  </div>
189
- ) : (
216
+ );
217
+ };
218
+
219
+ const renderOtherMessage = (message: UIMessage) => {
220
+ const textParts = message.parts.filter((p) => p.type === "text");
221
+ const content = textParts.map((p) => p.text).join("\n");
222
+
223
+ return (
190
224
  <div className="w-[95%] break-words">
191
225
  <div className="absolute right-1 top-1 opacity-0 group-hover:opacity-100 transition-opacity">
192
- <CopyClipboardIcon className="h-3 w-3" value={message.content} />
226
+ <CopyClipboardIcon className="h-3 w-3" value={content || ""} />
193
227
  </div>
194
- {message.parts?.map((part, i) => {
228
+ {message.parts.map((part, i) => {
229
+ if (isToolPart(part)) {
230
+ return (
231
+ <ToolCallAccordion
232
+ key={i}
233
+ index={i}
234
+ toolName={part.type}
235
+ result={part.output}
236
+ state={part.state}
237
+ />
238
+ );
239
+ }
240
+
195
241
  switch (part.type) {
196
242
  case "text":
197
243
  return <MarkdownRenderer key={i} content={part.text} />;
@@ -199,60 +245,100 @@ const ChatMessage: React.FC<ChatMessageProps> = memo(
199
245
  case "reasoning":
200
246
  return (
201
247
  <ReasoningAccordion
202
- reasoning={part.reasoning}
248
+ reasoning={part.text}
203
249
  key={i}
204
250
  index={i}
205
251
  isStreaming={
206
252
  isLast &&
207
253
  isStreamingReasoning &&
208
254
  // If there are multiple reasoning parts, only show the last one
209
- i === (message.parts?.length || 0) - 1
255
+ i === (message.parts.length || 0) - 1
210
256
  }
211
257
  />
212
258
  );
213
259
 
214
- case "tool-invocation":
260
+ case "dynamic-tool":
215
261
  return (
216
262
  <ToolCallAccordion
217
263
  key={i}
218
264
  index={i}
219
- toolName={part.toolInvocation.toolName}
220
- result={
221
- part.toolInvocation.state === "result"
222
- ? part.toolInvocation.result
223
- : null
224
- }
225
- state={part.toolInvocation.state}
265
+ toolName={part.type}
266
+ result={part.output}
267
+ state={part.state}
226
268
  />
227
269
  );
228
270
 
271
+ // These are cryptographic signatures, so we don't need to render them
272
+ case "data-reasoning-signature":
273
+ return null;
274
+
229
275
  /* handle other part types … */
230
276
  default:
231
- return null;
277
+ if (part.type.startsWith("data-")) {
278
+ Logger.log("Found data part", part);
279
+ return null;
280
+ }
281
+
282
+ Logger.error("Unhandled part type:", part.type);
283
+ try {
284
+ return (
285
+ <MarkdownRenderer
286
+ key={i}
287
+ content={JSON.stringify(part, null, 2)}
288
+ />
289
+ );
290
+ } catch (error) {
291
+ Logger.error("Error rendering part:", part.type, error);
292
+ return null;
293
+ }
232
294
  }
233
295
  })}
234
296
  </div>
235
- )}
236
- </div>
237
- ),
297
+ );
298
+ };
299
+
300
+ return (
301
+ <div
302
+ className={cn(
303
+ "flex group relative",
304
+ message.role === "user" ? "justify-end" : "justify-start",
305
+ )}
306
+ >
307
+ {message.role === "user"
308
+ ? renderUserMessage(message)
309
+ : renderOtherMessage(message)}
310
+ </div>
311
+ );
312
+ },
238
313
  );
239
- ChatMessage.displayName = "ChatMessage";
314
+ ChatMessageDisplay.displayName = "ChatMessage";
240
315
 
241
316
  interface ChatInputFooterProps {
242
317
  isEmpty: boolean;
243
318
  onSendClick: () => void;
244
319
  isLoading: boolean;
245
320
  onStop: () => void;
321
+ onAddFiles: (files: File[]) => void;
322
+ fileInputRef: React.RefObject<HTMLInputElement | null>;
323
+ onAddContext: () => void;
246
324
  }
247
325
 
248
- const DEFAULT_MODE = "manual";
249
-
250
326
  const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
251
- ({ isEmpty, onSendClick, isLoading, onStop }) => {
327
+ ({
328
+ isEmpty,
329
+ onSendClick,
330
+ isLoading,
331
+ onStop,
332
+ fileInputRef,
333
+ onAddFiles,
334
+ onAddContext,
335
+ }) => {
252
336
  const ai = useAtomValue(aiAtom);
253
337
  const currentMode = ai?.mode || DEFAULT_MODE;
254
338
  const currentModel = ai?.models?.chat_model || DEFAULT_AI_MODEL;
255
- const { saveModeChange, saveModelChange } = useModelChange();
339
+ const currentProvider = AiModelId.parse(currentModel).providerId;
340
+
341
+ const { saveModeChange } = useModelChange();
256
342
 
257
343
  const modeOptions = [
258
344
  {
@@ -268,59 +354,99 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
268
354
  },
269
355
  ];
270
356
 
357
+ const isAttachmentSupported =
358
+ PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
359
+
271
360
  return (
272
- <div className="px-3 py-2 border-t border-border/20 flex items-center justify-between">
273
- <div className="flex items-center gap-2">
274
- <FeatureFlagged feature="mcp_docs">
275
- <Select value={currentMode} onValueChange={saveModeChange}>
276
- <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1 capitalize">
277
- {currentMode}
278
- </SelectTrigger>
279
- <SelectContent>
280
- <SelectGroup>
281
- <SelectLabel>AI Mode</SelectLabel>
282
- {modeOptions.map((option) => (
283
- <SelectItem
284
- key={option.value}
285
- value={option.value}
286
- className="text-xs"
287
- >
288
- <div className="flex flex-col">
289
- {option.label}
290
- <div className="text-muted-foreground text-xs pt-1 block">
291
- {option.subtitle}
361
+ <TooltipProvider>
362
+ <div className="px-3 py-2 border-t border-border/20 flex flex-row items-center justify-between">
363
+ <div className="flex items-center gap-2">
364
+ <FeatureFlagged feature="mcp_docs">
365
+ <Select value={currentMode} onValueChange={saveModeChange}>
366
+ <SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1 capitalize">
367
+ {currentMode}
368
+ </SelectTrigger>
369
+ <SelectContent>
370
+ <SelectGroup>
371
+ <SelectLabel>AI Mode</SelectLabel>
372
+ {modeOptions.map((option) => (
373
+ <SelectItem
374
+ key={option.value}
375
+ value={option.value}
376
+ className="text-xs"
377
+ >
378
+ <div className="flex flex-col">
379
+ {option.label}
380
+ <div className="text-muted-foreground text-xs pt-1 block">
381
+ {option.subtitle}
382
+ </div>
292
383
  </div>
293
- </div>
294
- </SelectItem>
295
- ))}
296
- </SelectGroup>
297
- </SelectContent>
298
- </Select>
299
- </FeatureFlagged>
300
- <AIModelDropdown
301
- value={currentModel}
302
- placeholder="Model"
303
- onSelect={(model) => saveModelChange(model, "chat")}
304
- triggerClassName="h-6 text-xs shadow-none! ring-0! bg-muted hover:bg-muted/30 rounded-sm"
305
- iconSize="small"
306
- showAddCustomModelDocs={true}
307
- forRole="chat"
308
- />
384
+ </SelectItem>
385
+ ))}
386
+ </SelectGroup>
387
+ </SelectContent>
388
+ </Select>
389
+ </FeatureFlagged>
390
+ <AIModelDropdown
391
+ placeholder="Model"
392
+ triggerClassName="h-6 text-xs shadow-none! ring-0! bg-muted hover:bg-muted/30 rounded-sm"
393
+ iconSize="small"
394
+ showAddCustomModelDocs={true}
395
+ forRole="chat"
396
+ />
397
+ </div>
398
+ <div className="flex flex-row">
399
+ <Tooltip content="Add context">
400
+ <Button variant="text" size="icon" onClick={onAddContext}>
401
+ <AtSignIcon className="h-3.5 w-3.5" />
402
+ </Button>
403
+ </Tooltip>
404
+ {isAttachmentSupported && (
405
+ <>
406
+ <Tooltip content="Attach a file">
407
+ <Button
408
+ variant="text"
409
+ size="icon"
410
+ className="cursor-pointer"
411
+ onClick={() => fileInputRef.current?.click()}
412
+ title="Attach a file"
413
+ >
414
+ <PaperclipIcon className="h-3.5 w-3.5" />
415
+ </Button>
416
+ </Tooltip>
417
+ <Input
418
+ ref={fileInputRef}
419
+ type="file"
420
+ multiple={true}
421
+ hidden={true}
422
+ onChange={(event) => {
423
+ if (event.target.files) {
424
+ onAddFiles([...event.target.files]);
425
+ }
426
+ }}
427
+ accept={SUPPORTED_ATTACHMENT_TYPES.join(",")}
428
+ />
429
+ </>
430
+ )}
431
+
432
+ <Tooltip content="Submit">
433
+ <Button
434
+ variant="text"
435
+ size="sm"
436
+ className="h-6 w-6 p-0 hover:bg-muted/30 cursor-pointer"
437
+ onClick={isLoading ? onStop : onSendClick}
438
+ disabled={isLoading ? false : isEmpty}
439
+ >
440
+ {isLoading ? (
441
+ <SquareIcon className="h-3 w-3 fill-current" />
442
+ ) : (
443
+ <SendIcon className="h-3 w-3" />
444
+ )}
445
+ </Button>
446
+ </Tooltip>
447
+ </div>
309
448
  </div>
310
- <Button
311
- variant="ghost"
312
- size="sm"
313
- className="h-6 w-6 p-0 hover:bg-muted/30"
314
- onClick={isLoading ? onStop : onSendClick}
315
- disabled={isLoading ? false : isEmpty}
316
- >
317
- {isLoading ? (
318
- <SquareIcon className="h-3 w-3 fill-current" />
319
- ) : (
320
- <SendIcon className="h-3 w-3" />
321
- )}
322
- </Button>
323
- </div>
449
+ </TooltipProvider>
324
450
  );
325
451
  },
326
452
  );
@@ -328,16 +454,33 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
328
454
  ChatInputFooter.displayName = "ChatInputFooter";
329
455
 
330
456
  interface ChatInputProps {
457
+ placeholder?: string;
331
458
  input: string;
459
+ inputClassName?: string;
332
460
  setInput: (value: string) => void;
333
461
  onSubmit: (e: KeyboardEvent | undefined, value: string) => void;
334
462
  inputRef: React.RefObject<ReactCodeMirrorRef | null>;
335
463
  isLoading: boolean;
336
464
  onStop: () => void;
465
+ onClose: () => void;
466
+ fileInputRef: React.RefObject<HTMLInputElement | null>;
467
+ onAddFiles: (files: File[]) => void;
337
468
  }
338
469
 
339
470
  const ChatInput: React.FC<ChatInputProps> = memo(
340
- ({ input, setInput, onSubmit, inputRef, isLoading, onStop }) => {
471
+ ({
472
+ placeholder,
473
+ input,
474
+ inputClassName,
475
+ setInput,
476
+ onSubmit,
477
+ inputRef,
478
+ isLoading,
479
+ onStop,
480
+ fileInputRef,
481
+ onAddFiles,
482
+ onClose,
483
+ }) => {
341
484
  const handleSendClick = useEvent(() => {
342
485
  if (input.trim()) {
343
486
  onSubmit(undefined, input);
@@ -345,22 +488,26 @@ const ChatInput: React.FC<ChatInputProps> = memo(
345
488
  });
346
489
 
347
490
  return (
348
- <div className="border-t relative shrink-0 min-h-[80px] flex flex-col">
349
- <div className="px-2 py-3 flex-1">
491
+ <div className="relative shrink-0 min-h-[80px] flex flex-col border-t">
492
+ <div className={cn("px-2 py-3 flex-1", inputClassName)}>
350
493
  <PromptInput
351
494
  inputRef={inputRef}
352
495
  value={input}
353
496
  onChange={setInput}
354
497
  onSubmit={onSubmit}
355
- onClose={() => inputRef.current?.editor?.blur()}
356
- placeholder="Type your message..."
498
+ onClose={onClose}
499
+ onAddFiles={onAddFiles}
500
+ placeholder={placeholder || "Type your message..."}
357
501
  />
358
502
  </div>
359
503
  <ChatInputFooter
360
504
  isEmpty={!input.trim()}
505
+ onAddContext={() => addContextCompletion(inputRef)}
361
506
  onSendClick={handleSendClick}
362
507
  isLoading={isLoading}
363
508
  onStop={onStop}
509
+ fileInputRef={fileInputRef}
510
+ onAddFiles={onAddFiles}
364
511
  />
365
512
  </div>
366
513
  );
@@ -369,7 +516,7 @@ const ChatInput: React.FC<ChatInputProps> = memo(
369
516
 
370
517
  ChatInput.displayName = "ChatInput";
371
518
 
372
- export const ChatPanel = () => {
519
+ const ChatPanel = () => {
373
520
  const aiEnabled = useAtomValue(aiEnabledAtom);
374
521
  const { handleClick } = useOpenSettingsToTab();
375
522
 
@@ -392,130 +539,172 @@ export const ChatPanel = () => {
392
539
  };
393
540
 
394
541
  const ChatPanelBody = () => {
395
- const [chatState, setChatState] = useAtom(chatStateAtom);
542
+ const setChatState = useSetAtom(chatStateAtom);
396
543
  const [activeChat, setActiveChat] = useAtom(activeChatAtom);
544
+ const [input, setInput] = useState("");
397
545
  const [newThreadInput, setNewThreadInput] = useState("");
546
+ const [files, setFiles] = useState<File[]>();
398
547
  const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
399
548
  const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
400
549
  const scrollContainerRef = useRef<HTMLDivElement>(null);
550
+ const fileInputRef = useRef<HTMLInputElement>(null);
401
551
  const messagesEndRef = useRef<HTMLDivElement>(null);
402
552
  const runtimeManager = useRuntimeManager();
403
553
  const { invokeAiTool } = useRequestClient();
404
554
 
555
+ const activeChatId = activeChat?.id;
556
+
405
557
  const {
406
558
  messages,
407
- input,
408
- setInput,
409
- setMessages,
410
- append,
411
- handleSubmit,
559
+ sendMessage,
412
560
  error,
413
561
  status,
414
- reload,
562
+ regenerate,
415
563
  stop,
564
+ addToolResult,
565
+ id: chatId,
416
566
  } = useChat({
417
- id: activeChat?.id,
418
- maxSteps: 10,
419
- initialMessages: activeChat?.messages || [],
420
- keepLastMessageOnError: true,
421
- // Throttle the messages and data updates to 100ms
422
- // experimental_throttle: 100,
423
- api: runtimeManager.getAiURL("chat").toString(),
424
- headers: runtimeManager.headers(),
425
- experimental_prepareRequestBody: (options) => {
426
- return {
427
- ...options,
428
- ...getAICompletionBody({
429
- input: options.messages.map((m) => m.content).join("\n"),
430
- }),
431
- includeOtherCode: getCodes(""),
432
- };
567
+ id: activeChatId,
568
+ // Only automatically submit if we have tool calls but no text response yet
569
+ sendAutomaticallyWhen: ({ messages }) => {
570
+ if (messages.length === 0) {
571
+ return false;
572
+ }
573
+
574
+ const lastMessage = messages[messages.length - 1];
575
+ const parts = lastMessage.parts;
576
+
577
+ if (parts.length === 0) {
578
+ return false;
579
+ }
580
+
581
+ // Only auto-send if the last message is an assistant message
582
+ // Because assistant messages are the ones that can have tool calls
583
+ if (lastMessage.role !== "assistant") {
584
+ return false;
585
+ }
586
+
587
+ const toolParts = parts.filter((part) =>
588
+ part.type.startsWith("tool-"),
589
+ ) as ToolUIPart[];
590
+
591
+ const hasCompletedToolCalls = toolParts.some(
592
+ (part) => part.state === "output-available",
593
+ );
594
+
595
+ // Check if the last part has any text content
596
+ const lastPart = parts[parts.length - 1];
597
+ const hasTextContent =
598
+ lastPart.type === "text" && lastPart.text?.trim().length > 0;
599
+
600
+ // Only auto-send if we have completed tool calls and there is no reply yet
601
+ return hasCompletedToolCalls && !hasTextContent;
433
602
  },
434
- onFinish: (message) => {
603
+ messages: activeChat?.messages || [], // initial messages
604
+ transport: new DefaultChatTransport({
605
+ api: runtimeManager.getAiURL("chat").toString(),
606
+ headers: runtimeManager.headers(),
607
+ prepareSendMessagesRequest: async (options) => {
608
+ // Map from parts to a single string
609
+ function toContent(parts: UIMessage["parts"]): string {
610
+ return parts
611
+ .map((part) => (part.type === "text" ? part.text : ""))
612
+ .join("\n");
613
+ }
614
+
615
+ const input = toContent(options.messages.flatMap((m) => m.parts));
616
+ const completionBody = await getAICompletionBodyWithAttachments({
617
+ input,
618
+ });
619
+
620
+ // Map from UIMessage to our ChatMessage type
621
+ // If it's the last message, add the attachments from the completion body
622
+ function mapMessage(m: UIMessage, isLastMessage: boolean): ChatMessage {
623
+ const parts = m.parts;
624
+ if (isLastMessage) {
625
+ parts.push(...completionBody.attachments);
626
+ }
627
+ return {
628
+ role: m.role,
629
+ content: toContent(m.parts),
630
+ parts: parts,
631
+ };
632
+ }
633
+
634
+ return {
635
+ body: {
636
+ ...options,
637
+ ...completionBody.body,
638
+ messages: options.messages.map((m, idx) => ({
639
+ ...m,
640
+ ...mapMessage(m, idx === options.messages.length - 1),
641
+ })),
642
+ },
643
+ };
644
+ },
645
+ }),
646
+ onFinish: ({ messages }) => {
435
647
  setChatState((prev) => {
436
- return addMessageToChat(
437
- prev,
438
- prev.activeChatId,
439
- message.id,
440
- "assistant",
441
- message.content,
442
- message.parts,
443
- );
648
+ return replaceMessagesInChat({
649
+ chatState: prev,
650
+ chatId: prev.activeChatId,
651
+ messages: messages,
652
+ });
444
653
  });
445
654
  },
446
655
  onToolCall: async ({ toolCall }) => {
447
656
  try {
448
657
  const response = await invokeAiTool({
449
658
  toolName: toolCall.toolName,
450
- arguments: toolCall.args as Record<string, never>,
659
+ arguments: toolCall.input as Record<string, never>,
660
+ });
661
+ addToolResult({
662
+ tool: toolCall.toolName,
663
+ toolCallId: toolCall.toolCallId,
664
+ output: response.result || response.error,
451
665
  });
452
-
453
- // This response triggers the onFinish callback
454
- return response.result || response.error;
455
666
  } catch (error) {
456
667
  Logger.error("Tool call failed:", error);
457
- return `Error: ${error instanceof Error ? error.message : String(error)}`;
668
+ addToolResult({
669
+ tool: toolCall.toolName,
670
+ toolCallId: toolCall.toolCallId,
671
+ output: `Error: ${error instanceof Error ? error.message : String(error)}`,
672
+ });
458
673
  }
459
674
  },
460
675
  onError: (error) => {
461
676
  Logger.error("An error occurred:", error);
462
677
  },
463
- onResponse: (response) => {
464
- Logger.debug("Received HTTP response from server:", response);
465
- },
466
678
  });
467
679
 
468
- const isLoading = status === "submitted" || status === "streaming";
469
-
470
- // Sync user messages from useChat to storage when they become available
471
- // Only when we are done loading, for performance.
472
- useEffect(() => {
473
- if (!chatState.activeChatId || messages.length === 0 || isLoading) {
680
+ const onAddFiles = useEvent((files: File[]) => {
681
+ if (files.length === 0) {
474
682
  return;
475
683
  }
476
684
 
477
- // Only sync if the last message is from a user
478
- const lastMessage = messages[messages.length - 1];
479
- if (lastMessage?.role !== "user") {
480
- return;
685
+ let fileSize = 0;
686
+ for (const file of files) {
687
+ fileSize += file.size;
481
688
  }
482
689
 
483
- const currentChat = chatState.chats.get(chatState.activeChatId);
484
- if (!currentChat) {
690
+ if (fileSize > MAX_ATTACHMENT_SIZE) {
691
+ toast({
692
+ title: "File size exceeds 50MB limit",
693
+ description: "Please remove some files and try again.",
694
+ });
485
695
  return;
486
696
  }
487
697
 
488
- const storedMessageIds = new Set(currentChat.messages.map((m) => m.id));
489
-
490
- // Find user messages from useChat that aren't in storage yet
491
- const missingUserMessages = messages.filter(
492
- (m) => m.role === "user" && !storedMessageIds.has(m.id),
493
- );
494
-
495
- if (missingUserMessages.length > 0) {
496
- setChatState((prev) => {
497
- let result = prev;
498
-
499
- for (const userMessage of missingUserMessages) {
500
- result = addMessageToChat(
501
- result,
502
- prev.activeChatId,
503
- userMessage.id,
504
- "user",
505
- userMessage.content,
506
- );
507
- }
698
+ setFiles((prev) => [...(prev ?? []), ...files]);
699
+ });
508
700
 
509
- return result;
510
- });
701
+ const removeFile = useEvent((file: File) => {
702
+ if (files) {
703
+ setFiles(files.filter((f) => f !== file));
511
704
  }
512
- }, [
513
- messages,
514
- chatState.activeChatId,
515
- chatState.chats,
516
- setChatState,
517
- isLoading,
518
- ]);
705
+ });
706
+
707
+ const isLoading = status === "submitted" || status === "streaming";
519
708
 
520
709
  // Check if we're currently streaming reasoning in the latest message
521
710
  const isStreamingReasoning =
@@ -531,19 +720,19 @@ const ChatPanelBody = () => {
531
720
  };
532
721
 
533
722
  requestAnimationFrame(scrollToBottom);
534
- }, [chatState.activeChatId]);
723
+ }, [activeChatId]);
535
724
 
536
- const createNewThread = (initialMessage: string) => {
537
- const CURRENT_TIME = Date.now();
725
+ const createNewThread = async (
726
+ initialMessage: string,
727
+ initialAttachments?: File[],
728
+ ) => {
729
+ const now = Date.now();
538
730
  const newChat: Chat = {
539
- id: generateUUID() as ChatId,
540
- title:
541
- initialMessage.length > 50
542
- ? `${initialMessage.slice(0, 50)}...`
543
- : initialMessage,
544
- messages: [], // Don't pre-populate - let useChat handle it and sync back
545
- createdAt: CURRENT_TIME,
546
- updatedAt: CURRENT_TIME,
731
+ id: chatId as ChatId,
732
+ title: generateChatTitle(initialMessage),
733
+ messages: [],
734
+ createdAt: now,
735
+ updatedAt: now,
547
736
  };
548
737
 
549
738
  // Create new chat and set as active
@@ -558,12 +747,23 @@ const ChatPanelBody = () => {
558
747
  return newState;
559
748
  });
560
749
 
750
+ const fileParts =
751
+ initialAttachments && initialAttachments.length > 0
752
+ ? await convertToFileUIPart(initialAttachments)
753
+ : undefined;
754
+
561
755
  // Trigger AI conversation with append
562
- append({
563
- id: generateUUID(),
756
+ sendMessage({
564
757
  role: "user",
565
- content: initialMessage,
758
+ parts: [
759
+ {
760
+ type: "text" as const,
761
+ text: initialMessage,
762
+ },
763
+ ...(fileParts ?? []),
764
+ ],
566
765
  });
766
+ setFiles(undefined);
567
767
  setInput("");
568
768
  };
569
769
 
@@ -571,51 +771,43 @@ const ChatPanelBody = () => {
571
771
  setActiveChat(null);
572
772
  setInput("");
573
773
  setNewThreadInput("");
774
+ setFiles(undefined);
574
775
  });
575
776
 
576
777
  const handleMessageEdit = useEvent((index: number, newValue: string) => {
577
- // Truncate both useChat and storage
578
- setMessages((messages) => messages.slice(0, index));
579
- const activeChatId = chatState.activeChatId;
580
- if (activeChatId) {
581
- setChatState((prev) => {
582
- const nextChats = new Map(prev.chats);
583
- const activeChat = chatState.chats.get(activeChatId);
584
- if (activeChat) {
585
- nextChats.set(activeChat.id, {
586
- ...activeChat,
587
- messages: activeChat.messages.slice(0, index),
588
- updatedAt: Date.now(),
589
- });
590
- }
778
+ const editedMessage = messages[index];
779
+ const fileParts = editedMessage.parts?.filter((p) => p.type === "file");
591
780
 
592
- return {
593
- ...prev,
594
- chats: nextChats,
595
- };
596
- });
597
- }
598
-
599
- append({
781
+ const messageId = editedMessage.id;
782
+ sendMessage({
783
+ messageId: messageId, // replace the message
600
784
  role: "user",
601
- content: newValue,
785
+ parts: [{ type: "text", text: newValue }, ...fileParts],
602
786
  });
603
787
  });
604
788
 
605
789
  const handleChatInputSubmit = useEvent(
606
- (e: KeyboardEvent | undefined, newValue: string): void => {
790
+ async (e: KeyboardEvent | undefined, newValue: string): Promise<void> => {
607
791
  if (!newValue.trim()) {
608
792
  return;
609
793
  }
610
794
  if (newMessageInputRef.current?.view) {
611
795
  storePrompt(newMessageInputRef.current.view);
612
796
  }
613
- handleSubmit(e);
797
+ const fileParts = files ? await convertToFileUIPart(files) : undefined;
798
+
799
+ e?.preventDefault();
800
+ sendMessage({
801
+ text: newValue,
802
+ files: fileParts,
803
+ });
804
+ setInput("");
805
+ setFiles(undefined);
614
806
  },
615
807
  );
616
808
 
617
809
  const handleReload = () => {
618
- reload();
810
+ regenerate();
619
811
  };
620
812
 
621
813
  const handleNewThreadSubmit = useEvent(() => {
@@ -625,16 +817,57 @@ const ChatPanelBody = () => {
625
817
  if (newThreadInputRef.current?.view) {
626
818
  storePrompt(newThreadInputRef.current.view);
627
819
  }
628
- createNewThread(newThreadInput.trim());
820
+ createNewThread(newThreadInput.trim(), files);
629
821
  });
630
822
 
631
823
  const handleOnCloseThread = () => newThreadInputRef.current?.editor?.blur();
632
824
 
633
- const sortedChats = useMemo(() => {
634
- return [...chatState.chats.values()].sort(
635
- (a, b) => b.updatedAt - a.updatedAt,
636
- );
637
- }, [chatState.chats]);
825
+ const isNewThread = messages.length === 0;
826
+ const chatInput = isNewThread ? (
827
+ <ChatInput
828
+ key="new-thread-input"
829
+ placeholder={`Ask anything, ${CONTEXT_TRIGGER} to include context about tables or dataframes`}
830
+ input={newThreadInput}
831
+ inputRef={newThreadInputRef}
832
+ inputClassName="px-1 py-0"
833
+ setInput={setNewThreadInput}
834
+ onSubmit={handleNewThreadSubmit}
835
+ isLoading={isLoading}
836
+ onStop={stop}
837
+ fileInputRef={fileInputRef}
838
+ onAddFiles={onAddFiles}
839
+ onClose={handleOnCloseThread}
840
+ />
841
+ ) : (
842
+ <ChatInput
843
+ input={input}
844
+ setInput={setInput}
845
+ onSubmit={handleChatInputSubmit}
846
+ inputRef={newMessageInputRef}
847
+ isLoading={isLoading}
848
+ onStop={stop}
849
+ onClose={() => newMessageInputRef.current?.editor?.blur()}
850
+ fileInputRef={fileInputRef}
851
+ onAddFiles={onAddFiles}
852
+ />
853
+ );
854
+
855
+ const filesPills = files && files.length > 0 && (
856
+ <div
857
+ className={cn(
858
+ "flex flex-row gap-1 flex-wrap p-1",
859
+ isNewThread && "py-2 px-1",
860
+ )}
861
+ >
862
+ {files?.map((file) => (
863
+ <FileAttachmentPill
864
+ file={file}
865
+ key={file.name}
866
+ onRemove={() => removeFile(file)}
867
+ />
868
+ ))}
869
+ </div>
870
+ );
638
871
 
639
872
  return (
640
873
  <div className="flex flex-col h-[calc(100%-53px)]">
@@ -643,7 +876,6 @@ const ChatPanelBody = () => {
643
876
  onNewChat={handleNewChat}
644
877
  activeChatId={activeChat?.id}
645
878
  setActiveChat={setActiveChat}
646
- chats={sortedChats}
647
879
  />
648
880
  </TooltipProvider>
649
881
 
@@ -651,36 +883,19 @@ const ChatPanelBody = () => {
651
883
  className="flex-1 px-3 bg-(--slate-1) gap-4 py-3 flex flex-col overflow-y-auto"
652
884
  ref={scrollContainerRef}
653
885
  >
654
- {(!messages || messages.length === 0) && (
655
- <div className="flex flex-col rounded-md border bg-background">
656
- <div className="px-1">
657
- <PromptInput
658
- inputRef={newThreadInputRef}
659
- key="new-thread-input"
660
- value={newThreadInput}
661
- placeholder="Ask anything, @ to include context about tables or dataframes"
662
- onClose={handleOnCloseThread}
663
- onChange={setNewThreadInput}
664
- onSubmit={handleNewThreadSubmit}
665
- />
666
- </div>
667
- <ChatInputFooter
668
- isEmpty={!newThreadInput.trim()}
669
- onSendClick={handleNewThreadSubmit}
670
- isLoading={isLoading}
671
- onStop={stop}
672
- />
886
+ {isNewThread && (
887
+ <div className="rounded-md border bg-background">
888
+ {filesPills}
889
+ {chatInput}
673
890
  </div>
674
891
  )}
675
892
 
676
893
  {messages.map((message, idx) => (
677
- <ChatMessage
894
+ <ChatMessageDisplay
678
895
  key={message.id}
679
896
  message={message}
680
897
  index={idx}
681
898
  onEdit={handleMessageEdit}
682
- setChatState={setChatState}
683
- chatState={chatState}
684
899
  isStreamingReasoning={isStreamingReasoning}
685
900
  isLast={idx === messages.length - 1}
686
901
  />
@@ -712,40 +927,15 @@ const ChatPanelBody = () => {
712
927
  </div>
713
928
  )}
714
929
 
715
- {messages && messages.length > 0 && (
716
- <ChatInput
717
- input={input}
718
- setInput={setInput}
719
- onSubmit={handleChatInputSubmit}
720
- inputRef={newMessageInputRef}
721
- isLoading={isLoading}
722
- onStop={stop}
723
- />
930
+ {/* For existing threads, we place the chat input at the bottom */}
931
+ {!isNewThread && (
932
+ <>
933
+ {filesPills}
934
+ {chatInput}
935
+ </>
724
936
  )}
725
937
  </div>
726
938
  );
727
939
  };
728
940
 
729
- function isLastMessageReasoning(messages: Message[]): boolean {
730
- if (messages.length === 0) {
731
- return false;
732
- }
733
-
734
- const lastMessage = messages.at(-1);
735
- if (!lastMessage) {
736
- return false;
737
- }
738
-
739
- if (lastMessage.role !== "assistant" || !lastMessage.parts) {
740
- return false;
741
- }
742
-
743
- const parts = lastMessage.parts;
744
- if (parts.length === 0) {
745
- return false;
746
- }
747
-
748
- // Check if the last part is reasoning
749
- const lastPart = parts[parts.length - 1];
750
- return lastPart.type === "reasoning";
751
- }
941
+ export default ChatPanel;