@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.
- package/dist/{ConnectedDataExplorerComponent-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- package/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -21,10 +21,17 @@ beforeAll(() => {
|
|
|
21
21
|
|
|
22
22
|
type Row = Record<string, unknown>;
|
|
23
23
|
|
|
24
|
+
// Select, index, and nameless columns are non-hideable in production
|
|
25
|
+
// (see columns.tsx), so the harness mirrors that to keep visibility counts
|
|
26
|
+
// aligned with the real table.
|
|
24
27
|
const TEST_COLUMNS: ColumnDef<Row>[] = [
|
|
25
|
-
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID },
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
+
{ id: SELECT_COLUMN_ID, accessorKey: SELECT_COLUMN_ID, enableHiding: false },
|
|
29
|
+
{
|
|
30
|
+
id: INDEX_COLUMN_NAME,
|
|
31
|
+
accessorKey: INDEX_COLUMN_NAME,
|
|
32
|
+
enableHiding: false,
|
|
33
|
+
},
|
|
34
|
+
{ id: "__m_column__0", accessorKey: "__m_column__0", enableHiding: false },
|
|
28
35
|
{
|
|
29
36
|
id: "customer_name",
|
|
30
37
|
accessorKey: "customer_name",
|
|
@@ -101,6 +108,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
101
108
|
renderAndOpen({ initiallyHidden: ["cust_age"] });
|
|
102
109
|
expect(getOptionTexts()).toEqual([
|
|
103
110
|
"Show all",
|
|
111
|
+
"Hide all",
|
|
104
112
|
"cust_age",
|
|
105
113
|
"customer_name",
|
|
106
114
|
"order_total",
|
|
@@ -117,6 +125,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
117
125
|
|
|
118
126
|
expect(getOptionTexts()).toEqual([
|
|
119
127
|
"Show all",
|
|
128
|
+
"Hide all",
|
|
120
129
|
"cust_age",
|
|
121
130
|
"customer_name",
|
|
122
131
|
"order_total",
|
|
@@ -132,6 +141,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
132
141
|
fireEvent.click(getColumnOption("customer_name"));
|
|
133
142
|
expect(getOptionTexts()).toEqual([
|
|
134
143
|
"Show all",
|
|
144
|
+
"Hide all",
|
|
135
145
|
"cust_age",
|
|
136
146
|
"customer_name",
|
|
137
147
|
"order_total",
|
|
@@ -144,6 +154,7 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
144
154
|
// Reopen sorts both hidden columns first, preserving table order.
|
|
145
155
|
expect(getOptionTexts()).toEqual([
|
|
146
156
|
"Show all",
|
|
157
|
+
"Hide all",
|
|
147
158
|
"customer_name",
|
|
148
159
|
"cust_age",
|
|
149
160
|
"order_total",
|
|
@@ -189,6 +200,52 @@ describe("ColumnVisibilityDropdown", () => {
|
|
|
189
200
|
);
|
|
190
201
|
});
|
|
191
202
|
|
|
203
|
+
it("'Hide all' hides every hideable column", () => {
|
|
204
|
+
renderAndOpen();
|
|
205
|
+
fireEvent.click(getColumnOption("Hide all"));
|
|
206
|
+
|
|
207
|
+
expect(
|
|
208
|
+
getColumnOption("customer_name").querySelector(".lucide-eye-off"),
|
|
209
|
+
).not.toBeNull();
|
|
210
|
+
expect(
|
|
211
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
212
|
+
).not.toBeNull();
|
|
213
|
+
expect(
|
|
214
|
+
getColumnOption("order_total").querySelector(".lucide-eye-off"),
|
|
215
|
+
).not.toBeNull();
|
|
216
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
217
|
+
"aria-disabled",
|
|
218
|
+
"true",
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("disables 'Hide all' when every column is already hidden", () => {
|
|
223
|
+
renderAndOpen({
|
|
224
|
+
initiallyHidden: ["customer_name", "cust_age", "order_total"],
|
|
225
|
+
});
|
|
226
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
227
|
+
"aria-disabled",
|
|
228
|
+
"true",
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("'Hide all' leaves non-hideable columns visible", () => {
|
|
233
|
+
renderAndOpen({ nonHideable: ["customer_name"] });
|
|
234
|
+
fireEvent.click(getColumnOption("Hide all"));
|
|
235
|
+
|
|
236
|
+
expect(
|
|
237
|
+
getColumnOption("cust_age").querySelector(".lucide-eye-off"),
|
|
238
|
+
).not.toBeNull();
|
|
239
|
+
const nonHideable = getColumnOption("customer_name");
|
|
240
|
+
expect(nonHideable).toHaveAttribute("aria-disabled", "true");
|
|
241
|
+
expect(nonHideable.querySelector(".lucide-eye-off")).toBeNull();
|
|
242
|
+
// Every hideable column is now hidden, so the action gates off.
|
|
243
|
+
expect(getColumnOption("Hide all")).toHaveAttribute(
|
|
244
|
+
"aria-disabled",
|
|
245
|
+
"true",
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
192
249
|
it("hides and shows matching columns via bulk actions while searching", () => {
|
|
193
250
|
renderAndOpen();
|
|
194
251
|
fireEvent.change(getSearchInput(), { target: { value: "cust" } });
|
|
@@ -85,6 +85,10 @@ const Mocks = {
|
|
|
85
85
|
type: "arc",
|
|
86
86
|
innerRadius: 100,
|
|
87
87
|
},
|
|
88
|
+
arcDefault: {
|
|
89
|
+
type: "arc",
|
|
90
|
+
innerRadius: undefined,
|
|
91
|
+
},
|
|
88
92
|
},
|
|
89
93
|
};
|
|
90
94
|
|
|
@@ -128,6 +132,26 @@ describe("generateAltairChart", () => {
|
|
|
128
132
|
`);
|
|
129
133
|
});
|
|
130
134
|
|
|
135
|
+
it("should generate a pie chart when arc has undefined innerRadius", () => {
|
|
136
|
+
const spec = createSpec({
|
|
137
|
+
mark: Mocks.marks.arcDefault,
|
|
138
|
+
encoding: {
|
|
139
|
+
theta: Mocks.thetaAxis,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const datasource = "df";
|
|
143
|
+
|
|
144
|
+
const result = generateAltairChart(spec, datasource).toCode();
|
|
145
|
+
|
|
146
|
+
expect(result).toMatchInlineSnapshot(`
|
|
147
|
+
"alt.Chart(df)
|
|
148
|
+
.mark_arc()
|
|
149
|
+
.encode(theta=alt.Theta(field='theta', axis={
|
|
150
|
+
'title': 'Theta Axis'
|
|
151
|
+
}))"
|
|
152
|
+
`);
|
|
153
|
+
});
|
|
154
|
+
|
|
131
155
|
it("should generate a donut chart", () => {
|
|
132
156
|
const spec = createSpec({
|
|
133
157
|
mark: Mocks.marks.arc,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import type { FieldTypesWithExternalType } from "@/components/data-table/types";
|
|
5
|
+
import { mergeIndexFields } from "../charts";
|
|
6
|
+
|
|
7
|
+
describe("mergeIndexFields", () => {
|
|
8
|
+
it("appends row-header (index) fields to field types", () => {
|
|
9
|
+
const fieldTypes: FieldTypesWithExternalType = [["v", ["number", "int64"]]];
|
|
10
|
+
const rowHeaders: FieldTypesWithExternalType = [
|
|
11
|
+
["k", ["string", "object"]],
|
|
12
|
+
];
|
|
13
|
+
expect(mergeIndexFields(fieldTypes, rowHeaders)).toEqual([
|
|
14
|
+
["v", ["number", "int64"]],
|
|
15
|
+
["k", ["string", "object"]],
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("de-dupes when an index name matches a column name", () => {
|
|
20
|
+
const fieldTypes: FieldTypesWithExternalType = [["k", ["number", "int64"]]];
|
|
21
|
+
const rowHeaders: FieldTypesWithExternalType = [
|
|
22
|
+
["k", ["string", "object"]],
|
|
23
|
+
];
|
|
24
|
+
expect(mergeIndexFields(fieldTypes, rowHeaders)).toEqual([
|
|
25
|
+
["k", ["number", "int64"]],
|
|
26
|
+
]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("returns field types unchanged when there are no row headers", () => {
|
|
30
|
+
const fieldTypes: FieldTypesWithExternalType = [["v", ["number", "int64"]]];
|
|
31
|
+
expect(mergeIndexFields(fieldTypes, [])).toEqual(fieldTypes);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -30,7 +30,10 @@ export function generateAltairChart(
|
|
|
30
30
|
if (typeof spec.mark === "object" && "type" in spec.mark) {
|
|
31
31
|
markProps = Object.fromEntries(
|
|
32
32
|
Object.entries(spec.mark)
|
|
33
|
-
.filter(
|
|
33
|
+
.filter(
|
|
34
|
+
([key, value]) =>
|
|
35
|
+
key !== "type" && value !== undefined && value !== null,
|
|
36
|
+
)
|
|
34
37
|
.map(([key, value]) => [key, new Literal(value)]),
|
|
35
38
|
);
|
|
36
39
|
}
|
|
@@ -27,6 +27,7 @@ import { useDebouncedCallback } from "@/hooks/useDebounce";
|
|
|
27
27
|
import type { GetDataUrl } from "@/plugins/impl/DataTablePlugin";
|
|
28
28
|
import { vegaLoadData } from "@/plugins/impl/vega/loader";
|
|
29
29
|
import { useTheme } from "@/theme/useTheme";
|
|
30
|
+
import { uniqueBy } from "@/utils/arrays";
|
|
30
31
|
import { inferFieldTypes } from "../columns";
|
|
31
32
|
import {
|
|
32
33
|
type FieldTypesWithExternalType,
|
|
@@ -54,6 +55,22 @@ const CHART_HEIGHT = 290;
|
|
|
54
55
|
const CHART_MAX_ROWS = 50_000;
|
|
55
56
|
const CHART_MAX_COLUMNS = 50;
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Append row-header (index) fields to the chart field list, skipping any whose
|
|
60
|
+
* name already exists as a data column so the chart builder never offers the
|
|
61
|
+
* same axis twice.
|
|
62
|
+
*/
|
|
63
|
+
export function mergeIndexFields(
|
|
64
|
+
fieldTypes: FieldTypesWithExternalType | null | undefined,
|
|
65
|
+
rowHeaders: FieldTypesWithExternalType | null | undefined,
|
|
66
|
+
): FieldTypesWithExternalType {
|
|
67
|
+
const base = fieldTypes ?? [];
|
|
68
|
+
if (!rowHeaders || rowHeaders.length === 0) {
|
|
69
|
+
return base;
|
|
70
|
+
}
|
|
71
|
+
return uniqueBy([...base, ...rowHeaders], (f) => f[0]);
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
export interface TablePanelProps {
|
|
58
75
|
cellId: CellId | null;
|
|
59
76
|
data: unknown[];
|
|
@@ -64,6 +81,7 @@ export interface TablePanelProps {
|
|
|
64
81
|
onCloseChartBuilder?: () => void;
|
|
65
82
|
getDataUrl?: GetDataUrl;
|
|
66
83
|
fieldTypes?: FieldTypesWithExternalType | null;
|
|
84
|
+
rowHeaders?: FieldTypesWithExternalType | null;
|
|
67
85
|
}
|
|
68
86
|
|
|
69
87
|
export const TablePanel: React.FC<TablePanelProps> = ({
|
|
@@ -74,6 +92,7 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
74
92
|
columns,
|
|
75
93
|
getDataUrl,
|
|
76
94
|
fieldTypes,
|
|
95
|
+
rowHeaders,
|
|
77
96
|
displayHeader,
|
|
78
97
|
onCloseChartBuilder,
|
|
79
98
|
}) => {
|
|
@@ -265,7 +284,10 @@ export const TablePanel: React.FC<TablePanelProps> = ({
|
|
|
265
284
|
saveChart={saveChart}
|
|
266
285
|
saveChartType={saveChartType}
|
|
267
286
|
getDataUrl={getDataUrl}
|
|
268
|
-
fieldTypes={
|
|
287
|
+
fieldTypes={mergeIndexFields(
|
|
288
|
+
fieldTypes ?? inferFieldTypes(dataTable.props.data),
|
|
289
|
+
rowHeaders,
|
|
290
|
+
)}
|
|
269
291
|
isLargeDataset={isLargeDataset}
|
|
270
292
|
/>
|
|
271
293
|
</TabsContent>
|
|
@@ -50,7 +50,10 @@ import {
|
|
|
50
50
|
import { smartMatch } from "@/utils/smartMatch";
|
|
51
51
|
import type { Column, Table } from "@tanstack/react-table";
|
|
52
52
|
import { cn } from "@/utils/cn";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
getColumnCountForDisplay,
|
|
55
|
+
getUserColumnVisibilityCounts,
|
|
56
|
+
} from "../hooks/use-column-visibility";
|
|
54
57
|
|
|
55
58
|
interface ColumnExplorerPanelProps<TData> {
|
|
56
59
|
previewColumn: PreviewColumn;
|
|
@@ -90,6 +93,7 @@ export function ColumnExplorerPanel<TData>({
|
|
|
90
93
|
totalColumns: effectiveTotalColumns,
|
|
91
94
|
hiddenColumns: hiddenColumnCount,
|
|
92
95
|
} = getColumnCountForDisplay(table, totalColumns);
|
|
96
|
+
const { visible: visibleColumnCount } = getUserColumnVisibilityCounts(table);
|
|
93
97
|
|
|
94
98
|
const { rowsAndColumns, hiddenSuffix } = prettifyRowColumnCount({
|
|
95
99
|
numRows: totalRows,
|
|
@@ -108,17 +112,34 @@ export function ColumnExplorerPanel<TData>({
|
|
|
108
112
|
value={columns?.map(([columnName]) => columnName).join(",\n") || ""}
|
|
109
113
|
className="h-3 w-3"
|
|
110
114
|
/>
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
<div className="ml-auto flex items-center gap-1">
|
|
116
|
+
<Tooltip content="Show all columns" delayDuration={400}>
|
|
117
|
+
<Button
|
|
118
|
+
type="button"
|
|
119
|
+
variant="ghost"
|
|
120
|
+
size="xs"
|
|
121
|
+
className="gap-1"
|
|
122
|
+
disabled={hiddenColumnCount === 0}
|
|
123
|
+
onClick={() => table.toggleAllColumnsVisible(true)}
|
|
124
|
+
>
|
|
125
|
+
<EyeIcon className="w-3 h-3" />
|
|
126
|
+
Show all
|
|
127
|
+
</Button>
|
|
128
|
+
</Tooltip>
|
|
129
|
+
<Tooltip content="Hide all columns" delayDuration={400}>
|
|
130
|
+
<Button
|
|
131
|
+
type="button"
|
|
132
|
+
variant="ghost"
|
|
133
|
+
size="xs"
|
|
134
|
+
className="gap-1"
|
|
135
|
+
disabled={visibleColumnCount === 0}
|
|
136
|
+
onClick={() => table.toggleAllColumnsVisible(false)}
|
|
137
|
+
>
|
|
138
|
+
<EyeOffIcon className="w-3 h-3" />
|
|
139
|
+
Hide all
|
|
140
|
+
</Button>
|
|
141
|
+
</Tooltip>
|
|
142
|
+
</div>
|
|
122
143
|
</div>
|
|
123
144
|
<Command className="h-5/6 bg-background" shouldFilter={false}>
|
|
124
145
|
<CommandInput
|
|
@@ -59,6 +59,12 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
59
59
|
const hiddenIds = userColumns
|
|
60
60
|
.filter((column) => !column.getIsVisible())
|
|
61
61
|
.map((column) => column.id);
|
|
62
|
+
const hideableIds = userColumns
|
|
63
|
+
.filter((column) => column.getCanHide())
|
|
64
|
+
.map((column) => column.id);
|
|
65
|
+
const visibleHideableCount = hideableIds.filter(
|
|
66
|
+
(id) => !hiddenIds.includes(id),
|
|
67
|
+
).length;
|
|
62
68
|
|
|
63
69
|
const applyHidden = (next: string[] | string | null) => {
|
|
64
70
|
const hidden = new Set(Array.isArray(next) ? next : []);
|
|
@@ -127,6 +133,15 @@ export const ColumnVisibilityDropdown = <TData,>({
|
|
|
127
133
|
<EyeIcon className="w-3 h-3 mr-1.5" />
|
|
128
134
|
Show all
|
|
129
135
|
</CommandItem>
|
|
136
|
+
<CommandItem
|
|
137
|
+
value="__hide_all__"
|
|
138
|
+
disabled={visibleHideableCount === 0}
|
|
139
|
+
onSelect={() => applyHidden(hideableIds)}
|
|
140
|
+
className="cursor-pointer"
|
|
141
|
+
>
|
|
142
|
+
<EyeOffIcon className="w-3 h-3 mr-1.5" />
|
|
143
|
+
Hide all
|
|
144
|
+
</CommandItem>
|
|
130
145
|
<CommandSeparator />
|
|
131
146
|
</>
|
|
132
147
|
) : (
|
|
@@ -208,7 +208,7 @@ export const DebuggerControls: React.FC<{
|
|
|
208
208
|
</Button>
|
|
209
209
|
</Tooltip>
|
|
210
210
|
{onClear && (
|
|
211
|
-
<Tooltip content="
|
|
211
|
+
<Tooltip content="Quit and clear">
|
|
212
212
|
<Button
|
|
213
213
|
variant="text"
|
|
214
214
|
size="icon"
|
|
@@ -217,7 +217,12 @@ export const DebuggerControls: React.FC<{
|
|
|
217
217
|
buttonClasses,
|
|
218
218
|
"text-(--red-11) hover:text-(--red-11) hover:bg-(--red-2) hover:border-(--red-8)",
|
|
219
219
|
)}
|
|
220
|
-
onClick={
|
|
220
|
+
onClick={() => {
|
|
221
|
+
// Quit the debugger (`q`) before tearing down the console, so
|
|
222
|
+
// the stdin response targets the still-present pdb prompt.
|
|
223
|
+
onSubmit("q");
|
|
224
|
+
onClear();
|
|
225
|
+
}}
|
|
221
226
|
>
|
|
222
227
|
<TrashIcon fontSize={36} className={iconClasses} />
|
|
223
228
|
</Button>
|
|
@@ -688,13 +688,13 @@ export function useNotebookActions() {
|
|
|
688
688
|
{
|
|
689
689
|
divider: true,
|
|
690
690
|
icon: <Home size={14} strokeWidth={1.5} />,
|
|
691
|
-
label: "
|
|
691
|
+
label: "Open home",
|
|
692
692
|
// If file is in the url, then we ran `marimo edit`
|
|
693
693
|
// without a specific file
|
|
694
694
|
hidden: !location.search.includes("file"),
|
|
695
695
|
handle: () => {
|
|
696
696
|
const withoutSearch = document.baseURI.split("?")[0];
|
|
697
|
-
window.open(withoutSearch, "
|
|
697
|
+
window.open(withoutSearch, "_blank", "noopener");
|
|
698
698
|
},
|
|
699
699
|
},
|
|
700
700
|
|
|
@@ -14,7 +14,7 @@ import { Tooltip } from "../../ui/tooltip";
|
|
|
14
14
|
|
|
15
15
|
import "./cell-status.css";
|
|
16
16
|
import { formatDistanceToNow } from "date-fns";
|
|
17
|
-
import { Time } from "@/utils/time";
|
|
17
|
+
import { formatElapsedTime, Time } from "@/utils/time";
|
|
18
18
|
|
|
19
19
|
export interface CellStatusComponentProps extends Pick<
|
|
20
20
|
CellRuntimeState,
|
|
@@ -348,25 +348,6 @@ const LastRanTime = (props: { lastRanTime: number }) => {
|
|
|
348
348
|
);
|
|
349
349
|
};
|
|
350
350
|
|
|
351
|
-
export function formatElapsedTime(elapsedTime: number | null) {
|
|
352
|
-
if (elapsedTime === null) {
|
|
353
|
-
return "";
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const milliseconds = elapsedTime;
|
|
357
|
-
const seconds = milliseconds / 1000;
|
|
358
|
-
|
|
359
|
-
if (seconds >= 60) {
|
|
360
|
-
const minutes = Math.floor(seconds / 60);
|
|
361
|
-
const remainingSeconds = Math.floor(seconds % 60);
|
|
362
|
-
return `${minutes}m${remainingSeconds}s`;
|
|
363
|
-
}
|
|
364
|
-
if (seconds >= 1) {
|
|
365
|
-
return `${seconds.toFixed(2).toString()}s`;
|
|
366
|
-
}
|
|
367
|
-
return `${milliseconds.toFixed(0).toString()}ms`;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
351
|
const CellTimer = (props: { startTime: Time }) => {
|
|
371
352
|
const time = useElapsedTime(props.startTime.toMilliseconds());
|
|
372
353
|
return <span>{formatElapsedTime(time)}</span>;
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
import { AlertTriangleIcon } from "lucide-react";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { FocusScope } from "react-aria";
|
|
6
|
-
import { formatElapsedTime } from "@/components/editor/cell/CellStatus";
|
|
7
6
|
import { CellLink } from "@/components/editor/links/cell-link";
|
|
8
7
|
import { Button } from "@/components/ui/button";
|
|
9
8
|
import type { CellId } from "@/core/cells/ids";
|
|
10
9
|
import { usePendingDelete } from "@/core/cells/pending-delete-service";
|
|
11
10
|
import { cn } from "@/utils/cn";
|
|
11
|
+
import { formatElapsedTime } from "@/utils/time";
|
|
12
12
|
|
|
13
13
|
export const PendingDeleteConfirmation: React.FC<{ cellId: CellId }> = ({
|
|
14
14
|
cellId,
|
|
@@ -23,7 +23,7 @@ import { toast } from "@/components/ui/use-toast";
|
|
|
23
23
|
import { useCellData, useCellRuntime } from "@/core/cells/cells";
|
|
24
24
|
import { CellOutputId } from "@/core/cells/ids";
|
|
25
25
|
import { isOutputEmpty } from "@/core/cells/outputs";
|
|
26
|
-
import {
|
|
26
|
+
import { goToDefinitionWithLspFallback } from "@/core/codemirror/go-to-definition/utils";
|
|
27
27
|
import { sendToPanelManager } from "@/core/vscode/vscode-bindings";
|
|
28
28
|
import { copyImageToClipboard, copyToClipboard } from "@/utils/copy";
|
|
29
29
|
import { getImageExtension } from "@/utils/filenames";
|
|
@@ -170,7 +170,7 @@ export const CellActionsContextMenu = ({
|
|
|
170
170
|
// Only suppress focus restoration when we actually navigated;
|
|
171
171
|
// otherwise let Radix return focus to the trigger cell.
|
|
172
172
|
suppressCloseAutoFocus.current =
|
|
173
|
-
|
|
173
|
+
goToDefinitionWithLspFallback(editorView);
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
176
|
},
|
|
@@ -31,7 +31,7 @@ import { cn } from "@/utils/cn";
|
|
|
31
31
|
import { HideInKioskMode } from "../../kiosk-mode";
|
|
32
32
|
import { ContributeSnippetButton } from "../components/contribute-snippet-button";
|
|
33
33
|
import { usePanelOrientation, usePanelSection } from "./panel-context";
|
|
34
|
-
import {
|
|
34
|
+
import { getReadonlyCodeDisplay } from "@/core/cells/readonly-code-display";
|
|
35
35
|
|
|
36
36
|
const extensions = [EditorView.lineWrapping];
|
|
37
37
|
|
|
@@ -188,7 +188,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
|
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
const { language,
|
|
191
|
+
const { language, code: displayCode } = getReadonlyCodeDisplay(code);
|
|
192
192
|
|
|
193
193
|
return (
|
|
194
194
|
<div
|
|
@@ -216,7 +216,7 @@ const SnippetViewer: React.FC<{ snippet: Snippet; onClose: () => void }> = ({
|
|
|
216
216
|
language={language}
|
|
217
217
|
className="cm border rounded overflow-hidden"
|
|
218
218
|
extensions={extensions}
|
|
219
|
-
value={
|
|
219
|
+
value={displayCode}
|
|
220
220
|
readOnly={true}
|
|
221
221
|
/>
|
|
222
222
|
</Suspense>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
const getFeatureFlag = vi.fn<(flag: string) => boolean>();
|
|
6
|
+
|
|
7
|
+
vi.mock("@/core/config/feature-flag", () => ({
|
|
8
|
+
getFeatureFlag: (flag: string) => getFeatureFlag(flag),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { resolveAiPanelTab } = await import("../useOpenAiAssistant");
|
|
12
|
+
|
|
13
|
+
describe("resolveAiPanelTab", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
getFeatureFlag.mockReset();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("honors an explicit panel regardless of the feature flag", () => {
|
|
19
|
+
getFeatureFlag.mockReturnValue(false);
|
|
20
|
+
expect(resolveAiPanelTab("agents", "chat")).toBe("agents");
|
|
21
|
+
expect(resolveAiPanelTab("chat", "agents")).toBe("chat");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("uses the stored tab when external agents are enabled", () => {
|
|
25
|
+
getFeatureFlag.mockReturnValue(true);
|
|
26
|
+
expect(resolveAiPanelTab(undefined, "agents")).toBe("agents");
|
|
27
|
+
expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("falls back to chat when external agents are disabled", () => {
|
|
31
|
+
getFeatureFlag.mockReturnValue(false);
|
|
32
|
+
// A stale "agents" preference must not strand the prompt.
|
|
33
|
+
expect(resolveAiPanelTab(undefined, "agents")).toBe("chat");
|
|
34
|
+
expect(resolveAiPanelTab(undefined, "chat")).toBe("chat");
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,47 +1,17 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import { useAtomValue } from "jotai";
|
|
4
|
-
import { AlertCircleIcon } from "lucide-react";
|
|
5
3
|
import type React from "react";
|
|
6
|
-
import {
|
|
7
|
-
import { Tooltip } from "@/components/ui/tooltip";
|
|
8
|
-
import { wasmInitializationAtom, wasmInitStatusAtom } from "@/core/wasm/state";
|
|
4
|
+
import { RuntimeStatusBadge } from "@/components/lifecycle/RuntimeStatusBadge";
|
|
9
5
|
import { isWasm } from "@/core/wasm/utils";
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
|
-
* Footer indicator that surfaces Pyodide initialization progress.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Footer indicator that surfaces Pyodide initialization progress. Thin
|
|
9
|
+
* wrapper around `<RuntimeStatusBadge>` so the rest of the footer chrome
|
|
10
|
+
* doesn't need to know about adapters.
|
|
15
11
|
*/
|
|
16
12
|
export const PyodideStatus: React.FC = () => {
|
|
17
|
-
|
|
18
|
-
const message = useAtomValue(wasmInitializationAtom);
|
|
19
|
-
|
|
20
|
-
if (!isWasm() || status === "ready") {
|
|
13
|
+
if (!isWasm()) {
|
|
21
14
|
return null;
|
|
22
15
|
}
|
|
23
|
-
|
|
24
|
-
const icon =
|
|
25
|
-
status === "error" ? (
|
|
26
|
-
<AlertCircleIcon className="w-4 h-4 text-destructive" />
|
|
27
|
-
) : (
|
|
28
|
-
<Spinner size="small" />
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
const tooltip = status === "error" ? "Pyodide failed to initialize" : message;
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<Tooltip
|
|
35
|
-
content={<div className="text-sm whitespace-pre-line">{tooltip}</div>}
|
|
36
|
-
data-testid="footer-pyodide-status"
|
|
37
|
-
>
|
|
38
|
-
<div
|
|
39
|
-
className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
|
|
40
|
-
data-testid="pyodide-status"
|
|
41
|
-
>
|
|
42
|
-
{icon}
|
|
43
|
-
<span>Pyodide</span>
|
|
44
|
-
</div>
|
|
45
|
-
</Tooltip>
|
|
46
|
-
);
|
|
16
|
+
return <RuntimeStatusBadge />;
|
|
47
17
|
};
|
|
@@ -6,7 +6,9 @@ import { jotaiJsonStorage } from "@/utils/storage/jotai";
|
|
|
6
6
|
|
|
7
7
|
const AI_PANEL_TAB_KEY = "marimo:chrome:ai-panel-tab";
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
export type AiPanelTab = "chat" | "agents";
|
|
10
|
+
|
|
11
|
+
export const aiPanelTabAtom = atomWithStorage<AiPanelTab>(
|
|
10
12
|
AI_PANEL_TAB_KEY,
|
|
11
13
|
"chat",
|
|
12
14
|
jotaiJsonStorage,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { useSetAtom, useStore } from "jotai";
|
|
4
|
+
import useEvent from "react-use-event-hook";
|
|
5
|
+
import { agentSessionStateAtom } from "@/components/chat/acp/state";
|
|
6
|
+
import { toast } from "@/components/ui/use-toast";
|
|
7
|
+
import { useModelChange } from "@/core/ai/config";
|
|
8
|
+
import { pendingAiPromptAtom } from "@/core/ai/state";
|
|
9
|
+
import type { CopilotMode } from "@/core/ai/tools/registry";
|
|
10
|
+
import { aiModelConfiguredAtom } from "@/core/config/config";
|
|
11
|
+
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
12
|
+
import { Logger } from "@/utils/Logger";
|
|
13
|
+
import { useChromeActions } from "../state";
|
|
14
|
+
import { type AiPanelTab, aiPanelTabAtom, useAiPanelTab } from "./useAiPanel";
|
|
15
|
+
|
|
16
|
+
// Error fixes work best with kernel access, so we default the chat panel to
|
|
17
|
+
// code mode. The agents panel manages its own mode, so this is chat-only.
|
|
18
|
+
const FIX_CHAT_MODE: CopilotMode = "code_mode";
|
|
19
|
+
|
|
20
|
+
export interface OpenAiAssistantOptions {
|
|
21
|
+
prompt: string;
|
|
22
|
+
submit?: boolean;
|
|
23
|
+
/** Override the user's AI sidebar tab. When omitted, the stored tab is used. */
|
|
24
|
+
panel?: AiPanelTab;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Resolve which AI panel to open.
|
|
28
|
+
export function resolveAiPanelTab(
|
|
29
|
+
panel: AiPanelTab | undefined,
|
|
30
|
+
storedTab: AiPanelTab,
|
|
31
|
+
): AiPanelTab {
|
|
32
|
+
if (panel) {
|
|
33
|
+
return panel;
|
|
34
|
+
}
|
|
35
|
+
return getFeatureFlag("external_agents") ? storedTab : "chat";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Opens the AI assistant sidebar panel and delivers a prompt to it.
|
|
40
|
+
*
|
|
41
|
+
* If no `panel` is given, the user's configured tab is respected.
|
|
42
|
+
*/
|
|
43
|
+
export function useOpenAiAssistant() {
|
|
44
|
+
const { openApplication } = useChromeActions();
|
|
45
|
+
const { setAiPanelTab } = useAiPanelTab();
|
|
46
|
+
const { saveModeChange } = useModelChange();
|
|
47
|
+
const setPendingPrompt = useSetAtom(pendingAiPromptAtom);
|
|
48
|
+
const store = useStore();
|
|
49
|
+
|
|
50
|
+
return useEvent(async (opts: OpenAiAssistantOptions) => {
|
|
51
|
+
const tab = resolveAiPanelTab(opts.panel, store.get(aiPanelTabAtom));
|
|
52
|
+
|
|
53
|
+
const chatPanelReady = store.get(aiModelConfiguredAtom);
|
|
54
|
+
const agentsPanelReady =
|
|
55
|
+
getFeatureFlag("external_agents") &&
|
|
56
|
+
store.get(agentSessionStateAtom).sessions.length > 0;
|
|
57
|
+
const isReady = tab === "agents" ? agentsPanelReady : chatPanelReady;
|
|
58
|
+
|
|
59
|
+
if (!isReady) {
|
|
60
|
+
toast({
|
|
61
|
+
title: tab === "agents" ? "No agent session" : "AI not configured",
|
|
62
|
+
description:
|
|
63
|
+
tab === "agents"
|
|
64
|
+
? "Start an agent session or switch to the chat panel."
|
|
65
|
+
: "Configure an AI provider and chat model to fix with AI.",
|
|
66
|
+
variant: "danger",
|
|
67
|
+
});
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (opts.panel) {
|
|
72
|
+
setAiPanelTab(opts.panel);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (tab === "chat") {
|
|
76
|
+
await saveModeChange(FIX_CHAT_MODE).catch(() =>
|
|
77
|
+
Logger.error("Failed to save mode change"),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setPendingPrompt({
|
|
82
|
+
prompt: opts.prompt,
|
|
83
|
+
submit: opts.submit ?? false,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
openApplication("ai");
|
|
87
|
+
});
|
|
88
|
+
}
|