@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
@@ -0,0 +1,70 @@
1
+ import type { FeedbackEnvelope } from "@rose42/feedback-capture";
2
+ import { type T3FeedbackReportTypeGlyphResolved } from "../types.js";
3
+ export { dataUrlSizeBytes, envelopeImageAttachments, envelopeScreenshotDataUrl, envelopeScreenshotDataUrls, isT3FeedbackAttachmentsValue, type T3FeedbackAttachmentsValue, type T3FeedbackImageAttachment, } from "./attachments.js";
4
+ export declare const REPORT_TYPES: readonly ["bug", "feedback", "suggestion"];
5
+ export type FeedbackReportType = (typeof REPORT_TYPES)[number];
6
+ /**
7
+ * Title prefix for a report type: textual when glyph is `"icon"`, otherwise the
8
+ * emoji (built-in or custom map).
9
+ */
10
+ export declare function reportTypeTitleLabel(type: FeedbackReportType, reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved): string;
11
+ /**
12
+ * Split a derived thread title into report type + display text without the
13
+ * `Bug:` / `Feedback:` / `Suggestion:` / emoji label (keeps an optional custom prefix).
14
+ */
15
+ export declare function splitThreadTitleReportType(title: string, reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved): {
16
+ reportType?: FeedbackReportType;
17
+ displayTitle: string;
18
+ };
19
+ /** Resolve report type from the envelope; unknown/missing → `bug`. */
20
+ export declare function envelopeReportType(envelope: FeedbackEnvelope): FeedbackReportType;
21
+ /** Collect-time hint: `**label:** value` */
22
+ export type T3FeedbackFormatLabelValue = {
23
+ label: string;
24
+ value: string | number | boolean;
25
+ };
26
+ /** Collect-time hint: `### heading` + body paragraph */
27
+ export type T3FeedbackFormatHeadingBody = {
28
+ heading: string;
29
+ body: string;
30
+ };
31
+ /** Collect-time hint: raw markdown section */
32
+ export type T3FeedbackFormatMarkdown = {
33
+ markdown: string;
34
+ };
35
+ /**
36
+ * Render a custom plugin collect value using optional format hints, else defaults.
37
+ * Returns markdown lines (no trailing blank required from caller).
38
+ */
39
+ export declare function formatCustomPluginSection(key: string, value: unknown): string[];
40
+ /** Build the markdown body for a T3 turn from a plugin-keyed envelope. */
41
+ export declare function formatFeedbackMessage(envelope: FeedbackEnvelope): string;
42
+ /** Pathname from pageContext URL, or `/` when missing/invalid. */
43
+ export declare function envelopePathname(envelope: FeedbackEnvelope): string;
44
+ /**
45
+ * Display title for a new T3 thread (not a sticky lookup key).
46
+ * Shape: `[customPrefix: ]ReportType|Emoji: pathname · comment snippet`
47
+ * When `reportTypeGlyph` is `"emoji"` or a custom map, the type segment is an emoji.
48
+ */
49
+ export declare function deriveThreadTitle(envelope: FeedbackEnvelope, prefix?: string, reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved): string;
50
+ /**
51
+ * Fallback only when `/.well-known/t3/environment` is unreachable.
52
+ * Desktop bridge registrations use this id; standalone `t3` servers mint a UUID.
53
+ */
54
+ export declare const T3_PRIMARY_ENVIRONMENT_ID = "primary";
55
+ /** Strip a trailing slash from an HTTP(S) origin. */
56
+ export declare function normalizeOrigin(origin: string): string;
57
+ /**
58
+ * Browser-facing origin for Open-in-T3 links.
59
+ * Explicit `openOrigin` / `T3_FEEDBACK_OPEN_ORIGIN` wins; otherwise the API
60
+ * `origin` is used as-is (no hostname rewrite — that can open a different
61
+ * container's host-forwarded T3 on shared ports).
62
+ */
63
+ export declare function resolveOpenOrigin(apiOrigin: string, openOrigin?: string): string;
64
+ /**
65
+ * Deep link into a thread: T3 web routes are `/$environmentId/$threadId`.
66
+ * Pass the server's `environmentId` from `/.well-known/t3/environment`.
67
+ */
68
+ export declare function buildThreadOpenUrl(origin: string, threadId: string, environmentId: string): string;
69
+ /** One-time pairing URL that T3 auto-submits (`/pair#token=…`). */
70
+ export declare function buildPairOpenUrl(origin: string, credential: string): string;
@@ -0,0 +1,358 @@
1
+ import { rankSourceHierarchy } from "@rose42/feedback-capture";
2
+ import { envelopeImageAttachments, isT3FeedbackAttachmentsValue } from "./attachments.js";
3
+ import { DEFAULT_REPORT_TYPE_EMOJIS } from "../types.js";
4
+ export { dataUrlSizeBytes, envelopeImageAttachments, envelopeScreenshotDataUrl, envelopeScreenshotDataUrls, isT3FeedbackAttachmentsValue, } from "./attachments.js";
5
+ export const REPORT_TYPES = ["bug", "feedback", "suggestion"];
6
+ const REPORT_TYPE_HEADING = {
7
+ bug: "Bug report",
8
+ feedback: "Feedback",
9
+ suggestion: "Suggestion",
10
+ };
11
+ const REPORT_TYPE_TITLE = {
12
+ bug: "Bug",
13
+ feedback: "Feedback",
14
+ suggestion: "Suggestion",
15
+ };
16
+ /** Escape a literal for use inside a RegExp character class / alternation. */
17
+ function escapeRegExp(value) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+ /**
21
+ * Title prefix for a report type: textual when glyph is `"icon"`, otherwise the
22
+ * emoji (built-in or custom map).
23
+ */
24
+ export function reportTypeTitleLabel(type, reportTypeGlyph = "icon") {
25
+ if (reportTypeGlyph === "icon")
26
+ return REPORT_TYPE_TITLE[type];
27
+ if (reportTypeGlyph === "emoji")
28
+ return DEFAULT_REPORT_TYPE_EMOJIS[type];
29
+ return reportTypeGlyph[type];
30
+ }
31
+ /** Labels that may appear as `Label:` in a stored thread title, longest first. */
32
+ function titleLabelsToMatch(reportTypeGlyph) {
33
+ const byLabel = new Map();
34
+ // Prefer longer textual labels first when building the alternation.
35
+ for (const type of ["suggestion", "feedback", "bug"]) {
36
+ byLabel.set(REPORT_TYPE_TITLE[type], type);
37
+ byLabel.set(DEFAULT_REPORT_TYPE_EMOJIS[type], type);
38
+ }
39
+ if (reportTypeGlyph && typeof reportTypeGlyph === "object") {
40
+ for (const type of REPORT_TYPES) {
41
+ byLabel.set(reportTypeGlyph[type], type);
42
+ }
43
+ }
44
+ return [...byLabel.entries()]
45
+ .map(([label, reportType]) => ({ label, reportType }))
46
+ .sort((a, b) => b.label.length - a.label.length);
47
+ }
48
+ /**
49
+ * Split a derived thread title into report type + display text without the
50
+ * `Bug:` / `Feedback:` / `Suggestion:` / emoji label (keeps an optional custom prefix).
51
+ */
52
+ export function splitThreadTitleReportType(title, reportTypeGlyph) {
53
+ const trimmed = title.replace(/\s+/g, " ").trim();
54
+ if (!trimmed)
55
+ return { displayTitle: "" };
56
+ const labels = titleLabelsToMatch(reportTypeGlyph);
57
+ if (labels.length === 0)
58
+ return { displayTitle: trimmed };
59
+ const alternation = labels.map(({ label }) => escapeRegExp(label)).join("|");
60
+ const re = new RegExp(`^(?:(.+?):\\s+)?(${alternation}):\\s*(.*)$`);
61
+ const match = trimmed.match(re);
62
+ if (!match)
63
+ return { displayTitle: trimmed };
64
+ const custom = match[1]?.trim();
65
+ const typeLabel = match[2] ?? "";
66
+ const rest = (match[3] ?? "").trim();
67
+ const reportType = labels.find(({ label }) => label === typeLabel)?.reportType;
68
+ if (!reportType)
69
+ return { displayTitle: trimmed };
70
+ const displayTitle = custom ? `${custom}: ${rest}` : rest;
71
+ return { reportType, displayTitle: displayTitle || trimmed };
72
+ }
73
+ function formatSourceLocation(loc) {
74
+ const at = loc.column != null && loc.column > 0
75
+ ? `${loc.fileName}:${loc.line}:${loc.column}`
76
+ : `${loc.fileName}:${loc.line}`;
77
+ return loc.name ? `${at} (${loc.name})` : at;
78
+ }
79
+ function sameLoc(a, b) {
80
+ return (a.fileName === b.fileName &&
81
+ a.line === b.line &&
82
+ (a.column ?? 0) === (b.column ?? 0) &&
83
+ (a.name ?? "") === (b.name ?? ""));
84
+ }
85
+ /**
86
+ * Agent-facing component roles from a (possibly unranked) element target.
87
+ * Always surfaces leaf when anything resolved; primary/secondary when hierarchy.
88
+ */
89
+ function componentRolesForAgent(target) {
90
+ const ranked = rankSourceHierarchy({
91
+ ...(target.component ? { component: target.component } : {}),
92
+ ...(target.leaf ? { leaf: target.leaf } : {}),
93
+ ...(target.owners && target.owners.length > 0 ? { owners: target.owners } : {}),
94
+ });
95
+ if (!ranked?.component && !ranked?.leaf)
96
+ return {};
97
+ const leaf = ranked.leaf ?? ranked.component;
98
+ const primary = ranked.leaf && ranked.component && !sameLoc(ranked.leaf, ranked.component)
99
+ ? ranked.component
100
+ : undefined;
101
+ const owners = ranked.owners ?? [];
102
+ const secondary = owners[0];
103
+ const rest = owners.slice(1);
104
+ return {
105
+ ...(leaf ? { leaf } : {}),
106
+ ...(primary ? { primary } : {}),
107
+ ...(secondary ? { secondary } : {}),
108
+ ...(rest.length > 0 ? { owners: rest } : {}),
109
+ };
110
+ }
111
+ function valueOf(result) {
112
+ return result?.ok === true ? result.value : undefined;
113
+ }
114
+ function formatArg(value) {
115
+ if (typeof value === "string")
116
+ return value;
117
+ try {
118
+ return JSON.stringify(value);
119
+ }
120
+ catch {
121
+ return String(value);
122
+ }
123
+ }
124
+ function formatElementSection(target) {
125
+ const lines = [
126
+ "### Element",
127
+ `- **selector:** \`${target.selector}\``,
128
+ `- **tag:** \`${target.tagName}\``,
129
+ ];
130
+ if (target.id)
131
+ lines.push(`- **id:** \`${target.id}\``);
132
+ if (target.role)
133
+ lines.push(`- **role:** \`${target.role}\``);
134
+ if (target.name)
135
+ lines.push(`- **name:** ${target.name}`);
136
+ if (target.textSnippet)
137
+ lines.push(`- **text:** ${target.textSnippet}`);
138
+ const { x, y, width, height } = target.boundingBox;
139
+ lines.push(`- **bbox:** ${Math.round(x)}, ${Math.round(y)}, ${Math.round(width)}×${Math.round(height)}`);
140
+ const roles = componentRolesForAgent(target);
141
+ if (roles.leaf) {
142
+ lines.push(`- **leaf:** \`${formatSourceLocation(roles.leaf)}\``);
143
+ }
144
+ if (roles.primary) {
145
+ lines.push(`- **primary:** \`${formatSourceLocation(roles.primary)}\``);
146
+ }
147
+ if (roles.secondary) {
148
+ lines.push(`- **secondary:** \`${formatSourceLocation(roles.secondary)}\``);
149
+ }
150
+ if (roles.owners && roles.owners.length > 0) {
151
+ lines.push(`- **owners:** ${roles.owners.map((o) => `\`${formatSourceLocation(o)}\``).join(" ← ")}`);
152
+ }
153
+ if (target.styles && Object.keys(target.styles).length > 0) {
154
+ lines.push("", "**Computed styles:**", "```css");
155
+ for (const [prop, value] of Object.entries(target.styles)) {
156
+ lines.push(`${prop}: ${value};`);
157
+ }
158
+ lines.push("```");
159
+ }
160
+ if (target.innerHTMLSnippet) {
161
+ lines.push("", "```html", target.innerHTMLSnippet, "```");
162
+ }
163
+ lines.push("");
164
+ return lines;
165
+ }
166
+ /** Resolve report type from the envelope; unknown/missing → `bug`. */
167
+ export function envelopeReportType(envelope) {
168
+ const raw = valueOf(envelope.reportType);
169
+ if (raw === "feedback" || raw === "suggestion" || raw === "bug")
170
+ return raw;
171
+ return "bug";
172
+ }
173
+ function isFormatLabelValue(value) {
174
+ if (!value || typeof value !== "object" || Array.isArray(value))
175
+ return false;
176
+ const record = value;
177
+ if (typeof record.label !== "string" || !record.label.trim())
178
+ return false;
179
+ const v = record.value;
180
+ return typeof v === "string" || typeof v === "number" || typeof v === "boolean";
181
+ }
182
+ function isFormatHeadingBody(value) {
183
+ if (!value || typeof value !== "object" || Array.isArray(value))
184
+ return false;
185
+ const record = value;
186
+ return (typeof record.heading === "string" &&
187
+ record.heading.trim().length > 0 &&
188
+ typeof record.body === "string");
189
+ }
190
+ function isFormatMarkdown(value) {
191
+ if (!value || typeof value !== "object" || Array.isArray(value))
192
+ return false;
193
+ const record = value;
194
+ return typeof record.markdown === "string" && record.markdown.trim().length > 0;
195
+ }
196
+ /**
197
+ * Render a custom plugin collect value using optional format hints, else defaults.
198
+ * Returns markdown lines (no trailing blank required from caller).
199
+ */
200
+ export function formatCustomPluginSection(key, value) {
201
+ if (isFormatMarkdown(value)) {
202
+ return [value.markdown.trim(), ""];
203
+ }
204
+ if (isFormatHeadingBody(value)) {
205
+ return [`### ${value.heading.trim()}`, value.body, ""];
206
+ }
207
+ if (isFormatLabelValue(value)) {
208
+ return [`**${value.label.trim()}:** ${String(value.value)}`, ""];
209
+ }
210
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
211
+ return [`**${key}:** ${String(value)}`, ""];
212
+ }
213
+ return [`### ${key}`, "```json", JSON.stringify(value, null, 2), "```", ""];
214
+ }
215
+ /** Build the markdown body for a T3 turn from a plugin-keyed envelope. */
216
+ export function formatFeedbackMessage(envelope) {
217
+ const reportType = envelopeReportType(envelope);
218
+ const comment = valueOf(envelope.comment);
219
+ const page = valueOf(envelope.pageContext);
220
+ const versionInfo = valueOf(envelope.version);
221
+ const logs = valueOf(envelope.consoleLogs);
222
+ const element = valueOf(envelope.elementTarget);
223
+ const lines = [`## ${REPORT_TYPE_HEADING[reportType]}`, ""];
224
+ const documentTitle = valueOf(envelope.documentTitle);
225
+ // Render whatever collect plugins put in the envelope (reportTypes / field
226
+ // filtering happen at collect time — format does not re-gate by report type).
227
+ if (page) {
228
+ lines.push(`**URL:** ${page.url}`);
229
+ if (typeof documentTitle === "string" && documentTitle.trim()) {
230
+ lines.push(`**Title:** ${documentTitle.trim()}`);
231
+ }
232
+ if (page.viewport) {
233
+ lines.push(`**Viewport:** ${page.viewport.width}×${page.viewport.height} @${page.viewport.devicePixelRatio}x`);
234
+ }
235
+ if (page.capturedAt) {
236
+ lines.push(`**Captured:** ${page.capturedAt}`);
237
+ }
238
+ if (page.userAgent) {
239
+ lines.push(`**UA:** ${page.userAgent}`);
240
+ }
241
+ lines.push("");
242
+ }
243
+ else if (typeof documentTitle === "string" && documentTitle.trim()) {
244
+ lines.push(`**Title:** ${documentTitle.trim()}`, "");
245
+ }
246
+ if (versionInfo?.version) {
247
+ const hmr = typeof versionInfo.hmr === "number" && Number.isFinite(versionInfo.hmr)
248
+ ? ` · #${versionInfo.hmr}`
249
+ : "";
250
+ lines.push(`**Version:** ${versionInfo.version}${hmr}`, "");
251
+ }
252
+ lines.push("### Comment", comment?.trim() || "(no comment)", "");
253
+ const screenshotCount = envelopeImageAttachments(envelope).length;
254
+ if (screenshotCount > 0) {
255
+ lines.push(`**Screenshots:** ${screenshotCount} attached`, "");
256
+ }
257
+ if (element) {
258
+ lines.push(...formatElementSection(element));
259
+ }
260
+ if (logs && logs.length > 0) {
261
+ lines.push("### Console logs", "```");
262
+ for (const entry of logs.slice(-50)) {
263
+ const args = entry.args.map(formatArg).join(" ");
264
+ lines.push(`[${entry.timestamp}] ${entry.level}: ${args}`);
265
+ }
266
+ lines.push("```", "");
267
+ }
268
+ // Surface other plugin keys for custom / future plugins.
269
+ for (const [key, result] of Object.entries(envelope)) {
270
+ if (key === "comment" ||
271
+ key === "pageContext" ||
272
+ key === "screenshot" ||
273
+ key === "consoleLogs" ||
274
+ key === "elementTarget" ||
275
+ key === "reportType" ||
276
+ key === "version" ||
277
+ key === "documentTitle") {
278
+ continue;
279
+ }
280
+ if (!result || typeof result !== "object" || !("ok" in result))
281
+ continue;
282
+ const pr = result;
283
+ if (!pr.ok) {
284
+ lines.push(`### ${key}`, `_(collect failed: ${pr.error})_`, "");
285
+ continue;
286
+ }
287
+ // Image attachments are uploaded separately — never dump base64 into markdown.
288
+ if (isT3FeedbackAttachmentsValue(pr.value))
289
+ continue;
290
+ lines.push(...formatCustomPluginSection(key, pr.value));
291
+ }
292
+ return lines.join("\n").trimEnd() + "\n";
293
+ }
294
+ /** Pathname from pageContext URL, or `/` when missing/invalid. */
295
+ export function envelopePathname(envelope) {
296
+ const page = valueOf(envelope.pageContext);
297
+ if (!page?.url)
298
+ return "/";
299
+ try {
300
+ return new URL(page.url).pathname || "/";
301
+ }
302
+ catch {
303
+ return "/";
304
+ }
305
+ }
306
+ /**
307
+ * Display title for a new T3 thread (not a sticky lookup key).
308
+ * Shape: `[customPrefix: ]ReportType|Emoji: pathname · comment snippet`
309
+ * When `reportTypeGlyph` is `"emoji"` or a custom map, the type segment is an emoji.
310
+ */
311
+ export function deriveThreadTitle(envelope, prefix, reportTypeGlyph = "icon") {
312
+ const comment = valueOf(envelope.comment)
313
+ ?.replace(/\s+/g, " ")
314
+ .trim();
315
+ const path = envelopePathname(envelope);
316
+ const snippet = (comment || "feedback").slice(0, 48);
317
+ const typeLabel = reportTypeTitleLabel(envelopeReportType(envelope), reportTypeGlyph);
318
+ const base = `${typeLabel}: ${path} · ${snippet}`;
319
+ const trimmedPrefix = prefix?.trim();
320
+ const title = trimmedPrefix ? `${trimmedPrefix}: ${base}` : base;
321
+ return title.slice(0, 100);
322
+ }
323
+ /**
324
+ * Fallback only when `/.well-known/t3/environment` is unreachable.
325
+ * Desktop bridge registrations use this id; standalone `t3` servers mint a UUID.
326
+ */
327
+ export const T3_PRIMARY_ENVIRONMENT_ID = "primary";
328
+ /** Strip a trailing slash from an HTTP(S) origin. */
329
+ export function normalizeOrigin(origin) {
330
+ return origin.trim().replace(/\/$/, "");
331
+ }
332
+ /**
333
+ * Browser-facing origin for Open-in-T3 links.
334
+ * Explicit `openOrigin` / `T3_FEEDBACK_OPEN_ORIGIN` wins; otherwise the API
335
+ * `origin` is used as-is (no hostname rewrite — that can open a different
336
+ * container's host-forwarded T3 on shared ports).
337
+ */
338
+ export function resolveOpenOrigin(apiOrigin, openOrigin) {
339
+ const explicit = openOrigin?.trim();
340
+ if (explicit)
341
+ return normalizeOrigin(explicit);
342
+ return normalizeOrigin(apiOrigin);
343
+ }
344
+ /**
345
+ * Deep link into a thread: T3 web routes are `/$environmentId/$threadId`.
346
+ * Pass the server's `environmentId` from `/.well-known/t3/environment`.
347
+ */
348
+ export function buildThreadOpenUrl(origin, threadId, environmentId) {
349
+ const base = normalizeOrigin(origin);
350
+ const env = environmentId.trim() || T3_PRIMARY_ENVIRONMENT_ID;
351
+ return `${base}/${encodeURIComponent(env)}/${encodeURIComponent(threadId)}`;
352
+ }
353
+ /** One-time pairing URL that T3 auto-submits (`/pair#token=…`). */
354
+ export function buildPairOpenUrl(origin, credential) {
355
+ const base = normalizeOrigin(origin);
356
+ const token = credential.trim();
357
+ return `${base}/pair#token=${encodeURIComponent(token)}`;
358
+ }
@@ -0,0 +1,31 @@
1
+ import type { T3FeedbackReportTypeGlyphResolved } from "../types.js";
2
+ export type T3FeedbackBridgeRoute = "feedback" | "threads" | "settle" | "open" | "source" | null;
3
+ /**
4
+ * Options for the host-agnostic Fetch handler (Vite middleware, Next Route Handler, …).
5
+ * Env vars (`T3_FEEDBACK_*`) apply when fields are omitted — same as the Vite plugin.
6
+ */
7
+ export type T3FeedbackBridgeServerOptions = {
8
+ /**
9
+ * HTTP path prefix the client posts to (no trailing slash).
10
+ * Default: `/__t3-feedback`. Next hosts typically use `/api/t3-feedback`.
11
+ */
12
+ basePath?: string;
13
+ origin?: string;
14
+ openOrigin?: string;
15
+ workspaceRootHints?: string[];
16
+ threadTitlePrefix?: string;
17
+ reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved;
18
+ token?: string;
19
+ };
20
+ /** Resolve which bridge route a pathname maps to under `basePath`. */
21
+ export declare function matchT3FeedbackRoute(pathname: string, basePath?: string): T3FeedbackBridgeRoute;
22
+ /**
23
+ * Host-agnostic T3 feedback bridge for the Fetch API.
24
+ *
25
+ * Wire from Vite `configureServer` or a Next.js Route Handler. Returns `null`
26
+ * when the request is not for this bridge (caller should fall through).
27
+ *
28
+ * DEV-only: once a bridge route matches, refuses unless
29
+ * `process.env.NODE_ENV === "development"` (404 JSON).
30
+ */
31
+ export declare function handleT3FeedbackRequest(request: Request, options?: T3FeedbackBridgeServerOptions): Promise<Response | null>;
@@ -0,0 +1,169 @@
1
+ import { resolveOpenOrigin } from "./format.js";
2
+ import { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
3
+ import { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
4
+ import { readWorkspaceSourceFile } from "./readSource.js";
5
+ import { createOpenHandoff, listThreadsInT3, probeThreadsInT3, sendFeedbackToT3, settleThreadsInT3, } from "./sendFeedback.js";
6
+ import path from "node:path";
7
+ const DEFAULT_ORIGIN = "http://localhost:3773";
8
+ function pathResolveUp(from, levels) {
9
+ let current = from;
10
+ for (let i = 0; i < levels; i += 1)
11
+ current = path.resolve(current, "..");
12
+ return current;
13
+ }
14
+ function normalizeBasePath(basePath) {
15
+ const trimmed = basePath.trim() || T3_FEEDBACK_API_PATH;
16
+ return trimmed.replace(/\/$/, "") || T3_FEEDBACK_API_PATH;
17
+ }
18
+ /** Resolve which bridge route a pathname maps to under `basePath`. */
19
+ export function matchT3FeedbackRoute(pathname, basePath = T3_FEEDBACK_API_PATH) {
20
+ const base = normalizeBasePath(basePath);
21
+ if (pathname === `${base}${T3_FEEDBACK_THREADS_SUFFIX}`)
22
+ return "threads";
23
+ if (pathname === `${base}${T3_FEEDBACK_SETTLE_SUFFIX}`)
24
+ return "settle";
25
+ if (pathname === `${base}${T3_FEEDBACK_OPEN_SUFFIX}`)
26
+ return "open";
27
+ if (pathname === `${base}${T3_FEEDBACK_SOURCE_SUFFIX}`)
28
+ return "source";
29
+ if (pathname === base || pathname === `${base}/`)
30
+ return "feedback";
31
+ return null;
32
+ }
33
+ function jsonResponse(status, body) {
34
+ return new Response(JSON.stringify(body), {
35
+ status,
36
+ headers: { "Content-Type": "application/json" },
37
+ });
38
+ }
39
+ function resolveBridgeEnv(options) {
40
+ const origin = options.origin ?? process.env.T3_FEEDBACK_ORIGIN ?? process.env.T3_ORIGIN ?? DEFAULT_ORIGIN;
41
+ const openOrigin = resolveOpenOrigin(origin, options.openOrigin ?? process.env.T3_FEEDBACK_OPEN_ORIGIN);
42
+ const workspaceRootHints = options.workspaceRootHints ??
43
+ [process.env.T3_FEEDBACK_WORKSPACE_HINT, process.cwd()].filter((value) => Boolean(value));
44
+ const threadTitlePrefix = options.threadTitlePrefix?.trim() || undefined;
45
+ const token = options.token ?? process.env.T3_FEEDBACK_TOKEN;
46
+ return {
47
+ origin,
48
+ openOrigin,
49
+ workspaceRootHints,
50
+ ...(threadTitlePrefix ? { threadTitlePrefix } : {}),
51
+ ...(options.reportTypeGlyph ? { reportTypeGlyph: options.reportTypeGlyph } : {}),
52
+ ...(token ? { token } : {}),
53
+ basePath: normalizeBasePath(options.basePath ?? T3_FEEDBACK_API_PATH),
54
+ };
55
+ }
56
+ /**
57
+ * Host-agnostic T3 feedback bridge for the Fetch API.
58
+ *
59
+ * Wire from Vite `configureServer` or a Next.js Route Handler. Returns `null`
60
+ * when the request is not for this bridge (caller should fall through).
61
+ *
62
+ * DEV-only: once a bridge route matches, refuses unless
63
+ * `process.env.NODE_ENV === "development"` (404 JSON).
64
+ */
65
+ export async function handleT3FeedbackRequest(request, options = {}) {
66
+ const resolved = resolveBridgeEnv(options);
67
+ const url = new URL(request.url);
68
+ const route = matchT3FeedbackRoute(url.pathname, resolved.basePath);
69
+ if (!route)
70
+ return null;
71
+ if (process.env.NODE_ENV !== "development") {
72
+ return jsonResponse(404, {
73
+ error: "t3-feedback bridge is available only when NODE_ENV=development",
74
+ });
75
+ }
76
+ const bridgeOpts = {
77
+ origin: resolved.origin,
78
+ workspaceRootHints: resolved.workspaceRootHints,
79
+ ...(resolved.token ? { token: resolved.token } : {}),
80
+ };
81
+ try {
82
+ if (route === "threads" && request.method === "GET") {
83
+ const ids = parseThreadProbeIds(url.href);
84
+ if (ids.length === 0) {
85
+ const threads = await listThreadsInT3(bridgeOpts);
86
+ const body = { threads };
87
+ return jsonResponse(200, body);
88
+ }
89
+ const threads = await probeThreadsInT3(ids, bridgeOpts);
90
+ const body = { threads };
91
+ return jsonResponse(200, body);
92
+ }
93
+ if (route === "settle" && request.method === "POST") {
94
+ const body = await request.json().catch(() => null);
95
+ const ids = parseSettlePost(body);
96
+ if (ids === null) {
97
+ return jsonResponse(400, {
98
+ error: "Invalid settle body; expected { threadIds: string[] }",
99
+ });
100
+ }
101
+ const threads = await settleThreadsInT3(ids, bridgeOpts);
102
+ const response = { threads };
103
+ return jsonResponse(200, response);
104
+ }
105
+ if (route === "open" && (request.method === "GET" || request.method === "POST")) {
106
+ const body = request.method === "POST" ? await request.json().catch(() => undefined) : undefined;
107
+ const parsed = parseOpenHandoffRequest(url.href, body);
108
+ if (!parsed) {
109
+ return jsonResponse(400, {
110
+ error: "Invalid open request; expected threadId",
111
+ });
112
+ }
113
+ const handoff = await createOpenHandoff(parsed.threadId, {
114
+ origin: resolved.origin,
115
+ openOrigin: resolved.openOrigin,
116
+ });
117
+ return jsonResponse(200, handoff);
118
+ }
119
+ if (route === "source" && request.method === "GET") {
120
+ const file = url.searchParams.get("file")?.trim() ?? "";
121
+ if (!file) {
122
+ return jsonResponse(400, { error: "Missing file query param" });
123
+ }
124
+ const hints = [
125
+ ...resolved.workspaceRootHints,
126
+ // Next Route Handlers often run with cwd = apps/b2b.
127
+ process.cwd(),
128
+ pathResolveUp(process.cwd(), 1),
129
+ pathResolveUp(process.cwd(), 2),
130
+ ].filter((value, index, all) => {
131
+ return Boolean(value) && all.indexOf(value) === index;
132
+ });
133
+ const read = await readWorkspaceSourceFile(file, hints);
134
+ if (!read) {
135
+ return jsonResponse(404, { error: `Source not found: ${file}` });
136
+ }
137
+ return new Response(read.content, {
138
+ status: 200,
139
+ headers: {
140
+ "Content-Type": "text/plain; charset=utf-8",
141
+ "Cache-Control": "no-store",
142
+ },
143
+ });
144
+ }
145
+ if (route === "feedback" && request.method === "POST") {
146
+ const body = await request.json().catch(() => null);
147
+ const parsed = parseFeedbackPost(body);
148
+ if (!parsed) {
149
+ return jsonResponse(400, { error: "Invalid feedback envelope" });
150
+ }
151
+ const result = await sendFeedbackToT3(parsed.envelope, {
152
+ origin: resolved.origin,
153
+ openOrigin: resolved.openOrigin,
154
+ workspaceRootHints: resolved.workspaceRootHints,
155
+ ...(resolved.reportTypeGlyph ? { reportTypeGlyph: resolved.reportTypeGlyph } : {}),
156
+ ...(resolved.threadTitlePrefix ? { threadTitlePrefix: resolved.threadTitlePrefix } : {}),
157
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
158
+ ...(resolved.token ? { token: resolved.token } : {}),
159
+ });
160
+ return jsonResponse(200, result);
161
+ }
162
+ }
163
+ catch (error) {
164
+ const message = error instanceof Error ? error.message : String(error);
165
+ console.error("[t3-feedback]", message);
166
+ return jsonResponse(502, { error: message });
167
+ }
168
+ return null;
169
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Host-agnostic Node bridge for T3 feedback.
3
+ * Use from Vite middleware, Next Route Handlers, or any Fetch-capable HTTP host.
4
+ * Do not import this entry from browser code.
5
+ */
6
+ export { handleT3FeedbackRequest, matchT3FeedbackRoute, type T3FeedbackBridgeRoute, type T3FeedbackBridgeServerOptions, } from "./http.js";
7
+ export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
8
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
9
+ export { readWorkspaceSourceFile } from "./readSource.js";
10
+ export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, type ProbeThreadsOptions, type ShellSnapshot, type T3BridgeOptions, type T3EnvironmentDescriptor, } from "./sendFeedback.js";
11
+ export { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatCustomPluginSection, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, type T3FeedbackFormatHeadingBody, type T3FeedbackFormatLabelValue, type T3FeedbackFormatMarkdown, } from "./format.js";
12
+ export { dataUrlSizeBytes, envelopeImageAttachments, isT3FeedbackAttachmentsValue, type T3FeedbackAttachmentsValue, type T3FeedbackImageAttachment, } from "./attachments.js";
13
+ export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadSettleResult, ThreadsListResponse, ThreadsProbeResponse, ThreadsSettleResponse, T3FeedbackOpenHandoff, T3FeedbackReportTypeGlyphResolved, } from "../types.js";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Host-agnostic Node bridge for T3 feedback.
3
+ * Use from Vite middleware, Next Route Handlers, or any Fetch-capable HTTP host.
4
+ * Do not import this entry from browser code.
5
+ */
6
+ export { handleT3FeedbackRequest, matchT3FeedbackRoute, } from "./http.js";
7
+ export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
8
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
9
+ export { readWorkspaceSourceFile } from "./readSource.js";
10
+ export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, } from "./sendFeedback.js";
11
+ export { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatCustomPluginSection, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, } from "./format.js";
12
+ export { dataUrlSizeBytes, envelopeImageAttachments, isT3FeedbackAttachmentsValue, } from "./attachments.js";
@@ -0,0 +1,16 @@
1
+ import type { FeedbackEnvelope } from "@rose42/feedback-capture";
2
+ /** POST body: raw envelope, or `{ envelope, threadId? }` for session reuse. */
3
+ export declare function parseFeedbackPost(body: unknown): {
4
+ envelope: FeedbackEnvelope;
5
+ threadId?: string;
6
+ } | null;
7
+ /** Parse `?ids=a,b` from the threads probe URL. */
8
+ export declare function parseThreadProbeIds(url: string): string[];
9
+ /** POST body: `{ threadIds: string[] }` (also accepts singular `threadId`). */
10
+ export declare function parseSettlePost(body: unknown): string[] | null;
11
+ /** Parse `{ threadId, environmentId?, projectId? }` from open query string or JSON body. */
12
+ export declare function parseOpenHandoffRequest(url: string, body?: unknown): {
13
+ threadId: string;
14
+ environmentId?: string;
15
+ projectId?: string;
16
+ } | null;