diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..4416917fbf1633afe418705f8aa49209f6390381 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,76 @@ +# ============================================================================= +# GitPilot - Hugging Face Spaces Dockerfile +# ============================================================================= +# Deploys GitPilot (FastAPI backend + React frontend) as a single container +# with OllaBridge Cloud integration for LLM inference. +# +# Architecture: +# React UI (Vite build) -> FastAPI backend -> OllaBridge Cloud / any LLM +# ============================================================================= + +# -- Stage 1: Build React frontend ------------------------------------------- +FROM node:20-slim AS frontend-builder + +WORKDIR /build + +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci --production=false + +COPY frontend/ ./ +RUN npm run build + +# -- Stage 2: Python runtime ------------------------------------------------- +FROM python:3.12-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +RUN useradd -m -u 1000 appuser && \ + mkdir -p /app /tmp/gitpilot && \ + chown -R appuser:appuser /app /tmp/gitpilot + +WORKDIR /app + +COPY pyproject.toml README.md ./ +COPY gitpilot ./gitpilot + +# Copy built frontend into gitpilot/web/ +COPY --from=frontend-builder /build/dist/ ./gitpilot/web/ + +# Install Python dependencies (pip-only for reliability on HF Spaces) +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir \ + "fastapi>=0.111.0" \ + "uvicorn[standard]>=0.30.0" \ + "httpx>=0.27.0" \ + "python-dotenv>=1.1.0" \ + "typer>=0.12.0" \ + "pydantic>=2.7.0" \ + "rich>=13.0.0" \ + "pyjwt[crypto]>=2.8.0" \ + "litellm" \ + "crewai>=0.76.9" \ + "crewai-tools>=0.13.4" \ + "anthropic>=0.39.0" && \ + pip install --no-cache-dir -e . + +COPY deploy/huggingface/start.sh /app/start.sh +RUN chmod +x /app/start.sh + +ENV PORT=7860 \ + HOST=0.0.0.0 \ + HOME=/tmp \ + GITPILOT_PROVIDER=ollabridge \ + OLLABRIDGE_BASE_URL=https://ruslanmv-ollabridge.hf.space \ + GITPILOT_OLLABRIDGE_MODEL=qwen2.5:1.5b \ + CORS_ORIGINS="*" \ + GITPILOT_CONFIG_DIR=/tmp/gitpilot + +USER appuser +EXPOSE 7860 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -f http://localhost:7860/api/health || exit 1 + +CMD ["/app/start.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bcd3db1b1e9d062629facdc4715df42b86e0b153 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +--- +title: GitPilot +emoji: "\U0001F916" +colorFrom: blue +colorTo: indigo +sdk: docker +app_port: 7860 +pinned: true +license: mit +short_description: Enterprise AI Coding Assistant for GitHub Repositories +--- + +# GitPilot β€” Hugging Face Spaces + +**Enterprise-grade AI coding assistant** for GitHub repositories with multi-LLM support, visual workflow insights, and intelligent code analysis. + +## What This Does + +This Space runs the full GitPilot stack: +1. **React Frontend** β€” Professional dark-theme UI with chat, file browser, and workflow visualization +2. **FastAPI Backend** β€” 80+ API endpoints for repository management, AI chat, planning, and execution +3. **Multi-Agent AI** β€” CrewAI orchestration with 7 switchable agent topologies + +## LLM Providers + +GitPilot connects to your favorite LLM provider. Configure in **Admin / LLM Settings**: + +| Provider | Default | API Key Required | +|---|---|---| +| **OllaBridge Cloud** (default) | `qwen2.5:1.5b` | No | +| OpenAI | `gpt-4o-mini` | Yes | +| Anthropic Claude | `claude-sonnet-4-5` | Yes | +| Ollama (local) | `llama3` | No | +| Custom endpoint | Any model | Optional | + +## Quick Start + +1. Open the Space UI +2. Enter your **GitHub Token** (Settings -> GitHub) +3. Select a repository from the sidebar +4. Start chatting with your AI coding assistant + +## API Endpoints + +| Endpoint | Description | +|---|---| +| `GET /api/health` | Health check | +| `POST /api/chat/message` | Chat with AI assistant | +| `POST /api/chat/plan` | Generate implementation plan | +| `GET /api/repos` | List repositories | +| `GET /api/settings` | View/update settings | +| `GET /docs` | Interactive API docs (Swagger) | + +## Connect to OllaBridge Cloud + +By default, GitPilot connects to [OllaBridge Cloud](https://huggingface.co/spaces/ruslanmv/ollabridge) for LLM inference. This provides free access to open-source models without needing API keys. + +To use your own OllaBridge instance: +1. Go to **Admin / LLM Settings** +2. Select **OllaBridge** provider +3. Enter your OllaBridge URL and model + +## Environment Variables + +Configure via HF Spaces secrets: + +| Variable | Description | Default | +|---|---|---| +| `GITPILOT_PROVIDER` | LLM provider | `ollabridge` | +| `OLLABRIDGE_BASE_URL` | OllaBridge Cloud URL | `https://ruslanmv-ollabridge.hf.space` | +| `GITHUB_TOKEN` | GitHub personal access token | - | +| `OPENAI_API_KEY` | OpenAI API key (if using OpenAI) | - | +| `ANTHROPIC_API_KEY` | Anthropic API key (if using Claude) | - | + +## Links + +- [GitPilot Repository](https://github.com/ruslanmv/gitpilot) +- [OllaBridge Cloud](https://huggingface.co/spaces/ruslanmv/ollabridge) +- [Documentation](https://github.com/ruslanmv/gitpilot#readme) diff --git a/REPO_README.md b/REPO_README.md new file mode 100644 index 0000000000000000000000000000000000000000..75b69900e23aa65b264f09a30c195023f2a727f6 --- /dev/null +++ b/REPO_README.md @@ -0,0 +1,1241 @@ +# GitPilot + +
+ +**πŸš€ The AI Coding Companion That Understands Your GitHub Repositories** + +[![PyPI version](https://badge.fury.io/py/gitcopilot.svg)](https://pypi.org/project/gitcopilot/) +[![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![GitHub stars](https://img.shields.io/github/stars/ruslanmv/gitpilot.svg?style=social&label=Star)](https://github.com/ruslanmv/gitpilot) + +[Installation](#-installation) β€’ [Quick Start](#-quick-start) β€’ [Example Usage](#-example-usage) β€’ [Documentation](#-complete-workflow-guide) β€’ [Contributing](#-contributing) + +
+ +--- + +## ⭐ Star Us on GitHub! + +**If GitPilot saves you time or helps your projects, please give us a star!** ⭐ + +Your support helps us: +- πŸš€ Build new features faster +- πŸ› Fix bugs and improve stability +- πŸ“š Create better documentation +- 🌍 Grow the community + +**[⭐ Click here to star GitPilot on GitHub](https://github.com/ruslanmv/gitpilot)** β€” it takes just 2 seconds and means the world to us! πŸ’™ + +--- + +## 🌟 What is GitPilot? + +GitPilot is a **production-ready agentic AI assistant** that acts as your intelligent coding companion for GitHub repositories. Unlike copy-paste coding assistants, GitPilot: + +* **🧠 Understands your entire codebase** – Analyzes project structure, file relationships, and cross-repository dependencies +* **πŸ“‹ Shows clear plans before executing** – Always presents an "Answer + Action Plan" with structured file operations (CREATE/MODIFY/DELETE/READ) +* **πŸ”„ Manages multiple LLM providers** – Seamlessly switch between OpenAI, Claude, Watsonx, and Ollama with smart model routing +* **πŸ‘οΈ Visualizes agent workflows** – See exactly how the multi-agent system thinks and operates +* **πŸ”— Works locally and with GitHub** – Full local file editing, terminal execution, and GitHub integration +* **πŸ”Œ Extensible ecosystem** – MCP server connections, plugin marketplace, /command skills, and IDE extensions +* **πŸ›‘οΈ Built-in security scanning** – AI-powered vulnerability detection beyond traditional SAST tools +* **πŸ€– Self-improving agents** – Learns from outcomes and adapts strategies per project over time + +**Built with CrewAI, FastAPI, and React** β€” GitPilot combines the power of multi-agent AI with a beautiful, modern web interface and the extensibility of a platform. + +![](assets/2025-11-15-01-18-56.png) + +--- + +## ✨ Example Usage + +### Installation + +```bash +# Install from PyPI +pip install gitcopilot + +# Set your GitHub token +export GITPILOT_GITHUB_TOKEN="ghp_your_token_here" + +# Set your LLM API key (choose one) +export OPENAI_API_KEY="sk-..." +# or +export ANTHROPIC_API_KEY="sk-ant-..." +# or +export WATSONX_API_KEY="your_api_key" +export WATSONX_PROJECT_ID="your_project_id" + +# Launch GitPilot +gitpilot +``` + +### Basic Workflow + +```bash +# 1. Start GitPilot (opens in browser automatically) +gitpilot + +# 2. Select a repository from the sidebar +# β†’ Search your GitHub repos +# β†’ Click to connect + +# 3. Ask GitPilot to make changes +# Example: "Add a new API endpoint for user login with JWT authentication" + +# 4. Review the generated plan +# βœ“ Answer: Explanation of what will be done +# βœ“ Action Plan: Structured steps with file operations +# - CREATE: src/api/auth.py +# - MODIFY: src/routes/index.py +# - READ: README.md (for analysis) +# - DELETE: deprecated/old_auth.py + +# 5. Approve & Execute +# β†’ GitPilot applies changes to your repository +# β†’ View execution log with detailed results +``` + +### Real-World Examples + +**Add a new feature:** +``` +User: Add a rate limiter to all API endpoints to prevent abuse + +GitPilot Response: +Answer: I'll implement rate limiting using a middleware approach with Redis +for distributed rate limiting support. + +Action Plan: + Step 1: Create rate limiter middleware + - CREATE: src/middleware/rate_limiter.py + Step 2: Add Redis client configuration + - MODIFY: src/config.py + Step 3: Apply middleware to all routes + - MODIFY: src/main.py + Step 4: Add rate limit tests + - CREATE: tests/test_rate_limiter.py +``` + +**Refactor existing code:** +``` +User: Refactor the user authentication to use async/await instead of callbacks + +GitPilot Response: +Answer: I'll convert all authentication functions to async/await pattern, +update the database queries, and ensure all tests pass. + +Action Plan: + Step 1: Analyze current authentication implementation + - READ: src/services/auth_service.py + - READ: src/routes/auth.py + Step 2: Convert auth service to async + - MODIFY: src/services/auth_service.py (12 functions) + Step 3: Update route handlers + - MODIFY: src/routes/auth.py + Step 4: Migrate database calls + - MODIFY: src/db/user_repository.py + Step 5: Update unit tests + - MODIFY: tests/test_auth_service.py +``` + +**Fix a bug:** +``` +User: Fix the bug where empty email validation returns a 500 error instead of 400 + +GitPilot Response: +Answer: I'll add proper input validation with Pydantic models and return +appropriate HTTP 400 errors with helpful messages. + +Action Plan: + Step 1: Add Pydantic validation model + - CREATE: src/models/user_input.py + Step 2: Update login endpoint with validation + - MODIFY: src/routes/auth.py + Step 3: Add validation error handler + - MODIFY: src/main.py + Step 4: Add test cases for validation + - MODIFY: tests/test_validation.py +``` + +--- + +## 🎯 Key Features + +### 1. **Answer + Action Plan UX** +Every AI response is structured into two clear sections: +- **Answer**: Natural language explanation of what will be done and why +- **Action Plan**: Structured list of steps with explicit file operations: + - 🟒 **CREATE** – New files to be added + - πŸ”΅ **MODIFY** – Existing files to be changed + - πŸ”΄ **DELETE** – Files to be removed + - πŸ“– **READ** – Files to analyze (no changes) + +See exactly what will happen before approving execution! + +### 2. **Full Multi-LLM Support with Smart Routing** ✨ +All four LLM providers are fully operational and tested: +- βœ… **OpenAI** – GPT-4o, GPT-4o-mini, GPT-4-turbo +- βœ… **Claude (Anthropic)** – Claude 4.5 Sonnet, Claude 3 Opus +- βœ… **IBM Watsonx.ai** – Llama 3.3, Granite 3.x models +- βœ… **Ollama** – Local models (Llama3, Mistral, CodeLlama, Phi3) + +Switch between providers seamlessly through the Admin UI without restart. The **smart model router** automatically selects the optimal model (fast/balanced/powerful) for each task based on complexity analysis, keeping costs low while maintaining quality where it matters. + +### 3. **Local Workspace & Terminal Execution** πŸ†• +GitPilot now works directly on your local filesystem β€” just like Claude Code: +- **Local file editing** – Read, write, search, and delete files in a sandboxed workspace +- **Terminal execution** – Run shell commands (`npm test`, `make build`, `python -m pytest`) with timeout and output capping +- **Git operations** – Commit, push, diff, stash, merge β€” all from the workspace +- **Path traversal protection** – All file operations are sandboxed to prevent escaping the workspace directory + +### 4. **Session Management & Checkpoints** πŸ†• +Persistent sessions that survive restarts and support time-travel: +- **Resume any session** – Pick up exactly where you left off +- **Fork sessions** – Branch a conversation to try different approaches +- **Checkpoint & rewind** – Snapshot workspace state at key moments and roll back if something goes wrong +- **CI/CD headless mode** – Run GitPilot non-interactively via `gitpilot run --headless` for automation pipelines + +### 5. **Hook System & Permissions** πŸ†• +Fine-grained control over what agents can do: +- **Lifecycle hooks** – Register shell commands that fire on events like `pre_commit`, `post_edit`, or `pre_push` β€” blocking hooks can veto risky actions +- **Three permission modes** – `normal` (ask before risky ops), `plan` (read-only), `auto` (approve everything) +- **Path-based blocking** – Prevent agents from touching `.env`, `*.pem`, or any sensitive file pattern + +### 6. **Project Context Memory (GITPILOT.md)** πŸ†• +The equivalent of Claude Code's `CLAUDE.md` β€” teach your agents project-specific conventions: +- Create `.gitpilot/GITPILOT.md` with your code style, testing, and commit message rules +- Add modular rules in `.gitpilot/rules/*.md` +- Agents automatically learn patterns from session outcomes and store them in `.gitpilot/memory.json` + +### 7. **MCP Server Connections** πŸ†• +Connect GitPilot to any MCP (Model Context Protocol) server β€” databases, Slack, Figma, Sentry, and more: +- **stdio, HTTP, and SSE transports** – Connect to local subprocess servers or remote endpoints +- **JSON-RPC 2.0** – Full protocol compliance with tool discovery and invocation +- **Auto-wrap as CrewAI tools** – MCP tools are automatically available to all agents +- Configure servers in `.gitpilot/mcp.json` with environment variable expansion + +### 8. **Plugin Marketplace & /Command Skills** πŸ†• +Extend GitPilot with community plugins and project-specific skills: +- **Install plugins** from git URLs or local paths β€” each plugin can provide skills, hooks, and MCP configs +- **Define skills** as markdown files in `.gitpilot/skills/*.md` with YAML front-matter and template variables +- **Invoke skills** via `/command` syntax in chat or the CLI: `gitpilot skill review` +- **Auto-trigger skills** based on context patterns (e.g., auto-run lint after edits) + +### 9. **Vision & Image Analysis** πŸ†• +Multimodal capabilities powered by OpenAI, Anthropic, and Ollama vision models: +- **Analyse screenshots** – Describe UI bugs, extract text via OCR, review design mockups +- **Compare before/after** – Detect visual regressions between two screenshots +- **Base64 encoding** with format validation (PNG, JPG, GIF, WebP, BMP, SVG) and 20 MB size limit + +### 10. **AI-Powered Security Scanner** πŸ†• +Go beyond traditional SAST tools with pattern-based and context-aware vulnerability detection: +- **Secret detection** – AWS keys, GitHub tokens, JWTs, Slack tokens, private keys, passwords in code +- **Code vulnerability patterns** – SQL injection, command injection, XSS, SSRF, path traversal, weak crypto, insecure CORS, disabled SSL verification +- **Diff scanning** – Scan only added lines in a git diff for CI/CD integration +- **CWE mapping** – Every finding links to its CWE identifier with actionable recommendations +- **CLI command** – `gitpilot scan /path/to/repo` with confidence thresholds and severity summaries + +### 11. **Predictive Workflow Engine** πŸ†• +Proactive suggestions based on what you just did: +- After merging a PR β†’ suggest updating the changelog +- After test failure β†’ suggest debugging approach +- After dependency update β†’ suggest running full test suite +- After editing security-sensitive code β†’ suggest security review +- 8 built-in trigger rules with configurable cooldowns and relevance scoring +- Add custom prediction rules for your own workflows + +### 12. **Parallel Multi-Agent Teams** πŸ†• +Split large tasks across multiple agents working simultaneously: +- **Task decomposition** – Automatically split complex tasks into independent subtasks +- **Parallel execution** – Agents work concurrently via `asyncio.gather`, each on its own git worktree +- **Conflict detection** – Merging detects file-level conflicts when multiple agents touch the same files +- **Custom or auto-generated plans** β€” provide your own subtask descriptions or let the engine split evenly + +### 13. **Self-Improving Agents** πŸ†• +GitPilot learns from every interaction and becomes specialised to each project over time: +- **Outcome evaluation** – Checks signals like tests_passed, pr_approved, error_fixed +- **Pattern extraction** – Generates natural-language insights from successes and failures +- **Per-repo persistence** – Learned strategies are stored in JSON and loaded in future sessions +- **Preferred style tracking** – Record project-specific code style preferences that agents follow + +### 14. **Cross-Repository Intelligence** πŸ†• +Understand dependencies and impact across your entire codebase: +- **Dependency graph construction** – Parses `package.json`, `requirements.txt`, `pyproject.toml`, and `go.mod` +- **Impact analysis** – BFS traversal to find all affected repos when updating a dependency, with risk assessment (low/medium/high/critical) +- **Shared convention detection** – Find common config patterns across multiple repos +- **Migration planning** – Generate step-by-step migration plans when replacing one package with another + +### 15. **Natural Language Database Queries** πŸ†• +Query your project's databases using plain English through MCP connections: +- **NL-to-SQL translation** – Rule-based translation with schema-aware table and column matching +- **Safety validation** – Read-only mode blocks INSERT/UPDATE/DELETE; read-write mode blocks DROP/TRUNCATE +- **Query explanation** – Translate SQL back to human-readable descriptions +- **Tabular output** – Results formatted as plain-text tables for CLI or API consumption + +### 16. **IDE Extensions** πŸ†• +Use GitPilot from your favourite editor: +- **VS Code extension** – Sidebar chat panel, inline actions, keybindings (Ctrl+Shift+G), skill invocation, and server configuration +- Connects to the GitPilot API server β€” all the same agents and tools available from within your editor + +### 17. **Topology Registry β€” Switchable Agent Architectures** πŸ†• +Seven pre-wired agent topologies that control routing, execution, and visualization in one place: +- **System architectures** (Default fan-out, GitPilot Code ReAct loop) and **task pipelines** (Feature Builder, Bug Hunter, Code Inspector, Architect Mode, Quick Fix) β€” each with its own agent roster, execution style, and flow graph +- **Zero-latency classifier** selects the best topology from the user's message without an extra LLM call, while the Flow Viewer dropdown lets you switch or pin a topology at any time + +### 18. **Agent Flow Viewer** +Interactive visual representation of the CrewAI multi-agent system using ReactFlow: +- **Repository Explorer** – Thoroughly explores codebase structure +- **Refactor Planner** – Creates safe, step-by-step plans with verified file operations +- **Code Writer** – Implements approved changes with AI-generated content +- **Code Reviewer** – Reviews for quality and safety +- **Local Editor & Terminal** – Direct file editing and shell execution agents +- **GitHub API Tools** – Manages file operations and commits + +### 19. **Admin / Settings Console** +Full-featured LLM provider configuration with: +- **OpenAI** – API key, model selection, optional base URL +- **Claude** – API key, model selection (Claude 4.5 Sonnet recommended) +- **IBM Watsonx.ai** – API key, project ID, model selection, regional URLs +- **Ollama** – Base URL (local), model selection + +Settings are persisted to `~/.gitpilot/settings.json` and survive restarts. + +### 20. **MCP / A2A Agent Integration (ContextForge Compatible)** +GitPilot can optionally run as an **A2A agent server** that can be **imported by URL** into **MCP ContextForge (MCP Gateway)** and exposed as MCP tools. This makes GitPilot usable not only from the web UI, but also from: +- MCP-enabled IDEs and CLIs +- automation pipelines (CI/CD) +- other AI agents orchestrated by an MCP gateway + +A2A mode is **feature-flagged** and does **not** affect the existing UI/API unless enabled. + +--- + +## πŸš€ Installation + +### From PyPI (Recommended) + +```bash +pip install gitcopilot +``` + +### From Source + +```bash +# Clone the repository +git clone https://github.com/ruslanmv/gitpilot.git +cd gitpilot + +# Install dependencies +make install + +# Build frontend +make frontend-build + +# Run GitPilot +gitpilot +``` + +### Using Docker (Coming Soon) + +```bash +docker pull ruslanmv/gitpilot +docker run -p 8000:8000 -e GITHUB_TOKEN=your_token ruslanmv/gitpilot +``` + +--- + +## πŸš€ Quick Start + +### Prerequisites + +- **Python 3.11+** +- **GitHub Personal Access Token** (with `repo` scope) +- **API key** for at least one LLM provider (OpenAI, Claude, Watsonx, or Ollama) + +### 1. Configure GitHub Access + +Create a **GitHub Personal Access Token** at https://github.com/settings/tokens with `repo` scope: + +```bash +export GITPILOT_GITHUB_TOKEN="ghp_XXXXXXXXXXXXXXXXXXXX" +# or +export GITHUB_TOKEN="ghp_XXXXXXXXXXXXXXXXXXXX" +``` + +### 2. Configure LLM Provider + +You can configure providers via the web UI's Admin/Settings page, or set environment variables: + +#### OpenAI +```bash +export OPENAI_API_KEY="sk-..." +export GITPILOT_OPENAI_MODEL="gpt-4o-mini" # optional +``` + +#### Claude (Anthropic) +```bash +export ANTHROPIC_API_KEY="sk-ant-..." +export GITPILOT_CLAUDE_MODEL="claude-3-5-sonnet-20241022" # optional +``` + +**Note:** Claude integration now includes automatic environment variable configuration for seamless CrewAI compatibility. + +#### IBM Watsonx.ai +```bash +export WATSONX_API_KEY="your-watsonx-api-key" +export WATSONX_PROJECT_ID="your-project-id" # Required! +export WATSONX_BASE_URL="https://us-south.ml.cloud.ibm.com" # optional, region-specific +export GITPILOT_WATSONX_MODEL="ibm/granite-3-8b-instruct" # optional +``` + +**Note:** Watsonx integration requires both API key and Project ID for proper authentication. + +#### Ollama (Local Models) +```bash +export OLLAMA_BASE_URL="http://localhost:11434" +export GITPILOT_OLLAMA_MODEL="llama3" # optional +``` + +### 3. Run GitPilot + +```bash +gitpilot +``` + +This will: +1. Start the FastAPI backend on `http://127.0.0.1:8000` +2. Serve the web UI at the root URL +3. Open your default browser automatically + +Alternative commands: +```bash +# Custom host and port +gitpilot serve --host 0.0.0.0 --port 8000 + +# API only (no browser auto-open) +gitpilot-api + +# Headless mode for CI/CD +gitpilot run --repo owner/repo --message "fix the login bug" --headless + +# Initialize project conventions +gitpilot init + +# Security scan +gitpilot scan /path/to/repo + +# Get proactive suggestions +gitpilot predict "Tests failed in auth module" + +# Manage plugins +gitpilot plugin install https://github.com/example/my-plugin.git +gitpilot plugin list + +# List and invoke skills +gitpilot skill list +gitpilot skill review + +# List available models +gitpilot list-models --provider openai + +# Using make (for development) +make run +``` + +--- + +## πŸ”Œ MCP / A2A Integration πŸ†• + +GitPilot can run as a **self-contained MCP server** with A2A endpoints. You can use it standalone or optionally integrate with **MCP ContextForge gateway** for advanced multi-agent workflows. + +### Two deployment modes +1. **Simple MCP Server** (recommended for most users) + - Just GitPilot with A2A endpoints enabled + - Direct MCP client connections + - Use `make mcp` to deploy + +2. **Full MCP Gateway** (optional - with ContextForge) + - Complete MCP ContextForge infrastructure + - Advanced gateway features and orchestration + - Use `make gateway` to deploy + +### Why this matters +- **Direct MCP access**: Use GitPilot from MCP-enabled IDEs/CLIs without additional infrastructure +- **No UI required**: Call GitPilot programmatically from automation pipelines +- **Composable**: GitPilot can act as the "repo editor agent" inside larger multi-agent workflows +- **Gateway optional**: Full ContextForge gateway only needed for advanced orchestration scenarios + +### Enable A2A mode (does not change existing behavior) +A2A endpoints are disabled by default. Enable them using environment variables: + +```bash +export GITPILOT_ENABLE_A2A=true + +# Recommended: protect the A2A endpoint (gateway will inject this header) +export GITPILOT_A2A_REQUIRE_AUTH=true +export GITPILOT_A2A_SHARED_SECRET="REPLACE_WITH_LONG_RANDOM_SECRET" +``` + +Then start GitPilot as usual: + +```bash +gitpilot serve --host 0.0.0.0 --port 8000 +``` + +### A2A endpoints + +When enabled, GitPilot exposes: + +* `POST /a2a/invoke` – A2A invoke endpoint (JSON-RPC + envelope fallback) +* `POST /a2a/v1/invoke` – Versioned alias (recommended for gateways) +* `GET /a2a/health` – Health check +* `GET /a2a/manifest` – Capability discovery (methods + auth hints) + +### Auth model (gateway-friendly) + +GitPilot supports a gateway-friendly model: + +* **Gateway β†’ GitPilot authentication**: + * `X-A2A-Secret: ` *(recommended)* + or + * `Authorization: Bearer ` + +* **GitHub auth (optional)**: + * `X-Github-Token: ` + *(recommended when not using a GitHub App internally)* + +> Tip: Avoid sending GitHub tokens in request bodies. Prefer headers to reduce accidental logging exposure. + +### Register GitPilot in MCP ContextForge (Optional - Gateway Only) + +**Note:** This section is only needed if you're using the **full MCP ContextForge gateway** (`make gateway`). If you're using the simple MCP server (`make mcp`), you can connect MCP clients directly to GitPilot's A2A endpoints. + +Once the full gateway stack is deployed, register GitPilot as an A2A agent in ContextForge by providing the endpoint URL (note trailing `/` is recommended for JSON-RPC mode): + +* Endpoint URL: + * `https://YOUR_GITPILOT_DOMAIN/a2a/v1/invoke/` +* Agent type: + * `jsonrpc` +* Inject auth header: + * `X-A2A-Secret: ` + +After registration, MCP clients connected to the gateway will see GitPilot as an MCP tool (name depends on the gateway configuration). + +### Supported A2A methods (stable contract) + +GitPilot exposes a small, composable set of methods: + +* `repo.connect` – validate access and return repo metadata +* `repo.tree` – list repository tree / files +* `repo.read` – read a file +* `repo.write` – create/update a file (commit) +* `plan.generate` – generate an action plan for a goal +* `plan.execute` – execute an approved plan +* `repo.search` *(optional)* – search repositories + +These methods are designed to remain stable even if internal implementation changes. + +### Quick Start Deployment + +#### Option 1: Simple MCP Server (Recommended) +```bash +# Configure MCP server +cp .env.a2a.example .env.a2a +# Edit .env.a2a and set GITPILOT_A2A_SHARED_SECRET + +# Start GitPilot MCP server +make mcp +``` + +This starts GitPilot with A2A endpoints only - perfect for most use cases. + +#### Option 2: Full MCP Gateway (Optional - with ContextForge) +Only needed if you want the complete MCP ContextForge gateway infrastructure: + +```bash +# 1. Download ContextForge and place at: deploy/a2a-mcp/mcp-context-forge +# 2. Configure environment +cd deploy/a2a-mcp +cp .env.stack.example .env.stack +# Edit .env.stack and set secrets + +# 3. Start full gateway stack +cd ../.. +make gateway + +# 4. Register GitPilot agent in ContextForge +export CF_ADMIN_BEARER="" +export GITPILOT_A2A_SECRET="" +make gateway-register +``` + +**Note:** Most users only need `make mcp`. The full gateway is optional for advanced setups. + +See `deploy/a2a-mcp/README.md` for detailed deployment instructions. + +### Cloud deployment note +Because the A2A adapter is stateless, GitPilot can be deployed with multiple replicas behind a load balancer. For long-running executions, consider adding async job execution (Redis/Postgres) in a future release. + +--- + +## πŸ“– Complete Workflow Guide + +### Initial Setup + +**Step 1: Launch GitPilot** +```bash +gitpilot +``` +Your browser opens to `http://127.0.0.1:8000` + +**Step 2: Configure LLM Provider** +1. Click **"βš™οΈ Admin / Settings"** in the sidebar +2. Select your preferred provider (e.g., OpenAI, Claude, Watsonx, or Ollama) +3. Enter your credentials: + - **OpenAI**: API key + model + - **Claude**: API key + model + - **Watsonx**: API key + Project ID + model + base URL + - **Ollama**: Base URL + model +4. Click **"Save settings"** +5. See the success message confirming your settings are saved + +**Step 3: Connect to GitHub Repository** +1. Click **"πŸ“ Workspace"** to return to the main interface +2. In the sidebar, use the search box to find your repository +3. Click **"Search my repos"** to list all accessible repositories +4. Click on any repository to connect +5. The **Project Context Panel** will show repository information +6. Use the **Refresh** button to update permissions and file counts + +### Development Workflow + +**Step 1: Browse Your Codebase** +- The **Project Context** panel shows repository metadata +- Browse the file tree to understand structure +- Click on files to preview their contents +- Use the **Refresh** button to update the file tree after changes + +**Step 2: Describe Your Task** +In the chat panel, describe what you want in natural language: + +**Example 1: Add a Feature** +``` +Add a new API endpoint at /api/users/{id}/profile that returns +user profile information including name, email, and bio. +``` + +**Example 2: Refactor Code** +``` +Refactor the authentication middleware to use JWT tokens +instead of session cookies. Update all related tests. +``` + +**Example 3: Analyze and Generate** +``` +Analyze the README.md file and generate Python example code +that demonstrates the main features. +``` + +**Example 4: Fix a Bug** +``` +The login endpoint is returning 500 errors when the email +field is empty. Add proper validation and return a 400 +with a helpful error message. +``` + +**Step 3: Review the Answer + Action Plan** +GitPilot will show you: + +**Answer Section:** +- Clear explanation of what will be done +- Why this approach was chosen +- Overall summary of changes + +**Action Plan Section:** +- Numbered steps with descriptions +- File operations with colored pills: + - 🟒 CREATE – Files to be created + - πŸ”΅ MODIFY – Files to be modified + - πŸ”΄ DELETE – Files to be removed + - πŸ“– READ – Files to analyze (no changes) +- Summary totals (e.g., "2 files to create, 3 files to modify, 1 file to read") +- Risk warnings when applicable + +**Step 4: Execute or Refine** +- If the plan looks good: Click **"Approve & Execute"** +- If you want changes: Provide feedback in the chat + ``` + The plan looks good, but please also add rate limiting + to the new endpoint to prevent abuse. + ``` +- GitPilot will update the plan based on your feedback + +**Step 5: View Execution Results** +After execution, see a detailed log: +``` +Step 1: Create authentication endpoint + βœ“ Created src/api/auth.py + βœ“ Modified src/routes/index.py + +Step 2: Add authentication tests + βœ“ Created tests/test_auth.py + ℹ️ READ-only: inspected README.md +``` + +**Step 6: Refresh File Tree** +After agent operations: +- Click the **Refresh** button in the file tree header +- See newly created/modified files appear +- Verify changes were applied correctly + +**Step 7: View Agent Workflow (Optional)** +Click **"πŸ”„ Agent Flow"** to see: +- How agents collaborate (Explorer β†’ Planner β†’ Code Writer β†’ Reviewer) +- Data flow between components +- The complete multi-agent system architecture + +--- + +## πŸ—οΈ Architecture + +### Frontend Structure + +``` +frontend/ +β”œβ”€β”€ App.jsx # Main application with navigation +β”œβ”€β”€ components/ +β”‚ β”œβ”€β”€ AssistantMessage.jsx # Answer + Action Plan display +β”‚ β”œβ”€β”€ ChatPanel.jsx # AI chat interface +β”‚ β”œβ”€β”€ FileTree.jsx # Repository file browser with refresh +β”‚ β”œβ”€β”€ FlowViewer.jsx # Agent workflow visualization +β”‚ β”œβ”€β”€ Footer.jsx # Footer with GitHub star CTA +β”‚ β”œβ”€β”€ LlmSettings.jsx # Provider configuration UI +β”‚ β”œβ”€β”€ PlanView.jsx # Enhanced plan rendering with READ support +β”‚ β”œβ”€β”€ ProjectContextPanel.jsx # Repository context with refresh +β”‚ └── RepoSelector.jsx # Repository search/selection +β”œβ”€β”€ styles.css # Global styles with dark theme +β”œβ”€β”€ index.html # Entry point +└── package.json # Dependencies (React, ReactFlow) +``` + +### Backend Structure + +``` +gitpilot/ +β”œβ”€β”€ __init__.py +β”œβ”€β”€ api.py # FastAPI routes (80+ endpoints) +β”œβ”€β”€ agentic.py # CrewAI multi-agent orchestration +β”œβ”€β”€ agent_router.py # NLP-based request routing +β”œβ”€β”€ agent_tools.py # GitHub API exploration tools +β”œβ”€β”€ cli.py # CLI (serve, run, scan, predict, plugin, skill) +β”œβ”€β”€ github_api.py # GitHub REST API client +β”œβ”€β”€ github_app.py # GitHub App installation management +β”œβ”€β”€ github_issues.py # Issue CRUD operations +β”œβ”€β”€ github_pulls.py # Pull request operations +β”œβ”€β”€ github_search.py # Code, issue, repo, user search +β”œβ”€β”€ github_oauth.py # OAuth web + device flow +β”œβ”€β”€ llm_provider.py # Multi-provider LLM factory +β”œβ”€β”€ settings.py # Configuration management +β”œβ”€β”€ topology_registry.py # Switchable agent topologies (7 built-in) +β”‚ +β”‚ # --- Phase 1: Feature Parity --- +β”œβ”€β”€ workspace.py # Local git clone & file operations +β”œβ”€β”€ local_tools.py # CrewAI tools for local editing +β”œβ”€β”€ terminal.py # Sandboxed shell command execution +β”œβ”€β”€ session.py # Session persistence & checkpoints +β”œβ”€β”€ hooks.py # Lifecycle event hooks +β”œβ”€β”€ memory.py # GITPILOT.md context memory +β”œβ”€β”€ permissions.py # Fine-grained permission policies +β”œβ”€β”€ headless.py # CI/CD headless execution mode +β”‚ +β”‚ # --- Phase 2: Ecosystem Superiority --- +β”œβ”€β”€ mcp_client.py # MCP server connector (stdio/HTTP/SSE) +β”œβ”€β”€ plugins.py # Plugin marketplace & management +β”œβ”€β”€ skills.py # /command skill system +β”œβ”€β”€ vision.py # Multimodal image analysis +β”œβ”€β”€ smart_model_router.py # Auto-route tasks to optimal model +β”‚ +β”‚ # --- Phase 3: Intelligence Superiority --- +β”œβ”€β”€ agent_teams.py # Parallel multi-agent on git worktrees +β”œβ”€β”€ learning.py # Self-improving agents (per-repo) +β”œβ”€β”€ cross_repo.py # Dependency graphs & impact analysis +β”œβ”€β”€ predictions.py # Predictive workflow suggestions +β”œβ”€β”€ security.py # AI-powered security scanner +β”œβ”€β”€ nl_database.py # Natural language SQL via MCP +β”‚ +β”œβ”€β”€ a2a_adapter.py # Optional A2A/MCP gateway adapter +└── web/ # Production frontend build + β”œβ”€β”€ index.html + └── assets/ +``` + +### API Endpoints (80+) + +#### Repository Management +- `GET /api/repos` – List user repositories (paginated + search) +- `GET /api/repos/{owner}/{repo}/tree` – Get repository file tree +- `GET /api/repos/{owner}/{repo}/file` – Get file contents +- `POST /api/repos/{owner}/{repo}/file` – Update/commit file + +#### Issues & Pull Requests +- `GET/POST/PATCH /api/repos/{owner}/{repo}/issues` – Full issue CRUD +- `GET/POST /api/repos/{owner}/{repo}/pulls` – Pull request management +- `PUT /api/repos/{owner}/{repo}/pulls/{n}/merge` – Merge pull request + +#### Search +- `GET /api/search/code` – Search code across GitHub +- `GET /api/search/issues` – Search issues and pull requests +- `GET /api/search/repositories` – Search repositories +- `GET /api/search/users` – Search users and organisations + +#### Chat & Planning +- `POST /api/chat/message` – Conversational dispatcher (auto-routes to agents) +- `POST /api/chat/plan` – Generate execution plan +- `POST /api/chat/execute` – Execute approved plan +- `POST /api/chat/execute-with-pr` – Execute and auto-create pull request +- `POST /api/chat/route` – Preview routing without execution + +#### Sessions & Hooks (Phase 1) +- `GET/POST/DELETE /api/sessions` – Session CRUD +- `POST /api/sessions/{id}/checkpoint` – Create checkpoint +- `GET/POST/DELETE /api/hooks` – Hook registration +- `GET/PUT /api/permissions` – Permission mode management +- `GET/POST /api/repos/{owner}/{repo}/context` – Project memory + +#### MCP, Plugins & Skills (Phase 2) +- `GET /api/mcp/servers` – List configured MCP servers +- `POST /api/mcp/connect/{name}` – Connect to MCP server +- `POST /api/mcp/call` – Call a tool on a connected server +- `GET/POST/DELETE /api/plugins` – Plugin management +- `GET/POST /api/skills` – Skill listing and invocation +- `POST /api/vision/analyze` – Analyse an image with a text prompt +- `POST /api/model-router/select` – Preview model selection for a task + +#### Intelligence (Phase 3) +- `POST /api/agent-teams/plan` – Split task into parallel subtasks +- `POST /api/agent-teams/execute` – Execute subtasks in parallel +- `POST /api/learning/evaluate` – Evaluate action outcome for learning +- `GET /api/learning/insights/{owner}/{repo}` – Get learned insights +- `POST /api/cross-repo/dependencies` – Build dependency graph +- `POST /api/cross-repo/impact` – Impact analysis for package update +- `POST /api/predictions/suggest` – Get proactive suggestions +- `POST /api/security/scan-file` – Scan file for vulnerabilities +- `POST /api/security/scan-directory` – Recursive directory scan +- `POST /api/security/scan-diff` – Scan git diff for issues +- `POST /api/nl-database/translate` – Natural language to SQL +- `POST /api/nl-database/explain` – Explain SQL in plain English + +#### Workflow Visualization & Topologies +- `GET /api/flow/current` – Get agent workflow graph (supports `?topology=` param) +- `GET /api/flow/topologies` – List available agent topologies +- `GET /api/flow/topology/{id}` – Get graph for a specific topology +- `POST /api/flow/classify` – Auto-detect best topology for a message +- `GET /api/settings/topology` – Read saved topology preference +- `POST /api/settings/topology` – Save topology preference + +#### A2A / MCP Integration (Optional) +Enabled only when `GITPILOT_ENABLE_A2A=true`: + +- `POST /a2a/invoke` – A2A invoke endpoint (JSON-RPC + envelope) +- `POST /a2a/v1/invoke` – Versioned A2A endpoint (recommended) +- `GET /a2a/health` – A2A health check +- `GET /a2a/manifest` – A2A capability discovery (methods + schemas) + +--- + +## πŸ› οΈ Development + +### Build Commands (Makefile) + +```bash +# Install all dependencies +make install + +# Install frontend dependencies only +make frontend-install + +# Build frontend for production +make frontend-build + +# Run development server +make run + +# Run tests (846 tests across 28 test files) +make test + +# Lint code +make lint + +# Format code +make fmt + +# Build Python package +make build + +# Clean build artifacts +make clean + +# MCP Server Deployment (Simple - Recommended) +make mcp # Start GitPilot MCP server (A2A endpoints) +make mcp-down # Stop GitPilot MCP server +make mcp-logs # View MCP server logs + +# MCP Gateway Deployment (Optional - Full ContextForge Stack) +make gateway # Start GitPilot + MCP ContextForge gateway +make gateway-down # Stop MCP ContextForge gateway +make gateway-logs # View gateway logs +make gateway-register # Register agent in ContextForge +``` + +### CLI Commands + +```bash +gitpilot # Start server with web UI (default) +gitpilot serve --host 0.0.0.0 # Custom host/port +gitpilot config # Show current configuration +gitpilot version # Show version +gitpilot run -r owner/repo -m "task" # Headless execution for CI/CD +gitpilot init # Initialize .gitpilot/ with template +gitpilot scan /path # Security scan a directory or file +gitpilot predict "context text" # Get proactive suggestions +gitpilot plugin install # Install a plugin +gitpilot plugin list # List installed plugins +gitpilot skill list # List available skills +gitpilot skill # Invoke a skill +gitpilot list-models # List LLM models for active provider +``` + +### Frontend Development + +```bash +cd frontend + +# Install dependencies +npm install + +# Development mode with hot reload +npm run dev + +# Build for production +npm run build +``` + +--- + +## πŸ“¦ Publishing to PyPI + +GitPilot uses automated publishing via GitHub Actions with OIDC-based trusted publishing. + +### Automated Release Workflow + +1. **Update version** in `gitpilot/version.py` +2. **Create and publish a GitHub release** (tag format: `vX.Y.Z`) +3. **GitHub Actions automatically**: + - Builds source distribution and wheel + - Uploads artifacts to the release + - Publishes to PyPI via trusted publishing + +See [.github/workflows/release.yml](.github/workflows/release.yml) for details. + +### Manual Publishing (Alternative) + +```bash +# Build distributions +make build + +# Publish to TestPyPI +make publish-test + +# Publish to PyPI +make publish +``` + +--- + +## πŸ“Έ Screenshots + +### Example: File Deletion +![](assets/2025-11-16-00-25-49.png) + +### Example: Content Generation +![](assets/2025-11-16-00-29-47.png) + +### Example: File Creation +![](assets/2025-11-16-01-01-40.png) + +### Example multiple operations +![](assets/2025-11-27-00-25-53.png) + +--- + +## 🀝 Contributing + +**We love contributions!** Whether it's bug fixes, new features, or documentation improvements. + +### How to Contribute + +1. ⭐ **Star the repository** (if you haven't already!) +2. 🍴 Fork the repository +3. 🌿 Create a feature branch (`git checkout -b feature/amazing-feature`) +4. ✍️ Make your changes +5. βœ… Run tests (`make test`) +6. 🎨 Run linter (`make lint`) +7. πŸ“ Commit your changes (`git commit -m 'Add amazing feature'`) +8. πŸš€ Push to the branch (`git push origin feature/amazing-feature`) +9. 🎯 Open a Pull Request + +### Development Setup + +```bash +# Clone your fork +git clone https://github.com/YOUR_USERNAME/gitpilot.git +cd gitpilot + +# Install dependencies +make install + +# Create a branch +git checkout -b feature/my-feature + +# Make changes and test +make run +make test +``` + +--- + +## πŸ“„ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +## πŸ‘¨β€πŸ’» Author + +**Ruslan Magana Vsevolodovna** + +- GitHub: [@ruslanmv](https://github.com/ruslanmv) +- Website: [ruslanmv.com](https://ruslanmv.com) + +--- + +## πŸ™ Acknowledgments + +- **CrewAI** – Multi-agent orchestration framework +- **FastAPI** – Modern, fast web framework +- **React** – UI library +- **ReactFlow** – Interactive node-based diagrams +- **Vite** – Fast build tool +- **MCP (Model Context Protocol)** – By Anthropic, for tool interoperability +- **OWASP Top 10** – Security vulnerability categorisation +- **BIRD benchmark** and **DIN-SQL** research – Text-to-SQL approaches +- **Horvitz (1999)** – Proactive assistance in HCI research +- **All our contributors and stargazers!** ⭐ + +--- + +## πŸ“ž Support + +- **Issues**: https://github.com/ruslanmv/gitpilot/issues +- **Discussions**: https://github.com/ruslanmv/gitpilot/discussions +- **Documentation**: [Full Documentation](https://github.com/ruslanmv/gitpilot#readme) + +--- + +## πŸ—ΊοΈ Roadmap + +### Recently Released (v0.2.0) πŸ†• + +**Phase 1 β€” Feature Parity with Claude Code:** +- βœ… **Local Workspace Manager** – Clone repos, read/write/search files locally +- βœ… **Terminal Execution** – Sandboxed shell commands with timeout and output capping +- βœ… **Session Persistence** – Resume, fork, checkpoint, and rewind sessions +- βœ… **Hook System** – Lifecycle events (pre_commit, post_edit) with blocking support +- βœ… **Permission Policies** – Three modes (normal/plan/auto) with path-based blocking +- βœ… **Context Memory (GITPILOT.md)** – Project conventions injected into agent prompts +- βœ… **Headless CI/CD Mode** – `gitpilot run --headless` for automation pipelines + +**Phase 2 β€” Ecosystem Superiority:** +- βœ… **MCP Client** – Connect to any MCP server (databases, Slack, Figma, Sentry) +- βœ… **Plugin Marketplace** – Install/uninstall plugins from git or local paths +- βœ… **Skill System** – /command skills defined as markdown with template variables +- βœ… **Vision Analysis** – Multimodal image analysis via OpenAI, Anthropic, or Ollama +- βœ… **Smart Model Router** – Auto-route tasks to optimal model by complexity +- βœ… **VS Code Extension** – Sidebar chat, inline actions, and keybindings + +**Phase 3 β€” Intelligence Superiority (no competitor has this):** +- βœ… **Agent Teams** – Parallel multi-agent execution on git worktrees +- βœ… **Self-Improving Agents** – Learn from outcomes and adapt per-project +- βœ… **Cross-Repo Intelligence** – Dependency graphs and impact analysis across repos +- βœ… **Predictive Workflows** – Proactive suggestions based on context patterns +- βœ… **AI Security Scanner** – Secret detection, injection analysis, CWE mapping +- βœ… **NL Database Queries** – Natural language to SQL translation via MCP +- βœ… **Topology Registry** – Seven switchable agent architectures with zero-latency message classification and visual topology selector + +### Previous Features (v0.1.2) +- βœ… Full Multi-LLM Support – All 4 providers fully tested +- βœ… Answer + Action Plan UX with structured file operations +- βœ… Real Execution Engine with GitHub operations +- βœ… Agent Flow Viewer with ReactFlow +- βœ… MCP / A2A Integration (ContextForge compatible) +- βœ… Issue and Pull Request management APIs +- βœ… Code, issue, repository, and user search +- βœ… OAuth web + device flow authentication + +### Planned Features (v0.2.1+) +- πŸ”„ Frontend components for terminal, sessions, checkpoints, and security dashboard +- πŸ”„ JetBrains IDE plugin +- πŸ”„ Real-time collaboration (shared sessions, audit log) +- πŸ”„ Automated test generation from code changes +- πŸ”„ Slack/Discord notification hooks +- πŸ”„ LLM-powered semantic diff review +- πŸ”„ OSV dependency vulnerability scanning +- πŸ”„ Custom agent templates and agent marketplace + +--- + +## ⚠️ Important Notes + +### Security Best Practices + +1. **Never commit API keys** to version control +2. **Use environment variables** or the Admin UI for credentials +3. **Rotate tokens regularly** +4. **Limit GitHub token scopes** to only what's needed +5. **Review all plans** before approving execution +6. **Verify GitHub App installations** before granting write access +7. **Run `gitpilot scan`** before releases to catch secrets, injection risks, and insecure configurations +8. **Use permission modes** – run agents in `plan` mode (read-only) when exploring unfamiliar codebases + +### LLM Provider Configuration + +**All providers now fully supported!** ✨ + +Each provider has specific requirements: + +**OpenAI** +- Requires: `OPENAI_API_KEY` +- Optional: `GITPILOT_OPENAI_MODEL`, `OPENAI_BASE_URL` + +**Claude (Anthropic)** +- Requires: `ANTHROPIC_API_KEY` +- Optional: `GITPILOT_CLAUDE_MODEL`, `ANTHROPIC_BASE_URL` +- Note: Environment variables are automatically configured by GitPilot + +**IBM Watsonx.ai** +- Requires: `WATSONX_API_KEY`, `WATSONX_PROJECT_ID` +- Optional: `WATSONX_BASE_URL`, `GITPILOT_WATSONX_MODEL` +- Note: Project ID is essential for proper authentication + +**Ollama** +- Requires: `OLLAMA_BASE_URL` +- Optional: `GITPILOT_OLLAMA_MODEL` +- Note: Runs locally, no API key needed + +### File Action Types + +GitPilot supports four file operation types in plans: + +- **CREATE** (🟒) – Add new files with AI-generated content +- **MODIFY** (πŸ”΅) – Update existing files intelligently +- **DELETE** (πŸ”΄) – Remove files safely +- **READ** (πŸ“–) – Analyze files without making changes (new!) + +READ operations allow agents to gather context and information without modifying your repository, enabling better-informed plans. + +--- + +## πŸŽ“ Learn More + +### Understanding the Agent System + +GitPilot uses a multi-agent architecture where each request is routed to the right set of agents by the **agent router**, which analyses your message using NLP patterns and dispatches to one of 12+ specialised agent types: + +**Core Agents:** +- **Repository Explorer** – Scans codebase structure and gathers context +- **Refactor Planner** – Creates structured step-by-step plans with file operations +- **Code Writer** – Generates AI-powered content for new and modified files +- **Code Reviewer** – Reviews changes for quality, safety, and adherence to conventions + +**Local Agents (Phase 1):** +- **Local Editor** – Reads, writes, and searches files directly on disk +- **Terminal Agent** – Executes shell commands (`npm test`, `make build`, `pytest`) + +**Specialised Agents (v2):** +- **Issue Agent** – Creates, updates, labels, and comments on GitHub issues +- **PR Agent** – Creates pull requests, lists files, manages merges +- **Search Agent** – Searches code, issues, repos, and users across GitHub +- **Learning Agent** – Evaluates outcomes and improves strategies over time + +**Intelligence Layer (Phase 3):** +- **Agent Teams** – Multiple agents work in parallel on subtasks with conflict detection +- **Predictive Engine** – Suggests next actions before you ask +- **Security Scanner** – Detects secrets, injection risks, and insecure configurations +- **Cross-Repo Analyser** – Maps dependencies and assesses impact across repositories + +The **smart model router** selects the optimal LLM for each task β€” simple queries go to fast/cheap models while complex reasoning gets the most powerful model available. + +### Choosing the Right LLM Provider + +**OpenAI (GPT-4o, GPT-4o-mini)** +- βœ… Best for: General-purpose coding, fast responses +- βœ… Strengths: Excellent code quality, great at following instructions +- βœ… Status: Fully tested and working +- ⚠️ Costs: Moderate to high + +**Claude (Claude 4.5 Sonnet)** +- βœ… Best for: Complex refactoring, detailed analysis +- βœ… Strengths: Deep reasoning, excellent at planning +- βœ… Status: Fully tested and working (latest integration fixes applied) +- ⚠️ Costs: Moderate to high + +**Watsonx (Llama 3.3, Granite 3.x)** +- βœ… Best for: Enterprise deployments, privacy-focused +- βœ… Strengths: On-premise option, compliance-friendly +- βœ… Status: Fully tested and working (project_id integration fixed) +- ⚠️ Costs: Subscription-based + +**Ollama (Local Models)** +- βœ… Best for: Cost-free operation, offline work +- βœ… Strengths: Zero API costs, complete privacy +- βœ… Status: Fully tested and working +- ⚠️ Performance: Depends on hardware, may be slower + +--- + +## πŸ› Troubleshooting + +### Common Issues and Solutions + +**Issue: "ANTHROPIC_API_KEY is required" error with Claude** +- **Solution**: This is now automatically handled. Update to latest version or ensure environment variables are set via Admin UI. + +**Issue: "Fallback to LiteLLM is not available" with Watsonx** +- **Solution**: Ensure you've set both `WATSONX_API_KEY` and `WATSONX_PROJECT_ID`. Install `litellm` if needed: `pip install litellm` + +**Issue: Plan generation fails with validation error** +- **Solution**: Update to latest version which includes READ action support in schema validation. + +**Issue: "Read Only" status despite having write access** +- **Solution**: Install the GitPilot GitHub App on your repository. Click the install link in the UI or refresh permissions. + +**Issue: File tree not updating after agent operations** +- **Solution**: Click the Refresh button in the file tree header to see newly created/modified files. + +For more issues, visit our [GitHub Issues](https://github.com/ruslanmv/gitpilot/issues) page. + +--- + +
+ +**⭐ Don't forget to star GitPilot if you find it useful! ⭐** + +[⭐ Star on GitHub](https://github.com/ruslanmv/gitpilot) β€’ [πŸ“– Documentation](https://github.com/ruslanmv/gitpilot#readme) β€’ [πŸ› Report Bug](https://github.com/ruslanmv/gitpilot/issues) β€’ [πŸ’‘ Request Feature](https://github.com/ruslanmv/gitpilot/issues) + +**GitPilot** – Your AI Coding Companion for GitHub πŸš€ + +Made with ❀️ by [Ruslan Magana Vsevolodovna](https://github.com/ruslanmv) + +
\ No newline at end of file diff --git a/deploy/huggingface/start.sh b/deploy/huggingface/start.sh new file mode 100644 index 0000000000000000000000000000000000000000..9db846f327fe4b65a8683d69339315a154e1d19f --- /dev/null +++ b/deploy/huggingface/start.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# ============================================================================= +# GitPilot β€” HF Spaces Startup Script +# ============================================================================= +# Starts GitPilot FastAPI server with React frontend on HuggingFace Spaces. +# Pre-configured to use OllaBridge Cloud as the default LLM provider. +# ============================================================================= + +set -e + +echo "==============================================" +echo " GitPilot β€” Hugging Face Spaces" +echo "==============================================" +echo "" + +# -- Ensure writable directories exist ---------------------------------------- +mkdir -p /tmp/gitpilot /tmp/gitpilot/workspaces /tmp/gitpilot/sessions +export HOME=/tmp +export GITPILOT_CONFIG_DIR=/tmp/gitpilot + +# -- Display configuration --------------------------------------------------- +echo "[1/2] Configuration:" +echo " Provider: ${GITPILOT_PROVIDER:-ollabridge}" +echo " OllaBridge URL: ${OLLABRIDGE_BASE_URL:-https://ruslanmv-ollabridge.hf.space}" +echo " Model: ${GITPILOT_OLLABRIDGE_MODEL:-qwen2.5:1.5b}" +echo "" + +# -- Check OllaBridge Cloud connectivity (non-blocking) ---------------------- +echo "[2/2] Checking LLM provider..." +if curl -sf "${OLLABRIDGE_BASE_URL:-https://ruslanmv-ollabridge.hf.space}/health" > /dev/null 2>&1; then + echo " OllaBridge Cloud is reachable" +else + echo " OllaBridge Cloud not reachable (will retry on first request)" + echo " You can configure a different provider in Admin / LLM Settings" +fi +echo "" + +echo "==============================================" +echo " Ready! Endpoints:" +echo " - UI: / (React frontend)" +echo " - API: /api/health" +echo " - API Docs: /docs" +echo " - Chat: /api/chat/message" +echo " - Settings: /api/settings" +echo "==============================================" +echo "" + +# -- Start GitPilot (foreground) ---------------------------------------------- +exec python -m uvicorn gitpilot.api:app \ + --host "${HOST:-0.0.0.0}" \ + --port "${PORT:-7860}" \ + --workers 1 \ + --timeout-keep-alive 120 \ + --no-access-log diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..9da1acab57b3a83f0649dc5deb28b33600fe4ad3 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,39 @@ +# Node +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# Build +dist/ +build/ + +# Environment +.env +.env.local +.env.development +.env.test +.env.production.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Testing +coverage/ +.nyc_output/ + +# Misc +*.log diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..81999b91b4596670b57112acfcda425f8b267fd4 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,7 @@ +# Frontend Environment Variables + +# Backend API URL +# Leave empty for local development (uses Vite proxy to localhost:8000) +# Set to your Render backend URL for production deployment on Vercel +# Example: VITE_BACKEND_URL=https://gitpilot-backend.onrender.com +VITE_BACKEND_URL= diff --git a/frontend/.env.production.example b/frontend/.env.production.example new file mode 100644 index 0000000000000000000000000000000000000000..57ccba24efcbd8fda76f886e361e4e5826e52ac0 --- /dev/null +++ b/frontend/.env.production.example @@ -0,0 +1,5 @@ +# Production Environment Variables (Vercel) + +# Backend API URL - REQUIRED for production +# Point this to your Render backend URL +VITE_BACKEND_URL=https://gitpilot-backend.onrender.com diff --git a/frontend/App.jsx b/frontend/App.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fceabed0d25e27e909aab7d0cf771ac0f4046d04 --- /dev/null +++ b/frontend/App.jsx @@ -0,0 +1,909 @@ +// frontend/App.jsx +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import LoginPage from "./components/LoginPage.jsx"; +import RepoSelector from "./components/RepoSelector.jsx"; +import ProjectContextPanel from "./components/ProjectContextPanel.jsx"; +import ChatPanel from "./components/ChatPanel.jsx"; +import LlmSettings from "./components/LlmSettings.jsx"; +import FlowViewer from "./components/FlowViewer.jsx"; +import Footer from "./components/Footer.jsx"; +import ProjectSettingsModal from "./components/ProjectSettingsModal.jsx"; +import SessionSidebar from "./components/SessionSidebar.jsx"; +import ContextBar from "./components/ContextBar.jsx"; +import AddRepoModal from "./components/AddRepoModal.jsx"; +import { apiUrl, safeFetchJSON, fetchStatus } from "./utils/api.js"; + +function makeRepoKey(repo) { + if (!repo) return null; + return repo.full_name || `${repo.owner}/${repo.name}`; +} + +function uniq(arr) { + return Array.from(new Set((arr || []).filter(Boolean))); +} + +export default function App() { + // ---- Multi-repo context state ---- + const [contextRepos, setContextRepos] = useState([]); + // Each entry: { repoKey: "owner/repo", repo: {...}, branch: "main" } + const [activeRepoKey, setActiveRepoKey] = useState(null); + const [addRepoOpen, setAddRepoOpen] = useState(false); + + const [activePage, setActivePage] = useState("workspace"); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [userInfo, setUserInfo] = useState(null); + + // Repo + Session State Machine + const [repoStateByKey, setRepoStateByKey] = useState({}); + const [toast, setToast] = useState(null); + const [settingsOpen, setSettingsOpen] = useState(false); + const [adminTab, setAdminTab] = useState("overview"); + const [adminStatus, setAdminStatus] = useState(null); + + // Fetch admin status when overview tab is active + useEffect(() => { + if (activePage === "admin" && adminTab === "overview") { + fetchStatus() + .then(data => setAdminStatus(data)) + .catch(() => setAdminStatus(null)); + } + }, [activePage, adminTab]); + + // Claude-Code-on-Web: Session sidebar + Environment state + const [activeSessionId, setActiveSessionId] = useState(null); + const [activeEnvId, setActiveEnvId] = useState("default"); + const [sessionRefreshNonce, setSessionRefreshNonce] = useState(0); + + // ---- Derived `repo` β€” keeps all downstream consumers unchanged ---- + const repo = useMemo(() => { + const entry = contextRepos.find((r) => r.repoKey === activeRepoKey); + return entry?.repo || null; + }, [contextRepos, activeRepoKey]); + + const repoKey = activeRepoKey; + + // Convenient selectors + const currentRepoState = repoKey ? repoStateByKey[repoKey] : null; + + const defaultBranch = currentRepoState?.defaultBranch || repo?.default_branch || "main"; + const currentBranch = currentRepoState?.currentBranch || defaultBranch; + const sessionBranches = currentRepoState?.sessionBranches || []; + const lastExecution = currentRepoState?.lastExecution || null; + const pulseNonce = currentRepoState?.pulseNonce || 0; + const chatByBranch = currentRepoState?.chatByBranch || {}; + + // --------------------------------------------------------------------------- + // Multi-repo context management + // --------------------------------------------------------------------------- + const addRepoToContext = useCallback((r) => { + const key = makeRepoKey(r); + if (!key) return; + + setContextRepos((prev) => { + // Don't add duplicates + if (prev.some((e) => e.repoKey === key)) { + // Already in context β€” just activate it + setActiveRepoKey(key); + return prev; + } + const entry = { repoKey: key, repo: r, branch: r.default_branch || "main" }; + const next = [...prev, entry]; + return next; + }); + setActiveRepoKey(key); + setAddRepoOpen(false); + }, []); + + const removeRepoFromContext = useCallback((key) => { + setContextRepos((prev) => { + const next = prev.filter((e) => e.repoKey !== key); + // Reassign active if we removed the active one + setActiveRepoKey((curActive) => { + if (curActive === key) { + return next.length > 0 ? next[0].repoKey : null; + } + return curActive; + }); + return next; + }); + }, []); + + const clearAllContext = useCallback(() => { + setContextRepos([]); + setActiveRepoKey(null); + }, []); + + const handleContextBranchChange = useCallback((targetRepoKey, newBranch) => { + // Update branch in contextRepos + setContextRepos((prev) => + prev.map((e) => + e.repoKey === targetRepoKey ? { ...e, branch: newBranch } : e + ) + ); + // Update branch in repoStateByKey + setRepoStateByKey((prev) => { + const cur = prev[targetRepoKey]; + if (!cur) return prev; + return { + ...prev, + [targetRepoKey]: { ...cur, currentBranch: newBranch }, + }; + }); + }, []); + + // Init / reconcile repo state when active repo changes + useEffect(() => { + if (!repoKey || !repo) return; + + setRepoStateByKey((prev) => { + const existing = prev[repoKey]; + const d = repo.default_branch || "main"; + + if (!existing) { + return { + ...prev, + [repoKey]: { + defaultBranch: d, + currentBranch: d, + sessionBranches: [], + lastExecution: null, + pulseNonce: 0, + chatByBranch: { + [d]: { messages: [], plan: null }, + }, + }, + }; + } + + const next = { ...existing }; + next.defaultBranch = d; + + if (!next.chatByBranch?.[d]) { + next.chatByBranch = { + ...(next.chatByBranch || {}), + [d]: { messages: [], plan: null }, + }; + } + + if (!next.currentBranch) next.currentBranch = d; + + return { ...prev, [repoKey]: next }; + }); + }, [repoKey, repo?.id, repo?.default_branch]); + + const showToast = (title, message) => { + setToast({ title, message }); + window.setTimeout(() => setToast(null), 5000); + }; + + // --------------------------------------------------------------------------- + // Session management β€” every chat is backed by a Session (Claude Code parity) + // --------------------------------------------------------------------------- + + // Guard against double-creation during concurrent send() calls + const _creatingSessionRef = useRef(false); + + /** + * ensureSession β€” Create a session on-demand (implicit). + * + * Called by ChatPanel before the first message is sent. If a session + * already exists it returns the current ID immediately. Otherwise it + * creates one, seeds the initial messages into chatBySession so the + * useEffect reset doesn't wipe them, and returns the new ID. + * + * @param {string} [sessionName] β€” optional title (first user prompt, truncated) + * @param {Array} [seedMessages] β€” messages to pre-populate into the new session + * @returns {Promise} the session ID + */ + const ensureSession = useCallback(async (sessionName, seedMessages) => { + if (activeSessionId) return activeSessionId; + if (!repo) return null; + if (_creatingSessionRef.current) return null; // already in flight + _creatingSessionRef.current = true; + + try { + const token = localStorage.getItem("github_token"); + const headers = { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + const res = await fetch("/api/sessions", { + method: "POST", + headers, + body: JSON.stringify({ + repo_full_name: repoKey, + branch: currentBranch, + name: sessionName || undefined, + repos: contextRepos.map((e) => ({ + full_name: e.repoKey, + branch: e.branch, + mode: e.repoKey === activeRepoKey ? "write" : "read", + })), + active_repo: activeRepoKey, + }), + }); + if (!res.ok) return null; + const data = await res.json(); + const newId = data.session_id; + + // Seed the session's chat state BEFORE setting activeSessionId so + // the ChatPanel useEffect sync picks up the messages instead of [] + if (seedMessages && seedMessages.length > 0) { + setChatBySession((prev) => ({ + ...prev, + [newId]: { messages: seedMessages, plan: null }, + })); + } + + setActiveSessionId(newId); + setSessionRefreshNonce((n) => n + 1); + return newId; + } catch (err) { + console.warn("Failed to create session:", err); + return null; + } finally { + _creatingSessionRef.current = false; + } + }, [activeSessionId, repo, repoKey, currentBranch, contextRepos, activeRepoKey]); + + // Explicit "New Session" button β€” clears chat and starts fresh + const handleNewSession = async () => { + // Clear the current session so ensureSession creates a new one + setActiveSessionId(null); + try { + const token = localStorage.getItem("github_token"); + const headers = { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }; + const res = await fetch("/api/sessions", { + method: "POST", + headers, + body: JSON.stringify({ + repo_full_name: repoKey, + branch: currentBranch, + repos: contextRepos.map((e) => ({ + full_name: e.repoKey, + branch: e.branch, + mode: e.repoKey === activeRepoKey ? "write" : "read", + })), + active_repo: activeRepoKey, + }), + }); + if (!res.ok) return; + const data = await res.json(); + setActiveSessionId(data.session_id); + setSessionRefreshNonce((n) => n + 1); + showToast("Session Created", `New session started.`); + } catch (err) { + console.warn("Failed to create session:", err); + } + }; + + const handleSelectSession = (session) => { + setActiveSessionId(session.id); + if (session.branch && session.branch !== currentBranch) { + handleBranchChange(session.branch); + } + }; + + // When a session is deleted: if it was the active session, clear the + // chat so the user returns to a fresh "new conversation" state. + // Non-active session deletions only affect the sidebar (handled there). + const handleDeleteSession = useCallback((deletedId) => { + if (deletedId === activeSessionId) { + setActiveSessionId(null); + // Clean up the in-memory chat state for the deleted session + setChatBySession((prev) => { + const next = { ...prev }; + delete next[deletedId]; + return next; + }); + // Also clear the branch-keyed chat (the persistence effect may have + // written the first user message there before the session was created) + if (repoKey) { + setRepoStateByKey((prev) => { + const cur = prev[repoKey]; + if (!cur) return prev; + const branchKey = cur.currentBranch || cur.defaultBranch || defaultBranch; + return { + ...prev, + [repoKey]: { + ...cur, + chatByBranch: { + ...(cur.chatByBranch || {}), + [branchKey]: { messages: [], plan: null }, + }, + }, + }; + }); + } + } + }, [activeSessionId, repoKey, defaultBranch]); + + // --------------------------------------------------------------------------- + // Chat persistence helpers + // --------------------------------------------------------------------------- + const updateChatForCurrentBranch = (patch) => { + if (!repoKey) return; + + setRepoStateByKey((prev) => { + const cur = prev[repoKey]; + if (!cur) return prev; + + const branchKey = cur.currentBranch || cur.defaultBranch || defaultBranch; + + const existing = cur.chatByBranch?.[branchKey] || { + messages: [], + plan: null, + }; + + return { + ...prev, + [repoKey]: { + ...cur, + chatByBranch: { + ...(cur.chatByBranch || {}), + [branchKey]: { ...existing, ...patch }, + }, + }, + }; + }); + }; + + const currentChatState = useMemo(() => { + const b = currentBranch || defaultBranch; + return chatByBranch[b] || { messages: [], plan: null }; + }, [chatByBranch, currentBranch, defaultBranch]); + + // --------------------------------------------------------------------------- + // Session-scoped chat state: isolate messages per (session + branch) instead + // of per-branch alone. This prevents session A's messages from leaking into + // session B when both sessions share the same branch. + // --------------------------------------------------------------------------- + const [chatBySession, setChatBySession] = useState({}); + + const sessionChatState = useMemo(() => { + if (!activeSessionId) { + // No session β€” fall back to legacy branch-keyed chat + return currentChatState; + } + return chatBySession[activeSessionId] || { messages: [], plan: null }; + }, [activeSessionId, chatBySession, currentChatState]); + + const updateSessionChat = (patch) => { + if (activeSessionId) { + setChatBySession((prev) => ({ + ...prev, + [activeSessionId]: { + ...(prev[activeSessionId] || { messages: [], plan: null }), + ...patch, + }, + })); + } else { + // No active session β€” use legacy branch-keyed persistence + updateChatForCurrentBranch(patch); + } + }; + + // --------------------------------------------------------------------------- + // Branch change (manual β€” for active repo) + // --------------------------------------------------------------------------- + const handleBranchChange = (nextBranch) => { + if (!repoKey) return; + if (!nextBranch || nextBranch === currentBranch) return; + + setRepoStateByKey((prev) => { + const cur = prev[repoKey]; + if (!cur) return prev; + + const nextState = { ...cur, currentBranch: nextBranch }; + + // If switching BACK to main/default -> clear main chat (new task start) + if (nextBranch === cur.defaultBranch) { + nextState.chatByBranch = { + ...nextState.chatByBranch, + [nextBranch]: { messages: [], plan: null }, + }; + } + + return { ...prev, [repoKey]: nextState }; + }); + + // Also update contextRepos branch tracking + setContextRepos((prev) => + prev.map((e) => + e.repoKey === repoKey ? { ...e, branch: nextBranch } : e + ) + ); + + if (nextBranch === defaultBranch) { + showToast("New Session", `Switched to ${defaultBranch}. Chat cleared.`); + } else { + showToast("Context Switched", `Now viewing ${nextBranch}.`); + } + }; + + // --------------------------------------------------------------------------- + // Execution complete + // --------------------------------------------------------------------------- + const handleExecutionComplete = ({ + branch, + mode, + commit_url, + message, + completionMsg, + sourceBranch, + }) => { + if (!repoKey || !branch) return; + + setRepoStateByKey((prev) => { + const cur = + prev[repoKey] || { + defaultBranch, + currentBranch: defaultBranch, + sessionBranches: [], + lastExecution: null, + pulseNonce: 0, + chatByBranch: { [defaultBranch]: { messages: [], plan: null } }, + }; + + const next = { ...cur }; + next.lastExecution = { mode, branch, ts: Date.now() }; + + if (!next.chatByBranch) next.chatByBranch = {}; + + const prevBranchKey = + sourceBranch || cur.currentBranch || cur.defaultBranch || defaultBranch; + + const successSystemMsg = { + role: "system", + isSuccess: true, + link: commit_url, + content: + mode === "hard-switch" + ? `🌱 **Session Started:** Created branch \`${branch}\`.` + : `βœ… **Update Published:** Commits pushed to \`${branch}\`.`, + }; + + const normalizedCompletion = + completionMsg && (completionMsg.answer || completionMsg.content || completionMsg.executionLog) + ? { + from: completionMsg.from || "ai", + role: completionMsg.role || "assistant", + answer: completionMsg.answer, + content: completionMsg.content, + executionLog: completionMsg.executionLog, + } + : null; + + if (mode === "hard-switch") { + next.sessionBranches = uniq([...(next.sessionBranches || []), branch]); + next.currentBranch = branch; + next.pulseNonce = (next.pulseNonce || 0) + 1; + + const existingTargetChat = next.chatByBranch[branch]; + const isExistingSession = + existingTargetChat && (existingTargetChat.messages || []).length > 0; + + if (isExistingSession) { + const appended = [ + ...(existingTargetChat.messages || []), + ...(normalizedCompletion ? [normalizedCompletion] : []), + successSystemMsg, + ]; + + next.chatByBranch[branch] = { + ...existingTargetChat, + messages: appended, + plan: null, + }; + } else { + const prevChat = + (cur.chatByBranch && cur.chatByBranch[prevBranchKey]) || { messages: [], plan: null }; + + next.chatByBranch[branch] = { + messages: [ + ...(prevChat.messages || []), + ...(normalizedCompletion ? [normalizedCompletion] : []), + successSystemMsg, + ], + plan: null, + }; + } + + if (!next.chatByBranch[next.defaultBranch]) { + next.chatByBranch[next.defaultBranch] = { messages: [], plan: null }; + } + } else if (mode === "sticky") { + next.currentBranch = cur.currentBranch || branch; + + const targetChat = next.chatByBranch[branch] || { messages: [], plan: null }; + + next.chatByBranch[branch] = { + messages: [ + ...(targetChat.messages || []), + ...(normalizedCompletion ? [normalizedCompletion] : []), + successSystemMsg, + ], + plan: null, + }; + } + + return { ...prev, [repoKey]: next }; + }); + + if (mode === "hard-switch") { + showToast("Context Switched", `Active on ${branch}.`); + } else { + showToast("Changes Committed", `Updated ${branch}.`); + } + }; + + // --------------------------------------------------------------------------- + // Auth & Render + // --------------------------------------------------------------------------- + useEffect(() => { + checkAuthentication(); + }, []); + + const checkAuthentication = async () => { + const token = localStorage.getItem("github_token"); + const user = localStorage.getItem("github_user"); + if (token && user) { + try { + const data = await safeFetchJSON(apiUrl("/api/auth/validate"), { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ access_token: token }), + }); + if (data.authenticated) { + setIsAuthenticated(true); + setUserInfo(JSON.parse(user)); + setIsLoading(false); + return; + } + } catch (err) { + console.error(err); + } + localStorage.removeItem("github_token"); + localStorage.removeItem("github_user"); + } + setIsAuthenticated(false); + setIsLoading(false); + }; + + const handleAuthenticated = (session) => { + setIsAuthenticated(true); + setUserInfo(session.user); + }; + + const handleLogout = () => { + localStorage.removeItem("github_token"); + localStorage.removeItem("github_user"); + setIsAuthenticated(false); + setUserInfo(null); + clearAllContext(); + }; + + if (isLoading) + return ( +
+
+
+ ); + + if (!isAuthenticated) return ; + + const hasContext = contextRepos.length > 0; + + return ( +
+
+ + +
+ {activePage === "admin" && ( +
+ {/* Admin Navigation */} +
+ {["overview", "providers", "workspace-modes", "integrations", "sessions", "skills", "security", "advanced"].map(tab => ( + + ))} +
+ + {/* Overview */} + {adminTab === "overview" && ( +
+
+
Server
+
{adminStatus?.server_ready ? "Connected" : "Checking..."}
+
127.0.0.1:8000
+
+
+
Provider
+
{adminStatus?.provider?.name || "Loading..."}
+
{adminStatus?.provider?.configured ? `${adminStatus.provider.model || "Ready"}` : "Not configured"}
+
+
+
Workspace Modes
+
Folder: {adminStatus?.workspace?.folder_mode_available ? "Yes" : "β€”"}
+
Local Git: {adminStatus?.workspace?.local_git_available ? "Yes" : "β€”"}
+
GitHub: {adminStatus?.workspace?.github_mode_available ? "Yes" : "Optional"}
+
+
+
GitHub
+
{adminStatus?.github?.connected ? "Connected" : "Optional"}
+
{adminStatus?.github?.username || "Not linked"}
+
+
+
Sessions
+
β€”
+
+
+
Get Started
+ +
+
+ )} + + {/* Providers */} + {adminTab === "providers" && ( +
+

AI Providers

+ +
+ )} + + {/* Workspace Modes */} + {adminTab === "workspace-modes" && ( +
+
+

Folder Mode

+

Work with any local folder. No Git required.

+
Requires: Open folder
+
Enables: Chat, explain, review
+
+
+

Local Git Mode

+

Full repo + branch context for AI assistance.

+
Requires: Git repository
+
Enables: All local features
+
+
+

GitHub Mode

+

PRs, issues, remote workflows via GitHub API.

+
Requires: GitHub token
+
Enables: Full platform features
+
+
+ )} + + {/* Integrations */} + {adminTab === "integrations" && ( +
+

GitHub Integration

+

GitHub is optional. Connect to enable PRs, issues, and remote workflows.

+ +
+ )} + + {/* Security */} + {adminTab === "security" && ( +
+

Security Scanning

+

Run security scans on your workspace to detect vulnerabilities, secrets, and code issues.

+ +
+ )} + + {/* Sessions */} + {adminTab === "sessions" && ( +
+

Sessions

+

Session management is available in the main workspace view.

+
+ )} + + {/* Skills & Plugins */} + {adminTab === "skills" && ( +
+

Skills & Plugins

+

Skills and plugins extend GitPilot capabilities. View and manage them from the main workspace.

+
+ )} + + {/* Advanced */} + {adminTab === "advanced" && ( +
+

Advanced Settings

+

Advanced configuration options are available in the Settings modal.

+ +
+ )} +
+ )} + {activePage === "flow" && } + {activePage === "workspace" && + (repo ? ( +
+ {/* ---- Context Bar (single source of truth for repo selection) ---- */} + setAddRepoOpen(true)} + onBranchChange={handleContextBranchChange} + /> + +
+ + +
+
+ GitPilot chat +
+ + +
+
+
+ ) : ( +
+
πŸ€–
+

Select a repository

+

Select a repo to begin agentic workflow.

+
+ ))} +
+
+ +
+ + {repo && ( + setSettingsOpen(false)} + activeEnvId={activeEnvId} + onEnvChange={setActiveEnvId} + /> + )} + + {/* Add Repo Modal */} + setAddRepoOpen(false)} + excludeKeys={contextRepos.map((e) => e.repoKey)} + /> + + {toast && ( +
+
{toast.title}
+
{toast.message}
+
+ )} + + +
+ ); +} diff --git a/frontend/components/AddRepoModal.jsx b/frontend/components/AddRepoModal.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7832877ed985bac5ec81f1b4c43978e525dd8bd2 --- /dev/null +++ b/frontend/components/AddRepoModal.jsx @@ -0,0 +1,256 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { authFetch } from "../utils/api.js"; + +/** + * AddRepoModal β€” lightweight portal modal for adding repos to context. + * + * Embeds a minimal repo search/list (not the full RepoSelector) to keep + * the modal focused. Filters out repos already in context. + */ +export default function AddRepoModal({ isOpen, onSelect, onClose, excludeKeys = [] }) { + const [query, setQuery] = useState(""); + const [repos, setRepos] = useState([]); + const [loading, setLoading] = useState(false); + + const fetchRepos = useCallback( + async (searchQuery) => { + setLoading(true); + try { + const params = new URLSearchParams({ per_page: "50" }); + if (searchQuery) params.set("query", searchQuery); + const res = await authFetch(`/api/repos?${params}`); + if (!res.ok) return; + const data = await res.json(); + setRepos(data.repositories || []); + } catch (err) { + console.warn("AddRepoModal: fetch failed:", err); + } finally { + setLoading(false); + } + }, + [] + ); + + useEffect(() => { + if (isOpen) { + setQuery(""); + fetchRepos(""); + } + }, [isOpen, fetchRepos]); + + // Debounced search + useEffect(() => { + if (!isOpen) return; + const t = setTimeout(() => fetchRepos(query), 300); + return () => clearTimeout(t); + }, [query, isOpen, fetchRepos]); + + const excludeSet = new Set(excludeKeys); + const filtered = repos.filter((r) => { + const key = r.full_name || `${r.owner}/${r.name}`; + return !excludeSet.has(key); + }); + + if (!isOpen) return null; + + return createPortal( +
{ + if (e.target === e.currentTarget) onClose(); + }} + > +
e.stopPropagation()}> +
+ Add Repository + +
+ +
+ setQuery(e.target.value)} + style={styles.searchInput} + autoFocus + onKeyDown={(e) => { + if (e.key === "Escape") onClose(); + }} + /> +
+ +
+ {loading && filtered.length === 0 && ( +
Loading...
+ )} + {!loading && filtered.length === 0 && ( +
+ {excludeKeys.length > 0 && repos.length > 0 + ? "All matching repos are already in context" + : "No repositories found"} +
+ )} + {filtered.map((r) => { + const key = r.full_name || `${r.owner}/${r.name}`; + return ( + + ); + })} + {loading && filtered.length > 0 && ( +
Updating...
+ )} +
+
+
, + document.body + ); +} + +const styles = { + overlay: { + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: "rgba(0, 0, 0, 0.6)", + zIndex: 10000, + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + modal: { + width: 440, + maxHeight: "70vh", + backgroundColor: "#131316", + border: "1px solid #27272A", + borderRadius: 12, + display: "flex", + flexDirection: "column", + overflow: "hidden", + boxShadow: "0 12px 40px rgba(0,0,0,0.5)", + }, + header: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + padding: "12px 14px", + borderBottom: "1px solid #27272A", + backgroundColor: "#18181B", + }, + headerTitle: { + fontSize: 14, + fontWeight: 600, + color: "#E4E4E7", + }, + closeBtn: { + width: 26, + height: 26, + borderRadius: 6, + border: "1px solid #3F3F46", + background: "transparent", + color: "#A1A1AA", + fontSize: 16, + cursor: "pointer", + display: "flex", + alignItems: "center", + justifyContent: "center", + }, + searchBox: { + padding: "10px 12px", + borderBottom: "1px solid #27272A", + }, + searchInput: { + width: "100%", + padding: "8px 10px", + borderRadius: 6, + border: "1px solid #3F3F46", + background: "#18181B", + color: "#E4E4E7", + fontSize: 13, + outline: "none", + fontFamily: "monospace", + boxSizing: "border-box", + }, + list: { + flex: 1, + overflowY: "auto", + maxHeight: 360, + }, + statusRow: { + padding: "16px 12px", + textAlign: "center", + fontSize: 12, + color: "#71717A", + }, + repoRow: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + width: "100%", + padding: "10px 14px", + border: "none", + borderBottom: "1px solid rgba(39, 39, 42, 0.5)", + background: "transparent", + color: "#E4E4E7", + cursor: "pointer", + textAlign: "left", + transition: "background-color 0.1s", + }, + repoInfo: { + display: "flex", + flexDirection: "column", + gap: 2, + minWidth: 0, + }, + repoName: { + fontSize: 13, + fontWeight: 600, + fontFamily: "monospace", + overflow: "hidden", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + }, + repoOwner: { + fontSize: 11, + color: "#71717A", + }, + repoMeta: { + display: "flex", + alignItems: "center", + gap: 8, + flexShrink: 0, + }, + privateBadge: { + fontSize: 9, + padding: "1px 5px", + borderRadius: 8, + backgroundColor: "rgba(239, 68, 68, 0.12)", + color: "#F87171", + fontWeight: 600, + textTransform: "uppercase", + }, + branchHint: { + fontSize: 10, + color: "#52525B", + fontFamily: "monospace", + }, +}; diff --git a/frontend/components/AssistantMessage.jsx b/frontend/components/AssistantMessage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5dd34a1f20c9d1026069ef69d69b92f2582c5d81 --- /dev/null +++ b/frontend/components/AssistantMessage.jsx @@ -0,0 +1,116 @@ +import React from "react"; +import PlanView from "./PlanView.jsx"; + +export default function AssistantMessage({ answer, plan, executionLog }) { + const styles = { + container: { + marginBottom: "20px", + padding: "20px", + backgroundColor: "#18181B", // Zinc-900 + borderRadius: "12px", + border: "1px solid #27272A", // Zinc-800 + color: "#F4F4F5", // Zinc-100 + fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", + }, + section: { + marginBottom: "20px", + }, + lastSection: { + marginBottom: "0", + }, + header: { + display: "flex", + alignItems: "center", + marginBottom: "12px", + paddingBottom: "8px", + borderBottom: "1px solid #3F3F46", // Zinc-700 + }, + title: { + fontSize: "12px", + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: "0.05em", + color: "#A1A1AA", // Zinc-400 + margin: 0, + }, + content: { + fontSize: "14px", + lineHeight: "1.6", + whiteSpace: "pre-wrap", + }, + executionList: { + listStyle: "none", + padding: 0, + margin: 0, + display: "flex", + flexDirection: "column", + gap: "8px", + }, + executionStep: { + display: "flex", + flexDirection: "column", + gap: "4px", + padding: "10px", + backgroundColor: "#09090B", // Zinc-950 + borderRadius: "6px", + border: "1px solid #27272A", + fontSize: "13px", + }, + stepNumber: { + fontSize: "11px", + fontWeight: "600", + color: "#10B981", // Emerald-500 + textTransform: "uppercase", + }, + stepSummary: { + color: "#D4D4D8", // Zinc-300 + fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace", + }, + }; + + return ( +
+ {/* Answer section */} +
+
+

Answer

+
+
+

{answer}

+
+
+ + {/* Action Plan section */} + {plan && ( +
+
+

Action Plan

+
+
+ +
+
+ )} + + {/* Execution Log section (shown after execution) */} + {executionLog && ( +
+
+

Execution Log

+
+
+
    + {executionLog.steps.map((s) => ( +
  • + Step {s.step_number} + {s.summary} +
  • + ))} +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/frontend/components/BranchPicker.jsx b/frontend/components/BranchPicker.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e04e04d77dbda1a8532b26c9c2d3fc92fb7d2616 --- /dev/null +++ b/frontend/components/BranchPicker.jsx @@ -0,0 +1,398 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { createPortal } from "react-dom"; + +/** + * BranchPicker β€” Claude-Code-on-Web parity branch selector. + * + * Fetches branches from the new /api/repos/{owner}/{repo}/branches endpoint. + * Shows search, default branch badge, AI session branch highlighting. + * + * Fixes applied: + * - Dropdown portaled to document.body (avoids overflow:hidden clipping) + * - Branches cached per repo (no "No branches found" flash) + * - Shows "Loading..." only on first fetch, keeps stale data otherwise + */ + +// Simple per-repo branch cache so reopening the dropdown is instant +const branchCache = {}; + +/** + * Props: + * repo, currentBranch, defaultBranch, sessionBranches, onBranchChange + * β€” standard branch-picker props + * + * externalAnchorRef (optional) β€” a React ref pointing to an external DOM + * element to anchor the dropdown to. When provided: + * - BranchPicker skips rendering its own trigger button + * - the dropdown opens immediately on mount + * - closing the dropdown calls onClose() + * + * onClose (optional) β€” called when the dropdown is dismissed (outside + * click or Escape). Only meaningful with externalAnchorRef. + */ +export default function BranchPicker({ + repo, + currentBranch, + defaultBranch, + sessionBranches = [], + onBranchChange, + externalAnchorRef, + onClose, +}) { + const isExternalMode = !!externalAnchorRef; + const [open, setOpen] = useState(isExternalMode); + const [query, setQuery] = useState(""); + const [branches, setBranches] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const triggerRef = useRef(null); + const dropdownRef = useRef(null); + const inputRef = useRef(null); + + const branch = currentBranch || defaultBranch || "main"; + const isAiSession = sessionBranches.includes(branch) && branch !== defaultBranch; + + // The element used for dropdown positioning + const anchorRef = isExternalMode ? externalAnchorRef : triggerRef; + + const cacheKey = repo ? `${repo.owner}/${repo.name}` : null; + + // Seed from cache on mount / repo change + useEffect(() => { + if (cacheKey && branchCache[cacheKey]) { + setBranches(branchCache[cacheKey]); + } + }, [cacheKey]); + + // Fetch branches from GitHub via backend + const fetchBranches = useCallback(async (searchQuery) => { + if (!repo) return; + setLoading(true); + setError(null); + try { + const token = localStorage.getItem("github_token"); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + const params = new URLSearchParams({ per_page: "100" }); + if (searchQuery) params.set("query", searchQuery); + + const res = await fetch( + `/api/repos/${repo.owner}/${repo.name}/branches?${params}`, + { headers, cache: "no-cache" } + ); + if (!res.ok) { + const errData = await res.json().catch(() => ({})); + const detail = errData.detail || `HTTP ${res.status}`; + console.warn("BranchPicker: fetch failed:", detail); + setError(detail); + return; + } + const data = await res.json(); + const fetched = data.branches || []; + setBranches(fetched); + + // Only cache the unfiltered result + if (!searchQuery && cacheKey) { + branchCache[cacheKey] = fetched; + } + } catch (err) { + console.warn("Failed to fetch branches:", err); + } finally { + setLoading(false); + } + }, [repo, cacheKey]); + + // Fetch + focus when opened + useEffect(() => { + if (open) { + fetchBranches(query); + setTimeout(() => inputRef.current?.focus(), 50); + } + }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + + // Debounced search + useEffect(() => { + if (!open) return; + const t = setTimeout(() => fetchBranches(query), 300); + return () => clearTimeout(t); + }, [query, open, fetchBranches]); + + // Close on outside click + useEffect(() => { + if (!open) return; + const handler = (e) => { + const inAnchor = anchorRef.current && anchorRef.current.contains(e.target); + const inDropdown = dropdownRef.current && dropdownRef.current.contains(e.target); + if (!inAnchor && !inDropdown) { + handleClose(); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [open]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleClose = useCallback(() => { + setOpen(false); + setQuery(""); + onClose?.(); + }, [onClose]); + + const handleSelect = (branchName) => { + handleClose(); + if (branchName !== branch) { + onBranchChange?.(branchName); + } + }; + + // Merge API branches with session branches (AI branches might not show in GitHub API) + const allBranches = [...branches]; + for (const sb of sessionBranches) { + if (!allBranches.find((b) => b.name === sb)) { + allBranches.push({ name: sb, is_default: false, protected: false }); + } + } + + // Calculate portal position from anchor element + const getDropdownPosition = () => { + if (!anchorRef.current) return { top: 0, left: 0 }; + const rect = anchorRef.current.getBoundingClientRect(); + return { + top: rect.bottom + 4, + left: rect.left, + }; + }; + + const pos = open ? getDropdownPosition() : { top: 0, left: 0 }; + + return ( +
+ {/* Trigger button β€” hidden when using external anchor */} + {!isExternalMode && ( + + )} + + {/* Dropdown β€” portaled to document.body to escape overflow:hidden */} + {open && createPortal( +
+ {/* Search input */} +
+ setQuery(e.target.value)} + style={styles.searchInput} + onKeyDown={(e) => { + if (e.key === "Escape") { + handleClose(); + } + }} + /> +
+ + {/* Branch list */} +
+ {loading && allBranches.length === 0 && ( +
Loading...
+ )} + + {!loading && error && ( +
{error}
+ )} + + {!loading && !error && allBranches.length === 0 && ( +
No branches found
+ )} + + {allBranches.map((b) => { + const isDefault = b.is_default || b.name === defaultBranch; + const isAi = sessionBranches.includes(b.name); + const isCurrent = b.name === branch; + + return ( +
handleSelect(b.name)} + > + + ✓ + + + {b.name} + + {isDefault && ( + default + )} + {isAi && !isDefault && ( + AI + )} + {b.protected && ( + + + + + + )} +
+ ); + })} + + {/* Subtle loading indicator when refreshing with cached data visible */} + {loading && allBranches.length > 0 && ( +
Updating...
+ )} +
+
, + document.body + )} +
+ ); +} + +const styles = { + container: { + position: "relative", + }, + trigger: { + display: "flex", + alignItems: "center", + gap: 6, + padding: "4px 8px", + borderRadius: 4, + border: "1px solid #3F3F46", + background: "transparent", + fontSize: 13, + cursor: "pointer", + fontFamily: "monospace", + maxWidth: 200, + }, + branchName: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + maxWidth: 140, + }, + dropdown: { + position: "fixed", + width: 280, + backgroundColor: "#1F1F23", + border: "1px solid #27272A", + borderRadius: 8, + boxShadow: "0 8px 24px rgba(0,0,0,0.6)", + zIndex: 9999, + overflow: "hidden", + }, + searchBox: { + padding: "8px 10px", + borderBottom: "1px solid #27272A", + }, + searchInput: { + width: "100%", + padding: "6px 8px", + borderRadius: 4, + border: "1px solid #3F3F46", + background: "#131316", + color: "#E4E4E7", + fontSize: 12, + outline: "none", + fontFamily: "monospace", + boxSizing: "border-box", + }, + branchList: { + maxHeight: 260, + overflowY: "auto", + }, + branchRow: { + display: "flex", + alignItems: "center", + gap: 6, + padding: "7px 10px", + cursor: "pointer", + transition: "background-color 0.1s", + borderBottom: "1px solid rgba(39, 39, 42, 0.5)", + }, + loadingRow: { + padding: "12px 10px", + textAlign: "center", + fontSize: 12, + color: "#71717A", + }, + errorRow: { + padding: "12px 10px", + textAlign: "center", + fontSize: 11, + color: "#F59E0B", + }, + defaultBadge: { + fontSize: 9, + padding: "1px 5px", + borderRadius: 8, + backgroundColor: "rgba(16, 185, 129, 0.15)", + color: "#10B981", + fontWeight: 600, + textTransform: "uppercase", + letterSpacing: "0.04em", + flexShrink: 0, + }, + aiBadge: { + fontSize: 9, + padding: "1px 5px", + borderRadius: 8, + backgroundColor: "rgba(59, 130, 246, 0.15)", + color: "#60a5fa", + fontWeight: 700, + flexShrink: 0, + }, + protectedBadge: { + color: "#F59E0B", + flexShrink: 0, + display: "flex", + alignItems: "center", + }, +}; diff --git a/frontend/components/ChatPanel.jsx b/frontend/components/ChatPanel.jsx new file mode 100644 index 0000000000000000000000000000000000000000..053690ac940ae2cc215191b8eed1cd6d5c19b030 --- /dev/null +++ b/frontend/components/ChatPanel.jsx @@ -0,0 +1,686 @@ +// frontend/components/ChatPanel.jsx +import React, { useEffect, useRef, useState } from "react"; +import AssistantMessage from "./AssistantMessage.jsx"; +import DiffStats from "./DiffStats.jsx"; +import DiffViewer from "./DiffViewer.jsx"; +import CreatePRButton from "./CreatePRButton.jsx"; +import StreamingMessage from "./StreamingMessage.jsx"; +import { SessionWebSocket } from "../utils/ws.js"; + +// Helper to get headers (inline safety if utility is missing) +const getHeaders = () => ({ + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("github_token") || ""}`, +}); + +export default function ChatPanel({ + repo, + defaultBranch = "main", + currentBranch, // do NOT default here; parent must pass the real one + onExecutionComplete, + sessionChatState, + onSessionChatStateChange, + sessionId, + onEnsureSession, + canChat = true, // readiness gate: false disables composer and shows blocker + chatBlocker = null, // { message: string, cta?: string, onCta?: () => void } +}) { + // Initialize state from props or defaults + const [messages, setMessages] = useState(sessionChatState?.messages || []); + const [goal, setGoal] = useState(""); + const [plan, setPlan] = useState(sessionChatState?.plan || null); + + const [loadingPlan, setLoadingPlan] = useState(false); + const [executing, setExecuting] = useState(false); + const [status, setStatus] = useState(""); + + // Claude-Code-on-Web: WebSocket streaming + diff + PR + const [wsConnected, setWsConnected] = useState(false); + const [streamingEvents, setStreamingEvents] = useState([]); + const [diffData, setDiffData] = useState(null); + const [showDiffViewer, setShowDiffViewer] = useState(false); + const wsRef = useRef(null); + + // Ref mirrors streamingEvents so WS callbacks avoid stale closures + const streamingEventsRef = useRef([]); + useEffect(() => { streamingEventsRef.current = streamingEvents; }, [streamingEvents]); + + // Skip the session-sync useEffect reset when we just created a session + // (the parent already seeded the messages into chatBySession) + const skipNextSyncRef = useRef(false); + + const messagesEndRef = useRef(null); + const prevMsgCountRef = useRef((sessionChatState?.messages || []).length); + + // --------------------------------------------------------------------------- + // WebSocket connection management + // --------------------------------------------------------------------------- + useEffect(() => { + // Clean up previous connection + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + setWsConnected(false); + } + + if (!sessionId) return; + + const ws = new SessionWebSocket(sessionId, { + onConnect: () => setWsConnected(true), + onDisconnect: () => setWsConnected(false), + onMessage: (data) => { + if (data.type === "agent_message") { + setStreamingEvents((prev) => [...prev, data]); + } else if (data.type === "tool_use" || data.type === "tool_result") { + setStreamingEvents((prev) => [...prev, data]); + } else if (data.type === "diff_update") { + setDiffData(data.stats || data); + } else if (data.type === "session_restored") { + // Session loaded + } + }, + onStatusChange: (newStatus) => { + if (newStatus === "waiting") { + // Always clear loading state when agent finishes + setLoadingPlan(false); + + // Consolidate streaming events into a chat message (use ref to + // avoid stale closure β€” streamingEvents state would be stale here) + const events = streamingEventsRef.current; + if (events.length > 0) { + const textParts = events + .filter((e) => e.type === "agent_message") + .map((e) => e.content); + if (textParts.length > 0) { + const consolidated = { + from: "ai", + role: "assistant", + answer: textParts.join(""), + content: textParts.join(""), + }; + setMessages((prev) => [...prev, consolidated]); + } + setStreamingEvents([]); + } + } + }, + onError: (err) => { + console.warn("[ws] Error:", err); + setLoadingPlan(false); + }, + }); + + ws.connect(); + wsRef.current = ws; + + return () => { + ws.close(); + }; + }, [sessionId]); // eslint-disable-line react-hooks/exhaustive-deps + + // --------------------------------------------------------------------------- + // 1) SESSION SYNC: Restore chat when branch, repo, OR session changes + // IMPORTANT: Do NOT depend on sessionChatState here (prevents prop/state loop) + // --------------------------------------------------------------------------- + useEffect(() => { + // When send() just created a session, the parent seeded the messages + // into chatBySession already. Skip the reset so we don't wipe + // the optimistic user message that was already rendered. + if (skipNextSyncRef.current) { + skipNextSyncRef.current = false; + return; + } + + const nextMessages = sessionChatState?.messages || []; + const nextPlan = sessionChatState?.plan || null; + + setMessages(nextMessages); + setPlan(nextPlan); + + // Reset transient UI state on branch/repo/session switch + setGoal(""); + setStatus(""); + setLoadingPlan(false); + setExecuting(false); + setStreamingEvents([]); + setDiffData(null); + + // Update msg count tracker so auto-scroll doesn't "jump" on switch + prevMsgCountRef.current = nextMessages.length; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentBranch, repo?.full_name, sessionId]); + + // --------------------------------------------------------------------------- + // 2) PERSISTENCE: Save chat to Parent (no loop now because sync only on branch) + // --------------------------------------------------------------------------- + useEffect(() => { + if (typeof onSessionChatStateChange === "function") { + // Avoid wiping parent state on mount + if (messages.length > 0 || plan) { + onSessionChatStateChange({ messages, plan }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messages, plan]); + + // --------------------------------------------------------------------------- + // 3) AUTO-SCROLL: Only scroll when a message is appended (reduces flicker) + // --------------------------------------------------------------------------- + useEffect(() => { + const curCount = messages.length + streamingEvents.length; + const prevCount = prevMsgCountRef.current; + + // Only scroll when new messages are added + if (curCount > prevCount) { + prevMsgCountRef.current = curCount; + requestAnimationFrame(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }); + } else { + prevMsgCountRef.current = curCount; + } + }, [messages.length, streamingEvents.length]); + + // --------------------------------------------------------------------------- + // HANDLERS + // --------------------------------------------------------------------------- + // --------------------------------------------------------------------------- + // Persist a message to the backend session (fire-and-forget) + // --------------------------------------------------------------------------- + const persistMessage = (sid, role, content) => { + if (!sid) return; + fetch(`/api/sessions/${sid}/message`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ role, content }), + }).catch(() => {}); // best-effort + }; + + const send = async () => { + if (!repo || !goal.trim()) return; + + const text = goal.trim(); + + // Optimistic update (user bubble appears immediately) + const userMsg = { from: "user", role: "user", text, content: text }; + setMessages((prev) => [...prev, userMsg]); + + setLoadingPlan(true); + setStatus(""); + setPlan(null); + setStreamingEvents([]); + + // ------- Implicit session creation (Claude Code parity) ------- + // Every chat must be backed by a session. If none exists yet, + // create one on-demand before sending the plan request. + let sid = sessionId; + if (!sid && typeof onEnsureSession === "function") { + // Derive a short title from the first message + const sessionName = text.length > 60 ? text.slice(0, 57) + "..." : text; + + // Tell the sync useEffect to skip the reset that would otherwise + // wipe the optimistic user message when activeSessionId changes. + skipNextSyncRef.current = true; + + sid = await onEnsureSession(sessionName, [userMsg]); + if (!sid) { + // Session creation failed β€” continue without session + skipNextSyncRef.current = false; + } + } + + // Persist user message to backend session + persistMessage(sid, "user", text); + + // Always use HTTP for plan generation (the original reliable flow). + // WebSocket is only used for real-time streaming feedback display. + const effectiveBranch = currentBranch || defaultBranch || "HEAD"; + + try { + const res = await fetch("/api/chat/plan", { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ + repo_owner: repo.owner, + repo_name: repo.name, + goal: text, + branch_name: effectiveBranch, + }), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || "Failed to generate plan"); + + setPlan(data); + + // Extract summary from nested plan structure or top-level + const summary = + data.plan?.summary || data.summary || data.message || + "Here is the proposed plan for your request."; + + // Assistant response (Answer + Action Plan) + setMessages((prev) => [ + ...prev, + { + from: "ai", + role: "assistant", + answer: summary, + content: summary, + plan: data, + }, + ]); + + // Persist assistant response to backend session + persistMessage(sid, "assistant", summary); + + // Clear input only after success + setGoal(""); + } catch (err) { + const msg = String(err?.message || err); + console.error(err); + setStatus(msg); + setMessages((prev) => [ + ...prev, + { from: "ai", role: "system", content: `Error: ${msg}` }, + ]); + } finally { + setLoadingPlan(false); + } + }; + + const execute = async () => { + if (!repo || !plan) return; + + setExecuting(true); + setStatus(""); + + try { + // Guard: currentBranch might be missing if parent didn't pass it yet + const safeCurrent = currentBranch || defaultBranch || "HEAD"; + const safeDefault = defaultBranch || "main"; + + // Sticky vs Hard Switch: + // - If on default branch -> undefined (backend creates new branch) + // - If already on AI branch -> currentBranch (backend updates existing) + const branch_name = safeCurrent === safeDefault ? undefined : safeCurrent; + + const res = await fetch("/api/chat/execute", { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ + repo_owner: repo.owner, + repo_name: repo.name, + plan, + branch_name, + }), + }); + + const data = await res.json(); + if (!res.ok) throw new Error(data.detail || "Execution failed"); + + setStatus(data.message || "Execution completed."); + + const completionMsg = { + from: "ai", + role: "assistant", + answer: data.message || "Execution completed.", + content: data.message || "Execution completed.", + executionLog: data.executionLog, + }; + + // Show completion immediately (keeps old "Execution Log" section) + setMessages((prev) => [...prev, completionMsg]); + + // Clear active plan UI + setPlan(null); + + // Pass completionMsg upward for seeding branch history + if (typeof onExecutionComplete === "function") { + onExecutionComplete({ + branch: data.branch || data.branch_name, + mode: data.mode, + commit_url: data.commit_url || data.html_url, + message: data.message, + completionMsg, + sourceBranch: safeCurrent, + }); + } + } catch (err) { + console.error(err); + setStatus(String(err?.message || err)); + } finally { + setExecuting(false); + } + }; + + // --------------------------------------------------------------------------- + // RENDER + // --------------------------------------------------------------------------- + const isOnSessionBranch = currentBranch && currentBranch !== defaultBranch; + + return ( +
+ + +
+ {messages.map((m, idx) => { + // Success message (App.jsx injected) + if (m.isSuccess) { + return ( +
+
πŸš€
+
+
{m.content}
+ {m.link && ( + + View Changes on GitHub → + + )} +
+
+ ); + } + + // User message + if (m.from === "user" || m.role === "user") { + return ( +
+ {m.text || m.content} +
+ ); + } + + // Assistant message (Answer / Plan / Execution Log) + return ( +
+ + {/* Diff stats indicator (Claude-Code-on-Web parity) */} + {m.diff && ( + { + setDiffData(m.diff); + setShowDiffViewer(true); + }} /> + )} +
+ ); + })} + + {/* Streaming events (real-time agent output) */} + {streamingEvents.length > 0 && ( +
+ +
+ )} + + {loadingPlan && streamingEvents.length === 0 && ( +
+ Thinking... +
+ )} + + {!messages.length && !plan && !loadingPlan && streamingEvents.length === 0 && ( +
+
πŸ’¬
+

Tell GitPilot what you want to do with this repository.

+

+ It will propose a safe step-by-step plan before any execution. +

+
+ )} + +
+
+ + {/* Diff stats bar (when agent has made changes) */} + {diffData && ( +
+ setShowDiffViewer(true)} /> +
+ )} + +
+ {/* Readiness blocker banner */} + {!canChat && chatBlocker && ( +
+ {chatBlocker.message || "Chat is not ready yet."} + {chatBlocker.cta && chatBlocker.onCta && ( + + )} +
+ )} + {status && ( +
+ {status} +
+ )} + +
+