@rose42/t3-feedback 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/dist/bridge/attachments.d.ts +27 -0
- package/dist/bridge/attachments.js +109 -0
- package/dist/bridge/format.d.ts +70 -0
- package/dist/bridge/format.js +358 -0
- package/dist/bridge/http.d.ts +31 -0
- package/dist/bridge/http.js +169 -0
- package/dist/bridge/index.d.ts +13 -0
- package/dist/bridge/index.js +12 -0
- package/dist/bridge/parse.d.ts +16 -0
- package/dist/bridge/parse.js +82 -0
- package/dist/bridge/paths.d.ts +10 -0
- package/dist/bridge/paths.js +10 -0
- package/dist/bridge/probeThreads.d.ts +13 -0
- package/dist/bridge/probeThreads.js +66 -0
- package/dist/bridge/readSource.d.ts +8 -0
- package/dist/bridge/readSource.js +64 -0
- package/dist/bridge/sendFeedback.d.ts +99 -0
- package/dist/bridge/sendFeedback.js +417 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +40 -0
- package/dist/cli/constants.d.ts +4 -0
- package/dist/cli/constants.js +4 -0
- package/dist/cli/detect.d.ts +17 -0
- package/dist/cli/detect.js +118 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/init.d.ts +18 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/pm.d.ts +12 -0
- package/dist/cli/pm.js +49 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/wire/next.d.ts +8 -0
- package/dist/cli/wire/next.js +71 -0
- package/dist/cli/wire/node.d.ts +3 -0
- package/dist/cli/wire/node.js +76 -0
- package/dist/cli/wire/vite.d.ts +25 -0
- package/dist/cli/wire/vite.js +71 -0
- package/dist/client/activeThreads.d.ts +23 -0
- package/dist/client/activeThreads.js +89 -0
- package/dist/client/cropScreenshot.d.ts +39 -0
- package/dist/client/cropScreenshot.js +212 -0
- package/dist/client/customPlugins.d.ts +7 -0
- package/dist/client/customPlugins.js +38 -0
- package/dist/client/draft.d.ts +33 -0
- package/dist/client/draft.js +222 -0
- package/dist/client/index.d.ts +24 -0
- package/dist/client/index.js +20 -0
- package/dist/client/mount.d.ts +210 -0
- package/dist/client/mount.js +2408 -0
- package/dist/client/openInT3.d.ts +28 -0
- package/dist/client/openInT3.js +81 -0
- package/dist/client/readClipboardImage.d.ts +2 -0
- package/dist/client/readClipboardImage.js +2 -0
- package/dist/client/reportType.d.ts +43 -0
- package/dist/client/reportType.js +110 -0
- package/dist/client/selectBurstFrames.d.ts +21 -0
- package/dist/client/selectBurstFrames.js +101 -0
- package/dist/client/sessionThread.d.ts +26 -0
- package/dist/client/sessionThread.js +93 -0
- package/dist/client/threadPicker.d.ts +20 -0
- package/dist/client/threadPicker.js +51 -0
- package/dist/client/transport.d.ts +11 -0
- package/dist/client/transport.js +35 -0
- package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
- package/dist/client/ui/commentPanelPlugin.js +126 -0
- package/dist/client/ui/defaultPlugins.d.ts +19 -0
- package/dist/client/ui/defaultPlugins.js +48 -0
- package/dist/client/ui/elementTargetChip.d.ts +64 -0
- package/dist/client/ui/elementTargetChip.js +185 -0
- package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
- package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
- package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
- package/dist/client/ui/elementTargetPanelSession.js +279 -0
- package/dist/client/ui/elementTargetUi.d.ts +36 -0
- package/dist/client/ui/elementTargetUi.js +105 -0
- package/dist/client/ui/host.d.ts +112 -0
- package/dist/client/ui/host.js +132 -0
- package/dist/client/ui/html.d.ts +22 -0
- package/dist/client/ui/html.js +26 -0
- package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
- package/dist/client/ui/labeledPluginPreview.js +71 -0
- package/dist/client/ui/resolveContributions.d.ts +13 -0
- package/dist/client/ui/resolveContributions.js +20 -0
- package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
- package/dist/client/ui/screenshotPanelPlugin.js +212 -0
- package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
- package/dist/client/ui/screenshotPanelSession.js +445 -0
- package/dist/client/ui/screenshotUi.d.ts +31 -0
- package/dist/client/ui/screenshotUi.js +130 -0
- package/dist/client/ui/shortcuts.d.ts +32 -0
- package/dist/client/ui/shortcuts.js +40 -0
- package/dist/client/ui/slots.d.ts +73 -0
- package/dist/client/ui/slots.js +112 -0
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +186 -0
- package/dist/types.d.ts +464 -0
- package/dist/types.js +283 -0
- package/docs/devcontainer.md +160 -0
- package/package.json +107 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type T3FeedbackShortcutConfig, type T3FeedbackShortcutOptions } from "../../types.js";
|
|
2
|
+
import { type FeedbackReportType } from "../reportType.js";
|
|
3
|
+
import type { T3FeedbackShortcutRunContext } from "./host.js";
|
|
4
|
+
export type { T3FeedbackShortcutRunContext } from "./host.js";
|
|
5
|
+
/**
|
|
6
|
+
* Shortcut chord a plugin registers with the host.
|
|
7
|
+
* Matched globally via {@link shouldOpenFromShortcut}.
|
|
8
|
+
*/
|
|
9
|
+
export type T3FeedbackPluginShortcut = {
|
|
10
|
+
id: string;
|
|
11
|
+
shortcut: T3FeedbackShortcutOptions;
|
|
12
|
+
reportTypes?: readonly FeedbackReportType[];
|
|
13
|
+
run: (ctx: T3FeedbackShortcutRunContext) => void | Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
/** Resolved shortcut ready for the host keydown loop. */
|
|
16
|
+
export type T3FeedbackRegisteredShortcut = {
|
|
17
|
+
id: string;
|
|
18
|
+
shortcut: T3FeedbackShortcutConfig;
|
|
19
|
+
reportTypes?: readonly FeedbackReportType[];
|
|
20
|
+
run: (ctx: T3FeedbackShortcutRunContext) => void | Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
type PluginWithShortcuts = {
|
|
23
|
+
shortcuts?: T3FeedbackPluginShortcut | readonly T3FeedbackPluginShortcut[];
|
|
24
|
+
};
|
|
25
|
+
/** Collect `shortcuts` declarations from capture plugins. */
|
|
26
|
+
export declare function collectPluginShortcuts(plugins: readonly PluginWithShortcuts[]): T3FeedbackPluginShortcut[];
|
|
27
|
+
/** Resolve plugin shortcut options into host-ready entries. */
|
|
28
|
+
export declare function resolvePluginShortcuts(plugins: readonly PluginWithShortcuts[]): T3FeedbackRegisteredShortcut[];
|
|
29
|
+
/**
|
|
30
|
+
* First matching shortcut wins (`preventDefault` is the caller's responsibility).
|
|
31
|
+
*/
|
|
32
|
+
export declare function matchRegisteredShortcut(event: Pick<KeyboardEvent, "key" | "ctrlKey" | "metaKey" | "shiftKey" | "altKey" | "target">, shortcuts: readonly T3FeedbackRegisteredShortcut[], reportType: FeedbackReportType): T3FeedbackRegisteredShortcut | undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { resolveShortcut, shouldOpenFromShortcut, } from "../../types.js";
|
|
2
|
+
import { pluginAppliesToReportType } from "../reportType.js";
|
|
3
|
+
function normalizeShortcutList(value) {
|
|
4
|
+
if (value == null)
|
|
5
|
+
return [];
|
|
6
|
+
const list = Array.isArray(value) ? value : [value];
|
|
7
|
+
return [...list];
|
|
8
|
+
}
|
|
9
|
+
/** Collect `shortcuts` declarations from capture plugins. */
|
|
10
|
+
export function collectPluginShortcuts(plugins) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const plugin of plugins) {
|
|
13
|
+
out.push(...normalizeShortcutList(plugin.shortcuts));
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** Resolve plugin shortcut options into host-ready entries. */
|
|
18
|
+
export function resolvePluginShortcuts(plugins) {
|
|
19
|
+
return collectPluginShortcuts(plugins).map((entry) => ({
|
|
20
|
+
id: entry.id,
|
|
21
|
+
shortcut: resolveShortcut(entry.shortcut),
|
|
22
|
+
...(entry.reportTypes ? { reportTypes: entry.reportTypes } : {}),
|
|
23
|
+
run: entry.run,
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* First matching shortcut wins (`preventDefault` is the caller's responsibility).
|
|
28
|
+
*/
|
|
29
|
+
export function matchRegisteredShortcut(event, shortcuts, reportType) {
|
|
30
|
+
for (const entry of shortcuts) {
|
|
31
|
+
if (entry.reportTypes &&
|
|
32
|
+
!pluginAppliesToReportType({ reportTypes: entry.reportTypes }, reportType)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (shouldOpenFromShortcut(event, entry.shortcut)) {
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { FeedbackCapturePlugin } from "@rose42/feedback-capture";
|
|
2
|
+
import type { FeedbackReportType } from "../reportType.js";
|
|
3
|
+
import type { T3FeedbackUiContext } from "./host.js";
|
|
4
|
+
import type { T3FeedbackPluginShortcut } from "./shortcuts.js";
|
|
5
|
+
export type { FeedbackReportType };
|
|
6
|
+
export type { T3FeedbackUiContext } from "./host.js";
|
|
7
|
+
/**
|
|
8
|
+
* Shared panel regions plugins may register into.
|
|
9
|
+
* Shell owns FAB + send-action cluster only; panel body is slot-hosted plugins.
|
|
10
|
+
*/
|
|
11
|
+
export type T3FeedbackUiSlot = "beforeComment" | "comment" | "attachments" | "actionsTools";
|
|
12
|
+
export declare const T3_FEEDBACK_UI_SLOTS: readonly T3FeedbackUiSlot[];
|
|
13
|
+
export type T3FeedbackUiMountResult = {
|
|
14
|
+
dispose?: () => void;
|
|
15
|
+
/** Optional imperative handle for the shell (draft restore, focus). */
|
|
16
|
+
api?: unknown;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* A UI fragment owned by a plugin (or built-in) and mounted into a shared slot.
|
|
20
|
+
*/
|
|
21
|
+
export type T3FeedbackUiContribution = {
|
|
22
|
+
id: string;
|
|
23
|
+
slot: T3FeedbackUiSlot;
|
|
24
|
+
/** Lower first. Default: `0`. */
|
|
25
|
+
order?: number;
|
|
26
|
+
/** When set, host hides this contribution outside these report types. */
|
|
27
|
+
reportTypes?: readonly FeedbackReportType[];
|
|
28
|
+
mount: (container: HTMLElement, ctx: T3FeedbackUiContext) => void | (() => void) | T3FeedbackUiMountResult;
|
|
29
|
+
};
|
|
30
|
+
/** Capture plugin that may also register overlay UI and/or shortcut chords. */
|
|
31
|
+
export type FeedbackCapturePluginWithUi = FeedbackCapturePlugin & {
|
|
32
|
+
/**
|
|
33
|
+
* Optional human label for the panel.
|
|
34
|
+
* When set without `ui`, auto-registers a live preview chip in `beforeComment`.
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
37
|
+
/**
|
|
38
|
+
* When set, only collect (and show gated UI/shortcuts) for these report types
|
|
39
|
+
* (omit = all report types).
|
|
40
|
+
*/
|
|
41
|
+
reportTypes?: readonly FeedbackReportType[];
|
|
42
|
+
ui?: T3FeedbackUiContribution | readonly T3FeedbackUiContribution[];
|
|
43
|
+
/**
|
|
44
|
+
* Global keyboard chords owned by this plugin.
|
|
45
|
+
* Registered for the lifetime of the feedback mount (panel open or closed).
|
|
46
|
+
*/
|
|
47
|
+
shortcuts?: T3FeedbackPluginShortcut | readonly T3FeedbackPluginShortcut[];
|
|
48
|
+
};
|
|
49
|
+
export type MountedUiContribution = {
|
|
50
|
+
id: string;
|
|
51
|
+
slot: T3FeedbackUiSlot;
|
|
52
|
+
host: HTMLElement;
|
|
53
|
+
api?: unknown;
|
|
54
|
+
dispose: () => void;
|
|
55
|
+
};
|
|
56
|
+
/** Sort contributions for stable mount order within and across slots. */
|
|
57
|
+
export declare function sortUiContributions(contributions: readonly T3FeedbackUiContribution[]): T3FeedbackUiContribution[];
|
|
58
|
+
/**
|
|
59
|
+
* Mount contributions into `[data-slot="…"]` hosts under `root`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function mountSlotContributions(root: ParentNode, contributions: readonly T3FeedbackUiContribution[], ctx: T3FeedbackUiContext): {
|
|
62
|
+
mounted: MountedUiContribution[];
|
|
63
|
+
dispose: () => void;
|
|
64
|
+
syncReportType: (type: FeedbackReportType) => void;
|
|
65
|
+
};
|
|
66
|
+
/** Collect explicit `ui` contributions from capture plugins. */
|
|
67
|
+
export declare function collectPluginUiContributions(plugins: readonly FeedbackCapturePluginWithUi[]): T3FeedbackUiContribution[];
|
|
68
|
+
/**
|
|
69
|
+
* Plugins with `label` and no `ui` get an auto preview contribution in `beforeComment`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function pluginsNeedingLabelPreview(plugins: readonly FeedbackCapturePluginWithUi[]): Array<FeedbackCapturePluginWithUi & {
|
|
72
|
+
label: string;
|
|
73
|
+
}>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
export const T3_FEEDBACK_UI_SLOTS = [
|
|
2
|
+
"beforeComment",
|
|
3
|
+
"comment",
|
|
4
|
+
"attachments",
|
|
5
|
+
"actionsTools",
|
|
6
|
+
];
|
|
7
|
+
function normalizeMountResult(result) {
|
|
8
|
+
if (typeof result === "function") {
|
|
9
|
+
return { dispose: result };
|
|
10
|
+
}
|
|
11
|
+
if (result && typeof result === "object") {
|
|
12
|
+
return {
|
|
13
|
+
dispose: result.dispose ?? (() => undefined),
|
|
14
|
+
...(result.api !== undefined ? { api: result.api } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
return { dispose: () => undefined };
|
|
18
|
+
}
|
|
19
|
+
function contributionVisible(contribution, reportType) {
|
|
20
|
+
const allowed = contribution.reportTypes;
|
|
21
|
+
if (!allowed || allowed.length === 0)
|
|
22
|
+
return true;
|
|
23
|
+
return allowed.includes(reportType);
|
|
24
|
+
}
|
|
25
|
+
/** Sort contributions for stable mount order within and across slots. */
|
|
26
|
+
export function sortUiContributions(contributions) {
|
|
27
|
+
return [...contributions].sort((a, b) => {
|
|
28
|
+
const slotDelta = T3_FEEDBACK_UI_SLOTS.indexOf(a.slot) - T3_FEEDBACK_UI_SLOTS.indexOf(b.slot);
|
|
29
|
+
if (slotDelta !== 0)
|
|
30
|
+
return slotDelta;
|
|
31
|
+
const orderDelta = (a.order ?? 0) - (b.order ?? 0);
|
|
32
|
+
if (orderDelta !== 0)
|
|
33
|
+
return orderDelta;
|
|
34
|
+
return a.id.localeCompare(b.id);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Mount contributions into `[data-slot="…"]` hosts under `root`.
|
|
39
|
+
*/
|
|
40
|
+
export function mountSlotContributions(root, contributions, ctx) {
|
|
41
|
+
const mounted = [];
|
|
42
|
+
const unsubReport = ctx.subscribeReportType((type) => {
|
|
43
|
+
for (const entry of mounted) {
|
|
44
|
+
const contrib = contributions.find((c) => c.id === entry.id);
|
|
45
|
+
if (!contrib)
|
|
46
|
+
continue;
|
|
47
|
+
entry.host.hidden = !contributionVisible(contrib, type);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
for (const contribution of sortUiContributions(contributions)) {
|
|
51
|
+
const slotEl = root.querySelector(`[data-slot="${contribution.slot}"]`);
|
|
52
|
+
if (!(slotEl instanceof HTMLElement))
|
|
53
|
+
continue;
|
|
54
|
+
const host = document.createElement("div");
|
|
55
|
+
host.dataset.uiId = contribution.id;
|
|
56
|
+
host.hidden = !contributionVisible(contribution, ctx.getReportType());
|
|
57
|
+
slotEl.append(host);
|
|
58
|
+
const { dispose, api } = normalizeMountResult(contribution.mount(host, ctx));
|
|
59
|
+
mounted.push({
|
|
60
|
+
id: contribution.id,
|
|
61
|
+
slot: contribution.slot,
|
|
62
|
+
host,
|
|
63
|
+
...(api !== undefined ? { api } : {}),
|
|
64
|
+
dispose,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
mounted,
|
|
69
|
+
syncReportType: (type) => {
|
|
70
|
+
for (const entry of mounted) {
|
|
71
|
+
const contrib = contributions.find((c) => c.id === entry.id);
|
|
72
|
+
if (!contrib)
|
|
73
|
+
continue;
|
|
74
|
+
entry.host.hidden = !contributionVisible(contrib, type);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
dispose: () => {
|
|
78
|
+
unsubReport();
|
|
79
|
+
for (let i = mounted.length - 1; i >= 0; i--) {
|
|
80
|
+
try {
|
|
81
|
+
mounted[i].dispose();
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
mounted.length = 0;
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Collect explicit `ui` contributions from capture plugins. */
|
|
92
|
+
export function collectPluginUiContributions(plugins) {
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const plugin of plugins) {
|
|
95
|
+
const ui = plugin.ui;
|
|
96
|
+
if (!ui)
|
|
97
|
+
continue;
|
|
98
|
+
if (Array.isArray(ui)) {
|
|
99
|
+
out.push(...ui);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
out.push(ui);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Plugins with `label` and no `ui` get an auto preview contribution in `beforeComment`.
|
|
109
|
+
*/
|
|
110
|
+
export function pluginsNeedingLabelPreview(plugins) {
|
|
111
|
+
return plugins.filter((plugin) => typeof plugin.label === "string" && plugin.label.trim().length > 0 && plugin.ui === undefined);
|
|
112
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Dispatched on `window` to open the mounted feedback panel. */
|
|
2
|
+
export declare const T3_FEEDBACK_OPEN_EVENT = "t3-feedback:open";
|
|
3
|
+
/** Dispatched on `window` to close the mounted feedback panel. */
|
|
4
|
+
export declare const T3_FEEDBACK_CLOSE_EVENT = "t3-feedback:close";
|
|
5
|
+
/** Open the feedback panel (no-op if the client is not mounted). */
|
|
6
|
+
export declare function openT3Feedback(): void;
|
|
7
|
+
/** Close the feedback panel (no-op if the client is not mounted). */
|
|
8
|
+
export declare function closeT3Feedback(): void;
|
package/dist/controls.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Dispatched on `window` to open the mounted feedback panel. */
|
|
2
|
+
export const T3_FEEDBACK_OPEN_EVENT = "t3-feedback:open";
|
|
3
|
+
/** Dispatched on `window` to close the mounted feedback panel. */
|
|
4
|
+
export const T3_FEEDBACK_CLOSE_EVENT = "t3-feedback:close";
|
|
5
|
+
/** Open the feedback panel (no-op if the client is not mounted). */
|
|
6
|
+
export function openT3Feedback() {
|
|
7
|
+
if (typeof window === "undefined")
|
|
8
|
+
return;
|
|
9
|
+
window.dispatchEvent(new CustomEvent(T3_FEEDBACK_OPEN_EVENT));
|
|
10
|
+
}
|
|
11
|
+
/** Close the feedback panel (no-op if the client is not mounted). */
|
|
12
|
+
export function closeT3Feedback() {
|
|
13
|
+
if (typeof window === "undefined")
|
|
14
|
+
return;
|
|
15
|
+
window.dispatchEvent(new CustomEvent(T3_FEEDBACK_CLOSE_EVENT));
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +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";
|
package/dist/index.js
ADDED
|
@@ -0,0 +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";
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
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";
|
|
4
|
+
export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./bridge/parse.js";
|
|
5
|
+
/** Resolve a `customPlugins` entry against the Vite project root (bare ids stay bare). */
|
|
6
|
+
export declare function resolveCustomPluginModuleId(entry: string, root: string): string;
|
|
7
|
+
/** Vite plugin that exposes POST /__t3-feedback and bridges envelopes to T3. */
|
|
8
|
+
export declare function t3FeedbackPlugin(options?: T3FeedbackPluginOptions): Plugin;
|
|
9
|
+
export default t3FeedbackPlugin;
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
import { handleT3FeedbackRequest } from "./bridge/http.js";
|
|
4
|
+
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";
|
|
7
|
+
export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./bridge/parse.js";
|
|
8
|
+
/** Resolve a `customPlugins` entry against the Vite project root (bare ids stay bare). */
|
|
9
|
+
export function resolveCustomPluginModuleId(entry, root) {
|
|
10
|
+
const trimmed = entry.trim();
|
|
11
|
+
if (!trimmed)
|
|
12
|
+
return trimmed;
|
|
13
|
+
if (path.isAbsolute(trimmed) || trimmed.startsWith("\0"))
|
|
14
|
+
return normalizePath(trimmed);
|
|
15
|
+
if (trimmed.startsWith(".")) {
|
|
16
|
+
return normalizePath(path.resolve(root, trimmed));
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
function resolvePluginOptions(options) {
|
|
21
|
+
if (!options)
|
|
22
|
+
return undefined;
|
|
23
|
+
const maxEntries = options.consoleLogs?.maxEntries;
|
|
24
|
+
if (maxEntries === undefined || !Number.isFinite(maxEntries)) {
|
|
25
|
+
return Object.keys(options).length > 0 ? options : undefined;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
...options,
|
|
29
|
+
consoleLogs: {
|
|
30
|
+
...options.consoleLogs,
|
|
31
|
+
maxEntries: Math.max(1, Math.floor(maxEntries)),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const DEFAULT_THREAD_REUSE = "ask";
|
|
36
|
+
const VIRTUAL_CLIENT_ID = "virtual:t3-feedback-client";
|
|
37
|
+
const RESOLVED_VIRTUAL_CLIENT_ID = `\0${VIRTUAL_CLIENT_ID}`;
|
|
38
|
+
/** Browser-safe URL for the virtual module (bare `virtual:` is not a fetchable scheme). */
|
|
39
|
+
const VIRTUAL_CLIENT_URL = `/@id/${VIRTUAL_CLIENT_ID}`;
|
|
40
|
+
function readRawBody(req) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const chunks = [];
|
|
43
|
+
req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
44
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
45
|
+
req.on("error", reject);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function connectRequestToFetch(req) {
|
|
49
|
+
const url = req.url ?? "/";
|
|
50
|
+
const method = req.method ?? "GET";
|
|
51
|
+
const headers = new Headers();
|
|
52
|
+
for (const [key, value] of Object.entries(req.headers ?? {})) {
|
|
53
|
+
if (value === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
for (const item of value)
|
|
57
|
+
headers.append(key, item);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
headers.set(key, value);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const init = { method, headers };
|
|
64
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
65
|
+
const raw = await readRawBody(req);
|
|
66
|
+
if (raw.length > 0) {
|
|
67
|
+
init.body = Uint8Array.from(raw);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return new Request(`http://vite.local${url}`, init);
|
|
71
|
+
}
|
|
72
|
+
async function writeFetchResponse(res, response) {
|
|
73
|
+
res.statusCode = response.status;
|
|
74
|
+
response.headers.forEach((value, key) => {
|
|
75
|
+
res.setHeader(key, value);
|
|
76
|
+
});
|
|
77
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
78
|
+
res.end(buf);
|
|
79
|
+
}
|
|
80
|
+
/** Vite plugin that exposes POST /__t3-feedback and bridges envelopes to T3. */
|
|
81
|
+
export function t3FeedbackPlugin(options = {}) {
|
|
82
|
+
const threadTitlePrefix = options.threadTitle?.trim() || undefined;
|
|
83
|
+
const workspaceRootHints = options.workspaceRootHints;
|
|
84
|
+
const injectClient = options.injectClient !== false;
|
|
85
|
+
const threadReuse = options.threadReuse ?? DEFAULT_THREAD_REUSE;
|
|
86
|
+
const reportTypeGlyph = resolveReportTypeGlyph(options.reportTypeGlyph);
|
|
87
|
+
const pickConfig = resolvePickConfig(options.pick);
|
|
88
|
+
const version = options.version?.trim() || undefined;
|
|
89
|
+
const pluginOptions = resolvePluginOptions(options.pluginOptions);
|
|
90
|
+
const clientConfig = {
|
|
91
|
+
shortcut: resolveShortcut(options.shortcut, options.shortcutKey),
|
|
92
|
+
endpoint: T3_FEEDBACK_API_PATH,
|
|
93
|
+
fab: resolveFabConfig(options.fab),
|
|
94
|
+
threadReuse,
|
|
95
|
+
pick: pickConfig,
|
|
96
|
+
screenshot: resolveScreenshotConfig(options.screenshot, pickConfig.cropPaddingPx),
|
|
97
|
+
record: resolveRecordConfig(options.record),
|
|
98
|
+
reportTypeGlyph,
|
|
99
|
+
activePlugins: resolveActivePluginList(options.activePlugins),
|
|
100
|
+
...(pluginOptions ? { pluginOptions } : {}),
|
|
101
|
+
...(version ? { version } : {}),
|
|
102
|
+
};
|
|
103
|
+
let viteRoot = process.cwd();
|
|
104
|
+
const customPluginEntries = (options.customPlugins ?? [])
|
|
105
|
+
.map((entry) => (typeof entry === "string" ? entry.trim() : ""))
|
|
106
|
+
.filter(Boolean);
|
|
107
|
+
const bridgeServerOptions = {
|
|
108
|
+
basePath: T3_FEEDBACK_API_PATH,
|
|
109
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
110
|
+
...(options.openOrigin ? { openOrigin: options.openOrigin } : {}),
|
|
111
|
+
...(workspaceRootHints ? { workspaceRootHints } : {}),
|
|
112
|
+
...(threadTitlePrefix ? { threadTitlePrefix } : {}),
|
|
113
|
+
reportTypeGlyph,
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
name: "t3-feedback",
|
|
117
|
+
apply: "serve",
|
|
118
|
+
configResolved(config) {
|
|
119
|
+
viteRoot = config.root;
|
|
120
|
+
},
|
|
121
|
+
resolveId(id) {
|
|
122
|
+
if (id === VIRTUAL_CLIENT_ID)
|
|
123
|
+
return RESOLVED_VIRTUAL_CLIENT_ID;
|
|
124
|
+
},
|
|
125
|
+
load(id) {
|
|
126
|
+
if (id !== RESOLVED_VIRTUAL_CLIENT_ID)
|
|
127
|
+
return;
|
|
128
|
+
const resolvedCustoms = customPluginEntries.map((entry) => resolveCustomPluginModuleId(entry, viteRoot));
|
|
129
|
+
const customImports = resolvedCustoms
|
|
130
|
+
.map((modId, i) => `import * as custom${i} from ${JSON.stringify(modId)};`)
|
|
131
|
+
.join("\n");
|
|
132
|
+
const customArgs = resolvedCustoms.length === 0
|
|
133
|
+
? ""
|
|
134
|
+
: `, [\n${resolvedCustoms
|
|
135
|
+
.map((_, i) => ` ...normalizeCustomPluginExport(custom${i})`)
|
|
136
|
+
.join(",\n")}\n]`;
|
|
137
|
+
const clientImports = resolvedCustoms.length === 0
|
|
138
|
+
? `import { mountT3FeedbackFromConfig } from "@rose42/t3-feedback/client";`
|
|
139
|
+
: `import { mountT3FeedbackFromConfig, normalizeCustomPluginExport } from "@rose42/t3-feedback/client";`;
|
|
140
|
+
return `
|
|
141
|
+
${clientImports}
|
|
142
|
+
${customImports}
|
|
143
|
+
mountT3FeedbackFromConfig(${JSON.stringify(clientConfig)}${customArgs});
|
|
144
|
+
`;
|
|
145
|
+
},
|
|
146
|
+
transformIndexHtml() {
|
|
147
|
+
if (!injectClient)
|
|
148
|
+
return;
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
tag: "script",
|
|
152
|
+
attrs: { type: "module", src: VIRTUAL_CLIENT_URL },
|
|
153
|
+
injectTo: "body",
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
},
|
|
157
|
+
configureServer(server) {
|
|
158
|
+
server.middlewares.use(async (req, res, next) => {
|
|
159
|
+
const url = req.url ?? "";
|
|
160
|
+
// Fast path: ignore unrelated requests without buffering the body.
|
|
161
|
+
// Subpaths (`/threads`, `/settle`, `/open`) also start with the API prefix.
|
|
162
|
+
if (!url.startsWith(T3_FEEDBACK_API_PATH)) {
|
|
163
|
+
next();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const request = await connectRequestToFetch(req);
|
|
168
|
+
const response = await handleT3FeedbackRequest(request, bridgeServerOptions);
|
|
169
|
+
if (!response) {
|
|
170
|
+
next();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await writeFetchResponse(res, response);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
console.error("[t3-feedback]", message);
|
|
178
|
+
res.statusCode = 502;
|
|
179
|
+
res.setHeader("Content-Type", "application/json");
|
|
180
|
+
res.end(JSON.stringify({ error: message }));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export default t3FeedbackPlugin;
|