@buoy-gg/jotai 7.0.16 → 7.0.18

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 (34) hide show
  1. package/lib/commonjs/index.js +1 -0
  2. package/lib/commonjs/jotai/components/JotaiAtomBrowser.js +11 -3
  3. package/lib/commonjs/jotai/components/JotaiAtomDetailContent.js +4 -3
  4. package/lib/commonjs/jotai/components/JotaiModal.js +35 -4
  5. package/lib/commonjs/jotai/sync/jotaiSyncAdapter.js +139 -5
  6. package/lib/commonjs/jotai/utils/jotaiStateStore.js +73 -7
  7. package/lib/commonjs/jotai/utils/snapshotProvider.js +233 -0
  8. package/lib/commonjs/jotai/utils/watchAtoms.js +5 -3
  9. package/lib/module/index.js +3 -0
  10. package/lib/module/jotai/components/JotaiAtomBrowser.js +11 -3
  11. package/lib/module/jotai/components/JotaiAtomDetailContent.js +4 -2
  12. package/lib/module/jotai/components/JotaiModal.js +36 -5
  13. package/lib/module/jotai/sync/jotaiSyncAdapter.js +138 -4
  14. package/lib/module/jotai/utils/jotaiStateStore.js +73 -7
  15. package/lib/module/jotai/utils/snapshotProvider.js +232 -0
  16. package/lib/module/jotai/utils/watchAtoms.js +5 -3
  17. package/lib/typescript/index.d.ts +2 -1
  18. package/lib/typescript/index.d.ts.map +1 -1
  19. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts +3 -1
  20. package/lib/typescript/jotai/components/JotaiAtomBrowser.d.ts.map +1 -1
  21. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts +7 -2
  22. package/lib/typescript/jotai/components/JotaiAtomDetailContent.d.ts.map +1 -1
  23. package/lib/typescript/jotai/components/JotaiModal.d.ts +1 -1
  24. package/lib/typescript/jotai/components/JotaiModal.d.ts.map +1 -1
  25. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts +54 -2
  26. package/lib/typescript/jotai/sync/jotaiSyncAdapter.d.ts.map +1 -1
  27. package/lib/typescript/jotai/types/index.d.ts +28 -0
  28. package/lib/typescript/jotai/types/index.d.ts.map +1 -1
  29. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts +10 -0
  30. package/lib/typescript/jotai/utils/jotaiStateStore.d.ts.map +1 -1
  31. package/lib/typescript/jotai/utils/snapshotProvider.d.ts +2 -0
  32. package/lib/typescript/jotai/utils/snapshotProvider.d.ts.map +1 -0
  33. package/lib/typescript/jotai/utils/watchAtoms.d.ts.map +1 -1
  34. package/package.json +7 -4
@@ -271,9 +271,11 @@ export function JotaiAtomDetailContent({
271
271
  changes,
272
272
  selectedIndex,
273
273
  onIndexChange,
274
- disableInternalFooter = false
274
+ disableInternalFooter = false,
275
+ initialView = "change"
275
276
  }) {
276
- const [activeView, setActiveView] = useState("change");
277
+ // View state (initialView seeds it once; taps own it afterwards)
278
+ const [activeView, setActiveView] = useState(initialView);
277
279
  const [diffMode, setDiffMode] = useState("tree");
278
280
  const totalChanges = changes.length;
279
281
  const atomColor = jotaiStateStore.getAtomColor(change.atomLabel);
@@ -8,7 +8,7 @@
8
8
  * - Events tab: Live atom change monitoring with diffs
9
9
  */
10
10
 
11
- import { useState, useMemo, useRef, useCallback } from "react";
11
+ import { useState, useMemo, useRef, useCallback, useEffect } from "react";
12
12
  import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity } from "react-native";
13
13
  import { JsModal, ModalHeader, TabSelector, macOSColors, buoyColors, Search, Filter, Power, X, Box, devToolsStorageKeys, ProUpgradeModal, PowerToggleButton, ToolbarCopyButton, ToolbarClearButton, truncatePayload, TickProvider } from "@buoy-gg/shared-ui";
14
14
  import { useIsPro } from "@buoy-gg/license";
@@ -42,11 +42,16 @@ export function JotaiModal({
42
42
  onBack,
43
43
  onMinimize,
44
44
  enableSharedModalDimensions = false,
45
- inline = false
45
+ inline = false,
46
+ initialTab,
47
+ initialSelectedChangeId,
48
+ initialDetailView,
49
+ onChangeSelect,
50
+ onAtomExpand
46
51
  }) {
47
52
  const isPro = useIsPro();
48
53
  const [showProModal, setShowProModal] = useState(false);
49
- const [activeTab, setActiveTab] = useState("atoms");
54
+ const [activeTab, setActiveTab] = useState(initialTab ?? "atoms");
50
55
  const [selectedHistoryAtom, setSelectedHistoryAtom] = useState(null);
51
56
  const [showFilters, setShowFilters] = useState(false);
52
57
  const [ignoredPatterns, setIgnoredPatterns] = useState(new Set());
@@ -98,6 +103,30 @@ export function JotaiModal({
98
103
  }, [selectedChangeId, activeChanges]);
99
104
  const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
100
105
 
106
+ // Notify the host on selection (keyed by change id so re-renders with the
107
+ // same selection don't re-fire; remote mirrors fetch atom values here).
108
+ const lastNotifiedSelectionRef = useRef(null);
109
+ useEffect(() => {
110
+ if (!onChangeSelect) return;
111
+ const id = selectedChange?.id ?? null;
112
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
113
+ lastNotifiedSelectionRef.current = id;
114
+ onChangeSelect(selectedChange);
115
+ }, [selectedChange, onChangeSelect]);
116
+
117
+ // Deep link into a change's detail (see initialSelectedChangeId). Applied
118
+ // at most once per id, so the list re-rendering — or the user pressing Back
119
+ // — never re-opens it.
120
+ const appliedInitialSelectionRef = useRef(null);
121
+ useEffect(() => {
122
+ if (!initialSelectedChangeId) return;
123
+ if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
124
+ const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
125
+ if (!exists) return;
126
+ appliedInitialSelectionRef.current = initialSelectedChangeId;
127
+ setSelectedChangeId(initialSelectedChangeId);
128
+ }, [initialSelectedChangeId, activeChanges]);
129
+
101
130
  // Search state
102
131
  const [searchText, setSearchText] = useState("");
103
132
  const [isSearchActive, setIsSearchActive] = useState(false);
@@ -338,7 +367,8 @@ export function JotaiModal({
338
367
  changes: activeChanges,
339
368
  selectedIndex: selectedChangeIndex,
340
369
  onIndexChange: handleIndexChange,
341
- disableInternalFooter: true
370
+ disableInternalFooter: true,
371
+ initialView: initialDetailView
342
372
  });
343
373
  }
344
374
  if (selectedHistoryAtom) {
@@ -371,7 +401,8 @@ export function JotaiModal({
371
401
  return /*#__PURE__*/_jsx(JotaiAtomBrowser, {
372
402
  atoms: displayedAtoms,
373
403
  searchQuery: searchText,
374
- onViewHistory: handleViewHistory
404
+ onViewHistory: handleViewHistory,
405
+ onAtomExpand: onAtomExpand
375
406
  });
376
407
  }
377
408
 
@@ -1,6 +1,60 @@
1
1
  "use strict";
2
2
 
3
3
  import { jotaiStateStore } from "../utils/jotaiStateStore";
4
+ /**
5
+ * Sentinel used in WIRE-FORM snapshots in place of raw prevValue/nextValue.
6
+ * Same hazard as redux/zustand: up to 200 changes × 2 values on every
7
+ * snapshot. Dashboards fetch values via `getChangeDetail`.
8
+ */
9
+ export const VALUE_ON_DEVICE = Object.freeze({
10
+ __buoyValueOnDevice: true,
11
+ note: "Atom values stay on the device — fetched on demand via getChangeDetail / getAtomValue."
12
+ });
13
+ const MAX_CHANGE_DETAIL_BYTES = 8 * 1024 * 1024;
14
+ const MAX_WIRE_PAYLOAD_BYTES = 16 * 1024;
15
+ function jsonLongerThan(value, limit) {
16
+ let bytes = 0;
17
+ const stack = [value];
18
+ const seen = new Set();
19
+ while (stack.length > 0) {
20
+ if (bytes > limit) return true;
21
+ const v = stack.pop();
22
+ if (v === null || v === undefined) bytes += 4;else if (typeof v === "string") bytes += v.length + 2;else if (typeof v === "number") bytes += 8;else if (typeof v === "boolean") bytes += 5;else if (typeof v === "object") {
23
+ if (seen.has(v)) continue;
24
+ seen.add(v);
25
+ if (Array.isArray(v)) {
26
+ bytes += 2 + v.length;
27
+ for (let i = 0; i < v.length; i++) stack.push(v[i]);
28
+ } else {
29
+ bytes += 2;
30
+ for (const k in v) {
31
+ bytes += k.length + 4;
32
+ stack.push(v[k]);
33
+ }
34
+ }
35
+ } else bytes += 4;
36
+ }
37
+ return bytes > limit;
38
+ }
39
+ function toWireChange(change) {
40
+ return {
41
+ ...change,
42
+ prevValue: VALUE_ON_DEVICE,
43
+ nextValue: VALUE_ON_DEVICE
44
+ };
45
+ }
46
+ function toWireValue(value) {
47
+ try {
48
+ JSON.stringify(value);
49
+ } catch {
50
+ return VALUE_ON_DEVICE;
51
+ }
52
+ if (jsonLongerThan(value, MAX_WIRE_PAYLOAD_BYTES)) {
53
+ return VALUE_ON_DEVICE;
54
+ }
55
+ return value;
56
+ }
57
+
4
58
  /**
5
59
  * Sync adapter for the jotai tool, consumed by @buoy-gg/external-sync's
6
60
  * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
@@ -11,12 +65,19 @@ import { jotaiStateStore } from "../utils/jotaiStateStore";
11
65
  * already records. The snapshot carries both the change timeline and the
12
66
  * atom registry (with each atom's current value) so the dashboard can render
13
67
  * the atom browser.
68
+ *
69
+ * v2: change-log prevValue/nextValue are replaced with on-device markers.
70
+ * Dashboards fetch values via `getChangeDetail`. Oversized / unserializable
71
+ * `atoms[].currentValue` uses the same marker and is fetched via `getAtomValue`.
14
72
  */
15
73
  export const jotaiSyncAdapter = {
16
- version: 1,
74
+ version: 2,
17
75
  getSnapshot: () => ({
18
- changes: jotaiStateStore.getAtomChanges(),
19
- atoms: jotaiStateStore.getAtomSnapshots()
76
+ changes: jotaiStateStore.getAtomChanges().map(toWireChange),
77
+ atoms: jotaiStateStore.getAtomSnapshots().map(atom => ({
78
+ ...atom,
79
+ currentValue: toWireValue(atom.currentValue)
80
+ }))
20
81
  }),
21
82
  subscribe: onChange => {
22
83
  const unsubscribeChanges = jotaiStateStore.subscribe(onChange);
@@ -30,6 +91,78 @@ export const jotaiSyncAdapter = {
30
91
  clearEvents: () => {
31
92
  jotaiStateStore.clearAtomChanges();
32
93
  },
94
+ /**
95
+ * On-demand detail: one change's real prevValue/nextValue.
96
+ * The per-snapshot stream stays value-free; this is the explicit,
97
+ * size-guarded channel for the detail pane.
98
+ */
99
+ getChangeDetail: params => {
100
+ const {
101
+ id
102
+ } = params ?? {};
103
+ if (!id) return {
104
+ found: false,
105
+ reason: "missing id"
106
+ };
107
+ const change = jotaiStateStore.getAtomChangeById(id);
108
+ if (!change) return {
109
+ found: false,
110
+ reason: "unknown id"
111
+ };
112
+ const cappedNote = {
113
+ __buoyTruncated: true,
114
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
115
+ };
116
+ const cap = value => jsonLongerThan(value, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : value;
117
+ return {
118
+ found: true,
119
+ id,
120
+ prevValue: cap(change.prevValue),
121
+ nextValue: cap(change.nextValue)
122
+ };
123
+ },
124
+ /**
125
+ * On-demand current value for one atom. Wire-form snapshots replace
126
+ * oversized / unserializable `currentValue` with VALUE_ON_DEVICE.
127
+ */
128
+ getAtomValue: params => {
129
+ const {
130
+ label
131
+ } = params ?? {};
132
+ if (!label) return {
133
+ found: false,
134
+ reason: "missing label"
135
+ };
136
+ const atom = jotaiStateStore.getAtom(label);
137
+ if (!atom) return {
138
+ found: false,
139
+ reason: "unknown label"
140
+ };
141
+ let currentValue;
142
+ try {
143
+ currentValue = atom.getValue();
144
+ } catch {
145
+ currentValue = undefined;
146
+ }
147
+ const cappedNote = {
148
+ __buoyTruncated: true,
149
+ note: `Exceeds the ${MAX_CHANGE_DETAIL_BYTES / (1024 * 1024)}MB detail limit — inspect it on-device.`
150
+ };
151
+ try {
152
+ JSON.stringify(currentValue);
153
+ } catch {
154
+ return {
155
+ found: true,
156
+ label,
157
+ currentValue: VALUE_ON_DEVICE
158
+ };
159
+ }
160
+ return {
161
+ found: true,
162
+ label,
163
+ currentValue: jsonLongerThan(currentValue, MAX_CHANGE_DETAIL_BYTES) ? cappedNote : currentValue
164
+ };
165
+ },
33
166
  /**
34
167
  * Compact current-value reader for a remote driver (MCP/LLM): registered
35
168
  * atoms with light metadata; each atom's `currentValue` stays on the device
@@ -73,7 +206,8 @@ export const jotaiSyncAdapter = {
73
206
  const {
74
207
  label,
75
208
  value
76
- } = params;
209
+ } = params ?? {};
210
+ if (!label) throw new Error("setAtom requires a `label`");
77
211
  jotaiStateStore.setAtomValue(label, value);
78
212
  }
79
213
  }
@@ -286,17 +286,83 @@ class JotaiStateStore {
286
286
  */
287
287
  replaceFromSnapshot(changes, atomSnapshots) {
288
288
  if (!this.isEnabled) return;
289
- this.atomChanges = changes.slice(0, this.maxChanges);
290
- this.atoms = new Map(atomSnapshots.map(snapshot => [snapshot.label, {
291
- label: snapshot.label,
292
- changeCount: snapshot.changeCount,
293
- color: snapshot.color,
294
- getValue: () => snapshot.currentValue
295
- }]));
289
+
290
+ // Preserve values a remote mirror already hydrated — wire-form snapshots
291
+ // always carry markers, and a later snapshot must not blank the detail pane.
292
+ const hydrated = new Map();
293
+ for (const existing of this.atomChanges) {
294
+ const prev = existing.prevValue;
295
+ if (!prev || prev.__buoyValueOnDevice !== true) {
296
+ hydrated.set(existing.id, existing);
297
+ }
298
+ }
299
+ this.atomChanges = changes.slice(0, this.maxChanges).map(change => {
300
+ const kept = hydrated.get(change.id);
301
+ const incoming = change.prevValue;
302
+ return kept && incoming?.__buoyValueOnDevice === true ? {
303
+ ...change,
304
+ prevValue: kept.prevValue,
305
+ nextValue: kept.nextValue
306
+ } : change;
307
+ });
308
+
309
+ // Preserve currentValue a remote mirror already hydrated — oversized
310
+ // atoms arrive as VALUE_ON_DEVICE markers on every snapshot.
311
+ const hydratedAtoms = new Map();
312
+ for (const [label, atom] of this.atoms) {
313
+ try {
314
+ const value = atom.getValue();
315
+ const marker = value;
316
+ if (!marker || marker.__buoyValueOnDevice !== true) {
317
+ hydratedAtoms.set(label, value);
318
+ }
319
+ } catch {
320
+ // ignore
321
+ }
322
+ }
323
+ this.atoms = new Map(atomSnapshots.map(snapshot => {
324
+ const incoming = snapshot.currentValue;
325
+ const kept = hydratedAtoms.get(snapshot.label);
326
+ const currentValue = kept !== undefined && incoming?.__buoyValueOnDevice === true ? kept : snapshot.currentValue;
327
+ return [snapshot.label, {
328
+ label: snapshot.label,
329
+ changeCount: snapshot.changeCount,
330
+ color: snapshot.color,
331
+ getValue: () => currentValue
332
+ }];
333
+ }));
296
334
  this.notifyListeners();
297
335
  this.notifyAtomListeners();
298
336
  }
299
337
 
338
+ /**
339
+ * Hydrate one mirrored change with real values fetched on demand from the
340
+ * device via the adapter's `getChangeDetail`. Remote-mirror (desktop) only.
341
+ */
342
+ hydrateChange(id, patch) {
343
+ const index = this.atomChanges.findIndex(c => c.id === id);
344
+ if (index === -1) return;
345
+ this.atomChanges = [...this.atomChanges.slice(0, index), {
346
+ ...this.atomChanges[index],
347
+ ...patch
348
+ }, ...this.atomChanges.slice(index + 1)];
349
+ this.notifyListeners();
350
+ }
351
+
352
+ /**
353
+ * Hydrate one mirrored atom's currentValue fetched on demand from the
354
+ * device via the adapter's `getAtomValue`. Remote-mirror (desktop) only.
355
+ */
356
+ hydrateAtomValue(label, currentValue) {
357
+ const atom = this.atoms.get(label);
358
+ if (!atom) return;
359
+ this.atoms.set(label, {
360
+ ...atom,
361
+ getValue: () => currentValue
362
+ });
363
+ this.notifyAtomListeners();
364
+ }
365
+
300
366
  // ---- Atom Registry ----
301
367
 
302
368
  registerAtom(label, getValue, setValue) {
@@ -0,0 +1,232 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Time Machine snapshot provider for Jotai.
5
+ *
6
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
7
+ * consumed by @buoy-gg/time-machine.
8
+ *
9
+ * Coverage is opt-in by nature: only atoms the app passed to `watchAtoms` /
10
+ * `watchDefaultStoreAtoms` are visible. Derived (read-only) atoms are captured
11
+ * for inspection but never restored — they recompute from their sources.
12
+ * Caveat (documented): atoms with custom write functions (`atomWithReducer`)
13
+ * receive ACTIONS through `store.set`, so restoring a raw value can misbehave;
14
+ * nothing in the registry distinguishes them from plain atoms.
15
+ */
16
+ import { registerStateSnapshotProvider } from "@buoy-gg/shared-ui";
17
+ import { boundedDeepEqual } from "@buoy-gg/shared-ui/dataViewer";
18
+ import { jotaiStateStore } from "./jotaiStateStore";
19
+ function status() {
20
+ const atoms = jotaiStateStore.getAtoms();
21
+ if (atoms.length === 0) {
22
+ return {
23
+ canCapture: false,
24
+ canRestore: false,
25
+ detail: "No Jotai atoms registered — pass your atoms to watchAtoms() / watchDefaultStoreAtoms()"
26
+ };
27
+ }
28
+ const writable = atoms.filter(a => a.setValue).length;
29
+ return {
30
+ canCapture: true,
31
+ canRestore: writable > 0,
32
+ detail: writable < atoms.length ? `${atoms.length - writable} derived atom(s) recompute automatically and are not restored` : undefined
33
+ };
34
+ }
35
+ async function capture() {
36
+ const warnings = [];
37
+ const atoms = {};
38
+ let derived = 0;
39
+ for (const info of jotaiStateStore.getAtoms()) {
40
+ try {
41
+ const value = info.getValue();
42
+ if (typeof value === "function") {
43
+ warnings.push(`atom "${info.label}": function value not capturable`);
44
+ continue;
45
+ }
46
+ if (!info.setValue) derived += 1;
47
+ atoms[info.label] = {
48
+ value,
49
+ writable: Boolean(info.setValue)
50
+ };
51
+ } catch (e) {
52
+ warnings.push(`atom "${info.label}": capture failed (${String(e)})`);
53
+ }
54
+ }
55
+ if (derived > 0) {
56
+ warnings.push(`${derived} derived atom(s) captured for inspection only (recompute on restore)`);
57
+ }
58
+ return {
59
+ data: {
60
+ atoms
61
+ },
62
+ warnings
63
+ };
64
+ }
65
+ async function restore(data, options) {
66
+ const snapshot = data;
67
+ const excludedSet = new Set(options?.excludeKeys ?? []);
68
+ const includedSet = options?.includeKeys ? new Set(options.includeKeys) : null;
69
+ const touch = key => !excludedSet.has(key) && (!includedSet || includedSet.has(key));
70
+ let excludedHits = 0;
71
+ const result = {
72
+ ok: true,
73
+ applied: 0,
74
+ skipped: [],
75
+ warnings: []
76
+ };
77
+ if (!snapshot?.atoms) {
78
+ return {
79
+ ok: false,
80
+ applied: 0,
81
+ skipped: [],
82
+ warnings: ["empty jotai snapshot"]
83
+ };
84
+ }
85
+ let derived = 0;
86
+ for (const [label, entry] of Object.entries(snapshot.atoms)) {
87
+ if (!touch(label)) {
88
+ excludedHits += 1;
89
+ continue;
90
+ }
91
+ if (!entry.writable) {
92
+ derived += 1;
93
+ continue;
94
+ }
95
+ const info = jotaiStateStore.getAtom(label);
96
+ if (!info) {
97
+ result.skipped.push({
98
+ id: label,
99
+ reason: "atom not registered (renamed, or unwatched since snapshot?)"
100
+ });
101
+ continue;
102
+ }
103
+ if (!info.setValue) {
104
+ result.skipped.push({
105
+ id: label,
106
+ reason: "atom is now read-only (derived)"
107
+ });
108
+ continue;
109
+ }
110
+ try {
111
+ info.setValue(entry.value);
112
+ result.applied += 1;
113
+ } catch (e) {
114
+ result.skipped.push({
115
+ id: label,
116
+ reason: String(e)
117
+ });
118
+ result.ok = false;
119
+ }
120
+ }
121
+ if (derived > 0) {
122
+ result.warnings.push(`${derived} derived atom(s) left to recompute`);
123
+ }
124
+ if (excludedHits > 0) {
125
+ result.warnings.push(`${excludedHits} atom(s) excluded by you`);
126
+ }
127
+ return result;
128
+ }
129
+
130
+ /** Per-atom restore preview. `base` omitted = compare against live atoms. */
131
+ async function previewItems(target, base) {
132
+ const targetAtoms = target?.atoms ?? {};
133
+ const liveMode = base === undefined;
134
+ const baseAtoms = {};
135
+ if (liveMode) {
136
+ for (const info of jotaiStateStore.getAtoms()) {
137
+ try {
138
+ baseAtoms[info.label] = {
139
+ value: info.getValue(),
140
+ writable: Boolean(info.setValue)
141
+ };
142
+ } catch {
143
+ // unreadable atom — treated as absent
144
+ }
145
+ }
146
+ } else {
147
+ Object.assign(baseAtoms, base?.atoms ?? {});
148
+ }
149
+ const items = [];
150
+ const notes = [];
151
+ const labels = new Set([...Object.keys(baseAtoms), ...Object.keys(targetAtoms)]);
152
+ for (const label of labels) {
153
+ const inTarget = label in targetAtoms;
154
+ if (!inTarget) {
155
+ if (liveMode) notes.push(`atom "${label}" not in snapshot — untouched`);else items.push({
156
+ key: label,
157
+ label,
158
+ verdict: "removed",
159
+ oldValue: baseAtoms[label]?.value
160
+ });
161
+ continue;
162
+ }
163
+ const entry = targetAtoms[label];
164
+ const equal = label in baseAtoms && boundedDeepEqual(baseAtoms[label].value, entry.value);
165
+ if (!entry.writable) {
166
+ items.push({
167
+ key: label,
168
+ label,
169
+ verdict: equal ? "unchanged" : "recomputes",
170
+ reason: equal ? undefined : "derived atom — recomputes from its sources",
171
+ oldValue: baseAtoms[label]?.value,
172
+ newValue: entry.value
173
+ });
174
+ continue;
175
+ }
176
+ if (liveMode) {
177
+ const info = jotaiStateStore.getAtom(label);
178
+ if (!info) {
179
+ items.push({
180
+ key: label,
181
+ label,
182
+ verdict: "wont-apply",
183
+ reason: "atom not registered (renamed, or unwatched since snapshot?)"
184
+ });
185
+ continue;
186
+ }
187
+ if (!info.setValue) {
188
+ items.push({
189
+ key: label,
190
+ label,
191
+ verdict: "wont-apply",
192
+ reason: "atom is now read-only (derived)"
193
+ });
194
+ continue;
195
+ }
196
+ }
197
+ if (!(label in baseAtoms)) {
198
+ items.push({
199
+ key: label,
200
+ label,
201
+ verdict: "added",
202
+ newValue: entry.value
203
+ });
204
+ } else if (equal) {
205
+ items.push({
206
+ key: label,
207
+ label,
208
+ verdict: "unchanged"
209
+ });
210
+ } else {
211
+ items.push({
212
+ key: label,
213
+ label,
214
+ verdict: "changed",
215
+ oldValue: baseAtoms[label].value,
216
+ newValue: entry.value
217
+ });
218
+ }
219
+ }
220
+ return {
221
+ items,
222
+ notes
223
+ };
224
+ }
225
+ registerStateSnapshotProvider({
226
+ id: "jotai",
227
+ label: "Jotai",
228
+ status,
229
+ capture,
230
+ restore,
231
+ previewItems
232
+ });
@@ -56,9 +56,11 @@ export function watchAtoms(store, atoms, options) {
56
56
  }
57
57
 
58
58
  // Register the atom for the browser view. A setter is captured only when
59
- // the store supports `set` (writable atoms); read-only/derived atoms throw
60
- // at set time and are reported as read-only.
61
- const setter = store.set ? value => {
59
+ // the store supports `set` AND the atom itself is writable (has a `write`
60
+ // function — primitive atoms get a default one, read-only derived atoms
61
+ // have none). Deriveds used to get a setter that threw inside jotai at
62
+ // set time; now they're correctly reported as read-only up front.
63
+ const setter = store.set && typeof atom.write === "function" ? value => {
62
64
  store.set(atom, value);
63
65
  } : undefined;
64
66
  jotaiStateStore.registerAtom(label, () => {
@@ -29,6 +29,7 @@
29
29
  * watchAtoms(myStore, { countAtom, authAtom });
30
30
  * ```
31
31
  */
32
+ import "./jotai/utils/snapshotProvider";
32
33
  export { jotaiToolPreset, createJotaiTool } from "./preset";
33
34
  export { watchAtoms } from "./jotai/utils/watchAtoms";
34
35
  export { watchDefaultStoreAtoms } from "./jotai/utils/watchAtoms";
@@ -41,7 +42,7 @@ export { JotaiAtomChangeItem } from "./jotai/components/JotaiAtomChangeItem";
41
42
  export { JotaiAtomDetailContent, JotaiAtomDetailFooter, } from "./jotai/components/JotaiAtomDetailContent";
42
43
  export { JotaiIcon, JOTAI_ICON_COLOR } from "./jotai/components/JotaiIcon";
43
44
  export { jotaiSyncAdapter } from "./jotai/sync/jotaiSyncAdapter";
44
- export type { JotaiModalProps, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
45
+ export type { JotaiModalProps, JotaiDetailViewMode, JotaiAtomChange, JotaiAtomInfo, JotaiAtomSnapshot, JotaiFilter, AtomChangeCategory, WatchAtomsOptions, } from "./jotai/types";
45
46
  /** @internal */
46
47
  export { jotaiStateStore } from "./jotai/utils/jotaiStateStore";
47
48
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAKH,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAOnF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,gCAAgC,CAAC;AAKxC,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAK5D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAKtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAKlE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,YAAY,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAK9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAOnF,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,EACL,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAK3E,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAKjE,YAAY,EACV,eAAe,EACf,mBAAmB,EACnB,eAAe,EACf,aAAa,EACb,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,eAAe,CAAC;AAKvB,gBAAgB;AAChB,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC"}
@@ -9,7 +9,9 @@ interface JotaiAtomBrowserProps {
9
9
  atoms: JotaiAtomInfo[];
10
10
  searchQuery: string;
11
11
  onViewHistory: (atomLabel: string) => void;
12
+ /** Fired when an atom row is expanded (not when collapsed). */
13
+ onAtomExpand?: (label: string) => void;
12
14
  }
13
- export declare function JotaiAtomBrowser({ atoms, searchQuery, onViewHistory, }: JotaiAtomBrowserProps): import("react").JSX.Element;
15
+ export declare function JotaiAtomBrowser({ atoms, searchQuery, onViewHistory, onAtomExpand, }: JotaiAtomBrowserProps): import("react").JSX.Element;
14
16
  export {};
15
17
  //# sourceMappingURL=JotaiAtomBrowser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC5C;AAyGD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,aAAa,GACd,EAAE,qBAAqB,+BAiFvB"}
1
+ {"version":3,"file":"JotaiAtomBrowser.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomBrowser.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAoBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAG9C,UAAU,qBAAqB;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,+DAA+D;IAC/D,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC;AAgHD,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,WAAW,EACX,aAAa,EACb,YAAY,GACb,EAAE,qBAAqB,+BAwFvB"}
@@ -9,15 +9,20 @@
9
9
  * Mirrors ZustandStateDetailContent.tsx from @buoy-gg/zustand
10
10
  */
11
11
  import React from "react";
12
- import type { JotaiAtomChange } from "../types";
12
+ import type { JotaiAtomChange, JotaiDetailViewMode } from "../types";
13
13
  interface JotaiAtomDetailContentProps {
14
14
  change: JotaiAtomChange;
15
15
  changes: JotaiAtomChange[];
16
16
  selectedIndex: number;
17
17
  onIndexChange: (index: number) => void;
18
18
  disableInternalFooter?: boolean;
19
+ /**
20
+ * Seeds the CHANGE / VALUE / DIFF toggle once on mount. Host deep-links
21
+ * (docs live tour) pass this; the user's own toggle taps take over after.
22
+ */
23
+ initialView?: JotaiDetailViewMode;
19
24
  }
20
- export declare function JotaiAtomDetailContent({ change, changes, selectedIndex, onIndexChange, disableInternalFooter, }: JotaiAtomDetailContentProps): React.JSX.Element;
25
+ export declare function JotaiAtomDetailContent({ change, changes, selectedIndex, onIndexChange, disableInternalFooter, initialView, }: JotaiAtomDetailContentProps): React.JSX.Element;
21
26
  export declare function JotaiAtomDetailFooter({ change, changes, selectedIndex, onIndexChange, }: {
22
27
  change: JotaiAtomChange;
23
28
  changes: JotaiAtomChange[];
@@ -1 +1 @@
1
- {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,UAAU,CAAC;AAmQpE,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,GAC9B,EAAE,2BAA2B,qBAyJ7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}
1
+ {"version":3,"file":"JotaiAtomDetailContent.d.ts","sourceRoot":"","sources":["../../../../src/jotai/components/JotaiAtomDetailContent.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAA+C,MAAM,OAAO,CAAC;AA0BpE,OAAO,KAAK,EACV,eAAe,EAEf,mBAAmB,EACpB,MAAM,UAAU,CAAC;AAiQlB,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAgB,sBAAsB,CAAC,EACrC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,EACb,qBAA6B,EAC7B,WAAsB,GACvB,EAAE,2BAA2B,qBA2J7B;AAED,wBAAgB,qBAAqB,CAAC,EACpC,MAAM,EACN,OAAO,EACP,aAAa,EACb,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,CAAC;IACxB,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACxC,4BAcA"}