@marimo-team/islands 0.21.2-dev12 → 0.21.2-dev14

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/main.js CHANGED
@@ -29325,6 +29325,8 @@ ${c.sqlString}
29325
29325
  var NotebookLanguageServerClient = (_a = class {
29326
29326
  constructor(e, r, c = defaultGetNotebookEditors) {
29327
29327
  __publicField(this, "completionItemCache", new LRUCache(10));
29328
+ __publicField(this, "latestDiagnosticsVersion", null);
29329
+ __publicField(this, "forwardedDiagnosticsVersion", 0);
29328
29330
  this.documentUri = getLSPDocument(), this.getNotebookEditors = c, this.initialSettings = r, this.client = e, this.patchProcessNotification(), this.initializePromise.then(() => {
29329
29331
  invariant(isClientWithNotify(this.client), "notify is not a method on the client"), this.client.notify("workspace/didChangeConfiguration", {
29330
29332
  settings: r
@@ -29372,7 +29374,7 @@ ${c.sqlString}
29372
29374
  settings: this.initialSettings
29373
29375
  });
29374
29376
  let { lens: e, version: r } = this.snapshotter.snapshot();
29375
- await this.client.textDocumentDidOpen({
29377
+ this.latestDiagnosticsVersion = null, this.forwardedDiagnosticsVersion = 0, await this.client.textDocumentDidOpen({
29376
29378
  textDocument: {
29377
29379
  languageId: "python",
29378
29380
  text: e.mergedText,
@@ -29570,39 +29572,49 @@ ${c.sqlString}
29570
29572
  invariant("processNotification" in this.client, "processNotification is not a method on the client");
29571
29573
  let r = this.client.processNotification.bind(this.client), c = (c2) => {
29572
29574
  if (c2.method === "textDocument/publishDiagnostics") {
29575
+ let d = c2.params.version;
29576
+ if (d != null) {
29577
+ let e = this.latestDiagnosticsVersion;
29578
+ if (e !== null && Number.isFinite(d) && d < e) {
29579
+ Logger.debug("[lsp] dropping stale diagnostics notification", c2);
29580
+ return;
29581
+ }
29582
+ this.latestDiagnosticsVersion = d;
29583
+ }
29573
29584
  Logger.debug("[lsp] handling diagnostics", c2);
29574
- let d = this.snapshotter.getLatestSnapshot(), f = c2.params.diagnostics, { lens: _, version: v } = d, y = /* @__PURE__ */ new Map();
29575
- for (let e of f) for (let r2 of _.cellIds) if (_.isInRange(e.range, r2)) {
29576
- y.has(r2) || y.set(r2, []);
29585
+ let f = this.snapshotter.getLatestSnapshot(), _ = c2.params.diagnostics, { lens: v } = f, y = ++this.forwardedDiagnosticsVersion, S = /* @__PURE__ */ new Map();
29586
+ for (let e of _) for (let r2 of v.cellIds) if (v.isInRange(e.range, r2)) {
29587
+ S.has(r2) || S.set(r2, []);
29577
29588
  let c3 = {
29578
29589
  ...e,
29579
- range: _.reverseRange(e.range, r2)
29590
+ range: v.reverseRange(e.range, r2)
29580
29591
  };
29581
- y.get(r2).push(c3);
29592
+ S.get(r2).push(c3);
29582
29593
  break;
29583
29594
  }
29584
- let S = new Set(_.cellIds);
29585
- _a.pruneSeenCellUris(S);
29586
- let w = new Set(_a.SEEN_CELL_DOCUMENT_URIS);
29587
- for (let [e, d2] of y.entries()) {
29595
+ let w = new Set(v.cellIds);
29596
+ _a.pruneSeenCellUris(w);
29597
+ let E = new Set(_a.SEEN_CELL_DOCUMENT_URIS);
29598
+ for (let [e, d2] of S.entries()) {
29588
29599
  Logger.debug("[lsp] diagnostics for cell", e, d2);
29589
29600
  let f2 = CellDocumentUri.of(e);
29590
- w.delete(f2), r({
29601
+ E.delete(f2), r({
29591
29602
  ...c2,
29592
29603
  params: {
29593
29604
  ...c2.params,
29594
29605
  uri: f2,
29595
- version: v,
29606
+ version: y,
29596
29607
  diagnostics: d2
29597
29608
  }
29598
29609
  });
29599
29610
  }
29600
- if (w.size > 0) {
29601
- Logger.debug("[lsp] clearing diagnostics", w);
29602
- for (let e of w) r({
29611
+ if (E.size > 0) {
29612
+ Logger.debug("[lsp] clearing diagnostics", E);
29613
+ for (let e of E) r({
29603
29614
  method: "textDocument/publishDiagnostics",
29604
29615
  params: {
29605
29616
  uri: e,
29617
+ version: y,
29606
29618
  diagnostics: []
29607
29619
  }
29608
29620
  });
@@ -70786,7 +70798,7 @@ Image URL: ${r.imageUrl}`)), contextToXml({
70786
70798
  return Logger.warn("Failed to get version from mount config"), null;
70787
70799
  }
70788
70800
  }
70789
- const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.21.2-dev12"), showCodeInRunModeAtom = atom(true);
70801
+ const marimoVersionAtom = atom(getVersionFromMountConfig() || "0.21.2-dev14"), showCodeInRunModeAtom = atom(true);
70790
70802
  atom(null);
70791
70803
  var import_compiler_runtime$89 = require_compiler_runtime();
70792
70804
  function useKeydownOnElement(e, r) {
@@ -93091,7 +93103,7 @@ ${c}
93091
93103
  });
93092
93104
  }
93093
93105
  }, SliderComponent = (e) => {
93094
- let r = (0, import_compiler_runtime$26.c)(55), { label: c, setValue: d, value: f, start: _, stop: v, step: y, debounce: S, orientation: w, showValue: E, fullWidth: O, valueMap: M, includeInput: I, disabled: z } = e, G = (0, import_react.useId)(), { locale: q } = $18f2051aff69b9bf$export$43bb16f9c6d9e3f7(), [IY, LY] = (0, import_react.useState)(f), RY, zY;
93106
+ let r = (0, import_compiler_runtime$26.c)(54), { label: c, setValue: d, value: f, start: _, stop: v, step: y, debounce: S, orientation: w, showValue: E, fullWidth: O, valueMap: M, includeInput: I, disabled: z } = e, G = (0, import_react.useId)(), { locale: q } = $18f2051aff69b9bf$export$43bb16f9c6d9e3f7(), [IY, LY] = (0, import_react.useState)(f), RY, zY;
93095
93107
  r[0] === f ? (RY = r[1], zY = r[2]) : (RY = () => {
93096
93108
  LY(f);
93097
93109
  }, zY = [
@@ -93137,10 +93149,10 @@ ${c}
93137
93149
  })
93138
93150
  }), r[27] = IY, r[28] = q, r[29] = E, r[30] = M, r[31] = ZY) : ZY = r[31];
93139
93151
  let QY;
93140
- r[32] !== S || r[33] !== z || r[34] !== I || r[35] !== IY || r[36] !== c || r[37] !== d || r[38] !== _ || r[39] !== y || r[40] !== v || r[41] !== M ? (QY = I && (0, import_jsx_runtime.jsx)(NumberField, {
93152
+ r[32] !== z || r[33] !== I || r[34] !== IY || r[35] !== c || r[36] !== d || r[37] !== _ || r[38] !== y || r[39] !== v || r[40] !== M ? (QY = I && (0, import_jsx_runtime.jsx)(NumberField, {
93141
93153
  value: M(IY),
93142
93154
  onChange: (e2) => {
93143
- (e2 == null || Number.isNaN(e2)) && (e2 = Number(_)), LY(e2), S || d(e2);
93155
+ (e2 == null || Number.isNaN(e2)) && (e2 = Number(_)), LY(e2), d(e2);
93144
93156
  },
93145
93157
  minValue: _,
93146
93158
  maxValue: v,
@@ -93148,25 +93160,25 @@ ${c}
93148
93160
  className: "w-24",
93149
93161
  "aria-label": `${c || "Slider"} value input`,
93150
93162
  isDisabled: z
93151
- }), r[32] = S, r[33] = z, r[34] = I, r[35] = IY, r[36] = c, r[37] = d, r[38] = _, r[39] = y, r[40] = v, r[41] = M, r[42] = QY) : QY = r[42];
93163
+ }), r[32] = z, r[33] = I, r[34] = IY, r[35] = c, r[36] = d, r[37] = _, r[38] = y, r[39] = v, r[40] = M, r[41] = QY) : QY = r[41];
93152
93164
  let $Y;
93153
- r[43] !== XY || r[44] !== ZY || r[45] !== QY || r[46] !== WY ? ($Y = (0, import_jsx_runtime.jsxs)("div", {
93165
+ r[42] !== XY || r[43] !== ZY || r[44] !== QY || r[45] !== WY ? ($Y = (0, import_jsx_runtime.jsxs)("div", {
93154
93166
  className: WY,
93155
93167
  children: [
93156
93168
  XY,
93157
93169
  ZY,
93158
93170
  QY
93159
93171
  ]
93160
- }), r[43] = XY, r[44] = ZY, r[45] = QY, r[46] = WY, r[47] = $Y) : $Y = r[47];
93172
+ }), r[42] = XY, r[43] = ZY, r[44] = QY, r[45] = WY, r[46] = $Y) : $Y = r[46];
93161
93173
  let eX;
93162
- return r[48] !== O || r[49] !== G || r[50] !== c || r[51] !== $Y || r[52] !== BY || r[53] !== HY ? (eX = (0, import_jsx_runtime.jsx)(Labeled, {
93174
+ return r[47] !== O || r[48] !== G || r[49] !== c || r[50] !== $Y || r[51] !== BY || r[52] !== HY ? (eX = (0, import_jsx_runtime.jsx)(Labeled, {
93163
93175
  label: c,
93164
93176
  id: G,
93165
93177
  align: BY,
93166
93178
  fullWidth: O,
93167
93179
  className: HY,
93168
93180
  children: $Y
93169
- }), r[48] = O, r[49] = G, r[50] = c, r[51] = $Y, r[52] = BY, r[53] = HY, r[54] = eX) : eX = r[54], eX;
93181
+ }), r[47] = O, r[48] = G, r[49] = c, r[50] = $Y, r[51] = BY, r[52] = HY, r[53] = eX) : eX = r[53], eX;
93170
93182
  }, import_compiler_runtime$25 = require_compiler_runtime(), SwitchPlugin = class {
93171
93183
  constructor() {
93172
93184
  __publicField(this, "tagName", "marimo-switch");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.21.2-dev12",
3
+ "version": "0.21.2-dev14",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { zodResolver } from "@hookform/resolvers/zod";
4
4
  import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
5
+ import { merge } from "lodash-es";
5
6
  import {
6
7
  AlertTriangleIcon,
7
8
  BrainIcon,
@@ -287,10 +288,10 @@ export const UserConfigForm: React.FC = () => {
287
288
  dirtyValues.ai = setAiModels(values.ai, dirtyValues.ai);
288
289
  }
289
290
 
290
- await saveUserConfig({ config: dirtyValues }).then(() => {
291
- // Update local state with form values
292
- setConfig((prev) => ({ ...prev, ...values }));
293
- });
291
+ await saveUserConfig({ config: dirtyValues });
292
+ // Only apply the changed keys; this avoids stale request responses
293
+ // overwriting newer config changes.
294
+ setConfig((prev) => merge({}, prev, dirtyValues));
294
295
  };
295
296
  const onSubmit = useDebouncedCallback(onSubmitNotDebounced, FORM_DEBOUNCE);
296
297
 
@@ -178,6 +178,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
178
178
  string,
179
179
  Promise<LSP.CompletionItem>
180
180
  >(10);
181
+ private latestDiagnosticsVersion: number | null = null;
182
+ private forwardedDiagnosticsVersion = 0;
181
183
 
182
184
  constructor(
183
185
  client: ILanguageServerClient,
@@ -270,6 +272,8 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
270
272
 
271
273
  // Get the current document state
272
274
  const { lens, version } = this.snapshotter.snapshot();
275
+ this.latestDiagnosticsVersion = null;
276
+ this.forwardedDiagnosticsVersion = 0;
273
277
 
274
278
  // Re-open the merged document with the LSP server
275
279
  // This sends a textDocument/didOpen for the entire notebook
@@ -768,13 +772,34 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
768
772
  | { method: "other"; params: unknown },
769
773
  ) => {
770
774
  if (notification.method === "textDocument/publishDiagnostics") {
775
+ const incomingVersion = notification.params.version;
776
+ if (incomingVersion != null) {
777
+ const latestVersion = this.latestDiagnosticsVersion;
778
+ if (
779
+ latestVersion !== null &&
780
+ Number.isFinite(incomingVersion) &&
781
+ incomingVersion < latestVersion
782
+ ) {
783
+ Logger.debug(
784
+ "[lsp] dropping stale diagnostics notification",
785
+ notification,
786
+ );
787
+ return;
788
+ }
789
+ this.latestDiagnosticsVersion = incomingVersion;
790
+ }
791
+
771
792
  Logger.debug("[lsp] handling diagnostics", notification);
772
793
  // Use the correct lens by version
773
794
  const payload = this.snapshotter.getLatestSnapshot();
774
795
 
775
796
  const diagnostics = notification.params.diagnostics;
776
797
 
777
- const { lens, version: cellVersion } = payload;
798
+ const { lens } = payload;
799
+ // Forward diagnostics with a strictly increasing version so downstream
800
+ // plugin updates/clears reliably, even when server repeats the same
801
+ // document version across multiple publishDiagnostics notifications.
802
+ const diagnosticsVersion = ++this.forwardedDiagnosticsVersion;
778
803
 
779
804
  // Pre-partition diagnostics by cell
780
805
  const diagnosticsByCellId = new Map<CellId, LSP.Diagnostic[]>();
@@ -817,7 +842,7 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
817
842
  params: {
818
843
  ...notification.params,
819
844
  uri: cellDocumentUri,
820
- version: cellVersion,
845
+ version: diagnosticsVersion,
821
846
  diagnostics: cellDiagnostics,
822
847
  },
823
848
  });
@@ -832,6 +857,7 @@ export class NotebookLanguageServerClient implements ILanguageServerClient {
832
857
  method: "textDocument/publishDiagnostics",
833
858
  params: {
834
859
  uri: cellDocumentUri,
860
+ version: diagnosticsVersion,
835
861
  diagnostics: [],
836
862
  },
837
863
  });
@@ -152,9 +152,7 @@ const SliderComponent = ({
152
152
  nextValue = Number(start);
153
153
  }
154
154
  setInternalValue(nextValue);
155
- if (!debounce) {
156
- setValue(nextValue);
157
- }
155
+ setValue(nextValue);
158
156
  }}
159
157
  minValue={start}
160
158
  maxValue={stop}
@@ -0,0 +1,120 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { act, fireEvent, render } from "@testing-library/react";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import type { z } from "zod";
6
+ import { SetupMocks } from "@/__mocks__/common";
7
+ import { initialModeAtom } from "@/core/mode";
8
+ import { store } from "@/core/state/jotai";
9
+ import type { IPluginProps } from "../../types";
10
+ import { SliderPlugin } from "../SliderPlugin";
11
+
12
+ SetupMocks.resizeObserver();
13
+
14
+ describe("SliderPlugin", () => {
15
+ beforeEach(() => {
16
+ vi.useFakeTimers();
17
+ store.set(initialModeAtom, "edit");
18
+ });
19
+
20
+ afterEach(() => {
21
+ vi.useRealTimers();
22
+ });
23
+
24
+ const createProps = (
25
+ debounce: boolean,
26
+ includeInput: boolean,
27
+ setValue: ReturnType<typeof vi.fn>,
28
+ ): IPluginProps<number, z.infer<typeof SliderPlugin.prototype.validator>> => {
29
+ return {
30
+ host: document.createElement("div"),
31
+ value: 5,
32
+ setValue,
33
+ data: {
34
+ initialValue: 5,
35
+ start: 0,
36
+ stop: 10,
37
+ step: 1,
38
+ label: "Test Slider",
39
+ debounce,
40
+ orientation: "horizontal" as const,
41
+ showValue: false,
42
+ fullWidth: false,
43
+ includeInput,
44
+ steps: null,
45
+ },
46
+ functions: {},
47
+ };
48
+ };
49
+
50
+ it("slider triggers setValue immediately when debounce is false", () => {
51
+ const plugin = new SliderPlugin();
52
+ const setValue = vi.fn();
53
+ const props = createProps(false, false, setValue);
54
+ const { container } = render(plugin.render(props));
55
+
56
+ act(() => {
57
+ vi.advanceTimersByTime(0);
58
+ });
59
+
60
+ const thumb = container.querySelector('[role="slider"]');
61
+ expect(thumb).toBeTruthy();
62
+
63
+ // Radix UI Slider updates on keyboard ArrowRight/ArrowLeft
64
+ act(() => {
65
+ (thumb as HTMLElement)?.focus();
66
+ fireEvent.keyDown(thumb!, { key: "ArrowRight" });
67
+ });
68
+
69
+ expect(setValue).toHaveBeenCalledWith(6);
70
+ });
71
+
72
+ it("slider does not trigger setValue immediately when debounce is true", () => {
73
+ const plugin = new SliderPlugin();
74
+ const setValue = vi.fn();
75
+ const props = createProps(true, false, setValue);
76
+ const { container } = render(plugin.render(props));
77
+
78
+ act(() => {
79
+ vi.advanceTimersByTime(0);
80
+ });
81
+
82
+ const thumb = container.querySelector('[role="slider"]');
83
+
84
+ act(() => {
85
+ (thumb as HTMLElement)?.focus();
86
+ // Simulate just a programmatic change that Radix would trigger via pointer move
87
+ // which fires onValueChange but not onValueCommit yet
88
+ // Because we can't easily separated Radix's internal pointer events in jsdom, we
89
+ // test the main issue: editable input. We can trust Radix's onValueChange vs onValueCommit.
90
+ });
91
+
92
+ // We verified above that NumberField works when debounce=true
93
+ expect(setValue).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it("editable input triggers setValue immediately even when slider debounce is true", () => {
97
+ const plugin = new SliderPlugin();
98
+ const setValue = vi.fn();
99
+ const props = createProps(true, true, setValue);
100
+ const { getByRole } = render(plugin.render(props));
101
+
102
+ act(() => {
103
+ vi.advanceTimersByTime(0);
104
+ });
105
+
106
+ // The react-aria NumberField renders an input textbox.
107
+ const numericInput = getByRole("textbox");
108
+
109
+ act(() => {
110
+ // Simulate typing a new value and pressing enter
111
+ // With React-Aria NumberField, onChange fires on blur or enter
112
+ fireEvent.change(numericInput, { target: { value: "9" } });
113
+ fireEvent.blur(numericInput);
114
+ });
115
+
116
+ // Because the user explicitly typed 9 in the editable input,
117
+ // setValue should be called immediately regardless of debounce=true.
118
+ expect(setValue).toHaveBeenCalledWith(9);
119
+ });
120
+ });