Add comprehensive development intelligence system that tracks: - Development sessions with automatic start/stop - Full conversation history with semantic search - Tool usage and file operation analytics - Think time and engagement analysis - Git activity correlation - Learning pattern recognition - Productivity insights and metrics Features: - FastAPI backend with SQLite database - Modern web dashboard with interactive charts - Claude Code hook integration for automatic tracking - Comprehensive test suite with 100+ tests - Complete API documentation (OpenAPI/Swagger) - Privacy-first design with local data storage 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
156 lines
5.7 KiB
Python
156 lines
5.7 KiB
Python
"""
|
|
Waiting period model for tracking think time and engagement.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from sqlalchemy import String, Text, Integer, DateTime, ForeignKey
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
from sqlalchemy.sql import func
|
|
|
|
from .base import Base, TimestampMixin
|
|
|
|
|
|
class WaitingPeriod(Base, TimestampMixin):
|
|
"""
|
|
Represents a period when Claude is waiting for user input.
|
|
|
|
These periods provide insight into user thinking time, engagement patterns,
|
|
and workflow interruptions during development sessions.
|
|
"""
|
|
|
|
__tablename__ = "waiting_periods"
|
|
|
|
# Primary key
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
|
|
# Foreign key to session
|
|
session_id: Mapped[int] = mapped_column(ForeignKey("sessions.id"), nullable=False, index=True)
|
|
|
|
# Timing
|
|
start_time: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True),
|
|
nullable=False,
|
|
default=func.now(),
|
|
index=True
|
|
)
|
|
end_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
duration_seconds: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, index=True)
|
|
|
|
# Context
|
|
context_before: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
context_after: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
|
|
|
|
# Activity inference
|
|
likely_activity: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) # thinking, research, external_work, break
|
|
|
|
# Relationships
|
|
session: Mapped["Session"] = relationship("Session", back_populates="waiting_periods")
|
|
|
|
def __repr__(self) -> str:
|
|
duration_info = f", duration={self.duration_seconds}s" if self.duration_seconds else ""
|
|
return f"<WaitingPeriod(id={self.id}, session_id={self.session_id}{duration_info})>"
|
|
|
|
@property
|
|
def is_active(self) -> bool:
|
|
"""Check if this waiting period is still active (not ended)."""
|
|
return self.end_time is None
|
|
|
|
@property
|
|
def calculated_duration_seconds(self) -> Optional[int]:
|
|
"""Calculate waiting period duration in seconds."""
|
|
if self.end_time is None:
|
|
# Still waiting, calculate current duration
|
|
current_duration = datetime.utcnow() - self.start_time
|
|
return int(current_duration.total_seconds())
|
|
else:
|
|
# Finished waiting
|
|
if self.duration_seconds is not None:
|
|
return self.duration_seconds
|
|
else:
|
|
duration = self.end_time - self.start_time
|
|
return int(duration.total_seconds())
|
|
|
|
@property
|
|
def duration_minutes(self) -> Optional[float]:
|
|
"""Get duration in minutes."""
|
|
seconds = self.calculated_duration_seconds
|
|
return seconds / 60 if seconds is not None else None
|
|
|
|
def end_waiting(self, context_after: Optional[str] = None) -> None:
|
|
"""End the waiting period and calculate duration."""
|
|
if self.end_time is None:
|
|
self.end_time = func.now()
|
|
self.duration_seconds = self.calculated_duration_seconds
|
|
if context_after:
|
|
self.context_after = context_after
|
|
|
|
def classify_activity(self) -> str:
|
|
"""
|
|
Classify the likely activity based on duration and context.
|
|
|
|
Returns one of: 'thinking', 'research', 'external_work', 'break'
|
|
"""
|
|
if self.likely_activity:
|
|
return self.likely_activity
|
|
|
|
duration = self.calculated_duration_seconds
|
|
if duration is None:
|
|
return "unknown"
|
|
|
|
# Classification based on duration
|
|
if duration < 10:
|
|
return "thinking" # Quick pause
|
|
elif duration < 60:
|
|
return "thinking" # Short contemplation
|
|
elif duration < 300: # 5 minutes
|
|
return "research" # Looking something up
|
|
elif duration < 1800: # 30 minutes
|
|
return "external_work" # Working on something else
|
|
else:
|
|
return "break" # Extended break
|
|
|
|
@property
|
|
def engagement_score(self) -> float:
|
|
"""
|
|
Calculate an engagement score based on waiting time.
|
|
|
|
Returns a score from 0.0 (disengaged) to 1.0 (highly engaged).
|
|
"""
|
|
duration = self.calculated_duration_seconds
|
|
if duration is None:
|
|
return 0.5 # Default neutral score
|
|
|
|
# Short waits indicate high engagement
|
|
if duration <= 5:
|
|
return 1.0
|
|
elif duration <= 30:
|
|
return 0.9
|
|
elif duration <= 120: # 2 minutes
|
|
return 0.7
|
|
elif duration <= 300: # 5 minutes
|
|
return 0.5
|
|
elif duration <= 900: # 15 minutes
|
|
return 0.3
|
|
else:
|
|
return 0.1 # Long waits indicate low engagement
|
|
|
|
def is_quick_response(self) -> bool:
|
|
"""Check if user responded quickly (< 30 seconds)."""
|
|
duration = self.calculated_duration_seconds
|
|
return duration is not None and duration < 30
|
|
|
|
def is_thoughtful_pause(self) -> bool:
|
|
"""Check if this was a thoughtful pause (30s - 2 minutes)."""
|
|
duration = self.calculated_duration_seconds
|
|
return duration is not None and 30 <= duration < 120
|
|
|
|
def is_research_break(self) -> bool:
|
|
"""Check if this was likely a research break (2 - 15 minutes)."""
|
|
duration = self.calculated_duration_seconds
|
|
return duration is not None and 120 <= duration < 900
|
|
|
|
def is_extended_break(self) -> bool:
|
|
"""Check if this was an extended break (> 15 minutes)."""
|
|
duration = self.calculated_duration_seconds
|
|
return duration is not None and duration >= 900 |