Skip to content

Conversation

@mattsolo1
Copy link
Contributor

No description provided.

mattsolo1 and others added 14 commits November 5, 2025 10:47
This is the first implementation of gnomAD Assistant, an AI-powered chat interface
that helps users navigate and explore the gnomAD database.

Key changes:
- Created separate CopilotKit server to avoid GraphQL conflicts
- Added CopilotKit React dependencies to browser
- Integrated CopilotSidebar component with variant navigation action
- Added Copilot button to navigation bar
- Connected to gmd MCP tool server for gnomAD-specific functionality
- Updated pnpm-lock.yaml with new dependencies

The assistant can currently:
- Navigate to variant pages based on variant IDs or rsIDs
- Use the gmd tool server for additional gnomAD queries

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
… gnomAD Assistant

- Create new GnomadCopilot component with non-floating layout
- Chat panel takes up 1/3 of screen by default (was 50%)
- Add resizable functionality with drag handle
- Remove Copilot menu item from navbar
- Increase chat input font size to 14px
- Update initial greeting message

The assistant now integrates better with the page layout and provides
a more intuitive user experience with adjustable panel width.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add page context tracking to DocumentTitle using useCopilotReadable
- Pass gene/variant context from GenePage and VariantPage to assistant
- Add useGnomadCopilotActions hook for displaying variant data in chat
- Add styles for chat components to properly display tables and data

This enables the assistant to understand what the user is viewing
and display rich data components directly in the chat interface.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Change action name from 'showVariantInfo' to 'get_variant_summary'
- Update parameter names to match MCP tool (variantId -> variant_id)
- Add optional dataset parameter with fallback to current dataset

This fixes the variant table display in the chat interface by ensuring
the frontend action properly invokes the backend MCP tool.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
Removed the get_variant_summary action and its associated useGnomadCopilotActions hook. This action is no longer needed in the CopilotKit integration.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add "Interpret this variant" suggestion on variant pages
- Set assistant chat window to be open by default
- Style suggestion chips with 14px font size to match chat UI
- Use conditional rendering to show suggestions only on relevant pages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
…on registration

The CopilotSidebar component was registering a duplicate navigateToVariantPage action
which caused "Action argument unparsable" errors due to receiving malformed JSON.
Since this component is not imported anywhere and GnomadCopilot provides the same
functionality with a better split-screen layout, it's safe to remove.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add Progress component for displaying MCP tool activity logs
- Implement useMCPStateRender hook for inline tool state visualization
- Integrate agent state rendering into GnomadCopilot
- Remove old ActionMessage/CustomRenderMessage code in favor of new system
- Add collapsible parameter display and status indicators

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
mattsolo1 and others added 13 commits November 18, 2025 10:38
Add region context to MCP tools by passing region data to DocumentTitle
component and handling it in the context detection logic. This enables
the assistant to access genomic region information (chrom, start, stop).

Add example pill questions for Juha Genetics API queries across variant,
gene, and region pages. Users can now easily query for credible sets,
colocalizations, QTL data, and gene-disease associations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create table column definitions for credible sets, colocalizations, and gene-disease data
- Implement JuhaToolsDisplay component with virtualized Grid tables for large datasets
- Add useJuhaActions hook defining 6 frontend MCP actions for Juha API tools
- Integrate Juha actions into GnomadCopilot
- Support proper sorting state management for each table type
- Fix React key uniqueness and variant link generation
- Add proper spacing to prevent visual overlap with LLM summaries

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ent issues

Major changes:
- Integrate CopilotKit server into GraphQL API as colocalized service
- Upgrade CopilotKit packages from v1.0.0 to v1.10.6 (fixes duplicate request bug)
- Add MCP client with singleton pattern to support multi-turn conversations
- Configure nginx to disable caching for /api/copilotkit endpoint
- Add deployment scripts and documentation for CopilotKit artifacts
- Update Docker configuration to include gmd binary (linux/amd64) and Mendelian TSV
- Add comprehensive logging for CopilotKit requests and MCP tool executions
- Remove copilotkit-server from pnpm workspace (code moved to graphql-api)

Technical details:
- CopilotKit now runs as part of the GraphQL API server (not standalone)
- MCP client reuses single instance per pod to maintain conversation state
- GraphQL upgraded from v15 to v16 for CopilotKit compatibility
- Nginx proxy cache explicitly bypassed for chat endpoint
- Build artifacts prepared via deploy/scripts/prepare-copilotkit-artifacts.sh

Fixes:
- Fix duplicate HTTP requests caused by frontend/backend version mismatch
- Fix cached responses in production by disabling nginx caching for chat
- Fix missing Juha tools by using gmd binary from juha-api worktree
- Fix architecture mismatch by building linux/amd64 gmd binary for Docker

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add fullscreen toggle button to chat panel with expand/compress icons
- Implement three display modes: closed, side panel, and fullscreen
- Hide main toggle button when in fullscreen mode
- Only show resize handle in side panel mode
- Disable HTML element scrolling to prevent page-level scrollbars

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ling

- Add settings modal with model selection and custom prompt configuration
- Replace bottom "Close Assistant" button with icon button in header
- Add close, settings, and fullscreen icon buttons to chat header
- Improve chat styling:
  - Fix input container width to prevent horizontal overflow
  - Add custom scrollbar styling (shows only when needed)
  - Increase suggestion chip font size to 14px for better readability
  - Add rounded corners to messages and input areas
  - Style suggestion chips with hover effects
- Apply gnomAD color scheme (#0d79d0) throughout chat UI
- Add proper overflow handling and box-sizing to prevent layout issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…y for gnomAD assistant

Implements comprehensive settings management for the CopilotKit assistant:

Backend changes:
- Created DynamicGeminiAdapter class to dynamically select AI model based on frontend parameters
- Added model logging to request/response middleware for better observability
- Support for Gemini 2.5/3 Flash and Pro models via forwardedParameters

Frontend changes:
- Moved model and prompt state management to App.tsx with localStorage persistence
- Added model badge displaying currently selected AI model with robot icon
- Added context badge showing current page context (gene/variant/region) and IDs
- Implemented saved prompts system with save/load/delete functionality
- Added settings modal with model selection and prompt management
- Fixed z-index layering to ensure proper modal display
- Connected settings UI to CopilotKit via forwardedParameters and useCopilotAdditionalInstructions

Storage keys:
- gnomad.copilot.model - Selected AI model
- gnomad.copilot.savedPrompts - Array of saved custom prompts
- gnomad.copilot.activePromptId - Currently active prompt ID

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…tion

- Add PostgreSQL-based chat history persistence for CopilotKit
  - Database schema with chat_threads and chat_messages tables
  - Database client with connection pooling and CRUD operations
  - Middleware hooks for message persistence in CopilotKit server
  - Thread management REST API endpoints (list, get, delete)
  - Health check endpoint for database connectivity

- Add chat history sidebar component
  - Displays thread list with titles and metadata
  - New chat button and thread selection
  - Integrated into fullscreen chat mode

- Fix environment variable configuration issues
  - Changed from CACHE_REDIS_URL/RATE_LIMITER_REDIS_URL to REDIS_HOST/REDIS_PORT
    (code was already using these legacy variables)
  - Added CopilotKit environment variables to start.sh
  - Fixed webpack proxy configuration for both GraphQL API and CopilotKit
  - Removed pathRewrite that was breaking API requests
  - Added IPv4 forcing (127.0.0.1) to avoid IPv6 connection issues
  - Fixed GNOMAD_API_URL for MCP server inside Docker container

- Improve Docker Compose configuration
  - Add PostgreSQL service for chat history
  - Fix Elasticsearch proxy cluster name (gnomad-v4)
  - Remove volume mounts to use compiled JavaScript (simpler, production-like)
  - Update to use correct environment variables

- Add CopilotKit utility scripts
  - init-db.sh: Initialize PostgreSQL schema
  - verify-copilot-build.sh: Prepare and verify CopilotKit artifacts
  - env.sh: Compartmentalized CopilotKit environment configuration

- Fix CopilotKit route ordering
  - Thread management endpoints must be registered before catch-all handler
  - Filter out messages without roles to avoid database constraint violations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…nstruction

This commit implements the ability to display historical chat messages when
users select a conversation from the chat history sidebar.

Key changes:
- Added useCopilotMessagesContext hook to manage CopilotKit's message state
- Implemented useEffect to fetch messages when threadId changes
- Added message class reconstruction to convert database JSON to proper
  CopilotKit message instances (TextMessage, ActionExecutionMessage, etc.)
- Added loading state UI to provide feedback during message fetching
- Added @copilotkit/runtime-client-gql dependency for message class imports

The critical fix was reconstructing message class instances from the database
objects, ensuring they have the required methods (like isResultMessage()) that
CopilotKit expects. This avoids serialization errors that occurred in previous
attempts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
When users click "New Chat", the application now creates a thread record
in the database immediately, making it appear in the chat history sidebar.

Backend changes (graphql-api/src/copilotkit/server.ts):
- Added POST /api/copilotkit/threads endpoint to create/ensure threads
- Added express.json() middleware to parse JSON request bodies
- Endpoint accepts threadId and optional model parameters

Frontend changes (browser/src/App.tsx):
- Updated handleNewChat to be async and call the POST endpoint
- Creates thread in database before setting as active
- Includes error handling to still set threadId if API call fails
- Passes current selectedModel to backend

Frontend changes (browser/src/ChatHistorySidebar.tsx):
- Added currentThreadId as dependency to useEffect
- Sidebar now refreshes immediately when new chat is created
- New threads appear in sidebar list right away

This improves UX by making new chats immediately visible in the history
sidebar, even before any messages are sent.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Users can now delete individual chat threads from the history sidebar.
Each thread item now displays a "Delete" button that:

- Shows confirmation dialog before deleting
- Calls DELETE /api/copilotkit/threads/:threadId endpoint
- Removes thread from database and local state
- Automatically starts new chat if deleting current thread
- Prevents event bubbling to avoid triggering thread selection
- Shows error alert if deletion fails

UI changes:
- Added DeleteButton styled component with red hover effect
- Updated ThreadItem layout to flex with space-between
- Wrapped thread info in ThreadContent container for proper layout
- Button has subtle opacity that increases on hover

This gives users control over their chat history and allows cleanup
of unwanted conversations.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add Auth0 authentication to the gnomAD Assistant, making chat threads
private and user-specific. Each authenticated user now has their own
isolated chat history.

Backend Changes:
- Add JWT validation middleware using express-oauth2-jwt-bearer and jose v4
- Update database layer to filter all operations by user_id
- Implement requestUserMap to pass userId context without modifying request body
- Protect all thread management API endpoints with JWT authentication
- Add thread ownership validation in CopilotKit runtime middleware
- Add comprehensive logging for debugging authentication flow

Frontend Changes:
- Integrate Auth0Provider for OAuth2/OIDC authentication
- Add token fetching with consent error handling
- Create Login and Logout components for authentication UI
- Update all API calls to include Authorization headers
- Display authenticated user info in settings modal
- Handle loading and error states during authentication

Configuration:
- Add Auth0 environment variables to Docker Compose configs
- Support toggling authentication via REACT_APP_AUTH0_ENABLE flag
- Configure webpack to inject Auth0 audience into browser bundle

Key Implementation Details:
- Used Map<threadId, userId> to avoid GraphQL validation errors
- Downgraded jose to v4 for CommonJS compatibility
- Added express.json() middleware before custom auth middleware
- Gracefully fallback to 'anonymous' userId when auth is disabled

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add development-specific Docker setup with hot reloading capabilities:
- Created api.dev.dockerfile with all dependencies including devDependencies
- Added @swc/core and @swc-node/register for ultra-fast TypeScript transpilation
- Configured Node.js built-in --watch mode (no nodemon needed)
- Mount graphql-api and dataset-metadata source directories for live code updates
- Optimized Docker build context with .dockerignore

Restart time is ~10s due to application initialization (GraphQL schema loading, service connections), with near-instant transpilation via SWC.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
mattsolo1 and others added 8 commits November 25, 2025 12:08
Implement comprehensive support for persisting and restoring tool execution
messages (ActionExecutionMessage and ResultMessage) in chat history:

Backend changes:
- Relax database schema to allow NULL for role and content fields
- Update database logic to handle all message types including tool messages
- Remove message filtering in server middleware to store complete history
- Add extensive logging for message types and field validation
- Fix init-db.sh script path bug

Frontend changes:
- Reconstruct tool messages from database with proper field handling
- Parse JSONB result and arguments fields when restoring messages
- Update render functions to handle stringified result data
- Add helper function for parsing results in Juha actions
- Fix VariantDisplay to handle both JSON data and text descriptions
- Filter empty threads from chat history sidebar

Known issues:
- Tool execution state restoration needs further investigation
- GraphQL validation errors when tool messages are in context

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Update hot reloading configuration to work with Docker volumes on macOS:
- Switch from Node's --watch to nodemon with legacyWatch mode
- Add nodemon.json configuration with polling for cross-platform compatibility
- Keep @swc-node/register for fast TypeScript transpilation

The legacyWatch option enables file polling, which is necessary for detecting
file changes in Docker volumes on macOS. Hot reloading now works successfully
with ~10s restart time (primarily application initialization).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…able

Refactor tool result handling to store large structured data payloads in a
separate `tool_results` table, preventing database bloat and payload size errors.

Backend changes (GraphQL API):
- Add `tool_results` table for storing large tool result payloads separately
- Add `tool_result_id` foreign key column to `chat_messages` table
- Implement result extraction logic to detect and store structured content
- Add API endpoint GET /api/copilotkit/tool_results/:resultId for fetching
- Parse JSON-stringified result fields before processing
- Derive parentMessageId from ResultMessage ID pattern (result-{actionId})
- Increase body-parser limit to 50MB to handle large payloads during save
- Add comprehensive logging for tool result processing

Frontend changes (Browser):
- Create useToolResult hook to centralize tool result fetching logic
- Refactor useGnomadVariantActions to use new VariantRenderComponent
- Refactor useJuhaActions to use new JuhaRenderComponent
- Fix React hooks error by converting render functions to proper components
- Simplify VariantDisplay to expect clean data format
- Simplify JuhaToolsDisplay to expect clean data format
- Update GnomadCopilot to not stringify ResultMessage result field

Build changes:
- Fix prepare-copilotkit-artifacts.sh path calculation for gmd source

Database schema:
- tool_results table with JSONB storage and user-based access control
- Unique constraint on (thread_id, message_id) to prevent duplicates
- Cascade delete on thread deletion, SET NULL on tool result deletion
- Indexes on user_id for efficient querying

This implementation follows a dual-channel pattern where the LLM receives
a text summary while the frontend receives rich structured data for
visualization, stored efficiently with on-demand fetching.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixed issue where authenticated users' chat messages were being saved
with userId 'anonymous' instead of their actual user ID, preventing
proper data retrieval and thread ownership.

Root cause: The original requestUserMap was keyed by threadId, but
threadId wasn't available in req.body during JWT verification (it's
provided later by the CopilotKit runtime). This timing mismatch left
the map unpopulated.

Solution: Implemented request-scoped userId tracking using a
currentRequestUserId variable that's set during JWT verification and
captured by onBeforeRequest when the runtime provides the threadId.

Changes:
- Replaced broken requestUserMap with currentRequestUserId + threadUserIdMap
- Added handler wrapper to set/clear currentRequestUserId per request
- Updated onBeforeRequest to associate userId with threadId
- Updated onAfterRequest to retrieve userId from threadUserIdMap
- Simplified Express middleware to store userId on req.copilotUserId

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…gation tracking

Add comprehensive context tracking system that captures genes, variants, and regions
viewed during conversations, enabling rich chat history with meaningful summaries.

Backend changes:
- Add contexts JSONB column to chat_threads table with GIN index
- Implement smart title generation based on browsing context
- Add POST /api/copilotkit/threads/:threadId/context endpoint
- Track context changes and prevent duplicates
- Generate titles like "Chat about BRCA1, PCSK9" from visited entities

Frontend changes:
- Automatically capture initial context when starting new chats
- Track navigation changes and send context updates to backend
- Display context notification banner when user navigates (5s auto-dismiss)
- Redesign chat history sidebar with context pills showing genes/variants/regions
- Move sidebar to left side in fullscreen mode (320px wide)
- Implement optimistic UI to show new chats immediately
- Add query parameter support (?chat=fullscreen/side/closed)
- Fix message count to only include TextMessage types
- Eliminate flickering with silent background refreshes

User experience improvements:
- New chats appear instantly in history with current context
- Thread titles automatically reflect entities discussed
- Visual context pills help identify conversations at a glance
- Smooth transitions without loading spinners during navigation
- URL-based chat mode for shareable links and browser navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace modal-based settings with an inline view that takes over the chat
panel for a more seamless user experience.

Changes:
- Create new ChatSettingsView component encapsulating all settings UI
- Move settings from modal to full-screen panel that replaces chat content
- Remove old modal implementation and duplicate styled components
- Hide chat control buttons (close/fullscreen) when settings view is active
- Update state management from isSettingsModalOpen to isSettingsViewOpen
- Refactor handleSavePrompt to accept promptName as parameter
- Move promptName state local to ChatSettingsView component
- Integrate logout button into settings view for better UX

Benefits:
- More integrated, less disruptive settings experience
- Better code organization with dedicated settings component
- Cleaner separation of concerns
- Works consistently in both side-panel and fullscreen modes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add extensible section-based navigation to settings view with sidebar
navigation and content switching.

Changes:
- Add left sidebar navigation with section buttons (180px wide)
- Create General section containing all existing settings
- Add Favorites section stub with "coming soon" placeholder
- Implement section state management and content switching
- Add active state highlighting with blue accent and left border
- Update layout to flex-based system with independent scrolling
- Add SectionTitle component for consistent section headings

Architecture:
- Easy to extend with new sections via SettingsSection type
- Clean separation between navigation and content
- Render functions for each section (renderGeneralSection, renderFavoritesSection)
- Switch statement for content routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add automatic title generation and updating for chat conversations using Google Generative AI. Titles are generated after the first few messages and updated periodically to reflect conversation content, with support for gene names and variant IDs.

- Add title generation service using Gemini 2.0 Flash model
- Implement async title generation triggered after message save
- Add database schema column to track title generation timing
- Configure thresholds for initial generation (4 messages) and updates (10 messages)
- Enhance sidebar UI to display titles on two lines for better readability
- Include gene/variant identification in title generation prompt

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines 429 to 538
app.use('/api/copilotkit', express.json({ limit: '50mb' }), cors(corsOptions), async (req, res, next) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(7);

// --- Authorization for CopilotKit runtime ---
if (isAuthEnabled) {
try {
const authHeader = req.headers.authorization;
logger.info({
message: 'CopilotKit auth check',
requestId,
hasAuthHeader: !!authHeader,
authHeaderPrefix: authHeader?.substring(0, 20),
method: req.method,
path: req.path
});

if (!authHeader || !authHeader.startsWith('Bearer ')) {
logger.warn({ message: 'Missing or invalid auth header', requestId });
return res.status(401).json({ error: 'Authorization header is missing or invalid.' });
}

const token = authHeader.substring(7);
const payload = await verifyJwt(token);
const userId = payload?.sub;

logger.info({
message: 'JWT verified successfully',
requestId,
userId,
hasPayload: !!payload,
payloadKeys: payload ? Object.keys(payload) : []
});

if (!userId) {
logger.error({ message: 'User ID not found in token payload', requestId, payload });
return res.status(401).json({ error: 'User ID not found in token.' });
}

// Store userId on the request object so it's available to the runtime handler
// We can't modify req.body because GraphQL validates it strictly
(req as any).copilotUserId = userId;

logger.info({
message: 'Stored userId on request object',
requestId,
userId,
});
} catch (error: any) {
logger.error({
message: 'JWT validation error',
requestId,
error: error.message,
stack: error.stack
});
return res.status(401).json({ error: 'Invalid authentication token.' });
}
}

// Log the request with more detail about the conversation
let threadId: string | undefined;
let messageCount: number | undefined;
let model: string | undefined;
try {
const body = req.body || {};
threadId = body.threadId;
messageCount = body.messages?.length || 0;
model = body.forwardedParameters?.model || 'gemini-2.5-flash';
} catch (e) {
// ignore parsing errors
}

logger.info({
message: 'CopilotKit request',
requestId,
threadId,
messageCount,
model,
method: req.method,
path: req.path,
userAgent: req.headers['user-agent'],
});

// Wrap the response to log completion
const originalSend = res.send;
res.send = function(data) {
const duration = Date.now() - startTime;
logger.info({
message: 'CopilotKit response',
requestId,
model,
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
});
return originalSend.call(this, data);
};

(async () => handler(req, res))().catch((error) => {
logger.error({
message: 'CopilotKit error',
error: error.message,
stack: error.stack,
method: req.method,
path: req.path,
});
next(error);
});
});

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 4 days ago

To address the missing rate limiting, add a rate-limiting middleware to the /api/copilotkit endpoint. The established best practice is to use the well-known express-rate-limit package for Express.

  1. Add import for express-rate-limit at the top of the file.
  2. Define a rate limiter instance before the handler middleware in mountCopilotKit. For example, limit each IP to 100 requests per 15 minutes (windowMs: 15 * 60 * 1000, max: 100).
  3. Apply the rate limiter in the middleware chain of app.use(...) for /api/copilotkit, before other expensive middleware and handlers.
  4. No logic outside the file needs modification.
    Specifically:
  • Edit graphql-api/src/copilotkit/server.ts.
  • Add the import.
  • Add the rate-limiter instance in mountCopilotKit.
  • Insert the limiter into the middleware stack at line 35 when mounting /api/copilotkit.

Suggested changeset 2
graphql-api/src/copilotkit/server.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/src/copilotkit/server.ts b/graphql-api/src/copilotkit/server.ts
--- a/graphql-api/src/copilotkit/server.ts
+++ b/graphql-api/src/copilotkit/server.ts
@@ -1,5 +1,6 @@
 import express, { Application } from 'express';
 import cors from 'cors';
+import rateLimit from 'express-rate-limit';
 import { copilotRuntimeNodeHttpEndpoint } from '@copilotkit/runtime';
 import { runtime, setCurrentRequestUserId } from './runtime';
 import { DynamicGeminiAdapter } from './adapter';
@@ -26,13 +27,26 @@
     credentials: true,
   };
 
+  // Set up rate limiter: max 100 requests per 15 minutes per IP
+  const copilotkitLimiter = rateLimit({
+    windowMs: 15 * 60 * 1000, // 15 minutes
+    max: 100, // limit each IP to 100 requests per windowMs
+    standardHeaders: true, // Return rate limit info in headers
+    legacyHeaders: false, // Disable the `X-RateLimit-*` headers
+  });
+
   // Mount all the modular API routes
   app.use('/api/copilotkit', apiRoutes);
 
   logger.info('CopilotKit thread management API mounted');
 
-  // Mount the main CopilotKit runtime handler
-  app.use('/api/copilotkit', express.json({ limit: '50mb' }), cors(corsOptions), async (req, res, next) => {
+  // Mount the main CopilotKit runtime handler with rate limiting
+  app.use(
+    '/api/copilotkit',
+    copilotkitLimiter,
+    express.json({ limit: '50mb' }),
+    cors(corsOptions),
+    async (req, res, next) => {
     const startTime = Date.now();
     const requestId = Math.random().toString(36).substring(7);
 
EOF
@@ -1,5 +1,6 @@
import express, { Application } from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { copilotRuntimeNodeHttpEndpoint } from '@copilotkit/runtime';
import { runtime, setCurrentRequestUserId } from './runtime';
import { DynamicGeminiAdapter } from './adapter';
@@ -26,13 +27,26 @@
credentials: true,
};

// Set up rate limiter: max 100 requests per 15 minutes per IP
const copilotkitLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});

// Mount all the modular API routes
app.use('/api/copilotkit', apiRoutes);

logger.info('CopilotKit thread management API mounted');

// Mount the main CopilotKit runtime handler
app.use('/api/copilotkit', express.json({ limit: '50mb' }), cors(corsOptions), async (req, res, next) => {
// Mount the main CopilotKit runtime handler with rate limiting
app.use(
'/api/copilotkit',
copilotkitLimiter,
express.json({ limit: '50mb' }),
cors(corsOptions),
async (req, res, next) => {
const startTime = Date.now();
const requestId = Math.random().toString(36).substring(7);

graphql-api/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/package.json b/graphql-api/package.json
--- a/graphql-api/package.json
+++ b/graphql-api/package.json
@@ -42,7 +42,8 @@
     "pg": "^8.11.3",
     "redis": "^3.1.1",
     "ts-migrate": "^0.1.35",
-    "typescript": "^5.0.4"
+    "typescript": "^5.0.4",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@swc-node/register": "^1.10.9",
EOF
@@ -42,7 +42,8 @@
"pg": "^8.11.3",
"redis": "^3.1.1",
"ts-migrate": "^0.1.35",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@swc-node/register": "^1.10.9",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
mattsolo1 and others added 12 commits December 2, 2025 09:35
…rsistence

This commit fixes two critical bugs in the CopilotKit chat implementation:

1. Transaction Isolation Bug (Backend):
   - Fixed foreign key constraint violation when saving tool results
   - Made saveToolResult() private (_saveToolResult) and added client parameter
   - Ensures tool_results and chat_messages inserts happen in same transaction
   - Prevents "violates foreign key constraint fk_tool_result" errors

2. GraphQL Type Validation Error (Frontend):
   - Fixed type mismatch when loading messages from database across page navigation
   - JSONB result fields were parsed as objects/arrays instead of strings
   - Added string serialization when reconstructing ResultMessage instances
   - Resolves "String cannot represent a non string value" GraphQL errors

3. Enhanced error logging in server.ts for better debugging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive backend infrastructure for user roles and authentication:

Backend Changes:
- Add user_role enum (user, viewer, admin) to database schema
- Add users table with role support and proper indexing
- Add chat_feedback table for user feedback collection
- Implement getUser, updateUserRole, getUsers, getFeedback database methods
- Add authorization middleware (addUserToRequest, isAdmin, isViewerOrAdmin)
- Fetch user profile from Auth0 /userinfo endpoint when not in token
- Add API endpoints:
  - GET /api/copilotkit/users/me - get current user profile
  - PUT /api/copilotkit/users/:userId/role - update user roles (admin only)
- Secure existing GET /users and GET /feedback endpoints with role checks
- Update init-db.sh script to handle new schema properly

Frontend Changes:
- Configure Auth0 to request 'openid profile email' scopes
- Scopes enable backend to fetch user info from Auth0 userinfo endpoint

Role-based access control:
- user (default): standard access, no admin pages
- viewer: can view users and feedback pages
- admin: full access including role management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Add comprehensive frontend views for user administration and feedback:

- Add UsersView component for user management
  - Display all users with email, name, role, and activity timestamps
  - Admin-only role management with dropdown selectors
  - Real-time role updates via API
  - Proper authorization checks and error handling

- Add ChatFeedbackView component for feedback monitoring
  - Display user feedback with ratings and comments
  - Show associated thread and user information
  - Pagination support for large feedback datasets
  - Viewer/admin access only

- Add CustomAssistantMessage component
  - Custom message rendering with thumbs up/down feedback
  - Thread-aware feedback submission
  - Integrated with feedback API

- Update ChatSettingsView
  - Add navigation for Users and Feedback sections
  - Support for section-based routing (?settings=users)
  - Conditional rendering based on active section
  - Props for external section control

- Update GnomadCopilot
  - Integrate feedback submission handlers
  - Wire up CustomAssistantMessage component
  - Pass threadId context for feedback tracking
  - URL state synchronization for settings navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Reorganize all gnomAD chat assistant files from scattered locations into
a single, well-structured directory for better maintainability.

New structure:
browser/src/assistant/
├── GnomadCopilot.tsx          # Main component
├── index.ts                    # Clean exports
├── hooks/                      # Assistant-specific hooks
│   ├── useMCPStateRender.tsx   # MCP state rendering
│   └── useToolResult.ts        # Tool result fetching
├── components/                 # UI components
│   ├── ChatHistorySidebar.tsx
│   ├── CustomAssistantMessage.tsx
│   └── settings/               # Settings views
│       ├── ChatSettingsView.tsx
│       ├── ChatFeedbackView.tsx
│       └── UsersView.tsx
└── gmd/                        # Tool components & hooks
    ├── components/             # Variant displays, table columns
    │   └── (7 files)
    └── hooks/                  # CopilotKit action hooks
        └── (2 files)

Changes:
- Move 18 assistant files from 4 different locations
- Update all import paths to reflect new structure
- Add index.ts for clean exports from './assistant'
- Remove empty chat/ and hooks/ directories

Benefits:
- All assistant code in one location
- Clear component hierarchy
- Easier to maintain and understand
- Separates assistant from core browser functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
…ation

This commit implements a dedicated admin panel separate from user settings,
refactors URL state management for better navigation support, and adds
role-based authorization for admin access.

Changes:
- Create new AdminView component with Feedback and Users sections
- Move ChatFeedbackView and UsersView to admin directory
- Remove admin sections from ChatSettingsView (Feedback, Users)
- Add useCurrentUser hook for fetching user profile and role
- Refactor GnomadCopilot URL state to use 'view' and 'section' parameters
- Add AdminButton visible only to admin/viewer roles
- Adjust button positioning to accommodate new admin button
- Implement authorization checks to prevent unauthorized admin access

The new navigation system allows for proper URL refresh support and clean
separation between chat, settings, and admin views.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the confusing "X to close" pattern in settings and admin views with
a persistent navigation bar that allows easy switching between Chat, Settings,
and Admin sections. This provides clearer navigation and better UX.

Changes:
- Create NavigationBar component with tab-based navigation
- Add chat icon for Chat tab in navigation
- Remove close buttons from AdminView and ChatSettingsView headers
- Remove Settings and Admin floating buttons (now in nav bar)
- Reposition control buttons for better spacing
- Move ModelBadge and ContextBadge from absolute to normal flow positioning
- Create BadgeContainer to hold badges at bottom of chat area
- Ensure Close/Fullscreen/Logout buttons visible on all views
- Adjust button positions: Close (20px), Fullscreen (60px), Logout (100px)

The navigation bar is now the primary way to switch between sections,
making it clear where users are and providing consistent navigation across
all views. Control buttons (close, fullscreen, logout) remain visible
regardless of active view.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
The feedback modal was not visible because the @gnomad/ui Modal component
uses react-aria-modal with getApplicationNode() that returns the root element,
causing the modal to render outside the chat window with a low z-index (1).

Created a custom ChatModal component that:
- Uses React portals to render directly into document.body
- Has a high z-index (100000) to appear above all chat UI elements
- Maintains the same styling as the original @gnomad/ui Modal
- Supports keyboard (Escape) and click-outside-to-close interactions
- Added proper padding to the feedback textarea

Also removed unused GlobalModalStyles and Modal import since they are no
longer needed with the custom portal-based implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Remove the separate feedback button and automatically open the feedback
modal when users click thumbs up or thumbs down. This simplifies the UI
and creates a more natural feedback flow by prompting for details
immediately after a rating.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Frontend improvements:
- Add suggestion click tracking in chat interface
- Add thread-level feedback button in chat history sidebar
- Add privacy toggle for admin viewing consent in settings
- Add general feedback form in settings
- Improve feedback view with better thread links and N/A display

New admin views:
- All Conversations: Browse and manage all user threads
- Usage & Costs: Track token usage and API costs
- User Interactions: Analyze suggestion clicks and user behavior

Backend enhancements:
- Track incremental token usage with detailed breakdown (system prompt,
  tool definitions, history, tool results, user messages)
- Store tool invocations with token counts and execution times
- Add analytics event tracking for user interactions
- Implement privacy preferences (allow_admin_viewing)
- Add admin endpoints for thread management and analytics
- Prevent duplicate system message storage

Database schema updates:
- Add token tracking columns to chat_messages table
- Create tool_invocations table
- Create analytics_events table
- Add privacy preference columns to users table
- Add database migrations structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fixes critical bug where tool result tokens were counted as ~867k instead of ~600.
CopilotKit serializes MCP tool results as JSON strings, causing our code to count
the entire serialized object (including large structuredContent arrays) instead of
just the text content sent to the LLM.

Changes:
- Parse JSON string results before extracting content
- Extract text only from MCP array format: [{type: "text", text: "..."}]
- Ignore structuredContent field completely (frontend-only data)
- Add extractTextContent() helper to centralize extraction logic
- Create debug-logger.ts with toggleable DEBUG_TOKEN_COUNTING flag
- Add tool invocation display in admin UI showing correct token counts

Token counting now correctly reports ~200-650 tokens per tool call instead of
800k+, reducing reported costs by ~1,350x and accurately tracking LLM context usage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Break down the monolithic server.ts (1223 lines) into focused modules:
- auth.ts: Authentication and authorization logic
- adapter.ts: Dynamic Gemini model adapter and token counting
- runtime.ts: CopilotRuntime configuration and MCP client setup
- routes/: Express routers organized by feature (users, threads, admin, misc)

This improves code organization, testability, and makes future changes
easier to implement and review. Server.ts is now a clean 168-line
orchestrator that assembles the modular components.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Comment on lines +35 to +56
router.post('/feedback', async (req, res) => {
try {
let userId = 'anonymous';
if (isAuthEnabled && req.headers.authorization?.startsWith('Bearer ')) {
try {
const token = req.headers.authorization.substring(7);
const payload = await verifyJwt(token);
userId = payload?.sub || 'anonymous';
if (userId !== 'anonymous') {
await chatDb.upsertUser({ userId, email: payload?.email as string, name: payload?.name as string });
}
} catch (error) {
logger.warn({ message: 'Failed to verify JWT for feedback, using anonymous', error });
}
}
await chatDb.saveFeedback({ ...req.body, userId });
res.status(201).json({ success: true });
} catch (error: any) {
logger.error({ message: 'Failed to save feedback', error: error.message });
res.status(500).json({ error: 'Failed to save feedback' });
}
});

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 4 days ago

The best way to fix this problem is to add a rate-limiting middleware to the /feedback route to prevent abuse. In Node.js/Express applications, the most common solution is the express-rate-limit package. The change should minimally add the import for express-rate-limit, define a suitable rate-limiter with reasonable defaults (e.g., N requests per window per IP), and apply it directly to the /feedback POST route. This should be done in graphql-api/src/copilotkit/routes/misc.ts, with the rate limiter instantiated near the top and added as a middleware argument to the handler for /feedback.


Suggested changeset 2
graphql-api/src/copilotkit/routes/misc.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/src/copilotkit/routes/misc.ts b/graphql-api/src/copilotkit/routes/misc.ts
--- a/graphql-api/src/copilotkit/routes/misc.ts
+++ b/graphql-api/src/copilotkit/routes/misc.ts
@@ -1,11 +1,10 @@
 import express from 'express';
+import rateLimit from 'express-rate-limit';
 import { chatDb } from '../database';
 import { checkJwt, addUserToRequest, isAuthEnabled, isViewerOrAdmin, verifyJwt } from '../auth';
 import logger from '../../logger';
 
 const router = express.Router();
-
-// Get a specific tool result
 router.get('/tool_results/:resultId', checkJwt, async (req, res) => {
   try {
     const userId = isAuthEnabled ? (req as any).auth.payload.sub : 'anonymous';
@@ -32,7 +26,7 @@
 });
 
 // Submit feedback
-router.post('/feedback', async (req, res) => {
+router.post('/feedback', feedbackLimiter, async (req, res) => {
   try {
     let userId = 'anonymous';
     if (isAuthEnabled && req.headers.authorization?.startsWith('Bearer ')) {
EOF
@@ -1,11 +1,10 @@
import express from 'express';
import rateLimit from 'express-rate-limit';
import { chatDb } from '../database';
import { checkJwt, addUserToRequest, isAuthEnabled, isViewerOrAdmin, verifyJwt } from '../auth';
import logger from '../../logger';

const router = express.Router();

// Get a specific tool result
router.get('/tool_results/:resultId', checkJwt, async (req, res) => {
try {
const userId = isAuthEnabled ? (req as any).auth.payload.sub : 'anonymous';
@@ -32,7 +26,7 @@
});

// Submit feedback
router.post('/feedback', async (req, res) => {
router.post('/feedback', feedbackLimiter, async (req, res) => {
try {
let userId = 'anonymous';
if (isAuthEnabled && req.headers.authorization?.startsWith('Bearer ')) {
graphql-api/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/package.json b/graphql-api/package.json
--- a/graphql-api/package.json
+++ b/graphql-api/package.json
@@ -42,7 +42,8 @@
     "pg": "^8.11.3",
     "redis": "^3.1.1",
     "ts-migrate": "^0.1.35",
-    "typescript": "^5.0.4"
+    "typescript": "^5.0.4",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@swc-node/register": "^1.10.9",
EOF
@@ -42,7 +42,8 @@
"pg": "^8.11.3",
"redis": "^3.1.1",
"ts-migrate": "^0.1.35",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@swc-node/register": "^1.10.9",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Comment on lines +72 to +96
router.post('/analytics/event', async (req, res) => {
try {
let userId: string | undefined;
if (isAuthEnabled) {
try {
const authHeader = req.headers.authorization;
if (authHeader) {
const token = authHeader.substring(7);
userId = (await verifyJwt(token))?.sub as string;
}
} catch (error) {
logger.warn({ message: 'Failed to verify JWT for analytics event', error });
}
}
const { threadId, eventType, payload, sessionId } = req.body;
if (!eventType) {
return res.status(400).json({ error: 'eventType is required' });
}
await chatDb.saveAnalyticsEvent({ userId, threadId, eventType, payload, sessionId });
res.status(201).json({ success: true });
} catch (error: any) {
logger.error({ message: 'Failed to save analytics event', error: error.message });
res.status(500).json({ error: 'Failed to save analytics event' });
}
});

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 4 days ago

To fix this issue, we should add a rate limiting middleware to the /analytics/event POST endpoint. The recommended approach is to use the widely adopted express-rate-limit npm package. Since the rest of the code uses CommonJS and/or ESModule imports and express, it is compatible. The fix involves:

  • Importing express-rate-limit at the top of the file.
  • Defining a suitable rate limiter instance (for example, 100 requests per 15 minutes per IP).
  • Applying this rate limiter as middleware specifically to the problematic endpoint (/analytics/event POST route), to minimize risk of blocking other routes.

File/region to change:

  • graphql-api/src/copilotkit/routes/misc.ts
    • Add import for express-rate-limit.
    • Define a analyticsEventLimiter before the route definition.
    • Apply the limiter to the /analytics/event POST route as middleware.

Suggested changeset 2
graphql-api/src/copilotkit/routes/misc.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/src/copilotkit/routes/misc.ts b/graphql-api/src/copilotkit/routes/misc.ts
--- a/graphql-api/src/copilotkit/routes/misc.ts
+++ b/graphql-api/src/copilotkit/routes/misc.ts
@@ -1,11 +1,10 @@
 import express from 'express';
+import rateLimit from 'express-rate-limit';
 import { chatDb } from '../database';
 import { checkJwt, addUserToRequest, isAuthEnabled, isViewerOrAdmin, verifyJwt } from '../auth';
 import logger from '../../logger';
 
 const router = express.Router();
-
-// Get a specific tool result
 router.get('/tool_results/:resultId', checkJwt, async (req, res) => {
   try {
     const userId = isAuthEnabled ? (req as any).auth.payload.sub : 'anonymous';
@@ -69,7 +63,7 @@
 });
 
 // Save an analytics event
-router.post('/analytics/event', async (req, res) => {
+router.post('/analytics/event', analyticsEventLimiter, async (req, res) => {
   try {
     let userId: string | undefined;
     if (isAuthEnabled) {
EOF
@@ -1,11 +1,10 @@
import express from 'express';
import rateLimit from 'express-rate-limit';
import { chatDb } from '../database';
import { checkJwt, addUserToRequest, isAuthEnabled, isViewerOrAdmin, verifyJwt } from '../auth';
import logger from '../../logger';

const router = express.Router();

// Get a specific tool result
router.get('/tool_results/:resultId', checkJwt, async (req, res) => {
try {
const userId = isAuthEnabled ? (req as any).auth.payload.sub : 'anonymous';
@@ -69,7 +63,7 @@
});

// Save an analytics event
router.post('/analytics/event', async (req, res) => {
router.post('/analytics/event', analyticsEventLimiter, async (req, res) => {
try {
let userId: string | undefined;
if (isAuthEnabled) {
graphql-api/package.json
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/graphql-api/package.json b/graphql-api/package.json
--- a/graphql-api/package.json
+++ b/graphql-api/package.json
@@ -42,7 +42,8 @@
     "pg": "^8.11.3",
     "redis": "^3.1.1",
     "ts-migrate": "^0.1.35",
-    "typescript": "^5.0.4"
+    "typescript": "^5.0.4",
+    "express-rate-limit": "^8.2.1"
   },
   "devDependencies": {
     "@swc-node/register": "^1.10.9",
EOF
@@ -42,7 +42,8 @@
"pg": "^8.11.3",
"redis": "^3.1.1",
"ts-migrate": "^0.1.35",
"typescript": "^5.0.4"
"typescript": "^5.0.4",
"express-rate-limit": "^8.2.1"
},
"devDependencies": {
"@swc-node/register": "^1.10.9",
This fix introduces these dependencies
Package Version Security advisories
express-rate-limit (npm) 8.2.1 None
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants