claude-code-tracker/app/models/waiting_period.py
Ryan Malloy 44ed9936b7 Initial commit: Claude Code Project Tracker
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>
2025-08-11 02:59:21 -06:00

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