// SPDX-License-Identifier: AGPL-3.0-or-later // Task chooser UI component. // Listens for taskrequest events on navigator.embedder, displays a list of // matching task providers, and responds with the user's selection. // Handles both window mode (new webview) and inline mode (overlay dialog). import { LitElement, html, css, } from "beaver://shared/third_party/lit/lit-all.min.js"; export class TaskChooser extends LitElement { static properties = { open: { type: Boolean, reflect: true }, _taskName: { state: true }, _providers: { state: true }, _requestId: { state: true }, }; static styles = css` :host { display: none; position: fixed; inset: 0; z-index: var(--z-modal); } :host([open]) { display: flex; } .overlay { position: absolute; inset: 0; background: var(--color-backdrop); display: flex; align-items: flex-end; justify-content: center; } .panel { background: var(--bg-surface); border-radius: var(--radius-md) var(--radius-md) 0 0; padding: var(--spacing-2xl); width: 100%; max-width: 400px; max-height: 60vh; overflow-y: auto; box-shadow: 0 -4px 20px var(--color-shadow); font-family: var(--font-family-base); } .title { font-size: var(--font-size-md); font-weight: var(--font-weight-bold); color: var(--color-text); margin-bottom: var(--spacing-2xl); } .provider { display: flex; align-items: center; gap: var(--spacing-2xl); padding: var(--spacing-md); border-radius: var(--radius-sm); cursor: pointer; transition: background var(--transition-fast); } .provider:hover { background: var(--bg-hover); } .provider-icon { width: 24px; height: 24px; border-radius: var(--radius-sm); object-fit: contain; flex-shrink: 0; } .provider-title { font-size: var(--font-size-md); font-weight: var(--font-weight-bold); color: var(--color-text); } .provider-description { font-size: var(--font-size-base); color: var(--color-text-secondary); } .provider-origin { font-size: var(--font-size-sm); color: var(--color-text-tertiary); } .cancel { display: block; width: 100%; margin-top: var(--spacing-2xl); padding: var(--spacing-md); border: 1px solid var(--color-border); border-radius: var(--radius-sm); background: var(--bg-surface); color: var(--color-text); font-size: var(--font-size-md); font-family: var(--font-family-base); cursor: pointer; text-align: center; transition: background var(--transition-fast); } .cancel:hover { background: var(--bg-hover); } .always-use { display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md); font-size: var(--font-size-base); color: var(--color-text-secondary); cursor: pointer; } `; constructor() { super(); this.open = false; this._taskName = ""; this._providers = []; this._requestId = ""; this._selectedDisplay = null; } connectedCallback() { super.connectedCallback(); if (navigator.embedder) { this._onTaskRequest = (e) => this._handleTaskRequest(e); navigator.embedder.addEventListener("taskrequest", this._onTaskRequest); this._onOpenProvider = (e) => this._handleOpenProvider(e); navigator.embedder.addEventListener( "opentaskprovider", this._onOpenProvider, ); this._onProvidersUpdate = (e) => this._handleProvidersUpdate(e); navigator.embedder.addEventListener( "taskprovidersupdate", this._onProvidersUpdate, ); } } disconnectedCallback() { super.disconnectedCallback(); if (navigator.embedder) { if (this._onTaskRequest) { navigator.embedder.removeEventListener( "taskrequest", this._onTaskRequest, ); } if (this._onOpenProvider) { navigator.embedder.removeEventListener( "opentaskprovider", this._onOpenProvider, ); } if (this._onProvidersUpdate) { navigator.embedder.removeEventListener( "taskprovidersupdate", this._onProvidersUpdate, ); } } } _handleProvidersUpdate(e) { const { requestId, providers } = e.detail; if (requestId !== this._requestId) return; try { const newProviders = typeof providers === "string" ? JSON.parse(providers) : providers; if (newProviders.length > 0) { console.log( `[TaskChooser] Remote providers update: +${newProviders.length}`, ); this._providers = [...this._providers, ...newProviders]; } } catch { // Ignore parse errors. } } _handleOpenProvider(e) { const { requestId, url, title } = e.detail; console.log(`[TaskChooser] Opening provider: ${title} (${url})`); if (this._selectedDisplay === "inline") { // Dispatch event to show inline provider on the caller's webview. this.dispatchEvent( new CustomEvent("show-inline-provider", { bubbles: true, composed: true, detail: { requestId, url, title }, }), ); } else { // Dispatch event to index.js to open the provider in a new webview. this.dispatchEvent( new CustomEvent("open-task-provider", { bubbles: true, composed: true, detail: { requestId, url, title }, }), ); } } _handleTaskRequest(e) { const { requestId, taskName, providers, defaultProvider } = e.detail; console.log("[TaskChooser] Task request received:", taskName, requestId); this._requestId = requestId; this._taskName = taskName; this._selectedDisplay = null; try { this._providers = typeof providers === "string" ? JSON.parse(providers) : providers; } catch { this._providers = []; } if (this._providers.length === 0) { console.log("[TaskChooser] No providers, cancelling"); this._respond(null); return; } // If there's a default and it's in the provider list, auto-select it. if (defaultProvider) { const defaultProv = this._providers.find((p) => p.id === defaultProvider); if (defaultProv) { console.log("[TaskChooser] Using default provider:", defaultProv.title); this._selectProvider(defaultProv); return; } } this.open = true; } _selectProvider(provider) { console.log( "[TaskChooser] Provider selected:", provider.id, provider.title, ); // If "always use this" is checked, save the default. const checkbox = this.shadowRoot?.querySelector("#always-use"); if (checkbox?.checked) { navigator.embedder.setTaskDefault( this._taskName, provider.href, provider.remote_peer_id || null, ); } // Store display mode for when opentaskprovider arrives. this._selectedDisplay = provider.display === "Inline" ? "inline" : "window"; this._respond(provider.id); this.open = false; } _cancel() { console.log("[TaskChooser] Cancelled"); this._respond(null); this.open = false; } _respond(providerId) { if (this._requestId && navigator.embedder) { navigator.embedder.respondToTaskRequest(this._requestId, providerId); this._requestId = ""; } } render() { if (!this.open) return html``; // Provider chooser. return html`
e.stopPropagation()}>
${this._taskName} with...
${this._providers.map( (p) => html`
this._selectProvider(p)}> ${ p.icon ? html`` : "" }
${p.title}
${ p.description ? html`
${p.description}
` : "" }
${p.origin}${ p.device_name ? html` (${p.device_name})` : "" }
`, )} ${ this._providers.length > 1 ? html`` : "" }
`; } } customElements.define("task-chooser", TaskChooser);