@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.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/dist/bridge/attachments.d.ts +27 -0
- package/dist/bridge/attachments.js +109 -0
- package/dist/bridge/format.d.ts +70 -0
- package/dist/bridge/format.js +358 -0
- package/dist/bridge/http.d.ts +31 -0
- package/dist/bridge/http.js +169 -0
- package/dist/bridge/index.d.ts +13 -0
- package/dist/bridge/index.js +12 -0
- package/dist/bridge/parse.d.ts +16 -0
- package/dist/bridge/parse.js +82 -0
- package/dist/bridge/paths.d.ts +10 -0
- package/dist/bridge/paths.js +10 -0
- package/dist/bridge/probeThreads.d.ts +13 -0
- package/dist/bridge/probeThreads.js +66 -0
- package/dist/bridge/readSource.d.ts +8 -0
- package/dist/bridge/readSource.js +64 -0
- package/dist/bridge/sendFeedback.d.ts +99 -0
- package/dist/bridge/sendFeedback.js +417 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +40 -0
- package/dist/cli/constants.d.ts +4 -0
- package/dist/cli/constants.js +4 -0
- package/dist/cli/detect.d.ts +17 -0
- package/dist/cli/detect.js +118 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/init.d.ts +18 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/pm.d.ts +12 -0
- package/dist/cli/pm.js +49 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/wire/next.d.ts +8 -0
- package/dist/cli/wire/next.js +71 -0
- package/dist/cli/wire/node.d.ts +3 -0
- package/dist/cli/wire/node.js +76 -0
- package/dist/cli/wire/vite.d.ts +25 -0
- package/dist/cli/wire/vite.js +71 -0
- package/dist/client/activeThreads.d.ts +23 -0
- package/dist/client/activeThreads.js +89 -0
- package/dist/client/cropScreenshot.d.ts +39 -0
- package/dist/client/cropScreenshot.js +212 -0
- package/dist/client/customPlugins.d.ts +7 -0
- package/dist/client/customPlugins.js +38 -0
- package/dist/client/draft.d.ts +33 -0
- package/dist/client/draft.js +222 -0
- package/dist/client/index.d.ts +24 -0
- package/dist/client/index.js +20 -0
- package/dist/client/mount.d.ts +210 -0
- package/dist/client/mount.js +2408 -0
- package/dist/client/openInT3.d.ts +28 -0
- package/dist/client/openInT3.js +81 -0
- package/dist/client/readClipboardImage.d.ts +2 -0
- package/dist/client/readClipboardImage.js +2 -0
- package/dist/client/reportType.d.ts +43 -0
- package/dist/client/reportType.js +110 -0
- package/dist/client/selectBurstFrames.d.ts +21 -0
- package/dist/client/selectBurstFrames.js +101 -0
- package/dist/client/sessionThread.d.ts +26 -0
- package/dist/client/sessionThread.js +93 -0
- package/dist/client/threadPicker.d.ts +20 -0
- package/dist/client/threadPicker.js +51 -0
- package/dist/client/transport.d.ts +11 -0
- package/dist/client/transport.js +35 -0
- package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
- package/dist/client/ui/commentPanelPlugin.js +126 -0
- package/dist/client/ui/defaultPlugins.d.ts +19 -0
- package/dist/client/ui/defaultPlugins.js +48 -0
- package/dist/client/ui/elementTargetChip.d.ts +64 -0
- package/dist/client/ui/elementTargetChip.js +185 -0
- package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
- package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
- package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
- package/dist/client/ui/elementTargetPanelSession.js +279 -0
- package/dist/client/ui/elementTargetUi.d.ts +36 -0
- package/dist/client/ui/elementTargetUi.js +105 -0
- package/dist/client/ui/host.d.ts +112 -0
- package/dist/client/ui/host.js +132 -0
- package/dist/client/ui/html.d.ts +22 -0
- package/dist/client/ui/html.js +26 -0
- package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
- package/dist/client/ui/labeledPluginPreview.js +71 -0
- package/dist/client/ui/resolveContributions.d.ts +13 -0
- package/dist/client/ui/resolveContributions.js +20 -0
- package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
- package/dist/client/ui/screenshotPanelPlugin.js +212 -0
- package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
- package/dist/client/ui/screenshotPanelSession.js +445 -0
- package/dist/client/ui/screenshotUi.d.ts +31 -0
- package/dist/client/ui/screenshotUi.js +130 -0
- package/dist/client/ui/shortcuts.d.ts +32 -0
- package/dist/client/ui/shortcuts.js +40 -0
- package/dist/client/ui/slots.d.ts +73 -0
- package/dist/client/ui/slots.js +112 -0
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +186 -0
- package/dist/types.d.ts +464 -0
- package/dist/types.js +283 -0
- package/docs/devcontainer.md +160 -0
- 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
|
+
}
|