@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
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
|
+
}
|