Switch sidebar from fixed px to proportional 35% so it resizes with
the viewport. Remove border between chat and input bar and between
weather and news. Use container query units for weather/forecast icons
so they scale with column width.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Collapse briefing from 3-column to 2-column grid (chat + sidebar).
Weather moves to top of news column, forecast uses an HTML table so
rows align cleanly. Drop verbose condition text from forecast days
(emoji already conveys it). Switch CI node_modules cache to npm
download cache to avoid tar size error from onnxruntime-web.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Swaps useSilenceDetector for useVad. Adds no-speech guard: when the
user manually stops recording without VAD ever detecting speech, show
a toast and skip the Whisper round-trip instead of sending pure noise.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Empty-state rethink: replace "Start a conversation." with a richer
landing showing recent conversations (top 5 as resume links) and a
voice-mode entry button. Gated to full variant only; widget/briefing
keep the simple message.
Context sidebar: switch from grid column (4-col layout) to absolute
overlay on the right gutter (3-col layout). The reading column is now
always symmetrically centered regardless of whether the sidebar is
visible — eliminates the leftward shift that occurred when context
notes appeared.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Streaming bubble (Generating response…) was flush against the input
wrapper top border. Give the scroll column 0.75rem of breathing room
so the assistant bubble sits visibly separate from the input.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Context sidebar sections (Auto-included / Suggested / In Context) are
now collapsible with chevrons, and collapsed state persists per
conversation via localStorage. A new Context (N) button in the chat
header toggles the whole sidebar — open by default on wide screens
(>=1200px), closed by default below. Visual differentiation: auto-
included notes get a muted background with an AUTO pill, in-context
notes get an active-chip style with a primary-colored left border,
and each section header shows its item count.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase B of chat view refinement. Bumps chat reading column to 1200px,
lifts the RAG scope chip out of the footer and into the ChatView header
(next to the title), and collapses the listen toggle / volume slider /
stop-playback buttons into a single speaker popover on the input bar.
Listen mode + TTS are now available in briefing and workspace chats too,
since the controls live on the shared ChatInputBar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the full-variant flex layout with a single CSS grid owning the
messages column, input bar, and context sidebar. Messages and input bar
share a centered ~820px reading track (--chat-reading-width token) so
the mic button can't get pushed off-screen on wide monitors, and the
context sidebar spans grid-row 1/-1 so it runs the full height from the
chat header down to the bottom edge instead of stopping above the input
bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The web silence detector previously ran RMS over getByteFrequencyData
bytes (which are already dB-scaled) and re-log'd the result, producing
numbers that didn't line up with real dBFS. Combined with a static
-40 dB threshold that sat right on top of room ambient, silence
detection rarely fired and the user had to click stop manually.
- Rewrite useSilenceDetector to use getFloatTimeDomainData for honest
linear RMS → dBFS.
- Silence threshold is now dynamic: track session peak dBFS and treat
"silent" as 15 dB below peak. Auto-calibrates per mic/room.
- Grace period (1500 ms) at start so the user can begin speaking
before checks arm; static -45 dB fallback until peak clears -25 dB
so dead-silent sessions don't spin forever.
- Silence duration bumped 1500 → 2000 ms for breathing room.
Visual: detached red radial-gradient disc sits behind the mic in a
wrapper; the button no longer scales, so the white mic icon stays
legible on top while the halo grows from 1x to ~2.6x with live
amplitude. Much more obvious "you are live" signal than the prior
subtle box-shadow pulse.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The mic button now scales and glows proportional to the silence
detector's RMS amplitude instead of sitting static. Gives obvious
feedback that audio is actually being picked up — silence still
breathes via a 0.1 floor, loud input caps at ~1.18x scale so it
doesn't punch through the input bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The frontend hardcoded think=true on every chat send (ChatPanel full +
widget variants, KnowledgeView minichat), which defeated the _should_think
gate on the backend and made qwen3:14b spend 5-20s on chain-of-thought
reasoning for every turn — even "hi". This was the root cause of the
warm-path TTFT variance tracked in followup_ttft_variance.md: the logged
ttft_ms was really prefill + full thinking phase, bouncing with the depth
of the model's reasoning, not with cache or eviction.
All three frontend callers now pass think=false and let _should_think be
authoritative. The classifier is now a real content-based gate: explicit
think_requested=True still forces on as an override (briefing discuss
actions, future UI toggles, MCP callers), otherwise messages <80 chars
without reasoning keywords skip thinking, messages >=400 chars or
containing keywords like why/explain/analyze/debug/review/etc. get it.
Generation timing now separately records think_requested, the final
think decision, first_token_ms (first any chunk), and thinking_ms
(duration of the thinking phase). ttft_ms keeps its existing semantic
(first content token) so existing log analysis still works. The timing
log line surfaces all four fields so the old "just a big ttft number"
ambiguity is gone.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge create_task into create_note (set status='todo' for tasks, omit
for notes), merge delete_task into delete_note, consolidate entity
tools (create/update_person → save_person, create/update_place →
save_place), rename get_note → read_note with clearer descriptions,
move calculate out of rag.py into utility.py, and extract shared
duplicate detection into check_duplicate() helper.
Updates all downstream references in generation_task.py, quick_capture.py,
ToolCallCard.vue, and WorkspaceView.vue.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three visual improvements for the briefing conversation:
1. Intermediate tool-call messages (empty content, briefing_intermediate:
true) now render as a compact dashed status row with per-tool pills
showing result counts, instead of an empty assistant bubble followed
by a stack of ToolCallCards. Click to expand the full cards.
2. Slot badge on non-intermediate briefing messages — "Full Briefing"
for compilation, "Morning/Midday/Afternoon Update" for slot
injections. Slot updates get a softer bubble treatment (transparent
background, muted border, smaller text) so the compilation stays
visually dominant.
3. Slot separator now triggers on briefing_slot metadata (not the
compilation-only rss_item_ids), and uses a look-behind so it only
fires when there's a prior slot message — no separator above the
first briefing of the day.
New component: BriefingToolStatusRow.vue handles the intermediate
pill row and delegates to ToolCallCard when expanded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- build_context: when conversation_type is 'briefing', inject a system
prompt instruction telling the model to answer from conversation history
and article context instead of searching the web
- Consolidate briefing conversation type detection to one DB query (was
being checked twice — once for the system prompt addition, once for
article context injection)
- ChatPanel: render a visual 'New Briefing Update' separator line before
2nd+ briefing slot messages (identified by metadata.rss_item_ids)
- types/chat.ts: add metadata field to Message interface
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- tools.py: apply UTC normalization to update_event datetime fields
(matched create_event which already did this)
- events.py service: allow end_dt/recurrence/project_id to be cleared
via update_event by permitting None for nullable fields
- events.py service: find_events_by_query now returns upcoming events
first, falling back to past — prevents AI tools from mutating stale
past events when a future match exists
- events.py service: list_events now uses overlap logic (start <= to
AND end >= from) so multi-day events spanning the query boundary
are included; previously only start_dt was checked
Frontend:
- ToolCallCard: fire fable:calendar-changed on created/updated/deleted
so CalendarView refetches without requiring a manual page refresh
- KnowledgeView: replace raw apiGet('/api/events') with listEvents()
client function; also fix today bar which was reading .events off a
flat array (always empty) — now correctly receives EventEntry[]
- HomeView: use full ISO strings for event date range instead of naive
UTC-midnight strings; deduplicate inline date math via _dateRange()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UTC midnight passed to FullCalendar's timeZone:'local' was being
converted to local time, shifting all-day events back by 1+ days for
users in UTC-X zones. The edit form had the same bug via new Date().
Fix: pass YYYY-MM-DD slices (UTC date) for all-day events in both
toFcEvent and EventSlideOver resetForm, bypassing timezone conversion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
KnowledgeView:
- Watch streamingToolCalls; call fetchItems+fetchTags on create/update
note or task so the card grid reflects changes made via the minichat
- Cap minichat to max-width: var(--page-max-width) so it matches chat column width
WorkspaceView + WorkspaceNoteEditor:
- Expose reload() from WorkspaceNoteEditor via defineExpose
- Call noteEditorRef.reload() alongside taskPanelRef.reload() when
create_note/update_note tools succeed in the SSE watcher
KnowledgeView list cards:
- Backend: parse markdown task list into list_items [{text, checked}] + body
- Card renders up to 6 items with real checkboxes; toggleListItem()
does an optimistic update then PATCHes /api/notes/:id
- Progress bar kept below items; "+N more" shown when list is long
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView mini-chat: replace harsh border-top with upward box-shadow
and rounded top corners (16px); remove padding-bottom from content area
so widget truly overlays cards without pushing layout; add collapse
toggle (chevron) that hides messages without closing the conversation
- WeatherCard: show precip_mm as fallback when precipitation_probability_max
is null but actual rainfall is expected (Open-Meteo omits probability for
some forecast days even when rain is shown)
- Pass precip_mm through weather service → frontend type definitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch precipitation_probability_max from Open-Meteo (replaces precip_sum
in the card display — probability is more useful at a glance than mm)
- Show WMO condition description text on each forecast day
- Convert wind speed to mph when temp unit is F; pass wind_unit in response
- Display 💧 X% chance of rain; 💨 X mph/km/h wind per day
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move voice status into settings store (voiceSttReady, voiceTtsReady),
checked once at login and refreshed after admin model reload
- ChatView, BriefingView, DashboardChatInput now use computed refs from
the store — mic buttons appear reactively without needing a page reload
- BriefingView: separate STT-only guard for mic PTT vs TTS-only guard for
listen mode / speak buttons
- Add ▶ Preview button to Voice & Speed section in Settings for single-
voice testing without enabling blend mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WeatherCard: show precipitation (mm) and max wind speed per forecast day
- DashboardChatInput: add PTT mic button (transcribe-to-input, voice-gated)
- Remove global VoiceOverlay floating button and Space PTT shortcut from
App.vue — inline mic buttons in chat/briefing/dashboard are the right UX;
global overlay had focus/latency/context issues
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pressing push-to-talk now immediately stops any ongoing TTS audio before
opening the microphone, preventing the assistant from hearing itself.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maps WMO condition strings to emoji icons for current conditions and
forecast days. No external dependencies — pure emoji lookup by condition text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add 'cancelled' status to TaskStatus type, StatusBadge, TaskCard,
TaskEditorView, TaskViewerView, TasksListView
- Add RecurrenceEditor component (none / interval / calendar rules)
- TaskEditorView: wire RecurrenceEditor, show started_at/completed_at
timestamps read-only, include recurrence_rule in save payload
- TaskViewerView: show recurrence summary, timestamps in meta row
- tasks.ts: statusFilter/priorityFilter as arrays, add recurrence_rule
to updateTask and createTask payloads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>