# AI Server IPC Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Decouple heavy AI SDK imports from GUI via subprocess command queue, achieving ~0.5s instant GUI startup.

**Architecture:** Subprocess spawns `python -m src.ai_server` which loads google.genai/anthropic. GUI communicates via JSON-RPC over stdin/stdout pipe. Command queue pattern with response matching by UUID.

**Tech Stack:** Python subprocess, JSON-RPC, threading, queue-based IPC

---

## File Structure

### New Files
- `src/ai_server.py` - Subprocess AI server (stdin/stdout JSON-RPC)
- `src/ai_client_proxy.py` - Queue client for GUI
- `tests/test_ai_server.py` - Server tests
- `tests/test_ai_client_proxy.py` - Proxy tests

### Modified Files
- `src/ai_client.py` - Add proxy routing when AI_SERVER_ENABLED env var
- `sloppy.py` - Set AI_SERVER_ENABLED=1

---

## Task 1: Create ai_client_proxy.py - Queue Client

**Files:**
- Create: `src/ai_client_proxy.py`
- Test: `tests/test_ai_client_proxy.py`

- [ ] **Step 1: Write failing test for proxy basic operations**

```python
# tests/test_ai_client_proxy.py
import pytest
from src.ai_client_proxy import AIProxyClient

def test_proxy_initialization():
    proxy = AIProxyClient()
    assert proxy._status == "disconnected"
    assert proxy._pending == {}

def test_proxy_status_states():
    proxy = AIProxyClient()
    assert proxy.status in ("disconnected", "init", "ready", "busy", "error")
```

Run: `uv run pytest tests/test_ai_client_proxy.py -v`
Expected: FAIL - module not found

- [ ] **Step 2: Implement minimal AIProxyClient**

```python
# src/ai_client_proxy.py
import json
import uuid
import threading
import subprocess
from typing import Any, Optional

class AIProxyClient:
    def __init__(self):
        self._process: Optional[subprocess.Popen] = None
        self._status: str = "disconnected"
        self._pending: dict[str, threading.Event] = {}
        self._reader_thread: Optional[threading.Thread] = None

    @property
    def status(self) -> str:
        return self._status

    def start_server(self):
        self._process = subprocess.Popen(
            ["python", "-m", "src.ai_server"],
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True
        )
        self._status = "init"
        self._reader_thread = threading.Thread(target=self._read_loop, daemon=True)
        self._reader_thread.start()

    def _read_loop(self):
        pass  # stub

    def stop(self):
        if self._process:
            self._process.terminate()
            self._process.wait(timeout=5)
            self._process = None
        self._status = "disconnected"

    def send_command(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
        request_id = str(uuid.uuid4())
        event = threading.Event()
        self._pending[request_id] = event
        command = {"id": request_id, "method": method, "params": params}
        self._process.stdin.write(json.dumps(command) + "\n")
        self._process.stdin.flush()
        event.wait(timeout=60)
        result = self._pending.pop(request_id, None)
        return result or {"error": "timeout"}
```

Run: `uv run pytest tests/test_ai_client_proxy.py::test_proxy_initialization -v`
Expected: PASS

- [ ] **Step 3: Write test for command/response matching**

```python
# Add to tests/test_ai_client_proxy.py

def test_send_command_returns_response():
    proxy = AIProxyClient()
    proxy._status = "ready"
    proxy._process = MockPopen()

    response = proxy.send_command("list_models", {"provider": "gemini"})
    assert "result" in response or "error" in response
```

Run: `uv run pytest tests/test_ai_client_proxy.py -v`
Expected: FAIL - MockPopen not defined

- [ ] **Step 4: Implement command/response matching**

Add to `src/ai_client_proxy.py`:

```python
def _read_loop(self):
    for line in self._process.stdout:
        try:
            response = json.loads(line.strip())
            rid = response.get("id")
            if rid in self._pending:
                self._pending[rid] = response
                self._pending[rid + "_event"].set()
        except json.JSONDecodeError:
            pass

def send_command(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
    request_id = str(uuid.uuid4())
    event = threading.Event()
    self._pending[request_id] = None
    self._pending[request_id + "_event"] = event
    command = {"id": request_id, "method": method, "params": params}
    self._process.stdin.write(json.dumps(command) + "\n")
    self._process.stdin.flush()
    if not event.wait(timeout=60):
        return {"error": "timeout"}
    result = self._pending.pop(request_id, {})
    self._pending.pop(request_id + "_event", None)
    return result
```

Run: `uv run pytest tests/test_ai_client_proxy.py -v`
Expected: PASS

- [ ] **Step 5: Write test for status tracking**

```python
# Add to tests/test_ai_client_proxy.py

def test_status_reflects_server_state():
    proxy = AIProxyClient()
    assert proxy.status == "disconnected"
    proxy._status = "ready"
    assert proxy.status == "ready"
```

Run: `uv run pytest tests/test_ai_client_proxy.py -v`
Expected: PASS

- [ ] **Step 6: Commit**

```bash
git add src/ai_client_proxy.py tests/test_ai_client_proxy.py
git commit -m "feat(ai-server): Add AIProxyClient queue communication layer"
```

---

## Task 2: Create ai_server.py - Subprocess Server

**Files:**
- Create: `src/ai_server.py`
- Test: `tests/test_ai_server.py`

- [ ] **Step 1: Write failing test for server startup**

```python
# tests/test_ai_server.py
import pytest
import subprocess
import json
import time
import sys
import os

def test_server_starts_and_loads():
    proc = subprocess.Popen(
        [sys.executable, "-m", "src.ai_server"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    # Server should output ready marker
    line = proc.stdout.readline()
    proc.stdin.close()
    proc.stdout.close()
    proc.wait(timeout=5)
    assert "ready" in line.lower() or proc.returncode == 0
```

Run: `uv run pytest tests/test_ai_server.py::test_server_starts_and_loads -v`
Expected: FAIL - module not found

- [ ] **Step 2: Implement minimal ai_server.py**

```python
# src/ai_server.py
#!/usr/bin/env python
import json
import sys

def main():
    # Signal ready
    print(json.dumps({"type": "ready"}))
    sys.stdout.flush()

    for line in sys.stdin:
        try:
            cmd = json.loads(line.strip())
            print(json.dumps({"id": cmd.get("id"), "result": {}}))
            sys.stdout.flush()
        except json.JSONDecodeError:
            pass

if __name__ == "__main__":
    main()
```

Run: `uv run pytest tests/test_ai_server.py::test_server_starts_and_loads -v`
Expected: PASS

- [ ] **Step 3: Write test for list_models command**

```python
# Add to tests/test_ai_server.py

def test_list_models_returns_models():
    proc = subprocess.Popen(
        [sys.executable, "-m", "src.ai_server"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    # Read ready
    proc.stdout.readline()
    # Send list_models command
    cmd = json.dumps({"id": "1", "method": "list_models", "params": {"provider": "gemini"}})
    proc.stdin.write(cmd + "\n")
    proc.stdin.flush()
    # Read response
    resp = proc.stdout.readline()
    result = json.loads(resp)
    assert "result" in result or "error" in result
    proc.stdin.close()
    proc.stdout.close()
    proc.wait()
```

Run: `uv run pytest tests/test_ai_server.py::test_list_models_returns_models -v`
Expected: FAIL - list_models not implemented

- [ ] **Step 4: Implement list_models command**

Update `src/ai_server.py`:

```python
# src/ai_server.py
#!/usr/bin/env python
import json
import sys

_PROVIDERS = {
    "gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview"],
    "anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
}

def handle_command(cmd: dict) -> dict:
    method = cmd.get("method", "")
    params = cmd.get("params", {})

    if method == "list_models":
        provider = params.get("provider", "gemini")
        return {"id": cmd.get("id"), "result": {"models": _PROVIDERS.get(provider, [])}}

    return {"id": cmd.get("id"), "error": f"Unknown method: {method}"}

def main():
    print(json.dumps({"type": "ready"}))
    sys.stdout.flush()

    for line in sys.stdin:
        try:
            cmd = json.loads(line.strip())
            response = handle_command(cmd)
            print(json.dumps(response))
            sys.stdout.flush()
        except json.JSONDecodeError:
            pass

if __name__ == "__main__":
    main()
```

Run: `uv run pytest tests/test_ai_server.py::test_list_models_returns_models -v`
Expected: PASS

- [ ] **Step 5: Write test for google.genai loading**

```python
# Add to tests/test_ai_server.py

def test_server_loads_google_genai():
    import time
    proc = subprocess.Popen(
        [sys.executable, "-m", "src.ai_server"],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True
    )
    start = time.time()
    ready_line = proc.stdout.readline()
    elapsed = time.time() - start
    proc.stdin.close()
    proc.stdout.close()
    proc.wait(timeout=10)
    # Should load in reasonable time (allow up to 5s for SDK)
    assert elapsed < 5, f"Server took {elapsed}s to start"
```

Run: `uv run pytest tests/test_ai_server.py::test_server_loads_google_genai -v`
Expected: FAIL - needs implementation

- [ ] **Step 6: Implement google.genai loading**

Update `src/ai_server.py`:

```python
# src/ai_server.py
#!/usr/bin/env python
import json
import sys
import time

_PROVIDERS = {
    "gemini": ["gemini-2.5-flash-lite", "gemini-3-flash-preview"],
    "anthropic": ["claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022"],
}

_google_genai = None
_anthropic = None

def _ensure_google_genai():
    global _google_genai
    if _google_genai is None:
        from google import genai
        _google_genai = genai
    return _google_genai

def handle_command(cmd: dict) -> dict:
    method = cmd.get("method", "")
    params = cmd.get("params", {})

    if method == "list_models":
        provider = params.get("provider", "gemini")
        if provider == "gemini":
            _ensure_google_genai()
        return {"id": cmd.get("id"), "result": {"models": _PROVIDERS.get(provider, [])}}

    if method == "send":
        _ensure_google_genai()
        return {"id": cmd.get("id"), "result": {"status": "processed"}}

    return {"id": cmd.get("id"), "error": f"Unknown method: {method}"}

def main():
    print(json.dumps({"type": "ready"}))
    sys.stdout.flush()

    for line in sys.stdin:
        try:
            cmd = json.loads(line.strip())
            response = handle_command(cmd)
            print(json.dumps(response))
            sys.stdout.flush()
        except Exception as e:
            print(json.dumps({"error": str(e)}))
            sys.stdout.flush()

if __name__ == "__main__":
    main()
```

Run: `uv run pytest tests/test_ai_server.py::test_server_loads_google_genai -v`
Expected: PASS

- [ ] **Step 7: Commit**

```bash
git add src/ai_server.py tests/test_ai_server.py
git commit -m "feat(ai-server): Add ai_server subprocess with google.genai lazy loading"
```

---

## Task 3: Wire AIProxyClient into ai_client.py

**Files:**
- Modify: `src/ai_client.py` (add proxy routing)
- Modify: `sloppy.py` (enable AI server)

- [ ] **Step 1: Write test for proxy integration**

```python
# tests/test_ai_client_integration.py
import pytest
import os

def test_ai_client_uses_proxy_when_enabled(monkeypatch):
    os.environ["AI_SERVER_ENABLED"] = "1"
    # Import should trigger proxy initialization
    from src import ai_client
    assert hasattr(ai_client, "_proxy")
    assert ai_client._proxy is not None
```

Run: `uv run pytest tests/test_ai_client_integration.py -v`
Expected: FAIL - AI_SERVER_ENABLED not checked

- [ ] **Step 2: Add proxy to ai_client.py**

Modify `src/ai_client.py` - add near top after imports:

```python
# At top of ai_client.py, after other imports
_ai_proxy = None

def _get_proxy():
    global _ai_proxy
    if _ai_proxy is None and os.environ.get("AI_SERVER_ENABLED"):
        from src.ai_client_proxy import AIProxyClient
        _ai_proxy = AIProxyClient()
        _ai_proxy.start_server()
    return _ai_proxy
```

Modify `_list_gemini_models()`:

```python
def _list_gemini_models() -> list[str]:
    proxy = _get_proxy()
    if proxy and proxy.status == "ready":
        result = proxy.send_command("list_models", {"provider": "gemini"})
        if "result" in result:
            return result["result"].get("models", [])
    # Fallback to direct import
    global _gemini_client
    _ensure_gemini_client()
    return [m.name for m in _gemini_client.models.list()]
```

- [ ] **Step 3: Test the integration**

Run: `uv run pytest tests/test_ai_client_integration.py -v`
Expected: PASS

- [ ] **Step 4: Modify sloppy.py to enable AI server**

```python
# sloppy.py - add near top
os.environ["AI_SERVER_ENABLED"] = "1"
```

- [ ] **Step 5: Commit**

```bash
git add src/ai_client.py sloppy.py tests/test_ai_client_integration.py
git commit -m "feat(ai-server): Wire AIProxyClient into ai_client"
```

---

## Task 4: Add GUI Status Indicator and Panel Tinting

**Files:**
- Modify: `src/gui_2.py` - add AI status tracking
- Modify: `src/app_controller.py` - track AI server status

- [ ] **Step 1: Write test for AI status in GUI**

```python
# tests/test_ai_status_gui.py
def test_gui_shows_ai_status():
    from src.gui_2 import App
    # App should have ai_status property
    app = App.__new__(App)
    assert hasattr(app, "ai_status") or "ai_status" in dir(app)
```

Run: `uv run pytest tests/test_ai_status_gui.py -v`
Expected: FAIL - ai_status not in App

- [ ] **Step 2: Add ai_status to App.__init__**

In `src/gui_2.py` `App.__init__`, add:

```python
self.ai_status = "disconnected"  # disconnected, init, ready, busy, error
```

- [ ] **Step 3: Add status panel rendering**

In `src/gui_2.py` `_render_provider_panel()` or similar, add tint indicator:

```python
# At top of panel
if getattr(self, 'ai_status', 'ready') != 'ready':
    imgui.push_style_color(imgui.COLOR_WINDOW_BACKGROUND, 0.5, 0.5, 0.5, 1.0)
    imgui.text_colored(imgui.Vec4(1, 0.5, 0, 1), "[AI: Initializing...]")
# ... rest of panel ...
if self.ai_status != 'ready':
    imgui.pop_style_color()
```

- [ ] **Step 4: Test tinted panel**

Run: `uv run pytest tests/test_ai_status_gui.py -v`
Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add src/gui_2.py tests/test_ai_status_gui.py
git commit -m "feat(gui): Add AI status indicator and panel tinting"
```

---

## Task 5: End-to-End Test

- [ ] **Step 1: Full startup test**

```bash
cd C:\projects\manual_slop
timeout 30 uv run python sloppy.py &
sleep 3
# Check if GUI responds
curl http://localhost:8999/status 2>/dev/null || echo "Hook API not ready"
```

- [ ] **Step 2: Verify startup time**

```powershell
Measure-Command { uv run python sloppy.py } | Select TotalSeconds
```

Target: < 2 seconds to GUI visible

- [ ] **Step 3: Commit**

```bash
git add -A
git commit -m "feat(ai-server): Complete AI server IPC integration"
```

---

## Verification

1. **Startup time**: GUI should appear in < 2 seconds
2. **AI panels**: Should show "Initializing..." tint until AI server ready
3. **AI functionality**: After ~1.2s, AI panels become active
4. **No regressions**: Existing tests pass
