@marimo-team/islands 0.23.9-dev34 → 0.23.9-dev36
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-MJy-Ll40.js → ConnectedDataExplorerComponent-BQBH2XAd.js} +4 -4
- package/dist/assets/__vite-browser-external-TaZstNaH.js +1 -0
- package/dist/assets/{worker-BoAkAmaG.js → worker-CZaLU0G8.js} +2 -2
- package/dist/{chat-ui-CpX2YcGy.js → chat-ui-IyGT4sju.js} +6 -6
- package/dist/{code-visibility-y3APpJ-N.js → code-visibility-DP2xSfeW.js} +525 -410
- package/dist/{formats-BIKFEOlR.js → formats-B7_JC7Ba.js} +1 -1
- package/dist/{glide-data-editor-DjQd6fKp.js → glide-data-editor-BmM4MCbn.js} +2 -2
- package/dist/{html-to-image-QL7QveRm.js → html-to-image-CGbhD84m.js} +5 -5
- package/dist/{input-Dh0iMVFM.js → input-Ld3tUgdF.js} +1 -1
- package/dist/main.js +18 -18
- package/dist/{mermaid-CAibas-0.js → mermaid-BrUZ2PpQ.js} +2 -2
- package/dist/{process-output-C657UH7t.js → process-output-B8Cqiywi.js} +1 -1
- package/dist/{reveal-component-Cbw9hzrS.js → reveal-component-B7RA3HR2.js} +5 -5
- package/dist/{spec-BKuFJIDz.js → spec-nqxKYdNH.js} +1 -1
- package/dist/{toDate-BeKbrOvs.js → toDate-DLCQY32Y.js} +1 -1
- package/dist/{useAsyncData-yp6n17kh.js → useAsyncData-3f5sSgzf.js} +1 -1
- package/dist/{useDeepCompareMemoize-DJvAHUIC.js → useDeepCompareMemoize-Cu37j2QD.js} +1 -1
- package/dist/{useLifecycle-CsYXf0Ln.js → useLifecycle-DVkMZA_I.js} +1 -1
- package/dist/{useTheme-CK_R9Mn8.js → useTheme-DNcgchnA.js} +11 -2
- package/dist/{vega-component-ikfBfkZO.js → vega-component-7odw1pLZ.js} +5 -5
- package/package.json +1 -1
- package/src/components/app-config/ai-config.tsx +74 -15
- package/src/components/chat/chat-panel.tsx +2 -2
- package/src/components/data-table/__tests__/header-items.test.tsx +220 -10
- package/src/components/data-table/column-header.tsx +17 -12
- package/src/components/data-table/header-items.tsx +40 -16
- package/src/components/editor/actions/useCellActionButton.tsx +3 -3
- package/src/components/editor/cell/code/cell-editor.tsx +7 -4
- package/src/components/editor/chrome/types.ts +13 -6
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
- package/src/components/editor/chrome/wrapper/footer-items/ai-status.tsx +10 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +7 -5
- package/src/components/editor/errors/auto-fix.tsx +3 -3
- package/src/components/editor/navigation/__tests__/navigation.test.ts +15 -0
- package/src/components/editor/navigation/navigation.ts +5 -0
- package/src/components/editor/output/MarimoTracebackOutput.tsx +4 -3
- package/src/components/editor/renderers/cell-array.tsx +27 -24
- package/src/core/config/__tests__/config-schema.test.ts +2 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +16 -0
- package/dist/assets/__vite-browser-external-BBEFRPue.js +0 -1
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Column,
|
|
5
|
+
SortDirection,
|
|
6
|
+
SortingState,
|
|
7
|
+
Table,
|
|
8
|
+
} from "@tanstack/react-table";
|
|
4
9
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
5
10
|
import { describe, expect, it, vi } from "vitest";
|
|
6
11
|
import {
|
|
@@ -8,7 +13,23 @@ import {
|
|
|
8
13
|
DropdownMenuContent,
|
|
9
14
|
DropdownMenuTrigger,
|
|
10
15
|
} from "@/components/ui/dropdown-menu";
|
|
11
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
ColumnPinning,
|
|
18
|
+
ColumnWrapping,
|
|
19
|
+
CopyColumn,
|
|
20
|
+
DataType,
|
|
21
|
+
FormatOptions,
|
|
22
|
+
HideColumn,
|
|
23
|
+
Sorts,
|
|
24
|
+
} from "../header-items";
|
|
25
|
+
|
|
26
|
+
const renderInMenu = (node: React.ReactNode) =>
|
|
27
|
+
render(
|
|
28
|
+
<DropdownMenu open={true}>
|
|
29
|
+
<DropdownMenuTrigger />
|
|
30
|
+
<DropdownMenuContent>{node}</DropdownMenuContent>
|
|
31
|
+
</DropdownMenu>,
|
|
32
|
+
);
|
|
12
33
|
|
|
13
34
|
describe("multi-column sorting logic", () => {
|
|
14
35
|
// Extract the core sorting logic to test in isolation
|
|
@@ -167,14 +188,6 @@ describe("HideColumn", () => {
|
|
|
167
188
|
toggleVisibility,
|
|
168
189
|
}) as unknown as Column<unknown, unknown>;
|
|
169
190
|
|
|
170
|
-
const renderInMenu = (node: React.ReactNode) =>
|
|
171
|
-
render(
|
|
172
|
-
<DropdownMenu open={true}>
|
|
173
|
-
<DropdownMenuTrigger />
|
|
174
|
-
<DropdownMenuContent>{node}</DropdownMenuContent>
|
|
175
|
-
</DropdownMenu>,
|
|
176
|
-
);
|
|
177
|
-
|
|
178
191
|
it("renders 'Hide column' when canHide is true", () => {
|
|
179
192
|
renderInMenu(<HideColumn column={makeColumn()} />);
|
|
180
193
|
expect(screen.getByText("Hide column")).toBeInTheDocument();
|
|
@@ -192,3 +205,200 @@ describe("HideColumn", () => {
|
|
|
192
205
|
expect(toggleVisibility).toHaveBeenCalledWith(false);
|
|
193
206
|
});
|
|
194
207
|
});
|
|
208
|
+
|
|
209
|
+
describe("DataType", () => {
|
|
210
|
+
const makeColumn = (dtype?: string) =>
|
|
211
|
+
({
|
|
212
|
+
columnDef: { meta: dtype === undefined ? {} : { dtype } },
|
|
213
|
+
}) as unknown as Column<unknown, unknown>;
|
|
214
|
+
|
|
215
|
+
it("renders the dtype label when present", () => {
|
|
216
|
+
renderInMenu(<DataType column={makeColumn("int64")} />);
|
|
217
|
+
expect(screen.getByText("int64")).toBeInTheDocument();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("returns null when dtype is absent", () => {
|
|
221
|
+
renderInMenu(<DataType column={makeColumn()} />);
|
|
222
|
+
expect(screen.queryByText("int64")).toBeNull();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe("Sorts", () => {
|
|
227
|
+
const makeColumn = ({
|
|
228
|
+
canSort = true,
|
|
229
|
+
sorted = false,
|
|
230
|
+
sortIndex = 0,
|
|
231
|
+
}: {
|
|
232
|
+
canSort?: boolean;
|
|
233
|
+
sorted?: false | SortDirection;
|
|
234
|
+
sortIndex?: number;
|
|
235
|
+
} = {}) =>
|
|
236
|
+
({
|
|
237
|
+
getCanSort: () => canSort,
|
|
238
|
+
getIsSorted: () => sorted,
|
|
239
|
+
getSortIndex: () => sortIndex,
|
|
240
|
+
clearSorting: vi.fn(),
|
|
241
|
+
toggleSorting: vi.fn(),
|
|
242
|
+
}) as unknown as Column<unknown, unknown>;
|
|
243
|
+
|
|
244
|
+
const makeTable = (sorting: SortingState) =>
|
|
245
|
+
({
|
|
246
|
+
getState: () => ({ sorting }),
|
|
247
|
+
resetSorting: vi.fn(),
|
|
248
|
+
}) as unknown as Table<unknown>;
|
|
249
|
+
|
|
250
|
+
it("returns null when the column cannot sort", () => {
|
|
251
|
+
renderInMenu(<Sorts column={makeColumn({ canSort: false })} />);
|
|
252
|
+
expect(screen.queryByText("Asc")).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("renders Asc and Desc items", () => {
|
|
256
|
+
renderInMenu(<Sorts column={makeColumn()} />);
|
|
257
|
+
expect(screen.getByText("Asc")).toBeInTheDocument();
|
|
258
|
+
expect(screen.getByText("Desc")).toBeInTheDocument();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("offers single-column 'Clear sort' when sorted without multi-sort", () => {
|
|
262
|
+
renderInMenu(<Sorts column={makeColumn({ sorted: "asc" })} />);
|
|
263
|
+
expect(screen.getByText("Clear sort")).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("offers 'Clear all sorts' when the table has multiple sorts", () => {
|
|
267
|
+
renderInMenu(
|
|
268
|
+
<Sorts
|
|
269
|
+
column={makeColumn({ sorted: "asc" })}
|
|
270
|
+
table={makeTable([
|
|
271
|
+
{ id: "a", desc: false },
|
|
272
|
+
{ id: "b", desc: true },
|
|
273
|
+
])}
|
|
274
|
+
/>,
|
|
275
|
+
);
|
|
276
|
+
expect(screen.getByText("Clear all sorts")).toBeInTheDocument();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
describe("CopyColumn", () => {
|
|
281
|
+
const makeColumn = ({
|
|
282
|
+
canCopy = true,
|
|
283
|
+
id = "name",
|
|
284
|
+
}: {
|
|
285
|
+
canCopy?: boolean;
|
|
286
|
+
id?: string;
|
|
287
|
+
} = {}) =>
|
|
288
|
+
({
|
|
289
|
+
id,
|
|
290
|
+
getCanCopy: () => canCopy,
|
|
291
|
+
}) as unknown as Column<unknown, unknown>;
|
|
292
|
+
|
|
293
|
+
it("renders 'Copy column name' when copyable", () => {
|
|
294
|
+
renderInMenu(<CopyColumn column={makeColumn()} />);
|
|
295
|
+
expect(screen.getByText("Copy column name")).toBeInTheDocument();
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("returns null when the column cannot be copied", () => {
|
|
299
|
+
renderInMenu(<CopyColumn column={makeColumn({ canCopy: false })} />);
|
|
300
|
+
expect(screen.queryByText("Copy column name")).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("ColumnPinning", () => {
|
|
305
|
+
const makeColumn = ({
|
|
306
|
+
canPin = true,
|
|
307
|
+
pinned = false,
|
|
308
|
+
}: {
|
|
309
|
+
canPin?: boolean;
|
|
310
|
+
pinned?: false | "left" | "right";
|
|
311
|
+
} = {}) =>
|
|
312
|
+
({
|
|
313
|
+
getCanPin: () => canPin,
|
|
314
|
+
getIsPinned: () => pinned,
|
|
315
|
+
pin: vi.fn(),
|
|
316
|
+
}) as unknown as Column<unknown, unknown>;
|
|
317
|
+
|
|
318
|
+
it("returns null when the column cannot be pinned", () => {
|
|
319
|
+
renderInMenu(<ColumnPinning column={makeColumn({ canPin: false })} />);
|
|
320
|
+
expect(screen.queryByText("Freeze left")).toBeNull();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("offers freeze options when unpinned", () => {
|
|
324
|
+
renderInMenu(<ColumnPinning column={makeColumn()} />);
|
|
325
|
+
expect(screen.getByText("Freeze left")).toBeInTheDocument();
|
|
326
|
+
expect(screen.getByText("Freeze right")).toBeInTheDocument();
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("offers 'Unfreeze' when pinned", () => {
|
|
330
|
+
renderInMenu(<ColumnPinning column={makeColumn({ pinned: "left" })} />);
|
|
331
|
+
expect(screen.getByText("Unfreeze")).toBeInTheDocument();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe("ColumnWrapping", () => {
|
|
336
|
+
const makeColumn = ({
|
|
337
|
+
canWrap = true,
|
|
338
|
+
wrapping = "nowrap",
|
|
339
|
+
}: {
|
|
340
|
+
canWrap?: boolean;
|
|
341
|
+
wrapping?: "wrap" | "nowrap";
|
|
342
|
+
} = {}) =>
|
|
343
|
+
({
|
|
344
|
+
getCanWrap: () => canWrap,
|
|
345
|
+
getColumnWrapping: () => wrapping,
|
|
346
|
+
toggleColumnWrapping: vi.fn(),
|
|
347
|
+
}) as unknown as Column<unknown, unknown>;
|
|
348
|
+
|
|
349
|
+
it("returns null when the column cannot wrap", () => {
|
|
350
|
+
renderInMenu(<ColumnWrapping column={makeColumn({ canWrap: false })} />);
|
|
351
|
+
expect(screen.queryByText("Wrap text")).toBeNull();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("offers 'Wrap text' when not wrapping", () => {
|
|
355
|
+
renderInMenu(<ColumnWrapping column={makeColumn()} />);
|
|
356
|
+
expect(screen.getByText("Wrap text")).toBeInTheDocument();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("offers 'No wrap text' when wrapping", () => {
|
|
360
|
+
renderInMenu(<ColumnWrapping column={makeColumn({ wrapping: "wrap" })} />);
|
|
361
|
+
expect(screen.getByText("No wrap text")).toBeInTheDocument();
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe("FormatOptions", () => {
|
|
366
|
+
const makeColumn = ({
|
|
367
|
+
dataType = "number",
|
|
368
|
+
canFormat = true,
|
|
369
|
+
}: {
|
|
370
|
+
dataType?: string;
|
|
371
|
+
canFormat?: boolean;
|
|
372
|
+
} = {}) =>
|
|
373
|
+
({
|
|
374
|
+
columnDef: { meta: { dataType } },
|
|
375
|
+
getCanFormat: () => canFormat,
|
|
376
|
+
getColumnFormatting: () => undefined,
|
|
377
|
+
setColumnFormatting: vi.fn(),
|
|
378
|
+
}) as unknown as Column<unknown, unknown>;
|
|
379
|
+
|
|
380
|
+
it("renders the 'Format' submenu trigger for formattable columns", () => {
|
|
381
|
+
renderInMenu(<FormatOptions column={makeColumn()} locale="en-US" />);
|
|
382
|
+
expect(screen.getByText("Format")).toBeInTheDocument();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("returns null when the column cannot be formatted", () => {
|
|
386
|
+
renderInMenu(
|
|
387
|
+
<FormatOptions
|
|
388
|
+
column={makeColumn({ canFormat: false })}
|
|
389
|
+
locale="en-US"
|
|
390
|
+
/>,
|
|
391
|
+
);
|
|
392
|
+
expect(screen.queryByText("Format")).toBeNull();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("returns null when the data type has no format options", () => {
|
|
396
|
+
renderInMenu(
|
|
397
|
+
<FormatOptions
|
|
398
|
+
column={makeColumn({ dataType: "unknown" })}
|
|
399
|
+
locale="en-US"
|
|
400
|
+
/>,
|
|
401
|
+
);
|
|
402
|
+
expect(screen.queryByText("Format")).toBeNull();
|
|
403
|
+
});
|
|
404
|
+
});
|
|
@@ -17,14 +17,14 @@ import { useFilterEditor } from "./filter-editor-context";
|
|
|
17
17
|
import { EDITABLE_FILTER_TYPES, isMembershipFilterType } from "./filters";
|
|
18
18
|
import {
|
|
19
19
|
ClearFilterMenuItem,
|
|
20
|
+
ColumnPinning,
|
|
21
|
+
ColumnWrapping,
|
|
22
|
+
CopyColumn,
|
|
23
|
+
DataType,
|
|
24
|
+
FormatOptions,
|
|
20
25
|
HideColumn,
|
|
21
|
-
renderColumnPinning,
|
|
22
|
-
renderColumnWrapping,
|
|
23
|
-
renderCopyColumn,
|
|
24
|
-
renderDataType,
|
|
25
|
-
renderFormatOptions,
|
|
26
26
|
renderSortIcon,
|
|
27
|
-
|
|
27
|
+
Sorts,
|
|
28
28
|
} from "./header-items";
|
|
29
29
|
|
|
30
30
|
interface DataTableColumnHeaderProps<
|
|
@@ -36,6 +36,11 @@ interface DataTableColumnHeaderProps<
|
|
|
36
36
|
subheader?: React.ReactNode;
|
|
37
37
|
justify?: "left" | "center" | "right";
|
|
38
38
|
calculateTopKRows?: CalculateTopKRows;
|
|
39
|
+
/**
|
|
40
|
+
* Optional: only used to surface multi-column sort actions ("Clear all
|
|
41
|
+
* sorts"). Omitted by call sites that define their header inside column
|
|
42
|
+
* definitions, where the table instance isn't yet available.
|
|
43
|
+
*/
|
|
39
44
|
table?: Table<TData>;
|
|
40
45
|
}
|
|
41
46
|
|
|
@@ -119,12 +124,12 @@ export const DataTableColumnHeader = <TData, TValue>({
|
|
|
119
124
|
</button>
|
|
120
125
|
</DropdownMenuTrigger>
|
|
121
126
|
<DropdownMenuContent align="start">
|
|
122
|
-
{
|
|
123
|
-
{
|
|
124
|
-
{
|
|
125
|
-
{
|
|
126
|
-
{
|
|
127
|
-
{
|
|
127
|
+
<DataType column={column} />
|
|
128
|
+
<Sorts column={column} table={table} />
|
|
129
|
+
<CopyColumn column={column} />
|
|
130
|
+
<ColumnPinning column={column} />
|
|
131
|
+
<ColumnWrapping column={column} />
|
|
132
|
+
<FormatOptions column={column} locale={locale} />
|
|
128
133
|
<HideColumn column={column} />
|
|
129
134
|
{canEditFilter && <DropdownMenuSeparator />}
|
|
130
135
|
{canEditFilter && (
|
|
@@ -29,10 +29,13 @@ import { formattingExample } from "./column-formatting/feature";
|
|
|
29
29
|
import { formatOptions } from "./column-formatting/types";
|
|
30
30
|
import { NAMELESS_COLUMN_PREFIX } from "./columns";
|
|
31
31
|
|
|
32
|
-
export function
|
|
33
|
-
column
|
|
34
|
-
locale
|
|
35
|
-
|
|
32
|
+
export function FormatOptions<TData, TValue>({
|
|
33
|
+
column,
|
|
34
|
+
locale,
|
|
35
|
+
}: {
|
|
36
|
+
column: Column<TData, TValue>;
|
|
37
|
+
locale: string;
|
|
38
|
+
}) {
|
|
36
39
|
const dataType: DataType | undefined = column.columnDef.meta?.dataType;
|
|
37
40
|
const columnFormatOptions = dataType ? formatOptions[dataType] : [];
|
|
38
41
|
|
|
@@ -83,9 +86,11 @@ export function renderFormatOptions<TData, TValue>(
|
|
|
83
86
|
);
|
|
84
87
|
}
|
|
85
88
|
|
|
86
|
-
export function
|
|
87
|
-
column
|
|
88
|
-
|
|
89
|
+
export function ColumnWrapping<TData, TValue>({
|
|
90
|
+
column,
|
|
91
|
+
}: {
|
|
92
|
+
column: Column<TData, TValue>;
|
|
93
|
+
}) {
|
|
89
94
|
if (!column.getCanWrap?.() || !column.getColumnWrapping) {
|
|
90
95
|
return null;
|
|
91
96
|
}
|
|
@@ -108,9 +113,11 @@ export function renderColumnWrapping<TData, TValue>(
|
|
|
108
113
|
);
|
|
109
114
|
}
|
|
110
115
|
|
|
111
|
-
export function
|
|
112
|
-
column
|
|
113
|
-
|
|
116
|
+
export function ColumnPinning<TData, TValue>({
|
|
117
|
+
column,
|
|
118
|
+
}: {
|
|
119
|
+
column: Column<TData, TValue>;
|
|
120
|
+
}) {
|
|
114
121
|
if (!column.getCanPin?.() || !column.getIsPinned) {
|
|
115
122
|
return null;
|
|
116
123
|
}
|
|
@@ -157,7 +164,11 @@ export function HideColumn<TData, TValue>({
|
|
|
157
164
|
);
|
|
158
165
|
}
|
|
159
166
|
|
|
160
|
-
export function
|
|
167
|
+
export function CopyColumn<TData, TValue>({
|
|
168
|
+
column,
|
|
169
|
+
}: {
|
|
170
|
+
column: Column<TData, TValue>;
|
|
171
|
+
}) {
|
|
161
172
|
if (!column.getCanCopy?.()) {
|
|
162
173
|
return null;
|
|
163
174
|
}
|
|
@@ -177,10 +188,19 @@ export function renderCopyColumn<TData, TValue>(column: Column<TData, TValue>) {
|
|
|
177
188
|
const AscIcon = ArrowUpNarrowWideIcon;
|
|
178
189
|
const DescIcon = ArrowDownWideNarrowIcon;
|
|
179
190
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
)
|
|
191
|
+
/**
|
|
192
|
+
* `table` is optional: it is only needed to detect multi-column sorting and
|
|
193
|
+
* offer "Clear all sorts". Call sites that build their header inside column
|
|
194
|
+
* definitions (where the table instance isn't yet in scope) omit it and fall
|
|
195
|
+
* back to single-column "Clear sort".
|
|
196
|
+
*/
|
|
197
|
+
export function Sorts<TData, TValue>({
|
|
198
|
+
column,
|
|
199
|
+
table,
|
|
200
|
+
}: {
|
|
201
|
+
column: Column<TData, TValue>;
|
|
202
|
+
table?: Table<TData>;
|
|
203
|
+
}) {
|
|
184
204
|
if (!column.getCanSort()) {
|
|
185
205
|
return null;
|
|
186
206
|
}
|
|
@@ -271,7 +291,11 @@ export function renderSortIcon<TData, TValue>(column: Column<TData, TValue>) {
|
|
|
271
291
|
return <Icon className="h-3 w-3" />;
|
|
272
292
|
}
|
|
273
293
|
|
|
274
|
-
export function
|
|
294
|
+
export function DataType<TData, TValue>({
|
|
295
|
+
column,
|
|
296
|
+
}: {
|
|
297
|
+
column: Column<TData, TValue>;
|
|
298
|
+
}) {
|
|
275
299
|
const dtype: string | undefined = column.columnDef.meta?.dtype;
|
|
276
300
|
if (!dtype) {
|
|
277
301
|
return null;
|
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
import { switchLanguage } from "@/core/codemirror/language/extension";
|
|
49
49
|
import { MARKDOWN_INITIAL_HIDE_CODE } from "@/core/codemirror/language/languages/markdown";
|
|
50
50
|
import {
|
|
51
|
-
|
|
51
|
+
aiFeaturesEnabledAtom,
|
|
52
52
|
appWidthAtom,
|
|
53
53
|
autoInstantiateAtom,
|
|
54
54
|
} from "@/core/config/config";
|
|
@@ -100,7 +100,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
|
|
|
100
100
|
const deleteCell = useDeleteCellCallback();
|
|
101
101
|
const { openModal } = useImperativeModal();
|
|
102
102
|
const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
|
|
103
|
-
const
|
|
103
|
+
const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
|
|
104
104
|
const autoInstantiate = useAtomValue(autoInstantiateAtom);
|
|
105
105
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
106
106
|
const appWidth = useAtomValue(appWidthAtom);
|
|
@@ -162,7 +162,7 @@ export function useCellActionButtons({ cell, closePopover }: Props) {
|
|
|
162
162
|
{
|
|
163
163
|
icon: <SparklesIcon size={13} strokeWidth={1.5} />,
|
|
164
164
|
label: "Refactor with AI",
|
|
165
|
-
hidden: !
|
|
165
|
+
hidden: !aiFeaturesEnabled,
|
|
166
166
|
handle: () => {
|
|
167
167
|
setAiCompletionCell((current) =>
|
|
168
168
|
current?.cellId === cellId ? null : { cellId },
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
connectedDocAtom,
|
|
27
27
|
realTimeCollaboration,
|
|
28
28
|
} from "@/core/codemirror/rtc/extension";
|
|
29
|
-
import { autoInstantiateAtom,
|
|
29
|
+
import { autoInstantiateAtom, isAiFeatureEnabled } from "@/core/config/config";
|
|
30
30
|
import type { UserConfig } from "@/core/config/config-schema";
|
|
31
31
|
import { OverridingHotkeyProvider } from "@/core/hotkeys/hotkeys";
|
|
32
32
|
import { connectionAtom } from "@/core/network/connection";
|
|
@@ -173,13 +173,13 @@ const CellEditorInternal = ({
|
|
|
173
173
|
});
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
-
const
|
|
176
|
+
const aiFeaturesEnabled = isAiFeatureEnabled(userConfig);
|
|
177
177
|
|
|
178
178
|
const extensions = useMemo(() => {
|
|
179
179
|
const extensions = setupCodeMirror({
|
|
180
180
|
cellId,
|
|
181
181
|
showPlaceholder,
|
|
182
|
-
enableAI:
|
|
182
|
+
enableAI: aiFeaturesEnabled,
|
|
183
183
|
cellActions: {
|
|
184
184
|
...cellActions,
|
|
185
185
|
afterToggleMarkdown,
|
|
@@ -201,6 +201,9 @@ const CellEditorInternal = ({
|
|
|
201
201
|
splitCell,
|
|
202
202
|
toggleHideCode,
|
|
203
203
|
aiCellCompletion: () => {
|
|
204
|
+
if (!aiFeaturesEnabled) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
204
207
|
let closed = false;
|
|
205
208
|
setAiCompletionCell((v) => {
|
|
206
209
|
// Toggle close
|
|
@@ -271,7 +274,7 @@ const CellEditorInternal = ({
|
|
|
271
274
|
userConfig.display,
|
|
272
275
|
userConfig.diagnostics,
|
|
273
276
|
userConfig.ai?.inline_tooltip,
|
|
274
|
-
|
|
277
|
+
aiFeaturesEnabled,
|
|
275
278
|
theme,
|
|
276
279
|
showPlaceholder,
|
|
277
280
|
cellActions,
|
|
@@ -201,16 +201,23 @@ export const PANEL_MAP = new Map<PanelType, PanelDescriptor>(
|
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
/**
|
|
204
|
-
* Check if a panel should be hidden based on its
|
|
205
|
-
* and `requiredCapability`.
|
|
204
|
+
* Check if a panel should be hidden based on its descriptor and runtime state.
|
|
206
205
|
*/
|
|
207
|
-
export function isPanelHidden(
|
|
208
|
-
panel
|
|
209
|
-
capabilities
|
|
210
|
-
|
|
206
|
+
export function isPanelHidden({
|
|
207
|
+
panel,
|
|
208
|
+
capabilities,
|
|
209
|
+
aiEnabled,
|
|
210
|
+
}: {
|
|
211
|
+
panel: PanelDescriptor;
|
|
212
|
+
capabilities: Capabilities;
|
|
213
|
+
aiEnabled: boolean;
|
|
214
|
+
}): boolean {
|
|
211
215
|
if (panel.hidden) {
|
|
212
216
|
return true;
|
|
213
217
|
}
|
|
218
|
+
if (panel.type === "ai" && !aiEnabled) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
214
221
|
if (panel.requiredCapability && !capabilities[panel.requiredCapability]) {
|
|
215
222
|
return true;
|
|
216
223
|
}
|
|
@@ -27,6 +27,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
27
27
|
import { LazyActivity } from "@/components/utils/lazy-mount";
|
|
28
28
|
import { cellErrorCount } from "@/core/cells/cells";
|
|
29
29
|
import { capabilitiesAtom } from "@/core/config/capabilities";
|
|
30
|
+
import { aiEnabledAtom } from "@/core/config/config";
|
|
30
31
|
import { getFeatureFlag } from "@/core/config/feature-flag";
|
|
31
32
|
import { cn } from "@/utils/cn";
|
|
32
33
|
import { ErrorBoundary } from "../../boundary/ErrorBoundary";
|
|
@@ -93,18 +94,19 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
93
94
|
const [panelLayout, setPanelLayout] = useAtom(panelLayoutAtom);
|
|
94
95
|
// Subscribe to capabilities to re-render when they change (e.g., terminal capability)
|
|
95
96
|
const capabilities = useAtomValue(capabilitiesAtom);
|
|
97
|
+
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
96
98
|
|
|
97
99
|
// Convert current developer panel items to PanelDescriptors
|
|
98
100
|
// Filter out hidden panels (e.g., terminal when capability is not available)
|
|
99
101
|
const devPanelItems = useMemo(() => {
|
|
100
102
|
return panelLayout.developerPanel.flatMap((id) => {
|
|
101
103
|
const panel = PANEL_MAP.get(id);
|
|
102
|
-
if (!panel || isPanelHidden(panel, capabilities)) {
|
|
104
|
+
if (!panel || isPanelHidden({ panel, capabilities, aiEnabled })) {
|
|
103
105
|
return [];
|
|
104
106
|
}
|
|
105
107
|
return [panel];
|
|
106
108
|
});
|
|
107
|
-
}, [panelLayout.developerPanel, capabilities]);
|
|
109
|
+
}, [panelLayout.developerPanel, capabilities, aiEnabled]);
|
|
108
110
|
|
|
109
111
|
const handleSetDevPanelItems = (items: PanelDescriptor[]) => {
|
|
110
112
|
setPanelLayout((prev) => ({
|
|
@@ -141,7 +143,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
141
143
|
const availableDevPanels = useMemo(() => {
|
|
142
144
|
const sidebarIds = new Set(panelLayout.sidebar);
|
|
143
145
|
return PANELS.filter((p) => {
|
|
144
|
-
if (isPanelHidden(p, capabilities)) {
|
|
146
|
+
if (isPanelHidden({ panel: p, capabilities, aiEnabled })) {
|
|
145
147
|
return false;
|
|
146
148
|
}
|
|
147
149
|
// Exclude panels that are in the sidebar
|
|
@@ -150,7 +152,7 @@ export const AppChrome: React.FC<PropsWithChildren> = ({ children }) => {
|
|
|
150
152
|
}
|
|
151
153
|
return true;
|
|
152
154
|
});
|
|
153
|
-
}, [panelLayout.sidebar, capabilities]);
|
|
155
|
+
}, [panelLayout.sidebar, capabilities, aiEnabled]);
|
|
154
156
|
|
|
155
157
|
const emitResizeEvent = useEvent(() => {
|
|
156
158
|
// HACK: Unfortunately, we have to do this twice to make sure the
|
|
@@ -4,18 +4,27 @@ import { useAtomValue } from "jotai";
|
|
|
4
4
|
import { SparklesIcon } from "lucide-react";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { useOpenSettingsToTab } from "@/components/app-config/state";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
aiAtom,
|
|
9
|
+
aiEnabledAtom,
|
|
10
|
+
aiModelConfiguredAtom,
|
|
11
|
+
} from "@/core/config/config";
|
|
8
12
|
import { DEFAULT_AI_MODEL } from "@/core/config/config-schema";
|
|
9
13
|
import { FooterItem } from "../footer-item";
|
|
10
14
|
|
|
11
15
|
export const AIStatusIcon: React.FC = () => {
|
|
12
16
|
const ai = useAtomValue(aiAtom);
|
|
13
17
|
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
18
|
+
const aiModelConfigured = useAtomValue(aiModelConfiguredAtom);
|
|
14
19
|
const chatModel = ai?.models?.chat_model || DEFAULT_AI_MODEL;
|
|
15
20
|
const editModel = ai?.models?.edit_model || chatModel;
|
|
16
21
|
const { handleClick } = useOpenSettingsToTab();
|
|
17
22
|
|
|
18
23
|
if (!aiEnabled) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!aiModelConfigured) {
|
|
19
28
|
return (
|
|
20
29
|
<FooterItem
|
|
21
30
|
tooltip="Assist is disabled"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
notebookQueuedOrRunningCountAtom,
|
|
13
13
|
} from "@/core/cells/cells";
|
|
14
14
|
import { capabilitiesAtom } from "@/core/config/capabilities";
|
|
15
|
+
import { aiEnabledAtom } from "@/core/config/config";
|
|
15
16
|
import { cn } from "@/utils/cn";
|
|
16
17
|
import { FeedbackButton } from "../components/feedback-button";
|
|
17
18
|
import { panelLayoutAtom, useChromeActions, useChromeState } from "../state";
|
|
@@ -30,6 +31,7 @@ export const Sidebar: React.FC = () => {
|
|
|
30
31
|
const [panelLayout, setPanelLayout] = useAtom(panelLayoutAtom);
|
|
31
32
|
// Subscribe to capabilities to re-render when they change
|
|
32
33
|
const capabilities = useAtomValue(capabilitiesAtom);
|
|
34
|
+
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
33
35
|
|
|
34
36
|
const renderIcon = ({ Icon }: PanelDescriptor, className?: string) => {
|
|
35
37
|
return <Icon className={cn("h-5 w-5", className)} />;
|
|
@@ -40,7 +42,7 @@ export const Sidebar: React.FC = () => {
|
|
|
40
42
|
const availableSidebarPanels = useMemo(() => {
|
|
41
43
|
const devPanelIds = new Set(panelLayout.developerPanel);
|
|
42
44
|
return PANELS.filter((p) => {
|
|
43
|
-
if (isPanelHidden(p, capabilities)) {
|
|
45
|
+
if (isPanelHidden({ panel: p, capabilities, aiEnabled })) {
|
|
44
46
|
return false;
|
|
45
47
|
}
|
|
46
48
|
// Exclude panels that are in the developer panel
|
|
@@ -49,19 +51,19 @@ export const Sidebar: React.FC = () => {
|
|
|
49
51
|
}
|
|
50
52
|
return true;
|
|
51
53
|
});
|
|
52
|
-
}, [panelLayout.developerPanel, capabilities]);
|
|
54
|
+
}, [panelLayout.developerPanel, capabilities, aiEnabled]);
|
|
53
55
|
|
|
54
56
|
// Convert current sidebar items to PanelDescriptors
|
|
55
57
|
// Filter out hidden panels (e.g., when capability is not available)
|
|
56
58
|
const sidebarItems = useMemo(() => {
|
|
57
59
|
return panelLayout.sidebar.flatMap((id) => {
|
|
58
60
|
const panel = PANEL_MAP.get(id);
|
|
59
|
-
if (!panel || isPanelHidden(panel, capabilities)) {
|
|
61
|
+
if (!panel || isPanelHidden({ panel, capabilities, aiEnabled })) {
|
|
60
62
|
return [];
|
|
61
63
|
}
|
|
62
64
|
return [panel];
|
|
63
65
|
});
|
|
64
|
-
}, [panelLayout.sidebar, capabilities]);
|
|
66
|
+
}, [panelLayout.sidebar, capabilities, aiEnabled]);
|
|
65
67
|
|
|
66
68
|
const handleSetSidebarItems = (items: PanelDescriptor[]) => {
|
|
67
69
|
setPanelLayout((prev) => ({
|
|
@@ -218,7 +220,7 @@ const SidebarItem: React.FC<
|
|
|
218
220
|
// Render as div when not clickable (e.g., inside ReorderableList)
|
|
219
221
|
// This avoids nested interactive elements which break react-aria's drag behavior
|
|
220
222
|
const content = onClick ? (
|
|
221
|
-
<button className={itemClassName} onClick={onClick}>
|
|
223
|
+
<button type="button" className={itemClassName} onClick={onClick}>
|
|
222
224
|
{children}
|
|
223
225
|
</button>
|
|
224
226
|
) : (
|
|
@@ -13,7 +13,7 @@ import { Tooltip } from "@/components/ui/tooltip";
|
|
|
13
13
|
import { aiCompletionCellAtom } from "@/core/ai/state";
|
|
14
14
|
import { notebookAtom, useCellActions } from "@/core/cells/cells";
|
|
15
15
|
import type { CellId } from "@/core/cells/ids";
|
|
16
|
-
import {
|
|
16
|
+
import { aiFeaturesEnabledAtom } from "@/core/config/config";
|
|
17
17
|
import { getAutoFixes } from "@/core/errors/errors";
|
|
18
18
|
import type { MarimoError } from "@/core/kernel/messages";
|
|
19
19
|
import { cn } from "@/utils/cn";
|
|
@@ -30,9 +30,9 @@ export const AutoFixButton = ({
|
|
|
30
30
|
}) => {
|
|
31
31
|
const store = useStore();
|
|
32
32
|
const { createNewCell } = useCellActions();
|
|
33
|
-
const
|
|
33
|
+
const aiFeaturesEnabled = useAtomValue(aiFeaturesEnabledAtom);
|
|
34
34
|
const autoFixes = errors.flatMap((error) =>
|
|
35
|
-
getAutoFixes(error, { aiEnabled }),
|
|
35
|
+
getAutoFixes(error, { aiEnabled: aiFeaturesEnabled }),
|
|
36
36
|
);
|
|
37
37
|
const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
|
|
38
38
|
|
|
@@ -1207,6 +1207,21 @@ describe("useCellNavigationProps", () => {
|
|
|
1207
1207
|
});
|
|
1208
1208
|
|
|
1209
1209
|
describe("AI completion functionality", () => {
|
|
1210
|
+
beforeEach(() => {
|
|
1211
|
+
const config = defaultUserConfig();
|
|
1212
|
+
store.set(userConfigAtom, {
|
|
1213
|
+
...config,
|
|
1214
|
+
ai: {
|
|
1215
|
+
...config.ai,
|
|
1216
|
+
models: {
|
|
1217
|
+
displayed_models: [],
|
|
1218
|
+
custom_models: [],
|
|
1219
|
+
edit_model: "openai/gpt-4o",
|
|
1220
|
+
},
|
|
1221
|
+
},
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1210
1225
|
it("should toggle AI completion when shortcut is pressed", () => {
|
|
1211
1226
|
const { result } = renderWithProvider(() =>
|
|
1212
1227
|
useCellNavigationProps(cellId1, optionsWithMockEditor),
|