@cam5/baby-bird 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/{chunk-RWSCST2I.js → chunk-7ALXKEST.js} +546 -42
- package/dist/chunk-7ALXKEST.js.map +1 -0
- package/dist/cli.js +270 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +240 -5
- package/dist/index.js +9 -1
- package/package.json +2 -1
- package/dist/chunk-RWSCST2I.js.map +0 -1
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
var LlmExcerptRefSchema = z.object({
|
|
4
4
|
hunk: z.string().min(1),
|
|
5
|
-
/** Optional [start, end]
|
|
6
|
-
lines: z.
|
|
5
|
+
/** Optional [start, end] new-file line numbers to narrow the hunk; fewer than two numbers means the whole hunk. */
|
|
6
|
+
lines: z.array(z.number().int().nonnegative()).optional(),
|
|
7
7
|
note: z.string().optional()
|
|
8
8
|
});
|
|
9
9
|
var LlmSectionSchema = z.object({
|
|
@@ -139,22 +139,27 @@ var CLAUDE_BASE = ["claude", "-p", "--no-session-persistence", "--setting-source
|
|
|
139
139
|
var BUILTIN_PRESETS = Object.freeze({
|
|
140
140
|
claude: {
|
|
141
141
|
command: [...CLAUDE_BASE],
|
|
142
|
+
kind: "claude",
|
|
142
143
|
description: "Claude Code CLI with its default model"
|
|
143
144
|
},
|
|
144
145
|
"claude-sonnet": {
|
|
145
146
|
command: [...CLAUDE_BASE, "--model", "sonnet", "--effort", "high"],
|
|
147
|
+
kind: "claude",
|
|
146
148
|
description: "Claude Code CLI, Sonnet at high effort"
|
|
147
149
|
},
|
|
148
150
|
"claude-opus": {
|
|
149
151
|
command: [...CLAUDE_BASE, "--model", "opus", "--effort", "high"],
|
|
152
|
+
kind: "claude",
|
|
150
153
|
description: "Claude Code CLI, Opus at high effort"
|
|
151
154
|
},
|
|
152
155
|
"claude-fable": {
|
|
153
156
|
command: [...CLAUDE_BASE, "--model", "fable", "--effort", "high"],
|
|
157
|
+
kind: "claude",
|
|
154
158
|
description: "Claude Code CLI, Fable at high effort"
|
|
155
159
|
},
|
|
156
160
|
"claude-haiku": {
|
|
157
161
|
command: [...CLAUDE_BASE, "--model", "haiku"],
|
|
162
|
+
kind: "claude",
|
|
158
163
|
description: "Claude Code CLI, Haiku (fast and cheap)"
|
|
159
164
|
},
|
|
160
165
|
llm: {
|
|
@@ -163,9 +168,11 @@ var BUILTIN_PRESETS = Object.freeze({
|
|
|
163
168
|
}
|
|
164
169
|
});
|
|
165
170
|
var PromptViaSchema = z2.enum(["stdin", "arg"]);
|
|
171
|
+
var LlmKindSchema = z2.enum(["plain", "claude"]);
|
|
166
172
|
var LlmPresetSchema = z2.object({
|
|
167
173
|
command: z2.array(z2.string()).min(1),
|
|
168
174
|
promptVia: PromptViaSchema.optional(),
|
|
175
|
+
kind: LlmKindSchema.optional(),
|
|
169
176
|
description: z2.string().optional()
|
|
170
177
|
});
|
|
171
178
|
var ConfigSchema = z2.object({
|
|
@@ -175,6 +182,9 @@ var ConfigSchema = z2.object({
|
|
|
175
182
|
args: z2.array(z2.string()),
|
|
176
183
|
command: z2.array(z2.string()).min(1).nullable(),
|
|
177
184
|
promptVia: PromptViaSchema.nullable(),
|
|
185
|
+
kind: LlmKindSchema.nullable(),
|
|
186
|
+
/** Claude kind only: pass --json-schema so the CLI validates the answer itself. Off by default (see README). */
|
|
187
|
+
jsonSchema: z2.boolean(),
|
|
178
188
|
timeoutMs: z2.number().int().positive(),
|
|
179
189
|
maxPromptBytes: z2.number().int().positive(),
|
|
180
190
|
env: z2.record(z2.string(), z2.string())
|
|
@@ -190,7 +200,11 @@ var ConfigSchema = z2.object({
|
|
|
190
200
|
color: z2.enum(["auto", "always", "never"]),
|
|
191
201
|
pager: z2.enum(["auto", "always", "never"]),
|
|
192
202
|
maxExcerptLines: z2.number().int().positive(),
|
|
193
|
-
width: z2.number().int().positive().nullable()
|
|
203
|
+
width: z2.number().int().positive().nullable(),
|
|
204
|
+
/** Language-aware token colors in excerpts (needs a 256-color terminal for the add/del tints). */
|
|
205
|
+
highlight: z2.enum(["auto", "always", "never"]),
|
|
206
|
+
/** Background tint palette for added/removed lines. */
|
|
207
|
+
theme: z2.enum(["dark", "light"])
|
|
194
208
|
}),
|
|
195
209
|
cache: z2.object({
|
|
196
210
|
enabled: z2.boolean(),
|
|
@@ -211,6 +225,8 @@ var DEFAULT_CONFIG = {
|
|
|
211
225
|
args: [],
|
|
212
226
|
command: null,
|
|
213
227
|
promptVia: null,
|
|
228
|
+
kind: null,
|
|
229
|
+
jsonSchema: false,
|
|
214
230
|
timeoutMs: 18e4,
|
|
215
231
|
maxPromptBytes: 2e5,
|
|
216
232
|
env: {}
|
|
@@ -229,7 +245,7 @@ var DEFAULT_CONFIG = {
|
|
|
229
245
|
"**/*.map"
|
|
230
246
|
]
|
|
231
247
|
},
|
|
232
|
-
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null },
|
|
248
|
+
render: { color: "auto", pager: "auto", maxExcerptLines: 60, width: null, highlight: "auto", theme: "dark" },
|
|
233
249
|
cache: { enabled: true, dir: null }
|
|
234
250
|
};
|
|
235
251
|
function userConfigPath(env = process.env) {
|
|
@@ -343,9 +359,9 @@ function allPresets(config) {
|
|
|
343
359
|
}
|
|
344
360
|
function resolveLlm(config) {
|
|
345
361
|
const { llm } = config;
|
|
346
|
-
const
|
|
362
|
+
const common2 = { timeoutMs: llm.timeoutMs, maxPromptBytes: llm.maxPromptBytes, env: llm.env, jsonSchema: llm.jsonSchema };
|
|
347
363
|
if (llm.command) {
|
|
348
|
-
return { command: [...llm.command, ...llm.args], promptVia: llm.promptVia ?? "stdin", preset: null, ...
|
|
364
|
+
return { command: [...llm.command, ...llm.args], promptVia: llm.promptVia ?? "stdin", kind: llm.kind ?? "plain", preset: null, ...common2 };
|
|
349
365
|
}
|
|
350
366
|
const presets = allPresets(config);
|
|
351
367
|
const preset = presets[llm.preset];
|
|
@@ -356,8 +372,9 @@ function resolveLlm(config) {
|
|
|
356
372
|
return {
|
|
357
373
|
command: [...preset.command, ...llm.args],
|
|
358
374
|
promptVia: llm.promptVia ?? preset.promptVia ?? "stdin",
|
|
375
|
+
kind: llm.kind ?? preset.kind ?? "plain",
|
|
359
376
|
preset: llm.preset,
|
|
360
|
-
...
|
|
377
|
+
...common2
|
|
361
378
|
};
|
|
362
379
|
}
|
|
363
380
|
function shellSplit(input) {
|
|
@@ -767,7 +784,8 @@ function materializeTour(input) {
|
|
|
767
784
|
continue;
|
|
768
785
|
}
|
|
769
786
|
files.add(hit.file.path);
|
|
770
|
-
const
|
|
787
|
+
const range = ref.lines && ref.lines.length >= 2 ? [ref.lines[0], ref.lines[1]] : void 0;
|
|
788
|
+
const lines = sliceHunk(hit.hunk, range, input.maxExcerptLines);
|
|
771
789
|
const excerpt = {
|
|
772
790
|
file: hit.file.path,
|
|
773
791
|
hunkId: hit.hunk.id,
|
|
@@ -851,8 +869,8 @@ function sliceHunk(hunk, range, maxLines) {
|
|
|
851
869
|
}
|
|
852
870
|
|
|
853
871
|
// src/core/prompt/template.ts
|
|
854
|
-
var PROMPT_VERSION =
|
|
855
|
-
var
|
|
872
|
+
var PROMPT_VERSION = 3;
|
|
873
|
+
var PROMPT_HEADER_TEXT = `You are writing a guided code tour of a change for a reviewer who has not seen it before.
|
|
856
874
|
|
|
857
875
|
A code tour is an ordered list of sections. Each section explains one coherent part of the change: what it does, why it is there, and how it connects to the rest. Sections are ordered the way a reader should encounter them: start with the change that makes everything else make sense (a new type, an interface, a data model, a configuration knob), then the code that builds on it, then wiring and plumbing, then tests and housekeeping.
|
|
858
876
|
|
|
@@ -862,7 +880,7 @@ A code tour is an ordered list of sections. Each section explains one coherent p
|
|
|
862
880
|
- Group by concept, not by file. A section may span many files, and a file may appear in several sections.
|
|
863
881
|
- Every file in the change must be claimed by at least one section. Put unrelated housekeeping (formatting, generated code, renames, dependency bumps) in one short final section rather than sprinkling it around.
|
|
864
882
|
- A section's description is 2 to 5 sentences of plain prose written for a colleague. Lead with the purpose (why), then what changed, then anything a reviewer should look at carefully: behavior changes, edge cases, risk. Do not narrate line by line and do not restate the diff.
|
|
865
|
-
- Choose 1 to 3 excerpts per section: the hunks that best show the idea. Reference hunks by their id exactly as given (for example "F2.H1").
|
|
883
|
+
- Choose 1 to 3 excerpts per section: the hunks that best show the idea. Reference hunks by their id exactly as given (for example "F2.H1"). Narrow a hunk to the interesting part with "lines": [start, end] using NEW-file line numbers as they appear in the diff; prefer 10 to 30 lines over a whole hunk. Use an empty "lines" array for the whole hunk. Never quote code in the JSON; the real diff is rendered from your references.
|
|
866
884
|
- Give each excerpt a short "note" (under 15 words) saying what to look at.
|
|
867
885
|
- The tour "title" is a short imperative phrase naming the change, like a good commit subject. The "summary" is 2 to 4 sentences describing the whole change and its motivation.
|
|
868
886
|
- Use the pull request description and commit messages as evidence of intent, but trust the diff over them when they disagree.
|
|
@@ -871,7 +889,7 @@ A code tour is an ordered list of sections. Each section explains one coherent p
|
|
|
871
889
|
|
|
872
890
|
## Output
|
|
873
891
|
|
|
874
|
-
|
|
892
|
+
%%OUTPUT_INSTRUCTION%%
|
|
875
893
|
|
|
876
894
|
{
|
|
877
895
|
"title": "Short imperative title",
|
|
@@ -889,6 +907,12 @@ Respond with ONLY a JSON object: no prose before or after it, and no code fences
|
|
|
889
907
|
]
|
|
890
908
|
}
|
|
891
909
|
`;
|
|
910
|
+
var OUTPUT_PLAIN = "Respond with ONLY a JSON object: no prose before or after it, and no code fences.";
|
|
911
|
+
var OUTPUT_STRUCTURED = "Submit the tour through the structured output tool. Its input is the tour object itself, with title, summary and sections as top-level fields exactly as shown below (do not nest them under another key). Do not write prose before it.";
|
|
912
|
+
function promptHeader(structured) {
|
|
913
|
+
return PROMPT_HEADER_TEXT.replace("%%OUTPUT_INSTRUCTION%%", structured ? OUTPUT_STRUCTURED : OUTPUT_PLAIN);
|
|
914
|
+
}
|
|
915
|
+
var PROMPT_HEADER = promptHeader(false);
|
|
892
916
|
var REPAIR_SUFFIX = (reason) => `
|
|
893
917
|
|
|
894
918
|
---
|
|
@@ -901,6 +925,7 @@ Respond again with ONLY the JSON object described above. No prose, no code fence
|
|
|
901
925
|
var KEEP_LINES = 40;
|
|
902
926
|
var MAX_PR_BODY_BYTES = 12e3;
|
|
903
927
|
function buildPrompt(input) {
|
|
928
|
+
const header = promptHeader(Boolean(input.structured));
|
|
904
929
|
const context = renderContext(input);
|
|
905
930
|
const truncation = { truncated: [], omitted: [] };
|
|
906
931
|
const blocks = input.diff.files.map((file) => {
|
|
@@ -917,7 +942,7 @@ function buildPrompt(input) {
|
|
|
917
942
|
});
|
|
918
943
|
const text = (b) => b.mode === "full" ? b.full : b.mode === "truncated" ? b.truncated : b.omitted;
|
|
919
944
|
const total = () => bytes(diffPreamble(truncation)) + blocks.reduce((n, b) => n + bytes(text(b)), 0);
|
|
920
|
-
const budget = input.maxBytes - bytes(
|
|
945
|
+
const budget = input.maxBytes - bytes(header) - bytes(context) - 8;
|
|
921
946
|
if (total() > budget) {
|
|
922
947
|
const bySize = [...blocks].sort((a, b) => bytes(b.full) - bytes(a.full));
|
|
923
948
|
for (const block of bySize) {
|
|
@@ -934,7 +959,7 @@ function buildPrompt(input) {
|
|
|
934
959
|
truncation.omitted.push(block.file.path);
|
|
935
960
|
}
|
|
936
961
|
}
|
|
937
|
-
const prompt = [
|
|
962
|
+
const prompt = [header, context, diffPreamble(truncation), ...blocks.map(text)].join("\n");
|
|
938
963
|
return { prompt, truncation };
|
|
939
964
|
}
|
|
940
965
|
function bytes(s) {
|
|
@@ -1326,32 +1351,237 @@ async function nearestAncestorBranch(branch, headSha, defaultBranch, cwd, debug)
|
|
|
1326
1351
|
return best?.name ?? null;
|
|
1327
1352
|
}
|
|
1328
1353
|
|
|
1354
|
+
// src/core/prompt/json-schema.ts
|
|
1355
|
+
var TOUR_JSON_SCHEMA = {
|
|
1356
|
+
type: "object",
|
|
1357
|
+
description: "The tour itself. Its fields (title, summary, sections) are the top-level properties of this object; do not wrap them in another key.",
|
|
1358
|
+
additionalProperties: false,
|
|
1359
|
+
required: ["title", "summary", "sections"],
|
|
1360
|
+
properties: {
|
|
1361
|
+
title: { type: "string", description: "Short imperative title naming the change, like a good commit subject." },
|
|
1362
|
+
summary: { type: "string", description: "2 to 4 sentences describing the whole change and its motivation." },
|
|
1363
|
+
sections: {
|
|
1364
|
+
type: "array",
|
|
1365
|
+
description: "Ordered sections of the tour, 2 to 8 of them.",
|
|
1366
|
+
items: {
|
|
1367
|
+
type: "object",
|
|
1368
|
+
additionalProperties: false,
|
|
1369
|
+
required: ["title", "description", "files", "excerpts"],
|
|
1370
|
+
properties: {
|
|
1371
|
+
title: { type: "string" },
|
|
1372
|
+
description: { type: "string", description: "Why, then what, then what to watch. 2 to 5 sentences." },
|
|
1373
|
+
files: { type: "array", items: { type: "string" }, description: "Paths this section is about, exactly as listed." },
|
|
1374
|
+
excerpts: {
|
|
1375
|
+
type: "array",
|
|
1376
|
+
description: "1 to 3 hunks that best show the idea.",
|
|
1377
|
+
items: {
|
|
1378
|
+
type: "object",
|
|
1379
|
+
additionalProperties: false,
|
|
1380
|
+
required: ["hunk", "lines", "note"],
|
|
1381
|
+
properties: {
|
|
1382
|
+
hunk: { type: "string", description: 'A hunk id exactly as given in the diff, e.g. "F2.H1".' },
|
|
1383
|
+
lines: {
|
|
1384
|
+
type: "array",
|
|
1385
|
+
items: { type: "integer" },
|
|
1386
|
+
description: "Empty for the whole hunk, or [start, end] new-file line numbers to narrow it."
|
|
1387
|
+
},
|
|
1388
|
+
note: { type: "string", description: "Under 15 words: what to look at. May be empty." }
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
// src/llm/claude-stream.ts
|
|
1399
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1400
|
+
var ClaudeStreamParser = class {
|
|
1401
|
+
constructor(onEvent = () => {
|
|
1402
|
+
}) {
|
|
1403
|
+
this.onEvent = onEvent;
|
|
1404
|
+
}
|
|
1405
|
+
onEvent;
|
|
1406
|
+
buffer = "";
|
|
1407
|
+
text = "";
|
|
1408
|
+
partialJson = "";
|
|
1409
|
+
messageStructured = null;
|
|
1410
|
+
resultStructured = null;
|
|
1411
|
+
resultText = null;
|
|
1412
|
+
isError = false;
|
|
1413
|
+
errorMessage;
|
|
1414
|
+
usage = {};
|
|
1415
|
+
unparsed = 0;
|
|
1416
|
+
push(chunk) {
|
|
1417
|
+
this.buffer += chunk;
|
|
1418
|
+
let idx;
|
|
1419
|
+
while ((idx = this.buffer.indexOf("\n")) !== -1) {
|
|
1420
|
+
const line = this.buffer.slice(0, idx);
|
|
1421
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
1422
|
+
this.handleLine(line);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
finish() {
|
|
1426
|
+
if (this.buffer.trim()) this.handleLine(this.buffer);
|
|
1427
|
+
this.buffer = "";
|
|
1428
|
+
const result = this.resultStructured ?? this.messageStructured ?? (this.partialJson.trim() || null) ?? (this.resultText?.trim() || null) ?? (this.text.trim() || null);
|
|
1429
|
+
const outcome = { result, isError: this.isError, usage: this.usage, unparsedLines: this.unparsed };
|
|
1430
|
+
if (this.errorMessage) outcome.errorMessage = this.errorMessage;
|
|
1431
|
+
return outcome;
|
|
1432
|
+
}
|
|
1433
|
+
handleLine(line) {
|
|
1434
|
+
const trimmed = line.trim();
|
|
1435
|
+
if (!trimmed) return;
|
|
1436
|
+
let ev;
|
|
1437
|
+
try {
|
|
1438
|
+
ev = JSON.parse(trimmed);
|
|
1439
|
+
} catch {
|
|
1440
|
+
this.unparsed++;
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
if (!isObject(ev)) return;
|
|
1444
|
+
switch (ev.type) {
|
|
1445
|
+
case "stream_event":
|
|
1446
|
+
if (isObject(ev.event)) this.handleStreamEvent(ev.event);
|
|
1447
|
+
break;
|
|
1448
|
+
case "assistant":
|
|
1449
|
+
if (isObject(ev.message)) this.handleAssistant(ev.message);
|
|
1450
|
+
break;
|
|
1451
|
+
case "user":
|
|
1452
|
+
if (isObject(ev.message)) this.handleUser(ev.message);
|
|
1453
|
+
break;
|
|
1454
|
+
case "result":
|
|
1455
|
+
this.handleResult(ev);
|
|
1456
|
+
break;
|
|
1457
|
+
default:
|
|
1458
|
+
break;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
handleStreamEvent(event) {
|
|
1462
|
+
switch (event.type) {
|
|
1463
|
+
case "message_start": {
|
|
1464
|
+
const model = isObject(event.message) && typeof event.message.model === "string" ? event.message.model : void 0;
|
|
1465
|
+
if (model) {
|
|
1466
|
+
this.usage.model = model;
|
|
1467
|
+
this.onEvent({ type: "usage", model });
|
|
1468
|
+
}
|
|
1469
|
+
break;
|
|
1470
|
+
}
|
|
1471
|
+
case "content_block_start": {
|
|
1472
|
+
const block = isObject(event.content_block) ? event.content_block : {};
|
|
1473
|
+
if (block.type === "text" || block.type === "tool_use") {
|
|
1474
|
+
this.text = "";
|
|
1475
|
+
this.partialJson = "";
|
|
1476
|
+
this.onEvent({ type: "answer-start" });
|
|
1477
|
+
}
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
case "content_block_delta": {
|
|
1481
|
+
const delta = isObject(event.delta) ? event.delta : {};
|
|
1482
|
+
if (delta.type === "thinking_delta") {
|
|
1483
|
+
const text = typeof delta.thinking === "string" ? delta.thinking : "";
|
|
1484
|
+
this.onEvent(text ? { type: "thinking", text } : { type: "thinking-pulse" });
|
|
1485
|
+
} else if (delta.type === "text_delta" && typeof delta.text === "string" && delta.text) {
|
|
1486
|
+
this.text += delta.text;
|
|
1487
|
+
this.onEvent({ type: "text", text: delta.text });
|
|
1488
|
+
} else if (delta.type === "input_json_delta" && typeof delta.partial_json === "string" && delta.partial_json) {
|
|
1489
|
+
this.partialJson += delta.partial_json;
|
|
1490
|
+
this.onEvent({ type: "text", text: delta.partial_json });
|
|
1491
|
+
}
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
case "message_delta": {
|
|
1495
|
+
if (isObject(event.usage)) this.recordUsage(event.usage);
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
default:
|
|
1499
|
+
break;
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
handleAssistant(message) {
|
|
1503
|
+
if (!Array.isArray(message.content)) return;
|
|
1504
|
+
for (const block of message.content) {
|
|
1505
|
+
if (isObject(block) && block.type === "tool_use" && isObject(block.input)) {
|
|
1506
|
+
this.messageStructured = JSON.stringify(block.input);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
/** Tool results the CLI feeds back, e.g. a structured-output validation failure. */
|
|
1511
|
+
handleUser(message) {
|
|
1512
|
+
if (!Array.isArray(message.content)) return;
|
|
1513
|
+
for (const block of message.content) {
|
|
1514
|
+
if (!isObject(block) || block.type !== "tool_result" || block.is_error !== true) continue;
|
|
1515
|
+
const content = typeof block.content === "string" ? block.content : JSON.stringify(block.content ?? "");
|
|
1516
|
+
this.onEvent({ type: "notice", text: `Answer rejected (${content.replace(/^Output does not match required schema:\s*/i, "schema: ").slice(0, 300)}); the model is retrying` });
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
handleResult(ev) {
|
|
1520
|
+
if (isObject(ev.structured_output)) this.resultStructured = JSON.stringify(ev.structured_output);
|
|
1521
|
+
if (typeof ev.result === "string") this.resultText = ev.result;
|
|
1522
|
+
const subtype = typeof ev.subtype === "string" ? ev.subtype : "";
|
|
1523
|
+
if (ev.is_error === true || subtype.startsWith("error")) {
|
|
1524
|
+
this.isError = true;
|
|
1525
|
+
const errors = Array.isArray(ev.errors) ? ev.errors.filter((e) => typeof e === "string").join("; ") : "";
|
|
1526
|
+
this.errorMessage = errors || typeof ev.result === "string" && ev.result || subtype || "unknown error";
|
|
1527
|
+
}
|
|
1528
|
+
if (isObject(ev.usage)) this.recordUsage(ev.usage);
|
|
1529
|
+
if (typeof ev.total_cost_usd === "number") this.usage.costUsd = ev.total_cost_usd;
|
|
1530
|
+
this.onEvent({ type: "usage", ...this.usage });
|
|
1531
|
+
}
|
|
1532
|
+
recordUsage(usage) {
|
|
1533
|
+
if (typeof usage.output_tokens === "number") this.usage.outputTokens = usage.output_tokens;
|
|
1534
|
+
const details = usage.output_tokens_details;
|
|
1535
|
+
if (isObject(details) && typeof details.thinking_tokens === "number") this.usage.thinkingTokens = details.thinking_tokens;
|
|
1536
|
+
this.onEvent({ type: "usage", ...this.usage });
|
|
1537
|
+
}
|
|
1538
|
+
};
|
|
1539
|
+
|
|
1329
1540
|
// src/llm/command.ts
|
|
1330
1541
|
import { spawn } from "child_process";
|
|
1542
|
+
import { StringDecoder } from "string_decoder";
|
|
1331
1543
|
var STDERR_TAIL_LINES = 20;
|
|
1544
|
+
var CLAUDE_STREAM_ARGS = ["--output-format", "stream-json", "--verbose", "--include-partial-messages"];
|
|
1332
1545
|
var CommandProvider = class {
|
|
1333
1546
|
constructor(opts) {
|
|
1334
1547
|
this.opts = opts;
|
|
1335
1548
|
if (opts.command.length === 0) throw new LlmFailedError("LLM command is empty.");
|
|
1336
1549
|
}
|
|
1337
1550
|
opts;
|
|
1551
|
+
get kind() {
|
|
1552
|
+
return this.opts.kind ?? "plain";
|
|
1553
|
+
}
|
|
1338
1554
|
describe() {
|
|
1339
1555
|
return this.opts.command.map(shellQuote).join(" ");
|
|
1340
1556
|
}
|
|
1341
|
-
|
|
1557
|
+
/** The full argv that will run, including any flags added for the command kind. */
|
|
1558
|
+
argv(prompt) {
|
|
1559
|
+
let argv = [...this.opts.command];
|
|
1560
|
+
if (this.kind === "claude") {
|
|
1561
|
+
argv.push(...CLAUDE_STREAM_ARGS);
|
|
1562
|
+
if (this.opts.jsonSchema) argv.push("--json-schema", JSON.stringify(this.opts.jsonSchema));
|
|
1563
|
+
}
|
|
1564
|
+
if (prompt !== void 0 && this.opts.promptVia === "arg") argv = argv.map((a) => a.replaceAll("{prompt}", prompt));
|
|
1565
|
+
return argv;
|
|
1566
|
+
}
|
|
1567
|
+
complete(prompt, opts = {}) {
|
|
1342
1568
|
const { promptVia, timeoutMs } = this.opts;
|
|
1343
|
-
const
|
|
1344
|
-
|
|
1569
|
+
const onEvent = opts.onEvent ?? (() => {
|
|
1570
|
+
});
|
|
1571
|
+
const [bin, ...args] = this.argv(prompt);
|
|
1345
1572
|
const debug = this.opts.debug ?? (() => {
|
|
1346
1573
|
});
|
|
1347
1574
|
const started = Date.now();
|
|
1348
|
-
debug(`running ${this.describe()} (prompt ${Buffer.byteLength(prompt)} bytes via ${promptVia})`);
|
|
1575
|
+
debug(`running ${this.describe()}${this.kind === "claude" ? " (+stream-json)" : ""} (prompt ${Buffer.byteLength(prompt)} bytes via ${promptVia})`);
|
|
1349
1576
|
return new Promise((resolve, reject) => {
|
|
1350
1577
|
const env = { ...process.env, ...this.opts.env };
|
|
1351
1578
|
delete env.CLAUDECODE;
|
|
1352
1579
|
const child = spawn(bin, args, { cwd: this.opts.cwd, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
1580
|
+
const parser = this.kind === "claude" ? new ClaudeStreamParser(onEvent) : null;
|
|
1581
|
+
const decoder = new StringDecoder("utf8");
|
|
1353
1582
|
const out = [];
|
|
1354
1583
|
const err = [];
|
|
1584
|
+
let outBytes = 0;
|
|
1355
1585
|
let settled = false;
|
|
1356
1586
|
const finish = (fn) => {
|
|
1357
1587
|
if (settled) return;
|
|
@@ -1365,7 +1595,16 @@ var CommandProvider = class {
|
|
|
1365
1595
|
hint: "Raise llm.timeoutMs in your config, or pick a faster preset."
|
|
1366
1596
|
})));
|
|
1367
1597
|
}, timeoutMs);
|
|
1368
|
-
|
|
1598
|
+
onEvent({ type: "started" });
|
|
1599
|
+
child.stdout.on("data", (b) => {
|
|
1600
|
+
outBytes += b.length;
|
|
1601
|
+
if (parser) {
|
|
1602
|
+
parser.push(decoder.write(b));
|
|
1603
|
+
} else {
|
|
1604
|
+
out.push(b);
|
|
1605
|
+
onEvent({ type: "output", bytes: outBytes });
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1369
1608
|
child.stderr.on("data", (b) => err.push(b));
|
|
1370
1609
|
child.on("error", (e) => {
|
|
1371
1610
|
finish(() => {
|
|
@@ -1378,15 +1617,33 @@ var CommandProvider = class {
|
|
|
1378
1617
|
});
|
|
1379
1618
|
child.on("close", (code, signal) => {
|
|
1380
1619
|
finish(() => {
|
|
1381
|
-
const stdout = Buffer.concat(out).toString("utf8");
|
|
1382
1620
|
const stderr = Buffer.concat(err).toString("utf8");
|
|
1383
|
-
|
|
1621
|
+
const tail = stderr.trim().split("\n").slice(-STDERR_TAIL_LINES).join("\n");
|
|
1622
|
+
debug(`command exited with ${signal ? `signal ${signal}` : `code ${code}`} after ${Date.now() - started}ms; ${outBytes} bytes of stdout`);
|
|
1623
|
+
if (parser) {
|
|
1624
|
+
parser.push(decoder.end());
|
|
1625
|
+
const outcome = parser.finish();
|
|
1626
|
+
if (outcome.unparsedLines) debug(`${outcome.unparsedLines} non-JSON line(s) on stdout were ignored`);
|
|
1627
|
+
if (outcome.isError) {
|
|
1628
|
+
reject(new LlmFailedError(`Claude reported an error: ${outcome.errorMessage ?? "unknown"}`, { hint: tail || void 0 }));
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
if (code !== 0) {
|
|
1632
|
+
reject(new LlmFailedError(`LLM command failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${this.describe()}${tail ? "\n" + tail : ""}`));
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
if (!outcome.result) {
|
|
1636
|
+
reject(new LlmFailedError("Claude produced no result.", { hint: "Run with --debug to see what came back." }));
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
resolve(outcome.result);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1384
1642
|
if (code !== 0) {
|
|
1385
|
-
const tail = stderr.trim().split("\n").slice(-STDERR_TAIL_LINES).join("\n");
|
|
1386
1643
|
reject(new LlmFailedError(`LLM command failed (${signal ? `signal ${signal}` : `exit ${code}`}): ${this.describe()}${tail ? "\n" + tail : ""}`));
|
|
1387
1644
|
return;
|
|
1388
1645
|
}
|
|
1389
|
-
resolve(
|
|
1646
|
+
resolve(Buffer.concat(out).toString("utf8"));
|
|
1390
1647
|
});
|
|
1391
1648
|
});
|
|
1392
1649
|
if (promptVia === "stdin") {
|
|
@@ -1410,21 +1667,30 @@ function createProvider(llm, opts = {}) {
|
|
|
1410
1667
|
command: llm.command,
|
|
1411
1668
|
promptVia: llm.promptVia,
|
|
1412
1669
|
timeoutMs: llm.timeoutMs,
|
|
1670
|
+
kind: llm.kind,
|
|
1671
|
+
jsonSchema: llm.kind === "claude" && llm.jsonSchema ? TOUR_JSON_SCHEMA : void 0,
|
|
1413
1672
|
env: llm.env,
|
|
1414
1673
|
cwd: opts.cwd,
|
|
1415
1674
|
debug: opts.debug
|
|
1416
1675
|
});
|
|
1417
1676
|
}
|
|
1418
1677
|
|
|
1678
|
+
// src/core/progress.ts
|
|
1679
|
+
var noProgress = { phase() {
|
|
1680
|
+
}, llm() {
|
|
1681
|
+
} };
|
|
1682
|
+
|
|
1419
1683
|
// src/core/tour.ts
|
|
1420
1684
|
async function prepareTour(opts) {
|
|
1421
1685
|
const debug = opts.debug ?? (() => {
|
|
1422
1686
|
});
|
|
1423
1687
|
const warn = opts.warn ?? (() => {
|
|
1424
1688
|
});
|
|
1689
|
+
const progress = opts.progress ?? noProgress;
|
|
1425
1690
|
const { config } = opts;
|
|
1426
1691
|
const llm = resolveLlm(config);
|
|
1427
1692
|
const codehost = opts.codehost ?? createCodeHost(config, { debug });
|
|
1693
|
+
progress.phase("Working out what to tour");
|
|
1428
1694
|
const resolved = await resolveRange(opts.range ?? {}, {
|
|
1429
1695
|
cwd: opts.cwd,
|
|
1430
1696
|
exclude: config.git.exclude,
|
|
@@ -1433,6 +1699,7 @@ async function prepareTour(opts) {
|
|
|
1433
1699
|
debug
|
|
1434
1700
|
});
|
|
1435
1701
|
debug(`source: ${JSON.stringify(resolved.source)}`);
|
|
1702
|
+
progress.phase("Collecting the diff");
|
|
1436
1703
|
const diff = await collectDiff(resolved.source, { cwd: opts.cwd, exclude: config.git.exclude, warn });
|
|
1437
1704
|
if (diff.files.length === 0) {
|
|
1438
1705
|
throw new NoChangesError("The selected range has no changes (after excludes).", "Check git.exclude in your config, or pass a different range.");
|
|
@@ -1440,7 +1707,8 @@ async function prepareTour(opts) {
|
|
|
1440
1707
|
const commits = await collectCommits(resolved.source, opts.cwd);
|
|
1441
1708
|
const context = { source: resolved.source, branch: resolved.branch, diff, commits };
|
|
1442
1709
|
if (resolved.pullRequest) context.pullRequest = resolved.pullRequest;
|
|
1443
|
-
|
|
1710
|
+
progress.phase("Building the prompt");
|
|
1711
|
+
const built = buildPrompt({ ...context, maxBytes: llm.maxPromptBytes, structured: llm.kind === "claude" && llm.jsonSchema });
|
|
1444
1712
|
if (built.truncation.truncated.length || built.truncation.omitted.length) {
|
|
1445
1713
|
warn(`Prompt exceeded ${llm.maxPromptBytes} bytes; truncated ${built.truncation.truncated.length} and omitted ${built.truncation.omitted.length} file diff(s).`);
|
|
1446
1714
|
}
|
|
@@ -1454,11 +1722,13 @@ async function generateTour(opts, prepared) {
|
|
|
1454
1722
|
});
|
|
1455
1723
|
const warn = opts.warn ?? (() => {
|
|
1456
1724
|
});
|
|
1725
|
+
const progress = opts.progress ?? noProgress;
|
|
1457
1726
|
const { config } = opts;
|
|
1458
1727
|
const prep = prepared ?? await prepareTour(opts);
|
|
1459
1728
|
const useCache = config.cache.enabled && !opts.noCache;
|
|
1460
1729
|
const cache = useCache ? new TourCache(opts.cacheDir) : null;
|
|
1461
1730
|
if (cache && !opts.refresh) {
|
|
1731
|
+
progress.phase("Checking the cache");
|
|
1462
1732
|
const hit = await cache.get(prep.cacheKey);
|
|
1463
1733
|
if (hit) {
|
|
1464
1734
|
debug(`cache hit: ${cache.pathFor(prep.cacheKey)}`);
|
|
@@ -1468,7 +1738,9 @@ async function generateTour(opts, prepared) {
|
|
|
1468
1738
|
}
|
|
1469
1739
|
const llm = resolveLlm(config);
|
|
1470
1740
|
const provider = opts.provider ?? createProvider(llm, { cwd: opts.cwd, debug });
|
|
1471
|
-
|
|
1741
|
+
progress.phase(`Asking ${llm.preset ?? llm.command[0]}`);
|
|
1742
|
+
const output = await completeWithRepair(provider, prep.built.prompt, debug, progress);
|
|
1743
|
+
progress.phase("Assembling the tour");
|
|
1472
1744
|
const tour = materializeTour({
|
|
1473
1745
|
output,
|
|
1474
1746
|
diff: prep.context.diff,
|
|
@@ -1485,14 +1757,16 @@ async function generateTour(opts, prepared) {
|
|
|
1485
1757
|
}
|
|
1486
1758
|
return { tour, fromCache: false, cacheKey: cache ? prep.cacheKey : null, cachePath, prompt: prep.built.prompt };
|
|
1487
1759
|
}
|
|
1488
|
-
async function completeWithRepair(provider, prompt, debug) {
|
|
1489
|
-
|
|
1760
|
+
async function completeWithRepair(provider, prompt, debug, progress) {
|
|
1761
|
+
const onEvent = (event) => progress.llm(event);
|
|
1762
|
+
let raw = await provider.complete(prompt, { onEvent });
|
|
1490
1763
|
debug(`raw model output (attempt 1):
|
|
1491
1764
|
${raw}`);
|
|
1492
1765
|
const first = parseOutput(raw);
|
|
1493
1766
|
if (first.ok) return first.value;
|
|
1494
1767
|
debug(`attempt 1 unusable: ${first.reason}; retrying with repair prompt`);
|
|
1495
|
-
|
|
1768
|
+
progress.phase("Asking again (the first answer was not valid JSON)");
|
|
1769
|
+
raw = await provider.complete(prompt + REPAIR_SUFFIX(first.reason), { onEvent });
|
|
1496
1770
|
debug(`raw model output (attempt 2):
|
|
1497
1771
|
${raw}`);
|
|
1498
1772
|
const second = parseOutput(raw);
|
|
@@ -1513,12 +1787,222 @@ function parseOutput(raw) {
|
|
|
1513
1787
|
|
|
1514
1788
|
// src/render/cli.ts
|
|
1515
1789
|
import pc from "picocolors";
|
|
1790
|
+
|
|
1791
|
+
// src/render/highlight.ts
|
|
1792
|
+
import { common, createLowlight } from "lowlight";
|
|
1793
|
+
var lowlight = createLowlight(common);
|
|
1794
|
+
var BY_EXT = {
|
|
1795
|
+
ts: "typescript",
|
|
1796
|
+
mts: "typescript",
|
|
1797
|
+
cts: "typescript",
|
|
1798
|
+
tsx: "typescript",
|
|
1799
|
+
js: "javascript",
|
|
1800
|
+
mjs: "javascript",
|
|
1801
|
+
cjs: "javascript",
|
|
1802
|
+
jsx: "javascript",
|
|
1803
|
+
json: "json",
|
|
1804
|
+
jsonc: "json",
|
|
1805
|
+
json5: "json",
|
|
1806
|
+
py: "python",
|
|
1807
|
+
pyi: "python",
|
|
1808
|
+
rb: "ruby",
|
|
1809
|
+
rake: "ruby",
|
|
1810
|
+
gemspec: "ruby",
|
|
1811
|
+
go: "go",
|
|
1812
|
+
rs: "rust",
|
|
1813
|
+
java: "java",
|
|
1814
|
+
kt: "kotlin",
|
|
1815
|
+
kts: "kotlin",
|
|
1816
|
+
swift: "swift",
|
|
1817
|
+
scala: "scala",
|
|
1818
|
+
c: "c",
|
|
1819
|
+
h: "c",
|
|
1820
|
+
cc: "cpp",
|
|
1821
|
+
cpp: "cpp",
|
|
1822
|
+
cxx: "cpp",
|
|
1823
|
+
hpp: "cpp",
|
|
1824
|
+
hh: "cpp",
|
|
1825
|
+
cs: "csharp",
|
|
1826
|
+
m: "objectivec",
|
|
1827
|
+
mm: "objectivec",
|
|
1828
|
+
php: "php",
|
|
1829
|
+
pl: "perl",
|
|
1830
|
+
pm: "perl",
|
|
1831
|
+
lua: "lua",
|
|
1832
|
+
r: "r",
|
|
1833
|
+
dart: "dart",
|
|
1834
|
+
sh: "bash",
|
|
1835
|
+
bash: "bash",
|
|
1836
|
+
zsh: "bash",
|
|
1837
|
+
fish: "bash",
|
|
1838
|
+
yml: "yaml",
|
|
1839
|
+
yaml: "yaml",
|
|
1840
|
+
toml: "ini",
|
|
1841
|
+
ini: "ini",
|
|
1842
|
+
cfg: "ini",
|
|
1843
|
+
conf: "ini",
|
|
1844
|
+
md: "markdown",
|
|
1845
|
+
markdown: "markdown",
|
|
1846
|
+
html: "xml",
|
|
1847
|
+
htm: "xml",
|
|
1848
|
+
xml: "xml",
|
|
1849
|
+
svg: "xml",
|
|
1850
|
+
vue: "xml",
|
|
1851
|
+
css: "css",
|
|
1852
|
+
scss: "scss",
|
|
1853
|
+
less: "less",
|
|
1854
|
+
sql: "sql",
|
|
1855
|
+
graphql: "graphql",
|
|
1856
|
+
gql: "graphql",
|
|
1857
|
+
diff: "diff",
|
|
1858
|
+
patch: "diff",
|
|
1859
|
+
makefile: "makefile",
|
|
1860
|
+
mk: "makefile",
|
|
1861
|
+
vb: "vbnet",
|
|
1862
|
+
vbnet: "vbnet",
|
|
1863
|
+
wasm: "wasm"
|
|
1864
|
+
};
|
|
1865
|
+
var BY_NAME = {
|
|
1866
|
+
makefile: "makefile",
|
|
1867
|
+
gnumakefile: "makefile",
|
|
1868
|
+
dockerfile: "bash",
|
|
1869
|
+
rakefile: "ruby",
|
|
1870
|
+
gemfile: "ruby",
|
|
1871
|
+
".bashrc": "bash",
|
|
1872
|
+
".zshrc": "bash",
|
|
1873
|
+
".profile": "bash",
|
|
1874
|
+
"cmakelists.txt": "cmake"
|
|
1875
|
+
};
|
|
1876
|
+
function languageForPath(path) {
|
|
1877
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
1878
|
+
const byName = BY_NAME[base];
|
|
1879
|
+
if (byName && lowlight.registered(byName)) return byName;
|
|
1880
|
+
const dot = base.lastIndexOf(".");
|
|
1881
|
+
if (dot === -1) return null;
|
|
1882
|
+
const lang = BY_EXT[base.slice(dot + 1)];
|
|
1883
|
+
return lang && lowlight.registered(lang) ? lang : null;
|
|
1884
|
+
}
|
|
1885
|
+
function stylers(c) {
|
|
1886
|
+
return {
|
|
1887
|
+
keyword: c.magenta,
|
|
1888
|
+
"selector-tag": c.magenta,
|
|
1889
|
+
"template-tag": c.magenta,
|
|
1890
|
+
doctag: c.magenta,
|
|
1891
|
+
string: c.yellow,
|
|
1892
|
+
regexp: c.yellow,
|
|
1893
|
+
"template-variable": c.yellow,
|
|
1894
|
+
subst: (s) => s,
|
|
1895
|
+
comment: c.gray,
|
|
1896
|
+
quote: c.gray,
|
|
1897
|
+
number: c.cyan,
|
|
1898
|
+
literal: c.cyan,
|
|
1899
|
+
symbol: c.cyan,
|
|
1900
|
+
bullet: c.cyan,
|
|
1901
|
+
link: c.cyan,
|
|
1902
|
+
title: c.blue,
|
|
1903
|
+
section: c.blue,
|
|
1904
|
+
"selector-id": c.blue,
|
|
1905
|
+
"selector-class": c.blue,
|
|
1906
|
+
type: c.cyan,
|
|
1907
|
+
built_in: c.cyan,
|
|
1908
|
+
class: c.cyan,
|
|
1909
|
+
attr: c.blue,
|
|
1910
|
+
attribute: c.blue,
|
|
1911
|
+
property: c.blue,
|
|
1912
|
+
variable: (s) => s,
|
|
1913
|
+
params: (s) => s,
|
|
1914
|
+
meta: c.gray,
|
|
1915
|
+
tag: c.blue,
|
|
1916
|
+
name: c.blue,
|
|
1917
|
+
"selector-attr": c.blue,
|
|
1918
|
+
"selector-pseudo": c.blue,
|
|
1919
|
+
addition: (s) => s,
|
|
1920
|
+
deletion: (s) => s,
|
|
1921
|
+
emphasis: c.italic,
|
|
1922
|
+
strong: c.bold
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function highlightCode(code, lang, c) {
|
|
1926
|
+
if (!lowlight.registered(lang)) return null;
|
|
1927
|
+
let tree;
|
|
1928
|
+
try {
|
|
1929
|
+
tree = lowlight.highlight(lang, code);
|
|
1930
|
+
} catch {
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
const map = stylers(c);
|
|
1934
|
+
const lines = [""];
|
|
1935
|
+
const visit = (node, style) => {
|
|
1936
|
+
if (node.type === "text" && typeof node.value === "string") {
|
|
1937
|
+
const parts = node.value.split("\n");
|
|
1938
|
+
parts.forEach((part, i) => {
|
|
1939
|
+
if (i > 0) lines.push("");
|
|
1940
|
+
if (part) lines[lines.length - 1] += style ? style(part) : part;
|
|
1941
|
+
});
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
let own = style;
|
|
1945
|
+
for (const cls of node.properties?.className ?? []) {
|
|
1946
|
+
const scope = cls.startsWith("hljs-") ? cls.slice(5) : cls;
|
|
1947
|
+
const s = map[scope];
|
|
1948
|
+
if (s) {
|
|
1949
|
+
own = s;
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
for (const child of node.children ?? []) visit(child, own);
|
|
1954
|
+
};
|
|
1955
|
+
visit(tree, null);
|
|
1956
|
+
const expected = code.split("\n").length;
|
|
1957
|
+
if (lines.length !== expected) return null;
|
|
1958
|
+
return lines;
|
|
1959
|
+
}
|
|
1960
|
+
var ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
1961
|
+
function stripAnsi(s) {
|
|
1962
|
+
return s.replace(ANSI_RE, "");
|
|
1963
|
+
}
|
|
1964
|
+
function visibleLength(s) {
|
|
1965
|
+
return stripAnsi(s).length;
|
|
1966
|
+
}
|
|
1967
|
+
var SOFT_RESET = "\x1B[39m\x1B[22m\x1B[23m\x1B[24m";
|
|
1968
|
+
function truncateAnsi(s, max) {
|
|
1969
|
+
if (visibleLength(s) <= max) return s;
|
|
1970
|
+
let out = "";
|
|
1971
|
+
let seen = 0;
|
|
1972
|
+
const limit = Math.max(0, max - 1);
|
|
1973
|
+
for (let i = 0; i < s.length; ) {
|
|
1974
|
+
if (s[i] === "\x1B") {
|
|
1975
|
+
const m = /^\x1b\[[0-9;]*m/.exec(s.slice(i));
|
|
1976
|
+
if (m) {
|
|
1977
|
+
out += m[0];
|
|
1978
|
+
i += m[0].length;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (seen >= limit) break;
|
|
1983
|
+
out += s[i];
|
|
1984
|
+
seen++;
|
|
1985
|
+
i++;
|
|
1986
|
+
}
|
|
1987
|
+
return out + SOFT_RESET + "\u2026";
|
|
1988
|
+
}
|
|
1989
|
+
function lineTint(type, theme) {
|
|
1990
|
+
const code = theme === "dark" ? type === "add" ? 22 : 52 : type === "add" ? 194 : 224;
|
|
1991
|
+
return { open: `\x1B[48;5;${code}m`, close: "\x1B[49m" };
|
|
1992
|
+
}
|
|
1993
|
+
function supports256Colors(env = process.env) {
|
|
1994
|
+
if (env.TERM === "dumb") return false;
|
|
1995
|
+
return Boolean(env.COLORTERM) || /256color|truecolor|direct/i.test(env.TERM ?? "") || Boolean(env.TERM_PROGRAM) || Boolean(env.WT_SESSION);
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/render/cli.ts
|
|
1516
1999
|
var MIN_WIDTH = 40;
|
|
1517
2000
|
var MAX_WIDTH = 120;
|
|
1518
2001
|
var CliRenderer = class {
|
|
1519
2002
|
render(tour, opts) {
|
|
1520
2003
|
const c = pc.createColors(opts.color);
|
|
1521
2004
|
const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, opts.width));
|
|
2005
|
+
const style = { highlight: Boolean(opts.color && opts.highlight), theme: opts.theme ?? "dark" };
|
|
1522
2006
|
const out = [];
|
|
1523
2007
|
out.push(...renderHeader(tour, c, width, opts.fromCache ?? false));
|
|
1524
2008
|
out.push("");
|
|
@@ -1527,12 +2011,12 @@ var CliRenderer = class {
|
|
|
1527
2011
|
if (!section) {
|
|
1528
2012
|
throw new UsageError(`No section ${opts.section}; this tour has ${tour.sections.length} section${tour.sections.length === 1 ? "" : "s"}.`);
|
|
1529
2013
|
}
|
|
1530
|
-
out.push(...renderSection(section, opts.section, tour.sections.length, c, width));
|
|
2014
|
+
out.push(...renderSection(section, opts.section, tour.sections.length, c, width, style));
|
|
1531
2015
|
} else {
|
|
1532
2016
|
out.push(...renderToc(tour, c, width));
|
|
1533
2017
|
out.push("");
|
|
1534
2018
|
tour.sections.forEach((s, i) => {
|
|
1535
|
-
out.push(...renderSection(s, i + 1, tour.sections.length, c, width));
|
|
2019
|
+
out.push(...renderSection(s, i + 1, tour.sections.length, c, width, style));
|
|
1536
2020
|
out.push("");
|
|
1537
2021
|
});
|
|
1538
2022
|
}
|
|
@@ -1566,12 +2050,12 @@ function renderToc(tour, c, width) {
|
|
|
1566
2050
|
const label = ` ${n}. ${s.title}`;
|
|
1567
2051
|
const right = `${s.stats.files} file${s.stats.files === 1 ? "" : "s"} \xB7 ${statsLine(s.stats, c, true)}`;
|
|
1568
2052
|
const rightPlain = `${s.stats.files} file${s.stats.files === 1 ? "" : "s"} \xB7 +${s.stats.additions} -${s.stats.deletions}`;
|
|
1569
|
-
const gap = Math.max(2, width -
|
|
2053
|
+
const gap = Math.max(2, width - visibleLength2(label) - rightPlain.length);
|
|
1570
2054
|
lines.push(label + " ".repeat(gap) + c.dim(right));
|
|
1571
2055
|
}
|
|
1572
2056
|
return lines;
|
|
1573
2057
|
}
|
|
1574
|
-
function renderSection(s, index, count, c, width) {
|
|
2058
|
+
function renderSection(s, index, count, c, width, style) {
|
|
1575
2059
|
const lines = [];
|
|
1576
2060
|
lines.push(c.dim("\u2500".repeat(width)));
|
|
1577
2061
|
lines.push(c.bold(`${index}. ${s.title}`) + c.dim(` (${index}/${count})`));
|
|
@@ -1586,26 +2070,41 @@ function renderSection(s, index, count, c, width) {
|
|
|
1586
2070
|
}
|
|
1587
2071
|
for (const e of s.excerpts) {
|
|
1588
2072
|
lines.push("");
|
|
1589
|
-
lines.push(...renderExcerpt(e, c, width));
|
|
2073
|
+
lines.push(...renderExcerpt(e, c, width, style));
|
|
1590
2074
|
}
|
|
1591
2075
|
return lines;
|
|
1592
2076
|
}
|
|
1593
|
-
function renderExcerpt(e, c, width) {
|
|
2077
|
+
function renderExcerpt(e, c, width, style) {
|
|
1594
2078
|
const lines = [];
|
|
1595
2079
|
const title = ` ${c.cyan(e.file)}${c.dim(":" + e.newStart)}`;
|
|
1596
2080
|
lines.push(e.note ? `${title} ${c.italic(c.dim(e.note))}` : title);
|
|
1597
2081
|
const maxNo = Math.max(...e.lines.map((l) => Math.max(l.oldNo ?? 0, l.newNo ?? 0)), 1);
|
|
1598
2082
|
const w = String(maxNo).length;
|
|
1599
|
-
const budget = Math.max(20, width - (3 + w * 2 +
|
|
1600
|
-
|
|
2083
|
+
const budget = Math.max(20, width - (3 + w * 2 + 3) - 1);
|
|
2084
|
+
const texts = e.lines.map((l) => expandTabs(l.text));
|
|
2085
|
+
const lang = style.highlight ? languageForPath(e.file) : null;
|
|
2086
|
+
const highlighted = lang ? highlightCode(texts.join("\n"), lang, c) : null;
|
|
2087
|
+
e.lines.forEach((l, i) => {
|
|
1601
2088
|
const oldNo = l.oldNo === void 0 ? " ".repeat(w) : String(l.oldNo).padStart(w);
|
|
1602
2089
|
const newNo = l.newNo === void 0 ? " ".repeat(w) : String(l.newNo).padStart(w);
|
|
1603
2090
|
const sign = l.type === "add" ? "+" : l.type === "del" ? "-" : " ";
|
|
1604
|
-
const text = truncate(expandTabs(l.text), budget);
|
|
1605
2091
|
const gutter = c.dim(` ${oldNo} ${newNo} \u2502`);
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
2092
|
+
const plain = texts[i];
|
|
2093
|
+
if (!highlighted) {
|
|
2094
|
+
const body = `${sign}${truncate(plain, budget)}`;
|
|
2095
|
+
lines.push(`${gutter}${l.type === "add" ? c.green(body) : l.type === "del" ? c.red(body) : body}`);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
const text = truncateAnsi(highlighted[i], budget);
|
|
2099
|
+
const pad = " ".repeat(Math.max(0, budget - visibleLength(text)));
|
|
2100
|
+
if (l.type === "ctx") {
|
|
2101
|
+
lines.push(`${gutter} ${text}`);
|
|
2102
|
+
return;
|
|
2103
|
+
}
|
|
2104
|
+
const tint = lineTint(l.type, style.theme);
|
|
2105
|
+
const signStyled = l.type === "add" ? c.green(c.bold("+")) : c.red(c.bold("-"));
|
|
2106
|
+
lines.push(`${gutter}${tint.open}${signStyled}${text}${pad}${tint.close}`);
|
|
2107
|
+
});
|
|
1609
2108
|
return lines;
|
|
1610
2109
|
}
|
|
1611
2110
|
function statsLine(stats, c, omitFiles = false) {
|
|
@@ -1645,7 +2144,7 @@ function truncate(s, max) {
|
|
|
1645
2144
|
function expandTabs(s) {
|
|
1646
2145
|
return s.replaceAll(" ", " ");
|
|
1647
2146
|
}
|
|
1648
|
-
function
|
|
2147
|
+
function visibleLength2(s) {
|
|
1649
2148
|
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
1650
2149
|
}
|
|
1651
2150
|
function relativeTime(iso, now = /* @__PURE__ */ new Date()) {
|
|
@@ -1727,6 +2226,7 @@ export {
|
|
|
1727
2226
|
materializeTour,
|
|
1728
2227
|
sliceHunk,
|
|
1729
2228
|
PROMPT_VERSION,
|
|
2229
|
+
promptHeader,
|
|
1730
2230
|
buildPrompt,
|
|
1731
2231
|
describeSource,
|
|
1732
2232
|
GhCodeHost,
|
|
@@ -1739,10 +2239,14 @@ export {
|
|
|
1739
2239
|
collectCommits,
|
|
1740
2240
|
resolveRange,
|
|
1741
2241
|
detectDefaultBranch,
|
|
2242
|
+
TOUR_JSON_SCHEMA,
|
|
2243
|
+
ClaudeStreamParser,
|
|
1742
2244
|
CommandProvider,
|
|
1743
2245
|
createProvider,
|
|
2246
|
+
noProgress,
|
|
1744
2247
|
prepareTour,
|
|
1745
2248
|
generateTour,
|
|
2249
|
+
supports256Colors,
|
|
1746
2250
|
CliRenderer,
|
|
1747
2251
|
describeSource2,
|
|
1748
2252
|
wrap,
|
|
@@ -1750,4 +2254,4 @@ export {
|
|
|
1750
2254
|
writeMaybePaged,
|
|
1751
2255
|
writeStdout
|
|
1752
2256
|
};
|
|
1753
|
-
//# sourceMappingURL=chunk-
|
|
2257
|
+
//# sourceMappingURL=chunk-7ALXKEST.js.map
|