@aayambansal/squint 0.2.1 → 0.2.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
@@ -123,9 +123,10 @@ squint check # gates: typecheck -> lint -> test -> build
123
123
  squint shot http://localhost:5173 # screenshots at 390/768/1440
124
124
  squint brief # list design directions
125
125
  squint brief cinematic-dark # commit one for this repo
126
- squint tag # add the Alt+S element picker to any Vite app
126
+ squint tag # Alt+S element picker: pin elements + notes, alt+enter copies all
127
127
  squint variants gen 3 "<ask>" # 3 parallel design explorations
128
128
  squint variants apply terminal # keep the winner
129
+ squint skills init # scaffold .squint/rules.md + a trigger-matched skill
129
130
  squint config set engine claude
130
131
  squint config set models.claude claude-sonnet-5
131
132
  squint config set autoDev true # dev server starts with the TUI
@@ -134,6 +135,7 @@ squint config set autoCheck false # skip the per-turn typecheck+lint pass
134
135
  squint config set theme ocean # amber · ocean · moss · rose · mono
135
136
  squint config set bell false # no bell on turn completion
136
137
  squint doctor # engines + Chrome + WebSocket check
138
+ squint doctor --probe # run every engine end to end, verify auth actually works
137
139
  ```
138
140
 
139
141
  **Inside the TUI:**
@@ -144,10 +146,21 @@ squint doctor # engines + Chrome + WebSocket check
144
146
  order; `/queue clear` drops them. `Esc` interrupts the current turn.
145
147
  - **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
146
148
  `↑/↓` history. `ctrl+c` twice exits with a session summary.
147
- - **Commands**: `/dev` `/check` `/fix` `/shot` `/review [focus]` `/undo` `/checkpoints`
148
- `/restore <n>` `/mode` `/theme` `/resume` `/engine <id>` `/model <name>` `/clear`.
149
- - Assistant output renders as markdown; the footer tracks session turns and cost; a bell
150
- rings when a turn finishes.
149
+ - **Problems**: findings from gates, the dev server, the runtime probe, and a11y sweeps
150
+ collect into a list — `/problems` shows it, `/fix` sends everything as one turn,
151
+ `/fix <n>` targets one. The footer counts what's open.
152
+ - **Variants without leaving**: `/variants 3 <ask>` runs parallel explorations with
153
+ streaming per-family status; `/variants apply <id>` keeps the winner.
154
+ - **Commands**: `/dev` `/check` `/problems` `/fix [n]` `/shot` `/review [focus]`
155
+ `/variants` `/undo` `/checkpoints` `/restore <n>` `/mode` `/theme` `/resume`
156
+ `/engine <id>` `/model <name>` `/clear`.
157
+ - Assistant output renders as markdown; the done line measures real work via git
158
+ (`3 files +42 −7`); the footer tracks session turns and cost; a bell rings when a
159
+ turn finishes.
160
+
161
+ **Project knowledge** rides along automatically: `.squint/rules.md` on every ask, and
162
+ `.squint/skills/*.md` (frontmatter `triggers: auth, login`) only when an ask mentions a
163
+ trigger — deterministic context routing, no embeddings.
151
164
 
152
165
  ## Design directions
153
166
 
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ cdpCapture,
4
+ hasWebSocket,
5
+ pixelDiffPct
6
+ } from "./chunk-OC6RU6XH.js";
7
+ export {
8
+ cdpCapture,
9
+ hasWebSocket,
10
+ pixelDiffPct
11
+ };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  cdpCapture,
4
4
  hasWebSocket
5
- } from "./chunk-VJGE7HSP.js";
5
+ } from "./chunk-OC6RU6XH.js";
6
6
  import {
7
7
  findChrome,
8
8
  screenshot
@@ -126,16 +126,29 @@ ${blocks.join("\n\n")}
126
126
 
127
127
  Fix these first \u2014 a page that errors is broken regardless of how it looks.`;
128
128
  }
129
- async function probeRuntime(url) {
129
+ async function probeRuntime(url, cwd) {
130
130
  const chrome = findChrome();
131
131
  if (!chrome || !hasWebSocket()) return null;
132
132
  try {
133
- const { report } = await cdpCapture(chrome, url, os.tmpdir(), [], 1500);
134
- return report;
133
+ const dir = cwd ? previewDir(cwd) : os.tmpdir();
134
+ const { report, shots } = await cdpCapture(
135
+ chrome,
136
+ url,
137
+ dir,
138
+ cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
139
+ 1500
140
+ );
141
+ return { report, pulsePath: shots[0]?.path };
135
142
  } catch {
136
143
  return null;
137
144
  }
138
145
  }
146
+ async function comparePulse(previous, current) {
147
+ const chrome = findChrome();
148
+ if (!chrome || !hasWebSocket()) return null;
149
+ const { pixelDiffPct } = await import("./cdp-SFWNP7OA.js");
150
+ return pixelDiffPct(chrome, previous, current);
151
+ }
139
152
  function buildRuntimeFixPrompt(report) {
140
153
  return `The running app has runtime problems.${runtimeSection(report)}
141
154
 
@@ -169,6 +182,7 @@ export {
169
182
  captureViewports,
170
183
  runtimeSummary,
171
184
  probeRuntime,
185
+ comparePulse,
172
186
  buildRuntimeFixPrompt,
173
187
  buildReviewPrompt
174
188
  };
@@ -152,6 +152,55 @@ var describe = (value) => {
152
152
  }
153
153
  return String(value);
154
154
  };
155
+ async function pixelDiffPct(chromePath, pngA, pngB) {
156
+ const { child, wsUrl, profileDir } = await launchChrome(chromePath);
157
+ let connection = null;
158
+ try {
159
+ connection = await CdpConnection.connect(wsUrl, 1e4);
160
+ const { targetId } = await connection.send("Target.createTarget", { url: "about:blank" });
161
+ const { sessionId } = await connection.send("Target.attachToTarget", { targetId, flatten: true });
162
+ await connection.send("Runtime.enable", {}, sessionId);
163
+ const expression = `(async () => {
164
+ const load = (src) => new Promise((resolve, reject) => {
165
+ const img = new Image();
166
+ img.onload = () => resolve(img);
167
+ img.onerror = () => reject(new Error('decode'));
168
+ img.src = src;
169
+ });
170
+ const a = await load('data:image/png;base64,${pngA.toString("base64")}');
171
+ const b = await load('data:image/png;base64,${pngB.toString("base64")}');
172
+ const w = Math.min(a.width, b.width), h = Math.min(a.height, b.height);
173
+ if (w === 0 || h === 0) return null;
174
+ const draw = (img) => {
175
+ const c = document.createElement('canvas');
176
+ c.width = w; c.height = h;
177
+ const ctx = c.getContext('2d');
178
+ ctx.drawImage(img, 0, 0);
179
+ return ctx.getImageData(0, 0, w, h).data;
180
+ };
181
+ const da = draw(a), db = draw(b);
182
+ let differ = 0, total = 0;
183
+ for (let i = 0; i < da.length; i += 8) {
184
+ total++;
185
+ 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) differ++;
186
+ }
187
+ const sizePenalty = (a.width !== b.width || a.height !== b.height) ? 1 : 0;
188
+ return Math.min(100, (differ / total) * 100 + sizePenalty);
189
+ })()`;
190
+ const { result } = await connection.send(
191
+ "Runtime.evaluate",
192
+ { expression, awaitPromise: true, returnByValue: true },
193
+ sessionId
194
+ );
195
+ return typeof result?.value === "number" ? result.value : null;
196
+ } catch {
197
+ return null;
198
+ } finally {
199
+ connection?.close();
200
+ child.kill("SIGKILL");
201
+ setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
202
+ }
203
+ }
155
204
  async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, audit = false) {
156
205
  const { child, wsUrl, profileDir } = await launchChrome(chromePath);
157
206
  const report = { consoleErrors: [], pageErrors: [], failedRequests: [] };
@@ -239,5 +288,6 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
239
288
 
240
289
  export {
241
290
  hasWebSocket,
291
+ pixelDiffPct,
242
292
  cdpCapture
243
293
  };
@@ -28,6 +28,22 @@ function takeSnapshot(cwd) {
28
28
  return null;
29
29
  }
30
30
  }
31
+ function diffStatSince(cwd, snapshot) {
32
+ try {
33
+ const source = snapshot.stashHash ?? "HEAD";
34
+ const stat = git(cwd, ["diff", "--shortstat", source]);
35
+ const files = /(\d+) files? changed/.exec(stat);
36
+ if (!files) return null;
37
+ const insertions = /(\d+) insertions?/.exec(stat);
38
+ const deletions = /(\d+) deletions?/.exec(stat);
39
+ let out = `${files[1]} file${files[1] === "1" ? "" : "s"}`;
40
+ if (insertions) out += ` +${insertions[1]}`;
41
+ if (deletions) out += ` \u2212${deletions[1]}`;
42
+ return out;
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
31
47
  function restoreSnapshot(cwd, snapshot) {
32
48
  try {
33
49
  const before = new Set(snapshot.untracked);
@@ -50,5 +66,6 @@ function restoreSnapshot(cwd, snapshot) {
50
66
  export {
51
67
  isGitRepo,
52
68
  takeSnapshot,
69
+ diffStatSince,
53
70
  restoreSnapshot
54
71
  };
package/dist/cli.js CHANGED
@@ -28,12 +28,13 @@ import {
28
28
  buildRuntimeFixPrompt,
29
29
  captureViewports,
30
30
  clearState,
31
+ comparePulse,
31
32
  loadState,
32
33
  probeRuntime,
33
34
  runtimeSummary,
34
35
  saveState
35
- } from "./chunk-REDPR3KI.js";
36
- import "./chunk-VJGE7HSP.js";
36
+ } from "./chunk-73ULM6HF.js";
37
+ import "./chunk-OC6RU6XH.js";
37
38
  import {
38
39
  findChrome
39
40
  } from "./chunk-P3V5CWH5.js";
@@ -45,10 +46,11 @@ import {
45
46
  enrich
46
47
  } from "./chunk-XZKQZKEE.js";
47
48
  import {
49
+ diffStatSince,
48
50
  isGitRepo,
49
51
  restoreSnapshot,
50
52
  takeSnapshot
51
- } from "./chunk-WTA6YYBY.js";
53
+ } from "./chunk-YHRAOBI2.js";
52
54
 
53
55
  // src/cli.tsx
54
56
  import { Command } from "commander";
@@ -161,7 +163,8 @@ var Session = class {
161
163
  devUrl: null,
162
164
  totals: { costUsd: 0, turns: 0 },
163
165
  queue: [],
164
- mode: "safe"
166
+ mode: "safe",
167
+ problems: []
165
168
  };
166
169
  if (opts.autoDev && detectDevCommand(opts.cwd)) {
167
170
  this.devServer().start();
@@ -188,10 +191,13 @@ var Session = class {
188
191
  sessionId;
189
192
  dev = null;
190
193
  abort = null;
191
- pendingFix = null;
194
+ /** Full problem records; state carries the summaries. */
195
+ problemPrompts = /* @__PURE__ */ new Map();
196
+ nextProblemId = 0;
192
197
  checkpoints = [];
193
198
  fixAttempts = 0;
194
199
  reviewTipShown = false;
200
+ lastPulse = null;
195
201
  startedAt = Date.now();
196
202
  subscribe(listener) {
197
203
  this.listeners.add(listener);
@@ -211,6 +217,47 @@ var Session = class {
211
217
  note(text) {
212
218
  this.push("status", text);
213
219
  }
220
+ /** Register a finding; a fresh finding from a source supersedes its old one. */
221
+ addProblem(source, summary, prompt) {
222
+ const kept = this.state.problems.filter((p) => p.source !== source);
223
+ for (const gone of this.state.problems) {
224
+ if (gone.source === source) this.problemPrompts.delete(gone.id);
225
+ }
226
+ this.nextProblemId += 1;
227
+ this.problemPrompts.set(this.nextProblemId, prompt);
228
+ this.notify({ problems: [...kept, { id: this.nextProblemId, source, summary }] });
229
+ }
230
+ clearProblems(source) {
231
+ const removed = this.state.problems.filter((p) => p.source === source);
232
+ if (removed.length === 0) return;
233
+ for (const problem of removed) this.problemPrompts.delete(problem.id);
234
+ this.notify({ problems: this.state.problems.filter((p) => p.source !== source) });
235
+ }
236
+ /** One prompt covering the given problems, oldest first. */
237
+ combinedFixPrompt(problems) {
238
+ const sections = problems.map(
239
+ (p) => this.problemPrompts.get(p.id) ?? `Fix this reported problem: ${p.summary}`
240
+ );
241
+ return sections.join("\n\n---\n\n");
242
+ }
243
+ dispatchFix(problems) {
244
+ if (problems.length === 0) return;
245
+ const prompt = this.combinedFixPrompt(problems);
246
+ const display = `\u26D1 fix: ${problems.map((p) => p.source).join(" + ")}`;
247
+ for (const problem of problems) this.problemPrompts.delete(problem.id);
248
+ this.notify({ problems: this.state.problems.filter((p) => !problems.includes(p)) });
249
+ void this.runTurn(prompt, display);
250
+ }
251
+ /** Launch a capped auto-fix turn over all open problems. Returns true if launched. */
252
+ maybeAutoFix() {
253
+ if (!this.opts.autoFix || this.fixAttempts >= MAX_AUTO_FIX_ATTEMPTS) return false;
254
+ if (this.state.problems.length === 0) return false;
255
+ this.fixAttempts += 1;
256
+ this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
257
+ this.notify({ running: false });
258
+ this.dispatchFix([...this.state.problems]);
259
+ return true;
260
+ }
214
261
  setMode(mode) {
215
262
  this.notify({ mode });
216
263
  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";
@@ -363,7 +410,9 @@ var Session = class {
363
410
  if (result.ok) {
364
411
  const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
365
412
  const secs = result.durationMs !== void 0 ? ` \xB7 ${(result.durationMs / 1e3).toFixed(0)}s` : "";
366
- const work = this.turnEdits > 0 ? ` \xB7 ${this.turnEdits} edit${this.turnEdits === 1 ? "" : "s"}` : this.turnTools > 0 ? ` \xB7 ${this.turnTools} tool call${this.turnTools === 1 ? "" : "s"}` : "";
413
+ const checkpoint = this.checkpoints.at(-1);
414
+ const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
415
+ 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"}` : "";
367
416
  this.push("status", `done${secs}${cost}${work}`);
368
417
  this.notify({
369
418
  totals: {
@@ -387,26 +436,22 @@ var Session = class {
387
436
  const gateResults = await runGates(this.opts.cwd, fastGates);
388
437
  const failures = gateResults.filter((r) => !r.ok);
389
438
  if (failures.length > 0) {
390
- this.pendingFix = {
391
- prompt: buildGatePrompt(failures),
392
- display: `\u26D1 fix ${failures.map((f) => f.gate.id).join(" + ")} errors`
393
- };
439
+ this.addProblem(
440
+ "gates",
441
+ failures.map((f) => f.gate.id).join(" + "),
442
+ buildGatePrompt(failures)
443
+ );
394
444
  this.push(
395
445
  "error",
396
446
  failures.map((f) => `\u2717 ${f.gate.id} \xB7 ${f.outputTail.split("\n").slice(-3).join("\n")}`).join("\n")
397
447
  );
398
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
399
- this.fixAttempts += 1;
400
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
401
- this.notify({ running: false });
402
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
403
- return;
404
- }
405
- this.push("status", "type /fix to send them to the engine");
448
+ if (this.maybeAutoFix()) return;
449
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
406
450
  this.notify({ running: false });
407
451
  this.drainQueue();
408
452
  return;
409
453
  }
454
+ this.clearProblems("gates");
410
455
  }
411
456
  }
412
457
  const dev = this.dev;
@@ -414,39 +459,28 @@ var Session = class {
414
459
  await delay(1500);
415
460
  const errors = dev.errorsSince(runStart);
416
461
  if (errors.length > 0) {
417
- this.pendingFix = {
418
- prompt: buildFixPrompt(errors, dev.tail(30)),
419
- display: "\u26D1 fix dev server errors"
420
- };
462
+ this.addProblem(
463
+ "dev",
464
+ `${errors.length} dev server error line(s)`,
465
+ buildFixPrompt(errors, dev.tail(30))
466
+ );
421
467
  this.push("error", `dev server: ${errors.length} error line(s)
422
468
  ${errors.slice(-5).join("\n")}`);
423
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
424
- this.fixAttempts += 1;
425
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
426
- this.notify({ running: false });
427
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
428
- return;
429
- }
430
- this.push("status", "type /fix to send them to the engine");
469
+ if (this.maybeAutoFix()) return;
470
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
431
471
  } else {
432
- this.pendingFix = null;
472
+ this.clearProblems("dev");
433
473
  if (this.opts.autoProbe !== false && this.state.devUrl) {
434
- const report = await probeRuntime(this.state.devUrl);
435
- const summary = report ? runtimeSummary(report) : null;
436
- if (report && summary) {
437
- this.pendingFix = {
438
- prompt: buildRuntimeFixPrompt(report),
439
- display: "\u26D1 fix runtime errors"
440
- };
474
+ const probe = await probeRuntime(this.state.devUrl, this.opts.cwd);
475
+ const summary = probe ? runtimeSummary(probe.report) : null;
476
+ if (probe && summary) {
477
+ this.addProblem("runtime", summary, buildRuntimeFixPrompt(probe.report));
441
478
  this.push("error", `runtime: ${summary}`);
442
- if (this.opts.autoFix && this.fixAttempts < MAX_AUTO_FIX_ATTEMPTS) {
443
- this.fixAttempts += 1;
444
- this.push("status", `auto-fix attempt ${this.fixAttempts}/${MAX_AUTO_FIX_ATTEMPTS}`);
445
- this.notify({ running: false });
446
- await this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
447
- return;
448
- }
449
- this.push("status", "type /fix to send them to the engine");
479
+ if (this.maybeAutoFix()) return;
480
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
481
+ } else if (probe) {
482
+ this.clearProblems("runtime");
483
+ await this.visualPulse(probe.pulsePath);
450
484
  }
451
485
  }
452
486
  if (!this.reviewTipShown && this.state.devUrl) {
@@ -497,6 +531,32 @@ ${errors.slice(-5).join("\n")}`);
497
531
  this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
498
532
  }
499
533
  }
534
+ /**
535
+ * Cross-turn visual drift check: compare this turn's pulse screenshot
536
+ * with the previous one and report how much of the page changed.
537
+ * Informational — changes are usually intended; surprises shouldn't be.
538
+ */
539
+ async visualPulse(pulsePath) {
540
+ if (!pulsePath) return;
541
+ let current;
542
+ try {
543
+ current = (await import("fs")).readFileSync(pulsePath);
544
+ } catch {
545
+ return;
546
+ }
547
+ const previous = this.lastPulse;
548
+ this.lastPulse = current;
549
+ if (!previous) {
550
+ this.push("status", "visual pulse: baseline captured");
551
+ return;
552
+ }
553
+ const pct = await comparePulse(previous, current);
554
+ if (pct === null) return;
555
+ this.push(
556
+ "status",
557
+ pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
558
+ );
559
+ }
500
560
  /** Screenshot the running app (and watch its runtime where CDP is available). */
501
561
  async capture() {
502
562
  if (!this.state.devUrl) {
@@ -520,16 +580,28 @@ ${errors.slice(-5).join("\n")}`);
520
580
  const summary = runtimeSummary(result.runtime);
521
581
  if (summary) {
522
582
  this.push("error", `runtime: ${summary}`);
523
- this.pendingFix = { prompt: buildRuntimeFixPrompt(result.runtime), display: "\u26D1 fix runtime errors" };
524
- this.push("status", "type /fix to send them to the engine");
583
+ this.addProblem("runtime", summary, buildRuntimeFixPrompt(result.runtime));
584
+ this.push("status", "/fix sends open problems to the engine");
525
585
  } else {
586
+ this.clearProblems("runtime");
526
587
  this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
527
588
  }
528
589
  }
529
590
  if (result.a11y && result.a11y.length > 0) {
530
591
  this.push("error", `a11y: ${result.a11y.length} finding(s)
531
592
  ${result.a11y.slice(0, 5).join("\n")}`);
532
- this.push("status", "/review folds these into the fix pass");
593
+ this.addProblem(
594
+ "a11y",
595
+ `${result.a11y.length} accessibility finding(s)`,
596
+ `Fix these accessibility defects found by an automated sweep of the running app:
597
+
598
+ ${result.a11y.join("\n")}
599
+
600
+ They are objective defects, not style preferences.`
601
+ );
602
+ this.push("status", "/review folds these into the fix pass \xB7 /fix sends them directly");
603
+ } else if (result.runtime) {
604
+ this.clearProblems("a11y");
533
605
  }
534
606
  return result.shots.length > 0 ? result : null;
535
607
  }
@@ -579,11 +651,47 @@ ${result.a11y.slice(0, 5).join("\n")}`);
579
651
  }
580
652
  break;
581
653
  }
582
- case "fix":
583
- if (!this.pendingFix) {
584
- this.push("status", "nothing to fix \u2014 no captured errors or failed gates");
654
+ case "fix": {
655
+ if (this.state.problems.length === 0) {
656
+ this.push("status", "nothing to fix \u2014 no open problems");
657
+ break;
658
+ }
659
+ const index = Number.parseInt(arg, 10);
660
+ if (arg && Number.isInteger(index)) {
661
+ const target = this.state.problems[index - 1];
662
+ if (!target) {
663
+ this.push("status", `usage: /fix [1\u2013${this.state.problems.length}] \u2014 see /problems`);
664
+ break;
665
+ }
666
+ this.dispatchFix([target]);
667
+ } else {
668
+ this.dispatchFix([...this.state.problems]);
669
+ }
670
+ break;
671
+ }
672
+ case "copy": {
673
+ const last = this.state.items.findLast((i) => i.role === "assistant");
674
+ if (!last) {
675
+ this.push("status", "nothing to copy yet");
676
+ break;
677
+ }
678
+ void import("child_process").then(({ spawn }) => {
679
+ const cmd = process.platform === "darwin" ? spawn("pbcopy") : process.platform === "win32" ? spawn("clip") : spawn("xclip", ["-selection", "clipboard"]);
680
+ cmd.on("error", () => this.push("error", "no clipboard tool found"));
681
+ cmd.on("close", (code) => {
682
+ if (code === 0) this.push("status", `copied last reply (${last.text.length} chars)`);
683
+ });
684
+ cmd.stdin?.end(last.text);
685
+ });
686
+ break;
687
+ }
688
+ case "problems":
689
+ if (this.state.problems.length === 0) {
690
+ this.push("status", "no open problems");
585
691
  } else {
586
- void this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
692
+ const lines = this.state.problems.map((p, i) => `${i + 1}. [${p.source}] ${p.summary}`);
693
+ this.push("status", `${lines.join("\n")}
694
+ /fix sends all \xB7 /fix <n> targets one`);
587
695
  }
588
696
  break;
589
697
  case "check":
@@ -605,12 +713,10 @@ ${result.a11y.slice(0, 5).join("\n")}`);
605
713
  this.drainQueue();
606
714
  const failures = results.filter((r) => !r.ok);
607
715
  if (failures.length > 0) {
608
- this.pendingFix = {
609
- prompt: buildGatePrompt(failures),
610
- display: `\u26D1 fix failing gates: ${failures.map((f) => f.gate.id).join(", ")}`
611
- };
612
- this.push("status", "type /fix to send failures to the engine");
716
+ this.addProblem("gates", failures.map((f) => f.gate.id).join(" + "), buildGatePrompt(failures));
717
+ this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
613
718
  } else {
719
+ this.clearProblems("gates");
614
720
  this.push("status", "all gates passed");
615
721
  }
616
722
  })();
@@ -754,7 +860,7 @@ ${result.a11y.slice(0, 5).join("\n")}`);
754
860
  case "help":
755
861
  this.push(
756
862
  "status",
757
- "/engine <id> \xB7 /model <name> \xB7 /mode plan|safe|yolo \xB7 /dev \xB7 /check (gates) \xB7 /fix \xB7 /shot \xB7 /review [focus] \xB7 /variants <2-4> <ask> \xB7 /undo \xB7 /checkpoints \xB7 /restore <n> \xB7 /resume \xB7 /clear \xB7 /quit"
863
+ "/engine <id> \xB7 /model <name> \xB7 /mode plan|safe|yolo \xB7 /dev \xB7 /check (gates) \xB7 /problems \xB7 /fix [n] \xB7 /shot \xB7 /review [focus] \xB7 /variants <2-4> <ask> \xB7 /undo \xB7 /checkpoints \xB7 /restore <n> \xB7 /copy (last reply) \xB7 /resume \xB7 /clear \xB7 /quit"
758
864
  );
759
865
  break;
760
866
  case "quit":
@@ -1225,6 +1331,11 @@ function App({
1225
1331
  return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", paddingX: 1, children: [
1226
1332
  /* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
1227
1333
  state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
1334
+ state.items.length === 0 && state.liveText.length === 0 && !state.running && /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", marginTop: 1, children: [
1335
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: "describe what to build \u2014 or try:" }),
1336
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " /dev start the preview server \xB7 /review after a change \xB7 /variants 3 explore wide" }),
1337
+ /* @__PURE__ */ jsx3(Text3, { color: theme2.dim, children: " shift+tab cycles plan/safe/yolo \xB7 type while the agent works to queue asks" })
1338
+ ] }),
1228
1339
  state.running && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
1229
1340
  state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
1230
1341
  "\u22EF queued: ",
@@ -1256,14 +1367,21 @@ function App({
1256
1367
  path3.basename(cwd),
1257
1368
  devBadge,
1258
1369
  totalsBadge,
1259
- " \xB7 shift+tab mode \xB7 /help"
1370
+ state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
1371
+ " \xB7 ",
1372
+ state.problems.length,
1373
+ " problem",
1374
+ state.problems.length === 1 ? "" : "s"
1375
+ ] }),
1376
+ " ",
1377
+ "\xB7 shift+tab mode \xB7 /help"
1260
1378
  ] }) })
1261
1379
  ] }) });
1262
1380
  }
1263
1381
 
1264
1382
  // src/cli.tsx
1265
1383
  import { jsx as jsx4 } from "react/jsx-runtime";
1266
- var VERSION = "0.2.1";
1384
+ var VERSION = "0.2.2";
1267
1385
  var program = new Command();
1268
1386
  program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
1269
1387
  program.command("run").description("Run one prompt headlessly and stream the result").argument("<prompt...>", "what to build or change").option("-e, --engine <id>", "engine to use (claude, codex, gemini, opencode)").option("-m, --model <name>", "model override for the engine").option("--no-brief", "send the prompt without the squint design brief").option("--json", "emit normalized agent events as ndjson").action(
@@ -1330,7 +1448,7 @@ program.command("doctor").description("Check squint prerequisites and engine ava
1330
1448
  }
1331
1449
  }
1332
1450
  const { findChrome: findChrome2 } = await import("./chrome-RD7XQ767.js");
1333
- const { hasWebSocket } = await import("./cdp-RTWPFIX5.js");
1451
+ const { hasWebSocket } = await import("./cdp-SFWNP7OA.js");
1334
1452
  const chrome = findChrome2();
1335
1453
  console.log(
1336
1454
  chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
@@ -1450,7 +1568,7 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
1450
1568
  process.exitCode = 1;
1451
1569
  return;
1452
1570
  }
1453
- const { isGitRepo: isGitRepo2 } = await import("./snapshot-KKUY5RR6.js");
1571
+ const { isGitRepo: isGitRepo2 } = await import("./snapshot-H6VYFHLN.js");
1454
1572
  if (!isGitRepo2(cwd)) {
1455
1573
  console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
1456
1574
  process.exitCode = 1;
@@ -1560,7 +1678,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
1560
1678
  if (results.some((r) => !r.ok)) process.exitCode = 1;
1561
1679
  });
1562
1680
  program.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
1563
- const { captureViewports: captureViewports2 } = await import("./preview-XQ7EOHV4.js");
1681
+ const { captureViewports: captureViewports2 } = await import("./preview-JJJLDF7H.js");
1564
1682
  const result = await captureViewports2(process.cwd(), url);
1565
1683
  if (!result) {
1566
1684
  console.error(pc.red("\u2717 no Chrome/Chromium found"));
@@ -4,11 +4,12 @@ import {
4
4
  buildReviewPrompt,
5
5
  buildRuntimeFixPrompt,
6
6
  captureViewports,
7
+ comparePulse,
7
8
  previewDir,
8
9
  probeRuntime,
9
10
  runtimeSummary
10
- } from "./chunk-REDPR3KI.js";
11
- import "./chunk-VJGE7HSP.js";
11
+ } from "./chunk-73ULM6HF.js";
12
+ import "./chunk-OC6RU6XH.js";
12
13
  import "./chunk-P3V5CWH5.js";
13
14
  import "./chunk-2LRIKWBU.js";
14
15
  export {
@@ -16,6 +17,7 @@ export {
16
17
  buildReviewPrompt,
17
18
  buildRuntimeFixPrompt,
18
19
  captureViewports,
20
+ comparePulse,
19
21
  previewDir,
20
22
  probeRuntime,
21
23
  runtimeSummary
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ diffStatSince,
3
4
  isGitRepo,
4
5
  restoreSnapshot,
5
6
  takeSnapshot
6
- } from "./chunk-WTA6YYBY.js";
7
+ } from "./chunk-YHRAOBI2.js";
7
8
  export {
9
+ diffStatSince,
8
10
  isGitRepo,
9
11
  restoreSnapshot,
10
12
  takeSnapshot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aayambansal/squint",
3
- "version": "0.2.1",
3
+ "version": "0.2.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",
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- cdpCapture,
4
- hasWebSocket
5
- } from "./chunk-VJGE7HSP.js";
6
- export {
7
- cdpCapture,
8
- hasWebSocket
9
- };