// — shows the active project and a live feed of what happened // (user AND Claude actions). A self-contained control (AGENT.md §1.1): shadow // DOM, fetches its own initial data, subscribes to the /events SSE stream, and // cleans up on disconnect. It reflects the coordination state that Claude reads // over MCP (§8.2), so the user can see the two staying in sync. class ActivityFeed extends HTMLElement { #es = null; #controller = null; #events = []; constructor() { super(); this.attachShadow({ mode: 'open' }); } connectedCallback() { this.#renderShell(); this.#loadInitial(); // Live updates. EventSource auto-reconnects if the stream drops. this.#es = new EventSource('/events'); this.#es.addEventListener('activity', (e) => { try { this.#onEvent(JSON.parse(e.data)); } catch { /* ignore malformed */ } }); } disconnectedCallback() { this.#es?.close(); this.#controller?.abort(); } async #loadInitial() { this.#controller?.abort(); this.#controller = new AbortController(); try { const [apRes, actRes] = await Promise.all([ fetch('/api/active-project', { signal: this.#controller.signal }), fetch('/api/activity', { signal: this.#controller.signal }), ]); const ap = await apRes.json(); const events = await actRes.json(); this.#events = Array.isArray(events) ? events : []; this.#renderActive(ap.path || ''); this.#renderFeed(); } catch (err) { if (err.name !== 'AbortError') this.#renderError(err); } } #onEvent(ev) { this.#events.push(ev); if (this.#events.length > 200) this.#events = this.#events.slice(-200); if (ev.kind === 'active-project-changed') this.#renderActive(ev.repo || ''); this.#renderFeed(); } #renderActive(path) { const el = this.shadowRoot.getElementById('active'); el.textContent = path ? this.#base(path) : 'none'; el.title = path; } #renderFeed() { const ul = this.shadowRoot.getElementById('feed'); // Newest first. ul.replaceChildren(...[...this.#events].reverse().map((ev) => { const li = document.createElement('li'); const repo = ev.repo ? this.#base(ev.repo) : ''; li.innerHTML = ` ${this.#esc(ev.actor)} ${this.#esc(this.#label(ev.kind))} ${repo ? `${this.#esc(repo)}` : ''} ${ev.detail ? `${this.#esc(ev.detail)}` : ''} ${this.#time(ev.time)}`; return li; })); } #renderError(err) { this.shadowRoot.getElementById('feed').innerHTML = `
  • Could not load activity: ${this.#esc(err.message)}
  • `; } #renderShell() { this.shadowRoot.innerHTML = `
    Active project: none

    Activity

    `; } #base(p) { return String(p).split(/[/\\]/).filter(Boolean).pop() || p; } #label(kind) { return String(kind || '').replace(/-/g, ' '); } #time(t) { const d = new Date(t); return isNaN(d) ? '' : d.toLocaleTimeString(); } #esc(s) { return String(s ?? '').replace(/[&<>"']/g, (c) => ( { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c] )); } } customElements.define('activity-feed', ActivityFeed);