fix: address performance and code quality issues in conversation history

Performance improvements:
- Fix N+1 query in get_conversations() using COUNT subquery instead of
  len(c.messages) which triggered lazy loading for each conversation
- Add SQLAlchemy engine caching to avoid creating new database connections
  on every request
- Add React.memo to ChatMessage component to prevent unnecessary re-renders
  during message streaming
- Move BOLD_REGEX to module scope to avoid recreating on each render

Code quality improvements:
- Remove 10+ console.log debug statements from AssistantChat.tsx and
  AssistantPanel.tsx that were left from development
- Add user feedback for delete errors in ConversationHistory - dialog now
  stays open and shows error message instead of silently failing
- Update ConfirmDialog to accept ReactNode for message prop to support
  rich error content

These changes address issues identified in the code review of PR #74
(conversation history feature).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-22 09:09:05 +02:00
parent 35ed14dfe3
commit 28e8bd6da8
6 changed files with 91 additions and 55 deletions

View File

@@ -11,13 +11,17 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, create_engine
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, create_engine, func
from sqlalchemy.orm import declarative_base, relationship, sessionmaker
logger = logging.getLogger(__name__)
Base = declarative_base()
# Engine cache to avoid creating new engines for each request
# Key: project directory path (as posix string), Value: SQLAlchemy engine
_engine_cache: dict[str, object] = {}
def _utc_now() -> datetime:
"""Return current UTC time. Replacement for deprecated datetime.utcnow()."""
@@ -56,13 +60,23 @@ def get_db_path(project_dir: Path) -> Path:
def get_engine(project_dir: Path):
"""Get or create a SQLAlchemy engine for a project's assistant database."""
db_path = get_db_path(project_dir)
# Use as_posix() for cross-platform compatibility with SQLite connection strings
db_url = f"sqlite:///{db_path.as_posix()}"
engine = create_engine(db_url, echo=False)
Base.metadata.create_all(engine)
return engine
"""Get or create a SQLAlchemy engine for a project's assistant database.
Uses a cache to avoid creating new engines for each request, which improves
performance by reusing database connections.
"""
cache_key = project_dir.as_posix()
if cache_key not in _engine_cache:
db_path = get_db_path(project_dir)
# Use as_posix() for cross-platform compatibility with SQLite connection strings
db_url = f"sqlite:///{db_path.as_posix()}"
engine = create_engine(db_url, echo=False)
Base.metadata.create_all(engine)
_engine_cache[cache_key] = engine
logger.debug(f"Created new database engine for {cache_key}")
return _engine_cache[cache_key]
def get_session(project_dir: Path):
@@ -94,23 +108,44 @@ def create_conversation(project_dir: Path, project_name: str, title: Optional[st
def get_conversations(project_dir: Path, project_name: str) -> list[dict]:
"""Get all conversations for a project with message counts."""
"""Get all conversations for a project with message counts.
Uses a subquery for message_count to avoid N+1 query problem.
"""
session = get_session(project_dir)
try:
# Subquery to count messages per conversation (avoids N+1 query)
message_count_subquery = (
session.query(
ConversationMessage.conversation_id,
func.count(ConversationMessage.id).label("message_count")
)
.group_by(ConversationMessage.conversation_id)
.subquery()
)
# Join conversation with message counts
conversations = (
session.query(Conversation)
session.query(
Conversation,
func.coalesce(message_count_subquery.c.message_count, 0).label("message_count")
)
.outerjoin(
message_count_subquery,
Conversation.id == message_count_subquery.c.conversation_id
)
.filter(Conversation.project_name == project_name)
.order_by(Conversation.updated_at.desc())
.all()
)
return [
{
"id": c.id,
"project_name": c.project_name,
"title": c.title,
"created_at": c.created_at.isoformat() if c.created_at else None,
"updated_at": c.updated_at.isoformat() if c.updated_at else None,
"message_count": len(c.messages),
"id": c.Conversation.id,
"project_name": c.Conversation.project_name,
"title": c.Conversation.title,
"created_at": c.Conversation.created_at.isoformat() if c.Conversation.created_at else None,
"updated_at": c.Conversation.updated_at.isoformat() if c.Conversation.updated_at else None,
"message_count": c.message_count,
}
for c in conversations
]