"""Messages: one conversation per person, read backwards on demand. The page is the ordinary chat shell with two differences: it opens on the most recent turns rather than on all of them, and above them sits a sentinel that fetches the page before whenever it is scrolled into view. That sentinel is the mirror of `GET /api/chats/{id}/tail`, which polls forwards, and it keeps the same four properties for the same reasons — most of all answering **204 to a cursor it cannot place** rather than falling back to "the oldest hundred", which would prepend a block the page already holds. """ from __future__ import annotations import logging from fastapi import APIRouter, Request, Response, status from lembas.api.deps import Db, RequiredUser from lembas.api.pages import _chat_context, sidebar_context from lembas.db.models import Message, Schedule from lembas.services import messages as messages_service from lembas.services import schedules as schedules_service from lembas.services.schedule import clock from lembas.services.schedule import rule as rule_service from lembas.web.templating import render log = logging.getLogger(__name__) router = APIRouter(tags=["messages"]) @router.get("/messages") async def messages_page(request: Request, db: Db, user: RequiredUser): conversation = messages_service.for_user(db, user) live = messages_service.live_messages(db, conversation) # The schedules that post in here, listed beside the conversation because # this is where somebody would look for them -- a schedule whose output # arrives in this thread and whose controls are two pages away is one nobody # will find when they want to stop it. posting = list( db.scalars( schedules_service.visible(user) .where(Schedule.target == "messages") .order_by(Schedule.created_at.desc()) ) ) zone = clock.zone_for(user) return render( request, "messages/index.html", { "chat": conversation, "messages": live, "compacted": [], "inherited_prompt": "", "inherited_from": "", "more_before": bool(live) and messages_service.has_more_before( db, conversation, live[0] ), "oldest_id": live[0].id if live else "", "schedules": [ { "row": row, "summary": rule_service.describe(row.rule_json or {}, zone=zone), } for row in posting ], **_chat_context(db, user, conversation), **sidebar_context(db, user), }, ) @router.get("/api/messages/history") async def messages_history( request: Request, db: Db, user: RequiredUser, before: str = "" ) -> Response: """The page of turns immediately before `before`, oldest first. 204 rather than a fallback whenever the cursor cannot be placed: an absent one, one from another chat, one belonging to a message that has gone. The alternative -- answering with the oldest page -- would prepend a block the reader is already looking at, and a duplicated transcript is something only a reload can reconcile. """ conversation = messages_service.for_user(db, user) cursor = db.get(Message, before) if before else None if cursor is None or cursor.chat_id != conversation.id: return Response(status_code=status.HTTP_204_NO_CONTENT) page = messages_service.older_than(db, conversation, cursor) if not page: return Response(status_code=status.HTTP_204_NO_CONTENT) from lembas.web.templating import templates return templates.TemplateResponse( request, "messages/_history.html", { "messages": page, "more_before": messages_service.has_more_before(db, conversation, page[0]), "oldest_id": page[0].id, # `render()` injects `user` and friends; `TemplateResponse` does # not, and `chat/_message.html` dereferences both `user` and `chat` # -- the same reason the SSE path passes them by hand. Missing # either is a 500 on scroll and nothing at all on the page that # rendered fine. "user": user, "chat": conversation, **_chat_context(db, user, conversation), }, )