@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,165 @@
1
+ import path from "node:path";
2
+ import { CLI_NAME, PACKAGE_NAME } from "./constants.js";
3
+ import { detectProject } from "./detect.js";
4
+ import { detectPackageManager, installDevDependency } from "./pm.js";
5
+ import { confirm } from "./prompt.js";
6
+ import { wireNextApp } from "./wire/next.js";
7
+ import { wireNodeBridge } from "./wire/node.js";
8
+ import { wireViteConfig, writeNewViteConfig } from "./wire/vite.js";
9
+ function formatWire(result) {
10
+ const rel = result.path;
11
+ switch (result.status) {
12
+ case "written":
13
+ return `write ${rel}${result.detail ? ` (${result.detail})` : ""}`;
14
+ case "updated":
15
+ return `update ${rel}${result.detail ? ` (${result.detail})` : ""}`;
16
+ case "skipped":
17
+ return `skip ${rel} — ${result.detail}`;
18
+ case "manual":
19
+ return `manual ${rel} — ${result.detail}`;
20
+ }
21
+ }
22
+ function printPlan(detection, pm, willInstall) {
23
+ console.log("");
24
+ console.log(`${CLI_NAME} init`);
25
+ console.log(` root: ${detection.root}`);
26
+ console.log(` host: ${detection.host} (${detection.reasons.join("; ")})`);
27
+ console.log(` pm: ${pm}`);
28
+ console.log(` pkg: ${PACKAGE_NAME}${detection.alreadyInstalled ? " (already installed)" : ""}`);
29
+ if (detection.alreadyWiredHint) {
30
+ console.log(" note: project already looks wired (will skip existing files)");
31
+ }
32
+ console.log("");
33
+ console.log("Plan:");
34
+ if (willInstall) {
35
+ console.log(` 1. install ${PACKAGE_NAME} as a devDependency via ${pm}`);
36
+ }
37
+ else {
38
+ console.log(" 1. skip install");
39
+ }
40
+ if (detection.host === "vite") {
41
+ console.log(detection.viteConfigPath
42
+ ? ` 2. wire Vite plugin into ${path.basename(detection.viteConfigPath)}`
43
+ : " 2. create vite.config.ts with t3FeedbackPlugin");
44
+ }
45
+ else if (detection.host === "next") {
46
+ console.log(" 2. add App Router route: app/api/t3-feedback/[[...path]]/route.ts");
47
+ console.log(" 3. add client bootstrap: app/components/T3FeedbackDev.tsx");
48
+ console.log(" 4. you mount <T3FeedbackDev /> in app/layout.tsx (printed after)");
49
+ }
50
+ else {
51
+ console.log(" 2. write t3-feedback-bridge.mjs (Fetch bridge stub, no UI)");
52
+ }
53
+ console.log("");
54
+ }
55
+ async function applyWire(detection) {
56
+ if (detection.host === "vite") {
57
+ if (detection.viteConfigPath) {
58
+ return [wireViteConfig(detection.viteConfigPath)];
59
+ }
60
+ return [writeNewViteConfig(path.join(detection.root, "vite.config.ts"))];
61
+ }
62
+ if (detection.host === "next") {
63
+ const { route, client, layoutHint } = wireNextApp(detection.root);
64
+ console.log("");
65
+ console.log(layoutHint);
66
+ return [route, client];
67
+ }
68
+ return [wireNodeBridge(detection.root)];
69
+ }
70
+ /**
71
+ * Hybrid init: detect host → confirm → install if missing → wire files.
72
+ */
73
+ export async function runInit(options = {}) {
74
+ const cwd = path.resolve(options.cwd ?? process.cwd());
75
+ let detection = detectProject(cwd);
76
+ if (options.host) {
77
+ detection = { ...detection, host: options.host, reasons: [`forced --host ${options.host}`] };
78
+ }
79
+ if (!detection.packageJsonPath) {
80
+ console.error(`No package.json in ${cwd}. Run this from your app root.`);
81
+ return 1;
82
+ }
83
+ const pm = detectPackageManager(cwd);
84
+ const willInstall = !options.noInstall && !detection.alreadyInstalled;
85
+ printPlan(detection, pm, willInstall);
86
+ if (options.dryRun) {
87
+ console.log("Dry run — no changes made.");
88
+ return 0;
89
+ }
90
+ const ok = options.yes === true ? true : await confirm("Apply this plan?", true);
91
+ if (!ok) {
92
+ console.log("Aborted.");
93
+ return 1;
94
+ }
95
+ if (willInstall) {
96
+ console.log("");
97
+ console.log(`Installing with ${pm}…`);
98
+ const result = installDevDependency(cwd, PACKAGE_NAME, pm);
99
+ if (!result.ok) {
100
+ console.error(`Install failed (${result.command}): ${result.detail}`);
101
+ console.error("Re-run with --no-install after adding the dependency yourself.");
102
+ return 1;
103
+ }
104
+ }
105
+ const results = await applyWire(detection);
106
+ console.log("");
107
+ console.log("Result:");
108
+ for (const result of results) {
109
+ console.log(` ${formatWire(result)}`);
110
+ if (result.status === "manual") {
111
+ console.log("");
112
+ console.log("Snippet to paste:");
113
+ console.log(result.snippet);
114
+ }
115
+ }
116
+ console.log("");
117
+ console.log("Next: start your DEV server with T3 Code running, then open the feedback panel.");
118
+ console.log(`Docs: https://gitlab.com/rose42/t3-feedback`);
119
+ return 0;
120
+ }
121
+ export function parseInitArgs(argv) {
122
+ const options = {};
123
+ for (let i = 0; i < argv.length; i += 1) {
124
+ const arg = argv[i];
125
+ if (arg === "--yes" || arg === "-y")
126
+ options.yes = true;
127
+ else if (arg === "--dry-run")
128
+ options.dryRun = true;
129
+ else if (arg === "--no-install")
130
+ options.noInstall = true;
131
+ else if (arg === "--cwd" && argv[i + 1]) {
132
+ options.cwd = argv[i + 1];
133
+ i += 1;
134
+ }
135
+ else if (arg === "--host" && argv[i + 1]) {
136
+ const host = argv[i + 1];
137
+ i += 1;
138
+ if (host === "next" || host === "vite" || host === "node")
139
+ options.host = host;
140
+ else
141
+ throw new Error(`Unknown --host ${host} (expected next|vite|node)`);
142
+ }
143
+ else if (arg === "--help" || arg === "-h") {
144
+ throw new Error("help");
145
+ }
146
+ else if (arg?.startsWith("-")) {
147
+ throw new Error(`Unknown flag ${arg}`);
148
+ }
149
+ }
150
+ return options;
151
+ }
152
+ export function printInitHelp() {
153
+ console.log(`Usage: ${CLI_NAME} init [options]
154
+
155
+ Detect Next / Vite / Node, confirm, install ${PACKAGE_NAME} if missing, then wire files.
156
+
157
+ Options:
158
+ --yes, -y Skip confirmation
159
+ --dry-run Show plan only
160
+ --no-install Do not install the package (wire only)
161
+ --host <kind> Force next | vite | node
162
+ --cwd <path> App root (default: cwd)
163
+ -h, --help Show help
164
+ `);
165
+ }
@@ -0,0 +1,12 @@
1
+ export type PackageManager = "pnpm" | "npm";
2
+ export declare function detectPackageManager(root: string): PackageManager;
3
+ export type InstallResult = {
4
+ ok: true;
5
+ command: string;
6
+ } | {
7
+ ok: false;
8
+ command: string;
9
+ detail: string;
10
+ };
11
+ /** Install `packageName` as a devDependency using the detected package manager. */
12
+ export declare function installDevDependency(root: string, packageName: string, pm?: PackageManager): InstallResult;
package/dist/cli/pm.js ADDED
@@ -0,0 +1,49 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ export function detectPackageManager(root) {
5
+ const pkgPath = path.join(root, "package.json");
6
+ if (fs.existsSync(pkgPath)) {
7
+ try {
8
+ const data = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
9
+ const pm = data.packageManager?.trim() ?? "";
10
+ if (pm.startsWith("pnpm@"))
11
+ return "pnpm";
12
+ if (pm.startsWith("npm@"))
13
+ return "npm";
14
+ }
15
+ catch {
16
+ // fall through
17
+ }
18
+ }
19
+ if (fs.existsSync(path.join(root, "pnpm-lock.yaml")))
20
+ return "pnpm";
21
+ if (fs.existsSync(path.join(root, "package-lock.json")))
22
+ return "npm";
23
+ if (fs.existsSync(path.join(root, "yarn.lock")))
24
+ return "npm";
25
+ if (fs.existsSync(path.join(root, "bun.lock")) || fs.existsSync(path.join(root, "bun.lockb"))) {
26
+ return "npm";
27
+ }
28
+ const ua = process.env.npm_config_user_agent ?? "";
29
+ if (ua.includes("pnpm"))
30
+ return "pnpm";
31
+ return "npm";
32
+ }
33
+ /** Install `packageName` as a devDependency using the detected package manager. */
34
+ export function installDevDependency(root, packageName, pm = detectPackageManager(root)) {
35
+ const args = pm === "pnpm" ? ["add", "-D", packageName] : ["install", "-D", packageName];
36
+ const command = `${pm} ${args.join(" ")}`;
37
+ const result = spawnSync(pm, args, {
38
+ cwd: root,
39
+ encoding: "utf8",
40
+ stdio: "inherit",
41
+ });
42
+ if (result.status === 0)
43
+ return { ok: true, command };
44
+ const detail = result.error?.message ||
45
+ result.stderr?.trim() ||
46
+ result.stdout?.trim() ||
47
+ `exit ${result.status ?? "unknown"}`;
48
+ return { ok: false, command, detail };
49
+ }
@@ -0,0 +1,2 @@
1
+ /** Ask a yes/no question. Default is yes when `defaultYes` is true. */
2
+ export declare function confirm(question: string, defaultYes?: boolean): Promise<boolean>;
@@ -0,0 +1,19 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ /** Ask a yes/no question. Default is yes when `defaultYes` is true. */
4
+ export async function confirm(question, defaultYes = true) {
5
+ if (!input.isTTY || !output.isTTY) {
6
+ return defaultYes;
7
+ }
8
+ const rl = readline.createInterface({ input, output });
9
+ try {
10
+ const hint = defaultYes ? "Y/n" : "y/N";
11
+ const answer = (await rl.question(`${question} [${hint}] `)).trim().toLowerCase();
12
+ if (!answer)
13
+ return defaultYes;
14
+ return answer === "y" || answer === "yes";
15
+ }
16
+ finally {
17
+ rl.close();
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ import type { WireResult } from "./vite.js";
2
+ export type NextWireResult = {
3
+ route: WireResult;
4
+ client: WireResult;
5
+ layoutHint: string;
6
+ };
7
+ /** Write App Router route handler + client bootstrap component. */
8
+ export declare function wireNextApp(root: string): NextWireResult;
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PACKAGE_NAME } from "../constants.js";
4
+ function ensureDir(filePath) {
5
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
6
+ }
7
+ function writeNewFile(filePath, contents, skipIfExistsDetail) {
8
+ if (fs.existsSync(filePath)) {
9
+ return { status: "skipped", path: filePath, detail: skipIfExistsDetail };
10
+ }
11
+ ensureDir(filePath);
12
+ fs.writeFileSync(filePath, contents, "utf8");
13
+ return { status: "written", path: filePath };
14
+ }
15
+ const ROUTE_SOURCE = `import { handleT3FeedbackRequest } from "${PACKAGE_NAME}/bridge";
16
+
17
+ const bridge = {
18
+ basePath: "/api/t3-feedback",
19
+ workspaceRootHints: [process.cwd()],
20
+ };
21
+
22
+ async function handle(request: Request) {
23
+ const response = await handleT3FeedbackRequest(request, bridge);
24
+ return response ?? new Response(null, { status: 404 });
25
+ }
26
+
27
+ export const GET = handle;
28
+ export const POST = handle;
29
+ `;
30
+ const CLIENT_SOURCE = `"use client";
31
+
32
+ import { useEffect } from "react";
33
+
34
+ /**
35
+ * DEV-only T3 feedback overlay. Mount once near the root layout.
36
+ * The bridge refuses requests unless NODE_ENV=development.
37
+ */
38
+ export function T3FeedbackDev() {
39
+ useEffect(() => {
40
+ if (process.env.NODE_ENV !== "development") return;
41
+ let cancelled = false;
42
+ let unmount: (() => void) | undefined;
43
+ void import("${PACKAGE_NAME}/client").then(({ mountT3Feedback }) => {
44
+ if (cancelled) return;
45
+ const api = mountT3Feedback({
46
+ endpoint: "/api/t3-feedback",
47
+ });
48
+ unmount = () => api.unmount();
49
+ });
50
+ return () => {
51
+ cancelled = true;
52
+ unmount?.();
53
+ };
54
+ }, []);
55
+ return null;
56
+ }
57
+ `;
58
+ /** Write App Router route handler + client bootstrap component. */
59
+ export function wireNextApp(root) {
60
+ const routePath = path.join(root, "app", "api", "t3-feedback", "[[...path]]", "route.ts");
61
+ const clientPath = path.join(root, "app", "components", "T3FeedbackDev.tsx");
62
+ const route = writeNewFile(routePath, ROUTE_SOURCE, "route handler already exists");
63
+ const client = writeNewFile(clientPath, CLIENT_SOURCE, "client component already exists");
64
+ const layoutHint = `Add to app/layout.tsx (DEV-only mount is inside the component):
65
+
66
+ import { T3FeedbackDev } from "./components/T3FeedbackDev";
67
+ // …
68
+ <T3FeedbackDev />
69
+ `;
70
+ return { route, client, layoutHint };
71
+ }
@@ -0,0 +1,3 @@
1
+ import type { WireResult } from "./vite.js";
2
+ /** Write a standalone Node HTTP bridge stub (no UI). */
3
+ export declare function wireNodeBridge(root: string): WireResult;
@@ -0,0 +1,76 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { PACKAGE_NAME } from "../constants.js";
4
+ const BRIDGE_SOURCE = `/**
5
+ * Generic Node bridge stub for ${PACKAGE_NAME}.
6
+ * DEV-only: handleT3FeedbackRequest returns 404 unless NODE_ENV=development.
7
+ *
8
+ * Mount this Fetch handler in your HTTP framework, or run the tiny server below.
9
+ */
10
+ import { createServer } from "node:http";
11
+ import { handleT3FeedbackRequest } from "${PACKAGE_NAME}/bridge";
12
+
13
+ const basePath = "/__t3-feedback";
14
+ const bridge = {
15
+ basePath,
16
+ workspaceRootHints: [process.cwd()],
17
+ };
18
+
19
+ async function toFetchRequest(req, body) {
20
+ const host = req.headers.host ?? "127.0.0.1";
21
+ const url = new URL(req.url ?? "/", \`http://\${host}\`);
22
+ const headers = new Headers();
23
+ for (const [key, value] of Object.entries(req.headers)) {
24
+ if (value === undefined) continue;
25
+ if (Array.isArray(value)) for (const item of value) headers.append(key, item);
26
+ else headers.set(key, value);
27
+ }
28
+ const init = { method: req.method ?? "GET", headers };
29
+ if (body?.length && req.method !== "GET" && req.method !== "HEAD") {
30
+ init.body = body;
31
+ }
32
+ return new Request(url, init);
33
+ }
34
+
35
+ const server = createServer((req, res) => {
36
+ const chunks = [];
37
+ req.on("data", (c) => chunks.push(c));
38
+ req.on("end", async () => {
39
+ try {
40
+ const body = Buffer.concat(chunks);
41
+ const request = await toFetchRequest(req, body);
42
+ const response = await handleT3FeedbackRequest(request, bridge);
43
+ if (!response) {
44
+ res.statusCode = 404;
45
+ res.end("Not found");
46
+ return;
47
+ }
48
+ res.statusCode = response.status;
49
+ response.headers.forEach((value, key) => res.setHeader(key, value));
50
+ res.end(Buffer.from(await response.arrayBuffer()));
51
+ } catch (error) {
52
+ res.statusCode = 500;
53
+ res.end(error instanceof Error ? error.message : String(error));
54
+ }
55
+ });
56
+ });
57
+
58
+ const port = Number(process.env.PORT ?? 8787);
59
+ server.listen(port, () => {
60
+ console.log(\`[t3-feedback] bridge on http://127.0.0.1:\${port}\${basePath}\`);
61
+ console.log("[t3-feedback] Mount the /client overlay from your frontend separately.");
62
+ });
63
+ `;
64
+ /** Write a standalone Node HTTP bridge stub (no UI). */
65
+ export function wireNodeBridge(root) {
66
+ const target = path.join(root, "t3-feedback-bridge.mjs");
67
+ if (fs.existsSync(target)) {
68
+ return { status: "skipped", path: target, detail: "bridge stub already exists" };
69
+ }
70
+ fs.writeFileSync(target, BRIDGE_SOURCE, "utf8");
71
+ return {
72
+ status: "written",
73
+ path: target,
74
+ detail: "run with: node t3-feedback-bridge.mjs (NODE_ENV=development)",
75
+ };
76
+ }
@@ -0,0 +1,25 @@
1
+ export type WireResult = {
2
+ status: "written";
3
+ path: string;
4
+ detail?: string;
5
+ } | {
6
+ status: "updated";
7
+ path: string;
8
+ detail?: string;
9
+ } | {
10
+ status: "skipped";
11
+ path: string;
12
+ detail: string;
13
+ } | {
14
+ status: "manual";
15
+ path: string;
16
+ detail: string;
17
+ snippet: string;
18
+ };
19
+ /**
20
+ * Add the Vite plugin to an existing config, or report a manual snippet if
21
+ * the file shape is too unusual to edit safely.
22
+ */
23
+ export declare function wireViteConfig(viteConfigPath: string): WireResult;
24
+ /** Create a minimal vite.config.ts when none exists. */
25
+ export declare function writeNewViteConfig(targetPath: string): WireResult;
@@ -0,0 +1,71 @@
1
+ import fs from "node:fs";
2
+ import { PACKAGE_NAME } from "../constants.js";
3
+ const IMPORT_LINE = `import { t3FeedbackPlugin } from "${PACKAGE_NAME}/plugin";`;
4
+ const PLUGIN_ENTRY = `t3FeedbackPlugin({
5
+ workspaceRootHints: [process.cwd()],
6
+ })`;
7
+ /**
8
+ * Add the Vite plugin to an existing config, or report a manual snippet if
9
+ * the file shape is too unusual to edit safely.
10
+ */
11
+ export function wireViteConfig(viteConfigPath) {
12
+ const original = fs.readFileSync(viteConfigPath, "utf8");
13
+ if (original.includes("t3FeedbackPlugin") || original.includes(`${PACKAGE_NAME}/plugin`)) {
14
+ return {
15
+ status: "skipped",
16
+ path: viteConfigPath,
17
+ detail: "t3FeedbackPlugin already referenced",
18
+ };
19
+ }
20
+ let next = original;
21
+ if (!next.includes(IMPORT_LINE)) {
22
+ const importBlock = next.match(/^(?:import[\s\S]*?;\s*)+/m);
23
+ if (importBlock) {
24
+ const end = importBlock.index + importBlock[0].length;
25
+ next = `${next.slice(0, end)}${IMPORT_LINE}\n${next.slice(end)}`;
26
+ }
27
+ else {
28
+ next = `${IMPORT_LINE}\n${next}`;
29
+ }
30
+ }
31
+ const pluginsOpen = next.match(/plugins\s*:\s*\[/);
32
+ if (!pluginsOpen || pluginsOpen.index === undefined) {
33
+ const snippet = `${IMPORT_LINE}\n\n// add to defineConfig({ plugins: [ … ] })\n${PLUGIN_ENTRY},`;
34
+ return {
35
+ status: "manual",
36
+ path: viteConfigPath,
37
+ detail: "could not find a plugins: [ … ] array to edit safely",
38
+ snippet,
39
+ };
40
+ }
41
+ const insertAt = pluginsOpen.index + pluginsOpen[0].length;
42
+ const before = next.slice(0, insertAt);
43
+ const after = next.slice(insertAt);
44
+ // `plugins: []` — expand to a short multi-line list; otherwise prepend an entry.
45
+ if (/^\s*\]/.test(after)) {
46
+ next = `${before}\n ${PLUGIN_ENTRY},\n ${after.replace(/^\s*/, "")}`;
47
+ }
48
+ else {
49
+ const sep = after.startsWith("\n") ? "" : "\n";
50
+ next = `${before}\n ${PLUGIN_ENTRY},${sep}${after}`;
51
+ }
52
+ fs.writeFileSync(viteConfigPath, next, "utf8");
53
+ return { status: "updated", path: viteConfigPath, detail: "inserted t3FeedbackPlugin" };
54
+ }
55
+ /** Create a minimal vite.config.ts when none exists. */
56
+ export function writeNewViteConfig(targetPath) {
57
+ if (fs.existsSync(targetPath)) {
58
+ return wireViteConfig(targetPath);
59
+ }
60
+ const contents = `import { defineConfig } from "vite";
61
+ ${IMPORT_LINE}
62
+
63
+ export default defineConfig({
64
+ plugins: [
65
+ ${PLUGIN_ENTRY},
66
+ ],
67
+ });
68
+ `;
69
+ fs.writeFileSync(targetPath, contents, "utf8");
70
+ return { status: "written", path: targetPath };
71
+ }
@@ -0,0 +1,23 @@
1
+ import type { SessionThreadReportType } from "./sessionThread.js";
2
+ /** How long a thread stays in the activity list without live settle / dismiss. */
3
+ export declare const ACTIVE_THREAD_TTL_MS: number;
4
+ /** How often the client polls `/__t3-feedback/threads` while any are working. */
5
+ export declare const ACTIVE_THREAD_POLL_MS = 18000;
6
+ export type ActiveThreadRecord = {
7
+ threadId: string;
8
+ threadTitle: string;
9
+ openUrl: string;
10
+ projectId: string;
11
+ /** T3 environment id for deep links; optional for older sessionStorage rows. */
12
+ environmentId?: string;
13
+ pathname: string;
14
+ reportType: SessionThreadReportType;
15
+ startedAt: number;
16
+ };
17
+ /** Drop TTL-expired entries; returns the survivors. */
18
+ export declare function pruneActiveThreads(now?: number, ttlMs?: number): ActiveThreadRecord[];
19
+ export declare function listActiveThreads(now?: number, ttlMs?: number): ActiveThreadRecord[];
20
+ export declare function upsertActiveThread(record: ActiveThreadRecord): ActiveThreadRecord[];
21
+ export declare function removeActiveThread(threadId: string): ActiveThreadRecord[];
22
+ export declare function clearActiveThreads(): void;
23
+ export declare function removeActiveThreads(threadIds: Iterable<string>): ActiveThreadRecord[];
@@ -0,0 +1,89 @@
1
+ const STORAGE_KEY = "t3-feedback:active-threads";
2
+ /** How long a thread stays in the activity list without live settle / dismiss. */
3
+ export const ACTIVE_THREAD_TTL_MS = 45 * 60 * 1000;
4
+ /** How often the client polls `/__t3-feedback/threads` while any are working. */
5
+ export const ACTIVE_THREAD_POLL_MS = 18_000;
6
+ function parseRecord(value) {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ return undefined;
9
+ const record = value;
10
+ const threadId = typeof record.threadId === "string" ? record.threadId.trim() : "";
11
+ const openUrl = typeof record.openUrl === "string" ? record.openUrl.trim() : "";
12
+ const projectId = typeof record.projectId === "string" ? record.projectId.trim() : "";
13
+ if (!threadId || !openUrl || !projectId)
14
+ return undefined;
15
+ const reportType = record.reportType;
16
+ if (reportType !== "bug" && reportType !== "feedback" && reportType !== "suggestion") {
17
+ return undefined;
18
+ }
19
+ const startedAt = typeof record.startedAt === "number" && Number.isFinite(record.startedAt)
20
+ ? record.startedAt
21
+ : 0;
22
+ const environmentId = typeof record.environmentId === "string" && record.environmentId.trim()
23
+ ? record.environmentId.trim()
24
+ : undefined;
25
+ return {
26
+ threadId,
27
+ threadTitle: typeof record.threadTitle === "string" ? record.threadTitle.trim() : "",
28
+ openUrl,
29
+ projectId,
30
+ ...(environmentId ? { environmentId } : {}),
31
+ pathname: typeof record.pathname === "string" ? record.pathname : "/",
32
+ reportType,
33
+ startedAt,
34
+ };
35
+ }
36
+ function readRaw() {
37
+ try {
38
+ const raw = sessionStorage.getItem(STORAGE_KEY);
39
+ if (!raw)
40
+ return [];
41
+ const parsed = JSON.parse(raw);
42
+ if (!Array.isArray(parsed))
43
+ return [];
44
+ return parsed.map(parseRecord).filter((r) => r !== undefined);
45
+ }
46
+ catch {
47
+ return [];
48
+ }
49
+ }
50
+ function writeRaw(records) {
51
+ try {
52
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify(records));
53
+ }
54
+ catch {
55
+ // Private mode / quota — activity is best-effort.
56
+ }
57
+ }
58
+ /** Drop TTL-expired entries; returns the survivors. */
59
+ export function pruneActiveThreads(now = Date.now(), ttlMs = ACTIVE_THREAD_TTL_MS) {
60
+ const next = readRaw().filter((r) => now - r.startedAt <= ttlMs);
61
+ writeRaw(next);
62
+ return next;
63
+ }
64
+ export function listActiveThreads(now = Date.now(), ttlMs = ACTIVE_THREAD_TTL_MS) {
65
+ return pruneActiveThreads(now, ttlMs);
66
+ }
67
+ export function upsertActiveThread(record) {
68
+ const pruned = pruneActiveThreads(Date.now());
69
+ const without = pruned.filter((r) => r.threadId !== record.threadId);
70
+ const next = [record, ...without];
71
+ writeRaw(next);
72
+ return next;
73
+ }
74
+ export function removeActiveThread(threadId) {
75
+ const next = readRaw().filter((r) => r.threadId !== threadId);
76
+ writeRaw(next);
77
+ return pruneActiveThreads();
78
+ }
79
+ export function clearActiveThreads() {
80
+ writeRaw([]);
81
+ }
82
+ export function removeActiveThreads(threadIds) {
83
+ const drop = new Set(threadIds);
84
+ if (drop.size === 0)
85
+ return pruneActiveThreads();
86
+ const next = readRaw().filter((r) => !drop.has(r.threadId));
87
+ writeRaw(next);
88
+ return pruneActiveThreads();
89
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Fullscreen crop dialog lives here; headless crop geometry + `cropDataUrl` come
3
+ * from `@rose42/feedback-capture`.
4
+ */
5
+ import { clientPointToNatural, containedImageLayout, cropDataUrl, rectFromPoints, type ContainedImageLayout, type CropRect } from "@rose42/feedback-capture";
6
+ export type { ContainedImageLayout, CropRect };
7
+ export { clientPointToNatural, containedImageLayout, cropDataUrl, rectFromPoints };
8
+ export type SelectScreenshotRegionCopy = {
9
+ hint: string;
10
+ cropLabel: string;
11
+ useFullLabel: string;
12
+ cancelLabel: string;
13
+ discardLabel: string;
14
+ };
15
+ export type SelectScreenshotRegionOptions = {
16
+ parent: HTMLElement;
17
+ dataUrl: string;
18
+ /** Prefill the marquee (natural image pixels); user can still adjust. */
19
+ initialRect?: CropRect;
20
+ /** When false, hide Discard (e.g. first capture before the image is in the gallery). Default true. */
21
+ allowDiscard?: boolean;
22
+ copy?: Partial<SelectScreenshotRegionCopy>;
23
+ };
24
+ /** Outcome of the fullscreen crop dialog. */
25
+ export type SelectScreenshotRegionResult = {
26
+ action: "cancel";
27
+ } | {
28
+ action: "discard";
29
+ } | {
30
+ action: "image";
31
+ dataUrl: string;
32
+ };
33
+ /** Cancel any crop dialogs still open under `parent` and drop their window listeners. */
34
+ export declare function abortScreenshotRegions(parent: ParentNode): void;
35
+ /**
36
+ * Fullscreen overlay: drag a region on the screenshot.
37
+ * Resolves with an image data URL, cancel (keep prior), or discard (drop the shot).
38
+ */
39
+ export declare function selectScreenshotRegion(options: SelectScreenshotRegionOptions): Promise<SelectScreenshotRegionResult>;