@rose42/t3-feedback 0.1.1 → 0.1.2

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.
@@ -1,7 +1,7 @@
1
1
  import { captureFrameBurst, captureScreenshot, createSourceResolver, initFeedbackCapture, } from "@rose42/feedback-capture";
2
2
  import { T3_FEEDBACK_CLOSE_EVENT, T3_FEEDBACK_OPEN_EVENT } from "../controls.js";
3
3
  import { splitThreadTitleReportType } from "../bridge/format.js";
4
- import { formatShortcutLabel, resolveActivePlugins, resolveFabConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, } from "../types.js";
4
+ import { formatShortcutLabel, resolveActivePlugins, resolveFabConfig, resolveOpenInEditorEndpointForApi, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, } from "../types.js";
5
5
  import { openInT3Seamless, shouldBypassSeamlessOpen } from "./openInT3.js";
6
6
  import { ACTIVE_THREAD_POLL_MS, clearActiveThreads, listActiveThreads, removeActiveThread, removeActiveThreads, upsertActiveThread, } from "./activeThreads.js";
7
7
  import { abortScreenshotRegions } from "./cropScreenshot.js";
@@ -14,6 +14,7 @@ import { createActionRegistry, createAfterPickRegistry, createBusyRegistry, crea
14
14
  import { createCommentPanelPlugin, createReportTypePanelPlugin } from "./ui/commentPanelPlugin.js";
15
15
  import { createCollectOnlyBuiltinPlugins, resolveAllPanelUiContributions, } from "./ui/defaultPlugins.js";
16
16
  import { createElementTargetPanelPlugin } from "./ui/elementTargetPanelPlugin.js";
17
+ import { prefixSourceEnrichment } from "./ui/sourcePathPrefix.js";
17
18
  import { asPluginsWithUi } from "./ui/resolveContributions.js";
18
19
  import { createScreenshotPanelPlugin } from "./ui/screenshotPanelPlugin.js";
19
20
  import { mountSlotContributions } from "./ui/slots.js";
@@ -275,6 +276,9 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
275
276
  display: flex; align-items: center; justify-content: center;
276
277
  padding: 16px;
277
278
  }
279
+ /* Author display:flex beats UA [hidden]; pick/capture set hidden while the overlay is down. */
280
+ #${rootId} .t3f-backdrop[hidden],
281
+ #${rootId} .t3f-backdrop.t3f-away { display: none !important; }
278
282
  #${rootId} .t3f-panel {
279
283
  position: relative;
280
284
  z-index: 1;
@@ -367,6 +371,8 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
367
371
  #${rootId} .t3f-section {
368
372
  display: grid; gap: 8px; min-width: 0; max-width: 100%;
369
373
  }
374
+ /* Author display:grid beats UA [hidden] — sections start closed and clear must hide them. */
375
+ #${rootId} .t3f-section[hidden] { display: none !important; }
370
376
  #${rootId} .t3f-section-head {
371
377
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
372
378
  min-width: 0;
@@ -457,6 +463,7 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
457
463
  display: flex; flex-wrap: nowrap; align-items: center; gap: 10px;
458
464
  min-width: 0;
459
465
  }
466
+ #${rootId} .t3f-clipboard-offer[hidden] { display: none !important; }
460
467
  #${rootId} .t3f-clipboard-preview-btn {
461
468
  flex: 0 0 auto;
462
469
  padding: 0; margin: 0; border: none; background: transparent;
@@ -487,6 +494,7 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
487
494
  #${rootId} .t3f-preview-wrap {
488
495
  display: grid; gap: 8px; min-width: 0;
489
496
  }
497
+ #${rootId} .t3f-preview-wrap[hidden] { display: none !important; }
490
498
  #${rootId} .t3f-gallery {
491
499
  display: flex; flex-wrap: nowrap; gap: 8px; min-width: 0;
492
500
  }
@@ -523,12 +531,21 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
523
531
  }
524
532
  #${rootId} .t3f-thumb-tools {
525
533
  position: absolute; top: 2px; right: 2px; display: flex; gap: 2px;
526
- opacity: 0; pointer-events: none;
534
+ opacity: 0;
527
535
  transition: opacity 0.12s ease;
528
536
  }
529
537
  #${rootId} .t3f-thumb:hover .t3f-thumb-tools,
530
538
  #${rootId} .t3f-thumb:focus-within .t3f-thumb-tools {
531
- opacity: 1; pointer-events: auto;
539
+ opacity: 1;
540
+ }
541
+ /* Keep tools clickable while the pointer moves onto them (avoid pointer-events:none traps). */
542
+ #${rootId} .t3f-thumb-tools {
543
+ pointer-events: none;
544
+ }
545
+ #${rootId} .t3f-thumb:hover .t3f-thumb-tools,
546
+ #${rootId} .t3f-thumb:focus-within .t3f-thumb-tools,
547
+ #${rootId} .t3f-thumb-tools:focus-within {
548
+ pointer-events: auto;
532
549
  }
533
550
  @media (hover: none) {
534
551
  #${rootId} .t3f-thumb-tools {
@@ -786,6 +803,8 @@ ${mobileOnly}#${rootId} .t3f-backdrop {
786
803
  min-width: 0;
787
804
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
788
805
  }
806
+ /* Author display:inline-flex beats UA [hidden] — send XOR (solo vs New/Continue). */
807
+ #${rootId} button.t3f-btn[hidden] { display: none !important; }
789
808
  #${rootId} button.t3f-btn:hover:not(:disabled) {
790
809
  background: #334155; border-color: #38bdf8;
791
810
  }
@@ -941,10 +960,16 @@ export function mountT3Feedback(options = {}) {
941
960
  return noopHandle();
942
961
  const rootId = options.rootId ?? "t3-feedback-root";
943
962
  const shortcut = resolveShortcut(options.shortcut, options.shortcutKey);
944
- const pickConfig = resolvePickConfig(options.pick);
963
+ const endpoint = options.endpoint ?? "/__t3-feedback";
964
+ const pickConfig = (() => {
965
+ const resolved = resolvePickConfig(options.pick);
966
+ return {
967
+ ...resolved,
968
+ openInEditor: resolveOpenInEditorEndpointForApi(resolved.openInEditor, endpoint),
969
+ };
970
+ })();
945
971
  const screenshotConfig = resolveScreenshotConfig(options.screenshot, pickConfig.cropPaddingPx);
946
972
  const recordConfig = resolveRecordConfig(options.record);
947
- const endpoint = options.endpoint ?? "/__t3-feedback";
948
973
  const activePluginSet = resolveActivePlugins(options.activePlugins, options.includeConsoleLogs);
949
974
  const pageContextActive = activePluginSet.has("pageContext");
950
975
  const consoleLogsActive = activePluginSet.has("consoleLogs");
@@ -1030,13 +1055,20 @@ export function mountT3Feedback(options = {}) {
1030
1055
  }
1031
1056
  return hostRef.current;
1032
1057
  };
1033
- const resolveSource = createSourceResolver({
1058
+ const resolveSourceBase = createSourceResolver({
1034
1059
  framework: pickConfig.sourceFramework,
1035
1060
  sourceEndpoint: endpoint,
1036
1061
  ...(options.pick && "resolveSource" in options.pick && options.pick.resolveSource
1037
1062
  ? { resolveSource: options.pick.resolveSource }
1038
1063
  : {}),
1039
1064
  });
1065
+ const sourcePathPrefix = pickConfig.sourcePathPrefix;
1066
+ const resolveSource = resolveSourceBase && sourcePathPrefix
1067
+ ? async (el) => {
1068
+ const hit = await resolveSourceBase(el);
1069
+ return hit ? prefixSourceEnrichment(hit, sourcePathPrefix) : hit;
1070
+ }
1071
+ : resolveSourceBase;
1040
1072
  const commentPanel = createCommentPanelPlugin({ copy });
1041
1073
  const reportTypeHintCaps = {
1042
1074
  pageContext: pageContextActive,
@@ -2382,14 +2414,20 @@ export function mountT3FeedbackFromConfig(config, customPlugins = []) {
2382
2414
  maxInnerHTMLLength: config.pick.maxInnerHTMLLength,
2383
2415
  styles: config.pick.styles,
2384
2416
  sourceFramework: config.pick.sourceFramework,
2417
+ ...(config.pick.sourcePathPrefix ? { sourcePathPrefix: config.pick.sourcePathPrefix } : {}),
2385
2418
  openInEditor: config.pick.openInEditor === null
2386
2419
  ? 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 },
2420
+ : (() => {
2421
+ const rewritten = resolveOpenInEditorEndpointForApi(config.pick.openInEditor, config.endpoint);
2422
+ if (!rewritten)
2423
+ return false;
2424
+ return rewritten.urlTemplate
2425
+ ? {
2426
+ endpoint: rewritten.endpoint,
2427
+ urlTemplate: rewritten.urlTemplate,
2428
+ }
2429
+ : { endpoint: rewritten.endpoint };
2430
+ })(),
2393
2431
  },
2394
2432
  screenshot: {
2395
2433
  ...(config.screenshot.shortcut === null
@@ -0,0 +1,2 @@
1
+ /** Hide/show the compose backdrop during pick / capture / record. */
2
+ export declare function setBackdropAway(backdrop: HTMLElement, away: boolean): void;
@@ -0,0 +1,12 @@
1
+ /** Hide/show the compose backdrop during pick / capture / record. */
2
+ export function setBackdropAway(backdrop, away) {
3
+ backdrop.hidden = away;
4
+ backdrop.classList.toggle("t3f-away", away);
5
+ // Inline !important — author `display:flex` on `.t3f-backdrop` otherwise wins over UA [hidden].
6
+ if (away) {
7
+ backdrop.style.setProperty("display", "none", "important");
8
+ }
9
+ else {
10
+ backdrop.style.removeProperty("display");
11
+ }
12
+ }
@@ -29,23 +29,31 @@ export declare function formatComponentLocation(loc: {
29
29
  }): string;
30
30
  /**
31
31
  * Build the URL used to open a source location.
32
- * - `urlTemplate` → navigate (IDE protocol / custom)
33
- * - else → Vite-style GET endpoint with `?file=`
32
+ * - `urlTemplate` → navigate (IDE protocol / custom); uses the displayed path
33
+ * - else → GET endpoint with `?file=` (Vite `/__open-in-editor` or the bridge
34
+ * `/open-editor`). Strips `sourcePathPrefix` only for Vite's native endpoint,
35
+ * which resolves against the app cwd; the bridge expects workspace-relative paths.
34
36
  */
35
37
  export declare function buildOpenInEditorUrl(loc: {
36
38
  fileName: string;
37
39
  line: number;
38
40
  column?: number;
39
- }, config: T3FeedbackOpenInEditorConfig): {
41
+ }, config: T3FeedbackOpenInEditorConfig, options?: {
42
+ sourcePathPrefix?: string;
43
+ }): {
40
44
  kind: "navigate" | "fetch";
41
45
  url: string;
42
46
  };
47
+ /** True for Vite's built-in launcher (resolves paths against the Vite project root). */
48
+ export declare function isViteNativeOpenInEditorEndpoint(endpoint: string): boolean;
43
49
  /** Open a resolved component location in the IDE (best-effort; never throws). */
44
50
  export declare function openSourceInEditor(loc: {
45
51
  fileName: string;
46
52
  line: number;
47
53
  column?: number;
48
- }, config: T3FeedbackOpenInEditorConfig, fetchFn?: typeof fetch): Promise<void>;
54
+ }, config: T3FeedbackOpenInEditorConfig, fetchFn?: typeof fetch, options?: {
55
+ sourcePathPrefix?: string;
56
+ }): Promise<void>;
49
57
  /** Split a `>`-joined CSS path into segments (trim empties). */
50
58
  export declare function splitCssSelectorParts(selector: string): string[];
51
59
  /**
@@ -61,4 +69,5 @@ export declare function formatElementChip(target: ElementChipTarget): string;
61
69
  export declare function buildTargetChipHtml(target: ElementChipTarget, options?: {
62
70
  openInEditor?: T3FeedbackOpenInEditorConfig | null;
63
71
  openInEditorLabel?: string;
72
+ sourcePathPrefix?: string;
64
73
  }): string;
@@ -1,5 +1,6 @@
1
1
  import { rankSourceHierarchy } from "@rose42/feedback-capture";
2
2
  import { escapeHtml } from "./html.js";
3
+ import { stripSourcePathPrefix } from "./sourcePathPrefix.js";
3
4
  function asLoc(loc) {
4
5
  return loc;
5
6
  }
@@ -57,10 +58,12 @@ export function formatComponentLocation(loc) {
57
58
  }
58
59
  /**
59
60
  * Build the URL used to open a source location.
60
- * - `urlTemplate` → navigate (IDE protocol / custom)
61
- * - else → Vite-style GET endpoint with `?file=`
61
+ * - `urlTemplate` → navigate (IDE protocol / custom); uses the displayed path
62
+ * - else → GET endpoint with `?file=` (Vite `/__open-in-editor` or the bridge
63
+ * `/open-editor`). Strips `sourcePathPrefix` only for Vite's native endpoint,
64
+ * which resolves against the app cwd; the bridge expects workspace-relative paths.
62
65
  */
63
- export function buildOpenInEditorUrl(loc, config) {
66
+ export function buildOpenInEditorUrl(loc, config, options) {
64
67
  const column = typeof loc.column === "number" && Number.isFinite(loc.column)
65
68
  ? Math.max(1, Math.floor(loc.column))
66
69
  : 1;
@@ -72,16 +75,25 @@ export function buildOpenInEditorUrl(loc, config) {
72
75
  .replaceAll("{column}", String(column));
73
76
  return { kind: "navigate", url };
74
77
  }
75
- const fileParam = `${loc.fileName}:${line}:${column}`;
78
+ const stripForViteNative = Boolean(options?.sourcePathPrefix) && isViteNativeOpenInEditorEndpoint(config.endpoint);
79
+ const fileForEditor = stripForViteNative && options?.sourcePathPrefix
80
+ ? stripSourcePathPrefix(loc.fileName, options.sourcePathPrefix)
81
+ : loc.fileName;
82
+ const fileParam = `${fileForEditor}:${line}:${column}`;
76
83
  const sep = config.endpoint.includes("?") ? "&" : "?";
77
84
  return {
78
85
  kind: "fetch",
79
86
  url: `${config.endpoint}${sep}file=${encodeURIComponent(fileParam)}`,
80
87
  };
81
88
  }
89
+ /** True for Vite's built-in launcher (resolves paths against the Vite project root). */
90
+ export function isViteNativeOpenInEditorEndpoint(endpoint) {
91
+ const pathOnly = endpoint.split("?")[0] ?? endpoint;
92
+ return pathOnly === "/__open-in-editor" || pathOnly.endsWith("/__open-in-editor");
93
+ }
82
94
  /** Open a resolved component location in the IDE (best-effort; never throws). */
83
- export async function openSourceInEditor(loc, config, fetchFn = fetch) {
84
- const built = buildOpenInEditorUrl(loc, config);
95
+ export async function openSourceInEditor(loc, config, fetchFn = fetch, options) {
96
+ const built = buildOpenInEditorUrl(loc, config, options);
85
97
  try {
86
98
  if (built.kind === "navigate") {
87
99
  window.location.href = built.url;
@@ -136,7 +148,9 @@ function buildOpenableLocHtml(loc, options) {
136
148
  ? { column: loc.column }
137
149
  : {}),
138
150
  };
139
- const built = buildOpenInEditorUrl(locAttrs, openConfig);
151
+ const built = buildOpenInEditorUrl(locAttrs, openConfig, {
152
+ ...(options?.sourcePathPrefix ? { sourcePathPrefix: options.sourcePathPrefix } : {}),
153
+ });
140
154
  if (built.kind === "navigate") {
141
155
  return `<a class="t3f-target-file" href="${escapeHtml(built.url)}" title="${openLabel}" aria-label="${openLabel}: ${text}">${text}</a>`;
142
156
  }
@@ -1,4 +1,5 @@
1
1
  import { collectElementTargetAsync, createElementTargetPlugin, } from "@rose42/feedback-capture";
2
+ import { setBackdropAway } from "./backdropAway.js";
2
3
  import { buildTargetChipHtml, formatElementChip, openSourceInEditor, selectorAtAncestorDepth, } from "./elementTargetChip.js";
3
4
  const PICK_DETAILS_STORAGE_KEY = "t3-feedback:pick-details";
4
5
  export function readPickDetailsPrefs(fallback) {
@@ -99,6 +100,7 @@ export function bindElementTargetPanelSession(options) {
99
100
  targetChip.innerHTML = buildTargetChipHtml(target, {
100
101
  openInEditor: pickConfig.openInEditor,
101
102
  openInEditorLabel: copy.openInEditorLabel,
103
+ ...(pickConfig.sourcePathPrefix ? { sourcePathPrefix: pickConfig.sourcePathPrefix } : {}),
102
104
  });
103
105
  targetChip.title = chip;
104
106
  targetBlock.hidden = false;
@@ -171,7 +173,7 @@ export function bindElementTargetPanelSession(options) {
171
173
  fileName,
172
174
  line,
173
175
  ...(typeof column === "number" && Number.isFinite(column) ? { column } : {}),
174
- }, pickConfig.openInEditor);
176
+ }, pickConfig.openInEditor, fetch, pickConfig.sourcePathPrefix ? { sourcePathPrefix: pickConfig.sourcePathPrefix } : undefined);
175
177
  });
176
178
  targetChip.addEventListener("pointerover", (event) => {
177
179
  const target = elementPlugin.getTarget();
@@ -213,7 +215,7 @@ export function bindElementTargetPanelSession(options) {
213
215
  });
214
216
  const runPickFlow = async () => {
215
217
  const releasePicking = registerBusy("picking");
216
- backdrop.hidden = true;
218
+ setBackdropAway(backdrop, true);
217
219
  setStatus(copy.pickingStatus);
218
220
  try {
219
221
  const details = readPickDetailsFromUi();
@@ -225,7 +227,7 @@ export function bindElementTargetPanelSession(options) {
225
227
  });
226
228
  if (!isOpen())
227
229
  return;
228
- backdrop.hidden = false;
230
+ setBackdropAway(backdrop, false);
229
231
  if (!picked) {
230
232
  setStatus(copy.pickCancelledStatus);
231
233
  focusComment();
@@ -243,7 +245,7 @@ export function bindElementTargetPanelSession(options) {
243
245
  finally {
244
246
  releasePicking();
245
247
  if (isOpen())
246
- backdrop.hidden = false;
248
+ setBackdropAway(backdrop, false);
247
249
  }
248
250
  };
249
251
  form.querySelector('[data-act="clear-pick"]')?.addEventListener("click", () => {
@@ -2,6 +2,7 @@ import { attachImagePasteHandler, captureFrameBurst, captureScreenshot, cropData
2
2
  import { selectScreenshotRegion } from "../cropScreenshot.js";
3
3
  import { readClipboardImageDataUrl } from "../readClipboardImage.js";
4
4
  import { selectBurstFrames } from "../selectBurstFrames.js";
5
+ import { setBackdropAway } from "./backdropAway.js";
5
6
  import { CLOSE_ICON_SVG, CROP_ICON_SVG, escapeHtml } from "./html.js";
6
7
  function loadImageNaturalSize(dataUrl) {
7
8
  return new Promise((resolve, reject) => {
@@ -252,7 +253,7 @@ export function bindScreenshotPanelSession(options) {
252
253
  return;
253
254
  }
254
255
  const releaseRecording = registerBusy("recording");
255
- backdrop.hidden = true;
256
+ setBackdropAway(backdrop, true);
256
257
  setStatus(copy.recordingStatus);
257
258
  hideClipboardOffer();
258
259
  const overlay = document.createElement("div");
@@ -280,7 +281,7 @@ export function bindScreenshotPanelSession(options) {
280
281
  overlay.remove();
281
282
  if (!isOpen())
282
283
  return;
283
- backdrop.hidden = false;
284
+ setBackdropAway(backdrop, false);
284
285
  if (!frames || frames.length === 0) {
285
286
  setStatus(copy.recordFailedStatus, "err");
286
287
  return;
@@ -315,7 +316,7 @@ export function bindScreenshotPanelSession(options) {
315
316
  overlay.remove();
316
317
  releaseRecording();
317
318
  if (isOpen())
318
- backdrop.hidden = false;
319
+ setBackdropAway(backdrop, false);
319
320
  }
320
321
  };
321
322
  const insertClipboardOffer = async () => {
@@ -325,34 +326,40 @@ export function bindScreenshotPanelSession(options) {
325
326
  hideClipboardOffer();
326
327
  await attachScreenshot(dataUrl, copy.pastedAttachedStatus, "direct");
327
328
  };
328
- form.querySelector('[data-act="clear-shot"]')?.addEventListener("click", () => {
329
- clearGallery();
330
- setStatus("");
331
- });
332
- form.querySelectorAll('[data-act="shot"]').forEach((el) => {
333
- el.addEventListener("click", () => {
329
+ // Delegate so clear/dismiss keep working if slot hosts remount or icons swallow hits.
330
+ const onFormClick = (event) => {
331
+ const hit = event.target?.closest?.("[data-act]");
332
+ if (!hit || !form.contains(hit))
333
+ return;
334
+ const act = hit.getAttribute("data-act");
335
+ if (act === "clear-shot") {
336
+ event.preventDefault();
337
+ clearGallery();
338
+ setStatus("");
339
+ return;
340
+ }
341
+ if (act === "clipboard-dismiss") {
342
+ event.preventDefault();
343
+ hideClipboardOffer();
344
+ syncGallery();
345
+ return;
346
+ }
347
+ if (act === "shot") {
348
+ event.preventDefault();
334
349
  void runShotFlow();
335
- });
336
- });
337
- form.querySelectorAll('[data-act="record"]').forEach((el) => {
338
- el.addEventListener("click", () => {
350
+ return;
351
+ }
352
+ if (act === "record") {
353
+ event.preventDefault();
339
354
  void runRecordFlow();
340
- });
341
- });
342
- form.querySelector('[data-act="clipboard-dismiss"]')?.addEventListener("click", () => {
343
- hideClipboardOffer();
344
- });
345
- form.querySelectorAll('[data-act="clipboard-insert"]').forEach((el) => {
346
- el.addEventListener("click", () => {
347
- void insertClipboardOffer();
348
- });
349
- });
350
- galleryEl.addEventListener("click", (event) => {
351
- const hit = event.target?.closest?.("button.t3f-thumb-clipboard");
352
- if (!hit || !galleryEl.contains(hit))
353
355
  return;
354
- void insertClipboardOffer();
355
- });
356
+ }
357
+ if (act === "clipboard-insert") {
358
+ event.preventDefault();
359
+ void insertClipboardOffer();
360
+ }
361
+ };
362
+ form.addEventListener("click", onFormClick);
356
363
  const detachPaste = attachImagePasteHandler(form, (dataUrl) => {
357
364
  hideClipboardOffer();
358
365
  void attachScreenshot(dataUrl, copy.pastedAttachedStatus, "direct");
@@ -429,6 +436,7 @@ export function bindScreenshotPanelSession(options) {
429
436
  }
430
437
  return {
431
438
  dispose: () => {
439
+ form.removeEventListener("click", onFormClick);
432
440
  detachAfterPick?.();
433
441
  detachPaste();
434
442
  },
@@ -0,0 +1,12 @@
1
+ import type { ElementSourceEnrichment } from "@rose42/feedback-capture";
2
+ import { normalizePickSourcePathPrefix } from "../../types.js";
3
+ export { normalizePickSourcePathPrefix as normalizeSourcePathPrefix };
4
+ /** Prefix an app-relative file path when it is not already under `prefix`. */
5
+ export declare function prefixSourceFileName(fileName: string, prefix: string): string;
6
+ /**
7
+ * Strip a previously applied prefix (for Vite `/__open-in-editor`, which resolves
8
+ * against the app cwd rather than the monorepo / workspace root).
9
+ */
10
+ export declare function stripSourcePathPrefix(fileName: string, prefix: string): string;
11
+ /** Prefix component / leaf / owners file names on a source enrichment. */
12
+ export declare function prefixSourceEnrichment(enrichment: ElementSourceEnrichment, prefix: string): ElementSourceEnrichment;
@@ -0,0 +1,39 @@
1
+ import { normalizePickSourcePathPrefix } from "../../types.js";
2
+ export { normalizePickSourcePathPrefix as normalizeSourcePathPrefix };
3
+ /** Prefix an app-relative file path when it is not already under `prefix`. */
4
+ export function prefixSourceFileName(fileName, prefix) {
5
+ const file = fileName.replace(/\\/g, "/").trim();
6
+ if (!file || file.startsWith("/") || /^[a-zA-Z]:\//.test(file))
7
+ return fileName;
8
+ if (file === prefix || file.startsWith(`${prefix}/`))
9
+ return file;
10
+ return `${prefix}/${file.replace(/^\/+/, "")}`;
11
+ }
12
+ /**
13
+ * Strip a previously applied prefix (for Vite `/__open-in-editor`, which resolves
14
+ * against the app cwd rather than the monorepo / workspace root).
15
+ */
16
+ export function stripSourcePathPrefix(fileName, prefix) {
17
+ const file = fileName.replace(/\\/g, "/");
18
+ const p = prefix.replace(/\\/g, "/").replace(/\/+$/, "");
19
+ if (file === p)
20
+ return fileName;
21
+ if (file.startsWith(`${p}/`))
22
+ return file.slice(p.length + 1);
23
+ return fileName;
24
+ }
25
+ function mapLoc(loc, prefix) {
26
+ return { ...loc, fileName: prefixSourceFileName(loc.fileName, prefix) };
27
+ }
28
+ /** Prefix component / leaf / owners file names on a source enrichment. */
29
+ export function prefixSourceEnrichment(enrichment, prefix) {
30
+ const out = { ...enrichment };
31
+ if (enrichment.component)
32
+ out.component = mapLoc(enrichment.component, prefix);
33
+ if (enrichment.leaf)
34
+ out.leaf = mapLoc(enrichment.leaf, prefix);
35
+ if (enrichment.owners) {
36
+ out.owners = enrichment.owners.map((loc) => mapLoc(loc, prefix));
37
+ }
38
+ return out;
39
+ }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { t3FeedbackPlugin, T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, default, } from "./plugin.js";
2
- export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadSettleResult, ThreadsListResponse, ThreadsProbeResponse, ThreadsSettleResponse, T3BuiltinPluginId, T3FeedbackAfterPick, T3FeedbackBuiltinPluginOptions, T3FeedbackClientConfig, T3FeedbackFabConfig, T3FeedbackFabOptions, T3FeedbackFabPosition, T3FeedbackFabVisibility, T3FeedbackOpenHandoff, T3FeedbackOpenInEditorConfig, T3FeedbackOpenInEditorOptions, T3FeedbackPickConfig, T3FeedbackPickOptions, T3FeedbackPluginOptions, T3FeedbackRecordConfig, T3FeedbackRecordOptions, T3FeedbackReportTypeGlyph, T3FeedbackReportTypeGlyphCustom, T3FeedbackReportTypeGlyphMap, T3FeedbackReportTypeGlyphResolved, T3FeedbackScreenshotConfig, T3FeedbackScreenshotCrop, T3FeedbackScreenshotOptions, T3FeedbackShortcutConfig, T3FeedbackShortcutOptions, T3FeedbackThreadReuse, } from "./types.js";
3
- export { DEFAULT_FAB_CONFIG, DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, resolveActivePluginList, resolveActivePlugins, resolveFabConfig, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "./types.js";
1
+ export { t3FeedbackPlugin, T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, default, } from "./plugin.js";
2
+ export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadSettleResult, ThreadsListResponse, ThreadsProbeResponse, ThreadsSettleResponse, T3BuiltinPluginId, T3FeedbackAfterPick, T3FeedbackBuiltinPluginOptions, T3FeedbackClientConfig, T3FeedbackFabConfig, T3FeedbackFabOptions, T3FeedbackFabPosition, T3FeedbackFabVisibility, T3FeedbackModelSelection, T3FeedbackOpenHandoff, T3FeedbackOpenInEditorConfig, T3FeedbackOpenInEditorOptions, T3FeedbackPickConfig, T3FeedbackPickOptions, T3FeedbackPluginOptions, T3FeedbackRecordConfig, T3FeedbackRecordOptions, T3FeedbackReportTypeGlyph, T3FeedbackReportTypeGlyphCustom, T3FeedbackReportTypeGlyphMap, T3FeedbackReportTypeGlyphResolved, T3FeedbackScreenshotConfig, T3FeedbackScreenshotCrop, T3FeedbackScreenshotOptions, T3FeedbackShortcutConfig, T3FeedbackShortcutOptions, T3FeedbackThreadReuse, } from "./types.js";
3
+ export { DEFAULT_FAB_CONFIG, DEFAULT_MODEL_SELECTION, DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, normalizeModelSelection, resolveActivePluginList, resolveActivePlugins, resolveFabConfig, resolveOpenInEditorConfig, resolveOpenInEditorEndpointForApi, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "./types.js";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { t3FeedbackPlugin, T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, default, } from "./plugin.js";
2
- export { DEFAULT_FAB_CONFIG, DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, resolveActivePluginList, resolveActivePlugins, resolveFabConfig, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "./types.js";
1
+ export { t3FeedbackPlugin, T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, default, } from "./plugin.js";
2
+ export { DEFAULT_FAB_CONFIG, DEFAULT_MODEL_SELECTION, DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, normalizeModelSelection, resolveActivePluginList, resolveActivePlugins, resolveFabConfig, resolveOpenInEditorConfig, resolveOpenInEditorEndpointForApi, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "./types.js";
package/dist/plugin.d.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { type T3FeedbackPluginOptions } from "./types.js";
3
- export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, } from "./bridge/paths.js";
3
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, } from "./bridge/paths.js";
4
4
  export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./bridge/parse.js";
5
5
  /** Resolve a `customPlugins` entry against the Vite project root (bare ids stay bare). */
6
6
  export declare function resolveCustomPluginModuleId(entry: string, root: string): string;
7
+ /**
8
+ * When Vite root sits under a workspace hint, return the relative prefix
9
+ * (e.g. hint=`/repo`, root=`/repo/examples/vite-react` → `examples/vite-react`).
10
+ */
11
+ export declare function deriveSourcePathPrefix(viteRoot: string, workspaceRootHints: readonly string[] | undefined): string | undefined;
7
12
  /** Vite plugin that exposes POST /__t3-feedback and bridges envelopes to T3. */
8
13
  export declare function t3FeedbackPlugin(options?: T3FeedbackPluginOptions): Plugin;
9
14
  export default t3FeedbackPlugin;
package/dist/plugin.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
2
3
  import { normalizePath } from "vite";
3
4
  import { handleT3FeedbackRequest } from "./bridge/http.js";
4
5
  import { T3_FEEDBACK_API_PATH } from "./bridge/paths.js";
5
- import { resolveActivePluginList, resolveFabConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, } from "./types.js";
6
- export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, } from "./bridge/paths.js";
6
+ import { resolveActivePluginList, resolveFabConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, normalizePickSourcePathPrefix, } from "./types.js";
7
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_THREADS_PATH, } from "./bridge/paths.js";
7
8
  export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./bridge/parse.js";
8
9
  /** Resolve a `customPlugins` entry against the Vite project root (bare ids stay bare). */
9
10
  export function resolveCustomPluginModuleId(entry, root) {
@@ -17,6 +18,25 @@ export function resolveCustomPluginModuleId(entry, root) {
17
18
  }
18
19
  return trimmed;
19
20
  }
21
+ /**
22
+ * When Vite root sits under a workspace hint, return the relative prefix
23
+ * (e.g. hint=`/repo`, root=`/repo/examples/vite-react` → `examples/vite-react`).
24
+ */
25
+ export function deriveSourcePathPrefix(viteRoot, workspaceRootHints) {
26
+ if (!workspaceRootHints?.length)
27
+ return undefined;
28
+ const rootResolved = path.resolve(viteRoot);
29
+ for (const hint of workspaceRootHints) {
30
+ if (typeof hint !== "string" || !hint.trim())
31
+ continue;
32
+ const hintResolved = path.resolve(hint.trim());
33
+ const rel = path.relative(hintResolved, rootResolved);
34
+ if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
35
+ continue;
36
+ return normalizePickSourcePathPrefix(rel.replace(/\\/g, "/"));
37
+ }
38
+ return undefined;
39
+ }
20
40
  function resolvePluginOptions(options) {
21
41
  if (!options)
22
42
  return undefined;
@@ -104,23 +124,70 @@ export function t3FeedbackPlugin(options = {}) {
104
124
  const customPluginEntries = (options.customPlugins ?? [])
105
125
  .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
106
126
  .filter(Boolean);
127
+ // Absolute dist entry. Prefer a real filesystem id (not `/@fs/...`) so Vite still
128
+ // runs import-analysis and rewrites bare deps like `@rose42/feedback-capture`.
129
+ // Combined with `link:` example installs, `tsc --watch` updates are visible without
130
+ // a stale optimizeDeps prebundle of this package.
131
+ const clientEntry = normalizePath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "client/index.js"));
132
+ // `dist/client/index.js` → package root (must be fs-allowed when linked from an example).
133
+ const packageRoot = normalizePath(path.resolve(path.dirname(clientEntry), "../.."));
107
134
  const bridgeServerOptions = {
108
135
  basePath: T3_FEEDBACK_API_PATH,
109
136
  ...(options.origin ? { origin: options.origin } : {}),
110
137
  ...(options.openOrigin ? { openOrigin: options.openOrigin } : {}),
111
138
  ...(workspaceRootHints ? { workspaceRootHints } : {}),
112
139
  ...(threadTitlePrefix ? { threadTitlePrefix } : {}),
140
+ ...(options.modelSelection ? { modelSelection: options.modelSelection } : {}),
113
141
  reportTypeGlyph,
114
142
  };
115
143
  return {
116
144
  name: "t3-feedback",
117
145
  apply: "serve",
146
+ enforce: "pre",
147
+ config() {
148
+ return {
149
+ resolve: {
150
+ alias: {
151
+ "@rose42/t3-feedback/client": clientEntry,
152
+ },
153
+ // qwikVite sets `resolve.conditions: []` in DEV, which drops Vite's
154
+ // default "development" export condition. `@builder.io/qwik` then
155
+ // resolves to `core.prod.mjs` (qInspector hard-disabled), so
156
+ // `data-qwik-inspector` is never written and element pick can't
157
+ // resolve components. Restoring "development" is a no-op for other
158
+ // frameworks (Vite's client default already includes it).
159
+ conditions: ["development"],
160
+ },
161
+ optimizeDeps: {
162
+ // Keep the linked client out of the deps cache; prebundle its runtime dep.
163
+ exclude: ["@rose42/t3-feedback", "@rose42/t3-feedback/client"],
164
+ include: ["@rose42/feedback-capture"],
165
+ },
166
+ server: {
167
+ fs: {
168
+ allow: [packageRoot],
169
+ },
170
+ watch: {
171
+ ignored: ["!**/node_modules/@rose42/t3-feedback/**"],
172
+ },
173
+ },
174
+ };
175
+ },
118
176
  configResolved(config) {
119
177
  viteRoot = config.root;
178
+ // Nested app under a monorepo / T3 workspace: prefix chip + agent paths.
179
+ if (!clientConfig.pick.sourcePathPrefix) {
180
+ const derived = deriveSourcePathPrefix(config.root, workspaceRootHints);
181
+ if (derived) {
182
+ clientConfig.pick = { ...clientConfig.pick, sourcePathPrefix: derived };
183
+ }
184
+ }
120
185
  },
121
186
  resolveId(id) {
122
187
  if (id === VIRTUAL_CLIENT_ID)
123
188
  return RESOLVED_VIRTUAL_CLIENT_ID;
189
+ if (id === "@rose42/t3-feedback/client")
190
+ return clientEntry;
124
191
  },
125
192
  load(id) {
126
193
  if (id !== RESOLVED_VIRTUAL_CLIENT_ID)
@@ -135,8 +202,8 @@ export function t3FeedbackPlugin(options = {}) {
135
202
  .map((_, i) => ` ...normalizeCustomPluginExport(custom${i})`)
136
203
  .join(",\n")}\n]`;
137
204
  const clientImports = resolvedCustoms.length === 0
138
- ? `import { mountT3FeedbackFromConfig } from "@rose42/t3-feedback/client";`
139
- : `import { mountT3FeedbackFromConfig, normalizeCustomPluginExport } from "@rose42/t3-feedback/client";`;
205
+ ? `import { mountT3FeedbackFromConfig } from ${JSON.stringify(clientEntry)};`
206
+ : `import { mountT3FeedbackFromConfig, normalizeCustomPluginExport } from ${JSON.stringify(clientEntry)};`;
140
207
  return `
141
208
  ${clientImports}
142
209
  ${customImports}