@aayambansal/squint 0.2.9 → 0.3.1

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-DAETGO2M.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,
@@ -5,6 +5,32 @@ import { spawn } from "child_process";
5
5
  import fs from "fs";
6
6
  import os from "os";
7
7
  import path from "path";
8
+ var PERF_PROBE = `(() => {
9
+ const out = {};
10
+ const buffered = (type) => {
11
+ try {
12
+ const po = new PerformanceObserver(() => {});
13
+ po.observe({ type, buffered: true });
14
+ const records = po.takeRecords();
15
+ po.disconnect();
16
+ return records;
17
+ } catch { return []; }
18
+ };
19
+ const lcp = buffered('largest-contentful-paint');
20
+ if (lcp.length > 0) out.lcpMs = Math.round(lcp[lcp.length - 1].startTime);
21
+ let cls = 0;
22
+ for (const e of buffered('layout-shift')) { if (!e.hadRecentInput) cls += e.value; }
23
+ out.cls = Math.round(cls * 1000) / 1000;
24
+ try {
25
+ const resources = performance.getEntriesByType('resource');
26
+ const nav = performance.getEntriesByType('navigation')[0];
27
+ let bytes = nav ? (nav.transferSize || 0) : 0;
28
+ for (const r of resources) bytes += r.transferSize || 0;
29
+ out.transferBytes = bytes;
30
+ out.requests = resources.length + (nav ? 1 : 0);
31
+ } catch {}
32
+ return out;
33
+ })()`;
8
34
  var A11Y_AUDIT = `(() => {
9
35
  const out = [];
10
36
  const name = (el) => (el.getAttribute('aria-label') || el.textContent || el.getAttribute('title') || '').trim();
@@ -201,11 +227,57 @@ async function pixelDiffPct(chromePath, pngA, pngB) {
201
227
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
202
228
  }
203
229
  }
230
+ var SLOP_AUDIT = `(() => {
231
+ const out = [];
232
+ const bodyFont = (getComputedStyle(document.body).fontFamily || '').toLowerCase();
233
+ const h1 = document.querySelector('h1,h2');
234
+ const displayFont = h1 ? (getComputedStyle(h1).fontFamily || '').toLowerCase() : bodyFont;
235
+ for (const tell of ['inter', 'roboto', 'arial', 'space grotesk']) {
236
+ if (displayFont.includes(tell) || bodyFont.split(',')[0].includes(tell)) {
237
+ out.push('generic font stack: ' + tell + ' (' + (displayFont.includes(tell) ? 'display' : 'body') + ')');
238
+ break;
239
+ }
240
+ }
241
+ const vw = innerWidth, vh = innerHeight;
242
+ for (const el of document.querySelectorAll('*')) {
243
+ const r = el.getBoundingClientRect();
244
+ if (r.top > vh || r.width * r.height < vw * vh * 0.2) continue;
245
+ const bg = getComputedStyle(el).backgroundImage || '';
246
+ if (bg.includes('gradient')) {
247
+ const purples = bg.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)/g) || [];
248
+ if (purples.some((c) => { const [r2, g2, b2] = c.slice(4).split(',').map(Number); return b2 > 150 && r2 > 80 && r2 < 200 && g2 < r2; })) {
249
+ out.push('purple/violet gradient on a hero-scale element');
250
+ break;
251
+ }
252
+ }
253
+ }
254
+ for (const container of document.querySelectorAll('section, div')) {
255
+ const kids = [...container.children];
256
+ if (kids.length < 3 || kids.length > 4) continue;
257
+ const rects = kids.map((k) => k.getBoundingClientRect());
258
+ if (rects[0].width < 150 || rects[0].top > vh * 2) continue;
259
+ const sameSize = rects.every((r) => Math.abs(r.width - rects[0].width) < 4 && Math.abs(r.height - rects[0].height) < 24);
260
+ const cardish = kids.every((k) => k.querySelector('svg, img') && k.querySelector('h2,h3,h4') && k.querySelector('p'));
261
+ if (sameSize && cardish) { out.push('identical icon-card grid (' + kids.length + ' cards)'); break; }
262
+ }
263
+ let emojiBullets = 0;
264
+ for (const li of document.querySelectorAll('li')) {
265
+ if (/^[\\u{1F300}-\\u{1FAFF}\\u{2600}-\\u{27BF}]/u.test((li.textContent || '').trim())) emojiBullets++;
266
+ }
267
+ if (emojiBullets >= 3) out.push(emojiBullets + ' emoji-bulleted list items');
268
+ const rootStyle = getComputedStyle(document.documentElement);
269
+ if (rootStyle.getPropertyValue('--radius').trim() === '0.5rem' && rootStyle.getPropertyValue('--primary').trim() === '222.2 47.4% 11.2%') {
270
+ out.push('untouched shadcn default theme tokens');
271
+ }
272
+ return out.slice(0, 8);
273
+ })()`;
204
274
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
205
275
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
206
276
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
207
277
  const shots = [];
208
278
  let a11y = [];
279
+ let slop = [];
280
+ let perf = {};
209
281
  const requests = /* @__PURE__ */ new Map();
210
282
  let connection = null;
211
283
  try {
@@ -250,6 +322,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
250
322
  await new Promise((resolve) => setTimeout(resolve, 100));
251
323
  }
252
324
  await new Promise((resolve) => setTimeout(resolve, settleMs));
325
+ try {
326
+ const { result } = await connection.send(
327
+ "Runtime.evaluate",
328
+ { expression: PERF_PROBE, returnByValue: true },
329
+ sessionId
330
+ );
331
+ if (result?.value && typeof result.value === "object") perf = result.value;
332
+ } catch {
333
+ }
253
334
  if (audit) {
254
335
  try {
255
336
  const { result } = await connection.send(
@@ -260,6 +341,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
260
341
  if (Array.isArray(result?.value)) a11y = result.value.map(String);
261
342
  } catch {
262
343
  }
344
+ try {
345
+ const { result } = await connection.send(
346
+ "Runtime.evaluate",
347
+ { expression: SLOP_AUDIT, returnByValue: true },
348
+ sessionId
349
+ );
350
+ if (Array.isArray(result?.value)) slop = result.value.map(String);
351
+ } catch {
352
+ }
263
353
  }
264
354
  for (const viewport of viewports) {
265
355
  await connection.send(
@@ -283,7 +373,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
283
373
  child.kill("SIGKILL");
284
374
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
285
375
  }
286
- return { report, shots, a11y };
376
+ return { report, shots, a11y, slop, perf };
287
377
  }
288
378
 
289
379
  export {
@@ -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";
@@ -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" },
@@ -19,6 +19,7 @@ var COMMANDS = [
19
19
  { name: "checkpoints", group: "explore", description: "list per-ask checkpoints" },
20
20
  { name: "restore", args: "<n>", group: "explore", description: "rewind files to before ask n" },
21
21
  { name: "theme", args: "[name]", group: "session", description: "switch the TUI theme", viewLevel: true },
22
+ { name: "btw", args: "<question>", group: "session", description: "read-only side question; the main thread is untouched" },
22
23
  { name: "copy", group: "session", description: "copy the last reply to the clipboard" },
23
24
  { name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
24
25
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
@@ -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:
@@ -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-DAETGO2M.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
  }
@@ -118,14 +118,14 @@ async function probeRuntime(url, cwd) {
118
118
  if (!chrome || !hasWebSocket()) return null;
119
119
  try {
120
120
  const dir = cwd ? previewDir(cwd) : os.tmpdir();
121
- const { report, shots } = await cdpCapture(
121
+ const { report, shots, perf } = await cdpCapture(
122
122
  chrome,
123
123
  url,
124
124
  dir,
125
125
  cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
126
126
  1500
127
127
  );
128
- return { report, pulsePath: shots[0]?.path };
128
+ return { report, pulsePath: shots[0]?.path, perf };
129
129
  } catch {
130
130
  return null;
131
131
  }
@@ -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-2XAU5MEA.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-IH4L2KR6.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-OTCH66M7.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-DAETGO2M.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-2XAU5MEA.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-PS7WJ2KO.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
 
@@ -671,6 +672,7 @@ var Session = class {
671
672
  fixAttempts = 0;
672
673
  reviewTipShown = false;
673
674
  lastPulse = null;
675
+ lastPerf = null;
674
676
  autoReviewedThisAsk = false;
675
677
  startedAt = Date.now();
676
678
  subscribe(listener) {
@@ -732,6 +734,41 @@ var Session = class {
732
734
  this.dispatchFix([...this.state.problems]);
733
735
  return true;
734
736
  }
737
+ /**
738
+ * A side question: read-only, no session resume, so the main thread's
739
+ * context is untouched (Cursor's /btw). Costs count; loops don't run.
740
+ */
741
+ async btw(question) {
742
+ this.push("user", `\u{1F4AC} btw: ${question}`);
743
+ this.notify({ running: true, runStartedAt: Date.now() });
744
+ const engine = getEngine(this.state.engineId);
745
+ this.abort = new AbortController();
746
+ const result = await runAgent(
747
+ engine,
748
+ {
749
+ prompt: `Answer this question about the repository. Investigate as needed but make no changes:
750
+
751
+ ${question}`,
752
+ cwd: this.execCwd(),
753
+ model: this.state.model,
754
+ mode: "plan"
755
+ },
756
+ this.handleEvent,
757
+ this.abort.signal
758
+ );
759
+ this.abort = null;
760
+ this.commitLive();
761
+ this.flushToolCollapse();
762
+ if (result.ok) {
763
+ const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
764
+ this.push("status", `btw answered${cost}`);
765
+ this.notify({
766
+ totals: { costUsd: this.state.totals.costUsd + (result.costUsd ?? 0), turns: this.state.totals.turns }
767
+ });
768
+ }
769
+ this.notify({ running: false });
770
+ this.drainQueue();
771
+ }
735
772
  setMode(mode) {
736
773
  this.notify({ mode });
737
774
  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";
@@ -765,6 +802,18 @@ var Session = class {
765
802
  this.push("status", "queue cleared");
766
803
  return;
767
804
  }
805
+ const drop = /^\/queue drop (\d+)$/.exec(value);
806
+ if (drop) {
807
+ const index = Number.parseInt(drop[1], 10) - 1;
808
+ if (index >= 0 && index < this.state.queue.length) {
809
+ const removed = this.state.queue[index];
810
+ this.notify({ queue: this.state.queue.filter((_, i) => i !== index) });
811
+ this.push("status", `dropped from queue: ${removed}`);
812
+ } else {
813
+ this.push("status", `queue has ${this.state.queue.length} item(s) \u2014 /queue drop <1-${Math.max(this.state.queue.length, 1)}>`);
814
+ }
815
+ return;
816
+ }
768
817
  this.notify({ queue: [...this.state.queue, value] });
769
818
  return;
770
819
  }
@@ -930,6 +979,18 @@ var Session = class {
930
979
  const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
931
980
  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"}` : "";
932
981
  this.push("status", `done${secs}${cost}${work}`);
982
+ if (checkpoint) {
983
+ try {
984
+ const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
985
+ const index = loadTokenIndex(this.execCwd());
986
+ const drift = scanDrift(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD", index);
987
+ if (drift.length > 0) {
988
+ this.push("status", `token drift: ${drift.length} hardcoded color(s)
989
+ ${driftSummary(drift)}`);
990
+ }
991
+ } catch {
992
+ }
993
+ }
933
994
  const before = this.state.totals.costUsd;
934
995
  this.notify({
935
996
  totals: {
@@ -950,6 +1011,7 @@ var Session = class {
950
1011
  sessionId: this.sessionId,
951
1012
  model: this.state.model,
952
1013
  lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
1014
+ totals: this.state.totals,
953
1015
  at: Date.now()
954
1016
  });
955
1017
  }
@@ -1004,6 +1066,7 @@ ${errors.slice(-5).join("\n")}`);
1004
1066
  this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
1005
1067
  } else if (probe) {
1006
1068
  this.clearProblems("runtime");
1069
+ this.perfPulse(probe.perf);
1007
1070
  const pct = await this.visualPulse(probe.pulsePath);
1008
1071
  if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
1009
1072
  this.autoReviewedThisAsk = true;
@@ -1012,7 +1075,7 @@ ${errors.slice(-5).join("\n")}`);
1012
1075
  const captureResult = await this.capture();
1013
1076
  if (captureResult) {
1014
1077
  await this.runTurn(
1015
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y),
1078
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop),
1016
1079
  "\u{1F441} auto-review rendered UI"
1017
1080
  );
1018
1081
  }
@@ -1069,6 +1132,25 @@ ${errors.slice(-5).join("\n")}`);
1069
1132
  this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
1070
1133
  }
1071
1134
  }
1135
+ /** Cross-turn load-performance deltas: the perf twin of the visual pulse. */
1136
+ perfPulse(perf) {
1137
+ if (!perf || perf.lcpMs === void 0 && perf.transferBytes === void 0) return;
1138
+ const previous = this.lastPerf;
1139
+ this.lastPerf = perf;
1140
+ const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)}MB`;
1141
+ const parts = [];
1142
+ if (perf.lcpMs !== void 0) {
1143
+ 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)` : "";
1144
+ parts.push(`LCP ${perf.lcpMs}ms${delta}`);
1145
+ }
1146
+ if (perf.cls !== void 0 && perf.cls > 0.05) parts.push(`CLS ${perf.cls}`);
1147
+ if (perf.transferBytes !== void 0) {
1148
+ 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))})` : "";
1149
+ parts.push(`${mb(perf.transferBytes)}${delta}`);
1150
+ }
1151
+ if (perf.requests !== void 0) parts.push(`${perf.requests} req`);
1152
+ if (parts.length > 0) this.push("status", `perf: ${parts.join(" \xB7 ")}`);
1153
+ }
1072
1154
  /**
1073
1155
  * Cross-turn visual drift check: compare this turn's pulse screenshot
1074
1156
  * with the previous one and report how much of the page changed.
@@ -1086,6 +1168,7 @@ ${errors.slice(-5).join("\n")}`);
1086
1168
  this.lastPulse = current;
1087
1169
  if (!previous) {
1088
1170
  this.push("status", "visual pulse: baseline captured");
1171
+ this.push("image", pulsePath);
1089
1172
  return null;
1090
1173
  }
1091
1174
  const pct = await comparePulse(previous, current);
@@ -1094,6 +1177,7 @@ ${errors.slice(-5).join("\n")}`);
1094
1177
  "status",
1095
1178
  pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
1096
1179
  );
1180
+ if (pct >= 0.5) this.push("image", pulsePath);
1097
1181
  return pct;
1098
1182
  }
1099
1183
  /** Screenshot the running app (and watch its runtime where CDP is available). */
@@ -1115,6 +1199,8 @@ ${errors.slice(-5).join("\n")}`);
1115
1199
  "status",
1116
1200
  `captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`
1117
1201
  );
1202
+ const desktop = result.shots.find((s) => s.name === "desktop") ?? result.shots[0];
1203
+ this.push("image", desktop.path);
1118
1204
  }
1119
1205
  if (result.runtime) {
1120
1206
  const summary = runtimeSummary(result.runtime);
@@ -1127,6 +1213,10 @@ ${errors.slice(-5).join("\n")}`);
1127
1213
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1128
1214
  }
1129
1215
  }
1216
+ if (result.slop && result.slop.length > 0) {
1217
+ this.push("status", `distinctiveness: ${result.slop.length} tell(s)
1218
+ ${result.slop.join("\n")}`);
1219
+ }
1130
1220
  if (result.a11y && result.a11y.length > 0) {
1131
1221
  this.push("error", `a11y: ${result.a11y.length} finding(s)
1132
1222
  ${result.a11y.slice(0, 5).join("\n")}`);
@@ -1150,7 +1240,7 @@ They are objective defects, not style preferences.`
1150
1240
  const arg = rest.join(" ").trim();
1151
1241
  switch (name) {
1152
1242
  case "engines": {
1153
- void import("./registry-S3PCXJVF.js").then(({ detectEngines: detectEngines2 }) => {
1243
+ void import("./registry-MIJ6LSAY.js").then(({ detectEngines: detectEngines2 }) => {
1154
1244
  const lines = detectEngines2().map(({ engine, path: binaryPath }) => {
1155
1245
  const mark = binaryPath ? "\u2713" : "\u2717";
1156
1246
  const traits = [engine.createParser ? "stream" : "text", engine.supportsResume ? "resume" : null].filter(Boolean).join(" \xB7 ");
@@ -1258,6 +1348,9 @@ They are objective defects, not style preferences.`
1258
1348
  case "tool":
1259
1349
  lines.push(`- \u2699 ${item.text}`);
1260
1350
  break;
1351
+ case "image":
1352
+ lines.push(`![screenshot](${item.text})`, "");
1353
+ break;
1261
1354
  case "thinking":
1262
1355
  break;
1263
1356
  default:
@@ -1270,6 +1363,13 @@ They are objective defects, not style preferences.`
1270
1363
  })();
1271
1364
  break;
1272
1365
  }
1366
+ case "btw":
1367
+ if (!arg) {
1368
+ this.push("status", "usage: /btw <question> \u2014 read-only side question, main thread untouched");
1369
+ } else {
1370
+ void this.btw(arg);
1371
+ }
1372
+ break;
1273
1373
  case "copy": {
1274
1374
  const last = this.state.items.findLast((i) => i.role === "assistant");
1275
1375
  if (!last) {
@@ -1330,7 +1430,7 @@ They are objective defects, not style preferences.`
1330
1430
  const result = await this.capture();
1331
1431
  if (result) {
1332
1432
  await this.runTurn(
1333
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y),
1433
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop),
1334
1434
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1335
1435
  );
1336
1436
  }
@@ -1380,7 +1480,11 @@ They are objective defects, not style preferences.`
1380
1480
  break;
1381
1481
  }
1382
1482
  this.sessionId = saved.sessionId;
1383
- this.notify({ engineId: saved.engine, model: saved.model ?? this.state.model });
1483
+ this.notify({
1484
+ engineId: saved.engine,
1485
+ model: saved.model ?? this.state.model,
1486
+ totals: saved.totals ?? this.state.totals
1487
+ });
1384
1488
  this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
1385
1489
  break;
1386
1490
  }
@@ -1510,7 +1614,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1510
1614
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1511
1615
  break;
1512
1616
  case "help": {
1513
- void import("./commands-TERCCWFM.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1617
+ void import("./commands-E7JIVJDP.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1514
1618
  break;
1515
1619
  }
1516
1620
  case "quit":
@@ -1772,9 +1876,23 @@ function Markdown({ text }) {
1772
1876
  }
1773
1877
 
1774
1878
  // src/tui/messages.tsx
1775
- import { Text as Text2 } from "ink";
1879
+ import { Box as Box2, Text as Text2 } from "ink";
1880
+ import Image from "ink-picture";
1776
1881
  import { useEffect, useState } from "react";
1882
+
1883
+ // src/tui/termImage.ts
1884
+ function supportsInlineImages(env = process.env) {
1885
+ if (env.TMUX) return false;
1886
+ if (env.TERM === "xterm-kitty" || env.KITTY_WINDOW_ID) return true;
1887
+ if (env.TERM === "xterm-ghostty" || env.GHOSTTY_RESOURCES_DIR) return true;
1888
+ if (env.TERM_PROGRAM === "WezTerm") return true;
1889
+ if (env.TERM_PROGRAM === "iTerm.app") return true;
1890
+ return false;
1891
+ }
1892
+
1893
+ // src/tui/messages.tsx
1777
1894
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1895
+ var INLINE_IMAGES = supportsInlineImages();
1778
1896
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1779
1897
  var PHRASES = ["working", "thinking", "squinting", "crafting", "still at it"];
1780
1898
  function WorkingLine({ startedAt }) {
@@ -1839,6 +1957,20 @@ function MessageLine({ message }) {
1839
1957
  "\u2717 ",
1840
1958
  message.text
1841
1959
  ] });
1960
+ case "image":
1961
+ if (!INLINE_IMAGES) {
1962
+ return /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, wrap: "wrap", children: [
1963
+ "\u25A3 ",
1964
+ message.text
1965
+ ] });
1966
+ }
1967
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1968
+ /* @__PURE__ */ jsx2(Image, { src: message.text, width: 48, height: 14, alt: "screenshot" }),
1969
+ /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, children: [
1970
+ "\u25A3 ",
1971
+ message.text
1972
+ ] })
1973
+ ] });
1842
1974
  }
1843
1975
  }
1844
1976
 
@@ -1998,20 +2130,23 @@ function App({
1998
2130
  });
1999
2131
  const devBadge = state.devState === "running" ? ` \xB7 ${state.devUrl ?? "dev running"}` : state.devState === "starting" ? " \xB7 dev starting\u2026" : state.devState === "crashed" ? " \xB7 dev crashed" : "";
2000
2132
  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: [
2133
+ return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsx3(InkPictureProvider, { children: /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingX: 1, children: [
2134
+ /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
2135
+ state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
2136
+ state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", marginTop: 1, children: [
2005
2137
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
2006
2138
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
2007
2139
  /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
2008
2140
  ] }),
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
2141
+ state.running && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
2142
+ state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2143
+ "\u22EF ",
2144
+ index + 1,
2145
+ ". ",
2146
+ queued,
2147
+ index === state.queue.length - 1 ? " (/queue drop <n> removes)" : ""
2013
2148
  ] }) }, 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: [
2149
+ 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
2150
  "/",
2016
2151
  command.name,
2017
2152
  command.args ? ` ${command.args}` : "",
@@ -2021,13 +2156,13 @@ function App({
2021
2156
  command.description
2022
2157
  ] })
2023
2158
  ] }, command.name)) }),
2024
- /* @__PURE__ */ jsx3(Box2, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
2159
+ /* @__PURE__ */ jsx3(Box3, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
2025
2160
  /* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
2026
2161
  line.text.slice(0, line.cursor),
2027
2162
  /* @__PURE__ */ jsx3(Text3, { inverse: true, children: line.text[line.cursor] ?? " " }),
2028
2163
  line.text.slice(line.cursor + 1)
2029
2164
  ] }) }),
2030
- /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2165
+ /* @__PURE__ */ jsx3(Box3, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
2031
2166
  /* @__PURE__ */ jsxs3(
2032
2167
  Text3,
2033
2168
  {
@@ -2057,7 +2192,7 @@ function App({
2057
2192
  " ",
2058
2193
  "\xB7 shift+tab mode \xB7 /help"
2059
2194
  ] }) })
2060
- ] }) });
2195
+ ] }) }) });
2061
2196
  }
2062
2197
 
2063
2198
  // src/cli/tui.tsx
@@ -2095,7 +2230,7 @@ function registerTui(program2) {
2095
2230
  }
2096
2231
 
2097
2232
  // src/cli.tsx
2098
- var VERSION = true ? "0.2.9" : "0.0.0-dev";
2233
+ var VERSION = true ? "0.3.1" : "0.0.0-dev";
2099
2234
  var program = new Command();
2100
2235
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2101
2236
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-RMNG2NTU.js";
6
+ } from "./chunk-IH4L2KR6.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-OTCH66M7.js";
14
+ import "./chunk-IMDRXXFU.js";
15
+ import "./chunk-KVYGPLWW.js";
16
+ import "./chunk-DAETGO2M.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
+ };
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/quality/tokens.ts
4
+ import { execFileSync } from "child_process";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ var HEX_RE = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
8
+ var VAR_DEF_RE = /(--[a-zA-Z0-9-_]+)\s*:\s*([^;]+);/g;
9
+ function parseHex(hex) {
10
+ const raw = hex.replace("#", "");
11
+ const full = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
12
+ if (full.length !== 6) return null;
13
+ const n = Number.parseInt(full, 16);
14
+ if (Number.isNaN(n)) return null;
15
+ return [n >> 16 & 255, n >> 8 & 255, n & 255];
16
+ }
17
+ function loadTokenIndex(cwd) {
18
+ const colors = /* @__PURE__ */ new Map();
19
+ const cssFiles = [];
20
+ const walk = (dir, depth) => {
21
+ if (depth > 4 || cssFiles.length > 30) return;
22
+ let entries;
23
+ try {
24
+ entries = fs.readdirSync(dir, { withFileTypes: true });
25
+ } catch {
26
+ return;
27
+ }
28
+ for (const entry of entries) {
29
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
30
+ const full = path.join(dir, entry.name);
31
+ if (entry.isDirectory()) walk(full, depth + 1);
32
+ else if (entry.name.endsWith(".css")) cssFiles.push(full);
33
+ }
34
+ };
35
+ walk(path.join(cwd, "src"), 0);
36
+ walk(cwd, 1);
37
+ for (const file of cssFiles.slice(0, 30)) {
38
+ let text;
39
+ try {
40
+ text = fs.readFileSync(file, "utf8");
41
+ } catch {
42
+ continue;
43
+ }
44
+ for (const match of text.matchAll(VAR_DEF_RE)) {
45
+ const value = match[2].trim();
46
+ const hex = value.match(/#[0-9a-fA-F]{3,6}\b/)?.[0];
47
+ const rgbFn = /rgb\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value);
48
+ let rgb = null;
49
+ if (hex) rgb = parseHex(hex);
50
+ else if (rgbFn) rgb = [Number(rgbFn[1]), Number(rgbFn[2]), Number(rgbFn[3])];
51
+ if (rgb) colors.set(match[1], { value, rgb });
52
+ }
53
+ }
54
+ return { colors };
55
+ }
56
+ var distance = (a, b) => Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2);
57
+ function scanDrift(cwd, source, index) {
58
+ if (index.colors.size === 0) return [];
59
+ let diff;
60
+ try {
61
+ diff = execFileSync("git", ["diff", "-U0", source, "--", "*.css", "*.tsx", "*.jsx", "*.ts", "*.html"], {
62
+ cwd,
63
+ encoding: "utf8",
64
+ stdio: ["ignore", "pipe", "pipe"]
65
+ });
66
+ } catch {
67
+ return [];
68
+ }
69
+ const findings = [];
70
+ let currentFile = "";
71
+ for (const line of diff.split("\n")) {
72
+ if (line.startsWith("+++ b/")) {
73
+ currentFile = line.slice(6);
74
+ continue;
75
+ }
76
+ if (!line.startsWith("+") || line.startsWith("+++")) continue;
77
+ if (/--[a-zA-Z0-9-_]+\s*:/.test(line)) continue;
78
+ for (const match of line.matchAll(HEX_RE)) {
79
+ const rgb = parseHex(match[0]);
80
+ if (!rgb) continue;
81
+ let best = null;
82
+ for (const [token, entry] of index.colors) {
83
+ const d = distance(rgb, entry.rgb);
84
+ if (!best || d < best.d) best = { token, d };
85
+ }
86
+ if (best) {
87
+ findings.push({ file: currentFile, literal: match[0], token: best.token, distance: Math.round(best.d) });
88
+ }
89
+ if (findings.length >= 10) return findings;
90
+ }
91
+ }
92
+ return findings;
93
+ }
94
+ function driftSummary(findings) {
95
+ return findings.map(
96
+ (f) => `${f.file}: ${f.literal} \u2192 use var(${f.token})${f.distance === 0 ? " (exact match)" : ` (closest, \u0394${f.distance})`}`
97
+ ).join("\n");
98
+ }
99
+ export {
100
+ driftSummary,
101
+ loadTokenIndex,
102
+ parseHex,
103
+ scanDrift
104
+ };
@@ -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.1",
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
- };