@rose42/t3-feedback 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/dist/bridge/attachments.d.ts +27 -0
- package/dist/bridge/attachments.js +109 -0
- package/dist/bridge/format.d.ts +70 -0
- package/dist/bridge/format.js +358 -0
- package/dist/bridge/http.d.ts +31 -0
- package/dist/bridge/http.js +169 -0
- package/dist/bridge/index.d.ts +13 -0
- package/dist/bridge/index.js +12 -0
- package/dist/bridge/parse.d.ts +16 -0
- package/dist/bridge/parse.js +82 -0
- package/dist/bridge/paths.d.ts +10 -0
- package/dist/bridge/paths.js +10 -0
- package/dist/bridge/probeThreads.d.ts +13 -0
- package/dist/bridge/probeThreads.js +66 -0
- package/dist/bridge/readSource.d.ts +8 -0
- package/dist/bridge/readSource.js +64 -0
- package/dist/bridge/sendFeedback.d.ts +99 -0
- package/dist/bridge/sendFeedback.js +417 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +40 -0
- package/dist/cli/constants.d.ts +4 -0
- package/dist/cli/constants.js +4 -0
- package/dist/cli/detect.d.ts +17 -0
- package/dist/cli/detect.js +118 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/init.d.ts +18 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/pm.d.ts +12 -0
- package/dist/cli/pm.js +49 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/wire/next.d.ts +8 -0
- package/dist/cli/wire/next.js +71 -0
- package/dist/cli/wire/node.d.ts +3 -0
- package/dist/cli/wire/node.js +76 -0
- package/dist/cli/wire/vite.d.ts +25 -0
- package/dist/cli/wire/vite.js +71 -0
- package/dist/client/activeThreads.d.ts +23 -0
- package/dist/client/activeThreads.js +89 -0
- package/dist/client/cropScreenshot.d.ts +39 -0
- package/dist/client/cropScreenshot.js +212 -0
- package/dist/client/customPlugins.d.ts +7 -0
- package/dist/client/customPlugins.js +38 -0
- package/dist/client/draft.d.ts +33 -0
- package/dist/client/draft.js +222 -0
- package/dist/client/index.d.ts +24 -0
- package/dist/client/index.js +20 -0
- package/dist/client/mount.d.ts +210 -0
- package/dist/client/mount.js +2408 -0
- package/dist/client/openInT3.d.ts +28 -0
- package/dist/client/openInT3.js +81 -0
- package/dist/client/readClipboardImage.d.ts +2 -0
- package/dist/client/readClipboardImage.js +2 -0
- package/dist/client/reportType.d.ts +43 -0
- package/dist/client/reportType.js +110 -0
- package/dist/client/selectBurstFrames.d.ts +21 -0
- package/dist/client/selectBurstFrames.js +101 -0
- package/dist/client/sessionThread.d.ts +26 -0
- package/dist/client/sessionThread.js +93 -0
- package/dist/client/threadPicker.d.ts +20 -0
- package/dist/client/threadPicker.js +51 -0
- package/dist/client/transport.d.ts +11 -0
- package/dist/client/transport.js +35 -0
- package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
- package/dist/client/ui/commentPanelPlugin.js +126 -0
- package/dist/client/ui/defaultPlugins.d.ts +19 -0
- package/dist/client/ui/defaultPlugins.js +48 -0
- package/dist/client/ui/elementTargetChip.d.ts +64 -0
- package/dist/client/ui/elementTargetChip.js +185 -0
- package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
- package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
- package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
- package/dist/client/ui/elementTargetPanelSession.js +279 -0
- package/dist/client/ui/elementTargetUi.d.ts +36 -0
- package/dist/client/ui/elementTargetUi.js +105 -0
- package/dist/client/ui/host.d.ts +112 -0
- package/dist/client/ui/host.js +132 -0
- package/dist/client/ui/html.d.ts +22 -0
- package/dist/client/ui/html.js +26 -0
- package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
- package/dist/client/ui/labeledPluginPreview.js +71 -0
- package/dist/client/ui/resolveContributions.d.ts +13 -0
- package/dist/client/ui/resolveContributions.js +20 -0
- package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
- package/dist/client/ui/screenshotPanelPlugin.js +212 -0
- package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
- package/dist/client/ui/screenshotPanelSession.js +445 -0
- package/dist/client/ui/screenshotUi.d.ts +31 -0
- package/dist/client/ui/screenshotUi.js +130 -0
- package/dist/client/ui/shortcuts.d.ts +32 -0
- package/dist/client/ui/shortcuts.js +40 -0
- package/dist/client/ui/slots.d.ts +73 -0
- package/dist/client/ui/slots.js +112 -0
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +186 -0
- package/dist/types.d.ts +464 -0
- package/dist/types.js +283 -0
- package/docs/devcontainer.md +160 -0
- package/package.json +107 -0
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { dataUrlSizeBytes, envelopeImageAttachments } from "./attachments.js";
|
|
4
|
+
import { buildPairOpenUrl, buildThreadOpenUrl, deriveThreadTitle, formatFeedbackMessage, normalizeOrigin, resolveOpenOrigin, } from "./format.js";
|
|
5
|
+
import { isShellThreadArchived, listItemStateFromShellThread } from "./probeThreads.js";
|
|
6
|
+
const environmentCache = new Map();
|
|
7
|
+
/** Clear cached environment descriptors (tests). */
|
|
8
|
+
export function clearT3EnvironmentIdCache() {
|
|
9
|
+
environmentCache.clear();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Read this server's environment descriptor from the public well-known document.
|
|
13
|
+
* Cached per API origin for the process lifetime. Always use the bridge `origin`
|
|
14
|
+
* (same server that owns the thread) — never a browser-only openOrigin.
|
|
15
|
+
*/
|
|
16
|
+
export async function resolveT3Environment(apiOrigin) {
|
|
17
|
+
const key = normalizeOrigin(apiOrigin);
|
|
18
|
+
const cached = environmentCache.get(key);
|
|
19
|
+
if (cached)
|
|
20
|
+
return cached;
|
|
21
|
+
const res = await fetch(`${key}/.well-known/t3/environment`, {
|
|
22
|
+
headers: { Accept: "application/json" },
|
|
23
|
+
});
|
|
24
|
+
const body = (await res.json().catch(() => null));
|
|
25
|
+
const environmentId = body?.environmentId?.trim() ?? "";
|
|
26
|
+
if (!res.ok || !environmentId) {
|
|
27
|
+
throw new Error(`Failed to read T3 environment id from ${key}/.well-known/t3/environment: ${JSON.stringify(body)}`);
|
|
28
|
+
}
|
|
29
|
+
const label = body?.label?.trim() || environmentId;
|
|
30
|
+
const resolved = { environmentId, label };
|
|
31
|
+
environmentCache.set(key, resolved);
|
|
32
|
+
console.info(`[t3-feedback] T3 environment ${environmentId} (${label}) via ${key}`);
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
/** @deprecated Prefer {@link resolveT3Environment}. */
|
|
36
|
+
export async function resolveT3EnvironmentId(apiOrigin) {
|
|
37
|
+
return (await resolveT3Environment(apiOrigin)).environmentId;
|
|
38
|
+
}
|
|
39
|
+
function issueToken() {
|
|
40
|
+
const result = spawnSync("t3", ["auth", "session", "issue", "--ttl", "8h", "--label", "t3-feedback-bridge", "--token-only"], { encoding: "utf8" });
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
throw new Error(`Failed to issue T3 session token: ${result.stderr || result.stdout || result.error}`);
|
|
43
|
+
}
|
|
44
|
+
const token = result.stdout.trim().split("\n").at(-1)?.trim();
|
|
45
|
+
if (!token)
|
|
46
|
+
throw new Error("t3 auth session issue returned an empty token");
|
|
47
|
+
return token;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse JSON from a `t3 … --json` stdout blob.
|
|
51
|
+
* Newer CLIs pretty-print; older ones emit a single line (sometimes after log lines).
|
|
52
|
+
*/
|
|
53
|
+
export function parseT3JsonStdout(stdout) {
|
|
54
|
+
const trimmed = stdout.trim();
|
|
55
|
+
if (!trimmed)
|
|
56
|
+
throw new Error("t3 returned empty output");
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(trimmed);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Fall through — may be logs + trailing JSON, or a single compact line.
|
|
62
|
+
}
|
|
63
|
+
const start = trimmed.indexOf("{");
|
|
64
|
+
const end = trimmed.lastIndexOf("}");
|
|
65
|
+
if (start >= 0 && end > start) {
|
|
66
|
+
try {
|
|
67
|
+
return JSON.parse(trimmed.slice(start, end + 1));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Fall through to last-line attempt.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const lastLine = trimmed.split("\n").at(-1)?.trim() ?? "";
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(lastLine);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new Error(`t3 returned non-JSON: ${lastLine || trimmed.slice(0, 120)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Mint a one-time browser pairing credential (`t3 auth pairing create`). */
|
|
82
|
+
export function issuePairingCredential(openOrigin) {
|
|
83
|
+
const baseUrl = normalizeOrigin(openOrigin);
|
|
84
|
+
const result = spawnSync("t3", [
|
|
85
|
+
"auth",
|
|
86
|
+
"pairing",
|
|
87
|
+
"create",
|
|
88
|
+
"--ttl",
|
|
89
|
+
"5m",
|
|
90
|
+
"--label",
|
|
91
|
+
"t3-feedback-open",
|
|
92
|
+
"--base-url",
|
|
93
|
+
baseUrl,
|
|
94
|
+
"--json",
|
|
95
|
+
], { encoding: "utf8" });
|
|
96
|
+
if (result.status !== 0) {
|
|
97
|
+
throw new Error(`Failed to mint T3 pairing token: ${result.stderr || result.stdout || result.error}`);
|
|
98
|
+
}
|
|
99
|
+
let parsed;
|
|
100
|
+
try {
|
|
101
|
+
parsed = parseT3JsonStdout(result.stdout);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
105
|
+
throw new Error(`t3 auth pairing create ${detail}`);
|
|
106
|
+
}
|
|
107
|
+
const credential = parsed.credential?.trim() ?? "";
|
|
108
|
+
if (!credential)
|
|
109
|
+
throw new Error("t3 auth pairing create returned no credential");
|
|
110
|
+
// Always build from `openOrigin` so T3's server-runtime origin (often
|
|
111
|
+
// http://127.0.0.1:port) cannot override the browser-facing host.
|
|
112
|
+
return { credential, pairUrl: buildPairOpenUrl(baseUrl, credential) };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build a seamless Open-in-T3 handoff: pair URL (auto-submits) + thread deep link.
|
|
116
|
+
* Call on click so the one-time pairing token is fresh.
|
|
117
|
+
*
|
|
118
|
+
* Environment id is always read from the bridge `origin` (the server that owns
|
|
119
|
+
* the thread). Client-supplied ids are ignored — they can be stale when multiple
|
|
120
|
+
* apps share a Vite origin / sessionStorage.
|
|
121
|
+
*/
|
|
122
|
+
export async function createOpenHandoff(threadId, options) {
|
|
123
|
+
const browserOrigin = resolveOpenOrigin(options.origin, options.openOrigin);
|
|
124
|
+
const { environmentId } = await resolveT3Environment(options.origin);
|
|
125
|
+
const threadUrl = buildThreadOpenUrl(browserOrigin, threadId, environmentId);
|
|
126
|
+
const { pairUrl } = issuePairingCredential(browserOrigin);
|
|
127
|
+
return { pairUrl, threadUrl, environmentId };
|
|
128
|
+
}
|
|
129
|
+
async function getWsTicket(origin, token) {
|
|
130
|
+
const res = await fetch(`${origin}/api/auth/websocket-ticket`, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: {
|
|
133
|
+
Authorization: `Bearer ${token}`,
|
|
134
|
+
"Content-Type": "application/json",
|
|
135
|
+
},
|
|
136
|
+
body: "{}",
|
|
137
|
+
});
|
|
138
|
+
const body = (await res.json());
|
|
139
|
+
if (!res.ok || !body.ticket) {
|
|
140
|
+
throw new Error(`websocket-ticket failed: ${JSON.stringify(body)}`);
|
|
141
|
+
}
|
|
142
|
+
return body.ticket;
|
|
143
|
+
}
|
|
144
|
+
class T3RpcClient {
|
|
145
|
+
#ws;
|
|
146
|
+
#pending = new Map();
|
|
147
|
+
#streams = new Map();
|
|
148
|
+
constructor(ws) {
|
|
149
|
+
this.#ws = ws;
|
|
150
|
+
ws.on("message", (raw) => this.#onMessage(String(raw)));
|
|
151
|
+
}
|
|
152
|
+
static connect(wsUrl) {
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const ws = new WebSocket(wsUrl);
|
|
155
|
+
ws.once("open", () => resolve(new T3RpcClient(ws)));
|
|
156
|
+
ws.once("error", reject);
|
|
157
|
+
ws.once("unexpected-response", (_req, res) => {
|
|
158
|
+
let body = "";
|
|
159
|
+
res.on("data", (chunk) => {
|
|
160
|
+
body += chunk;
|
|
161
|
+
});
|
|
162
|
+
res.on("end", () => {
|
|
163
|
+
reject(new Error(`WS upgrade ${res.statusCode}: ${body}`));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
#onMessage(raw) {
|
|
169
|
+
const msg = JSON.parse(raw);
|
|
170
|
+
if (msg._tag === "Chunk" && msg.requestId) {
|
|
171
|
+
const stream = this.#streams.get(msg.requestId);
|
|
172
|
+
if (stream) {
|
|
173
|
+
for (const value of msg.values ?? [])
|
|
174
|
+
stream.onChunk(value);
|
|
175
|
+
}
|
|
176
|
+
this.#ws.send(JSON.stringify({ _tag: "Ack", requestId: msg.requestId }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (msg._tag === "Exit" && msg.requestId && msg.exit) {
|
|
180
|
+
const pending = this.#pending.get(msg.requestId);
|
|
181
|
+
if (!pending)
|
|
182
|
+
return;
|
|
183
|
+
this.#pending.delete(msg.requestId);
|
|
184
|
+
clearTimeout(pending.timer);
|
|
185
|
+
if (msg.exit._tag === "Success")
|
|
186
|
+
pending.resolve(msg.exit.value);
|
|
187
|
+
else
|
|
188
|
+
pending.reject(new Error(JSON.stringify(msg.exit)));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (msg._tag === "Defect") {
|
|
192
|
+
console.warn("[t3-feedback] RPC defect", msg.defect);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
request(tag, payload, timeoutMs = 20_000) {
|
|
196
|
+
const id = crypto.randomUUID();
|
|
197
|
+
return new Promise((resolve, reject) => {
|
|
198
|
+
const timer = setTimeout(() => {
|
|
199
|
+
this.#pending.delete(id);
|
|
200
|
+
reject(new Error(`RPC timeout: ${tag}`));
|
|
201
|
+
}, timeoutMs);
|
|
202
|
+
this.#pending.set(id, { resolve, reject, timer });
|
|
203
|
+
this.#ws.send(JSON.stringify({ _tag: "Request", id, tag, payload, headers: [] }));
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
subscribeOnce(tag, payload, predicate, timeoutMs = 20_000) {
|
|
207
|
+
const id = crypto.randomUUID();
|
|
208
|
+
return new Promise((resolve, reject) => {
|
|
209
|
+
const timer = setTimeout(() => {
|
|
210
|
+
this.#streams.delete(id);
|
|
211
|
+
reject(new Error(`subscribe timeout: ${tag}`));
|
|
212
|
+
}, timeoutMs);
|
|
213
|
+
this.#streams.set(id, {
|
|
214
|
+
onChunk: (value) => {
|
|
215
|
+
if (!predicate(value))
|
|
216
|
+
return;
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
this.#streams.delete(id);
|
|
219
|
+
resolve(value);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
this.#ws.send(JSON.stringify({ _tag: "Request", id, tag, payload, headers: [] }));
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
close() {
|
|
226
|
+
this.#ws.close();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async function withShellClient(options, fn) {
|
|
230
|
+
const token = options.token?.trim() || issueToken();
|
|
231
|
+
const ticket = await getWsTicket(options.origin, token);
|
|
232
|
+
const wsUrl = `${options.origin.replace(/^http/, "ws")}/ws?wsTicket=${encodeURIComponent(ticket)}`;
|
|
233
|
+
const client = await T3RpcClient.connect(wsUrl);
|
|
234
|
+
try {
|
|
235
|
+
const shellEvent = (await client.subscribeOnce("orchestration.subscribeShell", { requestCompletionMarker: true }, (value) => typeof value === "object" &&
|
|
236
|
+
value !== null &&
|
|
237
|
+
value.kind === "snapshot"));
|
|
238
|
+
return await fn(client, shellEvent.snapshot);
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
client.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export function pickProject(projects, hints) {
|
|
245
|
+
for (const hint of hints) {
|
|
246
|
+
const match = projects.find((p) => p.workspaceRoot.includes(hint));
|
|
247
|
+
if (match)
|
|
248
|
+
return match;
|
|
249
|
+
}
|
|
250
|
+
if (projects[0])
|
|
251
|
+
return projects[0];
|
|
252
|
+
throw new Error("No T3 projects available");
|
|
253
|
+
}
|
|
254
|
+
/** Authenticate to T3 and start a turn with the feedback envelope. */
|
|
255
|
+
export async function sendFeedbackToT3(envelope, options) {
|
|
256
|
+
return withShellClient(options, async (client, snapshot) => {
|
|
257
|
+
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
|
+
};
|
|
264
|
+
const branch = snapshot.threads.find((t) => t.projectId === project.id)?.branch ?? null;
|
|
265
|
+
const existing = options.threadId
|
|
266
|
+
? snapshot.threads.find((t) => t.id === options.threadId && t.projectId === project.id)
|
|
267
|
+
: undefined;
|
|
268
|
+
let threadId;
|
|
269
|
+
let threadTitle;
|
|
270
|
+
if (existing) {
|
|
271
|
+
threadId = existing.id;
|
|
272
|
+
threadTitle = existing.title;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
threadId = crypto.randomUUID();
|
|
276
|
+
threadTitle = deriveThreadTitle(envelope, options.threadTitlePrefix, options.reportTypeGlyph ?? "icon");
|
|
277
|
+
await client.request("orchestration.dispatchCommand", {
|
|
278
|
+
type: "thread.create",
|
|
279
|
+
commandId: crypto.randomUUID(),
|
|
280
|
+
threadId,
|
|
281
|
+
projectId: project.id,
|
|
282
|
+
title: threadTitle,
|
|
283
|
+
modelSelection,
|
|
284
|
+
runtimeMode: "full-access",
|
|
285
|
+
interactionMode: "default",
|
|
286
|
+
branch,
|
|
287
|
+
worktreePath: null,
|
|
288
|
+
createdAt: new Date().toISOString(),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
const imageAttachments = envelopeImageAttachments(envelope);
|
|
292
|
+
const attachments = imageAttachments.map((item, index) => {
|
|
293
|
+
const mimeType = item.mimeType ?? "image/png";
|
|
294
|
+
const defaultName = imageAttachments.length === 1 ? "feedback.png" : `feedback-${index + 1}.png`;
|
|
295
|
+
return {
|
|
296
|
+
type: "image",
|
|
297
|
+
name: item.name ?? defaultName,
|
|
298
|
+
mimeType,
|
|
299
|
+
sizeBytes: dataUrlSizeBytes(item.dataUrl),
|
|
300
|
+
dataUrl: item.dataUrl,
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
const result = (await client.request("orchestration.dispatchCommand", {
|
|
304
|
+
type: "thread.turn.start",
|
|
305
|
+
commandId: crypto.randomUUID(),
|
|
306
|
+
threadId,
|
|
307
|
+
message: {
|
|
308
|
+
messageId: crypto.randomUUID(),
|
|
309
|
+
role: "user",
|
|
310
|
+
text: formatFeedbackMessage(envelope),
|
|
311
|
+
attachments,
|
|
312
|
+
},
|
|
313
|
+
runtimeMode: "full-access",
|
|
314
|
+
interactionMode: "default",
|
|
315
|
+
createdAt: new Date().toISOString(),
|
|
316
|
+
}));
|
|
317
|
+
const browserOrigin = resolveOpenOrigin(options.origin, options.openOrigin);
|
|
318
|
+
const { environmentId, label: environmentLabel } = await resolveT3Environment(options.origin);
|
|
319
|
+
return {
|
|
320
|
+
threadId,
|
|
321
|
+
threadTitle,
|
|
322
|
+
projectId: project.id,
|
|
323
|
+
environmentId,
|
|
324
|
+
environmentLabel,
|
|
325
|
+
sequence: result.sequence,
|
|
326
|
+
openUrl: buildThreadOpenUrl(browserOrigin, threadId, environmentId),
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** List non-archived threads for the matched T3 project (continue picker). */
|
|
331
|
+
export async function listThreadsInT3(options) {
|
|
332
|
+
return withShellClient(options, async (_client, snapshot) => {
|
|
333
|
+
const project = pickProject(snapshot.projects, options.workspaceRootHints);
|
|
334
|
+
const items = [];
|
|
335
|
+
for (const thread of snapshot.threads) {
|
|
336
|
+
if (thread.projectId !== project.id)
|
|
337
|
+
continue;
|
|
338
|
+
if (isShellThreadArchived(thread))
|
|
339
|
+
continue;
|
|
340
|
+
const threadId = typeof thread.id === "string" ? thread.id.trim() : "";
|
|
341
|
+
const title = typeof thread.title === "string" ? thread.title.trim() : "";
|
|
342
|
+
if (!threadId || !title)
|
|
343
|
+
continue;
|
|
344
|
+
const updatedAt = typeof thread.updatedAt === "string" && thread.updatedAt.trim()
|
|
345
|
+
? thread.updatedAt.trim()
|
|
346
|
+
: undefined;
|
|
347
|
+
items.push({
|
|
348
|
+
threadId,
|
|
349
|
+
title,
|
|
350
|
+
state: listItemStateFromShellThread(thread),
|
|
351
|
+
...(updatedAt ? { updatedAt } : {}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
items.sort((a, b) => {
|
|
355
|
+
const aAt = a.updatedAt ?? "";
|
|
356
|
+
const bAt = b.updatedAt ?? "";
|
|
357
|
+
if (aAt !== bAt)
|
|
358
|
+
return bAt.localeCompare(aAt);
|
|
359
|
+
return a.title.localeCompare(b.title);
|
|
360
|
+
});
|
|
361
|
+
return items;
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** Probe working/settled state for feedback threads in the T3 shell snapshot. */
|
|
365
|
+
export async function probeThreadsInT3(threadIds, options) {
|
|
366
|
+
const ids = [...new Set(threadIds.map((id) => id.trim()).filter(Boolean))];
|
|
367
|
+
if (ids.length === 0)
|
|
368
|
+
return [];
|
|
369
|
+
return withShellClient(options, async (_client, snapshot) => {
|
|
370
|
+
const project = pickProject(snapshot.projects, options.workspaceRootHints);
|
|
371
|
+
return ids.map((threadId) => {
|
|
372
|
+
const thread = snapshot.threads.find((t) => t.id === threadId && t.projectId === project.id);
|
|
373
|
+
if (!thread) {
|
|
374
|
+
const any = snapshot.threads.find((t) => t.id === threadId);
|
|
375
|
+
if (!any)
|
|
376
|
+
return { threadId, state: "settled" };
|
|
377
|
+
return { threadId, state: listItemStateFromShellThread(any) };
|
|
378
|
+
}
|
|
379
|
+
return { threadId, state: listItemStateFromShellThread(thread) };
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Settle feedback threads in T3 (`thread.settle`). Missing threads are reported
|
|
385
|
+
* as `{ ok: true, skipped: true }` so dismiss can clear the local activity list.
|
|
386
|
+
*/
|
|
387
|
+
export async function settleThreadsInT3(threadIds, options) {
|
|
388
|
+
const ids = [...new Set(threadIds.map((id) => id.trim()).filter(Boolean))];
|
|
389
|
+
if (ids.length === 0)
|
|
390
|
+
return [];
|
|
391
|
+
return withShellClient(options, async (client, snapshot) => {
|
|
392
|
+
const results = [];
|
|
393
|
+
for (const threadId of ids) {
|
|
394
|
+
const exists = snapshot.threads.some((t) => t.id === threadId);
|
|
395
|
+
if (!exists) {
|
|
396
|
+
results.push({ threadId, ok: true, skipped: true });
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
await client.request("orchestration.dispatchCommand", {
|
|
401
|
+
type: "thread.settle",
|
|
402
|
+
commandId: crypto.randomUUID(),
|
|
403
|
+
threadId,
|
|
404
|
+
});
|
|
405
|
+
results.push({ threadId, ok: true });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
results.push({
|
|
409
|
+
threadId,
|
|
410
|
+
ok: false,
|
|
411
|
+
error: error instanceof Error ? error.message : String(error),
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return results;
|
|
416
|
+
});
|
|
417
|
+
}
|
package/dist/cli/bin.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CLI_NAME } from "./constants.js";
|
|
3
|
+
import { parseInitArgs, printInitHelp, runInit } from "./init.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const argv = process.argv.slice(2);
|
|
6
|
+
const [command, ...rest] = argv;
|
|
7
|
+
if (!command || command === "--help" || command === "-h") {
|
|
8
|
+
console.log(`Usage: ${CLI_NAME} <command>
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
init Detect project, confirm, install if needed, wire connector files
|
|
12
|
+
|
|
13
|
+
Run \`${CLI_NAME} init --help\` for init options.
|
|
14
|
+
`);
|
|
15
|
+
process.exit(command ? 0 : 1);
|
|
16
|
+
}
|
|
17
|
+
if (command === "init") {
|
|
18
|
+
try {
|
|
19
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
20
|
+
printInitHelp();
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
const options = parseInitArgs(rest);
|
|
24
|
+
const code = await runInit(options);
|
|
25
|
+
process.exit(code);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof Error && error.message === "help") {
|
|
29
|
+
printInitHelp();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
console.error(`Unknown command: ${command}`);
|
|
37
|
+
console.error(`Try \`${CLI_NAME} init\``);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
void main();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type HostKind = "next" | "vite" | "node";
|
|
2
|
+
export type ProjectDetection = {
|
|
3
|
+
root: string;
|
|
4
|
+
host: HostKind;
|
|
5
|
+
reasons: string[];
|
|
6
|
+
packageJsonPath: string | null;
|
|
7
|
+
viteConfigPath: string | null;
|
|
8
|
+
nextConfigPath: string | null;
|
|
9
|
+
hasAppDir: boolean;
|
|
10
|
+
alreadyInstalled: boolean;
|
|
11
|
+
alreadyWiredHint: boolean;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Detect the best host connector for `root`.
|
|
15
|
+
* Priority: Next (app/pages) > Vite config > generic Node.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectProject(root: string): ProjectDetection;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { PACKAGE_NAME } from "./constants.js";
|
|
4
|
+
const VITE_CONFIG_NAMES = [
|
|
5
|
+
"vite.config.ts",
|
|
6
|
+
"vite.config.mts",
|
|
7
|
+
"vite.config.js",
|
|
8
|
+
"vite.config.mjs",
|
|
9
|
+
"vite.config.cjs",
|
|
10
|
+
];
|
|
11
|
+
const NEXT_CONFIG_NAMES = [
|
|
12
|
+
"next.config.ts",
|
|
13
|
+
"next.config.mjs",
|
|
14
|
+
"next.config.js",
|
|
15
|
+
"next.config.cjs",
|
|
16
|
+
];
|
|
17
|
+
function readPackageJson(root) {
|
|
18
|
+
const pkgPath = path.join(root, "package.json");
|
|
19
|
+
if (!fs.existsSync(pkgPath))
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const data = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
23
|
+
return { path: pkgPath, data };
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function depMap(data) {
|
|
30
|
+
const deps = {
|
|
31
|
+
...(typeof data.dependencies === "object" && data.dependencies
|
|
32
|
+
? data.dependencies
|
|
33
|
+
: {}),
|
|
34
|
+
...(typeof data.devDependencies === "object" && data.devDependencies
|
|
35
|
+
? data.devDependencies
|
|
36
|
+
: {}),
|
|
37
|
+
...(typeof data.optionalDependencies === "object" && data.optionalDependencies
|
|
38
|
+
? data.optionalDependencies
|
|
39
|
+
: {}),
|
|
40
|
+
};
|
|
41
|
+
return deps;
|
|
42
|
+
}
|
|
43
|
+
function firstExisting(root, names) {
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const full = path.join(root, name);
|
|
46
|
+
if (fs.existsSync(full))
|
|
47
|
+
return full;
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function contentMentionsPackage(filePath) {
|
|
52
|
+
if (!filePath || !fs.existsSync(filePath))
|
|
53
|
+
return false;
|
|
54
|
+
try {
|
|
55
|
+
const text = fs.readFileSync(filePath, "utf8");
|
|
56
|
+
return text.includes(PACKAGE_NAME) || text.includes("t3FeedbackPlugin");
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Detect the best host connector for `root`.
|
|
64
|
+
* Priority: Next (app/pages) > Vite config > generic Node.
|
|
65
|
+
*/
|
|
66
|
+
export function detectProject(root) {
|
|
67
|
+
const abs = path.resolve(root);
|
|
68
|
+
const pkg = readPackageJson(abs);
|
|
69
|
+
const deps = pkg ? depMap(pkg.data) : {};
|
|
70
|
+
const reasons = [];
|
|
71
|
+
const viteConfigPath = firstExisting(abs, VITE_CONFIG_NAMES);
|
|
72
|
+
const nextConfigPath = firstExisting(abs, NEXT_CONFIG_NAMES);
|
|
73
|
+
const hasNextDep = Boolean(deps.next);
|
|
74
|
+
const hasViteDep = Boolean(deps.vite);
|
|
75
|
+
const hasAppDir = fs.existsSync(path.join(abs, "app"));
|
|
76
|
+
const hasPagesDir = fs.existsSync(path.join(abs, "pages"));
|
|
77
|
+
const alreadyInstalled = Boolean(deps[PACKAGE_NAME]);
|
|
78
|
+
let host = "node";
|
|
79
|
+
if (hasNextDep || nextConfigPath || hasAppDir || hasPagesDir) {
|
|
80
|
+
host = "next";
|
|
81
|
+
if (hasNextDep)
|
|
82
|
+
reasons.push("package.json lists next");
|
|
83
|
+
if (nextConfigPath)
|
|
84
|
+
reasons.push(`found ${path.basename(nextConfigPath)}`);
|
|
85
|
+
if (hasAppDir)
|
|
86
|
+
reasons.push("found app/");
|
|
87
|
+
if (hasPagesDir)
|
|
88
|
+
reasons.push("found pages/");
|
|
89
|
+
}
|
|
90
|
+
else if (viteConfigPath || hasViteDep) {
|
|
91
|
+
host = "vite";
|
|
92
|
+
if (viteConfigPath)
|
|
93
|
+
reasons.push(`found ${path.basename(viteConfigPath)}`);
|
|
94
|
+
if (hasViteDep)
|
|
95
|
+
reasons.push("package.json lists vite");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
reasons.push("no Next/Vite markers — generic Node bridge stub");
|
|
99
|
+
}
|
|
100
|
+
const nextRoute = path.join(abs, "app", "api", "t3-feedback", "[[...path]]", "route.ts");
|
|
101
|
+
const alreadyWiredHint = host === "vite"
|
|
102
|
+
? contentMentionsPackage(viteConfigPath)
|
|
103
|
+
: host === "next"
|
|
104
|
+
? fs.existsSync(nextRoute) || contentMentionsPackage(nextConfigPath)
|
|
105
|
+
: fs.existsSync(path.join(abs, "t3-feedback-bridge.mjs")) ||
|
|
106
|
+
fs.existsSync(path.join(abs, "t3-feedback-bridge.ts"));
|
|
107
|
+
return {
|
|
108
|
+
root: abs,
|
|
109
|
+
host,
|
|
110
|
+
reasons,
|
|
111
|
+
packageJsonPath: pkg?.path ?? null,
|
|
112
|
+
viteConfigPath,
|
|
113
|
+
nextConfigPath,
|
|
114
|
+
hasAppDir,
|
|
115
|
+
alreadyInstalled,
|
|
116
|
+
alreadyWiredHint,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { PACKAGE_NAME, CLI_NAME } from "./constants.js";
|
|
2
|
+
export { detectProject, type HostKind, type ProjectDetection } from "./detect.js";
|
|
3
|
+
export { detectPackageManager, installDevDependency, type PackageManager } from "./pm.js";
|
|
4
|
+
export { runInit, parseInitArgs, printInitHelp, type InitOptions } from "./init.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type HostKind } from "./detect.js";
|
|
2
|
+
export type InitOptions = {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
/** Skip prompts (assume yes). */
|
|
5
|
+
yes?: boolean;
|
|
6
|
+
/** Print the plan only. */
|
|
7
|
+
dryRun?: boolean;
|
|
8
|
+
/** Never run package install. */
|
|
9
|
+
noInstall?: boolean;
|
|
10
|
+
/** Force host instead of detection. */
|
|
11
|
+
host?: HostKind;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Hybrid init: detect host → confirm → install if missing → wire files.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runInit(options?: InitOptions): Promise<number>;
|
|
17
|
+
export declare function parseInitArgs(argv: string[]): InitOptions;
|
|
18
|
+
export declare function printInitHelp(): void;
|