Add database migration for branch chat tables
Intent: Create the schema to store per-artifact, per-user chat sessions and their messages, scoped so that a new tutorial artifact version starts a fresh discussion thread.
Affected files: crates/pika-news/migrations/0023_branch_chat.sql, crates/pika-news/src/storage.rs
Evidence
@@ -0,0 +1,26 @@
+ALTER TABLE branch_artifact_versions
+ ADD COLUMN claude_session_id TEXT;
+
+CREATE TABLE branch_chat_sessions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ branch_artifact_id INTEGER NOT NULL REFERENCES branch_artifact_versions(id) ON DELETE CASCADE,
+ npub TEXT NOT NULL,
+ claude_session_id TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(branch_artifact_id, npub)
+);
+
+CREATE TABLE branch_chat_messages (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id INTEGER NOT NULL REFERENCES branch_chat_sessions(id) ON DELETE CASCADE,
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant')),
+ content TEXT NOT NULL,
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
@@ -2657,6 +2798,11 @@ fn migrations() -> Vec<Migration> {
+ Migration {
+ version: 23,
+ name: "0023_branch_chat",
+ sql: include_str!("../migrations/0023_branch_chat.sql"),
+ },
Migration 0023 performs three schema changes:
- Adds
claude_session_id to branch_artifact_versions so the Claude session used during tutorial generation can be stored alongside the artifact and later resumed for discussion.
- Creates
branch_chat_sessions with a UNIQUE(branch_artifact_id, npub) constraint. This ensures each user gets exactly one chat session per artifact version. When a new artifact version becomes current, the old session is naturally orphaned and the user gets a fresh thread.
- Creates
branch_chat_messages linked to sessions via foreign key with ON DELETE CASCADE, ordered by created_at ASC for chronological retrieval.
The migration is registered in the migrations() vector at version 23, ensuring it runs automatically on startup.
Extend BranchDetailRecord and query to include claude_session_id
Intent: Thread the Claude session ID from the database through to the detail view so the front end can determine whether branch discussion is available.
Affected files: crates/pika-news/src/branch_store.rs
Evidence
@@ -81,6 +81,7 @@ pub struct BranchDetailRecord {
+ pub claude_session_id: Option<String>,
@@ -556,6 +557,7 @@ impl Store {
+ ba_current.claude_session_id,
@@ -598,8 +600,9 @@ impl Store {
+ claude_session_id: row.get(13)?,
- error_message: row.get(13)?,
- ci_status: row.get(14)?,
+ error_message: row.get(14)?,
+ ci_status: row.get(15)?,
The BranchDetailRecord struct gains an claude_session_id: Option<String> field. The SQL query in get_branch_detail is updated to select ba_current.claude_session_id as the 13th column, bumping subsequent column indices by one. This value is Some(...) only when the worker successfully generated a tutorial with a Claude session, signaling that discussion is available.
Update mark_branch_generation_ready to persist claude_session_id
Intent: Allow the worker to save the Claude session ID when marking a branch artifact as ready, so it can be used as the base session for user discussions.
Affected files: crates/pika-news/src/branch_store.rs
Evidence
@@ -1895,6 +1898,7 @@ impl Store {
+ claude_session_id: Option<&str>,
@@ -1948,16 +1952,18 @@ impl Store {
+ claude_session_id = ?5,
- is_current = ?5,
+ is_current = ?6,
- WHERE id = ?6",
+ WHERE id = ?7",
params![
tutorial_json,
html,
generated_head_sha,
unified_diff,
+ claude_session_id,
mark_branch_generation_ready receives a new claude_session_id: Option<&str> parameter. The UPDATE statement now sets claude_session_id = ?5 on the artifact row, shifting the is_current and WHERE id bind indices accordingly. When the worker passes None, the column remains NULL and the front end will show discussion as unavailable. All existing test call-sites are updated to pass None to maintain compatibility.
Wire claude_session_id through the worker pipeline
Intent: Pass the Claude session ID obtained during tutorial generation into the storage layer so it is persisted with the artifact.
Affected files: crates/pika-news/src/worker.rs
Evidence
@@ -171,7 +171,14 @@ fn process_job(
+ .mark_branch_generation_ready(
+ job.artifact_id,
+ &tutorial_json,
+ &html,
+ &job.head_sha,
+ &diff,
+ gen_output.session_id.as_deref(),
+ )
In process_job, the call to mark_branch_generation_ready is expanded to pass gen_output.session_id.as_deref(). The gen_output struct already carries an optional session_id from the Claude API interaction during tutorial generation. By forwarding it here, every successfully generated tutorial artifact records the session that produced it, enabling conversation resumption in the discussion sidebar.
Add storage-layer CRUD methods for branch review chat
Intent: Provide the data access layer for creating sessions, retrieving history, updating session IDs, and appending messages for branch discussions.
Affected files: crates/pika-news/src/storage.rs
Evidence
@@ -717,6 +717,97 @@ impl Store {
+ pub fn get_branch_review_artifact_session_id(
+ pub fn get_or_create_branch_review_chat_session(
@@ -728,6 +819,22 @@ impl Store {
+ pub fn get_branch_review_chat_claude_session_id(
@@ -743,6 +850,23 @@ impl Store {
+ pub fn update_branch_review_chat_claude_session_id(
@@ -759,6 +883,23 @@ impl Store {
+ pub fn append_branch_review_chat_message(
Five new methods are added to Store:
| Method | Purpose |
get_branch_review_artifact_session_id | Fetches the claude_session_id from the current ready artifact for a branch. Returns None if no ready artifact exists or the session ID is NULL. |
get_or_create_branch_review_chat_session | Looks up the current artifact, then either returns an existing chat session for the (artifact_id, npub) pair or inserts a new one. Returns the session ID and all existing messages. |
get_branch_review_chat_claude_session_id | Retrieves the Claude API session ID for a given chat session row. |
update_branch_review_chat_claude_session_id | Updates the Claude session ID after each API turn so subsequent messages resume the correct conversation. |
append_branch_review_chat_message | Inserts a user or assistant message into branch_chat_messages. |
The UNIQUE(branch_artifact_id, npub) constraint in the schema guarantees that get_or_create_branch_review_chat_session is idempotent per artifact version.
Add branch chat HTTP endpoints
Intent: Expose GET and POST handlers at /news/branch/:branch_id/chat for loading discussion history and sending new messages via the Claude API.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -1395,6 +1397,10 @@ pub async fn serve(
+ .route(
+ "/news/branch/:branch_id/chat",
+ get(branch_chat_history_handler).post(branch_chat_send_handler),
+ )
@@ -3895,6 +3903,71 @@
+async fn branch_chat_history_handler(
@@ -4048,6 +4121,151 @@
+async fn branch_chat_send_handler(
Two new Axum handlers are registered at /news/branch/:branch_id/chat:
branch_chat_history_handler (GET)
- Authenticates the request via
require_chat_auth.
- Calls
get_branch_review_artifact_session_id — returns 404 if no ready artifact with a session exists.
- Calls
get_or_create_branch_review_chat_session to either create or retrieve the session.
- Returns
{"messages": [...]} as JSON.
branch_chat_send_handler (POST)
- Same auth and session resolution flow.
- Appends the user's message to the database.
- Calls
model::chat_with_session with the current Claude session ID.
- Updates the Claude session ID (it may rotate) and appends the assistant response.
- Returns
{"role": "assistant", "content": "..."} as JSON.
Both handlers use spawn_blocking for all database and Claude API calls since they are synchronous operations.
Extend the detail template with a branch_chat_ready flag
Intent: Pass the chat-readiness boolean into the template so the front end can conditionally render the discussion panel in active or disabled state.
Affected files: crates/pika-news/src/web.rs
Evidence
@@ -306,6 +306,7 @@ struct DetailTemplate {
+ merge_commit_sha: Option<String>,
@@ -315,6 +316,7 @@ struct DetailTemplate {
+ branch_chat_ready: bool,
@@ -2210,6 +2217,7 @@
+ branch_chat_ready: record.claude_session_id.is_some(),
The DetailTemplate struct gains branch_chat_ready: bool and merge_commit_sha: Option<String>. In render_detail_template_with_notices, branch_chat_ready is set to true when record.claude_session_id.is_some(), indicating a ready artifact with a stored Claude session. The template uses this flag to toggle between the disabled placeholder and the active discussion panel.
Build the front-end discussion sidebar panel
Intent: Render the full discussion UI in the branch detail page with auth-gated loading, message display, compose input, and send functionality.
Affected files: crates/pika-news/templates/detail.html
Evidence
@@ -181,13 +181,108 @@
+ .discussion-panel {
+ .discussion-head {
+ .discussion-messages {
+ .discussion-msg {
+ .discussion-compose {
@@ -318,6 +418,37 @@
+ <section class="panel discussion-panel">
+ <div class="discussion-head">
+ <div>
+ <p class="branch-kicker">Branch Review</p>
+ <h2>Discussion</h2>
+ </div>
@@ -508,6 +801,17 @@
+ if (discussionSend) {
+ discussionSend.addEventListener('click', sendBranchDiscussion);
+ }
+ if (discussionInput) {
+ discussionInput.addEventListener('keydown', (event) => {
@@ -677,8 +981,10 @@
+ window.addEventListener('pika-auth', loadBranchDiscussion);
+ loadBranchDiscussion();
The template changes fall into three areas:
CSS (~90 lines)
New styles for .discussion-panel, .discussion-messages, .discussion-msg.user/.assistant, .discussion-compose, .discussion-disabled, and .discussion-empty provide a chat-style layout with scrollable message area, right-aligned user bubbles, left-aligned assistant bubbles, and a compose bar.
HTML structure
A new <section class="panel discussion-panel"> is inserted at the top of .tutorial-column, above the existing tutorial/error panels. It contains:
- A header with "Branch Review / Discussion" and a status badge (
artifact ready or artifact pending).
- A scope explanation paragraph noting that discussions reset per artifact version.
- A
#discussion-disabled block shown when the user is not authenticated or the artifact isn't ready.
- A
#discussion-main block (initially hidden) with the message container and compose input.
JavaScript (~150 lines)
Key functions:
loadBranchDiscussion() — checks branchChatReady and auth token, fetches GET /news/branch/:id/chat, handles 401/403/404, and renders messages or empty state.
sendBranchDiscussion() — posts user input to POST /news/branch/:id/chat, optimistically renders the user message, shows "Thinking..." status, and appends the assistant response.
addDiscussionMessage(role, content) — creates a DOM element with the appropriate role class and auto-scrolls.
- Event listeners on the send button (click) and input field (Enter key) trigger
sendBranchDiscussion.
loadBranchDiscussion is called both on page load and on the pika-auth event so the panel activates immediately after sign-in.
Add tests for artifact-scoped branch chat sessions
Intent: Verify that chat sessions are correctly scoped to the current artifact version, ensuring new tutorial generations produce fresh discussion threads.
Affected files: crates/pika-news/src/storage.rs, crates/pika-news/src/web.rs
Evidence
@@ -4089,6 +4236,49 @@
+ #[test]
+ fn branch_chat_sessions_are_scoped_to_the_current_artifact_version() {
@@ -8013,6 +8236,60 @@
+ #[test]
+ fn branch_detail_renders_branch_discussion_sidebar_when_artifact_ready() {
Two new tests validate the feature end-to-end:
branch_chat_sessions_are_scoped_to_the_current_artifact_version (storage.rs)
- Creates a branch, marks its first artifact ready, and creates a chat session with a message.
- Upserts the branch with a new head SHA, creating a second artifact version, and marks it ready.
- Creates a new chat session for the same
npub — asserts the session ID differs and messages are empty, confirming artifact-version scoping.
branch_detail_renders_branch_discussion_sidebar_when_artifact_ready (web.rs)
- Creates a branch and marks its artifact ready with a
claude_session_id.
- Fetches the detail record and renders the template.
- Asserts the rendered HTML contains "Branch Review", "Discussion", "artifact ready", and the compose placeholder text, confirming the discussion panel renders when chat is available.