d2d-feedbackkit 0.0.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/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # d2d-feedbackkit
2
+
3
+ Public SolidJS feedback widget package.
4
+
5
+ ```sh
6
+ bun add d2d-feedbackkit
7
+ ```
8
+
9
+ ```ts
10
+ import { createFeedbackLocator } from "d2d-feedbackkit";
11
+ import { mountFeedbackLocatorElement } from "d2d-feedbackkit/web-component";
12
+ import feedbackKitBabelPlugin from "d2d-feedbackkit/vite-plugin";
13
+ ```
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "d2d-feedbackkit",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "SolidJS feedback widget, source locator, and integration helpers.",
6
+ "license": "UNLICENSED",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "main": "./src/index.ts",
11
+ "types": "./src/index.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./src/index.ts",
15
+ "default": "./src/index.ts"
16
+ },
17
+ "./widget": {
18
+ "types": "./src/widget.tsx",
19
+ "default": "./src/widget.tsx"
20
+ },
21
+ "./web-component": {
22
+ "types": "./src/web-component.tsx",
23
+ "default": "./src/web-component.tsx"
24
+ },
25
+ "./vite-plugin": {
26
+ "types": "./src/vite-plugin.ts",
27
+ "default": "./src/vite-plugin.ts"
28
+ },
29
+ "./client": {
30
+ "types": "./src/client.ts",
31
+ "default": "./src/client.ts"
32
+ },
33
+ "./linear": {
34
+ "types": "./src/linear.ts",
35
+ "default": "./src/linear.ts"
36
+ },
37
+ "./agent": {
38
+ "types": "./src/agent.ts",
39
+ "default": "./src/agent.ts"
40
+ }
41
+ },
42
+ "files": [
43
+ "README.md",
44
+ "src/agent.ts",
45
+ "src/babel.ts",
46
+ "src/client.ts",
47
+ "src/index.ts",
48
+ "src/linear.ts",
49
+ "src/shortcuts.ts",
50
+ "src/solid.tsx",
51
+ "src/source.ts",
52
+ "src/types.ts",
53
+ "src/vite-plugin.ts",
54
+ "src/web-component.tsx",
55
+ "src/widget.tsx"
56
+ ],
57
+ "scripts": {
58
+ "build": "bun run type-check",
59
+ "clean": "rm -rf node_modules dist",
60
+ "pack:dry-run": "npm pack --dry-run",
61
+ "publish:public": "npm publish --access public",
62
+ "test": "vitest run src",
63
+ "type-check": "tsc --noEmit"
64
+ },
65
+ "dependencies": {
66
+ "@babel/parser": "^7.28.0",
67
+ "html-to-image": "^1.11.13"
68
+ },
69
+ "peerDependencies": {
70
+ "lucide-solid": ">=0.464",
71
+ "solid-js": ">=1.9"
72
+ },
73
+ "devDependencies": {
74
+ "@babel/core": "^7.28.0",
75
+ "@babel/types": "^7.28.0",
76
+ "@types/babel__core": "^7.20.5",
77
+ "@types/node": "^20.12.7",
78
+ "jsdom": "^25.0.1",
79
+ "typescript": "^5.9.3",
80
+ "vite-plugin-solid": "^2.11.7",
81
+ "vitest": "^2.1.8"
82
+ }
83
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,161 @@
1
+ import type {
2
+ FeedbackLocatorAsset,
3
+ FeedbackLocatorLinearIssueReference,
4
+ FeedbackLocatorMetadata,
5
+ } from "./types";
6
+
7
+ export type AgentSessionAssets = {
8
+ screenshotUrl?: string;
9
+ annotatedScreenshotUrl?: string;
10
+ recording?: FeedbackLocatorAsset;
11
+ attachments?: FeedbackLocatorAsset[];
12
+ contextAttachments?: FeedbackLocatorAsset[];
13
+ linearIssue?: FeedbackLocatorLinearIssueReference;
14
+ };
15
+
16
+ export function buildAgentSessionTitle(metadata: FeedbackLocatorMetadata) {
17
+ const sourceLabel =
18
+ metadata.source?.componentName ??
19
+ metadata.source?.elementName ??
20
+ metadata.element.tagName;
21
+ const promptSummary = metadata.prompt
22
+ .replace(/\s+/g, " ")
23
+ .trim()
24
+ .slice(0, 80);
25
+
26
+ return `Feedback fix: ${sourceLabel}${promptSummary ? ` - ${promptSummary}` : ""}`;
27
+ }
28
+
29
+ export function buildAgentSessionPrompt(
30
+ metadata: FeedbackLocatorMetadata,
31
+ assets: AgentSessionAssets = metadata.assets ?? {},
32
+ ) {
33
+ const source = metadata.source;
34
+ const screenshotUrl = assets.annotatedScreenshotUrl ?? assets.screenshotUrl;
35
+ const feedbackContext = metadata.context.feedbackLocator as
36
+ | {
37
+ captureMode?: string;
38
+ }
39
+ | undefined;
40
+
41
+ return [
42
+ "Je bent een coding agent in de host-app repository.",
43
+ "Gebruik deze feedback om het kleinste passende codepad te vinden, de oorzaak te fixen en gerichte tests te draaien.",
44
+ "",
45
+ "## Feedback",
46
+ "",
47
+ metadata.prompt,
48
+ "",
49
+ "## Linear issue",
50
+ "",
51
+ ...formatLinearIssueLines(assets.linearIssue),
52
+ "",
53
+ "## Startpunt in de code",
54
+ "",
55
+ `- App: ${metadata.appName} (${metadata.appKey})`,
56
+ `- Route: ${metadata.route}`,
57
+ `- URL: ${metadata.url}`,
58
+ `- Component: ${source?.componentName ?? "niet gevonden"}`,
59
+ `- JSX element: ${source?.elementName ?? "niet gevonden"}`,
60
+ `- Bestand: ${source ? `${source.filePath}:${source.line}:${source.column}` : "niet gevonden"}`,
61
+ `- Component definitie: ${
62
+ source?.componentLine
63
+ ? `${source.filePath}:${source.componentLine}:${source.componentColumn ?? 1}`
64
+ : "niet gevonden"
65
+ }`,
66
+ `- Capture: ${feedbackContext?.captureMode === "page" ? "volledige pagina" : "element"}`,
67
+ "",
68
+ "## Visuele context",
69
+ "",
70
+ screenshotUrl ? `- Screenshot: ${screenshotUrl}` : "- Screenshot: niet beschikbaar",
71
+ assets.recording ? `- Video-opname: ${assets.recording.url}` : "- Video-opname: niet toegevoegd",
72
+ "",
73
+ "## Bijlagen",
74
+ "",
75
+ ...formatAssetLines(assets.attachments, "Geen extra bijlagen toegevoegd."),
76
+ "",
77
+ "## Directe context",
78
+ "",
79
+ "Deze context is direct vanuit de browser/app meegegeven en hoeft niet uit Linear of S3 opgehaald te worden.",
80
+ "",
81
+ "```json",
82
+ JSON.stringify(buildAgentDirectContext(metadata), null, 2),
83
+ "```",
84
+ "",
85
+ "## Acceptatiecriteria",
86
+ "",
87
+ "- Reproduceer of inspecteer het probleem vanuit de meegegeven route/context.",
88
+ "- Pas alleen relevante code aan.",
89
+ "- Voeg of update een regressietest waar dit zinvol is.",
90
+ "- Rapporteer gewijzigde bestanden, testresultaten en eventuele resterende onzekerheid.",
91
+ ].join("\n");
92
+ }
93
+
94
+ export function buildAgentDirectContext(metadata: FeedbackLocatorMetadata) {
95
+ return {
96
+ context: removeNoisyFeedbackLocatorContext(metadata.context),
97
+ contextErrors: metadata.contextErrors,
98
+ };
99
+ }
100
+
101
+ function removeNoisyFeedbackLocatorContext(context: Record<string, unknown>) {
102
+ const filteredContext = { ...context };
103
+ const feedbackLocatorContext = filteredContext.feedbackLocator;
104
+
105
+ if (
106
+ typeof feedbackLocatorContext !== "object" ||
107
+ feedbackLocatorContext === null ||
108
+ Array.isArray(feedbackLocatorContext)
109
+ ) {
110
+ return filteredContext;
111
+ }
112
+
113
+ const {
114
+ networkLogs: _networkLogs,
115
+ performance: _performance,
116
+ ...usefulFeedbackLocatorContext
117
+ } = feedbackLocatorContext as Record<string, unknown>;
118
+
119
+ if (Object.keys(usefulFeedbackLocatorContext).length > 0) {
120
+ filteredContext.feedbackLocator = usefulFeedbackLocatorContext;
121
+ } else {
122
+ delete filteredContext.feedbackLocator;
123
+ }
124
+
125
+ return filteredContext;
126
+ }
127
+
128
+ function formatLinearIssueLines(issue: FeedbackLocatorLinearIssueReference | undefined) {
129
+ if (!issue) {
130
+ return ["- Niet gekoppeld aan een Linear issue."];
131
+ }
132
+
133
+ return [
134
+ `- Issue: ${issue.issueIdentifier}`,
135
+ `- Titel: ${issue.issueTitle}`,
136
+ `- URL: ${issue.issueUrl}`,
137
+ `- ID: ${issue.issueId}`,
138
+ ];
139
+ }
140
+
141
+ function formatAssetLines(assets: FeedbackLocatorAsset[] | undefined, emptyText: string) {
142
+ if (!assets || assets.length === 0) {
143
+ return [`- ${emptyText}`];
144
+ }
145
+
146
+ return assets.map(
147
+ (asset) => `- ${asset.name}: ${asset.url} (${asset.contentType}, ${formatBytes(asset.size)})`,
148
+ );
149
+ }
150
+
151
+ function formatBytes(size: number) {
152
+ if (size < 1024) {
153
+ return `${size} B`;
154
+ }
155
+
156
+ if (size < 1024 * 1024) {
157
+ return `${Math.round(size / 1024)} KB`;
158
+ }
159
+
160
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`;
161
+ }
package/src/babel.ts ADDED
@@ -0,0 +1,285 @@
1
+ import path from "node:path";
2
+ import { parseExpression } from "@babel/parser";
3
+
4
+ type BabelApi = {
5
+ types: typeof import("@babel/types");
6
+ };
7
+
8
+ type BabelState = {
9
+ filename?: string;
10
+ cwd?: string;
11
+ opts?: FeedbackLocatorBabelOptions;
12
+ feedbackLocator?: FeedbackLocatorTransformState;
13
+ };
14
+
15
+ type FeedbackLocatorBabelOptions = {
16
+ projectRoot?: string;
17
+ appRoot?: string;
18
+ ignoreComponentNames?: string[];
19
+ };
20
+
21
+ type SourceLocation = {
22
+ start: {
23
+ line: number;
24
+ column: number;
25
+ };
26
+ end: {
27
+ line: number;
28
+ column: number;
29
+ };
30
+ };
31
+
32
+ type FeedbackLocatorTransformState = {
33
+ projectRoot: string;
34
+ filePath: string;
35
+ fileKey: string;
36
+ components: Array<{
37
+ name: string;
38
+ loc: SourceLocation;
39
+ }>;
40
+ expressions: Array<{
41
+ name: string;
42
+ loc: SourceLocation;
43
+ componentId: number | null;
44
+ }>;
45
+ componentStack: number[];
46
+ };
47
+
48
+ export default function feedbackLocatorBabelPlugin(babel: BabelApi) {
49
+ const t = babel.types;
50
+
51
+ function ensureState(state: BabelState) {
52
+ if (!state.feedbackLocator) {
53
+ throw new Error("Feedback Locator Babel state is not initialized");
54
+ }
55
+
56
+ return state.feedbackLocator;
57
+ }
58
+
59
+ function shouldSkip(state: BabelState) {
60
+ return !state.filename || state.filename.includes("node_modules");
61
+ }
62
+
63
+ function enterComponent(
64
+ state: BabelState,
65
+ name: string | null | undefined,
66
+ loc: SourceLocation | null | undefined,
67
+ ) {
68
+ if (!name || !loc || !isComponentName(name)) {
69
+ return null;
70
+ }
71
+
72
+ const transformState = ensureState(state);
73
+
74
+ if (state.opts?.ignoreComponentNames?.includes(name)) {
75
+ return null;
76
+ }
77
+
78
+ const id = transformState.components.length;
79
+ transformState.components.push({ name, loc });
80
+ transformState.componentStack.push(id);
81
+ return id;
82
+ }
83
+
84
+ function exitComponent(state: BabelState, id: number | null) {
85
+ if (id === null) {
86
+ return;
87
+ }
88
+
89
+ const transformState = ensureState(state);
90
+ const last = transformState.componentStack.at(-1);
91
+ if (last === id) {
92
+ transformState.componentStack.pop();
93
+ }
94
+ }
95
+
96
+ return {
97
+ name: "feedbackkit-source-locator-babel",
98
+ visitor: {
99
+ Program: {
100
+ enter(_programPath: unknown, state: BabelState) {
101
+ if (shouldSkip(state)) {
102
+ return;
103
+ }
104
+
105
+ const projectRoot = normalizePath(
106
+ state.opts?.projectRoot ?? state.cwd ?? process.cwd(),
107
+ );
108
+ const filename = normalizePath(state.filename!);
109
+ const filePath = toRepoRelativePath(filename, projectRoot);
110
+ const fileKey = filePath;
111
+
112
+ state.feedbackLocator = {
113
+ projectRoot,
114
+ filePath,
115
+ fileKey,
116
+ components: [],
117
+ expressions: [],
118
+ componentStack: [],
119
+ };
120
+ },
121
+ exit(programPath: any, state: BabelState) {
122
+ if (shouldSkip(state) || !state.feedbackLocator) {
123
+ return;
124
+ }
125
+
126
+ const transformState = state.feedbackLocator;
127
+ if (transformState.expressions.length === 0) {
128
+ return;
129
+ }
130
+
131
+ const data = {
132
+ filePath: transformState.filePath,
133
+ projectPath: transformState.projectRoot,
134
+ expressions: transformState.expressions,
135
+ components: transformState.components,
136
+ };
137
+ const serializedData = JSON.stringify(data);
138
+ const serializedFileKey = JSON.stringify(transformState.fileKey);
139
+ const registerCode = `(() => {
140
+ if (typeof window !== "undefined") {
141
+ window.__FEEDBACK_LOCATOR_DATA__ = window.__FEEDBACK_LOCATOR_DATA__ || {};
142
+ window.__FEEDBACK_LOCATOR_DATA__[${serializedFileKey}] = ${serializedData};
143
+ }
144
+ })()`;
145
+
146
+ programPath.pushContainer(
147
+ "body",
148
+ t.expressionStatement(parseExpression(registerCode) as any),
149
+ );
150
+ },
151
+ },
152
+ FunctionDeclaration: {
153
+ enter(pathNode: any, state: BabelState) {
154
+ if (shouldSkip(state) || !state.feedbackLocator) {
155
+ return;
156
+ }
157
+
158
+ pathNode.setData(
159
+ "feedbackLocatorComponentId",
160
+ enterComponent(state, pathNode.node.id?.name, pathNode.node.loc),
161
+ );
162
+ },
163
+ exit(pathNode: any, state: BabelState) {
164
+ if (shouldSkip(state) || !state.feedbackLocator) {
165
+ return;
166
+ }
167
+
168
+ exitComponent(
169
+ state,
170
+ pathNode.getData("feedbackLocatorComponentId") ?? null,
171
+ );
172
+ },
173
+ },
174
+ VariableDeclarator: {
175
+ enter(pathNode: any, state: BabelState) {
176
+ if (shouldSkip(state) || !state.feedbackLocator) {
177
+ return;
178
+ }
179
+
180
+ const initType = pathNode.node.init?.type;
181
+ const isFunction =
182
+ initType === "ArrowFunctionExpression" ||
183
+ initType === "FunctionExpression";
184
+ const name =
185
+ pathNode.node.id?.type === "Identifier"
186
+ ? pathNode.node.id.name
187
+ : null;
188
+
189
+ pathNode.setData(
190
+ "feedbackLocatorComponentId",
191
+ isFunction
192
+ ? enterComponent(state, name, pathNode.node.loc)
193
+ : null,
194
+ );
195
+ },
196
+ exit(pathNode: any, state: BabelState) {
197
+ if (shouldSkip(state) || !state.feedbackLocator) {
198
+ return;
199
+ }
200
+
201
+ exitComponent(
202
+ state,
203
+ pathNode.getData("feedbackLocatorComponentId") ?? null,
204
+ );
205
+ },
206
+ },
207
+ JSXElement(pathNode: any, state: BabelState) {
208
+ if (shouldSkip(state) || !state.feedbackLocator || !pathNode.node.loc) {
209
+ return;
210
+ }
211
+
212
+ const openingElement = pathNode.node.openingElement;
213
+ const name = getJsxName(openingElement.name);
214
+ if (!isDomJsxName(openingElement.name)) {
215
+ return;
216
+ }
217
+
218
+ if (
219
+ openingElement.attributes.some(
220
+ (attribute: any) =>
221
+ attribute.type === "JSXAttribute" &&
222
+ attribute.name?.name === "data-feedback-locator-id",
223
+ )
224
+ ) {
225
+ return;
226
+ }
227
+
228
+ const transformState = state.feedbackLocator;
229
+ const expressionId = transformState.expressions.length;
230
+ const componentId = transformState.componentStack.at(-1) ?? null;
231
+ const locatorId = `${transformState.fileKey}::${expressionId}`;
232
+
233
+ transformState.expressions.push({
234
+ name,
235
+ loc: pathNode.node.loc,
236
+ componentId,
237
+ });
238
+
239
+ openingElement.attributes.push(
240
+ t.jsxAttribute(
241
+ t.jsxIdentifier("data-feedback-locator-id"),
242
+ t.stringLiteral(locatorId),
243
+ ),
244
+ );
245
+ },
246
+ },
247
+ };
248
+ }
249
+
250
+ function getJsxName(node: any): string {
251
+ if (node.type === "JSXIdentifier") {
252
+ return node.name;
253
+ }
254
+
255
+ if (node.type === "JSXMemberExpression") {
256
+ return `${getJsxName(node.object)}.${node.property.name}`;
257
+ }
258
+
259
+ if (node.type === "JSXNamespacedName") {
260
+ return `${node.namespace.name}.${node.name.name}`;
261
+ }
262
+
263
+ return "unknown";
264
+ }
265
+
266
+ function isDomJsxName(node: any) {
267
+ if (node.type !== "JSXIdentifier") {
268
+ return false;
269
+ }
270
+
271
+ return /^[a-z]/.test(node.name);
272
+ }
273
+
274
+ function isComponentName(name: string) {
275
+ return /^[A-Z]/.test(name);
276
+ }
277
+
278
+ function normalizePath(value: string) {
279
+ return value.split(path.sep).join("/");
280
+ }
281
+
282
+ function toRepoRelativePath(filename: string, projectRoot: string) {
283
+ const relative = normalizePath(path.relative(projectRoot, filename));
284
+ return relative.startsWith(".") ? filename : relative;
285
+ }
package/src/client.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./index";