@rose42/t3-feedback 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,6 +101,7 @@ export default defineConfig({
101
101
  // // or { key: "f", mod: true, shift: true } for Ctrl/⌘+Shift+F
102
102
  // threadReuse: "ask", // or "session-route" | "never"
103
103
  // threadTitle: "Checkout", // optional title prefix
104
+ // modelSelection: { instanceId: "cursor", model: "auto" },
104
105
  // origin: "http://localhost:3773",
105
106
  }),
106
107
  ],
@@ -398,25 +399,26 @@ only clear the local activity list.
398
399
 
399
400
  ## Options
400
401
 
401
- | Option | Default | Notes |
402
- | -------------------- | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
403
- | `origin` | `http://localhost:3773` | Overridable via `T3_FEEDBACK_ORIGIN` / `T3_ORIGIN` |
404
- | `openOrigin` | same as `origin` | Host-browser URL for Open-in-T3 (`T3_FEEDBACK_OPEN_ORIGIN`). Set explicitly in multi-container setups; do not point at another project's T3. |
405
- | `threadReuse` | `"ask"` | `"ask"` replaces Send with New / ▶ {title} ▾: **Ctrl/⌘+Enter** soft-default, **Ctrl/⌘+Shift+Enter** New, **Alt+Enter** Continue; chevron lists project threads (search); `"session-route"` always reuses per pathname; `"never"` always creates |
406
- | `threadTitle` | _(none)_ | Optional prefix for new thread titles (`Bug: path · comment`) |
407
- | `workspaceRootHints` | `[cwd]` (+ `T3_FEEDBACK_WORKSPACE_HINT`) | Match T3 project by workspace path |
408
- | `shortcut` | _(omit for Ctrl/⌘+Shift+F)_ | When set, omitted `mod`/`shift`/`alt` are `false` (e.g. `{ key: "f" }` = bare F). Bare keys skip editable fields; Ctrl/⌘+Shift+key still opens while typing |
409
- | `shortcutKey` | _(deprecated)_ | Key-only alias; prefer `shortcut.key` |
410
- | `pick` | Ctrl/⌘+Shift+E, `afterPick: "crop"` | Pick shortcut (`false` to disable), after-pick capture/crop, padding; `openInEditor` (default on) makes resolved file paths clickable via Vite `/__open-in-editor` or a `urlTemplate` |
411
- | `screenshot` | Ctrl/⌘+Shift+S, `crop: "ui"`, `maxImages: 5` | Capture shortcut (`false` to disable); crop `"ui"` (on-demand from thumbs) \| `"auto"` \| `"none"`; gallery cap |
412
- | `record` | Ctrl/⌘+Shift+R | Record / frame-burst shortcut (`false` to disable); gated with `screenshot` in `activePlugins` |
413
- | `activePlugins` | all four built-ins | `pageContext` \| `consoleLogs` \| `screenshot` \| `elementTarget` — omit for defaults |
414
- | `pluginOptions` | `consoleLogs.maxEntries: 200` | Serializable options for selected built-ins |
415
- | `customPlugins` | `[]` | Vite-resolved module paths/ids imported into the injected client |
416
- | `injectClient` | `true` | HTML inject of the overlay |
417
- | `fab` | mobile-only, bottom-right | FAB visibility / position / size |
418
- | `reportTypeGlyph` | `"icon"` | `"icon"` (SVG + textual titles), `"emoji"`, or a map `{ Bug, Feedback, Suggestion }` — emoji/map also prefix new chat names |
419
- | `version` | _(none)_ | App version on bug/feedback reports; DEV also attaches HMR `#n` |
402
+ | Option | Default | Notes |
403
+ | -------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
404
+ | `origin` | `http://localhost:3773` | Overridable via `T3_FEEDBACK_ORIGIN` / `T3_ORIGIN` |
405
+ | `openOrigin` | same as `origin` | Host-browser URL for Open-in-T3 (`T3_FEEDBACK_OPEN_ORIGIN`). Set explicitly in multi-container setups; do not point at another project's T3. |
406
+ | `threadReuse` | `"ask"` | `"ask"` replaces Send with New / ▶ {title} ▾: **Ctrl/⌘+Enter** soft-default, **Ctrl/⌘+Shift+Enter** New, **Alt+Enter** Continue; chevron lists project threads (search); `"session-route"` always reuses per pathname; `"never"` always creates |
407
+ | `threadTitle` | _(none)_ | Optional prefix for new thread titles (`Bug: path · comment`) |
408
+ | `modelSelection` | `{ instanceId: "cursor", model: "default" }` | Provider/model for **new** threads. When omitted, prefers an existing same-project Cursor thread (e.g. Auto) instead of copying the first thread. Env: `T3_FEEDBACK_INSTANCE_ID` / `T3_FEEDBACK_MODEL` |
409
+ | `workspaceRootHints` | `[cwd]` (+ `T3_FEEDBACK_WORKSPACE_HINT`) | Match T3 project by workspace path |
410
+ | `shortcut` | _(omit for Ctrl/⌘+Shift+F)_ | When set, omitted `mod`/`shift`/`alt` are `false` (e.g. `{ key: "f" }` = bare F). Bare keys skip editable fields; Ctrl/⌘+Shift+key still opens while typing |
411
+ | `shortcutKey` | _(deprecated)_ | Key-only alias; prefer `shortcut.key` |
412
+ | `pick` | Ctrl/⌘+Shift+E, `afterPick: "crop"` | Pick shortcut (`false` to disable), after-pick capture/crop, padding; `openInEditor` (default on) makes resolved file paths clickable via Vite `/__open-in-editor` or a `urlTemplate`; `sourcePathPrefix` makes paths workspace-relative (e.g. `examples/vite-react`) also auto-derived by the Vite plugin when `workspaceRootHints` is an ancestor of the Vite root |
413
+ | `screenshot` | Ctrl/⌘+Shift+S, `crop: "ui"`, `maxImages: 5` | Capture shortcut (`false` to disable); crop `"ui"` (on-demand from thumbs) \| `"auto"` \| `"none"`; gallery cap |
414
+ | `record` | Ctrl/⌘+Shift+R | Record / frame-burst shortcut (`false` to disable); gated with `screenshot` in `activePlugins` |
415
+ | `activePlugins` | all four built-ins | `pageContext` \| `consoleLogs` \| `screenshot` \| `elementTarget` — omit for defaults |
416
+ | `pluginOptions` | `consoleLogs.maxEntries: 200` | Serializable options for selected built-ins |
417
+ | `customPlugins` | `[]` | Vite-resolved module paths/ids imported into the injected client |
418
+ | `injectClient` | `true` | HTML inject of the overlay |
419
+ | `fab` | mobile-only, bottom-right | FAB visibility / position / size |
420
+ | `reportTypeGlyph` | `"icon"` | `"icon"` (SVG + textual titles), `"emoji"`, or a map `{ Bug, Feedback, Suggestion }` — emoji/map also prefix new chat names |
421
+ | `version` | _(none)_ | App version on bug/feedback reports; DEV also attaches HMR `#n` |
420
422
 
421
423
  ## Environment
422
424
 
@@ -424,6 +426,8 @@ only clear the local activity list.
424
426
  | ---------------------------------- | ----------------------------------------------------------------------------------------- |
425
427
  | `T3_FEEDBACK_ORIGIN` / `T3_ORIGIN` | T3 HTTP origin |
426
428
  | `T3_FEEDBACK_OPEN_ORIGIN` | Browser origin for Open-in-T3 (default: same as `T3_FEEDBACK_ORIGIN`) |
429
+ | `T3_FEEDBACK_INSTANCE_ID` | Default provider instance for new threads (e.g. `cursor`) |
430
+ | `T3_FEEDBACK_MODEL` | Default model id for new threads (e.g. `auto`, `default`) |
427
431
  | `T3_FEEDBACK_TOKEN` | Pre-issued bearer (skips `t3 auth session issue`) |
428
432
  | `T3_FEEDBACK_WORKSPACE_HINT` | Extra workspace path hint |
429
433
  | `LAUNCH_EDITOR` | Editor binary for Vite `/__open-in-editor` (e.g. `cursor`, `code`) when auto-detect fails |
@@ -439,28 +443,58 @@ container avoid opening the wrong environment (check the Vite log line
439
443
  `T3_FEEDBACK_TOKEN` set. Browser needs `getDisplayMedia` (paste image works as fallback).
440
444
 
441
445
  When element pick resolves a component file, the ELEMENT chip path is a link that
442
- opens the IDE. Default is Vite’s `/__open-in-editor` (same as the error overlay) —
443
- that only works when the browser and Vite share a machine that can launch an
444
- editor (`LAUNCH_EDITOR=cursor` or `code` if auto-detect fails).
446
+ opens the IDE. Default is the bridge `GET {api}/open-editor?file=…` (same origin
447
+ as feedback posts), which launches the Cursor/VS Code **remote CLI** so
448
+ container `/workspaces/…` paths remap to the host. Vite’s `/__open-in-editor`
449
+ still works if you set `pick.openInEditor.endpoint` explicitly
450
+ (`LAUNCH_EDITOR=cursor` or `code` if auto-detect fails).
445
451
 
446
- When the **browser is on the host** and Vite runs in a **container/remote**, use a
447
- protocol URL instead so the OS / Cursor handles the open:
452
+ When the Vite/Next app lives in a **subfolder** of the T3 / IDE workspace (e.g.
453
+ `examples/vite-react`), set `workspaceRootHints` to the workspace root so the
454
+ plugin prefixes chip + agent paths (`examples/vite-react/src/App.tsx`). Or set
455
+ `pick.sourcePathPrefix` explicitly.
456
+
457
+ ```ts
458
+ t3FeedbackPlugin({
459
+ workspaceRootHints: [path.resolve(import.meta.dirname, "../..")],
460
+ pick: { sourceFramework: "react" },
461
+ });
462
+ ```
463
+
464
+ When the **browser is on the host** and the remote CLI is unavailable, use a
465
+ protocol URL with a **host** filesystem root — not the container `/workspaces/…`
466
+ path:
448
467
 
449
468
  ```ts
450
469
  t3FeedbackPlugin({
451
470
  pick: {
452
471
  openInEditor: {
453
- // `{file}` is whatever the sourcemap returns (often relative). Prefix with
454
- // an absolute project root when your scheme requires absolute paths.
455
- urlTemplate: `cursor://file${process.cwd()}/{file}:{line}:{column}`,
472
+ // Prefer the host checkout path (e.g. from your bind-mount source).
473
+ urlTemplate: `cursor://file${process.env.HOST_WORKSPACE_ROOT}/{file}:{line}:{column}`,
456
474
  },
457
475
  },
458
476
  });
459
477
  ```
460
478
 
461
- With `urlTemplate`, the chip renders a real `<a href>` (not a fetch to Vite).
479
+ With `urlTemplate`, the chip renders a real `<a href>` (not a fetch to the bridge).
462
480
  `vscode://file/…` also works if that scheme is bound to your editor.
463
481
 
482
+ ## Examples
483
+
484
+ Minimal demos for each host connector live under [`examples/`](examples/) and run
485
+ in this same Dev Container against the existing T3 (`T3CODE_PORT`). Each example
486
+ declares `@rose42/t3-feedback` from npm and overrides it to `link:../..` so local
487
+ `dist/` is used.
488
+
489
+ ```bash
490
+ pnpm build
491
+ pnpm examples:install
492
+ pnpm examples:vite-react # also: vite-vue / vite-svelte / vite-qwik / next / node
493
+ pnpm examples:all # every example in parallel
494
+ ```
495
+
496
+ See [examples/README.md](examples/README.md) for ports and the dependency model.
497
+
464
498
  ## Package scripts
465
499
 
466
500
  ```bash
@@ -1,5 +1,5 @@
1
- import type { T3FeedbackReportTypeGlyphResolved } from "../types.js";
2
- export type T3FeedbackBridgeRoute = "feedback" | "threads" | "settle" | "open" | "source" | null;
1
+ import type { T3FeedbackModelSelection, T3FeedbackReportTypeGlyphResolved } from "../types.js";
2
+ export type T3FeedbackBridgeRoute = "feedback" | "threads" | "settle" | "open" | "open-editor" | "source" | null;
3
3
  /**
4
4
  * Options for the host-agnostic Fetch handler (Vite middleware, Next Route Handler, …).
5
5
  * Env vars (`T3_FEEDBACK_*`) apply when fields are omitted — same as the Vite plugin.
@@ -14,6 +14,11 @@ export type T3FeedbackBridgeServerOptions = {
14
14
  openOrigin?: string;
15
15
  workspaceRootHints?: string[];
16
16
  threadTitlePrefix?: string;
17
+ /**
18
+ * Provider/model for newly created threads.
19
+ * Env fallback: `T3_FEEDBACK_INSTANCE_ID` + `T3_FEEDBACK_MODEL`.
20
+ */
21
+ modelSelection?: T3FeedbackModelSelection;
17
22
  reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved;
18
23
  token?: string;
19
24
  };
@@ -1,8 +1,10 @@
1
1
  import { resolveOpenOrigin } from "./format.js";
2
- import { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
2
+ import { launchEditorAt, parseEditorFileParam, resolveEditorFilePath } from "./launchEditor.js";
3
+ import { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_EDITOR_SUFFIX, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
3
4
  import { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
4
5
  import { readWorkspaceSourceFile } from "./readSource.js";
5
6
  import { createOpenHandoff, listThreadsInT3, probeThreadsInT3, sendFeedbackToT3, settleThreadsInT3, } from "./sendFeedback.js";
7
+ import { normalizeModelSelection } from "../types.js";
6
8
  import path from "node:path";
7
9
  const DEFAULT_ORIGIN = "http://localhost:3773";
8
10
  function pathResolveUp(from, levels) {
@@ -22,6 +24,8 @@ export function matchT3FeedbackRoute(pathname, basePath = T3_FEEDBACK_API_PATH)
22
24
  return "threads";
23
25
  if (pathname === `${base}${T3_FEEDBACK_SETTLE_SUFFIX}`)
24
26
  return "settle";
27
+ if (pathname === `${base}${T3_FEEDBACK_OPEN_EDITOR_SUFFIX}`)
28
+ return "open-editor";
25
29
  if (pathname === `${base}${T3_FEEDBACK_OPEN_SUFFIX}`)
26
30
  return "open";
27
31
  if (pathname === `${base}${T3_FEEDBACK_SOURCE_SUFFIX}`)
@@ -42,12 +46,20 @@ function resolveBridgeEnv(options) {
42
46
  const workspaceRootHints = options.workspaceRootHints ??
43
47
  [process.env.T3_FEEDBACK_WORKSPACE_HINT, process.cwd()].filter((value) => Boolean(value));
44
48
  const threadTitlePrefix = options.threadTitlePrefix?.trim() || undefined;
49
+ const modelSelection = normalizeModelSelection(options.modelSelection) ??
50
+ normalizeModelSelection({
51
+ ...(process.env.T3_FEEDBACK_INSTANCE_ID
52
+ ? { instanceId: process.env.T3_FEEDBACK_INSTANCE_ID }
53
+ : {}),
54
+ ...(process.env.T3_FEEDBACK_MODEL ? { model: process.env.T3_FEEDBACK_MODEL } : {}),
55
+ });
45
56
  const token = options.token ?? process.env.T3_FEEDBACK_TOKEN;
46
57
  return {
47
58
  origin,
48
59
  openOrigin,
49
60
  workspaceRootHints,
50
61
  ...(threadTitlePrefix ? { threadTitlePrefix } : {}),
62
+ ...(modelSelection ? { modelSelection } : {}),
51
63
  ...(options.reportTypeGlyph ? { reportTypeGlyph: options.reportTypeGlyph } : {}),
52
64
  ...(token ? { token } : {}),
53
65
  basePath: normalizeBasePath(options.basePath ?? T3_FEEDBACK_API_PATH),
@@ -116,6 +128,33 @@ export async function handleT3FeedbackRequest(request, options = {}) {
116
128
  });
117
129
  return jsonResponse(200, handoff);
118
130
  }
131
+ if (route === "open-editor" && request.method === "GET") {
132
+ const fileParam = url.searchParams.get("file")?.trim() ?? "";
133
+ const parsed = parseEditorFileParam(fileParam);
134
+ if (!parsed) {
135
+ return jsonResponse(400, {
136
+ error: "Missing or invalid file query param (expected path:line:column)",
137
+ });
138
+ }
139
+ const hints = [
140
+ ...resolved.workspaceRootHints,
141
+ process.cwd(),
142
+ pathResolveUp(process.cwd(), 1),
143
+ pathResolveUp(process.cwd(), 2),
144
+ ].filter((value, index, all) => {
145
+ return Boolean(value) && all.indexOf(value) === index;
146
+ });
147
+ const absolutePath = resolveEditorFilePath(parsed.fileName, hints);
148
+ if (!absolutePath) {
149
+ return jsonResponse(404, { error: `File not found: ${parsed.fileName}` });
150
+ }
151
+ if (!launchEditorAt(absolutePath, parsed.line, parsed.column)) {
152
+ return jsonResponse(503, {
153
+ error: "No editor CLI found (set LAUNCH_EDITOR, or install Cursor/VS Code remote CLI)",
154
+ });
155
+ }
156
+ return new Response(null, { status: 204 });
157
+ }
119
158
  if (route === "source" && request.method === "GET") {
120
159
  const file = url.searchParams.get("file")?.trim() ?? "";
121
160
  if (!file) {
@@ -154,6 +193,7 @@ export async function handleT3FeedbackRequest(request, options = {}) {
154
193
  workspaceRootHints: resolved.workspaceRootHints,
155
194
  ...(resolved.reportTypeGlyph ? { reportTypeGlyph: resolved.reportTypeGlyph } : {}),
156
195
  ...(resolved.threadTitlePrefix ? { threadTitlePrefix: resolved.threadTitlePrefix } : {}),
196
+ ...(resolved.modelSelection ? { modelSelection: resolved.modelSelection } : {}),
157
197
  ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
158
198
  ...(resolved.token ? { token: resolved.token } : {}),
159
199
  });
@@ -5,9 +5,11 @@
5
5
  */
6
6
  export { handleT3FeedbackRequest, matchT3FeedbackRoute, type T3FeedbackBridgeRoute, type T3FeedbackBridgeServerOptions, } from "./http.js";
7
7
  export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
8
- export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
8
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_OPEN_EDITOR_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
9
9
  export { readWorkspaceSourceFile } from "./readSource.js";
10
- export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, type ProbeThreadsOptions, type ShellSnapshot, type T3BridgeOptions, type T3EnvironmentDescriptor, } from "./sendFeedback.js";
10
+ export { launchEditorAt, parseEditorFileParam, resolveEditorBinary, resolveEditorFilePath, } from "./launchEditor.js";
11
+ export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveModelSelection, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, type ProbeThreadsOptions, type ShellSnapshot, type T3BridgeOptions, type T3EnvironmentDescriptor, } from "./sendFeedback.js";
11
12
  export { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatCustomPluginSection, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, type T3FeedbackFormatHeadingBody, type T3FeedbackFormatLabelValue, type T3FeedbackFormatMarkdown, } from "./format.js";
12
13
  export { dataUrlSizeBytes, envelopeImageAttachments, isT3FeedbackAttachmentsValue, type T3FeedbackAttachmentsValue, type T3FeedbackImageAttachment, } from "./attachments.js";
13
- export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadSettleResult, ThreadsListResponse, ThreadsProbeResponse, ThreadsSettleResponse, T3FeedbackOpenHandoff, T3FeedbackReportTypeGlyphResolved, } from "../types.js";
14
+ export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadSettleResult, ThreadsListResponse, ThreadsProbeResponse, ThreadsSettleResponse, T3FeedbackModelSelection, T3FeedbackOpenHandoff, T3FeedbackReportTypeGlyphResolved, } from "../types.js";
15
+ export { DEFAULT_MODEL_SELECTION, normalizeModelSelection } from "../types.js";
@@ -5,8 +5,10 @@
5
5
  */
6
6
  export { handleT3FeedbackRequest, matchT3FeedbackRoute, } from "./http.js";
7
7
  export { parseFeedbackPost, parseOpenHandoffRequest, parseSettlePost, parseThreadProbeIds, } from "./parse.js";
8
- export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
8
+ export { T3_FEEDBACK_API_PATH, T3_FEEDBACK_OPEN_PATH, T3_FEEDBACK_OPEN_SUFFIX, T3_FEEDBACK_OPEN_EDITOR_PATH, T3_FEEDBACK_OPEN_EDITOR_SUFFIX, T3_FEEDBACK_SETTLE_PATH, T3_FEEDBACK_SETTLE_SUFFIX, T3_FEEDBACK_SOURCE_SUFFIX, T3_FEEDBACK_THREADS_PATH, T3_FEEDBACK_THREADS_SUFFIX, } from "./paths.js";
9
9
  export { readWorkspaceSourceFile } from "./readSource.js";
10
- export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, } from "./sendFeedback.js";
10
+ export { launchEditorAt, parseEditorFileParam, resolveEditorBinary, resolveEditorFilePath, } from "./launchEditor.js";
11
+ export { clearT3EnvironmentIdCache, createOpenHandoff, issuePairingCredential, listThreadsInT3, parseT3JsonStdout, pickProject, probeThreadsInT3, resolveModelSelection, resolveT3Environment, resolveT3EnvironmentId, sendFeedbackToT3, settleThreadsInT3, } from "./sendFeedback.js";
11
12
  export { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatCustomPluginSection, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, } from "./format.js";
12
13
  export { dataUrlSizeBytes, envelopeImageAttachments, isT3FeedbackAttachmentsValue, } from "./attachments.js";
14
+ export { DEFAULT_MODEL_SELECTION, normalizeModelSelection } from "../types.js";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Locate a Cursor / VS Code remote CLI binary (or honor `LAUNCH_EDITOR`).
3
+ * Returns an absolute path, or `null` when nothing usable is found.
4
+ */
5
+ export declare function resolveEditorBinary(): string | null;
6
+ /** Parse Vite-style `path:line:column` (column optional). */
7
+ export declare function parseEditorFileParam(fileParam: string): {
8
+ fileName: string;
9
+ line: number;
10
+ column: number;
11
+ } | null;
12
+ /**
13
+ * Resolve a workspace- or app-relative source path under one of the hint roots.
14
+ * Rejects path traversal.
15
+ */
16
+ export declare function resolveEditorFilePath(fileName: string, workspaceRootHints: readonly string[]): string | null;
17
+ /**
18
+ * Open a file in the IDE via the remote CLI / LAUNCH_EDITOR.
19
+ * Returns true when a spawn was started (does not wait for the editor).
20
+ */
21
+ export declare function launchEditorAt(absolutePath: string, line?: number, column?: number): boolean;
@@ -0,0 +1,139 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ const REMOTE_CLI_NAMES = ["cursor", "code"];
5
+ /**
6
+ * Locate a Cursor / VS Code remote CLI binary (or honor `LAUNCH_EDITOR`).
7
+ * Returns an absolute path, or `null` when nothing usable is found.
8
+ */
9
+ export function resolveEditorBinary() {
10
+ const fromEnv = process.env.LAUNCH_EDITOR?.trim();
11
+ if (fromEnv && fs.existsSync(fromEnv))
12
+ return fromEnv;
13
+ const home = process.env.HOME?.trim();
14
+ if (home) {
15
+ for (const product of ["cursor-server", "vscode-server"]) {
16
+ const binRoot = path.join(home, `.${product}`, "bin");
17
+ if (!fs.existsSync(binRoot))
18
+ continue;
19
+ let versions = [];
20
+ try {
21
+ versions = fs.readdirSync(binRoot);
22
+ }
23
+ catch {
24
+ continue;
25
+ }
26
+ // Prefer newest install dir name (Cursor hashes are not strictly sortable;
27
+ // mtime is a better signal).
28
+ const ranked = versions
29
+ .map((name) => {
30
+ const cliDir = path.join(binRoot, name, "bin", "remote-cli");
31
+ try {
32
+ return { cliDir, mtime: fs.statSync(path.join(binRoot, name)).mtimeMs };
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ })
38
+ .filter((entry) => Boolean(entry))
39
+ .sort((a, b) => b.mtime - a.mtime);
40
+ for (const { cliDir } of ranked) {
41
+ for (const name of REMOTE_CLI_NAMES) {
42
+ const candidate = path.join(cliDir, name);
43
+ if (fs.existsSync(candidate))
44
+ return candidate;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ for (const name of REMOTE_CLI_NAMES) {
50
+ const fromPath = lookupOnPath(name);
51
+ if (fromPath)
52
+ return fromPath;
53
+ }
54
+ return null;
55
+ }
56
+ function lookupOnPath(command) {
57
+ const pathEnv = process.env.PATH ?? "";
58
+ for (const dir of pathEnv.split(path.delimiter)) {
59
+ if (!dir)
60
+ continue;
61
+ const candidate = path.join(dir, command);
62
+ try {
63
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile())
64
+ return candidate;
65
+ }
66
+ catch {
67
+ // continue
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ /** Parse Vite-style `path:line:column` (column optional). */
73
+ export function parseEditorFileParam(fileParam) {
74
+ const trimmed = fileParam.trim();
75
+ if (!trimmed)
76
+ return null;
77
+ // Split from the right so Windows drive letters / absolute paths stay intact.
78
+ const match = /^(.+?):(\d+)(?::(\d+))?$/.exec(trimmed);
79
+ if (!match?.[1] || !match[2]) {
80
+ return { fileName: trimmed, line: 1, column: 1 };
81
+ }
82
+ const line = Math.max(1, Number(match[2]));
83
+ const column = match[3] ? Math.max(1, Number(match[3])) : 1;
84
+ if (!Number.isFinite(line) || !Number.isFinite(column))
85
+ return null;
86
+ return { fileName: match[1], line, column };
87
+ }
88
+ /**
89
+ * Resolve a workspace- or app-relative source path under one of the hint roots.
90
+ * Rejects path traversal.
91
+ */
92
+ export function resolveEditorFilePath(fileName, workspaceRootHints) {
93
+ const cleaned = fileName.replace(/\\/g, "/").trim();
94
+ if (!cleaned || cleaned.includes("\0") || cleaned.includes(".."))
95
+ return null;
96
+ if (path.isAbsolute(cleaned)) {
97
+ if (!fs.existsSync(cleaned))
98
+ return null;
99
+ const abs = path.resolve(cleaned);
100
+ const allowed = workspaceRootHints.some((root) => {
101
+ const rootResolved = path.resolve(root);
102
+ return abs === rootResolved || abs.startsWith(rootResolved + path.sep);
103
+ });
104
+ return allowed || workspaceRootHints.length === 0 ? abs : null;
105
+ }
106
+ for (const root of workspaceRootHints) {
107
+ const absolutePath = path.resolve(root, cleaned);
108
+ const rootResolved = path.resolve(root);
109
+ if (absolutePath !== rootResolved && !absolutePath.startsWith(rootResolved + path.sep)) {
110
+ continue;
111
+ }
112
+ if (fs.existsSync(absolutePath))
113
+ return absolutePath;
114
+ }
115
+ return null;
116
+ }
117
+ /**
118
+ * Open a file in the IDE via the remote CLI / LAUNCH_EDITOR.
119
+ * Returns true when a spawn was started (does not wait for the editor).
120
+ */
121
+ export function launchEditorAt(absolutePath, line = 1, column = 1) {
122
+ const editor = resolveEditorBinary();
123
+ if (!editor)
124
+ return false;
125
+ const target = `${absolutePath}:${Math.max(1, line)}:${Math.max(1, column)}`;
126
+ try {
127
+ // Cursor / VS Code remote CLI: `cursor -g file:line:column`
128
+ const child = spawn(editor, ["-g", target], {
129
+ detached: true,
130
+ stdio: "ignore",
131
+ env: process.env,
132
+ });
133
+ child.unref();
134
+ return true;
135
+ }
136
+ catch {
137
+ return false;
138
+ }
139
+ }
@@ -3,8 +3,10 @@ export declare const T3_FEEDBACK_API_PATH = "/__t3-feedback";
3
3
  export declare const T3_FEEDBACK_THREADS_PATH = "/__t3-feedback/threads";
4
4
  export declare const T3_FEEDBACK_SETTLE_PATH = "/__t3-feedback/settle";
5
5
  export declare const T3_FEEDBACK_OPEN_PATH = "/__t3-feedback/open";
6
+ export declare const T3_FEEDBACK_OPEN_EDITOR_PATH = "/__t3-feedback/open-editor";
6
7
  /** Suffixes under a host-chosen base path (e.g. `/api/t3-feedback`). */
7
8
  export declare const T3_FEEDBACK_THREADS_SUFFIX = "/threads";
8
9
  export declare const T3_FEEDBACK_SETTLE_SUFFIX = "/settle";
9
10
  export declare const T3_FEEDBACK_OPEN_SUFFIX = "/open";
11
+ export declare const T3_FEEDBACK_OPEN_EDITOR_SUFFIX = "/open-editor";
10
12
  export declare const T3_FEEDBACK_SOURCE_SUFFIX = "/source";
@@ -3,8 +3,10 @@ export const T3_FEEDBACK_API_PATH = "/__t3-feedback";
3
3
  export const T3_FEEDBACK_THREADS_PATH = "/__t3-feedback/threads";
4
4
  export const T3_FEEDBACK_SETTLE_PATH = "/__t3-feedback/settle";
5
5
  export const T3_FEEDBACK_OPEN_PATH = "/__t3-feedback/open";
6
+ export const T3_FEEDBACK_OPEN_EDITOR_PATH = "/__t3-feedback/open-editor";
6
7
  /** Suffixes under a host-chosen base path (e.g. `/api/t3-feedback`). */
7
8
  export const T3_FEEDBACK_THREADS_SUFFIX = "/threads";
8
9
  export const T3_FEEDBACK_SETTLE_SUFFIX = "/settle";
9
10
  export const T3_FEEDBACK_OPEN_SUFFIX = "/open";
11
+ export const T3_FEEDBACK_OPEN_EDITOR_SUFFIX = "/open-editor";
10
12
  export const T3_FEEDBACK_SOURCE_SUFFIX = "/source";
@@ -15,9 +15,11 @@ export async function readWorkspaceSourceFile(fileParam, workspaceRootHints) {
15
15
  if (!ALLOWED_EXT.has(ext))
16
16
  return null;
17
17
  // Prefer app/package sources; allow either apps/<pkg>/… or app/… (Next app cwd).
18
+ // `examples/<name>/…` covers monorepo demo apps whose IDE workspace is the repo root.
18
19
  if (!cleaned.startsWith("app/") &&
19
20
  !cleaned.startsWith("apps/") &&
20
21
  !cleaned.startsWith("packages/") &&
22
+ !cleaned.startsWith("examples/") &&
21
23
  !cleaned.startsWith("src/")) {
22
24
  return null;
23
25
  }
@@ -1,5 +1,5 @@
1
1
  import type { FeedbackEnvelope } from "@rose42/feedback-capture";
2
- import type { FeedbackSendResult, T3FeedbackReportTypeGlyphResolved, ThreadListItem, ThreadProbeResult, ThreadSettleResult } from "../types.js";
2
+ import type { FeedbackSendResult, T3FeedbackModelSelection, T3FeedbackReportTypeGlyphResolved, ThreadListItem, ThreadProbeResult, ThreadSettleResult } from "../types.js";
3
3
  import type { T3FeedbackOpenHandoff } from "../types.js";
4
4
  export type ShellSnapshot = {
5
5
  snapshotSequence: number;
@@ -34,6 +34,12 @@ export type T3BridgeOptions = {
34
34
  * instead of `Bug` / `Feedback` / `Suggestion`.
35
35
  */
36
36
  reportTypeGlyph?: T3FeedbackReportTypeGlyphResolved;
37
+ /**
38
+ * Provider/model for newly created threads. When set, used as-is.
39
+ * When omitted, {@link resolveModelSelection} prefers a same-project thread
40
+ * matching {@link DEFAULT_MODEL_SELECTION}.instanceId.
41
+ */
42
+ modelSelection?: T3FeedbackModelSelection;
37
43
  /**
38
44
  * Reuse this thread when it still exists in the project snapshot.
39
45
  * Stale / missing ids create a new thread instead.
@@ -86,6 +92,16 @@ export declare function pickProject(projects: Array<{
86
92
  title: string;
87
93
  workspaceRoot: string;
88
94
  };
95
+ /**
96
+ * Pick provider/model for a new thread.
97
+ *
98
+ * 1. Explicit `configured` (plugin / env) wins.
99
+ * 2. Else reuse a same-project thread whose `instanceId` matches the preferred
100
+ * default (`cursor`) so settings like Cursor Auto are preserved.
101
+ * 3. Else {@link DEFAULT_MODEL_SELECTION} — never copy an arbitrary first thread
102
+ * (that previously inherited disabled providers such as Codex).
103
+ */
104
+ export declare function resolveModelSelection(threads: ShellSnapshot["threads"], projectId: string, configured?: T3FeedbackModelSelection): T3FeedbackModelSelection;
89
105
  /** Authenticate to T3 and start a turn with the feedback envelope. */
90
106
  export declare function sendFeedbackToT3(envelope: FeedbackEnvelope, options: T3BridgeOptions): Promise<FeedbackSendResult>;
91
107
  /** List non-archived threads for the matched T3 project (continue picker). */
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import WebSocket from "ws";
3
+ import { DEFAULT_MODEL_SELECTION, normalizeModelSelection } from "../types.js";
3
4
  import { dataUrlSizeBytes, envelopeImageAttachments } from "./attachments.js";
4
5
  import { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, } from "./format.js";
5
6
  import { isShellThreadArchived, listItemStateFromShellThread } from "./probeThreads.js";
@@ -251,16 +252,32 @@ export function pickProject(projects, hints) {
251
252
  return projects[0];
252
253
  throw new Error("No T3 projects available");
253
254
  }
255
+ /**
256
+ * Pick provider/model for a new thread.
257
+ *
258
+ * 1. Explicit `configured` (plugin / env) wins.
259
+ * 2. Else reuse a same-project thread whose `instanceId` matches the preferred
260
+ * default (`cursor`) so settings like Cursor Auto are preserved.
261
+ * 3. Else {@link DEFAULT_MODEL_SELECTION} — never copy an arbitrary first thread
262
+ * (that previously inherited disabled providers such as Codex).
263
+ */
264
+ export function resolveModelSelection(threads, projectId, configured) {
265
+ const explicit = normalizeModelSelection(configured);
266
+ if (explicit)
267
+ return explicit;
268
+ const preferredInstance = DEFAULT_MODEL_SELECTION.instanceId;
269
+ const projectThreads = threads.filter((t) => t.projectId === projectId);
270
+ const matching = projectThreads.find((t) => t.modelSelection?.instanceId?.trim() === preferredInstance);
271
+ const inherited = normalizeModelSelection(matching?.modelSelection);
272
+ if (inherited)
273
+ return inherited;
274
+ return { ...DEFAULT_MODEL_SELECTION };
275
+ }
254
276
  /** Authenticate to T3 and start a turn with the feedback envelope. */
255
277
  export async function sendFeedbackToT3(envelope, options) {
256
278
  return withShellClient(options, async (client, snapshot) => {
257
279
  const project = pickProject(snapshot.projects, options.workspaceRootHints);
258
- const modelSelection = snapshot.threads.find((t) => t.projectId === project.id)
259
- ?.modelSelection ??
260
- snapshot.threads[0]?.modelSelection ?? {
261
- instanceId: "cursor",
262
- model: "default",
263
- };
280
+ const modelSelection = resolveModelSelection(snapshot.threads, project.id, options.modelSelection);
264
281
  const branch = snapshot.threads.find((t) => t.projectId === project.id)?.branch ?? null;
265
282
  const existing = options.threadId
266
283
  ? snapshot.threads.find((t) => t.id === options.threadId && t.projectId === project.id)
@@ -21,4 +21,4 @@ export { SESSION_THREAD_TTL_MS, defaultThreadDestination, readSessionThreadId, r
21
21
  export { createT3FeedbackHttpTransport, type T3FeedbackHttpTransportOptions } from "./transport.js";
22
22
  export { T3_OPEN_WINDOW_NAME, T3_PAIR_THEN_THREAD_DELAY_MS, fetchOpenHandoff, openInT3Seamless, shouldBypassSeamlessOpen, type OpenInT3SeamlessOptions, } from "./openInT3.js";
23
23
  export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadsListResponse, ThreadsProbeResponse, T3BuiltinPluginId, T3FeedbackAfterPick, T3FeedbackBuiltinPluginOptions, T3FeedbackFabConfig, T3FeedbackFabOptions, T3FeedbackFabPosition, T3FeedbackFabVisibility, T3FeedbackOpenHandoff, T3FeedbackOpenInEditorConfig, T3FeedbackOpenInEditorOptions, T3FeedbackPickConfig, T3FeedbackPickOptions, T3FeedbackRecordConfig, T3FeedbackRecordOptions, T3FeedbackReportTypeGlyph, T3FeedbackReportTypeGlyphCustom, T3FeedbackReportTypeGlyphMap, T3FeedbackReportTypeGlyphResolved, T3FeedbackScreenshotConfig, T3FeedbackScreenshotCrop, T3FeedbackScreenshotOptions, T3FeedbackShortcutConfig, T3FeedbackShortcutOptions, T3FeedbackThreadReuse, } from "../types.js";
24
- export { 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, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";
24
+ export { 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, resolveOpenInEditorConfig, resolveOpenInEditorEndpointForApi, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";
@@ -17,4 +17,4 @@ export { ACTIVE_THREAD_POLL_MS, ACTIVE_THREAD_TTL_MS, clearActiveThreads, listAc
17
17
  export { SESSION_THREAD_TTL_MS, defaultThreadDestination, readSessionThreadId, readSessionThreadRecord, sessionThreadStorageKey, writeSessionThreadId, writeSessionThreadRecord, } from "./sessionThread.js";
18
18
  export { createT3FeedbackHttpTransport } from "./transport.js";
19
19
  export { T3_OPEN_WINDOW_NAME, T3_PAIR_THEN_THREAD_DELAY_MS, fetchOpenHandoff, openInT3Seamless, shouldBypassSeamlessOpen, } from "./openInT3.js";
20
- export { 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, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";
20
+ export { 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, resolveOpenInEditorConfig, resolveOpenInEditorEndpointForApi, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";