@rozenite/controls-plugin 1.11.0 → 1.12.0
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/CHANGELOG.md +13 -0
- package/README.md +33 -0
- package/dist/react-native/chunks/useRozeniteControlsPlugin.require.cjs +1 -1
- package/dist/react-native/chunks/useRozeniteControlsPlugin.require.js +320 -256
- package/dist/react-native/index.d.ts +5 -1
- package/dist/rozenite.json +1 -1
- package/package.json +8 -7
- package/react-native.d.ts +8 -4
- package/react-native.ts +2 -0
- package/src/react-native/__tests__/controlsRegistry.test.ts +92 -0
- package/src/react-native/__tests__/useRozeniteControlsPlugin.test.tsx +211 -0
- package/src/react-native/controlsRegistry.ts +101 -0
- package/src/react-native/useControlsAgentTools.ts +21 -14
- package/src/react-native/useRozeniteControlsPlugin.ts +56 -27
- package/src/shared/types.ts +9 -1
|
@@ -127,8 +127,12 @@ export declare type RozeniteControlsPluginOptions = {
|
|
|
127
127
|
sections: ControlsSection[];
|
|
128
128
|
};
|
|
129
129
|
|
|
130
|
+
export declare type RozeniteControlsPluginOptionsInput = RozeniteControlsPluginOptions | RozeniteControlsPluginOptionsUpdater;
|
|
131
|
+
|
|
132
|
+
export declare type RozeniteControlsPluginOptionsUpdater = (previousOptions: RozeniteControlsPluginOptions) => RozeniteControlsPluginOptions;
|
|
133
|
+
|
|
130
134
|
export declare let useRozeniteControlsPlugin: typeof useRozeniteControlsPlugin_2;
|
|
131
135
|
|
|
132
|
-
declare const useRozeniteControlsPlugin_2: (
|
|
136
|
+
declare const useRozeniteControlsPlugin_2: (optionsInput: RozeniteControlsPluginOptionsInput) => RozeniteDevToolsClient<ControlsEventMap> | null;
|
|
133
137
|
|
|
134
138
|
export { }
|
package/dist/rozenite.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"name":"@rozenite/controls-plugin","version":"1.
|
|
1
|
+
{"name":"@rozenite/controls-plugin","version":"1.12.0","description":"A Rozenite plugin for exposing app-defined controls directly in React Native DevTools.","panels":[{"name":"Controls","source":"/devtools/panel.html"}]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rozenite/controls-plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"description": "A Rozenite plugin for exposing app-defined controls directly in React Native DevTools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/react-native/index.cjs",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
"url": "https://github.com/callstackincubator/rozenite.git"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@rozenite/agent-shared": "1.
|
|
19
|
-
"@rozenite/agent-bridge": "1.
|
|
20
|
-
"@rozenite/plugin-bridge": "1.
|
|
18
|
+
"@rozenite/agent-shared": "1.12.0",
|
|
19
|
+
"@rozenite/agent-bridge": "1.12.0",
|
|
20
|
+
"@rozenite/plugin-bridge": "1.12.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/react": "~19.2.2",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"tailwindcss-animate": "^1.0.7",
|
|
33
33
|
"typescript": "~5.9.3",
|
|
34
34
|
"vite": "^7.3.1",
|
|
35
|
-
"@rozenite/vite-plugin": "1.
|
|
36
|
-
"rozenite": "1.
|
|
35
|
+
"@rozenite/vite-plugin": "1.12.0",
|
|
36
|
+
"rozenite": "1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"react": "*",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"build": "rozenite build",
|
|
62
62
|
"dev": "rozenite dev",
|
|
63
63
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
64
|
-
"lint": "eslint ."
|
|
64
|
+
"lint": "eslint .",
|
|
65
|
+
"test": "vitest --run --passWithNoTests"
|
|
65
66
|
}
|
|
66
67
|
}
|
package/react-native.d.ts
CHANGED
|
@@ -8,12 +8,16 @@ export {
|
|
|
8
8
|
type ControlsSection,
|
|
9
9
|
type ControlsTextItem,
|
|
10
10
|
type ControlsToggleItem,
|
|
11
|
+
type RozeniteControlsPluginOptionsInput,
|
|
11
12
|
type ControlsValidationResult,
|
|
12
13
|
type RozeniteControlsPluginOptions,
|
|
14
|
+
type RozeniteControlsPluginOptionsUpdater,
|
|
13
15
|
} from './src/shared/types';
|
|
14
16
|
|
|
15
17
|
export declare const useRozeniteControlsPlugin: (
|
|
16
|
-
options: import('./src/shared/types').
|
|
17
|
-
) =>
|
|
18
|
-
import('
|
|
19
|
-
|
|
18
|
+
options: import('./src/shared/types').RozeniteControlsPluginOptionsInput,
|
|
19
|
+
) =>
|
|
20
|
+
| import('@rozenite/plugin-bridge').RozeniteDevToolsClient<
|
|
21
|
+
import('./src/shared/messaging').ControlsEventMap
|
|
22
|
+
>
|
|
23
|
+
| null;
|
package/react-native.ts
CHANGED
|
@@ -8,8 +8,10 @@ export {
|
|
|
8
8
|
type ControlsSection,
|
|
9
9
|
type ControlsTextItem,
|
|
10
10
|
type ControlsToggleItem,
|
|
11
|
+
type RozeniteControlsPluginOptionsInput,
|
|
11
12
|
type ControlsValidationResult,
|
|
12
13
|
type RozeniteControlsPluginOptions,
|
|
14
|
+
type RozeniteControlsPluginOptionsUpdater,
|
|
13
15
|
} from './src/shared/types';
|
|
14
16
|
|
|
15
17
|
export let useRozeniteControlsPlugin: typeof import('./src/react-native/useRozeniteControlsPlugin').useRozeniteControlsPlugin;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createControlsRegistry } from '../controlsRegistry';
|
|
3
|
+
import type { ControlsSection } from '../../shared/types';
|
|
4
|
+
|
|
5
|
+
const section = (id: string): ControlsSection => ({
|
|
6
|
+
id,
|
|
7
|
+
title: id,
|
|
8
|
+
items: [],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('controlsRegistry', () => {
|
|
12
|
+
it('combines section registrations in registration order', () => {
|
|
13
|
+
const registry = createControlsRegistry();
|
|
14
|
+
const first = Symbol('first');
|
|
15
|
+
const second = Symbol('second');
|
|
16
|
+
|
|
17
|
+
registry.set(first, { sections: [section('app')] });
|
|
18
|
+
registry.set(second, { sections: [section('locale')] });
|
|
19
|
+
|
|
20
|
+
expect(registry.getOptions().sections.map(({ id }) => id)).toEqual([
|
|
21
|
+
'app',
|
|
22
|
+
'locale',
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('lets updater registrations derive from previous options', () => {
|
|
27
|
+
const registry = createControlsRegistry();
|
|
28
|
+
const first = Symbol('first');
|
|
29
|
+
const second = Symbol('second');
|
|
30
|
+
|
|
31
|
+
registry.set(first, { sections: [section('app')] });
|
|
32
|
+
registry.set(second, (previousOptions) => ({
|
|
33
|
+
sections: [...previousOptions.sections, section('locale')],
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
expect(registry.getOptions().sections.map(({ id }) => id)).toEqual([
|
|
37
|
+
'app',
|
|
38
|
+
'locale',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('lets updater registrations replace previous options when needed', () => {
|
|
43
|
+
const registry = createControlsRegistry();
|
|
44
|
+
const first = Symbol('first');
|
|
45
|
+
const second = Symbol('second');
|
|
46
|
+
|
|
47
|
+
registry.set(first, { sections: [section('app')] });
|
|
48
|
+
registry.set(second, () => ({
|
|
49
|
+
sections: [section('replacement')],
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
expect(registry.getOptions().sections.map(({ id }) => id)).toEqual([
|
|
53
|
+
'replacement',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('can include the current render input before the effect registers it', () => {
|
|
58
|
+
const registry = createControlsRegistry();
|
|
59
|
+
const id = Symbol('pending');
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
registry
|
|
63
|
+
.getOptions({ id, input: { sections: [section('pending')] } })
|
|
64
|
+
.sections.map((registeredSection) => registeredSection.id),
|
|
65
|
+
).toEqual(['pending']);
|
|
66
|
+
expect(registry.isOwner(id, { id, input: { sections: [] } })).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('notifies subscribers when ownership changes or registrations are removed', () => {
|
|
70
|
+
const registry = createControlsRegistry();
|
|
71
|
+
const listener = vi.fn();
|
|
72
|
+
const first = Symbol('first');
|
|
73
|
+
const second = Symbol('second');
|
|
74
|
+
|
|
75
|
+
registry.subscribe(listener);
|
|
76
|
+
expect(registry.getSnapshot()).toBe(0);
|
|
77
|
+
|
|
78
|
+
registry.set(first, { sections: [section('app')] });
|
|
79
|
+
expect(registry.getSnapshot()).toBe(1);
|
|
80
|
+
|
|
81
|
+
registry.set(second, { sections: [section('locale')] });
|
|
82
|
+
expect(registry.getSnapshot()).toBe(1);
|
|
83
|
+
|
|
84
|
+
registry.delete(second);
|
|
85
|
+
expect(registry.getSnapshot()).toBe(2);
|
|
86
|
+
|
|
87
|
+
registry.delete(first);
|
|
88
|
+
expect(registry.getSnapshot()).toBe(3);
|
|
89
|
+
|
|
90
|
+
expect(listener).toHaveBeenCalledTimes(3);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, type ReactNode } from 'react';
|
|
4
|
+
import { createRoot, type Root } from 'react-dom/client';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { createSection } from '../../shared/types';
|
|
7
|
+
import type { ControlsEventMap } from '../../shared/messaging';
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Listener = (payload: unknown) => void;
|
|
14
|
+
|
|
15
|
+
const mocks = vi.hoisted(() => {
|
|
16
|
+
const listeners = new Map<string, Set<Listener>>();
|
|
17
|
+
const send = vi.fn();
|
|
18
|
+
const onMessage = vi.fn((type: string, listener: Listener) => {
|
|
19
|
+
const typeListeners = listeners.get(type) ?? new Set<Listener>();
|
|
20
|
+
typeListeners.add(listener);
|
|
21
|
+
listeners.set(type, typeListeners);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
remove: () => {
|
|
25
|
+
typeListeners.delete(listener);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
client: {
|
|
32
|
+
send,
|
|
33
|
+
onMessage,
|
|
34
|
+
},
|
|
35
|
+
emit: (type: keyof ControlsEventMap, payload: unknown) => {
|
|
36
|
+
listeners.get(type)?.forEach((listener) => {
|
|
37
|
+
listener(payload);
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
reset: () => {
|
|
41
|
+
listeners.clear();
|
|
42
|
+
send.mockReset();
|
|
43
|
+
onMessage.mockClear();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
vi.mock('@rozenite/plugin-bridge', () => ({
|
|
49
|
+
useRozeniteDevToolsClient: () => mocks.client,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock('../useControlsAgentTools', () => ({
|
|
53
|
+
useControlsAgentTools: vi.fn(),
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
import { useRozeniteControlsPlugin } from '../useRozeniteControlsPlugin';
|
|
57
|
+
|
|
58
|
+
const appSection = createSection({
|
|
59
|
+
id: 'app',
|
|
60
|
+
title: 'App',
|
|
61
|
+
items: [],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const localeSection = (onUpdate: (value: string) => void = vi.fn()) =>
|
|
65
|
+
createSection({
|
|
66
|
+
id: 'locale',
|
|
67
|
+
title: 'Locale',
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
id: 'language',
|
|
71
|
+
type: 'select',
|
|
72
|
+
title: 'Language',
|
|
73
|
+
value: 'en',
|
|
74
|
+
options: [
|
|
75
|
+
{ label: 'English', value: 'en' },
|
|
76
|
+
{ label: 'Polish', value: 'pl' },
|
|
77
|
+
],
|
|
78
|
+
onUpdate,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function AppControls() {
|
|
84
|
+
useRozeniteControlsPlugin({
|
|
85
|
+
sections: [appSection],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function LocaleControls({
|
|
92
|
+
onUpdate = vi.fn(),
|
|
93
|
+
}: {
|
|
94
|
+
onUpdate?: (value: string) => void;
|
|
95
|
+
}) {
|
|
96
|
+
useRozeniteControlsPlugin((previousOptions) => ({
|
|
97
|
+
sections: [...previousOptions.sections, localeSection(onUpdate)],
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function TestApp({
|
|
104
|
+
showLocale = true,
|
|
105
|
+
onUpdate,
|
|
106
|
+
}: {
|
|
107
|
+
showLocale?: boolean;
|
|
108
|
+
onUpdate?: (value: string) => void;
|
|
109
|
+
}) {
|
|
110
|
+
return (
|
|
111
|
+
<>
|
|
112
|
+
<AppControls />
|
|
113
|
+
{showLocale ? <LocaleControls onUpdate={onUpdate} /> : null}
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const renderControls = async (
|
|
119
|
+
element: ReactNode,
|
|
120
|
+
): Promise<{
|
|
121
|
+
root: Root;
|
|
122
|
+
container: HTMLDivElement;
|
|
123
|
+
}> => {
|
|
124
|
+
const container = document.createElement('div');
|
|
125
|
+
document.body.appendChild(container);
|
|
126
|
+
const root = createRoot(container);
|
|
127
|
+
|
|
128
|
+
await act(async () => {
|
|
129
|
+
root.render(element);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { root, container };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const unmountControls = async (root: Root, container: HTMLDivElement) => {
|
|
136
|
+
await act(async () => {
|
|
137
|
+
root.unmount();
|
|
138
|
+
});
|
|
139
|
+
container.remove();
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getLastSnapshotSectionIds = () => {
|
|
143
|
+
const snapshots = mocks.client.send.mock.calls.filter(
|
|
144
|
+
([type]) => type === 'snapshot',
|
|
145
|
+
);
|
|
146
|
+
const lastSnapshot = snapshots.at(-1)?.[1] as
|
|
147
|
+
| ControlsEventMap['snapshot']
|
|
148
|
+
| undefined;
|
|
149
|
+
|
|
150
|
+
return lastSnapshot?.sections.map((section) => section.id);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
describe('useRozeniteControlsPlugin', () => {
|
|
154
|
+
beforeEach(() => {
|
|
155
|
+
mocks.reset();
|
|
156
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
afterEach(() => {
|
|
160
|
+
document.body.innerHTML = '';
|
|
161
|
+
delete globalThis.IS_REACT_ACT_ENVIRONMENT;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('publishes a combined snapshot from multiple hook instances', async () => {
|
|
165
|
+
const { root, container } = await renderControls(<TestApp />);
|
|
166
|
+
|
|
167
|
+
expect(getLastSnapshotSectionIds()).toEqual(['app', 'locale']);
|
|
168
|
+
|
|
169
|
+
await unmountControls(root, container);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('removes sections when their hook instance unmounts', async () => {
|
|
173
|
+
const { root, container } = await renderControls(<TestApp />);
|
|
174
|
+
|
|
175
|
+
await act(async () => {
|
|
176
|
+
root.render(<TestApp showLocale={false} />);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(getLastSnapshotSectionIds()).toEqual(['app']);
|
|
180
|
+
|
|
181
|
+
await unmountControls(root, container);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('routes updates to sections registered by another hook instance', async () => {
|
|
185
|
+
const onUpdate = vi.fn();
|
|
186
|
+
const { root, container } = await renderControls(
|
|
187
|
+
<TestApp onUpdate={onUpdate} />,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
await act(async () => {
|
|
191
|
+
mocks.emit('update-request', {
|
|
192
|
+
type: 'update-request',
|
|
193
|
+
requestId: 'request-1',
|
|
194
|
+
sectionId: 'locale',
|
|
195
|
+
itemId: 'language',
|
|
196
|
+
value: 'pl',
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(onUpdate).toHaveBeenCalledWith('pl');
|
|
201
|
+
expect(mocks.client.send).toHaveBeenCalledWith('update-result', {
|
|
202
|
+
type: 'update-result',
|
|
203
|
+
requestId: 'request-1',
|
|
204
|
+
sectionId: 'locale',
|
|
205
|
+
itemId: 'language',
|
|
206
|
+
status: 'ok',
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await unmountControls(root, container);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RozeniteControlsPluginOptions,
|
|
3
|
+
RozeniteControlsPluginOptionsInput,
|
|
4
|
+
} from '../shared/types';
|
|
5
|
+
|
|
6
|
+
type ControlsRegistryListener = () => void;
|
|
7
|
+
type ControlsRegistryOverride = {
|
|
8
|
+
id: symbol;
|
|
9
|
+
input: RozeniteControlsPluginOptionsInput;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const EMPTY_OPTIONS: RozeniteControlsPluginOptions = {
|
|
13
|
+
sections: [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const resolveOptionsInput = (
|
|
17
|
+
previousOptions: RozeniteControlsPluginOptions,
|
|
18
|
+
input: RozeniteControlsPluginOptionsInput,
|
|
19
|
+
): RozeniteControlsPluginOptions => {
|
|
20
|
+
if (typeof input === 'function') {
|
|
21
|
+
return input(previousOptions);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
...previousOptions,
|
|
26
|
+
...input,
|
|
27
|
+
sections: [...previousOptions.sections, ...input.sections],
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const createControlsRegistry = () => {
|
|
32
|
+
const registrations = new Map<symbol, RozeniteControlsPluginOptionsInput>();
|
|
33
|
+
const listeners = new Set<ControlsRegistryListener>();
|
|
34
|
+
let snapshot = 0;
|
|
35
|
+
|
|
36
|
+
const getOwnerId = () => registrations.keys().next().value;
|
|
37
|
+
|
|
38
|
+
const notify = () => {
|
|
39
|
+
snapshot += 1;
|
|
40
|
+
listeners.forEach((listener) => listener());
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const getRegistrationEntries = (override?: ControlsRegistryOverride) => {
|
|
44
|
+
const entries = Array.from(registrations.entries());
|
|
45
|
+
|
|
46
|
+
if (!override) {
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const overrideIndex = entries.findIndex(([id]) => id === override.id);
|
|
51
|
+
|
|
52
|
+
if (overrideIndex === -1) {
|
|
53
|
+
return [...entries, [override.id, override.input] as const];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries.map(([id, input]) =>
|
|
57
|
+
id === override.id
|
|
58
|
+
? ([id, override.input] as const)
|
|
59
|
+
: ([id, input] as const),
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
set(id: symbol, input: RozeniteControlsPluginOptionsInput) {
|
|
65
|
+
const previousOwnerId = getOwnerId();
|
|
66
|
+
registrations.set(id, input);
|
|
67
|
+
|
|
68
|
+
if (previousOwnerId !== getOwnerId()) {
|
|
69
|
+
notify();
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
delete(id: symbol) {
|
|
73
|
+
registrations.delete(id);
|
|
74
|
+
notify();
|
|
75
|
+
},
|
|
76
|
+
getOptions(
|
|
77
|
+
override?: ControlsRegistryOverride,
|
|
78
|
+
): RozeniteControlsPluginOptions {
|
|
79
|
+
return getRegistrationEntries(override)
|
|
80
|
+
.map(([, input]) => input)
|
|
81
|
+
.reduce(resolveOptionsInput, EMPTY_OPTIONS);
|
|
82
|
+
},
|
|
83
|
+
isOwner(id: symbol, override?: ControlsRegistryOverride) {
|
|
84
|
+
const ownerEntry = getRegistrationEntries(override)[0];
|
|
85
|
+
|
|
86
|
+
return ownerEntry?.[0] === id;
|
|
87
|
+
},
|
|
88
|
+
getSnapshot() {
|
|
89
|
+
return snapshot;
|
|
90
|
+
},
|
|
91
|
+
subscribe(listener: ControlsRegistryListener) {
|
|
92
|
+
listeners.add(listener);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
listeners.delete(listener);
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const controlsRegistry = createControlsRegistry();
|
|
@@ -8,14 +8,14 @@ import type { ControlsSection } from '../shared/types';
|
|
|
8
8
|
const resolveItem = (
|
|
9
9
|
sections: ControlsSection[],
|
|
10
10
|
sectionId: string,
|
|
11
|
-
itemId: string
|
|
11
|
+
itemId: string,
|
|
12
12
|
) => {
|
|
13
13
|
const section = sections.find((s) => s.id === sectionId);
|
|
14
14
|
|
|
15
15
|
if (!section) {
|
|
16
16
|
const available = sections.map((s) => s.id).join(', ');
|
|
17
17
|
throw new Error(
|
|
18
|
-
`Section "${sectionId}" not found. Available: ${available || '(none)'}
|
|
18
|
+
`Section "${sectionId}" not found. Available: ${available || '(none)'}`,
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -24,19 +24,23 @@ const resolveItem = (
|
|
|
24
24
|
if (!item) {
|
|
25
25
|
const available = section.items.map((i) => i.id).join(', ');
|
|
26
26
|
throw new Error(
|
|
27
|
-
`Item "${itemId}" not found in section "${sectionId}". Available: ${available || '(none)'}
|
|
27
|
+
`Item "${itemId}" not found in section "${sectionId}". Available: ${available || '(none)'}`,
|
|
28
28
|
);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
return { section, item };
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
-
export const useControlsAgentTools = (
|
|
34
|
+
export const useControlsAgentTools = (
|
|
35
|
+
getSections: () => ControlsSection[],
|
|
36
|
+
enabled = true,
|
|
37
|
+
) => {
|
|
35
38
|
useRozenitePluginAgentTool({
|
|
36
39
|
pluginId: CONTROLS_AGENT_PLUGIN_ID,
|
|
37
40
|
tool: controlsToolDefinitions.listSections,
|
|
41
|
+
enabled,
|
|
38
42
|
handler: () => ({
|
|
39
|
-
sections:
|
|
43
|
+
sections: getSections().map((section) => ({
|
|
40
44
|
id: section.id,
|
|
41
45
|
title: section.title,
|
|
42
46
|
description: section.description,
|
|
@@ -53,8 +57,9 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
53
57
|
useRozenitePluginAgentTool({
|
|
54
58
|
pluginId: CONTROLS_AGENT_PLUGIN_ID,
|
|
55
59
|
tool: controlsToolDefinitions.getItem,
|
|
60
|
+
enabled,
|
|
56
61
|
handler: ({ sectionId, itemId }) => {
|
|
57
|
-
const { item } = resolveItem(
|
|
62
|
+
const { item } = resolveItem(getSections(), sectionId, itemId);
|
|
58
63
|
|
|
59
64
|
if (item.type === 'text') {
|
|
60
65
|
return {
|
|
@@ -131,18 +136,19 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
131
136
|
useRozenitePluginAgentTool({
|
|
132
137
|
pluginId: CONTROLS_AGENT_PLUGIN_ID,
|
|
133
138
|
tool: controlsToolDefinitions.setValue,
|
|
139
|
+
enabled,
|
|
134
140
|
handler: async ({ sectionId, itemId, value }) => {
|
|
135
|
-
const { item } = resolveItem(
|
|
141
|
+
const { item } = resolveItem(getSections(), sectionId, itemId);
|
|
136
142
|
|
|
137
143
|
if (item.type === 'text') {
|
|
138
144
|
throw new Error(
|
|
139
|
-
`Item "${itemId}" is a read-only text item and cannot be updated
|
|
145
|
+
`Item "${itemId}" is a read-only text item and cannot be updated.`,
|
|
140
146
|
);
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
if (item.type === 'button') {
|
|
144
150
|
throw new Error(
|
|
145
|
-
`Item "${itemId}" is a button. Use press-button to trigger its action
|
|
151
|
+
`Item "${itemId}" is a button. Use press-button to trigger its action.`,
|
|
146
152
|
);
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -153,7 +159,7 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
153
159
|
if (item.type === 'toggle') {
|
|
154
160
|
if (typeof value !== 'boolean') {
|
|
155
161
|
throw new Error(
|
|
156
|
-
`Expected boolean value for toggle item "${itemId}"
|
|
162
|
+
`Expected boolean value for toggle item "${itemId}".`,
|
|
157
163
|
);
|
|
158
164
|
}
|
|
159
165
|
|
|
@@ -170,7 +176,7 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
170
176
|
|
|
171
177
|
if (typeof value !== 'string') {
|
|
172
178
|
throw new Error(
|
|
173
|
-
`Expected string value for ${item.type} item "${itemId}"
|
|
179
|
+
`Expected string value for ${item.type} item "${itemId}".`,
|
|
174
180
|
);
|
|
175
181
|
}
|
|
176
182
|
|
|
@@ -178,7 +184,7 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
178
184
|
const validOptions = item.options.map((o) => o.value);
|
|
179
185
|
if (!validOptions.includes(value)) {
|
|
180
186
|
throw new Error(
|
|
181
|
-
`Invalid option "${value}" for item "${itemId}". Valid options: ${validOptions.join(', ')}
|
|
187
|
+
`Invalid option "${value}" for item "${itemId}". Valid options: ${validOptions.join(', ')}`,
|
|
182
188
|
);
|
|
183
189
|
}
|
|
184
190
|
}
|
|
@@ -198,12 +204,13 @@ export const useControlsAgentTools = (sections: ControlsSection[]) => {
|
|
|
198
204
|
useRozenitePluginAgentTool({
|
|
199
205
|
pluginId: CONTROLS_AGENT_PLUGIN_ID,
|
|
200
206
|
tool: controlsToolDefinitions.pressButton,
|
|
207
|
+
enabled,
|
|
201
208
|
handler: async ({ sectionId, itemId }) => {
|
|
202
|
-
const { item } = resolveItem(
|
|
209
|
+
const { item } = resolveItem(getSections(), sectionId, itemId);
|
|
203
210
|
|
|
204
211
|
if (item.type !== 'button') {
|
|
205
212
|
throw new Error(
|
|
206
|
-
`Item "${itemId}" is not a button (type: ${item.type}). Use set-value to update its value
|
|
213
|
+
`Item "${itemId}" is not a button (type: ${item.type}). Use set-value to update its value.`,
|
|
207
214
|
);
|
|
208
215
|
}
|
|
209
216
|
|