{# Chat — the cold open, and a bound session (043 C6). Derived from docs/design/040/web/chat-cold-open.html and chat-read-only.html. ONE template, two states, decided by `is_new`: is_new the hero, four starters, the composer. No model picker, no tool checkboxes, no max-steps slider — they move to Agent › Settings in phase 4, and the session request carries only the task. bound the thread, live, drawn by static/app/transcript.js. It reads the session from document.body's data attributes, which is why no inline script is needed here. The read-only frame comes from shell.html and is stated ONCE there; all this page does is swap the composer for the sentence that says why it is off. The thread itself is a READ and renders on every posture. #} {% extends "shell.html" %} {% block body_attrs %} data-session-id="{{ session_id or 'new' }}" data-is-new="{{ 'true' if is_new else 'false' }}" data-is-owner="{{ 'true' if is_owner else 'false' }}" data-is-authenticated="{{ 'true' if is_authenticated else 'false' }}" data-read-only="{{ 'true' if read_only else 'false' }}"{% endblock %} {% block body %} {%- if is_new %}

{{ hero|safe }}

{{ t('chat.open.sub') }}

{%- if not read_only %} {# A starter is a SHORTCUT INTO THE COMPOSER, and read-only has no composer — chat-open.js binds them only after it finds the form, so rendering them here shipped four buttons that did nothing. #}
{%- for starter in starters %} {%- endfor %}
{%- endif %}
{%- elif ownership_unknown %} {# ⚠️ The ownership PROBE failed — that is not "this session does not exist", which is what a 404 would have said. No thread either: the modules would bind to a session the check never cleared. #}

{{ t('chat.title') }}

{{ t('chat.ownership_unknown') }}

{%- else %}

{{ t('chat.title') }}

{# The Work pane sits BESIDE the thread — a disclosure, not a nav slot: what this chat MADE (Files, three tiers) beside what it DID (Timeline), both read-only. The two-column layout is the one inline declaration the schema (docs/design/040/web/chat-work-pane.html) carries; the CSP already has style-src 'unsafe-inline'. On a narrow screen the aside wraps below the thread. workpane.js fills it, or leaves the honest empty / unreadable state — it never blanks. #}
{# The words the running transcript says, crossing from the copy layer into transcript.js the same way chats.js reads #chats-copy — no 043 template carries an inline script. ⚠️ The names carry an UNDERSCORE, not a hyphen: `dataset` camel-cases a hyphen away, and the JS keys them as written. #} {# The words the Work pane says, crossing from the copy layer into workpane.js the same way. ⚠️ Underscore, not hyphen: `dataset` camel-cases a hyphen away, and workpane.js keys them as written. The narrator's own fallback (chat.act.*) rides on #transcript-copy above; workpane.js merges both so the Timeline says the same name-free line the thread does. #} {%- endif %}
{%- if ownership_unknown %}

{{ t('chat.ownership_unknown_note') }}

{%- elif read_only %} {# The off state (schema: chat-read-only.html) — a dashed, dimmed composer that cannot be typed into. `style-src 'unsafe-inline'` is already the CSP. #}

{{ t('chat.read_only.composer') }}

{{ t('chat.read_only.note') }}

{# E14: an owner verb typed here is refused too. Before this the box said only that the console could not TALK, so `/pause` looked like a thing you could still do from a read-only seat. #}

{{ t('chat.read_only.verbs') }}

{%- else %}
{%- if not is_new %} {# A24: the file picker the README has always described. The `accept=` is rendered from the ONE upload allowlist (core.surfaces.inbound_attachments), never hand-written here; the per-file cap and the MIME sniff are the endpoint's. It exists only on a BOUND session, because the upload route is session-scoped — the cold open has no folder to put a file in yet. #} {# ⚠️ `upload_accept` is a Jinja global installed by webview/template_globals.py::register, which `pages_new`'s OWN environment does not currently call — so the attribute is emitted only when the list is really there. An `accept=""` would tell the browser "any file", which is a claim about the allowlist, and a wrong one; an ABSENT accept claims nothing and the endpoint still refuses. #} {%- endif %}

{{ t('chat.composer.hint') }}

{%- if not is_new %} {%- endif %} {%- endif %}

{{ t('chat.sources')|safe }}

{# Loaded on EVERY posture. The thread is a READ, and gating the script on read-only left /c/{id} permanently blank on the one posture prod runs — the composer is already absent above, and `data-read-only` is what keeps the imported session modules in look-only mode. #} {%- if not ownership_unknown %} {%- endif %} {% endblock %}