Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/cli/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
POSTHOG_API_KEY=
POSTHOG_HOST=https://us.i.posthog.com
4 changes: 2 additions & 2 deletions packages/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "getwired",
"version": "0.0.19",
"version": "0.0.20",
"description": "AI-powered CLI for visual regression and exploratory testing",
"type": "module",
"bin": {
Expand Down Expand Up @@ -49,6 +49,7 @@
"commander": "^13.0.0",
"ink": "^6.8.0",
"playwright": "^1.59.1",
"posthog-node": "^5.29.2",
"react": "^19.0.0",
"zod": "^4.3.6"
},
Expand Down
70 changes: 68 additions & 2 deletions packages/cli/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ProviderStream } from "./ProviderStream.js";
import { readFile, readdir, rm, mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import type { GetwiredSettings } from "../config/settings.js";
import type { DeviceProfile, TestFinding, TestPersona, TestReport, NativePlatform } from "../providers/types.js";
import type { DesktopPlatform } from "../desktop/types.js";
Expand Down Expand Up @@ -659,6 +660,9 @@ export function App({ mode, initProvider }: AppProps) {
const idx = formats.indexOf(settings.reporting.outputFormat);
return idx >= 0 ? idx : 0;
}
if (section === "telemetry") {
return settings.telemetry ? 0 : 1;
}
return 0;
}

Expand All @@ -670,6 +674,7 @@ export function App({ mode, initProvider }: AppProps) {
if (section === "device") return 2;
if (section === "screenshot") return 9;
if (section === "reporting") return 2;
if (section === "telemetry") return 1;
return 0;
};

Expand Down Expand Up @@ -701,6 +706,8 @@ export function App({ mode, initProvider }: AppProps) {
} else if (section === "reporting") {
const formats = ["json", "html", "markdown"] as const;
updated = { ...updated, reporting: { ...updated.reporting, outputFormat: formats[settingEditIndex] } };
} else if (section === "telemetry") {
updated = { ...updated, telemetry: settingEditIndex === 0 };
}
setSettings(updated);
try {
Expand Down Expand Up @@ -1019,6 +1026,24 @@ export function App({ mode, initProvider }: AppProps) {
<Text color="green" dimColor>
{testReport.summary.duration}ms · .getwired/reports/{testReport.id}/{testReport.id}.json
</Text>
{(() => {
const htmlPath = join(process.cwd(), ".getwired", "reports", testReport.id, "report.html");
const htmlExists = existsSync(htmlPath);
if (htmlExists) {
const fileUrl = pathToFileURL(htmlPath).href;
return (
<Box flexDirection="column">
<Text color="greenBright" bold>
📄 Report: <Text color="cyan">{fileUrl}</Text>
</Text>
<Text color="green" dimColor>
↑ Click the link above to open in your browser
</Text>
</Box>
);
}
return null;
})()}
{testReport.execution?.screenshots ? (
<Text color="green" dimColor>
Screenshots: .getwired/reports/{testReport.id}/screenshots/
Expand Down Expand Up @@ -1333,6 +1358,24 @@ export function App({ mode, initProvider }: AppProps) {
<Text color="green" dimColor>
{testReport.summary.duration}ms · .getwired/reports/{testReport.id}/{testReport.id}.json
</Text>
{(() => {
const htmlPath = join(process.cwd(), ".getwired", "reports", testReport.id, "report.html");
const htmlExists = existsSync(htmlPath);
if (htmlExists) {
const fileUrl = pathToFileURL(htmlPath).href;
return (
<Box flexDirection="column">
<Text color="greenBright" bold>
📄 Report: <Text color="cyan">{fileUrl}</Text>
</Text>
<Text color="green" dimColor>
↑ Click the link above to open in your browser
</Text>
</Box>
);
}
return null;
})()}
</Box>
)}
{testError && (
Expand Down Expand Up @@ -1430,11 +1473,15 @@ export function App({ mode, initProvider }: AppProps) {
<Box marginTop={1} flexDirection="column">
<Text color="greenBright" bold>── Findings ──────────────────────────</Text>
{activeReport.findings.map((f, i) => (
<Box key={i} paddingLeft={1} flexDirection="column">
<Box key={i} paddingLeft={1} flexDirection="column" marginBottom={1}>
<Text color={f.severity === "critical" || f.severity === "high" ? "redBright" : "yellow"}>
{f.severity === "critical" || f.severity === "high" ? "✘" : "⚠"} [{f.severity}] {f.title}
</Text>
<Text color="green" dimColor> {f.description.slice(0, 120)}</Text>
<Text color="green" dimColor wrap="wrap"> What: {f.description}</Text>
{f.url && <Text color="cyan" dimColor> URL: {f.url}</Text>}
{f.steps && f.steps.length > 0 && (
<Text color="green" dimColor> Steps: {f.steps.slice(0, 3).join(" → ")}{f.steps.length > 3 ? " …" : ""}</Text>
)}
</Box>
))}
</Box>
Expand Down Expand Up @@ -1583,6 +1630,23 @@ export function App({ mode, initProvider }: AppProps) {
))}
</Box>
)}
{settingEditing === "telemetry" && (
<Box flexDirection="column" paddingLeft={2}>
<Text color="greenBright" bold>Telemetry:</Text>
<Text color="green" dimColor>Anonymous usage analytics to help improve GetWired.</Text>
<Text color="green" dimColor>No private data, URLs, code, or file paths are ever sent.</Text>
{["Enabled", "Disabled"].map((label, i) => (
<Box key={label} gap={1}>
<Text color={i === settingEditIndex ? "greenBright" : "green"}>
{i === settingEditIndex ? " ▸ " : " "}
</Text>
<Text color={i === settingEditIndex ? "greenBright" : "green"} bold={i === settingEditIndex}>
{label}
</Text>
</Box>
))}
</Box>
)}
<Text color="greenBright" bold>└──────────────────────────────────────────────────</Text>
</Box>
<Box paddingX={2} gap={2}>
Expand Down Expand Up @@ -1643,6 +1707,7 @@ const SETTINGS_SECTIONS = [
{ key: "device", label: "Device Profile", description: "Test desktop, mobile, or both" },
{ key: "screenshot", label: "Screenshot Settings", description: "Capture & comparison options" },
{ key: "reporting", label: "Report Output", description: "Report format and behavior" },
{ key: "telemetry", label: "Telemetry", description: "Anonymous usage analytics to improve GetWired" },
];

const TEST_PERSONAS: Array<{ id: TestPersona; label: string; description: string }> = [
Expand All @@ -1664,6 +1729,7 @@ function getSettingValue(settings: GetwiredSettings, key: string): string {
case "device": return settings.testing.deviceProfile;
case "screenshot": return `fullPage:${settings.testing.screenshotFullPage} delay:${settings.testing.screenshotDelay}ms threshold:${(settings.testing.diffThreshold * 100).toFixed(0)}% browser:${settings.testing.showBrowser ? "visible" : "headless"}`;
case "reporting": return settings.reporting.outputFormat;
case "telemetry": return settings.telemetry ? "enabled" : "disabled";
default: return "";
}
}
Expand Down
43 changes: 37 additions & 6 deletions packages/cli/src/components/ProviderStream.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ interface ProviderStreamProps {
}

const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const ACTIVITY_MESSAGES = [
"Thinking…",
"Analyzing project…",
"Processing…",
"Generating scenarios…",
"Evaluating…",
"Working on it…",
"Almost there…",
];

function formatElapsed(ms: number): string {
const secs = Math.floor(ms / 1000);
Expand All @@ -30,24 +39,29 @@ export function ProviderStream({
const lastOutputLen = useRef(output.length);
const lastLineCount = useRef(output.split("\n").length);
const lastChangeTime = useRef(Date.now());
const startTime = useRef(Date.now());
const chunkCount = useRef(0);

// Single timer drives both cursor blink and spinner
useEffect(() => {
const timer = setInterval(() => setTick((t) => t + 1), 120);
return () => clearInterval(timer);
}, []);

// Track when output last changed
// Track when output last changed and count chunks
useEffect(() => {
if (output.length !== lastOutputLen.current) {
lastOutputLen.current = output.length;
lastChangeTime.current = Date.now();
chunkCount.current++;
}
}, [output]);

const cursorVisible = tick % 8 < 4;
const spinnerFrame = SPINNER_FRAMES[tick % SPINNER_FRAMES.length];
const silentFor = Date.now() - lastChangeTime.current;
const totalElapsed = Date.now() - startTime.current;
const activityMsg = ACTIVITY_MESSAGES[Math.floor(tick / 40) % ACTIVITY_MESSAGES.length];

// Extract report folder ID from output if not passed as prop
const extractedReportId = !reportId
Expand Down Expand Up @@ -122,6 +136,11 @@ export function ProviderStream({
{spinnerFrame} LIVE
</Text>
)}
{isStreaming && (
<Text color="green" dimColor>
{formatElapsed(totalElapsed)}
</Text>
)}
{(reportId || extractedReportId) && (
<Text color="gray">
[{reportId || extractedReportId}]
Expand All @@ -131,9 +150,19 @@ export function ProviderStream({

{/* Output lines */}
<Box flexDirection="column" flexGrow={1}>
{visible.length === 0 && (
{visible.length === 0 && isStreaming && (
<Box flexDirection="column" gap={0}>
<Text color="green">
{spinnerFrame} Connecting to {providerName ?? "provider"}...
</Text>
<Text color="green" dimColor>
{activityMsg}
</Text>
</Box>
)}
{visible.length === 0 && !isStreaming && (
<Text color="green" dimColor>
Waiting for provider response...
No output received.
</Text>
)}
{visible.map((line, i) => {
Expand Down Expand Up @@ -167,8 +196,10 @@ export function ProviderStream({
<Box marginTop={1}>
<Text color="green" dimColor>
{isStreaming && silentFor > 3000
? `${spinnerFrame} working... ${formatElapsed(silentFor).padEnd(6)}`
: " "}
? `${spinnerFrame} ${activityMsg} (${formatElapsed(silentFor)} since last output)`
: isStreaming && visible.length > 0
? `${spinnerFrame} Receiving data...`
: " "}
</Text>
</Box>
</Box>
Expand All @@ -181,7 +212,7 @@ export function ProviderStream({
</Box>
<Box gap={1}>
<Text color="green" dimColor>
{allLines.length} lines
{allLines.length} lines · {chunkCount.current} chunks
</Text>
{hasOverflow && scrollOffset > 0 && (
<Text color="green" dimColor>
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/components/ReportView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,15 @@ export function ReportView({ reportId }: ReportViewProps) {
<Box marginTop={1} flexDirection="column">
<Text color="greenBright" bold>── Findings ──────────────────────────</Text>
{report.findings.map((f, i) => (
<Box key={i} paddingLeft={1} flexDirection="column">
<Box key={i} paddingLeft={1} flexDirection="column" marginBottom={1}>
<Text color={f.severity === "critical" || f.severity === "high" ? "redBright" : "yellow"}>
{f.severity === "critical" || f.severity === "high" ? "✘" : "⚠"} [{f.severity}] {f.title}
</Text>
<Text color="green" dimColor> {f.description.slice(0, 120)}</Text>
<Text color="green" dimColor wrap="wrap"> What: {f.description}</Text>
{f.url && <Text color="cyan" dimColor> URL: {f.url}</Text>}
{f.steps && f.steps.length > 0 && (
<Text color="green" dimColor> Steps: {f.steps.slice(0, 3).join(" → ")}{f.steps.length > 3 ? " …" : ""}</Text>
)}
</Box>
))}
</Box>
Expand Down
22 changes: 22 additions & 0 deletions packages/cli/src/components/RunCommand.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { useState, useEffect } from "react";
import { Box, Text, useApp } from "ink";
import { join } from "node:path";
import { existsSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { exec } from "node:child_process";
import { Header } from "./Header.js";
import { StatusBar } from "./StatusBar.js";
import { TestProgress } from "./TestProgress.js";
Expand Down Expand Up @@ -165,6 +169,24 @@ export function RunCommand({ options }: RunCommandProps) {
<Text color="green" dimColor>
{report.summary.duration}ms · .getwired/reports/{report.id}/{report.id}.json
</Text>
{(() => {
const htmlPath = join(process.cwd(), ".getwired", "reports", report.id, "report.html");
const htmlExists = existsSync(htmlPath);
if (htmlExists) {
const fileUrl = pathToFileURL(htmlPath).href;
return (
<Box flexDirection="column">
<Text color="greenBright" bold>
📄 Report: <Text color="cyan">{fileUrl}</Text>
</Text>
<Text color="green" dimColor>
↑ Click the link above to open in your browser
</Text>
</Box>
);
}
return null;
})()}
{report.execution?.screenshots ? (
<Text color="green" dimColor>
Screenshots: .getwired/reports/{report.id}/screenshots/
Expand Down
Loading
Loading