feat: conversational branching, AI markers, TaskLane improvements, collapsible sidebar #120

Merged
chihlasm merged 58 commits from feat/conversational-branching into main 2026-03-27 13:16:44 +00:00
3 changed files with 87 additions and 2 deletions
Showing only changes of commit e96c94efbd - Show all commits

View File

@@ -20,6 +20,10 @@ if TYPE_CHECKING:
from app.models.account import Account
from app.models.tree import Tree
from app.models.psa_connection import PsaConnection
from app.models.session_branch import SessionBranch
from app.models.fork_point import ForkPoint
from app.models.session_handoff import SessionHandoff
from app.models.session_resolution_output import SessionResolutionOutput
class AISession(Base):
@@ -206,6 +210,28 @@ class AISession(Base):
comment="Full LLM message history for context continuity",
)
# ── Branching ──
is_branching: Mapped[bool] = mapped_column(
default=False,
comment="Whether conversational branching is active for this session",
)
active_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True), nullable=True,
comment="Currently viewed branch. No FK — soft pointer to avoid circular FK with session_branches",
)
handoff_count: Mapped[int] = mapped_column(
Integer, nullable=False, default=0,
comment="Number of times this session has been handed off",
)
total_active_seconds: Mapped[int] = mapped_column(
Integer, nullable=False, default=0,
comment="Cumulative active time in seconds",
)
total_parked_seconds: Mapped[int] = mapped_column(
Integer, nullable=False, default=0,
comment="Cumulative parked time in seconds",
)
# ── Relationships ──
user: Mapped["User"] = relationship("User", foreign_keys=[user_id])
account: Mapped["Account"] = relationship("Account")
@@ -218,3 +244,18 @@ class AISession(Base):
cascade="all, delete-orphan",
order_by="AISessionStep.step_order",
)
branches: Mapped[list["SessionBranch"]] = relationship(
"SessionBranch",
foreign_keys="SessionBranch.session_id",
cascade="all, delete-orphan",
order_by="SessionBranch.branch_order",
)
handoffs: Mapped[list["SessionHandoff"]] = relationship(
"SessionHandoff",
cascade="all, delete-orphan",
order_by="SessionHandoff.created_at",
)
resolution_outputs: Mapped[list["SessionResolutionOutput"]] = relationship(
"SessionResolutionOutput",
cascade="all, delete-orphan",
)

View File

@@ -16,6 +16,8 @@ from app.core.database import Base
if TYPE_CHECKING:
from app.models.ai_session import AISession
from app.models.script_template import ScriptGeneration
from app.models.session_branch import SessionBranch
from app.models.fork_point import ForkPoint
class AISessionStep(Base):
@@ -34,7 +36,7 @@ class AISessionStep(Base):
__table_args__ = (
CheckConstraint(
"step_type IN ('question', 'action', 'script_generation', 'verification', "
"'info_request', 'note', 'intake_analysis')",
"'info_request', 'note', 'intake_analysis', 'fork')",
name="ck_ai_session_steps_step_type",
),
)
@@ -119,6 +121,24 @@ class AISessionStep(Base):
Integer, nullable=False, default=0,
)
# ── Branching ──
branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("session_branches.id", ondelete="SET NULL"),
nullable=True,
index=True,
comment="NULL = pre-branching/root messages",
)
is_fork_point: Mapped[bool] = mapped_column(
default=False,
comment="Whether this step triggered a fork",
)
fork_point_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("fork_points.id", ondelete="SET NULL"),
nullable=True,
)
# ── Timestamps ──
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)

View File

@@ -3,7 +3,7 @@ import uuid
from datetime import datetime, timezone
from typing import Optional
from sqlalchemy import String, Integer, DateTime, ForeignKey
from sqlalchemy import String, Integer, DateTime, ForeignKey, Text
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.dialects.postgresql import UUID
@@ -30,3 +30,27 @@ class FileUpload(Base):
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
)
# ── AI description + branching context ──
ai_description: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
comment="AI-generated one-sentence description of the file",
)
extracted_content: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
comment="Extracted text from logs/configs",
)
content_summary: Mapped[Optional[str]] = mapped_column(
Text, nullable=True,
comment="AI summary for long text files (>2000 tokens)",
)
uploaded_on_branch_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("session_branches.id", ondelete="SET NULL"),
nullable=True,
)
uploaded_at_step_id: Mapped[Optional[uuid.UUID]] = mapped_column(
UUID(as_uuid=True),
ForeignKey("ai_session_steps.id", ondelete="SET NULL"),
nullable=True,
)