@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/dist/cli.js CHANGED
@@ -1,72 +1,35 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- completeCommand
4
- } from "./chunk-43NQNIJY.js";
3
+ connectDaemon,
4
+ socketPath,
5
+ startDaemon
6
+ } from "./chunk-NX7XQY2X.js";
5
7
  import {
6
- applySandbox,
7
- discardSandbox,
8
- openSandbox,
9
- sandboxDiffStat,
10
- sandboxDir,
11
- sandboxExists,
12
- sandboxFiles
13
- } from "./chunk-XEQ6JXXL.js";
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
- findChrome
56
- } from "./chunk-IMDRXXFU.js";
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/env.ts
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(pc2.dim(" id name stream resume where"));
266
+ console.log(pc3.dim(" id name stream resume where"));
197
267
  for (const { engine, path: binaryPath } of detectEngines()) {
198
- const status = binaryPath ? pc2.green("\u2713") : pc2.red("\u2717");
199
- const stream = engine.createParser ? pc2.green("yes") : pc2.dim("text");
200
- const resume = engine.supportsResume ? pc2.green("yes") : pc2.dim("no");
201
- const location = binaryPath ?? pc2.dim(`not found \u2014 ${engine.install}`);
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(pc2.dim("\nplan/safe/yolo modes map onto every engine \xB7 squint doctor --probe verifies auth"));
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 ? pc2.green("\u2713") : pc2.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`
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 ? pc2.green("\u2713") : pc2.yellow("\u25CB");
217
- console.log(`${status} ${engine.name}${binaryPath ? "" : pc2.dim(` \u2014 install: ${engine.install}`)}`);
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(pc2.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
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 ? `${pc2.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc2.red("\u2717")} ${engine.id.padEnd(10)} ${pc2.dim(detail.slice(0, 110))}`
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: findChrome2 } = await import("./chrome-SBV3H77F.js");
243
- const { hasWebSocket } = await import("./cdp-Q4H6ZHPT.js");
244
- const chrome = findChrome2();
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 ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
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() ? `${pc2.green("\u2713")} WebSocket ${pc2.dim("runtime console/network capture available")}` : `${pc2.yellow("\u25CB")} WebSocket ${pc2.dim("\u2014 node 22+ enables runtime capture; screenshots still work")}`
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(pc2.red("\nNo engines found. Install at least one to use squint."));
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(pc2.dim(`
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 pc3 from "picocolors";
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-6NZP67QT.js");
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 ? `${pc3.green("\u2713")} rules.md ${pc3.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc3.dim("\u25CB no .squint/rules.md")
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(pc3.dim("\u25CB no skills \u2014 squint skills init writes an example"));
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(`${pc3.green("\u2713")} ${skill.name.padEnd(20)} ${pc3.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
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 fs4 = await import("fs");
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
- fs4.mkdirSync(skillsDir, { recursive: true });
357
+ fs2.mkdirSync(skillsDir, { recursive: true });
288
358
  const rules = nodePath.join(cwd, ".squint", "rules.md");
289
- if (!fs4.existsSync(rules)) {
290
- fs4.writeFileSync(
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(pc3.green("\u2713 .squint/rules.md"));
364
+ console.log(pc4.green("\u2713 .squint/rules.md"));
295
365
  }
296
366
  const example = nodePath.join(skillsDir, "example.md");
297
- if (!fs4.existsSync(example)) {
298
- fs4.writeFileSync(
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(pc3.green("\u2713 .squint/skills/example.md"));
372
+ console.log(pc4.green("\u2713 .squint/skills/example.md"));
303
373
  }
304
- console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
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 fs4 = await import("fs");
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(pc3.bold("Aesthetic families") + pc3.dim(" \u2014 squint brief <id>\n"));
381
+ console.log(pc4.bold("Aesthetic families") + pc4.dim(" \u2014 squint brief <id>\n"));
312
382
  for (const family2 of FAMILIES) {
313
- console.log(`${pc3.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc3.dim(family2.summary)}`);
383
+ console.log(`${pc4.green(family2.id.padEnd(18))} ${family2.name.padEnd(22)} ${pc4.dim(family2.summary)}`);
314
384
  }
315
- console.log(pc3.dim("\nThe brief wraps every ask; edit .squint/brief.md to remix."));
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(pc3.red(`\u2717 unknown family "${familyId}" \u2014 run squint brief to list`));
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 (fs4.existsSync(target) && !options.force) {
326
- console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
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
- fs4.mkdirSync(nodePath.dirname(target), { recursive: true });
331
- fs4.writeFileSync(target, renderFamilyBrief(family) + "\n");
332
- console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
333
- console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
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: sandboxDiffStat2, sandboxExists: sandboxExists2, sandboxFiles: sandboxFiles2 } = await import("./sandbox-J2NCPYHJ.js");
407
+ const { sandboxDiffStat, sandboxExists, sandboxFiles } = await import("./sandbox-MK7Q2YNO.js");
338
408
  const cwd = process.cwd();
339
- if (!sandboxExists2(cwd)) {
340
- console.log(pc3.dim("no sandbox open \u2014 /sandbox on inside the TUI"));
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 = sandboxDiffStat2(cwd);
413
+ const stat = sandboxDiffStat(cwd);
344
414
  if (!stat) {
345
- console.log(pc3.dim("sandbox is clean"));
415
+ console.log(pc4.dim("sandbox is clean"));
346
416
  return;
347
417
  }
348
418
  console.log(stat);
349
- for (const line of sandboxFiles2(cwd)) console.log(pc3.dim(line));
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: applySandbox2, discardSandbox: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
422
+ const { applySandbox, discardSandbox } = await import("./sandbox-MK7Q2YNO.js");
353
423
  const cwd = process.cwd();
354
- const result = applySandbox2(cwd);
424
+ const result = applySandbox(cwd);
355
425
  if (!result.ok) {
356
- console.error(pc3.red(`\u2717 ${result.detail}`));
426
+ console.error(pc4.red(`\u2717 ${result.detail}`));
357
427
  process.exitCode = 1;
358
428
  return;
359
429
  }
360
- discardSandbox2(cwd);
361
- console.log(pc3.green("\u2713 sandbox applied to the real tree") + pc3.dim(" \u2014 review with git diff"));
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: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
365
- const had = discardSandbox2(process.cwd());
366
- console.log(pc3.dim(had ? "sandbox discarded" : "no sandbox open"));
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(pc3.red("\u2717 n must be 2\u20134"));
444
+ console.error(pc4.red("\u2717 n must be 2\u20134"));
375
445
  process.exitCode = 1;
376
446
  return;
377
447
  }
378
- const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
379
- if (!isGitRepo2(cwd)) {
380
- console.error(pc3.red("\u2717 variants need a git repo with at least one commit"));
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: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
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
- cleanVariants2(cwd);
391
- console.log(pc3.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
392
- const runs = await runVariants2(
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(`${pc3.cyan(familyId.padEnd(18))} ${pc3.dim(text)}`)
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: screenshotVariants2 } = await import("./shots-OKWOYF7F.js");
403
- console.log(pc3.dim("capturing screenshots\u2026"));
404
- const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
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(`${pc3.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc3.dim(shot.error ?? "")}`);
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 ` + pc3.bold("squint variants apply <id>") + pc3.dim(" applies the winner, squint variants clean discards all")
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: listVariants2 } = await import("./variants-3IEP7DFY.js");
418
- const ids = listVariants2(process.cwd());
487
+ const { listVariants } = await import("./variants-3IEP7DFY.js");
488
+ const ids = listVariants(process.cwd());
419
489
  if (ids.length === 0) {
420
- console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
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: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
496
+ const { applyVariant, cleanVariants } = await import("./variants-3IEP7DFY.js");
427
497
  const cwd = process.cwd();
428
- const result = applyVariant2(cwd, id);
498
+ const result = applyVariant(cwd, id);
429
499
  if (!result.ok) {
430
- console.error(pc3.red(`\u2717 ${result.detail}`));
500
+ console.error(pc4.red(`\u2717 ${result.detail}`));
431
501
  process.exitCode = 1;
432
502
  return;
433
503
  }
434
- cleanVariants2(cwd);
435
- console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
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: cleanVariants2 } = await import("./variants-3IEP7DFY.js");
439
- const count = cleanVariants2(process.cwd());
440
- console.log(pc3.dim(`removed ${count} variant(s)`));
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 pc4 from "picocolors";
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: detectGates2, runGates: runGates2 } = await import("./gates-ZC67NMW3.js");
518
+ const { detectGates, runGates } = await import("./gates-ZC67NMW3.js");
449
519
  const cwd = process.cwd();
450
- const gates = detectGates2(cwd);
520
+ const gates = detectGates(cwd);
451
521
  if (gates.length === 0) {
452
- console.log(pc4.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
522
+ console.log(pc5.dim("no gates detected (no package.json scripts, tsconfig, or eslint config)"));
453
523
  return;
454
524
  }
455
- console.log(pc4.dim(`running ${gates.map((g) => g.id).join(" \u2192 ")}`));
456
- const results = await runGates2(cwd, gates, (result) => {
457
- const mark = result.ok ? pc4.green("\u2713") : pc4.red("\u2717");
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)} ${pc4.dim(`${(result.durationMs / 1e3).toFixed(1)}s \xB7 ${result.gate.display}`)}`
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(pc4.dim(result.outputTail.split("\n").slice(-12).join("\n")));
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: captureViewports2 } = await import("./preview-ZUCVWGZG.js");
467
- const result = await captureViewports2(process.cwd(), url);
536
+ const { captureViewports } = await import("./preview-WT34NVCH.js");
537
+ const result = await captureViewports(process.cwd(), url);
468
538
  if (!result) {
469
- console.error(pc4.red("\u2717 no Chrome/Chromium found"));
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(`${pc4.green("\u2713")} ${shot.name.padEnd(8)} ${shot.path}`);
474
- for (const error of result.errors) console.error(pc4.red(`\u2717 ${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 pc5 from "picocolors";
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(pc5.dim(`\xB7 ${event.text}`));
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(pc5.dim(pc5.italic(event.text)));
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(pc5.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
578
+ console.log(pc6.cyan(`\u2699 ${event.name}${event.detail ? ` \xB7 ${event.detail}` : ""}`));
509
579
  break;
510
580
  case "error":
511
- console.error(pc5.red(`\u2717 ${event.text}`));
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(pc5.red("\u2717 --mode must be plan, safe, or yolo"));
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
- pc5.dim(`squint \xB7 ${engine.id}${model ? ` \xB7 ${model}` : ""}${mode && mode !== "safe" ? ` \xB7 ${mode}` : ""}`)
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(pc5.green(`\u2713 done${secs}${cost}`));
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 pc6 from "picocolors";
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(pc6.red(`\u2717 ${err instanceof Error ? err.message : String(err)}`));
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(pc6.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
638
+ console.log(pc7.green(`\u2713 scaffolded ${result.files.length} files in ${result.dir}`));
569
639
  if (options.install) {
570
- console.log(pc6.dim("installing dependencies\u2026"));
640
+ console.log(pc7.dim("installing dependencies\u2026"));
571
641
  const ok = await installDependencies(result.dir);
572
642
  if (!ok) {
573
- console.error(pc6.red("\u2717 npm install failed \u2014 run it manually"));
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: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
581
- console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
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 fs4 = await import("fs");
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
- fs4.writeFileSync(taggerPath, TAGGER_SOURCE);
590
- console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
591
- const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs4.existsSync(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(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
594
- console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
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 = fs4.readFileSync(configPath, "utf8");
668
+ const source = fs2.readFileSync(configPath, "utf8");
599
669
  const patched = patchViteConfig(source);
600
670
  if (patched === "already") {
601
- console.log(pc6.dim("vite config already wired"));
671
+ console.log(pc7.dim("vite config already wired"));
602
672
  } else if (patched === null) {
603
- console.log(pc6.yellow(`\u25CB could not patch ${nodePath.basename(configPath)} automatically \u2014 add:`));
604
- console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
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
- fs4.writeFileSync(configPath, patched);
608
- console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
677
+ fs2.writeFileSync(configPath, patched);
678
+ console.log(pc7.green(`\u2713 ${nodePath.basename(configPath)} wired`));
609
679
  }
610
680
  console.log(
611
- pc6.dim("\nin the running app: Alt+S \u2192 click elements to pin, alt+enter copies all \u2014 paste into squint")
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(`![screenshot](${item.text})`, "");
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 theme2 = config.theme;
2535
- if (!theme2 && !process.env.NO_COLOR) {
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") theme2 = "light";
698
+ if (await detectBackground() === "light") theme = "light";
2538
699
  }
2539
700
  render(
2540
- /* @__PURE__ */ jsx4(
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: theme2
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.5" : "0.0.0-dev";
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);