@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61

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 (171) hide show
  1. package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
  2. package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
  3. package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
  4. package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
  5. package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
  6. package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
  7. package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
  8. package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
  9. package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
  10. package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
  11. package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
  12. package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
  13. package/dist/main.js +3630 -1801
  14. package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
  15. package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
  16. package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
  17. package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
  18. package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
  19. package/dist/style.css +1 -1
  20. package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
  21. package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
  22. package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
  23. package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
  24. package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
  25. package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
  26. package/package.json +4 -4
  27. package/src/__mocks__/requests.ts +1 -0
  28. package/src/__tests__/CellStatus.test.tsx +1 -2
  29. package/src/components/app-config/user-config-form.tsx +52 -0
  30. package/src/components/chat/acp/agent-panel.tsx +35 -1
  31. package/src/components/chat/chat-panel.tsx +68 -29
  32. package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
  33. package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
  34. package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
  35. package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
  36. package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
  37. package/src/components/data-table/charts/charts.tsx +23 -1
  38. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
  39. package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
  40. package/src/components/debugger/debugger-code.tsx +7 -2
  41. package/src/components/editor/actions/useNotebookActions.tsx +2 -2
  42. package/src/components/editor/cell/CellStatus.tsx +1 -20
  43. package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
  44. package/src/components/editor/cell/cell-context-menu.tsx +2 -2
  45. package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
  46. package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
  47. package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
  48. package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
  49. package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
  50. package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
  51. package/src/components/editor/code/readonly-python-code.tsx +60 -33
  52. package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
  53. package/src/components/editor/errors/auto-fix.tsx +88 -34
  54. package/src/components/editor/errors/fix-mode.ts +1 -1
  55. package/src/components/editor/notebook-cell.tsx +7 -0
  56. package/src/components/editor/output/ImageOutput.tsx +20 -6
  57. package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
  58. package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
  59. package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
  60. package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
  61. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
  62. package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
  63. package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
  64. package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
  65. package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
  66. package/src/components/slides/__tests__/slide.test.tsx +33 -0
  67. package/src/components/slides/minimap.tsx +7 -1
  68. package/src/components/slides/reveal-component.tsx +3 -0
  69. package/src/components/slides/slide-cell-view.tsx +10 -6
  70. package/src/components/slides/slide.tsx +16 -13
  71. package/src/components/tracing/tracing.tsx +2 -1
  72. package/src/core/ai/config.ts +2 -2
  73. package/src/core/ai/context/__tests__/registry.test.ts +133 -3
  74. package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
  75. package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
  76. package/src/core/ai/context/providers/datasource.ts +27 -2
  77. package/src/core/ai/context/providers/error.ts +226 -36
  78. package/src/core/ai/context/registry.ts +77 -43
  79. package/src/core/ai/state.ts +11 -0
  80. package/src/core/cells/__tests__/cell.test.ts +39 -0
  81. package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
  82. package/src/core/cells/cell.ts +5 -3
  83. package/src/core/cells/readonly-code-display.ts +35 -0
  84. package/src/core/codemirror/__tests__/setup.test.ts +33 -1
  85. package/src/core/codemirror/ai/resources.ts +15 -10
  86. package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
  87. package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
  88. package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
  89. package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
  90. package/src/core/codemirror/cells/debugger-state.ts +133 -0
  91. package/src/core/codemirror/cells/extensions.ts +32 -4
  92. package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
  93. package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
  94. package/src/core/codemirror/go-to-definition/extension.ts +3 -3
  95. package/src/core/codemirror/go-to-definition/underline.ts +5 -14
  96. package/src/core/codemirror/go-to-definition/utils.ts +49 -1
  97. package/src/core/codemirror/utils.ts +15 -0
  98. package/src/core/config/__tests__/config-schema.test.ts +17 -0
  99. package/src/core/config/config-schema.ts +1 -0
  100. package/src/core/config/config.ts +10 -0
  101. package/src/core/config/feature-flag.tsx +4 -0
  102. package/src/core/edit-app.tsx +8 -5
  103. package/src/core/errors/errors.ts +2 -1
  104. package/src/core/islands/bootstrap.ts +4 -3
  105. package/src/core/islands/bridge.ts +1 -0
  106. package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
  107. package/src/core/lifecycle/render-policy.ts +125 -0
  108. package/src/core/mime.ts +11 -4
  109. package/src/core/network/__tests__/api.test.ts +17 -0
  110. package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
  111. package/src/core/network/api.ts +17 -6
  112. package/src/core/network/requests-lazy.ts +1 -0
  113. package/src/core/network/requests-network.ts +8 -0
  114. package/src/core/network/requests-static.ts +1 -0
  115. package/src/core/network/requests-toasting.tsx +10 -1
  116. package/src/core/network/types.ts +2 -0
  117. package/src/core/run-app.tsx +27 -23
  118. package/src/core/runtime/__tests__/adapter.test.ts +160 -0
  119. package/src/core/runtime/__tests__/runtime.test.ts +45 -0
  120. package/src/core/runtime/adapter.ts +182 -0
  121. package/src/core/runtime/runtime.ts +30 -16
  122. package/src/core/wasm/PyodideLoader.tsx +20 -62
  123. package/src/core/wasm/bridge.ts +13 -4
  124. package/src/core/wasm/state.ts +8 -19
  125. package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
  126. package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
  127. package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
  128. package/src/core/websocket/transports/sse.ts +308 -0
  129. package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
  130. package/src/core/websocket/useWebSocket.tsx +12 -2
  131. package/src/css/app/codemirror.css +14 -0
  132. package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
  133. package/src/hooks/useDelayElapsed.ts +27 -0
  134. package/src/mount.tsx +3 -3
  135. package/src/plugins/core/RenderHTML.tsx +35 -1
  136. package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
  137. package/src/plugins/impl/DataTablePlugin.tsx +1 -0
  138. package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
  139. package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
  140. package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
  141. package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
  142. package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
  143. package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
  144. package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
  145. package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
  146. package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
  147. package/src/plugins/impl/anywidget/host.ts +54 -0
  148. package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
  149. package/src/plugins/impl/anywidget/model.ts +59 -239
  150. package/src/plugins/impl/anywidget/registry.ts +268 -0
  151. package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
  152. package/src/plugins/impl/anywidget/runtime.ts +422 -0
  153. package/src/plugins/impl/anywidget/types.ts +14 -0
  154. package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
  155. package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
  156. package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
  157. package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
  158. package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
  159. package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
  160. package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
  161. package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
  162. package/src/utils/errors.ts +15 -0
  163. package/src/utils/time.ts +20 -0
  164. package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
  165. package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
  166. package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
  167. package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
  168. package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
  169. package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
  170. package/src/core/wasm/__tests__/state.test.ts +0 -124
  171. package/src/plugins/impl/anywidget/schemas.ts +0 -32
@@ -1,26 +1,133 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
3
  import type { Completion } from "@codemirror/autocomplete";
4
- import { cellErrorsAtom } from "@/core/cells/cells";
4
+ import { notebookAtom } from "@/core/cells/cells";
5
5
  import type { CellId } from "@/core/cells/ids";
6
- import type { MarimoError } from "@/core/kernel/messages";
6
+ import { displayCellName } from "@/core/cells/names";
7
+ import type { MarimoError, OutputMessage } from "@/core/kernel/messages";
8
+ import { isErrorMime, isMarimoErrorsMime, isTracebackMime } from "@/core/mime";
7
9
  import type { JotaiStore } from "@/core/state/jotai";
8
10
  import { logNever } from "@/utils/assertNever";
11
+ import { parseHtmlContent } from "@/utils/dom";
9
12
  import { PluralWord } from "@/utils/pluralize";
10
13
  import { type AIContextItem, AIContextProvider } from "../registry";
11
14
  import { contextToXml } from "../utils";
12
15
  import { Sections } from "./common";
16
+ import { formatDatasourceContextForCell } from "./datasource";
17
+
18
+ export interface CellErrorEntry {
19
+ cellId: CellId;
20
+ cellName: string;
21
+ cellCode: string;
22
+ errorData: MarimoError[];
23
+ tracebackHtml?: string;
24
+ }
25
+
26
+ export type ErrorContextItemData =
27
+ | {
28
+ type: "all-errors";
29
+ errors: CellErrorEntry[];
30
+ }
31
+ | {
32
+ type: "cell-error";
33
+ error: CellErrorEntry;
34
+ };
13
35
 
14
36
  export interface ErrorContextItem extends AIContextItem {
15
37
  type: "error";
16
- data: {
17
- type: "all-errors";
18
- errors: {
19
- cellId: CellId;
20
- cellName: string;
21
- errorData: MarimoError[];
22
- }[];
23
- };
38
+ data: ErrorContextItemData;
39
+ }
40
+
41
+ function parseCellErrorOutput(
42
+ output: OutputMessage,
43
+ ): Pick<CellErrorEntry, "errorData" | "tracebackHtml"> | null {
44
+ if (isMarimoErrorsMime(output.mimetype)) {
45
+ if (!Array.isArray(output.data)) {
46
+ return null;
47
+ }
48
+ const errorData = output.data.filter(
49
+ (error) => !error.type.includes("ancestor"),
50
+ );
51
+ if (errorData.length === 0) {
52
+ return null;
53
+ }
54
+ return { errorData };
55
+ }
56
+
57
+ if (isTracebackMime(output.mimetype)) {
58
+ if (typeof output.data !== "string" || output.data.length === 0) {
59
+ return null;
60
+ }
61
+ return { errorData: [], tracebackHtml: output.data };
62
+ }
63
+
64
+ return null;
65
+ }
66
+
67
+ function errorDataHasTraceback(errorData: MarimoError[]): boolean {
68
+ return errorData.some((error) => "traceback" in error && error.traceback);
69
+ }
70
+
71
+ function getTracebackFromConsole(
72
+ consoleOutputs: OutputMessage[] | undefined,
73
+ ): string | undefined {
74
+ const tracebackOutput = consoleOutputs?.find((output) =>
75
+ isTracebackMime(output.mimetype),
76
+ );
77
+ if (
78
+ !tracebackOutput ||
79
+ typeof tracebackOutput.data !== "string" ||
80
+ tracebackOutput.data.length === 0
81
+ ) {
82
+ return undefined;
83
+ }
84
+ return tracebackOutput.data;
85
+ }
86
+
87
+ function resolveTracebackHtml(
88
+ parsed: Pick<CellErrorEntry, "errorData" | "tracebackHtml">,
89
+ consoleOutputs: OutputMessage[] | undefined,
90
+ ): string | undefined {
91
+ if (parsed.tracebackHtml) {
92
+ return parsed.tracebackHtml;
93
+ }
94
+ if (errorDataHasTraceback(parsed.errorData)) {
95
+ return undefined;
96
+ }
97
+ return getTracebackFromConsole(consoleOutputs);
98
+ }
99
+
100
+ export function getCellErrorEntries(store: JotaiStore): CellErrorEntry[] {
101
+ const { cellIds, cellRuntime, cellData } = store.get(notebookAtom);
102
+ const entries: CellErrorEntry[] = [];
103
+
104
+ for (const [cellIndex, cellId] of cellIds.inOrderIds.entries()) {
105
+ const cell = cellRuntime[cellId];
106
+ const output = cell.output;
107
+ if (!output || !isErrorMime(output.mimetype)) {
108
+ continue;
109
+ }
110
+
111
+ const parsed = parseCellErrorOutput(output);
112
+ if (!parsed) {
113
+ continue;
114
+ }
115
+
116
+ const cellName = displayCellName(cellData[cellId].name, cellIndex);
117
+ // Prefer the code from the last execution: runtime errors correspond to
118
+ // the previous run, while `code` may already contain unsaved edits.
119
+ const cellCode = cellData[cellId].lastCodeRun ?? cellData[cellId].code;
120
+
121
+ entries.push({
122
+ cellId,
123
+ cellName,
124
+ cellCode,
125
+ errorData: parsed.errorData,
126
+ tracebackHtml: resolveTracebackHtml(parsed, cell.consoleOutputs),
127
+ });
128
+ }
129
+
130
+ return entries;
24
131
  }
25
132
 
26
133
  function describeError(error: MarimoError): string {
@@ -67,6 +174,77 @@ function describeError(error: MarimoError): string {
67
174
  return "Unknown error";
68
175
  }
69
176
 
177
+ function formatSingleError(error: MarimoError): string {
178
+ let text = describeError(error);
179
+ if ("traceback" in error && error.traceback) {
180
+ text += `\n\nTraceback:\n${parseHtmlContent(error.traceback)}`;
181
+ }
182
+ return text;
183
+ }
184
+
185
+ function formatCellErrorDetails(
186
+ entry: CellErrorEntry,
187
+ store: JotaiStore,
188
+ ): string {
189
+ const parts: string[] = [`Code:\n${entry.cellCode}`];
190
+
191
+ if (entry.tracebackHtml) {
192
+ parts.push(`Traceback:\n${parseHtmlContent(entry.tracebackHtml)}`);
193
+ }
194
+
195
+ if (entry.errorData.length > 0) {
196
+ parts.push(
197
+ entry.errorData.map((error) => formatSingleError(error)).join("\n\n"),
198
+ );
199
+ }
200
+
201
+ if (entry.errorData.some((error) => error.type === "sql-error")) {
202
+ const datasourceContext = formatDatasourceContextForCell(
203
+ entry.cellId,
204
+ store,
205
+ );
206
+ if (datasourceContext) {
207
+ parts.push(`Database schema:\n${datasourceContext}`);
208
+ }
209
+ }
210
+
211
+ return parts.join("\n\n");
212
+ }
213
+
214
+ function formatCellErrorXml(entry: CellErrorEntry, store: JotaiStore): string {
215
+ return contextToXml({
216
+ type: "error",
217
+ data: {
218
+ name: entry.cellName || `Cell ${entry.cellId}`,
219
+ cellId: entry.cellId,
220
+ },
221
+ details: formatCellErrorDetails(entry, store),
222
+ });
223
+ }
224
+
225
+ function summarizeCellError(entry: CellErrorEntry): string {
226
+ if (entry.errorData.length === 1) {
227
+ return describeError(entry.errorData[0]);
228
+ }
229
+ if (entry.errorData.length > 1) {
230
+ return `${entry.errorData.length} errors`;
231
+ }
232
+ if (entry.tracebackHtml) {
233
+ const text = parseHtmlContent(entry.tracebackHtml).trim();
234
+ const firstLine = text
235
+ .split("\n")
236
+ .find((line) => line.trim())
237
+ ?.trim();
238
+ return firstLine || "traceback";
239
+ }
240
+ return "error";
241
+ }
242
+
243
+ function errorContextName(entry: CellErrorEntry): string {
244
+ const cellName = entry.cellName || `Cell ${entry.cellId}`;
245
+ return `Error: ${cellName}`;
246
+ }
247
+
70
248
  const errorsTxt = new PluralWord("error", "errors");
71
249
  export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
72
250
  readonly title = "Errors";
@@ -80,39 +258,50 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
80
258
  }
81
259
 
82
260
  getItems(): ErrorContextItem[] {
83
- const errors = this.store.get(cellErrorsAtom);
261
+ const errors = getCellErrorEntries(this.store);
262
+
84
263
  if (errors.length === 0) {
85
264
  return [];
86
265
  }
87
266
 
88
- return [
267
+ const items: ErrorContextItem[] = [
89
268
  {
90
269
  uri: this.asURI("all"),
91
270
  name: "Errors",
92
271
  type: this.contextType,
93
272
  data: {
94
273
  type: "all-errors",
95
- errors: errors.map((error) => ({
96
- cellId: error.cellId,
97
- cellName: error.cellName,
98
- errorData: error.output.data,
99
- })),
274
+ errors,
100
275
  },
101
276
  description: "All errors in the notebook",
102
277
  },
103
278
  ];
104
279
 
105
- // TODO: maybe handle single errors or grouped by types
280
+ for (const error of errors) {
281
+ items.push({
282
+ uri: this.asURI(error.cellId),
283
+ name: errorContextName(error),
284
+ type: this.contextType,
285
+ data: {
286
+ type: "cell-error",
287
+ error,
288
+ },
289
+ description: summarizeCellError(error),
290
+ });
291
+ }
292
+
293
+ return items;
106
294
  }
107
295
 
108
296
  formatCompletion(item: ErrorContextItem): Completion {
109
297
  if (item.data.type === "all-errors") {
298
+ const errorCount = item.data.errors.length;
110
299
  return {
111
300
  label: "@Errors",
112
301
  displayLabel: "Errors",
113
- detail: `${item.data.errors.length} ${errorsTxt.pluralize(item.data.errors.length)}`,
302
+ detail: `${errorCount} ${errorsTxt.pluralize(errorCount)}`,
114
303
  type: "error",
115
- apply: "@Errors",
304
+ apply: "@error://all",
116
305
  section: Sections.ERROR,
117
306
  info: () => {
118
307
  const infoContainer = document.createElement("div");
@@ -136,7 +325,7 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
136
325
 
137
326
  const descriptionDiv = document.createElement("div");
138
327
  descriptionDiv.classList.add("text-sm", "text-muted-foreground");
139
- descriptionDiv.textContent = `${item.data.errors.length} ${errorsTxt.pluralize(item.data.errors.length)}`;
328
+ descriptionDiv.textContent = `${errorCount} ${errorsTxt.pluralize(errorCount)}`;
140
329
  headerDiv.append(descriptionDiv);
141
330
 
142
331
  infoContainer.append(headerDiv);
@@ -146,6 +335,17 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
146
335
  };
147
336
  }
148
337
 
338
+ if (item.data.type === "cell-error") {
339
+ const { error } = item.data;
340
+ return {
341
+ ...this.createBasicCompletion(item, {
342
+ detail: summarizeCellError(error),
343
+ type: "error",
344
+ }),
345
+ section: Sections.ERROR,
346
+ };
347
+ }
348
+
149
349
  return {
150
350
  label: "Error",
151
351
  displayLabel: "Error",
@@ -154,21 +354,11 @@ export class ErrorContextProvider extends AIContextProvider<ErrorContextItem> {
154
354
  }
155
355
 
156
356
  formatContext(item: ErrorContextItem): string {
157
- const { data } = item;
158
-
159
- const xmls = data.errors.map((err) => {
160
- return contextToXml({
161
- type: this.contextType,
162
- data: {
163
- name: err.cellName || `Cell ${err.cellId}`,
164
- description: err.errorData
165
- .map((err) => describeError(err))
166
- .join("\n"),
167
- },
168
- });
169
- });
357
+ const entries =
358
+ item.data.type === "all-errors" ? item.data.errors : [item.data.error];
170
359
 
171
- const xml = xmls.join("\n\n");
172
- return xml;
360
+ return entries
361
+ .map((entry) => formatCellErrorXml(entry, this.store))
362
+ .join("\n\n");
173
363
  }
174
364
  }
@@ -9,11 +9,19 @@ import { MultiMap } from "@/utils/multi-map";
9
9
  import type { TypedString } from "@/utils/typed";
10
10
 
11
11
  /**
12
- * Unique identifier for a context item in the format "type:id"
12
+ * Unique identifier for a context item in the format "type://id"
13
13
  * e.g., "variable://my_var", "data://users", "file://config.py"
14
14
  */
15
15
  export type ContextLocatorId = TypedString<"ContextLocatorId">;
16
16
 
17
+ function parseContextType(uri: ContextLocatorId): string | undefined {
18
+ const separator = uri.indexOf("://");
19
+ if (separator === -1) {
20
+ return undefined;
21
+ }
22
+ return uri.slice(0, separator);
23
+ }
24
+
17
25
  /**
18
26
  * Base interface for context items that can be mentioned in AI prompts
19
27
  */
@@ -142,40 +150,80 @@ export class AIContextRegistry<T extends AIContextItem> {
142
150
  );
143
151
  }
144
152
 
153
+ private findProviderForUri(
154
+ uri: ContextLocatorId,
155
+ ): AIContextProvider | undefined {
156
+ const type = parseContextType(uri);
157
+ if (!type) {
158
+ return undefined;
159
+ }
160
+ return [...this.providers].find(
161
+ (provider) =>
162
+ provider.contextType === type &&
163
+ provider.getItems().some((item) => item.uri === uri),
164
+ );
165
+ }
166
+
145
167
  /**
146
- * Get context information for mentioned items
168
+ * Resolve only the requested context items, querying each matching provider
147
169
  */
148
- getContextInfo(contextIds: ContextLocatorId[]): T[] {
149
- const contextInfo: T[] = [];
150
- const allItems = new Map<ContextLocatorId, T>(
151
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
152
- );
170
+ resolveItems(contextIds: ContextLocatorId[]): T[] {
171
+ if (contextIds.length === 0) {
172
+ return [];
173
+ }
174
+
175
+ const idsByType = new MultiMap<string, ContextLocatorId>();
176
+ for (const contextId of contextIds) {
177
+ const type = parseContextType(contextId);
178
+ if (type) {
179
+ idsByType.add(type, contextId);
180
+ }
181
+ }
182
+
183
+ const itemsById = new Map<ContextLocatorId, T>();
184
+ for (const [type, ids] of idsByType.entries()) {
185
+ const providers = [...this.providers].filter(
186
+ (provider) => provider.contextType === type,
187
+ );
188
+ for (const provider of providers) {
189
+ const itemsByUri = new Map<ContextLocatorId, T>(
190
+ provider
191
+ .getItems()
192
+ .map((item) => [item.uri as ContextLocatorId, item as T]),
193
+ );
194
+ for (const id of ids) {
195
+ const item = itemsByUri.get(id);
196
+ if (item) {
197
+ itemsById.set(id, item);
198
+ }
199
+ }
200
+ }
201
+ }
153
202
 
203
+ // Preserve the order in which the ids were requested, so formatted context
204
+ // matches the order the user mentioned them in the prompt.
205
+ const results: T[] = [];
154
206
  for (const contextId of contextIds) {
155
- const item = allItems.get(contextId);
207
+ const item = itemsById.get(contextId);
156
208
  if (item) {
157
- contextInfo.push(item);
209
+ results.push(item);
158
210
  }
159
211
  }
212
+ return results;
213
+ }
160
214
 
161
- return contextInfo;
215
+ /**
216
+ * Get context information for mentioned items
217
+ */
218
+ getContextInfo(contextIds: ContextLocatorId[]): T[] {
219
+ return this.resolveItems(contextIds);
162
220
  }
163
221
 
164
222
  /**
165
223
  * Format context for AI prompt inclusion
166
224
  */
167
225
  formatContextForAI(contextIds: ContextLocatorId[]): string {
168
- const allItems = new Map<ContextLocatorId, T>(
169
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
170
- );
171
-
172
- const contextInfo: T[] = [];
173
- for (const contextId of contextIds) {
174
- const item = allItems.get(contextId);
175
- if (item) {
176
- contextInfo.push(item);
177
- }
178
- }
226
+ const contextInfo = this.resolveItems(contextIds);
179
227
 
180
228
  if (contextInfo.length === 0) {
181
229
  return "";
@@ -183,7 +231,7 @@ export class AIContextRegistry<T extends AIContextItem> {
183
231
 
184
232
  return contextInfo
185
233
  .map((item) => {
186
- const provider = this.getProvider(item.type);
234
+ const provider = this.findProviderForUri(item.uri as ContextLocatorId);
187
235
  return provider?.formatContext(item) || "";
188
236
  })
189
237
  .join("\n\n");
@@ -195,36 +243,22 @@ export class AIContextRegistry<T extends AIContextItem> {
195
243
  async getAttachmentsForContext(
196
244
  contextIds: ContextLocatorId[],
197
245
  ): Promise<FileUIPart[]> {
198
- const allItems = new Map<ContextLocatorId, T>(
199
- this.getAllItems().map((item) => [item.uri as ContextLocatorId, item]),
200
- );
201
-
202
- const contextInfo: T[] = [];
203
- for (const contextId of contextIds) {
204
- const item = allItems.get(contextId);
205
- if (item) {
206
- contextInfo.push(item);
207
- }
208
- }
246
+ const contextInfo = this.resolveItems(contextIds);
209
247
 
210
248
  if (contextInfo.length === 0) {
211
249
  return [];
212
250
  }
213
251
 
214
- // Group items by provider type to batch attachment requests
215
- const itemsByProvider = new MultiMap<string, T>();
252
+ const itemsByProvider = new MultiMap<AIContextProvider, T>();
216
253
  for (const item of contextInfo) {
217
- const providerType = item.type;
218
- itemsByProvider.add(providerType, item);
254
+ const provider = this.findProviderForUri(item.uri as ContextLocatorId);
255
+ if (provider) {
256
+ itemsByProvider.add(provider, item);
257
+ }
219
258
  }
220
259
 
221
- // Collect attachments from all providers
222
260
  const attachmentPromises = [...itemsByProvider.entries()].map(
223
- async ([providerType, items]) => {
224
- const provider = this.getProvider(providerType);
225
- if (!provider) {
226
- return [];
227
- }
261
+ async ([provider, items]) => {
228
262
  try {
229
263
  return await provider.getAttachments(items);
230
264
  } catch (error) {
@@ -21,6 +21,17 @@ export interface AiCompletionCell {
21
21
 
22
22
  export const aiCompletionCellAtom = atom<AiCompletionCell | null>(null);
23
23
 
24
+ /**
25
+ * A prompt queued to be delivered to the AI assistant panel.
26
+ * The active panel consumes this on mount/when ready, then clears it.
27
+ */
28
+ export interface PendingAiPrompt {
29
+ prompt: string;
30
+ submit: boolean;
31
+ }
32
+
33
+ export const pendingAiPromptAtom = atom<PendingAiPrompt | null>(null);
34
+
24
35
  const INCLUDE_OTHER_CELLS_KEY = "marimo:ai:includeOtherCells";
25
36
  export const includeOtherCellsAtom = atomWithStorage<boolean>(
26
37
  INCLUDE_OTHER_CELLS_KEY,
@@ -218,3 +218,42 @@ describe("transitionCell serialization", () => {
218
218
  );
219
219
  });
220
220
  });
221
+
222
+ describe("transitionCell stdin resolution", () => {
223
+ function stdinOutput(): OutputMessage {
224
+ return {
225
+ channel: "stdin",
226
+ mimetype: "text/plain",
227
+ data: "(Pdb) ",
228
+ timestamp: Date.now() as Seconds,
229
+ } as OutputMessage;
230
+ }
231
+
232
+ it("resolves a dangling pdb prompt when the cell goes idle", () => {
233
+ // A paused-pdb cell shows an unanswered stdin prompt (the debugger box);
234
+ // once it finishes (e.g. after quitting with `q`), the box is removed.
235
+ const cell = createCellRuntimeState({
236
+ status: "running",
237
+ debuggerActive: true,
238
+ consoleOutputs: [stdinOutput()],
239
+ });
240
+ const next = transitionCell(cell, {
241
+ cell_id: "cell-1",
242
+ status: "idle",
243
+ } as CellMessage);
244
+ expect(next.debuggerActive).toBe(false);
245
+ expect(next.consoleOutputs[0].response).toBe("");
246
+ });
247
+
248
+ it("leaves an unanswered prompt interactive while still running", () => {
249
+ const cell = createCellRuntimeState({
250
+ status: "running",
251
+ consoleOutputs: [stdinOutput()],
252
+ });
253
+ const next = transitionCell(cell, {
254
+ cell_id: "cell-1",
255
+ status: null,
256
+ } as CellMessage);
257
+ expect(next.consoleOutputs[0].response).toBeUndefined();
258
+ });
259
+ });
@@ -0,0 +1,46 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { getReadonlyCodeDisplay } from "../readonly-code-display";
5
+
6
+ describe("getReadonlyCodeDisplay", () => {
7
+ it("unwraps SQL cells to their inner query and marks them as sql", () => {
8
+ const result = getReadonlyCodeDisplay(
9
+ 'my_table = mo.sql("""SELECT 1 AS id""")',
10
+ );
11
+ expect(result.language).toBe("sql");
12
+ expect(result.code).toBe("SELECT 1 AS id");
13
+ });
14
+
15
+ it("unwraps f-string SQL cells", () => {
16
+ const result = getReadonlyCodeDisplay(
17
+ 'my_table = mo.sql(f"""SELECT 1 AS id""")',
18
+ );
19
+ expect(result.language).toBe("sql");
20
+ expect(result.code).toBe("SELECT 1 AS id");
21
+ });
22
+
23
+ it("leaves plain Python cells untouched", () => {
24
+ const code = "x = 1 + 2\nprint(x)";
25
+ const result = getReadonlyCodeDisplay(code);
26
+ expect(result.language).toBe("python");
27
+ expect(result.code).toBe(code);
28
+ });
29
+
30
+ it("unwraps markdown cells to their inner content", () => {
31
+ const result = getReadonlyCodeDisplay('mo.md("""## Heading""")');
32
+ expect(result.language).toBe("markdown");
33
+ expect(result.code).toBe("## Heading");
34
+ });
35
+
36
+ it("treats empty or whitespace-only code as python", () => {
37
+ expect(getReadonlyCodeDisplay("")).toEqual({
38
+ code: "",
39
+ language: "python",
40
+ });
41
+ expect(getReadonlyCodeDisplay(" \n\t ")).toEqual({
42
+ code: " \n\t ",
43
+ language: "python",
44
+ });
45
+ });
46
+ });
@@ -118,9 +118,11 @@ export function transitionCell(
118
118
  // Coalesce console outputs, which are streamed during execution.
119
119
  let consoleOutputs = cell.consoleOutputs;
120
120
 
121
- // If interrupted on the incoming message,
122
- // remove the debugger and resolve all stdin for previous console outputs
123
- if (didInterruptFromThisMessage) {
121
+ // When the cell is interrupted or finishes (idle), there is no live
122
+ // stdin/pdb session, so resolve any dangling stdin prompt. This removes the
123
+ // debugger box — e.g. after quitting pdb with `q`, which stops the cell
124
+ // (idle) rather than interrupting it.
125
+ if (didInterruptFromThisMessage || message.status === "idle") {
124
126
  nextCell.debuggerActive = false;
125
127
  consoleOutputs = consoleOutputs.map((output) => {
126
128
  if (output.channel === "stdin") {
@@ -0,0 +1,35 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { MarkdownParser, SQLParser } from "@marimo-team/smart-cells";
4
+ import type { LanguageAdapterType } from "@/core/codemirror/language/types";
5
+
6
+ export interface ReadonlyCodeDisplay {
7
+ code: string;
8
+ language: LanguageAdapterType;
9
+ }
10
+
11
+ const markdownParser = new MarkdownParser();
12
+ const sqlParser = new SQLParser();
13
+
14
+ /**
15
+ * Unwrap SQL and markdown cells so read-only views show inner content with the
16
+ * correct syntax highlighting instead of the raw Python wrapper.
17
+ */
18
+ export function getReadonlyCodeDisplay(code: string): ReadonlyCodeDisplay {
19
+ const trimmed = code.trim();
20
+ if (!trimmed) {
21
+ return { code, language: "python" };
22
+ }
23
+
24
+ if (markdownParser.isSupported(trimmed)) {
25
+ return {
26
+ code: markdownParser.transformIn(trimmed).code,
27
+ language: "markdown",
28
+ };
29
+ }
30
+
31
+ if (sqlParser.isSupported(trimmed)) {
32
+ return { code: sqlParser.transformIn(trimmed).code, language: "sql" };
33
+ }
34
+ return { code, language: "python" };
35
+ }