var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators.js"; import { ModelSelector } from "../dialogs/ModelSelector.js"; import "./MessageEditor.js"; import "./MessageList.js"; import "./Messages.js"; // Import for side effects to register the custom elements import { getAppStorage } from "../storage/app-storage.js"; import "./StreamingMessageContainer.js"; import { formatUsage } from "../utils/format.js"; import { i18n } from "../utils/i18n.js"; let AgentInterface = class AgentInterface extends LitElement { constructor() { super(...arguments); this.enableAttachments = true; this.enableModelSelector = true; this.enableThinkingSelector = true; this.showThemeToggle = false; this.sessionThinkingLevel = "off"; this.pendingThinkingLevel = null; this._autoScroll = true; this._lastScrollTop = 0; this._lastClientHeight = 0; this._handleScroll = (_ev) => { if (!this._scrollContainer) return; const currentScrollTop = this._scrollContainer.scrollTop; const scrollHeight = this._scrollContainer.scrollHeight; const clientHeight = this._scrollContainer.clientHeight; const distanceFromBottom = scrollHeight - currentScrollTop - clientHeight; // Ignore relayout due to message editor getting pushed up by stats if (clientHeight < this._lastClientHeight) { this._lastClientHeight = clientHeight; return; } // Only disable auto-scroll if user scrolled UP or is far from bottom if (currentScrollTop !== 0 && currentScrollTop < this._lastScrollTop && distanceFromBottom > 50) { this._autoScroll = false; } else if (distanceFromBottom < 10) { // Re-enable if very close to bottom this._autoScroll = true; } this._lastScrollTop = currentScrollTop; this._lastClientHeight = clientHeight; }; } setInput(text, attachments) { const update = () => { if (!this._messageEditor) requestAnimationFrame(update); else { this._messageEditor.value = text; this._messageEditor.attachments = attachments || []; } }; update(); } setAutoScroll(enabled) { this._autoScroll = enabled; } createRenderRoot() { return this; } willUpdate(changedProperties) { super.willUpdate(changedProperties); // Re-subscribe when session property changes if (changedProperties.has("session")) { this.setupSessionSubscription(); } } async connectedCallback() { super.connectedCallback(); this.style.display = "flex"; this.style.flexDirection = "column"; this.style.height = "100%"; this.style.minHeight = "0"; // Wait for first render to get scroll container await this.updateComplete; this._scrollContainer = this.querySelector(".overflow-y-auto"); if (this._scrollContainer) { // Set up ResizeObserver to detect content changes this._resizeObserver = new ResizeObserver(() => { if (this._autoScroll && this._scrollContainer) { this._scrollContainer.scrollTop = this._scrollContainer.scrollHeight; } }); // Observe the content container inside the scroll container const contentContainer = this._scrollContainer.querySelector(".max-w-3xl"); if (contentContainer) { this._resizeObserver.observe(contentContainer); } // Set up scroll listener with better detection this._scrollContainer.addEventListener("scroll", this._handleScroll); } // Subscribe to external session if provided this.setupSessionSubscription(); } disconnectedCallback() { super.disconnectedCallback(); // Clean up observers and listeners if (this._resizeObserver) { this._resizeObserver.disconnect(); this._resizeObserver = undefined; } if (this._scrollContainer) { this._scrollContainer.removeEventListener("scroll", this._handleScroll); } if (this._unsubscribeSession) { this._unsubscribeSession(); this._unsubscribeSession = undefined; } } setupSessionSubscription() { if (this._unsubscribeSession) { this._unsubscribeSession(); this._unsubscribeSession = undefined; } if (!this.session) return; this._unsubscribeSession = this.session.subscribe(async (ev) => { if (ev.type === "state-update") { if (this.pendingThinkingLevel === null && ev.state.thinkingLevel) { this.sessionThinkingLevel = ev.state.thinkingLevel; } if (this._streamingContainer) { this._streamingContainer.isStreaming = ev.state.isStreaming; this._streamingContainer.setMessage(ev.state.streamMessage, !ev.state.isStreaming); } this.requestUpdate(); } else if (ev.type === "error-no-model") { // TODO show some UI feedback } else if (ev.type === "error-no-api-key") { // Handled by onApiKeyRequired callback } }); } async sendMessage(input, attachments) { if ((!input.trim() && attachments?.length === 0) || this.session?.state.isStreaming) return; const session = this.session; if (!session) throw new Error("No session set on AgentInterface"); if (!session.state.model) throw new Error("No model set on AgentInterface"); // Check if API key exists for the provider (only needed in direct mode) const provider = session.state.model.provider; const apiKey = await getAppStorage().providerKeys.get(provider); // If no API key, prompt for it if (!apiKey) { if (!this.onApiKeyRequired) { console.error("No API key configured and no onApiKeyRequired handler set"); return; } const success = await this.onApiKeyRequired(provider); // If still no API key, abort the send if (!success) { return; } } // Call onBeforeSend hook before sending if (this.onBeforeSend) { await this.onBeforeSend(); } const baseThinking = this.sessionThinkingLevel || session.state.thinkingLevel || "off"; const thinkingOverride = this.pendingThinkingLevel ?? baseThinking; const transient = this.pendingThinkingLevel !== null && this.pendingThinkingLevel !== baseThinking; // Only clear editor after we know we can send this._messageEditor.value = ""; this._messageEditor.attachments = []; this._autoScroll = true; // Enable auto-scroll when sending a message await this.session?.prompt(input, attachments, { thinkingOverride, transient, }); this.pendingThinkingLevel = null; // Reset editor thinking selector to session baseline if (this._messageEditor) { this._messageEditor.thinkingLevel = this.sessionThinkingLevel || "off"; } } renderMessages() { if (!this.session) return html `