@aayambansal/squint 0.2.9 → 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.
@@ -3,7 +3,7 @@ import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
5
  pixelDiffPct
6
- } from "./chunk-OC6RU6XH.js";
6
+ } from "./chunk-QEENJGCL.js";
7
7
  export {
8
8
  cdpCapture,
9
9
  hasWebSocket,
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  findChrome,
4
4
  screenshot
5
- } from "./chunk-B7LOERSP.js";
6
- import "./chunk-BWZFACBT.js";
5
+ } from "./chunk-IMDRXXFU.js";
6
+ import "./chunk-KVYGPLWW.js";
7
7
  export {
8
8
  findChrome,
9
9
  screenshot
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runAgent
4
- } from "./chunk-C7WKNJG6.js";
4
+ } from "./chunk-VH7OOFQP.js";
5
5
  import {
6
6
  FAMILIES,
7
7
  FIRST_TURN_ADDENDUM,
@@ -7,7 +7,7 @@ var COMMANDS = [
7
7
  { name: "engine", args: "<id>", group: "build", description: "switch backend (new session)" },
8
8
  { name: "engines", group: "build", description: "list installed engines with streaming/resume support" },
9
9
  { name: "model", args: "[name]", group: "build", description: "model override for the engine" },
10
- { name: "queue", args: "clear", group: "build", description: "drop queued asks" },
10
+ { name: "queue", args: "clear|drop <n>", group: "build", description: "clear the queue or drop one queued ask" },
11
11
  { name: "check", group: "verify", description: "run all quality gates (typecheck, lint, format, test, build)" },
12
12
  { name: "problems", group: "verify", description: "list open findings from gates, dev server, runtime, a11y" },
13
13
  { name: "fix", args: "[n]", group: "verify", description: "send all open problems to the engine, or just problem n" },
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  variantsRoot
4
- } from "./chunk-X6MDBKED.js";
4
+ } from "./chunk-A64VVTPU.js";
5
5
  import {
6
6
  findChrome,
7
7
  screenshot
8
- } from "./chunk-B7LOERSP.js";
8
+ } from "./chunk-IMDRXXFU.js";
9
9
  import {
10
10
  lineSplitter
11
- } from "./chunk-BWZFACBT.js";
11
+ } from "./chunk-KVYGPLWW.js";
12
12
 
13
13
  // src/variants/shots.ts
14
14
  import path2 from "path";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  findBinary
4
- } from "./chunk-BWZFACBT.js";
4
+ } from "./chunk-KVYGPLWW.js";
5
5
 
6
6
  // src/preview/chrome.ts
7
7
  import { spawn } from "child_process";
@@ -98,8 +98,18 @@ function createClaudeStreamParser(readyLabel) {
98
98
  durationMs: data.duration_ms
99
99
  }
100
100
  ];
101
- case "user":
102
- return [];
101
+ case "user": {
102
+ if (data.parent_tool_use_id) return [];
103
+ const events = [];
104
+ for (const block of data.message?.content ?? []) {
105
+ if (block.type === "tool_result" && block.is_error === true) {
106
+ const raw = Array.isArray(block.content) ? block.content.map((c) => c?.text ?? "").join(" ") : String(block.content ?? "");
107
+ const text = raw.trim().split("\n").at(-1) ?? "";
108
+ if (text) events.push({ type: "status", text: `\u26A0 tool error \xB7 ${truncate(text, 100)}` });
109
+ }
110
+ }
111
+ return events;
112
+ }
103
113
  case "rate_limit_event":
104
114
  return [];
105
115
  default:
@@ -201,11 +201,56 @@ async function pixelDiffPct(chromePath, pngA, pngB) {
201
201
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
202
202
  }
203
203
  }
204
+ var SLOP_AUDIT = `(() => {
205
+ const out = [];
206
+ const bodyFont = (getComputedStyle(document.body).fontFamily || '').toLowerCase();
207
+ const h1 = document.querySelector('h1,h2');
208
+ const displayFont = h1 ? (getComputedStyle(h1).fontFamily || '').toLowerCase() : bodyFont;
209
+ for (const tell of ['inter', 'roboto', 'arial', 'space grotesk']) {
210
+ if (displayFont.includes(tell) || bodyFont.split(',')[0].includes(tell)) {
211
+ out.push('generic font stack: ' + tell + ' (' + (displayFont.includes(tell) ? 'display' : 'body') + ')');
212
+ break;
213
+ }
214
+ }
215
+ const vw = innerWidth, vh = innerHeight;
216
+ for (const el of document.querySelectorAll('*')) {
217
+ const r = el.getBoundingClientRect();
218
+ if (r.top > vh || r.width * r.height < vw * vh * 0.2) continue;
219
+ const bg = getComputedStyle(el).backgroundImage || '';
220
+ if (bg.includes('gradient')) {
221
+ const purples = bg.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)/g) || [];
222
+ if (purples.some((c) => { const [r2, g2, b2] = c.slice(4).split(',').map(Number); return b2 > 150 && r2 > 80 && r2 < 200 && g2 < r2; })) {
223
+ out.push('purple/violet gradient on a hero-scale element');
224
+ break;
225
+ }
226
+ }
227
+ }
228
+ for (const container of document.querySelectorAll('section, div')) {
229
+ const kids = [...container.children];
230
+ if (kids.length < 3 || kids.length > 4) continue;
231
+ const rects = kids.map((k) => k.getBoundingClientRect());
232
+ if (rects[0].width < 150 || rects[0].top > vh * 2) continue;
233
+ const sameSize = rects.every((r) => Math.abs(r.width - rects[0].width) < 4 && Math.abs(r.height - rects[0].height) < 24);
234
+ const cardish = kids.every((k) => k.querySelector('svg, img') && k.querySelector('h2,h3,h4') && k.querySelector('p'));
235
+ if (sameSize && cardish) { out.push('identical icon-card grid (' + kids.length + ' cards)'); break; }
236
+ }
237
+ let emojiBullets = 0;
238
+ for (const li of document.querySelectorAll('li')) {
239
+ if (/^[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/u.test((li.textContent || '').trim())) emojiBullets++;
240
+ }
241
+ if (emojiBullets >= 3) out.push(emojiBullets + ' emoji-bulleted list items');
242
+ const rootStyle = getComputedStyle(document.documentElement);
243
+ if (rootStyle.getPropertyValue('--radius').trim() === '0.5rem' && rootStyle.getPropertyValue('--primary').trim() === '222.2 47.4% 11.2%') {
244
+ out.push('untouched shadcn default theme tokens');
245
+ }
246
+ return out.slice(0, 8);
247
+ })()`;
204
248
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
205
249
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
206
250
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
207
251
  const shots = [];
208
252
  let a11y = [];
253
+ let slop = [];
209
254
  const requests = /* @__PURE__ */ new Map();
210
255
  let connection = null;
211
256
  try {
@@ -260,6 +305,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
260
305
  if (Array.isArray(result?.value)) a11y = result.value.map(String);
261
306
  } catch {
262
307
  }
308
+ try {
309
+ const { result } = await connection.send(
310
+ "Runtime.evaluate",
311
+ { expression: SLOP_AUDIT, returnByValue: true },
312
+ sessionId
313
+ );
314
+ if (Array.isArray(result?.value)) slop = result.value.map(String);
315
+ } catch {
316
+ }
263
317
  }
264
318
  for (const viewport of viewports) {
265
319
  await connection.send(
@@ -283,7 +337,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
283
337
  child.kill("SIGKILL");
284
338
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
285
339
  }
286
- return { report, shots, a11y };
340
+ return { report, shots, a11y, slop };
287
341
  }
288
342
 
289
343
  export {
@@ -2,11 +2,11 @@
2
2
  import {
3
3
  findChrome,
4
4
  screenshot
5
- } from "./chunk-B7LOERSP.js";
5
+ } from "./chunk-IMDRXXFU.js";
6
6
  import {
7
7
  cdpCapture,
8
8
  hasWebSocket
9
- } from "./chunk-OC6RU6XH.js";
9
+ } from "./chunk-QEENJGCL.js";
10
10
  import {
11
11
  ensureSquintIgnore
12
12
  } from "./chunk-O2S6PAJE.js";
@@ -47,7 +47,7 @@ async function captureViewports(cwd, url) {
47
47
  const base = url.replace(/\/+$/, "");
48
48
  if (hasWebSocket()) {
49
49
  try {
50
- const { report, shots: shots2, a11y } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
50
+ const { report, shots: shots2, a11y, slop } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
51
51
  const errors2 = [];
52
52
  for (const route of routes.slice(1)) {
53
53
  try {
@@ -62,7 +62,7 @@ async function captureViewports(cwd, url) {
62
62
  errors2.push(`${route}: capture failed`);
63
63
  }
64
64
  }
65
- return { shots: shots2, errors: errors2, runtime: report, a11y };
65
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop };
66
66
  } catch {
67
67
  }
68
68
  }
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
133
133
  async function comparePulse(previous, current) {
134
134
  const chrome = findChrome();
135
135
  if (!chrome || !hasWebSocket()) return null;
136
- const { pixelDiffPct } = await import("./cdp-SFWNP7OA.js");
136
+ const { pixelDiffPct } = await import("./cdp-4IZHB3W5.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
@@ -151,13 +151,23 @@ ${findings.join("\n")}
151
151
 
152
152
  Fix these as part of the pass \u2014 they are objective defects, not style preferences.`;
153
153
  }
154
- function buildReviewPrompt(shots, extra, runtime, a11y) {
154
+ function slopSection(findings) {
155
+ if (!findings || findings.length === 0) return "";
156
+ return `
157
+
158
+ ## Distinctiveness debt (detected mechanically)
159
+
160
+ ${findings.join("\n")}
161
+
162
+ These patterns make the page read as template output. Rework them within the committed design direction \u2014 this is style debt, not a defect list.`;
163
+ }
164
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop) {
155
165
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
156
166
  return `Screenshots of the running app were just captured:
157
167
 
158
168
  ${list}
159
169
 
160
- Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}`;
170
+ Read each screenshot and review the rendered UI against the design standards you were given. Check: visual hierarchy and spacing rhythm, typography, color and contrast, alignment, empty-looking or broken regions, and whether the mobile capture shows horizontal overflow or cramped layout. List the concrete issues you can SEE (not hypothetical ones), ranked by visual impact${extra ? `, with special attention to: ${extra}` : ""}. Then fix them and verify the app still builds.${runtimeSection(runtime)}${a11ySection(a11y)}${slopSection(slop)}`;
161
171
  }
162
172
 
163
173
  export {
@@ -3,7 +3,7 @@ import {
3
3
  findEngineBinary,
4
4
  lineSplitter,
5
5
  truncate
6
- } from "./chunk-BWZFACBT.js";
6
+ } from "./chunk-KVYGPLWW.js";
7
7
 
8
8
  // src/runner/run.ts
9
9
  import { spawn } from "child_process";
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-RMNG2NTU.js";
4
+ } from "./chunk-CFWKEIJJ.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -22,13 +22,13 @@ import {
22
22
  buildFixPrompt,
23
23
  detectDevCommand,
24
24
  screenshotVariants
25
- } from "./chunk-5VKUJPOL.js";
25
+ } from "./chunk-F6JZSXEO.js";
26
26
  import {
27
27
  applyVariant,
28
28
  cleanVariants,
29
29
  listVariants,
30
30
  runVariants
31
- } from "./chunk-X6MDBKED.js";
31
+ } from "./chunk-A64VVTPU.js";
32
32
  import {
33
33
  buildGatePrompt,
34
34
  detectFastGates,
@@ -42,18 +42,18 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-GOAFH2VN.js";
45
+ } from "./chunk-RQFFAJFE.js";
46
46
  import {
47
47
  runAgent
48
- } from "./chunk-C7WKNJG6.js";
48
+ } from "./chunk-VH7OOFQP.js";
49
49
  import {
50
50
  findChrome
51
- } from "./chunk-B7LOERSP.js";
51
+ } from "./chunk-IMDRXXFU.js";
52
52
  import {
53
53
  detectEngines,
54
54
  getEngine
55
- } from "./chunk-BWZFACBT.js";
56
- import "./chunk-OC6RU6XH.js";
55
+ } from "./chunk-KVYGPLWW.js";
56
+ import "./chunk-QEENJGCL.js";
57
57
  import {
58
58
  enrich
59
59
  } from "./chunk-4LAU5TGK.js";
@@ -213,7 +213,7 @@ function registerEnv(program2) {
213
213
  console.log(`${status} ${engine.name}${binaryPath ? "" : pc2.dim(` \u2014 install: ${engine.install}`)}`);
214
214
  }
215
215
  if (options.probe) {
216
- const { runAgent: runAgent2 } = await import("./run-S772BPMZ.js");
216
+ const { runAgent: runAgent2 } = await import("./run-NDSNTVYP.js");
217
217
  console.log(pc2.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
218
218
  for (const { engine, path: binaryPath } of detected) {
219
219
  if (!binaryPath) continue;
@@ -235,8 +235,8 @@ function registerEnv(program2) {
235
235
  );
236
236
  }
237
237
  }
238
- const { findChrome: findChrome2 } = await import("./chrome-NVU47PLK.js");
239
- const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
238
+ const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
239
+ const { hasWebSocket } = await import("./cdp-4IZHB3W5.js");
240
240
  const chrome = findChrome2();
241
241
  console.log(
242
242
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -377,7 +377,7 @@ function registerProject(program2) {
377
377
  process.exitCode = 1;
378
378
  return;
379
379
  }
380
- const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
380
+ const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
381
381
  const config = loadConfig(defaultPaths(cwd));
382
382
  const engineId = resolveEngineId(config, options.engine);
383
383
  const engine = getEngine(engineId);
@@ -395,7 +395,7 @@ function registerProject(program2) {
395
395
  );
396
396
  const succeeded = runs.filter((r) => r.result.ok);
397
397
  if (options.shots && succeeded.length > 0) {
398
- const { screenshotVariants: screenshotVariants2 } = await import("./shots-FVG4CDVK.js");
398
+ const { screenshotVariants: screenshotVariants2 } = await import("./shots-E2AWBDCN.js");
399
399
  console.log(pc3.dim("capturing screenshots\u2026"));
400
400
  const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
401
401
  for (const shot of shots) {
@@ -410,7 +410,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
410
410
  }
411
411
  );
412
412
  variantsCommand.command("list").description("List generated variants").action(async () => {
413
- const { listVariants: listVariants2 } = await import("./variants-UULPQ7Q5.js");
413
+ const { listVariants: listVariants2 } = await import("./variants-3ZSINJGX.js");
414
414
  const ids = listVariants2(process.cwd());
415
415
  if (ids.length === 0) {
416
416
  console.log(pc3.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
@@ -419,7 +419,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
419
419
  for (const id of ids) console.log(id);
420
420
  });
421
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) => {
422
- const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
422
+ const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
423
423
  const cwd = process.cwd();
424
424
  const result = applyVariant2(cwd, id);
425
425
  if (!result.ok) {
@@ -431,7 +431,7 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
431
431
  console.log(pc3.green(`\u2713 applied ${id} to the working tree`) + pc3.dim(" \u2014 review with git diff"));
432
432
  });
433
433
  variantsCommand.command("clean").description("Discard all variants").action(async () => {
434
- const { cleanVariants: cleanVariants2 } = await import("./variants-UULPQ7Q5.js");
434
+ const { cleanVariants: cleanVariants2 } = await import("./variants-3ZSINJGX.js");
435
435
  const count = cleanVariants2(process.cwd());
436
436
  console.log(pc3.dim(`removed ${count} variant(s)`));
437
437
  });
@@ -459,7 +459,7 @@ function registerQuality(program2) {
459
459
  if (results.some((r) => !r.ok)) process.exitCode = 1;
460
460
  });
461
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) => {
462
- const { captureViewports: captureViewports2 } = await import("./preview-VW5DAJNV.js");
462
+ const { captureViewports: captureViewports2 } = await import("./preview-56UCV5A3.js");
463
463
  const result = await captureViewports2(process.cwd(), url);
464
464
  if (!result) {
465
465
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -613,7 +613,8 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
613
613
  import { render } from "ink";
614
614
 
615
615
  // src/tui/App.tsx
616
- import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
616
+ import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
617
+ import { InkPictureProvider } from "ink-picture";
617
618
  import path3 from "path";
618
619
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
619
620
 
@@ -765,6 +766,18 @@ var Session = class {
765
766
  this.push("status", "queue cleared");
766
767
  return;
767
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
+ }
768
781
  this.notify({ queue: [...this.state.queue, value] });
769
782
  return;
770
783
  }
@@ -950,6 +963,7 @@ var Session = class {
950
963
  sessionId: this.sessionId,
951
964
  model: this.state.model,
952
965
  lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
966
+ totals: this.state.totals,
953
967
  at: Date.now()
954
968
  });
955
969
  }
@@ -1012,7 +1026,7 @@ ${errors.slice(-5).join("\n")}`);
1012
1026
  const captureResult = await this.capture();
1013
1027
  if (captureResult) {
1014
1028
  await this.runTurn(
1015
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y),
1029
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop),
1016
1030
  "\u{1F441} auto-review rendered UI"
1017
1031
  );
1018
1032
  }
@@ -1086,6 +1100,7 @@ ${errors.slice(-5).join("\n")}`);
1086
1100
  this.lastPulse = current;
1087
1101
  if (!previous) {
1088
1102
  this.push("status", "visual pulse: baseline captured");
1103
+ this.push("image", pulsePath);
1089
1104
  return null;
1090
1105
  }
1091
1106
  const pct = await comparePulse(previous, current);
@@ -1094,6 +1109,7 @@ ${errors.slice(-5).join("\n")}`);
1094
1109
  "status",
1095
1110
  pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
1096
1111
  );
1112
+ if (pct >= 0.5) this.push("image", pulsePath);
1097
1113
  return pct;
1098
1114
  }
1099
1115
  /** Screenshot the running app (and watch its runtime where CDP is available). */
@@ -1115,6 +1131,8 @@ ${errors.slice(-5).join("\n")}`);
1115
1131
  "status",
1116
1132
  `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`
1117
1133
  );
1134
+ const desktop = result.shots.find((s) => s.name === "desktop") ?? result.shots[0];
1135
+ this.push("image", desktop.path);
1118
1136
  }
1119
1137
  if (result.runtime) {
1120
1138
  const summary = runtimeSummary(result.runtime);
@@ -1127,6 +1145,10 @@ ${errors.slice(-5).join("\n")}`);
1127
1145
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1128
1146
  }
1129
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
+ }
1130
1152
  if (result.a11y && result.a11y.length > 0) {
1131
1153
  this.push("error", `a11y: ${result.a11y.length} finding(s)
1132
1154
  ${result.a11y.slice(0, 5).join("\n")}`);
@@ -1150,7 +1172,7 @@ They are objective defects, not style preferences.`
1150
1172
  const arg = rest.join(" ").trim();
1151
1173
  switch (name) {
1152
1174
  case "engines": {
1153
- void import("./registry-S3PCXJVF.js").then(({ detectEngines: detectEngines2 }) => {
1175
+ void import("./registry-MIJ6LSAY.js").then(({ detectEngines: detectEngines2 }) => {
1154
1176
  const lines = detectEngines2().map(({ engine, path: binaryPath }) => {
1155
1177
  const mark = binaryPath ? "\u2713" : "\u2717";
1156
1178
  const traits = [engine.createParser ? "stream" : "text", engine.supportsResume ? "resume" : null].filter(Boolean).join(" \xB7 ");
@@ -1258,6 +1280,9 @@ They are objective defects, not style preferences.`
1258
1280
  case "tool":
1259
1281
  lines.push(`- \u2699 ${item.text}`);
1260
1282
  break;
1283
+ case "image":
1284
+ lines.push(`![screenshot](${item.text})`, "");
1285
+ break;
1261
1286
  case "thinking":
1262
1287
  break;
1263
1288
  default:
@@ -1330,7 +1355,7 @@ They are objective defects, not style preferences.`
1330
1355
  const result = await this.capture();
1331
1356
  if (result) {
1332
1357
  await this.runTurn(
1333
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y),
1358
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop),
1334
1359
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1335
1360
  );
1336
1361
  }
@@ -1380,7 +1405,11 @@ They are objective defects, not style preferences.`
1380
1405
  break;
1381
1406
  }
1382
1407
  this.sessionId = saved.sessionId;
1383
- 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
+ });
1384
1413
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
1385
1414
  break;
1386
1415
  }
@@ -1510,7 +1539,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1510
1539
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1511
1540
  break;
1512
1541
  case "help": {
1513
- void import("./commands-TERCCWFM.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1542
+ void import("./commands-QKDWUD4K.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1514
1543
  break;
1515
1544
  }
1516
1545
  case "quit":
@@ -1772,9 +1801,23 @@ function Markdown({ text }) {
1772
1801
  }
1773
1802
 
1774
1803
  // src/tui/messages.tsx
1775
- import { Text as Text2 } from "ink";
1804
+ import { Box as Box2, Text as Text2 } from "ink";
1805
+ import Image from "ink-picture";
1776
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
1777
1819
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1820
+ var INLINE_IMAGES = supportsInlineImages();
1778
1821
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1779
1822
  var PHRASES = ["working", "thinking", "squinting", "crafting", "still at it"];
1780
1823
  function WorkingLine({ startedAt }) {
@@ -1839,6 +1882,20 @@ function MessageLine({ message }) {
1839
1882
  "\u2717 ",
1840
1883
  message.text
1841
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
+ ] });
1842
1899
  }
1843
1900
  }
1844
1901
 
@@ -1998,20 +2055,23 @@ function App({
1998
2055
  });
1999
2056
  const devBadge = state.devState === "running" ? ` \xB7 ${state.devUrl ?? "dev running"}` : state.devState === "starting" ? " \xB7 dev starting\u2026" : state.devState === "crashed" ? " \xB7 dev crashed" : "";
2000
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)}` : ""}` : "";
2001
- return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", paddingX: 1, children: [
2002
- /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
2003
- state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
2004
- 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: [
2005
2062
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
2006
2063
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
2007
2064
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
2008
2065
  ] }),
2009
- state.running && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
2010
- state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2011
- "\u22EF queued: ",
2012
- 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)" : ""
2013
2073
  ] }) }, index)),
2014
- 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: [
2015
2075
  "/",
2016
2076
  command.name,
2017
2077
  command.args ? ` ${command.args}` : "",
@@ -2021,13 +2081,13 @@ function App({
2021
2081
  command.description
2022
2082
  ] })
2023
2083
  ] }, command.name)) }),
2024
- /* @__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: [
2025
2085
  /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
2026
2086
  line.text.slice(0, line.cursor),
2027
2087
  /* @__PURE__ */ jsx3(Text3, { inverse: true, children: line.text[line.cursor] ?? " " }),
2028
2088
  line.text.slice(line.cursor + 1)
2029
2089
  ] }) }),
2030
- /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2090
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2031
2091
  /* @__PURE__ */ jsxs3(
2032
2092
  Text3,
2033
2093
  {
@@ -2057,7 +2117,7 @@ function App({
2057
2117
  " ",
2058
2118
  "\xB7 shift+tab mode \xB7 /help"
2059
2119
  ] }) })
2060
- ] }) });
2120
+ ] }) }) });
2061
2121
  }
2062
2122
 
2063
2123
  // src/cli/tui.tsx
@@ -2095,7 +2155,7 @@ function registerTui(program2) {
2095
2155
  }
2096
2156
 
2097
2157
  // src/cli.tsx
2098
- var VERSION = true ? "0.2.9" : "0.0.0-dev";
2158
+ var VERSION = true ? "0.3.0" : "0.0.0-dev";
2099
2159
  var program = new Command();
2100
2160
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2101
2161
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-RMNG2NTU.js";
6
+ } from "./chunk-CFWKEIJJ.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -10,10 +10,10 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-GOAFH2VN.js";
14
- import "./chunk-B7LOERSP.js";
15
- import "./chunk-BWZFACBT.js";
16
- import "./chunk-OC6RU6XH.js";
13
+ } from "./chunk-RQFFAJFE.js";
14
+ import "./chunk-IMDRXXFU.js";
15
+ import "./chunk-KVYGPLWW.js";
16
+ import "./chunk-QEENJGCL.js";
17
17
  import "./chunk-O2S6PAJE.js";
18
18
  export {
19
19
  VIEWPORTS,
@@ -5,7 +5,7 @@ import {
5
5
  findBinary,
6
6
  findEngineBinary,
7
7
  getEngine
8
- } from "./chunk-BWZFACBT.js";
8
+ } from "./chunk-KVYGPLWW.js";
9
9
  export {
10
10
  detectEngines,
11
11
  engines,
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runAgent
4
- } from "./chunk-C7WKNJG6.js";
5
- import "./chunk-BWZFACBT.js";
4
+ } from "./chunk-VH7OOFQP.js";
5
+ import "./chunk-KVYGPLWW.js";
6
6
  export {
7
7
  runAgent
8
8
  };
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ screenshotVariants
4
+ } from "./chunk-F6JZSXEO.js";
5
+ import "./chunk-A64VVTPU.js";
6
+ import "./chunk-VH7OOFQP.js";
7
+ import "./chunk-IMDRXXFU.js";
8
+ import "./chunk-KVYGPLWW.js";
9
+ import "./chunk-P3H4N2EN.js";
10
+ export {
11
+ screenshotVariants
12
+ };
@@ -9,9 +9,9 @@ import {
9
9
  runVariants,
10
10
  variantPrompt,
11
11
  variantsRoot
12
- } from "./chunk-X6MDBKED.js";
13
- import "./chunk-C7WKNJG6.js";
14
- import "./chunk-BWZFACBT.js";
12
+ } from "./chunk-A64VVTPU.js";
13
+ import "./chunk-VH7OOFQP.js";
14
+ import "./chunk-KVYGPLWW.js";
15
15
  import "./chunk-P3H4N2EN.js";
16
16
  export {
17
17
  MAX_VARIANTS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.9",
3
+ "version": "0.3.0",
4
4
  "description": "Lovable for your terminal — a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,6 +49,7 @@
49
49
  "dependencies": {
50
50
  "commander": "^15.0.0",
51
51
  "ink": "^7.1.1",
52
+ "ink-picture": "^2.1.0",
52
53
  "picocolors": "^1.1.1",
53
54
  "react": "^19.1.0",
54
55
  "zod": "^4.4.3"
@@ -1,12 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- screenshotVariants
4
- } from "./chunk-5VKUJPOL.js";
5
- import "./chunk-X6MDBKED.js";
6
- import "./chunk-C7WKNJG6.js";
7
- import "./chunk-B7LOERSP.js";
8
- import "./chunk-BWZFACBT.js";
9
- import "./chunk-P3H4N2EN.js";
10
- export {
11
- screenshotVariants
12
- };