@marimo-team/frontend 0.25.3-dev95 → 0.25.3-dev97

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 (118) hide show
  1. package/dist/assets/{CellStatus-CplYFNTy.js → CellStatus-fK4n2NPc.js} +1 -1
  2. package/dist/assets/{ErrorBoundary-CdGuz4a_.js → ErrorBoundary-CAEGdoFW.js} +2 -2
  3. package/dist/assets/{JsonOutput-Csbov8Fx.js → JsonOutput-CyAKmZDD.js} +1 -1
  4. package/dist/assets/{MarimoErrorOutput-czclLlOn.js → MarimoErrorOutput-CgZvP0p0.js} +1 -1
  5. package/dist/assets/{RenderHTML-CNDedpS4.js → RenderHTML-DQvxDDNS.js} +1 -1
  6. package/dist/assets/{RunButton-CRfdOxVd.js → RunButton-BBDnDL6G.js} +1 -1
  7. package/dist/assets/{add-cell-with-ai-BAIX7r0P.js → add-cell-with-ai-Clg5FELx.js} +2 -2
  8. package/dist/assets/{add-connection-dialog-DrDuM-tI.js → add-connection-dialog-BqdWmD-7.js} +1 -1
  9. package/dist/assets/{agent-panel-CoXq_rlA.js → agent-panel-5RxG5Y82.js} +3 -3
  10. package/dist/assets/{ai-model-dropdown-COvWn46z.js → ai-model-dropdown-Bvl1ktQq.js} +2 -2
  11. package/dist/assets/{app-config-button-CtxoYG4F.js → app-config-button-0QTeQ2Mv.js} +1 -1
  12. package/dist/assets/{blob-DCjrjZKW.js → blob-5VcB8K_t.js} +1 -1
  13. package/dist/assets/{cache-panel-CbWZcBu0.js → cache-panel-BV-c7ed0.js} +1 -1
  14. package/dist/assets/{cell-editor-BnL0Sorl.js → cell-editor-Byw5y-oI.js} +2 -2
  15. package/dist/assets/{cell-link-BFbgKiRH.js → cell-link-C5yYyzuF.js} +1 -1
  16. package/dist/assets/{cells-yOs72eZm.js → cells-D_11MjVB.js} +13 -13
  17. package/dist/assets/{chat-display-DwRM41t_.js → chat-display-BCR_ySiV.js} +1 -1
  18. package/dist/assets/{chat-panel-DeTEdjxF.js → chat-panel-RPRvBsaU.js} +2 -2
  19. package/dist/assets/{chat-ui-D6XucD07.js → chat-ui-CGviVaRA.js} +3 -3
  20. package/dist/assets/column-preview-xe533-Cn.js +1 -0
  21. package/dist/assets/{command-palette-CA6yfNLb.js → command-palette-BB9bWdWp.js} +1 -1
  22. package/dist/assets/{common-B-ZYnO1x.js → common-CaeItbcI.js} +1 -1
  23. package/dist/assets/{components-vu2jLG08.js → components-G6frbw0c.js} +1 -1
  24. package/dist/assets/{datasource-BakEOWsf.js → datasource-deuXnlU-.js} +1 -1
  25. package/dist/assets/{dependency-graph-panel-DCGB3_id.js → dependency-graph-panel-CIykLnJe.js} +2 -2
  26. package/dist/assets/{documentation-panel-CpGZ2Dfn.js → documentation-panel-2PQ8kQLa.js} +1 -1
  27. package/dist/assets/{download-CV1k4XQ4.js → download-dK52bLxZ.js} +1 -1
  28. package/dist/assets/{edit-page-By7n2M84.js → edit-page-C2phWLKH.js} +6 -6
  29. package/dist/assets/{error-entries-JgrZYMbe.js → error-entries-3GVk2KjS.js} +1 -1
  30. package/dist/assets/{error-panel-DS7HWph-.js → error-panel-CzDRGPeC.js} +1 -1
  31. package/dist/assets/{feedback-button-eYsTCK-g.js → feedback-button-C5NgWpFg.js} +1 -1
  32. package/dist/assets/feedback-button-CONwLFpw.js +1 -0
  33. package/dist/assets/{file-explorer-panel-BTLwwwD4.js → file-explorer-panel-B50xDnTZ.js} +1 -1
  34. package/dist/assets/{file-name-input-BaR4h2Ud.js → file-name-input-DqwMSgGU.js} +1 -1
  35. package/dist/assets/{floating-outline-Drd73Quo.js → floating-outline-ChmAuvxz.js} +1 -1
  36. package/dist/assets/{gallery-page-DXUD11Rm.js → gallery-page-CiL8aMS1.js} +1 -1
  37. package/dist/assets/{glide-data-editor-DqBBoQBl.js → glide-data-editor-CqRclBkp.js} +1 -1
  38. package/dist/assets/{home-page-pgFFsmga.js → home-page-BgZVl4dP.js} +1 -1
  39. package/dist/assets/{hooks-D25kP4Sr.js → hooks-DM5n7Osw.js} +1 -1
  40. package/dist/assets/index-vCxv6WVQ.js +30 -0
  41. package/dist/assets/{layout-DLHIGZTK.js → layout-BpP8nTjb.js} +4 -4
  42. package/dist/assets/{logs-panel-D5AAD2s9.js → logs-panel-B5eVAZtH.js} +1 -1
  43. package/dist/assets/{markdown-renderer--CwVm4kJ.js → markdown-renderer-BWUzqkJM.js} +1 -1
  44. package/dist/assets/{name-cell-input-BFKZaNI5.js → name-cell-input-DXW68gRW.js} +1 -1
  45. package/dist/assets/{outline-panel-qcwRnl0R.js → outline-panel-DPxJhJ_A.js} +1 -1
  46. package/dist/assets/{packages-panel-DnSryNcz.js → packages-panel-gBE2FWQr.js} +1 -1
  47. package/dist/assets/{panel-accordion-state-BwJpDMVt.js → panel-accordion-state-CCF5Gxnc.js} +1 -1
  48. package/dist/assets/{panels-Aqzwb3UR.js → panels-BQYMF6p_.js} +1 -1
  49. package/dist/assets/{process-output-DuUfHYSr.js → process-output-Cw9IDcKQ.js} +1 -1
  50. package/dist/assets/{readonly-code-display-BY-9I_Yk.js → readonly-code-display-DYcZQ28T.js} +1 -1
  51. package/dist/assets/{readonly-python-code-DSyr-yE2.js → readonly-python-code-RFuQP50v.js} +1 -1
  52. package/dist/assets/{reveal-component-C_Lb7Mom.js → reveal-component-yRrDmCQz.js} +2 -2
  53. package/dist/assets/{run-page-ZTw-kCU-.js → run-page-CY65WUOu.js} +1 -1
  54. package/dist/assets/{runtime-details-state-B7gpnuXx.js → runtime-details-state-pobtu6TO.js} +1 -1
  55. package/dist/assets/scratchpad-panel-H44_pvb_.js +1 -0
  56. package/dist/assets/session-panel-Bnd1iXkO.js +1 -0
  57. package/dist/assets/{snippets-panel-BNadUJLU.js → snippets-panel-DKApxKx9.js} +1 -1
  58. package/dist/assets/{state-BaYudep3.js → state-Bl9BiY_s.js} +1 -1
  59. package/dist/assets/{state-_1LTGJQM.js → state-Dv059B3A.js} +1 -1
  60. package/dist/assets/{tracing-DbTgS4u9.js → tracing-Clwy4SlW.js} +1 -1
  61. package/dist/assets/{tracing-panel-CjlrpXPy.js → tracing-panel-BwjLG_aC.js} +2 -2
  62. package/dist/assets/{tree-actions-C_KYlaSv.js → tree-actions-BZNTQ12N.js} +1 -1
  63. package/dist/assets/{useAddCell-C0J8quGE.js → useAddCell-Cjuqk2YR.js} +1 -1
  64. package/dist/assets/{useCellActionButton-CbkOj6BE.js → useCellActionButton-MoWLvuGe.js} +1 -1
  65. package/dist/assets/{useDeleteCell-DHkF5wgS.js → useDeleteCell-BC-MW7d1.js} +1 -1
  66. package/dist/assets/{useDependencyPanelTab--yXJW6X5.js → useDependencyPanelTab-CZU7i-B3.js} +1 -1
  67. package/dist/assets/{useNotebookActions-dJQalORg.js → useNotebookActions-wFxVdRXQ.js} +1 -1
  68. package/dist/assets/{useRunCells-C_IwVJR_.js → useRunCells-ByaWxoIt.js} +1 -1
  69. package/dist/assets/{useSplitCell-Ci4MEUj8.js → useSplitCell-YJIystwv.js} +1 -1
  70. package/dist/index.html +20 -20
  71. package/package.json +1 -1
  72. package/src/components/app-config/ai-config.tsx +1 -0
  73. package/src/components/chat/acp/blocks.tsx +2 -2
  74. package/src/components/chat/chat-components.tsx +2 -0
  75. package/src/components/chat/chat-panel.tsx +6 -1
  76. package/src/components/data-table/__tests__/column-explorer.test.tsx +27 -1
  77. package/src/components/data-table/charts/__tests__/chart-panel.test.tsx +74 -1
  78. package/src/components/data-table/charts/__tests__/storage.test.ts +4 -1
  79. package/src/components/data-table/charts/charts.tsx +21 -6
  80. package/src/components/data-table/charts/storage.ts +16 -5
  81. package/src/components/data-table/column-explorer-panel/column-explorer.tsx +1 -1
  82. package/src/components/datasources/__tests__/column-actions.test.tsx +97 -0
  83. package/src/components/datasources/__tests__/column-preview.test.tsx +63 -2
  84. package/src/components/datasources/column-preview.tsx +8 -2
  85. package/src/components/datasources/datasources.tsx +12 -14
  86. package/src/components/debugger/debugger-code.tsx +5 -0
  87. package/src/components/dependency-graph/panels.tsx +2 -1
  88. package/src/components/editor/ai/ai-completion-editor.tsx +1 -0
  89. package/src/components/editor/alerts/startup-logs-alert.tsx +1 -0
  90. package/src/components/editor/cell/DeleteButton.tsx +1 -0
  91. package/src/components/editor/chrome/panels/cache-panel.tsx +1 -0
  92. package/src/components/editor/chrome/wrapper/__tests__/pending-ai-cells.test.tsx +58 -1
  93. package/src/components/editor/chrome/wrapper/pending-ai-cells.tsx +12 -2
  94. package/src/components/editor/notebook-banner.tsx +1 -0
  95. package/src/components/editor/output/ansi-reduce.ts +6 -0
  96. package/src/components/editor/output/console/ConsoleOutput.tsx +11 -7
  97. package/src/components/editor/package-alert.tsx +6 -4
  98. package/src/components/editor/renderers/vertical-layout/sidebar/toggle.tsx +7 -1
  99. package/src/components/mcp/mcp-status-indicator.tsx +1 -1
  100. package/src/components/scratchpad/scratchpad.tsx +2 -2
  101. package/src/components/slides/reveal-component.tsx +4 -2
  102. package/src/components/slides/slide-form.tsx +1 -0
  103. package/src/core/cells/__tests__/__snapshots__/cells.test.ts.snap +4 -0
  104. package/src/core/cells/__tests__/cell.test.ts +39 -0
  105. package/src/core/cells/__tests__/cells.test.ts +3 -1
  106. package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +54 -0
  107. package/src/core/cells/cell.ts +14 -3
  108. package/src/core/cells/cells.ts +1 -0
  109. package/src/core/cells/types.ts +10 -1
  110. package/src/core/codemirror/language/panel/panel.tsx +1 -0
  111. package/src/plugins/impl/RefreshPlugin.tsx +1 -0
  112. package/src/plugins/impl/chat/chat-ui.tsx +2 -1
  113. package/src/stories/button.stories.tsx +1 -0
  114. package/dist/assets/column-preview-DXM8SqGY.js +0 -1
  115. package/dist/assets/feedback-button-Dgs_HOOC.js +0 -1
  116. package/dist/assets/index-DNG4qQ-E.js +0 -30
  117. package/dist/assets/scratchpad-panel-D2IHkKoG.js +0 -1
  118. package/dist/assets/session-panel-cOSO3WCX.js +0 -1
@@ -1,16 +1,24 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { render } from "@testing-library/react";
4
- import { Provider } from "jotai";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import { createStore, Provider } from "jotai";
5
5
  import type React from "react";
6
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { MockRequestClient } from "@/__mocks__/requests";
8
+ import { TooltipProvider } from "@/components/ui/tooltip";
8
9
  import type { SQLTableContext } from "@/core/datasets/data-source-connections";
9
10
  import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
10
11
  import { requestClientAtom } from "@/core/network/requests";
11
12
  import { store } from "@/core/state/jotai";
12
13
  import { DatasetColumnPreview } from "../column-preview";
13
14
 
15
+ const { createNewCell } = vi.hoisted(() => ({ createNewCell: vi.fn() }));
16
+
17
+ vi.mock("@/core/cells/cells", async (importOriginal) => ({
18
+ ...(await importOriginal<typeof import("@/core/cells/cells")>()),
19
+ useCellActions: () => ({ createNewCell }),
20
+ }));
21
+
14
22
  const wrapper = ({ children }: { children: React.ReactNode }) => (
15
23
  <Provider store={store}>{children}</Provider>
16
24
  );
@@ -95,3 +103,56 @@ describe("DatasetColumnPreview fullyQualifiedTableName", () => {
95
103
  );
96
104
  });
97
105
  });
106
+
107
+ describe("DatasetColumnPreview chart actions", () => {
108
+ it("identifies each column and inserts its corresponding chart", () => {
109
+ const store = createStore();
110
+ store.set(requestClientAtom, MockRequestClient.create());
111
+ const columns: DataTableColumn[] = [
112
+ {
113
+ name: "email",
114
+ type: "string",
115
+ external_type: "str",
116
+ sample_values: [],
117
+ },
118
+ { name: "age", type: "integer", external_type: "int", sample_values: [] },
119
+ ];
120
+ render(
121
+ <Provider store={store}>
122
+ <TooltipProvider>
123
+ {columns.map((column) => (
124
+ <DatasetColumnPreview
125
+ key={column.name}
126
+ table={{ ...table, source_type: "local", engine: null }}
127
+ column={column}
128
+ onAddColumnChart={vi.fn()}
129
+ preview={{
130
+ table_name: table.name,
131
+ column_name: column.name,
132
+ chart_code: `chart_${column.name}`,
133
+ chart_spec: null,
134
+ error: null,
135
+ missing_packages: null,
136
+ stats: {},
137
+ }}
138
+ />
139
+ ))}
140
+ </TooltipProvider>
141
+ </Provider>,
142
+ );
143
+
144
+ for (const column of columns) {
145
+ fireEvent.click(
146
+ screen.getByRole("button", {
147
+ name: `Add chart to notebook for ${column.name}`,
148
+ }),
149
+ );
150
+ expect(createNewCell).toHaveBeenLastCalledWith({
151
+ code: `chart_${column.name}`,
152
+ before: false,
153
+ cellId: "__end__",
154
+ });
155
+ }
156
+ expect(createNewCell).toHaveBeenCalledTimes(2);
157
+ });
158
+ });
@@ -112,12 +112,16 @@ export const DatasetColumnPreview: React.FC<{
112
112
 
113
113
  const addDataframeChart = preview.chart_code &&
114
114
  table.source_type === "local" && (
115
- <AddDataframeChart chartCode={preview.chart_code} />
115
+ <AddDataframeChart
116
+ chartCode={preview.chart_code}
117
+ columnName={column.name}
118
+ />
116
119
  );
117
120
 
118
121
  const addSQLChart = table.source_type === "duckdb" && (
119
122
  <Tooltip content="Add SQL cell" delayDuration={400}>
120
123
  <Button
124
+ aria-label={`Add SQL cell for ${column.name}`}
121
125
  variant="outline"
122
126
  size="icon"
123
127
  className="z-10 bg-background absolute right-1 -top-1"
@@ -238,7 +242,8 @@ export function renderChart(chartSpec: string, theme: Theme) {
238
242
 
239
243
  export const AddDataframeChart: React.FC<{
240
244
  chartCode: string;
241
- }> = ({ chartCode }) => {
245
+ columnName: string;
246
+ }> = ({ chartCode, columnName }) => {
242
247
  const autoInstantiate = useAtomValue(autoInstantiateAtom);
243
248
  const lastFocusedCellId = useLastFocusedCellId();
244
249
  const { createNewCell } = useCellActions();
@@ -261,6 +266,7 @@ export const AddDataframeChart: React.FC<{
261
266
  return (
262
267
  <Tooltip content="Add chart to notebook" delayDuration={400}>
263
268
  <Button
269
+ aria-label={`Add chart to notebook for ${columnName}`}
264
270
  variant="outline"
265
271
  size="icon"
266
272
  className="z-10 bg-background absolute right-1 -top-0.5"
@@ -13,7 +13,7 @@ import {
13
13
  VisibilityToggleButton,
14
14
  } from "@/components/editor/file-tree/tree-actions";
15
15
  import { CopyClipboardIcon } from "@/components/icons/copy-icon";
16
- import { Button } from "@/components/ui/button";
16
+ import { Button, buttonVariants } from "@/components/ui/button";
17
17
  import { Command, CommandInput, CommandItem } from "@/components/ui/command";
18
18
  import { Tooltip } from "@/components/ui/tooltip";
19
19
  import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
@@ -978,6 +978,7 @@ const DatasetTableItem: React.FC<{
978
978
  {renderRowsByColumns()}
979
979
  <Tooltip content="Add table to notebook" delayDuration={400}>
980
980
  <Button
981
+ aria-label={`Add table ${table.name} to notebook`}
981
982
  className="group-hover:inline-flex hidden"
982
983
  variant="text"
983
984
  size="icon"
@@ -1068,19 +1069,16 @@ const DatasetColumnItem: React.FC<{
1068
1069
  {isIndexed &&
1069
1070
  renderItemSubtext({ tooltipContent: "Indexed", content: "IDX" })}
1070
1071
  </div>
1071
- <Tooltip content="Copy column name" delayDuration={400}>
1072
- <Button
1073
- variant="text"
1074
- size="icon"
1075
- className="group-hover:opacity-100 opacity-0 hover:bg-muted text-muted-foreground hover:text-foreground"
1076
- >
1077
- <CopyClipboardIcon
1078
- tooltip={false}
1079
- value={column.name}
1080
- className="h-3 w-3"
1081
- />
1082
- </Button>
1083
- </Tooltip>
1072
+ <CopyClipboardIcon
1073
+ tooltip="Copy column name"
1074
+ value={column.name}
1075
+ ariaLabel={`Copy column name ${column.name}`}
1076
+ className="h-3 w-3"
1077
+ buttonClassName={cn(
1078
+ buttonVariants({ variant: "text", size: "icon" }),
1079
+ "group-hover:opacity-100 opacity-0 hover:bg-muted text-muted-foreground hover:text-foreground",
1080
+ )}
1081
+ />
1084
1082
  <span className="text-xs text-muted-foreground">
1085
1083
  {column.external_type}
1086
1084
  </span>
@@ -162,6 +162,7 @@ export const DebuggerControls: React.FC<{
162
162
  <div className="flex">
163
163
  <Tooltip content="Next line">
164
164
  <Button
165
+ aria-label="Next line"
165
166
  variant="text"
166
167
  size="icon"
167
168
  data-testid="debugger-next-button"
@@ -173,6 +174,7 @@ export const DebuggerControls: React.FC<{
173
174
  </Tooltip>
174
175
  <Tooltip content="Continue execution">
175
176
  <Button
177
+ aria-label="Continue execution"
176
178
  variant="text"
177
179
  size="icon"
178
180
  data-testid="debugger-continue-button"
@@ -187,6 +189,7 @@ export const DebuggerControls: React.FC<{
187
189
  </Tooltip>
188
190
  <Tooltip content="Print stack trace">
189
191
  <Button
192
+ aria-label="Print stack trace"
190
193
  variant="text"
191
194
  size="icon"
192
195
  data-testid="debugger-stack-button"
@@ -198,6 +201,7 @@ export const DebuggerControls: React.FC<{
198
201
  </Tooltip>
199
202
  <Tooltip content="Help">
200
203
  <Button
204
+ aria-label="Debugger help"
201
205
  variant="text"
202
206
  size="icon"
203
207
  data-testid="debugger-help-button"
@@ -210,6 +214,7 @@ export const DebuggerControls: React.FC<{
210
214
  {onClear && (
211
215
  <Tooltip content="Quit and clear">
212
216
  <Button
217
+ aria-label="Quit and clear debugger"
213
218
  variant="text"
214
219
  size="icon"
215
220
  data-testid="debugger-clear-button"
@@ -216,11 +216,12 @@ export const GraphSelectionPanel: React.FC<{
216
216
  </Button>
217
217
  </Tooltip>
218
218
  <ConnectionCellActionsDropdown cellId={selection.id}>
219
- <Button variant="ghost" size="icon">
219
+ <Button aria-label="Cell actions" variant="ghost" size="icon">
220
220
  <MoreVerticalIcon className="w-4 h-4" />
221
221
  </Button>
222
222
  </ConnectionCellActionsDropdown>
223
223
  <Button
224
+ aria-label="Close cell details"
224
225
  variant="text"
225
226
  size="icon"
226
227
  onClick={() => {
@@ -408,6 +408,7 @@ export const AiCompletionEditor: React.FC<Props> = ({
408
408
  </div>
409
409
  </Tooltip>
410
410
  <Button
411
+ aria-label="Discard AI completion"
411
412
  data-testid="decline-completion-button"
412
413
  variant="text"
413
414
  size="icon"
@@ -51,6 +51,7 @@ export const StartupLogsAlert: React.FC = () => {
51
51
  {isRunning ? "Initializing..." : "Initialized"}
52
52
  </span>
53
53
  <Button
54
+ aria-label="Dismiss startup logs"
54
55
  variant="text"
55
56
  data-testid="remove-startup-logs-button"
56
57
  size="icon"
@@ -37,6 +37,7 @@ export const DeleteButton = (props: {
37
37
  return (
38
38
  <Tooltip content={tooltipMsg} usePortal={false}>
39
39
  <Button
40
+ aria-label="Delete cell"
40
41
  variant="ghost"
41
42
  size="icon"
42
43
  onClick={onClick}
@@ -104,6 +104,7 @@ const CachePanel = () => {
104
104
  {/* Header with Refresh Button */}
105
105
  <div className="flex items-center justify-end">
106
106
  <Button
107
+ aria-label="Refresh cache"
107
108
  variant="ghost"
108
109
  size="icon"
109
110
  className="h-6 w-6"
@@ -1,11 +1,12 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
- import { act, render, screen, within } from "@testing-library/react";
3
+ import { act, fireEvent, render, screen, within } from "@testing-library/react";
4
4
  import { createStore, Provider } from "jotai";
5
5
  import { describe, expect, it, vi } from "vitest";
6
6
  import { cellId } from "@/__tests__/branded";
7
7
  import { TooltipProvider } from "@/components/ui/tooltip";
8
8
  import { stagedAICellsAtom, visibleForTesting } from "@/core/ai/staged-cells";
9
+ import { scrollAndHighlightCell } from "../../../links/cell-link";
9
10
  import { PendingAICells } from "../pending-ai-cells";
10
11
 
11
12
  vi.mock("@/components/editor/cell/useDeleteCell", () => ({
@@ -29,6 +30,62 @@ vi.mock("@/components/editor/links/cell-link", () => ({
29
30
  }));
30
31
 
31
32
  describe("PendingAICells", () => {
33
+ it("navigates between pending cells through named controls", () => {
34
+ const store = createStore();
35
+ const first = cellId("first-pending-cell");
36
+ const second = cellId("second-pending-cell");
37
+ const third = cellId("third-pending-cell");
38
+ store.set(
39
+ stagedAICellsAtom,
40
+ new Map([
41
+ [first, { type: "add_cell" }],
42
+ [second, { type: "add_cell" }],
43
+ [third, { type: "add_cell" }],
44
+ ]),
45
+ );
46
+
47
+ render(
48
+ <Provider store={store}>
49
+ <TooltipProvider>
50
+ <PendingAICells />
51
+ </TooltipProvider>
52
+ </Provider>,
53
+ );
54
+
55
+ const next = screen.getByRole("button", { name: "Next pending AI cell" });
56
+ const previous = screen.getByRole("button", {
57
+ name: "Previous pending AI cell",
58
+ });
59
+
60
+ fireEvent.click(next);
61
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(first, "focus");
62
+ expect(screen.getByText("1 / 3")).toBeInTheDocument();
63
+
64
+ fireEvent.click(next);
65
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(second, "focus");
66
+ expect(screen.getByText("2 / 3")).toBeInTheDocument();
67
+
68
+ fireEvent.click(previous);
69
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(first, "focus");
70
+ expect(screen.getByText("1 / 3")).toBeInTheDocument();
71
+
72
+ fireEvent.click(next);
73
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(second, "focus");
74
+ expect(screen.getByText("2 / 3")).toBeInTheDocument();
75
+
76
+ fireEvent.click(next);
77
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(third, "focus");
78
+ expect(screen.getByText("3 / 3")).toBeInTheDocument();
79
+
80
+ fireEvent.click(next);
81
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(first, "focus");
82
+ expect(screen.getByText("1 / 3")).toBeInTheDocument();
83
+
84
+ fireEvent.click(previous);
85
+ expect(scrollAndHighlightCell).toHaveBeenLastCalledWith(third, "focus");
86
+ expect(screen.getByText("3 / 3")).toBeInTheDocument();
87
+ });
88
+
32
89
  it("disables bulk actions while cells are still generating", () => {
33
90
  const store = createStore();
34
91
  const generationId = Symbol("staged-cell-generation");
@@ -79,7 +79,12 @@ export const PendingAICells: React.FC = () => {
79
79
  <SparklesIcon className="h-4 w-4 text-primary" />
80
80
 
81
81
  <div className="flex items-center">
82
- <Button variant="ghost" size="icon" onClick={() => clickNext("up")}>
82
+ <Button
83
+ aria-label="Previous pending AI cell"
84
+ variant="ghost"
85
+ size="icon"
86
+ onClick={() => clickNext("down")}
87
+ >
83
88
  <ChevronUp className="h-3.5 w-3.5" />
84
89
  </Button>
85
90
  <span className="text-xs font-mono min-w-14 text-center">
@@ -87,7 +92,12 @@ export const PendingAICells: React.FC = () => {
87
92
  ? `${listStagedCells.length} pending`
88
93
  : `${currentIndex + 1} / ${listStagedCells.length}`}
89
94
  </span>
90
- <Button variant="ghost" size="icon" onClick={() => clickNext("down")}>
95
+ <Button
96
+ aria-label="Next pending AI cell"
97
+ variant="ghost"
98
+ size="icon"
99
+ onClick={() => clickNext("up")}
100
+ >
91
101
  <ChevronDown className="h-3.5 w-3.5" />
92
102
  </Button>
93
103
  </div>
@@ -41,6 +41,7 @@ export const NotebookBanner: React.FC<Props> = ({ width }) => {
41
41
  {banner.title}
42
42
  </span>
43
43
  <Button
44
+ aria-label={`Dismiss ${banner.title}`}
44
45
  data-testid="remove-banner-button"
45
46
  variant="text"
46
47
  size="icon"
@@ -358,6 +358,7 @@ export class AnsiReducer {
358
358
 
359
359
  export type StringOutputMessage = Omit<OutputMessage, "data"> & {
360
360
  data: string;
361
+ id?: number;
361
362
  };
362
363
 
363
364
  /**
@@ -369,6 +370,7 @@ export class StatefulOutputMessage implements OutputMessage {
369
370
  public readonly channel: OutputMessage["channel"];
370
371
  public readonly timestamp: OutputMessage["timestamp"];
371
372
  public readonly code: OutputMessage["code"];
373
+ public readonly id: StringOutputMessage["id"];
372
374
  private ansiReducer = new AnsiReducer();
373
375
  private _data: string;
374
376
 
@@ -385,6 +387,7 @@ export class StatefulOutputMessage implements OutputMessage {
385
387
  message.timestamp,
386
388
  ansiReducer,
387
389
  message.code,
390
+ message.id,
388
391
  );
389
392
  }
390
393
 
@@ -394,11 +397,13 @@ export class StatefulOutputMessage implements OutputMessage {
394
397
  timestamp: OutputMessage["timestamp"],
395
398
  ansiReducer: AnsiReducer,
396
399
  code: OutputMessage["code"],
400
+ id: StringOutputMessage["id"],
397
401
  ) {
398
402
  this.mimetype = mimetype;
399
403
  this.channel = channel;
400
404
  this.timestamp = timestamp;
401
405
  this.code = code;
406
+ this.id = id;
402
407
  this.ansiReducer = ansiReducer;
403
408
  this._data = this.ansiReducer.render();
404
409
  }
@@ -411,6 +416,7 @@ export class StatefulOutputMessage implements OutputMessage {
411
416
  this.timestamp,
412
417
  this.ansiReducer,
413
418
  this.code,
419
+ this.id,
414
420
  );
415
421
  }
416
422
 
@@ -16,8 +16,7 @@ import { Tooltip } from "@/components/ui/tooltip";
16
16
  import type { CellId } from "@/core/cells/ids";
17
17
  import { isInternalCellName } from "@/core/cells/names";
18
18
  import { useExpandedConsoleOutput } from "@/core/cells/outputs";
19
- import type { WithResponse } from "@/core/cells/types";
20
- import type { OutputMessage } from "@/core/kernel/messages";
19
+ import type { ConsoleOutputMessage } from "@/core/cells/types";
21
20
  import {
22
21
  type UseInputHistoryReturn,
23
22
  useInputHistory,
@@ -73,7 +72,7 @@ interface Props {
73
72
  cellId: CellId;
74
73
  cellName: string;
75
74
  className?: string;
76
- consoleOutputs: WithResponse<OutputMessage>[];
75
+ consoleOutputs: ConsoleOutputMessage[];
77
76
  stale: boolean;
78
77
  interrupted: boolean;
79
78
  debuggerActive: boolean;
@@ -241,19 +240,24 @@ const ConsoleOutputInternal = (props: Props): React.ReactNode => {
241
240
  return null;
242
241
  }
243
242
 
243
+ const originalIdx = consoleOutputs.length - idx - 1;
244
+ // Outputs render in reverse, so the reversed index shifts on every
245
+ // new line. The assigned id survives collapse and truncation. The
246
+ // original position is the fallback for outputs without one.
247
+ const key = output.id ?? `position-${originalIdx}`;
248
+
244
249
  if (output.channel === "stdin") {
245
250
  invariant(
246
251
  typeof output.data === "string",
247
252
  "Expected data to be a string",
248
253
  );
249
254
 
250
- const originalIdx = consoleOutputs.length - idx - 1;
251
255
  const isPassword = output.mimetype === "text/password";
252
256
 
253
257
  if (output.response == null && lastStdInputIdx === idx) {
254
258
  return (
255
259
  <StdInput
256
- key={idx}
260
+ key={key}
257
261
  output={output.data}
258
262
  isPdb={isPdb}
259
263
  isPassword={isPassword}
@@ -268,7 +272,7 @@ const ConsoleOutputInternal = (props: Props): React.ReactNode => {
268
272
 
269
273
  return (
270
274
  <StdInputWithResponse
271
- key={idx}
275
+ key={key}
272
276
  output={output.data}
273
277
  response={output.response}
274
278
  isPassword={isPassword}
@@ -278,7 +282,7 @@ const ConsoleOutputInternal = (props: Props): React.ReactNode => {
278
282
  }
279
283
 
280
284
  return (
281
- <React.Fragment key={idx}>
285
+ <React.Fragment key={key}>
282
286
  <OutputRenderer
283
287
  cellId={cellId}
284
288
  onRefactorWithAI={onRefactorWithAI}
@@ -158,6 +158,7 @@ export const PackageAlert: React.FC = () => {
158
158
  <SourceBadge source={packageAlert.source} />
159
159
  </span>
160
160
  <Button
161
+ aria-label="Dismiss missing packages alert"
161
162
  variant="text"
162
163
  data-testid="remove-banner-button"
163
164
  size="icon"
@@ -171,12 +172,12 @@ export const PackageAlert: React.FC = () => {
171
172
  <p>The following packages were not found:</p>
172
173
  <table className="list-disc ml-2 mt-1">
173
174
  <tbody>
174
- {packageAlert.packages.map((pkg, index) => {
175
+ {packageAlert.packages.map((pkg) => {
175
176
  const parsed = parsePackageSpecifier(pkg);
176
177
  const currentExtras = selectedExtras[pkg] || parsed.extras;
177
178
 
178
179
  return (
179
- <tr className="font-mono text-sm" key={index}>
180
+ <tr className="font-mono text-sm" key={pkg}>
180
181
  <td className="pr-2 py-1 align-middle">
181
182
  <BoxIcon size="1rem" />
182
183
  </td>
@@ -270,6 +271,7 @@ export const PackageAlert: React.FC = () => {
270
271
  <SourceBadge source={packageAlert.source} />
271
272
  </span>
272
273
  <Button
274
+ aria-label={`Dismiss ${title}`}
273
275
  variant="text"
274
276
  data-testid="remove-banner-button"
275
277
  size="icon"
@@ -294,7 +296,7 @@ export const PackageAlert: React.FC = () => {
294
296
  </>
295
297
  )}
296
298
  <ul className="list-disc ml-2 mt-1">
297
- {Object.entries(packageAlert.packages).map(([pkg, st], index) => (
299
+ {Object.entries(packageAlert.packages).map(([pkg, st]) => (
298
300
  <li
299
301
  className={cn(
300
302
  "flex items-center gap-1 font-mono text-sm",
@@ -305,7 +307,7 @@ export const PackageAlert: React.FC = () => {
305
307
  status === "failed" &&
306
308
  "text-muted-foreground",
307
309
  )}
308
- key={index}
310
+ key={pkg}
309
311
  >
310
312
  <ProgressIcon status={st} />
311
313
  {pkg}
@@ -12,7 +12,13 @@ interface SidebarToggleProps {
12
12
  export const SidebarToggle = ({ isOpen, toggle }: SidebarToggleProps) => {
13
13
  return (
14
14
  <div className="invisible lg:visible absolute top-[12px] right-[16px] z-20">
15
- <Button onClick={toggle} className="w-10 h-8" variant="ghost" size="icon">
15
+ <Button
16
+ aria-label={isOpen ? "Collapse sidebar" : "Expand sidebar"}
17
+ onClick={toggle}
18
+ className="w-10 h-8"
19
+ variant="ghost"
20
+ size="icon"
21
+ >
16
22
  <ChevronLeft
17
23
  className={cn(
18
24
  "h-5 w-5 transition-transform ease-in-out duration-700",
@@ -41,7 +41,7 @@ export const MCPStatusIndicator: React.FC = () => {
41
41
  <Popover>
42
42
  <Tooltip content="MCP Status">
43
43
  <PopoverTrigger asChild={true}>
44
- <Button variant="text" size="icon">
44
+ <Button aria-label="MCP status" variant="text" size="icon">
45
45
  <PlugIcon
46
46
  className={cn(
47
47
  "h-4 w-4",
@@ -135,9 +135,9 @@ export const ScratchPad: React.FC = () => {
135
135
  return (
136
136
  <div className="absolute inset-0 z-100 bg-background p-3 border-none overflow-auto">
137
137
  <div className="overflow-auto flex flex-col gap-3">
138
- {history.map((item, index) => (
138
+ {history.map((item) => (
139
139
  <div
140
- key={index}
140
+ key={item}
141
141
  className="border rounded-md hover:shadow-sm cursor-pointer hover:border-input overflow-hidden"
142
142
  onClick={() => handleSelectHistoryItem(item)}
143
143
  >
@@ -404,13 +404,15 @@ const SubslideView = ({
404
404
  if (block.isFragment) {
405
405
  const cumulative = cumulativeByBlock.get(i);
406
406
  return (
407
- <Fragment key={i} as="div">
407
+ <Fragment key={block.cells[0].id} as="div">
408
408
  {rendered}
409
409
  {cumulative && <NotesAside text={cumulative} />}
410
410
  </Fragment>
411
411
  );
412
412
  }
413
- return <ReactFragment key={i}>{rendered}</ReactFragment>;
413
+ return (
414
+ <ReactFragment key={block.cells[0].id}>{rendered}</ReactFragment>
415
+ );
414
416
  })}
415
417
  </div>
416
418
  </SlideScrollContainer>
@@ -450,6 +450,7 @@ export const SlideSidebar = ({
450
450
  )}
451
451
  <Tooltip content={isConfigOpen ? "Collapse panel" : "Expand panel"}>
452
452
  <Button
453
+ aria-label={isConfigOpen ? "Collapse panel" : "Expand panel"}
453
454
  variant="ghost"
454
455
  size="icon"
455
456
  className="h-7 w-7 text-muted-foreground hover:text-foreground"
@@ -139,6 +139,7 @@ exports[`cell reducer > can initialize stdin > second stdin requested 1`] = `
139
139
  {
140
140
  "channel": "stdin",
141
141
  "data": "what is your name?",
142
+ "id": 2,
142
143
  "mimetype": "text/plain",
143
144
  "response": "Marimo!",
144
145
  "timestamp": 2,
@@ -146,6 +147,7 @@ exports[`cell reducer > can initialize stdin > second stdin requested 1`] = `
146
147
  {
147
148
  "channel": "stdin",
148
149
  "data": "how old are you?",
150
+ "id": 3,
149
151
  "mimetype": "text/plain",
150
152
  "timestamp": 3,
151
153
  },
@@ -189,6 +191,7 @@ exports[`cell reducer > can initialize stdin > stdin answered 1`] = `
189
191
  {
190
192
  "channel": "stdin",
191
193
  "data": "what is your name?",
194
+ "id": 2,
192
195
  "mimetype": "text/plain",
193
196
  "response": "Marimo!",
194
197
  "timestamp": 2,
@@ -233,6 +236,7 @@ exports[`cell reducer > can initialize stdin > stdin requested 1`] = `
233
236
  {
234
237
  "channel": "stdin",
235
238
  "data": "what is your name?",
239
+ "id": 2,
236
240
  "mimetype": "text/plain",
237
241
  "timestamp": 2,
238
242
  },