@marimo-team/islands 0.25.3-dev266 → 0.25.3-dev268

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 (184) hide show
  1. package/dist/{ConnectedDataExplorerComponent-C_hj7Poi.js → ConnectedDataExplorerComponent-eLHPh6uu.js} +9 -9
  2. package/dist/{ErrorBoundary-CvxlC3F1.js → ErrorBoundary-CQYAoD7X.js} +64 -69
  3. package/dist/{abnfDiagram-VCTEODGH-DzrZZXRD.js → abnfDiagram-VCTEODGH-hqd0IBtX.js} +2 -2
  4. package/dist/{any-language-editor-CFeWfC3o.js → any-language-editor-DnG26QXh.js} +1 -1
  5. package/dist/{architectureDiagram-5GKGNRK7-iF_snyFL.js → architectureDiagram-5GKGNRK7-BzLAFcc0.js} +1 -1
  6. package/dist/{strings-BCKpLFNU.js → assertNever-DIU3JJbt.js} +14 -14
  7. package/dist/{chat-ui-DFxNgzXF.js → chat-ui-BAnLgpTe.js} +1964 -1740
  8. package/dist/{chunk-IMKFNOWR-B7xytD4B.js → chunk-IMKFNOWR-DOeVvuyE.js} +1 -1
  9. package/dist/{chunk-SHT3W25Y-I8ZFP5L1.js → chunk-SHT3W25Y-DZoFI8wR.js} +1 -1
  10. package/dist/{chunk-SVP7TREG-Di-uAPdT.js → chunk-SVP7TREG-CWu86PSW.js} +1 -1
  11. package/dist/{chunk-TICWLB2K-BFpUy0ud.js → chunk-TICWLB2K-BrM0GoP3.js} +1 -1
  12. package/dist/{classDiagram-ZZMXUADV-Cqm4JGpx.js → classDiagram-ZZMXUADV-DYi4V2bB.js} +2 -2
  13. package/dist/{classDiagram-v2-VYDZK3BY-DFHRJpN3.js → classDiagram-v2-VYDZK3BY-tN7FIk-m.js} +2 -2
  14. package/dist/{code-visibility-SRM1JiD_.js → code-visibility-DcZ_RwaH.js} +44 -47
  15. package/dist/{common-yiRhZ6ZZ.js → common-BGeQilvh.js} +5846 -5519
  16. package/dist/{config-CmeG_Pb6.js → config-BsPruWMR.js} +2 -2
  17. package/dist/{createReducer-C_syF8Td.js → createReducer-DdtT_7ji.js} +1 -1
  18. package/dist/{cynefinDiagram-5FMLGOSQ-cuGOeTBC.js → cynefinDiagram-5FMLGOSQ-D5nCnDc-.js} +1 -1
  19. package/dist/{dagre-GXQ25YYZ-DJ76aC4k.js → dagre-GXQ25YYZ-BHkd7f_i.js} +1 -1
  20. package/dist/{data-grid-overlay-editor-DLTzcQ4O.js → data-grid-overlay-editor-Dc4lW4A3.js} +1 -1
  21. package/dist/{diagram-S7CK7UJ4-DLLc0sw7.js → diagram-S7CK7UJ4-D1ALppT2.js} +1 -1
  22. package/dist/{diagram-UQ7AKVKN-Tz_d6qmk.js → diagram-UQ7AKVKN-DLv0b83L.js} +1 -1
  23. package/dist/{diagram-VX7I27RA-D7SzPrDN.js → diagram-VX7I27RA-7XJ7J6qg.js} +1 -1
  24. package/dist/{diagram-Z3DM3KII-qr37XV58.js → diagram-Z3DM3KII-7M7zbHp_.js} +1 -1
  25. package/dist/{dialog-BtprQCUw.js → dialog-DzCj1qQv.js} +1 -1
  26. package/dist/{dropdown-menu-CkJ7ErY6.js → dropdown-menu-CdXE15an.js} +1214 -1208
  27. package/dist/{ebnfDiagram-PWID7BFC-BKOQb4J0.js → ebnfDiagram-PWID7BFC-DHH1qlmV.js} +2 -2
  28. package/dist/{erDiagram-RLTQ6QDP-CM15Vq68.js → erDiagram-RLTQ6QDP-fI_0dGef.js} +1 -1
  29. package/dist/{error-banner-DbRFOd2O.js → error-banner-DPc9eIw5.js} +3 -3
  30. package/dist/{error-entries-CMoaUX9V.js → error-entries-CxN-Nq3V.js} +524 -456
  31. package/dist/{feedback-button-DBUrE4OL.js → feedback-button-BqaeQcvX.js} +77 -77
  32. package/dist/{flowDiagram-HODETNUW-Dt3N5yFT.js → flowDiagram-HODETNUW-Brg9MA6i.js} +2 -2
  33. package/dist/{formats-gGospKWJ.js → formats-DbSvAPY-.js} +1 -1
  34. package/dist/{glide-data-editor-Cfv4HeOh.js → glide-data-editor-CiijPXA7.js} +2042 -2041
  35. package/dist/{glide-utils-Ba9WD2IN.js → glide-utils-BSw3SSHF.js} +29 -35
  36. package/dist/{globals-dIbBIR5-.js → globals-CpmSWKI_.js} +2 -2
  37. package/dist/{html-to-image-D_5CMreP.js → html-to-image-8sywF4my.js} +5452 -5751
  38. package/dist/{infoDiagram-27XIBGKW-C7axJMCl.js → infoDiagram-27XIBGKW-E9e403YT.js} +1 -1
  39. package/dist/{ishikawaDiagram-5VMMS53U-CdC9Eur7.js → ishikawaDiagram-5VMMS53U-C-RlYDRo.js} +1 -1
  40. package/dist/{kanban-definition-UXKFOSKX-BHnMIgD7.js → kanban-definition-UXKFOSKX-CLFl_uqK.js} +1 -1
  41. package/dist/main.js +6294 -6324
  42. package/dist/{mermaid-DFYN_PXd.js → mermaid-rd09e1JL.js} +35 -35
  43. package/dist/{mindmap-definition-YA3MSWOX-BagHoCGU.js → mindmap-definition-YA3MSWOX-BQ8TWtyB.js} +1 -1
  44. package/dist/{number-overlay-editor-CTiJp5Mj.js → number-overlay-editor-Ft4NiAEN.js} +1 -1
  45. package/dist/{pegDiagram-XKGWAZYB-8IIZczip.js → pegDiagram-XKGWAZYB-CN8UHFPi.js} +2 -2
  46. package/dist/pencil-DcT8AE0h.js +10 -0
  47. package/dist/{pieDiagram-E7YTZNPT-CE6dKBIr.js → pieDiagram-E7YTZNPT-DGuJ0EBd.js} +1 -1
  48. package/dist/{process-output-CGFNVC60.js → process-output-Dku5POvm.js} +3 -3
  49. package/dist/{railroadDiagram-O6MQD6OU-CkexPq3b.js → railroadDiagram-O6MQD6OU-BPDA8wy8.js} +2 -2
  50. package/dist/{requirementDiagram-BXWQKSXE-B0-OkuAe.js → requirementDiagram-BXWQKSXE-ChzLGbGW.js} +1 -1
  51. package/dist/{reveal-component-DH09ygHo.js → reveal-component-D0cfwbbl.js} +1094 -1094
  52. package/dist/{select-C33FVPPN.js → select-OxgozOC8.js} +1 -1
  53. package/dist/{sequenceDiagram-WJ2MYXX4-Dxysg8DX.js → sequenceDiagram-WJ2MYXX4-DFX-bE6O.js} +1 -1
  54. package/dist/{spec-DH1lmF7Q.js → spec-oJn0RhpQ.js} +2 -2
  55. package/dist/{stateDiagram-D77RDMKH-DTEN3-OJ.js → stateDiagram-D77RDMKH-BR7cI3wK.js} +2 -2
  56. package/dist/{stateDiagram-v2-MP3YSRHH-vktk3Xc5.js → stateDiagram-v2-MP3YSRHH-oGqB5UDI.js} +2 -2
  57. package/dist/{static-state-BldGd99T.js → static-state-CuV6Pc3a.js} +1 -1
  58. package/dist/style.css +1 -1
  59. package/dist/{swimlanes-42K2YHIH-Cjveh9OM.js → swimlanes-42K2YHIH-DwSZl1yC.js} +1 -1
  60. package/dist/{swimlanesDiagram-VR7AAH4N-Bdr7Ev_2.js → swimlanesDiagram-VR7AAH4N-BHlcd-Ri.js} +2 -2
  61. package/dist/{timeline-definition-24CTP7MA-KIuDcefn.js → timeline-definition-24CTP7MA-DMmP-G2O.js} +1 -1
  62. package/dist/{toast-CY1_Qv41.js → toast-DbBHmxmB.js} +2 -2
  63. package/dist/{useAsyncData-DsZMvNXn.js → useAsyncData-BYIAoMqn.js} +1 -1
  64. package/dist/{useDateFormatter-Dk_xFci4.js → useDateFormatter-ButWkdcK.js} +1 -1
  65. package/dist/{useDeepCompareMemoize-KMBBux8C.js → useDeepCompareMemoize-Bz0c5GN6.js} +1 -1
  66. package/dist/{useTheme-BZ2GGLgm.js → useTheme-DPLobSFs.js} +7 -7
  67. package/dist/{utils-DXkj82sK.js → utils-EBklsC5R.js} +31 -31
  68. package/dist/{vega-component-C-lqjV0q.js → vega-component-rxIxAAh9.js} +7 -7
  69. package/dist/{vennDiagram-4TSXK5OY-DrIOUHfb.js → vennDiagram-4TSXK5OY-Db8GtH1G.js} +1 -1
  70. package/dist/{wardleyDiagram-VM6X3IG4-CJNjf4OH.js → wardleyDiagram-VM6X3IG4-DmIiC3FU.js} +1 -1
  71. package/dist/{xychartDiagram-S5SC5T6Z-DEshFho-.js → xychartDiagram-S5SC5T6Z-DS9OPrNq.js} +1 -1
  72. package/package.json +1 -1
  73. package/src/__mocks__/executions.ts +195 -0
  74. package/src/__mocks__/notebook.ts +0 -1
  75. package/src/__tests__/CellStatus.test.tsx +19 -20
  76. package/src/components/chat/__tests__/chat-display.test.tsx +157 -86
  77. package/src/components/chat/__tests__/chat-utils.test.ts +42 -0
  78. package/src/components/chat/__tests__/turn-timing.test.ts +84 -0
  79. package/src/components/chat/__tests__/use-assistant-turn.test.ts +95 -0
  80. package/src/components/chat/__tests__/user-message.test.tsx +27 -0
  81. package/src/components/chat/acp/agent-panel.tsx +2 -0
  82. package/src/components/chat/agent-activity.tsx +132 -0
  83. package/src/components/chat/chat-display.tsx +125 -96
  84. package/src/components/chat/chat-panel.tsx +123 -106
  85. package/src/components/chat/chat-utils.ts +12 -24
  86. package/src/components/chat/tool-call/__tests__/tool-labels.test.ts +76 -0
  87. package/src/components/chat/tool-call/__tests__/tool-step.test.tsx +57 -0
  88. package/src/components/chat/tool-call/shared.ts +6 -0
  89. package/src/components/chat/tool-call/tool-call-view.tsx +22 -76
  90. package/src/components/chat/tool-call/tool-labels.ts +172 -0
  91. package/src/components/chat/tool-call/tool-step.tsx +145 -0
  92. package/src/components/chat/turn-timing.ts +73 -0
  93. package/src/components/chat/use-assistant-turn.ts +65 -0
  94. package/src/components/chat/use-chat-generation.ts +5 -4
  95. package/src/components/chat/user-message.tsx +145 -0
  96. package/src/components/dependency-graph/minimap-content.tsx +3 -2
  97. package/src/components/editor/__tests__/data-attributes.test.tsx +0 -7
  98. package/src/components/editor/__tests__/output-persistence.test.tsx +2 -2
  99. package/src/components/editor/actions/__tests__/useCellActionButton.test.tsx +19 -1
  100. package/src/components/editor/actions/useCellActionButton.tsx +10 -3
  101. package/src/components/editor/ai/completion-handlers.tsx +0 -37
  102. package/src/components/editor/alerts/stdin-blocking-alert.tsx +3 -1
  103. package/src/components/editor/cell/CellStatus.tsx +23 -19
  104. package/src/components/editor/cell/RunButton.tsx +16 -6
  105. package/src/components/editor/cell/StagedAICell.tsx +51 -144
  106. package/src/components/editor/cell/__tests__/useRunCells.test.ts +45 -0
  107. package/src/components/editor/cell/cell-context-menu.tsx +4 -1
  108. package/src/components/editor/cell/code/cell-editor.tsx +7 -5
  109. package/src/components/editor/cell/collapse.tsx +4 -0
  110. package/src/components/editor/cell/useRunCells.ts +18 -3
  111. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +193 -32
  112. package/src/components/editor/chrome/wrapper/app-chrome.tsx +2 -2
  113. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +145 -81
  114. package/src/components/editor/chrome/wrapper/pending-ai-changes-badge.tsx +25 -0
  115. package/src/components/editor/chrome/wrapper/sidebar.tsx +8 -4
  116. package/src/components/editor/header/status.tsx +4 -2
  117. package/src/components/editor/links/cell-link.tsx +4 -4
  118. package/src/components/editor/navigation/navigation.ts +3 -2
  119. package/src/components/editor/notebook-cell.tsx +85 -94
  120. package/src/components/editor/renderers/grid-layout/__tests__/grid-layout.test.tsx +75 -0
  121. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +22 -16
  122. package/src/components/editor/renderers/slides-layout/__tests__/plugin.test.ts +0 -1
  123. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +12 -33
  124. package/src/components/editor/stdin-blocking-alert.tsx +3 -1
  125. package/src/components/scratchpad/scratchpad.tsx +4 -1
  126. package/src/components/slides/minimap.tsx +4 -2
  127. package/src/components/slides/reveal-component.tsx +14 -10
  128. package/src/components/slides/slide-cell-view.tsx +28 -24
  129. package/src/components/slides/slide.tsx +3 -4
  130. package/src/components/ui/toast.tsx +1 -1
  131. package/src/core/ai/__tests__/staged-cells.test.ts +74 -126
  132. package/src/core/ai/staged-cells.ts +48 -38
  133. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +0 -389
  134. package/src/core/cells/__tests__/cell.test.ts +49 -154
  135. package/src/core/cells/__tests__/cells.test.ts +18 -127
  136. package/src/core/cells/__tests__/pending-delete-service.test.tsx +11 -20
  137. package/src/core/cells/__tests__/utils.test.ts +75 -247
  138. package/src/core/cells/actions.ts +5 -2
  139. package/src/core/cells/cell.ts +31 -133
  140. package/src/core/cells/cells.ts +14 -29
  141. package/src/core/cells/document.ts +3 -3
  142. package/src/core/cells/focus.ts +27 -6
  143. package/src/core/cells/layout.ts +1 -1
  144. package/src/core/cells/pending-delete-service.ts +5 -7
  145. package/src/core/cells/scrollCellIntoView.ts +6 -0
  146. package/src/core/cells/types.ts +13 -36
  147. package/src/core/cells/utils.ts +37 -68
  148. package/src/core/codemirror/placeholder/__tests__/extensions.test.ts +59 -0
  149. package/src/core/codemirror/placeholder/extensions.ts +23 -0
  150. package/src/core/edit-app.tsx +2 -1
  151. package/src/core/executions/__tests__/atoms.test.ts +99 -0
  152. package/src/core/executions/__tests__/select.test.ts +268 -0
  153. package/src/core/executions/atoms.ts +206 -0
  154. package/src/core/executions/hooks.ts +31 -0
  155. package/src/core/executions/index.ts +31 -0
  156. package/src/core/executions/select.ts +175 -0
  157. package/src/core/export/__tests__/hooks.test.ts +0 -5
  158. package/src/core/islands/bootstrap.ts +4 -1
  159. package/src/core/islands/components/output-wrapper.tsx +5 -5
  160. package/src/core/kernel/handlers.ts +2 -11
  161. package/src/core/kernel/state.ts +0 -15
  162. package/src/core/lifecycle/render-policy.ts +12 -26
  163. package/src/core/network/__tests__/requests-lazy.test.ts +5 -5
  164. package/src/core/network/requests-lazy.ts +3 -3
  165. package/src/core/notebook/operations.ts +1 -1
  166. package/src/core/run-app.tsx +2 -1
  167. package/src/core/runtime/__tests__/adapter.test.ts +3 -3
  168. package/src/core/runtime/adapter.ts +4 -4
  169. package/src/core/session/__tests__/useNotebookSession.hook.test.tsx +34 -0
  170. package/src/core/session/useNotebookSession.tsx +10 -5
  171. package/src/core/wasm/bridge.ts +1 -1
  172. package/src/css/app/Cell.css +7 -36
  173. package/src/plugins/core/registerReactComponent.tsx +4 -13
  174. package/src/plugins/impl/chat/chat-ui.tsx +2 -3
  175. package/src/plugins/impl/common/error-banner.tsx +5 -3
  176. package/src/stories/cell-status.stories.tsx +17 -10
  177. package/src/stories/cell.stories.tsx +0 -31
  178. package/src/stories/layout/vertical/one-column.stories.tsx +0 -9
  179. package/src/utils/time.ts +10 -0
  180. package/src/components/chat/reasoning-accordion.tsx +0 -61
  181. package/src/components/chat/tool-call/tool-activity-group.tsx +0 -49
  182. package/src/components/chat/tool-call/tool-error-card.tsx +0 -81
  183. package/src/components/chat/tool-call/tool-history-row.tsx +0 -147
  184. package/src/components/icons/loading-ellipsis.tsx +0 -46
@@ -0,0 +1,73 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessage } from "ai";
4
+
5
+ export interface TurnTiming {
6
+ startedAt: number;
7
+ finishedAt?: number;
8
+ stopped?: boolean;
9
+ }
10
+
11
+ export function getTurnTiming(message: UIMessage): TurnTiming | undefined {
12
+ const metadata = message.metadata;
13
+ if (typeof metadata !== "object" || metadata === null) {
14
+ return undefined;
15
+ }
16
+ const { startedAt, finishedAt, stopped } = metadata as Record<
17
+ string,
18
+ unknown
19
+ >;
20
+ if (typeof startedAt !== "number") {
21
+ return undefined;
22
+ }
23
+ return {
24
+ startedAt,
25
+ finishedAt: typeof finishedAt === "number" ? finishedAt : undefined,
26
+ stopped: stopped === true,
27
+ };
28
+ }
29
+
30
+ export function withTurnTiming(
31
+ messages: UIMessage[],
32
+ turn: { startedAt: number | null; stopped: boolean },
33
+ now = Date.now(),
34
+ ): UIMessage[] {
35
+ const last = messages.at(-1);
36
+ if (last?.role !== "assistant") {
37
+ return messages;
38
+ }
39
+ const startedAt = turn.startedAt ?? getTurnTiming(last)?.startedAt;
40
+ if (startedAt == null) {
41
+ return messages;
42
+ }
43
+ const metadata =
44
+ typeof last.metadata === "object" && last.metadata !== null
45
+ ? last.metadata
46
+ : {};
47
+ return [
48
+ ...messages.slice(0, -1),
49
+ {
50
+ ...last,
51
+ metadata: {
52
+ ...metadata,
53
+ startedAt,
54
+ finishedAt: now,
55
+ stopped: turn.stopped,
56
+ },
57
+ },
58
+ ];
59
+ }
60
+
61
+ export function formatDuration(ms: number): string {
62
+ const totalSeconds = Math.max(1, Math.round(ms / 1000));
63
+ const hours = Math.floor(totalSeconds / 3600);
64
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
65
+ const seconds = totalSeconds % 60;
66
+ if (hours > 0) {
67
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
68
+ }
69
+ if (minutes > 0) {
70
+ return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
71
+ }
72
+ return `${seconds}s`;
73
+ }
@@ -0,0 +1,65 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { UIMessage } from "ai";
4
+ import { useState } from "react";
5
+ import useEvent from "react-use-event-hook";
6
+ import { willAutoContinue } from "./chat-utils";
7
+ import { getTurnTiming, withTurnTiming } from "./turn-timing";
8
+
9
+ export function useAssistantTurn() {
10
+ const [turn, setTurn] = useState<{
11
+ startedAt: number;
12
+ ended: boolean;
13
+ } | null>(null);
14
+
15
+ const start = useEvent(() =>
16
+ setTurn({ startedAt: Date.now(), ended: false }),
17
+ );
18
+ const reset = useEvent(() => setTurn(null));
19
+ const stop = useEvent(() =>
20
+ setTurn((prev) => prev && { ...prev, ended: true }),
21
+ );
22
+
23
+ // Shift the start forward so time spent waiting on the user isn't counted.
24
+ const resume = useEvent((reply: UIMessage | undefined) => {
25
+ const timing = reply && getTurnTiming(reply);
26
+ const workedMs =
27
+ timing?.finishedAt == null ? 0 : timing.finishedAt - timing.startedAt;
28
+ setTurn({ startedAt: Date.now() - workedMs, ended: false });
29
+ });
30
+
31
+ const finish = useEvent(
32
+ (finished: UIMessage[], opts: { isAbort: boolean; isError: boolean }) => {
33
+ if (opts.isAbort || opts.isError) {
34
+ stop();
35
+ }
36
+ return withTurnTiming(finished, {
37
+ startedAt: turn?.startedAt ?? null,
38
+ stopped: opts.isAbort,
39
+ });
40
+ },
41
+ );
42
+
43
+ // Between requests of one turn the SDK briefly reports "ready".
44
+ const isActive = (state: {
45
+ isLoading: boolean;
46
+ hasError: boolean;
47
+ messages: UIMessage[];
48
+ }) =>
49
+ state.isLoading ||
50
+ (turn != null &&
51
+ !turn.ended &&
52
+ !state.hasError &&
53
+ willAutoContinue(state.messages));
54
+
55
+ return {
56
+ startedAt: turn?.startedAt ?? null,
57
+ hasEnded: turn?.ended === true,
58
+ start,
59
+ resume,
60
+ stop,
61
+ reset,
62
+ finish,
63
+ isActive,
64
+ };
65
+ }
@@ -5,15 +5,16 @@ import { useSetAtom } from "jotai";
5
5
  import { useEffect } from "react";
6
6
  import { chatGenerationInProgressAtom } from "@/core/ai/state";
7
7
 
8
- export function useChatGeneration(status: ChatStatus) {
8
+ export function isChatBusy(status: ChatStatus): boolean {
9
+ return status === "submitted" || status === "streaming";
10
+ }
11
+
12
+ export function useChatGeneration(isGenerating: boolean) {
9
13
  const setGenerating = useSetAtom(chatGenerationInProgressAtom);
10
- const isGenerating = status === "submitted" || status === "streaming";
11
14
 
12
15
  useEffect(() => {
13
16
  setGenerating(isGenerating);
14
17
  // useChat stops its request when the panel unmounts or becomes hidden.
15
18
  return () => setGenerating(false);
16
19
  }, [isGenerating, setGenerating]);
17
-
18
- return isGenerating;
19
20
  }
@@ -0,0 +1,145 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
4
+ import type { FileUIPart, TextUIPart, UIMessage } from "ai";
5
+ import { PencilIcon } from "lucide-react";
6
+ import React from "react";
7
+ import { Button } from "@/components/ui/button";
8
+ import { Tooltip } from "@/components/ui/tooltip";
9
+ import { focusInputAndMoveToEnd } from "@/core/codemirror/utils";
10
+ import { PromptInput } from "../editor/ai/prompt-input";
11
+ import { CopyClipboardIcon } from "../icons/copy-icon";
12
+ import { AttachmentRenderer } from "./chat-components";
13
+
14
+ interface UserMessageProps {
15
+ message: UIMessage;
16
+ canEdit: boolean;
17
+ onEdit: (newValue: string) => void;
18
+ }
19
+
20
+ export const UserMessage: React.FC<UserMessageProps> = ({
21
+ message,
22
+ canEdit,
23
+ onEdit,
24
+ }) => {
25
+ const content = message.parts
26
+ .filter((p): p is TextUIPart => p.type === "text")
27
+ .map((p) => p.text)
28
+ .join("\n");
29
+ const attachments = message.parts
30
+ .filter((p): p is FileUIPart => p.type === "file")
31
+ .map((part, idx) => (
32
+ // Sent parts never reorder, and a file attached twice repeats its URL.
33
+ // react-doctor-disable-next-line react-doctor/no-array-index-as-key
34
+ <AttachmentRenderer attachment={part} key={idx} />
35
+ ));
36
+
37
+ const [draft, setDraft] = React.useState<string | null>(null);
38
+ const inputRef = React.useRef<ReactCodeMirrorRef>(null);
39
+ const editButtonRef = React.useRef<HTMLButtonElement>(null);
40
+ const editing = draft != null;
41
+
42
+ const startEditing = () => {
43
+ setDraft(content);
44
+ focusInputAndMoveToEnd(inputRef);
45
+ };
46
+
47
+ const cancel = () => {
48
+ setDraft(null);
49
+ requestAnimationFrame(() => editButtonRef.current?.focus());
50
+ };
51
+
52
+ const submit = (value: string) => {
53
+ if (!canEdit || !value.trim()) {
54
+ return;
55
+ }
56
+ onEdit(value);
57
+ setDraft(null);
58
+ };
59
+
60
+ if (editing) {
61
+ return (
62
+ <div className="w-full rounded-[10px] border border-border bg-background">
63
+ {attachments}
64
+ <div className="px-2 py-1.5">
65
+ <PromptInput
66
+ inputRef={inputRef}
67
+ className="max-h-[400px]"
68
+ value={draft}
69
+ onChange={setDraft}
70
+ onSubmit={(_e, value) => submit(value)}
71
+ onClose={cancel}
72
+ placeholder="Type your message. Use @ to reference variables and cells."
73
+ />
74
+ </div>
75
+ <div className="flex items-center gap-2 border-t border-border/20 px-3 py-1.5">
76
+ <span className="mr-auto text-xs text-muted-foreground">
77
+ Responses after this message will be replaced.
78
+ </span>
79
+ <Button variant="text" size="xs" onClick={cancel}>
80
+ Cancel
81
+ </Button>
82
+ <Button
83
+ size="xs"
84
+ disabled={!canEdit || !draft.trim()}
85
+ onClick={() => submit(draft)}
86
+ >
87
+ Send
88
+ </Button>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ return (
95
+ <div className="flex max-w-[90%] flex-col items-end gap-1">
96
+ {attachments}
97
+ <div className="rounded-[10px] bg-(--slate-3) px-3 py-1.5 whitespace-pre-wrap wrap-break-word">
98
+ <MentionText text={content} />
99
+ </div>
100
+ <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
101
+ <Tooltip content="Edit">
102
+ <Button
103
+ ref={editButtonRef}
104
+ variant="text"
105
+ size="icon"
106
+ aria-label="Edit message"
107
+ className="chat-control"
108
+ disabled={!canEdit}
109
+ onClick={startEditing}
110
+ >
111
+ <PencilIcon className="size-3" />
112
+ </Button>
113
+ </Tooltip>
114
+ <CopyClipboardIcon
115
+ value={content}
116
+ className="size-3"
117
+ buttonClassName="chat-control flex size-6 items-center justify-center rounded-md"
118
+ />
119
+ </div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ const MENTION = /([@#])[\w-]+:\/\/([\w./-]+)/g;
125
+
126
+ const MentionText: React.FC<{ text: string }> = ({ text }) => {
127
+ const nodes: React.ReactNode[] = [];
128
+ let last = 0;
129
+ for (const match of text.matchAll(MENTION)) {
130
+ nodes.push(text.slice(last, match.index));
131
+ nodes.push(
132
+ <span
133
+ key={match.index}
134
+ title={match[0].slice(1)}
135
+ className="rounded-sm bg-(--slate-5) px-1 font-mono text-[0.85em]"
136
+ >
137
+ {match[1]}
138
+ {match[2]}
139
+ </span>,
140
+ );
141
+ last = match.index + match[0].length;
142
+ }
143
+ nodes.push(text.slice(last));
144
+ return nodes;
145
+ };
@@ -7,11 +7,12 @@ import {
7
7
  cellGraphsAtom,
8
8
  isVariableAffectedBySelectedCell,
9
9
  } from "@/components/editor/chrome/wrapper/minimap-state";
10
- import { useCellData, useCellRuntime } from "@/core/cells/cells";
10
+ import { useCellData } from "@/core/cells/cells";
11
11
  import { useCellActions } from "@/core/cells/actions";
12
12
  import { cellFocusAtom, useCellFocusActions } from "@/core/cells/focus";
13
13
  import type { CellId } from "@/core/cells/ids";
14
14
  import { useLayout } from "@/core/cells/layout";
15
+ import { useCellExecution } from "@/core/executions";
15
16
  import { useVariables } from "@/core/variables/state";
16
17
  import { cn } from "@/utils/cn";
17
18
  import { extractCellPreview } from "./utils/cell-preview";
@@ -32,7 +33,7 @@ const MinimapCell: React.FC<MinimapCellProps> = (props) => {
32
33
  id: props.cellId,
33
34
  graph: graphs[props.cellId],
34
35
  code: useCellData(props.cellId).code,
35
- hasError: useCellRuntime(props.cellId).errored,
36
+ hasError: useCellExecution(props.cellId).outcome === "error",
36
37
  };
37
38
 
38
39
  let selectedCell: undefined | { id: CellId; graph: CellGraph };
@@ -104,16 +104,9 @@ describe("Cell data attributes", () => {
104
104
  });
105
105
 
106
106
  notebook.cellRuntime[cid] = createCellRuntimeState({
107
- status: "idle",
108
107
  output: null,
109
108
  consoleOutputs: [],
110
- interrupted: false,
111
- errored: false,
112
- stopped: false,
113
109
  staleInputs: false,
114
- runStartTimestamp: null,
115
- lastRunStartTimestamp: null,
116
- runElapsedTimeMs: null,
117
110
  debuggerActive: false,
118
111
  outline: null,
119
112
  });
@@ -4,6 +4,7 @@ import { createStore, Provider } from "jotai";
4
4
  import { beforeAll, describe, expect, it } from "vitest";
5
5
  import { SetupMocks } from "@/__mocks__/common";
6
6
  import { mockDocumentModel } from "@/__mocks__/document";
7
+ import { MockExecutions } from "@/__mocks__/executions";
7
8
  import { MockNotebook } from "@/__mocks__/notebook";
8
9
  import { MockRequestClient } from "@/__mocks__/requests";
9
10
  import { cellId } from "@/__tests__/branded";
@@ -50,7 +51,6 @@ function seedNotebook(
50
51
  },
51
52
  });
52
53
  notebook.cellRuntime[cid] = createCellRuntimeState({
53
- status: "idle",
54
54
  output: {
55
55
  channel: "output",
56
56
  mimetype: "text/html",
@@ -207,8 +207,8 @@ describe("output persistence across mode changes", () => {
207
207
 
208
208
  it("hides errored cells while presenting without unmounting them", () => {
209
209
  const { store, wrapper } = createTestWrapper();
210
+ MockExecutions.seed(store, [MockExecutions.errored(cid)]);
210
211
  seedNotebook(store, {
211
- errored: true,
212
212
  output: {
213
213
  channel: "marimo-error",
214
214
  mimetype: "application/vnd.marimo+error",
@@ -10,6 +10,7 @@ import { partial } from "@/__mocks__/partial";
10
10
  import { MockRequestClient } from "@/__mocks__/requests";
11
11
  import { ModalProvider } from "@/components/modal/ImperativeModal";
12
12
  import { toast } from "@/components/ui/use-toast";
13
+ import { type Edit, stagedAICellsAtom } from "@/core/ai/staged-cells";
13
14
  import { CellId } from "@/core/cells/ids";
14
15
  import { requestClientAtom } from "@/core/network/requests";
15
16
  import type { NotebookReplica } from "@/core/notebook/replica";
@@ -21,8 +22,11 @@ import { useCellActionButtons } from "../useCellActionButton";
21
22
  vi.mock("@/utils/copy", () => ({ copyToClipboard: vi.fn() }));
22
23
  vi.mock("@/components/ui/use-toast", () => ({ toast: vi.fn() }));
23
24
 
24
- function renderActions(cellId: CellId, name: string) {
25
+ function renderActions(cellId: CellId, name: string, edit?: Edit) {
25
26
  const store = createStore();
27
+ if (edit) {
28
+ store.set(stagedAICellsAtom, new Map([[cellId, edit]]));
29
+ }
26
30
  const { model, documentAtom, stop } = mockDocumentModel(store);
27
31
  onTestFinished(stop);
28
32
  store.set(requestClientAtom, MockRequestClient.create());
@@ -48,6 +52,7 @@ function renderActions(cellId: CellId, name: string) {
48
52
  name,
49
53
  config: { disabled: false, hide_code: false, column: null },
50
54
  status: "idle",
55
+ blocked: false,
51
56
  hasOutput: false,
52
57
  hasConsoleOutput: false,
53
58
  getEditorView: () => null,
@@ -111,3 +116,16 @@ describe("useCellActionButtons", () => {
111
116
  expect(getAction(groups, "Copy cell ID").disabled).toBeUndefined();
112
117
  });
113
118
  });
119
+
120
+ it("offers individual AI review actions in the cell menu only for pending changes", () => {
121
+ const id = CellId.create();
122
+ const pending = renderActions(id, "_", { type: "add_cell" });
123
+ expect(pending[0].map((action) => action.label)).toEqual([
124
+ "Keep cell",
125
+ "Discard cell",
126
+ ]);
127
+ const ordinary = renderActions(id, "_");
128
+ expect(ordinary.flat().some((action) => action.label === "Keep cell")).toBe(
129
+ false,
130
+ );
131
+ });
@@ -56,13 +56,15 @@ import {
56
56
  appWidthAtom,
57
57
  autoInstantiateAtom,
58
58
  } from "@/core/config/config";
59
- import type { CellConfig, RuntimeState } from "@/core/network/types";
59
+ import type { CellRunStatus } from "@/core/executions";
60
+ import type { CellConfig } from "@/core/network/types";
60
61
  import { canEditAtom } from "@/core/notebook";
61
62
  import { canLinkToCell, createCellLink } from "@/utils/cell-urls";
62
63
  import { copyToClipboard } from "@/utils/copy";
63
64
  import { downloadCellOutputAsImage } from "@/utils/download";
64
65
  import { MarkdownIcon, PythonIcon } from "../cell/code/icons";
65
66
  import { useDeleteCellCallback } from "../cell/useDeleteCell";
67
+ import { useStagedAICellActions } from "../cell/StagedAICell";
66
68
  import { useRunCell } from "../cell/useRunCells";
67
69
  import { useSplitCellCallback } from "../cell/useSplitCell";
68
70
  import { NameCellInput } from "./name-cell-input";
@@ -73,7 +75,9 @@ export interface CellActionButtonProps extends Pick<
73
75
  "name" | "config"
74
76
  > {
75
77
  cellId: CellId;
76
- status: RuntimeState;
78
+ status: CellRunStatus;
79
+ /** Whether a cell it depends on is disabled. */
80
+ blocked: boolean;
77
81
  hasOutput: boolean;
78
82
  hasConsoleOutput: boolean;
79
83
  getEditorView: () => EditorView | null;
@@ -93,6 +97,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
93
97
  clearCellOutput,
94
98
  createCell,
95
99
  } = useCellActions();
100
+ const stagedActions = useStagedAICellActions(cell?.cellId);
96
101
  const showHiddenCode = useSetAtom(showHiddenCodeAtom);
97
102
  const edit = useNotebookDocumentEdit();
98
103
  const splitCell = useSplitCellCallback();
@@ -119,6 +124,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
119
124
  hasOutput,
120
125
  hasConsoleOutput,
121
126
  status,
127
+ blocked,
122
128
  } = cell;
123
129
 
124
130
  const toggleDisabled = () => {
@@ -144,6 +150,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
144
150
 
145
151
  // Actions
146
152
  const actions: ActionButton[][] = [
153
+ stagedActions,
147
154
  [
148
155
  {
149
156
  icon: <PlayIcon size={13} strokeWidth={1.5} />,
@@ -152,7 +159,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
152
159
  hidden:
153
160
  status === "running" ||
154
161
  status === "queued" ||
155
- status === "disabled-transitively" ||
162
+ blocked ||
156
163
  config.disabled,
157
164
  redundant: true,
158
165
  handle: () => runCell(),
@@ -46,43 +46,6 @@ export const createAiCompletionOnKeydown = (opts: {
46
46
  };
47
47
  };
48
48
 
49
- export const CompletionActionsCellFooter: React.FC<{
50
- isLoading: boolean;
51
- onAccept: () => void;
52
- onDecline: () => void;
53
- size?: "xs" | "sm";
54
- multipleCompletions?: boolean;
55
- runCell?: () => void;
56
- acceptLabel?: string;
57
- declineLabel?: string;
58
- }> = ({
59
- isLoading,
60
- onAccept,
61
- onDecline,
62
- runCell,
63
- acceptLabel,
64
- declineLabel,
65
- size,
66
- }) => {
67
- return (
68
- <>
69
- <AcceptCompletionButton
70
- isLoading={isLoading}
71
- onAccept={onAccept}
72
- size={size}
73
- runCell={runCell}
74
- label={acceptLabel}
75
- />
76
- <RejectCompletionButton
77
- onDecline={onDecline}
78
- size={size}
79
- label={declineLabel}
80
- disabled={isLoading}
81
- />
82
- </>
83
- );
84
- };
85
-
86
49
  export const AcceptCompletionButton: React.FC<{
87
50
  isLoading: boolean;
88
51
  onAccept: () => void;
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { atom, useAtomValue } from "jotai";
4
4
  import { notebookAtom } from "@/core/cells/cells";
5
+ import { cellExecutionInputsAtom, cellExecutionView } from "@/core/executions";
5
6
  import { Logger } from "@/utils/Logger";
6
7
  import { Button } from "../../ui/button";
7
8
  import { Kbd } from "../../ui/kbd";
@@ -10,12 +11,13 @@ import { FloatingAlert } from "./floating-alert";
10
11
  // Atom to check if there is a cell with unresolved stdin
11
12
  const hasBlockingStdinAtom = atom((get) => {
12
13
  const notebook = get(notebookAtom);
14
+ const executions = get(cellExecutionInputsAtom);
13
15
 
14
16
  // Check each cell in order
15
17
  for (const cellId of notebook.cellIds) {
16
18
  // Check if the cell is idle and has unresolved stdin
17
19
  const runtime = notebook.cellRuntime[cellId];
18
- if (runtime.status === "idle") {
20
+ if (cellExecutionView(executions, cellId).status === "idle") {
19
21
  const hasUnresolvedStdin = runtime.consoleOutputs.some(
20
22
  (output) => output.channel === "stdin" && output.response === undefined,
21
23
  );
@@ -7,24 +7,29 @@ import {
7
7
  } from "lucide-react";
8
8
  import { useDateFormatter } from "react-aria";
9
9
  import { MultiIcon } from "@/components/icons/multi-icon";
10
+ import type { CellRunStatus } from "@/core/executions";
10
11
  import { Logger } from "@/utils/Logger";
11
- import type { CellRuntimeState } from "../../../core/cells/types";
12
12
  import { useElapsedTime } from "../../../hooks/useElapsedTime";
13
13
  import { Tooltip } from "../../ui/tooltip";
14
14
 
15
15
  import "./cell-status.css";
16
16
  import { formatDistanceToNow } from "date-fns";
17
- import { formatElapsedTime, Time } from "@/utils/time";
17
+ import { formatElapsedTime, type Milliseconds, Time } from "@/utils/time";
18
18
 
19
- export interface CellStatusComponentProps extends Pick<
20
- CellRuntimeState,
21
- "status" | "runStartTimestamp" | "interrupted" | "lastRunStartTimestamp"
22
- > {
19
+ export interface CellStatusComponentProps {
23
20
  editing: boolean;
24
21
  edited: boolean;
25
22
  disabled: boolean;
23
+ /** Whether a cell it depends on is disabled. */
24
+ blocked: boolean;
26
25
  staleInputs: boolean;
27
- elapsedTime: number | null;
26
+ status: CellRunStatus;
27
+ /** Whether the cell's last run was interrupted. */
28
+ interrupted: boolean;
29
+ /** How long the last run took, once it has ended. */
30
+ elapsedTime: Milliseconds | null;
31
+ /** When the current run started, or the last one, since the epoch. */
32
+ startedAt: Milliseconds | null;
28
33
  uninstantiated: boolean;
29
34
  }
30
35
 
@@ -32,20 +37,21 @@ export const CellStatusComponent: React.FC<CellStatusComponentProps> = ({
32
37
  editing,
33
38
  status,
34
39
  disabled,
40
+ blocked,
35
41
  staleInputs,
36
42
  edited,
37
43
  interrupted,
38
44
  elapsedTime,
39
- runStartTimestamp,
40
- lastRunStartTimestamp,
45
+ startedAt,
41
46
  uninstantiated,
42
47
  }) => {
43
48
  if (!editing) {
44
49
  return null;
45
50
  }
46
51
 
47
- const start = runStartTimestamp ?? lastRunStartTimestamp;
48
- const lastRanTime = start ? <LastRanTime lastRanTime={start} /> : null;
52
+ const lastRanTime = startedAt ? (
53
+ <LastRanTime lastRanTime={startedAt} />
54
+ ) : null;
49
55
 
50
56
  // unexpected states
51
57
  if (disabled && status === "running") {
@@ -103,7 +109,7 @@ export const CellStatusComponent: React.FC<CellStatusComponentProps> = ({
103
109
  }
104
110
 
105
111
  // disabled from parent
106
- if (!staleInputs && status === "disabled-transitively") {
112
+ if (!staleInputs && blocked) {
107
113
  return (
108
114
  <Tooltip
109
115
  content={
@@ -131,7 +137,7 @@ export const CellStatusComponent: React.FC<CellStatusComponentProps> = ({
131
137
  }
132
138
 
133
139
  // stale from parent being disabled
134
- if (staleInputs && status === "disabled-transitively") {
140
+ if (staleInputs && blocked) {
135
141
  return (
136
142
  <Tooltip
137
143
  content={
@@ -175,9 +181,7 @@ export const CellStatusComponent: React.FC<CellStatusComponentProps> = ({
175
181
  data-testid="cell-status"
176
182
  data-status="running"
177
183
  >
178
- <CellTimer
179
- startTime={Time.fromSeconds(runStartTimestamp) || Time.now()}
180
- />
184
+ <CellTimer startTime={startedAt ?? Time.now().toMilliseconds()} />
181
185
  </div>
182
186
  </Tooltip>
183
187
  );
@@ -307,7 +311,7 @@ export const ElapsedTime = (props: { elapsedTime: string }) => {
307
311
  );
308
312
  };
309
313
  const LastRanTime = (props: { lastRanTime: number }) => {
310
- const date = new Date(props.lastRanTime * 1000);
314
+ const date = new Date(props.lastRanTime);
311
315
  const today = new Date();
312
316
 
313
317
  // Looks like HH:MM:SS.SSS AM/PM
@@ -348,7 +352,7 @@ const LastRanTime = (props: { lastRanTime: number }) => {
348
352
  );
349
353
  };
350
354
 
351
- const CellTimer = (props: { startTime: Time }) => {
352
- const time = useElapsedTime(props.startTime.toMilliseconds());
355
+ const CellTimer = (props: { startTime: Milliseconds }) => {
356
+ const time = useElapsedTime(props.startTime);
353
357
  return <span>{formatElapsedTime(time)}</span>;
354
358
  };