@marimo-team/islands 0.15.2 → 0.15.3

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 (189) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C39nQwtD.js → ConnectedDataExplorerComponent-DfvW3rBn.js} +323 -328
  2. package/dist/{ImageComparisonComponent-BhkiyswP.js → ImageComparisonComponent-XaJshw7d.js} +13 -13
  3. package/dist/{_baseUniq-DdHL34FO.js → _baseUniq-dN9WKF9m.js} +67 -67
  4. package/dist/any-language-editor-CpFniVi-.js +27 -0
  5. package/dist/{arc-BXrety1g.js → arc-BOhn-m2C.js} +1 -1
  6. package/dist/{architectureDiagram-KFL7JDKH-BMy6ywCF.js → architectureDiagram-W76B3OCA-Bpg85ZKv.js} +144 -144
  7. package/dist/assets/{worker-COGufAQn.js → worker-Y-Q4G-N2.js} +30 -26
  8. package/dist/asterisk-DS281yxp.js +271 -0
  9. package/dist/{blockDiagram-ZYB65J3Q-DYT2-nlI.js → blockDiagram-QIGZ2CNN-DS1kOHlW.js} +10 -10
  10. package/dist/{c4Diagram-AAMF2YG6-ZiQzioe6.js → c4Diagram-FPNF74CW-CyRVKssw.js} +8 -8
  11. package/dist/{channel-CeuXqUAU.js → channel-BilGXox7.js} +1 -1
  12. package/dist/{chunk-ANTBXLJU-BvYnIrdq.js → chunk-4BX2VUAB-CZR39zCO.js} +1 -1
  13. package/dist/{chunk-WVR4S24B-DXj8yaUk.js → chunk-55IACEB6-BIH-MYov.js} +1 -1
  14. package/dist/{chunk-GLLZNHP4-CyFsosAe.js → chunk-FMBD7UC4-4PZXFZE8.js} +1 -1
  15. package/dist/{chunk-JBRWN2VN-DA_EEhy2.js → chunk-K7UQS3LO-CEvWKznk.js} +117 -117
  16. package/dist/{chunk-NRVI72HA-BYx2jMlI.js → chunk-QN33PNHL-D5LO5Jq_.js} +1 -1
  17. package/dist/{chunk-FHKO5MBM-DfCztBk8.js → chunk-QZHKN3VN-6gwUonWI.js} +1 -1
  18. package/dist/{chunk-LXBSTHXV-Se7vdY6J.js → chunk-TVAH2DTR-3gm06QdU.js} +7 -7
  19. package/dist/{chunk-OMD6QJNC-CqgcPMgL.js → chunk-TZMSLE5B-Cm8Iy9bO.js} +1 -1
  20. package/dist/{classDiagram-v2-QTMF73CY-B19A3G1l.js → classDiagram-KNZD7YFC-DC529O_z.js} +2 -2
  21. package/dist/{classDiagram-3BZAVTQC-B19A3G1l.js → classDiagram-v2-RKCZMP56-DC529O_z.js} +2 -2
  22. package/dist/{clone-78au0tn1.js → clone-CLoRX3j6.js} +1 -1
  23. package/dist/cose-bilkent-S5V4N54A-qf5DlS6Y.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-Ceocls0m.js} +6 -6
  26. package/dist/{data-grid-overlay-editor-CH_qLkV2.js → data-grid-overlay-editor-AqDS_UKe.js} +11 -11
  27. package/dist/{diagram-4IRLE6MV-CL8xidnG.js → diagram-N5W7TBWH-CP66oSiv.js} +59 -60
  28. package/dist/{diagram-RP2FKANI-B1BPcUew.js → diagram-QEK2KX5R-_YD4kxxi.js} +15 -15
  29. package/dist/{diagram-GUPCWM2R-CZ5cfqlq.js → diagram-S2PKOQOG-Cnj8T-OP.js} +10 -10
  30. package/dist/dockerfile-Cm8cRYCN.js +194 -0
  31. package/dist/ebnf-DUPDuY4r.js +78 -0
  32. package/dist/{erDiagram-HZWUO2LU-BEAIww50.js → erDiagram-AWTI2OKA-CGnvoHx6.js} +8 -8
  33. package/dist/fcl-CPC2WYrI.js +103 -0
  34. package/dist/{flowDiagram-THRYKUMA-Czs2UAI2.js → flowDiagram-PVAE7QVJ-DG-pr9R9.js} +9 -9
  35. package/dist/{ganttDiagram-WV7ZQ7D5-ByYIAVFO.js → ganttDiagram-OWAHRB6G-JmChtxvn.js} +34 -34
  36. package/dist/{gitGraphDiagram-OJR772UL-BcpDsiyB.js → gitGraphDiagram-NY62KEGX-D8wLfOPd.js} +4 -4
  37. package/dist/{glide-data-editor-CmN6FVyi.js → glide-data-editor-9nC3iCIZ.js} +33 -33
  38. package/dist/{graph-77W6heli.js → graph-CoRe7vAN.js} +3 -3
  39. package/dist/http-D9LttvKF.js +44 -0
  40. package/dist/{index-Bfk9dnyS.js → index-6qYeHHjQ.js} +33090 -32892
  41. package/dist/{index-BOojn38D.js → index-BpzLh4Qe.js} +7711 -7711
  42. package/dist/{index-CmozKMxx.js → index-BthgsgYX.js} +6 -6
  43. package/dist/{index-pBmAzQJl.js → index-MCx5v1x0.js} +2 -2
  44. package/dist/index-jkm77Jrz.js +98 -0
  45. package/dist/{infoDiagram-6WOFNB3A-CfzLHHVP.js → infoDiagram-STP46IZ2-BlXxvOrR.js} +2 -2
  46. package/dist/{journeyDiagram-FFXJYRFH-ndAcpkGn.js → journeyDiagram-BIP6EPQ6-CNRYs_Fc.js} +24 -26
  47. package/dist/{kanban-definition-KOZQBZVT-DcQYzNvc.js → kanban-definition-6OIFK2YF-B9HeMAuP.js} +14 -14
  48. package/dist/{layout-XySVHJgD.js → layout-m2vOUiW1.js} +81 -81
  49. package/dist/{linear-PbooOqg7.js → linear-DU6Q5CX3.js} +35 -35
  50. package/dist/{main-B5yML0bw.js → main-BD2KGFpU.js} +74594 -68034
  51. package/dist/main.js +1 -1
  52. package/dist/{mermaid-Cg5IX6Nv.js → mermaid-HVCtvbyx.js} +6160 -7493
  53. package/dist/min-DcGMA4e_.js +80 -0
  54. package/dist/mindmap-definition-Q6HEUPPD-BW8UmIDQ.js +785 -0
  55. package/dist/nginx-zDPm3Z74.js +89 -0
  56. package/dist/{number-overlay-editor-DUhfZqtP.js → number-overlay-editor-D8Hl0Syo.js} +19 -19
  57. package/dist/{pieDiagram-DBDJKBY4-DTOlNsja.js → pieDiagram-ADFJNKIX-Bg-3zg5U.js} +17 -17
  58. package/dist/{quadrantDiagram-YPSRARAO-BX2d8VS-.js → quadrantDiagram-LMRXKWRM-BO4IG6Yz.js} +6 -6
  59. package/dist/{react-plotly-Dcyw-3Sa.js → react-plotly-dkvHVuRb.js} +3577 -3577
  60. package/dist/{requirementDiagram-EGVEC5DT-D1T5u-wG.js → requirementDiagram-4UW4RH46-5sdTguSM.js} +7 -7
  61. package/dist/{sankeyDiagram-HRAUVNP4-G6xDfnp-.js → sankeyDiagram-GR3RE2ED-Buhlv9OI.js} +5 -5
  62. package/dist/sequenceDiagram-C3RYC4MD-C3qsM2UP.js +2519 -0
  63. package/dist/{slides-component-BJLlPJSr.js → slides-component-D209A0-s.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-CopJ7G6P.js} +5 -5
  67. package/dist/{stateDiagram-v2-EYPG3UTE-Br1HYKT6.js → stateDiagram-v2-UMBNRL4Z-CejL8AKf.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-BwSBitlN.js} +58 -58
  72. package/dist/{timeline-definition-3HZDQTIS-DeK_ZRD0.js → timeline-definition-XQNQX7LJ-DzMNTX-C.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-zeJG07dk.js} +14 -14
  75. package/dist/{vega-component-CpgdqX2d.js → vega-component-CUkiTayd.js} +30 -30
  76. package/dist/{xychartDiagram-FDP5SA34-AMEPsx_R.js → xychartDiagram-6GGTOJPD-DiENNXMS.js} +7 -7
  77. package/package.json +39 -39
  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/app-config/ai-config.tsx +7 -0
  84. package/src/components/chat/chat-components.tsx +71 -0
  85. package/src/components/chat/chat-panel.tsx +481 -291
  86. package/src/components/chat/chat-utils.ts +50 -0
  87. package/src/components/chat/markdown-renderer.tsx +3 -7
  88. package/src/components/chat/tool-call-accordion.tsx +5 -5
  89. package/src/components/datasources/__tests__/utils.test.ts +6 -0
  90. package/src/components/datasources/column-preview.tsx +1 -3
  91. package/src/components/editor/actions/useNotebookActions.tsx +1 -1
  92. package/src/components/editor/ai/add-cell-with-ai.tsx +20 -15
  93. package/src/components/editor/ai/ai-completion-editor.tsx +22 -3
  94. package/src/components/editor/ai/completion-handlers.tsx +2 -4
  95. package/src/components/editor/ai/completion-utils.ts +85 -11
  96. package/src/components/editor/alerts/startup-logs-alert.tsx +72 -0
  97. package/src/components/editor/chrome/panels/datasources-panel.tsx +3 -1
  98. package/src/components/editor/chrome/panels/dependency-graph-panel.tsx +3 -1
  99. package/src/components/editor/chrome/panels/documentation-panel.tsx +3 -1
  100. package/src/components/editor/chrome/panels/error-panel.tsx +3 -1
  101. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +3 -1
  102. package/src/components/editor/chrome/panels/logs-panel.tsx +3 -1
  103. package/src/components/editor/chrome/panels/outline-panel.tsx +3 -1
  104. package/src/components/editor/chrome/panels/packages-panel.tsx +4 -2
  105. package/src/components/editor/chrome/panels/scratchpad-panel.tsx +3 -1
  106. package/src/components/editor/chrome/panels/secrets-panel.tsx +3 -1
  107. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -1
  108. package/src/components/editor/chrome/panels/tracing-panel.tsx +3 -1
  109. package/src/components/editor/chrome/panels/variable-panel.tsx +3 -1
  110. package/src/components/editor/chrome/wrapper/app-chrome.tsx +38 -28
  111. package/src/components/editor/controls/command-palette-button.tsx +1 -1
  112. package/src/components/editor/controls/command-palette.tsx +5 -4
  113. package/src/components/editor/controls/state.ts +4 -0
  114. package/src/components/editor/package-alert.tsx +108 -58
  115. package/src/components/editor/renderers/CellArray.tsx +2 -0
  116. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +0 -1
  117. package/src/components/pages/edit-page.tsx +7 -3
  118. package/src/core/ai/chat-utils.ts +26 -43
  119. package/src/core/ai/config.ts +1 -1
  120. package/src/core/ai/context/__tests__/registry.test.ts +277 -3
  121. package/src/core/ai/context/context.ts +11 -1
  122. package/src/core/ai/context/providers/__tests__/cell-output.test.ts +378 -0
  123. package/src/core/ai/context/providers/__tests__/error.test.ts +3 -2
  124. package/src/core/ai/context/providers/__tests__/file.test.ts +119 -0
  125. package/src/core/ai/context/providers/cell-output.ts +349 -0
  126. package/src/core/ai/context/providers/common.ts +5 -1
  127. package/src/core/ai/context/providers/file.ts +287 -0
  128. package/src/core/ai/context/registry.ts +79 -0
  129. package/src/core/ai/state.ts +22 -5
  130. package/src/core/alerts/state.ts +71 -3
  131. package/src/core/cells/cell.ts +2 -2
  132. package/src/core/cells/cells.ts +1 -1
  133. package/src/core/cells/logs.ts +1 -1
  134. package/src/core/cells/runs.ts +6 -5
  135. package/src/core/codemirror/ai/resources.ts +47 -5
  136. package/src/core/codemirror/ai/state.ts +12 -0
  137. package/src/core/codemirror/language/__tests__/sql.test.ts +45 -0
  138. package/src/core/codemirror/markdown/__tests__/commands.test.ts +1 -0
  139. package/src/core/codemirror/theme/dark.ts +1 -1
  140. package/src/core/config/capabilities.ts +1 -1
  141. package/src/core/datasets/__tests__/data-source.test.ts +24 -0
  142. package/src/core/errors/__tests__/errors.test.ts +2 -0
  143. package/src/core/islands/bridge.ts +1 -0
  144. package/src/core/islands/main.ts +1 -0
  145. package/src/core/kernel/messages.ts +12 -6
  146. package/src/core/layout/layout.ts +3 -3
  147. package/src/core/network/requests-network.ts +8 -0
  148. package/src/core/network/requests-static.ts +1 -0
  149. package/src/core/network/requests-toasting.ts +1 -0
  150. package/src/core/network/types.ts +4 -1
  151. package/src/core/wasm/bridge.ts +18 -2
  152. package/src/core/wasm/worker/bootstrap.ts +3 -1
  153. package/src/core/wasm/worker/getMarimoWheel.ts +3 -8
  154. package/src/core/wasm/worker/types.ts +3 -0
  155. package/src/core/websocket/useMarimoWebSocket.tsx +7 -1
  156. package/src/css/app/Cell.css +42 -21
  157. package/src/css/app/codemirror.css +5 -1
  158. package/src/css/globals.css +3 -0
  159. package/src/css/md.css +1 -1
  160. package/src/plugins/impl/MicrophonePlugin.tsx +2 -2
  161. package/src/plugins/impl/chat/ChatPlugin.tsx +2 -9
  162. package/src/plugins/impl/chat/chat-ui.tsx +129 -110
  163. package/src/plugins/impl/chat/types.ts +5 -8
  164. package/src/plugins/impl/code/__tests__/language.test.ts +15 -0
  165. package/src/plugins/impl/code/any-language-editor.tsx +11 -8
  166. package/src/plugins/layout/MimeRenderPlugin.tsx +3 -6
  167. package/src/stories/cell.stories.tsx +6 -0
  168. package/src/stories/layout/vertical/one-column.stories.tsx +215 -0
  169. package/src/theme/useTheme.ts +11 -6
  170. package/src/utils/__tests__/blob.test.ts +37 -0
  171. package/src/utils/arrays.ts +13 -0
  172. package/src/utils/fileToBase64.ts +21 -6
  173. package/src/utils/json/base64.ts +5 -2
  174. package/src/utils/numbers.ts +9 -7
  175. package/dist/any-language-editor-DC5170DQ.js +0 -45
  176. package/dist/asn1-jKiBa2Ya.js +0 -95
  177. package/dist/clojure-CCKyeQKf.js +0 -800
  178. package/dist/css-BkF-NPzE.js +0 -1553
  179. package/dist/index-5ZH_qS8j.js +0 -288
  180. package/dist/index-U4yn89qO.js +0 -341
  181. package/dist/javascript-C2yteZeJ.js +0 -691
  182. package/dist/min-DS5Jz-hg.js +0 -80
  183. package/dist/mindmap-definition-LNHGMQRG-0aOVaMR8.js +0 -3234
  184. package/dist/mllike-BSnXJBGA.js +0 -272
  185. package/dist/pug-CwAQJzGR.js +0 -248
  186. package/dist/python-BkR3uSy8.js +0 -313
  187. package/dist/rpm-IznJm2Xc.js +0 -57
  188. package/dist/sequenceDiagram-WFGC7UMF-DMhHzllb.js +0 -2284
  189. package/dist/ttcn-cfg-Bac_acMi.js +0 -88
@@ -104,12 +104,14 @@ export const MockNotebook = {
104
104
  type: "exception",
105
105
  msg,
106
106
  exception_type,
107
+ raising_cell: null,
107
108
  }),
108
109
 
109
110
  strictException: (msg: string, ref: string): MarimoError => ({
110
111
  type: "strict-exception",
111
112
  msg,
112
113
  ref,
114
+ blamed_cell: null,
113
115
  }),
114
116
 
115
117
  interruption: (): MarimoError => ({
@@ -124,6 +126,7 @@ export const MockNotebook = {
124
126
  unknown: (msg: string): MarimoError => ({
125
127
  type: "unknown",
126
128
  msg,
129
+ error_type: null,
127
130
  }),
128
131
  },
129
132
 
@@ -44,6 +44,9 @@ export const MockRequestClient = {
44
44
  getUsageStats: vi.fn().mockResolvedValue({}),
45
45
  sendPdb: vi.fn().mockResolvedValue({}),
46
46
  sendListFiles: vi.fn().mockResolvedValue({ files: [] }),
47
+ sendSearchFiles: vi
48
+ .fn()
49
+ .mockResolvedValue({ files: [], query: "", total_found: 0 }),
47
50
  sendCreateFileOrFolder: vi.fn().mockResolvedValue({}),
48
51
  sendDeleteFileOrFolder: vi.fn().mockResolvedValue({}),
49
52
  sendRenameFileOrFolder: vi.fn().mockResolvedValue({}),
@@ -39,14 +39,14 @@ exports[`CellStatusComponent > renders disabled and stale state 1`] = `
39
39
  width="24"
40
40
  xmlns="http://www.w3.org/2000/svg"
41
41
  >
42
+ <path
43
+ d="M4.929 4.929 19.07 19.071"
44
+ />
42
45
  <circle
43
46
  cx="12"
44
47
  cy="12"
45
48
  r="10"
46
49
  />
47
- <path
48
- d="m4.9 4.9 14.2 14.2"
49
- />
50
50
  </svg>
51
51
  <div
52
52
  class="second-icon absolute bottom-[-2px] right-[-2px] rounded-full"
@@ -104,14 +104,14 @@ exports[`CellStatusComponent > renders disabled state 1`] = `
104
104
  width="24"
105
105
  xmlns="http://www.w3.org/2000/svg"
106
106
  >
107
+ <path
108
+ d="M4.929 4.929 19.07 19.071"
109
+ />
107
110
  <circle
108
111
  cx="12"
109
112
  cy="12"
110
113
  r="10"
111
114
  />
112
- <path
113
- d="m4.9 4.9 14.2 14.2"
114
- />
115
115
  </svg>
116
116
  </div>
117
117
  </div>
@@ -175,14 +175,14 @@ exports[`CellStatusComponent > renders disabled transitively state 1`] = `
175
175
  width="24"
176
176
  xmlns="http://www.w3.org/2000/svg"
177
177
  >
178
+ <path
179
+ d="M4.929 4.929 19.07 19.071"
180
+ />
178
181
  <circle
179
182
  cx="12"
180
183
  cy="12"
181
184
  r="10"
182
185
  />
183
- <path
184
- d="m4.9 4.9 14.2 14.2"
185
- />
186
186
  </svg>
187
187
  </div>
188
188
  </div>
@@ -403,14 +403,14 @@ exports[`CellStatusComponent > renders stale and disabled transitively state 1`]
403
403
  width="24"
404
404
  xmlns="http://www.w3.org/2000/svg"
405
405
  >
406
+ <path
407
+ d="M4.929 4.929 19.07 19.071"
408
+ />
406
409
  <circle
407
410
  cx="12"
408
411
  cy="12"
409
412
  r="10"
410
413
  />
411
- <path
412
- d="m4.9 4.9 14.2 14.2"
413
- />
414
414
  </svg>
415
415
  </div>
416
416
  </div>
@@ -1,22 +1,17 @@
1
1
  /* Copyright 2024 Marimo. All rights reserved. */
2
2
 
3
+ import type { UIMessage } from "ai";
3
4
  import { describe, expect, it } from "vitest";
4
5
  import { Maps } from "@/utils/maps";
5
- import { addMessageToChat } from "../core/ai/chat-utils";
6
+ import { replaceMessagesInChat } from "../core/ai/chat-utils";
6
7
  import type { Chat, ChatId, ChatState } from "../core/ai/state";
7
8
 
8
9
  const CHAT_1 = "chat-1" as ChatId;
9
- const CHAT_2 = "chat-2" as ChatId;
10
-
11
- function first(map: Map<ChatId, Chat>) {
12
- return [...map.values()][0];
13
- }
14
10
 
15
11
  function asMap(list: Iterable<Chat>) {
16
12
  return Maps.keyBy(list, (c) => c.id);
17
13
  }
18
-
19
- describe("addMessageToChat", () => {
14
+ describe("replaceMessagesInChat", () => {
20
15
  const mockChatState: ChatState = {
21
16
  chats: asMap([
22
17
  {
@@ -26,223 +21,43 @@ describe("addMessageToChat", () => {
26
21
  {
27
22
  id: "msg-1",
28
23
  role: "user",
29
- content: "Hello",
30
- timestamp: 1000,
31
- },
32
- {
33
- id: "msg-2",
34
- role: "assistant",
35
- content: "Hi there!",
36
- timestamp: 2000,
24
+ parts: [{ type: "text", text: "Hello" }],
25
+ metadata: { timestamp: 1000 },
37
26
  },
38
27
  ],
39
28
  createdAt: 1000,
40
29
  updatedAt: 2000,
41
30
  },
42
- {
43
- id: CHAT_2,
44
- title: "Test Chat 2",
45
- messages: [
46
- {
47
- id: "msg-3",
48
- role: "user",
49
- content: "How are you?",
50
- timestamp: 3000,
51
- },
52
- ],
53
- createdAt: 3000,
54
- updatedAt: 3000,
55
- },
56
31
  ]),
57
32
  activeChatId: CHAT_1,
58
33
  };
59
34
 
60
- it("should add a new message to an existing chat", () => {
61
- const result = addMessageToChat(
62
- mockChatState,
63
- CHAT_1,
64
- "msg-4",
65
- "user",
66
- "New message",
67
- );
68
-
69
- expect(result.chats).toHaveLength(2);
70
- const updatedChat = result.chats.get(CHAT_1);
71
- expect(updatedChat?.messages).toHaveLength(3);
72
- expect(updatedChat?.messages[2]).toEqual({
73
- id: "msg-4",
74
- role: "user",
75
- content: "New message",
76
- timestamp: expect.any(Number),
35
+ it("replaces messages in a chat", () => {
36
+ const newMessages: UIMessage[] = [
37
+ {
38
+ id: "msg-2",
39
+ role: "assistant",
40
+ parts: [{ type: "text", text: "Hi there!" }],
41
+ metadata: { timestamp: 2000 },
42
+ },
43
+ ];
44
+ const result = replaceMessagesInChat({
45
+ chatState: mockChatState,
46
+ chatId: CHAT_1,
47
+ messages: newMessages,
77
48
  });
78
- expect(updatedChat?.updatedAt).toBeGreaterThan(
79
- first(mockChatState.chats).updatedAt,
49
+ expect(result.chats.get(CHAT_1)?.messages).toEqual(newMessages);
50
+ expect(result.chats.get(CHAT_1)?.updatedAt).toBeGreaterThan(
51
+ mockChatState.chats.get(CHAT_1)?.updatedAt ?? 0,
80
52
  );
81
53
  });
82
54
 
83
- it("should update an existing message", () => {
84
- const result = addMessageToChat(
85
- mockChatState,
86
- CHAT_1,
87
- "msg-1",
88
- "user",
89
- "Updated content",
90
- );
91
-
92
- expect(result.chats).toHaveLength(2);
93
- const updatedChat = result.chats.get(CHAT_1);
94
- expect(updatedChat?.messages).toHaveLength(2);
95
- expect(updatedChat?.messages[0]).toEqual({
96
- id: "msg-1",
97
- role: "user",
98
- content: "Updated content",
99
- timestamp: 1000,
55
+ it("returns unchanged state if chatId is null", () => {
56
+ const result = replaceMessagesInChat({
57
+ chatState: mockChatState,
58
+ chatId: null,
59
+ messages: [],
100
60
  });
101
- expect(updatedChat?.updatedAt).toBeGreaterThan(
102
- first(mockChatState.chats).updatedAt,
103
- );
104
- });
105
-
106
- it("should handle message parts", () => {
107
- const parts = [{ type: "text" as const, text: "Part content" }];
108
- const result = addMessageToChat(
109
- mockChatState,
110
- CHAT_1,
111
- "msg-5",
112
- "assistant",
113
- "Message with parts",
114
- parts,
115
- );
116
-
117
- const updatedChat = result.chats.get(CHAT_1);
118
- expect(updatedChat?.messages[2].parts).toEqual(parts);
119
- });
120
-
121
- it("should update message parts", () => {
122
- const originalParts = [{ type: "text" as const, text: "Original" }];
123
- const updatedParts = [{ type: "text" as const, text: "Updated" }];
124
- const chats = [...mockChatState.chats.values()];
125
-
126
- const stateWithParts: ChatState = {
127
- ...mockChatState,
128
- chats: asMap([
129
- {
130
- ...chats[0],
131
- messages: [
132
- {
133
- ...chats[0].messages[0],
134
- parts: originalParts,
135
- },
136
- chats[0].messages[1],
137
- ],
138
- },
139
- chats[1],
140
- ]),
141
- };
142
-
143
- const result = addMessageToChat(
144
- stateWithParts,
145
- CHAT_1,
146
- "msg-1",
147
- "user",
148
- "Updated content",
149
- updatedParts,
150
- );
151
-
152
- const updatedChat = result.chats.get(CHAT_1);
153
- expect(updatedChat?.messages[0].parts).toEqual(updatedParts);
154
- });
155
-
156
- it("should return unchanged state when chatId is null", () => {
157
- const result = addMessageToChat(
158
- mockChatState,
159
- null,
160
- "msg-4",
161
- "user",
162
- "New message",
163
- );
164
-
165
- expect(result).toEqual(mockChatState);
166
- });
167
-
168
- it("should return unchanged state when chatId does not exist", () => {
169
- const result = addMessageToChat(
170
- mockChatState,
171
- "non-existent-chat" as ChatId,
172
- "msg-4",
173
- "user",
174
- "New message",
175
- );
176
-
177
61
  expect(result).toEqual(mockChatState);
178
62
  });
179
-
180
- it("should not modify other chats when updating a specific chat", () => {
181
- const result = addMessageToChat(
182
- mockChatState,
183
- CHAT_1,
184
- "msg-4",
185
- "user",
186
- "New message",
187
- );
188
-
189
- const unchangedChat = result.chats.get(CHAT_2);
190
- expect(unchangedChat).toEqual([...mockChatState.chats.values()][1]);
191
- });
192
-
193
- it("should preserve message order when adding new messages", () => {
194
- const result = addMessageToChat(
195
- mockChatState,
196
- CHAT_1,
197
- "msg-4",
198
- "user",
199
- "New message",
200
- );
201
-
202
- const updatedChat = result.chats.get(CHAT_1);
203
- expect(updatedChat?.messages[0].id).toBe("msg-1");
204
- expect(updatedChat?.messages[1].id).toBe("msg-2");
205
- expect(updatedChat?.messages[2].id).toBe("msg-4");
206
- });
207
-
208
- it("should handle empty chat messages array", () => {
209
- const chatId = "empty-chat" as ChatId;
210
- const emptyChatState: ChatState = {
211
- chats: asMap([
212
- {
213
- id: chatId,
214
- title: "Empty Chat",
215
- messages: [],
216
- createdAt: 1000,
217
- updatedAt: 1000,
218
- },
219
- ]),
220
- activeChatId: chatId,
221
- };
222
-
223
- const result = addMessageToChat(
224
- emptyChatState,
225
- chatId,
226
- "msg-1",
227
- "user",
228
- "First message",
229
- );
230
-
231
- const updatedChat = result.chats.get(chatId);
232
- expect(updatedChat?.messages).toHaveLength(1);
233
- expect(updatedChat?.messages[0].content).toBe("First message");
234
- });
235
-
236
- it("should handle different message roles", () => {
237
- const result = addMessageToChat(
238
- mockChatState,
239
- CHAT_1,
240
- "msg-4",
241
- "assistant",
242
- "Assistant response",
243
- );
244
-
245
- const updatedChat = result.chats.get(CHAT_1);
246
- expect(updatedChat?.messages[2].role).toBe("assistant");
247
- });
248
63
  });
@@ -10,6 +10,7 @@ import {
10
10
  CircleHelpIcon,
11
11
  } from "lucide-react";
12
12
  import React from "react";
13
+ import { type SupportedRole, useModelChange } from "@/core/ai/config";
13
14
  import {
14
15
  AiModelId,
15
16
  isKnownAIProvider,
@@ -36,12 +37,13 @@ import { getCurrentRoleTooltip, getTagColour } from "./display-helpers";
36
37
  interface AIModelDropdownProps {
37
38
  value?: string;
38
39
  placeholder?: string;
39
- onSelect: (modelId: QualifiedModelId) => void;
40
+ onSelect?: (modelId: QualifiedModelId) => void;
40
41
  triggerClassName?: string;
41
42
  customDropdownContent?: React.ReactNode;
42
43
  iconSize?: "medium" | "small";
43
44
  showAddCustomModelDocs?: boolean;
44
- forRole?: Role;
45
+ displayIconOnly?: boolean;
46
+ forRole: SupportedRole;
45
47
  }
46
48
 
47
49
  export const AIModelDropdown = ({
@@ -53,12 +55,13 @@ export const AIModelDropdown = ({
53
55
  iconSize = "medium",
54
56
  showAddCustomModelDocs = false,
55
57
  forRole,
58
+ displayIconOnly = false,
56
59
  }: AIModelDropdownProps) => {
57
- const currentValue = value ? AiModelId.parse(value) : undefined;
58
60
  const [isOpen, setIsOpen] = React.useState(false);
59
61
 
60
62
  const ai = useAtomValue(aiAtom);
61
63
  const completion = useAtomValue(completionAtom);
64
+ const { saveModelChange } = useModelChange();
62
65
 
63
66
  // Only include autocompleteModel if copilot is set to "custom"
64
67
  const autocompleteModel =
@@ -88,6 +91,13 @@ export const AIModelDropdown = ({
88
91
  ? ai?.models?.edit_model
89
92
  : undefined;
90
93
 
94
+ // If value is provided, use it, otherwise use the active model
95
+ const currentValue = value
96
+ ? AiModelId.parse(value)
97
+ : activeModel
98
+ ? AiModelId.parse(activeModel)
99
+ : undefined;
100
+
91
101
  const iconSizeClass = iconSize === "medium" ? "h-4 w-4" : "h-3 w-3";
92
102
 
93
103
  const renderModelWithRole = (modelId: AiModelId, role: Role) => {
@@ -119,7 +129,11 @@ export const AIModelDropdown = ({
119
129
  };
120
130
 
121
131
  const handleSelect = (modelId: QualifiedModelId) => {
122
- onSelect(modelId);
132
+ if (onSelect) {
133
+ onSelect(modelId);
134
+ } else {
135
+ saveModelChange(modelId, forRole);
136
+ }
123
137
  setIsOpen(false);
124
138
  };
125
139
 
@@ -136,11 +150,13 @@ export const AIModelDropdown = ({
136
150
  provider={currentValue.providerId}
137
151
  className={iconSizeClass}
138
152
  />
139
- <span className="truncate">
140
- {isKnownAIProvider(currentValue.providerId)
141
- ? currentValue.shortModelId
142
- : currentValue.id}
143
- </span>
153
+ {displayIconOnly ? null : (
154
+ <span className="truncate">
155
+ {isKnownAIProvider(currentValue.providerId)
156
+ ? currentValue.shortModelId
157
+ : currentValue.id}
158
+ </span>
159
+ )}
144
160
  </>
145
161
  ) : (
146
162
  <span className="text-muted-foreground truncate">
@@ -31,6 +31,7 @@ import { Input } from "@/components/ui/input";
31
31
  import { Kbd } from "@/components/ui/kbd";
32
32
  import { NativeSelect } from "@/components/ui/native-select";
33
33
  import { Textarea } from "@/components/ui/textarea";
34
+ import type { SupportedRole } from "@/core/ai/config";
34
35
  import {
35
36
  AiModelId,
36
37
  PROVIDERS,
@@ -225,6 +226,7 @@ interface ModelSelectorProps {
225
226
  description?: React.ReactNode;
226
227
  disabled?: boolean;
227
228
  label: string;
229
+ forRole: SupportedRole;
228
230
  onSubmit: (values: UserConfig) => void;
229
231
  }
230
232
 
@@ -237,6 +239,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
237
239
  description,
238
240
  disabled = false,
239
241
  label,
242
+ forRole,
240
243
  onSubmit,
241
244
  }) => {
242
245
  return (
@@ -289,6 +292,7 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
289
292
  </div>
290
293
  </>
291
294
  }
295
+ forRole={forRole}
292
296
  />
293
297
  </FormControl>
294
298
  <FormMessage />
@@ -420,6 +424,7 @@ const renderCopilotProvider = ({
420
424
  testId="custom-model-input"
421
425
  description="Model to use for code completion when using a custom provider."
422
426
  onSubmit={onSubmit}
427
+ forRole="autocomplete"
423
428
  />
424
429
  );
425
430
  }
@@ -905,6 +910,7 @@ export const AiAssistConfig: React.FC<AiConfigProps> = ({
905
910
  description={
906
911
  <span>Model to use for chat conversations in the Chat panel.</span>
907
912
  }
913
+ forRole="chat"
908
914
  onSubmit={onSubmit}
909
915
  />
910
916
  <ModelSelector
@@ -921,6 +927,7 @@ export const AiAssistConfig: React.FC<AiConfigProps> = ({
921
927
  <Kbd className="inline">Generate with AI</Kbd> button.
922
928
  </span>
923
929
  }
930
+ forRole="edit"
924
931
  onSubmit={onSubmit}
925
932
  />
926
933
 
@@ -0,0 +1,71 @@
1
+ /* Copyright 2024 Marimo. All rights reserved. */
2
+
3
+ import type { FileUIPart } from "ai";
4
+ import { FileIcon, FileTextIcon, ImageIcon, XIcon } from "lucide-react";
5
+ import { useState } from "react";
6
+ import { cn } from "@/utils/cn";
7
+
8
+ export const AttachmentRenderer = ({
9
+ attachment,
10
+ }: {
11
+ attachment: FileUIPart;
12
+ }) => {
13
+ if (attachment.mediaType?.startsWith("image/")) {
14
+ return (
15
+ <img
16
+ src={attachment.url}
17
+ alt={attachment.filename}
18
+ className="max-h-[100px] max-w-[100px] object-contain mb-1.5"
19
+ />
20
+ );
21
+ }
22
+
23
+ return (
24
+ <div className="flex flex-row gap-1 items-center text-xs">
25
+ <FileIcon className="h-3 w-3 mt-0.5" />
26
+ {attachment.filename}
27
+ </div>
28
+ );
29
+ };
30
+
31
+ export const FileAttachmentPill = ({
32
+ file,
33
+ className,
34
+ onRemove,
35
+ }: {
36
+ file: File;
37
+ className?: string;
38
+ onRemove: () => void;
39
+ }) => {
40
+ const [isHovered, setIsHovered] = useState(false);
41
+
42
+ return (
43
+ <div
44
+ className={cn(
45
+ "py-1 px-1.5 bg-muted rounded-md cursor-pointer flex flex-row gap-1 items-center text-xs",
46
+ className,
47
+ )}
48
+ onMouseEnter={() => setIsHovered(true)}
49
+ onMouseLeave={() => setIsHovered(false)}
50
+ >
51
+ {isHovered ? (
52
+ <XIcon className="h-3 w-3 mt-0.5" onClick={onRemove} />
53
+ ) : (
54
+ renderFileIcon(file)
55
+ )}
56
+ {file.name}
57
+ </div>
58
+ );
59
+ };
60
+
61
+ function renderFileIcon(file: File): React.ReactNode {
62
+ const classNames = "h-3 w-3 mt-0.5";
63
+
64
+ if (file.type.startsWith("image/")) {
65
+ return <ImageIcon className={classNames} />;
66
+ } else if (file.type.startsWith("text/")) {
67
+ return <FileTextIcon className={classNames} />;
68
+ }
69
+
70
+ return <FileIcon className={classNames} />;
71
+ }