@rose42/t3-feedback 0.1.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.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/dist/bridge/attachments.d.ts +27 -0
  4. package/dist/bridge/attachments.js +109 -0
  5. package/dist/bridge/format.d.ts +70 -0
  6. package/dist/bridge/format.js +358 -0
  7. package/dist/bridge/http.d.ts +31 -0
  8. package/dist/bridge/http.js +169 -0
  9. package/dist/bridge/index.d.ts +13 -0
  10. package/dist/bridge/index.js +12 -0
  11. package/dist/bridge/parse.d.ts +16 -0
  12. package/dist/bridge/parse.js +82 -0
  13. package/dist/bridge/paths.d.ts +10 -0
  14. package/dist/bridge/paths.js +10 -0
  15. package/dist/bridge/probeThreads.d.ts +13 -0
  16. package/dist/bridge/probeThreads.js +66 -0
  17. package/dist/bridge/readSource.d.ts +8 -0
  18. package/dist/bridge/readSource.js +64 -0
  19. package/dist/bridge/sendFeedback.d.ts +99 -0
  20. package/dist/bridge/sendFeedback.js +417 -0
  21. package/dist/cli/bin.d.ts +2 -0
  22. package/dist/cli/bin.js +40 -0
  23. package/dist/cli/constants.d.ts +4 -0
  24. package/dist/cli/constants.js +4 -0
  25. package/dist/cli/detect.d.ts +17 -0
  26. package/dist/cli/detect.js +118 -0
  27. package/dist/cli/index.d.ts +4 -0
  28. package/dist/cli/index.js +4 -0
  29. package/dist/cli/init.d.ts +18 -0
  30. package/dist/cli/init.js +165 -0
  31. package/dist/cli/pm.d.ts +12 -0
  32. package/dist/cli/pm.js +49 -0
  33. package/dist/cli/prompt.d.ts +2 -0
  34. package/dist/cli/prompt.js +19 -0
  35. package/dist/cli/wire/next.d.ts +8 -0
  36. package/dist/cli/wire/next.js +71 -0
  37. package/dist/cli/wire/node.d.ts +3 -0
  38. package/dist/cli/wire/node.js +76 -0
  39. package/dist/cli/wire/vite.d.ts +25 -0
  40. package/dist/cli/wire/vite.js +71 -0
  41. package/dist/client/activeThreads.d.ts +23 -0
  42. package/dist/client/activeThreads.js +89 -0
  43. package/dist/client/cropScreenshot.d.ts +39 -0
  44. package/dist/client/cropScreenshot.js +212 -0
  45. package/dist/client/customPlugins.d.ts +7 -0
  46. package/dist/client/customPlugins.js +38 -0
  47. package/dist/client/draft.d.ts +33 -0
  48. package/dist/client/draft.js +222 -0
  49. package/dist/client/index.d.ts +24 -0
  50. package/dist/client/index.js +20 -0
  51. package/dist/client/mount.d.ts +210 -0
  52. package/dist/client/mount.js +2408 -0
  53. package/dist/client/openInT3.d.ts +28 -0
  54. package/dist/client/openInT3.js +81 -0
  55. package/dist/client/readClipboardImage.d.ts +2 -0
  56. package/dist/client/readClipboardImage.js +2 -0
  57. package/dist/client/reportType.d.ts +43 -0
  58. package/dist/client/reportType.js +110 -0
  59. package/dist/client/selectBurstFrames.d.ts +21 -0
  60. package/dist/client/selectBurstFrames.js +101 -0
  61. package/dist/client/sessionThread.d.ts +26 -0
  62. package/dist/client/sessionThread.js +93 -0
  63. package/dist/client/threadPicker.d.ts +20 -0
  64. package/dist/client/threadPicker.js +51 -0
  65. package/dist/client/transport.d.ts +11 -0
  66. package/dist/client/transport.js +35 -0
  67. package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
  68. package/dist/client/ui/commentPanelPlugin.js +126 -0
  69. package/dist/client/ui/defaultPlugins.d.ts +19 -0
  70. package/dist/client/ui/defaultPlugins.js +48 -0
  71. package/dist/client/ui/elementTargetChip.d.ts +64 -0
  72. package/dist/client/ui/elementTargetChip.js +185 -0
  73. package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
  74. package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
  75. package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
  76. package/dist/client/ui/elementTargetPanelSession.js +279 -0
  77. package/dist/client/ui/elementTargetUi.d.ts +36 -0
  78. package/dist/client/ui/elementTargetUi.js +105 -0
  79. package/dist/client/ui/host.d.ts +112 -0
  80. package/dist/client/ui/host.js +132 -0
  81. package/dist/client/ui/html.d.ts +22 -0
  82. package/dist/client/ui/html.js +26 -0
  83. package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
  84. package/dist/client/ui/labeledPluginPreview.js +71 -0
  85. package/dist/client/ui/resolveContributions.d.ts +13 -0
  86. package/dist/client/ui/resolveContributions.js +20 -0
  87. package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
  88. package/dist/client/ui/screenshotPanelPlugin.js +212 -0
  89. package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
  90. package/dist/client/ui/screenshotPanelSession.js +445 -0
  91. package/dist/client/ui/screenshotUi.d.ts +31 -0
  92. package/dist/client/ui/screenshotUi.js +130 -0
  93. package/dist/client/ui/shortcuts.d.ts +32 -0
  94. package/dist/client/ui/shortcuts.js +40 -0
  95. package/dist/client/ui/slots.d.ts +73 -0
  96. package/dist/client/ui/slots.js +112 -0
  97. package/dist/controls.d.ts +8 -0
  98. package/dist/controls.js +16 -0
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.js +2 -0
  101. package/dist/plugin.d.ts +9 -0
  102. package/dist/plugin.js +186 -0
  103. package/dist/types.d.ts +464 -0
  104. package/dist/types.js +283 -0
  105. package/docs/devcontainer.md +160 -0
  106. package/package.json +107 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rose42
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,516 @@
1
+ # @rose42/t3-feedback
2
+
3
+ > **DEV-only community tool.** In development, capture a screenshot + note from your
4
+ > running app and send it into a [T3 Code](https://t3.codes) thread — without leaving
5
+ > the page. Not affiliated with T3 / pingdotgg.
6
+
7
+ **Status:** unofficial. The bridge talks to T3 via the `t3` CLI (`auth session` /
8
+ `auth pairing`), `/.well-known/t3/environment`, websocket tickets, and private WS
9
+ RPCs (e.g. thread create / settle). Those surfaces can change without notice; pin
10
+ a known-good T3 / `t3` CLI version and expect breakage across upgrades. There is no
11
+ supported public “ingest feedback” API yet.
12
+
13
+ **Never for production.** Vite integration is serve-only (`apply: "serve"`). Next
14
+ (and any custom host) must mount the bridge + client in development only.
15
+
16
+ Wire it as a Vite plugin (`/plugin`) or, for Next and other hosts, the same overlay
17
+ (`/client`) plus a Node Fetch bridge (`/bridge`). Headless envelopes and silent
18
+ capture helpers come from [`@rose42/feedback-capture`](https://www.npmjs.com/package/@rose42/feedback-capture)
19
+ (UI-free); this package owns the overlay UI, host plugin fields (`label` /
20
+ `reportTypes` / `ui` / `shortcuts`), and the T3 bridge.
21
+
22
+ ### Core loop
23
+
24
+ 1. In DEV, open the panel (shortcut or FAB), optionally pick an element / crop a shot.
25
+ 2. Send → **New** thread or **Continue** an existing one.
26
+ 3. Toast offers **Open in T3** (pairing handoff); working threads stay on the activity chip until they settle.
27
+
28
+ By default the panel offers **New** and a play-icon **Continue** control (thread title excerpt) as the send actions (soft-defaults Continue when the last
29
+ send on this route was the same report type within ~15 minutes). A chevron opens a searchable list of project threads from T3 (search
30
+ prefilled with the current pathname). From the comment field: **Ctrl/⌘+Enter** submits the soft-default,
31
+ **Ctrl/⌘+Shift+Enter** always New, **Alt+Enter** always Continue.
32
+
33
+ After a successful send the compose panel closes immediately. A short toast offers **Open in T3**, and
34
+ working threads stay on the FAB as a badge + activity popover (Open / Continue / Settle / Dismiss) until they
35
+ settle (live probe when available, **Settle** via `thread.settle`, local dismiss, or ~45 minute TTL). The activity chip stays visible even
36
+ when the normal FAB is mobile-only or hidden.
37
+
38
+ **Open in T3** mints a one-time pairing link (`/pair#token=…`), opens it in a named window so the browser
39
+ session cookie is established, then navigates that window to the thread. Browser links use `openOrigin`
40
+ (default: same as `origin`, typically `localhost`) so host Port-forward cookies match.
41
+
42
+ In-progress compose state (comment, screenshots, picked element) is saved to `sessionStorage` and
43
+ restored after a page reload; if the panel was open, it reopens automatically. **Cancel** (panel ×)
44
+ discards the draft; Esc or clicking outside closes and keeps it. A successful send also clears the
45
+ draft.
46
+
47
+ ## Install
48
+
49
+ **Recommended (hybrid init):** from your app root, detect Next / Vite / Node, confirm,
50
+ install this package if missing (npm or pnpm), then wire connector files:
51
+
52
+ ```bash
53
+ # one-shot (downloads the package, runs its `t3-feedback` bin):
54
+ pnpm dlx @rose42/t3-feedback init
55
+ npx -p @rose42/t3-feedback t3-feedback init
56
+
57
+ # after it is already a dependency:
58
+ pnpm exec t3-feedback init
59
+ npx t3-feedback init
60
+ ```
61
+
62
+ `init` prints a plan and asks before writing. Useful flags: `--yes`, `--dry-run`,
63
+ `--no-install`, `--host next|vite|node`.
64
+
65
+ Manual install only:
66
+
67
+ ```bash
68
+ pnpm add -D @rose42/t3-feedback
69
+ # or: npm i -D @rose42/t3-feedback
70
+ ```
71
+
72
+ Until published, develop against a sibling clone via the
73
+ [devcontainer](https://gitlab.com/rose42/t3-feedback/-/blob/main/docs/devcontainer.md)
74
+ (local symlink of a sibling [`feedback-capture`](https://gitlab.com/rose42/feedback-capture) clone).
75
+ `package.json` stays on the registry version (`0.1.0`).
76
+
77
+ Then from a consumer app that depends on this package via `file:` / workspace link:
78
+
79
+ ```bash
80
+ pnpm exec t3-feedback init --no-install
81
+ ```
82
+
83
+ See [docs/devcontainer.md](https://gitlab.com/rose42/t3-feedback/-/blob/main/docs/devcontainer.md)
84
+ for the local DX container (`T3CODE_PORT`, default **3775** — [change in
85
+ `.devcontainer/devcontainer.json`](https://gitlab.com/rose42/t3-feedback/-/blob/main/.devcontainer/devcontainer.json),
86
+ sibling bind-mount).
87
+
88
+ ## Usage
89
+
90
+ ```ts
91
+ // vite.config.ts
92
+ import { defineConfig } from "vite";
93
+ import { t3FeedbackPlugin } from "@rose42/t3-feedback/plugin";
94
+
95
+ export default defineConfig({
96
+ plugins: [
97
+ t3FeedbackPlugin({
98
+ workspaceRootHints: [process.cwd()],
99
+ // injectClient: true by default
100
+ // shortcut: { key: "f" }, // bare F; omitted mods are false
101
+ // // or { key: "f", mod: true, shift: true } for Ctrl/⌘+Shift+F
102
+ // threadReuse: "ask", // or "session-route" | "never"
103
+ // threadTitle: "Checkout", // optional title prefix
104
+ // origin: "http://localhost:3773",
105
+ }),
106
+ ],
107
+ });
108
+ ```
109
+
110
+ In DEV, open the app and press **Ctrl/⌘+Shift+F** (or click the ✎ FAB),
111
+ **Ctrl/⌘+Shift+E** to pick an element, or **Ctrl/⌘+Shift+S** to capture a
112
+ screenshot. No `main.tsx` import is required when `injectClient` is left on.
113
+
114
+ **Pick element** (button or shortcut) stores a DOM target. Component source is
115
+ best-effort via `pick.sourceFramework` (`"auto"` default — try qwik/locator/next/react/vue/svelte;
116
+ pin `"next"` on Next.js hosts so Turbopack `_debugStack` frames skip `react-jsx-runtime`);
117
+ or pin one / `"none"`). By default
118
+ (`pick.afterPick: "crop"`) it then captures the tab and silently crops to the
119
+ element — no crop overlay. Configure with:
120
+
121
+ ```ts
122
+ t3FeedbackPlugin({
123
+ // Open panel
124
+ shortcut: { key: "f", mod: true, shift: true },
125
+ pick: {
126
+ // Start picking immediately (default Ctrl/⌘+Shift+E). `false` to disable.
127
+ shortcut: { key: "e", mod: true, shift: true },
128
+ // After pick: "crop" (default) | "capture" | "none"
129
+ afterPick: "crop",
130
+ cropPaddingPx: 12,
131
+ // Component source: "auto" | "react" | "vue" | "svelte" | "qwik" | "locator" | "none"
132
+ sourceFramework: "auto",
133
+ // Clickable path → open in IDE via Vite `/__open-in-editor` (default: on).
134
+ // openInEditor: false,
135
+ // openInEditor: { urlTemplate: "vscode://file/{file}:{line}:{column}" },
136
+ // Include truncated innerHTML in the agent message (default: false)
137
+ includeInnerHTML: false,
138
+ // Curated getComputedStyle: "none" | "layout" | "computed"
139
+ styles: "none",
140
+ },
141
+ screenshot: {
142
+ // Capture / Change region: "ui" (default — crop on demand from thumbs;
143
+ // Capture focuses the new crop control so Enter opens it) | "auto" | "none"
144
+ crop: "ui",
145
+ // Max images per report (capture + paste + burst). Default 5 (1–10).
146
+ // maxImages: 5,
147
+ },
148
+ // Record / burst shortcut (default Ctrl/⌘+Shift+R). `false` to disable.
149
+ // record: { shortcut: { key: "r", mod: true, shift: true } },
150
+ // Built-in capture plugins (omit = all four). Comment is always collected.
151
+ // activePlugins: ["pageContext", "consoleLogs", "screenshot", "elementTarget"],
152
+ // pluginOptions: { consoleLogs: { maxEntries: 200 } },
153
+ // Vite-resolved module paths — default export is a FeedbackCapturePlugin (or array/factory).
154
+ // customPlugins: ["./src/dev/routePlugin.ts"],
155
+ });
156
+ ```
157
+
158
+ Those `pick.includeInnerHTML` / `pick.styles` values are **panel defaults**. The overlay
159
+ also shows per-session toggles (innerHTML checkbox + styles segmented control) that override them
160
+ and persist in `sessionStorage` for the tab.
161
+
162
+ ### Capture plugins
163
+
164
+ | Built-in | Role |
165
+ | --------------- | --------------------------------------------------------------------- |
166
+ | `pageContext` | URL / viewport / locale on the envelope |
167
+ | `consoleLogs` | Ring-buffered console output (`pluginOptions.consoleLogs.maxEntries`) |
168
+ | `screenshot` | Capture / paste / record gallery → `envelope.screenshot` |
169
+ | `elementTarget` | Pick-element UI → `envelope.elementTarget` |
170
+
171
+ Omit `activePlugins` to keep today’s defaults (all four). An explicit list enables only those
172
+ ids; the comment field is always collected. Custom plugins cannot be JSON-serialized into the
173
+ injected client — pass Vite-resolved module ids/paths via `customPlugins` instead.
174
+
175
+ **Report-type profiles.** Panel hints come from `buildReportTypeHint` using the active
176
+ builtins (`pageContext`, `consoleLogs`, screenshot attachments) and the same
177
+ `reportTypes` / page-context field filtering as collect. Markdown formatting just
178
+ renders what landed in the envelope. `screenshot` / `elementTarget` stay available for
179
+ every type when listed in `activePlugins`. Customs can set their own `reportTypes` the
180
+ same way. Override a hint via `copy.reportTypeBugHint` / `reportTypeFeedbackHint` /
181
+ `reportTypeSuggestionHint` if needed.
182
+
183
+ ```ts
184
+ // src/dev/routePlugin.ts
185
+ import type { FeedbackCapturePluginWithUi } from "@rose42/t3-feedback/client";
186
+
187
+ export default {
188
+ id: "route",
189
+ collect: () => window.location.pathname,
190
+ } satisfies FeedbackCapturePluginWithUi;
191
+ ```
192
+
193
+ ```ts
194
+ t3FeedbackPlugin({
195
+ activePlugins: ["pageContext", "consoleLogs", "screenshot", "elementTarget"],
196
+ customPlugins: ["./src/dev/routePlugin.ts"],
197
+ });
198
+ ```
199
+
200
+ Each entry’s **default export** (or plugin-shaped named exports) may be a plugin, an array, or a
201
+ zero-arg factory. Relative paths resolve against the Vite project root; bare package ids are left
202
+ as import specifiers.
203
+
204
+ Optional **host** plugin fields on `FeedbackCapturePluginWithUi` (not on core
205
+ `@rose42/feedback-capture` plugins):
206
+
207
+ | Field | Role |
208
+ | ------------- | ---------------------------------------------------------------------------------------------- |
209
+ | `label` | Auto-register a live preview chip in the `beforeComment` slot (skipped when `ui` is set) |
210
+ | `reportTypes` | Only collect when the selected type is in the list (`"bug"` \| `"feedback"` \| `"suggestion"`) |
211
+ | `ui` | Own panel UI: one contribution or an array, each targeting a shared slot |
212
+ | `shortcuts` | Own global keyboard chords (one or an array); registered for the mount lifetime |
213
+
214
+ **Image attachments.** Any plugin may `collect()` a value shaped as
215
+ `{ attachments: [{ dataUrl, name?, mimeType? }] }`. Those images are merged with the built-in
216
+ `screenshot` gallery (deduped by `dataUrl`) and uploaded as T3 message attachments. Use
217
+ `ctx.media` from UI / shortcut host context for viewport capture, crop, clipboard image, and
218
+ frame burst helpers (the feedback root is hidden automatically during capture).
219
+
220
+ **Markdown formatting.** Custom collect values are dumped generically unless you return a hint
221
+ shape: `{ label, value }` → `**label:** value`, `{ heading, body }` → a `###` section, or
222
+ `{ markdown }` → raw markdown. Reserved built-in keys keep their first-class formatting.
223
+
224
+ ```ts
225
+ import type { FeedbackCapturePluginWithUi } from "@rose42/t3-feedback/client";
226
+
227
+ let lastShot: string | undefined;
228
+
229
+ export default {
230
+ id: "routeShot",
231
+ collect: () =>
232
+ lastShot ? { attachments: [{ dataUrl: lastShot, name: "route.png" }] } : undefined,
233
+ ui: {
234
+ id: "routeShot-ui",
235
+ slot: "actionsTools",
236
+ mount(container, ctx) {
237
+ const btn = document.createElement("button");
238
+ btn.type = "button";
239
+ btn.textContent = "Route shot";
240
+ btn.onclick = async () => {
241
+ const url = await ctx.media.captureScreenshot();
242
+ if (url) lastShot = url;
243
+ ctx.persistDraft();
244
+ };
245
+ container.append(btn);
246
+ },
247
+ },
248
+ } satisfies FeedbackCapturePluginWithUi;
249
+ ```
250
+
251
+ **Shell vs plugins.** The host owns only the FAB (open / activity / toast) and the send-action
252
+ cluster (New / Continue / thread picker / submit, plus comment-field send keydowns and the
253
+ open-panel chord). Everything else in the panel — report type, comment field, element target,
254
+ screenshot gallery, collect-only builtins, and customs — is a capture plugin on the same
255
+ `ui` / `shortcuts` / `collect` surface.
256
+
257
+ Shared slots: `beforeComment`, `comment`, `attachments`, `actionsTools`.
258
+
259
+ ```ts
260
+ import type { FeedbackCapturePluginWithUi } from "@rose42/t3-feedback/client";
261
+
262
+ export default {
263
+ id: "documentTitle",
264
+ label: "Document title",
265
+ reportTypes: ["feedback"],
266
+ collect: () => document.title.trim() || undefined,
267
+ } satisfies FeedbackCapturePluginWithUi;
268
+ ```
269
+
270
+ Register interactive UI into a slot (instead of / in addition to `label`):
271
+
272
+ ```ts
273
+ import type { FeedbackCapturePluginWithUi } from "@rose42/t3-feedback/client";
274
+
275
+ export default {
276
+ id: "routeHint",
277
+ collect: () => window.location.pathname,
278
+ ui: {
279
+ id: "routeHint-panel",
280
+ slot: "beforeComment",
281
+ mount(container) {
282
+ const el = document.createElement("div");
283
+ el.textContent = `Path: ${window.location.pathname}`;
284
+ container.append(el);
285
+ },
286
+ },
287
+ shortcuts: {
288
+ id: "routeHint-open",
289
+ shortcut: { key: "h", mod: true, shift: true },
290
+ run: ({ openPanel }) => openPanel(),
291
+ },
292
+ } satisfies FeedbackCapturePluginWithUi;
293
+ ```
294
+
295
+ Built-in pick / capture / record chords go through the same plugin shortcut registry as customs
296
+ (first match wins). The shell only registers the open-panel chord. Built-in element, screenshot,
297
+ comment, and report-type panels are slot contributions (element / screenshot gated by
298
+ `activePlugins`).
299
+
300
+ ### Open from your UI
301
+
302
+ With the client injected (default), open or close the panel from app code with
303
+ zero imports — useful for a touchscreen menu item:
304
+
305
+ ```ts
306
+ if (import.meta.env.DEV) {
307
+ window.dispatchEvent(new CustomEvent("t3-feedback:open"));
308
+ // window.dispatchEvent(new CustomEvent("t3-feedback:close"));
309
+ }
310
+ ```
311
+
312
+ Optional typed helpers (tiny; does not pull the overlay/`/client` bundle):
313
+
314
+ ```ts
315
+ import { openT3Feedback } from "@rose42/t3-feedback/controls";
316
+
317
+ if (import.meta.env.DEV) {
318
+ openT3Feedback();
319
+ }
320
+ ```
321
+
322
+ Events are a no-op if the feedback client is not mounted.
323
+
324
+ ### Manual mount (advanced / no inject)
325
+
326
+ Prefer `customPlugins` + inject when possible. For imperative mount (custom `transport`,
327
+ `resolveSource`, or extra `plugins` instances):
328
+
329
+ ```ts
330
+ // vite.config.ts
331
+ t3FeedbackPlugin({ injectClient: false, workspaceRootHints: [process.cwd()] });
332
+ ```
333
+
334
+ ```ts
335
+ // main.ts
336
+ if (import.meta.env.DEV) {
337
+ void import("@rose42/t3-feedback/client").then(({ mountT3Feedback }) => {
338
+ const feedback = mountT3Feedback({
339
+ // activePlugins: ["pageContext", "screenshot"],
340
+ // plugins: [/* extra @rose42/feedback-capture plugins */],
341
+ });
342
+ // feedback.open() / feedback.pick() / feedback.capture() / feedback.record() / feedback.close() / feedback.unmount()
343
+ });
344
+ }
345
+ ```
346
+
347
+ ### Next.js (DEV Route Handler + explicit mount)
348
+
349
+ The Vite plugin cannot run under Next. Use the same `/client` overlay and the Node
350
+ `/bridge` entry. **DEV only** — do not ship this route or client mount in production
351
+ builds (see the status note at the top of this README).
352
+
353
+ 1. **Route Handler** — call `handleT3FeedbackRequest`. The handler itself
354
+ returns 404 unless `NODE_ENV === "development"` (keep the route out of
355
+ production bundles anyway):
356
+
357
+ ```ts
358
+ // app/api/t3-feedback/[[...path]]/route.ts
359
+ import { handleT3FeedbackRequest } from "@rose42/t3-feedback/bridge";
360
+
361
+ const bridge = {
362
+ basePath: "/api/t3-feedback",
363
+ workspaceRootHints: [process.cwd()],
364
+ };
365
+
366
+ async function handle(request: Request) {
367
+ const response = await handleT3FeedbackRequest(request, bridge);
368
+ return response ?? new Response(null, { status: 404 });
369
+ }
370
+
371
+ export const GET = handle;
372
+ export const POST = handle;
373
+ ```
374
+
375
+ 2. **Client mount** — DEV-only `"use client"` bootstrap that
376
+ `import()`s `@rose42/t3-feedback/client` and calls
377
+ `mountT3Feedback({ endpoint: "/api/t3-feedback", … })` (same options as the
378
+ Vite plugin’s client config). Open from the app with
379
+ `window.dispatchEvent(new CustomEvent("t3-feedback:open"))`.
380
+
381
+ `vite` is an **optional** peer — Next hosts only need `/bridge` + `/client`.
382
+
383
+ Fully custom UI: build an envelope with `@rose42/feedback-capture` and
384
+ `POST /__t3-feedback` yourself (raw envelope, or
385
+ `{ envelope, threadId? }` to reuse a thread); keep the Vite plugin for the T3
386
+ bridge. After a successful send, the JSON body includes `openUrl` for an
387
+ Open-in-T3 handoff (bare thread URL using the server's `environmentId` from
388
+ `/.well-known/t3/environment`; the injected UI also calls
389
+ `GET /__t3-feedback/open?threadId=` for a fresh `pairUrl` + `threadUrl`
390
+ so pairing is automatic). Working-thread status can be polled via
391
+ `GET /__t3-feedback/threads?ids=…` (returns `{ threads: [{ threadId, state }] }`
392
+ with `state` of `working` | `settled` | `unknown`). Without `ids`, the same path
393
+ lists the matched project’s threads for the continue picker
394
+ (`{ threads: [{ threadId, title, state, updatedAt? }] }`). Activity **Settle** calls
395
+ `POST /__t3-feedback/settle` with `{ threadIds }` to dispatch T3 `thread.settle`
396
+ (returns `{ threads: [{ threadId, ok, skipped? }] }`). **Dismiss** / **Clear all**
397
+ only clear the local activity list.
398
+
399
+ ## Options
400
+
401
+ | Option | Default | Notes |
402
+ | -------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
403
+ | `origin` | `http://localhost:3773` | Overridable via `T3_FEEDBACK_ORIGIN` / `T3_ORIGIN` |
404
+ | `openOrigin` | same as `origin` | Host-browser URL for Open-in-T3 (`T3_FEEDBACK_OPEN_ORIGIN`). Set explicitly in multi-container setups; do not point at another project's T3. |
405
+ | `threadReuse` | `"ask"` | `"ask"` replaces Send with New / ▶ {title} ▾: **Ctrl/⌘+Enter** soft-default, **Ctrl/⌘+Shift+Enter** New, **Alt+Enter** Continue; chevron lists project threads (search); `"session-route"` always reuses per pathname; `"never"` always creates |
406
+ | `threadTitle` | _(none)_ | Optional prefix for new thread titles (`Bug: path · comment`) |
407
+ | `workspaceRootHints` | `[cwd]` (+ `T3_FEEDBACK_WORKSPACE_HINT`) | Match T3 project by workspace path |
408
+ | `shortcut` | _(omit for Ctrl/⌘+Shift+F)_ | When set, omitted `mod`/`shift`/`alt` are `false` (e.g. `{ key: "f" }` = bare F). Bare keys skip editable fields; Ctrl/⌘+Shift+key still opens while typing |
409
+ | `shortcutKey` | _(deprecated)_ | Key-only alias; prefer `shortcut.key` |
410
+ | `pick` | Ctrl/⌘+Shift+E, `afterPick: "crop"` | Pick shortcut (`false` to disable), after-pick capture/crop, padding; `openInEditor` (default on) makes resolved file paths clickable via Vite `/__open-in-editor` or a `urlTemplate` |
411
+ | `screenshot` | Ctrl/⌘+Shift+S, `crop: "ui"`, `maxImages: 5` | Capture shortcut (`false` to disable); crop `"ui"` (on-demand from thumbs) \| `"auto"` \| `"none"`; gallery cap |
412
+ | `record` | Ctrl/⌘+Shift+R | Record / frame-burst shortcut (`false` to disable); gated with `screenshot` in `activePlugins` |
413
+ | `activePlugins` | all four built-ins | `pageContext` \| `consoleLogs` \| `screenshot` \| `elementTarget` — omit for defaults |
414
+ | `pluginOptions` | `consoleLogs.maxEntries: 200` | Serializable options for selected built-ins |
415
+ | `customPlugins` | `[]` | Vite-resolved module paths/ids imported into the injected client |
416
+ | `injectClient` | `true` | HTML inject of the overlay |
417
+ | `fab` | mobile-only, bottom-right | FAB visibility / position / size |
418
+ | `reportTypeGlyph` | `"icon"` | `"icon"` (SVG + textual titles), `"emoji"`, or a map `{ Bug, Feedback, Suggestion }` — emoji/map also prefix new chat names |
419
+ | `version` | _(none)_ | App version on bug/feedback reports; DEV also attaches HMR `#n` |
420
+
421
+ ## Environment
422
+
423
+ | Variable | Role |
424
+ | ---------------------------------- | ----------------------------------------------------------------------------------------- |
425
+ | `T3_FEEDBACK_ORIGIN` / `T3_ORIGIN` | T3 HTTP origin |
426
+ | `T3_FEEDBACK_OPEN_ORIGIN` | Browser origin for Open-in-T3 (default: same as `T3_FEEDBACK_ORIGIN`) |
427
+ | `T3_FEEDBACK_TOKEN` | Pre-issued bearer (skips `t3 auth session issue`) |
428
+ | `T3_FEEDBACK_WORKSPACE_HINT` | Extra workspace path hint |
429
+ | `LAUNCH_EDITOR` | Editor binary for Vite `/__open-in-editor` (e.g. `cursor`, `code`) when auto-detect fails |
430
+
431
+ When the feedback plugin runs in a **different** container/project than this one, point
432
+ `T3_FEEDBACK_ORIGIN` at **that** project's T3 (`http://localhost:<its-port>` or an
433
+ in-container hostname), and set `T3_FEEDBACK_OPEN_ORIGIN` to the host Port-forward URL for
434
+ the **same** port (`http://localhost:<its-port>`) when they differ. Unique host ports per
435
+ container avoid opening the wrong environment (check the Vite log line
436
+ `[t3-feedback] T3 environment … (label)`).
437
+
438
+ **Runtime prerequisites:** T3 Code running, and either `t3` on `PATH` or
439
+ `T3_FEEDBACK_TOKEN` set. Browser needs `getDisplayMedia` (paste image works as fallback).
440
+
441
+ When element pick resolves a component file, the ELEMENT chip path is a link that
442
+ opens the IDE. Default is Vite’s `/__open-in-editor` (same as the error overlay) —
443
+ that only works when the browser and Vite share a machine that can launch an
444
+ editor (`LAUNCH_EDITOR=cursor` or `code` if auto-detect fails).
445
+
446
+ When the **browser is on the host** and Vite runs in a **container/remote**, use a
447
+ protocol URL instead so the OS / Cursor handles the open:
448
+
449
+ ```ts
450
+ t3FeedbackPlugin({
451
+ pick: {
452
+ openInEditor: {
453
+ // `{file}` is whatever the sourcemap returns (often relative). Prefix with
454
+ // an absolute project root when your scheme requires absolute paths.
455
+ urlTemplate: `cursor://file${process.cwd()}/{file}:{line}:{column}`,
456
+ },
457
+ },
458
+ });
459
+ ```
460
+
461
+ With `urlTemplate`, the chip renders a real `<a href>` (not a fetch to Vite).
462
+ `vscode://file/…` also works if that scheme is bound to your editor.
463
+
464
+ ## Package scripts
465
+
466
+ ```bash
467
+ pnpm install
468
+ pnpm typecheck
469
+ pnpm lint
470
+ pnpm format:check
471
+ pnpm test
472
+ pnpm build
473
+ ```
474
+
475
+ Watch `dist/` while developing against a consumer:
476
+
477
+ ```bash
478
+ pnpm dev
479
+ ```
480
+
481
+ ## Release
482
+
483
+ Releases are driven by **version tags** (`vX.Y.Z`). Pushing a tag runs CI: verify → GitLab Release (dist tarball in the package registry) → publish to public npm.
484
+
485
+ CI installs `@rose42/feedback-capture@0.1.0` from npm. Local/devcontainer work
486
+ symlinks a sibling [`feedback-capture`](https://gitlab.com/rose42/feedback-capture) clone
487
+ into `node_modules` without changing the declared dependency.
488
+
489
+ ### One-time npm + GitLab setup
490
+
491
+ 1. Create or join the **`rose42`** org on [npmjs.com](https://www.npmjs.com) with permission to publish `@rose42/*`.
492
+ 2. Create an npm **Automation** (or granular) access token with publish rights for `@rose42/t3-feedback` (or the whole `@rose42` org).
493
+ 3. In GitLab: **Settings → CI/CD → Variables** → add:
494
+ - Key: `NPM_TOKEN`
495
+ - Value: the token
496
+ - Flags: **Masked**; use **Protected** only if `v*` tags are protected (otherwise leave Protected off so tag pipelines can read it).
497
+ 4. Optional local check: `pnpm login` then `pnpm publish --dry-run --access public`.
498
+
499
+ ### Cut a release
500
+
501
+ Requires a clean git worktree. Bumps `package.json`, commits, tags `vX.Y.Z`, and pushes (with `--follow-tags`) so the tag pipeline runs. Ensure `NPM_TOKEN` is set in GitLab before the first publish.
502
+
503
+ ```bash
504
+ pnpm release -- 0.1.0 # first release
505
+ # later:
506
+ pnpm release -- patch # or: minor | major
507
+ ```
508
+
509
+ After the pipeline succeeds:
510
+
511
+ - npm: [`@rose42/t3-feedback`](https://www.npmjs.com/package/@rose42/t3-feedback)
512
+ - GitLab: **Deploy → Releases** (asset links to the generic package tarball)
513
+
514
+ ## License
515
+
516
+ MIT © Rose42
@@ -0,0 +1,27 @@
1
+ import type { FeedbackEnvelope } from "@rose42/feedback-capture";
2
+ export type T3FeedbackImageAttachment = {
3
+ dataUrl: string;
4
+ name?: string;
5
+ /** Defaults to `image/png` when omitted. */
6
+ mimeType?: string;
7
+ };
8
+ /** Recognized `collect()` value for any plugin id — merged into T3 message attachments. */
9
+ export type T3FeedbackAttachmentsValue = {
10
+ attachments: T3FeedbackImageAttachment[];
11
+ };
12
+ /**
13
+ * PNG data URLs from the screenshot plugin.
14
+ * Accepts legacy single `string` or `string[]`.
15
+ */
16
+ export declare function envelopeScreenshotDataUrls(envelope: FeedbackEnvelope): string[];
17
+ /** First PNG data URL from the screenshot plugin, if present. */
18
+ export declare function envelopeScreenshotDataUrl(envelope: FeedbackEnvelope): string | undefined;
19
+ export declare function dataUrlSizeBytes(dataUrl: string): number;
20
+ /** True when `value` is the attachment collect shape (at least one valid item). */
21
+ export declare function isT3FeedbackAttachmentsValue(value: unknown): value is T3FeedbackAttachmentsValue;
22
+ /**
23
+ * Image attachments for the T3 thread message: legacy `screenshot` URLs plus any
24
+ * plugin collect value shaped as {@link T3FeedbackAttachmentsValue}.
25
+ * Dedupes by `dataUrl` (first wins).
26
+ */
27
+ export declare function envelopeImageAttachments(envelope: FeedbackEnvelope): T3FeedbackImageAttachment[];
@@ -0,0 +1,109 @@
1
+ function valueOf(result) {
2
+ if (!result || !result.ok)
3
+ return undefined;
4
+ return result.value;
5
+ }
6
+ /**
7
+ * PNG data URLs from the screenshot plugin.
8
+ * Accepts legacy single `string` or `string[]`.
9
+ */
10
+ export function envelopeScreenshotDataUrls(envelope) {
11
+ const value = valueOf(envelope.screenshot);
12
+ if (value === undefined)
13
+ return [];
14
+ if (typeof value === "string")
15
+ return value ? [value] : [];
16
+ if (Array.isArray(value)) {
17
+ return value.filter((item) => typeof item === "string" && item.length > 0);
18
+ }
19
+ return [];
20
+ }
21
+ /** First PNG data URL from the screenshot plugin, if present. */
22
+ export function envelopeScreenshotDataUrl(envelope) {
23
+ return envelopeScreenshotDataUrls(envelope)[0];
24
+ }
25
+ export function dataUrlSizeBytes(dataUrl) {
26
+ const base64 = dataUrl.split(",")[1] ?? "";
27
+ return Math.floor((base64.length * 3) / 4);
28
+ }
29
+ function isNonEmptyDataUrl(value) {
30
+ return typeof value === "string" && value.length > 0;
31
+ }
32
+ function normalizeAttachmentItem(item) {
33
+ if (!item || typeof item !== "object")
34
+ return undefined;
35
+ const record = item;
36
+ if (!isNonEmptyDataUrl(record.dataUrl))
37
+ return undefined;
38
+ const out = { dataUrl: record.dataUrl };
39
+ if (typeof record.name === "string" && record.name.trim()) {
40
+ out.name = record.name.trim();
41
+ }
42
+ if (typeof record.mimeType === "string" && record.mimeType.trim()) {
43
+ out.mimeType = record.mimeType.trim();
44
+ }
45
+ return out;
46
+ }
47
+ /** True when `value` is the attachment collect shape (at least one valid item). */
48
+ export function isT3FeedbackAttachmentsValue(value) {
49
+ if (!value || typeof value !== "object")
50
+ return false;
51
+ const attachments = value.attachments;
52
+ if (!Array.isArray(attachments))
53
+ return false;
54
+ return attachments.some((item) => normalizeAttachmentItem(item) !== undefined);
55
+ }
56
+ function attachmentsFromValue(value) {
57
+ if (!value || typeof value !== "object")
58
+ return [];
59
+ const attachments = value.attachments;
60
+ if (!Array.isArray(attachments))
61
+ return [];
62
+ const out = [];
63
+ for (const item of attachments) {
64
+ const normalized = normalizeAttachmentItem(item);
65
+ if (normalized)
66
+ out.push(normalized);
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Image attachments for the T3 thread message: legacy `screenshot` URLs plus any
72
+ * plugin collect value shaped as {@link T3FeedbackAttachmentsValue}.
73
+ * Dedupes by `dataUrl` (first wins).
74
+ */
75
+ export function envelopeImageAttachments(envelope) {
76
+ const out = [];
77
+ const seen = new Set();
78
+ const push = (item) => {
79
+ if (seen.has(item.dataUrl))
80
+ return;
81
+ seen.add(item.dataUrl);
82
+ out.push(item);
83
+ };
84
+ const legacy = envelopeScreenshotDataUrls(envelope);
85
+ for (let i = 0; i < legacy.length; i++) {
86
+ const dataUrl = legacy[i];
87
+ push({
88
+ dataUrl,
89
+ name: legacy.length === 1 ? "feedback.png" : `feedback-${i + 1}.png`,
90
+ mimeType: "image/png",
91
+ });
92
+ }
93
+ for (const [key, result] of Object.entries(envelope)) {
94
+ if (key === "screenshot")
95
+ continue;
96
+ if (!result || typeof result !== "object" || !("ok" in result))
97
+ continue;
98
+ const pr = result;
99
+ if (!pr.ok)
100
+ continue;
101
+ for (const item of attachmentsFromValue(pr.value)) {
102
+ push({
103
+ ...item,
104
+ mimeType: item.mimeType ?? "image/png",
105
+ });
106
+ }
107
+ }
108
+ return out;
109
+ }