""" Activity model for tracking tool usage and file operations. """ from datetime import datetime from typing import Optional, Dict, Any from sqlalchemy import String, Text, Integer, DateTime, JSON, ForeignKey, Boolean from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.sql import func from .base import Base, TimestampMixin class Activity(Base, TimestampMixin): """ Represents a single tool usage or file operation during development. Activities are generated by Claude Code tool usage and provide detailed insight into the development workflow. """ __tablename__ = "activities" # Primary key id: Mapped[int] = mapped_column(primary_key=True) # Foreign keys session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True) conversation_id: Mapped[Optional[int]] = mapped_column( ForeignKey("conversations.id"), nullable=True, index=True ) # Activity timing timestamp: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=func.now(), index=True ) # Activity details tool_name: Mapped[str] = mapped_column(String(50), nullable=False, index=True) action: Mapped[str] = mapped_column(String(100), nullable=False) file_path: Mapped[Optional[str]] = mapped_column(Text, nullable=True, index=True) # Metadata and results metadata: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True) success: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) # Code change metrics (for Edit/Write operations) lines_added: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) lines_removed: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # Relationships session: Mapped["Session"] = relationship("Session", back_populates="activities") conversation: Mapped[Optional["Conversation"]] = relationship( "Conversation", foreign_keys=[conversation_id] ) def __repr__(self) -> str: file_info = f", file='{self.file_path}'" if self.file_path else "" return f"" @property def is_file_operation(self) -> bool: """Check if this activity involves file operations.""" return self.tool_name in {"Edit", "Write", "Read"} @property def is_code_execution(self) -> bool: """Check if this activity involves code/command execution.""" return self.tool_name in {"Bash", "Task"} @property def is_search_operation(self) -> bool: """Check if this activity involves searching.""" return self.tool_name in {"Grep", "Glob"} @property def total_lines_changed(self) -> int: """Get total lines changed (added + removed).""" added = self.lines_added or 0 removed = self.lines_removed or 0 return added + removed @property def net_lines_changed(self) -> int: """Get net lines changed (added - removed).""" added = self.lines_added or 0 removed = self.lines_removed or 0 return added - removed def get_file_extension(self) -> Optional[str]: """Extract file extension from file path.""" if not self.file_path: return None if "." in self.file_path: return self.file_path.split(".")[-1].lower() return None def get_programming_language(self) -> Optional[str]: """Infer programming language from file extension.""" ext = self.get_file_extension() if not ext: return None language_map = { "py": "python", "js": "javascript", "ts": "typescript", "jsx": "javascript", "tsx": "typescript", "go": "go", "rs": "rust", "java": "java", "cpp": "cpp", "c": "c", "h": "c", "hpp": "cpp", "rb": "ruby", "php": "php", "html": "html", "css": "css", "scss": "scss", "sql": "sql", "md": "markdown", "yml": "yaml", "yaml": "yaml", "json": "json", "xml": "xml", "sh": "shell", "bash": "shell", } return language_map.get(ext) def is_successful(self) -> bool: """Check if the activity completed successfully.""" return self.success and not self.error_message def get_command_executed(self) -> Optional[str]: """Get the command that was executed (for Bash activities).""" if self.tool_name == "Bash" and self.metadata: return self.metadata.get("command") return None def get_search_pattern(self) -> Optional[str]: """Get the search pattern (for Grep activities).""" if self.tool_name == "Grep" and self.metadata: return self.metadata.get("pattern") return None def get_task_type(self) -> Optional[str]: """Get the task type (for Task activities).""" if self.tool_name == "Task" and self.metadata: return self.metadata.get("task_type") return None