@aayambansal/squint 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,16 +1,34 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ completeCommand
4
+ } from "./chunk-CFWKEIJJ.js";
5
+ 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";
2
20
  import {
3
21
  DevServer,
4
22
  buildFixPrompt,
5
23
  detectDevCommand,
6
24
  screenshotVariants
7
- } from "./chunk-5VKUJPOL.js";
25
+ } from "./chunk-F6JZSXEO.js";
8
26
  import {
9
27
  applyVariant,
10
28
  cleanVariants,
11
29
  listVariants,
12
30
  runVariants
13
- } from "./chunk-X6MDBKED.js";
31
+ } from "./chunk-A64VVTPU.js";
14
32
  import {
15
33
  buildGatePrompt,
16
34
  detectFastGates,
@@ -21,27 +39,21 @@ import {
21
39
  buildReviewPrompt,
22
40
  buildRuntimeFixPrompt,
23
41
  captureViewports,
24
- clearState,
25
42
  comparePulse,
26
- loadState,
27
43
  probeRuntime,
28
- runtimeSummary,
29
- saveState
30
- } from "./chunk-CHZE3UHK.js";
31
- import {
32
- completeCommand
33
- } from "./chunk-YTO2YRF7.js";
44
+ runtimeSummary
45
+ } from "./chunk-RQFFAJFE.js";
34
46
  import {
35
47
  runAgent
36
- } from "./chunk-C7WKNJG6.js";
48
+ } from "./chunk-VH7OOFQP.js";
37
49
  import {
38
50
  findChrome
39
- } from "./chunk-B7LOERSP.js";
51
+ } from "./chunk-IMDRXXFU.js";
40
52
  import {
41
53
  detectEngines,
42
54
  getEngine
43
- } from "./chunk-BWZFACBT.js";
44
- import "./chunk-OC6RU6XH.js";
55
+ } from "./chunk-KVYGPLWW.js";
56
+ import "./chunk-QEENJGCL.js";
45
57
  import {
46
58
  enrich
47
59
  } from "./chunk-4LAU5TGK.js";
@@ -49,11 +61,10 @@ import {
49
61
  composePrompt
50
62
  } from "./chunk-P3H4N2EN.js";
51
63
  import {
52
- diffStatSince,
53
- isGitRepo,
54
- restoreSnapshot,
55
- takeSnapshot
56
- } from "./chunk-YHRAOBI2.js";
64
+ clearState,
65
+ loadState,
66
+ saveState
67
+ } from "./chunk-O2S6PAJE.js";
57
68
 
58
69
  // src/cli.tsx
59
70
  import { Command } from "commander";
@@ -202,7 +213,7 @@ function registerEnv(program2) {
202
213
  console.log(`${status} ${engine.name}${binaryPath ? "" : pc2.dim(` \u2014 install: ${engine.install}`)}`);
203
214
  }
204
215
  if (options.probe) {
205
- const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
216
+ const { runAgent: runAgent2 } = await import("./run-NDSNTVYP.js");
206
217
  console.log(pc2.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
207
218
  for (const { engine, path: binaryPath } of detected) {
208
219
  if (!binaryPath) continue;
@@ -224,8 +235,8 @@ function registerEnv(program2) {
224
235
  );
225
236
  }
226
237
  }
227
- const { findChrome: findChrome2 } = await import("./chrome-NVU47PLK.js");
228
- const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
238
+ const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
239
+ const { hasWebSocket } = await import("./cdp-4IZHB3W5.js");
229
240
  const chrome = findChrome2();
230
241
  console.log(
231
242
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -265,22 +276,22 @@ function registerProject(program2) {
265
276
  }
266
277
  });
267
278
  skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
268
- const fs3 = await import("fs");
279
+ const fs2 = await import("fs");
269
280
  const nodePath = await import("path");
270
281
  const cwd = process.cwd();
271
282
  const skillsDir = nodePath.join(cwd, ".squint", "skills");
272
- fs3.mkdirSync(skillsDir, { recursive: true });
283
+ fs2.mkdirSync(skillsDir, { recursive: true });
273
284
  const rules = nodePath.join(cwd, ".squint", "rules.md");
274
- if (!fs3.existsSync(rules)) {
275
- fs3.writeFileSync(
285
+ if (!fs2.existsSync(rules)) {
286
+ fs2.writeFileSync(
276
287
  rules,
277
288
  "# 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"
278
289
  );
279
290
  console.log(pc3.green("\u2713 .squint/rules.md"));
280
291
  }
281
292
  const example = nodePath.join(skillsDir, "example.md");
282
- if (!fs3.existsSync(example)) {
283
- fs3.writeFileSync(
293
+ if (!fs2.existsSync(example)) {
294
+ fs2.writeFileSync(
284
295
  example,
285
296
  "---\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"
286
297
  );
@@ -289,7 +300,7 @@ function registerProject(program2) {
289
300
  console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
290
301
  });
291
302
  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) => {
292
- const fs3 = await import("fs");
303
+ const fs2 = await import("fs");
293
304
  const nodePath = await import("path");
294
305
  const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
295
306
  if (!familyId) {
@@ -307,16 +318,49 @@ function registerProject(program2) {
307
318
  return;
308
319
  }
309
320
  const target = nodePath.join(process.cwd(), ".squint", "brief.md");
310
- if (fs3.existsSync(target) && !options.force) {
321
+ if (fs2.existsSync(target) && !options.force) {
311
322
  console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
312
323
  process.exitCode = 1;
313
324
  return;
314
325
  }
315
- fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
316
- fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
326
+ fs2.mkdirSync(nodePath.dirname(target), { recursive: true });
327
+ fs2.writeFileSync(target, renderFamilyBrief(family) + "\n");
317
328
  console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
318
329
  console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
319
330
  });
331
+ const sandboxCommand = program2.command("sandbox").description("Cumulative diff worktree \u2014 asks accumulate until you apply");
332
+ sandboxCommand.command("diff").description("Show accumulated sandbox changes").action(async () => {
333
+ const { sandboxDiffStat: sandboxDiffStat2, sandboxExists: sandboxExists2, sandboxFiles: sandboxFiles2 } = await import("./sandbox-J2NCPYHJ.js");
334
+ const cwd = process.cwd();
335
+ if (!sandboxExists2(cwd)) {
336
+ console.log(pc3.dim("no sandbox open \u2014 /sandbox on inside the TUI"));
337
+ return;
338
+ }
339
+ const stat = sandboxDiffStat2(cwd);
340
+ if (!stat) {
341
+ console.log(pc3.dim("sandbox is clean"));
342
+ return;
343
+ }
344
+ console.log(stat);
345
+ for (const line of sandboxFiles2(cwd)) console.log(pc3.dim(line));
346
+ });
347
+ sandboxCommand.command("apply").description("Land the sandbox diff on the real tree and close it").action(async () => {
348
+ const { applySandbox: applySandbox2, discardSandbox: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
349
+ const cwd = process.cwd();
350
+ const result = applySandbox2(cwd);
351
+ if (!result.ok) {
352
+ console.error(pc3.red(`\u2717 ${result.detail}`));
353
+ process.exitCode = 1;
354
+ return;
355
+ }
356
+ discardSandbox2(cwd);
357
+ console.log(pc3.green("\u2713 sandbox applied to the real tree") + pc3.dim(" \u2014 review with git diff"));
358
+ });
359
+ sandboxCommand.command("discard").description("Close the sandbox without touching the real tree").action(async () => {
360
+ const { discardSandbox: discardSandbox2 } = await import("./sandbox-J2NCPYHJ.js");
361
+ const had = discardSandbox2(process.cwd());
362
+ console.log(pc3.dim(had ? "sandbox discarded" : "no sandbox open"));
363
+ });
320
364
  const variantsCommand = program2.command("variants").description("Parallel design explorations \u2014 one aesthetic family each, pick with your eyes");
321
365
  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(
322
366
  async (nRaw, promptWords, options) => {
@@ -333,7 +377,7 @@ function registerProject(program2) {
333
377
  process.exitCode = 1;
334
378
  return;
335
379
  }
336
- const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
380
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
337
381
  const config = loadConfig(defaultPaths(cwd));
338
382
  const engineId = resolveEngineId(config, options.engine);
339
383
  const engine = getEngine(engineId);
@@ -351,7 +395,7 @@ function registerProject(program2) {
351
395
  );
352
396
  const succeeded = runs.filter((r) => r.result.ok);
353
397
  if (options.shots && succeeded.length > 0) {
354
- const { screenshotVariants: screenshotVariants2 } = await import("./shots-FVG4CDVK.js");
398
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-E2AWBDCN.js");
355
399
  console.log(pc3.dim("capturing screenshots\u2026"));
356
400
  const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
357
401
  for (const shot of shots) {
@@ -366,7 +410,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
366
410
  }
367
411
  );
368
412
  variantsCommand.command("list").description("List generated variants").action(async () => {
369
- const { listVariants: listVariants2 } = await import("./variants-UULPQ7Q5.js");
413
+ const { listVariants: listVariants2 } = await import("./variants-3ZSINJGX.js");
370
414
  const ids = listVariants2(process.cwd());
371
415
  if (ids.length === 0) {
372
416
  console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
@@ -375,7 +419,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
375
419
  for (const id of ids) console.log(id);
376
420
  });
377
421
  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) => {
378
- const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
422
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
379
423
  const cwd = process.cwd();
380
424
  const result = applyVariant2(cwd, id);
381
425
  if (!result.ok) {
@@ -387,7 +431,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
387
431
  console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
388
432
  });
389
433
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
390
- const { cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
434
+ const { cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
391
435
  const count = cleanVariants2(process.cwd());
392
436
  console.log(pc3.dim(`removed ${count} variant(s)`));
393
437
  });
@@ -415,7 +459,7 @@ function registerQuality(program2) {
415
459
  if (results.some((r) => !r.ok)) process.exitCode = 1;
416
460
  });
417
461
  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) => {
418
- const { captureViewports: captureViewports2 } = await import("./preview-XGASKUXR.js");
462
+ const { captureViewports: captureViewports2 } = await import("./preview-56UCV5A3.js");
419
463
  const result = await captureViewports2(process.cwd(), url);
420
464
  if (!result) {
421
465
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -533,21 +577,21 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
533
577
  console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
534
578
  });
535
579
  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 () => {
536
- const fs3 = await import("fs");
580
+ const fs2 = await import("fs");
537
581
  const nodePath = await import("path");
538
582
  const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
539
583
  const cwd = process.cwd();
540
584
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
541
- fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
585
+ fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
542
586
  console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
543
- const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
587
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs2.existsSync(candidate));
544
588
  if (!configPath) {
545
589
  console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
546
590
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
547
591
  plugins: [squintTagger(), \u2026]`));
548
592
  return;
549
593
  }
550
- const source = fs3.readFileSync(configPath, "utf8");
594
+ const source = fs2.readFileSync(configPath, "utf8");
551
595
  const patched = patchViteConfig(source);
552
596
  if (patched === "already") {
553
597
  console.log(pc6.dim("vite config already wired"));
@@ -556,7 +600,7 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
556
600
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
557
601
  plugins: [squintTagger(), \u2026]`));
558
602
  } else {
559
- fs3.writeFileSync(configPath, patched);
603
+ fs2.writeFileSync(configPath, patched);
560
604
  console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
561
605
  }
562
606
  console.log(
@@ -569,98 +613,13 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
569
613
  import { render } from "ink";
570
614
 
571
615
  // src/tui/App.tsx
572
- import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
573
- import path4 from "path";
616
+ import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
617
+ import { InkPictureProvider } from "ink-picture";
618
+ import path3 from "path";
574
619
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
575
620
 
576
621
  // src/session/engine.ts
577
- import path3 from "path";
578
-
579
- // src/vcs/sandbox.ts
580
- import { execFileSync } from "child_process";
581
- import fs2 from "fs";
582
622
  import path2 from "path";
583
- function git(cwd, args) {
584
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
585
- }
586
- function sandboxDir(cwd) {
587
- return path2.join(cwd, ".squint", "sandbox");
588
- }
589
- function sandboxExists(cwd) {
590
- return fs2.existsSync(path2.join(sandboxDir(cwd), ".git"));
591
- }
592
- function openSandbox(cwd) {
593
- const dir = sandboxDir(cwd);
594
- if (sandboxExists(cwd)) return { dir, reused: true };
595
- fs2.rmSync(dir, { recursive: true, force: true });
596
- fs2.mkdirSync(path2.dirname(dir), { recursive: true });
597
- git(cwd, ["worktree", "add", "--force", "--detach", dir, "HEAD"]);
598
- const mainModules = path2.join(cwd, "node_modules");
599
- const sandboxModules = path2.join(dir, "node_modules");
600
- if (fs2.existsSync(mainModules) && !fs2.existsSync(sandboxModules)) {
601
- fs2.symlinkSync(mainModules, sandboxModules, "dir");
602
- }
603
- return { dir, reused: false };
604
- }
605
- function sandboxDiffStat(cwd) {
606
- if (!sandboxExists(cwd)) return null;
607
- const dir = sandboxDir(cwd);
608
- try {
609
- git(dir, ["add", "-A"]);
610
- const stat = git(dir, ["diff", "--cached", "--shortstat", "HEAD"]);
611
- return stat.length > 0 ? stat : null;
612
- } catch {
613
- return null;
614
- }
615
- }
616
- function sandboxFiles(cwd) {
617
- if (!sandboxExists(cwd)) return [];
618
- const dir = sandboxDir(cwd);
619
- try {
620
- git(dir, ["add", "-A"]);
621
- const out = git(dir, ["diff", "--cached", "--name-status", "HEAD"]);
622
- return out.length > 0 ? out.split("\n") : [];
623
- } catch {
624
- return [];
625
- }
626
- }
627
- function applySandbox(cwd) {
628
- if (!sandboxExists(cwd)) return { ok: false, detail: "no sandbox open" };
629
- const dir = sandboxDir(cwd);
630
- try {
631
- git(dir, ["add", "-A"]);
632
- const patch = git(dir, ["diff", "--binary", "--cached", "HEAD"]);
633
- if (patch.length === 0) return { ok: false, detail: "sandbox has no changes" };
634
- const patchFile = path2.join(cwd, ".squint", "sandbox.patch");
635
- fs2.writeFileSync(patchFile, patch + "\n");
636
- git(cwd, ["apply", "--whitespace=nowarn", patchFile]);
637
- fs2.rmSync(patchFile, { force: true });
638
- return { ok: true };
639
- } catch (err) {
640
- return { ok: false, detail: err instanceof Error ? err.message.split("\n")[0] : String(err) };
641
- }
642
- }
643
- function discardSandbox(cwd) {
644
- if (!sandboxExists(cwd)) return false;
645
- const dir = sandboxDir(cwd);
646
- try {
647
- const link = path2.join(dir, "node_modules");
648
- if (fs2.existsSync(link) && fs2.lstatSync(link).isSymbolicLink()) fs2.rmSync(link);
649
- } catch {
650
- }
651
- try {
652
- git(cwd, ["worktree", "remove", "--force", dir]);
653
- } catch {
654
- fs2.rmSync(dir, { recursive: true, force: true });
655
- }
656
- try {
657
- git(cwd, ["worktree", "prune"]);
658
- } catch {
659
- }
660
- return true;
661
- }
662
-
663
- // src/session/engine.ts
664
623
  var MAX_AUTO_FIX_ATTEMPTS = 2;
665
624
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
666
625
  var Session = class {
@@ -807,6 +766,18 @@ var Session = class {
807
766
  this.push("status", "queue cleared");
808
767
  return;
809
768
  }
769
+ const drop = /^\/queue drop (\d+)$/.exec(value);
770
+ if (drop) {
771
+ const index = Number.parseInt(drop[1], 10) - 1;
772
+ if (index >= 0 && index < this.state.queue.length) {
773
+ const removed = this.state.queue[index];
774
+ this.notify({ queue: this.state.queue.filter((_, i) => i !== index) });
775
+ this.push("status", `dropped from queue: ${removed}`);
776
+ } else {
777
+ this.push("status", `queue has ${this.state.queue.length} item(s) \u2014 /queue drop <1-${Math.max(this.state.queue.length, 1)}>`);
778
+ }
779
+ return;
780
+ }
810
781
  this.notify({ queue: [...this.state.queue, value] });
811
782
  return;
812
783
  }
@@ -992,6 +963,7 @@ var Session = class {
992
963
  sessionId: this.sessionId,
993
964
  model: this.state.model,
994
965
  lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
966
+ totals: this.state.totals,
995
967
  at: Date.now()
996
968
  });
997
969
  }
@@ -1054,7 +1026,7 @@ ${errors.slice(-5).join("\n")}`);
1054
1026
  const captureResult = await this.capture();
1055
1027
  if (captureResult) {
1056
1028
  await this.runTurn(
1057
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y),
1029
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop),
1058
1030
  "\u{1F441} auto-review rendered UI"
1059
1031
  );
1060
1032
  }
@@ -1128,6 +1100,7 @@ ${errors.slice(-5).join("\n")}`);
1128
1100
  this.lastPulse = current;
1129
1101
  if (!previous) {
1130
1102
  this.push("status", "visual pulse: baseline captured");
1103
+ this.push("image", pulsePath);
1131
1104
  return null;
1132
1105
  }
1133
1106
  const pct = await comparePulse(previous, current);
@@ -1136,16 +1109,18 @@ ${errors.slice(-5).join("\n")}`);
1136
1109
  "status",
1137
1110
  pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
1138
1111
  );
1112
+ if (pct >= 0.5) this.push("image", pulsePath);
1139
1113
  return pct;
1140
1114
  }
1141
1115
  /** Screenshot the running app (and watch its runtime where CDP is available). */
1142
- async capture() {
1143
- if (!this.state.devUrl) {
1144
- this.push("error", "dev server not running \u2014 /dev first");
1116
+ async capture(urlOverride) {
1117
+ const url = urlOverride || this.state.devUrl;
1118
+ if (!url) {
1119
+ this.push("error", "dev server not running \u2014 /dev first, or /shot <url>");
1145
1120
  return null;
1146
1121
  }
1147
1122
  this.push("status", "capturing screenshots\u2026");
1148
- const result = await captureViewports(this.opts.cwd, this.state.devUrl);
1123
+ const result = await captureViewports(this.opts.cwd, url);
1149
1124
  if (!result) {
1150
1125
  this.push("error", "no Chrome/Chromium found for screenshots");
1151
1126
  return null;
@@ -1154,8 +1129,10 @@ ${errors.slice(-5).join("\n")}`);
1154
1129
  if (result.shots.length > 0) {
1155
1130
  this.push(
1156
1131
  "status",
1157
- `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path3.dirname(result.shots[0].path)}`
1132
+ `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`
1158
1133
  );
1134
+ const desktop = result.shots.find((s) => s.name === "desktop") ?? result.shots[0];
1135
+ this.push("image", desktop.path);
1159
1136
  }
1160
1137
  if (result.runtime) {
1161
1138
  const summary = runtimeSummary(result.runtime);
@@ -1168,6 +1145,10 @@ ${errors.slice(-5).join("\n")}`);
1168
1145
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1169
1146
  }
1170
1147
  }
1148
+ if (result.slop && result.slop.length > 0) {
1149
+ this.push("status", `distinctiveness: ${result.slop.length} tell(s)
1150
+ ${result.slop.join("\n")}`);
1151
+ }
1171
1152
  if (result.a11y && result.a11y.length > 0) {
1172
1153
  this.push("error", `a11y: ${result.a11y.length} finding(s)
1173
1154
  ${result.a11y.slice(0, 5).join("\n")}`);
@@ -1191,7 +1172,7 @@ They are objective defects, not style preferences.`
1191
1172
  const arg = rest.join(" ").trim();
1192
1173
  switch (name) {
1193
1174
  case "engines": {
1194
- void import("./registry-S3PCXJVF.js").then(({ detectEngines: detectEngines2 }) => {
1175
+ void import("./registry-MIJ6LSAY.js").then(({ detectEngines: detectEngines2 }) => {
1195
1176
  const lines = detectEngines2().map(({ engine, path: binaryPath }) => {
1196
1177
  const mark = binaryPath ? "\u2713" : "\u2717";
1197
1178
  const traits = [engine.createParser ? "stream" : "text", engine.supportsResume ? "resume" : null].filter(Boolean).join(" \xB7 ");
@@ -1281,12 +1262,12 @@ They are objective defects, not style preferences.`
1281
1262
  }
1282
1263
  case "save": {
1283
1264
  void (async () => {
1284
- const fs3 = await import("fs");
1285
- const path5 = await import("path");
1286
- const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1287
- fs3.mkdirSync(dir, { recursive: true });
1265
+ const fs2 = await import("fs");
1266
+ const path4 = await import("path");
1267
+ const dir = path4.join(this.opts.cwd, ".squint", "transcripts");
1268
+ fs2.mkdirSync(dir, { recursive: true });
1288
1269
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1289
- const file = path5.join(dir, `${stamp}.md`);
1270
+ const file = path4.join(dir, `${stamp}.md`);
1290
1271
  const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
1291
1272
  for (const item of this.state.items) {
1292
1273
  switch (item.role) {
@@ -1299,6 +1280,9 @@ They are objective defects, not style preferences.`
1299
1280
  case "tool":
1300
1281
  lines.push(`- \u2699 ${item.text}`);
1301
1282
  break;
1283
+ case "image":
1284
+ lines.push(`![screenshot](${item.text})`, "");
1285
+ break;
1302
1286
  case "thinking":
1303
1287
  break;
1304
1288
  default:
@@ -1306,8 +1290,8 @@ They are objective defects, not style preferences.`
1306
1290
  }
1307
1291
  }
1308
1292
  lines.push("", `> ${this.summary()}`);
1309
- fs3.writeFileSync(file, lines.join("\n") + "\n");
1310
- this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
1293
+ fs2.writeFileSync(file, lines.join("\n") + "\n");
1294
+ this.push("status", `saved transcript \u2192 ${path4.relative(this.opts.cwd, file)}`);
1311
1295
  })();
1312
1296
  break;
1313
1297
  }
@@ -1364,14 +1348,14 @@ They are objective defects, not style preferences.`
1364
1348
  })();
1365
1349
  break;
1366
1350
  case "shot":
1367
- void this.capture();
1351
+ void this.capture(arg || void 0);
1368
1352
  break;
1369
1353
  case "review":
1370
1354
  void (async () => {
1371
1355
  const result = await this.capture();
1372
1356
  if (result) {
1373
1357
  await this.runTurn(
1374
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y),
1358
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop),
1375
1359
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1376
1360
  );
1377
1361
  }
@@ -1421,7 +1405,11 @@ They are objective defects, not style preferences.`
1421
1405
  break;
1422
1406
  }
1423
1407
  this.sessionId = saved.sessionId;
1424
- this.notify({ engineId: saved.engine, model: saved.model ?? this.state.model });
1408
+ this.notify({
1409
+ engineId: saved.engine,
1410
+ model: saved.model ?? this.state.model,
1411
+ totals: saved.totals ?? this.state.totals
1412
+ });
1425
1413
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
1426
1414
  break;
1427
1415
  }
@@ -1551,7 +1539,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1551
1539
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1552
1540
  break;
1553
1541
  case "help": {
1554
- void import("./commands-N33DKZKC.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1542
+ void import("./commands-QKDWUD4K.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1555
1543
  break;
1556
1544
  }
1557
1545
  case "quit":
@@ -1813,9 +1801,23 @@ function Markdown({ text }) {
1813
1801
  }
1814
1802
 
1815
1803
  // src/tui/messages.tsx
1816
- import { Text as Text2 } from "ink";
1804
+ import { Box as Box2, Text as Text2 } from "ink";
1805
+ import Image from "ink-picture";
1817
1806
  import { useEffect, useState } from "react";
1807
+
1808
+ // src/tui/termImage.ts
1809
+ function supportsInlineImages(env = process.env) {
1810
+ if (env.TMUX) return false;
1811
+ if (env.TERM === "xterm-kitty" || env.KITTY_WINDOW_ID) return true;
1812
+ if (env.TERM === "xterm-ghostty" || env.GHOSTTY_RESOURCES_DIR) return true;
1813
+ if (env.TERM_PROGRAM === "WezTerm") return true;
1814
+ if (env.TERM_PROGRAM === "iTerm.app") return true;
1815
+ return false;
1816
+ }
1817
+
1818
+ // src/tui/messages.tsx
1818
1819
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1820
+ var INLINE_IMAGES = supportsInlineImages();
1819
1821
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1820
1822
  var PHRASES = ["working", "thinking", "squinting", "crafting", "still at it"];
1821
1823
  function WorkingLine({ startedAt }) {
@@ -1880,6 +1882,20 @@ function MessageLine({ message }) {
1880
1882
  "\u2717 ",
1881
1883
  message.text
1882
1884
  ] });
1885
+ case "image":
1886
+ if (!INLINE_IMAGES) {
1887
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, wrap: "wrap", children: [
1888
+ "\u25A3 ",
1889
+ message.text
1890
+ ] });
1891
+ }
1892
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1893
+ /* @__PURE__ */ jsx2(Image, { src: message.text, width: 48, height: 14, alt: "screenshot" }),
1894
+ /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, children: [
1895
+ "\u25A3 ",
1896
+ message.text
1897
+ ] })
1898
+ ] });
1883
1899
  }
1884
1900
  }
1885
1901
 
@@ -2039,20 +2055,23 @@ function App({
2039
2055
  });
2040
2056
  const devBadge = state.devState === "running" ? ` \xB7 ${state.devUrl ?? "dev running"}` : state.devState === "starting" ? " \xB7 dev starting\u2026" : state.devState === "crashed" ? " \xB7 dev crashed" : "";
2041
2057
  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)}` : ""}` : "";
2042
- return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", paddingX: 1, children: [
2043
- /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
2044
- state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
2045
- state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
2058
+ return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsx3(InkPictureProvider, { children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
2059
+ /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
2060
+ state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
2061
+ state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
2046
2062
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
2047
2063
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
2048
2064
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
2049
2065
  ] }),
2050
- state.running && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
2051
- state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2052
- "\u22EF queued: ",
2053
- queued
2066
+ state.running && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
2067
+ state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2068
+ "\u22EF ",
2069
+ index + 1,
2070
+ ". ",
2071
+ queued,
2072
+ index === state.queue.length - 1 ? " (/queue drop <n> removes)" : ""
2054
2073
  ] }) }, index)),
2055
- line.text.startsWith("/") && !line.text.includes(" ") && /* @__PURE__ */ jsx3(Box2, { 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: [
2074
+ 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: [
2056
2075
  "/",
2057
2076
  command.name,
2058
2077
  command.args ? ` ${command.args}` : "",
@@ -2062,13 +2081,13 @@ function App({
2062
2081
  command.description
2063
2082
  ] })
2064
2083
  ] }, command.name)) }),
2065
- /* @__PURE__ */ jsx3(Box2, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
2084
+ /* @__PURE__ */ jsx3(Box3, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
2066
2085
  /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
2067
2086
  line.text.slice(0, line.cursor),
2068
2087
  /* @__PURE__ */ jsx3(Text3, { inverse: true, children: line.text[line.cursor] ?? " " }),
2069
2088
  line.text.slice(line.cursor + 1)
2070
2089
  ] }) }),
2071
- /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2090
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2072
2091
  /* @__PURE__ */ jsxs3(
2073
2092
  Text3,
2074
2093
  {
@@ -2086,7 +2105,7 @@ function App({
2086
2105
  state.engineId,
2087
2106
  state.model ? ` \xB7 ${state.model}` : "",
2088
2107
  " \xB7 ",
2089
- path4.basename(cwd),
2108
+ path3.basename(cwd),
2090
2109
  devBadge,
2091
2110
  totalsBadge,
2092
2111
  state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
@@ -2098,7 +2117,7 @@ function App({
2098
2117
  " ",
2099
2118
  "\xB7 shift+tab mode \xB7 /help"
2100
2119
  ] }) })
2101
- ] }) });
2120
+ ] }) }) });
2102
2121
  }
2103
2122
 
2104
2123
  // src/cli/tui.tsx
@@ -2136,7 +2155,7 @@ function registerTui(program2) {
2136
2155
  }
2137
2156
 
2138
2157
  // src/cli.tsx
2139
- var VERSION = "0.2.8";
2158
+ var VERSION = true ? "0.3.0" : "0.0.0-dev";
2140
2159
  var program = new Command();
2141
2160
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2142
2161
  registerRun(program);