@aayambansal/squint 0.1.3 → 0.2.0

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