@aayambansal/squint 0.1.2 → 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/README.md +10 -5
- package/dist/{cdp-R7Y2XSZQ.js → cdp-RTWPFIX5.js} +1 -1
- package/dist/{chrome-4WNYZZ42.js → chrome-K4BKFOYS.js} +2 -2
- package/dist/{chunk-MOYLED4S.js → chunk-4MMIJXYD.js} +2 -2
- package/dist/{chunk-UHVE42IQ.js → chunk-7XUORM3K.js} +1 -1
- package/dist/{chunk-43VZK47W.js → chunk-DLCRYRWQ.js} +4 -0
- package/dist/{chunk-MGHJSERZ.js → chunk-FPTBBHOH.js} +1 -1
- package/dist/{chunk-YIVPCWSG.js → chunk-MCYQXOE7.js} +9 -5
- package/dist/{chunk-ABF4QAMZ.js → chunk-VJGE7HSP.js} +8 -5
- package/dist/{chunk-MURKQ7SI.js → chunk-Z46MXU64.js} +3 -3
- package/dist/{chunk-FQDSJRTY.js → chunk-ZJOU46X4.js} +1 -1
- package/dist/cli.js +985 -457
- package/dist/{gates-G5DABJWQ.js → gates-I2GGQGMA.js} +3 -1
- package/dist/{preview-AA6YIE5W.js → preview-LTWJK3LF.js} +4 -4
- package/dist/{shots-ADBOBALL.js → shots-VJBLDMF3.js} +5 -5
- package/dist/{variants-VVIYT7WI.js → variants-4NCQXTP7.js} +3 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,18 +3,19 @@ import {
|
|
|
3
3
|
DevServer,
|
|
4
4
|
buildFixPrompt,
|
|
5
5
|
detectDevCommand
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-ZJOU46X4.js";
|
|
7
7
|
import {
|
|
8
8
|
runAgent
|
|
9
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
28
|
-
import "./chunk-
|
|
29
|
-
import "./chunk-
|
|
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-
|
|
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,
|
|
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(
|
|
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
|
|
122
|
-
import
|
|
123
|
-
import {
|
|
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/
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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(
|
|
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
|
-
|
|
404
|
-
|
|
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
|
-
}
|
|
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");
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
554
|
+
dev.start(devCommand);
|
|
555
|
+
this.push("status", `dev server starting \xB7 ${devCommand.display}`);
|
|
474
556
|
}
|
|
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;
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
push(
|
|
529
|
-
"status",
|
|
530
|
-
|
|
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("
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
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 (
|
|
587
|
-
|
|
1054
|
+
if (key.tab && key.shift) {
|
|
1055
|
+
session.cycleMode();
|
|
588
1056
|
return;
|
|
589
1057
|
}
|
|
590
1058
|
if (key.return) {
|
|
591
|
-
const value =
|
|
592
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1093
|
+
setLine(emptyLine);
|
|
618
1094
|
} else {
|
|
619
1095
|
historyIndexRef.current = next;
|
|
620
|
-
|
|
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
|
-
|
|
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.
|
|
629
|
-
|
|
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
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
/* @__PURE__ */
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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__ */
|
|
642
|
-
|
|
643
|
-
|
|
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
|
-
|
|
1169
|
+
path3.basename(cwd),
|
|
646
1170
|
devBadge,
|
|
647
|
-
|
|
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
|
|
654
|
-
var VERSION = "0.
|
|
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-
|
|
698
|
-
const { hasWebSocket } = await import("./cdp-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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__ */
|
|
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
|
);
|