Codebase Chat

August 15, 2026 · View on GitHub

The codebase chat feature lets users have an interactive conversation with their codebase. The agent uses whichever LLM provider the user has configured, has access to 7 tools from the MCP surface (a curated chat subset — not the full 11-tool MCP default), and streams responses back to the browser in real time showing tool calls as they happen and rendering results in an artifact panel.


Table of Contents

  1. Architecture Overview
  2. Database Schema
  3. ChatProvider Protocol
  4. Tool Registry
  5. Provider Configuration
  6. SSE Streaming Protocol
  7. Agentic Loop
  8. REST API Endpoints
  9. Frontend Architecture
  10. Provider-Specific Notes

1. Architecture Overview

User types question
        |
        v
POST /api/repos/{repo_id}/chat/messages
        |
        v
+------ Chat Router (SSE stream) ------+
|                                       |
|  1. Create/load conversation          |
|  2. Save user message to DB           |
|  3. Build LLM message history         |
|  4. Call provider.stream_chat()  <----+---- tool_executor callback
|        |                              |
|        v                              |
|  5. Stream text_delta events -------> SSE to browser
|  6. On tool_start:                    |
|     - Execute tool (or provider       |
|       executes internally)            |
|     - Emit tool_result event -------> SSE to browser
|  7. If tool calls found:             |
|     - Append to history, loop to 4   |
|  8. If no tool calls:                |
|     - Save assistant message to DB   |
|     - Emit done event                |
+---------------------------------------+

The agentic loop is in the chat router for most providers (OpenAI, Anthropic, Ollama, LiteLLM). For Gemini, the loop runs inside stream_chat() using native Content objects to preserve thought signatures. The router passes a tool_executor callback that Gemini calls internally.


2. Database Schema

Two tables added in migration 0005_chat_conversations.py:

conversations

ColumnTypeNotes
idString(32) PKUUID hex
repository_idString(32) FKCASCADE delete
titleTextAuto-generated from first 6 words
created_atDateTime(tz)
updated_atDateTime(tz)Auto-updated on new messages

Index: ix_conversations_repo_updated on (repository_id, updated_at)

chat_messages

ColumnTypeNotes
idString(32) PKUUID hex
conversation_idString(32) FKCASCADE delete
roleString(32)user or assistant
content_jsonTextJSON blob (see below)
created_atDateTime(tz)

Index: ix_chat_messages_conv_created on (conversation_id, created_at)

Message content format

User messages:

{"text": "What does the auth module do?"}

Assistant messages:

{
  "text": "The auth module handles...",
  "tool_calls": [
    {
      "id": "call_abc123",
      "name": "get_context",
      "arguments": {"targets": ["src/auth"]},
      "result": { ... }
    }
  ]
}

3. ChatProvider Protocol

Defined in packages/core/src/repowise/core/providers/base.py.

The existing BaseProvider.generate() is untouched. A new ChatProvider protocol class (using typing.Protocol + @runtime_checkable) adds streaming chat with tool use as an opt-in capability.

@runtime_checkable
class ChatProvider(Protocol):
    def stream_chat(
        self,
        messages: list[dict],          # OpenAI-format message list
        tools: list[dict],             # OpenAI-format tool definitions
        system_prompt: str,
        max_tokens: int = 8192,
        temperature: float = 0.7,
        request_id: str | None = None,
        tool_executor: Any | None = None,  # async callable(name, args) -> dict
    ) -> AsyncIterator[ChatStreamEvent]: ...

Supporting dataclasses:

  • ChatToolCall(id, name, arguments) — a tool call the LLM wants to make
  • ChatStreamEvent(type, text?, tool_call?, tool_result_data?, stop_reason?, input_tokens, output_tokens) — a single event in the stream

Event types:

typePopulated fieldsMeaning
text_deltatextIncremental text token(s)
tool_starttool_callLLM wants to call a tool
tool_resulttool_call, tool_result_dataTool executed (by provider internally)
usageinput_tokens, output_tokensToken usage update
stopstop_reasonGeneration ended (end_turn, tool_use, max_tokens)

Implementations: Anthropic, OpenAI, Gemini, Ollama, LiteLLM. All accept the tool_executor parameter; only Gemini uses it (for thought signature handling).


4. Tool Registry

Defined in packages/server/src/repowise/server/chat_tools.py.

Single source of truth for chat tool schemas and execution. Imports 7 MCP tool functions from repowise.server.mcp_server (the chat agent does not advertise the full MCP default surface — no get_answer, get_symbol, get_health, or list_repos here).

TOOL_REGISTRY: dict[str, ToolDef]  # name -> ToolDef(name, description, parameters, function, artifact_type)

Key functions:

FunctionPurpose
get_tool_schemas_for_llm()Returns OpenAI-format tool definitions for the LLM
execute_tool(name, args)Runs a tool and ensures JSON-serializable output
get_artifact_type(name)Maps tool name to frontend artifact type
init_tool_state(...)Bridges FastAPI app state to MCP module globals

Tool to artifact type mapping (7 tools):

ToolArtifact Type
get_overviewoverview
get_contextwiki_page
get_riskrisk_report
get_change_riskrisk_report
get_whydecisions
search_codebasesearch_results
get_dead_codedead_code

5. Provider Configuration

Defined in packages/server/src/repowise/server/provider_config.py.

API keys and active provider/model selection are stored in a server-side provider_config.json file. Environment variables take precedence over stored keys.

Resolution order for API keys:

  1. Environment variable (e.g. GEMINI_API_KEY, ANTHROPIC_API_KEY)
  2. Stored key in provider_config.json

Active provider resolution:

  1. Explicitly set via PATCH /api/providers/active
  2. Auto-detect from first configured provider

Provider catalog: Gemini, Anthropic, OpenAI, Ollama (local, no key), LiteLLM.


6. SSE Streaming Protocol

The chat endpoint returns Content-Type: text/event-stream. Each event is:

event: data
data: {"type": "...", ...}

Event shapes:

// Incremental text from the LLM
{"type": "text_delta", "text": "The auth module..."}

// LLM wants to call a tool
{"type": "tool_start", "tool_id": "call_123", "tool_name": "get_context", "input": {"targets": ["src/auth"]}}

// Tool execution completed
{"type": "tool_result", "tool_id": "call_123", "tool_name": "get_context", "summary": "Context for 1 target(s)", "artifact": {"type": "wiki_page", "data": {...}}}

// Stream complete
{"type": "done", "conversation_id": "abc123", "message_id": "def456"}

// Error
{"type": "error", "message": "Provider error: ..."}

Headers: Cache-Control: no-cache, X-Accel-Buffering: no, Connection: keep-alive

Retry: retry: 3000 sent at stream start.

Terminal event: every stream ends with done or error on the data channel. useChat switches on type and nothing else, so an event sent on a different channel, or without a type, is dropped: the client then sees a stream that simply stopped mid-answer. The client settles its own state when the reader ends without a terminal event, but the server still owes it one.


7. Agentic Loop

The loop runs up to 10 iterations per request.

for each iteration:
    1. Call provider.stream_chat(messages, tools, system_prompt, tool_executor)
    2. Collect text_delta events -> stream to client
    3. Collect tool_start events -> stream to client
    4. Collect tool_result events (from internal execution) -> stream to client
    5. If there are pending tool calls (not internally executed):
       a. Execute each tool
       b. Emit tool_result to client
       c. Append assistant + tool results to message history
       d. Continue loop
    6. If no tool calls: break

After the loop, the assistant message (text + all tool calls with results) is saved to the database and a done event is emitted.


8. REST API Endpoints

Chat

MethodPathDescription
POST/api/repos/{repo_id}/chat/messagesSSE stream — send a message and get a streaming response
GET/api/repos/{repo_id}/chat/conversationsList conversations for a repo
GET/api/repos/{repo_id}/chat/conversations/{id}Get conversation with all messages
DELETE/api/repos/{repo_id}/chat/conversations/{id}Delete a conversation

POST body:

{
  "message": "What does the auth module do?",
  "conversation_id": null,
  "provider": null,
  "model": null
}

conversation_id — omit or null to start a new conversation. provider / model — optional per-request overrides.

Providers

MethodPathDescription
GET/api/providersList all providers with status and active selection
PATCH/api/providers/activeSet active provider and model
POST/api/providers/{id}/keyStore an API key
DELETE/api/providers/{id}/keyRemove an API key

9. Frontend Architecture

API Layer (src/lib/api/)

  • chat.tslistConversations, getConversation, deleteConversation, postChatMessage (returns raw Response for SSE reading)
  • providers.tsgetProviders, setActiveProvider, addProviderKey, removeProviderKey

Hooks (src/lib/hooks/)

  • useChat(repoId) — full chat state machine. Uses fetch + ReadableStream (not EventSource, which is GET-only). Manages messages, streaming state, conversation ID, error handling, and abort control. Exposes sendMessage, loadConversation, reset.
  • useProviders() — SWR wrapper for provider management. Exposes providers, activeProvider, activeModel, activate, saveKey, removeKey.

Components (src/components/chat/)

ComponentPurpose
ChatInterfaceMain container — empty state (greeting + suggestions + model selector) and active state (message list + input)
ChatMessageRenders user bubble or assistant message (tool blocks + markdown)
ChatMarkdownClient-side markdown renderer using react-markdown + remark-gfm with design token styling
ToolCallBlockInline tool call visualization — running (spinner), done collapsed (checkmark + summary), done expanded (input/output JSON)
ArtifactPanelRight slide-in panel with tabs for multiple artifacts. Renders by type: markdown, Mermaid diagrams, search results, raw JSON
ModelSelectorCompact popover for switching provider/model and adding API keys inline
ConversationHistoryDropdown listing past conversations with delete and new-conversation actions

Page Structure

The repo landing page (/repos/[id]) is the chat interface:

  • Compact header (repo name + commit badge + branch badge)
  • ChatInterface filling remaining viewport height
  • Sidebar nav item updated from "Overview" to "Chat"

All other repo sub-pages (graph, wiki, coverage, etc.) are unchanged.


10. Provider-Specific Notes

Anthropic

Uses client.messages.stream() with native Anthropic message format. Converts OpenAI-format messages to Anthropic format (tool results as user role with tool_result content blocks, tool calls as tool_use content blocks). The agentic loop runs in the chat router.

OpenAI

Uses client.chat.completions.create(stream=True). Native OpenAI format — minimal conversion needed. Tool call fragments are accumulated across stream chunks and emitted as complete tool_start events. The agentic loop runs in the chat router.

Gemini

Uses client.models.generate_content() (non-streaming, in a thread pool). Runs the agentic loop internally via the tool_executor callback to preserve thought_signature on function call parts. Gemini's API requires these signatures when replaying function calls in conversation history; the OpenAI-format round-trip through the router would lose them. Native Content objects are used throughout the internal loop.

Ollama

Uses the OpenAI-compatible endpoint (localhost:11434/v1) via AsyncOpenAI. Same streaming pattern as OpenAI. The agentic loop runs in the chat router.

LiteLLM

Uses litellm.acompletion(stream=True). OpenAI-compatible streaming. The agentic loop runs in the chat router.