Fix react-doctor warnings: remove dead code, fix keys and accessibility (92→99)

Delete unused files (run-logs.tsx, welcome/), remove unused mock data
and functions from data modules, un-export internal types, replace array
index keys with stable identifiers, fix label/htmlFor association, add
role+onKeyDown to click-intercepting div, and convert anchor to button
in settings sidebar nav.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-06 09:40:20 -05:00
parent a637a90ec6
commit 3a2e91a123
20 changed files with 33 additions and 1125 deletions

View file

@ -30,7 +30,7 @@ async function signToken(sub?: string): Promise<string> {
.sign(key);
}
export interface ApiOptions {
interface ApiOptions {
init?: RequestInit;
request?: Request;
}

View file

@ -1,13 +1,13 @@
export type SmoothnessRating = "effortless" | "smooth" | "bumpy" | "struggled" | "failed";
export type LearningCategory = "repo" | "code" | "workflow" | "tool";
type LearningCategory = "repo" | "code" | "workflow" | "tool";
export interface Learning {
category: LearningCategory;
text: string;
}
export type FrictionKind = "retry" | "timeout" | "wrong_approach" | "tool_failure" | "ambiguity";
type FrictionKind = "retry" | "timeout" | "wrong_approach" | "tool_failure" | "ambiguity";
export interface FrictionPoint {
kind: FrictionKind;
@ -15,7 +15,7 @@ export interface FrictionPoint {
stage_id?: string;
}
export type OpenItemKind = "tech_debt" | "follow_up" | "investigation" | "test_gap";
type OpenItemKind = "tech_debt" | "follow_up" | "investigation" | "test_gap";
export interface OpenItem {
kind: OpenItemKind;
@ -88,340 +88,6 @@ export const openItemKindConfig: Record<OpenItemKind, { label: string; text: str
test_gap: { label: "Test Gap", text: "text-coral" },
};
const mockRetros: Retro[] = [
{
run_id: "run-1",
workflow_name: "implement",
goal: "Add rate limiting to auth endpoints",
timestamp: "2026-02-28T14:32:00Z",
smoothness: "smooth",
intent: "Implement token-bucket rate limiting on /auth/login and /auth/register to prevent brute-force attacks.",
outcome: "Rate limiter deployed with configurable per-IP limits. Integration tests added. Redis-backed counter with sliding window.",
stages: [
{
stage_id: "detect-drift",
stage_label: "Detect Drift",
status: "completed",
duration_ms: 72_000,
retries: 0,
cost: 0.48,
files_touched: ["src/middleware/rate-limit.ts"],
},
{
stage_id: "propose-changes",
stage_label: "Propose Changes",
status: "completed",
duration_ms: 154_000,
retries: 0,
cost: 1.12,
files_touched: ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts"],
},
{
stage_id: "review-changes",
stage_label: "Review Changes",
status: "completed",
duration_ms: 45_000,
retries: 0,
cost: 0.31,
files_touched: [],
},
{
stage_id: "apply-changes",
stage_label: "Apply Changes",
status: "completed",
duration_ms: 118_000,
retries: 0,
cost: 0.87,
files_touched: ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"],
},
],
stats: {
total_duration_ms: 389_000,
total_cost: 2.78,
total_retries: 0,
files_touched: ["src/middleware/rate-limit.ts", "src/routes/auth.ts", "src/config.ts", "tests/rate-limit.test.ts"],
stages_completed: 4,
stages_failed: 0,
},
learnings: [
{ category: "repo", text: "Redis client is initialized lazily in src/infra/redis.ts -- reuse existing connection pool." },
{ category: "code", text: "Auth middleware chain order matters: rate-limit must run before JWT validation." },
],
friction_points: [],
open_items: [
{ kind: "follow_up", description: "Add rate-limit headers (X-RateLimit-Remaining) to response." },
],
},
{
run_id: "run-2",
workflow_name: "implement",
goal: "Migrate to React Router v7",
timestamp: "2026-02-28T10:15:00Z",
smoothness: "bumpy",
intent: "Upgrade react-router from v6 to v7, updating all route definitions and loader/action patterns to the new API.",
outcome: "Migration completed but required 3 retries in the apply stage due to breaking changes in nested route handling. All routes now use the v7 data API.",
stages: [
{
stage_id: "detect-drift",
stage_label: "Detect Drift",
status: "completed",
duration_ms: 95_000,
retries: 0,
cost: 0.62,
files_touched: ["package.json"],
},
{
stage_id: "propose-changes",
stage_label: "Propose Changes",
status: "completed",
duration_ms: 312_000,
retries: 1,
cost: 2.45,
notes: "First proposal missed nested outlet patterns. Retry produced correct migration.",
files_touched: ["src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx"],
},
{
stage_id: "review-changes",
stage_label: "Review Changes",
status: "completed",
duration_ms: 88_000,
retries: 0,
cost: 0.54,
files_touched: [],
},
{
stage_id: "apply-changes",
stage_label: "Apply Changes",
status: "completed",
duration_ms: 480_000,
retries: 3,
cost: 3.21,
notes: "Type errors in nested layouts required multiple correction passes.",
files_touched: ["src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx", "src/routes/profile.tsx", "tests/routes.test.tsx"],
},
],
stats: {
total_duration_ms: 975_000,
total_cost: 6.82,
total_retries: 4,
files_touched: ["package.json", "src/routes.ts", "src/app.tsx", "src/routes/dashboard.tsx", "src/routes/settings.tsx", "src/routes/profile.tsx", "tests/routes.test.tsx"],
stages_completed: 4,
stages_failed: 0,
},
learnings: [
{ category: "workflow", text: "Framework migration tasks benefit from running type-check after each stage, not just at the end." },
{ category: "code", text: "React Router v7 outlets require explicit type annotations for loader data in nested routes." },
{ category: "tool", text: "The codemod tool missed JSX spread patterns -- manual fixup was needed." },
],
friction_points: [
{ kind: "retry", description: "Nested route outlet types were incorrect on first 3 attempts.", stage_id: "apply-changes" },
{ kind: "wrong_approach", description: "Initially tried to keep v6 compat layer, which created more issues than a clean migration.", stage_id: "propose-changes" },
],
open_items: [
{ kind: "tech_debt", description: "Leftover v6 compat shims in src/utils/router-compat.ts should be deleted." },
{ kind: "test_gap", description: "No E2E coverage for the new nested layout error boundaries." },
],
},
{
run_id: "run-6",
workflow_name: "implement",
goal: "Add dark mode toggle",
timestamp: "2026-02-27T16:45:00Z",
smoothness: "effortless",
intent: "Add a theme toggle component to the dashboard header with system/light/dark options, persisting preference to localStorage.",
outcome: "Dark mode toggle shipped with smooth CSS transitions. All existing components already used CSS variables, so no style refactoring was needed.",
stages: [
{
stage_id: "detect-drift",
stage_label: "Detect Drift",
status: "completed",
duration_ms: 42_000,
retries: 0,
cost: 0.28,
files_touched: [],
},
{
stage_id: "propose-changes",
stage_label: "Propose Changes",
status: "completed",
duration_ms: 98_000,
retries: 0,
cost: 0.71,
files_touched: ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts"],
},
{
stage_id: "apply-changes",
stage_label: "Apply Changes",
status: "completed",
duration_ms: 76_000,
retries: 0,
cost: 0.52,
files_touched: ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts", "src/layouts/Header.tsx"],
},
],
stats: {
total_duration_ms: 216_000,
total_cost: 1.51,
total_retries: 0,
files_touched: ["src/components/ThemeToggle.tsx", "src/hooks/useTheme.ts", "src/layouts/Header.tsx"],
stages_completed: 3,
stages_failed: 0,
},
learnings: [
{ category: "repo", text: "CSS variables are defined in src/styles/tokens.css and already support dark values." },
],
friction_points: [],
open_items: [],
},
{
run_id: "run-3",
workflow_name: "fix_build",
goal: "Fix config parsing for nested values",
timestamp: "2026-02-27T09:20:00Z",
smoothness: "struggled",
intent: "Fix TOML config parser to handle deeply nested table arrays, which was causing silent data loss on certain pipeline configs.",
outcome: "Root cause identified as incorrect recursion depth limit in the TOML walker. Fix applied but exposed a second bug in default value merging that required additional changes.",
stages: [
{
stage_id: "investigate",
stage_label: "Investigate",
status: "completed",
duration_ms: 340_000,
retries: 2,
cost: 1.85,
notes: "First investigation looked at wrong parser path. Second attempt found the actual recursion limit.",
files_touched: ["src/config/parser.ts", "src/config/defaults.ts"],
},
{
stage_id: "propose-fix",
stage_label: "Propose Fix",
status: "completed",
duration_ms: 210_000,
retries: 1,
cost: 1.42,
files_touched: ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts"],
},
{
stage_id: "apply-fix",
stage_label: "Apply Fix",
status: "completed",
duration_ms: 185_000,
retries: 1,
cost: 1.15,
failure_reason: "Initial fix broke the default value merging path. Required a second pass.",
files_touched: ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts", "tests/config-parser.test.ts"],
},
{
stage_id: "verify",
stage_label: "Verify",
status: "completed",
duration_ms: 95_000,
retries: 0,
cost: 0.55,
files_touched: [],
},
],
stats: {
total_duration_ms: 830_000,
total_cost: 4.97,
total_retries: 4,
files_touched: ["src/config/parser.ts", "src/config/defaults.ts", "src/config/merge.ts", "tests/config-parser.test.ts"],
stages_completed: 4,
stages_failed: 0,
},
learnings: [
{ category: "code", text: "TOML walker in parser.ts has a hardcoded depth limit of 8 -- needs to be configurable." },
{ category: "code", text: "Default merging in merge.ts uses shallow spread, which silently drops nested keys." },
{ category: "workflow", text: "Bug fix pipelines should include a regression test stage before verification." },
],
friction_points: [
{ kind: "wrong_approach", description: "Initial investigation focused on the YAML compatibility layer instead of the TOML parser.", stage_id: "investigate" },
{ kind: "retry", description: "Fix introduced a regression in default value merging that required rework.", stage_id: "apply-fix" },
{ kind: "ambiguity", description: "Config schema docs were outdated, making it unclear which nesting depth was intended." },
],
open_items: [
{ kind: "tech_debt", description: "Remove the hardcoded depth limit in src/config/parser.ts and make it configurable." },
{ kind: "investigation", description: "Audit other parsers for similar shallow-spread bugs in merging logic." },
{ kind: "test_gap", description: "No tests for configs nested deeper than 4 levels." },
],
},
{
run_id: "run-8",
workflow_name: "implement",
goal: "Implement webhook retry logic",
timestamp: "2026-02-26T11:00:00Z",
smoothness: "smooth",
intent: "Add exponential backoff retry logic for failed webhook deliveries with configurable max attempts and dead-letter queue.",
outcome: "Webhook retry system implemented with exponential backoff (base 2s, max 5 retries). Failed deliveries route to SQS dead-letter queue. Dashboard shows retry status.",
stages: [
{
stage_id: "detect-drift",
stage_label: "Detect Drift",
status: "completed",
duration_ms: 55_000,
retries: 0,
cost: 0.35,
files_touched: [],
},
{
stage_id: "propose-changes",
stage_label: "Propose Changes",
status: "completed",
duration_ms: 178_000,
retries: 0,
cost: 1.28,
files_touched: ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts"],
},
{
stage_id: "review-changes",
stage_label: "Review Changes",
status: "completed",
duration_ms: 62_000,
retries: 0,
cost: 0.41,
files_touched: [],
},
{
stage_id: "apply-changes",
stage_label: "Apply Changes",
status: "completed",
duration_ms: 145_000,
retries: 1,
cost: 1.05,
notes: "Minor type fix needed on retry delay calculation.",
files_touched: ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts", "tests/webhook-retry.test.ts"],
},
],
stats: {
total_duration_ms: 440_000,
total_cost: 3.09,
total_retries: 1,
files_touched: ["src/webhooks/retry.ts", "src/webhooks/dlq.ts", "src/webhooks/dispatcher.ts", "tests/webhook-retry.test.ts"],
stages_completed: 4,
stages_failed: 0,
},
learnings: [
{ category: "repo", text: "SQS client wrapper is in src/infra/sqs.ts with pre-configured DLQ ARNs per environment." },
{ category: "code", text: "Webhook dispatcher already had a hook point for retry logic via the onFailure callback." },
],
friction_points: [
{ kind: "retry", description: "Retry delay formula had an off-by-one in the exponent calculation.", stage_id: "apply-changes" },
],
open_items: [
{ kind: "follow_up", description: "Add webhook retry metrics to the Grafana dashboard." },
{ kind: "follow_up", description: "Document the DLQ reprocessing procedure in the runbook." },
],
},
];
export function allRetros(): Retro[] {
return mockRetros;
}
export function findRetro(runId: string): Retro | undefined {
return mockRetros.find((r) => r.run_id === runId);
}
function formatDuration(ms: number): string {
const totalSeconds = Math.floor(ms / 1000);
const minutes = Math.floor(totalSeconds / 60);

View file

@ -39,226 +39,6 @@ export function deriveCiStatus(checks: CheckRun[]): CiStatus {
return "passing";
}
export const columns: {
id: ColumnStatus;
name: string;
accent: string;
iconColor: string;
iconType: "branch" | "pr";
actions: string[];
items: RunItem[];
}[] = [
{
id: "working",
name: "Working",
accent: "bg-teal-500",
iconColor: "text-teal-500",
iconType: "branch",
actions: ["Watch", "Steer"],
items: [
{
id: "run-1",
repo: "api-server",
title: "Add rate limiting to auth endpoints",
workflow: "implement",
resources: "4 CPU / 8 GB",
elapsed: "7m",
sandboxId: "sb-a1b2c3d4",
},
{
id: "run-2",
repo: "web-dashboard",
title: "Migrate to React Router v7",
workflow: "implement",
resources: "8 CPU / 16 GB",
elapsed: "2h 15m",
sandboxId: "sb-e5f6g7h8",
},
{
id: "run-3",
repo: "cli-tools",
title: "Fix config parsing for nested values",
workflow: "fix_build",
resources: "2 CPU / 4 GB",
elapsed: "45m",
sandboxId: "sb-i9j0k1l2",
},
],
},
{
id: "pending",
name: "Pending",
accent: "bg-amber",
iconColor: "text-amber",
iconType: "branch",
actions: ["Answer Question"],
items: [
{
id: "run-4",
repo: "api-server",
title: "Update OpenAPI spec for v3",
workflow: "expand",
additions: 567,
deletions: 234,
elapsed: "1h 12m",
question: "Accept or push for another round?",
sandboxId: "sb-q7r8s9t0",
},
{
id: "run-5",
repo: "shared-types",
title: "Add pipeline event types",
workflow: "implement",
additions: 145,
deletions: 23,
elapsed: "28m",
question: "Proceed from investigation to fix?",
sandboxId: "sb-u1v2w3x4",
},
],
},
{
id: "review",
name: "Verify",
accent: "bg-mint",
iconColor: "text-mint",
iconType: "pr",
actions: ["Resolve"],
items: [
{
id: "run-6",
repo: "web-dashboard",
title: "Add dark mode toggle",
workflow: "implement",
number: 889,
additions: 234,
deletions: 67,
sandboxId: "sb-m3n4o5p6",
checks: [
{ name: "lint", status: "success", duration: "23s" },
{ name: "typecheck", status: "success", duration: "1m 12s" },
{ name: "unit-tests", status: "success", duration: "2m 34s" },
{ name: "integration-tests", status: "failure", duration: "4m 56s" },
{ name: "e2e / chrome", status: "failure", duration: "3m 2s" },
{ name: "build", status: "success", duration: "1m 45s" },
{ name: "coverage", status: "skipped" },
],
elapsed: "35m",
comments: 4,
},
{
id: "run-7",
repo: "infrastructure",
title: "Terraform module for Redis cluster",
workflow: "implement",
number: 156,
additions: 412,
deletions: 0,
checks: [
{ name: "lint", status: "success", duration: "18s" },
{ name: "typecheck", status: "success", duration: "56s" },
{ name: "unit-tests", status: "pending" },
{ name: "integration-tests", status: "queued" },
{ name: "build", status: "pending" },
],
elapsed: "12m",
sandboxId: "sb-y5z6a7b8",
actionDisabled: true,
comments: 1,
},
],
},
{
id: "merge",
name: "Merge",
accent: "bg-teal-300",
iconColor: "text-teal-300",
iconType: "pr",
actions: ["Merge"],
items: [
{
id: "run-8",
repo: "api-server",
title: "Implement webhook retry logic",
workflow: "implement",
number: 1249,
additions: 189,
deletions: 45,
checks: [
{ name: "lint", status: "success", duration: "21s" },
{ name: "typecheck", status: "success", duration: "1m 8s" },
{ name: "unit-tests", status: "success", duration: "3m 12s" },
{ name: "integration-tests", status: "success", duration: "5m 34s" },
{ name: "e2e / chrome", status: "success", duration: "4m 22s" },
{ name: "e2e / firefox", status: "success", duration: "4m 45s" },
{ name: "build", status: "success", duration: "2m 1s" },
{ name: "deploy-preview", status: "success", duration: "1m 33s" },
{ name: "security-scan", status: "skipped" },
{ name: "performance", status: "success", duration: "2m 18s" },
{ name: "bundle-size", status: "success", duration: "34s" },
{ name: "accessibility", status: "success", duration: "1m 12s" },
],
sandboxId: "sb-c9d0e1f2",
elapsed: "3d",
elapsedWarning: true,
comments: 7,
},
{
id: "run-9",
repo: "cli-tools",
title: "Add --verbose flag to run command",
workflow: "expand",
number: 430,
additions: 56,
deletions: 12,
checks: [
{ name: "lint", status: "success", duration: "15s" },
{ name: "typecheck", status: "success", duration: "48s" },
{ name: "unit-tests", status: "success", duration: "1m 56s" },
{ name: "build", status: "success", duration: "1m 22s" },
{ name: "coverage", status: "success", duration: "2m 4s" },
{ name: "bundle-size", status: "skipped" },
],
sandboxId: "sb-g3h4i5j6",
elapsed: "1h 5m",
comments: 2,
},
{
id: "run-10",
repo: "shared-types",
title: "Export utility type helpers",
workflow: "sync_drift",
number: 76,
additions: 34,
deletions: 8,
checks: [
{ name: "lint", status: "success", duration: "12s" },
{ name: "typecheck", status: "success", duration: "34s" },
{ name: "unit-tests", status: "success", duration: "1m 15s" },
{ name: "build", status: "success", duration: "58s" },
],
sandboxId: "sb-k7l8m9n0",
elapsed: "48m",
comments: 0,
},
],
},
];
export function allRunsFlat(): RunWithStatus[] {
return [...columns].reverse().flatMap((col) =>
col.items.map((item) => ({
...item,
status: col.id,
statusLabel: col.name,
})),
);
}
export function findRun(id: string): RunWithStatus | undefined {
return allRunsFlat().find((r) => r.id === id);
}
export const statusColors: Record<ColumnStatus, { dot: string; text: string }> = {
working: { dot: "bg-teal-500", text: "text-teal-500" },
pending: { dot: "bg-amber", text: "text-amber" },

View file

@ -53,96 +53,6 @@ export const typeConfig = {
{ label: string; color: string; bg: string }
>;
export const verificationCategories: VerificationCategory[] = [
{
name: "Traceability",
question: "Do we understand what this change is and why we're making it?",
status: "pass",
criteria: [
{ name: "Motivation", description: "Origin of proposal identified", type: "ai", status: "pass" },
{ name: "Specifications", description: "Requirements written down", type: "ai", status: "pass" },
{ name: "Documentation", description: "Developer and user docs added", type: "ai", status: "pass" },
{ name: "Minimization", description: "No extraneous changes", type: "ai", status: "pass" },
],
},
{
name: "Readability",
question: "Can a human or agent quickly read this and understand what it does?",
status: "pass",
criteria: [
{ name: "Formatting", description: "Code layout matches standard", type: "automated", status: "pass" },
{ name: "Linting", description: "Linter issues resolved", type: "automated", status: "pass" },
{ name: "Style", description: "House style applied", type: "ai", status: "pass" },
],
},
{
name: "Reliability",
question: "Will this behave correctly and safely under real-world conditions and failures?",
status: "pass",
criteria: [
{ name: "Completeness", description: "Implementation covers requirements", type: "ai", status: "pass" },
{ name: "Defects", description: "Potential or likely bugs remediated", type: "ai-analysis", status: "pass" },
{ name: "Performance", description: "Hot path impact identified", type: "ai", status: "pass" },
],
},
{
name: "Code Coverage",
question: "Do we have trustworthy, automated evidence that it works and won't regress?",
status: "fail",
criteria: [
{ name: "Test Coverage", description: "Production code exercised by unit tests", type: "analysis", status: "pass" },
{ name: "Test Quality", description: "Tests are robust and clear", type: "ai", status: "fail" },
{ name: "E2E Coverage", description: "Browser automation exercises UX", type: "analysis", status: "na" },
],
},
{
name: "Maintainability",
question: "Will this be easy to modify or extend later without creating new risk?",
status: "pass",
criteria: [
{ name: "Architecture", description: "Layering and dependency graph meets design", type: "analysis", status: "pass" },
{ name: "Interfaces", description: "", type: null, status: "pass" },
{ name: "Duplication", description: "Similar and identical code blocks identified", type: "analysis", status: "pass" },
{ name: "Simplicity", description: "Extra review for reducing complexity", type: "ai", status: "pass" },
{ name: "Dead Code", description: "Unexecuted code and dependencies removed", type: "analysis", status: "pass" },
],
},
{
name: "Security",
question: "Does this preserve or improve our security posture and avoid vulnerabilities?",
status: "pass",
criteria: [
{ name: "Vulnerabilities", description: "Security issues are remediated", type: "ai-analysis", status: "pass" },
{ name: "IaC Scanning", description: "", type: null, status: "pass" },
{ name: "Dependency Alerts", description: "Known CVEs are patched", type: "analysis", status: "pass" },
{ name: "Security Controls", description: "Organization standards applied", type: "ai", status: "pass" },
],
},
{
name: "Deployability",
question: "Is this changeset safe to ship to production immediately?",
status: "fail",
criteria: [
{ name: "Compatibility", description: "Breaking changes are avoided", type: "analysis", status: "pass" },
{ name: "Rollout / Rollback", description: "Known rollback plan if deploy fails", type: "ai", status: "fail" },
{ name: "Observability", description: "Logging, metrics, tracing instrumented", type: "ai", status: "fail" },
{ name: "Cost", description: "Tech ops costs estimated", type: "analysis", status: "pass" },
],
},
{
name: "Compliance",
question: "Does this meet our regulatory, contractual, and policy obligations?",
status: "pass",
criteria: [
{ name: "Change Control", description: "Separation of Duties policy met", type: "analysis", status: "pass" },
{ name: "AI Governance", description: "AI involvement was acceptable", type: "analysis", status: "pass" },
{ name: "Privacy", description: "PII is identified and handled to standards", type: "ai", status: "pass" },
{ name: "Accessibility", description: "Software meets accessibility requirements", type: "analysis", status: "pass" },
{ name: "Licensing", description: "Supply chain meets IP policy", type: "analysis", status: "pass" },
],
},
];
export type EvaluationResult = "pass" | "fail" | "skip";
export type VerificationMode = "active" | "evaluate" | "disabled";
@ -163,45 +73,6 @@ export const modeConfig = {
{ label: string; color: string; bg: string }
>;
export const criterionPerformance: Record<string, CriterionPerformance> = {
"Motivation": { f1: 0.87, passAt1: 0.82, mode: "active", evaluations: ["pass","pass","fail","pass","pass","pass","pass","fail","pass","pass"] },
"Specifications": { f1: 0.83, passAt1: 0.78, mode: "active", evaluations: ["pass","fail","pass","pass","pass","fail","pass","pass","pass","pass"] },
"Documentation": { f1: 0.79, passAt1: 0.74, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","fail","pass","pass","fail"] },
"Minimization": { f1: 0.72, passAt1: 0.68, mode: "evaluate", evaluations: ["pass","fail","pass","fail","pass","pass","fail","pass","pass","pass"] },
"Formatting": { f1: 0.99, passAt1: 0.98, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","pass"] },
"Linting": { f1: 0.98, passAt1: 0.97, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","fail","pass"] },
"Style": { f1: 0.81, passAt1: 0.76, mode: "active", evaluations: ["pass","fail","pass","pass","pass","pass","fail","pass","pass","pass"] },
"Completeness": { f1: 0.76, passAt1: 0.71, mode: "active", evaluations: ["pass","pass","fail","pass","fail","pass","pass","pass","fail","pass"] },
"Defects": { f1: 0.84, passAt1: 0.79, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","fail"] },
"Performance": { f1: 0.69, passAt1: 0.63, mode: "evaluate", evaluations: ["fail","pass","pass","fail","pass","fail","pass","pass","fail","pass"] },
"Test Coverage": { f1: 0.95, passAt1: 0.93, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","fail","pass","pass","pass"] },
"Test Quality": { f1: 0.71, passAt1: 0.65, mode: "evaluate", evaluations: ["pass","fail","fail","pass","pass","fail","pass","fail","pass","pass"] },
"E2E Coverage": { f1: 0.91, passAt1: 0.88, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","pass"] },
"Architecture": { f1: 0.88, passAt1: 0.84, mode: "active", evaluations: ["pass","pass","pass","pass","fail","pass","pass","pass","pass","pass"] },
"Interfaces": { f1: null, passAt1: null, mode: "disabled", evaluations: [] },
"Duplication": { f1: 0.96, passAt1: 0.94, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","fail","pass","pass"] },
"Simplicity": { f1: 0.74, passAt1: 0.69, mode: "active", evaluations: ["pass","fail","pass","pass","fail","pass","pass","fail","pass","pass"] },
"Dead Code": { f1: 0.93, passAt1: 0.90, mode: "active", evaluations: ["pass","pass","pass","pass","pass","fail","pass","pass","pass","pass"] },
"Vulnerabilities": { f1: 0.86, passAt1: 0.81, mode: "active", evaluations: ["pass","pass","fail","pass","pass","pass","pass","pass","fail","pass"] },
"IaC Scanning": { f1: null, passAt1: null, mode: "disabled", evaluations: [] },
"Dependency Alerts": { f1: 0.97, passAt1: 0.95, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","fail"] },
"Security Controls": { f1: 0.80, passAt1: 0.75, mode: "active", evaluations: ["pass","pass","fail","pass","pass","fail","pass","pass","pass","pass"] },
"Compatibility": { f1: 0.89, passAt1: 0.85, mode: "active", evaluations: ["pass","pass","pass","pass","fail","pass","pass","pass","pass","pass"] },
"Rollout / Rollback": { f1: 0.66, passAt1: 0.60, mode: "evaluate", evaluations: ["fail","pass","fail","pass","fail","pass","pass","fail","pass","fail"] },
"Observability": { f1: 0.73, passAt1: 0.67, mode: "evaluate", evaluations: ["pass","fail","pass","fail","pass","pass","fail","pass","fail","pass"] },
"Cost": { f1: 0.78, passAt1: 0.72, mode: "evaluate", evaluations: ["pass","pass","fail","pass","fail","pass","pass","fail","pass","pass"] },
"Change Control": { f1: 0.94, passAt1: 0.91, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","fail","pass"] },
"AI Governance": { f1: 0.85, passAt1: 0.80, mode: "active", evaluations: ["pass","pass","pass","fail","pass","pass","pass","pass","pass","pass"] },
"Privacy": { f1: 0.77, passAt1: 0.72, mode: "active", evaluations: ["pass","fail","pass","pass","pass","fail","pass","pass","pass","fail"] },
"Accessibility": { f1: 0.90, passAt1: 0.87, mode: "active", evaluations: ["pass","pass","pass","pass","pass","fail","pass","pass","pass","pass"] },
"Licensing": { f1: 0.96, passAt1: 0.93, mode: "active", evaluations: ["pass","pass","pass","pass","pass","pass","pass","pass","pass","pass"] },
};
export function getCategorySummary(categories: readonly VerificationCategory[]) {
const passing = categories.filter((c) => c.status === "pass").length;
return { passing, total: categories.length };
}
export function getCriteriaSummary(criteria: readonly Criterion[]) {
return {
passing: criteria.filter((c) => c.status === "pass").length,
@ -211,10 +82,6 @@ export function getCriteriaSummary(criteria: readonly Criterion[]) {
};
}
export function getAllCriteria(categories: readonly VerificationCategory[]) {
return categories.flatMap((c) => c.criteria);
}
export function slugify(name: string): string {
return name
.toLowerCase()
@ -222,261 +89,3 @@ export function slugify(name: string): string {
.replace(/(^-|-$)/g, "");
}
export function findCriterionBySlug(slug: string): {
criterion: Criterion;
category: VerificationCategory;
performance: CriterionPerformance;
} | null {
for (const category of verificationCategories) {
for (const criterion of category.criteria) {
if (slugify(criterion.name) === slug) {
const performance = criterionPerformance[criterion.name];
if (performance) {
return { criterion, category, performance };
}
}
}
}
return null;
}
export interface ControlDetail {
description: string;
checks: string[];
passExample: string;
failExample: string;
}
export const controlDetails: Record<string, ControlDetail> = {
"Motivation": {
description: "Verifies that every change traces back to a clear origin — whether a ticket, RFC, customer request, or incident. Without documented motivation, reviewers lack context for evaluating whether the change is appropriate.",
checks: ["PR body or linked issue explains why the change is needed", "Commit messages reference a ticket or context", "No orphaned changes without traceable origin"],
passExample: "PR links to JIRA-1234 and explains the user-facing pain point being resolved.",
failExample: "PR description is empty or says only 'fix stuff'.",
},
"Specifications": {
description: "Checks that functional and non-functional requirements are written down before implementation begins. Specifications prevent scope creep and ensure everyone agrees on what done looks like.",
checks: ["Acceptance criteria listed in the issue or PR", "Edge cases documented", "Non-functional requirements (performance, security) stated when relevant"],
passExample: "Issue includes acceptance criteria with three testable scenarios.",
failExample: "Issue body says 'implement the feature' with no acceptance criteria.",
},
"Documentation": {
description: "Ensures developer-facing and user-facing documentation is added or updated alongside code changes. Stale docs degrade team velocity and increase onboarding cost.",
checks: ["README or docs updated for new features", "API documentation reflects endpoint changes", "Inline comments for non-obvious logic"],
passExample: "New API endpoint has corresponding OpenAPI spec update and usage example in docs.",
failExample: "New CLI flag added with no mention in README or --help text.",
},
"Minimization": {
description: "Flags extraneous changes that inflate the diff — formatting-only edits, unrelated refactors, or drive-by fixes. Keeping PRs focused improves review quality and reduces revert risk.",
checks: ["No unrelated formatting or whitespace changes", "Refactors separated from feature work", "Each commit addresses a single concern"],
passExample: "PR touches only files directly related to the new caching layer.",
failExample: "PR adds a feature but also reformats 12 unrelated files.",
},
"Formatting": {
description: "Validates that code layout conforms to the project's formatting standard (e.g., Prettier, rustfmt). Automated formatting removes subjective style debates from code review.",
checks: ["All files pass the project formatter", "No manual formatting overrides without justification"],
passExample: "All changed files pass `prettier --check` and `rustfmt --check`.",
failExample: "Several files have inconsistent indentation that the formatter would fix.",
},
"Linting": {
description: "Confirms that static analysis findings are resolved. Linter warnings left unaddressed accumulate into tech debt and mask real issues.",
checks: ["No new linter warnings introduced", "Existing warnings not suppressed without explanation", "Lint config not weakened"],
passExample: "ESLint and Clippy pass with zero warnings on changed files.",
failExample: "New `// eslint-disable-next-line` added to suppress a legitimate warning.",
},
"Style": {
description: "Evaluates whether the code follows the team's house style conventions beyond what automated formatters catch — naming, file organization, import ordering, and idiomatic patterns.",
checks: ["Naming conventions followed (camelCase, snake_case as appropriate)", "Import ordering matches project convention", "Idiomatic patterns used for the language"],
passExample: "New TypeScript module uses camelCase variables, groups imports by source, and uses `Map` instead of plain objects for lookups.",
failExample: "Mix of camelCase and snake_case in the same module with random import ordering.",
},
"Completeness": {
description: "Checks that the implementation fully covers the specified requirements. Partial implementations ship broken experiences and create follow-up tickets that could have been avoided.",
checks: ["All acceptance criteria addressed", "Edge cases handled", "Error states implemented"],
passExample: "Feature handles all three specified user roles with appropriate permissions.",
failExample: "Only the happy path is implemented; error and empty states are missing.",
},
"Defects": {
description: "Identifies potential or likely bugs through static analysis and AI review. Catching defects before merge is orders of magnitude cheaper than finding them in production.",
checks: ["No off-by-one errors in loops or slices", "Null/undefined handled at boundaries", "Race conditions considered in async code"],
passExample: "API handler validates input, handles missing fields gracefully, and returns appropriate HTTP status codes.",
failExample: "Array index accessed without bounds check; crashes on empty input.",
},
"Performance": {
description: "Assesses whether the change impacts hot paths or introduces algorithmic regressions. Performance problems that ship to production are expensive to diagnose and fix.",
checks: ["No N+1 queries introduced", "Large collections not processed synchronously", "Caching considered for repeated expensive operations"],
passExample: "Database query uses a JOIN instead of N separate queries for related records.",
failExample: "Loop makes a separate HTTP call for each item in a 1000-element list.",
},
"Test Coverage": {
description: "Measures whether production code is exercised by automated tests. Coverage gaps mean regressions can ship undetected.",
checks: ["New code has corresponding unit tests", "Coverage does not decrease", "Critical paths have integration tests"],
passExample: "New service method has 6 unit tests covering happy path, error cases, and edge cases.",
failExample: "New 200-line module has zero test files.",
},
"Test Quality": {
description: "Evaluates whether tests are robust, readable, and actually verify behavior rather than implementation details. Low-quality tests give false confidence.",
checks: ["Tests verify behavior, not implementation", "Assertions are specific and meaningful", "Tests are independent and deterministic"],
passExample: "Tests assert on API response shape and status codes, not on internal method call counts.",
failExample: "Tests mock every dependency and only verify that mocks were called.",
},
"E2E Coverage": {
description: "Checks that user-facing workflows are exercised by end-to-end browser automation. E2E tests catch integration issues that unit tests miss.",
checks: ["Critical user flows have Playwright/Cypress tests", "E2E tests run in CI", "No flaky E2E tests introduced"],
passExample: "New checkout flow has a Playwright test that completes a purchase end-to-end.",
failExample: "New multi-step wizard has no browser automation tests.",
},
"Architecture": {
description: "Validates that layering and dependency directions conform to the project's architectural design. Architectural violations compound over time and make systems harder to evolve.",
checks: ["Dependencies point inward (domain doesn't depend on infra)", "No circular dependencies introduced", "Module boundaries respected"],
passExample: "New repository implementation depends on domain interfaces, not the other way around.",
failExample: "Domain model imports directly from the HTTP framework package.",
},
"Interfaces": {
description: "Reviews public API surfaces for clarity, consistency, and backward compatibility. Interfaces are contracts — once published, they're expensive to change.",
checks: ["Public API types are well-defined", "Breaking changes documented", "Consistent naming across endpoints"],
passExample: "New endpoint follows existing naming and error format conventions.",
failExample: "New endpoint uses different error format than all other endpoints.",
},
"Duplication": {
description: "Detects similar or identical code blocks that could be consolidated. Duplication increases maintenance burden and creates inconsistency risk.",
checks: ["No copy-pasted logic across files", "Shared utilities used for common patterns", "Similar test setup consolidated"],
passExample: "Date formatting logic extracted into a shared utility used by 4 components.",
failExample: "Same 15-line validation function copy-pasted into three different handlers.",
},
"Simplicity": {
description: "Flags unnecessarily complex code that could be simplified without changing behavior. Simpler code is easier to review, debug, and extend.",
checks: ["No premature abstractions", "Control flow is straightforward", "Functions are focused and short"],
passExample: "Conditional logic uses early returns instead of deeply nested if-else chains.",
failExample: "Three-level generic abstraction for a function called in one place.",
},
"Dead Code": {
description: "Identifies unexecuted code paths and unused dependencies. Dead code misleads readers and bloats bundles.",
checks: ["No unreachable code paths", "Unused imports and variables removed", "Deprecated functions removed if no longer called"],
passExample: "Old feature flag and its associated code paths removed after rollout completed.",
failExample: "Commented-out function left in file 'in case we need it later'.",
},
"Vulnerabilities": {
description: "Scans for known security vulnerabilities using both AI analysis and static scanning tools. Shipping known vulnerabilities exposes users and the organization to risk.",
checks: ["No SQL injection or XSS vectors", "User input sanitized at boundaries", "Authentication/authorization checks present"],
passExample: "User input passed through parameterized queries; HTML output escaped.",
failExample: "Raw SQL string concatenation with user-supplied values.",
},
"IaC Scanning": {
description: "Validates infrastructure-as-code definitions against security best practices. Misconfigured infrastructure is a leading cause of data breaches.",
checks: ["No publicly accessible storage buckets", "Encryption at rest enabled", "Least-privilege IAM policies"],
passExample: "Terraform module creates S3 bucket with encryption, versioning, and private ACL.",
failExample: "CloudFormation template creates an RDS instance with no encryption and public accessibility.",
},
"Dependency Alerts": {
description: "Checks that third-party dependencies are free from known CVEs. Vulnerable dependencies are an easy attack vector that automated tools can detect.",
checks: ["No dependencies with known critical CVEs", "Lock file updated to patched versions", "Unused dependencies removed"],
passExample: "Dependabot alert resolved by updating lodash from 4.17.20 to 4.17.21.",
failExample: "Package.json pins a version of axios with a known SSRF vulnerability.",
},
"Security Controls": {
description: "Verifies that organization-specific security standards are applied — rate limiting, audit logging, CORS policies, and secret management.",
checks: ["Secrets not hardcoded in source", "Rate limiting on public endpoints", "Audit logging for sensitive operations"],
passExample: "API key loaded from environment variable; rate limiter configured on login endpoint.",
failExample: "AWS credentials committed in a config file.",
},
"Compatibility": {
description: "Detects breaking changes in APIs, database schemas, or wire formats that could disrupt consumers. Breaking changes require coordination that surprises prevent.",
checks: ["No removed or renamed public API fields", "Database migrations are backward-compatible", "Wire format changes are additive"],
passExample: "New field added to API response; no existing fields removed or renamed.",
failExample: "Column renamed in migration while old code is still deployed.",
},
"Rollout / Rollback": {
description: "Confirms that the change has a clear deployment plan and can be safely rolled back if issues arise. Every production deploy should be reversible.",
checks: ["Feature flag available for gradual rollout", "Database migration is reversible", "Rollback procedure documented"],
passExample: "Feature behind a LaunchDarkly flag with 10% initial rollout and documented rollback steps.",
failExample: "Irreversible database migration with no rollback plan.",
},
"Observability": {
description: "Ensures that logging, metrics, and tracing are instrumented for new code paths. Without observability, production issues are invisible until users report them.",
checks: ["Structured logging for new operations", "Metrics emitted for key business events", "Distributed tracing propagated"],
passExample: "New payment endpoint logs transaction IDs, emits latency metrics, and propagates trace context.",
failExample: "New background job has no logging or metrics; failures are silent.",
},
"Cost": {
description: "Estimates the infrastructure and operational cost impact of the change. Unchecked cost growth erodes margins and can cause budget surprises.",
checks: ["New infrastructure resources sized appropriately", "No unbounded resource consumption", "Cost estimate provided for significant changes"],
passExample: "New Lambda function has memory limit set and estimated monthly cost noted in PR.",
failExample: "New service provisions a db.r5.4xlarge for a table with 100 rows.",
},
"Change Control": {
description: "Validates that separation-of-duties policies are met — the author is not the sole reviewer, approvals are obtained, and the change went through the proper process.",
checks: ["PR has at least one approval from non-author", "Required reviewers have signed off", "No self-merging without policy exception"],
passExample: "PR approved by two team members before merge; CI checks all green.",
failExample: "Author approved and merged their own PR with no other reviewers.",
},
"AI Governance": {
description: "Checks that AI-generated or AI-assisted code meets the organization's governance requirements — attribution, review depth, and acceptable use.",
checks: ["AI-generated code clearly attributed", "Human review of AI suggestions documented", "AI usage within acceptable-use policy"],
passExample: "PR notes that implementation was AI-assisted; human reviewer verified logic and tests.",
failExample: "Entire module generated by AI with no human review or attribution.",
},
"Privacy": {
description: "Ensures that personally identifiable information (PII) is identified, classified, and handled according to privacy standards (GDPR, CCPA).",
checks: ["PII fields identified and documented", "Data retention policies applied", "Consent mechanisms in place for data collection"],
passExample: "New user profile endpoint masks email in logs and respects data deletion requests.",
failExample: "User email addresses logged in plaintext to application logs.",
},
"Accessibility": {
description: "Verifies that UI changes meet accessibility requirements (WCAG 2.1 AA). Inaccessible software excludes users and creates legal risk.",
checks: ["Semantic HTML elements used", "ARIA labels present on interactive elements", "Color contrast meets WCAG AA standards"],
passExample: "New modal uses <dialog>, has aria-labelledby, and focus is trapped within.",
failExample: "Custom dropdown built with <div> elements, no keyboard navigation, no ARIA roles.",
},
"Licensing": {
description: "Ensures that all third-party dependencies comply with the organization's intellectual property policy. License violations can have severe legal consequences.",
checks: ["No GPL-licensed dependencies in proprietary code", "License file present for new dependencies", "Supply chain attestation where required"],
passExample: "New dependency uses MIT license; added to approved dependency list.",
failExample: "AGPL-licensed library added to a closed-source commercial product.",
},
};
export interface RecentControlResult {
runId: string;
runTitle: string;
workflow: string;
result: VerificationStatus;
timestamp: string;
}
export const recentControlResults: Record<string, RecentControlResult[]> = {
"Motivation": [
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "fail", timestamp: "1d ago" },
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
],
"Documentation": [
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "fail", timestamp: "5h ago" },
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "pass", timestamp: "1d ago" },
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "fail", timestamp: "3d ago" },
],
"Rollout / Rollback": [
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "fail", timestamp: "2h ago" },
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "fail", timestamp: "1d ago" },
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "fail", timestamp: "2d ago" },
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
],
};
// Default recent results for controls without specific data
const defaultRecentResults: RecentControlResult[] = [
{ runId: "run-047", runTitle: "PR #312 — Add OAuth2 PKCE flow", workflow: "code_review", result: "pass", timestamp: "2h ago" },
{ runId: "run-046", runTitle: "PR #311 — Update rate limiter config", workflow: "code_review", result: "pass", timestamp: "5h ago" },
{ runId: "run-044", runTitle: "PR #309 — Migrate to pnpm", workflow: "code_review", result: "pass", timestamp: "1d ago" },
{ runId: "run-042", runTitle: "PR #307 — Fix session timeout", workflow: "fix_build", result: "pass", timestamp: "2d ago" },
{ runId: "run-040", runTitle: "PR #305 — Add webhook retries", workflow: "code_review", result: "pass", timestamp: "3d ago" },
];
export function getRecentResults(criterionName: string): RecentControlResult[] {
return recentControlResults[criterionName] ?? defaultRecentResults;
}

View file

@ -28,7 +28,7 @@ interface WebConfig {
auth: AuthConfig;
}
export interface AppConfig {
interface AppConfig {
web: WebConfig;
api: ApiConfig;
git: GitConfig;

View file

@ -16,13 +16,3 @@ export function isGitHubAppConfigured(): boolean {
return getAppConfig().git.client_id !== null;
}
export function getGitHubAppPrivateKey(): string {
const raw = process.env.GITHUB_APP_PRIVATE_KEY;
if (!raw) {
throw new Error("GITHUB_APP_PRIVATE_KEY is not configured");
}
if (raw.startsWith("-----BEGIN")) {
return raw;
}
return Buffer.from(raw, "base64").toString("utf-8");
}

View file

@ -67,4 +67,3 @@ export function getGraphTheme(theme: Theme): GraphTheme {
return theme === "dark" ? dark : light;
}
export type { GraphTheme };

View file

@ -273,9 +273,9 @@ function ResultTable({ result }: { result: QueryResult }) {
</tr>
</thead>
<tbody>
{result.rows.map((row, i) => (
{result.rows.map((row) => (
<tr
key={i}
key={String(row[result.columns[0]])}
className="border-b border-line transition-colors hover:bg-overlay"
>
{result.columns.map((col) => {
@ -352,7 +352,7 @@ function SqlEditor({
aria-hidden="true"
>
{Array.from({ length: lineCount }, (_, i) => (
<span key={i} className="text-[11px]">
<span key={i + 1} className="text-[11px]">
{i + 1}
</span>
))}
@ -609,10 +609,11 @@ export default function InsightsEditor() {
</button>
</div>
<div className="p-5">
<label className="mb-2 block text-xs font-medium text-fg-3">
<label htmlFor="ai-query-prompt" className="mb-2 block text-xs font-medium text-fg-3">
Describe what you want to query
</label>
<textarea
id="ai-query-prompt"
value={aiPrompt}
onChange={(e) => setAiPrompt(e.target.value)}
placeholder="e.g. Show me the average build time per workflow over the last 30 days"

View file

@ -231,7 +231,7 @@ function DiffStat({ additions, deletions }: { additions: number; deletions: numb
<div className="flex gap-0.5">
{Array.from({ length: BLOCK_COUNT }, (_, i) => (
<span
key={i}
key={`block-${i}`}
className={`inline-block size-2.5 rounded-sm ${i < addBlocks ? "bg-mint" : "bg-coral"}`}
/>
))}

View file

@ -1,3 +0,0 @@
export default function RunLogs() {
return <p className="text-sm text-fg-muted">Logs will appear here.</p>;
}

View file

@ -86,7 +86,7 @@ export default function RunRetro({ loaderData }: Route.ComponentProps) {
{retro.learnings.map((learning, i) => {
const config = learningCategoryConfig[learning.category];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<div key={learning.text} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>
@ -106,7 +106,7 @@ export default function RunRetro({ loaderData }: Route.ComponentProps) {
{retro.friction_points.map((fp, i) => {
const config = frictionKindConfig[fp.kind];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<div key={fp.description} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>
@ -133,7 +133,7 @@ export default function RunRetro({ loaderData }: Route.ComponentProps) {
{retro.open_items.map((item, i) => {
const config = openItemKindConfig[item.kind];
return (
<div key={i} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<div key={item.description} className="flex items-start gap-3 rounded-md border border-line bg-panel/60 px-4 py-3">
<span className={`mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${config.text} bg-overlay`}>
{config.label}
</span>

View file

@ -97,8 +97,8 @@ function ToolRow({ tool }: { tool: ToolUse }) {
function ToolBlock({ tools }: { tools: ToolUse[] }) {
return (
<div className="rounded-md border border-line bg-overlay overflow-hidden">
{tools.map((tool, i) => (
<ToolRow key={i} tool={tool} />
{tools.map((tool) => (
<ToolRow key={tool.id} tool={tool} />
))}
</div>
);
@ -222,11 +222,11 @@ export default function RunStages({ loaderData }: Route.ComponentProps) {
{mappedTurns.map((turn, i) => {
switch (turn.kind) {
case "system":
return <SystemBlock key={i} content={turn.content} />;
return <SystemBlock key={`turn-${i}`} content={turn.content} />;
case "assistant":
return <AssistantBlock key={i} content={turn.content} />;
return <AssistantBlock key={`turn-${i}`} content={turn.content} />;
case "tool":
return <ToolBlock key={i} tools={turn.tools} />;
return <ToolBlock key={`turn-${i}`} tools={turn.tools} />;
}
})}
</div>

View file

@ -220,7 +220,9 @@ function ChecksStatus({ checks }: { checks: CheckRun[] }) {
return (
<div
className="-mx-4 mt-3 overflow-hidden border-y border-line"
role="group"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
onKeyDown={(e) => { e.stopPropagation(); }}
>
<button
type="button"

View file

@ -329,8 +329,8 @@ function ToolRow({ tool }: { tool: ToolUse }) {
function ToolBlock({ tools }: { tools: ToolUse[] }) {
return (
<div className="rounded-md border border-line bg-overlay overflow-hidden">
{tools.map((tool, i) => (
<ToolRow key={i} tool={tool} />
{tools.map((tool) => (
<ToolRow key={tool.id} tool={tool} />
))}
</div>
);
@ -462,14 +462,14 @@ export default function SessionDetail({ loaderData }: Route.ComponentProps) {
{session.turns.map((turn, i) => {
switch (turn.kind) {
case "user":
return <UserBlock key={i} content={turn.content} created_at={turn.created_at} />;
return <UserBlock key={`turn-${i}`} content={turn.content} created_at={turn.created_at} />;
case "assistant": {
const next = session.turns[i + 1];
const showCopy = next?.kind !== "tool";
return <AssistantBlock key={i} content={turn.content} showCopy={showCopy} />;
return <AssistantBlock key={`turn-${i}`} content={turn.content} showCopy={showCopy} />;
}
case "tool":
return <div key={i} className="pl-10"><ToolBlock tools={turn.tools} /></div>;
return <div key={`turn-${i}`} className="pl-10"><ToolBlock tools={turn.tools} /></div>;
}
})}
</div>

View file

@ -294,13 +294,12 @@ function SidebarNav({ activeId, groups }: { activeId: string; groups: SettingGro
const active = activeId === group.id;
return (
<li key={group.id}>
<a
href={`#section-${group.id}`}
onClick={(e) => {
e.preventDefault();
<button
type="button"
onClick={() => {
document.getElementById(`section-${group.id}`)?.scrollIntoView({ behavior: "smooth", block: "start" });
}}
className={`flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm transition-all duration-200 ${
className={`flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-left text-sm transition-all duration-200 ${
active
? `${colors.glow} ${colors.icon} font-medium`
: "text-fg-3 hover:text-fg-2 hover:bg-overlay"
@ -308,7 +307,7 @@ function SidebarNav({ activeId, groups }: { activeId: string; groups: SettingGro
>
<Icon className={`size-4 ${active ? colors.icon : "text-fg-muted"}`} />
{group.name}
</a>
</button>
</li>
);
})}

View file

@ -166,7 +166,7 @@ function EvaluationBar({ evaluations }: { evaluations: readonly EvaluationResult
<div className="flex items-center gap-1">
{evaluations.map((result, i) => (
<div
key={i}
key={`eval-${i}`}
className={`h-6 flex-1 rounded-sm ${
result === "pass"
? "bg-mint/70"

View file

@ -284,7 +284,7 @@ function EvaluationDots({ evaluations }: { evaluations: readonly EvaluationResul
<div className="flex items-center gap-0.5">
{evaluations.map((result, i) => (
<span
key={i}
key={`eval-${i}`}
className={`inline-block size-2.5 rounded-sm ${
result === "pass"
? "bg-mint/70"

View file

@ -1,23 +0,0 @@
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.061 58.1641C71.1037 58.1641 58.1677 71.0742 58.1677 86.9996C58.1677 102.925 71.1037 115.835 87.061 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="white"/>
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="white"/>
<path d="M289.314 144.671C289.314 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.314 160.596 289.314 144.671Z" fill="white"/>
<g clip-path="url(#clip0_202_2131)">
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.385 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.385 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="white"/>
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="white"/>
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="white"/>
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="white"/>
<path d="M547.32 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.365 2.95282 554.365 13.1239C554.365 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.317 21.6426C553.595 22.8372 554.365 23.2391 554.365 30.0273V31.5345H547.332H547.32ZM522.457 18.3601H547.32V7.88763H522.457V18.349V18.3601Z" fill="white"/>
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="white"/>
<path d="M655.562 31.5345L653.151 26.3429H633.746L631.335 31.5345H624.58L637.006 4.75034C637.71 3.22078 639.262 2.23828 640.936 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.283 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="white"/>
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="white"/>
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.147V7.02795H752.025V31.5345H745.282Z" fill="white"/>
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.675 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_202_2131">
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6 KiB

View file

@ -1,23 +0,0 @@
<svg width="1080" height="174" viewBox="0 0 1080 174" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M231.527 86.9999C231.527 94.9642 228.297 102.173 223.067 107.387C217.837 112.606 210.614 115.835 202.634 115.835C194.654 115.835 187.43 119.059 182.206 124.278C176.977 129.498 173.741 136.707 173.741 144.671C173.741 152.635 170.51 159.844 165.281 165.058C160.051 170.277 152.828 173.507 144.847 173.507C136.867 173.507 129.644 170.277 124.42 165.058C119.19 159.844 115.954 152.635 115.954 144.671C115.954 136.707 119.19 129.498 124.42 124.278C129.644 119.059 136.867 115.835 144.847 115.835C152.828 115.835 160.051 112.606 165.281 107.387C170.51 102.173 173.741 94.9642 173.741 86.9999C173.741 71.0711 160.808 58.1643 144.847 58.1643C136.867 58.1643 129.644 54.9347 124.42 49.7155C119.19 44.502 115.954 37.2931 115.954 29.3287C115.954 21.3643 119.19 14.1555 124.42 8.93622C129.644 3.71698 136.867 0.493164 144.847 0.493164C160.808 0.493164 173.741 13.4 173.741 29.3287C173.741 37.2931 176.977 44.502 182.206 49.7155C187.43 54.9347 194.654 58.1643 202.634 58.1643C218.594 58.1643 231.527 71.0711 231.527 86.9999Z" fill="#F44250"/>
<path d="M115.954 86.9996C115.954 71.0742 103.018 58.1641 87.0608 58.1641C71.1035 58.1641 58.1676 71.0742 58.1676 86.9996C58.1676 102.925 71.1035 115.835 87.0608 115.835C103.018 115.835 115.954 102.925 115.954 86.9996Z" fill="#121212"/>
<path d="M58.1676 144.671C58.1676 128.745 45.2316 115.835 29.2743 115.835C13.317 115.835 0.381104 128.745 0.381104 144.671C0.381104 160.596 13.317 173.506 29.2743 173.506C45.2316 173.506 58.1676 160.596 58.1676 144.671Z" fill="#121212"/>
<path d="M289.313 144.671C289.313 128.745 276.378 115.835 260.42 115.835C244.463 115.835 231.527 128.745 231.527 144.671C231.527 160.596 244.463 173.506 260.42 173.506C276.378 173.506 289.313 160.596 289.313 144.671Z" fill="#121212"/>
<g clip-path="url(#clip0_171_1761)">
<path d="M562.482 173.247C524.388 173.247 498.363 147.49 498.363 110.468C498.363 73.4455 524.388 47.6885 562.482 47.6885C600.576 47.6885 626.869 73.7135 626.869 110.468C626.869 147.222 600.576 173.247 562.482 173.247ZM562.482 144.007C579.386 144.007 587.703 130.319 587.703 110.468C587.703 90.6168 579.386 76.9289 562.482 76.9289C545.579 76.9289 537.529 90.6168 537.529 110.468C537.529 130.319 545.311 144.007 562.482 144.007Z" fill="#121212"/>
<path d="M833.64 141.116C824.217 141.116 819.237 136.684 819.237 126.156V74.8983H851.928V47.7792H819.237V1.15527H791.75L786.1 26.1978C783.343 36.4805 780.82 42.822 773.897 46.0821C773.105 46.4506 771.129 46.9976 769.409 47.3884C768.014 47.701 766.596 47.8573 765.167 47.8573H752.338V47.9243H734.832C723.578 47.9243 714.445 57.0459 714.445 68.3111V111.552C714.445 130.599 707.199 142.668 692.719 142.668C678.238 142.668 672.868 133.279 672.868 116.375V47.9243H634.249V125.765C634.249 151.254 644.442 173.248 676.63 173.248C691.915 173.248 703.895 167.231 711.096 157.182C712.145 155.72 714.445 156.49 714.445 158.276V170.022H753.332V83.8412C753.332 78.8953 757.34 74.8871 762.286 74.8871H779.882V136.952C779.882 164.663 797.89 173.248 817.842 173.248C833.908 173.248 844.436 169.374 853.58 162.441V136.126C846.1 139.453 839.725 141.116 833.629 141.116H833.64Z" fill="#121212"/>
<path d="M981.561 130.865C975.387 157.962 954.197 173.258 923.07 173.258C885.243 173.258 858.415 150.18 858.415 112.354C858.415 74.5281 885.779 47.6992 922.266 47.6992C961.699 47.6992 982.365 74.796 982.365 107.263V113.884H896.509C894.555 135.711 909.382 144.017 924.409 144.017C937.829 144.017 946.136 138.915 950.434 127.918L981.561 130.865ZM945.075 94.9372C944.271 83.1361 936.757 75.8567 921.998 75.8567C906.434 75.8567 899.188 82.321 897.045 94.9372H945.064H945.075Z" fill="#121212"/>
<path d="M1076.24 85.7486C1070.06 82.2652 1064.17 80.9142 1055.85 80.9142C1039.75 80.9142 1029.02 90.0358 1029.02 110.691V170.02H990.393V47.9225H1029.02V64.3235C1029.02 65.4623 1030.54 65.8195 1031.05 64.8035C1036.68 53.5718 1047.91 44.707 1062.03 44.707C1069.27 44.707 1075.45 46.8507 1078.66 49.5414L1076.25 85.7597L1076.24 85.7486Z" fill="#121212"/>
<path d="M547.321 31.5345V23.9983H522.457V31.5345H515.378V2.23828H542.14C553.562 2.23828 554.366 2.95282 554.366 13.1239C554.366 17.4111 553.472 18.5611 551.329 19.6553L549.408 20.6378L551.318 21.6426C553.595 22.8372 554.366 23.2391 554.366 30.0273V31.5345H547.332H547.321ZM522.457 18.3601H547.321V7.88763H522.457V18.349V18.3601Z" fill="#121212"/>
<path d="M578.493 2.23828H610.826V7.90996H580.067V14.5083H610.011V19.2868H580.067V25.8963H610.837V31.501L578.504 31.5345C575.344 31.5345 572.787 28.9778 572.787 25.8293V7.95462C572.787 4.80617 575.344 2.24945 578.493 2.24945V2.23828Z" fill="#121212"/>
<path d="M655.562 31.5345L653.151 26.3429H633.747L631.335 31.5345H624.58L637.007 4.75034C637.71 3.22078 639.262 2.23828 640.937 2.23828H645.927C647.613 2.23828 649.154 3.22078 649.857 4.75034L662.284 31.5345H655.529H655.562ZM643.46 8.06627C642.712 8.06627 642.053 8.49053 641.729 9.17158L635.968 21.5756H650.94L645.19 9.17158C644.878 8.49053 644.208 8.06627 643.46 8.06627Z" fill="#121212"/>
<path d="M694.862 32.4153C676.05 32.4153 675.313 32.4153 675.313 16.8852C675.313 1.35505 676.05 1.36621 694.862 1.36621C711.721 1.36621 713.764 2.06959 714.244 10.5325H707.333V7.01556H682.168V26.766H707.333V23.2714H714.244C713.775 31.7119 711.721 32.4153 694.862 32.4153Z" fill="#121212"/>
<path d="M745.282 31.5345V7.02795H729.16V2.23828H768.148V7.02795H752.026V31.5345H745.282Z" fill="#121212"/>
<path d="M454.419 169.819C450.935 165.264 448.792 154.814 447.452 137.397C446.112 118.104 437.806 113.817 422.532 113.817H392.254V169.83H347.494V0.986328H432.715C476.391 0.986328 498.106 21.6187 498.106 54.5882C498.106 79.2399 482.833 95.3171 462.201 98.0078C479.618 101.491 489.8 111.405 491.676 130.966C494.087 156.154 494.891 163.656 500.518 169.819H454.419ZM424.676 78.704C443.969 78.704 453.615 73.8808 453.615 58.3395C453.615 44.6739 443.969 37.4392 424.676 37.4392H392.254V78.7152H424.676V78.704Z" fill="#121212"/>
</g>
<defs>
<clipPath id="clip0_171_1761">
<rect width="731.156" height="172.261" fill="white" transform="translate(347.494 0.986328)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6 KiB

View file

@ -1,89 +0,0 @@
import logoDark from "./logo-dark.svg";
import logoLight from "./logo-light.svg";
export function Welcome() {
return (
<main className="flex items-center justify-center pt-16 pb-4">
<div className="flex-1 flex flex-col items-center gap-16 min-h-0">
<header className="flex flex-col items-center gap-9">
<div className="w-[500px] max-w-[100vw] p-4">
<img
src={logoLight}
alt="React Router"
className="block w-full dark:hidden"
/>
<img
src={logoDark}
alt="React Router"
className="hidden w-full dark:block"
/>
</div>
</header>
<div className="max-w-[300px] w-full space-y-6 px-4">
<nav className="rounded-3xl border border-gray-200 p-6 dark:border-gray-700 space-y-4">
<p className="leading-6 text-gray-700 dark:text-gray-200 text-center">
What&apos;s next?
</p>
<ul>
{resources.map(({ href, text, icon }) => (
<li key={href}>
<a
className="group flex items-center gap-3 self-stretch p-3 leading-normal text-blue-700 hover:underline dark:text-blue-500"
href={href}
target="_blank"
rel="noreferrer"
>
{icon}
{text}
</a>
</li>
))}
</ul>
</nav>
</div>
</div>
</main>
);
}
const resources = [
{
href: "https://reactrouter.com/docs",
text: "React Router Docs",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 20 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M9.99981 10.0751V9.99992M17.4688 17.4688C15.889 19.0485 11.2645 16.9853 7.13958 12.8604C3.01467 8.73546 0.951405 4.11091 2.53116 2.53116C4.11091 0.951405 8.73546 3.01467 12.8604 7.13958C16.9853 11.2645 19.0485 15.889 17.4688 17.4688ZM2.53132 17.4688C0.951566 15.8891 3.01483 11.2645 7.13974 7.13963C11.2647 3.01471 15.8892 0.951453 17.469 2.53121C19.0487 4.11096 16.9854 8.73551 12.8605 12.8604C8.73562 16.9853 4.11107 19.0486 2.53132 17.4688Z"
strokeWidth="1.5"
strokeLinecap="round"
/>
</svg>
),
},
{
href: "https://rmx.as/discord",
text: "Join Discord",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="20"
viewBox="0 0 24 20"
fill="none"
className="stroke-gray-600 group-hover:stroke-current dark:stroke-gray-300"
>
<path
d="M15.0686 1.25995L14.5477 1.17423L14.2913 1.63578C14.1754 1.84439 14.0545 2.08275 13.9422 2.31963C12.6461 2.16488 11.3406 2.16505 10.0445 2.32014C9.92822 2.08178 9.80478 1.84975 9.67412 1.62413L9.41449 1.17584L8.90333 1.25995C7.33547 1.51794 5.80717 1.99419 4.37748 2.66939L4.19 2.75793L4.07461 2.93019C1.23864 7.16437 0.46302 11.3053 0.838165 15.3924L0.868838 15.7266L1.13844 15.9264C2.81818 17.1714 4.68053 18.1233 6.68582 18.719L7.18892 18.8684L7.50166 18.4469C7.96179 17.8268 8.36504 17.1824 8.709 16.4944L8.71099 16.4904C10.8645 17.0471 13.128 17.0485 15.2821 16.4947C15.6261 17.1826 16.0293 17.8269 16.4892 18.4469L16.805 18.8725L17.3116 18.717C19.3056 18.105 21.1876 17.1751 22.8559 15.9238L23.1224 15.724L23.1528 15.3923C23.5873 10.6524 22.3579 6.53306 19.8947 2.90714L19.7759 2.73227L19.5833 2.64518C18.1437 1.99439 16.6386 1.51826 15.0686 1.25995ZM16.6074 10.7755L16.6074 10.7756C16.5934 11.6409 16.0212 12.1444 15.4783 12.1444C14.9297 12.1444 14.3493 11.6173 14.3493 10.7877C14.3493 9.94885 14.9378 9.41192 15.4783 9.41192C16.0471 9.41192 16.6209 9.93851 16.6074 10.7755ZM8.49373 12.1444C7.94513 12.1444 7.36471 11.6173 7.36471 10.7877C7.36471 9.94885 7.95323 9.41192 8.49373 9.41192C9.06038 9.41192 9.63892 9.93712 9.6417 10.7815C9.62517 11.6239 9.05462 12.1444 8.49373 12.1444Z"
strokeWidth="1.5"
/>
</svg>
),
},
];