@aayambansal/squint 0.4.5 → 0.4.7
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 +6 -0
- package/dist/App-HWMVSRMF.js +23 -0
- package/dist/{cdp-Q4H6ZHPT.js → cdp-PBQKXC6R.js} +1 -1
- package/dist/{chunk-O2S6PAJE.js → chunk-7CAGWFAQ.js} +1 -1
- package/dist/chunk-7MOKOZOR.js +1415 -0
- package/dist/{chunk-ZLEP2TWF.js → chunk-ARDV4XH6.js} +5 -1
- package/dist/{chunk-XEQ6JXXL.js → chunk-AUJJGMZG.js} +1 -1
- package/dist/{chunk-RQHOE5MV.js → chunk-HC4E42SV.js} +50 -2
- package/dist/chunk-M25IQ7C7.js +587 -0
- package/dist/chunk-NX7XQY2X.js +147 -0
- package/dist/{chunk-CV5WVKHU.js → chunk-ZSDCMYZR.js} +58 -18
- package/dist/cli.js +261 -2099
- package/dist/{contextDoctor-U3YTDFVG.js → contextDoctor-A26C2ZDN.js} +1 -1
- package/dist/{preview-ZUCVWGZG.js → preview-WT34NVCH.js} +3 -3
- package/dist/remote-6O6FPTUF.js +114 -0
- package/dist/{sandbox-J2NCPYHJ.js → sandbox-MK7Q2YNO.js} +1 -1
- package/dist/shots-OKWOYF7F.js +2 -2
- package/dist/{skills-6NZP67QT.js → skills-POB4ZZY5.js} +1 -1
- package/dist/{state-QOS7WCZO.js → state-PLY7YAD2.js} +1 -1
- package/dist/variants-3IEP7DFY.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,72 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
connectDaemon,
|
|
4
|
+
socketPath,
|
|
5
|
+
startDaemon
|
|
6
|
+
} from "./chunk-NX7XQY2X.js";
|
|
5
7
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import
|
|
15
|
-
diffStatSince,
|
|
16
|
-
isGitRepo,
|
|
17
|
-
restoreSnapshot,
|
|
18
|
-
takeSnapshot
|
|
19
|
-
} from "./chunk-YHRAOBI2.js";
|
|
20
|
-
import {
|
|
21
|
-
DevServer,
|
|
22
|
-
buildFixPrompt,
|
|
23
|
-
detectDevCommand,
|
|
24
|
-
screenshotVariants
|
|
25
|
-
} from "./chunk-QCHBDP46.js";
|
|
26
|
-
import {
|
|
27
|
-
applyVariant,
|
|
28
|
-
cleanVariants,
|
|
29
|
-
listVariants,
|
|
30
|
-
runVariants
|
|
31
|
-
} from "./chunk-YGSF2TSO.js";
|
|
32
|
-
import {
|
|
33
|
-
buildGatePrompt,
|
|
34
|
-
detectFastGates,
|
|
35
|
-
detectGates,
|
|
36
|
-
runGates
|
|
37
|
-
} from "./chunk-62JNF5M2.js";
|
|
38
|
-
import {
|
|
39
|
-
buildReviewPrompt,
|
|
40
|
-
buildRuntimeFixPrompt,
|
|
41
|
-
captureViewports,
|
|
42
|
-
comparePulse,
|
|
43
|
-
probeRuntime,
|
|
44
|
-
runtimeSummary
|
|
45
|
-
} from "./chunk-CV5WVKHU.js";
|
|
46
|
-
import {
|
|
47
|
-
clearState,
|
|
48
|
-
loadState,
|
|
49
|
-
saveState
|
|
50
|
-
} from "./chunk-O2S6PAJE.js";
|
|
8
|
+
App
|
|
9
|
+
} from "./chunk-M25IQ7C7.js";
|
|
10
|
+
import "./chunk-43NQNIJY.js";
|
|
11
|
+
import "./chunk-7MOKOZOR.js";
|
|
12
|
+
import "./chunk-ARDV4XH6.js";
|
|
13
|
+
import "./chunk-AUJJGMZG.js";
|
|
14
|
+
import "./chunk-YHRAOBI2.js";
|
|
15
|
+
import "./chunk-QCHBDP46.js";
|
|
16
|
+
import "./chunk-YGSF2TSO.js";
|
|
51
17
|
import {
|
|
52
18
|
runAgent
|
|
53
19
|
} from "./chunk-VH7OOFQP.js";
|
|
20
|
+
import "./chunk-K5QJMSJH.js";
|
|
54
21
|
import {
|
|
55
|
-
|
|
56
|
-
} from "./chunk-
|
|
22
|
+
composePrompt
|
|
23
|
+
} from "./chunk-WAJXATCO.js";
|
|
24
|
+
import "./chunk-62JNF5M2.js";
|
|
25
|
+
import "./chunk-ZSDCMYZR.js";
|
|
26
|
+
import "./chunk-7CAGWFAQ.js";
|
|
27
|
+
import "./chunk-HC4E42SV.js";
|
|
28
|
+
import "./chunk-IMDRXXFU.js";
|
|
57
29
|
import {
|
|
58
30
|
detectEngines,
|
|
59
31
|
getEngine
|
|
60
32
|
} from "./chunk-KVYGPLWW.js";
|
|
61
|
-
import "./chunk-RQHOE5MV.js";
|
|
62
|
-
import {
|
|
63
|
-
appendDecision,
|
|
64
|
-
enrich
|
|
65
|
-
} from "./chunk-ZLEP2TWF.js";
|
|
66
|
-
import "./chunk-K5QJMSJH.js";
|
|
67
|
-
import {
|
|
68
|
-
composePrompt
|
|
69
|
-
} from "./chunk-WAJXATCO.js";
|
|
70
33
|
|
|
71
34
|
// src/cli.tsx
|
|
72
35
|
import { Command } from "commander";
|
|
@@ -189,36 +152,143 @@ function registerConfig(program2) {
|
|
|
189
152
|
});
|
|
190
153
|
}
|
|
191
154
|
|
|
192
|
-
// src/cli/
|
|
155
|
+
// src/cli/daemon.ts
|
|
156
|
+
import readline from "readline";
|
|
193
157
|
import pc2 from "picocolors";
|
|
158
|
+
function registerDaemon(program2) {
|
|
159
|
+
program2.command("serve").description("run the session as a detachable daemon on .squint/daemon.sock").option("-e, --engine <id>", "engine to drive").option("-m, --model <model>", "model override").action(async (opts) => {
|
|
160
|
+
const cwd = process.cwd();
|
|
161
|
+
const config = loadConfig(defaultPaths(cwd));
|
|
162
|
+
const engineId = opts.engine ?? resolveEngineId(config);
|
|
163
|
+
const daemon = await startDaemon({
|
|
164
|
+
cwd,
|
|
165
|
+
engineId,
|
|
166
|
+
model: opts.model ?? resolveModel(config, engineId),
|
|
167
|
+
autoDev: config.autoDev,
|
|
168
|
+
autoFix: config.autoFix,
|
|
169
|
+
autoProbe: config.autoProbe,
|
|
170
|
+
autoCheck: config.autoCheck,
|
|
171
|
+
autoReview: config.autoReview,
|
|
172
|
+
fixModel: config.fixModel,
|
|
173
|
+
budgetUsd: config.budgetUsd,
|
|
174
|
+
onQuit: () => {
|
|
175
|
+
daemon.close();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
console.log(`squint daemon on ${socketPath(cwd)} (engine: ${engineId})`);
|
|
180
|
+
console.log("attach from another terminal with: squint attach");
|
|
181
|
+
const stop = () => {
|
|
182
|
+
daemon.close();
|
|
183
|
+
process.exit(0);
|
|
184
|
+
};
|
|
185
|
+
process.on("SIGINT", stop);
|
|
186
|
+
process.on("SIGTERM", stop);
|
|
187
|
+
});
|
|
188
|
+
program2.command("attach").description("attach this terminal to a running squint daemon (full TUI; --plain for line mode)").option("--plain", "line-mode attach instead of the full TUI").action(async (opts) => {
|
|
189
|
+
const cwd = process.cwd();
|
|
190
|
+
if (!opts.plain) {
|
|
191
|
+
try {
|
|
192
|
+
const { RemoteSession } = await import("./remote-6O6FPTUF.js");
|
|
193
|
+
const remote = await RemoteSession.connect(cwd);
|
|
194
|
+
const config = loadConfig(defaultPaths(cwd));
|
|
195
|
+
const { render: render2 } = await import("ink");
|
|
196
|
+
const { App: App2 } = await import("./App-HWMVSRMF.js");
|
|
197
|
+
const React = await import("react");
|
|
198
|
+
render2(
|
|
199
|
+
React.createElement(App2, {
|
|
200
|
+
cwd,
|
|
201
|
+
attachTo: remote,
|
|
202
|
+
initialEngine: remote.getState().engineId,
|
|
203
|
+
bell: config.bell,
|
|
204
|
+
initialTheme: config.theme
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
} catch {
|
|
208
|
+
console.error(`no daemon at ${socketPath(cwd)} \u2014 start one with: squint serve`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let client;
|
|
214
|
+
try {
|
|
215
|
+
client = await connectDaemon(socketPath(cwd));
|
|
216
|
+
} catch {
|
|
217
|
+
console.error(`no daemon at ${socketPath(cwd)} \u2014 start one with: squint serve`);
|
|
218
|
+
process.exitCode = 1;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
let seen = 0;
|
|
222
|
+
let role = "observer";
|
|
223
|
+
client.onMessage((msg) => {
|
|
224
|
+
if (msg.type === "hello") {
|
|
225
|
+
role = String(msg.role);
|
|
226
|
+
console.log(pc2.dim(`attached as ${role} (engine: ${msg.engineId})`));
|
|
227
|
+
if (role === "observer") console.log(pc2.dim("read-only until the driver detaches"));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (msg.type === "denied") {
|
|
231
|
+
console.log(pc2.yellow(`\u2717 ${msg.reason}`));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (msg.type !== "state") return;
|
|
235
|
+
const state = msg.state;
|
|
236
|
+
const items = state.items ?? [];
|
|
237
|
+
const fresh = seen === 0 ? items.slice(-20) : items.slice(seen);
|
|
238
|
+
seen = items.length;
|
|
239
|
+
for (const item of fresh) {
|
|
240
|
+
const line = item.role === "user" ? pc2.cyan(`> ${item.text}`) : item.role === "error" ? pc2.red(item.text) : item.role === "status" ? pc2.dim(item.text) : item.text;
|
|
241
|
+
if (item.role !== "image") console.log(line);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
245
|
+
rl.on("line", (line) => {
|
|
246
|
+
const text = line.trim();
|
|
247
|
+
if (!text) return;
|
|
248
|
+
if (text === "/detach") {
|
|
249
|
+
client.close();
|
|
250
|
+
rl.close();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
client.send(text.startsWith("/") ? { type: "command", text } : { type: "input", text });
|
|
254
|
+
});
|
|
255
|
+
rl.on("close", () => {
|
|
256
|
+
client.close();
|
|
257
|
+
process.exit(0);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// src/cli/env.ts
|
|
263
|
+
import pc3 from "picocolors";
|
|
194
264
|
function registerEnv(program2) {
|
|
195
265
|
program2.command("engines").description("List engines: installed, streaming, session resume").action(() => {
|
|
196
|
-
console.log(
|
|
266
|
+
console.log(pc3.dim(" id name stream resume where"));
|
|
197
267
|
for (const { engine, path: binaryPath } of detectEngines()) {
|
|
198
|
-
const status = binaryPath ?
|
|
199
|
-
const stream = engine.createParser ?
|
|
200
|
-
const resume = engine.supportsResume ?
|
|
201
|
-
const location = binaryPath ??
|
|
268
|
+
const status = binaryPath ? pc3.green("\u2713") : pc3.red("\u2717");
|
|
269
|
+
const stream = engine.createParser ? pc3.green("yes") : pc3.dim("text");
|
|
270
|
+
const resume = engine.supportsResume ? pc3.green("yes") : pc3.dim("no");
|
|
271
|
+
const location = binaryPath ?? pc3.dim(`not found \u2014 ${engine.install}`);
|
|
202
272
|
console.log(
|
|
203
273
|
`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${stream.padEnd(15)} ${resume.padEnd(14)} ${location}`
|
|
204
274
|
);
|
|
205
275
|
}
|
|
206
|
-
console.log(
|
|
276
|
+
console.log(pc3.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
|
|
207
277
|
});
|
|
208
278
|
program2.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
|
|
209
279
|
const [major] = process.versions.node.split(".");
|
|
210
280
|
const nodeOk = Number(major) >= 22;
|
|
211
281
|
console.log(
|
|
212
|
-
`${nodeOk ?
|
|
282
|
+
`${nodeOk ? pc3.green("\u2713") : pc3.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`
|
|
213
283
|
);
|
|
214
284
|
const detected = detectEngines();
|
|
215
285
|
for (const { engine, path: binaryPath } of detected) {
|
|
216
|
-
const status = binaryPath ?
|
|
217
|
-
console.log(`${status} ${engine.name}${binaryPath ? "" :
|
|
286
|
+
const status = binaryPath ? pc3.green("\u2713") : pc3.yellow("\u25CB");
|
|
287
|
+
console.log(`${status} ${engine.name}${binaryPath ? "" : pc3.dim(` \u2014 install: ${engine.install}`)}`);
|
|
218
288
|
}
|
|
219
289
|
if (options.probe) {
|
|
220
290
|
const { runAgent: runAgent2 } = await import("./run-NDSNTVYP.js");
|
|
221
|
-
console.log(
|
|
291
|
+
console.log(pc3.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
|
|
222
292
|
for (const { engine, path: binaryPath } of detected) {
|
|
223
293
|
if (!binaryPath) continue;
|
|
224
294
|
const startedAt = Date.now();
|
|
@@ -235,135 +305,135 @@ function registerEnv(program2) {
|
|
|
235
305
|
const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
|
|
236
306
|
const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
|
|
237
307
|
console.log(
|
|
238
|
-
result.ok ? `${
|
|
308
|
+
result.ok ? `${pc3.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc3.red("\u2717")} ${engine.id.padEnd(10)} ${pc3.dim(detail.slice(0, 110))}`
|
|
239
309
|
);
|
|
240
310
|
}
|
|
241
311
|
}
|
|
242
|
-
const { findChrome
|
|
243
|
-
const { hasWebSocket } = await import("./cdp-
|
|
244
|
-
const chrome =
|
|
312
|
+
const { findChrome } = await import("./chrome-SBV3H77F.js");
|
|
313
|
+
const { hasWebSocket } = await import("./cdp-PBQKXC6R.js");
|
|
314
|
+
const chrome = findChrome();
|
|
245
315
|
console.log(
|
|
246
|
-
chrome ? `${
|
|
316
|
+
chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
247
317
|
);
|
|
248
318
|
console.log(
|
|
249
|
-
hasWebSocket() ? `${
|
|
319
|
+
hasWebSocket() ? `${pc3.green("\u2713")} WebSocket ${pc3.dim("runtime console/network capture available")}` : `${pc3.yellow("\u25CB")} WebSocket ${pc3.dim("\u2014 node 22+ enables runtime capture; screenshots still work")}`
|
|
250
320
|
);
|
|
251
321
|
const available = detected.filter((d) => d.path !== null);
|
|
252
322
|
if (available.length === 0) {
|
|
253
|
-
console.log(
|
|
323
|
+
console.log(pc3.red("\nNo engines found. Install at least one to use squint."));
|
|
254
324
|
process.exitCode = 1;
|
|
255
325
|
} else {
|
|
256
|
-
console.log(
|
|
326
|
+
console.log(pc3.dim(`
|
|
257
327
|
${available.length} engine(s) ready.`));
|
|
258
328
|
}
|
|
259
329
|
});
|
|
260
330
|
}
|
|
261
331
|
|
|
262
332
|
// src/cli/project.ts
|
|
263
|
-
import
|
|
333
|
+
import pc4 from "picocolors";
|
|
264
334
|
function registerProject(program2) {
|
|
265
335
|
const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
266
336
|
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
267
|
-
const { loadRules, loadSkills } = await import("./skills-
|
|
337
|
+
const { loadRules, loadSkills } = await import("./skills-POB4ZZY5.js");
|
|
268
338
|
const cwd = process.cwd();
|
|
269
339
|
const rules = loadRules(cwd);
|
|
270
340
|
console.log(
|
|
271
|
-
rules ? `${
|
|
341
|
+
rules ? `${pc4.green("\u2713")} rules.md ${pc4.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc4.dim("\u25CB no .squint/rules.md")
|
|
272
342
|
);
|
|
273
343
|
const skills = loadSkills(cwd);
|
|
274
344
|
if (skills.length === 0) {
|
|
275
|
-
console.log(
|
|
345
|
+
console.log(pc4.dim("\u25CB no skills \u2014 squint skills init writes an example"));
|
|
276
346
|
return;
|
|
277
347
|
}
|
|
278
348
|
for (const skill of skills) {
|
|
279
|
-
console.log(`${
|
|
349
|
+
console.log(`${pc4.green("\u2713")} ${skill.name.padEnd(20)} ${pc4.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
|
|
280
350
|
}
|
|
281
351
|
});
|
|
282
352
|
skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
|
|
283
|
-
const
|
|
353
|
+
const fs2 = await import("fs");
|
|
284
354
|
const nodePath = await import("path");
|
|
285
355
|
const cwd = process.cwd();
|
|
286
356
|
const skillsDir = nodePath.join(cwd, ".squint", "skills");
|
|
287
|
-
|
|
357
|
+
fs2.mkdirSync(skillsDir, { recursive: true });
|
|
288
358
|
const rules = nodePath.join(cwd, ".squint", "rules.md");
|
|
289
|
-
if (!
|
|
290
|
-
|
|
359
|
+
if (!fs2.existsSync(rules)) {
|
|
360
|
+
fs2.writeFileSync(
|
|
291
361
|
rules,
|
|
292
362
|
"# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
|
|
293
363
|
);
|
|
294
|
-
console.log(
|
|
364
|
+
console.log(pc4.green("\u2713 .squint/rules.md"));
|
|
295
365
|
}
|
|
296
366
|
const example = nodePath.join(skillsDir, "example.md");
|
|
297
|
-
if (!
|
|
298
|
-
|
|
367
|
+
if (!fs2.existsSync(example)) {
|
|
368
|
+
fs2.writeFileSync(
|
|
299
369
|
example,
|
|
300
370
|
"---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
|
|
301
371
|
);
|
|
302
|
-
console.log(
|
|
372
|
+
console.log(pc4.green("\u2713 .squint/skills/example.md"));
|
|
303
373
|
}
|
|
304
|
-
console.log(
|
|
374
|
+
console.log(pc4.dim("rules are always-on; skills inject when an ask mentions a trigger"));
|
|
305
375
|
});
|
|
306
376
|
program2.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
|
|
307
|
-
const
|
|
377
|
+
const fs2 = await import("fs");
|
|
308
378
|
const nodePath = await import("path");
|
|
309
379
|
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
|
|
310
380
|
if (!familyId) {
|
|
311
|
-
console.log(
|
|
381
|
+
console.log(pc4.bold("Aesthetic families") + pc4.dim(" \u2014 squint brief <id>\n"));
|
|
312
382
|
for (const family2 of FAMILIES) {
|
|
313
|
-
console.log(`${
|
|
383
|
+
console.log(`${pc4.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc4.dim(family2.summary)}`);
|
|
314
384
|
}
|
|
315
|
-
console.log(
|
|
385
|
+
console.log(pc4.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
|
|
316
386
|
return;
|
|
317
387
|
}
|
|
318
388
|
const family = getFamily(familyId);
|
|
319
389
|
if (!family) {
|
|
320
|
-
console.error(
|
|
390
|
+
console.error(pc4.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
|
|
321
391
|
process.exitCode = 1;
|
|
322
392
|
return;
|
|
323
393
|
}
|
|
324
394
|
const target = nodePath.join(process.cwd(), ".squint", "brief.md");
|
|
325
|
-
if (
|
|
326
|
-
console.error(
|
|
395
|
+
if (fs2.existsSync(target) && !options.force) {
|
|
396
|
+
console.error(pc4.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
|
|
327
397
|
process.exitCode = 1;
|
|
328
398
|
return;
|
|
329
399
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
console.log(
|
|
333
|
-
console.log(
|
|
400
|
+
fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
401
|
+
fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
|
|
402
|
+
console.log(pc4.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
|
|
403
|
+
console.log(pc4.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
|
|
334
404
|
});
|
|
335
405
|
const sandboxCommand = program2.command("sandbox").description("Cumulative diff worktree \u2014 asks accumulate until you apply");
|
|
336
406
|
sandboxCommand.command("diff").description("Show accumulated sandbox changes").action(async () => {
|
|
337
|
-
const { sandboxDiffStat
|
|
407
|
+
const { sandboxDiffStat, sandboxExists, sandboxFiles } = await import("./sandbox-MK7Q2YNO.js");
|
|
338
408
|
const cwd = process.cwd();
|
|
339
|
-
if (!
|
|
340
|
-
console.log(
|
|
409
|
+
if (!sandboxExists(cwd)) {
|
|
410
|
+
console.log(pc4.dim("no sandbox open \u2014 /sandbox on inside the TUI"));
|
|
341
411
|
return;
|
|
342
412
|
}
|
|
343
|
-
const stat =
|
|
413
|
+
const stat = sandboxDiffStat(cwd);
|
|
344
414
|
if (!stat) {
|
|
345
|
-
console.log(
|
|
415
|
+
console.log(pc4.dim("sandbox is clean"));
|
|
346
416
|
return;
|
|
347
417
|
}
|
|
348
418
|
console.log(stat);
|
|
349
|
-
for (const line of
|
|
419
|
+
for (const line of sandboxFiles(cwd)) console.log(pc4.dim(line));
|
|
350
420
|
});
|
|
351
421
|
sandboxCommand.command("apply").description("Land the sandbox diff on the real tree and close it").action(async () => {
|
|
352
|
-
const { applySandbox
|
|
422
|
+
const { applySandbox, discardSandbox } = await import("./sandbox-MK7Q2YNO.js");
|
|
353
423
|
const cwd = process.cwd();
|
|
354
|
-
const result =
|
|
424
|
+
const result = applySandbox(cwd);
|
|
355
425
|
if (!result.ok) {
|
|
356
|
-
console.error(
|
|
426
|
+
console.error(pc4.red(`\u2717 ${result.detail}`));
|
|
357
427
|
process.exitCode = 1;
|
|
358
428
|
return;
|
|
359
429
|
}
|
|
360
|
-
|
|
361
|
-
console.log(
|
|
430
|
+
discardSandbox(cwd);
|
|
431
|
+
console.log(pc4.green("\u2713 sandbox applied to the real tree") + pc4.dim(" \u2014 review with git diff"));
|
|
362
432
|
});
|
|
363
433
|
sandboxCommand.command("discard").description("Close the sandbox without touching the real tree").action(async () => {
|
|
364
|
-
const { discardSandbox
|
|
365
|
-
const had =
|
|
366
|
-
console.log(
|
|
434
|
+
const { discardSandbox } = await import("./sandbox-MK7Q2YNO.js");
|
|
435
|
+
const had = discardSandbox(process.cwd());
|
|
436
|
+
console.log(pc4.dim(had ? "sandbox discarded" : "no sandbox open"));
|
|
367
437
|
});
|
|
368
438
|
const variantsCommand = program2.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
|
|
369
439
|
variantsCommand.command("gen").description("Generate n variants of one ask in parallel (n engine runs \u2014 n\xD7 cost)").argument("<n>", "how many variants (max 4)").argument("<prompt...>", "what to build").option("-e, --engine <id>", "engine to use").option("-m, --model <name>", "model override").option("--no-shots", "skip the screenshot pass").action(
|
|
@@ -371,119 +441,119 @@ function registerProject(program2) {
|
|
|
371
441
|
const cwd = process.cwd();
|
|
372
442
|
const n = Number.parseInt(nRaw, 10);
|
|
373
443
|
if (!Number.isInteger(n) || n < 2 || n > 4) {
|
|
374
|
-
console.error(
|
|
444
|
+
console.error(pc4.red("\u2717 n must be 2\u20134"));
|
|
375
445
|
process.exitCode = 1;
|
|
376
446
|
return;
|
|
377
447
|
}
|
|
378
|
-
const { isGitRepo
|
|
379
|
-
if (!
|
|
380
|
-
console.error(
|
|
448
|
+
const { isGitRepo } = await import("./snapshot-H6VYFHLN.js");
|
|
449
|
+
if (!isGitRepo(cwd)) {
|
|
450
|
+
console.error(pc4.red("\u2717 variants need a git repo with at least one commit"));
|
|
381
451
|
process.exitCode = 1;
|
|
382
452
|
return;
|
|
383
453
|
}
|
|
384
|
-
const { runVariants
|
|
454
|
+
const { runVariants, cleanVariants } = await import("./variants-3IEP7DFY.js");
|
|
385
455
|
const config = loadConfig(defaultPaths(cwd));
|
|
386
456
|
const engineId = resolveEngineId(config, options.engine);
|
|
387
457
|
const engine = getEngine(engineId);
|
|
388
458
|
const model = resolveModel(config, engineId, options.model);
|
|
389
459
|
const ask = promptWords.join(" ");
|
|
390
|
-
|
|
391
|
-
console.log(
|
|
392
|
-
const runs = await
|
|
460
|
+
cleanVariants(cwd);
|
|
461
|
+
console.log(pc4.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
|
|
462
|
+
const runs = await runVariants(
|
|
393
463
|
cwd,
|
|
394
464
|
ask,
|
|
395
465
|
n,
|
|
396
466
|
engine,
|
|
397
467
|
model,
|
|
398
|
-
(familyId, text) => console.log(`${
|
|
468
|
+
(familyId, text) => console.log(`${pc4.cyan(familyId.padEnd(18))} ${pc4.dim(text)}`)
|
|
399
469
|
);
|
|
400
470
|
const succeeded = runs.filter((r) => r.result.ok);
|
|
401
471
|
if (options.shots && succeeded.length > 0) {
|
|
402
|
-
const { screenshotVariants
|
|
403
|
-
console.log(
|
|
404
|
-
const shots = await
|
|
472
|
+
const { screenshotVariants } = await import("./shots-OKWOYF7F.js");
|
|
473
|
+
console.log(pc4.dim("capturing screenshots\u2026"));
|
|
474
|
+
const shots = await screenshotVariants(cwd, succeeded.map((r) => r.variant));
|
|
405
475
|
for (const shot of shots) {
|
|
406
|
-
console.log(`${
|
|
476
|
+
console.log(`${pc4.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc4.dim(shot.error ?? "")}`);
|
|
407
477
|
}
|
|
408
478
|
}
|
|
409
479
|
console.log(
|
|
410
480
|
`
|
|
411
|
-
${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` +
|
|
481
|
+
${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 ` + pc4.bold("squint variants apply <id>") + pc4.dim(" applies the winner, squint variants clean discards all")
|
|
412
482
|
);
|
|
413
483
|
if (succeeded.length === 0) process.exitCode = 1;
|
|
414
484
|
}
|
|
415
485
|
);
|
|
416
486
|
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
417
|
-
const { listVariants
|
|
418
|
-
const ids =
|
|
487
|
+
const { listVariants } = await import("./variants-3IEP7DFY.js");
|
|
488
|
+
const ids = listVariants(process.cwd());
|
|
419
489
|
if (ids.length === 0) {
|
|
420
|
-
console.log(
|
|
490
|
+
console.log(pc4.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
421
491
|
return;
|
|
422
492
|
}
|
|
423
493
|
for (const id of ids) console.log(id);
|
|
424
494
|
});
|
|
425
495
|
variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
|
|
426
|
-
const { applyVariant
|
|
496
|
+
const { applyVariant, cleanVariants } = await import("./variants-3IEP7DFY.js");
|
|
427
497
|
const cwd = process.cwd();
|
|
428
|
-
const result =
|
|
498
|
+
const result = applyVariant(cwd, id);
|
|
429
499
|
if (!result.ok) {
|
|
430
|
-
console.error(
|
|
500
|
+
console.error(pc4.red(`\u2717 ${result.detail}`));
|
|
431
501
|
process.exitCode = 1;
|
|
432
502
|
return;
|
|
433
503
|
}
|
|
434
|
-
|
|
435
|
-
console.log(
|
|
504
|
+
cleanVariants(cwd);
|
|
505
|
+
console.log(pc4.green(`\u2713 applied ${id} to the working tree`) + pc4.dim(" \u2014 review with git diff"));
|
|
436
506
|
});
|
|
437
507
|
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
438
|
-
const { cleanVariants
|
|
439
|
-
const count =
|
|
440
|
-
console.log(
|
|
508
|
+
const { cleanVariants } = await import("./variants-3IEP7DFY.js");
|
|
509
|
+
const count = cleanVariants(process.cwd());
|
|
510
|
+
console.log(pc4.dim(`removed ${count} variant(s)`));
|
|
441
511
|
});
|
|
442
512
|
}
|
|
443
513
|
|
|
444
514
|
// src/cli/quality.ts
|
|
445
|
-
import
|
|
515
|
+
import pc5 from "picocolors";
|
|
446
516
|
function registerQuality(program2) {
|
|
447
517
|
program2.command("check").description("Run this project\u2019s quality gates (typecheck, lint, format, test, build)").action(async () => {
|
|
448
|
-
const { detectGates
|
|
518
|
+
const { detectGates, runGates } = await import("./gates-ZC67NMW3.js");
|
|
449
519
|
const cwd = process.cwd();
|
|
450
|
-
const gates =
|
|
520
|
+
const gates = detectGates(cwd);
|
|
451
521
|
if (gates.length === 0) {
|
|
452
|
-
console.log(
|
|
522
|
+
console.log(pc5.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
|
|
453
523
|
return;
|
|
454
524
|
}
|
|
455
|
-
console.log(
|
|
456
|
-
const results = await
|
|
457
|
-
const mark = result.ok ?
|
|
525
|
+
console.log(pc5.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
|
|
526
|
+
const results = await runGates(cwd, gates, (result) => {
|
|
527
|
+
const mark = result.ok ? pc5.green("\u2713") : pc5.red("\u2717");
|
|
458
528
|
console.log(
|
|
459
|
-
`${mark} ${result.gate.id.padEnd(10)} ${
|
|
529
|
+
`${mark} ${result.gate.id.padEnd(10)} ${pc5.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`
|
|
460
530
|
);
|
|
461
|
-
if (!result.ok) console.log(
|
|
531
|
+
if (!result.ok) console.log(pc5.dim(result.outputTail.split("\n").slice(-12).join("\n")));
|
|
462
532
|
});
|
|
463
533
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
464
534
|
});
|
|
465
535
|
program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
466
|
-
const { captureViewports
|
|
467
|
-
const result = await
|
|
536
|
+
const { captureViewports } = await import("./preview-WT34NVCH.js");
|
|
537
|
+
const result = await captureViewports(process.cwd(), url);
|
|
468
538
|
if (!result) {
|
|
469
|
-
console.error(
|
|
539
|
+
console.error(pc5.red("\u2717 no Chrome/Chromium found"));
|
|
470
540
|
process.exitCode = 1;
|
|
471
541
|
return;
|
|
472
542
|
}
|
|
473
|
-
for (const shot of result.shots) console.log(`${
|
|
474
|
-
for (const error of result.errors) console.error(
|
|
543
|
+
for (const shot of result.shots) console.log(`${pc5.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
|
|
544
|
+
for (const error of result.errors) console.error(pc5.red(`\u2717 ${error}`));
|
|
475
545
|
if (result.shots.length === 0) process.exitCode = 1;
|
|
476
546
|
});
|
|
477
547
|
}
|
|
478
548
|
|
|
479
549
|
// src/cli/run.ts
|
|
480
|
-
import
|
|
550
|
+
import pc6 from "picocolors";
|
|
481
551
|
function createPrinter() {
|
|
482
552
|
let streaming = false;
|
|
483
553
|
return (event) => {
|
|
484
554
|
switch (event.type) {
|
|
485
555
|
case "status":
|
|
486
|
-
console.log(
|
|
556
|
+
console.log(pc6.dim(`\xB7 ${event.text}`));
|
|
487
557
|
break;
|
|
488
558
|
case "delta":
|
|
489
559
|
streaming = true;
|
|
@@ -498,17 +568,17 @@ function createPrinter() {
|
|
|
498
568
|
streaming = false;
|
|
499
569
|
break;
|
|
500
570
|
case "thinking":
|
|
501
|
-
console.log(
|
|
571
|
+
console.log(pc6.dim(pc6.italic(event.text)));
|
|
502
572
|
break;
|
|
503
573
|
case "tool":
|
|
504
574
|
if (streaming) {
|
|
505
575
|
process.stdout.write("\n");
|
|
506
576
|
streaming = false;
|
|
507
577
|
}
|
|
508
|
-
console.log(
|
|
578
|
+
console.log(pc6.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
|
|
509
579
|
break;
|
|
510
580
|
case "error":
|
|
511
|
-
console.error(
|
|
581
|
+
console.error(pc6.red(`\u2717 ${event.text}`));
|
|
512
582
|
break;
|
|
513
583
|
case "result":
|
|
514
584
|
case "raw":
|
|
@@ -521,7 +591,7 @@ function registerRun(program2) {
|
|
|
521
591
|
async (promptWords, options) => {
|
|
522
592
|
const cwd = process.cwd();
|
|
523
593
|
if (options.mode && !["plan", "safe", "yolo"].includes(options.mode)) {
|
|
524
|
-
console.error(
|
|
594
|
+
console.error(pc6.red("\u2717 --mode must be plan, safe, or yolo"));
|
|
525
595
|
process.exitCode = 1;
|
|
526
596
|
return;
|
|
527
597
|
}
|
|
@@ -537,7 +607,7 @@ function registerRun(program2) {
|
|
|
537
607
|
} : createPrinter();
|
|
538
608
|
if (!options.json)
|
|
539
609
|
console.log(
|
|
540
|
-
|
|
610
|
+
pc6.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`)
|
|
541
611
|
);
|
|
542
612
|
const result = await runAgent(engine, { prompt, cwd, model, mode }, onEvent);
|
|
543
613
|
if (options.json) {
|
|
@@ -545,7 +615,7 @@ function registerRun(program2) {
|
|
|
545
615
|
} else if (result.ok) {
|
|
546
616
|
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
547
617
|
const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
|
|
548
|
-
console.log(
|
|
618
|
+
console.log(pc6.green(`\u2713 done${secs}${cost}`));
|
|
549
619
|
}
|
|
550
620
|
if (!result.ok) process.exitCode = 1;
|
|
551
621
|
}
|
|
@@ -553,7 +623,7 @@ function registerRun(program2) {
|
|
|
553
623
|
}
|
|
554
624
|
|
|
555
625
|
// src/cli/scaffold.ts
|
|
556
|
-
import
|
|
626
|
+
import pc7 from "picocolors";
|
|
557
627
|
function registerScaffold(program2) {
|
|
558
628
|
program2.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
|
|
559
629
|
const { installDependencies, writeTemplate } = await import("./init-YB2IMA4N.js");
|
|
@@ -561,1983 +631,74 @@ function registerScaffold(program2) {
|
|
|
561
631
|
try {
|
|
562
632
|
result = writeTemplate(dir, { force: options.force });
|
|
563
633
|
} catch (err) {
|
|
564
|
-
console.error(
|
|
634
|
+
console.error(pc7.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
|
|
565
635
|
process.exitCode = 1;
|
|
566
636
|
return;
|
|
567
637
|
}
|
|
568
|
-
console.log(
|
|
638
|
+
console.log(pc7.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
|
|
569
639
|
if (options.install) {
|
|
570
|
-
console.log(
|
|
640
|
+
console.log(pc7.dim("installing dependencies\u2026"));
|
|
571
641
|
const ok = await installDependencies(result.dir);
|
|
572
642
|
if (!ok) {
|
|
573
|
-
console.error(
|
|
643
|
+
console.error(pc7.red("\u2717 npm install failed \u2014 run it manually"));
|
|
574
644
|
process.exitCode = 1;
|
|
575
645
|
return;
|
|
576
646
|
}
|
|
577
647
|
}
|
|
578
648
|
const cd = result.dir === "." ? "" : `cd ${result.dir} && `;
|
|
579
649
|
console.log(`
|
|
580
|
-
Next: ${
|
|
581
|
-
console.log(
|
|
650
|
+
Next: ${pc7.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
651
|
+
console.log(pc7.dim("then describe what to build \u2014 /dev starts the preview server"));
|
|
582
652
|
});
|
|
583
653
|
program2.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
|
|
584
|
-
const
|
|
654
|
+
const fs2 = await import("fs");
|
|
585
655
|
const nodePath = await import("path");
|
|
586
656
|
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
|
|
587
657
|
const cwd = process.cwd();
|
|
588
658
|
const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
|
|
589
|
-
|
|
590
|
-
console.log(
|
|
591
|
-
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) =>
|
|
659
|
+
fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
|
|
660
|
+
console.log(pc7.green(`\u2713 ${TAGGER_FILENAME} written`));
|
|
661
|
+
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs2.existsSync(candidate));
|
|
592
662
|
if (!configPath) {
|
|
593
|
-
console.log(
|
|
594
|
-
console.log(
|
|
663
|
+
console.log(pc7.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
|
|
664
|
+
console.log(pc7.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
595
665
|
plugins: [squintTagger(), \u2026]`));
|
|
596
666
|
return;
|
|
597
667
|
}
|
|
598
|
-
const source =
|
|
668
|
+
const source = fs2.readFileSync(configPath, "utf8");
|
|
599
669
|
const patched = patchViteConfig(source);
|
|
600
670
|
if (patched === "already") {
|
|
601
|
-
console.log(
|
|
671
|
+
console.log(pc7.dim("vite config already wired"));
|
|
602
672
|
} else if (patched === null) {
|
|
603
|
-
console.log(
|
|
604
|
-
console.log(
|
|
673
|
+
console.log(pc7.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
|
|
674
|
+
console.log(pc7.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
605
675
|
plugins: [squintTagger(), \u2026]`));
|
|
606
676
|
} else {
|
|
607
|
-
|
|
608
|
-
console.log(
|
|
677
|
+
fs2.writeFileSync(configPath, patched);
|
|
678
|
+
console.log(pc7.green(`\u2713 ${nodePath.basename(configPath)} wired`));
|
|
609
679
|
}
|
|
610
680
|
console.log(
|
|
611
|
-
|
|
681
|
+
pc7.dim("\nin the running app: Alt+S \u2192 click elements to pin, alt+enter copies all \u2014 paste into squint")
|
|
612
682
|
);
|
|
613
683
|
});
|
|
614
684
|
}
|
|
615
685
|
|
|
616
686
|
// src/cli/tui.tsx
|
|
617
687
|
import { render } from "ink";
|
|
618
|
-
|
|
619
|
-
// src/tui/App.tsx
|
|
620
|
-
import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
|
|
621
|
-
import { InkPictureProvider } from "ink-picture";
|
|
622
|
-
import path4 from "path";
|
|
623
|
-
import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
|
|
624
|
-
|
|
625
|
-
// src/session/engine.ts
|
|
626
|
-
import fs3 from "fs";
|
|
627
|
-
import path3 from "path";
|
|
628
|
-
|
|
629
|
-
// src/session/hooks.ts
|
|
630
|
-
import { spawn } from "child_process";
|
|
631
|
-
import fs2 from "fs";
|
|
632
|
-
import path2 from "path";
|
|
633
|
-
function runHook(cwd, event, payload) {
|
|
634
|
-
const script = path2.join(cwd, ".squint", "hooks", event);
|
|
635
|
-
try {
|
|
636
|
-
fs2.accessSync(script, fs2.constants.X_OK);
|
|
637
|
-
} catch {
|
|
638
|
-
return false;
|
|
639
|
-
}
|
|
640
|
-
try {
|
|
641
|
-
const child = spawn(script, [], {
|
|
642
|
-
cwd,
|
|
643
|
-
env: { ...process.env, SQUINT_EVENT: event, ...prefixed(payload) },
|
|
644
|
-
stdio: "ignore",
|
|
645
|
-
detached: false
|
|
646
|
-
});
|
|
647
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
648
|
-
child.on("close", () => clearTimeout(timer));
|
|
649
|
-
child.on("error", () => clearTimeout(timer));
|
|
650
|
-
return true;
|
|
651
|
-
} catch {
|
|
652
|
-
return false;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
function prefixed(payload) {
|
|
656
|
-
const out = {};
|
|
657
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
658
|
-
out[`SQUINT_${key.toUpperCase()}`] = value;
|
|
659
|
-
}
|
|
660
|
-
return out;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// src/session/engine.ts
|
|
664
|
-
var MAX_AUTO_FIX_ATTEMPTS = 2;
|
|
665
|
-
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
666
|
-
var Session = class {
|
|
667
|
-
constructor(opts) {
|
|
668
|
-
this.opts = opts;
|
|
669
|
-
this.state = {
|
|
670
|
-
items: [],
|
|
671
|
-
liveText: "",
|
|
672
|
-
running: false,
|
|
673
|
-
runStartedAt: 0,
|
|
674
|
-
engineId: opts.engineId,
|
|
675
|
-
model: opts.model,
|
|
676
|
-
devState: "stopped",
|
|
677
|
-
devUrl: null,
|
|
678
|
-
totals: { costUsd: 0, turns: 0 },
|
|
679
|
-
queue: [],
|
|
680
|
-
mode: "safe",
|
|
681
|
-
problems: [],
|
|
682
|
-
sandbox: false
|
|
683
|
-
};
|
|
684
|
-
if (opts.autoDev && detectDevCommand(opts.cwd)) {
|
|
685
|
-
this.devServer().start();
|
|
686
|
-
}
|
|
687
|
-
const saved = loadState(opts.cwd);
|
|
688
|
-
if (saved) {
|
|
689
|
-
try {
|
|
690
|
-
if (getEngine(saved.engine).supportsResume) {
|
|
691
|
-
const mins = Math.max(1, Math.round((Date.now() - saved.at) / 6e4));
|
|
692
|
-
this.push(
|
|
693
|
-
"status",
|
|
694
|
-
`previous session (${mins}m ago${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}) \u2014 /resume to continue`
|
|
695
|
-
);
|
|
696
|
-
}
|
|
697
|
-
} catch {
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
opts;
|
|
702
|
-
state;
|
|
703
|
-
listeners = /* @__PURE__ */ new Set();
|
|
704
|
-
nextId = 0;
|
|
705
|
-
live = "";
|
|
706
|
-
sessionId;
|
|
707
|
-
dev = null;
|
|
708
|
-
abort = null;
|
|
709
|
-
/** Full problem records; state carries the summaries. */
|
|
710
|
-
problemPrompts = /* @__PURE__ */ new Map();
|
|
711
|
-
nextProblemId = 0;
|
|
712
|
-
checkpoints = [];
|
|
713
|
-
fixAttempts = 0;
|
|
714
|
-
reviewTipShown = false;
|
|
715
|
-
pendingApproval = null;
|
|
716
|
-
lastPulse = null;
|
|
717
|
-
lastPerf = null;
|
|
718
|
-
autoReviewedThisAsk = false;
|
|
719
|
-
startedAt = Date.now();
|
|
720
|
-
subscribe(listener) {
|
|
721
|
-
this.listeners.add(listener);
|
|
722
|
-
return () => this.listeners.delete(listener);
|
|
723
|
-
}
|
|
724
|
-
getState() {
|
|
725
|
-
return this.state;
|
|
726
|
-
}
|
|
727
|
-
dispose() {
|
|
728
|
-
this.abort?.abort();
|
|
729
|
-
this.dev?.stop();
|
|
730
|
-
}
|
|
731
|
-
interrupt() {
|
|
732
|
-
this.abort?.abort();
|
|
733
|
-
}
|
|
734
|
-
/** Frontend-originated status line (view-level commands like /theme). */
|
|
735
|
-
note(text) {
|
|
736
|
-
this.push("status", text);
|
|
737
|
-
}
|
|
738
|
-
/** Register a finding; a fresh finding from a source supersedes its old one. */
|
|
739
|
-
addProblem(source, summary, prompt) {
|
|
740
|
-
const kept = this.state.problems.filter((p) => p.source !== source);
|
|
741
|
-
for (const gone of this.state.problems) {
|
|
742
|
-
if (gone.source === source) this.problemPrompts.delete(gone.id);
|
|
743
|
-
}
|
|
744
|
-
this.nextProblemId += 1;
|
|
745
|
-
this.problemPrompts.set(this.nextProblemId, prompt);
|
|
746
|
-
this.notify({ problems: [...kept, { id: this.nextProblemId, source, summary }] });
|
|
747
|
-
runHook(this.opts.cwd, "on-problem", { source, summary });
|
|
748
|
-
}
|
|
749
|
-
clearProblems(source) {
|
|
750
|
-
const removed = this.state.problems.filter((p) => p.source === source);
|
|
751
|
-
if (removed.length === 0) return;
|
|
752
|
-
for (const problem of removed) this.problemPrompts.delete(problem.id);
|
|
753
|
-
this.notify({ problems: this.state.problems.filter((p) => p.source !== source) });
|
|
754
|
-
}
|
|
755
|
-
/** One prompt covering the given problems, oldest first. */
|
|
756
|
-
combinedFixPrompt(problems) {
|
|
757
|
-
const sections = problems.map(
|
|
758
|
-
(p) => this.problemPrompts.get(p.id) ?? `Fix this reported problem: ${p.summary}`
|
|
759
|
-
);
|
|
760
|
-
return sections.join("\n\n---\n\n");
|
|
761
|
-
}
|
|
762
|
-
dispatchFix(problems) {
|
|
763
|
-
if (problems.length === 0) return;
|
|
764
|
-
const prompt = this.combinedFixPrompt(problems);
|
|
765
|
-
const fixModel = this.opts.fixModel;
|
|
766
|
-
const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}${fixModel ? ` \xB7 ${fixModel}` : ""}`;
|
|
767
|
-
for (const problem of problems) this.problemPrompts.delete(problem.id);
|
|
768
|
-
this.notify({ problems: this.state.problems.filter((p) => !problems.includes(p)) });
|
|
769
|
-
void this.runTurn(prompt, display, fixModel);
|
|
770
|
-
}
|
|
771
|
-
/** Launch a capped auto-fix turn over all open problems. Returns true if launched. */
|
|
772
|
-
maybeAutoFix() {
|
|
773
|
-
if (!this.opts.autoFix || this.fixAttempts >= MAX_AUTO_FIX_ATTEMPTS) return false;
|
|
774
|
-
if (this.state.problems.length === 0) return false;
|
|
775
|
-
this.fixAttempts += 1;
|
|
776
|
-
this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
|
|
777
|
-
this.notify({ running: false });
|
|
778
|
-
this.dispatchFix([...this.state.problems]);
|
|
779
|
-
return true;
|
|
780
|
-
}
|
|
781
|
-
/**
|
|
782
|
-
* A side question: read-only, no session resume, so the main thread's
|
|
783
|
-
* context is untouched (Cursor's /btw). Costs count; loops don't run.
|
|
784
|
-
*/
|
|
785
|
-
async btw(question) {
|
|
786
|
-
this.push("user", `\u{1F4AC} btw: ${question}`);
|
|
787
|
-
this.notify({ running: true, runStartedAt: Date.now() });
|
|
788
|
-
const engine = getEngine(this.state.engineId);
|
|
789
|
-
this.abort = new AbortController();
|
|
790
|
-
const result = await runAgent(
|
|
791
|
-
engine,
|
|
792
|
-
{
|
|
793
|
-
prompt: `Answer this question about the repository. Investigate as needed but make no changes:
|
|
794
|
-
|
|
795
|
-
${question}`,
|
|
796
|
-
cwd: this.execCwd(),
|
|
797
|
-
model: this.state.model,
|
|
798
|
-
mode: "plan"
|
|
799
|
-
},
|
|
800
|
-
this.handleEvent,
|
|
801
|
-
this.abort.signal
|
|
802
|
-
);
|
|
803
|
-
this.abort = null;
|
|
804
|
-
this.commitLive();
|
|
805
|
-
this.flushToolCollapse();
|
|
806
|
-
if (result.ok) {
|
|
807
|
-
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
808
|
-
this.push("status", `btw answered${cost}`);
|
|
809
|
-
this.notify({
|
|
810
|
-
totals: { costUsd: this.state.totals.costUsd + (result.costUsd ?? 0), turns: this.state.totals.turns }
|
|
811
|
-
});
|
|
812
|
-
}
|
|
813
|
-
this.notify({ running: false });
|
|
814
|
-
this.drainQueue();
|
|
815
|
-
}
|
|
816
|
-
/**
|
|
817
|
-
* Unattended polish: n rounds of screenshot-review-fix. Each round is
|
|
818
|
-
* a full review turn (its own cost); the per-ask auto-fix cap resets
|
|
819
|
-
* per round so fixes still flow. Fire it and step away.
|
|
820
|
-
*/
|
|
821
|
-
async polish(rounds) {
|
|
822
|
-
for (let round = 1; round <= rounds; round++) {
|
|
823
|
-
this.fixAttempts = 0;
|
|
824
|
-
const result = await this.capture();
|
|
825
|
-
if (!result) {
|
|
826
|
-
this.push("status", `polish stopped at round ${round}: nothing to capture`);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
await this.runTurn(
|
|
830
|
-
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components),
|
|
831
|
-
`\u{1F441} polish round ${round}/${rounds}`
|
|
832
|
-
);
|
|
833
|
-
}
|
|
834
|
-
this.push("status", `polish complete \u2014 ${rounds} round${rounds === 1 ? "" : "s"} of review and fixes`);
|
|
835
|
-
}
|
|
836
|
-
setMode(mode) {
|
|
837
|
-
this.notify({ mode });
|
|
838
|
-
const hint = mode === "plan" ? "read-only: the engine investigates and proposes, edits nothing" : mode === "yolo" ? "no approval friction \u2014 the engine can do anything" : "edits auto-approved inside the workspace";
|
|
839
|
-
this.push("status", `mode \u2192 ${mode} \xB7 ${hint}`);
|
|
840
|
-
}
|
|
841
|
-
cycleMode() {
|
|
842
|
-
const order = ["safe", "plan", "yolo"];
|
|
843
|
-
const next = order[(order.indexOf(this.state.mode) + 1) % order.length];
|
|
844
|
-
this.setMode(next);
|
|
845
|
-
}
|
|
846
|
-
/** One-line goodbye: what this session amounted to. */
|
|
847
|
-
summary() {
|
|
848
|
-
const mins = Math.max(1, Math.round((Date.now() - this.startedAt) / 6e4));
|
|
849
|
-
const { turns, costUsd } = this.state.totals;
|
|
850
|
-
const parts = [`${turns} turn${turns === 1 ? "" : "s"}`];
|
|
851
|
-
if (costUsd > 0) parts.push(`$${costUsd.toFixed(2)}`);
|
|
852
|
-
parts.push(`${mins}m`);
|
|
853
|
-
return `session: ${parts.join(" \xB7 ")}`;
|
|
854
|
-
}
|
|
855
|
-
/**
|
|
856
|
-
* Route one line of user input: slash command or an ask for the engine.
|
|
857
|
-
* Asks arriving mid-turn queue up and dispatch in order once the
|
|
858
|
-
* current turn (including its fix cycle) settles.
|
|
859
|
-
*/
|
|
860
|
-
input(raw) {
|
|
861
|
-
const value = raw.trim();
|
|
862
|
-
if (value.length === 0) return;
|
|
863
|
-
if (this.state.running) {
|
|
864
|
-
if (value === "/queue clear") {
|
|
865
|
-
this.notify({ queue: [] });
|
|
866
|
-
this.push("status", "queue cleared");
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const drop = /^\/queue drop (\d+)$/.exec(value);
|
|
870
|
-
if (drop) {
|
|
871
|
-
const index = Number.parseInt(drop[1], 10) - 1;
|
|
872
|
-
if (index >= 0 && index < this.state.queue.length) {
|
|
873
|
-
const removed = this.state.queue[index];
|
|
874
|
-
this.notify({ queue: this.state.queue.filter((_, i) => i !== index) });
|
|
875
|
-
this.push("status", `dropped from queue: ${removed}`);
|
|
876
|
-
} else {
|
|
877
|
-
this.push("status", `queue has ${this.state.queue.length} item(s) \u2014 /queue drop <1-${Math.max(this.state.queue.length, 1)}>`);
|
|
878
|
-
}
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
this.notify({ queue: [...this.state.queue, value] });
|
|
882
|
-
return;
|
|
883
|
-
}
|
|
884
|
-
if (value.startsWith("/")) {
|
|
885
|
-
this.command(value);
|
|
886
|
-
} else {
|
|
887
|
-
void this.submit(value);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
/** Dispatch queued input after the current work settles. */
|
|
891
|
-
drainQueue() {
|
|
892
|
-
if (this.state.running) return;
|
|
893
|
-
const [next, ...rest] = this.state.queue;
|
|
894
|
-
if (next === void 0) return;
|
|
895
|
-
this.notify({ queue: rest });
|
|
896
|
-
this.input(next);
|
|
897
|
-
}
|
|
898
|
-
// ---------- internals ----------
|
|
899
|
-
notify(patch) {
|
|
900
|
-
this.state = { ...this.state, ...patch };
|
|
901
|
-
for (const listener of this.listeners) listener();
|
|
902
|
-
}
|
|
903
|
-
push(role, text) {
|
|
904
|
-
this.nextId += 1;
|
|
905
|
-
this.notify({ items: [...this.state.items, { id: this.nextId, role, text }] });
|
|
906
|
-
}
|
|
907
|
-
setLive(text) {
|
|
908
|
-
this.live = text;
|
|
909
|
-
this.notify({ liveText: text });
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Static transcript items are immutable once rendered, so in-progress
|
|
913
|
-
* assistant text accumulates in a live buffer and commits as one block.
|
|
914
|
-
*/
|
|
915
|
-
commitLive() {
|
|
916
|
-
if (this.live.length > 0) {
|
|
917
|
-
const text = this.live;
|
|
918
|
-
this.live = "";
|
|
919
|
-
this.state = { ...this.state, liveText: "" };
|
|
920
|
-
this.push("assistant", text);
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
/** Where engines run and servers start: the sandbox worktree when on. */
|
|
924
|
-
execCwd() {
|
|
925
|
-
return this.state.sandbox ? sandboxDir(this.opts.cwd) : this.opts.cwd;
|
|
926
|
-
}
|
|
927
|
-
devServer() {
|
|
928
|
-
if (!this.dev) {
|
|
929
|
-
this.dev = new DevServer(this.execCwd(), {
|
|
930
|
-
onStateChange: (devState) => this.notify({ devState }),
|
|
931
|
-
onUrl: (url) => this.notify({ devUrl: url })
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
return this.dev;
|
|
935
|
-
}
|
|
936
|
-
/** Rebind the dev server after the execution tree changes. */
|
|
937
|
-
resetDevServer() {
|
|
938
|
-
const wasRunning = this.dev && this.dev.state !== "stopped";
|
|
939
|
-
this.dev?.stop();
|
|
940
|
-
this.dev = null;
|
|
941
|
-
this.notify({ devUrl: null, devState: "stopped" });
|
|
942
|
-
if (wasRunning) {
|
|
943
|
-
const command = detectDevCommand(this.execCwd());
|
|
944
|
-
if (command) {
|
|
945
|
-
this.devServer().start(command);
|
|
946
|
-
this.push("status", `dev server restarting in ${this.state.sandbox ? "the sandbox" : "the real tree"}`);
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
turnEdits = 0;
|
|
951
|
-
turnTools = 0;
|
|
952
|
-
toolStreak = 0;
|
|
953
|
-
collapsedTools = 0;
|
|
954
|
-
/**
|
|
955
|
-
* Long tool cascades collapse: the first three of a consecutive burst
|
|
956
|
-
* render, the rest fold into one "+N more" line pushed when the burst
|
|
957
|
-
* ends — append-only, so the Static transcript stays valid.
|
|
958
|
-
*/
|
|
959
|
-
flushToolCollapse() {
|
|
960
|
-
if (this.collapsedTools > 0) {
|
|
961
|
-
this.push("tool", `+${this.collapsedTools} more tool call${this.collapsedTools === 1 ? "" : "s"}`);
|
|
962
|
-
this.collapsedTools = 0;
|
|
963
|
-
}
|
|
964
|
-
this.toolStreak = 0;
|
|
965
|
-
}
|
|
966
|
-
handleEvent = (event) => {
|
|
967
|
-
switch (event.type) {
|
|
968
|
-
case "status":
|
|
969
|
-
this.commitLive();
|
|
970
|
-
this.flushToolCollapse();
|
|
971
|
-
this.push("status", event.text);
|
|
972
|
-
break;
|
|
973
|
-
case "delta":
|
|
974
|
-
this.setLive(this.live + event.text);
|
|
975
|
-
break;
|
|
976
|
-
case "text":
|
|
977
|
-
this.flushToolCollapse();
|
|
978
|
-
if (event.streamed) {
|
|
979
|
-
this.live = "";
|
|
980
|
-
this.state = { ...this.state, liveText: "" };
|
|
981
|
-
this.push("assistant", event.text);
|
|
982
|
-
} else {
|
|
983
|
-
this.setLive(this.live + (this.live.length > 0 ? "\n" : "") + event.text);
|
|
984
|
-
}
|
|
985
|
-
break;
|
|
986
|
-
case "thinking":
|
|
987
|
-
this.commitLive();
|
|
988
|
-
this.flushToolCollapse();
|
|
989
|
-
this.push("thinking", event.text);
|
|
990
|
-
break;
|
|
991
|
-
case "tool": {
|
|
992
|
-
this.commitLive();
|
|
993
|
-
this.turnTools += 1;
|
|
994
|
-
this.toolStreak += 1;
|
|
995
|
-
if (/edit|write|patch|apply/i.test(event.name)) this.turnEdits += 1;
|
|
996
|
-
if (this.toolStreak <= 3) {
|
|
997
|
-
this.push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
|
|
998
|
-
} else {
|
|
999
|
-
this.collapsedTools += 1;
|
|
1000
|
-
}
|
|
1001
|
-
break;
|
|
1002
|
-
}
|
|
1003
|
-
case "error":
|
|
1004
|
-
this.commitLive();
|
|
1005
|
-
this.flushToolCollapse();
|
|
1006
|
-
this.push("error", event.text);
|
|
1007
|
-
break;
|
|
1008
|
-
case "result":
|
|
1009
|
-
if (event.sessionId) this.sessionId = event.sessionId;
|
|
1010
|
-
break;
|
|
1011
|
-
case "raw":
|
|
1012
|
-
break;
|
|
1013
|
-
}
|
|
1014
|
-
};
|
|
1015
|
-
/** Run one engine turn. `display` is what the transcript shows as the ask. */
|
|
1016
|
-
async runTurn(prompt, display, modelOverride) {
|
|
1017
|
-
this.push("user", display);
|
|
1018
|
-
this.turnEdits = 0;
|
|
1019
|
-
this.turnTools = 0;
|
|
1020
|
-
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1021
|
-
const runStart = Date.now();
|
|
1022
|
-
const engine = getEngine(this.state.engineId);
|
|
1023
|
-
this.abort = new AbortController();
|
|
1024
|
-
const result = await runAgent(
|
|
1025
|
-
engine,
|
|
1026
|
-
{
|
|
1027
|
-
prompt,
|
|
1028
|
-
cwd: this.execCwd(),
|
|
1029
|
-
model: modelOverride ?? this.state.model,
|
|
1030
|
-
mode: this.state.mode,
|
|
1031
|
-
sessionId: engine.supportsResume ? this.sessionId : void 0
|
|
1032
|
-
},
|
|
1033
|
-
this.handleEvent,
|
|
1034
|
-
this.abort.signal
|
|
1035
|
-
);
|
|
1036
|
-
this.abort = null;
|
|
1037
|
-
this.commitLive();
|
|
1038
|
-
this.flushToolCollapse();
|
|
1039
|
-
if (result.ok) {
|
|
1040
|
-
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
1041
|
-
const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
|
|
1042
|
-
const checkpoint = this.checkpoints.at(-1);
|
|
1043
|
-
const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
|
|
1044
|
-
const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
|
|
1045
|
-
this.push("status", `done${secs}${cost}${work}`);
|
|
1046
|
-
try {
|
|
1047
|
-
const reqPath = path3.join(this.opts.cwd, ".squint", "approval-request.json");
|
|
1048
|
-
if (fs3.existsSync(reqPath)) {
|
|
1049
|
-
const req = JSON.parse(fs3.readFileSync(reqPath, "utf8"));
|
|
1050
|
-
fs3.rmSync(reqPath, { force: true });
|
|
1051
|
-
if (typeof req?.summary === "string" && req.summary.length > 0) {
|
|
1052
|
-
this.pendingApproval = req.summary;
|
|
1053
|
-
const shot = typeof req.screenshot === "string" ? path3.resolve(this.execCwd(), req.screenshot) : null;
|
|
1054
|
-
if (shot && fs3.existsSync(shot)) this.push("image", shot);
|
|
1055
|
-
this.push("status", `\u23F8 approval requested: ${req.summary}
|
|
1056
|
-
/yes approves \xB7 /no rejects \xB7 or type feedback`);
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
} catch {
|
|
1060
|
-
}
|
|
1061
|
-
if (checkpoint) {
|
|
1062
|
-
try {
|
|
1063
|
-
const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
|
|
1064
|
-
const index = loadTokenIndex(this.execCwd());
|
|
1065
|
-
const drift = scanDrift(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD", index);
|
|
1066
|
-
if (drift.length > 0) {
|
|
1067
|
-
this.push("status", `token drift: ${drift.length} hardcoded color(s)
|
|
1068
|
-
${driftSummary(drift)}`);
|
|
1069
|
-
}
|
|
1070
|
-
} catch {
|
|
1071
|
-
}
|
|
1072
|
-
try {
|
|
1073
|
-
const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
|
|
1074
|
-
const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
|
|
1075
|
-
const hard = findings.filter((f) => f.hard);
|
|
1076
|
-
if (hard.length > 0) {
|
|
1077
|
-
this.addProblem(
|
|
1078
|
-
"gates",
|
|
1079
|
-
`${hard.length} class(es)/idiom(s) from an older toolchain major
|
|
1080
|
-
${rulePackSummary(hard)}`,
|
|
1081
|
-
`This project's toolchain is newer than the patterns just written. Apply the exact renames below \u2014 do not downgrade dependencies or add compatibility configs:
|
|
1082
|
-
${rulePackSummary(hard)}`
|
|
1083
|
-
);
|
|
1084
|
-
}
|
|
1085
|
-
const soft = findings.filter((f) => !f.hard);
|
|
1086
|
-
if (soft.length > 0) {
|
|
1087
|
-
this.push("status", `renamed-scale traps (verify intent):
|
|
1088
|
-
${rulePackSummary(soft)}`);
|
|
1089
|
-
}
|
|
1090
|
-
} catch {
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
runHook(this.opts.cwd, "on-turn-end", {
|
|
1094
|
-
cost: String(result.costUsd ?? 0),
|
|
1095
|
-
duration_ms: String(result.durationMs ?? 0),
|
|
1096
|
-
stat: stat ?? ""
|
|
1097
|
-
});
|
|
1098
|
-
const before = this.state.totals.costUsd;
|
|
1099
|
-
this.notify({
|
|
1100
|
-
totals: {
|
|
1101
|
-
costUsd: before + (result.costUsd ?? 0),
|
|
1102
|
-
turns: this.state.totals.turns + 1
|
|
1103
|
-
}
|
|
1104
|
-
});
|
|
1105
|
-
const budget = this.opts.budgetUsd;
|
|
1106
|
-
if (budget && before < budget && this.state.totals.costUsd >= budget) {
|
|
1107
|
-
this.push(
|
|
1108
|
-
"error",
|
|
1109
|
-
`session cost $${this.state.totals.costUsd.toFixed(2)} crossed your $${budget.toFixed(2)} budget \u2014 squint keeps working, this is just the flag you asked for`
|
|
1110
|
-
);
|
|
1111
|
-
runHook(this.opts.cwd, "on-budget", { total: this.state.totals.costUsd.toFixed(2), budget: budget.toFixed(2) });
|
|
1112
|
-
}
|
|
1113
|
-
if (this.sessionId) {
|
|
1114
|
-
saveState(this.opts.cwd, {
|
|
1115
|
-
engine: this.state.engineId,
|
|
1116
|
-
sessionId: this.sessionId,
|
|
1117
|
-
model: this.state.model,
|
|
1118
|
-
lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
|
|
1119
|
-
totals: this.state.totals,
|
|
1120
|
-
at: Date.now()
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
if (result.ok && this.opts.autoCheck !== false) {
|
|
1125
|
-
const fastGates = detectFastGates(this.execCwd());
|
|
1126
|
-
if (fastGates.length > 0) {
|
|
1127
|
-
const gateResults = await runGates(this.execCwd(), fastGates);
|
|
1128
|
-
const failures = gateResults.filter((r) => !r.ok);
|
|
1129
|
-
if (failures.length > 0) {
|
|
1130
|
-
this.addProblem(
|
|
1131
|
-
"gates",
|
|
1132
|
-
failures.map((f) => f.gate.id).join(" + "),
|
|
1133
|
-
buildGatePrompt(failures)
|
|
1134
|
-
);
|
|
1135
|
-
this.push(
|
|
1136
|
-
"error",
|
|
1137
|
-
failures.map((f) => `\u2717 ${f.gate.id} \xB7 ${f.outputTail.split("\n").slice(-3).join("\n")}`).join("\n")
|
|
1138
|
-
);
|
|
1139
|
-
if (this.maybeAutoFix()) return;
|
|
1140
|
-
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1141
|
-
this.notify({ running: false });
|
|
1142
|
-
this.drainQueue();
|
|
1143
|
-
return;
|
|
1144
|
-
}
|
|
1145
|
-
this.clearProblems("gates");
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
const dev = this.dev;
|
|
1149
|
-
if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
|
|
1150
|
-
await delay(1500);
|
|
1151
|
-
const errors = dev.errorsSince(runStart);
|
|
1152
|
-
if (this.state.devUrl) {
|
|
1153
|
-
try {
|
|
1154
|
-
const { hasNextMcp, probeNextMcp } = await import("./nextMcp-42Y63M7W.js");
|
|
1155
|
-
if (hasNextMcp(this.execCwd())) {
|
|
1156
|
-
const mcp = await probeNextMcp(this.state.devUrl);
|
|
1157
|
-
if (mcp.available && mcp.errors.length > 0) {
|
|
1158
|
-
for (const err of mcp.errors) errors.push(`[next mcp] ${err}`);
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
} catch {
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
if (errors.length > 0) {
|
|
1165
|
-
this.addProblem(
|
|
1166
|
-
"dev",
|
|
1167
|
-
`${errors.length} dev server error line(s)`,
|
|
1168
|
-
buildFixPrompt(errors, dev.tail(30))
|
|
1169
|
-
);
|
|
1170
|
-
this.push("error", `dev server: ${errors.length} error line(s)
|
|
1171
|
-
${errors.slice(-5).join("\n")}`);
|
|
1172
|
-
if (this.maybeAutoFix()) return;
|
|
1173
|
-
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1174
|
-
} else {
|
|
1175
|
-
this.clearProblems("dev");
|
|
1176
|
-
if (this.opts.autoProbe !== false && this.state.devUrl) {
|
|
1177
|
-
const probe = await probeRuntime(this.state.devUrl, this.opts.cwd);
|
|
1178
|
-
const summary = probe ? runtimeSummary(probe.report) : null;
|
|
1179
|
-
if (probe && summary) {
|
|
1180
|
-
this.addProblem("runtime", summary, buildRuntimeFixPrompt(probe.report));
|
|
1181
|
-
this.push("error", `runtime: ${summary}`);
|
|
1182
|
-
if (this.maybeAutoFix()) return;
|
|
1183
|
-
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1184
|
-
} else if (probe) {
|
|
1185
|
-
this.clearProblems("runtime");
|
|
1186
|
-
this.perfPulse(probe.perf);
|
|
1187
|
-
const pct = await this.visualPulse(probe.pulsePath);
|
|
1188
|
-
if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
|
|
1189
|
-
this.autoReviewedThisAsk = true;
|
|
1190
|
-
this.push("status", `auto-review: the page changed ${pct.toFixed(0)}% \u2014 capturing for self-critique`);
|
|
1191
|
-
this.notify({ running: false });
|
|
1192
|
-
const captureResult = await this.capture();
|
|
1193
|
-
if (captureResult) {
|
|
1194
|
-
await this.runTurn(
|
|
1195
|
-
buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components),
|
|
1196
|
-
"\u{1F441} auto-review rendered UI"
|
|
1197
|
-
);
|
|
1198
|
-
}
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
if (!this.reviewTipShown && this.state.devUrl) {
|
|
1204
|
-
this.reviewTipShown = true;
|
|
1205
|
-
this.push("status", "tip: /review screenshots the app and has the engine critique its own work");
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
this.notify({ running: false });
|
|
1210
|
-
this.drainQueue();
|
|
1211
|
-
}
|
|
1212
|
-
async submit(ask) {
|
|
1213
|
-
this.fixAttempts = 0;
|
|
1214
|
-
this.autoReviewedThisAsk = false;
|
|
1215
|
-
const snapshot = this.state.sandbox ? null : takeSnapshot(this.opts.cwd);
|
|
1216
|
-
if (snapshot) {
|
|
1217
|
-
this.checkpoints.push({
|
|
1218
|
-
snapshot,
|
|
1219
|
-
label: ask.length > 60 ? `${ask.slice(0, 59)}\u2026` : ask,
|
|
1220
|
-
at: Date.now()
|
|
1221
|
-
});
|
|
1222
|
-
if (this.checkpoints.length > 20) this.checkpoints.shift();
|
|
1223
|
-
}
|
|
1224
|
-
const isFirstTurn = this.sessionId === void 0;
|
|
1225
|
-
let prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
|
|
1226
|
-
const enrichment = enrich(this.opts.cwd, ask);
|
|
1227
|
-
if (enrichment.matchedSkills.length > 0) {
|
|
1228
|
-
this.push("status", `skills: ${enrichment.matchedSkills.join(", ")}`);
|
|
1229
|
-
}
|
|
1230
|
-
prompt += enrichment.sections;
|
|
1231
|
-
await this.runTurn(prompt, ask);
|
|
1232
|
-
}
|
|
1233
|
-
/** Restore files to the state before checkpoint `index`; drop it and everything after. */
|
|
1234
|
-
restoreTo(index) {
|
|
1235
|
-
const checkpoint = this.checkpoints[index];
|
|
1236
|
-
if (!checkpoint) {
|
|
1237
|
-
this.push("status", "no such checkpoint \u2014 /checkpoints lists them");
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
|
|
1241
|
-
if (result.restored) {
|
|
1242
|
-
const dropped = this.checkpoints.length - index;
|
|
1243
|
-
appendDecision(this.opts.cwd, { decision: `rejected and rolled back: "${checkpoint.label}"`, source: "restore" });
|
|
1244
|
-
this.checkpoints = this.checkpoints.slice(0, index);
|
|
1245
|
-
this.push(
|
|
1246
|
-
"status",
|
|
1247
|
-
`restored files to before "${checkpoint.label}"${dropped > 1 ? ` \xB7 ${dropped} asks rolled back` : ""}${result.deletedFiles > 0 ? ` \xB7 removed ${result.deletedFiles} created file(s)` : ""} \u2014 the conversation continues from here`
|
|
1248
|
-
);
|
|
1249
|
-
} else {
|
|
1250
|
-
this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
/** Cross-turn load-performance deltas: the perf twin of the visual pulse. */
|
|
1254
|
-
perfPulse(perf) {
|
|
1255
|
-
if (!perf || perf.lcpMs === void 0 && perf.transferBytes === void 0) return;
|
|
1256
|
-
const previous = this.lastPerf;
|
|
1257
|
-
this.lastPerf = perf;
|
|
1258
|
-
const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
1259
|
-
const parts = [];
|
|
1260
|
-
if (perf.lcpMs !== void 0) {
|
|
1261
|
-
const delta = previous?.lcpMs !== void 0 && Math.abs(perf.lcpMs - previous.lcpMs) >= 100 ? ` (${perf.lcpMs > previous.lcpMs ? "+" : "\u2212"}${Math.abs(perf.lcpMs - previous.lcpMs)}ms)` : "";
|
|
1262
|
-
parts.push(`LCP ${perf.lcpMs}ms${delta}`);
|
|
1263
|
-
}
|
|
1264
|
-
if (perf.cls !== void 0 && perf.cls > 0.05) parts.push(`CLS ${perf.cls}`);
|
|
1265
|
-
if (perf.transferBytes !== void 0) {
|
|
1266
|
-
const delta = previous?.transferBytes !== void 0 && Math.abs(perf.transferBytes - previous.transferBytes) > 100 * 1024 ? ` (${perf.transferBytes > previous.transferBytes ? "+" : "\u2212"}${mb(Math.abs(perf.transferBytes - previous.transferBytes))})` : "";
|
|
1267
|
-
parts.push(`${mb(perf.transferBytes)}${delta}`);
|
|
1268
|
-
}
|
|
1269
|
-
if (perf.requests !== void 0) parts.push(`${perf.requests} req`);
|
|
1270
|
-
if (parts.length > 0) this.push("status", `perf: ${parts.join(" \xB7 ")}`);
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* Cross-turn visual drift check: compare this turn's pulse screenshot
|
|
1274
|
-
* with the previous one and report how much of the page changed.
|
|
1275
|
-
* Informational — changes are usually intended; surprises shouldn't be.
|
|
1276
|
-
*/
|
|
1277
|
-
async visualPulse(pulsePath) {
|
|
1278
|
-
if (!pulsePath) return null;
|
|
1279
|
-
let current;
|
|
1280
|
-
try {
|
|
1281
|
-
current = (await import("fs")).readFileSync(pulsePath);
|
|
1282
|
-
} catch {
|
|
1283
|
-
return null;
|
|
1284
|
-
}
|
|
1285
|
-
const previous = this.lastPulse;
|
|
1286
|
-
this.lastPulse = current;
|
|
1287
|
-
if (!previous) {
|
|
1288
|
-
this.push("status", "visual pulse: baseline captured");
|
|
1289
|
-
this.push("image", pulsePath);
|
|
1290
|
-
return null;
|
|
1291
|
-
}
|
|
1292
|
-
const pct = await comparePulse(previous, current);
|
|
1293
|
-
if (pct === null) return null;
|
|
1294
|
-
this.push(
|
|
1295
|
-
"status",
|
|
1296
|
-
pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
|
|
1297
|
-
);
|
|
1298
|
-
runHook(this.opts.cwd, "on-pulse-diff", { pct: pct.toFixed(1) });
|
|
1299
|
-
if (pct >= 0.5) this.push("image", pulsePath);
|
|
1300
|
-
return pct;
|
|
1301
|
-
}
|
|
1302
|
-
/** Screenshot the running app (and watch its runtime where CDP is available). */
|
|
1303
|
-
async capture(urlOverride) {
|
|
1304
|
-
const url = urlOverride || this.state.devUrl;
|
|
1305
|
-
if (!url) {
|
|
1306
|
-
this.push("error", "dev server not running \u2014 /dev first, or /shot <url>");
|
|
1307
|
-
return null;
|
|
1308
|
-
}
|
|
1309
|
-
this.push("status", "capturing screenshots\u2026");
|
|
1310
|
-
const result = await captureViewports(this.opts.cwd, url);
|
|
1311
|
-
if (!result) {
|
|
1312
|
-
this.push("error", "no Chrome/Chromium found for screenshots");
|
|
1313
|
-
return null;
|
|
1314
|
-
}
|
|
1315
|
-
for (const err of result.errors) this.push("error", `screenshot ${err}`);
|
|
1316
|
-
if (result.shots.length > 0) {
|
|
1317
|
-
this.push(
|
|
1318
|
-
"status",
|
|
1319
|
-
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path3.dirname(result.shots[0].path)}`
|
|
1320
|
-
);
|
|
1321
|
-
const desktop = result.shots.find((s) => s.name === "desktop") ?? result.shots[0];
|
|
1322
|
-
this.push("image", desktop.path);
|
|
1323
|
-
}
|
|
1324
|
-
if (result.runtime) {
|
|
1325
|
-
const summary = runtimeSummary(result.runtime);
|
|
1326
|
-
if (summary) {
|
|
1327
|
-
this.push("error", `runtime: ${summary}`);
|
|
1328
|
-
this.addProblem("runtime", summary, buildRuntimeFixPrompt(result.runtime));
|
|
1329
|
-
this.push("status", "/fix sends open problems to the engine");
|
|
1330
|
-
} else {
|
|
1331
|
-
this.clearProblems("runtime");
|
|
1332
|
-
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
const vtHard = (result.viewTransitions ?? []).filter((f) => f.startsWith("duplicate"));
|
|
1336
|
-
if (vtHard.length > 0) {
|
|
1337
|
-
this.push("error", `view transitions: ${vtHard.length} broken
|
|
1338
|
-
${vtHard.join("\n")}`);
|
|
1339
|
-
this.addProblem(
|
|
1340
|
-
"runtime",
|
|
1341
|
-
`${vtHard.length} view transition(s) the browser will skip`,
|
|
1342
|
-
`Duplicate view-transition-name values make the browser abort the entire transition at runtime:
|
|
1343
|
-
|
|
1344
|
-
${vtHard.join("\n")}
|
|
1345
|
-
|
|
1346
|
-
Give each simultaneously rendered element a unique name (or scope names per item, e.g. per list key). Do not remove the transitions.`
|
|
1347
|
-
);
|
|
1348
|
-
}
|
|
1349
|
-
const vtSoft = (result.viewTransitions ?? []).filter((f) => !f.startsWith("duplicate"));
|
|
1350
|
-
if (vtSoft.length > 0) {
|
|
1351
|
-
this.push("status", `view transitions: ${vtSoft.join("; ")}`);
|
|
1352
|
-
}
|
|
1353
|
-
if (result.phantoms && result.phantoms.length > 0) {
|
|
1354
|
-
this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
|
|
1355
|
-
${result.phantoms.slice(0, 5).join("\n")}`);
|
|
1356
|
-
this.addProblem(
|
|
1357
|
-
"runtime",
|
|
1358
|
-
`${result.phantoms.length} phantom class(es) \u2014 elements silently unstyled`,
|
|
1359
|
-
`These class tokens appear in the DOM but match no CSS rule, so the elements are silently unstyled. Usually a misspelled or version-mismatched utility (e.g. Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled:
|
|
1360
|
-
|
|
1361
|
-
${result.phantoms.join("\n")}
|
|
1362
|
-
|
|
1363
|
-
Fix the class names or define the styles, then confirm visually.`
|
|
1364
|
-
);
|
|
1365
|
-
this.push("status", "/fix sends open problems to the engine");
|
|
1366
|
-
}
|
|
1367
|
-
if (result.slop && result.slop.length > 0) {
|
|
1368
|
-
this.push("status", `distinctiveness: ${result.slop.length} tell(s)
|
|
1369
|
-
${result.slop.join("\n")}`);
|
|
1370
|
-
}
|
|
1371
|
-
if (result.a11y && result.a11y.length > 0) {
|
|
1372
|
-
this.push("error", `a11y: ${result.a11y.length} finding(s)
|
|
1373
|
-
${result.a11y.slice(0, 5).join("\n")}`);
|
|
1374
|
-
this.addProblem(
|
|
1375
|
-
"a11y",
|
|
1376
|
-
`${result.a11y.length} accessibility finding(s)`,
|
|
1377
|
-
`Fix these accessibility defects found by an automated sweep of the running app:
|
|
1378
|
-
|
|
1379
|
-
${result.a11y.join("\n")}
|
|
1380
|
-
|
|
1381
|
-
They are objective defects, not style preferences.`
|
|
1382
|
-
);
|
|
1383
|
-
this.push("status", "/review folds these into the fix pass \xB7 /fix sends them directly");
|
|
1384
|
-
} else if (result.runtime) {
|
|
1385
|
-
this.clearProblems("a11y");
|
|
1386
|
-
}
|
|
1387
|
-
return result.shots.length > 0 ? result : null;
|
|
1388
|
-
}
|
|
1389
|
-
command(commandLine) {
|
|
1390
|
-
const [name, ...rest] = commandLine.slice(1).split(/\s+/);
|
|
1391
|
-
const arg = rest.join(" ").trim();
|
|
1392
|
-
switch (name) {
|
|
1393
|
-
case "engines": {
|
|
1394
|
-
void import("./registry-MIJ6LSAY.js").then(({ detectEngines: detectEngines2 }) => {
|
|
1395
|
-
const lines = detectEngines2().map(({ engine, path: binaryPath }) => {
|
|
1396
|
-
const mark = binaryPath ? "\u2713" : "\u2717";
|
|
1397
|
-
const traits = [engine.createParser ? "stream" : "text", engine.supportsResume ? "resume" : null].filter(Boolean).join(" \xB7 ");
|
|
1398
|
-
return `${mark} ${engine.id} \u2014 ${traits}${binaryPath ? "" : ` \xB7 install: ${engine.install}`}`;
|
|
1399
|
-
});
|
|
1400
|
-
this.push("status", `${lines.join("\n")}
|
|
1401
|
-
/engine <id> switches (new session)`);
|
|
1402
|
-
});
|
|
1403
|
-
break;
|
|
1404
|
-
}
|
|
1405
|
-
case "engine":
|
|
1406
|
-
if (!arg) {
|
|
1407
|
-
this.command("/engines");
|
|
1408
|
-
} else {
|
|
1409
|
-
try {
|
|
1410
|
-
getEngine(arg);
|
|
1411
|
-
this.sessionId = void 0;
|
|
1412
|
-
this.notify({ engineId: arg });
|
|
1413
|
-
this.push("status", `engine \u2192 ${arg} (new session)`);
|
|
1414
|
-
} catch (err) {
|
|
1415
|
-
this.push("error", err instanceof Error ? err.message : String(err));
|
|
1416
|
-
}
|
|
1417
|
-
}
|
|
1418
|
-
break;
|
|
1419
|
-
case "model":
|
|
1420
|
-
this.notify({ model: arg || void 0 });
|
|
1421
|
-
this.push("status", arg ? `model \u2192 ${arg}` : "model \u2192 engine default");
|
|
1422
|
-
break;
|
|
1423
|
-
case "mode":
|
|
1424
|
-
if (arg === "plan" || arg === "safe" || arg === "yolo") {
|
|
1425
|
-
this.setMode(arg);
|
|
1426
|
-
} else {
|
|
1427
|
-
this.push("status", "usage: /mode plan|safe|yolo \u2014 or shift+tab to cycle");
|
|
1428
|
-
}
|
|
1429
|
-
break;
|
|
1430
|
-
case "dev": {
|
|
1431
|
-
const dev = this.devServer();
|
|
1432
|
-
if (arg === "logs") {
|
|
1433
|
-
const tail = dev.tail(15);
|
|
1434
|
-
this.push("status", tail.length > 0 ? tail.join("\n") : "no dev server output captured yet");
|
|
1435
|
-
break;
|
|
1436
|
-
}
|
|
1437
|
-
if (arg === "restart") {
|
|
1438
|
-
if (dev.state !== "stopped") dev.stop();
|
|
1439
|
-
this.notify({ devUrl: null });
|
|
1440
|
-
const devCommand = detectDevCommand(this.opts.cwd);
|
|
1441
|
-
if (!devCommand) {
|
|
1442
|
-
this.push("error", "no dev/start script found in package.json");
|
|
1443
|
-
} else {
|
|
1444
|
-
dev.start(devCommand);
|
|
1445
|
-
this.push("status", `dev server restarting \xB7 ${devCommand.display}`);
|
|
1446
|
-
}
|
|
1447
|
-
break;
|
|
1448
|
-
}
|
|
1449
|
-
if (dev.state === "stopped" || dev.state === "crashed") {
|
|
1450
|
-
const devCommand = detectDevCommand(this.opts.cwd);
|
|
1451
|
-
if (!devCommand) {
|
|
1452
|
-
this.push("error", "no dev/start script found in package.json");
|
|
1453
|
-
} else {
|
|
1454
|
-
dev.start(devCommand);
|
|
1455
|
-
this.push("status", `dev server starting \xB7 ${devCommand.display}`);
|
|
1456
|
-
}
|
|
1457
|
-
} else {
|
|
1458
|
-
dev.stop();
|
|
1459
|
-
this.notify({ devUrl: null });
|
|
1460
|
-
this.push("status", "dev server stopped");
|
|
1461
|
-
}
|
|
1462
|
-
break;
|
|
1463
|
-
}
|
|
1464
|
-
case "fix": {
|
|
1465
|
-
if (this.state.problems.length === 0) {
|
|
1466
|
-
this.push("status", "nothing to fix \u2014 no open problems");
|
|
1467
|
-
break;
|
|
1468
|
-
}
|
|
1469
|
-
const index = Number.parseInt(arg, 10);
|
|
1470
|
-
if (arg && Number.isInteger(index)) {
|
|
1471
|
-
const target = this.state.problems[index - 1];
|
|
1472
|
-
if (!target) {
|
|
1473
|
-
this.push("status", `usage: /fix [1\u2013${this.state.problems.length}] \u2014 see /problems`);
|
|
1474
|
-
break;
|
|
1475
|
-
}
|
|
1476
|
-
this.dispatchFix([target]);
|
|
1477
|
-
} else {
|
|
1478
|
-
this.dispatchFix([...this.state.problems]);
|
|
1479
|
-
}
|
|
1480
|
-
break;
|
|
1481
|
-
}
|
|
1482
|
-
case "context": {
|
|
1483
|
-
import("./contextDoctor-U3YTDFVG.js").then(({ contextReport, formatContextReport }) => {
|
|
1484
|
-
this.push("status", formatContextReport(contextReport(this.execCwd())));
|
|
1485
|
-
}).catch((error) => {
|
|
1486
|
-
this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1487
|
-
});
|
|
1488
|
-
break;
|
|
1489
|
-
}
|
|
1490
|
-
case "yes":
|
|
1491
|
-
case "no": {
|
|
1492
|
-
if (!this.pendingApproval) {
|
|
1493
|
-
this.push("status", `nothing awaiting approval \u2014 /${name} answers an engine's approval request`);
|
|
1494
|
-
break;
|
|
1495
|
-
}
|
|
1496
|
-
const summary = this.pendingApproval;
|
|
1497
|
-
this.pendingApproval = null;
|
|
1498
|
-
const approved = name === "yes";
|
|
1499
|
-
appendDecision(this.opts.cwd, {
|
|
1500
|
-
decision: `${approved ? "approved" : "rejected"}: ${summary}${arg ? ` \u2014 ${arg}` : ""}`,
|
|
1501
|
-
source: "approval"
|
|
1502
|
-
});
|
|
1503
|
-
void this.runTurn(
|
|
1504
|
-
approved ? `Approved: "${summary}". Proceed.${arg ? ` Note from the user: ${arg}` : ""}` : `Rejected: "${summary}". Do not proceed with it.${arg ? ` The user says: ${arg}` : " Stop and await direction."}`,
|
|
1505
|
-
approved ? `\u2713 approved${arg ? ` \u2014 ${arg}` : ""}` : `\u2717 rejected${arg ? ` \u2014 ${arg}` : ""}`
|
|
1506
|
-
);
|
|
1507
|
-
break;
|
|
1508
|
-
}
|
|
1509
|
-
case "decide": {
|
|
1510
|
-
if (!arg) {
|
|
1511
|
-
this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
|
|
1512
|
-
break;
|
|
1513
|
-
}
|
|
1514
|
-
appendDecision(this.opts.cwd, {
|
|
1515
|
-
decision: arg,
|
|
1516
|
-
source: "decide",
|
|
1517
|
-
screenshot: this.lastPulse ? ".squint/preview/pulse.png" : void 0
|
|
1518
|
-
});
|
|
1519
|
-
this.push("status", `decision recorded: ${arg}`);
|
|
1520
|
-
break;
|
|
1521
|
-
}
|
|
1522
|
-
case "find": {
|
|
1523
|
-
if (!arg) {
|
|
1524
|
-
this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
|
|
1525
|
-
break;
|
|
1526
|
-
}
|
|
1527
|
-
const needle = arg.toLowerCase();
|
|
1528
|
-
const matches = [];
|
|
1529
|
-
for (const item of this.state.items) {
|
|
1530
|
-
if ((item.role === "user" || item.role === "assistant") && item.text.toLowerCase().includes(needle)) {
|
|
1531
|
-
const line = item.text.split("\n").find((l) => l.toLowerCase().includes(needle)) ?? item.text;
|
|
1532
|
-
matches.push(`[live] ${item.role === "user" ? "\u276F " : ""}${line.trim().slice(0, 90)}`);
|
|
1533
|
-
if (matches.length >= 8) break;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
if (matches.length < 8) {
|
|
1537
|
-
void (async () => {
|
|
1538
|
-
try {
|
|
1539
|
-
const fs4 = await import("fs");
|
|
1540
|
-
const path5 = await import("path");
|
|
1541
|
-
const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
|
|
1542
|
-
const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
|
|
1543
|
-
for (const file of files) {
|
|
1544
|
-
if (matches.length >= 8) break;
|
|
1545
|
-
const text = fs4.readFileSync(path5.join(dir, file), "utf8");
|
|
1546
|
-
for (const line of text.split("\n")) {
|
|
1547
|
-
if (line.toLowerCase().includes(needle)) {
|
|
1548
|
-
matches.push(`[${file.replace(/\.md$/, "")}] ${line.trim().slice(0, 90)}`);
|
|
1549
|
-
break;
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
} catch {
|
|
1554
|
-
}
|
|
1555
|
-
this.push(
|
|
1556
|
-
"status",
|
|
1557
|
-
matches.length > 0 ? `${matches.join("\n")}
|
|
1558
|
-
/checkpoints can rewind \xB7 /save archives this session` : `no matches for "${arg}"`
|
|
1559
|
-
);
|
|
1560
|
-
})();
|
|
1561
|
-
} else {
|
|
1562
|
-
this.push("status", matches.join("\n"));
|
|
1563
|
-
}
|
|
1564
|
-
break;
|
|
1565
|
-
}
|
|
1566
|
-
case "save": {
|
|
1567
|
-
void (async () => {
|
|
1568
|
-
const fs4 = await import("fs");
|
|
1569
|
-
const path5 = await import("path");
|
|
1570
|
-
const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
|
|
1571
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
1572
|
-
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1573
|
-
const file = path5.join(dir, `${stamp}.md`);
|
|
1574
|
-
const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
|
|
1575
|
-
for (const item of this.state.items) {
|
|
1576
|
-
switch (item.role) {
|
|
1577
|
-
case "user":
|
|
1578
|
-
lines.push(`## \u276F ${item.text}`, "");
|
|
1579
|
-
break;
|
|
1580
|
-
case "assistant":
|
|
1581
|
-
lines.push(item.text, "");
|
|
1582
|
-
break;
|
|
1583
|
-
case "tool":
|
|
1584
|
-
lines.push(`- \u2699 ${item.text}`);
|
|
1585
|
-
break;
|
|
1586
|
-
case "image":
|
|
1587
|
-
lines.push(``, "");
|
|
1588
|
-
break;
|
|
1589
|
-
case "thinking":
|
|
1590
|
-
break;
|
|
1591
|
-
default:
|
|
1592
|
-
lines.push(`> ${item.role === "error" ? "\u2717 " : ""}${item.text.split("\n").join("\n> ")}`);
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
lines.push("", `> ${this.summary()}`);
|
|
1596
|
-
fs4.writeFileSync(file, lines.join("\n") + "\n");
|
|
1597
|
-
this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
|
|
1598
|
-
})();
|
|
1599
|
-
break;
|
|
1600
|
-
}
|
|
1601
|
-
case "flows": {
|
|
1602
|
-
if (!this.state.devUrl) {
|
|
1603
|
-
this.push("error", "dev server not running \u2014 /dev first");
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
1606
|
-
void (async () => {
|
|
1607
|
-
const { loadFlows } = await import("./flows-6LK7NGWS.js");
|
|
1608
|
-
const flows = loadFlows(this.opts.cwd);
|
|
1609
|
-
if (flows.length === 0) {
|
|
1610
|
-
this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
|
|
1611
|
-
return;
|
|
1612
|
-
}
|
|
1613
|
-
const chrome = findChrome();
|
|
1614
|
-
if (!chrome) {
|
|
1615
|
-
this.push("error", "no Chrome/Chromium found for flows");
|
|
1616
|
-
return;
|
|
1617
|
-
}
|
|
1618
|
-
const { runFlow } = await import("./cdp-Q4H6ZHPT.js");
|
|
1619
|
-
const { previewDir } = await import("./preview-ZUCVWGZG.js");
|
|
1620
|
-
this.push("status", `replaying ${flows.length} flow(s)\u2026`);
|
|
1621
|
-
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1622
|
-
const failures = [];
|
|
1623
|
-
for (const flow of flows) {
|
|
1624
|
-
const wanted = arg.trim();
|
|
1625
|
-
if (wanted && flow.name !== wanted) continue;
|
|
1626
|
-
const result = await runFlow(chrome, this.state.devUrl, flow, previewDir(this.opts.cwd));
|
|
1627
|
-
if (result.ok) {
|
|
1628
|
-
this.push("status", `\u2713 flow ${flow.name} \xB7 ${flow.steps.length} steps${result.shots.length > 0 ? ` \xB7 ${result.shots.length} shot(s)` : ""}`);
|
|
1629
|
-
for (const shot of result.shots) this.push("image", shot);
|
|
1630
|
-
} else {
|
|
1631
|
-
const where = result.failedStep ? ` at step ${result.failedStep}` : "";
|
|
1632
|
-
this.push("error", `\u2717 flow ${flow.name}${where}: ${result.detail}`);
|
|
1633
|
-
failures.push(`Flow "${flow.name}" fails${where}: ${result.detail}. The flow file is .squint/flows/${flow.name}.flow \u2014 fix the app (or the flow if the UI legitimately changed).`);
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
this.notify({ running: false });
|
|
1637
|
-
if (failures.length > 0) {
|
|
1638
|
-
this.addProblem("flow", `${failures.length} flow(s) failing`, failures.join("\n\n"));
|
|
1639
|
-
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1640
|
-
} else {
|
|
1641
|
-
this.clearProblems("flow");
|
|
1642
|
-
}
|
|
1643
|
-
this.drainQueue();
|
|
1644
|
-
})();
|
|
1645
|
-
break;
|
|
1646
|
-
}
|
|
1647
|
-
case "score": {
|
|
1648
|
-
if (!this.state.devUrl) {
|
|
1649
|
-
this.push("error", "dev server not running \u2014 /dev first");
|
|
1650
|
-
break;
|
|
1651
|
-
}
|
|
1652
|
-
void (async () => {
|
|
1653
|
-
const result = await this.capture();
|
|
1654
|
-
if (!result) return;
|
|
1655
|
-
const a11yCount = result.a11y?.length ?? 0;
|
|
1656
|
-
const slopCount = result.slop?.length ?? 0;
|
|
1657
|
-
const runtimeBad = result.runtime ? runtimeSummary(result.runtime) ? 1 : 0 : 0;
|
|
1658
|
-
const problems = this.state.problems.length;
|
|
1659
|
-
const score = Math.max(
|
|
1660
|
-
0,
|
|
1661
|
-
5 - problems * 0.75 - Math.min(a11yCount, 4) * 0.25 - Math.min(slopCount, 4) * 0.25 - runtimeBad
|
|
1662
|
-
);
|
|
1663
|
-
const lcp = this.lastPerf?.lcpMs !== void 0 ? ` \xB7 LCP ${this.lastPerf.lcpMs}ms` : "";
|
|
1664
|
-
this.push(
|
|
1665
|
-
"status",
|
|
1666
|
-
[
|
|
1667
|
-
`score: ${score.toFixed(2)}/5 (deterministic axes)`,
|
|
1668
|
-
` open problems: ${problems}${problems > 0 ? ` (${this.state.problems.map((p) => p.source).join(", ")})` : ""}`,
|
|
1669
|
-
` a11y findings: ${a11yCount} \xB7 distinctiveness tells: ${slopCount} \xB7 runtime ${runtimeBad ? "dirty" : "clean"}${lcp}`,
|
|
1670
|
-
" /review judges what numbers cannot \u2014 hierarchy, taste, coherence"
|
|
1671
|
-
].join("\n")
|
|
1672
|
-
);
|
|
1673
|
-
})();
|
|
1674
|
-
break;
|
|
1675
|
-
}
|
|
1676
|
-
case "polish": {
|
|
1677
|
-
const rounds = arg ? Number.parseInt(arg, 10) : 2;
|
|
1678
|
-
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
|
|
1679
|
-
this.push("status", "usage: /polish [1-5] \u2014 rounds of screenshot \u2192 critique \u2192 fix (each costs a turn)");
|
|
1680
|
-
break;
|
|
1681
|
-
}
|
|
1682
|
-
if (!this.state.devUrl) {
|
|
1683
|
-
this.push("error", "dev server not running \u2014 /dev first");
|
|
1684
|
-
break;
|
|
1685
|
-
}
|
|
1686
|
-
this.push("status", `polishing: ${rounds} round${rounds === 1 ? "" : "s"} of review \u2192 fix`);
|
|
1687
|
-
void this.polish(rounds);
|
|
1688
|
-
break;
|
|
1689
|
-
}
|
|
1690
|
-
case "btw":
|
|
1691
|
-
if (!arg) {
|
|
1692
|
-
this.push("status", "usage: /btw <question> \u2014 read-only side question, main thread untouched");
|
|
1693
|
-
} else {
|
|
1694
|
-
void this.btw(arg);
|
|
1695
|
-
}
|
|
1696
|
-
break;
|
|
1697
|
-
case "copy": {
|
|
1698
|
-
const last = this.state.items.findLast((i) => i.role === "assistant");
|
|
1699
|
-
if (!last) {
|
|
1700
|
-
this.push("status", "nothing to copy yet");
|
|
1701
|
-
break;
|
|
1702
|
-
}
|
|
1703
|
-
void import("child_process").then(({ spawn: spawn2 }) => {
|
|
1704
|
-
const cmd = process.platform === "darwin" ? spawn2("pbcopy") : process.platform === "win32" ? spawn2("clip") : spawn2("xclip", ["-selection", "clipboard"]);
|
|
1705
|
-
cmd.on("error", () => this.push("error", "no clipboard tool found"));
|
|
1706
|
-
cmd.on("close", (code) => {
|
|
1707
|
-
if (code === 0) this.push("status", `copied last reply (${last.text.length} chars)`);
|
|
1708
|
-
});
|
|
1709
|
-
cmd.stdin?.end(last.text);
|
|
1710
|
-
});
|
|
1711
|
-
break;
|
|
1712
|
-
}
|
|
1713
|
-
case "problems":
|
|
1714
|
-
if (this.state.problems.length === 0) {
|
|
1715
|
-
this.push("status", "no open problems");
|
|
1716
|
-
} else {
|
|
1717
|
-
const lines = this.state.problems.map((p, i) => `${i + 1}. [${p.source}] ${p.summary}`);
|
|
1718
|
-
this.push("status", `${lines.join("\n")}
|
|
1719
|
-
/fix sends all \xB7 /fix <n> targets one`);
|
|
1720
|
-
}
|
|
1721
|
-
break;
|
|
1722
|
-
case "check":
|
|
1723
|
-
void (async () => {
|
|
1724
|
-
const gates = detectGates(this.opts.cwd);
|
|
1725
|
-
if (gates.length === 0) {
|
|
1726
|
-
this.push("status", "no gates detected in this project");
|
|
1727
|
-
return;
|
|
1728
|
-
}
|
|
1729
|
-
this.push("status", `running gates: ${gates.map((g) => g.id).join(" \u2192 ")}`);
|
|
1730
|
-
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1731
|
-
const results = await runGates(this.opts.cwd, gates, (result) => {
|
|
1732
|
-
this.push(
|
|
1733
|
-
result.ok ? "status" : "error",
|
|
1734
|
-
`${result.ok ? "\u2713" : "\u2717"} ${result.gate.id} \xB7 ${(result.durationMs / 1e3).toFixed(1)}s`
|
|
1735
|
-
);
|
|
1736
|
-
});
|
|
1737
|
-
this.notify({ running: false });
|
|
1738
|
-
this.drainQueue();
|
|
1739
|
-
const failures = results.filter((r) => !r.ok);
|
|
1740
|
-
if (failures.length > 0) {
|
|
1741
|
-
this.addProblem("gates", failures.map((f) => f.gate.id).join(" + "), buildGatePrompt(failures));
|
|
1742
|
-
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1743
|
-
} else {
|
|
1744
|
-
this.clearProblems("gates");
|
|
1745
|
-
this.push("status", "all gates passed");
|
|
1746
|
-
}
|
|
1747
|
-
})();
|
|
1748
|
-
break;
|
|
1749
|
-
case "shot":
|
|
1750
|
-
void this.capture(arg || void 0);
|
|
1751
|
-
break;
|
|
1752
|
-
case "review":
|
|
1753
|
-
void (async () => {
|
|
1754
|
-
const result = await this.capture();
|
|
1755
|
-
if (result) {
|
|
1756
|
-
await this.runTurn(
|
|
1757
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components),
|
|
1758
|
-
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
1759
|
-
);
|
|
1760
|
-
}
|
|
1761
|
-
})();
|
|
1762
|
-
break;
|
|
1763
|
-
case "undo":
|
|
1764
|
-
if (this.checkpoints.length === 0) {
|
|
1765
|
-
this.push("status", "nothing to undo \u2014 no ask this session, or not a git repo with commits");
|
|
1766
|
-
} else {
|
|
1767
|
-
this.restoreTo(this.checkpoints.length - 1);
|
|
1768
|
-
}
|
|
1769
|
-
break;
|
|
1770
|
-
case "checkpoints":
|
|
1771
|
-
if (this.checkpoints.length === 0) {
|
|
1772
|
-
this.push("status", "no checkpoints yet \u2014 one is taken before every ask in a git repo");
|
|
1773
|
-
} else {
|
|
1774
|
-
const lines = this.checkpoints.map((c, i) => {
|
|
1775
|
-
const mins = Math.max(0, Math.round((Date.now() - c.at) / 6e4));
|
|
1776
|
-
return `${i + 1}. ${c.label} \xB7 ${mins}m ago`;
|
|
1777
|
-
});
|
|
1778
|
-
this.push("status", `${lines.join("\n")}
|
|
1779
|
-
/restore <n> rewinds files to before that ask \xB7 /undo pops the last`);
|
|
1780
|
-
}
|
|
1781
|
-
break;
|
|
1782
|
-
case "restore": {
|
|
1783
|
-
const index = Number.parseInt(arg, 10);
|
|
1784
|
-
if (!Number.isInteger(index) || index < 1 || index > this.checkpoints.length) {
|
|
1785
|
-
this.push("status", `usage: /restore <1\u2013${Math.max(this.checkpoints.length, 1)}> \u2014 see /checkpoints`);
|
|
1786
|
-
} else {
|
|
1787
|
-
this.restoreTo(index - 1);
|
|
1788
|
-
}
|
|
1789
|
-
break;
|
|
1790
|
-
}
|
|
1791
|
-
case "resume": {
|
|
1792
|
-
const saved = loadState(this.opts.cwd);
|
|
1793
|
-
if (!saved) {
|
|
1794
|
-
this.push("status", "no previous session for this project");
|
|
1795
|
-
break;
|
|
1796
|
-
}
|
|
1797
|
-
try {
|
|
1798
|
-
if (!getEngine(saved.engine).supportsResume) {
|
|
1799
|
-
this.push("status", `previous engine ${saved.engine} cannot resume sessions`);
|
|
1800
|
-
break;
|
|
1801
|
-
}
|
|
1802
|
-
} catch {
|
|
1803
|
-
this.push("error", `previous engine ${saved.engine} is no longer available`);
|
|
1804
|
-
break;
|
|
1805
|
-
}
|
|
1806
|
-
this.sessionId = saved.sessionId;
|
|
1807
|
-
this.notify({
|
|
1808
|
-
engineId: saved.engine,
|
|
1809
|
-
model: saved.model ?? this.state.model,
|
|
1810
|
-
totals: saved.totals ?? this.state.totals
|
|
1811
|
-
});
|
|
1812
|
-
this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
|
|
1813
|
-
break;
|
|
1814
|
-
}
|
|
1815
|
-
case "sandbox": {
|
|
1816
|
-
if (this.state.running) {
|
|
1817
|
-
this.push("status", "wait for the current turn before changing sandbox state");
|
|
1818
|
-
break;
|
|
1819
|
-
}
|
|
1820
|
-
if (arg === "diff") {
|
|
1821
|
-
const stat = sandboxDiffStat(this.opts.cwd);
|
|
1822
|
-
if (!stat) {
|
|
1823
|
-
this.push("status", sandboxExists(this.opts.cwd) ? "sandbox is clean" : "no sandbox open \u2014 /sandbox on");
|
|
1824
|
-
} else {
|
|
1825
|
-
this.push("status", `${stat}
|
|
1826
|
-
${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
1827
|
-
}
|
|
1828
|
-
break;
|
|
1829
|
-
}
|
|
1830
|
-
if (arg === "apply") {
|
|
1831
|
-
const applied = applySandbox(this.opts.cwd);
|
|
1832
|
-
if (applied.ok) {
|
|
1833
|
-
discardSandbox(this.opts.cwd);
|
|
1834
|
-
this.notify({ sandbox: false });
|
|
1835
|
-
this.resetDevServer();
|
|
1836
|
-
appendDecision(this.opts.cwd, { decision: "accepted the sandboxed session onto the real tree", source: "sandbox" });
|
|
1837
|
-
this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
|
|
1838
|
-
} else {
|
|
1839
|
-
this.push("error", applied.detail ?? "apply failed");
|
|
1840
|
-
}
|
|
1841
|
-
break;
|
|
1842
|
-
}
|
|
1843
|
-
if (arg === "discard" || arg === "off") {
|
|
1844
|
-
const had = discardSandbox(this.opts.cwd);
|
|
1845
|
-
this.notify({ sandbox: false });
|
|
1846
|
-
this.resetDevServer();
|
|
1847
|
-
this.push("status", had ? "sandbox discarded \u2014 the real tree was never touched" : "no sandbox open");
|
|
1848
|
-
break;
|
|
1849
|
-
}
|
|
1850
|
-
if (arg === "on" || arg === "") {
|
|
1851
|
-
if (!isGitRepo(this.opts.cwd)) {
|
|
1852
|
-
this.push("error", "sandbox needs a git repo with at least one commit");
|
|
1853
|
-
break;
|
|
1854
|
-
}
|
|
1855
|
-
const { reused } = openSandbox(this.opts.cwd);
|
|
1856
|
-
this.notify({ sandbox: true });
|
|
1857
|
-
this.resetDevServer();
|
|
1858
|
-
this.push(
|
|
1859
|
-
"status",
|
|
1860
|
-
`${reused ? "rejoined the open sandbox" : "sandbox opened"} \u2014 asks now accumulate in a shadow worktree; /sandbox diff \xB7 apply \xB7 discard`
|
|
1861
|
-
);
|
|
1862
|
-
break;
|
|
1863
|
-
}
|
|
1864
|
-
this.push("status", "usage: /sandbox [on] \xB7 diff \xB7 apply \xB7 discard");
|
|
1865
|
-
break;
|
|
1866
|
-
}
|
|
1867
|
-
case "variants": {
|
|
1868
|
-
const [sub, ...subRest] = arg.split(/\s+/);
|
|
1869
|
-
if (sub === "apply") {
|
|
1870
|
-
const id = subRest[0];
|
|
1871
|
-
if (!id) {
|
|
1872
|
-
this.push("status", "usage: /variants apply <id>");
|
|
1873
|
-
break;
|
|
1874
|
-
}
|
|
1875
|
-
const applied = applyVariant(this.opts.cwd, id);
|
|
1876
|
-
if (applied.ok) {
|
|
1877
|
-
cleanVariants(this.opts.cwd);
|
|
1878
|
-
appendDecision(this.opts.cwd, { decision: `chose the ${id} direction over the other variants`, source: "variant" });
|
|
1879
|
-
this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
|
|
1880
|
-
} else {
|
|
1881
|
-
this.push("error", applied.detail ?? "apply failed");
|
|
1882
|
-
}
|
|
1883
|
-
break;
|
|
1884
|
-
}
|
|
1885
|
-
if (sub === "clean") {
|
|
1886
|
-
this.push("status", `removed ${cleanVariants(this.opts.cwd)} variant(s)`);
|
|
1887
|
-
break;
|
|
1888
|
-
}
|
|
1889
|
-
if (sub === "list") {
|
|
1890
|
-
const ids = listVariants(this.opts.cwd);
|
|
1891
|
-
this.push("status", ids.length > 0 ? ids.join(" \xB7 ") : "no variants \u2014 /variants <2-4> <ask>");
|
|
1892
|
-
break;
|
|
1893
|
-
}
|
|
1894
|
-
const n = Number.parseInt(sub ?? "", 10);
|
|
1895
|
-
const ask = subRest.join(" ").trim();
|
|
1896
|
-
if (!Number.isInteger(n) || n < 2 || n > 4 || ask.length === 0) {
|
|
1897
|
-
this.push("status", "usage: /variants <2-4> <ask> \xB7 /variants apply <id> \xB7 list \xB7 clean");
|
|
1898
|
-
break;
|
|
1899
|
-
}
|
|
1900
|
-
if (!isGitRepo(this.opts.cwd)) {
|
|
1901
|
-
this.push("error", "variants need a git repo with at least one commit");
|
|
1902
|
-
break;
|
|
1903
|
-
}
|
|
1904
|
-
void (async () => {
|
|
1905
|
-
cleanVariants(this.opts.cwd);
|
|
1906
|
-
this.push("status", `generating ${n} directions in parallel \u2014 this runs ${n} engine sessions`);
|
|
1907
|
-
this.notify({ running: true, runStartedAt: Date.now() });
|
|
1908
|
-
const engine = getEngine(this.state.engineId);
|
|
1909
|
-
const runs = await runVariants(
|
|
1910
|
-
this.opts.cwd,
|
|
1911
|
-
ask,
|
|
1912
|
-
n,
|
|
1913
|
-
engine,
|
|
1914
|
-
this.state.model,
|
|
1915
|
-
(familyId, text) => this.push("status", `[${familyId}] ${text}`)
|
|
1916
|
-
);
|
|
1917
|
-
const succeeded = runs.filter((r) => r.result.ok);
|
|
1918
|
-
if (succeeded.length > 0 && findChrome() && detectDevCommand(this.opts.cwd)) {
|
|
1919
|
-
this.push("status", "capturing one screenshot per variant\u2026");
|
|
1920
|
-
const shots = await screenshotVariants(this.opts.cwd, succeeded.map((r) => r.variant));
|
|
1921
|
-
for (const shot of shots) {
|
|
1922
|
-
this.push(
|
|
1923
|
-
shot.path ? "status" : "error",
|
|
1924
|
-
`[${shot.familyId}] ${shot.path ?? shot.error ?? "screenshot failed"}`
|
|
1925
|
-
);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
this.notify({ running: false });
|
|
1929
|
-
this.push(
|
|
1930
|
-
succeeded.length > 0 ? "status" : "error",
|
|
1931
|
-
`${succeeded.length}/${runs.length} variants ready \u2014 /variants apply <id> keeps one, /variants clean discards`
|
|
1932
|
-
);
|
|
1933
|
-
this.drainQueue();
|
|
1934
|
-
})();
|
|
1935
|
-
break;
|
|
1936
|
-
}
|
|
1937
|
-
case "clear":
|
|
1938
|
-
this.sessionId = void 0;
|
|
1939
|
-
clearState(this.opts.cwd);
|
|
1940
|
-
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1941
|
-
break;
|
|
1942
|
-
case "help": {
|
|
1943
|
-
void import("./commands-BY44HDQ6.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1944
|
-
break;
|
|
1945
|
-
}
|
|
1946
|
-
case "quit":
|
|
1947
|
-
case "exit":
|
|
1948
|
-
this.push("status", this.summary());
|
|
1949
|
-
this.dispose();
|
|
1950
|
-
this.opts.onQuit?.();
|
|
1951
|
-
break;
|
|
1952
|
-
default:
|
|
1953
|
-
this.push("error", `unknown command /${name} \u2014 try /help`);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
};
|
|
1957
|
-
|
|
1958
|
-
// src/tui/lineEditor.ts
|
|
1959
|
-
var emptyLine = { text: "", cursor: 0 };
|
|
1960
|
-
function fromText(text) {
|
|
1961
|
-
return { text, cursor: text.length };
|
|
1962
|
-
}
|
|
1963
|
-
function insert(line, str) {
|
|
1964
|
-
const clean = str.replace(/\r/g, "").replace(/\n+/g, " ");
|
|
1965
|
-
return {
|
|
1966
|
-
text: line.text.slice(0, line.cursor) + clean + line.text.slice(line.cursor),
|
|
1967
|
-
cursor: line.cursor + clean.length
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
function backspace(line) {
|
|
1971
|
-
if (line.cursor === 0) return line;
|
|
1972
|
-
return {
|
|
1973
|
-
text: line.text.slice(0, line.cursor - 1) + line.text.slice(line.cursor),
|
|
1974
|
-
cursor: line.cursor - 1
|
|
1975
|
-
};
|
|
1976
|
-
}
|
|
1977
|
-
function left(line) {
|
|
1978
|
-
return { ...line, cursor: Math.max(0, line.cursor - 1) };
|
|
1979
|
-
}
|
|
1980
|
-
function right(line) {
|
|
1981
|
-
return { ...line, cursor: Math.min(line.text.length, line.cursor + 1) };
|
|
1982
|
-
}
|
|
1983
|
-
function home(line) {
|
|
1984
|
-
return { ...line, cursor: 0 };
|
|
1985
|
-
}
|
|
1986
|
-
function end(line) {
|
|
1987
|
-
return { ...line, cursor: line.text.length };
|
|
1988
|
-
}
|
|
1989
|
-
var isWordChar = (ch) => /[\p{L}\p{N}_/@.-]/u.test(ch);
|
|
1990
|
-
function wordLeft(line) {
|
|
1991
|
-
let i = line.cursor;
|
|
1992
|
-
while (i > 0 && !isWordChar(line.text[i - 1])) i--;
|
|
1993
|
-
while (i > 0 && isWordChar(line.text[i - 1])) i--;
|
|
1994
|
-
return { ...line, cursor: i };
|
|
1995
|
-
}
|
|
1996
|
-
function wordRight(line) {
|
|
1997
|
-
let i = line.cursor;
|
|
1998
|
-
const n = line.text.length;
|
|
1999
|
-
while (i < n && !isWordChar(line.text[i])) i++;
|
|
2000
|
-
while (i < n && isWordChar(line.text[i])) i++;
|
|
2001
|
-
return { ...line, cursor: i };
|
|
2002
|
-
}
|
|
2003
|
-
function killToEnd(line) {
|
|
2004
|
-
return { text: line.text.slice(0, line.cursor), cursor: line.cursor };
|
|
2005
|
-
}
|
|
2006
|
-
function killToStart(line) {
|
|
2007
|
-
return { text: line.text.slice(line.cursor), cursor: 0 };
|
|
2008
|
-
}
|
|
2009
|
-
function killWordBack(line) {
|
|
2010
|
-
const target = wordLeft(line).cursor;
|
|
2011
|
-
return {
|
|
2012
|
-
text: line.text.slice(0, target) + line.text.slice(line.cursor),
|
|
2013
|
-
cursor: target
|
|
2014
|
-
};
|
|
2015
|
-
}
|
|
2016
|
-
|
|
2017
|
-
// src/tui/markdown.tsx
|
|
2018
|
-
import { Box, Text } from "ink";
|
|
2019
|
-
|
|
2020
|
-
// src/tui/themeContext.tsx
|
|
2021
|
-
import { createContext, useContext } from "react";
|
|
2022
|
-
|
|
2023
|
-
// src/tui/theme.ts
|
|
2024
|
-
var THEMES = {
|
|
2025
|
-
amber: {
|
|
2026
|
-
name: "amber",
|
|
2027
|
-
accent: "#e8a33d",
|
|
2028
|
-
dim: "gray",
|
|
2029
|
-
user: "#7aa2f7",
|
|
2030
|
-
error: "#f7768e",
|
|
2031
|
-
success: "#9ece6a",
|
|
2032
|
-
tool: "#7dcfff"
|
|
2033
|
-
},
|
|
2034
|
-
ocean: {
|
|
2035
|
-
name: "ocean",
|
|
2036
|
-
accent: "#56b6c2",
|
|
2037
|
-
dim: "gray",
|
|
2038
|
-
user: "#61afef",
|
|
2039
|
-
error: "#e06c75",
|
|
2040
|
-
success: "#98c379",
|
|
2041
|
-
tool: "#c678dd"
|
|
2042
|
-
},
|
|
2043
|
-
moss: {
|
|
2044
|
-
name: "moss",
|
|
2045
|
-
accent: "#a7c080",
|
|
2046
|
-
dim: "gray",
|
|
2047
|
-
user: "#7fbbb3",
|
|
2048
|
-
error: "#e67e80",
|
|
2049
|
-
success: "#83c092",
|
|
2050
|
-
tool: "#d699b6"
|
|
2051
|
-
},
|
|
2052
|
-
rose: {
|
|
2053
|
-
name: "rose",
|
|
2054
|
-
accent: "#ebbcba",
|
|
2055
|
-
dim: "gray",
|
|
2056
|
-
user: "#9ccfd8",
|
|
2057
|
-
error: "#eb6f92",
|
|
2058
|
-
success: "#31748f",
|
|
2059
|
-
tool: "#c4a7e7"
|
|
2060
|
-
},
|
|
2061
|
-
light: {
|
|
2062
|
-
name: "light",
|
|
2063
|
-
accent: "#9a6b1f",
|
|
2064
|
-
dim: "#6b6f76",
|
|
2065
|
-
user: "#2a5db0",
|
|
2066
|
-
error: "#c4322e",
|
|
2067
|
-
success: "#3d7a37",
|
|
2068
|
-
tool: "#0f7b8a"
|
|
2069
|
-
},
|
|
2070
|
-
mono: {
|
|
2071
|
-
name: "mono",
|
|
2072
|
-
accent: "white",
|
|
2073
|
-
dim: "gray",
|
|
2074
|
-
user: "white",
|
|
2075
|
-
error: "white",
|
|
2076
|
-
success: "white",
|
|
2077
|
-
tool: "gray"
|
|
2078
|
-
}
|
|
2079
|
-
};
|
|
2080
|
-
var DEFAULT_THEME = "amber";
|
|
2081
|
-
function resolveTheme(name, env = process.env) {
|
|
2082
|
-
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return THEMES.mono;
|
|
2083
|
-
return THEMES[name ?? DEFAULT_THEME] ?? THEMES[DEFAULT_THEME];
|
|
2084
|
-
}
|
|
2085
|
-
var theme = THEMES[DEFAULT_THEME];
|
|
2086
|
-
|
|
2087
|
-
// src/tui/themeContext.tsx
|
|
2088
|
-
var ThemeContext = createContext(theme);
|
|
2089
|
-
var ThemeProvider = ThemeContext.Provider;
|
|
2090
|
-
function useTheme() {
|
|
2091
|
-
return useContext(ThemeContext);
|
|
2092
|
-
}
|
|
2093
|
-
|
|
2094
|
-
// src/tui/markdown.tsx
|
|
2095
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2096
|
-
var INLINE_RE = /(\*\*[^*]+\*\*|\*[^*\s][^*]*\*|`[^`]+`)/g;
|
|
2097
|
-
function parseInline(text) {
|
|
2098
|
-
const segments = [];
|
|
2099
|
-
let last = 0;
|
|
2100
|
-
for (const match of text.matchAll(INLINE_RE)) {
|
|
2101
|
-
if (match.index > last) segments.push({ text: text.slice(last, match.index) });
|
|
2102
|
-
const token = match[0];
|
|
2103
|
-
if (token.startsWith("**")) segments.push({ text: token.slice(2, -2), bold: true });
|
|
2104
|
-
else if (token.startsWith("`")) segments.push({ text: token.slice(1, -1), code: true });
|
|
2105
|
-
else segments.push({ text: token.slice(1, -1), italic: true });
|
|
2106
|
-
last = match.index + token.length;
|
|
2107
|
-
}
|
|
2108
|
-
if (last < text.length) segments.push({ text: text.slice(last) });
|
|
2109
|
-
return segments;
|
|
2110
|
-
}
|
|
2111
|
-
function parseBlocks(text) {
|
|
2112
|
-
const blocks = [];
|
|
2113
|
-
let code = null;
|
|
2114
|
-
for (const rawLine of text.split("\n")) {
|
|
2115
|
-
const line = rawLine.replace(/\s+$/, "");
|
|
2116
|
-
if (code) {
|
|
2117
|
-
if (/^\s*```/.test(line)) {
|
|
2118
|
-
blocks.push({ type: "code", ...code });
|
|
2119
|
-
code = null;
|
|
2120
|
-
} else {
|
|
2121
|
-
code.lines.push(rawLine);
|
|
2122
|
-
}
|
|
2123
|
-
continue;
|
|
2124
|
-
}
|
|
2125
|
-
const fence = /^\s*```(\S*)/.exec(line);
|
|
2126
|
-
if (fence) {
|
|
2127
|
-
code = { lang: fence[1] ?? "", lines: [] };
|
|
2128
|
-
continue;
|
|
2129
|
-
}
|
|
2130
|
-
const heading = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
2131
|
-
if (heading) {
|
|
2132
|
-
blocks.push({ type: "heading", level: heading[1].length, text: heading[2] });
|
|
2133
|
-
continue;
|
|
2134
|
-
}
|
|
2135
|
-
if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(line) && line.trim().length >= 3) {
|
|
2136
|
-
blocks.push({ type: "hr" });
|
|
2137
|
-
continue;
|
|
2138
|
-
}
|
|
2139
|
-
const item = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
2140
|
-
if (item) {
|
|
2141
|
-
blocks.push({ type: "item", text: item[2], indent: Math.floor(item[1].length / 2) });
|
|
2142
|
-
continue;
|
|
2143
|
-
}
|
|
2144
|
-
const ordered = /^(\s*)(\d+)[.)]\s+(.*)$/.exec(line);
|
|
2145
|
-
if (ordered) {
|
|
2146
|
-
blocks.push({
|
|
2147
|
-
type: "item",
|
|
2148
|
-
text: ordered[3],
|
|
2149
|
-
indent: Math.floor(ordered[1].length / 2),
|
|
2150
|
-
ordered: ordered[2]
|
|
2151
|
-
});
|
|
2152
|
-
continue;
|
|
2153
|
-
}
|
|
2154
|
-
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
2155
|
-
if (quote) {
|
|
2156
|
-
blocks.push({ type: "quote", text: quote[1] });
|
|
2157
|
-
continue;
|
|
2158
|
-
}
|
|
2159
|
-
blocks.push({ type: "para", text: line });
|
|
2160
|
-
}
|
|
2161
|
-
if (code) blocks.push({ type: "code", ...code });
|
|
2162
|
-
return blocks;
|
|
2163
|
-
}
|
|
2164
|
-
function Inline({ text, dim }) {
|
|
2165
|
-
const theme2 = useTheme();
|
|
2166
|
-
const segments = parseInline(text);
|
|
2167
|
-
return /* @__PURE__ */ jsx(Text, { wrap: "wrap", dimColor: dim, children: segments.map(
|
|
2168
|
-
(segment, index) => segment.code ? /* @__PURE__ */ jsx(Text, { color: theme2.tool, children: segment.text }, index) : /* @__PURE__ */ jsx(Text, { bold: segment.bold, italic: segment.italic, children: segment.text }, index)
|
|
2169
|
-
) });
|
|
2170
|
-
}
|
|
2171
|
-
function Markdown({ text }) {
|
|
2172
|
-
const theme2 = useTheme();
|
|
2173
|
-
const blocks = parseBlocks(text);
|
|
2174
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: blocks.map((block, index) => {
|
|
2175
|
-
switch (block.type) {
|
|
2176
|
-
case "heading":
|
|
2177
|
-
return /* @__PURE__ */ jsx(Text, { bold: true, color: block.level <= 2 ? theme2.accent : void 0, wrap: "wrap", children: block.text }, index);
|
|
2178
|
-
case "item":
|
|
2179
|
-
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 1 + block.indent * 2, children: [
|
|
2180
|
-
/* @__PURE__ */ jsxs(Text, { color: theme2.accent, children: [
|
|
2181
|
-
block.ordered ? `${block.ordered}.` : "\u2022",
|
|
2182
|
-
" "
|
|
2183
|
-
] }),
|
|
2184
|
-
/* @__PURE__ */ jsx(Inline, { text: block.text })
|
|
2185
|
-
] }, index);
|
|
2186
|
-
case "quote":
|
|
2187
|
-
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
|
|
2188
|
-
/* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u2502 " }),
|
|
2189
|
-
/* @__PURE__ */ jsx(Inline, { text: block.text, dim: true })
|
|
2190
|
-
] }, index);
|
|
2191
|
-
case "code":
|
|
2192
|
-
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 1, children: block.lines.map((line, lineIndex) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
2193
|
-
/* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u258F " }),
|
|
2194
|
-
/* @__PURE__ */ jsx(Text, { color: theme2.tool, children: line.length > 0 ? line : " " })
|
|
2195
|
-
] }, lineIndex)) }, index);
|
|
2196
|
-
case "hr":
|
|
2197
|
-
return /* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u2500".repeat(32) }, index);
|
|
2198
|
-
case "para":
|
|
2199
|
-
return block.text.length === 0 ? /* @__PURE__ */ jsx(Text, { children: " " }, index) : /* @__PURE__ */ jsx(Inline, { text: block.text }, index);
|
|
2200
|
-
}
|
|
2201
|
-
}) });
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
// src/tui/messages.tsx
|
|
2205
|
-
import { Box as Box2, Text as Text2 } from "ink";
|
|
2206
|
-
import Image from "ink-picture";
|
|
2207
|
-
import { useEffect, useState } from "react";
|
|
2208
|
-
|
|
2209
|
-
// src/tui/termImage.ts
|
|
2210
|
-
function supportsInlineImages(env = process.env) {
|
|
2211
|
-
if (env.TMUX) return false;
|
|
2212
|
-
if (env.TERM === "xterm-kitty" || env.KITTY_WINDOW_ID) return true;
|
|
2213
|
-
if (env.TERM === "xterm-ghostty" || env.GHOSTTY_RESOURCES_DIR) return true;
|
|
2214
|
-
if (env.TERM_PROGRAM === "WezTerm") return true;
|
|
2215
|
-
if (env.TERM_PROGRAM === "iTerm.app") return true;
|
|
2216
|
-
return false;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
// src/tui/messages.tsx
|
|
2220
|
-
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2221
|
-
var INLINE_IMAGES = supportsInlineImages();
|
|
2222
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2223
|
-
var PHRASES = ["working", "thinking", "squinting", "crafting", "still at it"];
|
|
2224
|
-
function WorkingLine({ startedAt }) {
|
|
2225
|
-
const theme2 = useTheme();
|
|
2226
|
-
const [frame, setFrame] = useState(0);
|
|
2227
|
-
const [elapsed, setElapsed] = useState(0);
|
|
2228
|
-
useEffect(() => {
|
|
2229
|
-
const timer = setInterval(() => {
|
|
2230
|
-
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
2231
|
-
setElapsed(Math.floor((Date.now() - startedAt) / 1e3));
|
|
2232
|
-
}, 80);
|
|
2233
|
-
return () => clearInterval(timer);
|
|
2234
|
-
}, [startedAt]);
|
|
2235
|
-
const phrase = PHRASES[Math.min(Math.floor(elapsed / 8), PHRASES.length - 1)];
|
|
2236
|
-
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
2237
|
-
/* @__PURE__ */ jsx2(Text2, { color: theme2.accent, children: SPINNER_FRAMES[frame] }),
|
|
2238
|
-
/* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, children: [
|
|
2239
|
-
" ",
|
|
2240
|
-
phrase,
|
|
2241
|
-
"\u2026 ",
|
|
2242
|
-
elapsed,
|
|
2243
|
-
"s \xB7 esc to interrupt"
|
|
2244
|
-
] })
|
|
2245
|
-
] });
|
|
2246
|
-
}
|
|
2247
|
-
function toolGlyph(text) {
|
|
2248
|
-
const name = text.split(/[\s·]/, 1)[0]?.toLowerCase() ?? "";
|
|
2249
|
-
if (name.includes("todo")) return "\u2630";
|
|
2250
|
-
if (name.includes("read") || name.includes("view") || name.includes("cat")) return "\u2299";
|
|
2251
|
-
if (name.includes("edit") || name.includes("write") || name.includes("patch") || name.includes("apply")) return "\u270E";
|
|
2252
|
-
if (name.includes("bash") || name.includes("shell") || name.includes("exec") || name.includes("command")) return "$";
|
|
2253
|
-
if (name.includes("grep") || name.includes("glob") || name.includes("search") || name.includes("find")) return "\u2315";
|
|
2254
|
-
if (name.includes("web") || name.includes("fetch") || name.includes("http")) return "\u21E3";
|
|
2255
|
-
if (name.includes("task") || name.includes("agent")) return "\u25C7";
|
|
2256
|
-
return "\u2699";
|
|
2257
|
-
}
|
|
2258
|
-
function MessageLine({ message }) {
|
|
2259
|
-
const theme2 = useTheme();
|
|
2260
|
-
switch (message.role) {
|
|
2261
|
-
case "user":
|
|
2262
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.user, wrap: "wrap", children: [
|
|
2263
|
-
"\u276F ",
|
|
2264
|
-
message.text
|
|
2265
|
-
] });
|
|
2266
|
-
case "assistant":
|
|
2267
|
-
return /* @__PURE__ */ jsx2(Markdown, { text: message.text });
|
|
2268
|
-
case "status":
|
|
2269
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, wrap: "wrap", children: [
|
|
2270
|
-
"\xB7 ",
|
|
2271
|
-
message.text
|
|
2272
|
-
] });
|
|
2273
|
-
case "tool":
|
|
2274
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.tool, wrap: "wrap", children: [
|
|
2275
|
-
toolGlyph(message.text),
|
|
2276
|
-
" ",
|
|
2277
|
-
message.text
|
|
2278
|
-
] });
|
|
2279
|
-
case "thinking":
|
|
2280
|
-
return /* @__PURE__ */ jsx2(Text2, { color: theme2.dim, italic: true, wrap: "wrap", children: message.text });
|
|
2281
|
-
case "error":
|
|
2282
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.error, wrap: "wrap", children: [
|
|
2283
|
-
"\u2717 ",
|
|
2284
|
-
message.text
|
|
2285
|
-
] });
|
|
2286
|
-
case "image":
|
|
2287
|
-
if (!INLINE_IMAGES) {
|
|
2288
|
-
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, wrap: "wrap", children: [
|
|
2289
|
-
"\u25A3 ",
|
|
2290
|
-
message.text
|
|
2291
|
-
] });
|
|
2292
|
-
}
|
|
2293
|
-
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2294
|
-
/* @__PURE__ */ jsx2(Image, { src: message.text, width: 48, height: 14, alt: "screenshot" }),
|
|
2295
|
-
/* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, children: [
|
|
2296
|
-
"\u25A3 ",
|
|
2297
|
-
message.text
|
|
2298
|
-
] })
|
|
2299
|
-
] });
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// src/tui/App.tsx
|
|
2304
|
-
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2305
|
-
function App({
|
|
2306
|
-
cwd,
|
|
2307
|
-
initialEngine,
|
|
2308
|
-
initialModel,
|
|
2309
|
-
autoDev,
|
|
2310
|
-
autoFix,
|
|
2311
|
-
autoProbe,
|
|
2312
|
-
autoCheck,
|
|
2313
|
-
autoReview,
|
|
2314
|
-
fixModel,
|
|
2315
|
-
bell,
|
|
2316
|
-
budgetUsd,
|
|
2317
|
-
initialTheme
|
|
2318
|
-
}) {
|
|
2319
|
-
const { exit } = useApp();
|
|
2320
|
-
const [themeName, setThemeName] = useState2(() => resolveTheme(initialTheme).name);
|
|
2321
|
-
const theme2 = resolveTheme(themeName);
|
|
2322
|
-
const sessionRef = useRef(null);
|
|
2323
|
-
if (!sessionRef.current) {
|
|
2324
|
-
sessionRef.current = new Session({
|
|
2325
|
-
cwd,
|
|
2326
|
-
engineId: initialEngine,
|
|
2327
|
-
model: initialModel,
|
|
2328
|
-
autoDev,
|
|
2329
|
-
autoFix,
|
|
2330
|
-
autoProbe,
|
|
2331
|
-
autoCheck,
|
|
2332
|
-
autoReview,
|
|
2333
|
-
fixModel,
|
|
2334
|
-
budgetUsd,
|
|
2335
|
-
// Delay lets the goodbye summary land in the Static scrollback.
|
|
2336
|
-
onQuit: () => setTimeout(() => exit(), 60)
|
|
2337
|
-
});
|
|
2338
|
-
}
|
|
2339
|
-
const session = sessionRef.current;
|
|
2340
|
-
const state = useSyncExternalStore(
|
|
2341
|
-
useMemo(() => (listener) => session.subscribe(listener), [session]),
|
|
2342
|
-
() => session.getState()
|
|
2343
|
-
);
|
|
2344
|
-
const [line, setLine] = useState2(emptyLine);
|
|
2345
|
-
const historyRef = useRef([]);
|
|
2346
|
-
const historyIndexRef = useRef(-1);
|
|
2347
|
-
const ctrlCArmedAtRef = useRef(0);
|
|
2348
|
-
const wasRunningRef = useRef(false);
|
|
2349
|
-
if (wasRunningRef.current && !state.running && bell !== false) {
|
|
2350
|
-
process.stdout.write("\x07");
|
|
2351
|
-
}
|
|
2352
|
-
wasRunningRef.current = state.running;
|
|
2353
|
-
useInput((char, key) => {
|
|
2354
|
-
if (key.ctrl && char === "c") {
|
|
2355
|
-
const now = Date.now();
|
|
2356
|
-
if (now - ctrlCArmedAtRef.current < 2e3) {
|
|
2357
|
-
session.note(session.summary());
|
|
2358
|
-
session.dispose();
|
|
2359
|
-
setTimeout(() => exit(), 60);
|
|
2360
|
-
} else {
|
|
2361
|
-
ctrlCArmedAtRef.current = now;
|
|
2362
|
-
session.note("press ctrl+c again to exit");
|
|
2363
|
-
}
|
|
2364
|
-
return;
|
|
2365
|
-
}
|
|
2366
|
-
if (key.escape && state.running) {
|
|
2367
|
-
session.interrupt();
|
|
2368
|
-
return;
|
|
2369
|
-
}
|
|
2370
|
-
if (key.tab && key.shift) {
|
|
2371
|
-
session.cycleMode();
|
|
2372
|
-
return;
|
|
2373
|
-
}
|
|
2374
|
-
if (key.tab && line.text.startsWith("/") && !line.text.includes(" ")) {
|
|
2375
|
-
const matches = completeCommand(line.text.slice(1));
|
|
2376
|
-
if (matches.length > 0) {
|
|
2377
|
-
setLine(fromText(`/${matches[0].name}${matches[0].args ? " " : ""}`));
|
|
2378
|
-
}
|
|
2379
|
-
return;
|
|
2380
|
-
}
|
|
2381
|
-
if (key.return) {
|
|
2382
|
-
const value = line.text.trim();
|
|
2383
|
-
setLine(emptyLine);
|
|
2384
|
-
historyIndexRef.current = -1;
|
|
2385
|
-
if (!value) return;
|
|
2386
|
-
historyRef.current.push(value);
|
|
2387
|
-
if (value === "/theme" || value.startsWith("/theme ")) {
|
|
2388
|
-
const requested = value.slice("/theme".length).trim();
|
|
2389
|
-
if (!requested) {
|
|
2390
|
-
session.note(`themes: ${Object.keys(THEMES).join(", ")} \u2014 /theme <name>`);
|
|
2391
|
-
} else if (THEMES[requested]) {
|
|
2392
|
-
setThemeName(requested);
|
|
2393
|
-
session.note(`theme \u2192 ${requested}`);
|
|
2394
|
-
} else {
|
|
2395
|
-
session.note(`unknown theme "${requested}" \u2014 themes: ${Object.keys(THEMES).join(", ")}`);
|
|
2396
|
-
}
|
|
2397
|
-
return;
|
|
2398
|
-
}
|
|
2399
|
-
session.input(value);
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
|
-
if (key.upArrow) {
|
|
2403
|
-
const history = historyRef.current;
|
|
2404
|
-
if (history.length === 0) return;
|
|
2405
|
-
const next = historyIndexRef.current === -1 ? history.length - 1 : Math.max(historyIndexRef.current - 1, 0);
|
|
2406
|
-
historyIndexRef.current = next;
|
|
2407
|
-
setLine(fromText(history[next] ?? ""));
|
|
2408
|
-
return;
|
|
2409
|
-
}
|
|
2410
|
-
if (key.downArrow) {
|
|
2411
|
-
const history = historyRef.current;
|
|
2412
|
-
if (historyIndexRef.current === -1) return;
|
|
2413
|
-
const next = historyIndexRef.current + 1;
|
|
2414
|
-
if (next >= history.length) {
|
|
2415
|
-
historyIndexRef.current = -1;
|
|
2416
|
-
setLine(emptyLine);
|
|
2417
|
-
} else {
|
|
2418
|
-
historyIndexRef.current = next;
|
|
2419
|
-
setLine(fromText(history[next] ?? ""));
|
|
2420
|
-
}
|
|
2421
|
-
return;
|
|
2422
|
-
}
|
|
2423
|
-
if (key.leftArrow) {
|
|
2424
|
-
setLine((prev) => key.meta ? wordLeft(prev) : left(prev));
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
if (key.rightArrow) {
|
|
2428
|
-
setLine((prev) => key.meta ? wordRight(prev) : right(prev));
|
|
2429
|
-
return;
|
|
2430
|
-
}
|
|
2431
|
-
if (key.backspace || key.delete) {
|
|
2432
|
-
setLine((prev) => key.meta ? killWordBack(prev) : backspace(prev));
|
|
2433
|
-
return;
|
|
2434
|
-
}
|
|
2435
|
-
if (key.ctrl) {
|
|
2436
|
-
switch (char) {
|
|
2437
|
-
case "a":
|
|
2438
|
-
setLine(home);
|
|
2439
|
-
return;
|
|
2440
|
-
case "e":
|
|
2441
|
-
setLine(end);
|
|
2442
|
-
return;
|
|
2443
|
-
case "k":
|
|
2444
|
-
setLine(killToEnd);
|
|
2445
|
-
return;
|
|
2446
|
-
case "u":
|
|
2447
|
-
setLine(killToStart);
|
|
2448
|
-
return;
|
|
2449
|
-
case "w":
|
|
2450
|
-
setLine(killWordBack);
|
|
2451
|
-
return;
|
|
2452
|
-
}
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
if (char && !key.meta) {
|
|
2456
|
-
setLine((prev) => insert(prev, char));
|
|
2457
|
-
}
|
|
2458
|
-
});
|
|
2459
|
-
const devBadge = state.devState === "running" ? ` \xB7 ${state.devUrl ?? "dev running"}` : state.devState === "starting" ? " \xB7 dev starting\u2026" : state.devState === "crashed" ? " \xB7 dev crashed" : "";
|
|
2460
|
-
const totalsBadge = state.totals.turns > 0 ? ` \xB7 ${state.totals.turns} turn${state.totals.turns === 1 ? "" : "s"}${state.totals.costUsd > 0 ? ` \xB7 $${state.totals.costUsd.toFixed(2)}` : ""}` : "";
|
|
2461
|
-
return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsx3(InkPictureProvider, { children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
|
|
2462
|
-
/* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
|
|
2463
|
-
state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
|
|
2464
|
-
state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
|
|
2465
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
|
|
2466
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
|
|
2467
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
|
|
2468
|
-
] }),
|
|
2469
|
-
state.running && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
|
|
2470
|
-
state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
|
|
2471
|
-
"\u22EF ",
|
|
2472
|
-
index + 1,
|
|
2473
|
-
". ",
|
|
2474
|
-
queued,
|
|
2475
|
-
index === state.queue.length - 1 ? " (/queue drop <n> removes)" : ""
|
|
2476
|
-
] }) }, index)),
|
|
2477
|
-
line.text.startsWith("/") && !line.text.includes(" ") && /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: completeCommand(line.text.slice(1)).slice(0, 5).map((command, index) => /* @__PURE__ */ jsxs3(Text3, { color: index === 0 ? theme2.accent : theme2.dim, children: [
|
|
2478
|
-
"/",
|
|
2479
|
-
command.name,
|
|
2480
|
-
command.args ? ` ${command.args}` : "",
|
|
2481
|
-
" ",
|
|
2482
|
-
/* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
|
|
2483
|
-
"\u2014 ",
|
|
2484
|
-
command.description
|
|
2485
|
-
] })
|
|
2486
|
-
] }, command.name)) }),
|
|
2487
|
-
/* @__PURE__ */ jsx3(Box3, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2488
|
-
/* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
|
|
2489
|
-
line.text.slice(0, line.cursor),
|
|
2490
|
-
/* @__PURE__ */ jsx3(Text3, { inverse: true, children: line.text[line.cursor] ?? " " }),
|
|
2491
|
-
line.text.slice(line.cursor + 1)
|
|
2492
|
-
] }) }),
|
|
2493
|
-
/* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
|
|
2494
|
-
/* @__PURE__ */ jsxs3(
|
|
2495
|
-
Text3,
|
|
2496
|
-
{
|
|
2497
|
-
color: state.mode === "yolo" ? theme2.error : state.mode === "plan" ? theme2.user : theme2.dim,
|
|
2498
|
-
bold: state.mode !== "safe",
|
|
2499
|
-
children: [
|
|
2500
|
-
"[",
|
|
2501
|
-
state.mode,
|
|
2502
|
-
"]"
|
|
2503
|
-
]
|
|
2504
|
-
}
|
|
2505
|
-
),
|
|
2506
|
-
state.sandbox && /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, bold: true, children: " [sandbox]" }),
|
|
2507
|
-
" ",
|
|
2508
|
-
state.engineId,
|
|
2509
|
-
state.model ? ` \xB7 ${state.model}` : "",
|
|
2510
|
-
" \xB7 ",
|
|
2511
|
-
path4.basename(cwd),
|
|
2512
|
-
devBadge,
|
|
2513
|
-
totalsBadge,
|
|
2514
|
-
state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
|
|
2515
|
-
" \xB7 ",
|
|
2516
|
-
state.problems.length,
|
|
2517
|
-
" problem",
|
|
2518
|
-
state.problems.length === 1 ? "" : "s"
|
|
2519
|
-
] }),
|
|
2520
|
-
" ",
|
|
2521
|
-
"\xB7 shift+tab mode \xB7 /help"
|
|
2522
|
-
] }) })
|
|
2523
|
-
] }) }) });
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
// src/cli/tui.tsx
|
|
2527
|
-
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
688
|
+
import { jsx } from "react/jsx-runtime";
|
|
2528
689
|
function registerTui(program2) {
|
|
2529
690
|
program2.action(async () => {
|
|
2530
691
|
const cwd = process.cwd();
|
|
2531
692
|
const config = loadConfig(defaultPaths(cwd));
|
|
2532
693
|
const engineId = resolveEngineId(config);
|
|
2533
694
|
const model = resolveModel(config, engineId);
|
|
2534
|
-
let
|
|
2535
|
-
if (!
|
|
695
|
+
let theme = config.theme;
|
|
696
|
+
if (!theme && !process.env.NO_COLOR) {
|
|
2536
697
|
const { detectBackground } = await import("./background-J7OQTYEC.js");
|
|
2537
|
-
if (await detectBackground() === "light")
|
|
698
|
+
if (await detectBackground() === "light") theme = "light";
|
|
2538
699
|
}
|
|
2539
700
|
render(
|
|
2540
|
-
/* @__PURE__ */
|
|
701
|
+
/* @__PURE__ */ jsx(
|
|
2541
702
|
App,
|
|
2542
703
|
{
|
|
2543
704
|
cwd,
|
|
@@ -2551,7 +712,7 @@ function registerTui(program2) {
|
|
|
2551
712
|
fixModel: config.fixModel,
|
|
2552
713
|
bell: config.bell,
|
|
2553
714
|
budgetUsd: config.budgetUsd,
|
|
2554
|
-
initialTheme:
|
|
715
|
+
initialTheme: theme
|
|
2555
716
|
}
|
|
2556
717
|
)
|
|
2557
718
|
);
|
|
@@ -2559,7 +720,7 @@ function registerTui(program2) {
|
|
|
2559
720
|
}
|
|
2560
721
|
|
|
2561
722
|
// src/cli.tsx
|
|
2562
|
-
var VERSION = true ? "0.4.
|
|
723
|
+
var VERSION = true ? "0.4.7" : "0.0.0-dev";
|
|
2563
724
|
var program = new Command();
|
|
2564
725
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2565
726
|
registerRun(program);
|
|
@@ -2568,5 +729,6 @@ registerScaffold(program);
|
|
|
2568
729
|
registerProject(program);
|
|
2569
730
|
registerQuality(program);
|
|
2570
731
|
registerConfig(program);
|
|
732
|
+
registerDaemon(program);
|
|
2571
733
|
registerTui(program);
|
|
2572
734
|
program.parseAsync(process.argv);
|