@aayambansal/squint 0.3.0 → 0.3.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 +9 -3
- package/dist/{cdp-4IZHB3W5.js → cdp-2XAU5MEA.js} +1 -1
- package/dist/{chunk-QEENJGCL.js → chunk-DAETGO2M.js} +37 -1
- package/dist/{chunk-RQFFAJFE.js → chunk-OTCH66M7.js} +4 -4
- package/dist/{chunk-CFWKEIJJ.js → chunk-S2ODU4MN.js} +2 -0
- package/dist/cli.js +187 -34
- package/dist/{commands-QKDWUD4K.js → commands-Y5RUKBPS.js} +1 -1
- package/dist/{preview-56UCV5A3.js → preview-PS7WJ2KO.js} +2 -2
- package/dist/tokens-XYGRRAXC.js +104 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -158,9 +158,15 @@ squint doctor --probe # run every engine end to end, verify auth act
|
|
|
158
158
|
- **Commands**: type `/` and matching commands appear with descriptions; tab completes.
|
|
159
159
|
`/dev` `/check` `/problems` `/fix [n]` `/shot` `/review [focus]` `/variants` `/undo`
|
|
160
160
|
`/checkpoints` `/restore <n>` `/mode` `/theme` `/copy` `/save` `/resume` `/clear`.
|
|
161
|
-
- **
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
- **The harness sees, in your terminal**: on kitty/Ghostty/WezTerm/iTerm2, pulse and
|
|
162
|
+
capture screenshots render as real pixels inside the transcript. Every clean turn is
|
|
163
|
+
pixel-compared with the last (drift as a number), load performance is tracked with
|
|
164
|
+
deltas (`perf: LCP 812ms (+420ms)`), hardcoded colors get pointed at the nearest
|
|
165
|
+
design token, and the mechanical anti-slop sweep flags generic-AI tells as
|
|
166
|
+
distinctiveness debt in `/review`.
|
|
167
|
+
- **`/btw <question>`** asks about the codebase read-only without touching the main
|
|
168
|
+
thread's context. `.squint/locks` lists paths the engine must never touch; `/save`
|
|
169
|
+
exports the transcript as markdown.
|
|
164
170
|
- Assistant output renders as markdown; the done line measures real work via git
|
|
165
171
|
(`3 files +42 −7`); the footer tracks session turns and cost; a bell rings when a
|
|
166
172
|
turn finishes.
|
|
@@ -5,6 +5,32 @@ import { spawn } from "child_process";
|
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import os from "os";
|
|
7
7
|
import path from "path";
|
|
8
|
+
var PERF_PROBE = `(() => {
|
|
9
|
+
const out = {};
|
|
10
|
+
const buffered = (type) => {
|
|
11
|
+
try {
|
|
12
|
+
const po = new PerformanceObserver(() => {});
|
|
13
|
+
po.observe({ type, buffered: true });
|
|
14
|
+
const records = po.takeRecords();
|
|
15
|
+
po.disconnect();
|
|
16
|
+
return records;
|
|
17
|
+
} catch { return []; }
|
|
18
|
+
};
|
|
19
|
+
const lcp = buffered('largest-contentful-paint');
|
|
20
|
+
if (lcp.length > 0) out.lcpMs = Math.round(lcp[lcp.length - 1].startTime);
|
|
21
|
+
let cls = 0;
|
|
22
|
+
for (const e of buffered('layout-shift')) { if (!e.hadRecentInput) cls += e.value; }
|
|
23
|
+
out.cls = Math.round(cls * 1000) / 1000;
|
|
24
|
+
try {
|
|
25
|
+
const resources = performance.getEntriesByType('resource');
|
|
26
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
27
|
+
let bytes = nav ? (nav.transferSize || 0) : 0;
|
|
28
|
+
for (const r of resources) bytes += r.transferSize || 0;
|
|
29
|
+
out.transferBytes = bytes;
|
|
30
|
+
out.requests = resources.length + (nav ? 1 : 0);
|
|
31
|
+
} catch {}
|
|
32
|
+
return out;
|
|
33
|
+
})()`;
|
|
8
34
|
var A11Y_AUDIT = `(() => {
|
|
9
35
|
const out = [];
|
|
10
36
|
const name = (el) => (el.getAttribute('aria-label') || el.textContent || el.getAttribute('title') || '').trim();
|
|
@@ -251,6 +277,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
251
277
|
const shots = [];
|
|
252
278
|
let a11y = [];
|
|
253
279
|
let slop = [];
|
|
280
|
+
let perf = {};
|
|
254
281
|
const requests = /* @__PURE__ */ new Map();
|
|
255
282
|
let connection = null;
|
|
256
283
|
try {
|
|
@@ -295,6 +322,15 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
295
322
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
296
323
|
}
|
|
297
324
|
await new Promise((resolve) => setTimeout(resolve, settleMs));
|
|
325
|
+
try {
|
|
326
|
+
const { result } = await connection.send(
|
|
327
|
+
"Runtime.evaluate",
|
|
328
|
+
{ expression: PERF_PROBE, returnByValue: true },
|
|
329
|
+
sessionId
|
|
330
|
+
);
|
|
331
|
+
if (result?.value && typeof result.value === "object") perf = result.value;
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
298
334
|
if (audit) {
|
|
299
335
|
try {
|
|
300
336
|
const { result } = await connection.send(
|
|
@@ -337,7 +373,7 @@ async function cdpCapture(chromePath, url, outDir, viewports, settleMs = 2500, a
|
|
|
337
373
|
child.kill("SIGKILL");
|
|
338
374
|
setTimeout(() => fs.rmSync(profileDir, { recursive: true, force: true }), 500).unref?.();
|
|
339
375
|
}
|
|
340
|
-
return { report, shots, a11y, slop };
|
|
376
|
+
return { report, shots, a11y, slop, perf };
|
|
341
377
|
}
|
|
342
378
|
|
|
343
379
|
export {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
import {
|
|
7
7
|
cdpCapture,
|
|
8
8
|
hasWebSocket
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-DAETGO2M.js";
|
|
10
10
|
import {
|
|
11
11
|
ensureSquintIgnore
|
|
12
12
|
} from "./chunk-O2S6PAJE.js";
|
|
@@ -118,14 +118,14 @@ async function probeRuntime(url, cwd) {
|
|
|
118
118
|
if (!chrome || !hasWebSocket()) return null;
|
|
119
119
|
try {
|
|
120
120
|
const dir = cwd ? previewDir(cwd) : os.tmpdir();
|
|
121
|
-
const { report, shots } = await cdpCapture(
|
|
121
|
+
const { report, shots, perf } = await cdpCapture(
|
|
122
122
|
chrome,
|
|
123
123
|
url,
|
|
124
124
|
dir,
|
|
125
125
|
cwd ? [{ name: "pulse", width: 1280, height: 800 }] : [],
|
|
126
126
|
1500
|
|
127
127
|
);
|
|
128
|
-
return { report, pulsePath: shots[0]?.path };
|
|
128
|
+
return { report, pulsePath: shots[0]?.path, perf };
|
|
129
129
|
} catch {
|
|
130
130
|
return null;
|
|
131
131
|
}
|
|
@@ -133,7 +133,7 @@ async function probeRuntime(url, cwd) {
|
|
|
133
133
|
async function comparePulse(previous, current) {
|
|
134
134
|
const chrome = findChrome();
|
|
135
135
|
if (!chrome || !hasWebSocket()) return null;
|
|
136
|
-
const { pixelDiffPct } = await import("./cdp-
|
|
136
|
+
const { pixelDiffPct } = await import("./cdp-2XAU5MEA.js");
|
|
137
137
|
return pixelDiffPct(chrome, previous, current);
|
|
138
138
|
}
|
|
139
139
|
function buildRuntimeFixPrompt(report) {
|
|
@@ -13,12 +13,14 @@ var COMMANDS = [
|
|
|
13
13
|
{ name: "fix", args: "[n]", group: "verify", description: "send all open problems to the engine, or just problem n" },
|
|
14
14
|
{ name: "shot", args: "[url]", group: "verify", description: "screenshot the app (or any url) at mobile/tablet/desktop" },
|
|
15
15
|
{ name: "review", args: "[focus]", group: "verify", description: "screenshots + the engine critiques its own rendered work" },
|
|
16
|
+
{ name: "polish", args: "[1-5]", group: "verify", description: "unattended rounds of review \u2192 fix (default 2)" },
|
|
16
17
|
{ name: "variants", args: "<2-4> <ask>", group: "explore", description: "parallel design explorations; apply/list/clean" },
|
|
17
18
|
{ name: "sandbox", args: "[on|diff|apply|discard]", group: "explore", description: "asks accumulate in a shadow worktree until you apply" },
|
|
18
19
|
{ name: "undo", group: "explore", description: "revert the last ask (files only)" },
|
|
19
20
|
{ name: "checkpoints", group: "explore", description: "list per-ask checkpoints" },
|
|
20
21
|
{ name: "restore", args: "<n>", group: "explore", description: "rewind files to before ask n" },
|
|
21
22
|
{ name: "theme", args: "[name]", group: "session", description: "switch the TUI theme", viewLevel: true },
|
|
23
|
+
{ name: "btw", args: "<question>", group: "session", description: "read-only side question; the main thread is untouched" },
|
|
22
24
|
{ name: "copy", group: "session", description: "copy the last reply to the clipboard" },
|
|
23
25
|
{ name: "save", group: "session", description: "export the transcript to .squint/transcripts/" },
|
|
24
26
|
{ name: "resume", group: "session", description: "pick up the previous session for this repo" },
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
completeCommand
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-S2ODU4MN.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-
|
|
45
|
+
} from "./chunk-OTCH66M7.js";
|
|
46
46
|
import {
|
|
47
47
|
runAgent
|
|
48
48
|
} from "./chunk-VH7OOFQP.js";
|
|
@@ -53,7 +53,7 @@ import {
|
|
|
53
53
|
detectEngines,
|
|
54
54
|
getEngine
|
|
55
55
|
} from "./chunk-KVYGPLWW.js";
|
|
56
|
-
import "./chunk-
|
|
56
|
+
import "./chunk-DAETGO2M.js";
|
|
57
57
|
import {
|
|
58
58
|
enrich
|
|
59
59
|
} from "./chunk-4LAU5TGK.js";
|
|
@@ -236,7 +236,7 @@ function registerEnv(program2) {
|
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
const { findChrome: findChrome2 } = await import("./chrome-SBV3H77F.js");
|
|
239
|
-
const { hasWebSocket } = await import("./cdp-
|
|
239
|
+
const { hasWebSocket } = await import("./cdp-2XAU5MEA.js");
|
|
240
240
|
const chrome = findChrome2();
|
|
241
241
|
console.log(
|
|
242
242
|
chrome ? `${pc2.green("\u2713")} Chrome ${pc2.dim(chrome)}` : `${pc2.yellow("\u25CB")} Chrome ${pc2.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
@@ -276,22 +276,22 @@ function registerProject(program2) {
|
|
|
276
276
|
}
|
|
277
277
|
});
|
|
278
278
|
skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
|
|
279
|
-
const
|
|
279
|
+
const fs3 = await import("fs");
|
|
280
280
|
const nodePath = await import("path");
|
|
281
281
|
const cwd = process.cwd();
|
|
282
282
|
const skillsDir = nodePath.join(cwd, ".squint", "skills");
|
|
283
|
-
|
|
283
|
+
fs3.mkdirSync(skillsDir, { recursive: true });
|
|
284
284
|
const rules = nodePath.join(cwd, ".squint", "rules.md");
|
|
285
|
-
if (!
|
|
286
|
-
|
|
285
|
+
if (!fs3.existsSync(rules)) {
|
|
286
|
+
fs3.writeFileSync(
|
|
287
287
|
rules,
|
|
288
288
|
"# 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"
|
|
289
289
|
);
|
|
290
290
|
console.log(pc3.green("\u2713 .squint/rules.md"));
|
|
291
291
|
}
|
|
292
292
|
const example = nodePath.join(skillsDir, "example.md");
|
|
293
|
-
if (!
|
|
294
|
-
|
|
293
|
+
if (!fs3.existsSync(example)) {
|
|
294
|
+
fs3.writeFileSync(
|
|
295
295
|
example,
|
|
296
296
|
"---\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"
|
|
297
297
|
);
|
|
@@ -300,7 +300,7 @@ function registerProject(program2) {
|
|
|
300
300
|
console.log(pc3.dim("rules are always-on; skills inject when an ask mentions a trigger"));
|
|
301
301
|
});
|
|
302
302
|
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) => {
|
|
303
|
-
const
|
|
303
|
+
const fs3 = await import("fs");
|
|
304
304
|
const nodePath = await import("path");
|
|
305
305
|
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
|
|
306
306
|
if (!familyId) {
|
|
@@ -318,13 +318,13 @@ function registerProject(program2) {
|
|
|
318
318
|
return;
|
|
319
319
|
}
|
|
320
320
|
const target = nodePath.join(process.cwd(), ".squint", "brief.md");
|
|
321
|
-
if (
|
|
321
|
+
if (fs3.existsSync(target) && !options.force) {
|
|
322
322
|
console.error(pc3.red(`\u2717 ${target} exists \u2014 use --force to overwrite`));
|
|
323
323
|
process.exitCode = 1;
|
|
324
324
|
return;
|
|
325
325
|
}
|
|
326
|
-
|
|
327
|
-
|
|
326
|
+
fs3.mkdirSync(nodePath.dirname(target), { recursive: true });
|
|
327
|
+
fs3.writeFileSync(target, renderFamilyBrief(family) + "\n");
|
|
328
328
|
console.log(pc3.green(`\u2713 ${family.name} direction written to .squint/brief.md`));
|
|
329
329
|
console.log(pc3.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
|
|
330
330
|
});
|
|
@@ -459,7 +459,7 @@ function registerQuality(program2) {
|
|
|
459
459
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
460
460
|
});
|
|
461
461
|
program2.command("shot").description("Screenshot a running app at mobile/tablet/desktop viewports (+ .squint/routes)").argument("<url>", "URL of the running app (e.g. http://localhost:5173)").action(async (url) => {
|
|
462
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
462
|
+
const { captureViewports: captureViewports2 } = await import("./preview-PS7WJ2KO.js");
|
|
463
463
|
const result = await captureViewports2(process.cwd(), url);
|
|
464
464
|
if (!result) {
|
|
465
465
|
console.error(pc4.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -577,21 +577,21 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
|
577
577
|
console.log(pc6.dim("then describe what to build \u2014 /dev starts the preview server"));
|
|
578
578
|
});
|
|
579
579
|
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 () => {
|
|
580
|
-
const
|
|
580
|
+
const fs3 = await import("fs");
|
|
581
581
|
const nodePath = await import("path");
|
|
582
582
|
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
|
|
583
583
|
const cwd = process.cwd();
|
|
584
584
|
const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
|
|
585
|
-
|
|
585
|
+
fs3.writeFileSync(taggerPath, TAGGER_SOURCE);
|
|
586
586
|
console.log(pc6.green(`\u2713 ${TAGGER_FILENAME} written`));
|
|
587
|
-
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) =>
|
|
587
|
+
const configPath = ["vite.config.ts", "vite.config.js", "vite.config.mjs"].map((name) => nodePath.join(cwd, name)).find((candidate) => fs3.existsSync(candidate));
|
|
588
588
|
if (!configPath) {
|
|
589
589
|
console.log(pc6.yellow("\u25CB no vite config found \u2014 add the plugin manually:"));
|
|
590
590
|
console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
591
591
|
plugins: [squintTagger(), \u2026]`));
|
|
592
592
|
return;
|
|
593
593
|
}
|
|
594
|
-
const source =
|
|
594
|
+
const source = fs3.readFileSync(configPath, "utf8");
|
|
595
595
|
const patched = patchViteConfig(source);
|
|
596
596
|
if (patched === "already") {
|
|
597
597
|
console.log(pc6.dim("vite config already wired"));
|
|
@@ -600,7 +600,7 @@ Next: ${pc6.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
|
600
600
|
console.log(pc6.dim(` import squintTagger from './${TAGGER_FILENAME}'
|
|
601
601
|
plugins: [squintTagger(), \u2026]`));
|
|
602
602
|
} else {
|
|
603
|
-
|
|
603
|
+
fs3.writeFileSync(configPath, patched);
|
|
604
604
|
console.log(pc6.green(`\u2713 ${nodePath.basename(configPath)} wired`));
|
|
605
605
|
}
|
|
606
606
|
console.log(
|
|
@@ -615,11 +615,47 @@ import { render } from "ink";
|
|
|
615
615
|
// src/tui/App.tsx
|
|
616
616
|
import { Box as Box3, Static, Text as Text3, useApp, useInput } from "ink";
|
|
617
617
|
import { InkPictureProvider } from "ink-picture";
|
|
618
|
-
import
|
|
618
|
+
import path4 from "path";
|
|
619
619
|
import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
|
|
620
620
|
|
|
621
621
|
// src/session/engine.ts
|
|
622
|
+
import path3 from "path";
|
|
623
|
+
|
|
624
|
+
// src/session/hooks.ts
|
|
625
|
+
import { spawn } from "child_process";
|
|
626
|
+
import fs2 from "fs";
|
|
622
627
|
import path2 from "path";
|
|
628
|
+
function runHook(cwd, event, payload) {
|
|
629
|
+
const script = path2.join(cwd, ".squint", "hooks", event);
|
|
630
|
+
try {
|
|
631
|
+
fs2.accessSync(script, fs2.constants.X_OK);
|
|
632
|
+
} catch {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
const child = spawn(script, [], {
|
|
637
|
+
cwd,
|
|
638
|
+
env: { ...process.env, SQUINT_EVENT: event, ...prefixed(payload) },
|
|
639
|
+
stdio: "ignore",
|
|
640
|
+
detached: false
|
|
641
|
+
});
|
|
642
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 1e4);
|
|
643
|
+
child.on("close", () => clearTimeout(timer));
|
|
644
|
+
child.on("error", () => clearTimeout(timer));
|
|
645
|
+
return true;
|
|
646
|
+
} catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function prefixed(payload) {
|
|
651
|
+
const out = {};
|
|
652
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
653
|
+
out[`SQUINT_${key.toUpperCase()}`] = value;
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/session/engine.ts
|
|
623
659
|
var MAX_AUTO_FIX_ATTEMPTS = 2;
|
|
624
660
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
625
661
|
var Session = class {
|
|
@@ -672,6 +708,7 @@ var Session = class {
|
|
|
672
708
|
fixAttempts = 0;
|
|
673
709
|
reviewTipShown = false;
|
|
674
710
|
lastPulse = null;
|
|
711
|
+
lastPerf = null;
|
|
675
712
|
autoReviewedThisAsk = false;
|
|
676
713
|
startedAt = Date.now();
|
|
677
714
|
subscribe(listener) {
|
|
@@ -701,6 +738,7 @@ var Session = class {
|
|
|
701
738
|
this.nextProblemId += 1;
|
|
702
739
|
this.problemPrompts.set(this.nextProblemId, prompt);
|
|
703
740
|
this.notify({ problems: [...kept, { id: this.nextProblemId, source, summary }] });
|
|
741
|
+
runHook(this.opts.cwd, "on-problem", { source, summary });
|
|
704
742
|
}
|
|
705
743
|
clearProblems(source) {
|
|
706
744
|
const removed = this.state.problems.filter((p) => p.source === source);
|
|
@@ -733,6 +771,61 @@ var Session = class {
|
|
|
733
771
|
this.dispatchFix([...this.state.problems]);
|
|
734
772
|
return true;
|
|
735
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* A side question: read-only, no session resume, so the main thread's
|
|
776
|
+
* context is untouched (Cursor's /btw). Costs count; loops don't run.
|
|
777
|
+
*/
|
|
778
|
+
async btw(question) {
|
|
779
|
+
this.push("user", `\u{1F4AC} btw: ${question}`);
|
|
780
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
781
|
+
const engine = getEngine(this.state.engineId);
|
|
782
|
+
this.abort = new AbortController();
|
|
783
|
+
const result = await runAgent(
|
|
784
|
+
engine,
|
|
785
|
+
{
|
|
786
|
+
prompt: `Answer this question about the repository. Investigate as needed but make no changes:
|
|
787
|
+
|
|
788
|
+
${question}`,
|
|
789
|
+
cwd: this.execCwd(),
|
|
790
|
+
model: this.state.model,
|
|
791
|
+
mode: "plan"
|
|
792
|
+
},
|
|
793
|
+
this.handleEvent,
|
|
794
|
+
this.abort.signal
|
|
795
|
+
);
|
|
796
|
+
this.abort = null;
|
|
797
|
+
this.commitLive();
|
|
798
|
+
this.flushToolCollapse();
|
|
799
|
+
if (result.ok) {
|
|
800
|
+
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
801
|
+
this.push("status", `btw answered${cost}`);
|
|
802
|
+
this.notify({
|
|
803
|
+
totals: { costUsd: this.state.totals.costUsd + (result.costUsd ?? 0), turns: this.state.totals.turns }
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
this.notify({ running: false });
|
|
807
|
+
this.drainQueue();
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Unattended polish: n rounds of screenshot-review-fix. Each round is
|
|
811
|
+
* a full review turn (its own cost); the per-ask auto-fix cap resets
|
|
812
|
+
* per round so fixes still flow. Fire it and step away.
|
|
813
|
+
*/
|
|
814
|
+
async polish(rounds) {
|
|
815
|
+
for (let round = 1; round <= rounds; round++) {
|
|
816
|
+
this.fixAttempts = 0;
|
|
817
|
+
const result = await this.capture();
|
|
818
|
+
if (!result) {
|
|
819
|
+
this.push("status", `polish stopped at round ${round}: nothing to capture`);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
await this.runTurn(
|
|
823
|
+
buildReviewPrompt(result.shots, void 0, result.runtime, result.a11y, result.slop),
|
|
824
|
+
`\u{1F441} polish round ${round}/${rounds}`
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
this.push("status", `polish complete \u2014 ${rounds} round${rounds === 1 ? "" : "s"} of review and fixes`);
|
|
828
|
+
}
|
|
736
829
|
setMode(mode) {
|
|
737
830
|
this.notify({ mode });
|
|
738
831
|
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";
|
|
@@ -943,6 +1036,23 @@ var Session = class {
|
|
|
943
1036
|
const stat = checkpoint ? diffStatSince(this.opts.cwd, checkpoint.snapshot) : null;
|
|
944
1037
|
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"}` : "";
|
|
945
1038
|
this.push("status", `done${secs}${cost}${work}`);
|
|
1039
|
+
if (checkpoint) {
|
|
1040
|
+
try {
|
|
1041
|
+
const { driftSummary, loadTokenIndex, scanDrift } = await import("./tokens-XYGRRAXC.js");
|
|
1042
|
+
const index = loadTokenIndex(this.execCwd());
|
|
1043
|
+
const drift = scanDrift(this.execCwd(), checkpoint.snapshot.stashHash ?? "HEAD", index);
|
|
1044
|
+
if (drift.length > 0) {
|
|
1045
|
+
this.push("status", `token drift: ${drift.length} hardcoded color(s)
|
|
1046
|
+
${driftSummary(drift)}`);
|
|
1047
|
+
}
|
|
1048
|
+
} catch {
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
runHook(this.opts.cwd, "on-turn-end", {
|
|
1052
|
+
cost: String(result.costUsd ?? 0),
|
|
1053
|
+
duration_ms: String(result.durationMs ?? 0),
|
|
1054
|
+
stat: stat ?? ""
|
|
1055
|
+
});
|
|
946
1056
|
const before = this.state.totals.costUsd;
|
|
947
1057
|
this.notify({
|
|
948
1058
|
totals: {
|
|
@@ -956,6 +1066,7 @@ var Session = class {
|
|
|
956
1066
|
"error",
|
|
957
1067
|
`session cost $${this.state.totals.costUsd.toFixed(2)} crossed your $${budget.toFixed(2)} budget \u2014 squint keeps working, this is just the flag you asked for`
|
|
958
1068
|
);
|
|
1069
|
+
runHook(this.opts.cwd, "on-budget", { total: this.state.totals.costUsd.toFixed(2), budget: budget.toFixed(2) });
|
|
959
1070
|
}
|
|
960
1071
|
if (this.sessionId) {
|
|
961
1072
|
saveState(this.opts.cwd, {
|
|
@@ -1018,6 +1129,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1018
1129
|
this.push("status", "/fix sends open problems to the engine \xB7 /problems lists them");
|
|
1019
1130
|
} else if (probe) {
|
|
1020
1131
|
this.clearProblems("runtime");
|
|
1132
|
+
this.perfPulse(probe.perf);
|
|
1021
1133
|
const pct = await this.visualPulse(probe.pulsePath);
|
|
1022
1134
|
if (this.opts.autoReview && pct !== null && pct >= 10 && !this.autoReviewedThisAsk) {
|
|
1023
1135
|
this.autoReviewedThisAsk = true;
|
|
@@ -1083,6 +1195,25 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1083
1195
|
this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
|
|
1084
1196
|
}
|
|
1085
1197
|
}
|
|
1198
|
+
/** Cross-turn load-performance deltas: the perf twin of the visual pulse. */
|
|
1199
|
+
perfPulse(perf) {
|
|
1200
|
+
if (!perf || perf.lcpMs === void 0 && perf.transferBytes === void 0) return;
|
|
1201
|
+
const previous = this.lastPerf;
|
|
1202
|
+
this.lastPerf = perf;
|
|
1203
|
+
const mb = (bytes) => `${(bytes / 1024 / 1024).toFixed(1)}MB`;
|
|
1204
|
+
const parts = [];
|
|
1205
|
+
if (perf.lcpMs !== void 0) {
|
|
1206
|
+
const delta = previous?.lcpMs !== void 0 && Math.abs(perf.lcpMs - previous.lcpMs) >= 100 ? ` (${perf.lcpMs > previous.lcpMs ? "+" : "\u2212"}${Math.abs(perf.lcpMs - previous.lcpMs)}ms)` : "";
|
|
1207
|
+
parts.push(`LCP ${perf.lcpMs}ms${delta}`);
|
|
1208
|
+
}
|
|
1209
|
+
if (perf.cls !== void 0 && perf.cls > 0.05) parts.push(`CLS ${perf.cls}`);
|
|
1210
|
+
if (perf.transferBytes !== void 0) {
|
|
1211
|
+
const delta = previous?.transferBytes !== void 0 && Math.abs(perf.transferBytes - previous.transferBytes) > 100 * 1024 ? ` (${perf.transferBytes > previous.transferBytes ? "+" : "\u2212"}${mb(Math.abs(perf.transferBytes - previous.transferBytes))})` : "";
|
|
1212
|
+
parts.push(`${mb(perf.transferBytes)}${delta}`);
|
|
1213
|
+
}
|
|
1214
|
+
if (perf.requests !== void 0) parts.push(`${perf.requests} req`);
|
|
1215
|
+
if (parts.length > 0) this.push("status", `perf: ${parts.join(" \xB7 ")}`);
|
|
1216
|
+
}
|
|
1086
1217
|
/**
|
|
1087
1218
|
* Cross-turn visual drift check: compare this turn's pulse screenshot
|
|
1088
1219
|
* with the previous one and report how much of the page changed.
|
|
@@ -1109,6 +1240,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1109
1240
|
"status",
|
|
1110
1241
|
pct < 0.5 ? "visual pulse: stable vs last turn" : `visual pulse: ${pct.toFixed(1)}% of the page changed vs last turn`
|
|
1111
1242
|
);
|
|
1243
|
+
runHook(this.opts.cwd, "on-pulse-diff", { pct: pct.toFixed(1) });
|
|
1112
1244
|
if (pct >= 0.5) this.push("image", pulsePath);
|
|
1113
1245
|
return pct;
|
|
1114
1246
|
}
|
|
@@ -1129,7 +1261,7 @@ ${errors.slice(-5).join("\n")}`);
|
|
|
1129
1261
|
if (result.shots.length > 0) {
|
|
1130
1262
|
this.push(
|
|
1131
1263
|
"status",
|
|
1132
|
-
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${
|
|
1264
|
+
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path3.dirname(result.shots[0].path)}`
|
|
1133
1265
|
);
|
|
1134
1266
|
const desktop = result.shots.find((s) => s.name === "desktop") ?? result.shots[0];
|
|
1135
1267
|
this.push("image", desktop.path);
|
|
@@ -1262,12 +1394,12 @@ They are objective defects, not style preferences.`
|
|
|
1262
1394
|
}
|
|
1263
1395
|
case "save": {
|
|
1264
1396
|
void (async () => {
|
|
1265
|
-
const
|
|
1266
|
-
const
|
|
1267
|
-
const dir =
|
|
1268
|
-
|
|
1397
|
+
const fs3 = await import("fs");
|
|
1398
|
+
const path5 = await import("path");
|
|
1399
|
+
const dir = path5.join(this.opts.cwd, ".squint", "transcripts");
|
|
1400
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1269
1401
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
1270
|
-
const file =
|
|
1402
|
+
const file = path5.join(dir, `${stamp}.md`);
|
|
1271
1403
|
const lines = [`# squint session \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16)}`, ""];
|
|
1272
1404
|
for (const item of this.state.items) {
|
|
1273
1405
|
switch (item.role) {
|
|
@@ -1290,19 +1422,40 @@ They are objective defects, not style preferences.`
|
|
|
1290
1422
|
}
|
|
1291
1423
|
}
|
|
1292
1424
|
lines.push("", `> ${this.summary()}`);
|
|
1293
|
-
|
|
1294
|
-
this.push("status", `saved transcript \u2192 ${
|
|
1425
|
+
fs3.writeFileSync(file, lines.join("\n") + "\n");
|
|
1426
|
+
this.push("status", `saved transcript \u2192 ${path5.relative(this.opts.cwd, file)}`);
|
|
1295
1427
|
})();
|
|
1296
1428
|
break;
|
|
1297
1429
|
}
|
|
1430
|
+
case "polish": {
|
|
1431
|
+
const rounds = arg ? Number.parseInt(arg, 10) : 2;
|
|
1432
|
+
if (!Number.isInteger(rounds) || rounds < 1 || rounds > 5) {
|
|
1433
|
+
this.push("status", "usage: /polish [1-5] \u2014 rounds of screenshot \u2192 critique \u2192 fix (each costs a turn)");
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
if (!this.state.devUrl) {
|
|
1437
|
+
this.push("error", "dev server not running \u2014 /dev first");
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
this.push("status", `polishing: ${rounds} round${rounds === 1 ? "" : "s"} of review \u2192 fix`);
|
|
1441
|
+
void this.polish(rounds);
|
|
1442
|
+
break;
|
|
1443
|
+
}
|
|
1444
|
+
case "btw":
|
|
1445
|
+
if (!arg) {
|
|
1446
|
+
this.push("status", "usage: /btw <question> \u2014 read-only side question, main thread untouched");
|
|
1447
|
+
} else {
|
|
1448
|
+
void this.btw(arg);
|
|
1449
|
+
}
|
|
1450
|
+
break;
|
|
1298
1451
|
case "copy": {
|
|
1299
1452
|
const last = this.state.items.findLast((i) => i.role === "assistant");
|
|
1300
1453
|
if (!last) {
|
|
1301
1454
|
this.push("status", "nothing to copy yet");
|
|
1302
1455
|
break;
|
|
1303
1456
|
}
|
|
1304
|
-
void import("child_process").then(({ spawn }) => {
|
|
1305
|
-
const cmd = process.platform === "darwin" ?
|
|
1457
|
+
void import("child_process").then(({ spawn: spawn2 }) => {
|
|
1458
|
+
const cmd = process.platform === "darwin" ? spawn2("pbcopy") : process.platform === "win32" ? spawn2("clip") : spawn2("xclip", ["-selection", "clipboard"]);
|
|
1306
1459
|
cmd.on("error", () => this.push("error", "no clipboard tool found"));
|
|
1307
1460
|
cmd.on("close", (code) => {
|
|
1308
1461
|
if (code === 0) this.push("status", `copied last reply (${last.text.length} chars)`);
|
|
@@ -1539,7 +1692,7 @@ ${sandboxFiles(this.opts.cwd).join("\n")}`);
|
|
|
1539
1692
|
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
1540
1693
|
break;
|
|
1541
1694
|
case "help": {
|
|
1542
|
-
void import("./commands-
|
|
1695
|
+
void import("./commands-Y5RUKBPS.js").then(({ commandHelp }) => this.push("status", commandHelp()));
|
|
1543
1696
|
break;
|
|
1544
1697
|
}
|
|
1545
1698
|
case "quit":
|
|
@@ -2105,7 +2258,7 @@ function App({
|
|
|
2105
2258
|
state.engineId,
|
|
2106
2259
|
state.model ? ` \xB7 ${state.model}` : "",
|
|
2107
2260
|
" \xB7 ",
|
|
2108
|
-
|
|
2261
|
+
path4.basename(cwd),
|
|
2109
2262
|
devBadge,
|
|
2110
2263
|
totalsBadge,
|
|
2111
2264
|
state.problems.length > 0 && /* @__PURE__ */ jsxs3(Text3, { color: theme2.error, children: [
|
|
@@ -2155,7 +2308,7 @@ function registerTui(program2) {
|
|
|
2155
2308
|
}
|
|
2156
2309
|
|
|
2157
2310
|
// src/cli.tsx
|
|
2158
|
-
var VERSION = true ? "0.3.
|
|
2311
|
+
var VERSION = true ? "0.3.2" : "0.0.0-dev";
|
|
2159
2312
|
var program = new Command();
|
|
2160
2313
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
2161
2314
|
registerRun(program);
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
probeRuntime,
|
|
11
11
|
routeShotName,
|
|
12
12
|
runtimeSummary
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OTCH66M7.js";
|
|
14
14
|
import "./chunk-IMDRXXFU.js";
|
|
15
15
|
import "./chunk-KVYGPLWW.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-DAETGO2M.js";
|
|
17
17
|
import "./chunk-O2S6PAJE.js";
|
|
18
18
|
export {
|
|
19
19
|
VIEWPORTS,
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/quality/tokens.ts
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
var HEX_RE = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/g;
|
|
8
|
+
var VAR_DEF_RE = /(--[a-zA-Z0-9-_]+)\s*:\s*([^;]+);/g;
|
|
9
|
+
function parseHex(hex) {
|
|
10
|
+
const raw = hex.replace("#", "");
|
|
11
|
+
const full = raw.length === 3 ? raw.split("").map((c) => c + c).join("") : raw;
|
|
12
|
+
if (full.length !== 6) return null;
|
|
13
|
+
const n = Number.parseInt(full, 16);
|
|
14
|
+
if (Number.isNaN(n)) return null;
|
|
15
|
+
return [n >> 16 & 255, n >> 8 & 255, n & 255];
|
|
16
|
+
}
|
|
17
|
+
function loadTokenIndex(cwd) {
|
|
18
|
+
const colors = /* @__PURE__ */ new Map();
|
|
19
|
+
const cssFiles = [];
|
|
20
|
+
const walk = (dir, depth) => {
|
|
21
|
+
if (depth > 4 || cssFiles.length > 30) return;
|
|
22
|
+
let entries;
|
|
23
|
+
try {
|
|
24
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
} catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
30
|
+
const full = path.join(dir, entry.name);
|
|
31
|
+
if (entry.isDirectory()) walk(full, depth + 1);
|
|
32
|
+
else if (entry.name.endsWith(".css")) cssFiles.push(full);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
walk(path.join(cwd, "src"), 0);
|
|
36
|
+
walk(cwd, 1);
|
|
37
|
+
for (const file of cssFiles.slice(0, 30)) {
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = fs.readFileSync(file, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const match of text.matchAll(VAR_DEF_RE)) {
|
|
45
|
+
const value = match[2].trim();
|
|
46
|
+
const hex = value.match(/#[0-9a-fA-F]{3,6}\b/)?.[0];
|
|
47
|
+
const rgbFn = /rgb\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/.exec(value);
|
|
48
|
+
let rgb = null;
|
|
49
|
+
if (hex) rgb = parseHex(hex);
|
|
50
|
+
else if (rgbFn) rgb = [Number(rgbFn[1]), Number(rgbFn[2]), Number(rgbFn[3])];
|
|
51
|
+
if (rgb) colors.set(match[1], { value, rgb });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { colors };
|
|
55
|
+
}
|
|
56
|
+
var distance = (a, b) => Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2 + (a[2] - b[2]) ** 2);
|
|
57
|
+
function scanDrift(cwd, source, index) {
|
|
58
|
+
if (index.colors.size === 0) return [];
|
|
59
|
+
let diff;
|
|
60
|
+
try {
|
|
61
|
+
diff = execFileSync("git", ["diff", "-U0", source, "--", "*.css", "*.tsx", "*.jsx", "*.ts", "*.html"], {
|
|
62
|
+
cwd,
|
|
63
|
+
encoding: "utf8",
|
|
64
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const findings = [];
|
|
70
|
+
let currentFile = "";
|
|
71
|
+
for (const line of diff.split("\n")) {
|
|
72
|
+
if (line.startsWith("+++ b/")) {
|
|
73
|
+
currentFile = line.slice(6);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
|
|
77
|
+
if (/--[a-zA-Z0-9-_]+\s*:/.test(line)) continue;
|
|
78
|
+
for (const match of line.matchAll(HEX_RE)) {
|
|
79
|
+
const rgb = parseHex(match[0]);
|
|
80
|
+
if (!rgb) continue;
|
|
81
|
+
let best = null;
|
|
82
|
+
for (const [token, entry] of index.colors) {
|
|
83
|
+
const d = distance(rgb, entry.rgb);
|
|
84
|
+
if (!best || d < best.d) best = { token, d };
|
|
85
|
+
}
|
|
86
|
+
if (best) {
|
|
87
|
+
findings.push({ file: currentFile, literal: match[0], token: best.token, distance: Math.round(best.d) });
|
|
88
|
+
}
|
|
89
|
+
if (findings.length >= 10) return findings;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return findings;
|
|
93
|
+
}
|
|
94
|
+
function driftSummary(findings) {
|
|
95
|
+
return findings.map(
|
|
96
|
+
(f) => `${f.file}: ${f.literal} \u2192 use var(${f.token})${f.distance === 0 ? " (exact match)" : ` (closest, \u0394${f.distance})`}`
|
|
97
|
+
).join("\n");
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
driftSummary,
|
|
101
|
+
loadTokenIndex,
|
|
102
|
+
parseHex,
|
|
103
|
+
scanDrift
|
|
104
|
+
};
|
package/package.json
CHANGED