@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,2408 @@
1
+ import { captureFrameBurst, captureScreenshot, createSourceResolver, initFeedbackCapture, } from "@rose42/feedback-capture";
2
+ import { T3_FEEDBACK_CLOSE_EVENT, T3_FEEDBACK_OPEN_EVENT } from "../controls.js";
3
+ import { splitThreadTitleReportType } from "../bridge/format.js";
4
+ import { formatShortcutLabel, resolveActivePlugins, resolveFabConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, } from "../types.js";
5
+ import { openInT3Seamless, shouldBypassSeamlessOpen } from "./openInT3.js";
6
+ import { ACTIVE_THREAD_POLL_MS, clearActiveThreads, listActiveThreads, removeActiveThread, removeActiveThreads, upsertActiveThread, } from "./activeThreads.js";
7
+ import { abortScreenshotRegions } from "./cropScreenshot.js";
8
+ import { defaultThreadDestination, readSessionThreadRecord, writeSessionThreadRecord, } from "./sessionThread.js";
9
+ import { defaultThreadSearchQuery, filterThreadListItems } from "./threadPicker.js";
10
+ import { createT3FeedbackHttpTransport } from "./transport.js";
11
+ import { clearFeedbackDraft, migrateDraftExtras, readFeedbackDraft, writeFeedbackDraft, } from "./draft.js";
12
+ import { buildReportTypeHint, gatePluginByReportType, pageContextForReportType, pluginAppliesToReportType, } from "./reportType.js";
13
+ import { createActionRegistry, createAfterPickRegistry, createBusyRegistry, createDraftClearRegistry, createDraftExtrasStore, createHostMedia, } from "./ui/host.js";
14
+ import { createCommentPanelPlugin, createReportTypePanelPlugin } from "./ui/commentPanelPlugin.js";
15
+ import { createCollectOnlyBuiltinPlugins, resolveAllPanelUiContributions, } from "./ui/defaultPlugins.js";
16
+ import { createElementTargetPanelPlugin } from "./ui/elementTargetPanelPlugin.js";
17
+ import { asPluginsWithUi } from "./ui/resolveContributions.js";
18
+ import { createScreenshotPanelPlugin } from "./ui/screenshotPanelPlugin.js";
19
+ import { mountSlotContributions } from "./ui/slots.js";
20
+ import { matchRegisteredShortcut, resolvePluginShortcuts, } from "./ui/shortcuts.js";
21
+ export { buildReportTypeHint, gatePluginByReportType, pageContextForReportType, pluginAppliesToReportType, };
22
+ export { readFeedbackDraft } from "./draft.js";
23
+ const REPORT_TYPE_STORAGE_KEY = "t3-feedback:report-type";
24
+ function readReportType() {
25
+ try {
26
+ const raw = sessionStorage.getItem(REPORT_TYPE_STORAGE_KEY);
27
+ if (raw === "feedback" || raw === "suggestion" || raw === "bug")
28
+ return raw;
29
+ }
30
+ catch {
31
+ // sessionStorage may be unavailable
32
+ }
33
+ return "bug";
34
+ }
35
+ function writeReportType(value) {
36
+ try {
37
+ sessionStorage.setItem(REPORT_TYPE_STORAGE_KEY, value);
38
+ }
39
+ catch {
40
+ // ignore
41
+ }
42
+ }
43
+ const noopHandle = () => ({
44
+ open: () => undefined,
45
+ close: () => undefined,
46
+ pick: () => undefined,
47
+ capture: () => undefined,
48
+ record: () => undefined,
49
+ unmount: () => undefined,
50
+ });
51
+ function cssLength(value) {
52
+ return typeof value === "number" ? `${value}px` : value;
53
+ }
54
+ function fabPositionCss(position) {
55
+ const edges = ["top", "right", "bottom", "left"]
56
+ .filter((edge) => position[edge] !== undefined)
57
+ .map((edge) => `${edge}: ${cssLength(position[edge])};`)
58
+ .join(" ");
59
+ return edges;
60
+ }
61
+ const DEFAULT_COPY = {
62
+ fabTitle: "Send feedback to T3 (Ctrl/⌘+Shift+F)",
63
+ fabAriaLabel: "Send feedback to T3",
64
+ panelTitle: "Feedback → T3",
65
+ commentPlaceholder: "What’s wrong / what should change?",
66
+ captureLabel: "Capture screenshot",
67
+ pickLabel: "Pick element",
68
+ clearPickLabel: "Clear element",
69
+ openInEditorLabel: "Open in editor",
70
+ clearScreenshotLabel: "Clear screenshots",
71
+ recordLabel: "Record",
72
+ stopRecordLabel: "Stop",
73
+ recordingStatus: "Recording… click Stop when done (max 5s).",
74
+ recordFailedStatus: "Recording failed or cancelled.",
75
+ burstAttachedStatus: "Frames attached.",
76
+ galleryFullStatus: "Image limit reached — remove one to add more.",
77
+ galleryFullTooltip: "Image limit reached — remove one to add more.",
78
+ pickBurstHint: "Select frames to attach. Click to toggle.",
79
+ pickBurstConfirmLabel: "Add selected",
80
+ removeImageLabel: "Remove",
81
+ discardScreenshotLabel: "Discard",
82
+ pickingStatus: "Click an element… Esc cancels.",
83
+ pickCancelledStatus: "Element pick cancelled.",
84
+ cancelLabel: "Cancel",
85
+ cancelTitle: "Discard draft and close. Esc or click outside keeps your draft.",
86
+ sendLabel: "Send to T3",
87
+ capturingStatus: "Capturing… (this panel is hidden during capture)",
88
+ captureFailedStatus: "Capture failed or cancelled - paste an image with Ctrl/⌘+V, or retry and share this tab.",
89
+ screenshotAttachedStatus: "Screenshot attached.",
90
+ pastedAttachedStatus: "Pasted image attached.",
91
+ commentRequiredStatus: "Add a short comment.",
92
+ sendingStatus: "Sending to T3…",
93
+ sentStatus: "Sent.",
94
+ openInT3Label: "Open in T3",
95
+ toastSentLabel: "Sent to T3",
96
+ activityTitle: "Active chats",
97
+ activityEmptyLabel: "No active chats.",
98
+ activityNewLabel: "New feedback",
99
+ activityOpenLabel: "Open",
100
+ activityContinueLabel: "Continue",
101
+ activitySettleLabel: "Settle",
102
+ activityDismissLabel: "Dismiss",
103
+ activityClearLabel: "Clear all",
104
+ activityFabTitle: "Active T3 chats",
105
+ activityFabAriaLabel: "Active T3 chats",
106
+ cropHint: "Drag to select a region. Enter crops; Esc uses the full image.",
107
+ cropHintSeeded: "Region prefilled from the picked element. Adjust the selection, Enter crops, Esc uses full.",
108
+ cropLabel: "Crop",
109
+ useFullLabel: "Use full",
110
+ changeRegionLabel: "Change region",
111
+ /** Visible CTA on the clipboard offer button (keep short — full phrase is the title). */
112
+ insertClipboardLabel: "Insert",
113
+ /** Tooltip / aria for the clipboard insert button. */
114
+ insertClipboardTitle: "Insert screenshot from clipboard",
115
+ dismissClipboardLabel: "Dismiss",
116
+ autoCapturingStatus: "Capturing around the picked element…",
117
+ autoCroppedStatus: "Screenshot cropped to the picked element.",
118
+ autoCapturedStatus: "Screenshot attached.",
119
+ includeInnerHTMLLabel: "innerHTML",
120
+ stylesLabel: "Styles",
121
+ stylesNoneLabel: "None",
122
+ stylesLayoutLabel: "Layout",
123
+ stylesComputedLabel: "Computed",
124
+ elementSectionLabel: "Element",
125
+ screenshotSectionLabel: "Screenshots",
126
+ reportTypeBugLabel: "Bug",
127
+ reportTypeFeedbackLabel: "Feedback",
128
+ reportTypeSuggestionLabel: "Suggestion",
129
+ reportTypeBugEmoji: "🐛",
130
+ reportTypeFeedbackEmoji: "💬",
131
+ reportTypeSuggestionEmoji: "💡",
132
+ threadDestinationLabel: "Chat",
133
+ threadNewLabel: "New",
134
+ threadContinueLabel: "Continue",
135
+ threadContinueUntitledLabel: "previous",
136
+ threadPickLabel: "Choose thread",
137
+ threadSearchPlaceholder: "Search threads",
138
+ threadSearchEmptyLabel: "No matching threads",
139
+ threadStateWorkingLabel: "open",
140
+ threadStateSettledLabel: "settled",
141
+ threadSearchTruncatedLabel: "Showing {shown} of {total}",
142
+ };
143
+ /** Preferred send action (soft-default New vs Continue, or solo Send). */
144
+ const SEND_PREFERRED_SHORTCUT_LABEL = "Ctrl/⌘+Enter";
145
+ /** Ask-mode dedicated New (always creates a thread). */
146
+ const SEND_NEW_SHORTCUT_LABEL = "Ctrl/⌘+Shift+Enter";
147
+ /** Ask-mode dedicated Continue (always posts to the prior thread). */
148
+ const SEND_CONTINUE_SHORTCUT_LABEL = "Alt+Enter";
149
+ function css(rootId, fab) {
150
+ const size = cssLength(fab.size);
151
+ const position = fabPositionCss(fab.position);
152
+ const mobileOnly = fab.visibility === "mobile"
153
+ ? `
154
+ @media (min-width: ${fab.mobileMaxWidth + 1}px) {
155
+ #${rootId}:not(.t3f-has-activity) .t3f-fab { display: none; }
156
+ }
157
+ `
158
+ : "";
159
+ return `
160
+ #${rootId} {
161
+ all: initial;
162
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif;
163
+ }
164
+ #${rootId} * { box-sizing: border-box; }
165
+ #${rootId} .t3f-fab {
166
+ position: fixed; ${position} z-index: 2147483000;
167
+ width: ${size}; height: ${size}; border-radius: 999px; border: 1px solid #334155;
168
+ background: #0f172a; color: #f8fafc; cursor: pointer;
169
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.35);
170
+ font-size: calc(${size} * 0.4); line-height: 1;
171
+ display: inline-flex; align-items: center; justify-content: center;
172
+ }
173
+ #${rootId} .t3f-fab:hover { background: #1e293b; }
174
+ #${rootId} .t3f-fab[hidden] { display: none !important; }
175
+ #${rootId} .t3f-fab-badge {
176
+ position: absolute; top: -4px; right: -4px;
177
+ min-width: 18px; height: 18px; padding: 0 5px;
178
+ border-radius: 999px; background: #0284c7; color: #f8fafc;
179
+ font-size: 11px; font-weight: 700; line-height: 18px;
180
+ border: 2px solid #0b1220;
181
+ display: none; align-items: center; justify-content: center;
182
+ }
183
+ #${rootId}.t3f-has-activity .t3f-fab-badge { display: inline-flex; }
184
+ #${rootId}.t3f-has-activity .t3f-fab {
185
+ display: inline-flex !important;
186
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.35), 0 0 0 2px rgba(56, 189, 248, 0.35);
187
+ }
188
+ #${rootId} .t3f-toast {
189
+ position: fixed; ${position} z-index: 2147483003;
190
+ margin-bottom: calc(${size} + 12px);
191
+ max-width: min(320px, calc(100vw - 24px));
192
+ background: #0b1220; color: #e2e8f0;
193
+ border: 1px solid #334155; border-radius: 12px;
194
+ padding: 10px 12px; box-shadow: 0 16px 40px rgba(2, 6, 23, 0.45);
195
+ display: grid; gap: 8px;
196
+ font-size: 13px; line-height: 1.35;
197
+ }
198
+ #${rootId} .t3f-toast[hidden] { display: none !important; }
199
+ #${rootId} .t3f-toast-title { font-weight: 650; color: #f8fafc; }
200
+ #${rootId} .t3f-toast-meta {
201
+ font-size: 12px; color: #94a3b8;
202
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
203
+ }
204
+ #${rootId} .t3f-toast-actions {
205
+ display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
206
+ }
207
+ #${rootId} .t3f-toast a.t3f-open,
208
+ #${rootId} .t3f-activity a.t3f-open {
209
+ color: #38bdf8; text-decoration: underline; font-size: 12px; font-weight: 600;
210
+ display: inline-flex; align-items: center; gap: 4px;
211
+ }
212
+ #${rootId} .t3f-toast a.t3f-open svg,
213
+ #${rootId} .t3f-activity a.t3f-open svg {
214
+ width: 12px; height: 12px; flex: 0 0 auto; display: block;
215
+ }
216
+ #${rootId} .t3f-toast a.t3f-open:hover,
217
+ #${rootId} .t3f-activity a.t3f-open:hover { color: #7dd3fc; }
218
+ #${rootId} .t3f-activity {
219
+ position: fixed; ${position} z-index: 2147483002;
220
+ margin-bottom: calc(${size} + 12px);
221
+ width: min(340px, calc(100vw - 24px));
222
+ max-height: min(50dvh, 360px);
223
+ overflow: auto;
224
+ background: #0b1220; color: #e2e8f0;
225
+ border: 1px solid #334155; border-radius: 14px;
226
+ padding: 12px; box-shadow: 0 20px 48px rgba(2, 6, 23, 0.5);
227
+ display: grid; gap: 10px;
228
+ }
229
+ #${rootId} .t3f-activity[hidden] { display: none !important; }
230
+ #${rootId} .t3f-activity-head {
231
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
232
+ }
233
+ #${rootId} .t3f-activity-title {
234
+ font-size: 13px; font-weight: 650; color: #f8fafc;
235
+ }
236
+ #${rootId} .t3f-activity-list { display: grid; gap: 8px; }
237
+ #${rootId} .t3f-activity-empty {
238
+ font-size: 12px; color: #64748b; padding: 4px 0;
239
+ }
240
+ #${rootId} .t3f-activity-row {
241
+ display: grid; gap: 6px;
242
+ padding: 8px; border-radius: 10px;
243
+ background: #020617; border: 1px solid #1e293b;
244
+ }
245
+ #${rootId} .t3f-activity-row-title {
246
+ display: flex; align-items: center; gap: 6px; min-width: 0;
247
+ font-size: 12px; color: #e2e8f0; font-weight: 600;
248
+ }
249
+ #${rootId} .t3f-activity-row-title-icon {
250
+ flex: 0 0 auto; width: 14px; height: 14px; color: #94a3b8;
251
+ display: inline-flex; align-items: center; justify-content: center;
252
+ }
253
+ #${rootId} .t3f-activity-row-title-icon svg {
254
+ width: 14px; height: 14px; display: block;
255
+ }
256
+ #${rootId} .t3f-activity-row-title-text {
257
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
258
+ }
259
+ #${rootId} .t3f-activity-row-actions {
260
+ display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
261
+ }
262
+ #${rootId} .t3f-activity-row-actions button.t3f-btn,
263
+ #${rootId} .t3f-activity-head button.t3f-btn,
264
+ #${rootId} .t3f-toast-actions button.t3f-btn {
265
+ padding: 5px 8px; font-size: 12px; border-radius: 8px; gap: 5px;
266
+ }
267
+ #${rootId} .t3f-activity-row-actions button.t3f-btn svg,
268
+ #${rootId} .t3f-activity-head button.t3f-btn svg,
269
+ #${rootId} .t3f-toast-actions button.t3f-btn svg {
270
+ width: 12px; height: 12px; flex: 0 0 auto; display: block;
271
+ }
272
+ ${mobileOnly}#${rootId} .t3f-backdrop {
273
+ position: fixed; inset: 0; z-index: 2147483001;
274
+ background: rgba(2, 6, 23, 0.55);
275
+ display: flex; align-items: center; justify-content: center;
276
+ padding: 16px;
277
+ }
278
+ #${rootId} .t3f-panel {
279
+ position: relative;
280
+ z-index: 1;
281
+ width: min(520px, 100%);
282
+ max-width: 100%;
283
+ min-width: 0;
284
+ max-height: min(90dvh, 100%);
285
+ overflow-x: hidden;
286
+ overflow-y: auto;
287
+ background: #0b1220; color: #e2e8f0;
288
+ border: 1px solid #334155; border-radius: 16px;
289
+ padding: 18px; display: grid; gap: 14px;
290
+ box-shadow: 0 24px 56px rgba(0,0,0,0.5);
291
+ }
292
+ /* Grid/flex min-content otherwise lets long selectors / screenshots blow the panel out. */
293
+ #${rootId} .t3f-panel > * { min-width: 0; max-width: 100%; }
294
+ #${rootId} .t3f-header {
295
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
296
+ }
297
+ #${rootId} .t3f-header-text { flex: 1 1 auto; min-width: 0; }
298
+ #${rootId} .t3f-title { font-size: 15px; font-weight: 650; color: #f8fafc; line-height: 1.3; }
299
+ #${rootId} .t3f-report-type {
300
+ display: grid; gap: 6px; min-width: 0;
301
+ }
302
+ #${rootId} .t3f-report-type-seg {
303
+ display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
304
+ gap: 4px; padding: 3px;
305
+ border-radius: 10px; background: #020617; border: 1px solid #1e293b;
306
+ }
307
+ #${rootId} .t3f-report-type-seg button {
308
+ display: inline-flex; align-items: center; justify-content: center; gap: 6px;
309
+ border: none; border-radius: 8px; background: transparent;
310
+ color: #94a3b8; padding: 7px 8px; cursor: pointer;
311
+ font: inherit; font-size: 12px; font-weight: 600; line-height: 1.2;
312
+ min-width: 0;
313
+ }
314
+ #${rootId} .t3f-report-type-seg button svg {
315
+ width: 14px; height: 14px; flex: 0 0 auto; display: block;
316
+ }
317
+ #${rootId} .t3f-report-type-emoji {
318
+ flex: 0 0 auto; font-size: 14px; line-height: 1;
319
+ display: inline-flex; align-items: center; justify-content: center;
320
+ }
321
+ #${rootId} .t3f-report-type-seg button span {
322
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
323
+ }
324
+ #${rootId} .t3f-report-type-seg button:hover { color: #e2e8f0; background: #1e293b; }
325
+ #${rootId} .t3f-report-type-seg button[aria-pressed="true"] {
326
+ background: #1e293b; color: #f8fafc;
327
+ }
328
+ #${rootId} .t3f-report-type-seg button:focus-visible { outline: 2px solid #38bdf8; outline-offset: 1px; }
329
+ #${rootId} .t3f-report-type-hint {
330
+ font-size: 11px; color: #64748b; line-height: 1.4;
331
+ }
332
+ #${rootId} .t3f-custom-plugins {
333
+ display: grid; gap: 6px; min-width: 0;
334
+ }
335
+ #${rootId} .t3f-custom-plugins[hidden] { display: none !important; }
336
+ #${rootId} .t3f-custom-plugin {
337
+ display: grid; gap: 2px; min-width: 0;
338
+ padding: 8px 10px; border-radius: 8px;
339
+ background: #0f172a; border: 1px solid #1e293b;
340
+ }
341
+ #${rootId} .t3f-custom-plugin-label {
342
+ font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
343
+ text-transform: uppercase; color: #64748b;
344
+ }
345
+ #${rootId} .t3f-custom-plugin-value {
346
+ font-size: 12px; color: #e2e8f0; line-height: 1.35;
347
+ overflow-wrap: anywhere;
348
+ }
349
+ #${rootId} button.t3f-icon-btn {
350
+ flex: 0 0 auto;
351
+ display: inline-flex; align-items: center; justify-content: center;
352
+ width: 28px; height: 28px; margin: 0; padding: 0;
353
+ border: none; border-radius: 8px; background: transparent;
354
+ color: #94a3b8; cursor: pointer; font: inherit; line-height: 1;
355
+ transition: background-color 0.12s ease, color 0.12s ease;
356
+ }
357
+ #${rootId} button.t3f-icon-btn:hover:not(:disabled) { background: #1e293b; color: #f8fafc; }
358
+ #${rootId} button.t3f-icon-btn:focus-visible { outline: 2px solid #38bdf8; outline-offset: 1px; }
359
+ #${rootId} button.t3f-icon-btn:disabled {
360
+ opacity: 0.4; cursor: not-allowed; color: #64748b;
361
+ }
362
+ #${rootId} button.t3f-icon-btn:disabled:hover {
363
+ background: transparent; color: #64748b;
364
+ }
365
+ #${rootId} button.t3f-icon-btn svg { width: 16px; height: 16px; display: block; }
366
+ #${rootId} .t3f-header > button.t3f-icon-btn { margin: -2px -2px 0 0; }
367
+ #${rootId} .t3f-section {
368
+ display: grid; gap: 8px; min-width: 0; max-width: 100%;
369
+ }
370
+ #${rootId} .t3f-section-head {
371
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
372
+ min-width: 0;
373
+ }
374
+ #${rootId} .t3f-section-label {
375
+ font-size: 11px; font-weight: 600; letter-spacing: 0.04em;
376
+ text-transform: uppercase; color: #64748b;
377
+ }
378
+ #${rootId} .t3f-section-tools {
379
+ display: flex; align-items: center; gap: 2px; flex: 0 0 auto;
380
+ }
381
+ #${rootId} .t3f-target {
382
+ display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;
383
+ min-width: 0; max-width: 100%;
384
+ font-size: 12px; color: #cbd5e1;
385
+ }
386
+ #${rootId} .t3f-target-chip {
387
+ flex: 1 1 0;
388
+ min-width: 0;
389
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
390
+ overflow-x: auto; overflow-y: hidden; white-space: nowrap;
391
+ scrollbar-width: none; -ms-overflow-style: none;
392
+ }
393
+ #${rootId} .t3f-target-chip::-webkit-scrollbar { display: none; width: 0; height: 0; }
394
+ #${rootId} .t3f-target-path,
395
+ #${rootId} .t3f-target-hierarchy,
396
+ #${rootId} .t3f-target-comp,
397
+ #${rootId} .t3f-target-label,
398
+ #${rootId} .t3f-target-file { display: inline; }
399
+ #${rootId} .t3f-target-comp-sep { color: #64748b; user-select: none; }
400
+ #${rootId} .t3f-target-role-primary .t3f-target-label,
401
+ #${rootId} .t3f-target-role-primary.t3f-target-label {
402
+ color: #e2e8f0; font-weight: 600;
403
+ }
404
+ #${rootId} .t3f-target-role-leaf .t3f-target-label,
405
+ #${rootId} .t3f-target-role-secondary .t3f-target-label,
406
+ #${rootId} .t3f-target-role-owner .t3f-target-label {
407
+ color: #94a3b8;
408
+ }
409
+ #${rootId} .t3f-target-seg {
410
+ display: inline; margin: 0; padding: 1px 3px; border: none; border-radius: 4px;
411
+ background: transparent; color: inherit; font: inherit; line-height: 1.35;
412
+ cursor: default; vertical-align: baseline;
413
+ }
414
+ #${rootId} .t3f-target-seg:hover,
415
+ #${rootId} .t3f-target-label:hover,
416
+ #${rootId} .t3f-target-file:hover {
417
+ background: rgba(56, 189, 248, 0.16); color: #7dd3fc;
418
+ }
419
+ #${rootId} .t3f-target-seg:focus-visible,
420
+ #${rootId} .t3f-target-label:focus-visible,
421
+ #${rootId} .t3f-target-file:focus-visible {
422
+ outline: 2px solid #38bdf8; outline-offset: 1px;
423
+ }
424
+ #${rootId} .t3f-target-label {
425
+ margin: 0; padding: 1px 3px; border: none; border-radius: 4px;
426
+ background: transparent; color: inherit; font: inherit; line-height: 1.35;
427
+ cursor: default; vertical-align: baseline;
428
+ }
429
+ #${rootId} .t3f-target-file {
430
+ margin: 0; padding: 1px 3px; border: none; border-radius: 4px;
431
+ background: transparent; color: #7dd3fc; font: inherit; line-height: 1.35;
432
+ cursor: pointer; vertical-align: baseline;
433
+ text-decoration: underline;
434
+ text-decoration-color: rgba(125, 211, 252, 0.45);
435
+ text-underline-offset: 2px;
436
+ }
437
+ #${rootId} a.t3f-target-file {
438
+ color: #7dd3fc;
439
+ }
440
+ #${rootId} .t3f-target-sep { color: #64748b; margin: 0 1px; user-select: none; }
441
+ #${rootId} .t3f-dom-highlight {
442
+ position: fixed; pointer-events: none; display: none;
443
+ border: 2px solid #38bdf8; background: rgba(56, 189, 248, 0.15);
444
+ border-radius: 2px; box-sizing: border-box; z-index: 0;
445
+ }
446
+ #${rootId} textarea {
447
+ width: 100%; min-height: 120px; resize: vertical;
448
+ border-radius: 12px; border: 1px solid #334155; background: #020617;
449
+ color: #f1f5f9; padding: 12px; font: inherit; line-height: 1.45;
450
+ }
451
+ #${rootId} textarea:focus {
452
+ outline: 2px solid rgba(56, 189, 248, 0.55); outline-offset: 0;
453
+ border-color: #38bdf8;
454
+ }
455
+ /* Standalone clipboard offer (empty gallery only): preview thumb + Insert / dismiss. */
456
+ #${rootId} .t3f-clipboard-offer {
457
+ display: flex; flex-wrap: nowrap; align-items: center; gap: 10px;
458
+ min-width: 0;
459
+ }
460
+ #${rootId} .t3f-clipboard-preview-btn {
461
+ flex: 0 0 auto;
462
+ padding: 0; margin: 0; border: none; background: transparent;
463
+ cursor: pointer; border-radius: 8px;
464
+ line-height: 0;
465
+ }
466
+ #${rootId} .t3f-clipboard-preview-btn:focus-visible {
467
+ outline: 2px solid rgba(56, 189, 248, 0.55); outline-offset: 2px;
468
+ }
469
+ #${rootId} .t3f-clipboard-preview {
470
+ width: 96px; height: auto; aspect-ratio: 4 / 3;
471
+ object-fit: contain; object-position: center;
472
+ border-radius: 8px; background: #020617;
473
+ display: block;
474
+ pointer-events: none;
475
+ }
476
+ #${rootId} .t3f-clipboard-offer-actions {
477
+ display: flex; flex-wrap: nowrap; gap: 6px; align-items: center;
478
+ flex: 1 1 auto; min-width: 0; justify-content: flex-end;
479
+ }
480
+ #${rootId} .t3f-clipboard-offer-actions .t3f-btn-tool {
481
+ min-width: 0; max-width: 100%;
482
+ }
483
+ #${rootId} .t3f-clipboard-offer-actions .t3f-btn-tool span {
484
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
485
+ display: block; min-width: 0;
486
+ }
487
+ #${rootId} .t3f-preview-wrap {
488
+ display: grid; gap: 8px; min-width: 0;
489
+ }
490
+ #${rootId} .t3f-gallery {
491
+ display: flex; flex-wrap: nowrap; gap: 8px; min-width: 0;
492
+ }
493
+ #${rootId} .t3f-thumb {
494
+ position: relative; flex: 1 1 0; min-width: 0; max-width: 96px;
495
+ aspect-ratio: 4 / 3; height: auto;
496
+ border-radius: 8px; border: 1px solid #334155; background: #020617; overflow: hidden;
497
+ }
498
+ #${rootId} .t3f-thumb.t3f-thumb-croppable { cursor: pointer; }
499
+ #${rootId} .t3f-thumb.t3f-thumb-croppable:hover,
500
+ #${rootId} .t3f-thumb.t3f-thumb-croppable:focus-within {
501
+ border-color: #38bdf8;
502
+ }
503
+ /* In-gallery clipboard suggestion: dashed thumb, same row as attached shots. */
504
+ #${rootId} button.t3f-thumb.t3f-thumb-clipboard {
505
+ display: block; padding: 0; margin: 0;
506
+ font: inherit; color: inherit;
507
+ border-style: dashed; border-color: #64748b;
508
+ cursor: pointer;
509
+ }
510
+ #${rootId} button.t3f-thumb.t3f-thumb-clipboard:hover,
511
+ #${rootId} button.t3f-thumb.t3f-thumb-clipboard:focus-visible {
512
+ border-color: #38bdf8;
513
+ outline: none;
514
+ }
515
+ #${rootId} button.t3f-thumb.t3f-thumb-clipboard:focus-visible {
516
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.45);
517
+ }
518
+ #${rootId} button.t3f-thumb.t3f-thumb-clipboard img {
519
+ opacity: 0.72;
520
+ }
521
+ #${rootId} .t3f-thumb img {
522
+ width: 100%; height: 100%; object-fit: contain; display: block;
523
+ }
524
+ #${rootId} .t3f-thumb-tools {
525
+ position: absolute; top: 2px; right: 2px; display: flex; gap: 2px;
526
+ opacity: 0; pointer-events: none;
527
+ transition: opacity 0.12s ease;
528
+ }
529
+ #${rootId} .t3f-thumb:hover .t3f-thumb-tools,
530
+ #${rootId} .t3f-thumb:focus-within .t3f-thumb-tools {
531
+ opacity: 1; pointer-events: auto;
532
+ }
533
+ @media (hover: none) {
534
+ #${rootId} .t3f-thumb-tools {
535
+ opacity: 1; pointer-events: auto;
536
+ }
537
+ }
538
+ #${rootId} .t3f-thumb-tools button {
539
+ width: 22px; height: 22px; border: 1px solid #475569; border-radius: 6px; padding: 0;
540
+ background: rgba(15, 23, 42, 0.92); color: #e2e8f0; cursor: pointer;
541
+ display: inline-flex; align-items: center; justify-content: center;
542
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
543
+ }
544
+ #${rootId} .t3f-thumb-tools button:hover {
545
+ background: #334155; border-color: #38bdf8; color: #f8fafc;
546
+ }
547
+ #${rootId} .t3f-thumb-tools button.t3f-thumb-remove:hover {
548
+ background: #450a0a; border-color: #f87171; color: #fecaca;
549
+ }
550
+ #${rootId} .t3f-thumb-tools button:focus-visible {
551
+ outline: 2px solid #38bdf8; outline-offset: 1px;
552
+ }
553
+ #${rootId} .t3f-thumb-tools button svg {
554
+ width: 12px; height: 12px; display: block;
555
+ }
556
+ #${rootId} .t3f-preview-frame {
557
+ min-width: 0; max-width: 100%;
558
+ border-radius: 10px; background: #020617;
559
+ overflow: hidden;
560
+ }
561
+ #${rootId} .t3f-preview {
562
+ display: block; width: 100%; max-width: 100%; height: auto; max-height: 160px;
563
+ object-fit: contain;
564
+ background: #020617;
565
+ }
566
+ #${rootId} .t3f-record-overlay,
567
+ #${rootId} .t3f-burst-overlay {
568
+ position: fixed; inset: 0; z-index: 2147483010;
569
+ background: rgba(2, 6, 23, 0.72);
570
+ display: flex; align-items: center; justify-content: center; padding: 16px;
571
+ }
572
+ #${rootId} .t3f-record-card,
573
+ #${rootId} .t3f-burst-card {
574
+ width: min(560px, 100%);
575
+ background: #0b1220; color: #e2e8f0;
576
+ border: 1px solid #334155; border-radius: 14px; padding: 16px;
577
+ display: grid; gap: 12px;
578
+ box-shadow: 0 20px 50px rgba(0,0,0,0.45);
579
+ }
580
+ #${rootId} .t3f-burst-hint,
581
+ #${rootId} .t3f-burst-meta { font-size: 12px; color: #94a3b8; }
582
+ #${rootId} .t3f-burst-strip {
583
+ display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
584
+ gap: 8px; max-height: 42vh; overflow: auto; min-width: 0;
585
+ }
586
+ #${rootId} .t3f-burst-item {
587
+ width: auto; min-width: 0; border-radius: 8px; border: 2px solid #334155; padding: 0;
588
+ background: #020617; cursor: pointer; overflow: hidden;
589
+ }
590
+ #${rootId} .t3f-burst-item[aria-pressed="true"] { border-color: #38bdf8; }
591
+ #${rootId} .t3f-burst-item img {
592
+ width: 100%; aspect-ratio: 4 / 3; height: auto; object-fit: contain; display: block;
593
+ }
594
+ #${rootId} .t3f-burst-actions {
595
+ display: flex; gap: 8px; justify-content: flex-end; flex-wrap: wrap;
596
+ }
597
+ #${rootId} .t3f-pick-details {
598
+ display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: center;
599
+ font-size: 12px; color: #94a3b8;
600
+ }
601
+ #${rootId} .t3f-pick-details label {
602
+ display: inline-flex; align-items: center; gap: 6px; cursor: pointer; color: #cbd5e1;
603
+ user-select: none;
604
+ }
605
+ #${rootId} .t3f-styles-seg {
606
+ display: inline-grid; grid-template-columns: repeat(3, minmax(0, 1fr));
607
+ gap: 2px; padding: 2px;
608
+ border-radius: 8px; background: #020617; border: 1px solid #1e293b;
609
+ }
610
+ #${rootId} .t3f-styles-seg button {
611
+ border: none; border-radius: 6px; background: transparent;
612
+ color: #94a3b8; padding: 4px 8px; cursor: pointer;
613
+ font: inherit; font-size: 11px; font-weight: 600; line-height: 1.2;
614
+ white-space: nowrap;
615
+ }
616
+ #${rootId} .t3f-styles-seg button:hover { color: #e2e8f0; background: #1e293b; }
617
+ #${rootId} .t3f-styles-seg button[aria-pressed="true"] {
618
+ background: #1e293b; color: #f8fafc;
619
+ }
620
+ #${rootId} .t3f-styles-seg button:focus-visible { outline: 2px solid #38bdf8; outline-offset: 1px; }
621
+ #${rootId} .t3f-attachments { display: grid; gap: 12px; min-width: 0; }
622
+ #${rootId} .t3f-attachments > [data-slot="attachments"],
623
+ #${rootId} .t3f-attachments > [data-slot="attachments"] > [data-ui-id] {
624
+ display: contents;
625
+ }
626
+ #${rootId} .t3f-attachments > [data-slot="attachments"] > [data-ui-id][hidden] {
627
+ display: none !important;
628
+ }
629
+ #${rootId} .t3f-actions-tools > [data-ui-id] { display: contents; }
630
+ #${rootId} .t3f-actions-tools > [data-ui-id][hidden] { display: none !important; }
631
+ #${rootId} [data-slot="beforeComment"]:empty { display: none; }
632
+ #${rootId} .t3f-status {
633
+ font-size: 12px; color: #94a3b8; min-height: 0; line-height: 1.4;
634
+ }
635
+ #${rootId} .t3f-status:empty { display: none; }
636
+ #${rootId} .t3f-status.ok { color: #4ade80; }
637
+ #${rootId} .t3f-status.err { color: #f87171; }
638
+ #${rootId} .t3f-status a.t3f-open {
639
+ color: #38bdf8; margin-left: 0.4em; text-decoration: underline;
640
+ }
641
+ #${rootId} .t3f-status a.t3f-open:hover { color: #7dd3fc; }
642
+ #${rootId} .t3f-actions {
643
+ display: flex; gap: 10px; justify-content: space-between; align-items: center;
644
+ flex-wrap: nowrap;
645
+ margin-top: 2px; padding-top: 14px; border-top: 1px solid #1e293b;
646
+ }
647
+ #${rootId} .t3f-actions-tools {
648
+ display: flex; gap: 6px; flex: 0 0 auto; flex-wrap: nowrap; align-items: center;
649
+ }
650
+ #${rootId} .t3f-actions-tools:empty { display: none; }
651
+ #${rootId} .t3f-actions-tools .t3f-icon-btn {
652
+ width: 32px; height: 32px;
653
+ border: 1px solid #334155; border-radius: 10px;
654
+ background: #1e293b; color: #f8fafc;
655
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
656
+ }
657
+ #${rootId} .t3f-actions-tools .t3f-icon-btn:hover:not(:disabled) {
658
+ background: #334155; border-color: #38bdf8; color: #f8fafc;
659
+ }
660
+ #${rootId} .t3f-actions-tools .t3f-icon-btn:disabled {
661
+ opacity: 0.4; cursor: not-allowed; color: #64748b; border-color: #1e293b;
662
+ }
663
+ #${rootId} .t3f-actions-tools .t3f-icon-btn:disabled:hover {
664
+ background: #1e293b; color: #64748b; border-color: #1e293b;
665
+ }
666
+ #${rootId} .t3f-actions-send {
667
+ display: flex; gap: 8px; margin-left: auto; align-items: center;
668
+ flex: 1 1 auto; flex-wrap: nowrap; justify-content: flex-end; min-width: 0;
669
+ }
670
+ #${rootId} .t3f-actions-send .t3f-continue-group {
671
+ position: relative; display: inline-flex; align-items: stretch; gap: 0;
672
+ max-width: min(260px, 55vw); min-width: 0;
673
+ }
674
+ #${rootId} .t3f-actions-send .t3f-continue-group[hidden] { display: none !important; }
675
+ #${rootId} .t3f-actions-send .t3f-send-continue {
676
+ max-width: none; flex: 1 1 auto; min-width: 0; gap: 6px;
677
+ border-top-right-radius: 0; border-bottom-right-radius: 0;
678
+ }
679
+ #${rootId} .t3f-actions-send .t3f-continue-group:not(:has([data-act="thread-pick"]:not([hidden]))) .t3f-send-continue {
680
+ border-radius: 10px;
681
+ }
682
+ #${rootId} .t3f-actions-send .t3f-send-continue svg {
683
+ width: 14px; height: 14px; flex: 0 0 auto; display: block;
684
+ }
685
+ #${rootId} .t3f-actions-send .t3f-send-continue-type {
686
+ flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
687
+ color: #e2e8f0;
688
+ }
689
+ #${rootId} .t3f-actions-send .t3f-send-continue-type[hidden] { display: none !important; }
690
+ #${rootId} .t3f-actions-send .t3f-send-continue-type svg {
691
+ width: 14px; height: 14px; display: block;
692
+ }
693
+ #${rootId} .t3f-actions-send .t3f-send-continue span[data-thread-continue-label] {
694
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
695
+ display: block; min-width: 0;
696
+ }
697
+ #${rootId} .t3f-actions-send .t3f-thread-pick {
698
+ flex: 0 0 auto; width: 30px; min-width: 30px; padding: 8px 0;
699
+ border-top-left-radius: 0; border-bottom-left-radius: 0;
700
+ border-left-color: #475569;
701
+ }
702
+ #${rootId} .t3f-actions-send .t3f-thread-pick.primary {
703
+ border-left-color: rgba(255, 255, 255, 0.28);
704
+ }
705
+ #${rootId} .t3f-actions-send .t3f-thread-pick[hidden] { display: none !important; }
706
+ #${rootId} .t3f-actions-send .t3f-thread-pick svg {
707
+ width: 14px; height: 14px; display: block;
708
+ }
709
+ #${rootId} .t3f-thread-picker {
710
+ /* Fixed so panel overflow cannot clip the menu (short viewports / tall lists). */
711
+ position: fixed; z-index: 2147483005; width: min(360px, 86vw);
712
+ display: grid; gap: 8px; padding: 10px;
713
+ border-radius: 12px; border: 1px solid #334155;
714
+ background: #0f172a; box-shadow: 0 12px 32px rgba(2, 6, 23, 0.55);
715
+ }
716
+ #${rootId} .t3f-thread-picker[hidden] { display: none !important; }
717
+ #${rootId} .t3f-thread-picker input[data-thread-search] {
718
+ width: 100%; margin: 0; padding: 8px 10px;
719
+ border-radius: 8px; border: 1px solid #334155; background: #020617;
720
+ color: #f8fafc; font: inherit; font-size: 13px;
721
+ }
722
+ #${rootId} .t3f-thread-picker input[data-thread-search]:focus {
723
+ outline: 2px solid #38bdf8; outline-offset: 0; border-color: #38bdf8;
724
+ }
725
+ #${rootId} .t3f-thread-picker-list {
726
+ display: grid; gap: 2px; overflow: auto;
727
+ /* Fallback before layoutThreadPicker runs; inline max-height wins when open. */
728
+ max-height: min(320px, 42vh);
729
+ }
730
+ #${rootId} .t3f-thread-picker-option {
731
+ display: flex; align-items: center; gap: 8px; width: 100%;
732
+ margin: 0; padding: 7px 8px; border: none; border-radius: 8px;
733
+ background: transparent; color: #e2e8f0; font: inherit; font-size: 12.5px;
734
+ text-align: left; cursor: pointer; min-width: 0;
735
+ }
736
+ #${rootId} .t3f-thread-picker-option:hover,
737
+ #${rootId} .t3f-thread-picker-option[aria-selected="true"],
738
+ #${rootId} .t3f-thread-picker-option.is-active {
739
+ background: #1e293b;
740
+ }
741
+ #${rootId} .t3f-thread-picker-option.is-active {
742
+ outline: 1px solid rgba(56, 189, 248, 0.45);
743
+ outline-offset: -1px;
744
+ }
745
+ #${rootId} .t3f-thread-picker-option:focus-visible {
746
+ outline: 2px solid #38bdf8; outline-offset: -1px;
747
+ }
748
+ #${rootId} .t3f-thread-picker-option-type {
749
+ flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
750
+ width: 18px; height: 18px; color: #94a3b8;
751
+ }
752
+ #${rootId} .t3f-thread-picker-option-type svg {
753
+ width: 14px; height: 14px; display: block;
754
+ }
755
+ #${rootId} .t3f-thread-picker-option-type .t3f-report-type-emoji {
756
+ font-size: 13px; line-height: 1;
757
+ }
758
+ #${rootId} .t3f-thread-picker-option-title {
759
+ flex: 1 1 auto; min-width: 0;
760
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
761
+ color: #e2e8f0;
762
+ }
763
+ #${rootId} .t3f-thread-picker-option-state {
764
+ flex: 0 0 auto; font-size: 10px; font-weight: 600; letter-spacing: 0.03em;
765
+ text-transform: uppercase; padding: 2px 6px; border-radius: 999px;
766
+ line-height: 1.2;
767
+ }
768
+ #${rootId} .t3f-thread-picker-option-state.is-working {
769
+ color: #7dd3fc; background: rgba(14, 165, 233, 0.16);
770
+ }
771
+ #${rootId} .t3f-thread-picker-option-state.is-settled {
772
+ color: #94a3b8; background: rgba(148, 163, 184, 0.12);
773
+ }
774
+ #${rootId} .t3f-thread-picker-empty {
775
+ font-size: 12px; color: #94a3b8; padding: 6px 4px;
776
+ }
777
+ #${rootId} .t3f-thread-picker-empty[hidden] { display: none !important; }
778
+ #${rootId} .t3f-thread-picker-meta {
779
+ font-size: 11px; color: #64748b; padding: 0 2px;
780
+ }
781
+ #${rootId} .t3f-thread-picker-meta[hidden] { display: none !important; }
782
+ #${rootId} button.t3f-btn {
783
+ border-radius: 10px; border: 1px solid #334155; background: #1e293b;
784
+ color: #f8fafc; padding: 8px 12px; cursor: pointer; font: inherit; font-size: 13px;
785
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
786
+ min-width: 0;
787
+ transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
788
+ }
789
+ #${rootId} button.t3f-btn:hover:not(:disabled) {
790
+ background: #334155; border-color: #38bdf8;
791
+ }
792
+ #${rootId} button.t3f-btn:focus-visible {
793
+ outline: 2px solid #38bdf8; outline-offset: 1px;
794
+ }
795
+ #${rootId} button.t3f-btn.primary { background: #0284c7; border-color: #0284c7; }
796
+ #${rootId} button.t3f-btn.primary:hover:not(:disabled) {
797
+ background: #0369a1; border-color: #7dd3fc;
798
+ }
799
+ #${rootId} button.t3f-btn:disabled { opacity: 0.55; cursor: not-allowed; }
800
+ #${rootId} button.t3f-btn-tool {
801
+ display: inline-flex; align-items: center; gap: 8px;
802
+ }
803
+ #${rootId} button.t3f-btn-tool svg { width: 15px; height: 15px; flex: 0 0 auto; display: block; }
804
+ #${rootId} .t3f-crop {
805
+ position: fixed; inset: 0; z-index: 2147483002;
806
+ background: rgba(2, 6, 23, 0.92);
807
+ display: grid; grid-template-rows: 1fr auto; gap: 0; padding: 12px 12px 0;
808
+ }
809
+ #${rootId} .t3f-crop-stage {
810
+ position: relative; min-height: 0; width: 100%; height: 100%;
811
+ cursor: crosshair; user-select: none; touch-action: none;
812
+ background: transparent; border-radius: 0; border: none; overflow: hidden;
813
+ }
814
+ #${rootId} .t3f-crop-img {
815
+ position: absolute; inset: 0; width: 100%; height: 100%;
816
+ object-fit: contain; pointer-events: none;
817
+ }
818
+ #${rootId} .t3f-crop-marquee {
819
+ position: absolute; border: 2px solid #38bdf8;
820
+ box-shadow: 0 0 0 9999px rgba(2, 6, 23, 0.55);
821
+ pointer-events: none;
822
+ }
823
+ #${rootId} .t3f-crop-bar {
824
+ display: flex; flex-wrap: wrap; gap: 12px 16px; align-items: center; justify-content: space-between;
825
+ margin-top: 4px; padding: 14px 4px 14px;
826
+ border-top: 1px solid #1e293b;
827
+ }
828
+ #${rootId} .t3f-crop-hint {
829
+ font-size: 12px; color: #94a3b8; line-height: 1.4;
830
+ flex: 1 1 200px; min-width: 0;
831
+ }
832
+ #${rootId} .t3f-crop-actions {
833
+ display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
834
+ margin-left: auto;
835
+ }
836
+ #${rootId} .t3f-crop-actions .t3f-icon-btn {
837
+ background: rgba(15, 23, 42, 0.55);
838
+ }
839
+ #${rootId} .t3f-crop-actions .t3f-icon-btn:hover { background: #1e293b; color: #f8fafc; }
840
+ #${rootId} .t3f-crop-actions .t3f-crop-discard:hover { color: #fca5a5; }
841
+ `;
842
+ }
843
+ function escapeHtml(value) {
844
+ return value
845
+ .replaceAll("&", "&")
846
+ .replaceAll("<", "&lt;")
847
+ .replaceAll(">", "&gt;")
848
+ .replaceAll('"', "&quot;");
849
+ }
850
+ const CLOSE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>`;
851
+ const BUG_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg>`;
852
+ const MESSAGE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>`;
853
+ const LIGHTBULB_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>`;
854
+ const PLAY_ICON_SVG = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.14v13.72a1 1 0 0 0 1.5.86l11-6.86a1 1 0 0 0 0-1.72l-11-6.86A1 1 0 0 0 8 5.14z"/></svg>`;
855
+ const CHEVRON_DOWN_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`;
856
+ const PLUS_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="M12 5v14"/></svg>`;
857
+ const EXTERNAL_LINK_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>`;
858
+ const CHECK_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
859
+ const TRASH_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>`;
860
+ /** Icon + text label for activity / toast controls (SVG is trusted constant markup). */
861
+ function setIconLabel(el, iconSvg, label) {
862
+ el.replaceChildren();
863
+ el.insertAdjacentHTML("afterbegin", iconSvg);
864
+ const span = document.createElement("span");
865
+ span.textContent = label;
866
+ el.append(span);
867
+ }
868
+ /** Open-in-T3 anchor: pair-then-thread on plain click; bare href for modified clicks. */
869
+ function wireOpenInT3Link(link, options) {
870
+ link.className = "t3f-open";
871
+ link.href = options.openUrl;
872
+ link.target = "_blank";
873
+ link.rel = "noopener noreferrer";
874
+ link.dataset.threadId = options.threadId;
875
+ setIconLabel(link, EXTERNAL_LINK_ICON_SVG, options.label);
876
+ link.addEventListener("click", (event) => {
877
+ if (shouldBypassSeamlessOpen(event))
878
+ return;
879
+ event.preventDefault();
880
+ void openInT3Seamless({
881
+ threadId: options.threadId,
882
+ fallbackUrl: options.openUrl,
883
+ endpoint: options.endpoint,
884
+ });
885
+ });
886
+ }
887
+ function reportTypeEmojiFor(type, copy) {
888
+ if (type === "feedback")
889
+ return copy.reportTypeFeedbackEmoji;
890
+ if (type === "suggestion")
891
+ return copy.reportTypeSuggestionEmoji;
892
+ return copy.reportTypeBugEmoji;
893
+ }
894
+ function reportTypeIconSvg(type) {
895
+ if (type === "feedback")
896
+ return MESSAGE_ICON_SVG;
897
+ if (type === "suggestion")
898
+ return LIGHTBULB_ICON_SVG;
899
+ return BUG_ICON_SVG;
900
+ }
901
+ /** SVG icon or emoji span for a report type (aria-hidden). */
902
+ function reportTypeGlyphHtml(type, glyph, copy) {
903
+ if (glyph !== "icon") {
904
+ const emoji = typeof glyph === "object" ? glyph[type] : reportTypeEmojiFor(type, copy);
905
+ return `<span class="t3f-report-type-emoji" aria-hidden="true">${escapeHtml(emoji)}</span>`;
906
+ }
907
+ return reportTypeIconSvg(type);
908
+ }
909
+ function closeButtonHtml(opts) {
910
+ const cls = opts.className ? `t3f-icon-btn ${opts.className}` : "t3f-icon-btn";
911
+ const title = opts.titleAttr ?? opts.label;
912
+ return `<button type="button" class="${cls}" data-act="${escapeHtml(opts.act)}" aria-label="${escapeHtml(opts.label)}" title="${escapeHtml(title)}">${CLOSE_ICON_SVG}</button>`;
913
+ }
914
+ export { buildOpenInEditorUrl, buildTargetChipHtml, componentHierarchy, formatComponentLocation, formatElementChip, formatSelectorBreadcrumb, openSourceInEditor, selectorAtAncestorDepth, splitCssSelectorParts, } from "./ui/elementTargetChip.js";
915
+ /**
916
+ * Track Vite HMR generation for the version plugin.
917
+ * Starts at 0 when hot is available; null otherwise (production / no HMR).
918
+ */
919
+ function createHmrGenerationTracker() {
920
+ const hot = import.meta.hot;
921
+ if (!hot || typeof hot.on !== "function") {
922
+ return { getHmr: () => null, dispose: () => undefined };
923
+ }
924
+ let count = 0;
925
+ const onAfterUpdate = () => {
926
+ count += 1;
927
+ };
928
+ hot.on("vite:afterUpdate", onAfterUpdate);
929
+ return {
930
+ getHmr: () => count,
931
+ dispose: () => {
932
+ if (typeof hot.off === "function") {
933
+ hot.off("vite:afterUpdate", onAfterUpdate);
934
+ }
935
+ },
936
+ };
937
+ }
938
+ /** Mount the floating feedback control and keyboard shortcut; returns a handle. */
939
+ export function mountT3Feedback(options = {}) {
940
+ if (typeof document === "undefined")
941
+ return noopHandle();
942
+ const rootId = options.rootId ?? "t3-feedback-root";
943
+ const shortcut = resolveShortcut(options.shortcut, options.shortcutKey);
944
+ const pickConfig = resolvePickConfig(options.pick);
945
+ const screenshotConfig = resolveScreenshotConfig(options.screenshot, pickConfig.cropPaddingPx);
946
+ const recordConfig = resolveRecordConfig(options.record);
947
+ const endpoint = options.endpoint ?? "/__t3-feedback";
948
+ const activePluginSet = resolveActivePlugins(options.activePlugins, options.includeConsoleLogs);
949
+ const pageContextActive = activePluginSet.has("pageContext");
950
+ const consoleLogsActive = activePluginSet.has("consoleLogs");
951
+ const screenshotActive = activePluginSet.has("screenshot");
952
+ const elementTargetActive = activePluginSet.has("elementTarget");
953
+ const consoleLogsMaxEntries = (() => {
954
+ const raw = options.pluginOptions?.consoleLogs?.maxEntries;
955
+ if (raw === undefined || !Number.isFinite(raw))
956
+ return 200;
957
+ return Math.max(1, Math.floor(raw));
958
+ })();
959
+ const appVersion = options.version?.trim() || undefined;
960
+ const threadReuse = options.threadReuse ?? "ask";
961
+ const reportTypeGlyph = resolveReportTypeGlyph(options.reportTypeGlyph);
962
+ const fabConfig = resolveFabConfig(options.fab);
963
+ const openChord = formatShortcutLabel(shortcut);
964
+ const pickChord = elementTargetActive && pickConfig.shortcut !== null
965
+ ? formatShortcutLabel(pickConfig.shortcut)
966
+ : null;
967
+ const captureChord = screenshotActive && screenshotConfig.shortcut !== null
968
+ ? formatShortcutLabel(screenshotConfig.shortcut)
969
+ : null;
970
+ const recordChord = screenshotActive && recordConfig.shortcut !== null
971
+ ? formatShortcutLabel(recordConfig.shortcut)
972
+ : null;
973
+ const copy = {
974
+ ...DEFAULT_COPY,
975
+ ...options.copy,
976
+ fabTitle: options.copy?.fabTitle ??
977
+ [
978
+ `Send feedback to T3 (${openChord}`,
979
+ pickChord ? `; pick ${pickChord}` : "",
980
+ captureChord ? `; capture ${captureChord}` : "",
981
+ recordChord ? `; record ${recordChord}` : "",
982
+ ")",
983
+ ].join(""),
984
+ };
985
+ const captureTitle = captureChord ? `${copy.captureLabel} (${captureChord})` : copy.captureLabel;
986
+ const recordTitle = recordChord ? `${copy.recordLabel} (${recordChord})` : copy.recordLabel;
987
+ /** Set just before send in `"ask"` mode when Continue is selected. */
988
+ const askThreadIdRef = { current: undefined };
989
+ const transport = options.transport ??
990
+ createT3FeedbackHttpTransport({
991
+ url: endpoint,
992
+ ...(threadReuse === "never"
993
+ ? {}
994
+ : {
995
+ getThreadId: () => {
996
+ if (threadReuse === "session-route") {
997
+ return readSessionThreadRecord(window.location.pathname)?.threadId;
998
+ }
999
+ return askThreadIdRef.current;
1000
+ },
1001
+ }),
1002
+ });
1003
+ const userPlugins = options.plugins ?? [];
1004
+ const selectedReportTypeRef = {
1005
+ current: readReportType(),
1006
+ };
1007
+ const reportTypeListeners = new Set();
1008
+ const busy = createBusyRegistry();
1009
+ const afterPick = createAfterPickRegistry();
1010
+ const actions = createActionRegistry();
1011
+ const draftClears = createDraftClearRegistry();
1012
+ const draftExtras = createDraftExtrasStore();
1013
+ {
1014
+ const initialDraft = readFeedbackDraft();
1015
+ if (initialDraft) {
1016
+ draftExtras.replace(migrateDraftExtras(initialDraft));
1017
+ }
1018
+ }
1019
+ const root = document.createElement("div");
1020
+ root.id = rootId;
1021
+ const hostMedia = createHostMedia(root, {
1022
+ captureScreenshot,
1023
+ captureFrameBurst,
1024
+ });
1025
+ /** Filled after openPanel exists — plugins call through this ref. */
1026
+ const hostRef = { current: null };
1027
+ const getHost = () => {
1028
+ if (!hostRef.current) {
1029
+ throw new Error("t3-feedback host not ready");
1030
+ }
1031
+ return hostRef.current;
1032
+ };
1033
+ const resolveSource = createSourceResolver({
1034
+ framework: pickConfig.sourceFramework,
1035
+ sourceEndpoint: endpoint,
1036
+ ...(options.pick && "resolveSource" in options.pick && options.pick.resolveSource
1037
+ ? { resolveSource: options.pick.resolveSource }
1038
+ : {}),
1039
+ });
1040
+ const commentPanel = createCommentPanelPlugin({ copy });
1041
+ const reportTypeHintCaps = {
1042
+ pageContext: pageContextActive,
1043
+ consoleLogs: consoleLogsActive,
1044
+ attachments: screenshotActive,
1045
+ };
1046
+ const reportTypeHintFor = (type) => {
1047
+ if (type === "feedback" && copy.reportTypeFeedbackHint) {
1048
+ return copy.reportTypeFeedbackHint;
1049
+ }
1050
+ if (type === "suggestion" && copy.reportTypeSuggestionHint) {
1051
+ return copy.reportTypeSuggestionHint;
1052
+ }
1053
+ if (type === "bug" && copy.reportTypeBugHint)
1054
+ return copy.reportTypeBugHint;
1055
+ return buildReportTypeHint(type, reportTypeHintCaps);
1056
+ };
1057
+ const reportTypePanel = createReportTypePanelPlugin({
1058
+ copy,
1059
+ glyph: reportTypeGlyph,
1060
+ iconSvg: reportTypeIconSvg,
1061
+ hintFor: reportTypeHintFor,
1062
+ getReportType: () => selectedReportTypeRef.current,
1063
+ });
1064
+ const elementPanel = elementTargetActive
1065
+ ? createElementTargetPanelPlugin({
1066
+ copy,
1067
+ pickConfig,
1068
+ pickShortcut: pickConfig.shortcut,
1069
+ pickChord,
1070
+ root,
1071
+ afterPickMode: screenshotActive ? pickConfig.afterPick : "none",
1072
+ ...(resolveSource ? { resolveSource } : {}),
1073
+ getHost,
1074
+ })
1075
+ : null;
1076
+ const screenshotPanel = screenshotActive
1077
+ ? createScreenshotPanelPlugin({
1078
+ copy,
1079
+ captureShortcut: screenshotConfig.shortcut,
1080
+ recordShortcut: recordConfig.shortcut,
1081
+ captureTitle,
1082
+ recordTitle,
1083
+ screenshotConfig,
1084
+ afterPick: pickConfig.afterPick,
1085
+ afterPickCropPaddingPx: pickConfig.cropPaddingPx,
1086
+ getHost,
1087
+ })
1088
+ : null;
1089
+ const hmrTracker = appVersion ? createHmrGenerationTracker() : null;
1090
+ /** Panel plugins (same API as customs): reportType, comment, capture UIs, user plugins. */
1091
+ const panelPlugins = [
1092
+ reportTypePanel,
1093
+ commentPanel,
1094
+ ...(elementPanel ? [elementPanel.plugin] : []),
1095
+ ...(screenshotPanel ? [screenshotPanel.plugin] : []),
1096
+ ...asPluginsWithUi(userPlugins),
1097
+ ];
1098
+ const getSelectedReportType = () => selectedReportTypeRef.current;
1099
+ const sessionPlugins = [
1100
+ ...createCollectOnlyBuiltinPlugins({
1101
+ pageContext: pageContextActive,
1102
+ consoleLogs: consoleLogsActive,
1103
+ consoleLogsMaxEntries,
1104
+ getReportType: getSelectedReportType,
1105
+ ...(appVersion ? { version: appVersion, getHmr: () => hmrTracker?.getHmr() ?? null } : {}),
1106
+ }),
1107
+ reportTypePanel,
1108
+ ...(elementPanel ? [elementPanel.elementPlugin] : []),
1109
+ // screenshot + comment collect via panel plugin instances (no submit extras):
1110
+ ...(screenshotPanel ? [screenshotPanel.plugin] : []),
1111
+ commentPanel,
1112
+ ...userPlugins,
1113
+ ].map((plugin) => gatePluginByReportType(plugin, getSelectedReportType));
1114
+ const session = initFeedbackCapture({ plugins: sessionPlugins });
1115
+ if (document.getElementById(rootId)) {
1116
+ hmrTracker?.dispose();
1117
+ session.dispose();
1118
+ return noopHandle();
1119
+ }
1120
+ const style = document.createElement("style");
1121
+ style.textContent = css(rootId, fabConfig);
1122
+ root.appendChild(style);
1123
+ const fab = document.createElement("button");
1124
+ fab.type = "button";
1125
+ fab.className = "t3f-fab";
1126
+ fab.title = copy.fabTitle;
1127
+ fab.setAttribute("aria-label", copy.fabAriaLabel);
1128
+ const fabGlyph = document.createElement("span");
1129
+ fabGlyph.className = "t3f-fab-glyph";
1130
+ fabGlyph.textContent = "✎";
1131
+ fabGlyph.setAttribute("aria-hidden", "true");
1132
+ const fabBadge = document.createElement("span");
1133
+ fabBadge.className = "t3f-fab-badge";
1134
+ fabBadge.setAttribute("aria-hidden", "true");
1135
+ fab.append(fabGlyph, fabBadge);
1136
+ root.appendChild(fab);
1137
+ const toastEl = document.createElement("div");
1138
+ toastEl.className = "t3f-toast";
1139
+ toastEl.hidden = true;
1140
+ toastEl.setAttribute("role", "status");
1141
+ root.appendChild(toastEl);
1142
+ const activityEl = document.createElement("div");
1143
+ activityEl.className = "t3f-activity";
1144
+ activityEl.hidden = true;
1145
+ activityEl.setAttribute("role", "dialog");
1146
+ activityEl.setAttribute("aria-label", copy.activityTitle);
1147
+ root.appendChild(activityEl);
1148
+ document.body.appendChild(root);
1149
+ let open = false;
1150
+ let activityOpen = false;
1151
+ let backdrop = null;
1152
+ let detachThreadPicker = null;
1153
+ let detachSlotContributions = null;
1154
+ let toastTimer = null;
1155
+ let pollTimer = null;
1156
+ let probing = false;
1157
+ const persistDraft = (panelOpen) => {
1158
+ const extras = draftExtras.snapshot();
1159
+ const hasMeaningfulExtra = Object.entries(extras).some(([key, value]) => {
1160
+ if (value === undefined || value === "")
1161
+ return false;
1162
+ if (key === "comment" && typeof value === "string")
1163
+ return value.trim().length > 0;
1164
+ if (key === "screenshot") {
1165
+ const gallery = value?.gallery;
1166
+ return Array.isArray(gallery) && gallery.length > 0;
1167
+ }
1168
+ return true;
1169
+ });
1170
+ if (!hasMeaningfulExtra && !panelOpen) {
1171
+ clearFeedbackDraft();
1172
+ return;
1173
+ }
1174
+ writeFeedbackDraft({
1175
+ v: 1,
1176
+ comment: "",
1177
+ gallery: [],
1178
+ extras,
1179
+ panelOpen,
1180
+ });
1181
+ };
1182
+ const clearDraftState = () => {
1183
+ draftClears.runDraftClears();
1184
+ draftExtras.clear();
1185
+ clearFeedbackDraft();
1186
+ };
1187
+ /** Assigned when openPanel is declared; used by the shell host openPanel(). */
1188
+ let openPanelImpl = async () => undefined;
1189
+ /** Panel-local send UI sync (wired when the panel is open). */
1190
+ let syncSendActionsUiRef = () => undefined;
1191
+ const buildShellHost = (overrides = {}) => ({
1192
+ root,
1193
+ getReportType: () => selectedReportTypeRef.current,
1194
+ setReportType: (type) => {
1195
+ selectedReportTypeRef.current = type;
1196
+ writeReportType(type);
1197
+ for (const listener of reportTypeListeners)
1198
+ listener(type);
1199
+ syncSendActionsUiRef();
1200
+ },
1201
+ subscribeReportType: (listener) => {
1202
+ reportTypeListeners.add(listener);
1203
+ return () => {
1204
+ reportTypeListeners.delete(listener);
1205
+ };
1206
+ },
1207
+ isPanelOpen: () => open,
1208
+ openPanel: () => {
1209
+ void openPanelImpl();
1210
+ },
1211
+ setStatus: () => undefined,
1212
+ persistDraft: (panelOpen = true) => persistDraft(panelOpen),
1213
+ focusComment: () => commentPanel.getApi().focus(),
1214
+ getDraftExtra: (id) => draftExtras.get(id),
1215
+ setDraftExtra: (id, value) => draftExtras.set(id, value),
1216
+ registerBusy: busy.registerBusy,
1217
+ isBusy: busy.isBusy,
1218
+ registerAfterPick: afterPick.registerAfterPick,
1219
+ notifyPicked: afterPick.notifyPicked,
1220
+ registerAction: actions.registerAction,
1221
+ runAction: actions.runAction,
1222
+ registerDraftClear: draftClears.registerDraftClear,
1223
+ media: hostMedia,
1224
+ ...overrides,
1225
+ });
1226
+ hostRef.current = buildShellHost();
1227
+ commentPanel.installHostHooks(hostRef.current);
1228
+ elementPanel?.installHostHooks(hostRef.current);
1229
+ screenshotPanel?.installHostHooks(hostRef.current);
1230
+ const setStatus = (el, text, kind = "") => {
1231
+ el.replaceChildren();
1232
+ el.append(document.createTextNode(text));
1233
+ el.className = `t3f-status${kind ? ` ${kind}` : ""}`;
1234
+ };
1235
+ const hideToast = () => {
1236
+ if (toastTimer) {
1237
+ clearTimeout(toastTimer);
1238
+ toastTimer = null;
1239
+ }
1240
+ toastEl.hidden = true;
1241
+ toastEl.replaceChildren();
1242
+ };
1243
+ const showSentToast = (result) => {
1244
+ hideToast();
1245
+ const title = document.createElement("div");
1246
+ title.className = "t3f-toast-title";
1247
+ title.textContent = copy.toastSentLabel;
1248
+ const meta = document.createElement("div");
1249
+ meta.className = "t3f-toast-meta";
1250
+ meta.textContent =
1251
+ result.threadTitle.trim().length > 0 ? result.threadTitle.trim() : copy.sentStatus;
1252
+ const actions = document.createElement("div");
1253
+ actions.className = "t3f-toast-actions";
1254
+ if (result.openUrl) {
1255
+ const link = document.createElement("a");
1256
+ wireOpenInT3Link(link, {
1257
+ openUrl: result.openUrl,
1258
+ threadId: result.threadId,
1259
+ endpoint,
1260
+ label: copy.openInT3Label,
1261
+ });
1262
+ actions.append(link);
1263
+ }
1264
+ const dismiss = document.createElement("button");
1265
+ dismiss.type = "button";
1266
+ dismiss.className = "t3f-btn";
1267
+ setIconLabel(dismiss, CLOSE_ICON_SVG, copy.activityDismissLabel);
1268
+ dismiss.addEventListener("click", hideToast);
1269
+ actions.append(dismiss);
1270
+ toastEl.replaceChildren(title, meta, actions);
1271
+ toastEl.hidden = false;
1272
+ toastTimer = setTimeout(hideToast, 2500);
1273
+ };
1274
+ const closeActivity = () => {
1275
+ activityOpen = false;
1276
+ activityEl.hidden = true;
1277
+ activityEl.replaceChildren();
1278
+ };
1279
+ const syncFabVisibility = (activeCount) => {
1280
+ const hasActivity = activeCount > 0;
1281
+ root.classList.toggle("t3f-has-activity", hasActivity);
1282
+ fabBadge.textContent = hasActivity ? String(activeCount) : "";
1283
+ if (hasActivity) {
1284
+ fab.hidden = false;
1285
+ fab.title = copy.activityFabTitle;
1286
+ fab.setAttribute("aria-label", `${copy.activityFabAriaLabel}: ${activeCount}`);
1287
+ }
1288
+ else {
1289
+ fab.title = copy.fabTitle;
1290
+ fab.setAttribute("aria-label", copy.fabAriaLabel);
1291
+ fab.hidden = fabConfig.visibility === "never";
1292
+ closeActivity();
1293
+ }
1294
+ };
1295
+ const stopPolling = () => {
1296
+ if (pollTimer) {
1297
+ clearInterval(pollTimer);
1298
+ pollTimer = null;
1299
+ }
1300
+ };
1301
+ const probeActiveThreads = async () => {
1302
+ if (probing)
1303
+ return;
1304
+ const active = listActiveThreads();
1305
+ if (active.length === 0) {
1306
+ stopPolling();
1307
+ syncFabVisibility(0);
1308
+ return;
1309
+ }
1310
+ probing = true;
1311
+ try {
1312
+ const ids = active.map((t) => t.threadId).join(",");
1313
+ const res = await fetch(`${endpoint}/threads?ids=${encodeURIComponent(ids)}`, {
1314
+ headers: { Accept: "application/json" },
1315
+ });
1316
+ if (!res.ok)
1317
+ return;
1318
+ const body = (await res.json());
1319
+ const settled = (body.threads ?? [])
1320
+ .filter((t) => t.state === "settled")
1321
+ .map((t) => t.threadId);
1322
+ if (settled.length > 0) {
1323
+ const next = removeActiveThreads(settled);
1324
+ syncFabVisibility(next.length);
1325
+ if (activityOpen)
1326
+ renderActivity(next);
1327
+ if (next.length === 0)
1328
+ stopPolling();
1329
+ }
1330
+ }
1331
+ catch {
1332
+ // Probe is best-effort; TTL still settles.
1333
+ }
1334
+ finally {
1335
+ probing = false;
1336
+ }
1337
+ };
1338
+ const ensurePolling = () => {
1339
+ if (pollTimer)
1340
+ return;
1341
+ if (listActiveThreads().length === 0)
1342
+ return;
1343
+ pollTimer = setInterval(() => {
1344
+ void probeActiveThreads();
1345
+ }, ACTIVE_THREAD_POLL_MS);
1346
+ };
1347
+ /** Dispatch `thread.settle` via the Vite bridge; returns ids that settled (or were already gone). */
1348
+ const settleActiveThreads = async (threadIds) => {
1349
+ const ids = [...new Set(threadIds.map((id) => id.trim()).filter(Boolean))];
1350
+ if (ids.length === 0)
1351
+ return [];
1352
+ const res = await fetch(`${endpoint}/settle`, {
1353
+ method: "POST",
1354
+ headers: {
1355
+ "Content-Type": "application/json",
1356
+ Accept: "application/json",
1357
+ },
1358
+ body: JSON.stringify({ threadIds: ids }),
1359
+ });
1360
+ let parsed;
1361
+ try {
1362
+ parsed = await res.json();
1363
+ }
1364
+ catch {
1365
+ parsed = undefined;
1366
+ }
1367
+ if (!res.ok) {
1368
+ const errMsg = parsed &&
1369
+ typeof parsed === "object" &&
1370
+ "error" in parsed &&
1371
+ typeof parsed.error === "string"
1372
+ ? parsed.error
1373
+ : `HTTP ${res.status}`;
1374
+ throw new Error(errMsg);
1375
+ }
1376
+ const body = parsed;
1377
+ return (body.threads ?? []).filter((t) => t.ok).map((t) => t.threadId);
1378
+ };
1379
+ const applySettledLocally = (threadIds) => {
1380
+ const next = removeActiveThreads(threadIds);
1381
+ syncFabVisibility(next.length);
1382
+ if (next.length === 0) {
1383
+ closeActivity();
1384
+ stopPolling();
1385
+ }
1386
+ else if (activityOpen) {
1387
+ renderActivity(next);
1388
+ }
1389
+ return next;
1390
+ };
1391
+ let settling = false;
1392
+ const renderActivity = (records) => {
1393
+ activityEl.replaceChildren();
1394
+ const head = document.createElement("div");
1395
+ head.className = "t3f-activity-head";
1396
+ const title = document.createElement("div");
1397
+ title.className = "t3f-activity-title";
1398
+ title.textContent = copy.activityTitle;
1399
+ const headActions = document.createElement("div");
1400
+ headActions.style.display = "flex";
1401
+ headActions.style.gap = "6px";
1402
+ const newBtn = document.createElement("button");
1403
+ newBtn.type = "button";
1404
+ newBtn.className = "t3f-btn primary";
1405
+ setIconLabel(newBtn, PLUS_ICON_SVG, copy.activityNewLabel);
1406
+ newBtn.addEventListener("click", () => {
1407
+ closeActivity();
1408
+ void openPanel();
1409
+ });
1410
+ headActions.append(newBtn);
1411
+ if (records.length > 0) {
1412
+ const clearBtn = document.createElement("button");
1413
+ clearBtn.type = "button";
1414
+ clearBtn.className = "t3f-btn";
1415
+ setIconLabel(clearBtn, TRASH_ICON_SVG, copy.activityClearLabel);
1416
+ clearBtn.addEventListener("click", () => {
1417
+ clearActiveThreads();
1418
+ syncFabVisibility(0);
1419
+ closeActivity();
1420
+ stopPolling();
1421
+ });
1422
+ headActions.append(clearBtn);
1423
+ }
1424
+ head.append(title, headActions);
1425
+ activityEl.append(head);
1426
+ if (records.length === 0) {
1427
+ const empty = document.createElement("div");
1428
+ empty.className = "t3f-activity-empty";
1429
+ empty.textContent = copy.activityEmptyLabel;
1430
+ activityEl.append(empty);
1431
+ return;
1432
+ }
1433
+ const list = document.createElement("div");
1434
+ list.className = "t3f-activity-list";
1435
+ for (const record of records) {
1436
+ const row = document.createElement("div");
1437
+ row.className = "t3f-activity-row";
1438
+ const rowTitle = document.createElement("div");
1439
+ rowTitle.className = "t3f-activity-row-title";
1440
+ const fullTitle = record.threadTitle.trim() || record.threadId;
1441
+ const split = splitThreadTitleReportType(record.threadTitle, reportTypeGlyph);
1442
+ const typeForIcon = split.reportType ?? record.reportType;
1443
+ const displayTitle = split.reportType ? split.displayTitle || record.threadId : fullTitle;
1444
+ if (typeForIcon) {
1445
+ const icon = document.createElement("span");
1446
+ icon.className = "t3f-activity-row-title-icon";
1447
+ icon.setAttribute("aria-hidden", "true");
1448
+ icon.innerHTML = reportTypeGlyphHtml(typeForIcon, reportTypeGlyph, copy);
1449
+ rowTitle.append(icon);
1450
+ }
1451
+ const rowTitleText = document.createElement("span");
1452
+ rowTitleText.className = "t3f-activity-row-title-text";
1453
+ rowTitleText.textContent = displayTitle;
1454
+ rowTitle.append(rowTitleText);
1455
+ rowTitle.title = fullTitle;
1456
+ const actions = document.createElement("div");
1457
+ actions.className = "t3f-activity-row-actions";
1458
+ const openLink = document.createElement("a");
1459
+ wireOpenInT3Link(openLink, {
1460
+ openUrl: record.openUrl,
1461
+ threadId: record.threadId,
1462
+ endpoint,
1463
+ label: copy.activityOpenLabel,
1464
+ });
1465
+ const continueBtn = document.createElement("button");
1466
+ continueBtn.type = "button";
1467
+ continueBtn.className = "t3f-btn";
1468
+ setIconLabel(continueBtn, PLAY_ICON_SVG, copy.activityContinueLabel);
1469
+ continueBtn.addEventListener("click", () => {
1470
+ writeSessionThreadRecord(window.location.pathname, {
1471
+ threadId: record.threadId,
1472
+ threadTitle: record.threadTitle,
1473
+ reportType: record.reportType,
1474
+ lastSentAt: Date.now(),
1475
+ });
1476
+ closeActivity();
1477
+ void openPanel();
1478
+ });
1479
+ const dismissBtn = document.createElement("button");
1480
+ dismissBtn.type = "button";
1481
+ dismissBtn.className = "t3f-btn";
1482
+ setIconLabel(dismissBtn, CLOSE_ICON_SVG, copy.activityDismissLabel);
1483
+ dismissBtn.addEventListener("click", () => {
1484
+ const next = removeActiveThread(record.threadId);
1485
+ syncFabVisibility(next.length);
1486
+ if (next.length === 0) {
1487
+ closeActivity();
1488
+ stopPolling();
1489
+ }
1490
+ else {
1491
+ renderActivity(next);
1492
+ }
1493
+ });
1494
+ const settleBtn = document.createElement("button");
1495
+ settleBtn.type = "button";
1496
+ settleBtn.className = "t3f-btn";
1497
+ setIconLabel(settleBtn, CHECK_ICON_SVG, copy.activitySettleLabel);
1498
+ settleBtn.disabled = settling;
1499
+ settleBtn.addEventListener("click", () => {
1500
+ void (async () => {
1501
+ if (settling)
1502
+ return;
1503
+ settling = true;
1504
+ settleBtn.disabled = true;
1505
+ continueBtn.disabled = true;
1506
+ dismissBtn.disabled = true;
1507
+ try {
1508
+ const okIds = await settleActiveThreads([record.threadId]);
1509
+ if (!okIds.includes(record.threadId)) {
1510
+ settleBtn.disabled = false;
1511
+ continueBtn.disabled = false;
1512
+ dismissBtn.disabled = false;
1513
+ return;
1514
+ }
1515
+ settling = false;
1516
+ applySettledLocally(okIds);
1517
+ }
1518
+ catch {
1519
+ settleBtn.disabled = false;
1520
+ continueBtn.disabled = false;
1521
+ dismissBtn.disabled = false;
1522
+ }
1523
+ finally {
1524
+ settling = false;
1525
+ }
1526
+ })();
1527
+ });
1528
+ actions.append(openLink, continueBtn, settleBtn, dismissBtn);
1529
+ row.append(rowTitle, actions);
1530
+ list.append(row);
1531
+ }
1532
+ activityEl.append(list);
1533
+ };
1534
+ const openActivity = () => {
1535
+ hideToast();
1536
+ const records = listActiveThreads();
1537
+ syncFabVisibility(records.length);
1538
+ renderActivity(records);
1539
+ activityOpen = true;
1540
+ activityEl.hidden = false;
1541
+ };
1542
+ const registerSentThread = (result, reportType) => {
1543
+ const next = upsertActiveThread({
1544
+ threadId: result.threadId,
1545
+ threadTitle: typeof result.threadTitle === "string" ? result.threadTitle : "",
1546
+ openUrl: result.openUrl,
1547
+ projectId: result.projectId,
1548
+ ...(typeof result.environmentId === "string" && result.environmentId.trim()
1549
+ ? { environmentId: result.environmentId.trim() }
1550
+ : {}),
1551
+ pathname: window.location.pathname,
1552
+ reportType,
1553
+ startedAt: Date.now(),
1554
+ });
1555
+ syncFabVisibility(next.length);
1556
+ ensurePolling();
1557
+ };
1558
+ const close = (opts) => {
1559
+ if (opts?.clearDraft) {
1560
+ clearDraftState();
1561
+ }
1562
+ else if (open) {
1563
+ persistDraft(false);
1564
+ }
1565
+ open = false;
1566
+ detachThreadPicker?.();
1567
+ detachThreadPicker = null;
1568
+ detachSlotContributions?.();
1569
+ detachSlotContributions = null;
1570
+ backdrop?.remove();
1571
+ backdrop = null;
1572
+ };
1573
+ // Initial activity restore (e.g. soft refresh mid-session).
1574
+ {
1575
+ const initial = listActiveThreads();
1576
+ syncFabVisibility(initial.length);
1577
+ if (initial.length > 0)
1578
+ ensurePolling();
1579
+ }
1580
+ const openPanel = async () => {
1581
+ closeActivity();
1582
+ hideToast();
1583
+ if (open)
1584
+ return;
1585
+ open = true;
1586
+ // Restore in-progress compose state (survives close + full page reload).
1587
+ {
1588
+ const draft = readFeedbackDraft();
1589
+ const extras = draft ? migrateDraftExtras(draft) : {};
1590
+ draftExtras.replace(extras);
1591
+ }
1592
+ backdrop = document.createElement("div");
1593
+ backdrop.className = "t3f-backdrop";
1594
+ backdrop.innerHTML = `
1595
+ <div class="t3f-dom-highlight" hidden></div>
1596
+ <form class="t3f-panel" novalidate>
1597
+ <div class="t3f-header">
1598
+ <div class="t3f-header-text">
1599
+ <div class="t3f-title">${escapeHtml(copy.panelTitle)}</div>
1600
+ </div>
1601
+ ${closeButtonHtml({
1602
+ act: "cancel",
1603
+ label: copy.cancelLabel,
1604
+ titleAttr: copy.cancelTitle,
1605
+ })}
1606
+ </div>
1607
+ <div data-slot="beforeComment"></div>
1608
+ <div data-slot="comment"></div>
1609
+ <div class="t3f-attachments">
1610
+ <div data-slot="attachments"></div>
1611
+ <div class="t3f-status"></div>
1612
+ </div>
1613
+ <div class="t3f-actions">
1614
+ <div class="t3f-actions-tools" data-slot="actionsTools"></div>
1615
+ <div class="t3f-actions-send">
1616
+ <div class="t3f-continue-group" data-continue-group hidden>
1617
+ <button type="button" class="t3f-btn t3f-send-continue" data-act="send-continue" hidden title="${SEND_CONTINUE_SHORTCUT_LABEL}">
1618
+ ${PLAY_ICON_SVG}<span class="t3f-send-continue-type" data-thread-continue-type hidden aria-hidden="true"></span><span data-thread-continue-label></span>
1619
+ </button>
1620
+ <button type="button" class="t3f-btn t3f-thread-pick" data-act="thread-pick" hidden aria-expanded="false" aria-haspopup="listbox" title="${escapeHtml(copy.threadPickLabel)}" aria-label="${escapeHtml(copy.threadPickLabel)}">
1621
+ ${CHEVRON_DOWN_ICON_SVG}
1622
+ </button>
1623
+ <div class="t3f-thread-picker" data-thread-picker hidden>
1624
+ <input type="search" data-thread-search placeholder="${escapeHtml(copy.threadSearchPlaceholder)}" autocomplete="off" spellcheck="false" />
1625
+ <div class="t3f-thread-picker-list" data-thread-list role="listbox" aria-label="${escapeHtml(copy.threadPickLabel)}"></div>
1626
+ <div class="t3f-thread-picker-empty" data-thread-empty hidden>${escapeHtml(copy.threadSearchEmptyLabel)}</div>
1627
+ <div class="t3f-thread-picker-meta" data-thread-meta hidden></div>
1628
+ </div>
1629
+ </div>
1630
+ <button type="button" class="t3f-btn" data-act="send-new" hidden title="${SEND_NEW_SHORTCUT_LABEL}">${escapeHtml(copy.threadNewLabel)}</button>
1631
+ <button type="submit" class="t3f-btn primary" data-act="send" title="${SEND_PREFERRED_SHORTCUT_LABEL}">${escapeHtml(copy.sendLabel)}</button>
1632
+ </div>
1633
+ </div>
1634
+ </form>
1635
+ `;
1636
+ root.appendChild(backdrop);
1637
+ const form = backdrop.querySelector("form");
1638
+ const soloSendBtn = form.querySelector('[data-act="send"]');
1639
+ const sendNewBtn = form.querySelector('[data-act="send-new"]');
1640
+ const continueGroup = form.querySelector("[data-continue-group]");
1641
+ const sendContinueBtn = form.querySelector('[data-act="send-continue"]');
1642
+ const threadPickBtn = form.querySelector('[data-act="thread-pick"]');
1643
+ const threadPickerEl = form.querySelector("[data-thread-picker]");
1644
+ const threadSearchInput = form.querySelector("[data-thread-search]");
1645
+ const threadListEl = form.querySelector("[data-thread-list]");
1646
+ const threadEmptyEl = form.querySelector("[data-thread-empty]");
1647
+ const threadMetaEl = form.querySelector("[data-thread-meta]");
1648
+ const threadContinueLabelEl = form.querySelector("[data-thread-continue-label]");
1649
+ const threadContinueTypeEl = form.querySelector("[data-thread-continue-type]");
1650
+ let reportType = readReportType();
1651
+ selectedReportTypeRef.current = reportType;
1652
+ const emitReportType = (type) => {
1653
+ for (const listener of reportTypeListeners)
1654
+ listener(type);
1655
+ };
1656
+ let threadDestination = "new";
1657
+ const priorThread = threadReuse === "ask" ? readSessionThreadRecord(window.location.pathname) : undefined;
1658
+ let listedThreads = [];
1659
+ let selectedContinue = priorThread
1660
+ ? {
1661
+ threadId: priorThread.threadId,
1662
+ threadTitle: priorThread.threadTitle,
1663
+ ...(priorThread.reportType ? { reportType: priorThread.reportType } : {}),
1664
+ }
1665
+ : undefined;
1666
+ let threadSearchQuery = defaultThreadSearchQuery(window.location.pathname);
1667
+ threadSearchInput.value = threadSearchQuery;
1668
+ let pickerOpen = false;
1669
+ let pickerActiveIndex = -1;
1670
+ const status = form.querySelector(".t3f-status");
1671
+ hostRef.current = buildShellHost({
1672
+ setReportType: (type) => {
1673
+ reportType = type;
1674
+ selectedReportTypeRef.current = type;
1675
+ writeReportType(type);
1676
+ emitReportType(type);
1677
+ syncSendActionsUiRef();
1678
+ },
1679
+ setStatus: (message, kind = "") => setStatus(status, message, kind),
1680
+ });
1681
+ const uiCtx = {
1682
+ ...hostRef.current,
1683
+ form,
1684
+ backdrop,
1685
+ };
1686
+ const panelContributions = resolveAllPanelUiContributions(panelPlugins);
1687
+ const slotMount = mountSlotContributions(form, panelContributions, uiCtx);
1688
+ detachSlotContributions = slotMount.dispose;
1689
+ const textarea = commentPanel.getApi().getTextarea() ??
1690
+ form.querySelector("textarea[name='comment']");
1691
+ const continueTitleExcerpt = (title) => {
1692
+ const trimmed = title.replace(/\s+/g, " ").trim();
1693
+ if (!trimmed)
1694
+ return copy.threadContinueUntitledLabel;
1695
+ const max = 28;
1696
+ return trimmed.length > max ? `${trimmed.slice(0, max - 1)}…` : trimmed;
1697
+ };
1698
+ const continueAriaLabel = (title) => {
1699
+ const excerpt = continueTitleExcerpt(title);
1700
+ return `${copy.threadContinueLabel} ${excerpt}`;
1701
+ };
1702
+ const resolveContinueTarget = () => {
1703
+ const selected = selectedContinue;
1704
+ if (!selected)
1705
+ return undefined;
1706
+ const fromList = listedThreads.find((t) => t.threadId === selected.threadId);
1707
+ if (fromList) {
1708
+ return {
1709
+ threadId: fromList.threadId,
1710
+ threadTitle: fromList.title,
1711
+ ...(selected.reportType ? { reportType: selected.reportType } : {}),
1712
+ };
1713
+ }
1714
+ if (priorThread && priorThread.threadId === selected.threadId) {
1715
+ return {
1716
+ threadId: priorThread.threadId,
1717
+ threadTitle: priorThread.threadTitle,
1718
+ ...(priorThread.reportType ? { reportType: priorThread.reportType } : {}),
1719
+ };
1720
+ }
1721
+ // Sticky / picked id vanished from the live list — still allow continue.
1722
+ return selected;
1723
+ };
1724
+ const askHasContinueTarget = () => resolveContinueTarget() !== undefined;
1725
+ const askHasPicker = () => listedThreads.length > 0;
1726
+ const askShowSplit = () => threadReuse === "ask" &&
1727
+ (askHasContinueTarget() || askHasPicker() || priorThread !== undefined);
1728
+ const preferredThreadDestination = () => {
1729
+ const target = resolveContinueTarget();
1730
+ if (!target)
1731
+ return "new";
1732
+ if (priorThread && target.threadId === priorThread.threadId) {
1733
+ return defaultThreadDestination(priorThread, reportType);
1734
+ }
1735
+ if (listedThreads.some((t) => t.threadId === target.threadId)) {
1736
+ return "continue";
1737
+ }
1738
+ return defaultThreadDestination(priorThread, reportType);
1739
+ };
1740
+ const clearThreadPickerLayout = () => {
1741
+ threadPickerEl.style.top = "";
1742
+ threadPickerEl.style.left = "";
1743
+ threadPickerEl.style.right = "";
1744
+ threadPickerEl.style.bottom = "";
1745
+ threadPickerEl.style.width = "";
1746
+ threadPickerEl.removeAttribute("data-placement");
1747
+ threadListEl.style.maxHeight = "";
1748
+ };
1749
+ /** Place the fixed picker above/below the continue control within the viewport. */
1750
+ const layoutThreadPicker = () => {
1751
+ if (!pickerOpen || threadPickerEl.hidden)
1752
+ return;
1753
+ const anchor = continueGroup.getBoundingClientRect();
1754
+ const margin = 8;
1755
+ const gap = 6;
1756
+ const width = Math.min(360, Math.max(240, window.innerWidth - margin * 2), Math.max(anchor.width, 280));
1757
+ let left = anchor.right - width;
1758
+ left = Math.min(Math.max(margin, left), window.innerWidth - margin - width);
1759
+ threadPickerEl.style.right = "auto";
1760
+ threadPickerEl.style.bottom = "auto";
1761
+ threadPickerEl.style.width = `${Math.round(width)}px`;
1762
+ threadPickerEl.style.left = `${Math.round(left)}px`;
1763
+ // Measure chrome (search + padding + meta) with a collapsed list.
1764
+ threadListEl.style.maxHeight = "0px";
1765
+ threadPickerEl.style.top = `${Math.round(margin)}px`;
1766
+ const chrome = Math.ceil(threadPickerEl.getBoundingClientRect().height);
1767
+ const spaceAbove = anchor.top - gap - margin;
1768
+ const spaceBelow = window.innerHeight - anchor.bottom - gap - margin;
1769
+ const openAbove = spaceAbove >= 160 || spaceAbove >= spaceBelow;
1770
+ const available = Math.max(96, openAbove ? spaceAbove : spaceBelow);
1771
+ const listMax = Math.max(72, Math.min(320, available - chrome));
1772
+ threadListEl.style.maxHeight = `${Math.round(listMax)}px`;
1773
+ const pickerHeight = Math.ceil(threadPickerEl.getBoundingClientRect().height);
1774
+ if (openAbove) {
1775
+ const top = Math.max(margin, anchor.top - gap - pickerHeight);
1776
+ threadPickerEl.style.top = `${Math.round(top)}px`;
1777
+ threadPickerEl.dataset.placement = "above";
1778
+ }
1779
+ else {
1780
+ threadPickerEl.style.top = `${Math.round(anchor.bottom + gap)}px`;
1781
+ threadPickerEl.dataset.placement = "below";
1782
+ }
1783
+ };
1784
+ const closeThreadPicker = () => {
1785
+ pickerOpen = false;
1786
+ pickerActiveIndex = -1;
1787
+ threadPickerEl.hidden = true;
1788
+ threadPickBtn.setAttribute("aria-expanded", "false");
1789
+ threadListEl.removeAttribute("aria-activedescendant");
1790
+ clearThreadPickerLayout();
1791
+ };
1792
+ const selectListedThread = (item) => {
1793
+ selectedContinue = {
1794
+ threadId: item.threadId,
1795
+ threadTitle: item.title,
1796
+ };
1797
+ closeThreadPicker();
1798
+ syncSendActionsUi();
1799
+ };
1800
+ const visiblePickerMatches = () => {
1801
+ const selectedId = selectedContinue?.threadId;
1802
+ return filterThreadListItems(listedThreads, threadSearchQuery, {
1803
+ ...(selectedId ? { selectedId } : {}),
1804
+ }).items;
1805
+ };
1806
+ const pickerOptionButtons = () => Array.from(threadListEl.querySelectorAll("[data-thread-id]"));
1807
+ const syncPickerActiveOption = () => {
1808
+ const options = pickerOptionButtons();
1809
+ if (options.length === 0) {
1810
+ pickerActiveIndex = -1;
1811
+ threadListEl.removeAttribute("aria-activedescendant");
1812
+ return;
1813
+ }
1814
+ if (pickerActiveIndex < 0)
1815
+ pickerActiveIndex = 0;
1816
+ if (pickerActiveIndex >= options.length)
1817
+ pickerActiveIndex = options.length - 1;
1818
+ let activeId = "";
1819
+ for (let i = 0; i < options.length; i++) {
1820
+ const option = options[i];
1821
+ const active = i === pickerActiveIndex;
1822
+ option.classList.toggle("is-active", active);
1823
+ if (active) {
1824
+ activeId = option.id;
1825
+ option.scrollIntoView({ block: "nearest" });
1826
+ }
1827
+ }
1828
+ if (activeId)
1829
+ threadListEl.setAttribute("aria-activedescendant", activeId);
1830
+ else
1831
+ threadListEl.removeAttribute("aria-activedescendant");
1832
+ };
1833
+ const movePickerActive = (delta) => {
1834
+ const options = pickerOptionButtons();
1835
+ if (options.length === 0)
1836
+ return;
1837
+ if (pickerActiveIndex < 0) {
1838
+ pickerActiveIndex = delta > 0 ? 0 : options.length - 1;
1839
+ }
1840
+ else {
1841
+ pickerActiveIndex = (pickerActiveIndex + delta + options.length * 10) % options.length;
1842
+ }
1843
+ syncPickerActiveOption();
1844
+ };
1845
+ const renderThreadPickerList = () => {
1846
+ const selectedId = selectedContinue?.threadId;
1847
+ const { items: filtered, matchedCount, openMatchedCount, } = filterThreadListItems(listedThreads, threadSearchQuery, {
1848
+ ...(selectedId ? { selectedId } : {}),
1849
+ });
1850
+ threadListEl.replaceChildren();
1851
+ threadEmptyEl.hidden = filtered.length > 0;
1852
+ const truncated = matchedCount > filtered.length;
1853
+ if (truncated) {
1854
+ threadMetaEl.hidden = false;
1855
+ const base = copy.threadSearchTruncatedLabel
1856
+ .replaceAll("{shown}", String(filtered.length))
1857
+ .replaceAll("{total}", String(matchedCount));
1858
+ threadMetaEl.textContent =
1859
+ openMatchedCount > 0 ? `${base} · ${openMatchedCount} open` : base;
1860
+ }
1861
+ else if (matchedCount > 0) {
1862
+ threadMetaEl.hidden = false;
1863
+ threadMetaEl.textContent =
1864
+ openMatchedCount > 0
1865
+ ? `${matchedCount} thread${matchedCount === 1 ? "" : "s"} · ${openMatchedCount} open`
1866
+ : `${matchedCount} thread${matchedCount === 1 ? "" : "s"}`;
1867
+ }
1868
+ else {
1869
+ threadMetaEl.hidden = true;
1870
+ threadMetaEl.textContent = "";
1871
+ }
1872
+ const selectedIndex = selectedId
1873
+ ? filtered.findIndex((item) => item.threadId === selectedId)
1874
+ : -1;
1875
+ for (let index = 0; index < filtered.length; index++) {
1876
+ const item = filtered[index];
1877
+ const option = document.createElement("button");
1878
+ option.type = "button";
1879
+ option.className = "t3f-thread-picker-option";
1880
+ option.id = `t3f-thread-opt-${index}`;
1881
+ option.setAttribute("role", "option");
1882
+ option.tabIndex = -1;
1883
+ option.dataset.threadId = item.threadId;
1884
+ option.setAttribute("aria-selected", item.threadId === selectedId ? "true" : "false");
1885
+ const split = splitThreadTitleReportType(item.title, reportTypeGlyph);
1886
+ const displayTitle = split.reportType ? split.displayTitle || item.title : item.title;
1887
+ if (split.reportType) {
1888
+ const typeEl = document.createElement("span");
1889
+ typeEl.className = "t3f-thread-picker-option-type";
1890
+ typeEl.setAttribute("aria-hidden", "true");
1891
+ typeEl.innerHTML = reportTypeGlyphHtml(split.reportType, reportTypeGlyph, copy);
1892
+ option.append(typeEl);
1893
+ }
1894
+ const title = document.createElement("span");
1895
+ title.className = "t3f-thread-picker-option-title";
1896
+ title.textContent = displayTitle;
1897
+ option.append(title);
1898
+ if (item.state === "working" || item.state === "settled") {
1899
+ const state = document.createElement("span");
1900
+ state.className = `t3f-thread-picker-option-state ${item.state === "working" ? "is-working" : "is-settled"}`;
1901
+ state.textContent =
1902
+ item.state === "working" ? copy.threadStateWorkingLabel : copy.threadStateSettledLabel;
1903
+ option.append(state);
1904
+ }
1905
+ option.title = displayTitle;
1906
+ option.addEventListener("click", () => {
1907
+ selectListedThread(item);
1908
+ });
1909
+ option.addEventListener("mousemove", () => {
1910
+ if (pickerActiveIndex === index)
1911
+ return;
1912
+ pickerActiveIndex = index;
1913
+ syncPickerActiveOption();
1914
+ });
1915
+ threadListEl.append(option);
1916
+ }
1917
+ if (filtered.length === 0) {
1918
+ pickerActiveIndex = -1;
1919
+ threadListEl.removeAttribute("aria-activedescendant");
1920
+ }
1921
+ else if (pickerActiveIndex < 0 || pickerActiveIndex >= filtered.length) {
1922
+ pickerActiveIndex = selectedIndex >= 0 ? selectedIndex : 0;
1923
+ syncPickerActiveOption();
1924
+ }
1925
+ else {
1926
+ syncPickerActiveOption();
1927
+ }
1928
+ };
1929
+ const openThreadPicker = () => {
1930
+ if (!askHasPicker())
1931
+ return;
1932
+ pickerOpen = true;
1933
+ pickerActiveIndex = -1;
1934
+ threadPickerEl.hidden = false;
1935
+ threadPickBtn.setAttribute("aria-expanded", "true");
1936
+ renderThreadPickerList();
1937
+ layoutThreadPicker();
1938
+ queueMicrotask(() => {
1939
+ layoutThreadPicker();
1940
+ threadSearchInput.focus();
1941
+ threadSearchInput.select();
1942
+ });
1943
+ };
1944
+ const syncSendActionsUi = () => {
1945
+ const showSplit = askShowSplit();
1946
+ const target = resolveContinueTarget();
1947
+ const hasPicker = askHasPicker();
1948
+ continueGroup.hidden = !showSplit;
1949
+ soloSendBtn.hidden = showSplit;
1950
+ sendNewBtn.hidden = !showSplit;
1951
+ sendContinueBtn.hidden = !target;
1952
+ threadPickBtn.hidden = !hasPicker;
1953
+ if (!showSplit) {
1954
+ soloSendBtn.classList.add("primary");
1955
+ sendNewBtn.classList.remove("primary");
1956
+ sendContinueBtn.classList.remove("primary");
1957
+ threadPickBtn.classList.remove("primary");
1958
+ threadContinueTypeEl.hidden = true;
1959
+ threadContinueTypeEl.replaceChildren();
1960
+ closeThreadPicker();
1961
+ return;
1962
+ }
1963
+ if (!target) {
1964
+ sendContinueBtn.hidden = !hasPicker;
1965
+ if (hasPicker) {
1966
+ threadContinueLabelEl.textContent = copy.threadPickLabel;
1967
+ threadContinueTypeEl.hidden = true;
1968
+ threadContinueTypeEl.replaceChildren();
1969
+ sendContinueBtn.setAttribute("aria-label", copy.threadPickLabel);
1970
+ sendContinueBtn.title = copy.threadPickLabel;
1971
+ sendContinueBtn.classList.remove("primary");
1972
+ threadPickBtn.classList.remove("primary");
1973
+ }
1974
+ sendNewBtn.classList.add("primary");
1975
+ sendNewBtn.title = `${SEND_PREFERRED_SHORTCUT_LABEL} · ${SEND_NEW_SHORTCUT_LABEL}`;
1976
+ if (pickerOpen) {
1977
+ renderThreadPickerList();
1978
+ layoutThreadPicker();
1979
+ }
1980
+ return;
1981
+ }
1982
+ const preferred = preferredThreadDestination();
1983
+ const fullTitle = target.threadTitle.trim();
1984
+ const split = splitThreadTitleReportType(target.threadTitle, reportTypeGlyph);
1985
+ const typeForIcon = split.reportType ?? target.reportType;
1986
+ const displayTitle = split.reportType ? split.displayTitle : fullTitle;
1987
+ const excerpt = continueTitleExcerpt(displayTitle);
1988
+ const aria = continueAriaLabel(fullTitle || displayTitle);
1989
+ threadContinueLabelEl.textContent = excerpt;
1990
+ if (typeForIcon) {
1991
+ threadContinueTypeEl.hidden = false;
1992
+ threadContinueTypeEl.innerHTML = reportTypeGlyphHtml(typeForIcon, reportTypeGlyph, copy);
1993
+ }
1994
+ else {
1995
+ threadContinueTypeEl.hidden = true;
1996
+ threadContinueTypeEl.replaceChildren();
1997
+ }
1998
+ sendContinueBtn.setAttribute("aria-label", aria);
1999
+ soloSendBtn.classList.remove("primary");
2000
+ sendNewBtn.classList.toggle("primary", preferred === "new");
2001
+ sendContinueBtn.classList.toggle("primary", preferred === "continue");
2002
+ threadPickBtn.classList.toggle("primary", preferred === "continue");
2003
+ const newShortcut = preferred === "new"
2004
+ ? `${SEND_PREFERRED_SHORTCUT_LABEL} · ${SEND_NEW_SHORTCUT_LABEL}`
2005
+ : SEND_NEW_SHORTCUT_LABEL;
2006
+ const continueShortcut = preferred === "continue"
2007
+ ? `${SEND_PREFERRED_SHORTCUT_LABEL} · ${SEND_CONTINUE_SHORTCUT_LABEL}`
2008
+ : SEND_CONTINUE_SHORTCUT_LABEL;
2009
+ sendNewBtn.title = newShortcut;
2010
+ sendContinueBtn.title = fullTitle
2011
+ ? `${fullTitle} (${continueShortcut})`
2012
+ : `${aria} (${continueShortcut})`;
2013
+ if (pickerOpen) {
2014
+ renderThreadPickerList();
2015
+ layoutThreadPicker();
2016
+ }
2017
+ };
2018
+ syncSendActionsUiRef = syncSendActionsUi;
2019
+ const syncReportTypeUi = () => {
2020
+ slotMount.syncReportType(reportType);
2021
+ emitReportType(reportType);
2022
+ syncSendActionsUi();
2023
+ };
2024
+ syncReportTypeUi();
2025
+ threadPickBtn.addEventListener("click", (event) => {
2026
+ event.preventDefault();
2027
+ event.stopPropagation();
2028
+ if (pickerOpen)
2029
+ closeThreadPicker();
2030
+ else
2031
+ openThreadPicker();
2032
+ });
2033
+ threadSearchInput.addEventListener("input", () => {
2034
+ threadSearchQuery = threadSearchInput.value;
2035
+ pickerActiveIndex = -1;
2036
+ renderThreadPickerList();
2037
+ layoutThreadPicker();
2038
+ });
2039
+ threadSearchInput.addEventListener("keydown", (event) => {
2040
+ if (event.key === "Escape") {
2041
+ event.preventDefault();
2042
+ event.stopPropagation();
2043
+ closeThreadPicker();
2044
+ textarea.focus();
2045
+ return;
2046
+ }
2047
+ if (event.key === "ArrowDown") {
2048
+ event.preventDefault();
2049
+ event.stopPropagation();
2050
+ movePickerActive(1);
2051
+ return;
2052
+ }
2053
+ if (event.key === "ArrowUp") {
2054
+ event.preventDefault();
2055
+ event.stopPropagation();
2056
+ movePickerActive(-1);
2057
+ return;
2058
+ }
2059
+ if (event.key === "Enter") {
2060
+ const matches = visiblePickerMatches();
2061
+ if (matches.length === 0)
2062
+ return;
2063
+ const active = pickerActiveIndex >= 0 && pickerActiveIndex < matches.length
2064
+ ? matches[pickerActiveIndex]
2065
+ : matches.length === 1
2066
+ ? matches[0]
2067
+ : undefined;
2068
+ if (!active)
2069
+ return;
2070
+ event.preventDefault();
2071
+ event.stopPropagation();
2072
+ selectListedThread(active);
2073
+ }
2074
+ });
2075
+ threadPickerEl.addEventListener("click", (event) => {
2076
+ event.stopPropagation();
2077
+ });
2078
+ if (threadReuse === "ask") {
2079
+ void fetch(`${endpoint}/threads`, { headers: { Accept: "application/json" } })
2080
+ .then(async (res) => {
2081
+ if (!res.ok)
2082
+ throw new Error(`HTTP ${res.status}`);
2083
+ const body = (await res.json());
2084
+ listedThreads = Array.isArray(body.threads) ? body.threads : [];
2085
+ if (!selectedContinue &&
2086
+ priorThread &&
2087
+ listedThreads.some((t) => t.threadId === priorThread.threadId)) {
2088
+ selectedContinue = {
2089
+ threadId: priorThread.threadId,
2090
+ threadTitle: priorThread.threadTitle,
2091
+ ...(priorThread.reportType ? { reportType: priorThread.reportType } : {}),
2092
+ };
2093
+ }
2094
+ if (form.isConnected)
2095
+ syncSendActionsUi();
2096
+ })
2097
+ .catch(() => {
2098
+ listedThreads = [];
2099
+ if (form.isConnected)
2100
+ syncSendActionsUi();
2101
+ });
2102
+ }
2103
+ let sending = false;
2104
+ textarea.focus();
2105
+ const setSendButtonsDisabled = (disabled) => {
2106
+ soloSendBtn.disabled = disabled;
2107
+ sendNewBtn.disabled = disabled;
2108
+ sendContinueBtn.disabled = disabled;
2109
+ threadPickBtn.disabled = disabled;
2110
+ threadSearchInput.disabled = disabled;
2111
+ };
2112
+ textarea.addEventListener("keydown", (event) => {
2113
+ if (event.key !== "Enter")
2114
+ return;
2115
+ const mod = event.metaKey || event.ctrlKey;
2116
+ let destination;
2117
+ if (mod && event.shiftKey && !event.altKey) {
2118
+ destination = "new";
2119
+ }
2120
+ else if (event.altKey && !mod && !event.shiftKey) {
2121
+ if (!askHasContinueTarget())
2122
+ return;
2123
+ destination = "continue";
2124
+ }
2125
+ else if (mod && !event.shiftKey && !event.altKey) {
2126
+ destination = askShowSplit() ? preferredThreadDestination() : "new";
2127
+ }
2128
+ else {
2129
+ return;
2130
+ }
2131
+ event.preventDefault();
2132
+ event.stopPropagation();
2133
+ queueMicrotask(() => {
2134
+ void submitFeedback(destination);
2135
+ });
2136
+ });
2137
+ backdrop.addEventListener("click", (e) => {
2138
+ if (e.target === backdrop && !busy.isBusy())
2139
+ close();
2140
+ });
2141
+ form.querySelector('[data-act="cancel"]')?.addEventListener("click", () => {
2142
+ close({ clearDraft: true });
2143
+ });
2144
+ const submitFeedback = async (destination) => {
2145
+ if (sending)
2146
+ return;
2147
+ if (!commentPanel.getApi().validate()) {
2148
+ setStatus(status, copy.commentRequiredStatus, "err");
2149
+ return;
2150
+ }
2151
+ if (destination === "continue" && !resolveContinueTarget()) {
2152
+ openThreadPicker();
2153
+ return;
2154
+ }
2155
+ threadDestination = destination;
2156
+ sending = true;
2157
+ setSendButtonsDisabled(true);
2158
+ setStatus(status, copy.sendingStatus);
2159
+ try {
2160
+ // Session plugins (comment, reportType, screenshot, …) collect themselves.
2161
+ const envelope = await session.build();
2162
+ if (threadReuse === "ask") {
2163
+ const target = resolveContinueTarget();
2164
+ askThreadIdRef.current =
2165
+ threadDestination === "continue" && target ? target.threadId : undefined;
2166
+ }
2167
+ try {
2168
+ const result = (await transport.send(envelope));
2169
+ if (result &&
2170
+ typeof result === "object" &&
2171
+ typeof result.threadId === "string" &&
2172
+ typeof result.openUrl === "string") {
2173
+ if (threadReuse === "session-route" || threadReuse === "ask") {
2174
+ writeSessionThreadRecord(window.location.pathname, {
2175
+ threadId: result.threadId,
2176
+ threadTitle: typeof result.threadTitle === "string" ? result.threadTitle : "",
2177
+ reportType,
2178
+ lastSentAt: Date.now(),
2179
+ });
2180
+ }
2181
+ if (typeof result.projectId === "string" && result.projectId) {
2182
+ registerSentThread(result, reportType);
2183
+ }
2184
+ close({ clearDraft: true });
2185
+ showSentToast(result);
2186
+ }
2187
+ else {
2188
+ setStatus(status, copy.sentStatus, "ok");
2189
+ close({ clearDraft: true });
2190
+ }
2191
+ }
2192
+ finally {
2193
+ askThreadIdRef.current = undefined;
2194
+ }
2195
+ }
2196
+ catch (error) {
2197
+ setStatus(status, error instanceof Error ? error.message : String(error), "err");
2198
+ sending = false;
2199
+ setSendButtonsDisabled(false);
2200
+ }
2201
+ };
2202
+ sendNewBtn.addEventListener("click", () => {
2203
+ void submitFeedback("new");
2204
+ });
2205
+ sendContinueBtn.addEventListener("click", () => {
2206
+ if (!resolveContinueTarget()) {
2207
+ openThreadPicker();
2208
+ return;
2209
+ }
2210
+ void submitFeedback("continue");
2211
+ });
2212
+ form.addEventListener("submit", (event) => {
2213
+ event.preventDefault();
2214
+ void submitFeedback(askShowSplit() ? preferredThreadDestination() : "new");
2215
+ });
2216
+ const onDocPointerDown = (event) => {
2217
+ if (!pickerOpen)
2218
+ return;
2219
+ const target = event.target;
2220
+ if (!(target instanceof Node))
2221
+ return;
2222
+ if (continueGroup.contains(target))
2223
+ return;
2224
+ closeThreadPicker();
2225
+ };
2226
+ const onPickerReposition = () => {
2227
+ if (pickerOpen)
2228
+ layoutThreadPicker();
2229
+ };
2230
+ document.addEventListener("pointerdown", onDocPointerDown, true);
2231
+ window.addEventListener("resize", onPickerReposition);
2232
+ form.addEventListener("scroll", onPickerReposition, { passive: true });
2233
+ detachThreadPicker = () => {
2234
+ document.removeEventListener("pointerdown", onDocPointerDown, true);
2235
+ window.removeEventListener("resize", onPickerReposition);
2236
+ form.removeEventListener("scroll", onPickerReposition);
2237
+ detachThreadPicker = null;
2238
+ };
2239
+ persistDraft(true);
2240
+ };
2241
+ openPanelImpl = openPanel;
2242
+ fab.addEventListener("click", () => {
2243
+ const active = listActiveThreads();
2244
+ if (active.length > 0) {
2245
+ if (activityOpen)
2246
+ closeActivity();
2247
+ else
2248
+ openActivity();
2249
+ return;
2250
+ }
2251
+ void openPanel();
2252
+ });
2253
+ /**
2254
+ * Plugin chords (first match wins) + shell open-panel chord.
2255
+ * Built-in pick/capture/record register via their panel plugins — same path as customs.
2256
+ */
2257
+ const registeredShortcuts = [
2258
+ ...resolvePluginShortcuts(panelPlugins),
2259
+ {
2260
+ id: "builtin:open",
2261
+ shortcut,
2262
+ run: (ctx) => {
2263
+ ctx.openPanel();
2264
+ },
2265
+ },
2266
+ ];
2267
+ const onKeyDown = (event) => {
2268
+ const shortcutCtx = hostRef.current ?? buildShellHost();
2269
+ const matched = matchRegisteredShortcut(event, registeredShortcuts, selectedReportTypeRef.current);
2270
+ if (matched) {
2271
+ event.preventDefault();
2272
+ void matched.run(shortcutCtx);
2273
+ return;
2274
+ }
2275
+ if (event.key === "Escape") {
2276
+ if (busy.isBusy())
2277
+ return;
2278
+ if (activityOpen) {
2279
+ event.preventDefault();
2280
+ closeActivity();
2281
+ return;
2282
+ }
2283
+ if (toastEl.hidden === false) {
2284
+ event.preventDefault();
2285
+ hideToast();
2286
+ return;
2287
+ }
2288
+ if (open) {
2289
+ event.preventDefault();
2290
+ close();
2291
+ }
2292
+ }
2293
+ };
2294
+ window.addEventListener("keydown", onKeyDown);
2295
+ const onOpenEvent = () => {
2296
+ void openPanel();
2297
+ };
2298
+ const onCloseEvent = () => {
2299
+ if (busy.isBusy())
2300
+ return;
2301
+ closeActivity();
2302
+ hideToast();
2303
+ close();
2304
+ };
2305
+ window.addEventListener(T3_FEEDBACK_OPEN_EVENT, onOpenEvent);
2306
+ window.addEventListener(T3_FEEDBACK_CLOSE_EVENT, onCloseEvent);
2307
+ const onPageHide = () => {
2308
+ if (open)
2309
+ persistDraft(true);
2310
+ };
2311
+ window.addEventListener("pagehide", onPageHide);
2312
+ // Reopen an in-progress draft after reload (soft or hard).
2313
+ if (readFeedbackDraft()?.panelOpen) {
2314
+ queueMicrotask(() => {
2315
+ void openPanel();
2316
+ });
2317
+ }
2318
+ const unmount = () => {
2319
+ window.removeEventListener("keydown", onKeyDown);
2320
+ window.removeEventListener(T3_FEEDBACK_OPEN_EVENT, onOpenEvent);
2321
+ window.removeEventListener(T3_FEEDBACK_CLOSE_EVENT, onCloseEvent);
2322
+ window.removeEventListener("pagehide", onPageHide);
2323
+ stopPolling();
2324
+ hideToast();
2325
+ closeActivity();
2326
+ if (open)
2327
+ persistDraft(true);
2328
+ else
2329
+ persistDraft(false);
2330
+ open = false;
2331
+ abortScreenshotRegions(root);
2332
+ detachThreadPicker?.();
2333
+ detachThreadPicker = null;
2334
+ detachSlotContributions?.();
2335
+ detachSlotContributions = null;
2336
+ backdrop?.remove();
2337
+ backdrop = null;
2338
+ root.remove();
2339
+ hmrTracker?.dispose();
2340
+ session.dispose();
2341
+ };
2342
+ return {
2343
+ open: () => {
2344
+ void openPanel();
2345
+ },
2346
+ close: () => {
2347
+ if (busy.isBusy())
2348
+ return;
2349
+ close();
2350
+ },
2351
+ pick: () => {
2352
+ actions.runAction("pick");
2353
+ },
2354
+ capture: () => {
2355
+ actions.runAction("capture");
2356
+ },
2357
+ record: () => {
2358
+ actions.runAction("record");
2359
+ },
2360
+ unmount,
2361
+ };
2362
+ }
2363
+ /** Convenience for the injected virtual module (config + optional custom plugins). */
2364
+ export function mountT3FeedbackFromConfig(config, customPlugins = []) {
2365
+ return mountT3Feedback({
2366
+ shortcut: config.shortcut,
2367
+ endpoint: config.endpoint,
2368
+ fab: config.fab,
2369
+ threadReuse: config.threadReuse,
2370
+ reportTypeGlyph: config.reportTypeGlyph,
2371
+ activePlugins: config.activePlugins,
2372
+ ...(config.pluginOptions ? { pluginOptions: config.pluginOptions } : {}),
2373
+ ...(customPlugins.length > 0 ? { plugins: customPlugins } : {}),
2374
+ ...(config.version ? { version: config.version } : {}),
2375
+ pick: {
2376
+ ...(config.pick.shortcut === null
2377
+ ? { shortcut: false }
2378
+ : { shortcut: config.pick.shortcut }),
2379
+ afterPick: config.pick.afterPick,
2380
+ cropPaddingPx: config.pick.cropPaddingPx,
2381
+ includeInnerHTML: config.pick.includeInnerHTML,
2382
+ maxInnerHTMLLength: config.pick.maxInnerHTMLLength,
2383
+ styles: config.pick.styles,
2384
+ sourceFramework: config.pick.sourceFramework,
2385
+ openInEditor: config.pick.openInEditor === null
2386
+ ? false
2387
+ : config.pick.openInEditor.urlTemplate
2388
+ ? {
2389
+ endpoint: config.pick.openInEditor.endpoint,
2390
+ urlTemplate: config.pick.openInEditor.urlTemplate,
2391
+ }
2392
+ : { endpoint: config.pick.openInEditor.endpoint },
2393
+ },
2394
+ screenshot: {
2395
+ ...(config.screenshot.shortcut === null
2396
+ ? { shortcut: false }
2397
+ : { shortcut: config.screenshot.shortcut }),
2398
+ crop: config.screenshot.crop,
2399
+ cropPaddingPx: config.screenshot.cropPaddingPx,
2400
+ maxImages: config.screenshot.maxImages,
2401
+ },
2402
+ record: {
2403
+ ...(config.record.shortcut === null
2404
+ ? { shortcut: false }
2405
+ : { shortcut: config.record.shortcut }),
2406
+ },
2407
+ });
2408
+ }