@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
package/dist/types.js ADDED
@@ -0,0 +1,283 @@
1
+ export const DEFAULT_FAB_CONFIG = {
2
+ visibility: "mobile",
3
+ mobileMaxWidth: 719,
4
+ position: { right: 16, bottom: 16 },
5
+ size: 44,
6
+ };
7
+ export function resolveFabConfig(fab) {
8
+ return {
9
+ visibility: fab?.visibility ?? DEFAULT_FAB_CONFIG.visibility,
10
+ mobileMaxWidth: fab?.mobileMaxWidth ?? DEFAULT_FAB_CONFIG.mobileMaxWidth,
11
+ position: fab?.position ?? DEFAULT_FAB_CONFIG.position,
12
+ size: fab?.size ?? DEFAULT_FAB_CONFIG.size,
13
+ };
14
+ }
15
+ /** Built-in emoji glyphs for Bug / Feedback / Suggestion when using `"emoji"` or a partial map. */
16
+ export const DEFAULT_REPORT_TYPE_EMOJIS = {
17
+ bug: "🐛",
18
+ feedback: "💬",
19
+ suggestion: "💡",
20
+ };
21
+ export const DEFAULT_REPORT_TYPE_GLYPH = "icon";
22
+ function trimGlyph(value) {
23
+ if (typeof value !== "string")
24
+ return undefined;
25
+ const trimmed = value.trim();
26
+ return trimmed || undefined;
27
+ }
28
+ function glyphFromMap(map, lower, label) {
29
+ return trimGlyph(map[lower]) ?? trimGlyph(map[label]) ?? DEFAULT_REPORT_TYPE_EMOJIS[lower];
30
+ }
31
+ function isReportTypeGlyphMap(value) {
32
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
33
+ }
34
+ export function resolveReportTypeGlyph(value) {
35
+ if (value === "emoji")
36
+ return "emoji";
37
+ if (value === "icon" || value == null)
38
+ return DEFAULT_REPORT_TYPE_GLYPH;
39
+ if (!isReportTypeGlyphMap(value))
40
+ return DEFAULT_REPORT_TYPE_GLYPH;
41
+ return {
42
+ bug: glyphFromMap(value, "bug", "Bug"),
43
+ feedback: glyphFromMap(value, "feedback", "Feedback"),
44
+ suggestion: glyphFromMap(value, "suggestion", "Suggestion"),
45
+ };
46
+ }
47
+ export const DEFAULT_SHORTCUT = {
48
+ key: "f",
49
+ mod: true,
50
+ shift: true,
51
+ alt: false,
52
+ };
53
+ /** Default chord for element picking: Ctrl/⌘+Shift+E. */
54
+ export const DEFAULT_PICK_SHORTCUT = {
55
+ key: "e",
56
+ mod: true,
57
+ shift: true,
58
+ alt: false,
59
+ };
60
+ /** Default chord for Capture screenshot: Ctrl/⌘+Shift+S. */
61
+ export const DEFAULT_SCREENSHOT_SHORTCUT = {
62
+ key: "s",
63
+ mod: true,
64
+ shift: true,
65
+ alt: false,
66
+ };
67
+ /** Default chord for Record (frame burst): Ctrl/⌘+Shift+R. */
68
+ export const DEFAULT_RECORD_SHORTCUT = {
69
+ key: "r",
70
+ mod: true,
71
+ shift: true,
72
+ alt: false,
73
+ };
74
+ /** Default Vite DEV endpoint for opening a file in the editor. */
75
+ export const DEFAULT_OPEN_IN_EDITOR_ENDPOINT = "/__open-in-editor";
76
+ export function resolveOpenInEditorConfig(value) {
77
+ if (value === false)
78
+ return null;
79
+ if (value === undefined || value === true) {
80
+ return { endpoint: DEFAULT_OPEN_IN_EDITOR_ENDPOINT };
81
+ }
82
+ const endpoint = typeof value.endpoint === "string" && value.endpoint.trim()
83
+ ? value.endpoint.trim()
84
+ : DEFAULT_OPEN_IN_EDITOR_ENDPOINT;
85
+ const urlTemplate = typeof value.urlTemplate === "string" && value.urlTemplate.trim()
86
+ ? value.urlTemplate.trim()
87
+ : undefined;
88
+ return urlTemplate ? { endpoint, urlTemplate } : { endpoint };
89
+ }
90
+ export function resolvePickConfig(pick) {
91
+ const shortcut = pick?.shortcut === false
92
+ ? null
93
+ : pick?.shortcut !== undefined
94
+ ? resolveShortcut(pick.shortcut)
95
+ : { ...DEFAULT_PICK_SHORTCUT };
96
+ const sourceFramework = pick?.sourceFramework === "react" ||
97
+ pick?.sourceFramework === "next" ||
98
+ pick?.sourceFramework === "vue" ||
99
+ pick?.sourceFramework === "svelte" ||
100
+ pick?.sourceFramework === "qwik" ||
101
+ pick?.sourceFramework === "locator" ||
102
+ pick?.sourceFramework === "none" ||
103
+ pick?.sourceFramework === "auto"
104
+ ? pick.sourceFramework
105
+ : "auto";
106
+ return {
107
+ shortcut,
108
+ afterPick: pick?.afterPick ?? "crop",
109
+ cropPaddingPx: pick?.cropPaddingPx ?? 12,
110
+ includeInnerHTML: pick?.includeInnerHTML === true,
111
+ maxInnerHTMLLength: pick?.maxInnerHTMLLength ?? 500,
112
+ styles: pick?.styles ?? "none",
113
+ sourceFramework,
114
+ openInEditor: resolveOpenInEditorConfig(pick?.openInEditor),
115
+ };
116
+ }
117
+ export function resolveScreenshotConfig(screenshot, pickCropPaddingPx = 12) {
118
+ const shortcut = screenshot?.shortcut === false
119
+ ? null
120
+ : screenshot?.shortcut !== undefined
121
+ ? resolveShortcut(screenshot.shortcut)
122
+ : { ...DEFAULT_SCREENSHOT_SHORTCUT };
123
+ const rawMax = screenshot?.maxImages;
124
+ const maxImages = rawMax === undefined || !Number.isFinite(rawMax)
125
+ ? 5
126
+ : Math.min(10, Math.max(1, Math.floor(rawMax)));
127
+ return {
128
+ shortcut,
129
+ crop: screenshot?.crop ?? "ui",
130
+ cropPaddingPx: screenshot?.cropPaddingPx ?? pickCropPaddingPx,
131
+ maxImages,
132
+ };
133
+ }
134
+ export function resolveRecordConfig(record) {
135
+ const shortcut = record?.shortcut === false
136
+ ? null
137
+ : record?.shortcut !== undefined
138
+ ? resolveShortcut(record.shortcut)
139
+ : { ...DEFAULT_RECORD_SHORTCUT };
140
+ return { shortcut };
141
+ }
142
+ /**
143
+ * Resolve plugin/mount shortcut options.
144
+ * - No `shortcut` / `shortcutKey`: Ctrl/⌘+Shift+F
145
+ * - `shortcut` object: omitted modifiers are `false`
146
+ * - Legacy `shortcutKey` only: same key with Ctrl/⌘+Shift
147
+ */
148
+ export function resolveShortcut(shortcut, shortcutKey) {
149
+ if (shortcut) {
150
+ return {
151
+ key: (shortcut.key ?? shortcutKey ?? DEFAULT_SHORTCUT.key).toLowerCase(),
152
+ mod: shortcut.mod ?? false,
153
+ shift: shortcut.shift ?? false,
154
+ alt: shortcut.alt ?? false,
155
+ };
156
+ }
157
+ if (shortcutKey) {
158
+ return {
159
+ ...DEFAULT_SHORTCUT,
160
+ key: shortcutKey.toLowerCase(),
161
+ };
162
+ }
163
+ return { ...DEFAULT_SHORTCUT };
164
+ }
165
+ /** Human-readable chord label (e.g. `Ctrl/⌘+Shift+F` or `F (Ctrl/⌘+Shift+F in fields)`). */
166
+ export function formatShortcutLabel(shortcut) {
167
+ const parts = [];
168
+ if (shortcut.mod)
169
+ parts.push("Ctrl/⌘");
170
+ if (shortcut.alt)
171
+ parts.push("Alt");
172
+ if (shortcut.shift)
173
+ parts.push("Shift");
174
+ parts.push(shortcut.key.toUpperCase());
175
+ const primary = parts.join("+");
176
+ if (shortcutRequiresModifier(shortcut))
177
+ return primary;
178
+ return `${primary} (Ctrl/⌘+Shift+${shortcut.key.toUpperCase()} in fields)`;
179
+ }
180
+ /** Whether `event` matches the resolved chord (modifiers must match exactly). */
181
+ export function matchesShortcut(event, shortcut) {
182
+ if (event.key.toLowerCase() !== shortcut.key)
183
+ return false;
184
+ const modPressed = event.ctrlKey || event.metaKey;
185
+ if (shortcut.mod !== modPressed)
186
+ return false;
187
+ if (shortcut.shift !== event.shiftKey)
188
+ return false;
189
+ if (shortcut.alt !== event.altKey)
190
+ return false;
191
+ return true;
192
+ }
193
+ /** True when the chord requires at least one modifier (global; works while typing). */
194
+ export function shortcutRequiresModifier(shortcut) {
195
+ return shortcut.mod || shortcut.shift || shortcut.alt;
196
+ }
197
+ /**
198
+ * Fixed escape hatch for bare-key shortcuts: same key with Ctrl/⌘+Shift
199
+ * (works in editable fields where the bare key is ignored).
200
+ */
201
+ export function matchesBareKeyOverride(event, shortcut) {
202
+ if (shortcutRequiresModifier(shortcut))
203
+ return false;
204
+ if (event.key.toLowerCase() !== shortcut.key)
205
+ return false;
206
+ const modPressed = event.ctrlKey || event.metaKey;
207
+ return modPressed && event.shiftKey && !event.altKey;
208
+ }
209
+ const NON_TEXT_INPUT_TYPES = new Set([
210
+ "button",
211
+ "checkbox",
212
+ "radio",
213
+ "file",
214
+ "reset",
215
+ "submit",
216
+ "range",
217
+ "color",
218
+ "image",
219
+ "hidden",
220
+ ]);
221
+ /**
222
+ * True when `target` is an editable / typing surface where a bare key
223
+ * should insert text instead of opening feedback.
224
+ */
225
+ export function isEditableKeyboardTarget(target) {
226
+ if (!(target instanceof HTMLElement))
227
+ return false;
228
+ if (target.isContentEditable)
229
+ return true;
230
+ const tag = target.tagName;
231
+ if (tag === "TEXTAREA" || tag === "SELECT")
232
+ return true;
233
+ if (tag === "INPUT") {
234
+ const type = target.type.toLowerCase();
235
+ return !NON_TEXT_INPUT_TYPES.has(type);
236
+ }
237
+ return Boolean(target.closest('[contenteditable="true"], [role="textbox"]'));
238
+ }
239
+ /**
240
+ * Whether this keydown should open feedback.
241
+ * - Configured modified chords: exact match anywhere (including inputs).
242
+ * - Bare-key shortcuts: bare key outside editable fields, plus Ctrl/⌘+Shift+key
243
+ * anywhere as a global override while typing.
244
+ */
245
+ export function shouldOpenFromShortcut(event, shortcut) {
246
+ if (shortcutRequiresModifier(shortcut)) {
247
+ return matchesShortcut(event, shortcut);
248
+ }
249
+ if (matchesBareKeyOverride(event, shortcut))
250
+ return true;
251
+ if (!matchesShortcut(event, shortcut))
252
+ return false;
253
+ return !isEditableKeyboardTarget(event.target);
254
+ }
255
+ /** Built-in capture plugins selectable via `activePlugins`. Comment is always on. */
256
+ export const T3_BUILTIN_PLUGIN_IDS = [
257
+ "pageContext",
258
+ "consoleLogs",
259
+ "screenshot",
260
+ "elementTarget",
261
+ ];
262
+ /**
263
+ * Resolve which built-ins are active.
264
+ * - `active` omitted → all four (today’s defaults)
265
+ * - explicit list → only those ids (unknown strings ignored)
266
+ * - `includeConsoleLogs: false` strips `consoleLogs` (deprecated alias)
267
+ */
268
+ export function resolveActivePlugins(active, includeConsoleLogs) {
269
+ const allowed = new Set(T3_BUILTIN_PLUGIN_IDS);
270
+ const ids = active === undefined
271
+ ? [...T3_BUILTIN_PLUGIN_IDS]
272
+ : active.filter((id) => allowed.has(id));
273
+ const set = new Set(ids);
274
+ if (includeConsoleLogs === false) {
275
+ set.delete("consoleLogs");
276
+ }
277
+ return set;
278
+ }
279
+ /** Stable ordered list of active built-in ids (for JSON client config). */
280
+ export function resolveActivePluginList(active, includeConsoleLogs) {
281
+ const set = resolveActivePlugins(active, includeConsoleLogs);
282
+ return T3_BUILTIN_PLUGIN_IDS.filter((id) => set.has(id));
283
+ }
@@ -0,0 +1,160 @@
1
+ # Devcontainer: T3 Code
2
+
3
+ The Docker/devcontainer image ships with:
4
+
5
+ | Tool | Binary | Role |
6
+ | -------------------------------------------------------- | ------------------------ | -------------------------------------------------- |
7
+ | Node.js | `node` / `npm` | Runtime (pinned in root `Dockerfile`) |
8
+ | pnpm | `pnpm` | Package manager |
9
+ | [T3 Code](https://github.com/pingdotgg/t3code) | `t3` | Web UI / harness (`T3CODE_PORT`, default **3775**) |
10
+ | [Cursor Agent](https://cursor.com/docs/cli/installation) | `agent` / `cursor-agent` | T3 provider CLI (auth separately) |
11
+
12
+ Node / pnpm match the root `Dockerfile`. Rebuild the container after Dockerfile changes so these tools are present.
13
+
14
+ The container runs as **root**. Named volumes survive rebuilds:
15
+
16
+ | Volume | Mount | Persists |
17
+ | -------------------- | ------------------------ | ---------------------------------------------- |
18
+ | `t3-feedback-t3` | `/root/.t3` | T3 pairing, Connect credentials, `cloudflared` |
19
+ | `t3-feedback-cursor` | `/root/.cursor` | Cursor CLI config / project state |
20
+ | `cursor-agent-auth` | `/root/.config/cursor` | Cursor Agent login (`auth.json` on Linux) |
21
+ | `glab-auth` | `/root/.config/glab-cli` | `glab` auth |
22
+
23
+ See [`.devcontainer/devcontainer.json`](../.devcontainer/devcontainer.json).
24
+
25
+ ## Sibling dependency (`@rose42/feedback-capture`)
26
+
27
+ `package.json` depends on `@rose42/feedback-capture@0.1.0` from the registry.
28
+ The devcontainer bind-mounts the **host sibling** clone and `pnpm link`s it after
29
+ install so local edits to `feedback-capture` are used while developing:
30
+
31
+ | Host | Container |
32
+ | --------------------------------------------- | ------------------------------ |
33
+ | `${localWorkspaceFolder}/../feedback-capture` | `/workspaces/feedback-capture` |
34
+ | this repo | `/workspaces/t3-feedback` |
35
+
36
+ Clone both next to each other before opening the container:
37
+
38
+ ```text
39
+ Documents/git/feedback-capture
40
+ Documents/git/t3-feedback
41
+ ```
42
+
43
+ `postCreateCommand` installs both packages, builds the sibling, then symlinks
44
+ `node_modules/@rose42/feedback-capture` → `/workspaces/feedback-capture` (see
45
+ [`link-local-feedback-capture.sh`](../.devcontainer/link-local-feedback-capture.sh)).
46
+ That leaves `package.json` / the lockfile on registry `0.1.0`. Outside the
47
+ container (CI, plain clones), install uses npm with no sibling required.
48
+
49
+ On every container create/start, [`.devcontainer/t3-setup.sh`](../.devcontainer/t3-setup.sh)
50
+ sets `PRETTY_HOSTNAME=t3-feedback` and prints whether **T3 Connect** is linked.
51
+
52
+ ## Open the environment
53
+
54
+ 1. Install the **Dev Containers** extension in Cursor / VS Code.
55
+ 2. Open this repo → Command Palette → **Dev Containers: Reopen in Container**.
56
+ 3. `postCreateCommand` runs `t3-setup.sh` + `pnpm install` (+ sibling build).
57
+ 4. Allow automatic tasks if prompted so **t3** starts on folder open ([`.vscode/tasks.json`](../.vscode/tasks.json)).
58
+
59
+ ## Two ways to reach this server
60
+
61
+ | Path | What it is | Shows in T3 Code app? |
62
+ | ------------------------------------ | --------------------------------------------------------------------------- | -------------------------------------- |
63
+ | **Local browser** via forwarded port | `http://localhost:$T3CODE_PORT` + pairing token (default **3775**) | No |
64
+ | **T3 Connect** | Cloud link + managed relay for phone / [app.t3.codes](https://app.t3.codes) | **Yes** — only after `t3 connect link` |
65
+
66
+ Browser pairing and Connect are independent. Pairing a browser does **not** register the environment in the app.
67
+
68
+ ## Local UI (`T3CODE_PORT`)
69
+
70
+ Port comes from **`T3CODE_PORT`** (default **3775**), set once in [`.devcontainer/devcontainer.json`](../.devcontainer/devcontainer.json) `remoteEnv` and inherited by the folder-open **t3** task and `t3-setup.sh`.
71
+
72
+ T3 starts automatically when the folder opens (`runOn: folderOpen`). To start manually: Command Palette → **Tasks: Run Task** → **t3**, or:
73
+
74
+ ```bash
75
+ t3 --no-browser --port "${T3CODE_PORT:-3775}"
76
+ ```
77
+
78
+ Keep this project’s port unique vs other concurrent containers (`3773`, `3774`, …). Same host port ⇒ same browser origin ⇒ sessions kick each other off (`Invalid session token signature`).
79
+
80
+ `T3_FEEDBACK_ORIGIN` / `T3_FEEDBACK_OPEN_ORIGIN` follow the same `T3CODE_PORT` default so the plugin bridges to this instance. Consumer projects must set both to **their** T3 port — not this plugin container's — or Open links will embed this environment's id/label.
81
+
82
+ ### Override on conflict
83
+
84
+ 1. On the **host**: `export T3CODE_PORT=3776` (or any free port), then **Dev Containers: Rebuild Container**.
85
+ Or edit the default in `remoteEnv` (`${localEnv:T3CODE_PORT:3775}`).
86
+ 2. Update `forwardPorts` and `portsAttributes` in `devcontainer.json` to the same number (those keys cannot interpolate env).
87
+ 3. Reopen / restart the **t3** task; open `http://localhost:$T3CODE_PORT` from the Ports panel.
88
+
89
+ Mint another pairing token while the server is running:
90
+
91
+ ```bash
92
+ t3 pair
93
+ ```
94
+
95
+ ## T3 Connect (required for the phone / desktop app list)
96
+
97
+ OAuth cannot be completed non-interactively inside the container. Linking is **one-time per environment volume**, then persists on `t3-feedback-t3`.
98
+
99
+ ### Link this environment
100
+
101
+ 1. Command Palette → **Tasks: Run Task** → **t3: connect link**
102
+ (or `t3 connect link --headless`)
103
+ 2. Open the printed URL on your **host** browser, sign in, paste the code back into the terminal.
104
+ 3. Decline any background-service / systemd prompt (**n**). Do not use `t3 service install` in this image — the folder-open **t3** task owns the process.
105
+ 4. Restart **t3** (stop the task terminal / re-run the **t3** task).
106
+ 5. **Tasks: Run Task** → **t3: connect status** (or `t3 connect status`).
107
+
108
+ Expect:
109
+
110
+ ```text
111
+ Exposure: enabled
112
+ Environment link: provisioned
113
+ ```
114
+
115
+ After that, the app should list **t3-feedback** (from `/etc/machine-info` `PRETTY_HOSTNAME`).
116
+
117
+ ### Multi-container notes
118
+
119
+ - Each container needs its **own** `~/.t3` volume and its **own** `t3 connect link`.
120
+ - Display names come from Linux `PRETTY_HOSTNAME` (set by `t3-setup.sh` here).
121
+ - Connect has an environment-link quota (~3). Linking a 4th can fail with HTTP 403 — deregister an unused environment in the app (**Settings → T3 Connect**), then link again.
122
+
123
+ Useful commands: `t3 connect status`, `t3 connect unlink`, `t3 connect logout`. Broader options: [T3 remote access](https://github.com/pingdotgg/t3code/blob/main/docs/user/remote-access.md).
124
+
125
+ ## Providers
126
+
127
+ **Cursor Agent** is preinstalled (`agent` / `cursor-agent` on PATH). Authenticate once inside the container (survives rebuilds via the `cursor-agent-auth` volume):
128
+
129
+ ```bash
130
+ agent login
131
+ ```
132
+
133
+ On Linux the CLI writes `~/.config/cursor/auth.json`. `AGENT_CLI_CREDENTIAL_STORE=file` keeps credentials in that file instead of a keyring. Or set `CURSOR_API_KEY`.
134
+
135
+ ## Quick checklist (new machine / fresh volume)
136
+
137
+ 1. Ensure sibling `feedback-capture` is cloned next to this repo.
138
+ 2. Rebuild/reopen the devcontainer; read the `t3-setup.sh` banner in the create/start output.
139
+ 3. Allow automatic tasks (or run **t3**) → open forwarded **`T3CODE_PORT`** (default **3775**).
140
+ 4. If the banner says Connect is **not** linked: run **t3: connect link** → host OAuth → decline service prompt → restart **t3** → **t3: connect status** shows provisioned.
141
+ 5. `agent login` once (or set `CURSOR_API_KEY`), then start a thread against this workspace.
142
+
143
+ ## Troubleshooting
144
+
145
+ | Symptom | Fix |
146
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
147
+ | `t3: command not found` | Rebuild the image (`npm install -g t3@latest` is in the Dockerfile) |
148
+ | `Cannot find module '@rose42/feedback-capture'` / missing sibling | Clone `feedback-capture` as a sibling of this repo; reopen container so the bind mount exists |
149
+ | `cursor-agent: command not found` | Rebuild the image; confirm `/root/.local/bin` is on `PATH` |
150
+ | T3 UI unreachable | Confirm `T3CODE_PORT` forwarded; use Ports panel “Open in Browser” |
151
+ | Pairing one container kicks the other offline | Use unique host ports (`T3CODE_PORT`, default **3775**; feedback-capture: **3774**; often **3773**). Re-pair each tab to its own URL. |
152
+ | Need a different local port | Set host `T3CODE_PORT`, sync `forwardPorts` / `portsAttributes`, rebuild (see [Override on conflict](#override-on-conflict)) |
153
+ | Second environment missing in the T3 Code app | Connect not linked here (or `t3` not running). Run **t3: connect link**, restart **t3** |
154
+ | Stale T3 | `npm install -g t3@latest`, or rebuild the container |
155
+
156
+ ## Security notes
157
+
158
+ - Do not bake secrets into the image. Auth state belongs in the named `~/.t3` and `~/.config/cursor` volumes.
159
+ - Prefer T3 Connect over exposing the container on the LAN.
160
+ - `.dockerignore` keeps `.git`, `node_modules`, `.env*`, and build artifacts out of the image build context.
package/package.json ADDED
@@ -0,0 +1,107 @@
1
+ {
2
+ "name": "@rose42/t3-feedback",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "DEV-only: capture app feedback and send it to a T3 Code thread (unofficial community tool)",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "feedback",
9
+ "screenshot",
10
+ "bug-report",
11
+ "devtools",
12
+ "vite",
13
+ "t3"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://gitlab.com/rose42/t3-feedback.git"
18
+ },
19
+ "homepage": "https://gitlab.com/rose42/t3-feedback",
20
+ "bugs": {
21
+ "url": "https://gitlab.com/rose42/t3-feedback/-/issues"
22
+ },
23
+ "sideEffects": false,
24
+ "bin": {
25
+ "t3-feedback": "./dist/cli/bin.js"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "import": "./dist/index.js"
34
+ },
35
+ "./plugin": {
36
+ "types": "./dist/plugin.d.ts",
37
+ "import": "./dist/plugin.js"
38
+ },
39
+ "./bridge": {
40
+ "types": "./dist/bridge/index.d.ts",
41
+ "import": "./dist/bridge/index.js"
42
+ },
43
+ "./client": {
44
+ "types": "./dist/client/index.d.ts",
45
+ "import": "./dist/client/index.js"
46
+ },
47
+ "./controls": {
48
+ "types": "./dist/controls.d.ts",
49
+ "import": "./dist/controls.js"
50
+ },
51
+ "./cli": {
52
+ "types": "./dist/cli/index.d.ts",
53
+ "import": "./dist/cli/index.js"
54
+ }
55
+ },
56
+ "files": [
57
+ "dist",
58
+ "docs",
59
+ "!dist/**/*.map"
60
+ ],
61
+ "scripts": {
62
+ "dev": "tsc -p tsconfig.build.json --watch",
63
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
64
+ "typecheck": "tsc -p tsconfig.json --noEmit",
65
+ "lint": "eslint .",
66
+ "format": "prettier --write .",
67
+ "format:check": "prettier --check .",
68
+ "test": "vitest run",
69
+ "test:watch": "vitest",
70
+ "release": "bash scripts/release.sh",
71
+ "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm test && pnpm build"
72
+ },
73
+ "engines": {
74
+ "node": ">=24"
75
+ },
76
+ "packageManager": "pnpm@10.21.0",
77
+ "pnpm": {
78
+ "onlyBuiltDependencies": [
79
+ "esbuild"
80
+ ]
81
+ },
82
+ "dependencies": {
83
+ "@rose42/feedback-capture": "0.1.0",
84
+ "ws": "^8.18.3"
85
+ },
86
+ "peerDependencies": {
87
+ "vite": "^6.0.0 || ^7.0.0"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "vite": {
91
+ "optional": true
92
+ }
93
+ },
94
+ "devDependencies": {
95
+ "@eslint/js": "^9.39.1",
96
+ "@types/node": "^22.20.2",
97
+ "@types/ws": "^8.18.1",
98
+ "eslint": "^9.39.1",
99
+ "eslint-config-prettier": "^10.1.8",
100
+ "happy-dom": "^20.14.3",
101
+ "prettier": "^3.6.2",
102
+ "typescript": "^5.9.3",
103
+ "typescript-eslint": "^8.46.4",
104
+ "vite": "^7.0.0",
105
+ "vitest": "^3.2.7"
106
+ }
107
+ }