@aayambansal/squint 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -174,6 +174,9 @@ squint doctor --probe # run every engine end to end, verify auth act
174
174
  flagged from the live page, and on Next 16+ the framework's own `/_next/mcp`
175
175
  channel feeds structured errors straight into the fix loop. `/context` itemizes
176
176
  the injected-context bill per source, with staleness warnings.
177
+ - **The sentinel**: gate evasion (deleted tests, added skips, suppressed
178
+ diagnostics, weakened checks, locked-path touches) detected deterministically per
179
+ turn and reported to you — never sent back to the engine that did it.
177
180
  - **Detachable sessions**: `squint serve` owns the session on a unix socket;
178
181
  `squint attach` joins from any terminal (observer seats for teammates, driver
179
182
  seat inherited on detach). Crash-proof, ssh-friendly, no cloud.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  App
4
- } from "./chunk-M25IQ7C7.js";
4
+ } from "./chunk-JZMJGLIZ.js";
5
5
  import "./chunk-43NQNIJY.js";
6
- import "./chunk-7MOKOZOR.js";
6
+ import "./chunk-YGPVP4YB.js";
7
7
  import "./chunk-ARDV4XH6.js";
8
8
  import "./chunk-AUJJGMZG.js";
9
9
  import "./chunk-YHRAOBI2.js";
@@ -13,9 +13,9 @@ import "./chunk-VH7OOFQP.js";
13
13
  import "./chunk-K5QJMSJH.js";
14
14
  import "./chunk-WAJXATCO.js";
15
15
  import "./chunk-62JNF5M2.js";
16
- import "./chunk-ZSDCMYZR.js";
16
+ import "./chunk-7RFQWOQV.js";
17
17
  import "./chunk-7CAGWFAQ.js";
18
- import "./chunk-HC4E42SV.js";
18
+ import "./chunk-BGBG2MNY.js";
19
19
  import "./chunk-IMDRXXFU.js";
20
20
  import "./chunk-KVYGPLWW.js";
21
21
  export {
@@ -2,12 +2,14 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket,
5
+ pixelDiffAttributed,
5
6
  pixelDiffPct,
6
7
  runFlow
7
- } from "./chunk-HC4E42SV.js";
8
+ } from "./chunk-BGBG2MNY.js";
8
9
  export {
9
10
  cdpCapture,
10
11
  hasWebSocket,
12
+ pixelDiffAttributed,
11
13
  pixelDiffPct,
12
14
  runFlow
13
15
  };
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  cdpCapture,
7
7
  hasWebSocket
8
- } from "./chunk-HC4E42SV.js";
8
+ } from "./chunk-BGBG2MNY.js";
9
9
  import {
10
10
  findChrome,
11
11
  screenshot
@@ -75,7 +75,7 @@ async function captureViewports(cwd, url) {
75
75
  if (hasWebSocket()) {
76
76
  try {
77
77
  const checks = loadChecks(cwd);
78
- const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
78
+ const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true, checks);
79
79
  const errors2 = [];
80
80
  for (const route of routes.slice(1)) {
81
81
  try {
@@ -90,7 +90,7 @@ async function captureViewports(cwd, url) {
90
90
  errors2.push(`${route}: capture failed`);
91
91
  }
92
92
  }
93
- return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
93
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
94
94
  } catch {
95
95
  }
96
96
  }
@@ -163,9 +163,15 @@ async function probeRuntime(url, cwd) {
163
163
  async function comparePulse(previous, current) {
164
164
  const chrome = findChrome();
165
165
  if (!chrome || !hasWebSocket()) return null;
166
- const { pixelDiffPct } = await import("./cdp-PBQKXC6R.js");
166
+ const { pixelDiffPct } = await import("./cdp-2VVQOHWM.js");
167
167
  return pixelDiffPct(chrome, previous, current);
168
168
  }
169
+ async function comparePulseAttributed(previous, current, url) {
170
+ const chrome = findChrome();
171
+ if (!chrome || !hasWebSocket()) return null;
172
+ const { pixelDiffAttributed } = await import("./cdp-2VVQOHWM.js");
173
+ return pixelDiffAttributed(chrome, previous, current, url);
174
+ }
169
175
  function buildRuntimeFixPrompt(report) {
170
176
  return `The running app has runtime problems.${runtimeSection(report)}
171
177
 
@@ -191,6 +197,16 @@ ${narration.join("\n")}
191
197
 
192
198
  Judge this narration as an experience: does the reading order make sense? do names actually describe their targets? is anything announced as "(no accessible name)"? Fix real incoherence \u2014 this is how non-visual users meet the page.`;
193
199
  }
200
+ function jankSection(jank) {
201
+ if (!jank || jank.length === 0) return "";
202
+ return `
203
+
204
+ ## Jank attribution (main-thread frames \u226550ms)
205
+
206
+ ${jank.join("\n")}
207
+
208
+ Each line names the function behind a long animation frame observed during load and a scripted scroll. Fix the work (memoize, virtualize, move off the main thread) \u2014 do not remove the animation.`;
209
+ }
194
210
  function webmcpSection(webmcp) {
195
211
  if (!webmcp || webmcp.length === 0) return "";
196
212
  return `
@@ -241,13 +257,13 @@ ${findings.join("\n")}
241
257
 
242
258
  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.`;
243
259
  }
244
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp) {
260
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components, webmcp, jank) {
245
261
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
246
262
  return `Screenshots of the running app were just captured:
247
263
 
248
264
  ${list}
249
265
 
250
- 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)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}`;
266
+ 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)}${narrationSection(narration)}${phantomSection(phantoms)}${vtSection(viewTransitions)}${componentSection(components)}${webmcpSection(webmcp)}${jankSection(jank)}`;
251
267
  }
252
268
 
253
269
  export {
@@ -259,6 +275,7 @@ export {
259
275
  runtimeSummary,
260
276
  probeRuntime,
261
277
  comparePulse,
278
+ comparePulseAttributed,
262
279
  buildRuntimeFixPrompt,
263
280
  buildReviewPrompt
264
281
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  Session
4
- } from "./chunk-7MOKOZOR.js";
4
+ } from "./chunk-YGPVP4YB.js";
5
5
 
6
6
  // src/daemon/client.ts
7
7
  import net from "net";
@@ -189,6 +189,7 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
189
189
  const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
190
190
  await connection.send("Page.enable", {}, sessionId);
191
191
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
192
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
192
193
  await connection.send(
193
194
  "Emulation.setDeviceMetricsOverride",
194
195
  { width: 1280, height: 800, deviceScaleFactor: 1, mobile: false },
@@ -237,6 +238,145 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
237
238
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
238
239
  }
239
240
  }
241
+ async function pixelDiffAttributed(chromePath, pngA, pngB, url) {
242
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
243
+ let connection = null;
244
+ try {
245
+ connection = await CdpConnection.connect(wsUrl, 1e4);
246
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
247
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
248
+ await connection.send("Runtime.enable", {}, sessionId);
249
+ const expression = `(async () => {
250
+ const load = (src) => new Promise((resolve, reject) => {
251
+ const img = new Image();
252
+ img.onload = () => resolve(img);
253
+ img.onerror = () => reject(new Error('decode'));
254
+ img.src = src;
255
+ });
256
+ const a = await load('data:image/png;base64,${pngA.toString("base64")}');
257
+ const b = await load('data:image/png;base64,${pngB.toString("base64")}');
258
+ const w = Math.min(a.width, b.width), h = Math.min(a.height, b.height);
259
+ if (w === 0 || h === 0) return null;
260
+ const draw = (img) => {
261
+ const c = document.createElement('canvas');
262
+ c.width = w; c.height = h;
263
+ const ctx = c.getContext('2d');
264
+ ctx.drawImage(img, 0, 0);
265
+ return ctx.getImageData(0, 0, w, h).data;
266
+ };
267
+ const da = draw(a), db = draw(b);
268
+ const CELL = 32;
269
+ const gw = Math.ceil(w / CELL), gh = Math.ceil(h / CELL);
270
+ const marked = new Uint8Array(gw * gh);
271
+ let differ = 0, total = 0;
272
+ for (let y = 0; y < h; y += 2) {
273
+ for (let x = 0; x < w; x += 2) {
274
+ total++;
275
+ const i = (y * w + x) * 4;
276
+ if (Math.abs(da[i] - db[i]) > 8 || Math.abs(da[i + 1] - db[i + 1]) > 8 || Math.abs(da[i + 2] - db[i + 2]) > 8) {
277
+ differ++;
278
+ marked[Math.floor(y / CELL) * gw + Math.floor(x / CELL)] = 1;
279
+ }
280
+ }
281
+ }
282
+ // Flood-fill marked cells into region bounding boxes.
283
+ const seen = new Uint8Array(gw * gh);
284
+ const regions = [];
285
+ for (let cy = 0; cy < gh; cy++) {
286
+ for (let cx = 0; cx < gw; cx++) {
287
+ const start = cy * gw + cx;
288
+ if (!marked[start] || seen[start]) continue;
289
+ let minX = cx, maxX = cx, minY = cy, maxY = cy, cells = 0;
290
+ const stack = [start];
291
+ seen[start] = 1;
292
+ while (stack.length) {
293
+ const cell = stack.pop();
294
+ const x = cell % gw, y = Math.floor(cell / gw);
295
+ cells++;
296
+ if (x < minX) minX = x; if (x > maxX) maxX = x;
297
+ if (y < minY) minY = y; if (y > maxY) maxY = y;
298
+ for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
299
+ const nx = x + dx, ny = y + dy;
300
+ if (nx < 0 || ny < 0 || nx >= gw || ny >= gh) continue;
301
+ const n = ny * gw + nx;
302
+ if (marked[n] && !seen[n]) { seen[n] = 1; stack.push(n); }
303
+ }
304
+ }
305
+ regions.push({
306
+ x: minX * CELL, y: minY * CELL,
307
+ w: Math.min(w, (maxX + 1) * CELL) - minX * CELL,
308
+ h: Math.min(h, (maxY + 1) * CELL) - minY * CELL,
309
+ cells,
310
+ });
311
+ }
312
+ }
313
+ regions.sort((p, q) => q.cells - p.cells);
314
+ const sizePenalty = (a.width !== b.width || a.height !== b.height) ? 1 : 0;
315
+ return { pct: Math.min(100, (differ / total) * 100 + sizePenalty), regions: regions.slice(0, 5), w, h };
316
+ })()`;
317
+ const { result } = await connection.send(
318
+ "Runtime.evaluate",
319
+ { expression, awaitPromise: true, returnByValue: true },
320
+ sessionId
321
+ );
322
+ const value = result?.value;
323
+ if (!value || typeof value.pct !== "number") return null;
324
+ if (!url || value.regions.length === 0 || value.pct < 0.5) return { pct: value.pct, sentences: [] };
325
+ let sentences = [];
326
+ try {
327
+ const page = await connection.send("Target.createTarget", { url: "about:blank" });
328
+ const attach = await connection.send("Target.attachToTarget", { targetId: page.targetId, flatten: true });
329
+ const pageSession = attach.sessionId;
330
+ await connection.send("Runtime.enable", {}, pageSession);
331
+ await connection.send("Page.enable", {}, pageSession);
332
+ await connection.send(
333
+ "Emulation.setDeviceMetricsOverride",
334
+ { width: value.w, height: value.h, deviceScaleFactor: 1, mobile: false },
335
+ pageSession
336
+ );
337
+ await connection.send("Page.navigate", { url }, pageSession);
338
+ await new Promise((resolve) => setTimeout(resolve, 1200));
339
+ const hitTest = `((regions) => regions.map((r) => {
340
+ const el = document.elementsFromPoint(r.x + r.w / 2, r.y + r.h / 2)
341
+ .find((e) => e !== document.documentElement && e !== document.body);
342
+ const where = r.w + '\xD7' + r.h + ' region @ (' + r.x + ',' + r.y + ')';
343
+ if (!el) return where + ' changed';
344
+ let label = el.tagName.toLowerCase();
345
+ if (el.id) label += '#' + el.id;
346
+ else if (el.classList[0]) label += '.' + el.classList[0];
347
+ const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
348
+ let chain = '';
349
+ if (key) {
350
+ let fiber = el[key];
351
+ const names = [];
352
+ let hops = 0;
353
+ while (fiber && hops < 50 && names.length < 2) {
354
+ const t = fiber.type;
355
+ const n = typeof t === 'function' ? (t.displayName || t.name || '') : '';
356
+ if (n && !names.includes(n)) names.push(n);
357
+ fiber = fiber.return; hops++;
358
+ }
359
+ chain = names.join(' < ');
360
+ }
361
+ return '<' + label + '>' + (chain ? ' (' + chain + ')' : '') + ': ' + where + ' changed';
362
+ }))(${JSON.stringify(value.regions)})`;
363
+ const hit = await connection.send(
364
+ "Runtime.evaluate",
365
+ { expression: hitTest, returnByValue: true },
366
+ pageSession
367
+ );
368
+ if (Array.isArray(hit.result?.value)) sentences = hit.result.value.map(String);
369
+ } catch {
370
+ }
371
+ return { pct: value.pct, sentences };
372
+ } catch {
373
+ return null;
374
+ } finally {
375
+ connection?.close();
376
+ child.kill("SIGKILL");
377
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
378
+ }
379
+ }
240
380
  async function pixelDiffPct(chromePath, pngA, pngB) {
241
381
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
242
382
  let connection = null;
@@ -457,6 +597,40 @@ var WEBMCP_SHIM = `(() => {
457
597
  wrap(document.modelContext || (document.modelContext = {}));
458
598
  wrap(navigator.modelContext || (navigator.modelContext = {}));
459
599
  })()`;
600
+ var LOAF_SHIM = `(() => {
601
+ window.__squintLoaf = [];
602
+ try {
603
+ new PerformanceObserver((list) => {
604
+ for (const e of list.getEntries()) {
605
+ if (e.duration < 50) continue;
606
+ const s = (e.scripts && e.scripts[0]) || {};
607
+ window.__squintLoaf.push({
608
+ duration: Math.round(e.duration),
609
+ fn: s.sourceFunctionName || '',
610
+ url: (s.sourceURL || s.name || '').split('/').pop() || '',
611
+ invoker: s.invoker || '',
612
+ });
613
+ }
614
+ }).observe({ type: 'long-animation-frame', buffered: true });
615
+ } catch {}
616
+ })()`;
617
+ var SCRIPTED_SCROLL = `(async () => {
618
+ const half = Math.max(0, (document.body.scrollHeight - innerHeight) / 2);
619
+ window.scrollTo({ top: half, behavior: 'smooth' });
620
+ await new Promise((r) => setTimeout(r, 400));
621
+ window.scrollTo({ top: 0, behavior: 'smooth' });
622
+ await new Promise((r) => setTimeout(r, 300));
623
+ const seen = new Map();
624
+ for (const e of window.__squintLoaf || []) {
625
+ const key = e.fn + '@' + e.url + '@' + e.invoker;
626
+ const prev = seen.get(key);
627
+ if (!prev || e.duration > prev.duration) seen.set(key, e);
628
+ }
629
+ return [...seen.values()]
630
+ .sort((a, b) => b.duration - a.duration)
631
+ .slice(0, 6)
632
+ .map((e) => e.duration + 'ms frame \u2014 ' + (e.fn || e.invoker || 'script') + (e.url ? ' @ ' + e.url : ''));
633
+ })()`;
460
634
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false, checks = []) {
461
635
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
462
636
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -470,6 +644,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
470
644
  let components = [];
471
645
  const checkFailures = [];
472
646
  let webmcp = [];
647
+ let jank = [];
473
648
  const requests = /* @__PURE__ */ new Map();
474
649
  let connection = null;
475
650
  try {
@@ -509,6 +684,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
509
684
  await connection.send("Network.enable", {}, sessionId);
510
685
  await connection.send("Page.enable", {}, sessionId);
511
686
  await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBMCP_SHIM }, sessionId).catch(() => null);
687
+ await connection.send("Page.addScriptToEvaluateOnNewDocument", { source: LOAF_SHIM }, sessionId).catch(() => null);
512
688
  await connection.send("Page.navigate", { url }, sessionId);
513
689
  const deadline = Date.now() + 12e3;
514
690
  while (!loaded && Date.now() < deadline) {
@@ -524,6 +700,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
524
700
  if (result?.value && typeof result.value === "object") perf = result.value;
525
701
  } catch {
526
702
  }
703
+ try {
704
+ const { result } = await connection.send(
705
+ "Runtime.evaluate",
706
+ { expression: SCRIPTED_SCROLL, returnByValue: true, awaitPromise: true },
707
+ sessionId
708
+ );
709
+ if (Array.isArray(result?.value)) jank = result.value.map(String);
710
+ } catch {
711
+ }
527
712
  try {
528
713
  const { result } = await connection.send(
529
714
  "Runtime.evaluate",
@@ -659,12 +844,13 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
659
844
  child.kill("SIGKILL");
660
845
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
661
846
  }
662
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp };
847
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components, checkFailures, webmcp, jank };
663
848
  }
664
849
 
665
850
  export {
666
851
  hasWebSocket,
667
852
  runFlow,
853
+ pixelDiffAttributed,
668
854
  pixelDiffPct,
669
855
  cdpCapture
670
856
  };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-43NQNIJY.js";
5
5
  import {
6
6
  Session
7
- } from "./chunk-7MOKOZOR.js";
7
+ } from "./chunk-YGPVP4YB.js";
8
8
 
9
9
  // src/tui/App.tsx
10
10
  import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
@@ -46,10 +46,10 @@ import {
46
46
  buildReviewPrompt,
47
47
  buildRuntimeFixPrompt,
48
48
  captureViewports,
49
- comparePulse,
49
+ comparePulseAttributed,
50
50
  probeRuntime,
51
51
  runtimeSummary
52
- } from "./chunk-ZSDCMYZR.js";
52
+ } from "./chunk-7RFQWOQV.js";
53
53
  import {
54
54
  clearState,
55
55
  loadState,
@@ -267,7 +267,7 @@ ${question}`,
267
267
  return;
268
268
  }
269
269
  await this.runTurn(
270
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
270
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
271
271
  `\u{1F441} polish round ${round}/${rounds}`
272
272
  );
273
273
  }
@@ -509,6 +509,17 @@ ${driftSummary(drift)}`);
509
509
  }
510
510
  } catch {
511
511
  }
512
+ try {
513
+ const { scanEvasion, sentinelSummary } = await import("./sentinel-42NZRTTS.js");
514
+ const evasions = scanEvasion(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
515
+ if (evasions.length > 0) {
516
+ this.push("error", `\u26A0 sentinel: ${evasions.length} gate-evasion pattern(s) this turn
517
+ ${sentinelSummary(evasions)}
518
+ Review before trusting green \u2014 /undo rolls the turn back.`);
519
+ runHook(this.opts.cwd, "on-sentinel", { count: String(evasions.length), summary: sentinelSummary(evasions) });
520
+ }
521
+ } catch {
522
+ }
512
523
  try {
513
524
  const { rulePackSummary, scanRulePacks } = await import("./rulepacks-JOA675OJ.js");
514
525
  const findings = scanRulePacks(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD");
@@ -647,7 +658,7 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
647
658
  const captureResult = await this.capture();
648
659
  if (captureResult) {
649
660
  await this.runTurn(
650
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp),
661
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components, captureResult.webmcp, captureResult.jank),
651
662
  "\u{1F441} auto-review rendered UI"
652
663
  );
653
664
  }
@@ -744,11 +755,13 @@ ${probe.checkFailures.slice(0, 5).join("\n")}`);
744
755
  this.push("image", pulsePath);
745
756
  return null;
746
757
  }
747
- const pct = await comparePulse(previous, current);
748
- if (pct === null) return null;
758
+ const diff = await comparePulseAttributed(previous, current, this.state.devUrl ?? void 0);
759
+ if (diff === null) return null;
760
+ const pct = diff.pct;
749
761
  this.push(
750
762
  "status",
751
- pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
763
+ pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn${diff.sentences.length > 0 ? `
764
+ ${diff.sentences.map((s) => ` ${s}`).join("\n")}` : ""}`
752
765
  );
753
766
  runHook(this.opts.cwd, "on-pulse-diff", { pct: pct.toFixed(1) });
754
767
  if (pct >= 0.5) this.push("image", pulsePath);
@@ -1070,8 +1083,8 @@ They are objective defects, not style preferences.`
1070
1083
  this.push("error", "no Chrome/Chromium found for flows");
1071
1084
  return;
1072
1085
  }
1073
- const { runFlow } = await import("./cdp-PBQKXC6R.js");
1074
- const { previewDir } = await import("./preview-WT34NVCH.js");
1086
+ const { runFlow } = await import("./cdp-2VVQOHWM.js");
1087
+ const { previewDir } = await import("./preview-FBNVDQIV.js");
1075
1088
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1076
1089
  this.notify({ running: true, runStartedAt: Date.now() });
1077
1090
  const failures = [];
@@ -1209,7 +1222,7 @@ They are objective defects, not style preferences.`
1209
1222
  const result = await this.capture();
1210
1223
  if (result) {
1211
1224
  await this.runTurn(
1212
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp),
1225
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components, result.webmcp, result.jank),
1213
1226
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1214
1227
  );
1215
1228
  }
package/dist/cli.js CHANGED
@@ -3,12 +3,12 @@ import {
3
3
  connectDaemon,
4
4
  socketPath,
5
5
  startDaemon
6
- } from "./chunk-NX7XQY2X.js";
6
+ } from "./chunk-ATNTD3V5.js";
7
7
  import {
8
8
  App
9
- } from "./chunk-M25IQ7C7.js";
9
+ } from "./chunk-JZMJGLIZ.js";
10
10
  import "./chunk-43NQNIJY.js";
11
- import "./chunk-7MOKOZOR.js";
11
+ import "./chunk-YGPVP4YB.js";
12
12
  import "./chunk-ARDV4XH6.js";
13
13
  import "./chunk-AUJJGMZG.js";
14
14
  import "./chunk-YHRAOBI2.js";
@@ -22,9 +22,9 @@ import {
22
22
  composePrompt
23
23
  } from "./chunk-WAJXATCO.js";
24
24
  import "./chunk-62JNF5M2.js";
25
- import "./chunk-ZSDCMYZR.js";
25
+ import "./chunk-7RFQWOQV.js";
26
26
  import "./chunk-7CAGWFAQ.js";
27
- import "./chunk-HC4E42SV.js";
27
+ import "./chunk-BGBG2MNY.js";
28
28
  import "./chunk-IMDRXXFU.js";
29
29
  import {
30
30
  detectEngines,
@@ -189,11 +189,11 @@ function registerDaemon(program2) {
189
189
  const cwd = process.cwd();
190
190
  if (!opts.plain) {
191
191
  try {
192
- const { RemoteSession } = await import("./remote-6O6FPTUF.js");
192
+ const { RemoteSession } = await import("./remote-OH2P4S2H.js");
193
193
  const remote = await RemoteSession.connect(cwd);
194
194
  const config = loadConfig(defaultPaths(cwd));
195
195
  const { render: render2 } = await import("ink");
196
- const { App: App2 } = await import("./App-HWMVSRMF.js");
196
+ const { App: App2 } = await import("./App-766KSGON.js");
197
197
  const React = await import("react");
198
198
  render2(
199
199
  React.createElement(App2, {
@@ -310,7 +310,7 @@ function registerEnv(program2) {
310
310
  }
311
311
  }
312
312
  const { findChrome } = await import("./chrome-SBV3H77F.js");
313
- const { hasWebSocket } = await import("./cdp-PBQKXC6R.js");
313
+ const { hasWebSocket } = await import("./cdp-2VVQOHWM.js");
314
314
  const chrome = findChrome();
315
315
  console.log(
316
316
  chrome ? `${pc3.green("\u2713")} Chrome ${pc3.dim(chrome)}` : `${pc3.yellow("\u25CB")} Chrome ${pc3.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -533,7 +533,7 @@ function registerQuality(program2) {
533
533
  if (results.some((r) => !r.ok)) process.exitCode = 1;
534
534
  });
535
535
  program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
536
- const { captureViewports } = await import("./preview-WT34NVCH.js");
536
+ const { captureViewports } = await import("./preview-FBNVDQIV.js");
537
537
  const result = await captureViewports(process.cwd(), url);
538
538
  if (!result) {
539
539
  console.error(pc5.red("\u2717 no Chrome/Chromium found"));
@@ -720,7 +720,7 @@ function registerTui(program2) {
720
720
  }
721
721
 
722
722
  // src/cli.tsx
723
- var VERSION = true ? "0.4.7" : "0.0.0-dev";
723
+ var VERSION = true ? "0.4.8" : "0.0.0-dev";
724
724
  var program = new Command();
725
725
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
726
726
  registerRun(program);
@@ -5,14 +5,15 @@ import {
5
5
  buildRuntimeFixPrompt,
6
6
  captureViewports,
7
7
  comparePulse,
8
+ comparePulseAttributed,
8
9
  loadRoutes,
9
10
  previewDir,
10
11
  probeRuntime,
11
12
  routeShotName,
12
13
  runtimeSummary
13
- } from "./chunk-ZSDCMYZR.js";
14
+ } from "./chunk-7RFQWOQV.js";
14
15
  import "./chunk-7CAGWFAQ.js";
15
- import "./chunk-HC4E42SV.js";
16
+ import "./chunk-BGBG2MNY.js";
16
17
  import "./chunk-IMDRXXFU.js";
17
18
  import "./chunk-KVYGPLWW.js";
18
19
  export {
@@ -21,6 +22,7 @@ export {
21
22
  buildRuntimeFixPrompt,
22
23
  captureViewports,
23
24
  comparePulse,
25
+ comparePulseAttributed,
24
26
  loadRoutes,
25
27
  previewDir,
26
28
  probeRuntime,
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  connectDaemon,
4
4
  socketPath
5
- } from "./chunk-NX7XQY2X.js";
6
- import "./chunk-7MOKOZOR.js";
5
+ } from "./chunk-ATNTD3V5.js";
6
+ import "./chunk-YGPVP4YB.js";
7
7
  import "./chunk-ARDV4XH6.js";
8
8
  import "./chunk-AUJJGMZG.js";
9
9
  import "./chunk-YHRAOBI2.js";
@@ -13,9 +13,9 @@ import "./chunk-VH7OOFQP.js";
13
13
  import "./chunk-K5QJMSJH.js";
14
14
  import "./chunk-WAJXATCO.js";
15
15
  import "./chunk-62JNF5M2.js";
16
- import "./chunk-ZSDCMYZR.js";
16
+ import "./chunk-7RFQWOQV.js";
17
17
  import "./chunk-7CAGWFAQ.js";
18
- import "./chunk-HC4E42SV.js";
18
+ import "./chunk-BGBG2MNY.js";
19
19
  import "./chunk-IMDRXXFU.js";
20
20
  import "./chunk-KVYGPLWW.js";
21
21
 
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.7",
3
+ "version": "0.4.8",
4
4
  "description": "Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and other coding agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",