@happyvertical/smrt-svelte 0.38.5 → 0.38.6

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.
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Runs a caller-supplied ShellState mutation inside a consumer $effect. Tests
4
+ * use this to prove ShellState's internal reads do not become dependencies of
5
+ * the consumer effect.
6
+ */
7
+ import type { ShellState } from '../admin-shell/state.svelte.js';
8
+ import type { ShellActivity } from '../admin-shell/types.js';
9
+
10
+ type EffectCleanup = () => void;
11
+
12
+ interface MutationInputs {
13
+ activity: ShellActivity;
14
+ }
15
+
16
+ interface Props {
17
+ shell: ShellState;
18
+ run: (shell: ShellState, inputs: MutationInputs) => EffectCleanup | undefined;
19
+ onReady: (controls: {
20
+ getRuns: () => number;
21
+ setActivityProgress: (progress: number) => void;
22
+ }) => void;
23
+ }
24
+
25
+ const props: Props = $props();
26
+ const activity = $state<ShellActivity>({
27
+ id: 'reactive-sync',
28
+ label: 'Reactive sync',
29
+ kind: 'sync',
30
+ scope: 'system',
31
+ status: 'running',
32
+ progress: 0,
33
+ });
34
+
35
+ // Plain let on purpose: making this $state would add tracking noise to the
36
+ // effect-leak signal this harness exists to measure.
37
+ let runs = 0;
38
+
39
+ $effect(() => {
40
+ runs += 1;
41
+ return props.run(props.shell, { activity });
42
+ });
43
+
44
+ // This harness intentionally snapshots the callback during setup.
45
+ // svelte-ignore state_referenced_locally
46
+ props.onReady({
47
+ getRuns: () => runs,
48
+ setActivityProgress: (progress: number) => {
49
+ activity.progress = progress;
50
+ },
51
+ });
52
+ </script>
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Runs a caller-supplied ShellState mutation inside a consumer $effect. Tests
3
+ * use this to prove ShellState's internal reads do not become dependencies of
4
+ * the consumer effect.
5
+ */
6
+ import type { ShellState } from '../admin-shell/state.svelte.js';
7
+ import type { ShellActivity } from '../admin-shell/types.js';
8
+ type EffectCleanup = () => void;
9
+ interface MutationInputs {
10
+ activity: ShellActivity;
11
+ }
12
+ interface Props {
13
+ shell: ShellState;
14
+ run: (shell: ShellState, inputs: MutationInputs) => EffectCleanup | undefined;
15
+ onReady: (controls: {
16
+ getRuns: () => number;
17
+ setActivityProgress: (progress: number) => void;
18
+ }) => void;
19
+ }
20
+ declare const AdminShellMutationEffectHarness: import("svelte").Component<Props, {}, "">;
21
+ type AdminShellMutationEffectHarness = ReturnType<typeof AdminShellMutationEffectHarness>;
22
+ export default AdminShellMutationEffectHarness;
23
+ //# sourceMappingURL=admin-shell-mutation-effect-harness.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin-shell-mutation-effect-harness.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/__tests__/admin-shell-mutation-effect-harness.svelte.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAG7D,KAAK,aAAa,GAAG,MAAM,IAAI,CAAC;AAEhC,UAAU,cAAc;IACtB,QAAQ,EAAE,aAAa,CAAC;CACzB;AAED,UAAU,KAAK;IACb,KAAK,EAAE,UAAU,CAAC;IAClB,GAAG,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,KAAK,aAAa,GAAG,SAAS,CAAC;IAC9E,OAAO,EAAE,CAAC,QAAQ,EAAE;QAClB,OAAO,EAAE,MAAM,MAAM,CAAC;QACtB,mBAAmB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;KACjD,KAAK,IAAI,CAAC;CACZ;AAwCD,QAAA,MAAM,+BAA+B,2CAAwC,CAAC;AAC9E,KAAK,+BAA+B,GAAG,UAAU,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAC1F,eAAe,+BAA+B,CAAC"}
@@ -1,5 +1,32 @@
1
+ import { flushSync, mount, unmount } from 'svelte';
1
2
  import { describe, expect, it, vi } from 'vitest';
2
3
  import { createShellState } from '../admin-shell/state.svelte.js';
4
+ import MutationEffectHarness from './admin-shell-mutation-effect-harness.svelte';
5
+ function mountMutationEffect(shell, run) {
6
+ const target = document.createElement('div');
7
+ document.body.appendChild(target);
8
+ let getRuns = () => 0;
9
+ let setActivityProgress = (_progress) => { };
10
+ const component = mount(MutationEffectHarness, {
11
+ target,
12
+ props: {
13
+ shell,
14
+ run,
15
+ onReady: (controls) => {
16
+ getRuns = controls.getRuns;
17
+ setActivityProgress = controls.setActivityProgress;
18
+ },
19
+ },
20
+ });
21
+ return {
22
+ getRuns,
23
+ setActivityProgress,
24
+ teardown: () => {
25
+ unmount(component);
26
+ target.remove();
27
+ },
28
+ };
29
+ }
3
30
  describe('ShellState', () => {
4
31
  it('allows panels to expand independently by default', () => {
5
32
  const shell = createShellState();
@@ -107,4 +134,107 @@ describe('ShellState', () => {
107
134
  // upsert carrying `previous`, which the toaster filters out.
108
135
  expect(events).toEqual(['upsert:new', 'upsert:has-prev']);
109
136
  });
137
+ it('does not leak focus-tool state reads into consumer effects', () => {
138
+ const shell = createShellState();
139
+ const harness = mountMutationEffect(shell, (state) => {
140
+ const unregister = state.registerFocusTool({
141
+ id: 'activity',
142
+ label: 'Activity',
143
+ });
144
+ state.openFocusTool('activity');
145
+ return unregister;
146
+ });
147
+ try {
148
+ flushSync();
149
+ expect(harness.getRuns()).toBe(1);
150
+ shell.collapsePanel('right');
151
+ flushSync();
152
+ shell.openFocusTool('activity');
153
+ flushSync();
154
+ expect(harness.getRuns()).toBe(1);
155
+ expect(shell.focusTools.map((tool) => tool.id)).toEqual(['activity']);
156
+ }
157
+ finally {
158
+ harness.teardown();
159
+ }
160
+ });
161
+ it('does not leak panel state reads into consumer effects', () => {
162
+ const shell = createShellState();
163
+ const harness = mountMutationEffect(shell, (state) => {
164
+ state.setPanel('right', 'expanded');
165
+ return undefined;
166
+ });
167
+ try {
168
+ flushSync();
169
+ expect(harness.getRuns()).toBe(1);
170
+ shell.togglePanel('right');
171
+ flushSync();
172
+ expect(harness.getRuns()).toBe(1);
173
+ expect(shell.panels.right).toBe('collapsed');
174
+ }
175
+ finally {
176
+ harness.teardown();
177
+ }
178
+ });
179
+ it('does not leak settings state reads into consumer effects', () => {
180
+ const shell = createShellState();
181
+ const harness = mountMutationEffect(shell, (state) => {
182
+ state.applySettings({ panels: { right: 'expanded' } });
183
+ return undefined;
184
+ });
185
+ try {
186
+ flushSync();
187
+ expect(harness.getRuns()).toBe(1);
188
+ shell.setHotkey('right', 'KeyR');
189
+ flushSync();
190
+ expect(harness.getRuns()).toBe(1);
191
+ expect(shell.settings.keymap?.right).toEqual({ code: 'KeyR' });
192
+ }
193
+ finally {
194
+ harness.teardown();
195
+ }
196
+ });
197
+ it('does not leak activity state reads into consumer effects', () => {
198
+ const shell = createShellState();
199
+ const harness = mountMutationEffect(shell, (state) => {
200
+ state.upsertActivity({
201
+ id: 'sync-1',
202
+ label: 'Sync',
203
+ kind: 'sync',
204
+ scope: 'system',
205
+ status: 'running',
206
+ });
207
+ return undefined;
208
+ });
209
+ try {
210
+ flushSync();
211
+ expect(harness.getRuns()).toBe(1);
212
+ shell.updateActivity('sync-1', { progress: 50 });
213
+ flushSync();
214
+ expect(harness.getRuns()).toBe(1);
215
+ expect(shell.listActivities()[0]?.progress).toBe(50);
216
+ }
217
+ finally {
218
+ harness.teardown();
219
+ }
220
+ });
221
+ it('keeps caller-supplied reactive activity inputs tracked', () => {
222
+ const shell = createShellState();
223
+ const harness = mountMutationEffect(shell, (state, inputs) => {
224
+ state.upsertActivity(inputs.activity);
225
+ return undefined;
226
+ });
227
+ try {
228
+ flushSync();
229
+ expect(harness.getRuns()).toBe(1);
230
+ expect(shell.listActivities()[0]?.progress).toBe(0);
231
+ harness.setActivityProgress(75);
232
+ flushSync();
233
+ expect(harness.getRuns()).toBe(2);
234
+ expect(shell.listActivities()[0]?.progress).toBe(75);
235
+ }
236
+ finally {
237
+ harness.teardown();
238
+ }
239
+ });
110
240
  });
@@ -1,5 +1,6 @@
1
1
  import type { PanelEdge, PanelState, ResolvedShellConfig, ShellActivity, ShellActivityBadge, ShellActivityEvent, ShellActivityFilter, ShellFocusTool, ShellPanelDefaults, ShellScope, ShellSettingsAdapter, ShellSettingsDelta, ShellStateSnapshot, VisiblePanelState } from './types.js';
2
2
  type ActivityListener = (event: ShellActivityEvent) => void;
3
+ type ShellActivityPatch = Partial<Omit<ShellActivity, 'id'>>;
3
4
  export interface ShellStateOptions {
4
5
  config?: ShellPanelDefaults;
5
6
  settings?: ShellSettingsDelta;
@@ -32,7 +33,7 @@ export declare class ShellState {
32
33
  unregisterFocusTool(id: string): void;
33
34
  openFocusTool(id: string): void;
34
35
  upsertActivity(activity: ShellActivity): void;
35
- updateActivity(id: string, patch: Partial<Omit<ShellActivity, 'id'>>): void;
36
+ updateActivity(id: string, patch: ShellActivityPatch): void;
36
37
  removeActivity(id: string): void;
37
38
  watchActivities(listener: ActivityListener): () => void;
38
39
  listActivities(filter?: ShellActivityFilter): ShellActivity[];
@@ -1 +1 @@
1
- {"version":3,"file":"state.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/state.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAGpB,KAAK,gBAAgB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,eAAe,CAAC,EAAE,oBAAoB,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,UAAU;IACrB,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAE9C,QAAQ,qBAAkC;IAC1C,MAAM,gCAKH;IACH,UAAU,mBAAgC;IAC1C,iBAAiB,gBAA+B;IAChD,UAAU,kBAA+B;IAEzC,OAAO,CAAC,iBAAiB,CAA+B;gBAE5C,OAAO,GAAE,iBAAsB;IAUrC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,QAAQ,IAAI,kBAAkB;IAY9B,aAAa,CACX,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAClC,IAAI;IAcP,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAQrD,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAazD,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAQlC,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAIlC,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAIpC,oBAAoB,IAAI,OAAO;IAU/B,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,IAAI;IASnD,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAOrC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAU/B,cAAc,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IAuB7C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,GAAG,IAAI;IAW3E,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAOhC,eAAe,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAKvD,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,aAAa,EAAE;IASjE,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,kBAAkB;IAsBlD,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS;IAW9C,OAAO,CAAC,mBAAmB;YAmBb,eAAe;IAI7B,OAAO,CAAC,YAAY;CAGrB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAE5E"}
1
+ {"version":3,"file":"state.svelte.d.ts","sourceRoot":"","sources":["../../../../src/components/workspace/admin-shell/state.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAGpB,KAAK,gBAAgB,GAAG,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAC5D,KAAK,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;AAE7D,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B,eAAe,CAAC,EAAE,oBAAoB,CAAC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,UAAU;IACrB,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAE9C,QAAQ,qBAAkC;IAC1C,MAAM,gCAKH;IACH,UAAU,mBAAgC;IAC1C,iBAAiB,gBAA+B;IAChD,UAAU,kBAA+B;IAEzC,OAAO,CAAC,iBAAiB,CAA+B;gBAE5C,OAAO,GAAE,iBAAsB;IAUrC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAM9B,QAAQ,IAAI,kBAAkB;IAY9B,aAAa,CACX,KAAK,EAAE,kBAAkB,EACzB,OAAO,GAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAO,GAClC,IAAI;IAkBP,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAIzC,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAUrD,QAAQ,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAezD,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAUlC,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAIlC,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI;IAIpC,oBAAoB,IAAI,OAAO;IAY/B,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,IAAI;IAYnD,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IASrC,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAY/B,cAAc,CAAC,QAAQ,EAAE,aAAa,GAAG,IAAI;IA4B7C,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAc3D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAWhC,eAAe,CAAC,QAAQ,EAAE,gBAAgB,GAAG,MAAM,IAAI;IAKvD,cAAc,CAAC,MAAM,GAAE,mBAAwB,GAAG,aAAa,EAAE;IASjE,aAAa,CAAC,IAAI,EAAE,SAAS,GAAG,kBAAkB;IAsBlD,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS;IAW9C,OAAO,CAAC,mBAAmB;YAmBb,eAAe;IAI7B,OAAO,CAAC,YAAY;CAGrB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAE5E"}
@@ -1,3 +1,4 @@
1
+ import { untrack } from 'svelte';
1
2
  import { LocalStorageShellSettingsAdapter, mergeShellSettingsDelta, resolveInitialPanelState, resolveShellConfig, } from './settings.js';
2
3
  import { PANEL_EDGES, SCOPE_EDGES } from './types.js';
3
4
  export class ShellState {
@@ -42,121 +43,148 @@ export class ShellState {
42
43
  };
43
44
  }
44
45
  applySettings(delta, options = {}) {
45
- this.settings = mergeShellSettingsDelta(this.settings, delta);
46
- for (const edge of PANEL_EDGES) {
47
- this.panels[edge] = resolveInitialPanelState(edge, this.config.panels[edge], this.settings);
48
- }
49
- this.activeFocusToolId =
50
- this.settings.activeFocusToolId ?? this.activeFocusToolId;
51
- if (options.persist !== false)
52
- void this.persistSettings();
46
+ const nextDelta = snapshotShellSettingsDelta(delta);
47
+ const persist = options.persist;
48
+ untrack(() => {
49
+ this.settings = mergeShellSettingsDelta(this.settings, nextDelta);
50
+ for (const edge of PANEL_EDGES) {
51
+ this.panels[edge] = resolveInitialPanelState(edge, this.config.panels[edge], this.settings);
52
+ }
53
+ this.activeFocusToolId =
54
+ this.settings.activeFocusToolId ?? this.activeFocusToolId;
55
+ if (persist !== false)
56
+ void this.persistSettings();
57
+ });
53
58
  }
54
59
  setHotkeysEnabled(enabled) {
55
- this.applySettings({ hotkeysEnabled: enabled });
60
+ untrack(() => this.applySettings({ hotkeysEnabled: enabled }));
56
61
  }
57
62
  setHotkey(edge, code) {
58
- this.applySettings({
59
- keymap: {
60
- [edge]: code ? { code } : null,
61
- },
63
+ untrack(() => {
64
+ this.applySettings({
65
+ keymap: {
66
+ [edge]: code ? { code } : null,
67
+ },
68
+ });
62
69
  });
63
70
  }
64
71
  setPanel(edge, state) {
65
- if (this.config.panels[edge].initial === 'hidden') {
66
- this.panels[edge] = 'hidden';
67
- return;
68
- }
69
- if (state === 'expanded')
70
- this.closeExclusivePeers(edge);
71
- this.panels[edge] = state;
72
- this.settings = mergeShellSettingsDelta(this.settings, {
73
- panels: { [edge]: state },
72
+ untrack(() => {
73
+ if (this.config.panels[edge].initial === 'hidden') {
74
+ this.panels[edge] = 'hidden';
75
+ return;
76
+ }
77
+ if (state === 'expanded')
78
+ this.closeExclusivePeers(edge);
79
+ this.panels[edge] = state;
80
+ this.settings = mergeShellSettingsDelta(this.settings, {
81
+ panels: { [edge]: state },
82
+ });
83
+ void this.persistSettings();
74
84
  });
75
- void this.persistSettings();
76
85
  }
77
86
  togglePanel(edge) {
78
- if (this.panels[edge] === 'hidden')
79
- return;
80
- this.setPanel(edge, this.panels[edge] === 'expanded' ? 'collapsed' : 'expanded');
87
+ untrack(() => {
88
+ if (this.panels[edge] === 'hidden')
89
+ return;
90
+ this.setPanel(edge, this.panels[edge] === 'expanded' ? 'collapsed' : 'expanded');
91
+ });
81
92
  }
82
93
  expandPanel(edge) {
83
- this.setPanel(edge, 'expanded');
94
+ untrack(() => this.setPanel(edge, 'expanded'));
84
95
  }
85
96
  collapsePanel(edge) {
86
- this.setPanel(edge, 'collapsed');
97
+ untrack(() => this.setPanel(edge, 'collapsed'));
87
98
  }
88
99
  closeTopmostExpanded() {
89
- for (const edge of [...PANEL_EDGES].reverse()) {
90
- if (this.panels[edge] === 'expanded') {
91
- this.collapsePanel(edge);
92
- return true;
100
+ return untrack(() => {
101
+ for (const edge of [...PANEL_EDGES].reverse()) {
102
+ if (this.panels[edge] === 'expanded') {
103
+ this.collapsePanel(edge);
104
+ return true;
105
+ }
93
106
  }
94
- }
95
- return false;
107
+ return false;
108
+ });
96
109
  }
97
110
  registerFocusTool(tool) {
98
- this.focusTools = [
99
- ...this.focusTools.filter((existing) => existing.id !== tool.id),
100
- tool,
101
- ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
102
- if (!this.activeFocusToolId)
103
- this.activeFocusToolId = tool.id;
104
- return () => this.unregisterFocusTool(tool.id);
111
+ const nextTool = snapshotFocusTool(tool);
112
+ return untrack(() => {
113
+ this.focusTools = [
114
+ ...this.focusTools.filter((existing) => existing.id !== nextTool.id),
115
+ nextTool,
116
+ ].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
117
+ if (!this.activeFocusToolId)
118
+ this.activeFocusToolId = nextTool.id;
119
+ return () => this.unregisterFocusTool(nextTool.id);
120
+ });
105
121
  }
106
122
  unregisterFocusTool(id) {
107
- this.focusTools = this.focusTools.filter((tool) => tool.id !== id);
108
- if (this.activeFocusToolId === id) {
109
- this.activeFocusToolId = this.focusTools[0]?.id ?? null;
110
- }
123
+ untrack(() => {
124
+ this.focusTools = this.focusTools.filter((tool) => tool.id !== id);
125
+ if (this.activeFocusToolId === id) {
126
+ this.activeFocusToolId = this.focusTools[0]?.id ?? null;
127
+ }
128
+ });
111
129
  }
112
130
  openFocusTool(id) {
113
- if (!this.focusTools.some((tool) => tool.id === id))
114
- return;
115
- this.activeFocusToolId = id;
116
- this.settings = mergeShellSettingsDelta(this.settings, {
117
- activeFocusToolId: id,
131
+ untrack(() => {
132
+ if (!this.focusTools.some((tool) => tool.id === id))
133
+ return;
134
+ this.activeFocusToolId = id;
135
+ this.settings = mergeShellSettingsDelta(this.settings, {
136
+ activeFocusToolId: id,
137
+ });
138
+ this.expandPanel('right');
139
+ void this.persistSettings();
118
140
  });
119
- this.expandPanel('right');
120
- void this.persistSettings();
121
141
  }
122
142
  upsertActivity(activity) {
123
- const now = new Date().toISOString();
124
- const normalized = {
125
- ...activity,
126
- edge: activity.edge ?? this.homeEdgeForScope(activity.scope),
127
- createdAt: activity.createdAt ?? now,
128
- updatedAt: activity.updatedAt ?? now,
129
- };
130
- const previous = this.activities.find((item) => item.id === activity.id);
131
- this.activities = [
132
- ...this.activities.filter((item) => item.id !== activity.id),
133
- normalized,
134
- ];
135
- this.emitActivity({
136
- type: previous && previous.status !== normalized.status
137
- ? 'transition'
138
- : 'upsert',
139
- activity: normalized,
140
- previous,
143
+ const nextActivity = snapshotActivity(activity);
144
+ untrack(() => {
145
+ const now = new Date().toISOString();
146
+ const normalized = {
147
+ ...nextActivity,
148
+ edge: nextActivity.edge ?? this.homeEdgeForScope(nextActivity.scope),
149
+ createdAt: nextActivity.createdAt ?? now,
150
+ updatedAt: nextActivity.updatedAt ?? now,
151
+ };
152
+ const previous = this.activities.find((item) => item.id === nextActivity.id);
153
+ this.activities = [
154
+ ...this.activities.filter((item) => item.id !== nextActivity.id),
155
+ normalized,
156
+ ];
157
+ this.emitActivity({
158
+ type: previous && previous.status !== normalized.status
159
+ ? 'transition'
160
+ : 'upsert',
161
+ activity: normalized,
162
+ previous,
163
+ });
141
164
  });
142
165
  }
143
166
  updateActivity(id, patch) {
144
- const current = this.activities.find((activity) => activity.id === id);
145
- if (!current)
146
- return;
147
- this.upsertActivity({
148
- ...current,
149
- ...patch,
150
- id,
151
- updatedAt: new Date().toISOString(),
167
+ const nextPatch = snapshotActivityPatch(patch);
168
+ untrack(() => {
169
+ const current = this.activities.find((activity) => activity.id === id);
170
+ if (!current)
171
+ return;
172
+ this.upsertActivity({
173
+ ...current,
174
+ ...nextPatch,
175
+ id,
176
+ updatedAt: new Date().toISOString(),
177
+ });
152
178
  });
153
179
  }
154
180
  removeActivity(id) {
155
- const current = this.activities.find((activity) => activity.id === id);
156
- if (!current)
157
- return;
158
- this.activities = this.activities.filter((activity) => activity.id !== id);
159
- this.emitActivity({ type: 'remove', activity: current });
181
+ untrack(() => {
182
+ const current = this.activities.find((activity) => activity.id === id);
183
+ if (!current)
184
+ return;
185
+ this.activities = this.activities.filter((activity) => activity.id !== id);
186
+ this.emitActivity({ type: 'remove', activity: current });
187
+ });
160
188
  }
161
189
  watchActivities(listener) {
162
190
  this.activityListeners.add(listener);
@@ -250,3 +278,53 @@ function matchesOne(value, expected) {
250
278
  function isActiveStatus(status) {
251
279
  return status === 'queued' || status === 'running';
252
280
  }
281
+ function snapshotShellSettingsDelta(delta) {
282
+ const snapshot = { ...delta };
283
+ if ('keymap' in delta) {
284
+ snapshot.keymap = snapshotShellKeymap(delta.keymap);
285
+ }
286
+ if ('panels' in delta) {
287
+ snapshot.panels = delta.panels ? { ...delta.panels } : delta.panels;
288
+ }
289
+ return snapshot;
290
+ }
291
+ function snapshotShellKeymap(keymap) {
292
+ if (!keymap)
293
+ return keymap;
294
+ const snapshot = {};
295
+ for (const edge of PANEL_EDGES) {
296
+ if (edge in keymap) {
297
+ const binding = keymap[edge];
298
+ snapshot[edge] = binding ? { ...binding } : binding;
299
+ }
300
+ }
301
+ return snapshot;
302
+ }
303
+ function snapshotFocusTool(tool) {
304
+ const snapshot = { ...tool };
305
+ if ('subject' in tool) {
306
+ snapshot.subject = tool.subject ? { ...tool.subject } : tool.subject;
307
+ }
308
+ if ('activityKinds' in tool) {
309
+ snapshot.activityKinds = tool.activityKinds
310
+ ? [...tool.activityKinds]
311
+ : tool.activityKinds;
312
+ }
313
+ return snapshot;
314
+ }
315
+ function snapshotActivity(activity) {
316
+ const snapshot = { ...activity };
317
+ if ('subject' in activity) {
318
+ snapshot.subject = activity.subject
319
+ ? { ...activity.subject }
320
+ : activity.subject;
321
+ }
322
+ return snapshot;
323
+ }
324
+ function snapshotActivityPatch(patch) {
325
+ const snapshot = { ...patch };
326
+ if ('subject' in patch) {
327
+ snapshot.subject = patch.subject ? { ...patch.subject } : patch.subject;
328
+ }
329
+ return snapshot;
330
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.38.5",
3
+ "version": "0.38.6",
4
4
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
5
5
  "type": "module",
6
6
  "smrtRawPrimitives": "strict",
@@ -92,10 +92,10 @@
92
92
  "@tanstack/db": "^0.6.14",
93
93
  "@tanstack/svelte-db": "^0.1.91",
94
94
  "esm-env": "^1.2.2",
95
- "@happyvertical/smrt-languages": "0.38.5",
96
- "@happyvertical/smrt-types": "0.38.5",
97
- "@happyvertical/smrt-ui": "0.38.5",
98
- "@happyvertical/smrt-web": "0.38.5"
95
+ "@happyvertical/smrt-types": "0.38.6",
96
+ "@happyvertical/smrt-languages": "0.38.6",
97
+ "@happyvertical/smrt-ui": "0.38.6",
98
+ "@happyvertical/smrt-web": "0.38.6"
99
99
  },
100
100
  "peerDependencies": {
101
101
  "@huggingface/transformers": ">=3.8.1",