@aayambansal/squint 0.4.0 → 0.4.2

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
@@ -148,7 +148,10 @@ squint doctor --probe # run every engine end to end, verify auth act
148
148
  order; `/queue clear` drops them. `Esc` interrupts the current turn.
149
149
  - **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
150
150
  `↑/↓` history. `ctrl+c` twice exits with a session summary.
151
- - **Problems**: findings from gates, the dev server, the runtime probe, and a11y sweeps
151
+ - **Flows**: declare user journeys as six readable lines in `.squint/flows/`; `/flows`
152
+ replays them headlessly and failing steps join the fix loop. `/score` snapshots quality
153
+ deterministically.
154
+ - **Problems**: findings from gates, the dev server, the runtime probe, a11y sweeps, and flows
152
155
  collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
153
156
  `/fix <n>` targets one. The footer counts what's open.
154
157
  - **Sandbox**: `/sandbox on` and asks accumulate in a shadow worktree — the dev server,
@@ -4,7 +4,7 @@ import {
4
4
  hasWebSocket,
5
5
  pixelDiffPct,
6
6
  runFlow
7
- } from "./chunk-6FR4TRRQ.js";
7
+ } from "./chunk-Y47X4ENN.js";
8
8
  export {
9
9
  cdpCapture,
10
10
  hasWebSocket,
@@ -1,25 +1,70 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/prompt/skills.ts
4
- import fs2 from "fs";
5
- import path2 from "path";
4
+ import fs3 from "fs";
5
+ import path3 from "path";
6
6
 
7
- // src/prompt/registry.ts
7
+ // src/session/designLog.ts
8
8
  import fs from "fs";
9
9
  import path from "path";
10
+ function logFile(cwd) {
11
+ return path.join(cwd, ".squint", "design-log.jsonl");
12
+ }
13
+ function appendDecision(cwd, entry) {
14
+ try {
15
+ fs.mkdirSync(path.join(cwd, ".squint"), { recursive: true });
16
+ const record = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry };
17
+ fs.appendFileSync(logFile(cwd), JSON.stringify(record) + "\n");
18
+ } catch {
19
+ }
20
+ }
21
+ function loadDecisions(cwd, limit = 8) {
22
+ try {
23
+ const lines = fs.readFileSync(logFile(cwd), "utf8").trim().split("\n");
24
+ const decisions = [];
25
+ for (const line of lines.slice(-limit * 2)) {
26
+ try {
27
+ const parsed = JSON.parse(line);
28
+ if (typeof parsed?.decision === "string" && typeof parsed?.ts === "string") decisions.push(parsed);
29
+ } catch {
30
+ }
31
+ }
32
+ return decisions.slice(-limit);
33
+ } catch {
34
+ return [];
35
+ }
36
+ }
37
+ function decisionsSection(cwd) {
38
+ const decisions = loadDecisions(cwd);
39
+ if (decisions.length === 0) return "";
40
+ const lines = decisions.map((d) => {
41
+ const days = Math.floor((Date.now() - Date.parse(d.ts)) / 864e5);
42
+ const age = days <= 0 ? "today" : `${days}d ago`;
43
+ return `- ${d.decision} (${d.source}, ${age}${d.screenshot ? `, evidence: ${d.screenshot}` : ""})`;
44
+ });
45
+ return `## Design decisions on record
46
+
47
+ ${lines.join("\n")}
48
+
49
+ These were decided deliberately. Do not silently undo them; if a task genuinely requires reversing one, say so explicitly first.`;
50
+ }
51
+
52
+ // src/prompt/registry.ts
53
+ import fs2 from "fs";
54
+ import path2 from "path";
10
55
  function loadComponentInventory(cwd) {
11
56
  let config;
12
57
  try {
13
- config = JSON.parse(fs.readFileSync(path.join(cwd, "components.json"), "utf8"));
58
+ config = JSON.parse(fs2.readFileSync(path2.join(cwd, "components.json"), "utf8"));
14
59
  } catch {
15
60
  return null;
16
61
  }
17
62
  const alias = config.aliases?.ui ?? (config.aliases?.components ? `${config.aliases.components}/ui` : "@/components/ui");
18
63
  const relative = alias.replace(/^@\//, "src/").replace(/^~\//, "");
19
- const uiDir = path.join(cwd, relative);
64
+ const uiDir = path2.join(cwd, relative);
20
65
  let entries;
21
66
  try {
22
- entries = fs.readdirSync(uiDir);
67
+ entries = fs2.readdirSync(uiDir);
23
68
  } catch {
24
69
  return null;
25
70
  }
@@ -55,17 +100,17 @@ function parseSkill(name, raw) {
55
100
  return { name, triggers, body };
56
101
  }
57
102
  function loadSkills(cwd) {
58
- const dir = path2.join(cwd, ".squint", "skills");
103
+ const dir = path3.join(cwd, ".squint", "skills");
59
104
  let entries;
60
105
  try {
61
- entries = fs2.readdirSync(dir).filter((f) => f.endsWith(".md"));
106
+ entries = fs3.readdirSync(dir).filter((f) => f.endsWith(".md"));
62
107
  } catch {
63
108
  return [];
64
109
  }
65
110
  const skills = [];
66
111
  for (const entry of entries.sort()) {
67
112
  try {
68
- const skill = parseSkill(entry.replace(/\.md$/, ""), fs2.readFileSync(path2.join(dir, entry), "utf8"));
113
+ const skill = parseSkill(entry.replace(/\.md$/, ""), fs3.readFileSync(path3.join(dir, entry), "utf8"));
69
114
  if (skill) skills.push(skill);
70
115
  } catch {
71
116
  }
@@ -74,7 +119,7 @@ function loadSkills(cwd) {
74
119
  }
75
120
  function loadRules(cwd) {
76
121
  try {
77
- const text = fs2.readFileSync(path2.join(cwd, ".squint", "rules.md"), "utf8").trim();
122
+ const text = fs3.readFileSync(path3.join(cwd, ".squint", "rules.md"), "utf8").trim();
78
123
  return text.length > 0 ? text : null;
79
124
  } catch {
80
125
  return null;
@@ -86,7 +131,7 @@ function matchSkills(skills, ask) {
86
131
  }
87
132
  function loadLocks(cwd) {
88
133
  try {
89
- return fs2.readFileSync(path2.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
134
+ return fs3.readFileSync(path3.join(cwd, ".squint", "locks"), "utf8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
90
135
  } catch {
91
136
  return [];
92
137
  }
@@ -97,6 +142,8 @@ function enrich(cwd, ask) {
97
142
  if (rules) parts.push(`## Project rules (always apply)
98
143
 
99
144
  ${rules}`);
145
+ const decisions = decisionsSection(cwd);
146
+ if (decisions) parts.push(decisions);
100
147
  const inventory = loadComponentInventory(cwd);
101
148
  if (inventory) parts.push(inventorySection(inventory));
102
149
  const locks = loadLocks(cwd);
@@ -124,6 +171,7 @@ ${parts.join("\n\n")}` : "",
124
171
  }
125
172
 
126
173
  export {
174
+ appendDecision,
127
175
  parseSkill,
128
176
  loadSkills,
129
177
  loadRules,
@@ -26,6 +26,7 @@ var COMMANDS = [
26
26
  { name: "copy", group: "session", description: "copy the last reply to the clipboard" },
27
27
  { name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
28
28
  { name: "find", args: "<term>", group: "session", description: "search this session and saved transcripts" },
29
+ { name: "decide", args: "<text>", group: "session", description: "record a design decision; injected into every future ask" },
29
30
  { name: "resume", group: "session", description: "pick up the previous session for this repo" },
30
31
  { name: "clear", group: "session", description: "new session (transcript, totals, persisted state)" },
31
32
  { name: "help", group: "session", description: "list commands" },
@@ -9,7 +9,7 @@ import {
9
9
  import {
10
10
  cdpCapture,
11
11
  hasWebSocket
12
- } from "./chunk-6FR4TRRQ.js";
12
+ } from "./chunk-Y47X4ENN.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 } = await cdpCapture(chrome, url, dir, VIEWPORTS, 2500, true);
50
+ const { report, shots: shots2, a11y, slop, narration, phantoms } = 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 };
65
+ return { shots: shots2, errors: errors2, runtime: report, a11y, slop, narration, phantoms };
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-VRGCGN5R.js");
136
+ const { pixelDiffPct } = await import("./cdp-TPP2RGSR.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 phantomSection(phantoms) {
165
+ if (!phantoms || phantoms.length === 0) return "";
166
+ return `
167
+
168
+ ## Phantom classes (in the DOM, absent from the CSS)
169
+
170
+ ${phantoms.join("\n")}
171
+
172
+ Each of these is an element silently unstyled \u2014 usually a misspelled or version-mismatched utility (Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled. Fix the class names or define the styles.`;
173
+ }
164
174
  function slopSection(findings) {
165
175
  if (!findings || findings.length === 0) return "";
166
176
  return `
@@ -171,13 +181,13 @@ ${findings.join("\n")}
171
181
 
172
182
  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.`;
173
183
  }
174
- function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration) {
184
+ function buildReviewPrompt(shots, extra, runtime, a11y, slop, narration, phantoms) {
175
185
  const list = shots.map((s) => `- ${s.name}: ${s.path}`).join("\n");
176
186
  return `Screenshots of the running app were just captured:
177
187
 
178
188
  ${list}
179
189
 
180
- 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)}`;
190
+ 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)}`;
181
191
  }
182
192
 
183
193
  export {
@@ -179,7 +179,7 @@ var describe = (value) => {
179
179
  return String(value);
180
180
  };
181
181
  async function runFlow(chromePath, baseUrl, flow, outDir) {
182
- const { stepExpression } = await import("./flows-V5ZLVVSQ.js");
182
+ const { stepExpression } = await import("./flows-6LK7NGWS.js");
183
183
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
184
184
  const shots = [];
185
185
  let connection = null;
@@ -203,6 +203,10 @@ async function runFlow(chromePath, baseUrl, flow, outDir) {
203
203
  await new Promise((resolve) => setTimeout(resolve, 1800));
204
204
  continue;
205
205
  }
206
+ if (step.kind === "wait") {
207
+ await new Promise((resolve) => setTimeout(resolve, step.ms));
208
+ continue;
209
+ }
206
210
  if (step.kind === "shot") {
207
211
  const { data } = await connection.send("Page.captureScreenshot", { format: "png" }, sessionId);
208
212
  const outPath = path.join(outDir, `flow-${flow.name}-${step.name}.png`);
@@ -325,6 +329,37 @@ var SLOP_AUDIT = `(() => {
325
329
  }
326
330
  return out.slice(0, 8);
327
331
  })()`;
332
+ var PHANTOM_AUDIT = `(() => {
333
+ const defined = new Set();
334
+ for (const sheet of document.styleSheets) {
335
+ let rules; try { rules = sheet.cssRules; } catch { continue; }
336
+ const walk = (list) => {
337
+ for (const rule of list) {
338
+ if (rule.selectorText) {
339
+ for (const m of rule.selectorText.matchAll(/\\.((?:[\\w-]|\\\\.)+)/g)) {
340
+ defined.add(m[1].replace(/\\\\(.)/g, '$1'));
341
+ }
342
+ }
343
+ if (rule.cssRules) walk(rule.cssRules);
344
+ }
345
+ };
346
+ walk(rules);
347
+ }
348
+ if (defined.size < 10) return []; // no CSSOM visibility \u2014 stay silent
349
+ const seen = new Map();
350
+ for (const el of document.querySelectorAll('[class]')) {
351
+ for (const cls of el.classList) {
352
+ if (cls.length < 3) continue;
353
+ if (!defined.has(cls) && !seen.has(cls)) seen.set(cls, el.tagName.toLowerCase());
354
+ }
355
+ }
356
+ const out = [];
357
+ for (const [cls, tag] of seen) {
358
+ out.push(cls + ' (on <' + tag + '>)');
359
+ if (out.length >= 12) break;
360
+ }
361
+ return out;
362
+ })()`;
328
363
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
329
364
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
330
365
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -333,6 +368,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
333
368
  let slop = [];
334
369
  let perf = {};
335
370
  let narration = [];
371
+ let phantoms = [];
336
372
  const requests = /* @__PURE__ */ new Map();
337
373
  let connection = null;
338
374
  try {
@@ -405,6 +441,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
405
441
  if (Array.isArray(result?.value)) slop = result.value.map(String);
406
442
  } catch {
407
443
  }
444
+ try {
445
+ const { result } = await connection.send(
446
+ "Runtime.evaluate",
447
+ { expression: PHANTOM_AUDIT, returnByValue: true },
448
+ sessionId
449
+ );
450
+ if (Array.isArray(result?.value)) phantoms = result.value.map(String);
451
+ } catch {
452
+ }
408
453
  try {
409
454
  await connection.send("Accessibility.enable", {}, sessionId);
410
455
  const { nodes } = await connection.send("Accessibility.getFullAXTree", {}, sessionId);
@@ -469,7 +514,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
469
514
  child.kill("SIGKILL");
470
515
  setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
471
516
  }
472
- return { report, shots, a11y, slop, perf, narration };
517
+ return { report, shots, a11y, slop, perf, narration, phantoms };
473
518
  }
474
519
 
475
520
  export {
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  completeCommand
4
- } from "./chunk-SVKV77TO.js";
4
+ } from "./chunk-S3WV5C3X.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-2EJWUBUP.js";
45
+ } from "./chunk-WCLQZFOR.js";
46
46
  import {
47
47
  clearState,
48
48
  loadState,
@@ -58,10 +58,11 @@ import {
58
58
  detectEngines,
59
59
  getEngine
60
60
  } from "./chunk-KVYGPLWW.js";
61
- import "./chunk-6FR4TRRQ.js";
61
+ import "./chunk-Y47X4ENN.js";
62
62
  import {
63
+ appendDecision,
63
64
  enrich
64
- } from "./chunk-H5K55LXY.js";
65
+ } from "./chunk-KGZILC3S.js";
65
66
  import {
66
67
  composePrompt
67
68
  } from "./chunk-P3H4N2EN.js";
@@ -238,7 +239,7 @@ function registerEnv(program2) {
238
239
  }
239
240
  }
240
241
  const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
241
- const { hasWebSocket } = await import("./cdp-VRGCGN5R.js");
242
+ const { hasWebSocket } = await import("./cdp-TPP2RGSR.js");
242
243
  const chrome = findChrome2();
243
244
  console.log(
244
245
  chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -262,7 +263,7 @@ import pc3 from "picocolors";
262
263
  function registerProject(program2) {
263
264
  const skillsCommand = program2.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
264
265
  skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
265
- const { loadRules, loadSkills } = await import("./skills-ODOVNE6E.js");
266
+ const { loadRules, loadSkills } = await import("./skills-U2J7Z3B2.js");
266
267
  const cwd = process.cwd();
267
268
  const rules = loadRules(cwd);
268
269
  console.log(
@@ -461,7 +462,7 @@ function registerQuality(program2) {
461
462
  if (results.some((r) => !r.ok)) process.exitCode = 1;
462
463
  });
463
464
  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) => {
464
- const { captureViewports: captureViewports2 } = await import("./preview-X7EWTRCZ.js");
465
+ const { captureViewports: captureViewports2 } = await import("./preview-SKHIXVCE.js");
465
466
  const result = await captureViewports2(process.cwd(), url);
466
467
  if (!result) {
467
468
  console.error(pc4.red("\u2717 no Chrome/Chromium found"));
@@ -554,7 +555,7 @@ function registerRun(program2) {
554
555
  import pc6 from "picocolors";
555
556
  function registerScaffold(program2) {
556
557
  program2.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
557
- const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
558
+ const { installDependencies, writeTemplate } = await import("./init-YB2IMA4N.js");
558
559
  let result;
559
560
  try {
560
561
  result = writeTemplate(dir, { force: options.force });
@@ -823,7 +824,7 @@ ${question}`,
823
824
  return;
824
825
  }
825
826
  await this.runTurn(
826
- buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration),
827
+ buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
827
828
  `\u{1F441} polish round ${round}/${rounds}`
828
829
  );
829
830
  }
@@ -1141,7 +1142,7 @@ ${errors.slice(-5).join("\n")}`);
1141
1142
  const captureResult = await this.capture();
1142
1143
  if (captureResult) {
1143
1144
  await this.runTurn(
1144
- buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration),
1145
+ buildReviewPrompt(captureResult.shots, void 0, captureResult.runtime, captureResult.a11y, captureResult.slop, captureResult.narration, captureResult.phantoms),
1145
1146
  "\u{1F441} auto-review rendered UI"
1146
1147
  );
1147
1148
  }
@@ -1189,6 +1190,7 @@ ${errors.slice(-5).join("\n")}`);
1189
1190
  const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
1190
1191
  if (result.restored) {
1191
1192
  const dropped = this.checkpoints.length - index;
1193
+ appendDecision(this.opts.cwd, { decision: `rejected and rolled back: "${checkpoint.label}"`, source: "restore" });
1192
1194
  this.checkpoints = this.checkpoints.slice(0, index);
1193
1195
  this.push(
1194
1196
  "status",
@@ -1280,6 +1282,20 @@ ${errors.slice(-5).join("\n")}`);
1280
1282
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
1281
1283
  }
1282
1284
  }
1285
+ if (result.phantoms && result.phantoms.length > 0) {
1286
+ this.push("error", `phantom classes: ${result.phantoms.length} (in the DOM, absent from CSS)
1287
+ ${result.phantoms.slice(0, 5).join("\n")}`);
1288
+ this.addProblem(
1289
+ "runtime",
1290
+ `${result.phantoms.length} phantom class(es) \u2014 elements silently unstyled`,
1291
+ `These class tokens appear in the DOM but match no CSS rule, so the elements are silently unstyled. Usually a misspelled or version-mismatched utility (e.g. Tailwind v3 spellings in a v4 project) or a concatenated class the scanner never compiled:
1292
+
1293
+ ${result.phantoms.join("\n")}
1294
+
1295
+ Fix the class names or define the styles, then confirm visually.`
1296
+ );
1297
+ this.push("status", "/fix sends open problems to the engine");
1298
+ }
1283
1299
  if (result.slop && result.slop.length > 0) {
1284
1300
  this.push("status", `distinctiveness: ${result.slop.length} tell(s)
1285
1301
  ${result.slop.join("\n")}`);
@@ -1395,6 +1411,19 @@ They are objective defects, not style preferences.`
1395
1411
  }
1396
1412
  break;
1397
1413
  }
1414
+ case "decide": {
1415
+ if (!arg) {
1416
+ this.push("status", "usage: /decide <the decision> \u2014 recorded in .squint/design-log.jsonl and injected into every future ask");
1417
+ break;
1418
+ }
1419
+ appendDecision(this.opts.cwd, {
1420
+ decision: arg,
1421
+ source: "decide",
1422
+ screenshot: this.lastPulse ? ".squint/preview/pulse.png" : void 0
1423
+ });
1424
+ this.push("status", `decision recorded: ${arg}`);
1425
+ break;
1426
+ }
1398
1427
  case "find": {
1399
1428
  if (!arg) {
1400
1429
  this.push("status", "usage: /find <term> \u2014 searches this session and saved transcripts");
@@ -1480,7 +1509,7 @@ They are objective defects, not style preferences.`
1480
1509
  break;
1481
1510
  }
1482
1511
  void (async () => {
1483
- const { loadFlows } = await import("./flows-V5ZLVVSQ.js");
1512
+ const { loadFlows } = await import("./flows-6LK7NGWS.js");
1484
1513
  const flows = loadFlows(this.opts.cwd);
1485
1514
  if (flows.length === 0) {
1486
1515
  this.push("status", "no flows \u2014 add .squint/flows/<name>.flow (goto/click/fill/press/expect/shot lines), or ask the engine to write one");
@@ -1491,8 +1520,8 @@ They are objective defects, not style preferences.`
1491
1520
  this.push("error", "no Chrome/Chromium found for flows");
1492
1521
  return;
1493
1522
  }
1494
- const { runFlow } = await import("./cdp-VRGCGN5R.js");
1495
- const { previewDir } = await import("./preview-X7EWTRCZ.js");
1523
+ const { runFlow } = await import("./cdp-TPP2RGSR.js");
1524
+ const { previewDir } = await import("./preview-SKHIXVCE.js");
1496
1525
  this.push("status", `replaying ${flows.length} flow(s)\u2026`);
1497
1526
  this.notify({ running: true, runStartedAt: Date.now() });
1498
1527
  const failures = [];
@@ -1630,7 +1659,7 @@ They are objective defects, not style preferences.`
1630
1659
  const result = await this.capture();
1631
1660
  if (result) {
1632
1661
  await this.runTurn(
1633
- buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration),
1662
+ buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y, result.slop, result.narration, result.phantoms),
1634
1663
  `\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
1635
1664
  );
1636
1665
  }
@@ -1709,6 +1738,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1709
1738
  discardSandbox(this.opts.cwd);
1710
1739
  this.notify({ sandbox: false });
1711
1740
  this.resetDevServer();
1741
+ appendDecision(this.opts.cwd, { decision: "accepted the sandboxed session onto the real tree", source: "sandbox" });
1712
1742
  this.push("status", "sandbox applied to the real tree \u2014 review with git diff");
1713
1743
  } else {
1714
1744
  this.push("error", applied.detail ?? "apply failed");
@@ -1750,6 +1780,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1750
1780
  const applied = applyVariant(this.opts.cwd, id);
1751
1781
  if (applied.ok) {
1752
1782
  cleanVariants(this.opts.cwd);
1783
+ appendDecision(this.opts.cwd, { decision: `chose the ${id} direction over the other variants`, source: "variant" });
1753
1784
  this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
1754
1785
  } else {
1755
1786
  this.push("error", applied.detail ?? "apply failed");
@@ -1814,7 +1845,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
1814
1845
  this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
1815
1846
  break;
1816
1847
  case "help": {
1817
- void import("./commands-DE5Q7VWY.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1848
+ void import("./commands-B6I6C5LP.js").then(({ commandHelp }) => this.push("status", commandHelp()));
1818
1849
  break;
1819
1850
  }
1820
1851
  case "quit":
@@ -2433,7 +2464,7 @@ function registerTui(program2) {
2433
2464
  }
2434
2465
 
2435
2466
  // src/cli.tsx
2436
- var VERSION = true ? "0.4.0" : "0.0.0-dev";
2467
+ var VERSION = true ? "0.4.2" : "0.0.0-dev";
2437
2468
  var program = new Command();
2438
2469
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
2439
2470
  registerRun(program);
@@ -3,7 +3,7 @@ import {
3
3
  COMMANDS,
4
4
  commandHelp,
5
5
  completeCommand
6
- } from "./chunk-SVKV77TO.js";
6
+ } from "./chunk-S3WV5C3X.js";
7
7
  export {
8
8
  COMMANDS,
9
9
  commandHelp,
@@ -32,6 +32,18 @@ function parseFlow(name, text) {
32
32
  case "shot":
33
33
  steps.push({ kind: "shot", name: arg.replace(/[^a-zA-Z0-9-]/g, "-") });
34
34
  break;
35
+ case "hover":
36
+ steps.push({ kind: "hover", target: arg });
37
+ break;
38
+ case "scroll":
39
+ steps.push({ kind: "scroll", target: arg || "bottom" });
40
+ break;
41
+ case "wait": {
42
+ const ms = Number.parseInt(arg, 10);
43
+ if (!Number.isInteger(ms) || ms < 0 || ms > 1e4) return null;
44
+ steps.push({ kind: "wait", ms });
45
+ break;
46
+ }
35
47
  default:
36
48
  return null;
37
49
  }
@@ -96,8 +108,35 @@ function stepExpression(step) {
96
108
  }
97
109
  return { ok: true };
98
110
  })()`;
111
+ case "hover":
112
+ return `(() => {
113
+ const target = ${JSON.stringify(step.target)};
114
+ let el = null;
115
+ try { el = document.querySelector(target); } catch {}
116
+ if (!el) {
117
+ const all = [...document.querySelectorAll('*')];
118
+ el = all.find((e) => e.children.length === 0 && (e.textContent || '').trim().toLowerCase().includes(target.toLowerCase()));
119
+ }
120
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
121
+ for (const type of ['pointerover', 'mouseover', 'mouseenter']) {
122
+ el.dispatchEvent(new MouseEvent(type, { bubbles: type !== 'mouseenter' }));
123
+ }
124
+ return { ok: true };
125
+ })()`;
126
+ case "scroll":
127
+ return `(() => {
128
+ const target = ${JSON.stringify(step.target)};
129
+ if (target === 'bottom') { window.scrollTo(0, document.body.scrollHeight); return { ok: true }; }
130
+ if (target === 'top') { window.scrollTo(0, 0); return { ok: true }; }
131
+ let el = null;
132
+ try { el = document.querySelector(target); } catch {}
133
+ if (!el) return { ok: false, detail: 'no element matching ' + target };
134
+ el.scrollIntoView({ block: 'center' });
135
+ return { ok: true };
136
+ })()`;
99
137
  case "goto":
100
138
  case "shot":
139
+ case "wait":
101
140
  return null;
102
141
  }
103
142
  }
@@ -139,6 +139,12 @@ body {
139
139
  dist/
140
140
  *.log
141
141
  .DS_Store
142
+ `,
143
+ ".squint/flows/home.flow": `# The starter journey: replayed headlessly by /flows after every change.
144
+ # Grow this file as the app grows \u2014 or ask the engine to.
145
+ goto /
146
+ expect Ready
147
+ shot home
142
148
  `
143
149
  };
144
150
  }
@@ -10,11 +10,11 @@ import {
10
10
  probeRuntime,
11
11
  routeShotName,
12
12
  runtimeSummary
13
- } from "./chunk-2EJWUBUP.js";
13
+ } from "./chunk-WCLQZFOR.js";
14
14
  import "./chunk-O2S6PAJE.js";
15
15
  import "./chunk-IMDRXXFU.js";
16
16
  import "./chunk-KVYGPLWW.js";
17
- import "./chunk-6FR4TRRQ.js";
17
+ import "./chunk-Y47X4ENN.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-H5K55LXY.js";
9
+ } from "./chunk-KGZILC3S.js";
10
10
  export {
11
11
  enrich,
12
12
  loadLocks,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",