d2d-feedbackkit 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -64,6 +64,12 @@ const locator = createFeedbackLocator({
64
64
  appKey: "host-app",
65
65
  appName: "Host App",
66
66
  showFloatingButton: false,
67
+ enableVideoRecording: false,
68
+ theme: {
69
+ primary: "45 96% 53%",
70
+ primaryForeground: "30 70% 8%",
71
+ ring: "45 96% 53%",
72
+ },
67
73
  submitFeedback: async (input) => {
68
74
  const response = await fetch("https://feedbackkit.d2d.cloud/api/submissions", {
69
75
  method: "POST",
@@ -0,0 +1,319 @@
1
+ // src/babel.ts
2
+ import path from "node:path";
3
+ import { parseExpression } from "@babel/parser";
4
+ function feedbackLocatorBabelPlugin(babel) {
5
+ const t = babel.types;
6
+ function ensureState(state) {
7
+ if (!state.feedbackLocator) {
8
+ throw new Error("Feedback Locator Babel state is not initialized");
9
+ }
10
+ return state.feedbackLocator;
11
+ }
12
+ function shouldSkip(state) {
13
+ return !state.filename || state.filename.includes("node_modules");
14
+ }
15
+ function enterComponent(state, name, loc) {
16
+ if (!name || !loc || !isComponentName(name)) {
17
+ return null;
18
+ }
19
+ const transformState = ensureState(state);
20
+ if (state.opts?.ignoreComponentNames?.includes(name)) {
21
+ return null;
22
+ }
23
+ const id = transformState.components.length;
24
+ transformState.components.push({ name, loc });
25
+ transformState.componentStack.push(id);
26
+ return id;
27
+ }
28
+ function exitComponent(state, id) {
29
+ if (id === null) {
30
+ return;
31
+ }
32
+ const transformState = ensureState(state);
33
+ const last = transformState.componentStack.at(-1);
34
+ if (last === id) {
35
+ transformState.componentStack.pop();
36
+ }
37
+ }
38
+ return {
39
+ name: "feedbackkit-source-locator-babel",
40
+ visitor: {
41
+ Program: {
42
+ enter(_programPath, state) {
43
+ if (shouldSkip(state)) {
44
+ return;
45
+ }
46
+ const projectRoot = normalizePath(state.opts?.projectRoot ?? state.cwd ?? process.cwd());
47
+ const filename = normalizePath(state.filename);
48
+ const filePath = toRepoRelativePath(filename, projectRoot);
49
+ const fileKey = filePath;
50
+ state.feedbackLocator = {
51
+ projectRoot,
52
+ filePath,
53
+ fileKey,
54
+ components: [],
55
+ expressions: [],
56
+ componentStack: []
57
+ };
58
+ },
59
+ exit(programPath, state) {
60
+ if (shouldSkip(state) || !state.feedbackLocator) {
61
+ return;
62
+ }
63
+ const transformState = state.feedbackLocator;
64
+ if (transformState.expressions.length === 0) {
65
+ return;
66
+ }
67
+ const data = {
68
+ filePath: transformState.filePath,
69
+ projectPath: transformState.projectRoot,
70
+ expressions: transformState.expressions,
71
+ components: transformState.components
72
+ };
73
+ const serializedData = JSON.stringify(data);
74
+ const serializedFileKey = JSON.stringify(transformState.fileKey);
75
+ const registerCode = `(() => {
76
+ if (typeof window !== "undefined") {
77
+ window.__FEEDBACK_LOCATOR_DATA__ = window.__FEEDBACK_LOCATOR_DATA__ || {};
78
+ window.__FEEDBACK_LOCATOR_DATA__[${serializedFileKey}] = ${serializedData};
79
+ }
80
+ })()`;
81
+ programPath.pushContainer("body", t.expressionStatement(parseExpression(registerCode)));
82
+ }
83
+ },
84
+ FunctionDeclaration: {
85
+ enter(pathNode, state) {
86
+ if (shouldSkip(state) || !state.feedbackLocator) {
87
+ return;
88
+ }
89
+ pathNode.setData("feedbackLocatorComponentId", enterComponent(state, pathNode.node.id?.name, pathNode.node.loc));
90
+ },
91
+ exit(pathNode, state) {
92
+ if (shouldSkip(state) || !state.feedbackLocator) {
93
+ return;
94
+ }
95
+ exitComponent(state, pathNode.getData("feedbackLocatorComponentId") ?? null);
96
+ }
97
+ },
98
+ VariableDeclarator: {
99
+ enter(pathNode, state) {
100
+ if (shouldSkip(state) || !state.feedbackLocator) {
101
+ return;
102
+ }
103
+ const initType = pathNode.node.init?.type;
104
+ const isFunction = initType === "ArrowFunctionExpression" || initType === "FunctionExpression";
105
+ const name = pathNode.node.id?.type === "Identifier" ? pathNode.node.id.name : null;
106
+ pathNode.setData("feedbackLocatorComponentId", isFunction ? enterComponent(state, name, pathNode.node.loc) : null);
107
+ },
108
+ exit(pathNode, state) {
109
+ if (shouldSkip(state) || !state.feedbackLocator) {
110
+ return;
111
+ }
112
+ exitComponent(state, pathNode.getData("feedbackLocatorComponentId") ?? null);
113
+ }
114
+ },
115
+ JSXElement(pathNode, state) {
116
+ if (shouldSkip(state) || !state.feedbackLocator || !pathNode.node.loc) {
117
+ return;
118
+ }
119
+ const openingElement = pathNode.node.openingElement;
120
+ const name = getJsxName(openingElement.name);
121
+ if (!isDomJsxName(openingElement.name)) {
122
+ return;
123
+ }
124
+ if (openingElement.attributes.some((attribute) => attribute.type === "JSXAttribute" && attribute.name?.name === "data-feedback-locator-id")) {
125
+ return;
126
+ }
127
+ const transformState = state.feedbackLocator;
128
+ const expressionId = transformState.expressions.length;
129
+ const componentId = transformState.componentStack.at(-1) ?? null;
130
+ const locatorId = `${transformState.fileKey}::${expressionId}`;
131
+ transformState.expressions.push({
132
+ name,
133
+ loc: pathNode.node.loc,
134
+ componentId
135
+ });
136
+ openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier("data-feedback-locator-id"), t.stringLiteral(locatorId)));
137
+ }
138
+ }
139
+ };
140
+ }
141
+ function getJsxName(node) {
142
+ if (node.type === "JSXIdentifier") {
143
+ return node.name;
144
+ }
145
+ if (node.type === "JSXMemberExpression") {
146
+ return `${getJsxName(node.object)}.${node.property.name}`;
147
+ }
148
+ if (node.type === "JSXNamespacedName") {
149
+ return `${node.namespace.name}.${node.name.name}`;
150
+ }
151
+ return "unknown";
152
+ }
153
+ function isDomJsxName(node) {
154
+ if (node.type !== "JSXIdentifier") {
155
+ return false;
156
+ }
157
+ return /^[a-z]/.test(node.name);
158
+ }
159
+ function isComponentName(name) {
160
+ return /^[A-Z]/.test(name);
161
+ }
162
+ function normalizePath(value) {
163
+ return value.split(path.sep).join("/");
164
+ }
165
+ function toRepoRelativePath(filename, projectRoot) {
166
+ const relative = normalizePath(path.relative(projectRoot, filename));
167
+ return relative.startsWith(".") ? filename : relative;
168
+ }
169
+ // src/source.ts
170
+ var feedbackLocatorDataAttribute = "data-feedback-locator-id";
171
+ var feedbackLocatorDataKey = "__FEEDBACK_LOCATOR_DATA__";
172
+ var locatorJsDataAttribute = "data-locatorjs-id";
173
+ function parseFeedbackLocatorId(locatorId) {
174
+ const separatorIndex = locatorId.lastIndexOf("::");
175
+ if (separatorIndex === -1) {
176
+ throw new Error("feedback locator id is malformed");
177
+ }
178
+ const fileKey = locatorId.slice(0, separatorIndex);
179
+ const expressionId = locatorId.slice(separatorIndex + 2);
180
+ if (!fileKey || !expressionId) {
181
+ throw new Error("feedback locator id is malformed");
182
+ }
183
+ return {
184
+ fileKey,
185
+ expressionIndex: Number(expressionId)
186
+ };
187
+ }
188
+ function getFeedbackLocatorSourceForElement(target) {
189
+ const found = target.closest(`[${feedbackLocatorDataAttribute}]`);
190
+ if (!(found instanceof HTMLElement)) {
191
+ return getLocatorJsSourceForElement(target);
192
+ }
193
+ const locatorId = found.dataset.feedbackLocatorId;
194
+ if (!locatorId || typeof window === "undefined") {
195
+ return getLocatorJsSourceForElement(target);
196
+ }
197
+ const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
198
+ const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
199
+ const expression = fileData?.expressions[expressionIndex];
200
+ if (!fileData || !expression) {
201
+ return getLocatorJsSourceForElement(found);
202
+ }
203
+ const component = getFeedbackLocatorComponent(fileData, expression.componentId);
204
+ const locatorJsComponent = component ? null : getLocatorJsComponentForElement(found);
205
+ const ancestorComponent = component || locatorJsComponent ? null : getAncestorComponentForElement(found);
206
+ const resolvedComponent = component ?? locatorJsComponent ?? ancestorComponent;
207
+ return {
208
+ locatorId,
209
+ filePath: fileData.filePath,
210
+ elementName: expression.name,
211
+ componentName: resolvedComponent?.name ?? null,
212
+ line: expression.loc.start.line,
213
+ column: expression.loc.start.column + 1,
214
+ componentLine: resolvedComponent?.line ?? null,
215
+ componentColumn: resolvedComponent?.column ?? null
216
+ };
217
+ }
218
+ function getLocatorJsSourceForElement(target) {
219
+ const found = target.closest(`[${locatorJsDataAttribute}]`);
220
+ if (!(found instanceof HTMLElement) || typeof window === "undefined") {
221
+ return null;
222
+ }
223
+ const locatorId = found.getAttribute(locatorJsDataAttribute);
224
+ if (!locatorId) {
225
+ return null;
226
+ }
227
+ const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
228
+ const fileData = window.__LOCATOR_DATA__?.[fileKey];
229
+ const expression = fileData?.expressions?.[expressionIndex];
230
+ if (!fileData || !expression) {
231
+ return null;
232
+ }
233
+ const component = getLocatorJsComponentForElement(found);
234
+ return {
235
+ locatorId,
236
+ filePath: toLocatorJsFilePath(fileData, fileKey),
237
+ elementName: expression.name,
238
+ componentName: component?.name ?? null,
239
+ line: expression.loc.start.line,
240
+ column: expression.loc.start.column + 1,
241
+ componentLine: component?.line ?? null,
242
+ componentColumn: component?.column ?? null
243
+ };
244
+ }
245
+ function toLocatorJsFilePath(fileData, fileKey) {
246
+ const filePath = fileData.filePath ?? fileKey;
247
+ const projectPath = fileData.projectPath;
248
+ if (projectPath && filePath.startsWith(projectPath)) {
249
+ return filePath.slice(projectPath.length).replace(/^\//, "");
250
+ }
251
+ return filePath.replace(/^\//, "");
252
+ }
253
+ function getFeedbackLocatorComponent(fileData, componentId) {
254
+ if (componentId === null) {
255
+ return null;
256
+ }
257
+ const component = fileData.components[componentId];
258
+ if (!component) {
259
+ return null;
260
+ }
261
+ return {
262
+ name: component.name,
263
+ line: component.loc.start.line,
264
+ column: component.loc.start.column + 1
265
+ };
266
+ }
267
+ function getAncestorComponentForElement(element) {
268
+ let parent = element.parentElement;
269
+ while (parent) {
270
+ const locatorId = parent.getAttribute(feedbackLocatorDataAttribute);
271
+ if (locatorId) {
272
+ const component = getFeedbackLocatorComponentForLocatorId(locatorId);
273
+ if (component) {
274
+ return component;
275
+ }
276
+ }
277
+ parent = parent.parentElement;
278
+ }
279
+ return null;
280
+ }
281
+ function getFeedbackLocatorComponentForLocatorId(locatorId) {
282
+ const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
283
+ const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
284
+ const expression = fileData?.expressions[expressionIndex];
285
+ if (!fileData || !expression) {
286
+ return null;
287
+ }
288
+ return getFeedbackLocatorComponent(fileData, expression.componentId);
289
+ }
290
+ function getLocatorJsComponentForElement(element) {
291
+ const locatorJsId = element.getAttribute(locatorJsDataAttribute);
292
+ if (!locatorJsId) {
293
+ return null;
294
+ }
295
+ const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorJsId);
296
+ const fileData = window.__LOCATOR_DATA__?.[fileKey];
297
+ const expression = fileData?.expressions?.[expressionIndex];
298
+ const componentId = expression?.wrappingComponentId;
299
+ if (componentId === null || componentId === undefined) {
300
+ return null;
301
+ }
302
+ const component = fileData?.components?.[componentId];
303
+ if (!component) {
304
+ return null;
305
+ }
306
+ return {
307
+ name: component.name,
308
+ line: component.loc?.start.line ?? null,
309
+ column: component.loc?.start.column !== undefined ? component.loc.start.column + 1 : null
310
+ };
311
+ }
312
+ export {
313
+ parseFeedbackLocatorId,
314
+ getFeedbackLocatorSourceForElement,
315
+ feedbackLocatorDataKey,
316
+ feedbackLocatorDataAttribute,
317
+ feedbackLocatorBabelPlugin as feedbackKitBabelPlugin,
318
+ feedbackLocatorBabelPlugin as default
319
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d2d-feedbackkit",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "SolidJS feedback widget, source locator, and integration helpers.",
6
6
  "license": "UNLICENSED",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "./vite-plugin": {
26
26
  "types": "./src/vite-plugin.ts",
27
- "default": "./src/vite-plugin.ts"
27
+ "default": "./dist/vite-plugin.js"
28
28
  },
29
29
  "./client": {
30
30
  "types": "./src/client.ts",
@@ -61,10 +61,11 @@
61
61
  "src/types.ts",
62
62
  "src/vite-plugin.ts",
63
63
  "src/web-component.tsx",
64
- "src/widget.tsx"
64
+ "src/widget.tsx",
65
+ "dist/vite-plugin.js"
65
66
  ],
66
67
  "scripts": {
67
- "build": "bun run type-check",
68
+ "build": "bun run type-check && bun build src/vite-plugin.ts --outfile dist/vite-plugin.js --target node --format esm --packages external",
68
69
  "clean": "rm -rf node_modules dist",
69
70
  "pack:dry-run": "npm pack --dry-run",
70
71
  "publish:public": "npm publish --access public",
package/src/solid.tsx CHANGED
@@ -37,10 +37,11 @@ import type {
37
37
  FeedbackLocatorHotkey,
38
38
  FeedbackLocatorPoint,
39
39
  FeedbackLocatorSourceMetadata,
40
+ FeedbackLocatorTheme,
40
41
  } from "./types";
41
42
  import { createShortcutResolver } from "./shortcuts";
42
43
 
43
- type FeedbackLocatorRootProps = {
44
+ export type FeedbackLocatorRootProps = {
44
45
  locator: FeedbackLocator;
45
46
  open?: boolean;
46
47
  defaultOpen?: boolean;
@@ -48,6 +49,8 @@ type FeedbackLocatorRootProps = {
48
49
  onClose?: (reason: FeedbackLocatorCloseReason) => void;
49
50
  onSubmitted?: (result: FeedbackLocatorHostedSubmitResult) => void;
50
51
  showFloatingButton?: boolean;
52
+ enableVideoRecording?: boolean;
53
+ theme?: FeedbackLocatorTheme;
51
54
  };
52
55
 
53
56
  type FeedbackLocatorMode = "idle" | "selecting" | "annotating" | "submitting";
@@ -1309,6 +1312,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1309
1312
  const snapshot = () => selected();
1310
1313
  const selectionRect = () => hoverRect();
1311
1314
  const currentTextDraft = () => textDraft();
1315
+ const isVideoRecordingEnabled = () =>
1316
+ (props.enableVideoRecording ?? props.locator.config.enableVideoRecording) === true;
1317
+ const themeStyle = (): JSX.CSSProperties => {
1318
+ const theme = props.theme ?? props.locator.config.theme;
1319
+
1320
+ return {
1321
+ ...(theme?.primary ? { "--primary": theme.primary } : {}),
1322
+ ...(theme?.primaryForeground ? { "--primary-foreground": theme.primaryForeground } : {}),
1323
+ ...(theme?.ring ? { "--ring": theme.ring } : {}),
1324
+ };
1325
+ };
1312
1326
  const canvasCursor = () => {
1313
1327
  if (tool() === "select") {
1314
1328
  return isMovingAnnotation() ? "grabbing" : "grab";
@@ -1322,7 +1336,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1322
1336
  };
1323
1337
 
1324
1338
  return (
1325
- <div data-feedback-locator-ui="true">
1339
+ <div data-feedback-locator-ui="true" style={themeStyle()}>
1326
1340
  <Show
1327
1341
  when={
1328
1342
  mode() === "idle" &&
@@ -1615,70 +1629,72 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1615
1629
  </Show>
1616
1630
  </div>
1617
1631
 
1618
- <div>
1619
- <label class="mb-2 block text-sm font-medium">Video-opname</label>
1620
- <div class="space-y-2 rounded-md border bg-background p-3">
1621
- <div class="flex flex-wrap items-center gap-2">
1622
- <Show
1623
- when={recordingState() === "recording"}
1624
- fallback={
1625
- <button
1626
- type="button"
1627
- class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
1628
- disabled={mode() === "submitting" || !supportsScreenRecording()}
1629
- onClick={startRecording}
1630
- >
1631
- <Video class="h-4 w-4" />
1632
- Neem video op
1633
- </button>
1634
- }
1635
- >
1636
- <button
1637
- type="button"
1638
- class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
1639
- disabled={mode() === "submitting"}
1640
- onClick={stopRecording}
1641
- >
1642
- <StopCircle class="h-4 w-4" />
1643
- Stop opname
1644
- </button>
1645
- </Show>
1646
-
1647
- <Show when={recordingBlob()}>
1648
- {(blob) => (
1649
- <>
1650
- <a
1651
- class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
1652
- href={recordingUrl() ?? undefined}
1653
- target="_blank"
1654
- rel="noreferrer"
1655
- >
1656
- <Video class="h-4 w-4 shrink-0" />
1657
- <span class="truncate">
1658
- screen-recording.webm ({formatBytes(blob().size)},{" "}
1659
- {formatDuration(recordingDurationMs())})
1660
- </span>
1661
- </a>
1632
+ <Show when={isVideoRecordingEnabled()}>
1633
+ <div>
1634
+ <label class="mb-2 block text-sm font-medium">Video-opname</label>
1635
+ <div class="space-y-2 rounded-md border bg-background p-3">
1636
+ <div class="flex flex-wrap items-center gap-2">
1637
+ <Show
1638
+ when={recordingState() === "recording"}
1639
+ fallback={
1662
1640
  <button
1663
1641
  type="button"
1664
- class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
1665
- disabled={mode() === "submitting"}
1666
- onClick={clearRecording}
1642
+ class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
1643
+ disabled={mode() === "submitting" || !supportsScreenRecording()}
1644
+ onClick={startRecording}
1667
1645
  >
1668
- <Trash2 class="h-4 w-4" />
1669
- Wissen
1646
+ <Video class="h-4 w-4" />
1647
+ Neem video op
1670
1648
  </button>
1671
- </>
1672
- )}
1649
+ }
1650
+ >
1651
+ <button
1652
+ type="button"
1653
+ class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
1654
+ disabled={mode() === "submitting"}
1655
+ onClick={stopRecording}
1656
+ >
1657
+ <StopCircle class="h-4 w-4" />
1658
+ Stop opname
1659
+ </button>
1660
+ </Show>
1661
+
1662
+ <Show when={recordingBlob()}>
1663
+ {(blob) => (
1664
+ <>
1665
+ <a
1666
+ class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
1667
+ href={recordingUrl() ?? undefined}
1668
+ target="_blank"
1669
+ rel="noreferrer"
1670
+ >
1671
+ <Video class="h-4 w-4 shrink-0" />
1672
+ <span class="truncate">
1673
+ screen-recording.webm ({formatBytes(blob().size)},{" "}
1674
+ {formatDuration(recordingDurationMs())})
1675
+ </span>
1676
+ </a>
1677
+ <button
1678
+ type="button"
1679
+ class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
1680
+ disabled={mode() === "submitting"}
1681
+ onClick={clearRecording}
1682
+ >
1683
+ <Trash2 class="h-4 w-4" />
1684
+ Wissen
1685
+ </button>
1686
+ </>
1687
+ )}
1688
+ </Show>
1689
+ </div>
1690
+ <Show when={!supportsScreenRecording()}>
1691
+ <div class="text-xs text-muted-foreground">
1692
+ Video-opname is niet beschikbaar in deze browser.
1693
+ </div>
1673
1694
  </Show>
1674
1695
  </div>
1675
- <Show when={!supportsScreenRecording()}>
1676
- <div class="text-xs text-muted-foreground">
1677
- Video-opname is niet beschikbaar in deze browser.
1678
- </div>
1679
- </Show>
1680
1696
  </div>
1681
- </div>
1697
+ </Show>
1682
1698
 
1683
1699
  <Accordion title="Broncontext" defaultOpen>
1684
1700
  <div class="space-y-1">
package/src/types.ts CHANGED
@@ -191,6 +191,12 @@ export type FeedbackLocatorSourceCollector = (
191
191
  target: HTMLElement,
192
192
  ) => FeedbackLocatorSourceMetadata | null;
193
193
 
194
+ export type FeedbackLocatorTheme = {
195
+ primary?: string;
196
+ primaryForeground?: string;
197
+ ring?: string;
198
+ };
199
+
194
200
  export type FeedbackLocatorConfig = {
195
201
  appKey: string;
196
202
  appName: string;
@@ -209,6 +215,8 @@ export type FeedbackLocatorConfig = {
209
215
  contextProviders?: FeedbackLocatorContextProvider[];
210
216
  hotkey?: FeedbackLocatorHotkey;
211
217
  showFloatingButton?: boolean;
218
+ enableVideoRecording?: boolean;
219
+ theme?: FeedbackLocatorTheme;
212
220
  sourceCollector?: FeedbackLocatorSourceCollector;
213
221
  };
214
222
 
@@ -219,6 +227,8 @@ export type FeedbackLocator = {
219
227
  | "contextProviders"
220
228
  | "hotkey"
221
229
  | "showFloatingButton"
230
+ | "enableVideoRecording"
231
+ | "theme"
222
232
  | "sourceCollector"
223
233
  | "startAgentSession"
224
234
  | "startCodexThread"