@aayambansal/squint 0.1.3 → 0.2.1
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 +20 -6
- package/dist/{cdp-R7Y2XSZQ.js → cdp-RTWPFIX5.js} +1 -1
- package/dist/{chrome-4WNYZZ42.js → chrome-RD7XQ767.js} +2 -2
- package/dist/{chunk-FQDSJRTY.js → chunk-2IW2MMCY.js} +59 -2
- package/dist/{chunk-YIVPCWSG.js → chunk-2LRIKWBU.js} +27 -23
- package/dist/{chunk-43VZK47W.js → chunk-DLCRYRWQ.js} +4 -0
- package/dist/{chunk-UHVE42IQ.js → chunk-LCS47EAG.js} +3 -5
- package/dist/{chunk-MGHJSERZ.js → chunk-OTG4ZB4R.js} +1 -1
- package/dist/{chunk-5IR2AZVK.js → chunk-P3H4N2EN.js} +58 -3
- package/dist/{chunk-MURKQ7SI.js → chunk-P3V5CWH5.js} +3 -3
- package/dist/{chunk-MOYLED4S.js → chunk-REDPR3KI.js} +2 -2
- package/dist/{chunk-ABF4QAMZ.js → chunk-VJGE7HSP.js} +8 -5
- package/dist/{chunk-W6MYXQIU.js → chunk-VNLM53RJ.js} +60 -10
- package/dist/chunk-XZKQZKEE.js +80 -0
- package/dist/cli.js +1148 -471
- package/dist/{families-RVP5BWQD.js → families-2G5SLLY7.js} +1 -2
- package/dist/{gates-G5DABJWQ.js → gates-I2GGQGMA.js} +3 -1
- package/dist/{init-7AYGAKOS.js → init-LSYB32NS.js} +1 -1
- package/dist/{preview-AA6YIE5W.js → preview-XQ7EOHV4.js} +4 -4
- package/dist/run-XHLCTF6V.js +8 -0
- package/dist/shots-LRYYMTPK.js +12 -0
- package/dist/skills-UGHU22BS.js +15 -0
- package/dist/{source-MEXHWVP4.js → source-ZZU245VN.js} +1 -1
- package/dist/{variants-VVIYT7WI.js → variants-7A7723L7.js} +4 -5
- package/package.json +1 -1
- package/dist/chunk-NT2HR4RD.js +0 -63
- package/dist/shots-ADBOBALL.js +0 -66
package/dist/cli.js
CHANGED
|
@@ -2,19 +2,27 @@
|
|
|
2
2
|
import {
|
|
3
3
|
DevServer,
|
|
4
4
|
buildFixPrompt,
|
|
5
|
-
detectDevCommand
|
|
6
|
-
|
|
5
|
+
detectDevCommand,
|
|
6
|
+
screenshotVariants
|
|
7
|
+
} from "./chunk-2IW2MMCY.js";
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
applyVariant,
|
|
10
|
+
cleanVariants,
|
|
11
|
+
listVariants,
|
|
12
|
+
runVariants
|
|
13
|
+
} from "./chunk-LCS47EAG.js";
|
|
10
14
|
import {
|
|
11
15
|
composePrompt
|
|
12
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-P3H4N2EN.js";
|
|
17
|
+
import {
|
|
18
|
+
runAgent
|
|
19
|
+
} from "./chunk-OTG4ZB4R.js";
|
|
13
20
|
import {
|
|
14
21
|
buildGatePrompt,
|
|
22
|
+
detectFastGates,
|
|
15
23
|
detectGates,
|
|
16
24
|
runGates
|
|
17
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-DLCRYRWQ.js";
|
|
18
26
|
import {
|
|
19
27
|
buildReviewPrompt,
|
|
20
28
|
buildRuntimeFixPrompt,
|
|
@@ -24,15 +32,20 @@ import {
|
|
|
24
32
|
probeRuntime,
|
|
25
33
|
runtimeSummary,
|
|
26
34
|
saveState
|
|
27
|
-
} from "./chunk-
|
|
28
|
-
import "./chunk-
|
|
29
|
-
import
|
|
35
|
+
} from "./chunk-REDPR3KI.js";
|
|
36
|
+
import "./chunk-VJGE7HSP.js";
|
|
37
|
+
import {
|
|
38
|
+
findChrome
|
|
39
|
+
} from "./chunk-P3V5CWH5.js";
|
|
30
40
|
import {
|
|
31
41
|
detectEngines,
|
|
32
|
-
engines,
|
|
33
42
|
getEngine
|
|
34
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-2LRIKWBU.js";
|
|
44
|
+
import {
|
|
45
|
+
enrich
|
|
46
|
+
} from "./chunk-XZKQZKEE.js";
|
|
35
47
|
import {
|
|
48
|
+
isGitRepo,
|
|
36
49
|
restoreSnapshot,
|
|
37
50
|
takeSnapshot
|
|
38
51
|
} from "./chunk-WTA6YYBY.js";
|
|
@@ -57,7 +70,13 @@ var ConfigSchema = z.object({
|
|
|
57
70
|
/** Automatically send dev-server errors back to the engine (max 2 attempts). */
|
|
58
71
|
autoFix: z.boolean().optional(),
|
|
59
72
|
/** Probe the running app's runtime after each clean turn (default on). */
|
|
60
|
-
autoProbe: z.boolean().optional()
|
|
73
|
+
autoProbe: z.boolean().optional(),
|
|
74
|
+
/** Run typecheck+lint after every turn (default on where detected). */
|
|
75
|
+
autoCheck: z.boolean().optional(),
|
|
76
|
+
/** Terminal bell when a turn finishes (default on). */
|
|
77
|
+
bell: z.boolean().optional(),
|
|
78
|
+
/** TUI theme name (amber, ocean, moss, rose, mono). */
|
|
79
|
+
theme: z.string().optional()
|
|
61
80
|
});
|
|
62
81
|
function defaultPaths(cwd) {
|
|
63
82
|
const xdg = process.env.XDG_CONFIG_HOME;
|
|
@@ -98,9 +117,9 @@ function resolveModel(config, engineId, override) {
|
|
|
98
117
|
function setConfigValue(file, key, value) {
|
|
99
118
|
const current = readConfigFile(file);
|
|
100
119
|
let next;
|
|
101
|
-
if (key === "engine") {
|
|
102
|
-
next = { ...current,
|
|
103
|
-
} else if (key === "autoDev" || key === "autoFix" || key === "autoProbe") {
|
|
120
|
+
if (key === "engine" || key === "theme") {
|
|
121
|
+
next = { ...current, [key]: value };
|
|
122
|
+
} else if (key === "autoDev" || key === "autoFix" || key === "autoProbe" || key === "autoCheck" || key === "bell") {
|
|
104
123
|
if (value !== "true" && value !== "false") {
|
|
105
124
|
throw new Error(`"${key}" must be true or false`);
|
|
106
125
|
}
|
|
@@ -110,7 +129,9 @@ function setConfigValue(file, key, value) {
|
|
|
110
129
|
if (!engineId) throw new Error("Usage: squint config set models.<engineId> <model>");
|
|
111
130
|
next = { ...current, models: { ...current.models, [engineId]: value } };
|
|
112
131
|
} else {
|
|
113
|
-
throw new Error(
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Unknown config key "${key}". Supported: engine, theme, autoDev, autoFix, autoProbe, autoCheck, bell, models.<engineId>`
|
|
134
|
+
);
|
|
114
135
|
}
|
|
115
136
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
116
137
|
fs.writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
@@ -118,132 +139,39 @@ function setConfigValue(file, key, value) {
|
|
|
118
139
|
}
|
|
119
140
|
|
|
120
141
|
// src/tui/App.tsx
|
|
121
|
-
import { Box, Static, Text as
|
|
122
|
-
import
|
|
123
|
-
import {
|
|
124
|
-
|
|
125
|
-
// src/tui/messages.tsx
|
|
126
|
-
import { Text } from "ink";
|
|
127
|
-
import { useEffect, useState } from "react";
|
|
128
|
-
|
|
129
|
-
// src/tui/theme.ts
|
|
130
|
-
var theme = {
|
|
131
|
-
accent: "#e8a33d",
|
|
132
|
-
dim: "gray",
|
|
133
|
-
user: "#7aa2f7",
|
|
134
|
-
error: "#f7768e",
|
|
135
|
-
success: "#9ece6a",
|
|
136
|
-
tool: "#7dcfff"
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
// src/tui/messages.tsx
|
|
140
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
141
|
-
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
142
|
-
function WorkingLine({ startedAt }) {
|
|
143
|
-
const [frame, setFrame] = useState(0);
|
|
144
|
-
const [elapsed, setElapsed] = useState(0);
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
const timer = setInterval(() => {
|
|
147
|
-
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
148
|
-
setElapsed(Math.floor((Date.now() - startedAt) / 1e3));
|
|
149
|
-
}, 80);
|
|
150
|
-
return () => clearInterval(timer);
|
|
151
|
-
}, [startedAt]);
|
|
152
|
-
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
153
|
-
/* @__PURE__ */ jsx(Text, { color: theme.accent, children: SPINNER_FRAMES[frame] }),
|
|
154
|
-
/* @__PURE__ */ jsxs(Text, { color: theme.dim, children: [
|
|
155
|
-
" ",
|
|
156
|
-
"working\u2026 ",
|
|
157
|
-
elapsed,
|
|
158
|
-
"s \xB7 esc to interrupt"
|
|
159
|
-
] })
|
|
160
|
-
] });
|
|
161
|
-
}
|
|
162
|
-
function MessageLine({ message }) {
|
|
163
|
-
switch (message.role) {
|
|
164
|
-
case "user":
|
|
165
|
-
return /* @__PURE__ */ jsxs(Text, { color: theme.user, wrap: "wrap", children: [
|
|
166
|
-
"\u276F ",
|
|
167
|
-
message.text
|
|
168
|
-
] });
|
|
169
|
-
case "assistant":
|
|
170
|
-
return /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: message.text });
|
|
171
|
-
case "status":
|
|
172
|
-
return /* @__PURE__ */ jsxs(Text, { color: theme.dim, wrap: "wrap", children: [
|
|
173
|
-
"\xB7 ",
|
|
174
|
-
message.text
|
|
175
|
-
] });
|
|
176
|
-
case "tool":
|
|
177
|
-
return /* @__PURE__ */ jsxs(Text, { color: theme.tool, wrap: "wrap", children: [
|
|
178
|
-
"\u2699 ",
|
|
179
|
-
message.text
|
|
180
|
-
] });
|
|
181
|
-
case "thinking":
|
|
182
|
-
return /* @__PURE__ */ jsx(Text, { color: theme.dim, italic: true, wrap: "wrap", children: message.text });
|
|
183
|
-
case "error":
|
|
184
|
-
return /* @__PURE__ */ jsxs(Text, { color: theme.error, wrap: "wrap", children: [
|
|
185
|
-
"\u2717 ",
|
|
186
|
-
message.text
|
|
187
|
-
] });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
142
|
+
import { Box as Box2, Static, Text as Text3, useApp, useInput } from "ink";
|
|
143
|
+
import path3 from "path";
|
|
144
|
+
import { useMemo, useRef, useState as useState2, useSyncExternalStore } from "react";
|
|
190
145
|
|
|
191
|
-
// src/
|
|
192
|
-
import
|
|
146
|
+
// src/session/engine.ts
|
|
147
|
+
import path2 from "path";
|
|
193
148
|
var MAX_AUTO_FIX_ATTEMPTS = 2;
|
|
194
149
|
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const fixAttemptsRef = useRef(0);
|
|
214
|
-
const reviewTipShownRef = useRef(false);
|
|
215
|
-
const historyRef = useRef([]);
|
|
216
|
-
const historyIndexRef = useRef(-1);
|
|
217
|
-
const push = useCallback((role, text) => {
|
|
218
|
-
idRef.current += 1;
|
|
219
|
-
setMessages((prev) => [...prev, { id: idRef.current, role, text }]);
|
|
220
|
-
}, []);
|
|
221
|
-
const commitLive = useCallback(() => {
|
|
222
|
-
if (liveRef.current.length > 0) {
|
|
223
|
-
push("assistant", liveRef.current);
|
|
224
|
-
liveRef.current = "";
|
|
225
|
-
setLiveText("");
|
|
226
|
-
}
|
|
227
|
-
}, [push]);
|
|
228
|
-
const getDevServer = useCallback(() => {
|
|
229
|
-
if (!devRef.current) {
|
|
230
|
-
devRef.current = new DevServer(cwd, {
|
|
231
|
-
onStateChange: setDevState,
|
|
232
|
-
onUrl: setDevUrl
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
return devRef.current;
|
|
236
|
-
}, [cwd]);
|
|
237
|
-
useEffect2(() => {
|
|
238
|
-
if (autoDev && detectDevCommand(cwd)) {
|
|
239
|
-
getDevServer().start();
|
|
150
|
+
var Session = class {
|
|
151
|
+
constructor(opts) {
|
|
152
|
+
this.opts = opts;
|
|
153
|
+
this.state = {
|
|
154
|
+
items: [],
|
|
155
|
+
liveText: "",
|
|
156
|
+
running: false,
|
|
157
|
+
runStartedAt: 0,
|
|
158
|
+
engineId: opts.engineId,
|
|
159
|
+
model: opts.model,
|
|
160
|
+
devState: "stopped",
|
|
161
|
+
devUrl: null,
|
|
162
|
+
totals: { costUsd: 0, turns: 0 },
|
|
163
|
+
queue: [],
|
|
164
|
+
mode: "safe"
|
|
165
|
+
};
|
|
166
|
+
if (opts.autoDev && detectDevCommand(opts.cwd)) {
|
|
167
|
+
this.devServer().start();
|
|
240
168
|
}
|
|
241
|
-
const saved = loadState(cwd);
|
|
169
|
+
const saved = loadState(opts.cwd);
|
|
242
170
|
if (saved) {
|
|
243
171
|
try {
|
|
244
172
|
if (getEngine(saved.engine).supportsResume) {
|
|
245
173
|
const mins = Math.max(1, Math.round((Date.now() - saved.at) / 6e4));
|
|
246
|
-
push(
|
|
174
|
+
this.push(
|
|
247
175
|
"status",
|
|
248
176
|
`previous session (${mins}m ago${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}) \u2014 /resume to continue`
|
|
249
177
|
);
|
|
@@ -251,353 +179,988 @@ function App({ cwd, initialEngine, initialModel, autoDev, autoFix, autoProbe })
|
|
|
251
179
|
} catch {
|
|
252
180
|
}
|
|
253
181
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
182
|
+
}
|
|
183
|
+
opts;
|
|
184
|
+
state;
|
|
185
|
+
listeners = /* @__PURE__ */ new Set();
|
|
186
|
+
nextId = 0;
|
|
187
|
+
live = "";
|
|
188
|
+
sessionId;
|
|
189
|
+
dev = null;
|
|
190
|
+
abort = null;
|
|
191
|
+
pendingFix = null;
|
|
192
|
+
checkpoints = [];
|
|
193
|
+
fixAttempts = 0;
|
|
194
|
+
reviewTipShown = false;
|
|
195
|
+
startedAt = Date.now();
|
|
196
|
+
subscribe(listener) {
|
|
197
|
+
this.listeners.add(listener);
|
|
198
|
+
return () => this.listeners.delete(listener);
|
|
199
|
+
}
|
|
200
|
+
getState() {
|
|
201
|
+
return this.state;
|
|
202
|
+
}
|
|
203
|
+
dispose() {
|
|
204
|
+
this.abort?.abort();
|
|
205
|
+
this.dev?.stop();
|
|
206
|
+
}
|
|
207
|
+
interrupt() {
|
|
208
|
+
this.abort?.abort();
|
|
209
|
+
}
|
|
210
|
+
/** Frontend-originated status line (view-level commands like /theme). */
|
|
211
|
+
note(text) {
|
|
212
|
+
this.push("status", text);
|
|
213
|
+
}
|
|
214
|
+
setMode(mode) {
|
|
215
|
+
this.notify({ mode });
|
|
216
|
+
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";
|
|
217
|
+
this.push("status", `mode \u2192 ${mode} \xB7 ${hint}`);
|
|
218
|
+
}
|
|
219
|
+
cycleMode() {
|
|
220
|
+
const order = ["safe", "plan", "yolo"];
|
|
221
|
+
const next = order[(order.indexOf(this.state.mode) + 1) % order.length];
|
|
222
|
+
this.setMode(next);
|
|
223
|
+
}
|
|
224
|
+
/** One-line goodbye: what this session amounted to. */
|
|
225
|
+
summary() {
|
|
226
|
+
const mins = Math.max(1, Math.round((Date.now() - this.startedAt) / 6e4));
|
|
227
|
+
const { turns, costUsd } = this.state.totals;
|
|
228
|
+
const parts = [`${turns} turn${turns === 1 ? "" : "s"}`];
|
|
229
|
+
if (costUsd > 0) parts.push(`$${costUsd.toFixed(2)}`);
|
|
230
|
+
parts.push(`${mins}m`);
|
|
231
|
+
return `session: ${parts.join(" \xB7 ")}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Route one line of user input: slash command or an ask for the engine.
|
|
235
|
+
* Asks arriving mid-turn queue up and dispatch in order once the
|
|
236
|
+
* current turn (including its fix cycle) settles.
|
|
237
|
+
*/
|
|
238
|
+
input(raw) {
|
|
239
|
+
const value = raw.trim();
|
|
240
|
+
if (value.length === 0) return;
|
|
241
|
+
if (this.state.running) {
|
|
242
|
+
if (value === "/queue clear") {
|
|
243
|
+
this.notify({ queue: [] });
|
|
244
|
+
this.push("status", "queue cleared");
|
|
245
|
+
return;
|
|
294
246
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
247
|
+
this.notify({ queue: [...this.state.queue, value] });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
if (value.startsWith("/")) {
|
|
251
|
+
this.command(value);
|
|
252
|
+
} else {
|
|
253
|
+
void this.submit(value);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Dispatch queued input after the current work settles. */
|
|
257
|
+
drainQueue() {
|
|
258
|
+
if (this.state.running) return;
|
|
259
|
+
const [next, ...rest] = this.state.queue;
|
|
260
|
+
if (next === void 0) return;
|
|
261
|
+
this.notify({ queue: rest });
|
|
262
|
+
this.input(next);
|
|
263
|
+
}
|
|
264
|
+
// ---------- internals ----------
|
|
265
|
+
notify(patch) {
|
|
266
|
+
this.state = { ...this.state, ...patch };
|
|
267
|
+
for (const listener of this.listeners) listener();
|
|
268
|
+
}
|
|
269
|
+
push(role, text) {
|
|
270
|
+
this.nextId += 1;
|
|
271
|
+
this.notify({ items: [...this.state.items, { id: this.nextId, role, text }] });
|
|
272
|
+
}
|
|
273
|
+
setLive(text) {
|
|
274
|
+
this.live = text;
|
|
275
|
+
this.notify({ liveText: text });
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Static transcript items are immutable once rendered, so in-progress
|
|
279
|
+
* assistant text accumulates in a live buffer and commits as one block.
|
|
280
|
+
*/
|
|
281
|
+
commitLive() {
|
|
282
|
+
if (this.live.length > 0) {
|
|
283
|
+
const text = this.live;
|
|
284
|
+
this.live = "";
|
|
285
|
+
this.state = { ...this.state, liveText: "" };
|
|
286
|
+
this.push("assistant", text);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
devServer() {
|
|
290
|
+
if (!this.dev) {
|
|
291
|
+
this.dev = new DevServer(this.opts.cwd, {
|
|
292
|
+
onStateChange: (devState) => this.notify({ devState }),
|
|
293
|
+
onUrl: (url) => this.notify({ devUrl: url })
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return this.dev;
|
|
297
|
+
}
|
|
298
|
+
turnEdits = 0;
|
|
299
|
+
turnTools = 0;
|
|
300
|
+
handleEvent = (event) => {
|
|
301
|
+
switch (event.type) {
|
|
302
|
+
case "status":
|
|
303
|
+
this.commitLive();
|
|
304
|
+
this.push("status", event.text);
|
|
305
|
+
break;
|
|
306
|
+
case "delta":
|
|
307
|
+
this.setLive(this.live + event.text);
|
|
308
|
+
break;
|
|
309
|
+
case "text":
|
|
310
|
+
if (event.streamed) {
|
|
311
|
+
this.live = "";
|
|
312
|
+
this.state = { ...this.state, liveText: "" };
|
|
313
|
+
this.push("assistant", event.text);
|
|
314
|
+
} else {
|
|
315
|
+
this.setLive(this.live + (this.live.length > 0 ? "\n" : "") + event.text);
|
|
332
316
|
}
|
|
317
|
+
break;
|
|
318
|
+
case "thinking":
|
|
319
|
+
this.commitLive();
|
|
320
|
+
this.push("thinking", event.text);
|
|
321
|
+
break;
|
|
322
|
+
case "tool": {
|
|
323
|
+
this.commitLive();
|
|
324
|
+
this.turnTools += 1;
|
|
325
|
+
if (/edit|write|patch|apply/i.test(event.name)) this.turnEdits += 1;
|
|
326
|
+
this.push("tool", event.detail ? `${event.name} \xB7 ${event.detail}` : event.name);
|
|
327
|
+
break;
|
|
333
328
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
329
|
+
case "error":
|
|
330
|
+
this.commitLive();
|
|
331
|
+
this.push("error", event.text);
|
|
332
|
+
break;
|
|
333
|
+
case "result":
|
|
334
|
+
if (event.sessionId) this.sessionId = event.sessionId;
|
|
335
|
+
break;
|
|
336
|
+
case "raw":
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/** Run one engine turn. `display` is what the transcript shows as the ask. */
|
|
341
|
+
async runTurn(prompt, display) {
|
|
342
|
+
this.push("user", display);
|
|
343
|
+
this.turnEdits = 0;
|
|
344
|
+
this.turnTools = 0;
|
|
345
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
346
|
+
const runStart = Date.now();
|
|
347
|
+
const engine = getEngine(this.state.engineId);
|
|
348
|
+
this.abort = new AbortController();
|
|
349
|
+
const result = await runAgent(
|
|
350
|
+
engine,
|
|
351
|
+
{
|
|
352
|
+
prompt,
|
|
353
|
+
cwd: this.opts.cwd,
|
|
354
|
+
model: this.state.model,
|
|
355
|
+
mode: this.state.mode,
|
|
356
|
+
sessionId: engine.supportsResume ? this.sessionId : void 0
|
|
357
|
+
},
|
|
358
|
+
this.handleEvent,
|
|
359
|
+
this.abort.signal
|
|
360
|
+
);
|
|
361
|
+
this.abort = null;
|
|
362
|
+
this.commitLive();
|
|
363
|
+
if (result.ok) {
|
|
364
|
+
const cost = result.costUsd !== void 0 ? ` \xB7 $${result.costUsd.toFixed(2)}` : "";
|
|
365
|
+
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"}` : "";
|
|
367
|
+
this.push("status", `done${secs}${cost}${work}`);
|
|
368
|
+
this.notify({
|
|
369
|
+
totals: {
|
|
370
|
+
costUsd: this.state.totals.costUsd + (result.costUsd ?? 0),
|
|
371
|
+
turns: this.state.totals.turns + 1
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (this.sessionId) {
|
|
375
|
+
saveState(this.opts.cwd, {
|
|
376
|
+
engine: this.state.engineId,
|
|
377
|
+
sessionId: this.sessionId,
|
|
378
|
+
model: this.state.model,
|
|
379
|
+
lastAsk: display.length > 80 ? `${display.slice(0, 79)}\u2026` : display,
|
|
380
|
+
at: Date.now()
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (result.ok && this.opts.autoCheck !== false) {
|
|
385
|
+
const fastGates = detectFastGates(this.opts.cwd);
|
|
386
|
+
if (fastGates.length > 0) {
|
|
387
|
+
const gateResults = await runGates(this.opts.cwd, fastGates);
|
|
388
|
+
const failures = gateResults.filter((r) => !r.ok);
|
|
389
|
+
if (failures.length > 0) {
|
|
390
|
+
this.pendingFix = {
|
|
391
|
+
prompt: buildGatePrompt(failures),
|
|
392
|
+
display: `\u26D1 fix ${failures.map((f) => f.gate.id).join(" + ")} errors`
|
|
342
393
|
};
|
|
343
|
-
push(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
394
|
+
this.push(
|
|
395
|
+
"error",
|
|
396
|
+
failures.map((f) => `\u2717 ${f.gate.id} \xB7 ${f.outputTail.split("\n").slice(-3).join("\n")}`).join("\n")
|
|
397
|
+
);
|
|
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);
|
|
350
403
|
return;
|
|
351
404
|
}
|
|
352
|
-
push("status", "type /fix to send them to the engine");
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
this.push("status", "type /fix to send them to the engine");
|
|
406
|
+
this.notify({ running: false });
|
|
407
|
+
this.drainQueue();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
const dev = this.dev;
|
|
413
|
+
if (result.error !== "interrupted" && dev && (dev.state === "running" || dev.state === "starting")) {
|
|
414
|
+
await delay(1500);
|
|
415
|
+
const errors = dev.errorsSince(runStart);
|
|
416
|
+
if (errors.length > 0) {
|
|
417
|
+
this.pendingFix = {
|
|
418
|
+
prompt: buildFixPrompt(errors, dev.tail(30)),
|
|
419
|
+
display: "\u26D1 fix dev server errors"
|
|
420
|
+
};
|
|
421
|
+
this.push("error", `dev server: ${errors.length} error line(s)
|
|
422
|
+
${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");
|
|
431
|
+
} else {
|
|
432
|
+
this.pendingFix = null;
|
|
433
|
+
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
|
+
};
|
|
441
|
+
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;
|
|
372
448
|
}
|
|
449
|
+
this.push("status", "type /fix to send them to the engine");
|
|
373
450
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
451
|
+
}
|
|
452
|
+
if (!this.reviewTipShown && this.state.devUrl) {
|
|
453
|
+
this.reviewTipShown = true;
|
|
454
|
+
this.push("status", "tip: /review screenshots the app and has the engine critique its own work");
|
|
378
455
|
}
|
|
379
456
|
}
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
457
|
+
}
|
|
458
|
+
this.notify({ running: false });
|
|
459
|
+
this.drainQueue();
|
|
460
|
+
}
|
|
461
|
+
async submit(ask) {
|
|
462
|
+
this.fixAttempts = 0;
|
|
463
|
+
const snapshot = takeSnapshot(this.opts.cwd);
|
|
464
|
+
if (snapshot) {
|
|
465
|
+
this.checkpoints.push({
|
|
466
|
+
snapshot,
|
|
467
|
+
label: ask.length > 60 ? `${ask.slice(0, 59)}\u2026` : ask,
|
|
468
|
+
at: Date.now()
|
|
469
|
+
});
|
|
470
|
+
if (this.checkpoints.length > 20) this.checkpoints.shift();
|
|
471
|
+
}
|
|
472
|
+
const isFirstTurn = this.sessionId === void 0;
|
|
473
|
+
let prompt = isFirstTurn ? composePrompt(ask, { cwd: this.opts.cwd, firstTurn: true }) : ask;
|
|
474
|
+
const enrichment = enrich(this.opts.cwd, ask);
|
|
475
|
+
if (enrichment.matchedSkills.length > 0) {
|
|
476
|
+
this.push("status", `skills: ${enrichment.matchedSkills.join(", ")}`);
|
|
477
|
+
}
|
|
478
|
+
prompt += enrichment.sections;
|
|
479
|
+
await this.runTurn(prompt, ask);
|
|
480
|
+
}
|
|
481
|
+
/** Restore files to the state before checkpoint `index`; drop it and everything after. */
|
|
482
|
+
restoreTo(index) {
|
|
483
|
+
const checkpoint = this.checkpoints[index];
|
|
484
|
+
if (!checkpoint) {
|
|
485
|
+
this.push("status", "no such checkpoint \u2014 /checkpoints lists them");
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const result = restoreSnapshot(this.opts.cwd, checkpoint.snapshot);
|
|
489
|
+
if (result.restored) {
|
|
490
|
+
const dropped = this.checkpoints.length - index;
|
|
491
|
+
this.checkpoints = this.checkpoints.slice(0, index);
|
|
492
|
+
this.push(
|
|
493
|
+
"status",
|
|
494
|
+
`restored files to before "${checkpoint.label}"${dropped > 1 ? ` \xB7 ${dropped} asks rolled back` : ""}${result.deletedFiles > 0 ? ` \xB7 removed ${result.deletedFiles} created file(s)` : ""} \u2014 the conversation continues from here`
|
|
495
|
+
);
|
|
496
|
+
} else {
|
|
497
|
+
this.push("error", `restore failed: ${result.detail ?? "unknown error"}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** Screenshot the running app (and watch its runtime where CDP is available). */
|
|
501
|
+
async capture() {
|
|
502
|
+
if (!this.state.devUrl) {
|
|
503
|
+
this.push("error", "dev server not running \u2014 /dev first");
|
|
387
504
|
return null;
|
|
388
505
|
}
|
|
389
|
-
push("status", "capturing screenshots\u2026");
|
|
390
|
-
const result = await captureViewports(cwd, devUrl);
|
|
506
|
+
this.push("status", "capturing screenshots\u2026");
|
|
507
|
+
const result = await captureViewports(this.opts.cwd, this.state.devUrl);
|
|
391
508
|
if (!result) {
|
|
392
|
-
push("error", "no Chrome/Chromium found for screenshots");
|
|
509
|
+
this.push("error", "no Chrome/Chromium found for screenshots");
|
|
393
510
|
return null;
|
|
394
511
|
}
|
|
395
|
-
for (const err of result.errors) push("error", `screenshot ${err}`);
|
|
512
|
+
for (const err of result.errors) this.push("error", `screenshot ${err}`);
|
|
396
513
|
if (result.shots.length > 0) {
|
|
397
|
-
push(
|
|
514
|
+
this.push(
|
|
515
|
+
"status",
|
|
516
|
+
`captured ${result.shots.map((s) => s.name).join(", ")} \u2192 ${path2.dirname(result.shots[0].path)}`
|
|
517
|
+
);
|
|
398
518
|
}
|
|
399
519
|
if (result.runtime) {
|
|
400
520
|
const summary = runtimeSummary(result.runtime);
|
|
401
521
|
if (summary) {
|
|
402
|
-
push("error", `runtime: ${summary}`);
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
display: "\u26D1 fix runtime errors"
|
|
406
|
-
};
|
|
407
|
-
push("status", "type /fix to send them to the engine");
|
|
522
|
+
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");
|
|
408
525
|
} else {
|
|
409
|
-
push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
526
|
+
this.push("status", "runtime clean \u2014 no console errors, exceptions, or failed requests");
|
|
410
527
|
}
|
|
411
528
|
}
|
|
412
529
|
if (result.a11y && result.a11y.length > 0) {
|
|
413
|
-
push("error", `a11y: ${result.a11y.length} finding(s)
|
|
530
|
+
this.push("error", `a11y: ${result.a11y.length} finding(s)
|
|
414
531
|
${result.a11y.slice(0, 5).join("\n")}`);
|
|
415
|
-
push("status", "/review folds these into the fix pass");
|
|
532
|
+
this.push("status", "/review folds these into the fix pass");
|
|
416
533
|
}
|
|
417
534
|
return result.shots.length > 0 ? result : null;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
case "engine":
|
|
435
|
-
if (!arg) {
|
|
436
|
-
push("status", `engines: ${engines.map((e) => e.id).join(", ")}`);
|
|
437
|
-
} else {
|
|
438
|
-
try {
|
|
439
|
-
getEngine(arg);
|
|
440
|
-
setEngineId(arg);
|
|
441
|
-
sessionRef.current = void 0;
|
|
442
|
-
push("status", `engine \u2192 ${arg} (new session)`);
|
|
443
|
-
} catch (err) {
|
|
444
|
-
push("error", err instanceof Error ? err.message : String(err));
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
case "model":
|
|
449
|
-
setModel(arg || void 0);
|
|
450
|
-
push("status", arg ? `model \u2192 ${arg}` : "model \u2192 engine default");
|
|
451
|
-
break;
|
|
452
|
-
case "dev": {
|
|
453
|
-
const dev = getDevServer();
|
|
454
|
-
if (dev.state === "stopped" || dev.state === "crashed") {
|
|
455
|
-
const devCommand = detectDevCommand(cwd);
|
|
456
|
-
if (!devCommand) {
|
|
457
|
-
push("error", "no dev/start script found in package.json");
|
|
458
|
-
} else {
|
|
459
|
-
dev.start(devCommand);
|
|
460
|
-
push("status", `dev server starting \xB7 ${devCommand.display}`);
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
dev.stop();
|
|
464
|
-
setDevUrl(null);
|
|
465
|
-
push("status", "dev server stopped");
|
|
535
|
+
}
|
|
536
|
+
command(commandLine) {
|
|
537
|
+
const [name, ...rest] = commandLine.slice(1).split(/\s+/);
|
|
538
|
+
const arg = rest.join(" ").trim();
|
|
539
|
+
switch (name) {
|
|
540
|
+
case "engine":
|
|
541
|
+
if (!arg) {
|
|
542
|
+
this.push("status", "usage: /engine <id> \u2014 see squint engines");
|
|
543
|
+
} else {
|
|
544
|
+
try {
|
|
545
|
+
getEngine(arg);
|
|
546
|
+
this.sessionId = void 0;
|
|
547
|
+
this.notify({ engineId: arg });
|
|
548
|
+
this.push("status", `engine \u2192 ${arg} (new session)`);
|
|
549
|
+
} catch (err) {
|
|
550
|
+
this.push("error", err instanceof Error ? err.message : String(err));
|
|
466
551
|
}
|
|
467
|
-
break;
|
|
468
552
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
553
|
+
break;
|
|
554
|
+
case "model":
|
|
555
|
+
this.notify({ model: arg || void 0 });
|
|
556
|
+
this.push("status", arg ? `model \u2192 ${arg}` : "model \u2192 engine default");
|
|
557
|
+
break;
|
|
558
|
+
case "mode":
|
|
559
|
+
if (arg === "plan" || arg === "safe" || arg === "yolo") {
|
|
560
|
+
this.setMode(arg);
|
|
561
|
+
} else {
|
|
562
|
+
this.push("status", "usage: /mode plan|safe|yolo \u2014 or shift+tab to cycle");
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
case "dev": {
|
|
566
|
+
const dev = this.devServer();
|
|
567
|
+
if (dev.state === "stopped" || dev.state === "crashed") {
|
|
568
|
+
const devCommand = detectDevCommand(this.opts.cwd);
|
|
569
|
+
if (!devCommand) {
|
|
570
|
+
this.push("error", "no dev/start script found in package.json");
|
|
472
571
|
} else {
|
|
473
|
-
|
|
572
|
+
dev.start(devCommand);
|
|
573
|
+
this.push("status", `dev server starting \xB7 ${devCommand.display}`);
|
|
474
574
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
pendingFixRef.current = {
|
|
496
|
-
prompt: buildGatePrompt(failures),
|
|
497
|
-
display: `\u26D1 fix failing gates: ${failures.map((f) => f.gate.id).join(", ")}`
|
|
498
|
-
};
|
|
499
|
-
push("status", "type /fix to send failures to the engine");
|
|
500
|
-
} else {
|
|
501
|
-
push("status", "all gates passed");
|
|
502
|
-
}
|
|
503
|
-
})();
|
|
504
|
-
break;
|
|
505
|
-
case "shot":
|
|
506
|
-
void capture();
|
|
507
|
-
break;
|
|
508
|
-
case "review":
|
|
509
|
-
void (async () => {
|
|
510
|
-
const result = await capture();
|
|
511
|
-
if (result) {
|
|
512
|
-
await runTurn(
|
|
513
|
-
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y),
|
|
514
|
-
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
515
|
-
);
|
|
516
|
-
}
|
|
517
|
-
})();
|
|
518
|
-
break;
|
|
519
|
-
case "undo": {
|
|
520
|
-
const snapshot = snapshotRef.current;
|
|
521
|
-
if (!snapshot) {
|
|
522
|
-
push("status", "nothing to undo \u2014 no ask this session, or not a git repo with commits");
|
|
523
|
-
break;
|
|
575
|
+
} else {
|
|
576
|
+
dev.stop();
|
|
577
|
+
this.notify({ devUrl: null });
|
|
578
|
+
this.push("status", "dev server stopped");
|
|
579
|
+
}
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
case "fix":
|
|
583
|
+
if (!this.pendingFix) {
|
|
584
|
+
this.push("status", "nothing to fix \u2014 no captured errors or failed gates");
|
|
585
|
+
} else {
|
|
586
|
+
void this.runTurn(this.pendingFix.prompt, this.pendingFix.display);
|
|
587
|
+
}
|
|
588
|
+
break;
|
|
589
|
+
case "check":
|
|
590
|
+
void (async () => {
|
|
591
|
+
const gates = detectGates(this.opts.cwd);
|
|
592
|
+
if (gates.length === 0) {
|
|
593
|
+
this.push("status", "no gates detected in this project");
|
|
594
|
+
return;
|
|
524
595
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
push(
|
|
529
|
-
"status",
|
|
530
|
-
|
|
596
|
+
this.push("status", `running gates: ${gates.map((g) => g.id).join(" \u2192 ")}`);
|
|
597
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
598
|
+
const results = await runGates(this.opts.cwd, gates, (result) => {
|
|
599
|
+
this.push(
|
|
600
|
+
result.ok ? "status" : "error",
|
|
601
|
+
`${result.ok ? "\u2713" : "\u2717"} ${result.gate.id} \xB7 ${(result.durationMs / 1e3).toFixed(1)}s`
|
|
531
602
|
);
|
|
603
|
+
});
|
|
604
|
+
this.notify({ running: false });
|
|
605
|
+
this.drainQueue();
|
|
606
|
+
const failures = results.filter((r) => !r.ok);
|
|
607
|
+
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");
|
|
532
613
|
} else {
|
|
533
|
-
push("
|
|
614
|
+
this.push("status", "all gates passed");
|
|
615
|
+
}
|
|
616
|
+
})();
|
|
617
|
+
break;
|
|
618
|
+
case "shot":
|
|
619
|
+
void this.capture();
|
|
620
|
+
break;
|
|
621
|
+
case "review":
|
|
622
|
+
void (async () => {
|
|
623
|
+
const result = await this.capture();
|
|
624
|
+
if (result) {
|
|
625
|
+
await this.runTurn(
|
|
626
|
+
buildReviewPrompt(result.shots, arg || void 0, result.runtime, result.a11y),
|
|
627
|
+
`\u{1F441} review rendered UI${arg ? ` \xB7 ${arg}` : ""}`
|
|
628
|
+
);
|
|
534
629
|
}
|
|
630
|
+
})();
|
|
631
|
+
break;
|
|
632
|
+
case "undo":
|
|
633
|
+
if (this.checkpoints.length === 0) {
|
|
634
|
+
this.push("status", "nothing to undo \u2014 no ask this session, or not a git repo with commits");
|
|
635
|
+
} else {
|
|
636
|
+
this.restoreTo(this.checkpoints.length - 1);
|
|
637
|
+
}
|
|
638
|
+
break;
|
|
639
|
+
case "checkpoints":
|
|
640
|
+
if (this.checkpoints.length === 0) {
|
|
641
|
+
this.push("status", "no checkpoints yet \u2014 one is taken before every ask in a git repo");
|
|
642
|
+
} else {
|
|
643
|
+
const lines = this.checkpoints.map((c, i) => {
|
|
644
|
+
const mins = Math.max(0, Math.round((Date.now() - c.at) / 6e4));
|
|
645
|
+
return `${i + 1}. ${c.label} \xB7 ${mins}m ago`;
|
|
646
|
+
});
|
|
647
|
+
this.push("status", `${lines.join("\n")}
|
|
648
|
+
/restore <n> rewinds files to before that ask \xB7 /undo pops the last`);
|
|
649
|
+
}
|
|
650
|
+
break;
|
|
651
|
+
case "restore": {
|
|
652
|
+
const index = Number.parseInt(arg, 10);
|
|
653
|
+
if (!Number.isInteger(index) || index < 1 || index > this.checkpoints.length) {
|
|
654
|
+
this.push("status", `usage: /restore <1\u2013${Math.max(this.checkpoints.length, 1)}> \u2014 see /checkpoints`);
|
|
655
|
+
} else {
|
|
656
|
+
this.restoreTo(index - 1);
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case "resume": {
|
|
661
|
+
const saved = loadState(this.opts.cwd);
|
|
662
|
+
if (!saved) {
|
|
663
|
+
this.push("status", "no previous session for this project");
|
|
535
664
|
break;
|
|
536
665
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
push("status", "no previous session for this project");
|
|
666
|
+
try {
|
|
667
|
+
if (!getEngine(saved.engine).supportsResume) {
|
|
668
|
+
this.push("status", `previous engine ${saved.engine} cannot resume sessions`);
|
|
541
669
|
break;
|
|
542
670
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
671
|
+
} catch {
|
|
672
|
+
this.push("error", `previous engine ${saved.engine} is no longer available`);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
this.sessionId = saved.sessionId;
|
|
676
|
+
this.notify({ engineId: saved.engine, model: saved.model ?? this.state.model });
|
|
677
|
+
this.push("status", `resumed ${saved.engine} session${saved.lastAsk ? ` \xB7 "${saved.lastAsk}"` : ""}`);
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case "variants": {
|
|
681
|
+
const [sub, ...subRest] = arg.split(/\s+/);
|
|
682
|
+
if (sub === "apply") {
|
|
683
|
+
const id = subRest[0];
|
|
684
|
+
if (!id) {
|
|
685
|
+
this.push("status", "usage: /variants apply <id>");
|
|
550
686
|
break;
|
|
551
687
|
}
|
|
552
|
-
|
|
553
|
-
if (
|
|
554
|
-
|
|
555
|
-
|
|
688
|
+
const applied = applyVariant(this.opts.cwd, id);
|
|
689
|
+
if (applied.ok) {
|
|
690
|
+
cleanVariants(this.opts.cwd);
|
|
691
|
+
this.push("status", `applied ${id} to the working tree \u2014 review with git diff`);
|
|
692
|
+
} else {
|
|
693
|
+
this.push("error", applied.detail ?? "apply failed");
|
|
694
|
+
}
|
|
556
695
|
break;
|
|
557
696
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
sessionRef.current = void 0;
|
|
561
|
-
clearState(cwd);
|
|
697
|
+
if (sub === "clean") {
|
|
698
|
+
this.push("status", `removed ${cleanVariants(this.opts.cwd)} variant(s)`);
|
|
562
699
|
break;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
);
|
|
700
|
+
}
|
|
701
|
+
if (sub === "list") {
|
|
702
|
+
const ids = listVariants(this.opts.cwd);
|
|
703
|
+
this.push("status", ids.length > 0 ? ids.join(" \xB7 ") : "no variants \u2014 /variants <2-4> <ask>");
|
|
568
704
|
break;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
705
|
+
}
|
|
706
|
+
const n = Number.parseInt(sub ?? "", 10);
|
|
707
|
+
const ask = subRest.join(" ").trim();
|
|
708
|
+
if (!Number.isInteger(n) || n < 2 || n > 4 || ask.length === 0) {
|
|
709
|
+
this.push("status", "usage: /variants <2-4> <ask> \xB7 /variants apply <id> \xB7 list \xB7 clean");
|
|
573
710
|
break;
|
|
574
|
-
|
|
575
|
-
|
|
711
|
+
}
|
|
712
|
+
if (!isGitRepo(this.opts.cwd)) {
|
|
713
|
+
this.push("error", "variants need a git repo with at least one commit");
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
void (async () => {
|
|
717
|
+
cleanVariants(this.opts.cwd);
|
|
718
|
+
this.push("status", `generating ${n} directions in parallel \u2014 this runs ${n} engine sessions`);
|
|
719
|
+
this.notify({ running: true, runStartedAt: Date.now() });
|
|
720
|
+
const engine = getEngine(this.state.engineId);
|
|
721
|
+
const runs = await runVariants(
|
|
722
|
+
this.opts.cwd,
|
|
723
|
+
ask,
|
|
724
|
+
n,
|
|
725
|
+
engine,
|
|
726
|
+
this.state.model,
|
|
727
|
+
(familyId, text) => this.push("status", `[${familyId}] ${text}`)
|
|
728
|
+
);
|
|
729
|
+
const succeeded = runs.filter((r) => r.result.ok);
|
|
730
|
+
if (succeeded.length > 0 && findChrome() && detectDevCommand(this.opts.cwd)) {
|
|
731
|
+
this.push("status", "capturing one screenshot per variant\u2026");
|
|
732
|
+
const shots = await screenshotVariants(this.opts.cwd, succeeded.map((r) => r.variant));
|
|
733
|
+
for (const shot of shots) {
|
|
734
|
+
this.push(
|
|
735
|
+
shot.path ? "status" : "error",
|
|
736
|
+
`[${shot.familyId}] ${shot.path ?? shot.error ?? "screenshot failed"}`
|
|
737
|
+
);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
this.notify({ running: false });
|
|
741
|
+
this.push(
|
|
742
|
+
succeeded.length > 0 ? "status" : "error",
|
|
743
|
+
`${succeeded.length}/${runs.length} variants ready \u2014 /variants apply <id> keeps one, /variants clean discards`
|
|
744
|
+
);
|
|
745
|
+
this.drainQueue();
|
|
746
|
+
})();
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case "clear":
|
|
750
|
+
this.sessionId = void 0;
|
|
751
|
+
clearState(this.opts.cwd);
|
|
752
|
+
this.notify({ items: [], totals: { costUsd: 0, turns: 0 } });
|
|
753
|
+
break;
|
|
754
|
+
case "help":
|
|
755
|
+
this.push(
|
|
756
|
+
"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"
|
|
758
|
+
);
|
|
759
|
+
break;
|
|
760
|
+
case "quit":
|
|
761
|
+
case "exit":
|
|
762
|
+
this.push("status", this.summary());
|
|
763
|
+
this.dispose();
|
|
764
|
+
this.opts.onQuit?.();
|
|
765
|
+
break;
|
|
766
|
+
default:
|
|
767
|
+
this.push("error", `unknown command /${name} \u2014 try /help`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/tui/lineEditor.ts
|
|
773
|
+
var emptyLine = { text: "", cursor: 0 };
|
|
774
|
+
function fromText(text) {
|
|
775
|
+
return { text, cursor: text.length };
|
|
776
|
+
}
|
|
777
|
+
function insert(line, str) {
|
|
778
|
+
const clean = str.replace(/\r/g, "").replace(/\n+/g, " ");
|
|
779
|
+
return {
|
|
780
|
+
text: line.text.slice(0, line.cursor) + clean + line.text.slice(line.cursor),
|
|
781
|
+
cursor: line.cursor + clean.length
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
function backspace(line) {
|
|
785
|
+
if (line.cursor === 0) return line;
|
|
786
|
+
return {
|
|
787
|
+
text: line.text.slice(0, line.cursor - 1) + line.text.slice(line.cursor),
|
|
788
|
+
cursor: line.cursor - 1
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function left(line) {
|
|
792
|
+
return { ...line, cursor: Math.max(0, line.cursor - 1) };
|
|
793
|
+
}
|
|
794
|
+
function right(line) {
|
|
795
|
+
return { ...line, cursor: Math.min(line.text.length, line.cursor + 1) };
|
|
796
|
+
}
|
|
797
|
+
function home(line) {
|
|
798
|
+
return { ...line, cursor: 0 };
|
|
799
|
+
}
|
|
800
|
+
function end(line) {
|
|
801
|
+
return { ...line, cursor: line.text.length };
|
|
802
|
+
}
|
|
803
|
+
var isWordChar = (ch) => /[\p{L}\p{N}_/@.-]/u.test(ch);
|
|
804
|
+
function wordLeft(line) {
|
|
805
|
+
let i = line.cursor;
|
|
806
|
+
while (i > 0 && !isWordChar(line.text[i - 1])) i--;
|
|
807
|
+
while (i > 0 && isWordChar(line.text[i - 1])) i--;
|
|
808
|
+
return { ...line, cursor: i };
|
|
809
|
+
}
|
|
810
|
+
function wordRight(line) {
|
|
811
|
+
let i = line.cursor;
|
|
812
|
+
const n = line.text.length;
|
|
813
|
+
while (i < n && !isWordChar(line.text[i])) i++;
|
|
814
|
+
while (i < n && isWordChar(line.text[i])) i++;
|
|
815
|
+
return { ...line, cursor: i };
|
|
816
|
+
}
|
|
817
|
+
function killToEnd(line) {
|
|
818
|
+
return { text: line.text.slice(0, line.cursor), cursor: line.cursor };
|
|
819
|
+
}
|
|
820
|
+
function killToStart(line) {
|
|
821
|
+
return { text: line.text.slice(line.cursor), cursor: 0 };
|
|
822
|
+
}
|
|
823
|
+
function killWordBack(line) {
|
|
824
|
+
const target = wordLeft(line).cursor;
|
|
825
|
+
return {
|
|
826
|
+
text: line.text.slice(0, target) + line.text.slice(line.cursor),
|
|
827
|
+
cursor: target
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/tui/markdown.tsx
|
|
832
|
+
import { Box, Text } from "ink";
|
|
833
|
+
|
|
834
|
+
// src/tui/themeContext.tsx
|
|
835
|
+
import { createContext, useContext } from "react";
|
|
836
|
+
|
|
837
|
+
// src/tui/theme.ts
|
|
838
|
+
var THEMES = {
|
|
839
|
+
amber: {
|
|
840
|
+
name: "amber",
|
|
841
|
+
accent: "#e8a33d",
|
|
842
|
+
dim: "gray",
|
|
843
|
+
user: "#7aa2f7",
|
|
844
|
+
error: "#f7768e",
|
|
845
|
+
success: "#9ece6a",
|
|
846
|
+
tool: "#7dcfff"
|
|
847
|
+
},
|
|
848
|
+
ocean: {
|
|
849
|
+
name: "ocean",
|
|
850
|
+
accent: "#56b6c2",
|
|
851
|
+
dim: "gray",
|
|
852
|
+
user: "#61afef",
|
|
853
|
+
error: "#e06c75",
|
|
854
|
+
success: "#98c379",
|
|
855
|
+
tool: "#c678dd"
|
|
856
|
+
},
|
|
857
|
+
moss: {
|
|
858
|
+
name: "moss",
|
|
859
|
+
accent: "#a7c080",
|
|
860
|
+
dim: "gray",
|
|
861
|
+
user: "#7fbbb3",
|
|
862
|
+
error: "#e67e80",
|
|
863
|
+
success: "#83c092",
|
|
864
|
+
tool: "#d699b6"
|
|
865
|
+
},
|
|
866
|
+
rose: {
|
|
867
|
+
name: "rose",
|
|
868
|
+
accent: "#ebbcba",
|
|
869
|
+
dim: "gray",
|
|
870
|
+
user: "#9ccfd8",
|
|
871
|
+
error: "#eb6f92",
|
|
872
|
+
success: "#31748f",
|
|
873
|
+
tool: "#c4a7e7"
|
|
874
|
+
},
|
|
875
|
+
mono: {
|
|
876
|
+
name: "mono",
|
|
877
|
+
accent: "white",
|
|
878
|
+
dim: "gray",
|
|
879
|
+
user: "white",
|
|
880
|
+
error: "white",
|
|
881
|
+
success: "white",
|
|
882
|
+
tool: "gray"
|
|
883
|
+
}
|
|
884
|
+
};
|
|
885
|
+
var DEFAULT_THEME = "amber";
|
|
886
|
+
function resolveTheme(name, env = process.env) {
|
|
887
|
+
if (env.NO_COLOR !== void 0 && env.NO_COLOR !== "") return THEMES.mono;
|
|
888
|
+
return THEMES[name ?? DEFAULT_THEME] ?? THEMES[DEFAULT_THEME];
|
|
889
|
+
}
|
|
890
|
+
var theme = THEMES[DEFAULT_THEME];
|
|
891
|
+
|
|
892
|
+
// src/tui/themeContext.tsx
|
|
893
|
+
var ThemeContext = createContext(theme);
|
|
894
|
+
var ThemeProvider = ThemeContext.Provider;
|
|
895
|
+
function useTheme() {
|
|
896
|
+
return useContext(ThemeContext);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// src/tui/markdown.tsx
|
|
900
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
901
|
+
var INLINE_RE = /(\*\*[^*]+\*\*|\*[^*\s][^*]*\*|`[^`]+`)/g;
|
|
902
|
+
function parseInline(text) {
|
|
903
|
+
const segments = [];
|
|
904
|
+
let last = 0;
|
|
905
|
+
for (const match of text.matchAll(INLINE_RE)) {
|
|
906
|
+
if (match.index > last) segments.push({ text: text.slice(last, match.index) });
|
|
907
|
+
const token = match[0];
|
|
908
|
+
if (token.startsWith("**")) segments.push({ text: token.slice(2, -2), bold: true });
|
|
909
|
+
else if (token.startsWith("`")) segments.push({ text: token.slice(1, -1), code: true });
|
|
910
|
+
else segments.push({ text: token.slice(1, -1), italic: true });
|
|
911
|
+
last = match.index + token.length;
|
|
912
|
+
}
|
|
913
|
+
if (last < text.length) segments.push({ text: text.slice(last) });
|
|
914
|
+
return segments;
|
|
915
|
+
}
|
|
916
|
+
function parseBlocks(text) {
|
|
917
|
+
const blocks = [];
|
|
918
|
+
let code = null;
|
|
919
|
+
for (const rawLine of text.split("\n")) {
|
|
920
|
+
const line = rawLine.replace(/\s+$/, "");
|
|
921
|
+
if (code) {
|
|
922
|
+
if (/^\s*```/.test(line)) {
|
|
923
|
+
blocks.push({ type: "code", ...code });
|
|
924
|
+
code = null;
|
|
925
|
+
} else {
|
|
926
|
+
code.lines.push(rawLine);
|
|
576
927
|
}
|
|
577
|
-
|
|
578
|
-
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
const fence = /^\s*```(\S*)/.exec(line);
|
|
931
|
+
if (fence) {
|
|
932
|
+
code = { lang: fence[1] ?? "", lines: [] };
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const heading = /^(#{1,4})\s+(.*)$/.exec(line);
|
|
936
|
+
if (heading) {
|
|
937
|
+
blocks.push({ type: "heading", level: heading[1].length, text: heading[2] });
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(line) && line.trim().length >= 3) {
|
|
941
|
+
blocks.push({ type: "hr" });
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
const item = /^(\s*)[-*+]\s+(.*)$/.exec(line);
|
|
945
|
+
if (item) {
|
|
946
|
+
blocks.push({ type: "item", text: item[2], indent: Math.floor(item[1].length / 2) });
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const ordered = /^(\s*)(\d+)[.)]\s+(.*)$/.exec(line);
|
|
950
|
+
if (ordered) {
|
|
951
|
+
blocks.push({
|
|
952
|
+
type: "item",
|
|
953
|
+
text: ordered[3],
|
|
954
|
+
indent: Math.floor(ordered[1].length / 2),
|
|
955
|
+
ordered: ordered[2]
|
|
956
|
+
});
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const quote = /^\s*>\s?(.*)$/.exec(line);
|
|
960
|
+
if (quote) {
|
|
961
|
+
blocks.push({ type: "quote", text: quote[1] });
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
blocks.push({ type: "para", text: line });
|
|
965
|
+
}
|
|
966
|
+
if (code) blocks.push({ type: "code", ...code });
|
|
967
|
+
return blocks;
|
|
968
|
+
}
|
|
969
|
+
function Inline({ text, dim }) {
|
|
970
|
+
const theme2 = useTheme();
|
|
971
|
+
const segments = parseInline(text);
|
|
972
|
+
return /* @__PURE__ */ jsx(Text, { wrap: "wrap", dimColor: dim, children: segments.map(
|
|
973
|
+
(segment, index) => segment.code ? /* @__PURE__ */ jsx(Text, { color: theme2.tool, children: segment.text }, index) : /* @__PURE__ */ jsx(Text, { bold: segment.bold, italic: segment.italic, children: segment.text }, index)
|
|
974
|
+
) });
|
|
975
|
+
}
|
|
976
|
+
function Markdown({ text }) {
|
|
977
|
+
const theme2 = useTheme();
|
|
978
|
+
const blocks = parseBlocks(text);
|
|
979
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: blocks.map((block, index) => {
|
|
980
|
+
switch (block.type) {
|
|
981
|
+
case "heading":
|
|
982
|
+
return /* @__PURE__ */ jsx(Text, { bold: true, color: block.level <= 2 ? theme2.accent : void 0, wrap: "wrap", children: block.text }, index);
|
|
983
|
+
case "item":
|
|
984
|
+
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 1 + block.indent * 2, children: [
|
|
985
|
+
/* @__PURE__ */ jsxs(Text, { color: theme2.accent, children: [
|
|
986
|
+
block.ordered ? `${block.ordered}.` : "\u2022",
|
|
987
|
+
" "
|
|
988
|
+
] }),
|
|
989
|
+
/* @__PURE__ */ jsx(Inline, { text: block.text })
|
|
990
|
+
] }, index);
|
|
991
|
+
case "quote":
|
|
992
|
+
return /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
|
|
993
|
+
/* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u2502 " }),
|
|
994
|
+
/* @__PURE__ */ jsx(Inline, { text: block.text, dim: true })
|
|
995
|
+
] }, index);
|
|
996
|
+
case "code":
|
|
997
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: 1, children: block.lines.map((line, lineIndex) => /* @__PURE__ */ jsxs(Text, { children: [
|
|
998
|
+
/* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u258F " }),
|
|
999
|
+
/* @__PURE__ */ jsx(Text, { color: theme2.tool, children: line.length > 0 ? line : " " })
|
|
1000
|
+
] }, lineIndex)) }, index);
|
|
1001
|
+
case "hr":
|
|
1002
|
+
return /* @__PURE__ */ jsx(Text, { color: theme2.dim, children: "\u2500".repeat(32) }, index);
|
|
1003
|
+
case "para":
|
|
1004
|
+
return block.text.length === 0 ? /* @__PURE__ */ jsx(Text, { children: " " }, index) : /* @__PURE__ */ jsx(Inline, { text: block.text }, index);
|
|
1005
|
+
}
|
|
1006
|
+
}) });
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/tui/messages.tsx
|
|
1010
|
+
import { Text as Text2 } from "ink";
|
|
1011
|
+
import { useEffect, useState } from "react";
|
|
1012
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1013
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1014
|
+
var PHRASES = ["working", "thinking", "squinting", "crafting", "still at it"];
|
|
1015
|
+
function WorkingLine({ startedAt }) {
|
|
1016
|
+
const theme2 = useTheme();
|
|
1017
|
+
const [frame, setFrame] = useState(0);
|
|
1018
|
+
const [elapsed, setElapsed] = useState(0);
|
|
1019
|
+
useEffect(() => {
|
|
1020
|
+
const timer = setInterval(() => {
|
|
1021
|
+
setFrame((f) => (f + 1) % SPINNER_FRAMES.length);
|
|
1022
|
+
setElapsed(Math.floor((Date.now() - startedAt) / 1e3));
|
|
1023
|
+
}, 80);
|
|
1024
|
+
return () => clearInterval(timer);
|
|
1025
|
+
}, [startedAt]);
|
|
1026
|
+
const phrase = PHRASES[Math.min(Math.floor(elapsed / 8), PHRASES.length - 1)];
|
|
1027
|
+
return /* @__PURE__ */ jsxs2(Text2, { children: [
|
|
1028
|
+
/* @__PURE__ */ jsx2(Text2, { color: theme2.accent, children: SPINNER_FRAMES[frame] }),
|
|
1029
|
+
/* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, children: [
|
|
1030
|
+
" ",
|
|
1031
|
+
phrase,
|
|
1032
|
+
"\u2026 ",
|
|
1033
|
+
elapsed,
|
|
1034
|
+
"s \xB7 esc to interrupt"
|
|
1035
|
+
] })
|
|
1036
|
+
] });
|
|
1037
|
+
}
|
|
1038
|
+
function toolGlyph(text) {
|
|
1039
|
+
const name = text.split(/[\s·]/, 1)[0]?.toLowerCase() ?? "";
|
|
1040
|
+
if (name.includes("todo")) return "\u2630";
|
|
1041
|
+
if (name.includes("read") || name.includes("view") || name.includes("cat")) return "\u2299";
|
|
1042
|
+
if (name.includes("edit") || name.includes("write") || name.includes("patch") || name.includes("apply")) return "\u270E";
|
|
1043
|
+
if (name.includes("bash") || name.includes("shell") || name.includes("exec") || name.includes("command")) return "$";
|
|
1044
|
+
if (name.includes("grep") || name.includes("glob") || name.includes("search") || name.includes("find")) return "\u2315";
|
|
1045
|
+
if (name.includes("web") || name.includes("fetch") || name.includes("http")) return "\u21E3";
|
|
1046
|
+
if (name.includes("task") || name.includes("agent")) return "\u25C7";
|
|
1047
|
+
return "\u2699";
|
|
1048
|
+
}
|
|
1049
|
+
function MessageLine({ message }) {
|
|
1050
|
+
const theme2 = useTheme();
|
|
1051
|
+
switch (message.role) {
|
|
1052
|
+
case "user":
|
|
1053
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.user, wrap: "wrap", children: [
|
|
1054
|
+
"\u276F ",
|
|
1055
|
+
message.text
|
|
1056
|
+
] });
|
|
1057
|
+
case "assistant":
|
|
1058
|
+
return /* @__PURE__ */ jsx2(Markdown, { text: message.text });
|
|
1059
|
+
case "status":
|
|
1060
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.dim, wrap: "wrap", children: [
|
|
1061
|
+
"\xB7 ",
|
|
1062
|
+
message.text
|
|
1063
|
+
] });
|
|
1064
|
+
case "tool":
|
|
1065
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.tool, wrap: "wrap", children: [
|
|
1066
|
+
toolGlyph(message.text),
|
|
1067
|
+
" ",
|
|
1068
|
+
message.text
|
|
1069
|
+
] });
|
|
1070
|
+
case "thinking":
|
|
1071
|
+
return /* @__PURE__ */ jsx2(Text2, { color: theme2.dim, italic: true, wrap: "wrap", children: message.text });
|
|
1072
|
+
case "error":
|
|
1073
|
+
return /* @__PURE__ */ jsxs2(Text2, { color: theme2.error, wrap: "wrap", children: [
|
|
1074
|
+
"\u2717 ",
|
|
1075
|
+
message.text
|
|
1076
|
+
] });
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// src/tui/App.tsx
|
|
1081
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
1082
|
+
function App({
|
|
1083
|
+
cwd,
|
|
1084
|
+
initialEngine,
|
|
1085
|
+
initialModel,
|
|
1086
|
+
autoDev,
|
|
1087
|
+
autoFix,
|
|
1088
|
+
autoProbe,
|
|
1089
|
+
autoCheck,
|
|
1090
|
+
bell,
|
|
1091
|
+
initialTheme
|
|
1092
|
+
}) {
|
|
1093
|
+
const { exit } = useApp();
|
|
1094
|
+
const [themeName, setThemeName] = useState2(() => resolveTheme(initialTheme).name);
|
|
1095
|
+
const theme2 = resolveTheme(themeName);
|
|
1096
|
+
const sessionRef = useRef(null);
|
|
1097
|
+
if (!sessionRef.current) {
|
|
1098
|
+
sessionRef.current = new Session({
|
|
1099
|
+
cwd,
|
|
1100
|
+
engineId: initialEngine,
|
|
1101
|
+
model: initialModel,
|
|
1102
|
+
autoDev,
|
|
1103
|
+
autoFix,
|
|
1104
|
+
autoProbe,
|
|
1105
|
+
autoCheck,
|
|
1106
|
+
// Delay lets the goodbye summary land in the Static scrollback.
|
|
1107
|
+
onQuit: () => setTimeout(() => exit(), 60)
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
const session = sessionRef.current;
|
|
1111
|
+
const state = useSyncExternalStore(
|
|
1112
|
+
useMemo(() => (listener) => session.subscribe(listener), [session]),
|
|
1113
|
+
() => session.getState()
|
|
579
1114
|
);
|
|
1115
|
+
const [line, setLine] = useState2(emptyLine);
|
|
1116
|
+
const historyRef = useRef([]);
|
|
1117
|
+
const historyIndexRef = useRef(-1);
|
|
1118
|
+
const ctrlCArmedAtRef = useRef(0);
|
|
1119
|
+
const wasRunningRef = useRef(false);
|
|
1120
|
+
if (wasRunningRef.current && !state.running && bell !== false) {
|
|
1121
|
+
process.stdout.write("\x07");
|
|
1122
|
+
}
|
|
1123
|
+
wasRunningRef.current = state.running;
|
|
580
1124
|
useInput((char, key) => {
|
|
581
1125
|
if (key.ctrl && char === "c") {
|
|
582
|
-
|
|
583
|
-
|
|
1126
|
+
const now = Date.now();
|
|
1127
|
+
if (now - ctrlCArmedAtRef.current < 2e3) {
|
|
1128
|
+
session.note(session.summary());
|
|
1129
|
+
session.dispose();
|
|
1130
|
+
setTimeout(() => exit(), 60);
|
|
1131
|
+
} else {
|
|
1132
|
+
ctrlCArmedAtRef.current = now;
|
|
1133
|
+
session.note("press ctrl+c again to exit");
|
|
1134
|
+
}
|
|
584
1135
|
return;
|
|
585
1136
|
}
|
|
586
|
-
if (running) {
|
|
587
|
-
|
|
1137
|
+
if (key.escape && state.running) {
|
|
1138
|
+
session.interrupt();
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (key.tab && key.shift) {
|
|
1142
|
+
session.cycleMode();
|
|
588
1143
|
return;
|
|
589
1144
|
}
|
|
590
1145
|
if (key.return) {
|
|
591
|
-
const value =
|
|
592
|
-
|
|
1146
|
+
const value = line.text.trim();
|
|
1147
|
+
setLine(emptyLine);
|
|
593
1148
|
historyIndexRef.current = -1;
|
|
594
1149
|
if (!value) return;
|
|
595
1150
|
historyRef.current.push(value);
|
|
596
|
-
if (value.startsWith("/")) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1151
|
+
if (value === "/theme" || value.startsWith("/theme ")) {
|
|
1152
|
+
const requested = value.slice("/theme".length).trim();
|
|
1153
|
+
if (!requested) {
|
|
1154
|
+
session.note(`themes: ${Object.keys(THEMES).join(", ")} \u2014 /theme <name>`);
|
|
1155
|
+
} else if (THEMES[requested]) {
|
|
1156
|
+
setThemeName(requested);
|
|
1157
|
+
session.note(`theme \u2192 ${requested}`);
|
|
1158
|
+
} else {
|
|
1159
|
+
session.note(`unknown theme "${requested}" \u2014 themes: ${Object.keys(THEMES).join(", ")}`);
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
600
1162
|
}
|
|
1163
|
+
session.input(value);
|
|
601
1164
|
return;
|
|
602
1165
|
}
|
|
603
1166
|
if (key.upArrow) {
|
|
@@ -605,7 +1168,7 @@ ${result.a11y.slice(0, 5).join("\n")}`);
|
|
|
605
1168
|
if (history.length === 0) return;
|
|
606
1169
|
const next = historyIndexRef.current === -1 ? history.length - 1 : Math.max(historyIndexRef.current - 1, 0);
|
|
607
1170
|
historyIndexRef.current = next;
|
|
608
|
-
|
|
1171
|
+
setLine(fromText(history[next] ?? ""));
|
|
609
1172
|
return;
|
|
610
1173
|
}
|
|
611
1174
|
if (key.downArrow) {
|
|
@@ -614,44 +1177,93 @@ ${result.a11y.slice(0, 5).join("\n")}`);
|
|
|
614
1177
|
const next = historyIndexRef.current + 1;
|
|
615
1178
|
if (next >= history.length) {
|
|
616
1179
|
historyIndexRef.current = -1;
|
|
617
|
-
|
|
1180
|
+
setLine(emptyLine);
|
|
618
1181
|
} else {
|
|
619
1182
|
historyIndexRef.current = next;
|
|
620
|
-
|
|
1183
|
+
setLine(fromText(history[next] ?? ""));
|
|
621
1184
|
}
|
|
622
1185
|
return;
|
|
623
1186
|
}
|
|
1187
|
+
if (key.leftArrow) {
|
|
1188
|
+
setLine((prev) => key.meta ? wordLeft(prev) : left(prev));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (key.rightArrow) {
|
|
1192
|
+
setLine((prev) => key.meta ? wordRight(prev) : right(prev));
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
624
1195
|
if (key.backspace || key.delete) {
|
|
625
|
-
|
|
1196
|
+
setLine((prev) => key.meta ? killWordBack(prev) : backspace(prev));
|
|
626
1197
|
return;
|
|
627
1198
|
}
|
|
628
|
-
if (
|
|
629
|
-
|
|
1199
|
+
if (key.ctrl) {
|
|
1200
|
+
switch (char) {
|
|
1201
|
+
case "a":
|
|
1202
|
+
setLine(home);
|
|
1203
|
+
return;
|
|
1204
|
+
case "e":
|
|
1205
|
+
setLine(end);
|
|
1206
|
+
return;
|
|
1207
|
+
case "k":
|
|
1208
|
+
setLine(killToEnd);
|
|
1209
|
+
return;
|
|
1210
|
+
case "u":
|
|
1211
|
+
setLine(killToStart);
|
|
1212
|
+
return;
|
|
1213
|
+
case "w":
|
|
1214
|
+
setLine(killWordBack);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (char && !key.meta) {
|
|
1220
|
+
setLine((prev) => insert(prev, char));
|
|
630
1221
|
}
|
|
631
1222
|
});
|
|
632
|
-
const devBadge = devState === "running" ? ` \xB7 ${devUrl ?? "dev running"}` : devState === "starting" ? " \xB7 dev starting\u2026" : devState === "crashed" ? " \xB7 dev crashed" : "";
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
/* @__PURE__ */
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1223
|
+
const devBadge = state.devState === "running" ? ` \xB7 ${state.devUrl ?? "dev running"}` : state.devState === "starting" ? " \xB7 dev starting\u2026" : state.devState === "crashed" ? " \xB7 dev crashed" : "";
|
|
1224
|
+
const totalsBadge = state.totals.turns > 0 ? ` \xB7 ${state.totals.turns} turn${state.totals.turns === 1 ? "" : "s"}${state.totals.costUsd > 0 ? ` \xB7 $${state.totals.costUsd.toFixed(2)}` : ""}` : "";
|
|
1225
|
+
return /* @__PURE__ */ jsx3(ThemeProvider, { value: theme2, children: /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", paddingX: 1, children: [
|
|
1226
|
+
/* @__PURE__ */ jsx3(Static, { items: state.items, children: (item) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(MessageLine, { message: item }) }, item.id) }),
|
|
1227
|
+
state.liveText.length > 0 && /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsx3(Markdown, { text: state.liveText }) }),
|
|
1228
|
+
state.running && /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx3(WorkingLine, { startedAt: state.runStartedAt }) }),
|
|
1229
|
+
state.queue.map((queued, index) => /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
|
|
1230
|
+
"\u22EF queued: ",
|
|
1231
|
+
queued
|
|
1232
|
+
] }) }, index)),
|
|
1233
|
+
/* @__PURE__ */ jsx3(Box2, { marginTop: state.running ? 0 : 1, children: /* @__PURE__ */ jsxs3(Text3, { children: [
|
|
1234
|
+
/* @__PURE__ */ jsx3(Text3, { color: theme2.accent, children: "\u276F " }),
|
|
1235
|
+
line.text.slice(0, line.cursor),
|
|
1236
|
+
/* @__PURE__ */ jsx3(Text3, { inverse: true, children: line.text[line.cursor] ?? " " }),
|
|
1237
|
+
line.text.slice(line.cursor + 1)
|
|
640
1238
|
] }) }),
|
|
641
|
-
/* @__PURE__ */
|
|
642
|
-
|
|
643
|
-
|
|
1239
|
+
/* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs3(Text3, { color: theme2.dim, children: [
|
|
1240
|
+
/* @__PURE__ */ jsxs3(
|
|
1241
|
+
Text3,
|
|
1242
|
+
{
|
|
1243
|
+
color: state.mode === "yolo" ? theme2.error : state.mode === "plan" ? theme2.user : theme2.dim,
|
|
1244
|
+
bold: state.mode !== "safe",
|
|
1245
|
+
children: [
|
|
1246
|
+
"[",
|
|
1247
|
+
state.mode,
|
|
1248
|
+
"]"
|
|
1249
|
+
]
|
|
1250
|
+
}
|
|
1251
|
+
),
|
|
1252
|
+
" ",
|
|
1253
|
+
state.engineId,
|
|
1254
|
+
state.model ? ` \xB7 ${state.model}` : "",
|
|
644
1255
|
" \xB7 ",
|
|
645
|
-
|
|
1256
|
+
path3.basename(cwd),
|
|
646
1257
|
devBadge,
|
|
647
|
-
|
|
1258
|
+
totalsBadge,
|
|
1259
|
+
" \xB7 shift+tab mode \xB7 /help"
|
|
648
1260
|
] }) })
|
|
649
|
-
] });
|
|
1261
|
+
] }) });
|
|
650
1262
|
}
|
|
651
1263
|
|
|
652
1264
|
// src/cli.tsx
|
|
653
|
-
import { jsx as
|
|
654
|
-
var VERSION = "0.1
|
|
1265
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
1266
|
+
var VERSION = "0.2.1";
|
|
655
1267
|
var program = new Command();
|
|
656
1268
|
program.name("squint").description("Lovable for your terminal \u2014 a frontend harness on top of Claude Code, Codex, and friends.").version(VERSION);
|
|
657
1269
|
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(
|
|
@@ -685,18 +1297,41 @@ program.command("engines").description("List engines and whether they are instal
|
|
|
685
1297
|
console.log(`${status} ${engine.id.padEnd(10)} ${engine.name.padEnd(14)} ${location}`);
|
|
686
1298
|
}
|
|
687
1299
|
});
|
|
688
|
-
program.command("doctor").description("Check squint prerequisites and engine availability").action(async () => {
|
|
1300
|
+
program.command("doctor").description("Check squint prerequisites and engine availability").option("--probe", "run each detected engine with a one-word prompt to verify auth works").action(async (options) => {
|
|
689
1301
|
const [major] = process.versions.node.split(".");
|
|
690
|
-
const nodeOk = Number(major) >=
|
|
691
|
-
console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >=
|
|
1302
|
+
const nodeOk = Number(major) >= 22;
|
|
1303
|
+
console.log(`${nodeOk ? pc.green("\u2713") : pc.red("\u2717")} node ${process.versions.node}${nodeOk ? "" : " (need >= 22)"}`);
|
|
692
1304
|
const detected = detectEngines();
|
|
693
1305
|
for (const { engine, path: binaryPath } of detected) {
|
|
694
1306
|
const status = binaryPath ? pc.green("\u2713") : pc.yellow("\u25CB");
|
|
695
1307
|
console.log(`${status} ${engine.name}${binaryPath ? "" : pc.dim(` \u2014 install: ${engine.install}`)}`);
|
|
696
1308
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
1309
|
+
if (options.probe) {
|
|
1310
|
+
const { runAgent: runAgent2 } = await import("./run-XHLCTF6V.js");
|
|
1311
|
+
console.log(pc.dim("\nprobing engines with a one-word prompt (verifies auth end to end)\u2026"));
|
|
1312
|
+
for (const { engine, path: binaryPath } of detected) {
|
|
1313
|
+
if (!binaryPath) continue;
|
|
1314
|
+
const startedAt = Date.now();
|
|
1315
|
+
const abort = new AbortController();
|
|
1316
|
+
const timer = setTimeout(() => abort.abort(), 9e4);
|
|
1317
|
+
const result = await runAgent2(
|
|
1318
|
+
engine,
|
|
1319
|
+
{ prompt: "Reply with exactly: ok", cwd: process.cwd() },
|
|
1320
|
+
() => {
|
|
1321
|
+
},
|
|
1322
|
+
abort.signal
|
|
1323
|
+
);
|
|
1324
|
+
clearTimeout(timer);
|
|
1325
|
+
const secs = ((Date.now() - startedAt) / 1e3).toFixed(1);
|
|
1326
|
+
const detail = (result.error ?? "failed").split("\n").filter((l) => l.trim()).at(-1) ?? "failed";
|
|
1327
|
+
console.log(
|
|
1328
|
+
result.ok ? `${pc.green("\u2713")} ${engine.id.padEnd(10)} responded in ${secs}s` : `${pc.red("\u2717")} ${engine.id.padEnd(10)} ${pc.dim(detail.slice(0, 110))}`
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
const { findChrome: findChrome2 } = await import("./chrome-RD7XQ767.js");
|
|
1333
|
+
const { hasWebSocket } = await import("./cdp-RTWPFIX5.js");
|
|
1334
|
+
const chrome = findChrome2();
|
|
700
1335
|
console.log(
|
|
701
1336
|
chrome ? `${pc.green("\u2713")} Chrome ${pc.dim(chrome)}` : `${pc.yellow("\u25CB")} Chrome ${pc.dim("\u2014 screenshots and runtime probing disabled")}`
|
|
702
1337
|
);
|
|
@@ -713,7 +1348,7 @@ ${available.length} engine(s) ready.`));
|
|
|
713
1348
|
}
|
|
714
1349
|
});
|
|
715
1350
|
program.command("init").description("Scaffold a new Vite + React + TS + Tailwind app with token-first CSS").argument("[dir]", "target directory", ".").option("--force", "write into a non-empty directory").option("--no-install", "skip npm install").action(async (dir, options) => {
|
|
716
|
-
const { installDependencies, writeTemplate } = await import("./init-
|
|
1351
|
+
const { installDependencies, writeTemplate } = await import("./init-LSYB32NS.js");
|
|
717
1352
|
let result;
|
|
718
1353
|
try {
|
|
719
1354
|
result = writeTemplate(dir, { force: options.force });
|
|
@@ -737,10 +1372,49 @@ program.command("init").description("Scaffold a new Vite + React + TS + Tailwind
|
|
|
737
1372
|
Next: ${pc.bold(`${cd}${options.install ? "" : "npm install && "}squint`)}`);
|
|
738
1373
|
console.log(pc.dim("then describe what to build \u2014 /dev starts the preview server"));
|
|
739
1374
|
});
|
|
1375
|
+
var skillsCommand = program.command("skills").description("Project knowledge injected into asks (.squint/rules.md + .squint/skills/)");
|
|
1376
|
+
skillsCommand.command("list").description("Show always-on rules and trigger-matched skills").action(async () => {
|
|
1377
|
+
const { loadRules, loadSkills } = await import("./skills-UGHU22BS.js");
|
|
1378
|
+
const cwd = process.cwd();
|
|
1379
|
+
const rules = loadRules(cwd);
|
|
1380
|
+
console.log(rules ? `${pc.green("\u2713")} rules.md ${pc.dim(`(${rules.split("\n").length} lines, always on)`)}` : pc.dim("\u25CB no .squint/rules.md"));
|
|
1381
|
+
const skills = loadSkills(cwd);
|
|
1382
|
+
if (skills.length === 0) {
|
|
1383
|
+
console.log(pc.dim("\u25CB no skills \u2014 squint skills init writes an example"));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
for (const skill of skills) {
|
|
1387
|
+
console.log(`${pc.green("\u2713")} ${skill.name.padEnd(20)} ${pc.dim(`triggers: ${skill.triggers.join(", ")}`)}`);
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
skillsCommand.command("init").description("Scaffold .squint/rules.md and an example skill").action(async () => {
|
|
1391
|
+
const fs2 = await import("fs");
|
|
1392
|
+
const nodePath = await import("path");
|
|
1393
|
+
const cwd = process.cwd();
|
|
1394
|
+
const skillsDir = nodePath.join(cwd, ".squint", "skills");
|
|
1395
|
+
fs2.mkdirSync(skillsDir, { recursive: true });
|
|
1396
|
+
const rules = nodePath.join(cwd, ".squint", "rules.md");
|
|
1397
|
+
if (!fs2.existsSync(rules)) {
|
|
1398
|
+
fs2.writeFileSync(
|
|
1399
|
+
rules,
|
|
1400
|
+
"# 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"
|
|
1401
|
+
);
|
|
1402
|
+
console.log(pc.green("\u2713 .squint/rules.md"));
|
|
1403
|
+
}
|
|
1404
|
+
const example = nodePath.join(skillsDir, "example.md");
|
|
1405
|
+
if (!fs2.existsSync(example)) {
|
|
1406
|
+
fs2.writeFileSync(
|
|
1407
|
+
example,
|
|
1408
|
+
"---\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"
|
|
1409
|
+
);
|
|
1410
|
+
console.log(pc.green("\u2713 .squint/skills/example.md"));
|
|
1411
|
+
}
|
|
1412
|
+
console.log(pc.dim("rules are always-on; skills inject when an ask mentions a trigger"));
|
|
1413
|
+
});
|
|
740
1414
|
program.command("tag").description("Add the element picker to this Vite app (Alt+S in the browser \u2192 click \u2192 file:line:col)").action(async () => {
|
|
741
1415
|
const fs2 = await import("fs");
|
|
742
1416
|
const nodePath = await import("path");
|
|
743
|
-
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-
|
|
1417
|
+
const { patchViteConfig, TAGGER_FILENAME, TAGGER_SOURCE } = await import("./source-ZZU245VN.js");
|
|
744
1418
|
const cwd = process.cwd();
|
|
745
1419
|
const taggerPath = nodePath.join(cwd, TAGGER_FILENAME);
|
|
746
1420
|
fs2.writeFileSync(taggerPath, TAGGER_SOURCE);
|
|
@@ -776,21 +1450,21 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
|
|
|
776
1450
|
process.exitCode = 1;
|
|
777
1451
|
return;
|
|
778
1452
|
}
|
|
779
|
-
const { isGitRepo } = await import("./snapshot-KKUY5RR6.js");
|
|
780
|
-
if (!
|
|
1453
|
+
const { isGitRepo: isGitRepo2 } = await import("./snapshot-KKUY5RR6.js");
|
|
1454
|
+
if (!isGitRepo2(cwd)) {
|
|
781
1455
|
console.error(pc.red("\u2717 variants need a git repo with at least one commit"));
|
|
782
1456
|
process.exitCode = 1;
|
|
783
1457
|
return;
|
|
784
1458
|
}
|
|
785
|
-
const { runVariants, cleanVariants } = await import("./variants-
|
|
1459
|
+
const { runVariants: runVariants2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
|
|
786
1460
|
const config = loadConfig(defaultPaths(cwd));
|
|
787
1461
|
const engineId = resolveEngineId(config, options.engine);
|
|
788
1462
|
const engine = getEngine(engineId);
|
|
789
1463
|
const model = resolveModel(config, engineId, options.model);
|
|
790
1464
|
const ask = promptWords.join(" ");
|
|
791
|
-
|
|
1465
|
+
cleanVariants2(cwd);
|
|
792
1466
|
console.log(pc.dim(`generating ${n} directions in parallel via ${engine.id} \u2014 this runs ${n} engine sessions`));
|
|
793
|
-
const runs = await
|
|
1467
|
+
const runs = await runVariants2(
|
|
794
1468
|
cwd,
|
|
795
1469
|
ask,
|
|
796
1470
|
n,
|
|
@@ -800,9 +1474,9 @@ variantsCommand.command("gen").description("Generate n variants of one ask in pa
|
|
|
800
1474
|
);
|
|
801
1475
|
const succeeded = runs.filter((r) => r.result.ok);
|
|
802
1476
|
if (options.shots && succeeded.length > 0) {
|
|
803
|
-
const { screenshotVariants } = await import("./shots-
|
|
1477
|
+
const { screenshotVariants: screenshotVariants2 } = await import("./shots-LRYYMTPK.js");
|
|
804
1478
|
console.log(pc.dim("capturing screenshots\u2026"));
|
|
805
|
-
const shots = await
|
|
1479
|
+
const shots = await screenshotVariants2(cwd, succeeded.map((r) => r.variant));
|
|
806
1480
|
for (const shot of shots) {
|
|
807
1481
|
console.log(`${pc.green("\u2713")} ${shot.familyId.padEnd(18)} ${shot.path ?? pc.dim(shot.error ?? "")}`);
|
|
808
1482
|
}
|
|
@@ -815,8 +1489,8 @@ ${succeeded.length}/${runs.length} variants ready in .squint/variants/ \u2014 `
|
|
|
815
1489
|
}
|
|
816
1490
|
);
|
|
817
1491
|
variantsCommand.command("list").description("List generated variants").action(async () => {
|
|
818
|
-
const { listVariants } = await import("./variants-
|
|
819
|
-
const ids =
|
|
1492
|
+
const { listVariants: listVariants2 } = await import("./variants-7A7723L7.js");
|
|
1493
|
+
const ids = listVariants2(process.cwd());
|
|
820
1494
|
if (ids.length === 0) {
|
|
821
1495
|
console.log(pc.dim('no variants \u2014 squint variants gen <n> "<ask>"'));
|
|
822
1496
|
return;
|
|
@@ -824,26 +1498,26 @@ variantsCommand.command("list").description("List generated variants").action(as
|
|
|
824
1498
|
for (const id of ids) console.log(id);
|
|
825
1499
|
});
|
|
826
1500
|
variantsCommand.command("apply").description("Apply one variant\u2019s changes to the main tree and discard the rest").argument("<id>", "family id of the winning variant").action(async (id) => {
|
|
827
|
-
const { applyVariant, cleanVariants } = await import("./variants-
|
|
1501
|
+
const { applyVariant: applyVariant2, cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
|
|
828
1502
|
const cwd = process.cwd();
|
|
829
|
-
const result =
|
|
1503
|
+
const result = applyVariant2(cwd, id);
|
|
830
1504
|
if (!result.ok) {
|
|
831
1505
|
console.error(pc.red(`\u2717 ${result.detail}`));
|
|
832
1506
|
process.exitCode = 1;
|
|
833
1507
|
return;
|
|
834
1508
|
}
|
|
835
|
-
|
|
1509
|
+
cleanVariants2(cwd);
|
|
836
1510
|
console.log(pc.green(`\u2713 applied ${id} to the working tree`) + pc.dim(" \u2014 review with git diff"));
|
|
837
1511
|
});
|
|
838
1512
|
variantsCommand.command("clean").description("Discard all variants").action(async () => {
|
|
839
|
-
const { cleanVariants } = await import("./variants-
|
|
840
|
-
const count =
|
|
1513
|
+
const { cleanVariants: cleanVariants2 } = await import("./variants-7A7723L7.js");
|
|
1514
|
+
const count = cleanVariants2(process.cwd());
|
|
841
1515
|
console.log(pc.dim(`removed ${count} variant(s)`));
|
|
842
1516
|
});
|
|
843
1517
|
program.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) => {
|
|
844
1518
|
const fs2 = await import("fs");
|
|
845
1519
|
const nodePath = await import("path");
|
|
846
|
-
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-
|
|
1520
|
+
const { FAMILIES, getFamily, renderFamilyBrief } = await import("./families-2G5SLLY7.js");
|
|
847
1521
|
if (!familyId) {
|
|
848
1522
|
console.log(pc.bold("Aesthetic families") + pc.dim(" \u2014 squint brief <id>\n"));
|
|
849
1523
|
for (const family2 of FAMILIES) {
|
|
@@ -870,7 +1544,7 @@ program.command("brief").description("Set a committed design direction for this
|
|
|
870
1544
|
console.log(pc.dim("every squint ask in this repo now holds this direction \u2014 edit the file to remix"));
|
|
871
1545
|
});
|
|
872
1546
|
program.command("check").description("Run this project\u2019s quality gates (typecheck, lint, test, build)").action(async () => {
|
|
873
|
-
const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-
|
|
1547
|
+
const { detectGates: detectGates2, runGates: runGates2 } = await import("./gates-I2GGQGMA.js");
|
|
874
1548
|
const cwd = process.cwd();
|
|
875
1549
|
const gates = detectGates2(cwd);
|
|
876
1550
|
if (gates.length === 0) {
|
|
@@ -886,7 +1560,7 @@ program.command("check").description("Run this project\u2019s quality gates (typ
|
|
|
886
1560
|
if (results.some((r) => !r.ok)) process.exitCode = 1;
|
|
887
1561
|
});
|
|
888
1562
|
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) => {
|
|
889
|
-
const { captureViewports: captureViewports2 } = await import("./preview-
|
|
1563
|
+
const { captureViewports: captureViewports2 } = await import("./preview-XQ7EOHV4.js");
|
|
890
1564
|
const result = await captureViewports2(process.cwd(), url);
|
|
891
1565
|
if (!result) {
|
|
892
1566
|
console.error(pc.red("\u2717 no Chrome/Chromium found"));
|
|
@@ -919,7 +1593,7 @@ program.action(() => {
|
|
|
919
1593
|
const engineId = resolveEngineId(config);
|
|
920
1594
|
const model = resolveModel(config, engineId);
|
|
921
1595
|
render(
|
|
922
|
-
/* @__PURE__ */
|
|
1596
|
+
/* @__PURE__ */ jsx4(
|
|
923
1597
|
App,
|
|
924
1598
|
{
|
|
925
1599
|
cwd,
|
|
@@ -927,7 +1601,10 @@ program.action(() => {
|
|
|
927
1601
|
initialModel: model,
|
|
928
1602
|
autoDev: config.autoDev,
|
|
929
1603
|
autoFix: config.autoFix,
|
|
930
|
-
autoProbe: config.autoProbe
|
|
1604
|
+
autoProbe: config.autoProbe,
|
|
1605
|
+
autoCheck: config.autoCheck,
|
|
1606
|
+
bell: config.bell,
|
|
1607
|
+
initialTheme: config.theme
|
|
931
1608
|
}
|
|
932
1609
|
)
|
|
933
1610
|
);
|