@aayambansal/squint 0.2.0 → 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 +34 -7
- package/dist/cdp-SFWNP7OA.js +11 -0
- package/dist/{chrome-K4BKFOYS.js → chrome-RD7XQ767.js} +2 -2
- package/dist/{chunk-ZJOU46X4.js → chunk-2IW2MMCY.js} +59 -2
- package/dist/{chunk-4MMIJXYD.js → chunk-73ULM6HF.js} +19 -5
- package/dist/{chunk-7XUORM3K.js → chunk-LCS47EAG.js} +3 -5
- package/dist/{chunk-VJGE7HSP.js → chunk-OC6RU6XH.js} +50 -0
- package/dist/{chunk-FPTBBHOH.js → chunk-OTG4ZB4R.js} +1 -1
- package/dist/{chunk-5IR2AZVK.js → chunk-P3H4N2EN.js} +58 -3
- package/dist/{chunk-Z46MXU64.js → chunk-P3V5CWH5.js} +1 -1
- package/dist/{chunk-W6MYXQIU.js → chunk-VNLM53RJ.js} +60 -10
- package/dist/chunk-XZKQZKEE.js +80 -0
- package/dist/{chunk-WTA6YYBY.js → chunk-YHRAOBI2.js} +17 -0
- package/dist/cli.js +360 -93
- package/dist/{families-RVP5BWQD.js → families-2G5SLLY7.js} +1 -2
- package/dist/{init-7AYGAKOS.js → init-LSYB32NS.js} +1 -1
- package/dist/{preview-LTWJK3LF.js → preview-JJJLDF7H.js} +6 -4
- package/dist/run-XHLCTF6V.js +8 -0
- package/dist/shots-LRYYMTPK.js +12 -0
- package/dist/skills-UGHU22BS.js +15 -0
- package/dist/{snapshot-KKUY5RR6.js → snapshot-H6VYFHLN.js} +3 -1
- package/dist/{source-MEXHWVP4.js → source-ZZU245VN.js} +1 -1
- package/dist/{variants-4NCQXTP7.js → variants-7A7723L7.js} +4 -5
- package/package.json +1 -1
- package/dist/cdp-RTWPFIX5.js +0 -9
- package/dist/chunk-NT2HR4RD.js +0 -63
- package/dist/shots-VJBLDMF3.js +0 -66
- package/dist/{chunk-MCYQXOE7.js → chunk-2LRIKWBU.js} +18 -18
package/README.md
CHANGED
|
@@ -86,10 +86,11 @@ From source: `git clone https://github.com/aayambansal/squint.git && cd squint &
|
|
|
86
86
|
and catches what the server never prints: blank pages, exceptions, failed requests.
|
|
87
87
|
5. **The agent looks at its work.** `/review` screenshots mobile/tablet/desktop and
|
|
88
88
|
re-prompts the engine to critique what it can see — then fix it.
|
|
89
|
-
6. **Gates keep it honest.**
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
6. **Gates keep it honest.** Typecheck + lint run automatically after *every* turn and
|
|
90
|
+
auto-fix (capped); `/check` adds tests and the build — failures come back with orders
|
|
91
|
+
not to weaken the checks.
|
|
92
|
+
7. **Everything is reversible.** Every ask records a checkpoint; `/undo` pops the last,
|
|
93
|
+
`/restore <n>` rewinds files to any earlier point — your own uncommitted work survives.
|
|
93
94
|
8. **Point at things.** Alt+S in the browser, click any element, and a self-locating
|
|
94
95
|
reference lands on your clipboard to paste into squint.
|
|
95
96
|
9. **Explore in parallel.** `squint variants gen 3 "<ask>"` builds the same ask three ways —
|
|
@@ -122,18 +123,44 @@ squint check # gates: typecheck -> lint -> test -> build
|
|
|
122
123
|
squint shot http://localhost:5173 # screenshots at 390/768/1440
|
|
123
124
|
squint brief # list design directions
|
|
124
125
|
squint brief cinematic-dark # commit one for this repo
|
|
125
|
-
squint tag #
|
|
126
|
+
squint tag # Alt+S element picker: pin elements + notes, alt+enter copies all
|
|
126
127
|
squint variants gen 3 "<ask>" # 3 parallel design explorations
|
|
127
128
|
squint variants apply terminal # keep the winner
|
|
129
|
+
squint skills init # scaffold .squint/rules.md + a trigger-matched skill
|
|
128
130
|
squint config set engine claude
|
|
129
131
|
squint config set models.claude claude-sonnet-5
|
|
130
132
|
squint config set autoDev true # dev server starts with the TUI
|
|
131
133
|
squint config set autoFix true # errors auto-route back (max 2 tries)
|
|
134
|
+
squint config set autoCheck false # skip the per-turn typecheck+lint pass
|
|
135
|
+
squint config set theme ocean # amber · ocean · moss · rose · mono
|
|
136
|
+
squint config set bell false # no bell on turn completion
|
|
132
137
|
squint doctor # engines + Chrome + WebSocket check
|
|
138
|
+
squint doctor --probe # run every engine end to end, verify auth actually works
|
|
133
139
|
```
|
|
134
140
|
|
|
135
|
-
Inside the TUI
|
|
136
|
-
|
|
141
|
+
**Inside the TUI:**
|
|
142
|
+
|
|
143
|
+
- **Modes**: `shift+tab` cycles `safe` (edits auto-approved) → `plan` (read-only
|
|
144
|
+
investigation) → `yolo` (no friction), mapped to each engine's native permission flags.
|
|
145
|
+
- **Type ahead**: keep typing while the agent works — Enter queues asks that dispatch in
|
|
146
|
+
order; `/queue clear` drops them. `Esc` interrupts the current turn.
|
|
147
|
+
- **Editing**: a real line editor — arrows move, `alt+←/→` jump words, `ctrl+a/e/k/u/w`,
|
|
148
|
+
`↑/↓` history. `ctrl+c` twice exits with a session summary.
|
|
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.
|
|
137
164
|
|
|
138
165
|
## Design directions
|
|
139
166
|
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
variantsRoot
|
|
4
|
+
} from "./chunk-LCS47EAG.js";
|
|
5
|
+
import {
|
|
6
|
+
findChrome,
|
|
7
|
+
screenshot
|
|
8
|
+
} from "./chunk-P3V5CWH5.js";
|
|
2
9
|
import {
|
|
3
10
|
lineSplitter
|
|
4
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-2LRIKWBU.js";
|
|
12
|
+
|
|
13
|
+
// src/variants/shots.ts
|
|
14
|
+
import path2 from "path";
|
|
5
15
|
|
|
6
16
|
// src/devserver/devserver.ts
|
|
7
17
|
import { spawn } from "child_process";
|
|
@@ -149,8 +159,55 @@ ${errorBlock}
|
|
|
149
159
|
${context}`;
|
|
150
160
|
}
|
|
151
161
|
|
|
162
|
+
// src/variants/shots.ts
|
|
163
|
+
var URL_TIMEOUT_MS = 45e3;
|
|
164
|
+
function waitForUrl(server) {
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
const startedAt = Date.now();
|
|
167
|
+
const poll = setInterval(() => {
|
|
168
|
+
if (server.url) {
|
|
169
|
+
clearInterval(poll);
|
|
170
|
+
resolve(server.url);
|
|
171
|
+
} else if (Date.now() - startedAt > URL_TIMEOUT_MS || server.state === "crashed") {
|
|
172
|
+
clearInterval(poll);
|
|
173
|
+
resolve(null);
|
|
174
|
+
}
|
|
175
|
+
}, 200);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async function screenshotVariants(cwd, variants) {
|
|
179
|
+
const chrome = findChrome();
|
|
180
|
+
if (!chrome) {
|
|
181
|
+
return variants.map((v) => ({ familyId: v.family.id, error: "no Chrome found" }));
|
|
182
|
+
}
|
|
183
|
+
const shots = [];
|
|
184
|
+
for (const variant of variants) {
|
|
185
|
+
const command = detectDevCommand(variant.dir);
|
|
186
|
+
if (!command) {
|
|
187
|
+
shots.push({ familyId: variant.family.id, error: "no dev script" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const server = new DevServer(variant.dir);
|
|
191
|
+
server.start(command);
|
|
192
|
+
const url = await waitForUrl(server);
|
|
193
|
+
if (!url) {
|
|
194
|
+
server.stop();
|
|
195
|
+
shots.push({ familyId: variant.family.id, error: "dev server did not announce a URL" });
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const outPath = path2.join(variantsRoot(cwd), `${variant.family.id}.png`);
|
|
199
|
+
const result = await screenshot(chrome, url, outPath, { width: 1440, height: 900 });
|
|
200
|
+
server.stop();
|
|
201
|
+
shots.push(
|
|
202
|
+
result.ok ? { familyId: variant.family.id, path: outPath } : { familyId: variant.family.id, error: result.error }
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return shots;
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
export {
|
|
153
209
|
detectDevCommand,
|
|
154
210
|
DevServer,
|
|
155
|
-
buildFixPrompt
|
|
211
|
+
buildFixPrompt,
|
|
212
|
+
screenshotVariants
|
|
156
213
|
};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
cdpCapture,
|
|
4
4
|
hasWebSocket
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-OC6RU6XH.js";
|
|
6
6
|
import {
|
|
7
7
|
findChrome,
|
|
8
8
|
screenshot
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-P3V5CWH5.js";
|
|
10
10
|
|
|
11
11
|
// src/preview/preview.ts
|
|
12
12
|
import fs2 from "fs";
|
|
@@ -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
|
|
134
|
-
|
|
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
|
};
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
FAMILIES,
|
|
4
|
+
FIRST_TURN_ADDENDUM,
|
|
4
5
|
renderFamilyBrief
|
|
5
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-P3H4N2EN.js";
|
|
6
7
|
import {
|
|
7
8
|
runAgent
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import {
|
|
10
|
-
FIRST_TURN_ADDENDUM
|
|
11
|
-
} from "./chunk-NT2HR4RD.js";
|
|
9
|
+
} from "./chunk-OTG4ZB4R.js";
|
|
12
10
|
|
|
13
11
|
// src/variants/variants.ts
|
|
14
12
|
import { execFileSync } from "child_process";
|
|
@@ -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
|
};
|
|
@@ -1,7 +1,60 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
// src/prompt/brief.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var DEFAULT_BRIEF = `You are acting as a senior product designer and frontend engineer. Treat every change as production work, not a demo.
|
|
7
|
+
|
|
8
|
+
Direction before code:
|
|
9
|
+
- Commit to one specific visual direction derived from what this product is for, and state it in one sentence before implementing. If the direction could be guessed from the product category alone, sharpen it until it couldn't.
|
|
10
|
+
- Choose a color strategy deliberately: restrained (neutrals + one accent), committed (one dominant color owning 30\u201360% of the surface), or a small palette of 3\u20135 named roles. Never purple/violet gradients unless asked. Body text contrast stays at least 4.5:1 \u2014 no light gray body copy "for elegance".
|
|
11
|
+
- Typography: at most two families, paired on contrast (display serif + geometric sans, or sans + mono). Never Inter/Roboto/Open Sans/system defaults for display type. Pair weight extremes (300 against 700\u2013900); display sizes jump 3x over body, not 1.5x.
|
|
12
|
+
|
|
13
|
+
Tokens are the system:
|
|
14
|
+
- Define or extend design tokens (CSS variables / theme config) first, then compose the UI from them. Never scatter literal colors or one-off spacing values through components.
|
|
15
|
+
- One spacing rhythm on a 4/8px grid; one type scale with a ratio of at least 1.25 between steps.
|
|
16
|
+
|
|
17
|
+
Banned tells \u2014 these read instantly as machine-generated:
|
|
18
|
+
- Purple gradient on white; glassmorphism cards; cream/beige page background as a reflex "warmth" move
|
|
19
|
+
- Emoji as icons; identical icon-topped card grids; stat banner rows; numbered 01/02/03 section markers
|
|
20
|
+
- Tiny all-caps tracked eyebrow labels over every section; gradient text; colored left-border strips on cards
|
|
21
|
+
- Centered hero + badge + three feature cards; unmodified component-library defaults
|
|
22
|
+
If someone could look at the result and say "AI made that" without doubt, it has failed.
|
|
23
|
+
|
|
24
|
+
Craft details:
|
|
25
|
+
- Every interactive element gets hover, focus-visible, and active treatment; keyboard focus stays visible.
|
|
26
|
+
- Anything that loads data gets loading, empty, and error states.
|
|
27
|
+
- Motion: 150\u2013250ms ease-out; one orchestrated entrance with staggered reveals beats scattered micro-interactions; honor prefers-reduced-motion; never leave content invisible until a scroll observer fires.
|
|
28
|
+
- Body line length 65\u201375ch; text-wrap: balance on headings.
|
|
29
|
+
|
|
30
|
+
Engineering:
|
|
31
|
+
- Follow the repo's existing conventions and extend its patterns; new components in new files, small and focused; semantic HTML.
|
|
32
|
+
- Responsive from 360px up with no horizontal overflow \u2014 check intermediate widths, not just phone and desktop.
|
|
33
|
+
- Let errors surface instead of swallowing them in try/catch; log clearly so failures can be traced.
|
|
34
|
+
- Not done until the app builds cleanly, typechecks, and renders without console errors.`;
|
|
35
|
+
var FIRST_TURN_ADDENDUM = `This is the opening move on this task: establish the design foundation before building. State the visual direction, set up the tokens/theme first, then build components from them. The first render should feel like a designed product, not a scaffold \u2014 impressive on sight.`;
|
|
36
|
+
function loadBrief(cwd) {
|
|
37
|
+
const custom = path.join(cwd, ".squint", "brief.md");
|
|
38
|
+
try {
|
|
39
|
+
const text = fs.readFileSync(custom, "utf8").trim();
|
|
40
|
+
if (text.length > 0) return text;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
return DEFAULT_BRIEF;
|
|
44
|
+
}
|
|
45
|
+
function composePrompt(ask, opts) {
|
|
46
|
+
if (opts.noBrief) return ask;
|
|
47
|
+
const brief = loadBrief(opts.cwd);
|
|
48
|
+
const firstTurn = opts.firstTurn ?? true;
|
|
49
|
+
const addendum = firstTurn ? `
|
|
50
|
+
|
|
51
|
+
${FIRST_TURN_ADDENDUM}` : "";
|
|
52
|
+
return `${brief}${addendum}
|
|
53
|
+
|
|
54
|
+
## Task
|
|
55
|
+
|
|
56
|
+
${ask}`;
|
|
57
|
+
}
|
|
5
58
|
|
|
6
59
|
// src/prompt/families.ts
|
|
7
60
|
var FAMILIES = [
|
|
@@ -121,6 +174,8 @@ ${coreStandards()}`;
|
|
|
121
174
|
}
|
|
122
175
|
|
|
123
176
|
export {
|
|
177
|
+
FIRST_TURN_ADDENDUM,
|
|
178
|
+
composePrompt,
|
|
124
179
|
FAMILIES,
|
|
125
180
|
getFamily,
|
|
126
181
|
renderFamilyBrief
|
|
@@ -79,10 +79,11 @@ const PICKER = \`(() => {
|
|
|
79
79
|
addEventListener('keydown', (e) => {
|
|
80
80
|
if (e.altKey && (e.key === 's' || e.key === 'S' || e.code === 'KeyS')) {
|
|
81
81
|
active = !active;
|
|
82
|
-
if (!active) { box.style.display = 'none'; tip.style.display = 'none'; }
|
|
83
|
-
else toast('squint picker on \u2014 click
|
|
82
|
+
if (!active) { window.__squintFinishNote?.(true); window.__squintCompile?.(); box.style.display = 'none'; tip.style.display = 'none'; }
|
|
83
|
+
else toast('squint picker on \u2014 click elements to pin, alt+enter copies all (esc cancels)');
|
|
84
84
|
} else if (e.key === 'Escape' && active) {
|
|
85
|
-
active = false;
|
|
85
|
+
active = false; window.__squintFinishNote?.(false); window.__squintClearPins?.();
|
|
86
|
+
box.style.display = 'none'; tip.style.display = 'none';
|
|
86
87
|
}
|
|
87
88
|
});
|
|
88
89
|
addEventListener('mousemove', (e) => {
|
|
@@ -97,16 +98,65 @@ const PICKER = \`(() => {
|
|
|
97
98
|
tip.style.top = Math.max(r.top - 26, 2) + 'px';
|
|
98
99
|
tip.textContent = '<' + s.tag + '> ' + rel(s.file);
|
|
99
100
|
}, true);
|
|
101
|
+
// Multi-pin annotations: each click drops a numbered pin with a note;
|
|
102
|
+
// alt+enter (or toggling the picker off) compiles them into one blob.
|
|
103
|
+
const pins = [];
|
|
104
|
+
const badges = [];
|
|
105
|
+
const noteBox = document.createElement('input');
|
|
106
|
+
noteBox.style.cssText = 'position:fixed;z-index:2147483647;background:#1b1b1b;color:#fff;border:2px solid #e8a33d;border-radius:3px;padding:4px 8px;font:12px ui-monospace,monospace;display:none;width:280px;outline:none;';
|
|
107
|
+
noteBox.placeholder = 'note for this pin \u2014 enter to keep, esc to drop';
|
|
108
|
+
document.documentElement.append(noteBox);
|
|
109
|
+
let pending = null;
|
|
110
|
+
const addBadge = (el, n) => {
|
|
111
|
+
const r = el.getBoundingClientRect();
|
|
112
|
+
const b = document.createElement('div');
|
|
113
|
+
b.textContent = String(n);
|
|
114
|
+
b.style.cssText = 'position:fixed;z-index:2147483647;background:#e8a33d;color:#1b1b1b;font:bold 11px ui-monospace,monospace;border-radius:8px;min-width:16px;height:16px;text-align:center;line-height:16px;pointer-events:none;';
|
|
115
|
+
b.style.left = Math.max(r.left - 6, 2) + 'px';
|
|
116
|
+
b.style.top = Math.max(r.top - 6, 2) + 'px';
|
|
117
|
+
document.documentElement.append(b);
|
|
118
|
+
badges.push(b);
|
|
119
|
+
};
|
|
120
|
+
const clearPins = () => { pins.length = 0; badges.forEach((b) => b.remove()); badges.length = 0; };
|
|
121
|
+
const compile = () => {
|
|
122
|
+
if (pins.length === 0) return;
|
|
123
|
+
const blob = pins.length === 1 && !pins[0].note
|
|
124
|
+
? pins[0].ref
|
|
125
|
+
: pins.map((p, i) => (i + 1) + '. ' + p.ref + (p.note ? ' \u2014 ' + p.note : '')).join('\\\\n');
|
|
126
|
+
window.__squintLastPick = blob;
|
|
127
|
+
const count = pins.length;
|
|
128
|
+
clearPins();
|
|
129
|
+
const done = () => toast('copied ' + count + ' annotation' + (count === 1 ? '' : 's') + ' \u2014 paste into squint');
|
|
130
|
+
if (navigator.clipboard?.writeText) navigator.clipboard.writeText(blob).then(done, done);
|
|
131
|
+
else done();
|
|
132
|
+
};
|
|
133
|
+
const finishNote = (keep) => {
|
|
134
|
+
if (!pending) return;
|
|
135
|
+
if (keep) { pins.push({ ref: pending.ref, note: noteBox.value.trim() }); addBadge(pending.el, pins.length); }
|
|
136
|
+
pending = null; noteBox.value = ''; noteBox.style.display = 'none';
|
|
137
|
+
};
|
|
138
|
+
noteBox.addEventListener('keydown', (e) => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
if (e.key === 'Enter') { finishNote(true); if (e.altKey) { compile(); active = false; box.style.display = 'none'; } }
|
|
141
|
+
else if (e.key === 'Escape') finishNote(false);
|
|
142
|
+
});
|
|
143
|
+
window.__squintFinishNote = finishNote;
|
|
144
|
+
window.__squintCompile = compile;
|
|
145
|
+
window.__squintClearPins = clearPins;
|
|
146
|
+
addEventListener('keydown', (e) => {
|
|
147
|
+
if (active && e.altKey && e.key === 'Enter') { finishNote(true); compile(); active = false; box.style.display = 'none'; tip.style.display = 'none'; }
|
|
148
|
+
});
|
|
100
149
|
addEventListener('click', (e) => {
|
|
101
|
-
if (!active) return;
|
|
150
|
+
if (!active || e.target === noteBox) return;
|
|
102
151
|
e.preventDefault(); e.stopPropagation();
|
|
152
|
+
finishNote(true);
|
|
103
153
|
const el = find(e.target); if (!el) return;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
154
|
+
pending = { el, ref: describe(el) };
|
|
155
|
+
const r = el.getBoundingClientRect();
|
|
156
|
+
noteBox.style.display = 'block';
|
|
157
|
+
noteBox.style.left = Math.min(r.left, innerWidth - 310) + 'px';
|
|
158
|
+
noteBox.style.top = Math.min(r.bottom + 4, innerHeight - 34) + 'px';
|
|
159
|
+
noteBox.focus();
|
|
110
160
|
}, true);
|
|
111
161
|
})();\`
|
|
112
162
|
`;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/prompt/skills.ts
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
var FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
7
|
+
function parseSkill(name, raw) {
|
|
8
|
+
const match = FRONTMATTER_RE.exec(raw);
|
|
9
|
+
if (!match) return null;
|
|
10
|
+
const body = raw.slice(match[0].length).trim();
|
|
11
|
+
const triggersLine = /(^|\n)triggers:[ \t]*(.*)/.exec(match[1]);
|
|
12
|
+
if (!triggersLine) return null;
|
|
13
|
+
let triggers;
|
|
14
|
+
const inline = triggersLine[2].trim();
|
|
15
|
+
if (inline.length > 0) {
|
|
16
|
+
triggers = inline.split(",").map((t) => t.trim().toLowerCase());
|
|
17
|
+
} else {
|
|
18
|
+
triggers = [...match[1].matchAll(/\n\s*-\s+(.+)/g)].map((m) => m[1].trim().toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
triggers = triggers.filter((t) => t.length > 0);
|
|
21
|
+
if (triggers.length === 0 || body.length === 0) return null;
|
|
22
|
+
return { name, triggers, body };
|
|
23
|
+
}
|
|
24
|
+
function loadSkills(cwd) {
|
|
25
|
+
const dir = path.join(cwd, ".squint", "skills");
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
29
|
+
} catch {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const skills = [];
|
|
33
|
+
for (const entry of entries.sort()) {
|
|
34
|
+
try {
|
|
35
|
+
const skill = parseSkill(entry.replace(/\.md$/, ""), fs.readFileSync(path.join(dir, entry), "utf8"));
|
|
36
|
+
if (skill) skills.push(skill);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return skills;
|
|
41
|
+
}
|
|
42
|
+
function loadRules(cwd) {
|
|
43
|
+
try {
|
|
44
|
+
const text = fs.readFileSync(path.join(cwd, ".squint", "rules.md"), "utf8").trim();
|
|
45
|
+
return text.length > 0 ? text : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function matchSkills(skills, ask) {
|
|
51
|
+
const haystack = ask.toLowerCase();
|
|
52
|
+
return skills.filter((skill) => skill.triggers.some((t) => haystack.includes(t)));
|
|
53
|
+
}
|
|
54
|
+
function enrich(cwd, ask) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
const rules = loadRules(cwd);
|
|
57
|
+
if (rules) parts.push(`## Project rules (always apply)
|
|
58
|
+
|
|
59
|
+
${rules}`);
|
|
60
|
+
const matched = matchSkills(loadSkills(cwd), ask);
|
|
61
|
+
for (const skill of matched) {
|
|
62
|
+
parts.push(`## Project notes: ${skill.name}
|
|
63
|
+
|
|
64
|
+
${skill.body}`);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
sections: parts.length > 0 ? `
|
|
68
|
+
|
|
69
|
+
${parts.join("\n\n")}` : "",
|
|
70
|
+
matchedSkills: matched.map((s) => s.name)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
parseSkill,
|
|
76
|
+
loadSkills,
|
|
77
|
+
loadRules,
|
|
78
|
+
matchSkills,
|
|
79
|
+
enrich
|
|
80
|
+
};
|
|
@@ -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
|
};
|