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 `
${i18n("No session available")}
`; const state = this.session.state; // Build a map of tool results to allow inline rendering in assistant messages const toolResultsById = new Map(); for (const message of state.messages) { if (message.role === "toolResult") { toolResultsById.set(message.toolCallId, message); } } return html `
`; } renderStats() { if (!this.session) return html `
`; const state = this.session.state; const totals = state.messages .filter((m) => m.role === "assistant") .reduce((acc, msg) => { const usage = msg.usage; if (usage) { acc.input += usage.input; acc.output += usage.output; acc.cacheRead += usage.cacheRead; acc.cacheWrite += usage.cacheWrite; acc.cost.total += usage.cost.total; } return acc; }, { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }); const hasTotals = totals.input || totals.output || totals.cacheRead || totals.cacheWrite; const totalsText = hasTotals ? formatUsage(totals) : ""; return html `
${this.showThemeToggle ? html `` : html ``}
${totalsText ? this.onCostClick ? html `${totalsText}` : html `${totalsText}` : ""}
`; } render() { if (!this.session) return html `
${i18n("No session set")}
`; const session = this.session; const state = this.session.state; return html `
${this.renderMessages()}
{ this.sendMessage(input, attachments); }} .onAbort=${() => session.abort()} .onModelSelect=${() => { ModelSelector.open(state.model, (model) => session.setModel(model)); }} .onThinkingChange=${this.enableThinkingSelector ? (level) => { this.pendingThinkingLevel = level; if (this._messageEditor) { this._messageEditor.thinkingLevel = level; } this.requestUpdate(); } : undefined} > ${this.renderStats()}
`; } }; __decorate([ property({ attribute: false }) ], AgentInterface.prototype, "session", void 0); __decorate([ property({ type: Boolean }) ], AgentInterface.prototype, "enableAttachments", void 0); __decorate([ property({ type: Boolean }) ], AgentInterface.prototype, "enableModelSelector", void 0); __decorate([ property({ type: Boolean }) ], AgentInterface.prototype, "enableThinkingSelector", void 0); __decorate([ property({ type: String }) ], AgentInterface.prototype, "sessionThinkingLevel", void 0); __decorate([ property({ type: Boolean }) ], AgentInterface.prototype, "showThemeToggle", void 0); __decorate([ property({ attribute: false }) ], AgentInterface.prototype, "onApiKeyRequired", void 0); __decorate([ property({ attribute: false }) ], AgentInterface.prototype, "onBeforeSend", void 0); __decorate([ property({ attribute: false }) ], AgentInterface.prototype, "onBeforeToolCall", void 0); __decorate([ property({ attribute: false }) ], AgentInterface.prototype, "onCostClick", void 0); __decorate([ query("message-editor") ], AgentInterface.prototype, "_messageEditor", void 0); __decorate([ query("streaming-message-container") ], AgentInterface.prototype, "_streamingContainer", void 0); __decorate([ state() ], AgentInterface.prototype, "pendingThinkingLevel", void 0); AgentInterface = __decorate([ customElement("agent-interface") ], AgentInterface); export { AgentInterface }; // Register custom element with guard if (!customElements.get("agent-interface")) { customElements.define("agent-interface", AgentInterface); } //# sourceMappingURL=AgentInterface.js.map