@aayambansal/squint 0.4.4 → 0.4.5

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,8 @@ 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
+ - **Visual approval**: engines ask before contested changes — the request renders
178
+ with its screenshot, `/yes` / `/no` answer it, the ledger remembers.
177
179
  - **The design ledger**: `/decide` (plus chosen variants, rollbacks, accepted
178
180
  sandboxes) appends to a committed `.squint/design-log.jsonl`; recent decisions ride
179
181
  into every ask so they stop getting silently undone between sessions.
@@ -4,7 +4,7 @@ import {
4
4
  hasWebSocket,
5
5
  pixelDiffPct,
6
6
  runFlow
7
- } from "./chunk-JFFNQIBU.js";
7
+ } from "./chunk-RQHOE5MV.js";
8
8
  export {
9
9
  cdpCapture,
10
10
  hasWebSocket,
@@ -28,6 +28,8 @@ var COMMANDS = [
28
28
  { name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
29
29
  { name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
30
30
  { name: "context", group: "session", description: "what squint injects per ask, token-costed, with staleness warnings" },
31
+ { name: "yes", args: "[note]", group: "build", description: "approve the engine's pending visual-approval request" },
32
+ { name: "no", args: "[note]", group: "build", description: "reject the engine's pending visual-approval request" },
31
33
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
32
34
  { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
33
35
  { name: "help", group: "session", description: "list commands" },
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  cdpCapture,
11
11
  hasWebSocket
12
- } from "./chunk-JFFNQIBU.js";
12
+ } from "./chunk-RQHOE5MV.js";
13
13
 
14
14
  // src/preview/preview.ts
15
15
  import fs from "fs";
@@ -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, slop, narration, phantoms, viewTransitions } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
50
+ const { report, shots: shots2, a11y, slop, narration, phantoms, viewTransitions, components } = 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, slop, narration, phantoms, viewTransitions };
65
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms, viewTransitions, components };
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-C6MOKVEN.js");
136
+ const { pixelDiffPct } = await import("./cdp-Q4H6ZHPT.js");
137
137
  return pixelDiffPct(chrome, previous, current);
138
138
  }
139
139
  function buildRuntimeFixPrompt(report) {
@@ -161,6 +161,16 @@ ${narration.join("\n")}
161
161
 
162
162
  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.`;
163
163
  }
164
+ function componentSection(components) {
165
+ if (!components || components.length === 0) return "";
166
+ return `
167
+
168
+ ## Component map (from React fibers)
169
+
170
+ ${components.join("\n")}
171
+
172
+ Use these owner chains to name the component (and file) an issue lives in instead of describing regions.`;
173
+ }
164
174
  function vtSection(viewTransitions) {
165
175
  if (!viewTransitions || viewTransitions.length === 0) return "";
166
176
  return `
@@ -191,13 +201,13 @@ ${findings.join("\n")}
191
201
 
192
202
  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.`;
193
203
  }
194
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions) {
204
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms, viewTransitions, components) {
195
205
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
196
206
  return `Screenshots of the running app were just captured:
197
207
 
198
208
  ${list}
199
209
 
200
- 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)}`;
210
+ 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)}`;
201
211
  }
202
212
 
203
213
  export {
@@ -400,6 +400,43 @@ var VT_AUDIT = `(() => {
400
400
  }
401
401
  return findings;
402
402
  })()`;
403
+ var FIBER_AUDIT = `(() => {
404
+ const fiberKey = (el) => Object.keys(el).find((k) => k.startsWith('__reactFiber$'));
405
+ const all = document.querySelectorAll('*');
406
+ let reactSeen = false;
407
+ for (let i = 0; i < all.length && i < 300; i++) {
408
+ if (fiberKey(all[i])) { reactSeen = true; break; }
409
+ }
410
+ if (!reactSeen) return [];
411
+ const nameOf = (t) => {
412
+ if (typeof t === 'function') return t.displayName || t.name || '';
413
+ if (t && typeof t === 'object') return t.displayName || (t.render && (t.render.displayName || t.render.name)) || '';
414
+ return '';
415
+ };
416
+ const chainFor = (el) => {
417
+ const key = fiberKey(el);
418
+ if (!key) return null;
419
+ let fiber = el[key];
420
+ const names = [];
421
+ let hops = 0;
422
+ while (fiber && hops < 50 && names.length < 3) {
423
+ const n = nameOf(fiber.type);
424
+ if (n && !names.includes(n)) names.push(n);
425
+ fiber = fiber.return;
426
+ hops++;
427
+ }
428
+ return names.length > 0 ? names.join(' < ') : null;
429
+ };
430
+ const out = [];
431
+ for (const sel of ['header', 'nav', 'main', 'footer', 'h1', 'h2', 'form', 'aside', '[role="dialog"]', 'table']) {
432
+ const el = document.querySelector(sel);
433
+ if (!el) continue;
434
+ const chain = chainFor(el);
435
+ if (chain) out.push(sel + ' \u2014 ' + chain);
436
+ if (out.length >= 10) break;
437
+ }
438
+ return out;
439
+ })()`;
403
440
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
404
441
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
405
442
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -410,6 +447,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
410
447
  let narration = [];
411
448
  let phantoms = [];
412
449
  let viewTransitions = [];
450
+ let components = [];
413
451
  const requests = /* @__PURE__ */ new Map();
414
452
  let connection = null;
415
453
  try {
@@ -500,6 +538,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
500
538
  if (Array.isArray(result?.value)) viewTransitions = result.value.map(String);
501
539
  } catch {
502
540
  }
541
+ try {
542
+ const { result } = await connection.send(
543
+ "Runtime.evaluate",
544
+ { expression: FIBER_AUDIT, returnByValue: true },
545
+ sessionId
546
+ );
547
+ if (Array.isArray(result?.value)) components = result.value.map(String);
548
+ } catch {
549
+ }
503
550
  try {
504
551
  await connection.send("Accessibility.enable", {}, sessionId);
505
552
  const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
@@ -564,7 +611,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
564
611
  child.kill("SIGKILL");
565
612
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
566
613
  }
567
- return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions };
614
+ return { report, shots, a11y, slop, perf, narration, phantoms, viewTransitions, components };
568
615
  }
569
616
 
570
617
  export {
@@ -156,6 +156,11 @@ ${locks.map((l) => `- ${l}`).join("\n")}
156
156
  If the task appears to require changing them, stop and explain instead.`
157
157
  );
158
158
  }
159
+ parts.push(
160
+ `## Requesting visual approval
161
+
162
+ For a visual decision you should not make alone (a redesign direction, removing something deliberate, reversing a design decision on record): write .squint/approval-request.json containing {"summary": "<one line>", "screenshot": "<path, optional>"} and end your turn immediately without making the change. The user's verdict arrives as the next message.`
163
+ );
159
164
  const matched = matchSkills(loadSkills(cwd), ask);
160
165
  for (const skill of matched) {
161
166
  parts.push(`## Project notes: ${skill.name}
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-O7NWGLS3.js";
4
+ } from "./chunk-43NQNIJY.js";
5
5
  import {
6
6
  applySandbox,
7
7
  discardSandbox,
@@ -42,7 +42,7 @@ import {
42
42
  comparePulse,
43
43
  probeRuntime,
44
44
  runtimeSummary
45
- } from "./chunk-ZUEGDTAM.js";
45
+ } from "./chunk-CV5WVKHU.js";
46
46
  import {
47
47
  clearState,
48
48
  loadState,
@@ -58,11 +58,11 @@ import {
58
58
  detectEngines,
59
59
  getEngine
60
60
  } from "./chunk-KVYGPLWW.js";
61
- import "./chunk-JFFNQIBU.js";
61
+ import "./chunk-RQHOE5MV.js";
62
62
  import {
63
63
  appendDecision,
64
64
  enrich
65
- } from "./chunk-YT6K2YIS.js";
65
+ } from "./chunk-ZLEP2TWF.js";
66
66
  import "./chunk-K5QJMSJH.js";
67
67
  import {
68
68
  composePrompt
@@ -240,7 +240,7 @@ function registerEnv(program2) {
240
240
  }
241
241
  }
242
242
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
243
- const { hasWebSocket } = await import("./cdp-C6MOKVEN.js");
243
+ const { hasWebSocket } = await import("./cdp-Q4H6ZHPT.js");
244
244
  const chrome = findChrome2();
245
245
  console.log(
246
246
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -264,7 +264,7 @@ import pc3 from "picocolors";
264
264
  function registerProject(program2) {
265
265
  const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
266
266
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
267
- const { loadRules, loadSkills } = await import("./skills-GTTF4PNU.js");
267
+ const { loadRules, loadSkills } = await import("./skills-6NZP67QT.js");
268
268
  const cwd = process.cwd();
269
269
  const rules = loadRules(cwd);
270
270
  console.log(
@@ -280,22 +280,22 @@ function registerProject(program2) {
280
280
  }
281
281
  });
282
282
  skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
283
- const fs3 = await import("fs");
283
+ const fs4 = await import("fs");
284
284
  const nodePath = await import("path");
285
285
  const cwd = process.cwd();
286
286
  const skillsDir = nodePath.join(cwd, ".squint", "skills");
287
- fs3.mkdirSync(skillsDir, { recursive: true });
287
+ fs4.mkdirSync(skillsDir, { recursive: true });
288
288
  const rules = nodePath.join(cwd, ".squint", "rules.md");
289
- if (!fs3.existsSync(rules)) {
290
- fs3.writeFileSync(
289
+ if (!fs4.existsSync(rules)) {
290
+ fs4.writeFileSync(
291
291
  rules,
292
292
  "# Project rules\n\nThese ride along on every squint ask. Keep them short \u2014 cut anything that would not cause a mistake if removed.\n"
293
293
  );
294
294
  console.log(pc3.green("\u2713 .squint/rules.md"));
295
295
  }
296
296
  const example = nodePath.join(skillsDir, "example.md");
297
- if (!fs3.existsSync(example)) {
298
- fs3.writeFileSync(
297
+ if (!fs4.existsSync(example)) {
298
+ fs4.writeFileSync(
299
299
  example,
300
300
  "---\ntriggers: example, sample\n---\n\nThis note is injected only when an ask mentions one of the triggers above.\nDocument the parts of this repo an agent would otherwise rediscover every time:\nwhere state lives, which helpers to reuse, what not to touch.\n"
301
301
  );
@@ -304,7 +304,7 @@ function registerProject(program2) {
304
304
  console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
305
305
  });
306
306
  program2.command("brief").description("Set a committed design direction for this project (.squint/brief.md)").argument("[family]", "aesthetic family id (omit to list)").option("--force", "overwrite an existing project brief").action(async (familyId, options) => {
307
- const fs3 = await import("fs");
307
+ const fs4 = await import("fs");
308
308
  const nodePath = await import("path");
309
309
  const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-3ARYRBMH.js");
310
310
  if (!familyId) {
@@ -322,13 +322,13 @@ function registerProject(program2) {
322
322
  return;
323
323
  }
324
324
  const target = nodePath.join(process.cwd(), ".squint", "brief.md");
325
- if (fs3.existsSync(target) && !options.force) {
325
+ if (fs4.existsSync(target) && !options.force) {
326
326
  console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
327
327
  process.exitCode = 1;
328
328
  return;
329
329
  }
330
- fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
331
- fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
330
+ fs4.mkdirSync(nodePath.dirname(target), { recursive: true });
331
+ fs4.writeFileSync(target, renderFamilyBrief(family) + "\n");
332
332
  console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
333
333
  console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
334
334
  });
@@ -463,7 +463,7 @@ function registerQuality(program2) {
463
463
  if (results.some((r) => !r.ok)) process.exitCode = 1;
464
464
  });
465
465
  program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
466
- const { captureViewports: captureViewports2 } = await import("./preview-Q43LOK6P.js");
466
+ const { captureViewports: captureViewports2 } = await import("./preview-ZUCVWGZG.js");
467
467
  const result = await captureViewports2(process.cwd(), url);
468
468
  if (!result) {
469
469
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -581,21 +581,21 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
581
581
  console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
582
582
  });
583
583
  program2.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
584
- const fs3 = await import("fs");
584
+ const fs4 = await import("fs");
585
585
  const nodePath = await import("path");
586
586
  const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
587
587
  const cwd = process.cwd();
588
588
  const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
589
- fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
589
+ fs4.writeFileSync(taggerPath, TAGGER_SOURCE);
590
590
  console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
591
- const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
591
+ const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs4.existsSync(candidate));
592
592
  if (!configPath) {
593
593
  console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
594
594
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
595
595
  plugins: [squintTagger(), \u2026]`));
596
596
  return;
597
597
  }
598
- const source = fs3.readFileSync(configPath, "utf8");
598
+ const source = fs4.readFileSync(configPath, "utf8");
599
599
  const patched = patchViteConfig(source);
600
600
  if (patched === "already") {
601
601
  console.log(pc6.dim("vite config already wired"));
@@ -604,7 +604,7 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
604
604
  console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
605
605
  plugins: [squintTagger(), \u2026]`));
606
606
  } else {
607
- fs3.writeFileSync(configPath, patched);
607
+ fs4.writeFileSync(configPath, patched);
608
608
  console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
609
609
  }
610
610
  console.log(
@@ -623,6 +623,7 @@ import path4 from "path";
623
623
  import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
624
624
 
625
625
  // src/session/engine.ts
626
+ import fs3 from "fs";
626
627
  import path3 from "path";
627
628
 
628
629
  // src/session/hooks.ts
@@ -711,6 +712,7 @@ var Session = class {
711
712
  checkpoints = [];
712
713
  fixAttempts = 0;
713
714
  reviewTipShown = false;
715
+ pendingApproval = null;
714
716
  lastPulse = null;
715
717
  lastPerf = null;
716
718
  autoReviewedThisAsk = false;
@@ -825,7 +827,7 @@ ${question}`,
825
827
  return;
826
828
  }
827
829
  await this.runTurn(
828
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
830
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components),
829
831
  `\u{1F441} polish round ${round}/${rounds}`
830
832
  );
831
833
  }
@@ -1041,6 +1043,21 @@ ${question}`,
1041
1043
  const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
1042
1044
  const work = stat ? ` \xB7 ${stat}` : this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
1043
1045
  this.push("status", `done${secs}${cost}${work}`);
1046
+ try {
1047
+ const reqPath = path3.join(this.opts.cwd, ".squint", "approval-request.json");
1048
+ if (fs3.existsSync(reqPath)) {
1049
+ const req = JSON.parse(fs3.readFileSync(reqPath, "utf8"));
1050
+ fs3.rmSync(reqPath, { force: true });
1051
+ if (typeof req?.summary === "string" && req.summary.length > 0) {
1052
+ this.pendingApproval = req.summary;
1053
+ const shot = typeof req.screenshot === "string" ? path3.resolve(this.execCwd(), req.screenshot) : null;
1054
+ if (shot && fs3.existsSync(shot)) this.push("image", shot);
1055
+ this.push("status", `\u23F8 approval requested: ${req.summary}
1056
+ /yes approves \xB7 /no rejects \xB7 or type feedback`);
1057
+ }
1058
+ }
1059
+ } catch {
1060
+ }
1044
1061
  if (checkpoint) {
1045
1062
  try {
1046
1063
  const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
@@ -1175,7 +1192,7 @@ ${errors.slice(-5).join("\n")}`);
1175
1192
  const captureResult = await this.capture();
1176
1193
  if (captureResult) {
1177
1194
  await this.runTurn(
1178
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions),
1195
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms, captureResult.viewTransitions, captureResult.components),
1179
1196
  "\u{1F441} auto-review rendered UI"
1180
1197
  );
1181
1198
  }
@@ -1463,13 +1480,32 @@ They are objective defects, not style preferences.`
1463
1480
  break;
1464
1481
  }
1465
1482
  case "context": {
1466
- import("./contextDoctor-M3R3PICP.js").then(({ contextReport, formatContextReport }) => {
1483
+ import("./contextDoctor-U3YTDFVG.js").then(({ contextReport, formatContextReport }) => {
1467
1484
  this.push("status", formatContextReport(contextReport(this.execCwd())));
1468
1485
  }).catch((error) => {
1469
1486
  this.push("status", `context report failed: ${error instanceof Error ? error.message : String(error)}`);
1470
1487
  });
1471
1488
  break;
1472
1489
  }
1490
+ case "yes":
1491
+ case "no": {
1492
+ if (!this.pendingApproval) {
1493
+ this.push("status", `nothing awaiting approval \u2014 /${name} answers an engine's approval request`);
1494
+ break;
1495
+ }
1496
+ const summary = this.pendingApproval;
1497
+ this.pendingApproval = null;
1498
+ const approved = name === "yes";
1499
+ appendDecision(this.opts.cwd, {
1500
+ decision: `${approved ? "approved" : "rejected"}: ${summary}${arg ? ` \u2014 ${arg}` : ""}`,
1501
+ source: "approval"
1502
+ });
1503
+ void this.runTurn(
1504
+ approved ? `Approved: "${summary}". Proceed.${arg ? ` Note from the user: ${arg}` : ""}` : `Rejected: "${summary}". Do not proceed with it.${arg ? ` The user says: ${arg}` : " Stop and await direction."}`,
1505
+ approved ? `\u2713 approved${arg ? ` \u2014 ${arg}` : ""}` : `\u2717 rejected${arg ? ` \u2014 ${arg}` : ""}`
1506
+ );
1507
+ break;
1508
+ }
1473
1509
  case "decide": {
1474
1510
  if (!arg) {
1475
1511
  this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
@@ -1500,13 +1536,13 @@ They are objective defects, not style preferences.`
1500
1536
  if (matches.length < 8) {
1501
1537
  void (async () => {
1502
1538
  try {
1503
- const fs3 = await import("fs");
1539
+ const fs4 = await import("fs");
1504
1540
  const path5 = await import("path");
1505
1541
  const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1506
- const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
1542
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".md")).sort().reverse().slice(0, 20);
1507
1543
  for (const file of files) {
1508
1544
  if (matches.length >= 8) break;
1509
- const text = fs3.readFileSync(path5.join(dir, file), "utf8");
1545
+ const text = fs4.readFileSync(path5.join(dir, file), "utf8");
1510
1546
  for (const line of text.split("\n")) {
1511
1547
  if (line.toLowerCase().includes(needle)) {
1512
1548
  matches.push(`[${file.replace(/\.md$/, "")}] ${line.trim().slice(0, 90)}`);
@@ -1529,10 +1565,10 @@ They are objective defects, not style preferences.`
1529
1565
  }
1530
1566
  case "save": {
1531
1567
  void (async () => {
1532
- const fs3 = await import("fs");
1568
+ const fs4 = await import("fs");
1533
1569
  const path5 = await import("path");
1534
1570
  const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
1535
- fs3.mkdirSync(dir, { recursive: true });
1571
+ fs4.mkdirSync(dir, { recursive: true });
1536
1572
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
1537
1573
  const file = path5.join(dir, `${stamp}.md`);
1538
1574
  const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
@@ -1557,7 +1593,7 @@ They are objective defects, not style preferences.`
1557
1593
  }
1558
1594
  }
1559
1595
  lines.push("", `> ${this.summary()}`);
1560
- fs3.writeFileSync(file, lines.join("\n") + "\n");
1596
+ fs4.writeFileSync(file, lines.join("\n") + "\n");
1561
1597
  this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
1562
1598
  })();
1563
1599
  break;
@@ -1579,8 +1615,8 @@ They are objective defects, not style preferences.`
1579
1615
  this.push("error", "no Chrome/Chromium found for flows");
1580
1616
  return;
1581
1617
  }
1582
- const { runFlow } = await import("./cdp-C6MOKVEN.js");
1583
- const { previewDir } = await import("./preview-Q43LOK6P.js");
1618
+ const { runFlow } = await import("./cdp-Q4H6ZHPT.js");
1619
+ const { previewDir } = await import("./preview-ZUCVWGZG.js");
1584
1620
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1585
1621
  this.notify({ running: true, runStartedAt: Date.now() });
1586
1622
  const failures = [];
@@ -1718,7 +1754,7 @@ They are objective defects, not style preferences.`
1718
1754
  const result = await this.capture();
1719
1755
  if (result) {
1720
1756
  await this.runTurn(
1721
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions),
1757
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms, result.viewTransitions, result.components),
1722
1758
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1723
1759
  );
1724
1760
  }
@@ -1904,7 +1940,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1904
1940
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1905
1941
  break;
1906
1942
  case "help": {
1907
- void import("./commands-VDICJJT2.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1943
+ void import("./commands-BY44HDQ6.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1908
1944
  break;
1909
1945
  }
1910
1946
  case "quit":
@@ -2523,7 +2559,7 @@ function registerTui(program2) {
2523
2559
  }
2524
2560
 
2525
2561
  // src/cli.tsx
2526
- var VERSION = true ? "0.4.4" : "0.0.0-dev";
2562
+ var VERSION = true ? "0.4.5" : "0.0.0-dev";
2527
2563
  var program = new Command();
2528
2564
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2529
2565
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-O7NWGLS3.js";
6
+ } from "./chunk-43NQNIJY.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -7,7 +7,7 @@ import {
7
7
  loadLocks,
8
8
  loadRules,
9
9
  loadSkills
10
- } from "./chunk-YT6K2YIS.js";
10
+ } from "./chunk-ZLEP2TWF.js";
11
11
  import {
12
12
  loadBrief
13
13
  } from "./chunk-WAJXATCO.js";
@@ -54,6 +54,7 @@ function contextReport(cwd) {
54
54
  warnings.push(`skill "${skill.name}" has trigger(s) ${generic.map((t) => `"${t}"`).join(", ")} short enough to match almost any ask \u2014 make them more specific`);
55
55
  }
56
56
  }
57
+ lines.push({ source: "approval protocol (built-in)", tokens: 70, when: "every ask" });
57
58
  const totalAlways = lines.filter((l) => l.when === "every ask").reduce((sum, l) => sum + l.tokens, 0);
58
59
  if (totalAlways > 3e3) warnings.push(`~${totalAlways} tokens ride on every single ask \u2014 that is real money and real attention; trim the always-on set`);
59
60
  return { lines, warnings, totalAlways };
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-ZUEGDTAM.js";
13
+ } from "./chunk-CV5WVKHU.js";
14
14
  import "./chunk-O2S6PAJE.js";
15
15
  import "./chunk-IMDRXXFU.js";
16
16
  import "./chunk-KVYGPLWW.js";
17
- import "./chunk-JFFNQIBU.js";
17
+ import "./chunk-RQHOE5MV.js";
18
18
  export {
19
19
  VIEWPORTS,
20
20
  buildReviewPrompt,
@@ -6,7 +6,7 @@ import {
6
6
  loadSkills,
7
7
  matchSkills,
8
8
  parseSkill
9
- } from "./chunk-YT6K2YIS.js";
9
+ } from "./chunk-ZLEP2TWF.js";
10
10
  export {
11
11
  enrich,
12
12
  loadLocks,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.4",
4
- "description": "Lovable for your terminal a frontend harness on top of Claude Code, Codex, and other coding agents.",
3
+ "version": "0.4.5",
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",
7
7
  "author": "Aayam Bansal",