{% load static %} {% comment %}data-theme="dark" ACTIVATES scitex-ui's dark token set — it is not decoration. theme.css defines --text-primary / --bg-page / --border-default / … twice, once under [data-theme="light"] and once under [data-theme="dark"], and DEFAULTS TO LIGHT when the attribute is absent. Without this every base component on the page silently resolves light values onto a dark surface, which is exactly how scitex-ui 0.12.0's context menu rendered near-invisible on 2026-07-28. Dark is also the standing default (the operator's eyes are sensitive), so this is deliberate, not a preference to be "cleaned up". {% endcomment %} {% comment %} "DM", not "Chat". Operator, 2026-07-29 (TG): 「あと、"chat" となってますが、"DM" で揃えると良いと思います。」 The browser tab was the last place still saying Chat — the switcher, the heading and this title now all read DM. THE ROUTE IS STILL /chat AND STAYS THAT WAY. Renaming a published URL is a MIGRATION, not a rename: every bookmark the operator has and every agent reference to /chat would break for a label change. See _page_switcher.html, which carries the same decision next to the href it does NOT rename. chat_title.js PREFIXES this string at runtime — "(3) DM — SciTeX Cards v…" — so the base title lives here, in one place, and that module spells neither the version nor the word DM. {% endcomment %} DM — SciTeX Cards v{{ scitex_cards_version }} {% comment %} No favicon link here: scitex-ui's shell supplies the shared SciTeX brand mark as its default (>=0.7.1). One source, not two copies that drift. Do NOT re-add a local — if the mark is wrong, fix it in scitex-ui, which owns the asset. NB: this MUST be a {% templatetag openblock %} comment {% templatetag closeblock %} block, not Django's single-hash syntax — that one is single-line only, and a multi-line hash block leaks its body into the rendered page. {% endcomment %} {% comment %} Operator↔agent DM view — minimal slice (card fleet-agent-direct-message-board-pane-20260707). Mobile-first: the agent list is a slide-in drawer on narrow screens, a fixed left column on wide ones. All behavior lives in static/scitex_cards/chat/ — chat_diff.js (pure render planning) then chat.js (DOM + network); load order matters, chat.js reads window.ChatDiff. Line-limit discipline: template <1024 lines, js <512; this file is layout + CSS. {% endcomment %} {% comment %} Right-click menu comes from scitex-ui — we do NOT ship our own. The operator's standing rule: anything shareable lives in base and is consumed from there (「共通化できるものは scitex-ui に入れて消費してください」). We were about to write a private one; scitex-ui pointed out base has shipped it since 0.11.0. Two files, in this order, and NO shell adoption is required (scitex-ui confirmed from the source): theme.css is the TOKEN layer only — context-menu.css consumes --bg-secondary / --border-default / --text-primary / --text-secondary. It has hardcoded dark fallbacks, so it renders without theme.css, but then it is permanently dark regardless of the viewer. Linking the tokens is what makes it follow the theme. MINIMUM scitex-ui 0.11.1, not 0.11.0: 0.11.1 adds `font-family: inherit` on __item. Items are

Direct messages

SciTeX Cards v{{ scitex_cards_version }} {% include "scitex_cards/_page_switcher.html" with active="chat" %}
{% comment %} The agent list is now a filter row plus a scrolling list, not one flat nav. `renderAgents` in chat.js clears its container on every 5s poll, so the input CANNOT live inside the rebuilt element — it would be wiped four seconds after the operator typed in it. `#agent-list` exists to be the wiped part. The input is written HERE rather than created by chat_filter.js so the page can be asserted to actually SERVE it (same reason the context-menu items are in the template). `#agents` keeps its id because the mobile drawer is mounted on it and every `#agents .agent` rule still matches through the new wrapper. {% endcomment %}
{% comment %} Markup is scitex-ui's contract, verbatim — do not "tidy" it: * items MUST be
{% comment %} DELIBERATELY ABSENT, and both absences are decisions rather than oversights: Translate — the operator said 「translate はいらない」. Pin — nothing in this codebase pins a DM. There is no pinned-message store, no endpoint and no place to render one, so the item could only ever look like it worked. A dead control is worse than a missing one. Delete — the DM store is APPEND-ONLY by operator ruling (「一度書いたものは消えない」); `_threads` has no delete and `_reactions._save_events_unlocked` REFUSES a write that shrinks the log. Deleting a message honestly means an append-only tombstone event log plus an endpoint plus a fold plus a render — a feature, not a menu item — so it is carded rather than faked. A red "Delete" that leaves the message on screen after the next 5s poll is the worst of the three options. {% endcomment %} {% comment %} Reaction bar and forward picker — both second-level panels the context menu opens in place of itself, so only one thing is ever on screen. The emoji are NOT written here. chat_actions.js owns the palette and chat_menu.js builds the buttons from it, which is what keeps the set the picker offers identical to the set the Python side documents — a hardcoded row here would be a second copy, free to drift. Empty on purpose; both are populated at runtime (the forward list from the agent list chat.js already polls, so the picker cannot disagree with the drawer about who exists). {% endcomment %}
Select an agent to open the thread.
{% comment %} Selection mode's own bar. Hidden until "Select" is chosen, and it is the ONLY reason Select is in the menu at all: a selection that cannot be acted on is a mode with no exit and no purpose. Copy and Forward are both real destinations built on paths that already work — the clipboard, and the same POST a single Forward uses. {% endcomment %}
0 selected
No agent selected.
{% comment %} ORDER IS THE LAYOUT. Telegram's composer reads left to right: attach, field, send — with emoji tucked into the right edge of the field rather than standing beside it as a fourth control. This markup IS that order, so the row needs no `order:` juggling to produce it, and a screen reader meets the controls in the same sequence as the eye. The bar chat_compose.js inserts above this row (the offer to send an over-long draft as a .txt) is built in JS, not written here: it exists only while a draft is over the threshold, and markup that is hidden 99% of the time is markup nobody maintains. {% endcomment %}
{% comment %} Declarative mount: emoji_picker.js finds this by the attribute and builds its own toggle + panel here, so the composer's behaviour file (chat.js) gains not one line for this feature. The value is the id of the field the emoji land in. --right is the component's own modifier for a toggle at the END of its row: the default panel hangs from the toggle's LEFT edge, which from here would open a ~20rem popover rightwards off the side of a phone screen. {% endcomment %}
{% comment %} Load order matters and is one-directional: each file reads the ones before it off `window` and never the reverse. combobox.js scitex-ui's fuzzy matcher (STX.Combobox.fuzzyMatch) chat_diff.js pure render planning (no deps) chat_title.js the unread count in the browser tab (no deps) chat_actions.js pure message-action decisions (no deps) chat_select.js multi-select mode + its bulk actions (reads ChatActions) chat_menu.js the message context menu + reaction chips (reads both) chat_attach.js attachment uploads AND their rendering (no deps) chat_drawer.js the mobile drawer: state, inert-when-closed, scrim (no deps) chat_filter.js fuzzy filter over the agent list (reads STX.Combobox) chat_longtext.js the long-body clamp + .txt download (no deps) chat_compose.js the composer: auto-grow + the long-draft offer (reads ChatLongText for the threshold, and is handed ChatAttach's single-file upload by chat.js) chat.js the page orchestrator (mounts the modules above) chat_menu mounts chat_select rather than chat.js doing it: the menu already holds every seam selection needs (the message list, the peer, the forward picker), so routing them through chat.js would only add a pass-through — and chat.js is at its 512-line budget. combobox.js is scitex-ui's, and it is here for its MATCHER, not its widget: the bundle attaches `window.STX.Combobox` and exports `fuzzyMatch` as a static, which is what makes "matches" mean the same thing on this page as on the board. It is the same file board_v3.html loads. chat_filter.js degrades to a substring match if the symbol is missing, so an old scitex-ui costs behaviour, not the page. `defer` scripts execute in document order, so this list IS the order. {% endcomment %}