@ahmedalbarghouti/farq 0.0.2 → 0.1.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 +51 -13
- package/dist/concurrency.d.ts +2 -0
- package/dist/concurrency.js +16 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +25 -0
- package/dist/git.d.ts +7 -3
- package/dist/git.js +36 -27
- package/dist/index.js +116 -33
- package/dist/open-pr.d.ts +6 -1
- package/dist/open-pr.js +42 -16
- package/dist/providers/claude.d.ts +1 -0
- package/dist/providers/claude.js +49 -32
- package/dist/providers/fake.d.ts +5 -2
- package/dist/providers/fake.js +26 -15
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/render/pr.d.ts +6 -0
- package/dist/render/pr.js +29 -7
- package/dist/schema.d.ts +40 -7
- package/dist/schema.js +14 -1
- package/dist/summarize.js +7 -0
- package/dist/template.d.ts +6 -0
- package/dist/template.js +40 -19
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/lines.d.ts +19 -1
- package/dist/ui/lines.js +81 -39
- package/dist/ui/logger.d.ts +9 -2
- package/dist/ui/logger.js +15 -4
- package/dist/ui/spinner.d.ts +15 -2
- package/dist/ui/spinner.js +105 -22
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +3 -0
- package/dist/visual/chrome.js +21 -2
- package/dist/visual/cluster.d.ts +29 -0
- package/dist/visual/cluster.js +204 -0
- package/dist/visual/compose.d.ts +8 -1
- package/dist/visual/compose.js +17 -32
- package/dist/visual/css-scope.d.ts +6 -0
- package/dist/visual/css-scope.js +162 -0
- package/dist/visual/design.d.ts +103 -0
- package/dist/visual/design.js +327 -0
- package/dist/visual/diagram.d.ts +8 -0
- package/dist/visual/diagram.js +36 -14
- package/dist/visual/mockup.d.ts +16 -11
- package/dist/visual/mockup.js +45 -22
- package/dist/visual/pipeline.d.ts +35 -1
- package/dist/visual/pipeline.js +191 -69
- package/dist/visual/viewport.d.ts +16 -0
- package/dist/visual/viewport.js +19 -0
- package/package.json +1 -1
package/dist/ui/lines.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
import type { ProviderName } from "../config.js";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Rotating status lines. `{p}` is replaced with the provider name so a line
|
|
4
|
+
* always says who is doing the work. Keep templates <= 37 chars so the longest
|
|
5
|
+
* provider name still fits the one-line spinner.
|
|
6
|
+
*/
|
|
7
|
+
declare const BANKS: {
|
|
8
|
+
diff: string[];
|
|
9
|
+
summarize: string[];
|
|
10
|
+
topics: string[];
|
|
11
|
+
mockup: string[];
|
|
12
|
+
diagram: string[];
|
|
13
|
+
shoot: string[];
|
|
14
|
+
visual: string[];
|
|
15
|
+
open: string[];
|
|
16
|
+
};
|
|
17
|
+
export type LineBank = keyof typeof BANKS;
|
|
3
18
|
export declare function linesFor(bank: LineBank, provider?: ProviderName): string[];
|
|
19
|
+
/** Short, honest name for the stage — the witty line sits next to it. */
|
|
20
|
+
export declare function labelFor(bank: LineBank): string;
|
|
21
|
+
export {};
|
package/dist/ui/lines.js
CHANGED
|
@@ -1,48 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"fake is vibing on hard mode…",
|
|
14
|
-
"fake is inventing nothing new…",
|
|
15
|
-
"fake is shipping canned vibes…",
|
|
1
|
+
/**
|
|
2
|
+
* Rotating status lines. `{p}` is replaced with the provider name so a line
|
|
3
|
+
* always says who is doing the work. Keep templates <= 37 chars so the longest
|
|
4
|
+
* provider name still fits the one-line spinner.
|
|
5
|
+
*/
|
|
6
|
+
const BANKS = {
|
|
7
|
+
diff: [
|
|
8
|
+
"gathering the diff…",
|
|
9
|
+
"asking git politely…",
|
|
10
|
+
"reading what you actually did…",
|
|
11
|
+
"diffing against the merge base…",
|
|
12
|
+
"counting the damage…",
|
|
16
13
|
],
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
14
|
+
summarize: [
|
|
15
|
+
"{p} is reading the diff…",
|
|
16
|
+
"{p} is grouping the mess…",
|
|
17
|
+
"{p} is finding the story…",
|
|
18
|
+
"{p} is naming things (hard part)…",
|
|
19
|
+
"{p} is deciding what matters…",
|
|
20
|
+
"{p} is skipping the boring parts…",
|
|
21
|
+
"{p} is drafting your PR title…",
|
|
22
|
+
"{p} is writing the blurb…",
|
|
23
23
|
],
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
24
|
+
topics: [
|
|
25
|
+
"planning the visuals…",
|
|
26
|
+
"splitting the story into scenes…",
|
|
27
|
+
"deciding how many pictures…",
|
|
28
|
+
"grouping changes into topics…",
|
|
29
|
+
],
|
|
30
|
+
mockup: [
|
|
31
|
+
"{p} is sketching the before…",
|
|
32
|
+
"{p} is sketching the after…",
|
|
33
|
+
"{p} is laying out the mockup…",
|
|
34
|
+
"{p} is writing placeholder copy…",
|
|
35
|
+
"{p} is obeying the design tokens…",
|
|
36
|
+
"{p} is resisting a redesign…",
|
|
37
|
+
"{p} is measuring the spacing…",
|
|
28
38
|
],
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
39
|
+
diagram: [
|
|
40
|
+
"{p} is boxing a flowchart…",
|
|
41
|
+
"{p} is drawing arrows…",
|
|
42
|
+
"{p} is explaining it with boxes…",
|
|
43
|
+
"{p} is keeping it conceptual…",
|
|
44
|
+
"{p} is deleting a wall of text…",
|
|
45
|
+
],
|
|
46
|
+
shoot: [
|
|
32
47
|
"chrome is taking the shot…",
|
|
48
|
+
"chrome is scaling it to fit…",
|
|
49
|
+
"waiting on headless chrome…",
|
|
50
|
+
"pressing the shutter…",
|
|
51
|
+
],
|
|
52
|
+
visual: [
|
|
53
|
+
"{p} is drawing the difference…",
|
|
54
|
+
"{p} is picking a composition…",
|
|
55
|
+
"chrome is warming up…",
|
|
56
|
+
"making the change look obvious…",
|
|
57
|
+
],
|
|
58
|
+
open: [
|
|
59
|
+
"talking to gh…",
|
|
60
|
+
"filling the PR template…",
|
|
61
|
+
"uploading the visuals…",
|
|
62
|
+
"shipping the PR body…",
|
|
63
|
+
"almost there…",
|
|
33
64
|
],
|
|
34
65
|
};
|
|
35
|
-
const OPEN = [
|
|
36
|
-
"talking to gh…",
|
|
37
|
-
"shipping the PR body…",
|
|
38
|
-
"almost there…",
|
|
39
|
-
];
|
|
40
66
|
export function linesFor(bank, provider) {
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
const name = provider ?? "claude";
|
|
68
|
+
return BANKS[bank].map((line) => line.replaceAll("{p}", name));
|
|
69
|
+
}
|
|
70
|
+
/** Short, honest name for the stage — the witty line sits next to it. */
|
|
71
|
+
export function labelFor(bank) {
|
|
72
|
+
switch (bank) {
|
|
73
|
+
case "diff":
|
|
74
|
+
return "diff";
|
|
75
|
+
case "summarize":
|
|
76
|
+
return "summary";
|
|
77
|
+
case "topics":
|
|
78
|
+
return "planning";
|
|
79
|
+
case "mockup":
|
|
80
|
+
return "mockup";
|
|
81
|
+
case "diagram":
|
|
82
|
+
return "diagram";
|
|
83
|
+
case "shoot":
|
|
84
|
+
return "capture";
|
|
85
|
+
case "visual":
|
|
86
|
+
return "visuals";
|
|
87
|
+
case "open":
|
|
88
|
+
return "pull request";
|
|
43
89
|
}
|
|
44
|
-
if (bank === "open")
|
|
45
|
-
return OPEN;
|
|
46
|
-
const p = provider ?? "claude";
|
|
47
|
-
return bank === "summarize" ? SUMMARIZE[p] : VISUAL[p];
|
|
48
90
|
}
|
package/dist/ui/logger.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { ProviderName } from "../config.js";
|
|
2
2
|
import { type LineBank } from "./lines.js";
|
|
3
3
|
import { type SpinnerHandle } from "./spinner.js";
|
|
4
|
+
export type StageOptions = {
|
|
5
|
+
provider?: ProviderName;
|
|
6
|
+
/** Override the stage label shown before the witty line. */
|
|
7
|
+
label?: string;
|
|
8
|
+
};
|
|
4
9
|
export type Ui = {
|
|
10
|
+
/** Declare how many stages this run will have, so steps read as [2/4]. */
|
|
11
|
+
plan: (totalSteps: number) => void;
|
|
5
12
|
/** Plain note (no spinner). */
|
|
6
13
|
note: (text: string) => void;
|
|
7
|
-
/** Start a
|
|
8
|
-
stage: (bank: LineBank,
|
|
14
|
+
/** Start a numbered stage with rotating witty lines. */
|
|
15
|
+
stage: (bank: LineBank, options?: StageOptions | ProviderName) => SpinnerHandle;
|
|
9
16
|
error: (text: string) => void;
|
|
10
17
|
success: (text: string) => void;
|
|
11
18
|
};
|
package/dist/ui/logger.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
|
-
import { linesFor } from "./lines.js";
|
|
1
|
+
import { labelFor, linesFor } from "./lines.js";
|
|
2
2
|
import { startSpinner } from "./spinner.js";
|
|
3
3
|
import { brand, fail, muted, ok } from "./theme.js";
|
|
4
4
|
export function createUi() {
|
|
5
|
+
let total = 0;
|
|
6
|
+
let step = 0;
|
|
5
7
|
return {
|
|
8
|
+
plan(totalSteps) {
|
|
9
|
+
total = Math.max(0, Math.floor(totalSteps));
|
|
10
|
+
step = 0;
|
|
11
|
+
},
|
|
6
12
|
note(text) {
|
|
7
13
|
process.stderr.write(`${brand()} ${muted(text)}\n`);
|
|
8
14
|
},
|
|
9
|
-
stage(bank,
|
|
10
|
-
const
|
|
11
|
-
|
|
15
|
+
stage(bank, options) {
|
|
16
|
+
const opts = typeof options === "string" ? { provider: options } : (options ?? {});
|
|
17
|
+
step++;
|
|
18
|
+
return startSpinner({
|
|
19
|
+
lines: linesFor(bank, opts.provider),
|
|
20
|
+
label: opts.label ?? labelFor(bank),
|
|
21
|
+
step: total > 0 ? { index: Math.min(step, total), total } : undefined,
|
|
22
|
+
});
|
|
12
23
|
},
|
|
13
24
|
error(text) {
|
|
14
25
|
process.stderr.write(`${brand()} ${fail(text)}\n`);
|
package/dist/ui/spinner.d.ts
CHANGED
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
export type SpinnerHandle = {
|
|
2
|
-
|
|
2
|
+
/** Replace the stage label (rarely needed). */
|
|
3
|
+
update: (label: string) => void;
|
|
4
|
+
/** Swap the rotating witty lines mid-stage, e.g. summarize → mockup. */
|
|
5
|
+
setLines: (lines: string[]) => void;
|
|
6
|
+
/** Concrete sub-stage status shown after the witty line. */
|
|
7
|
+
detail: (text: string | null) => void;
|
|
3
8
|
succeed: (text: string) => void;
|
|
4
9
|
fail: (text: string) => void;
|
|
5
10
|
stop: () => void;
|
|
6
11
|
};
|
|
7
|
-
export
|
|
12
|
+
export type SpinnerOptions = {
|
|
13
|
+
lines: string[];
|
|
14
|
+
label?: string;
|
|
15
|
+
step?: {
|
|
16
|
+
index: number;
|
|
17
|
+
total: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function startSpinner(options: SpinnerOptions): SpinnerHandle;
|
package/dist/ui/spinner.js
CHANGED
|
@@ -1,46 +1,86 @@
|
|
|
1
|
-
import { brand,
|
|
1
|
+
import { accent, brand, dim, fail, isInteractive, muted, ok } from "./theme.js";
|
|
2
2
|
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
-
|
|
3
|
+
const FRAME_MS = 80;
|
|
4
|
+
const ROTATE_MS = 2600;
|
|
5
|
+
/** Only surface a timer once a stage is slow enough to feel slow. */
|
|
6
|
+
const ELAPSED_AFTER_MS = 3000;
|
|
7
|
+
export function startSpinner(options) {
|
|
4
8
|
const interactive = isInteractive();
|
|
5
|
-
|
|
6
|
-
let
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
let label = options.label ?? "working";
|
|
11
|
+
let lines = rotateOrder(options.lines);
|
|
7
12
|
let lineIdx = 0;
|
|
13
|
+
let detailText = null;
|
|
14
|
+
let frame = 0;
|
|
8
15
|
let timer = null;
|
|
9
16
|
let lineTimer = null;
|
|
10
17
|
let stopped = false;
|
|
18
|
+
let lastStatic = "";
|
|
19
|
+
const stepTag = options.step
|
|
20
|
+
? `[${options.step.index}/${options.step.total}] `
|
|
21
|
+
: "";
|
|
22
|
+
const elapsedSeconds = () => (Date.now() - started) / 1000;
|
|
23
|
+
const compose = () => {
|
|
24
|
+
const parts = [lines[lineIdx] ?? label];
|
|
25
|
+
if (detailText)
|
|
26
|
+
parts.push(detailText);
|
|
27
|
+
const elapsed = Date.now() - started >= ELAPSED_AFTER_MS
|
|
28
|
+
? `${Math.round(elapsedSeconds())}s`
|
|
29
|
+
: "";
|
|
30
|
+
const body = parts.join(dim(" · "));
|
|
31
|
+
const tail = elapsed ? `${dim(" · ")}${dim(elapsed)}` : "";
|
|
32
|
+
return `${accent(stepTag)}${label}${dim(" · ")}${muted(body)}${tail}`;
|
|
33
|
+
};
|
|
11
34
|
const render = () => {
|
|
12
35
|
if (!interactive || stopped)
|
|
13
36
|
return;
|
|
14
|
-
const
|
|
15
|
-
process.stderr.write(`\r\x1b[2K${
|
|
16
|
-
|
|
37
|
+
const line = `${brand()} ${accent(FRAMES[frame % FRAMES.length])} ${compose()}`;
|
|
38
|
+
process.stderr.write(`\r\x1b[2K${truncate(line)}`);
|
|
39
|
+
frame++;
|
|
40
|
+
};
|
|
41
|
+
const printStatic = () => {
|
|
42
|
+
const next = detailText ? `${label} · ${detailText}` : label;
|
|
43
|
+
if (next === lastStatic)
|
|
44
|
+
return;
|
|
45
|
+
lastStatic = next;
|
|
46
|
+
process.stderr.write(`${brand()} ${muted(`${stepTag}${next}`)}\n`);
|
|
17
47
|
};
|
|
18
48
|
if (interactive) {
|
|
19
49
|
render();
|
|
20
|
-
timer = setInterval(render,
|
|
21
|
-
if (
|
|
50
|
+
timer = setInterval(render, FRAME_MS);
|
|
51
|
+
if (lines.length > 1) {
|
|
22
52
|
lineTimer = setInterval(() => {
|
|
23
|
-
lineIdx = (lineIdx + 1) %
|
|
24
|
-
|
|
25
|
-
}, 2000);
|
|
53
|
+
lineIdx = (lineIdx + 1) % lines.length;
|
|
54
|
+
}, ROTATE_MS);
|
|
26
55
|
}
|
|
27
56
|
}
|
|
28
57
|
else {
|
|
29
|
-
|
|
58
|
+
printStatic();
|
|
30
59
|
}
|
|
31
|
-
|
|
60
|
+
const handle = {
|
|
32
61
|
update(next) {
|
|
33
|
-
|
|
62
|
+
label = next;
|
|
63
|
+
if (!interactive)
|
|
64
|
+
printStatic();
|
|
65
|
+
},
|
|
66
|
+
setLines(next) {
|
|
67
|
+
if (next.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
lines = rotateOrder(next);
|
|
70
|
+
lineIdx = 0;
|
|
71
|
+
},
|
|
72
|
+
detail(text) {
|
|
73
|
+
detailText = text && text.trim() ? text.trim() : null;
|
|
34
74
|
if (!interactive)
|
|
35
|
-
|
|
75
|
+
printStatic();
|
|
36
76
|
},
|
|
37
|
-
succeed(
|
|
38
|
-
|
|
39
|
-
process.stderr.write(`${brand()} ${ok(
|
|
77
|
+
succeed(text) {
|
|
78
|
+
handle.stop();
|
|
79
|
+
process.stderr.write(`${brand()} ${ok(text)}${dim(` ${formatDuration(elapsedSeconds())}`)}\n`);
|
|
40
80
|
},
|
|
41
|
-
fail(
|
|
42
|
-
|
|
43
|
-
process.stderr.write(`${brand()} ${fail(
|
|
81
|
+
fail(text) {
|
|
82
|
+
handle.stop();
|
|
83
|
+
process.stderr.write(`${brand()} ${fail(text)}\n`);
|
|
44
84
|
},
|
|
45
85
|
stop() {
|
|
46
86
|
if (stopped)
|
|
@@ -54,4 +94,47 @@ export function startSpinner(initial, rotateLines) {
|
|
|
54
94
|
process.stderr.write("\r\x1b[2K");
|
|
55
95
|
},
|
|
56
96
|
};
|
|
97
|
+
return handle;
|
|
98
|
+
}
|
|
99
|
+
function formatDuration(seconds) {
|
|
100
|
+
if (seconds < 10)
|
|
101
|
+
return `${seconds.toFixed(1)}s`;
|
|
102
|
+
if (seconds < 60)
|
|
103
|
+
return `${Math.round(seconds)}s`;
|
|
104
|
+
const mins = Math.floor(seconds / 60);
|
|
105
|
+
return `${mins}m${String(Math.round(seconds % 60)).padStart(2, "0")}s`;
|
|
106
|
+
}
|
|
107
|
+
/** Keep the first line stable, shuffle the rest so reruns feel different. */
|
|
108
|
+
function rotateOrder(lines) {
|
|
109
|
+
if (lines.length < 3)
|
|
110
|
+
return [...lines];
|
|
111
|
+
const [head, ...tail] = lines;
|
|
112
|
+
for (let i = tail.length - 1; i > 0; i--) {
|
|
113
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
114
|
+
[tail[i], tail[j]] = [tail[j], tail[i]];
|
|
115
|
+
}
|
|
116
|
+
return [head, ...tail];
|
|
117
|
+
}
|
|
118
|
+
/** Trim to the terminal width, ignoring ANSI escapes when measuring. */
|
|
119
|
+
function truncate(line) {
|
|
120
|
+
const columns = process.stderr.columns ?? 0;
|
|
121
|
+
if (!columns || columns < 20)
|
|
122
|
+
return line;
|
|
123
|
+
const max = columns - 1;
|
|
124
|
+
let visible = 0;
|
|
125
|
+
let i = 0;
|
|
126
|
+
while (i < line.length) {
|
|
127
|
+
if (line[i] === "\x1b") {
|
|
128
|
+
const end = line.indexOf("m", i);
|
|
129
|
+
if (end === -1)
|
|
130
|
+
break;
|
|
131
|
+
i = end + 1;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (visible === max)
|
|
135
|
+
return `${line.slice(0, i)}…\x1b[0m`;
|
|
136
|
+
visible++;
|
|
137
|
+
i++;
|
|
138
|
+
}
|
|
139
|
+
return line;
|
|
57
140
|
}
|
package/dist/ui/theme.d.ts
CHANGED
|
@@ -3,4 +3,5 @@ export declare function accent(text: string): string;
|
|
|
3
3
|
export declare function ok(text: string): string;
|
|
4
4
|
export declare function fail(text: string): string;
|
|
5
5
|
export declare function muted(text: string): string;
|
|
6
|
+
export declare function dim(text: string): string;
|
|
6
7
|
export declare function isInteractive(): boolean;
|
package/dist/ui/theme.js
CHANGED
|
@@ -14,6 +14,9 @@ export function fail(text) {
|
|
|
14
14
|
export function muted(text) {
|
|
15
15
|
return pc.dim(text);
|
|
16
16
|
}
|
|
17
|
+
export function dim(text) {
|
|
18
|
+
return pc.dim(text);
|
|
19
|
+
}
|
|
17
20
|
export function isInteractive() {
|
|
18
21
|
return Boolean(process.stderr.isTTY) && process.env.CI !== "true";
|
|
19
22
|
}
|
package/dist/visual/chrome.js
CHANGED
|
@@ -2,6 +2,7 @@ import { execa } from "execa";
|
|
|
2
2
|
import { platform } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { resolveExecutable, ToolNotFoundError, } from "../tools.js";
|
|
5
|
+
import { clampViewport } from "./viewport.js";
|
|
5
6
|
const TIMEOUT_MS = 30_000;
|
|
6
7
|
export class ChromeError extends Error {
|
|
7
8
|
constructor(message) {
|
|
@@ -9,7 +10,16 @@ export class ChromeError extends Error {
|
|
|
9
10
|
this.name = "ChromeError";
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
let cachedChromePath = null;
|
|
12
14
|
export function resolveChrome(env = process.env) {
|
|
15
|
+
if (env === process.env && cachedChromePath)
|
|
16
|
+
return cachedChromePath;
|
|
17
|
+
const path = lookupChrome(env);
|
|
18
|
+
if (env === process.env)
|
|
19
|
+
cachedChromePath = path;
|
|
20
|
+
return path;
|
|
21
|
+
}
|
|
22
|
+
function lookupChrome(env) {
|
|
13
23
|
const local = env.LOCALAPPDATA ?? "";
|
|
14
24
|
const pf = env.ProgramFiles ?? "C:\\Program Files";
|
|
15
25
|
const pf86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
@@ -55,13 +65,22 @@ export function resolveChrome(env = process.env) {
|
|
|
55
65
|
}
|
|
56
66
|
export async function screenshotHtml(options) {
|
|
57
67
|
const chrome = options.chromePath ?? resolveChrome();
|
|
58
|
-
const width
|
|
59
|
-
|
|
68
|
+
const { width, height } = clampViewport({
|
|
69
|
+
width: options.width,
|
|
70
|
+
height: options.height,
|
|
71
|
+
});
|
|
60
72
|
try {
|
|
61
73
|
await execa(chrome, [
|
|
62
74
|
"--headless=new",
|
|
63
75
|
"--disable-gpu",
|
|
64
76
|
"--hide-scrollbars",
|
|
77
|
+
"--no-first-run",
|
|
78
|
+
"--no-default-browser-check",
|
|
79
|
+
"--disable-extensions",
|
|
80
|
+
"--disable-background-networking",
|
|
81
|
+
"--disable-sync",
|
|
82
|
+
"--disable-dev-shm-usage",
|
|
83
|
+
"--force-color-profile=srgb",
|
|
65
84
|
`--window-size=${width},${height}`,
|
|
66
85
|
`--screenshot=${options.outPath}`,
|
|
67
86
|
options.url,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ChangeItem, ChangeSummary } from "../schema.js";
|
|
2
|
+
import type { Provider } from "../providers/index.js";
|
|
3
|
+
export declare const MAX_VISUAL_TOPICS = 5;
|
|
4
|
+
export type VisualTopic = {
|
|
5
|
+
id: number;
|
|
6
|
+
title: string;
|
|
7
|
+
items: ChangeItem[];
|
|
8
|
+
files: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Topic grouping, cheapest source first:
|
|
12
|
+
* 1. visual_topics returned by the summarize call (free — no extra request)
|
|
13
|
+
* 2. a dedicated cheap-model call
|
|
14
|
+
* 3. file-overlap union-find
|
|
15
|
+
* Same-theme items (one feature across many files) → one topic.
|
|
16
|
+
* Truly unrelated domains → separate topics.
|
|
17
|
+
*/
|
|
18
|
+
export declare function clusterVisualTopics(summary: ChangeSummary, options?: {
|
|
19
|
+
provider?: Provider;
|
|
20
|
+
model?: string;
|
|
21
|
+
maxTopics?: number;
|
|
22
|
+
log?: (msg: string) => void;
|
|
23
|
+
}): Promise<VisualTopic[]>;
|
|
24
|
+
/** File-overlap union-find (fallback). Exported for tests. */
|
|
25
|
+
export declare function clusterByFileOverlap(summary: ChangeSummary): VisualTopic[];
|
|
26
|
+
/** Merge the smallest topics until at most `limit` remain. */
|
|
27
|
+
export declare function capTopics(topics: VisualTopic[], limit?: number): VisualTopic[];
|
|
28
|
+
/** Build topics from model JSON; null if invalid. Exported for tests. */
|
|
29
|
+
export declare function topicsFromIntentJson(summary: ChangeSummary, raw: unknown): VisualTopic[] | null;
|