/**
 * Whitespace in FAST templates
 * ────────────────────────────
 * The formatter adds newlines/indentation to static template string parts,
 * which FAST renders as real text nodes. With `white-space: pre-wrap` these
 * are visible as blank lines. To avoid this:
 *
 * - Apply `white-space: pre-wrap` only on leaf elements whose *content* needs
 *   it, never on containers.
 * - Inside pre-wrap elements keep content on the same line as the opening tag:
 *   GOOD `<div>${expr}</div>` / BAD `<div>\n  ${expr}\n</div>`
 * - Extract shared or deeply-nested templates as named module-level consts to
 *   keep indentation shallow.
 * - Chain adjacent `when()` calls as `${when(a,tA)}${when(b,tB)}` — separate
 *   lines let the formatter inject whitespace between them.
 */
import { isChatToolCallUnknown } from '@genesislcap/foundation-ai';
import { classNames, html, ref, repeat, when } from '@genesislcap/web-core';
import { SettingsModalTemplate } from '../components/settings-modal';
import { compactionDividerLabel } from '../utils/history-transform';
import { LOADING_STYLE_ANIMATIONS } from './main.types';
function unknownToolPayload(tc) {
    if (!isChatToolCallUnknown(tc))
        return '';
    // A stale tool was real earlier in this agent's lifetime but isn't available
    // in the current state — distinct from a hallucinated name that never existed.
    const lines = [
        tc.stale ? `${tc.name} — no longer available at this step` : `${tc.name} — tool does not exist`,
    ];
    if (tc.availableTools.length > 0) {
        lines.push('Available tools:');
        tc.availableTools.forEach((t) => lines.push(`  • ${t}`));
    }
    return lines.join('\n');
}
const HALO_SPEED_DEFAULT = 1.5;
const HALO_SPEED_ORCHESTRATING = 0.4;
const HALO_BORDER_SIZE_DEFAULT = 3;
// Avatar markup is owned by the component (`assistantIconSafe` / `userIconSafe`),
// which holds the sanitized SVG string for the default or any consumer override
// and is injected via `:innerHTML`. A raster `image-src` / `user-image-src`
// takes precedence over the inline icon. The exported `*IconSvg` string
// constants in `./main` let apps opt back into a specific built-in mark.
const assistantAvatarTemplate = html `
  ${(m, c) => {
    const parent = c.parent;
    return parent.imageSrc
        ? html `
          <img src="${() => parent.imageSrc}" alt="Assistant" class="avatar-img" />
        `
        : html `
          <span class="avatar-icon" :innerHTML="${() => parent.assistantIconSafe}"></span>
        `;
}}
`;
const userAvatarTemplate = html `
  ${(m, c) => {
    const parent = c.parent;
    return parent.userImageSrc
        ? html `
          <img src="${() => parent.userImageSrc}" alt="You" class="avatar-img" />
        `
        : html `
          <span class="avatar-icon" :innerHTML="${() => parent.userIconSafe}"></span>
        `;
}}
`;
/**
 * @beta
 */
const messageType = (m) => {
    switch (true) {
        case m.role === 'system-event':
            return 'agent-switch';
        case m.role === 'user':
        // A synthetic-user message is a display-only echo of an interaction
        // outcome; render it identically to a real user message.
        case m.role === 'synthetic-user':
            return 'user';
        // Reasoning (chain-of-thought summary) and narration (interstitial prose the model emits
        // alongside a tool call) both render as the "Thinking" bubble — visually set apart from a real
        // final agent reply, even though they stay separate categories (own log entry + own toggle:
        // `showThinkingSteps` vs `showNarration`). `thinking` is the legacy flag for pre-split sessions.
        case m.category === 'reasoning':
        case m.category === 'narration':
        case !!m.thinking:
            return 'ai-thinking';
        case !!m.toolCalls:
            return 'ai-function';
        case !!m.interaction:
            return 'interaction';
        default:
            return 'ai';
    }
};
const senderLabel = {
    user: 'You',
    'ai-thinking': 'Thinking',
    'ai-function': 'Tool Call',
    interaction: 'Assistant',
    ai: 'Assistant',
};
/**
 * Sender label for a message. Reasoning and narration share the "Thinking" bubble, so the label is
 * the only thing that tells them apart: narration reads "Working" (the action it narrates alongside
 * its tool call), reasoning stays "Thinking". Everything else uses the {@link messageType} map.
 */
const senderLabelFor = (m) => m.category === 'narration' ? 'Working' : senderLabel[messageType(m)];
/**
 * Resolves how the host frames an interaction widget, defaulting to `'label'`
 * (the historical "Assistant" label, no bubble). Callers guard on
 * `m.interaction` first; non-interaction messages never consult this.
 */
const interactionPresentation = (m) => { var _a, _b; return (_b = (_a = m.interaction) === null || _a === void 0 ? void 0 : _a.presentation) !== null && _b !== void 0 ? _b : 'label'; };
/**
 * Space-separated `part` names for a message bubble, so a host can style bubbles
 * from its own stylesheet via `::part()`. Every bubble gets `message` plus its
 * type (`message-user`, `message-ai`, `message-ai-thinking`, `message-ai-function`,
 * `message-interaction`). Interaction bubbles additionally carry a
 * presentation-specific part (`message-interaction-label` / `-bare` / `-bubble`)
 * so a host can target, say, only the bubble-framed widgets.
 */
const messageParts = (m) => {
    const type = messageType(m);
    return type === 'interaction'
        ? `message message-interaction message-interaction-${interactionPresentation(m)}`
        : `message message-${type}`;
};
// ─── Sub-agent trace fragments ────────────────────────────────────────────────
const subAgentAssistantTemplate = html `
  <div class="sub-agent-message sub-agent-assistant">${(m) => m.content}</div>
`;
const subAgentToolCallTemplate = html `
  <div class="sub-agent-message sub-agent-tool-call">
    ${repeat((m) => m.toolCalls, html `
        <span class="sub-agent-tool-name">${(tc) => tc.name}</span>
      `)}
  </div>
`;
const subAgentToolResultTemplate = html `
  <div class="sub-agent-message sub-agent-tool-result">${(m) => m.toolResult.content}</div>
`;
/** Renders one row of a sub-agent trace (assistant text, tool call, or tool result). */
const subAgentMessageRowTemplate = html `
  ${when((m) => { var _a; return m.role === 'assistant' && !((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length) && !!m.content; }, subAgentAssistantTemplate)}${when((m) => { var _a; return !!((_a = m.toolCalls) === null || _a === void 0 ? void 0 : _a.length); }, subAgentToolCallTemplate)}${when((m) => { var _a; return m.role === 'tool' && !!((_a = m.toolResult) === null || _a === void 0 ? void 0 : _a.content); }, subAgentToolResultTemplate)}
`;
/**
 * Label for a trace block: the distinct agents that appear in it.
 *
 * One tool call can invoke several sub-agents — a scheduler dispatching a dependency
 * graph, or a retry — and their conversations arrive concatenated in one trace. Naming
 * only `[0]` would then label a block "gen_A trace" while `gen_B`'s turns sit inside
 * it. Two agents read as "gen_A, gen_B"; beyond that it degrades to a count rather
 * than growing an unbounded summary line.
 */
const subAgentTraceLabel = (trace) => {
    const names = [...new Set(trace.map((m) => m.agentName).filter(Boolean))];
    if (names.length === 0)
        return 'Sub-agent trace';
    if (names.length <= 2)
        return `${names.join(', ')} trace`;
    // Owns the whole string rather than returning a fragment the template suffixes, so the noun
    // agrees with the count — a shared ` trace` suffix rendered "3 sub-agents trace".
    return `${names.length} sub-agent traces`;
};
/** Collapsed <details> trace shown inside a tool-call card once the sub-agent finishes. */
const subAgentTraceTemplate = html `
  <details class="sub-agent-trace">
    <summary class="sub-agent-trace-summary">
      ${(tc) => subAgentTraceLabel(tc.subAgentTrace)}
    </summary>
    ${repeat((tc) => tc.subAgentTrace.filter((m) => m.role !== 'user'), subAgentMessageRowTemplate)}
  </details>
`;
/** Live streaming trace panel shown while a sub-agent is actively running. */
const liveSubAgentTraceTemplate = html `
  <div class="live-sub-agent-trace" part="live-sub-agent-trace">
    <span class="live-sub-agent-name">${(x) => { var _a; return (_a = x.liveSubAgentName) !== null && _a !== void 0 ? _a : 'Sub-agent'; }}</span>
    ${repeat((x) => x.liveSubAgentTrace.filter((m) => m.role !== 'user'), subAgentMessageRowTemplate)}
  </div>
`;
// The interchangeable loading indicators. These MUST be stable module-level
// instances: the binding that selects between them reads `enabledAnimations`,
// which is backed by the redux store proxy and re-evaluates on every change to
// the aiAssistant slice (i.e. every new message, including hidden tool-call and
// thinking-step messages). Returning a fresh `html` instance from that binding
// would make FAST tear down and rebuild the indicator DOM each time, restarting
// the CSS animations and rAF loops. Stable references let FAST reuse the
// existing view so the animation runs uninterrupted.
const thinkingDotsTemplate = html `
  <div class="thinking-dots">
    <div class="dot dot-1"></div>
    <div class="dot dot-2"></div>
    <div class="dot dot-3"></div>
    <div class="dot dot-4"></div>
  </div>
`;
const thinkingWavesTemplate = html `
  <div class="thinking-waves" part="thinking-waves">
    <ai-waves-indicator></ai-waves-indicator>
    <span class="thinking-caption">Thinking...</span>
  </div>
`;
const thinkingFlowingWavesTemplate = html `
  <div class="thinking-flowing-waves" part="thinking-flowing-waves">
    <ai-flowing-waves-indicator></ai-flowing-waves-indicator>
    <span class="thinking-caption">Thinking...</span>
  </div>
`;
const thinkingPlasmaTemplate = html `
  <div class="thinking-plasma" part="thinking-plasma">
    <ai-plasma-orb-indicator></ai-plasma-orb-indicator>
    <span class="thinking-caption">Thinking...</span>
  </div>
`;
/**
 * Picks the loading indicator for the currently enabled style, falling back to
 * the dots (also the default when no animations config is supplied). Returns a
 * stable template instance per style — see the note above.
 */
const selectThinkingTemplate = (x) => {
    const enabled = x.enabledAnimations;
    if (enabled.includes('waves'))
        return thinkingWavesTemplate;
    if (enabled.includes('flowingWaves'))
        return thinkingFlowingWavesTemplate;
    if (enabled.includes('plasma'))
        return thinkingPlasmaTemplate;
    return thinkingDotsTemplate;
};
/** Shown in place of the transcript while a manual compaction runs (GENC-1351 §5.7). */
const compactingTemplate = html `
  <div class="compacting-indicator" part="compacting-indicator">
    ${thinkingDotsTemplate}
    <span class="compacting-caption">Compacting…</span>
  </div>
`;
/**
 * Shown in place of the transcript while a page-reload restore is in flight
 * (GENC-1351 §6) — a slow, server-backed `provider.load()`. Reuses the compaction
 * indicator's visual (same centered dotted loader) with its own `part` for host
 * styling. Invisible for synchronous providers, which resolve before paint.
 */
const restoringTemplate = html `
  <div class="compacting-indicator" part="restoring-indicator">
    ${thinkingDotsTemplate}
    <span class="compacting-caption">Restoring…</span>
  </div>
`;
/**
 * Class list for the banner below, joined rather than interpolated so an
 * inapplicable modifier contributes nothing. Two interpolations directly in the
 * attribute emitted `class="blocked-banner  "` in the common (unblocked) case —
 * harmless to the browser, but it shows up in every DOM snapshot and every
 * innerHTML assertion a host writes against this element.
 *
 * The two modifiers name what they actually gate, which is why neither is
 * `is-blocked`: `is-visible` means the banner has something to say, and that
 * includes PARTIAL exhaustion — one vendor walled, composer still live, `blocked`
 * false. `is-partial` then softens the treatment for exactly that case. Naming
 * the first after `blocked` read as a contradiction beside the second, and made
 * the styles say `.blocked-banner.is-blocked` to mean "visible".
 *
 * Exported for the unit test that pins the attribute; not part of the element
 * API.
 *
 * @internal
 */
export const blockedBannerClasses = (x) => [
    'blocked-banner',
    x.bannerVisible ? 'is-visible' : '',
    x.bannerVisible && !x.blocked ? 'is-partial' : '',
]
    .filter(Boolean)
    .join(' ');
/**
 * Id of the blocked banner, referenced by the composer controls'
 * `aria-describedby`. Shadow-DOM-scoped, so a fixed string cannot collide with
 * the host page — and IDREF resolution is same-root, which is exactly where both
 * ends of this reference live.
 *
 * @internal
 */
export const BLOCKED_BANNER_ID = 'blocked-banner';
/**
 * `aria-describedby` for the composer's textarea, send button and attach button:
 * the banner's id whenever the banner has something to say, otherwise `null` (so
 * the attribute is omitted rather than emitted empty).
 *
 * Keyed on `bannerVisible`, NOT on `blocked`, and that is the point. The
 * PARTIAL state — some vendor walled, composer still live — is the state this
 * feature exists to create, and it was the one state with no accessible
 * explanation at all: `aria-disabled` and `aria-label` bind only on `blocked`, and
 * a live composer keeps the host's own placeholder, so a screen-reader user
 * arriving at the textarea heard "Type a message" with no hint that the next turn
 * might be refused. The banner's `role="status"` announces the text when it
 * CHANGES; this is what makes the same explanation reachable afterwards, on
 * demand, from the control it is about.
 *
 * Applied in the fully blocked state too, where it is additive: the `aria-label`
 * there states the reason as the control's name, and this restates it as its
 * description for the send/attach buttons, which carry neither.
 *
 * Exported for the unit test that pins it; not part of the element API.
 *
 * @internal
 */
export const composerDescribedBy = (x) => x.bannerVisible ? BLOCKED_BANNER_ID : null;
/**
 * Persistent banner shown above the composer while a backend condition blocks
 * the assistant — today an exhausted AI budget (GENC-1464).
 *
 * Deliberately NOT a transcript replacement like `compactingTemplate` /
 * `restoringTemplate`: those cover a transient rewrite where the history is
 * mid-flight and unsafe to show, whereas a block is indefinite. Hiding the
 * conversation behind it would strand the user's work behind a wall they
 * cannot clear, so the banner sits alongside a fully readable transcript.
 *
 * `role="status"` (not `alert`) — it is a standing condition the user can also
 * see in the disabled composer, not an interruption.
 *
 * **Rendered unconditionally, empty when unblocked.** This is an accessibility
 * requirement, not a stylistic choice: `role="status"` announces MUTATIONS to a
 * region the assistive technology was already observing. A region INSERTED into
 * the DOM already carrying its text is widely missed across NVDA, JAWS and
 * VoiceOver — and it is the only announcement a screen-reader user gets, since
 * the textarea simultaneously flips to `disabled`, which drops it out of the tab
 * order and means the swapped "AI usage limit reached" placeholder is never
 * voiced (`maybeAutoFocusChatInput` also no-ops against a disabled control). So
 * the region stays mounted and only its TEXT changes.
 *
 * It is deliberately **not** collapsed with `display: none` when unblocked —
 * that would drop it out of the accessibility tree and make the flip an
 * insertion again, defeating the point. Instead the visual treatment (padding,
 * border, background) hangs off the `is-visible` class, so the empty container
 * occupies no space while remaining a live, registered region.
 *
 * It carries {@link BLOCKED_BANNER_ID} so the composer controls can point
 * `aria-describedby` at it — see {@link composerDescribedBy}.
 */
const blockedBannerTemplate = html `
  <div
    id="${BLOCKED_BANNER_ID}"
    class="${blockedBannerClasses}"
    part="blocked-banner"
    role="status"
  >
    ${when((x) => x.bannerVisible, html `
        <span class="blocked-banner-text">${(x) => x.effectiveBlockedReason}</span>
      `)}
  </div>
`;
// ─── Public factory ───────────────────────────────────────────────────────────
/** @internal */
/**
 * GENC-1511 — blocking "not saving" modal.
 *
 * Deliberately has NO way out but Reload. The failure it reports is invisible by nature — a
 * prod session wrote nothing for 3h17m with a normal-looking UI throughout — and anything the
 * user can dismiss is an invitation to carry on producing work that will be lost. So:
 * `show-close-icon` off, our own `bottom` slot replacing the default close button, and the
 * native dialog's Escape route cancelled on `mainElement` in `onPersistenceBroken`.
 *
 * It is only ever shown at a point where reloading is SAFE — the persister gates on the active
 * agent being resumable — so the advice cannot cost the user in-flight work it is unable to carry.
 */
const persistenceBrokenModalTemplate = (designSystemPrefix) => {
    const modalTag = `${designSystemPrefix}-modal`;
    const buttonTag = `${designSystemPrefix}-button`;
    const iconTag = `${designSystemPrefix}-icon`;
    return html `
    <${modalTag}
      class="persistence-broken-modal"
      part="persistence-broken-modal"
      ${ref('persistenceModal')}
      initial-width="480"
      ?show-close-icon=${() => false}
    >
      <div slot="top" class="persistence-broken-header" part="persistence-broken-header">
        <h2 class="persistence-broken-title">
          <${iconTag} name="triangle-exclamation"></${iconTag}> Your work isn't being saved
        </h2>
      </div>
      <div class="persistence-broken-content" part="persistence-broken-content">
        <p>
          This session has stopped saving to the server. Anything you do from here will be lost,
          so please reload before carrying on.
        </p>
        <!--
          Quantified where we can, because "some work" is not actionable and the numbers are free:
          the persister knows the stored snapshot's savedAt and how far the live transcript has
          run past it. Both are omitted rather than guessed when unavailable.
        -->
        ${when((x) => x.persistenceUnsavedMessages > 0, html `
            <p class="persistence-broken-detail">
              ${(x) => x.persistenceUnsavedMessages}
              message${(x) => (x.persistenceUnsavedMessages === 1 ? '' : 's')} have not been
              saved${(x) => x.persistenceLastSavedAt
        ? ` — the last successful save was at ${new Date(x.persistenceLastSavedAt).toLocaleTimeString()}`
        : ''}.
            </p>
          `)}
        <p class="persistence-broken-detail">
          Reloading restores the last saved point. If you want a record of this conversation
          first, use <strong>Download agent log</strong> in Settings.
        </p>
      </div>
      <div slot="bottom" class="persistence-broken-actions" part="persistence-broken-actions">
        <${buttonTag}
          appearance="accent"
          part="persistence-broken-reload"
          @click=${(x) => x.reloadForPersistence()}
        >
          Reload the page
        </${buttonTag}>
      </div>
    </${modalTag}>
  `;
};
export const FoundationAiAssistantTemplate = (designSystemPrefix) => {
    const buttonTag = `${designSystemPrefix}-button`;
    const textareaTag = `${designSystemPrefix}-text-area`;
    const iconTag = `${designSystemPrefix}-icon`;
    // ── Tool call item ──────────────────────────────────────────────────────────
    const toolCallItemTemplate = html `
    ${when((tc) => tc.foldEvent === 'open', html `
        <pre class="payload fold-event fold-open">&#9658; ${(tc) => tc.name}</pre>
      `)}
    ${when((tc) => tc.foldEvent === 'close', html `
        <pre class="payload fold-event fold-close">&#9668; ${(tc) => tc.name}</pre>
      `)}
    ${when((tc) => !tc.foldEvent && !isChatToolCallUnknown(tc), html `
        <pre class="payload">
${(tc) => { var _a; return (((_a = tc.foldPath) === null || _a === void 0 ? void 0 : _a.length) ? `${tc.foldPath.join(' › ')} › ` : ''); }}<strong>${(tc) => tc.name}</strong>(${(tc) => JSON.stringify(tc.args, null, 2)})</pre
        >
      `)}
    ${when(isChatToolCallUnknown, html `
        <pre class="payload unknown-tool">${(tc) => unknownToolPayload(tc)}</pre>
      `)}
    ${when((tc) => { var _a; return !!((_a = tc.subAgentTrace) === null || _a === void 0 ? void 0 : _a.length); }, subAgentTraceTemplate)}
  `;
    // ── Message row ─────────────────────────────────────────────────────────────
    const messageRowTemplate = html `
    ${when((m) => m.role === 'system-event', html `
        <div class="agent-switch-indicator" part="agent-switch-indicator">
          <span class="agent-switch-label">${(m) => m.content}</span>
        </div>
      `)}
    ${when((m) => m.role === 'compacted-summary', html `
        <div class="agent-switch-indicator compaction-divider" part="compaction-divider">
          <span class="agent-switch-label">${(m) => compactionDividerLabel(m)}</span>
        </div>
      `)}
    ${when((m) => m.role !== 'system-event' && m.role !== 'compacted-summary', html `
        <div
          class="message-row ${(m) => messageType(m)} ${(m) => m.interaction ? `present-${interactionPresentation(m)}` : ''}"
        >
          <div class="avatar ${(m) => messageType(m)}">
            ${when(
    // Keyed on messageType (not raw role) so a synthetic-user message,
    // which renders as 'user', gets the user avatar rather than the
    // assistant one.
    (m) => messageType(m) === 'user', userAvatarTemplate)}${when((m) => messageType(m) !== 'user', assistantAvatarTemplate)}
          </div>
          <div
            class="message ${(m) => messageType(m)} ${(m) => classNames(['narration', m.category === 'narration'])}"
            part="${(m) => messageParts(m)}"
          >
            ${when(
    // A 'bare' interaction owns its full presentation — suppress the
    // host sender label. Every other message keeps it.
    (m) => !(m.interaction && interactionPresentation(m) === 'bare'), html `
                <div class="sender">
                  ${(m, c) => {
        var _a;
        return messageType(m) === 'ai-function' &&
            m.agentName &&
            c.parent.showAgentSwitchIndicator
            ? `Tool Call · ${(_a = m.agentLabel) !== null && _a !== void 0 ? _a : m.agentName}`
            : senderLabelFor(m);
    }}
                </div>
              `)}
            <div class="content">
              ${when((m) => m.content, html `
                  <ai-chat-markdown :content="${(m) => m.content}"></ai-chat-markdown>
                `)}
              ${when((m) => m.toolCalls, html `
                  ${repeat((m) => { var _a; return (_a = m.toolCalls) !== null && _a !== void 0 ? _a : []; }, toolCallItemTemplate)}
                `)}
              ${when((m) => m.interaction, html `
                  <ai-chat-interaction-wrapper
                    :componentName=${(m) => m.interaction.componentName}
                    :data=${(m) => m.interaction.data}
                    :interactionId=${(m) => m.interaction.interactionId}
                    :resolved=${(m) => m.interaction.resolved}
                    @interaction-completed=${(m, c) => c.parent.handleInteractionCompleted(c.event)}
                  ></ai-chat-interaction-wrapper>
                `)}
            </div>
          </div>
        </div>
      `)}
  `;
    // ── Agent picker ────────────────────────────────────────────────────────────
    const agentToggleButtonTemplate = html `
    <${buttonTag}
      class="agent-toggle-button"
      part="agent-toggle-button"
      appearance="stealth"
      title=${(x) => x.agentToggleTitle}
      ?disabled=${(x) => x.busy || x.pinLocked}
      @click=${(x) => x.toggleAgentPicker()}
    >
      ${when((x) => x.agentPickerOpen, html `<${iconTag} name="chevron-down"></${iconTag}>`)}
      ${when((x) => !x.agentPickerOpen && x.pinnedAgentName !== null, html `
          <${iconTag}
            name="thumbtack"
            style="${(x) => (x.pinnedAgentColour ? `color: ${x.pinnedAgentColour}` : '')}"
          ></${iconTag}>
        `)}
      ${when((x) => !x.agentPickerOpen && x.pinnedAgentName === null, html `
          <span class="agent-toggle-label">Auto</span>
        `)}
    </${buttonTag}>
  `;
    // The attach button shares the textarea's disable gate. Attaching a file that
    // can never be sent is a dead end — and `blocked`, `compacting` and `restoring`
    // were all missing from it, so files could be attached during a compaction or
    // against an exhausted budget and then never sent.
    const attachButtonTemplate = html `
    <${buttonTag}
      class="attach-button"
      part="attach-button"
      appearance="stealth"
      title=${(x) => { var _a; return `Attach file (${(_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles})`; }}
      ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
      aria-describedby=${composerDescribedBy}
      @click=${(x) => x.triggerFileInput()}
    ><${iconTag} name="paperclip"></${iconTag}></${buttonTag}>
  `;
    const inputLeftControlsTemplate = html `
    <div class="input-left-controls" part="input-left-controls">
      ${when((x) => x.agentPickerEnabled, agentToggleButtonTemplate)}
      ${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, attachButtonTemplate)}
    </div>
  `;
    const agentPickerPanelTemplate = html `
    <div class="agent-picker-panel" part="agent-picker-panel">
      <foundation-ai-agent-picker
        part="agent-picker"
        :designSystemPrefix="${(x) => x.designSystemPrefix}"
        :mode="${(x) => x.agentPicker}"
        :agents="${(x) => { var _a; return (_a = x.agents) !== null && _a !== void 0 ? _a : []; }}"
        :pinnedAgentName="${(x) => x.pinnedAgentName}"
        @agent-pinned="${(x, c) => {
        x.pinnedAgentName = c.event.detail;
        x.toggleAgentPicker();
    }}"
      ></foundation-ai-agent-picker>
    </div>
  `;
    // ── Session lifecycle menu (GENC-1351 §5.5) ──────────────────────────────────
    const sessionMenuButtonTemplate = html `
    <${buttonTag}
      class="session-menu-button"
      part="session-menu-button"
      appearance="stealth"
      title="Session options"
      aria-label="Session options"
      @click=${(x) => x.toggleSessionMenu()}
    ><span class="session-menu-icon" aria-hidden="true">⋮</span></${buttonTag}>
  `;
    // Two fixed, equal-width buttons (Compact + Clear), centered. Clicking either
    // swaps that pair *in place* for [Confirm] [Back] of identical size and shape —
    // only the labels and colour change, so nothing moves. Confirm runs the
    // action; Back returns to the pair. The two-step confirm is the safety; there
    // is no shifting warning text.
    const sessionMenuPanelTemplate = html `
    <div class="session-menu-panel" part="session-menu-panel">
      ${when((x) => !x.confirmingClear && !x.confirmingCompact, html `
          <${buttonTag}
            class="session-menu-item"
            appearance="stealth"
            ?disabled=${(x) => !x.compactable}
            title=${(x) => x.compactable
        ? 'Summarize older messages to free up context'
        : 'Not enough conversation yet to be worth compacting'}
            @click=${(x) => (x.confirmingCompact = true)}
          >Compact</${buttonTag}><${buttonTag}
            class="session-menu-item"
            appearance="stealth"
            @click=${(x) => (x.confirmingClear = true)}
          >Clear</${buttonTag}>
        `)}${when((x) => x.confirmingCompact, html `
          <${buttonTag}
            class="session-menu-item"
            appearance="accent"
            title="Summarize older messages now"
            @click=${(x) => {
        void x.compactSession();
    }}
          >Confirm</${buttonTag}><${buttonTag}
            class="session-menu-item"
            appearance="outline"
            @click=${(x) => (x.confirmingCompact = false)}
          >Back</${buttonTag}>
        `)}${when((x) => x.confirmingClear, html `
          <${buttonTag}
            class="session-menu-item danger"
            appearance="accent"
            title="Permanently clear this conversation"
            @click=${(x) => {
        void x.clearSession();
    }}
          >Confirm</${buttonTag}><${buttonTag}
            class="session-menu-item"
            appearance="outline"
            @click=${(x) => (x.confirmingClear = false)}
          >Back</${buttonTag}>
        `)}
    </div>
  `;
    // ── Root template ───────────────────────────────────────────────────────────
    return html `
    <div class="chat-wrapper" part="chat-wrapper">
      ${when((x) => { var _a; return !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, html `
          <input
            class="file-input"
            type="file"
            multiple
            accept=${(x) => { var _a; return (_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles; }}
            @change=${(x, c) => x.handleFileSelect(c.event)}
          />
        `)}
      ${when((x) => x.settingsCogVisible, html `
          <div
            class="chat-header ${(x) => (x.popoutMode !== 'collapse' ? 'draggable' : '')}"
            part="chat-header"
            @mousedown="${(x, c) => x.onChatHeaderMouseDown(c.event)}"
          >
            ${when((x) => !!x.headerTitle, html `
                <span class="chat-title" part="chat-title">${(x) => x.headerTitle}</span>
              `)}
            <div class="header-actions" part="header-actions">
              <slot name="header-actions-extra"></slot>
              ${when((x) => !!x.popoutMode, html `
                  <${buttonTag}
                    class="popout-button"
                    part="popout-button"
                    appearance="stealth"
                    title="${(x) => (x.popoutMode === 'expand' ? 'Expand to panel' : 'Pop back to bubble')}"
                    ?disabled=${(x) => x.hasActivePendingInteraction}
                    @click=${(x) => x.handlePopout()}
                  ><${iconTag} name="${(x) => (x.popoutMode === 'expand' ? 'external-link-alt' : 'compress-alt')}"></${iconTag}></${buttonTag}>
                `)}
              <${buttonTag}
                class="settings-button"
                part="settings-button"
                appearance="stealth"
                title="Settings"
                aria-haspopup="dialog"
                @click=${(x) => x.openSettingsModal()}
              ><${iconTag} name="cog"></${iconTag}></${buttonTag}>
            </div>
          </div>
        `)}

      <slot name="below-header"></slot>

      <div class="splash-wrapper" part="splash">
        <slot name="splash"></slot>
      </div>

      <div class="messages" part="messages" ${ref('messagesEl')}>
        <div class="messages-content" part="messages-content">
          ${when((x) => x.compacting, compactingTemplate)}
          ${when((x) => x.restoring, restoringTemplate)}
          ${repeat((x) => (x.compacting || x.restoring ? [] : x.visibleMessages), messageRowTemplate)}
          ${
    /* GENC-1429: recycle:false so each pending interaction gets a FRESH wrapper. With
        recycling, the trailing slot's wrapper is reused across interactions (e.g. a tool that
        shows the code-diff approval, then the preview-status card): the reused wrapper still
        carries the prior interaction's truthy `resolved` (bound last, after componentName/
        interactionId trigger the re-render), so the completion listener's `if (this.resolved)
        return` guard swallows the next auto-resolve widget's completion and the driver hangs
        awaiting it. A fresh wrapper starts with resolved=undefined, so the guard is correct. */ ''}
          ${repeat((x) => x.activeInteractionRow, messageRowTemplate, { recycle: false })}
          ${when((x) => x.liveSubAgentTrace.length > 0 && x.showToolCalls, liveSubAgentTraceTemplate)}
          ${when((x) => {
        var _a;
        return x.showLoadingIndicator &&
            (((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.animations) == null ||
                LOADING_STYLE_ANIMATIONS.some((s) => x.enabledAnimations.includes(s)));
    }, html `
              <div class="message-row ai" part="thinking">
                <div class="avatar">
                  ${(x) => x.imageSrc
        ? html `
                          <img src="${() => x.imageSrc}" alt="Assistant" class="avatar-img" />
                        `
        : html `
                          <span
                            class="avatar-icon"
                            :innerHTML="${() => x.assistantIconSafe}"
                          ></span>
                        `}
                </div>
                ${(x) => selectThinkingTemplate(x)}
              </div>
            `)}
        </div>
      </div>

      ${when((x) => !x.composerHiddenByConfig && (x.attachments.length > 0 || x.attachmentErrors.length > 0), html `
          <div class="attachment-chips" part="attachment-chips">
            ${repeat((x) => x.attachments, html `
                <span class="attachment-chip" part="attachment-chip">
                  ${(a) => a.name}
                  <button
                    class="chip-remove"
                    type="button"
                    @click=${(a, c) => c.parent.removeAttachment(a)}
                  >
                    &times;
                  </button>
                </span>
              `)}
            ${repeat((x) => x.attachmentErrors, html `
                <span class="attachment-chip attachment-chip-error" part="attachment-chip-error">
                  ${(err) => err}
                  <button
                    class="chip-remove"
                    type="button"
                    @click=${(err, c) => c.parent.removeAttachmentError(err)}
                  >
                    &times;
                  </button>
                </span>
              `)}
          </div>
        `)}
      ${when((x) => { var _a; return ((_a = x.chatConfig.suggestions) === null || _a === void 0 ? void 0 : _a.behavior) !== 'never' && !x.suggestionsBlocked; }, html `
          <chat-suggestions
            :state="${(x) => x.suggestionsState}"
            @suggestion-clicked="${(x, c) => x.handleSuggestionClick(c.event.detail)}"
          ></chat-suggestions>
        `)}
      ${when((x) => x.agentPickerEnabled && x.agentPickerOpen, agentPickerPanelTemplate)}${when((x) => x.sessionMenuOpen && !x.busy, sessionMenuPanelTemplate)}
      ${
    /* Rendered above the composer and independently of it — a host that hides the
       built-in composer (`composerHiddenByConfig`) still needs to be told why its own
       input is being refused. */ ''}
      ${blockedBannerTemplate}
      ${when((x) => !x.composerHiddenByConfig &&
        !(x.busy && x.effectiveChatInputDuringExecution === 'hidden'), html `
          ${when((x) => x.isComposerResizable, html `
              <div
                class="composer-resize-handle"
                part="composer-resize-handle"
                role="separator"
                aria-orientation="horizontal"
                title="Drag to resize the message box"
                @pointerdown=${(x, c) => x.onComposerResizeStart(c.event)}
                @pointermove=${(x, c) => x.onComposerResizeMove(c.event)}
                @pointerup=${(x, c) => x.onComposerResizeEnd(c.event)}
                @pointercancel=${(x, c) => x.onComposerResizeEnd(c.event)}
              ></div>
            `)}
          <div class="input-row" part="input-row">
            ${when((x) => { var _a; return x.agentPickerEnabled || !!((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.acceptedFiles); }, inputLeftControlsTemplate)}
            <${textareaTag}
              ${ref('chatInputEl')}
              class="chat-input"
              part="input"
              style=${(x) => x.composerHeightStyle}
              placeholder=${(x) => x.effectivePlaceholder}
              :value=${(x) => x.inputValue}
              ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked}
              aria-disabled=${(x) => (x.blocked ? 'true' : null)}
              aria-label=${(x) => (x.blocked ? x.effectiveBlockedReason : null)}
              aria-describedby=${composerDescribedBy}
              @input=${(x, c) => (x.inputValue = c.event.target.value)}
              @keydown=${(x, c) => {
        if (c.event.key === 'Enter' &&
            !c.event.shiftKey) {
            c.event.preventDefault();
            x.handleSendClick();
            return false;
        }
        return true;
    }}
            ></${textareaTag}>
            <div class="input-right-controls">
              ${when((x) => { var _a; return x.busy && !((_a = x.chatConfig.ui) === null || _a === void 0 ? void 0 : _a.hideStopButton); }, html `
                  <${buttonTag}
                    class="stop-button"
                    part="stop-button"
                    appearance="stealth"
                    title="${(x) => (x.state === 'cancelling' ? 'Stopping…' : 'Stop')}"
                    aria-label="${(x) => (x.state === 'cancelling' ? 'Stopping' : 'Stop')}"
                    ?disabled=${(x) => x.state === 'cancelling'}
                    @click=${(x) => x.handleStopClick()}
                  ><${iconTag}
                      class="${(x) => classNames('stop-icon', ['stop-icon-spinning', x.state === 'cancelling'])}"
                      name="${(x) => (x.state === 'cancelling' ? 'spinner' : 'stop')}"
                    ></${iconTag}></${buttonTag}>
                `)}
              ${when((x) => !x.busy && !x.compacting && !x.restoring && x.sessionMenuEnabled, sessionMenuButtonTemplate)}
              <${buttonTag}
                class="send-button"
                part="send-button"
                ?disabled=${(x) => x.busy || x.compacting || x.restoring || x.blocked || (!x.inputValue.trim() && !x.attachments.length)}
                aria-describedby=${composerDescribedBy}
                @click=${(x) => x.handleSendClick()}
              >Send</${buttonTag}>
            </div>
          </div>
        `)}
      <ai-halo-overlay
        part="halo-overlay"
        ?active=${(x) => x.showHalo !== 'no' && x.enabledAnimations.includes('halo')}
        :speed="${(x) => x.showHalo === 'orchestrating' ? HALO_SPEED_ORCHESTRATING : HALO_SPEED_DEFAULT}"
        direction="${(x) => (x.showHalo === 'orchestrating' ? 'ccw' : 'cw')}"
        :borderSize="${(x) => (x.showHalo === 'orchestrating' ? 2 : HALO_BORDER_SIZE_DEFAULT)}"
      ></ai-halo-overlay>
    </div>
    ${SettingsModalTemplate(designSystemPrefix)}
    ${persistenceBrokenModalTemplate(designSystemPrefix)}
  `;
};
