export function mount({ root, state, host }) { state = state || {}; const __cleanup = []; const __setInterval = (...args) => { const id = window.setInterval(...args); __cleanup.push(() => window.clearInterval(id)); return id; }; const __setTimeout = (...args) => { const id = window.setTimeout(...args); __cleanup.push(() => window.clearTimeout(id)); return id; }; if (!(root instanceof HTMLElement)) return; const subtitleEl = root.querySelector('[data-calendar-subtitle]'); const statusEl = root.querySelector('[data-calendar-status]'); const rangeEl = root.querySelector('[data-calendar-range]'); const emptyEl = root.querySelector('[data-calendar-empty]'); const listEl = root.querySelector('[data-calendar-list]'); const updatedEl = root.querySelector('[data-calendar-updated]'); if (!(subtitleEl instanceof HTMLElement) || !(rangeEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement) || !(listEl instanceof HTMLElement) || !(updatedEl instanceof HTMLElement)) return; const subtitle = typeof state.subtitle === 'string' ? state.subtitle : ''; const configuredToolName = typeof state.tool_name === 'string' ? state.tool_name.trim() : ''; const calendarNames = Array.isArray(state.calendar_names) ? state.calendar_names.map((value) => String(value || '').trim()).filter(Boolean) : []; const rangeDaysRaw = Number(state.range_days); const rangeDays = Number.isFinite(rangeDaysRaw) && rangeDaysRaw >= 1 ? Math.min(rangeDaysRaw, 7) : 1; const maxEventsRaw = Number(state.max_events); const maxEvents = Number.isFinite(maxEventsRaw) && maxEventsRaw >= 1 ? Math.min(maxEventsRaw, 30) : 8; const refreshMsRaw = Number(state.refresh_ms); const refreshMs = Number.isFinite(refreshMsRaw) && refreshMsRaw >= 60000 ? refreshMsRaw : 15 * 60 * 1000; const emptyText = typeof state.empty_text === 'string' && state.empty_text.trim() ? state.empty_text.trim() : 'No events scheduled.'; subtitleEl.textContent = subtitle || (calendarNames.length > 0 ? calendarNames.join(', ') : 'Loading calendars'); emptyEl.textContent = emptyText; const updateLiveContent = (snapshot) => { host.setLiveContent(snapshot); }; const setStatus = (label, color) => { if (!(statusEl instanceof HTMLElement)) return; statusEl.textContent = label; statusEl.style.color = color; }; const normalizeDateValue = (value) => { if (typeof value === 'string') return value; if (value && typeof value === 'object') { if (typeof value.dateTime === 'string') return value.dateTime; if (typeof value.date === 'string') return value.date; } return ''; }; const isAllDay = (start, end) => /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(start)) || /^\d{4}-\d{2}-\d{2}$/.test(normalizeDateValue(end)); const eventSortKey = (event) => { const raw = normalizeDateValue(event && event.start); const time = new Date(raw).getTime(); return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER; }; const formatTime = (value) => { const raw = normalizeDateValue(value); if (!raw) return '--:--'; if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return 'All day'; const date = new Date(raw); if (Number.isNaN(date.getTime())) return '--:--'; return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }; const formatDay = (value) => { const raw = normalizeDateValue(value); if (!raw) return '--'; const date = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? new Date(`${raw}T00:00:00`) : new Date(raw); if (Number.isNaN(date.getTime())) return '--'; return date.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' }); }; const formatRange = (start, end) => `${start.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })} to ${end.toLocaleDateString([], { weekday: 'short', month: 'short', day: 'numeric' })}`; const extractEvents = (toolResult) => { if (toolResult && toolResult.parsed && typeof toolResult.parsed === 'object') { const parsed = toolResult.parsed; if (Array.isArray(parsed.result)) return parsed.result; } if (typeof toolResult?.content === 'string') { try { const parsed = JSON.parse(toolResult.content); if (parsed && typeof parsed === 'object' && Array.isArray(parsed.result)) { return parsed.result; } } catch { return []; } } return []; }; const eventTime = (value) => { const raw = normalizeDateValue(value); if (!raw) return Number.MAX_SAFE_INTEGER; const normalized = /^\d{4}-\d{2}-\d{2}$/.test(raw) ? `${raw}T00:00:00` : raw; const time = new Date(normalized).getTime(); return Number.isFinite(time) ? time : Number.MAX_SAFE_INTEGER; }; const resolveToolConfig = async () => { const fallbackName = configuredToolName || 'mcp_home_assistant_calendar_get_events'; if (!host.listTools) { return { name: fallbackName, availableCalendars: calendarNames }; } try { const tools = await host.listTools(); const tool = Array.isArray(tools) ? tools.find((item) => /(^|_)calendar_get_events$/i.test(String(item?.name || ''))) : null; const enumValues = Array.isArray(tool?.parameters?.properties?.calendar?.enum) ? tool.parameters.properties.calendar.enum.map((value) => String(value || '').trim()).filter(Boolean) : []; return { name: tool?.name || fallbackName, availableCalendars: enumValues, }; } catch { return { name: fallbackName, availableCalendars: calendarNames }; } }; const renderEvents = (events) => { listEl.innerHTML = ''; if (!Array.isArray(events) || events.length === 0) { emptyEl.style.display = 'block'; return; } emptyEl.style.display = 'none'; for (const [index, event] of events.slice(0, maxEvents).entries()) { const item = document.createElement('li'); item.style.padding = index === 0 ? '10px 0 8px' : '10px 0 8px'; item.style.borderTop = '1px solid var(--theme-card-neutral-border)'; const summary = document.createElement('div'); summary.style.fontSize = '0.98rem'; summary.style.lineHeight = '1.3'; summary.style.fontWeight = '700'; summary.style.color = 'var(--theme-card-neutral-text)'; summary.textContent = String(event.summary || '(No title)'); item.appendChild(summary); const timing = document.createElement('div'); timing.style.marginTop = '4px'; timing.style.fontSize = '0.9rem'; timing.style.lineHeight = '1.35'; timing.style.color = 'var(--theme-card-neutral-subtle)'; const dayLabel = formatDay(event.start); const timeLabel = isAllDay(event.start, event.end) ? 'All day' : `${formatTime(event.start)} - ${formatTime(event.end)}`; timing.textContent = dayLabel === '--' ? timeLabel : `${dayLabel} ยท ${timeLabel}`; item.appendChild(timing); listEl.appendChild(item); } }; const refresh = async () => { setStatus('Refreshing', 'var(--theme-status-muted)'); const start = new Date(); start.setHours(0, 0, 0, 0); const end = new Date(start); end.setDate(end.getDate() + Math.max(rangeDays - 1, 0)); end.setHours(23, 59, 59, 999); rangeEl.textContent = formatRange(start, end); try { const toolConfig = await resolveToolConfig(); const selectedCalendars = calendarNames.length > 0 ? calendarNames : toolConfig.availableCalendars; if (!toolConfig.name) throw new Error('Calendar tool unavailable'); if (!Array.isArray(selectedCalendars) || selectedCalendars.length === 0) { throw new Error('No calendars configured'); } const resolvedSubtitle = subtitle || selectedCalendars.join(', '); subtitleEl.textContent = resolvedSubtitle; const allEvents = []; const rangeMode = rangeDays > 1 ? 'week' : 'today'; const endExclusiveTime = end.getTime() + 1; for (const calendarName of selectedCalendars) { const toolResult = await host.callTool(toolConfig.name, { calendar: calendarName, range: rangeMode, }); const events = extractEvents(toolResult); for (const event of events) { const startTime = eventTime(event?.start); if (startTime < start.getTime() || startTime >= endExclusiveTime) continue; allEvents.push({ ...event, _calendarName: calendarName }); } } allEvents.sort((left, right) => eventSortKey(left) - eventSortKey(right)); renderEvents(allEvents); const updatedText = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); updatedEl.textContent = updatedText; const statusLabel = rangeDays > 1 ? `${rangeDays}-day` : 'Today'; setStatus(statusLabel, 'var(--theme-status-live)'); const snapshotEvents = allEvents.slice(0, maxEvents).map((event) => { const dayLabel = formatDay(event.start); const timeLabel = isAllDay(event.start, event.end) ? 'All day' : `${formatTime(event.start)} - ${formatTime(event.end)}`; return { summary: String(event.summary || '(No title)'), start: normalizeDateValue(event.start) || null, end: normalizeDateValue(event.end) || null, day_label: dayLabel === '--' ? null : dayLabel, time_label: timeLabel, all_day: isAllDay(event.start, event.end), }; }); updateLiveContent({ kind: 'calendar_agenda', subtitle: resolvedSubtitle || null, tool_name: toolConfig.name, calendar_names: selectedCalendars, range_label: rangeEl.textContent || null, status: statusLabel, updated_at: updatedText, event_count: snapshotEvents.length, events: snapshotEvents, }); } catch (error) { const errorText = String(error); renderEvents([]); updatedEl.textContent = errorText; setStatus('Unavailable', 'var(--theme-status-danger)'); updateLiveContent({ kind: 'calendar_agenda', subtitle: subtitleEl.textContent || null, tool_name: configuredToolName || 'mcp_home_assistant_calendar_get_events', calendar_names: calendarNames, range_label: rangeEl.textContent || null, status: 'Unavailable', updated_at: errorText, event_count: 0, events: [], error: errorText, }); } }; host.setRefreshHandler(() => { void refresh(); }); void refresh(); __setInterval(() => { void refresh(); }, refreshMs); return { destroy() { host.setRefreshHandler(null); host.setLiveContent(null); host.clearSelection(); for (const cleanup of __cleanup.splice(0)) cleanup(); }, }; }