fix: KnowledgeView infinite scroll root + calendar event refresh

- KnowledgeView: IntersectionObserver was watching the viewport instead
  of the card-grid scroll container, causing infinite scroll to stop
  loading after only ~29 items. Pass card-grid element as `root`.
- CalendarView: listen for 'fable:calendar-changed' custom event and
  call refetchEvents() so tool-created events appear without navigation.
- ChatPanel: dispatch 'fable:calendar-changed' when create_event,
  update_event, or delete_event tool calls succeed.
- tools.py: normalize naive datetimes to UTC before storing events so
  timezone comparisons in list_events queries are always consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-03 19:28:33 -04:00
parent 86e718dda1
commit d624d38412
4 changed files with 35 additions and 7 deletions
+13
View File
@@ -76,6 +76,19 @@ function scrollToBottom() {
watch(() => store.streamingContent, () => scrollToBottom()) watch(() => store.streamingContent, () => scrollToBottom())
watch(() => store.currentConversation?.messages.length, () => scrollToBottom()) watch(() => store.currentConversation?.messages.length, () => scrollToBottom())
// Notify CalendarView when an event is created/updated/deleted via tool
const _calendarToolNames = new Set(['create_event', 'update_event', 'delete_event'])
const _seenCalendarToolIdx = new Set<number>()
watch(() => store.streamingToolCalls, (calls) => {
calls.forEach((tc, i) => {
if (!_seenCalendarToolIdx.has(i) && _calendarToolNames.has(tc.function) && tc.status === 'success') {
_seenCalendarToolIdx.add(i)
document.dispatchEvent(new CustomEvent('fable:calendar-changed'))
}
})
}, { deep: true })
watch(() => store.streaming, (s) => { if (!s) _seenCalendarToolIdx.clear() })
// ── RAG scope chip (full, non-briefing, non-workspace) ──────────────────────── // ── RAG scope chip (full, non-briefing, non-workspace) ────────────────────────
const projects = ref<{ id: number; title: string }[]>([]) const projects = ref<{ id: number; title: string }[]>([])
const scopeDropdownOpen = ref(false) const scopeDropdownOpen = ref(false)
+12 -2
View File
@@ -115,8 +115,18 @@ function onDocClick(e: MouseEvent) {
} }
} }
onMounted(() => document.addEventListener("mousedown", onDocClick)); function onCalendarChanged() {
onUnmounted(() => document.removeEventListener("mousedown", onDocClick)); calendarRef.value?.getApi().refetchEvents();
}
onMounted(() => {
document.addEventListener("mousedown", onDocClick);
document.addEventListener("fable:calendar-changed", onCalendarChanged);
});
onUnmounted(() => {
document.removeEventListener("mousedown", onDocClick);
document.removeEventListener("fable:calendar-changed", onCalendarChanged);
});
// ── Calendar callbacks ───────────────────────────────────────────────────── // ── Calendar callbacks ─────────────────────────────────────────────────────
async function loadEvents( async function loadEvents(
+5 -4
View File
@@ -333,11 +333,12 @@ function formatDate(iso: string): string {
// ─── Infinite scroll ────────────────────────────────────────────────────────── // ─── Infinite scroll ──────────────────────────────────────────────────────────
const sentinelEl = ref<HTMLElement | null>(null); const sentinelEl = ref<HTMLElement | null>(null);
const cardGridEl = ref<HTMLElement | null>(null);
let scrollObserver: IntersectionObserver | null = null; let scrollObserver: IntersectionObserver | null = null;
function setupScrollObserver() { function setupScrollObserver() {
if (scrollObserver) scrollObserver.disconnect(); if (scrollObserver) scrollObserver.disconnect();
if (!sentinelEl.value) return; if (!sentinelEl.value || !cardGridEl.value) return;
scrollObserver = new IntersectionObserver( scrollObserver = new IntersectionObserver(
(entries) => { (entries) => {
if (entries[0].isIntersecting && !loading.value && items.value.length < total.value) { if (entries[0].isIntersecting && !loading.value && items.value.length < total.value) {
@@ -345,7 +346,7 @@ function setupScrollObserver() {
fetchItems(); fetchItems();
} }
}, },
{ rootMargin: "200px" } { root: cardGridEl.value, rootMargin: "200px" }
); );
scrollObserver.observe(sentinelEl.value); scrollObserver.observe(sentinelEl.value);
} }
@@ -368,7 +369,7 @@ onUnmounted(() => {
}); });
watchEffect(() => { watchEffect(() => {
if (sentinelEl.value) setupScrollObserver(); if (sentinelEl.value && cardGridEl.value) setupScrollObserver();
}); });
</script> </script>
@@ -471,7 +472,7 @@ watchEffect(() => {
</div> </div>
<!-- Card grid --> <!-- Card grid -->
<div v-else class="card-grid"> <div v-else ref="cardGridEl" class="card-grid">
<div <div
v-for="item in items" v-for="item in items"
:key="item.id" :key="item.id"
+5 -1
View File
@@ -3,7 +3,7 @@
import asyncio import asyncio
import logging import logging
import re import re
from datetime import date, datetime from datetime import date, datetime, timezone
from difflib import SequenceMatcher from difflib import SequenceMatcher
from fabledassistant.services.caldav import ( from fabledassistant.services.caldav import (
@@ -1409,6 +1409,8 @@ async def execute_tool(
start_str = f"{start_str}T00:00:00" start_str = f"{start_str}T00:00:00"
try: try:
start_dt = datetime.fromisoformat(start_str) start_dt = datetime.fromisoformat(start_str)
if start_dt.tzinfo is None:
start_dt = start_dt.replace(tzinfo=timezone.utc)
except (ValueError, TypeError): except (ValueError, TypeError):
return {"success": False, "error": f"Invalid start datetime: {start_str!r}"} return {"success": False, "error": f"Invalid start datetime: {start_str!r}"}
end_dt = None end_dt = None
@@ -1417,6 +1419,8 @@ async def execute_tool(
end_str = f"{end_str}T00:00:00" end_str = f"{end_str}T00:00:00"
try: try:
end_dt = datetime.fromisoformat(end_str) end_dt = datetime.fromisoformat(end_str)
if end_dt.tzinfo is None:
end_dt = end_dt.replace(tzinfo=timezone.utc)
except (ValueError, TypeError): except (ValueError, TypeError):
return {"success": False, "error": f"Invalid end datetime: {end_str!r}"} return {"success": False, "error": f"Invalid end datetime: {end_str!r}"}
project_id = None project_id = None