{% from 'components/session_nav.html' import render_session_nav %} {% from 'components/archive_search_link.html' import archive_search_link as render_archive_search_link %}

{{ title }}

{% if page_info %} {% endif %} {% include 'components/timeline.html' %}

🔍 Search & Filter

{% include 'components/search_inline.html' %}
{# The toolbar above searches this page; this goes wider. Only rendered when the caller knows where `search.html` is (see HtmlRenderer.generate). #} {% if archive_search_link %} {{ render_archive_search_link(archive_search_link) }} {% endif %} {% if combined_transcript_link %} {% elif sessions and sessions|length > 1 %} {{ render_session_nav(sessions, "toc") }} {% endif %} {% macro render_message(message) %} {%- set message_title = message.rendered_title %} {%- set html_content = message.rendered_html %} {%- set formatted_timestamp = message.rendered_timestamp %} {% if message.fork_only %} {# Fork-only landmark (#233 follow-up): this slot is a fork point whose own message body was filtered out at the current detail level, but it is kept (not ghosted) so the fork point stays visible and anchorable — like the branches it connects. Render ONLY the fork-point box (no message card), at the slot's original position. The anchor id lives on the box so branch back-links (`from ⟂ Fork point`) and the nav fork item resolve here. #}
{% for child in message.children %}{{ render_message(child) }}{% endfor %}{% if message.junction_forward_links %}
⑂ Fork point{% if message.fork_point_preview %} • {{ message.fork_point_preview }}{% endif %}
{% for branch_sid, branch_idx, branch_preview in message.junction_forward_links %} ↳ Branch{% if branch_preview %} • {{ branch_preview }}{% endif %} {% endfor %}
{% endif %}
{% elif is_session_header(message) %} {% if not message.is_branch_header %}
{% endif %} {# Structural wrapper: the message card and its nested .children render as SIBLINGS here, so the card's border/background/margins frame only the card itself, not the whole subtree (issue #174 nested-DOM follow-up). #}
{% if message.is_branch_header %}↳ {% else %}Session: {% endif %}{{ html_content|safe }}{% if message.display_model and not message.is_branch_header %} {{ message.display_model }}{% endif %}
{% if message.has_children %}
{% if message.immediate_children_count == message.total_descendants_count %} {# Same count = only one level, show single full-width button #}
{{ message.get_immediate_children_label() }}
{% else %} {# Multiple levels, show both buttons #}
{{ message.get_immediate_children_label() }}
⏷⏷ {{ message.get_total_descendants_label() }} total
{% endif %}
{% endif %}
{% if message.children or message.junction_forward_links %}
{% for child in message.children %}{{ render_message(child) }}{% endfor %}{% if message.junction_forward_links %} {# Fork point — rendered INSIDE .children so it folds together with the subtree (folding hides .children). For a childless junction it is the sole occupant. The opening tag is glued to endfor so non-junction nodes emit no extra whitespace. #}
⑂ Fork point{% if message.fork_point_preview %} • {{ message.fork_point_preview }}{% endif %}
{% for branch_sid, branch_idx, branch_preview in message.junction_forward_links %} ↳ Branch{% if branch_preview %} • {{ branch_preview }}{% endif %} {% endfor %}
{% endif %}
{% endif %}
{% elif message.should_render %} {%- set msg_css_class = css_class_from_message(message) %} {% set markdown = message.content.has_markdown if message.content else false %}
{# data-uuid: the message's transcript UUID — stable across re-renders, unlike the positional msg-d-N slot ids (see work/archive-search-server.md, "The anchor problem"). Archive search / annotations can deep-link on it. NOT unique per card: sibling cards split from one entry (text + tool_use) share the entry's UUID, so anything needing per-card identity within a page must use the card's `id` (msg-…) or element identity instead. #}
{% set msg_emoji = get_message_emoji(message) -%} {% if message_title %}{% if message_title == 'Memory' %}💭 {% elif msg_emoji and (message.type != 'tool_use' or not starts_with_emoji(message_title)) %}{{ msg_emoji }} {% endif %}{{ message_title | safe }}{% endif %}
{{ formatted_timestamp }}
{% if message.token_usage %} {{ message.token_usage }} {% endif %}
{% if message.meta %}
{{ message.meta.uuid[:12] }}{% if message.meta.parent_uuid %} → {{ message.meta.parent_uuid[:12] }}{% endif %}
{% endif %}
{{ html_content | safe }}
{% if message.has_children %}
{% if message.immediate_children_count == message.total_descendants_count %} {# Same count = only one level, show single full-width button #}
{{ message.get_immediate_children_label() }}
{% else %} {# Multiple levels, show both buttons #}
{{ message.get_immediate_children_label() }}
⏷⏷ {{ message.get_total_descendants_label() }} total
{% endif %}
{% endif %}
{% if message.children or message.junction_forward_links %}
{% for child in message.children %}{{ render_message(child) }}{% endfor %}{% if message.junction_forward_links %} {# Fork point — rendered INSIDE .children so it folds together with the subtree (folding hides .children). For a childless junction it is the sole occupant. The opening tag is glued to endfor so non-junction nodes emit no extra whitespace. #}
⑂ Fork point{% if message.fork_point_preview %} • {{ message.fork_point_preview }}{% endif %}
{% for branch_sid, branch_idx, branch_preview in message.junction_forward_links %} ↳ Branch{% if branch_preview %} • {{ branch_preview }}{% endif %} {% endfor %}
{% endif %}
{% endif %}
{% endif %} {% endmacro %} {# The message tree lives in one addressable container so a client can replace it wholesale — that is what `serve --watch` needs to update a page in place without a navigation, and it keeps scroll position because everything above the viewport is untouched. Deliberately unstyled: `body` is already the 1200px centred column, so a plain block wrapper is layout-neutral (verified by comparing full-page screenshots and the first message's box before and after adding it). #}
{% for root in roots %}{{ render_message(root) }}{% endfor %}
{% if resume_command %} {% endif %} {# Follow toggle for `serve --watch`. Rendered unconditionally but hidden by CSS until live_update.js finds it can actually poll, so the markup stays identical between a served page and the same file opened from disk. #} 🔝 {% set is_transcript = True %} {% include 'components/search.html' %}