# Project export: Omni QA

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Automated bug detection and resolution with natural language
- Devpost: https://devpost.com/software/omni-qa
- GitHub: https://github.com/KevinWu098/omni
- Team: 4 GitHub contributor(s) — uno-p-5 (55 commits), Jay Wu (48 commits), Kevin Wu (32 commits), kylebtran (23 commits)

## Devpost submission (written by the team)

### Inspiration

With artificial intelligence, software is now being generated and deployed faster than ever. Entire applications are built in hours, not weeks. But here’s the catch: the code works… until it doesn’t. AI-generated code is particularly error-prone and buggy. For the startup that just went viral on social media, or the major corporation rolling out a critical feature—one crash, one broken flow, one bad user experience can mean lost customers, lost trust, and millions in lost revenue. Traditional quality assurance isn’t keeping up with the pace of AI-enabled software development. For users and developers alike, this represents a critical problem in our ecosystem. That's why we built Omni, natural language-based testing that proactively identifies and resolves issues before they reach your users.

### What it does

Omni modernizes QA with a fleet of AI agents that test like real users. Using natural language-based goals, our agents don’t just follow scripts—they explore your app with context, memory, and intent. They react to UI changes, latency, and feedback in real time, surfacing bugs that traditional tests miss. No more brittle test suites. Omni’s agents don’t depend on specific DOM selectors or content. They intelligently navigate websites even as the underlying codebase changes around them. We capture the full story when something breaks. Screenshots, DOM diffs, network logs, user intent—everything developers need to understand and fix bugs fast. Then we go a step further: Omni automatically generates context-aware pull requests in your repository to proactively catch and resolve bugs. QA built for the AI era. While AI accelerates code creation, Omni ensures that what ships actually works. No more tradeoff between speed and quality. Omni finds what others miss—so your users never end up as your QA testers.

### How we built it

Frontend: Next.js Tailwind CSS WebRTC Framer Motion Backend: AWS EC2 FastAPI FFMPEG Browser Use Playwright Fetch AI Our frontend was built using Next.js and Tailwind. On the backend, we have a FastAPI server operating as an orchestration layer between our frontend (client) and our fleet of QA agents. We then deploy browser use agents on an EC2 instance. Using natural language tests on the frontend, the QA agents navigate websites to identify bugs. It streams realtime video and events back to the client. If the agent detects an issue, it uses FetchAI and the Github API to create and solve PRs using context from the test cases.

### Challenges we ran into

Managing many concurrent agent sessions was difficult API rate limits, browser rate limits kept dragging us down

### Accomplishments we're proud of

Built a fully-featured testing suite Built an end-to-end testing pipeline that can identify AND solve bugs

### What we learned

Working with browser-use APIs Streaming concurrent video footage streams

### What's next

for Omni Higher concurrent agent capacity Stronger corroboration amongst agent journeys Consistent deployment for exhaustive coverage

## README (from the GitHub repository)

No README available.

## Detected evidence (automated analysis)

Indexed codebase: 69 recognized source files, 547 KB.
- CSS (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- FastAPI (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (78 of 78)

```
.gitignore
client/.gitignore
client/components.json
client/eslint.config.mjs
client/next.config.ts
client/package.json
client/postcss.config.mjs
client/prettier.config.cjs
client/README.md
client/src/app/(layout)/layout.tsx
client/src/app/(layout)/page.tsx
client/src/app/actions.ts
client/src/app/api/auth/[...nextauth]/route.ts
client/src/app/globals.css
client/src/app/home/page.tsx
client/src/app/landing/page.tsx
client/src/app/layout.tsx
client/src/app/playground/page.tsx
client/src/components/auth-provider.tsx
client/src/components/big-text.tsx
client/src/components/builder/all-side-panel.tsx
client/src/components/builder/builder-sidebar.tsx
client/src/components/builder/single/single-side-panel.tsx
client/src/components/builder/single/step.tsx
client/src/components/builder/test-card.tsx
client/src/components/builder/viewer.tsx
client/src/components/chip.tsx
client/src/components/comment-card.tsx
client/src/components/content.tsx
client/src/components/create-pr-form.tsx
client/src/components/login-button.tsx
client/src/components/MaskTextSplit.tsx
client/src/components/medium-text.tsx
client/src/components/prs-list.tsx
client/src/components/repos-list.tsx
client/src/components/runner/agent-card.tsx
client/src/components/runner/claude-loader.tsx
client/src/components/runner/runner-sidebar.tsx
client/src/components/runner/runner-viewer.tsx
client/src/components/runner/timeline/editor.tsx
client/src/components/runner/timeline/timeline.tsx
client/src/components/runner/timeline/tracks.tsx
client/src/components/runner/timeline/types.ts
client/src/components/sidebar-wrapper.tsx
client/src/components/timeline.tsx
client/src/components/ui/badge.tsx
client/src/components/ui/button.tsx
client/src/components/ui/collapsible.tsx
client/src/components/ui/input.tsx
client/src/components/ui/resizable.tsx
client/src/components/ui/scroll-area.tsx
client/src/components/ui/select.tsx
client/src/components/ui/separator.tsx
client/src/components/ui/sonner.tsx
client/src/components/ui/tabs.tsx
client/src/components/ui/video-bad.tsx
client/src/components/ui/video.tsx
client/src/lib/auth.ts
client/src/lib/github.ts
client/src/lib/globals.ts
client/src/lib/hooks/use-command-stream.ts
client/src/lib/utils.ts
client/src/types/next-auth.d.ts
client/tailwind.config.ts
client/tsconfig.json
server/agent/.gitignore
server/agent/app.py
server/agent/app2.py
server/agent/edit_agent.py
server/agent/fetch.py
server/agent/git.py
server/agent/service.py
server/server/app.py
server/server/README.md
server/testing/.gitignore
server/testing/fetch_ai_edit_test.py
server/testing/README.md
server/testing/stream.py
```

### Dependencies

- client/package.json: @eslint/eslintrc@^3, @gsap/react@^2.1.2, @ianvs/prettier-plugin-sort-imports@^4.4.1, @radix-ui/react-collapsible@^1.1.11, @radix-ui/react-scroll-area@^1.2.9, @radix-ui/react-select@^2.2.5, @radix-ui/react-separator@^1.1.7, @radix-ui/react-slot@^1.2.3, @radix-ui/react-tabs@^1.1.12, @remotion/player@^4.0.318, @tailwindcss/postcss@^4, @types/hls.js@^1.0.0, @types/node@^20, @types/react@^19, @types/react-dom@^19, class-variance-authority@^0.7.1, clsx@^2.1.1, eslint@^9, eslint-config-next@15.3.4, gsap@^3.13.0, gsap-trial@^3.13.0, hls.js@^1.6.5, locomotive-scroll@5.0.0-beta.21, lucide-react@^0.522.0, motion@^12.18.1, next@15.3.4, next-auth@^4.24.11, next-themes@^0.4.6, nuqs@^2.4.3, prettier@^3.5.3, prettier-plugin-tailwindcss@^0.6.8, react@^19.0.0, react-dom@^19.0.0, react-markdown@^10.1.0, react-resizable-panels@^3.0.3, rehype-raw@^7.0.0, remark-gfm@^4.0.1, remotion@^4.0.318, sonner@^2.0.5, tailwind-merge@^2.6.0, tailwindcss@^3.4.1, tailwindcss-animate@^1.0.7, typescript@^5

### Recent commits (newest first)

- feat: enhance landing page navigation and clean up side panel layout
- feat: dev dev
- Merge remote-tracking branch 'refs/remotes/origin/main'
- feat: dev dev dev
- Merge branch 'main' of https://github.com/KevinWu098/omni
- fix: force 4
- test
- fix: fix
- feat: add runner statistics tracking and update UI components
- feat: update test cases
- Merge branch 'main' of https://github.com/KevinWu098/omni
- fix: match with frontend
- feat: implement step status tracking and update UI accordingly
- fix: stop running command on error
- port pr maker to app2.py
- Merge branch 'main' of github.com:KevinWu098/omni
- logs
- Merge pull request #8 from KevinWu098/button
- Merge branch 'main' into button
- feat: add playbar

## Key source files (fetched from GitHub, selected and truncated for size)

### client/package.json

```
{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@gsap/react": "^2.1.2",
    "@radix-ui/react-collapsible": "^1.1.11",
    "@radix-ui/react-scroll-area": "^1.2.9",
    "@radix-ui/react-select": "^2.2.5",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-tabs": "^1.1.12",
    "@remotion/player": "^4.0.318",
    "@types/hls.js": "^1.0.0",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "gsap": "^3.13.0",
    "gsap-trial": "^3.13.0",
    "hls.js": "^1.6.5",
    "locomotive-scroll": "5.0.0-beta.21",
    "lucide-react": "^0.522.0",
    "motion": "^12.18.1",
    "next": "15.3.4",
    "next-auth": "^4.24.11",
    "next-themes": "^0.4.6",
    "nuqs": "^2.4.3",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-markdown": "^10.1.0",
    "react-resizable-panels": "^3.0.3",
    "rehype-raw": "^7.0.0",
    "remark-gfm": "^4.0.1",
    "remotion": "^4.0.318",
    "sonner": "^2.0.5",
    "tailwind-merge": "^2.6.0",
    "tailwindcss-animate": "^1.0.7"
  },
  "devDependencies": {
    "@eslint/eslintrc": "^3",
    "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "15.3.4",
    "prettier": "^3.5.3",
    "prettier-plugin-tailwindcss": "^0.6.8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

```

### server/server/app.py

```python
import asyncio
import json
import logging
import uuid
from typing import Any, Dict, List, Optional, Set, Tuple

import websockets
from websockets.server import WebSocketServerProtocol

# ——— Configuration ——————————————————————————————————————————————————————————————
HOST = "0.0.0.0"
PORT = 8765

# ——— Logging Setup —————————————————————————————————————————————————————————————
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# ——— In-Memory State ————————————————————————————————————————————————————————————
clients: Set[WebSocketServerProtocol] = set()
vms: Dict[str, WebSocketServerProtocol] = {}  # vm_id -> websocket connection
test_case_to_vm: Dict[str, str] = {}  # test_case_id -> vm_id
vm_to_test_case: Dict[str, str] = {}  # vm_id -> test_case_id (for tracking usage)


# ——— VM Management ————————————————————————————————————————————————————————————
async def assign_vms_to_tests(tests: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
    """
    Assign VMs to test cases. Reuse existing VMs or create new ones as needed.
    """
    result = []

    for test in tests:
        test_case_id = test.get("test_case_id")
        steps = test.get("steps", [])

        if not test_case_id:
            continue

        # Check if this test case already has a VM assigned
        if test_case_id in test_case_to_vm:
            vm_id = test_case_to_vm[test_case_id]
            logging.info(f"Reusing VM {vm_id} for test case {test_case_id}")
        else:
            # Find an unused VM or create a new one
            available_vm_id = None
            for vm_id in vms.keys():
                if vm_id not in vm_to_test_case:
                    available_vm_id = vm_id
                    break

            if available_vm_id:
                vm_id = available_vm_id
                logging.info(
                    f"Assigning existing VM {vm_id} to test case {test_case_id}"
                )
            else:
                # Create new VM (placeholder - would be actual IaC in production)
                vm_id = str(uuid.uuid4())
                logging.info(f"Created new VM {vm_id} for test case {test_case_id}")

            # Update mappings
            test_case_to_vm[test_case_id] = vm_id
            vm_to_test_case[vm_id] = test_case_id

        result.append({"test_case_id": test_case_id, "vm_id": vm_id, "steps": steps})

    logging.info(f"Assigned VMs to {len(result)} test cases")
    return result


# ——— Connection Lifecycle ———————————————————————————————————————————————————————
async def register_connection(
    ws: WebSocketServerProtocol,
) -> Tuple[str, Optional[str]]:
    """
    Perform initial registration handshake. Returns (role, vm_id).
    Raises ValueError on invalid registration message.
    """
    raw = await ws.recv()
    msg = json.loads(raw)

    if msg.get("type") != "register" or msg.get("role") not in ("client", "vm"):
        raise ValueError(
            "First message must be {type: 'register', role: 'client'|'vm'}"
        )

    role = msg["role"]
    if role == "client":
        clients.add(ws)
        logging.info("Registered new client: %s", ws.remote_address)
        return "client", None

    vm_id = msg.get("vm_id")
    if not vm_id:
        raise ValueError("VM registration requires a 'vm_id'")
    vms[vm_id] = ws
    logging.info("Registered new VM '%s': %s", vm_id, ws.remote_address)
    return "vm", vm_id


async def unregister(
    ws: WebSocketServerProtocol, role: str, vm_id: Optional[str] = None
) -> None:
    """
    Clean up on disconnect.
    """
    if role == "client":
        clients.discard(ws)
        logging.info("Client disconnected: %s", ws.remote_address)
    else:
        removed = vms.pop(vm_id, None)
        # Clean up test case mappings
        if vm_id in vm_to_test_case:
            test_case_id = vm_to_test_case.pop(vm_id)
            test_case_to_vm.pop(test_case_id, None)
        logging.info(
            "VM '%s' disconnected: %s",
            vm_id,
            ws.remote_address if removed else "not found",
        )


# ——— Message Handlers ——————————————————————————————————————————————————————————
async def handle_client_message(
    ws: WebSocketServerProtocol, msg: Dict[str, Any]
) -> None:
    msg_type = msg.get("type")

    if msg_type == "spin_up":
        tests = msg.get("tests", [])
        assigned_tests = await assign_vms_to_tests(tests)
        await ws.send(json.dumps({"type": "spin_up_response", "tests": assigned_tests}))

    elif msg_type == "list_vms":
        vm_list = []
        for vm_id, ws_conn in vms.items():
            test_case_id = vm_to_test_case.get(vm_id)
            vm_list.append(
                {"vm_id": vm_id, "test_case_id": test_case_id, "status": "connected"}
            )
        await ws.send(json.dumps({"type": "list_vms_response", "vms": vm_list}))

    elif msg_type == "command":
        vm_id = msg.get("vm_id")
        command = msg.get("command", "")
        if vm_id not in vms:
            await ws.send(
                json.dumps({"type": "error", "message": f"Unknown vm_id '{vm_id}'"})
            )
            return
        await vms[vm_id].send(json.dumps({"type": "command", "command": command}))

    elif msg_type == "screenshot":
        vm_id = msg.get("vm_id")
        full_page = msg.get("full_page", False)
        if vm_id not in vms:
            await ws.send(
                json.dumps({"type": "error", "message": f"Unknown vm_id '{vm_id}'"})
            )
            return
        await vms[vm_id].send(
            json.dumps({"type": "screenshot", "full_page": full_page})
        )

    else:
        await ws.send(
            json.dumps(
                {"type": "error", "message": f"Unknown message type '{msg_type}'"}
            )
        )


async def handle_vm_message(
    ws: WebSocketServerProtocol, vm_id: str, msg: Dict[str, Any]
) -> None:
    msg_type = msg.get("type")

    i
[truncated — 1907 more characters]
```

### server/agent/app.py

```python
from flask import Flask, request, jsonify, Response, stream_with_context
from dotenv import load_dotenv
import json
import asyncio
from uuid import uuid4
import os
import sys
from pathlib import Path

from service import VideoAgentService
from flask_cors import CORS

from edit_agent import pull_edit_pr_streaming
from collections import defaultdict

# Mapping from run IDs to VideoAgentService instances
agents = {}
logs = defaultdict(list)

load_dotenv()

app = Flask(__name__)
CORS(app)


@app.route("/diag", methods=["GET"])
def diag():
    return jsonify({"agents": list(agents.keys())})

@app.route("/agent_logs/<run_id>", methods=["GET"])
def agent_logs(run_id):
    return jsonify(logs[run_id])

@app.route("/run_command", methods=["POST"])
def run_command():

    data = request.get_json(force=True)
    commands = data.get("commands")

    if not commands:
        return jsonify({"error": "Missing 'commands' in JSON body"}), 400

    run_id = data.get(
        "run_id", None
    )  # if no run id specified then generate new browser
    if not run_id:
        run_id = str(uuid4())

        # Create a new service instance with a unique run ID
        service = VideoAgentService(run_id)
        agents[run_id] = service
    else:

        if run_id not in agents:
            # Create a new service instance with a unique run ID
            service = VideoAgentService(run_id)
            agents[run_id] = service
        else:
            service = agents.get(run_id)
            if not service:
                return (
                    jsonify(
                        {
                            "error": f"Run ID {run_id} not found or session already shut down."
                        }
                    ),
                    404,
                )
            # Ensure the service is not marked as done if it's being reused
            service.done = False

    print("Showing commands: ", commands)

    def generate():
        try:
            # Stream the unique run ID as the first message
            yield f"data: {{'type': 'uuid', 'id': '{run_id}'}}\n\n"
            # Ensure recorder is running
            asyncio.run_coroutine_threadsafe(service.recorder.start(), service.loop).result()
            # Stream logs and results
            for log_data in service.run_command_streaming(commands):
                if (log_data != "data: {'type': 'keepalive'}\n\n"):
                    logs[run_id].append(log_data)
                yield log_data
            # Signal completion to client
            yield "data: {'type': 'done'}\n\n"
            # Pause recording
            asyncio.run_coroutine_threadsafe(service.recorder.stop(), service.loop).result()
            if data.get("soft_shutdown_on_end", False):
                service.shutdown()
        except Exception as e:
            # Catch exceptions during streaming and send an message
            yield f"data: {{'type': 'error', 'content': '{str(e)}'}}\n\n"
            # Pause recording on error
            asyncio.run_coroutine_threadsafe(service.recorder.stop(), service.loop).result()
            service.shutdown()  # Shutdown on error
            del agents[run_id]  # Remove from active agents
        finally:
            pass

    return Response(
        stream_with_context(generate()),
        content_type="text/event-stream",
        headers={
            "Cache-Control": "no-cache",
            "Connection": "keep-alive",
        },
    )


@app.route("/shutdown_run/<run_id>", methods=["POST"])
def shutdown_run(run_id):
    data = request.get_json(force=True)
    if data.get("delete_video", False):
        service = agents.pop(run_id, None)
    else:
        service = agents.get(run_id)
    if service:
        service.shutdown()
        return jsonify({"message": f"Run ID {run_id} shut down successfully."})
    return jsonify({"error": f"Run ID {run_id} not found."}), 404


@app.route("/stream/<run_id>/playlist.m3u8", methods=["PUT", "GET"])
def playlist(run_id):
    service = agents.get(run_id)
    if not service:
        return "", 404
    if request.method == "PUT":
        service.hls.put_playlist(request.data)
        return ""
    pl = service.get_playlist()
    if not pl:
        return "", 404
    return Response(pl, mimetype="application/vnd.apple.mpegurl")


@app.route("/stream/<run_id>/segments/<name>", methods=["PUT", "GET"])
def segment(run_id, name):
    service = agents.get(run_id)
    if not service:
        return "", 404
    if request.method == "PUT":
        service.hls.put_segment(name, request.data)
        return ""
    data = service.get_segment(name)
    if not data:
        return "", 404
    return Response(data, mimetype="video/mp2t")


@app.route("/stream/<run_id>/offer", methods=["POST"])
def offer(run_id):
    service = agents.get(run_id)
    if not service:
        return jsonify({"error": "Unknown run_id"}), 404
    # If WebRTC stream is done, return gone
    if getattr(service, "done", False):
        return jsonify({"error": "WebRTC stream has ended"}), 410
    params = request.get_json(force=True)
    try:
        answer = service.handle_offer(params)
        return jsonify(answer)
    except RuntimeError as e:
        # Handle closed event loop gracefully
        return jsonify({"error": "Agent not available"}), 410
    



# Simple UI route for viewing HLS/WebRTC with live/DVR toggle
VIEW_HTML = """
<!doctype html><html><head>
<title>Live Stream Viewer</title>
<style>
html,body{margin:0;background:#000;font-family:sans-serif;color:#fff}
#bar{padding:6px;background:#111;display:flex;gap:8px;align-items:center}
button{padding:2px 6px}
#timeInfo{margin-left:auto;font-size:12px;color:#999}
video::-webkit-media-controls-loading-panel{display:none!important}
video::-webkit-media-controls-buffering-indicator{display:none!important}
.live-mode video{pointer-events:none}
.live-mode video::-webkit-media-controls{display:none!important}
.live-mode video::-webkit-media-controls-panel{display:none!importan
[truncated — 5426 more characters]
```

### client/src/app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import { NuqsAdapter } from "nuqs/adapters/next/app";

import "./globals.css";

import { AuthProvider } from "@/components/auth-provider";

const geistSans = Geist({
    variable: "--font-geist-sans",
    subsets: ["latin"],
});

const geistMono = Geist_Mono({
    variable: "--font-geist-mono",
    subsets: ["latin"],
});

export const metadata: Metadata = {
    title: "Omni",
    description: ":o",
};

export default function RootLayout({
    children,
}: Readonly<{
    children: React.ReactNode;
}>) {
    return (
        <html lang="en">
            <body
                className={`${geistSans.variable} ${geistMono.variable} antialiased`}
            >
                <AuthProvider>
                    <NuqsAdapter>
                        <div className="overflow-hidden bg-o-base-background">
                            <div className="w-full bg-o-header p-2">
                                <span
                                    className="tracking-tightest flex items-center text-sm font-semibold uppercase leading-none text-o-white"
                                    style={{
                                        fontVariationSettings:
                                            '"wght" 600, "wdth" 135',
                                    }}
                                >
                                    Omni
                                </span>
                            </div>
                            {children}
                        </div>

                        <Toaster richColors />
                    </NuqsAdapter>
                </AuthProvider>
            </body>
        </html>
    );
}

```

### client/src/app/playground/page.tsx

```typescript
import { Timeline } from "@/components/timeline";

export default function Page() {
    return <Timeline />;
}

```

### client/src/app/(layout)/layout.tsx

```typescript
export default function Layout({
    children,
}: Readonly<{ children: React.ReactNode }>) {
    return (
        <div className="overflow-hidden bg-o-base-background">
            <div className="h-[calc(100dvh-30px)] w-full">{children}</div>
        </div>
    );
}

```

### client/src/app/(layout)/page.tsx

```typescript
import { Content } from "@/components/content";
import { authOptions } from "@/lib/auth";
import { getPRData } from "@/lib/github";
import { getServerSession } from "next-auth";

interface PageProps {
    searchParams: Promise<{
        repo?: string;
        pr?: string;
    }>;
}

export default async function Page({ searchParams }: PageProps) {
    const params = await searchParams;
    let prData = null;
    if (params.repo && params.pr) {
        try {
            const session = await getServerSession(authOptions);
            if (session?.accessToken) {
                prData = await getPRData(
                    session.accessToken,
                    params.repo,
                    parseInt(params.pr)
                );
            }
        } catch (error) {
            console.error("Failed to fetch PR data:", error);
        }
    }

    return <Content prData={prData} />;
}

```

### client/src/app/home/page.tsx

```typescript
"use client";

import { useState } from "react";
import { CreatePRForm } from "@/components/create-pr-form";
import { LoginButton } from "@/components/login-button";
import { PRsList } from "@/components/prs-list";
import { ReposList } from "@/components/repos-list";

export default function GitHubPage() {
    const [selectedRepo, setSelectedRepo] = useState("");

    return (
        <div style={{ padding: "20px" }}>
            <h1>GitHub Integration</h1>

            <LoginButton />

            <hr style={{ margin: "20px 0" }} />

            <ReposList />

            <hr style={{ margin: "20px 0" }} />

            <div>
                <h3>View PRs for Repository</h3>
                <input
                    type="text"
                    placeholder="Enter repo (owner/repo)"
                    value={selectedRepo}
                    onChange={(e) => setSelectedRepo(e.target.value)}
                />
                {selectedRepo && <PRsList repo={selectedRepo} />}
            </div>

            <hr style={{ margin: "20px 0" }} />

            <CreatePRForm />
        </div>
    );
}

```

### client/src/app/landing/page.tsx

```typescript
"use client";

import React, { useEffect } from "react";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import "locomotive-scroll/dist/locomotive-scroll.css";

function page() {
    const router = useRouter();
    useEffect(() => {
        (async () => {
            const LocomotiveScroll = (await import("locomotive-scroll"))
                .default;
            const locomotiveScroll = new LocomotiveScroll();
        })();
    }, []);

    return (
        <div
            data-scroll-container
            className="relative flex w-full flex-col items-center overflow-visible bg-o-header text-o-white"
        >
            <div className="relative flex h-[800px] w-full justify-center overflow-hidden">
                <div
                    className="absolute inset-0 bg-[url('/swirl.png')] bg-cover bg-center opacity-40"
                    data-scroll
                    data-scroll-speed="-1.0"
                />
                <div className="absolute left-1/2 top-[490px] z-40 -translate-x-1/2">
                    <img
                        src="/chase.png"
                        alt="Chase"
                        className="w-[500px] object-contain"
                        data-scroll
                        data-scroll-speed="1.0"
                    />
                </div>
                <div className="absolute left-1/2 top-[350px] z-30 -translate-x-1/2 translate-x-[-640px] scale-[130%]">
                    <img
                        src="/kendrick.png"
                        alt="Kendrick"
                        className="w-[500px] object-contain"
                        data-scroll
                        data-scroll-speed="0.2"
                    />
                </div>
                <div className="absolute left-1/2 top-[380px] z-30 -translate-x-1/2 translate-x-[-50px] scale-[100%]">
                    <img
                        src="/target.png"
                        alt="Target"
                        className="w-[500px] object-contain"
                        data-scroll
                        data-scroll-speed="0.3"
                    />
                </div>
                <div className="absolute left-1/2 top-[350px] z-40 -translate-x-1/2 translate-x-[270px] scale-[90%]">
                    <img
                        src="/warning.png"
                        alt="Warning"
                        className="w-[500px] object-contain"
                        data-scroll
                        data-scroll-speed="0.6"
                    />
                </div>
                {/* Browser Box */}
                <div className="z-10 mt-6 box-border h-[1000px] w-[1300px] flex-col justify-center rounded-2xl border border-o-outline bg-o-header px-[12px]">
                    {/* Browser Buttons */}
                    <div className="flex space-x-2 py-[14px]">
                        <div className="box-border h-4 w-4 rounded-full border border-o-outline" />
                        <div className="box-border h-4 w-4 rounded-full border border-o-outline" />
                        <div className="box-border h-4 w-4 rounded-full border border-o-outline" />
                    </div>
                    {/* Browser Content */}
                    <div className="relative box-border flex h-full w-full flex-col items-center rounded-lg border border-o-outline bg-[radial-gradient(circle_at_top_left,#191818,#131313)] text-center">
                        <div className="absolute inset-0 z-0 rounded-lg bg-[url('/dots.svg')] opacity-80" />
                        <span
                            style={{
                                fontVariationSettings: '"wght" 400, "wdth" 125',
                            }}
                            className="relative z-10 mt-12 text-5xl leading-[1.2]"
                        >
                            {/* <MaskTextSplit> */}
                            Automated Bug Detection & Resolution <br />
                            Integrated Seamlessly with your Codebase
                            {/* </MaskTextSplit> */}
                        </span>
                        <span className="z-20 mt-4 text-2xl text-o-muted">
                            Omni's AI agents discover, validate, and resolve
                            critical edge cases and <br />
                            infrastructure failures before they reach
                            production.
                        </span>
                        <Button className="z-10 mt-6 w-max rounded-full px-4" onClick={() => router.push("/")}>
                            <span
                                style={{
                                    fontVariationSettings:
                                        '"wght" 500, "wdth" 130',
                                }}
                                className="relative z-10 text-lg"
                            >
                                Get Started
                            </span>
                        </Button>
                    </div>
                </div>
            </div>
            <div className="z-40 flex min-h-screen w-full flex-col items-center border-t border-o-outline bg-o-background">
                {/* Stats Section */}
                <div className="relative w-full max-w-[1200px] px-4 py-24">
                    <div className="relative z-10 mt-12 grid grid-cols-3 gap-8">
                        <div className="relative box-border flex w-full flex-col items-center justify-start overflow-hidden rounded-md border border-o-outline bg-o-background-light p-4">
                            <div className="absolute inset-0 z-0 bg-[url('/dots.svg')] bg-repeat opacity-80" />
                            <div className="flex flex-col">
                                <span
                                    className="z-10 text-6xl leading-tight"
                                    style={{
                           
[truncated — 9179 more characters]
```

### client/src/app/api/auth/[...nextauth]/route.ts

```typescript
import { authOptions } from "@/lib/auth";
import NextAuth from "next-auth";

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

```

[56 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]