π "Add to Calendar" Button β UX Specification
Author: Daedalus π¨
Director: Matt
Date: 2026-04-28
Status: Ready for Socrates implementation
Depends on: Icarus Telegram bot, calendar_sync.py, appointment_parser.py
1. Context & Problem
When Icarus processes a document (photo, PDF, email) and detects an appointment or event, the briefing card currently just tells the user about it. There's no way to act on that information from within Telegram.
The ask: Add an "Add to Calendar" button to briefing cards that contain appointment/event data, with a confirmation flow that shows exactly what was added and offers undo.
2. Trigger Conditions
The calendar button row appears on a briefing message only when the pipeline extracts appointment-eligible data:
| Condition | Show Button? |
|---|---|
category is "appointment" or "event" |
β Yes |
key_details contains date or start field |
β Yes |
suggested_actions contains "Add to calendar" or calendar-adjacent verb |
β Yes |
category is "info" with no date |
β No |
category is "reminder" with no date |
β No |
Logic: If the LLM produces a parsed appointment (via appointment_parser.py) with a valid start datetime, the button row is rendered. If no appointment can be constructed (missing date, ambiguous time), the button row is omitted and the briefing falls back to text-only.
3. Briefing Card Layout (Updated)
Before (text-only):
π *Dental Cleaning*
Sullivan's dental cleaning appointment at West Side Dental.
*Key Details:*
β’ Date: May 3, 2026
β’ Time: 2:00 PM
β’ Location: West Side Dental
*Confidence: 95%*
After (with action buttons):
π *Dental Cleaning*
Sullivan's dental cleaning appointment at West Side Dental.
*Key Details:*
β’ Date: May 3, 2026
β’ Time: 2:00 PM
β’ Location: West Side Dental
*Confidence: 95%*
[ποΈ Add to Calendar] [βοΈ Edit Details]
Layout Rules
| Element | Specification |
|---|---|
| Button row | Single row, two buttons |
| Primary button | ποΈ Add to Calendar β leftmost, always present when triggered |
| Secondary button | βοΈ Edit Details β rightmost, present when editable fields exist |
| Message format | MarkdownV2 (consistent with recipe toggle) |
| Keyboard type | InlineKeyboardMarkup |
| Maximum buttons | 2 per row (Telegram mobile UX) |
4. Button States & Callback Data
Callback Data Format
calendar:add:{briefing_id} β Add extracted appointment to Google Calendar
calendar:edit:{briefing_id} β Open edit flow (future phase)
calendar:delete:{event_id} β Remove event from calendar (undo)
calendar:done:{briefing_id} β Dismiss confirmation card
Where {briefing_id} is a unique ID generated from the briefing (hash of summary + date), and {event_id} is the Google Calendar event ID returned after insertion.
State Machine
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INITIAL STATE β
β β
β Briefing card with inline keyboard: β
β [ποΈ Add to Calendar] [βοΈ Edit Details] β
β β
β User taps "Add to Calendar" β
β β β
β βΌ β
β LOADING STATE β
β Message edits to: β
β "β³ Adding to calendar..." β
β Keyboard: [β³ Adding...] (single disabled-style btn) β
β β
β β β
β βββ success βββ SUCCESS STATE β
β βββ error βββββ ERROR STATE β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
5. Confirmation Card (Success State)
After the event is successfully added to Google Calendar, the message is edited in place (same chat message, same message_id) to show:
β
*Added to Calendar*
_Dental Cleaning_
May 3, 2026 at 2:00 PM
West Side Dental
[βοΈ Edit Event] [ποΈ Delete]
Specification
| Element | Format |
|---|---|
| Header | β
*Added to Calendar* |
| Event title | _Title_ (italic in MarkdownV2) |
| Date/time | Single line: May 3, 2026 at 2:00 PM |
| Location | Below date (omitted if empty) |
| Button row 1 | [βοΈ Edit Event] [ποΈ Delete] |
| No "Done" button | Telegram inline keyboards auto-dismiss after timeout or user scrolls away |
Design rationale: Showing the exact event details (not just "Success!") builds trust. The user can verify at a glance that the right date, time, and location were captured. The Delete button provides immediate undo β no need to open Google Calendar.
Message Text Template (MarkdownV2)
β
*Added to Calendar*
_{title}*
{date\_formatted} at {time\_formatted}
{location\_line}
_Confidence: {confidence}%_
Where:
- {title} = appointment summary (escaped for MarkdownV2)
- {date_formatted} = human-readable date (e.g., "May 3, 2026")
- {time_formatted} = human-readable time (e.g., "2:00 PM")
- {location_line} = location if present, empty line otherwise
- {confidence} = extraction confidence percentage
Location line: Only rendered if location is non-empty:
π {location} β with pin emoji if location exists
Inline Keyboard (Success State)
keyboard = {
"inline_keyboard": [
[
{"text": "βοΈ Edit Event", "callback_data": f"calendar:edit:{event_id}"},
{"text": "ποΈ Delete", "callback_data": f"calendar:delete:{event_id}"},
]
]
}
6. Delete Confirmation (Undo Flow)
When the user taps ποΈ Delete:
Step 1: Confirm intent
The message edits to show a deletion confirmation:
β οΈ *Delete Event?*
_Dental Cleaning_
May 3, 2026 at 2:00 PM
West Side Dental
This will remove the event from your calendar.
[β
Yes, Delete] [β Cancel]
Step 2: On confirm
Message edits to:
ποΈ *Event Deleted*
_Dental Cleaning_ has been removed from the calendar.
(No inline keyboard β final state.)
Step 2: On cancel
Message reverts to the Success State card (Section 5).
Callback Data
calendar:confirm_delete:{event_id} β Confirm deletion
calendar:cancel_delete:{briefing_id} β Cancel, revert to success state
7. Error State
If calendar insertion fails (API error, auth issue, network timeout):
β *Could Not Add to Calendar*
_Dental Cleaning_
May 3, 2026 at 2:00 PM
Error: {user_friendly_error_message}
[π Retry] [π Copy Details]
| Element | Specification |
|---|---|
| Header | β *Could Not Add to Calendar* |
| Event details | Title, date, time (so user can manually add) |
| Error message | User-friendly, NOT a stack trace |
| Retry button | π Retry β re-attempts the same calendar insertion |
| Copy button | π Copy Details β sends the event details as plain text in a new message so user can paste into Google Calendar manually |
Error Categories & Messages
| Error | User Message |
|---|---|
| Google API 401/403 | "Calendar permission denied. Check that the service account has edit access." |
| Google API 409 (conflict) | "This event conflicts with an existing calendar event." |
| Google API 429 (rate limit) | "Calendar is busy. Try again in a moment." |
| Network timeout | "Couldn't reach Google Calendar. Check your connection." |
| Generic/unknown | "Something went wrong adding this event." |
Callback Data (Error State)
calendar:retry:{briefing_id} β Re-attempt calendar insertion
calendar:copy_details:{briefing_id} β Send event details as plain text message
8. Edit Flow (Future Phase β Stub)
The βοΈ Edit Event button on the confirmation card and βοΈ Edit Details on the initial card are stubs for a future phase. For now:
Initial card βοΈ Edit Details:
- Callback: calendar:edit:{briefing_id}
- Response: answerCallbackQuery("Edit flow coming soon!") with toast text
- No message edit β button just shows a temporary toast
Confirmation card βοΈ Edit Event:
- Same stub behavior
Future implementation will allow inline editing of:
- Date/time
- Title
- Location
- Duration
This is deliberately deferred. The primary action (Add to Calendar) is the MVP.
9. Loading State
When the user taps ποΈ Add to Calendar, the message immediately edits to show a loading state while the backend processes:
β³ *Adding to Calendar\.\.\.*
_Dental Cleaning_
May 3, 2026 at 2:00 PM
West Side Dental
Inline keyboard during loading:
keyboard = {
"inline_keyboard": [
[
{"text": "β³ Adding...", "callback_data": "calendar:noop"}
]
]
}
Design rationale:
- Immediate visual feedback that something is happening
- The β³ Adding... button is non-functional (callback_data is noop) β prevents double-taps
- Message is edited in-place (same message_id) β no new message spam
- If the operation takes >5 seconds, the text remains as-is (no timeout message needed β the Telegram API call will either succeed or fail)
Transition: On success, the same message is edited again to the Success State. On error, edited to Error State. No intermediate "new" messages.
10. Data Flow (Sequence)
User sends document/photo to Icarus bot
β
βΌ
Vision pipeline processes document
β
βΌ
Briefing generated with extracted appointment data
β
βββ Has appointment data? ββYESβββΊ Build briefing + inline keyboard
β β
β βΌ
β Send message with:
β - MarkdownV2 briefing text
β - InlineKeyboardMarkup with calendar buttons
β
βββ No appointment data βββΊ Send text-only briefing (no keyboard)
User taps [ποΈ Add to Calendar]
β
βΌ
Callback: calendar:add:{briefing_id}
β
βΌ
1. Edit message β Loading state (β³)
2. answerCallbackQuery() (no toast β visual edit is sufficient)
β
βΌ
3. Call calendar_sync.create_event() with extracted appointment data
β
βββ SUCCESS βββΊ Edit message β Success card with [βοΈ Edit] [ποΈ Delete]
β answerCallbackQuery("β
Added!")
β
βββ ERROR βββββΊ Edit message β Error card with [π Retry] [π Copy]
answerCallbackQuery("β Failed to add")
11. Appointment Data Extraction
The calendar button needs structured appointment data. The appointment_parser.py module already extracts this. The briefing card must carry this data alongside the rendered text.
Briefing Response Schema (Extended)
The /vision/briefing API response (and the internal briefing dict) must include:
{
"title": "Dental Cleaning",
"summary": "Sullivan's dental cleaning appointment...",
"key_details": {
"date": "2026-05-03",
"time": "2:00 PM",
"location": "West Side Dental",
"contact": "(920) 555-0123",
},
"suggested_actions": ["Sign permission slip"],
"confidence": 0.95,
"category": "appointment",
# NEW: Structured appointment data (null if not an appointment)
"appointment": {
"summary": "Dental Cleaning",
"start": "2026-05-03T14:00:00-05:00", # ISO 8601, Chicago timezone
"end": "2026-05-03T14:30:00-05:00", # ISO 8601, Chicago timezone
"location": "West Side Dental",
"description": "Sullivan's dental cleaning appointment. Source: photo sent via Telegram.",
"who": ["sully"],
},
# NEW: Unique ID for callback data (deterministic hash)
"briefing_id": "dental-cleaning-20260503-ab3f"
}
briefing_id Generation
import hashlib
def generate_briefing_id(summary: str, start_iso: str) -> str:
"""Generate a deterministic, URL-safe briefing ID."""
raw = f"{summary}|{start_iso}".lower().strip()
hash_part = hashlib.sha256(raw.encode()).hexdigest()[:6]
slug = summary.lower().replace(" ", "-")[:20]
return f"{slug}-{hash_part}"
This ensures:
- Same document β same briefing_id (idempotent)
- Different appointments β different IDs (no collision)
- URL-safe for callback_data (max 64 bytes per Telegram spec)
What Socrates Needs to Implement
- Extend the briefing pipeline to extract structured appointment data alongside the text briefing when
category == "appointment"or"event". - Add
briefing_idto the briefing response. - Create a temporary state store (SQLite, like
recipe_toggle) to mapbriefing_idβ appointment data, so the callback handler can reconstruct the event for calendar insertion. - Implement callback handlers for
calendar:add,calendar:delete,calendar:confirm_delete,calendar:cancel_delete,calendar:retry,calendar:noop, andcalendar:copy_details. - Wire up the Telegram handler to render inline keyboards when appointment data is present.
12. SQLite Schema (Calendar State)
CREATE TABLE calendar_pending (
briefing_id TEXT PRIMARY KEY,
chat_id INTEGER NOT NULL,
message_id INTEGER NOT NULL,
summary TEXT NOT NULL,
start_iso TEXT NOT NULL, -- ISO 8601 datetime (Chicago timezone)
end_iso TEXT NOT NULL, -- ISO 8601 datetime (Chicago timezone)
location TEXT DEFAULT '',
description TEXT DEFAULT '',
who TEXT DEFAULT '[]', -- JSON array of family member IDs
source TEXT DEFAULT '', -- "photo", "pdf", "email"
confidence REAL DEFAULT 0.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE calendar_events (
event_id TEXT PRIMARY KEY, -- Google Calendar event ID
briefing_id TEXT NOT NULL,
chat_id INTEGER NOT NULL,
message_id INTEGER NOT NULL, -- The confirmation message ID
summary TEXT NOT NULL,
start_iso TEXT NOT NULL,
added_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
calendar_pending: Stored when briefing is sent, used bycalendar:addcallback to create the event.calendar_events: Stored after successful insertion, used bycalendar:deletecallback to find and delete the event.- TTL:
calendar_pendingrows expire after 24 hours (stale briefings).calendar_eventsrows persist for undo capability.
13. Callback Handler Routing
Extend the existing process_update callback routing in handler.py:
# Current routing
if cb_data.startswith("toggle:"):
await handle_toggle(callback, bot)
elif cb_data.startswith("commit:"):
await handle_commit(callback, bot)
elif cb_data.startswith("cancel:"):
await handle_cancel(callback, bot)
elif cb_data == "clear:groceries":
await handle_clear_groceries(callback, bot)
# NEW: Calendar action routing
elif cb_data.startswith("calendar:add:"):
await handle_calendar_add(callback, bot)
elif cb_data.startswith("calendar:delete:"):
await handle_calendar_delete(callback, bot)
elif cb_data.startswith("calendar:confirm_delete:"):
await handle_calendar_confirm_delete(callback, bot)
elif cb_data.startswith("calendar:cancel_delete:"):
await handle_calendar_cancel_delete(callback, bot)
elif cb_data.startswith("calendar:retry:"):
await handle_calendar_retry(callback, bot)
elif cb_data.startswith("calendar:copy_details:"):
await handle_calendar_copy_details(callback, bot)
elif cb_data.startswith("calendar:edit:"):
await handle_calendar_edit(callback, bot) # stub
elif cb_data == "calendar:noop":
await bot.answer_callback_query(callback["id"], "")
14. Visual Design Tokens
Button Styling (Telegram Limitations)
Telegram does not support custom colors or fonts on inline keyboard buttons. All styling is achieved through emoji + text composition:
| Button | Text | Emoji | Visual Weight |
|---|---|---|---|
| Primary action | ποΈ Add to Calendar |
ποΈ | High β emoji draws eye |
| Secondary action | βοΈ Edit Details |
βοΈ | Low β muted, secondary |
| Loading state | β³ Adding... |
β³ | Transitional β clear wait state |
| Success edit | βοΈ Edit Event |
βοΈ | Low β secondary action |
| Success delete | ποΈ Delete |
ποΈ | Medium β destructive but accessible |
| Confirm delete | β
Yes, Delete |
β | High β requires explicit confirmation |
| Cancel delete | β Cancel |
β | Medium β escape hatch |
| Retry | π Retry |
π | High β error recovery |
| Copy details | π Copy Details |
π | Low β fallback action |
Message Card Typography
| Element | Format | Example |
|---|---|---|
| Status header | β
*Bold* |
β
*Added to Calendar* |
| Event title | _Italic_ |
_Dental Cleaning_ |
| Date/time | Plain text | May 3, 2026 at 2:00 PM |
| Location | π {location} |
π West Side Dental |
| Confidence | _Italic_ (muted) |
_Confidence: 95%_ |
| Error text | β *Bold* |
β *Could Not Add to Calendar* |
Emoji Palette (consistent with Icarus existing patterns)
| Concept | Emoji | Usage |
|---|---|---|
| Calendar | ποΈ | Primary action button |
| Success | β | Confirmation headers, success states |
| Loading | β³ | In-progress states |
| Error | β | Error headers, destructive actions |
| Edit | βοΈ | Edit buttons |
| Delete | ποΈ | Delete buttons |
| Location | π | Location pin (in message body) |
| Document | π | Briefing header, copy details |
| Warning | β οΈ | Confirmation prompts |
15. Edge Cases
| Case | Behavior |
|---|---|
| Briefing has date but no time | Default to all-day event. Show "All day" in confirmation card. |
| Briefing has ambiguous date | Do not show calendar button. Fall back to text-only briefing. |
| Briefing has date + time but no end time | Default to 1-hour duration. Show time as "2:00 PM β 3:00 PM (1 hr)" in confirmation. |
| Briefing has multiple appointments | Show calendar button. On tap, create ALL events. Confirmation card lists all events. |
| Calendar API is unreachable | Show error state with retry option. |
| User taps "Add" on a stale briefing (>24h) | answerCallbackQuery("This briefing has expired. Send the document again.") + edit message to show expired state. |
| Event already exists (duplicate) | Detect via calendar_events table or Google Calendar search. Show: βΉοΈ This event is already in your calendar. with [βοΈ Edit] [ποΈ Delete] buttons. |
| User taps "Add" while already loading | Second tap is ignored (callback_data is calendar:noop during loading). |
| Multi-who appointment | Create single event, add all who members to description. Don't create separate events per person. |
16. Animation Spec
Telegram inline keyboard buttons don't support custom animations. The "animation" is achieved through message edits (same message_id, updated text + keyboard):
State 1 (Initial):
Briefing text + [ποΈ Add to Calendar] [βοΈ Edit Details]
β¬οΈ user taps "Add to Calendar"
State 2 (Loading):
"β³ Adding to Calendar..." text + [β³ Adding...]
β¬οΈ backend returns success (typically 1-3 seconds)
State 3 (Success):
"β
Added to Calendar" + event details + [βοΈ Edit Event] [ποΈ Delete]
β¬οΈ optional: user taps "Delete"
State 4 (Confirm Delete):
"β οΈ Delete Event?" + event details + [β
Yes, Delete] [β Cancel]
β¬οΈ user taps "Yes, Delete"
State 5 (Deleted):
"ποΈ Event Deleted" + title (no keyboard, final state)
Timing constraints:
- Loading β Success transition: typically 1-3 seconds (Google Calendar API latency)
- Callback query acknowledgment: immediate (answerCallbackQuery called within 200ms)
- Message edit: as soon as calendar API responds (no artificial delay)
17. Message Templates (MarkdownV2)
All text must be properly escaped using the escape_markdown() function from recipe_toggle.py (reused for consistency).
Initial Briefing Card (with calendar buttons)
def build_briefing_with_calendar(briefing: dict, briefing_id: str) -> tuple[str, dict]:
"""Build briefing message text + inline keyboard for appointment briefings."""
title = escape_markdown(briefing["title"])
summary = escape_markdown(briefing["summary"])
confidence = int(briefing.get("confidence", 0.5) * 100)
lines = [
f"π *{title}*",
"",
summary,
"",
]
# Key details
key_details = briefing.get("key_details", {})
if key_details:
for key, value in key_details.items():
if value:
label = escape_markdown(key.replace("_", " ").title())
val = escape_markdown(str(value))
lines.append(f"β’ {label}: {val}")
lines.append("")
# Confidence
lines.append(f"_Confidence: {confidence}%_")
# Keyboard
keyboard = {
"inline_keyboard": [
[
{"text": "ποΈ Add to Calendar", "callback_data": f"calendar:add:{briefing_id}"},
{"text": "βοΈ Edit Details", "callback_data": f"calendar:edit:{briefing_id}"},
]
]
}
return "\n".join(lines), keyboard
Success Confirmation Card
def build_calendar_success(event: dict, event_id: str) -> tuple[str, dict]:
"""Build success confirmation message + keyboard."""
summary = escape_markdown(event["summary"])
date_str = escape_markdown(event["date_formatted"]) # "May 3, 2026"
time_str = escape_markdown(event["time_formatted"]) # "2:00 PM"
location = escape_markdown(event.get("location", ""))
confidence = event.get("confidence", 0)
lines = [
f"β
*Added to Calendar*",
"",
f"_{summary}_",
f"{date_str} at {time_str}",
]
if location:
lines.append(f"π {location}")
lines.append("")
lines.append(f"_Confidence: {confidence}%_")
keyboard = {
"inline_keyboard": [
[
{"text": "βοΈ Edit Event", "callback_data": f"calendar:edit:{event_id}"},
{"text": "ποΈ Delete", "callback_data": f"calendar:delete:{event_id}"},
]
]
}
return "\n".join(lines), keyboard
Error Card
def build_calendar_error(briefing_id: str, event: dict, error_msg: str) -> tuple[str, dict]:
"""Build error message + retry/copy keyboard."""
summary = escape_markdown(event["summary"])
date_str = escape_markdown(event["date_formatted"])
time_str = escape_markdown(event["time_formatted"])
error = escape_markdown(error_msg)
lines = [
f"β *Could Not Add to Calendar*",
"",
f"_{summary}_",
f"{date_str} at {time_str}",
"",
f"Error: {error}",
]
keyboard = {
"inline_keyboard": [
[
{"text": "π Retry", "callback_data": f"calendar:retry:{briefing_id}"},
{"text": "π Copy Details", "callback_data": f"calendar:copy_details:{briefing_id}"},
]
]
}
return "\n".join(lines), keyboard
Delete Confirmation Card
def build_delete_confirmation(event: dict, event_id: str) -> tuple[str, dict]:
"""Build delete confirmation message + keyboard."""
summary = escape_markdown(event["summary"])
date_str = escape_markdown(event["date_formatted"])
time_str = escape_markdown(event["time_formatted"])
location = escape_markdown(event.get("location", ""))
lines = [
f"β οΈ *Delete Event\\?*",
"",
f"_{summary}_",
f"{date_str} at {time_str}",
]
if location:
lines.append(f"π {location}")
lines.append("")
lines.append("This will remove the event from your calendar\\.")
keyboard = {
"inline_keyboard": [
[
{"text": "β
Yes, Delete", "callback_data": f"calendar:confirm_delete:{event_id}"},
{"text": "β Cancel", "callback_data": f"calendar:cancel_delete:{briefing_id}"},
]
]
}
return "\n".join(lines), keyboard
18. Conflict Detection Integration
When the user taps ποΈ Add to Calendar, the handler should:
- Check for conflicts using
conflict_engine.detect_conflicts()with the new event's time range - If no conflicts β proceed with insertion β Success State
- If conflicts found β show conflict warning before insertion:
β οΈ *Calendar Conflict Detected*
_Dental Cleaning_ (May 3, 2:00 PM β 2:30 PM)
conflicts with:
β’ _Soccer Practice_ (2:00 PM β 3:00 PM)
[ποΈ Add Anyway] [β Cancel]
Callback data:
calendar:add_anyway:{briefing_id} β Force add despite conflict
calendar:cancel:{briefing_id} β Cancel, revert to initial state
This mirrors the existing conflict detection in conflict_engine.py and integrates naturally into the calendar flow.
19. Accessibility Considerations
- All emoji are supplementary β button text is always meaningful without the emoji
- Color is never the sole indicator of state (emoji + text together)
- Loading state uses both text change and button change (not just a spinner)
- Delete confirmation requires explicit two-tap confirmation (no single-tap destructive actions)
- Error messages are actionable (retry or copy), not dead ends
20. Implementation Checklist (for Socrates)
- [ ] Extend
appointment_parser.pyoutput or briefing pipeline to include structuredappointmentdict - [ ] Add
briefing_idgeneration to briefing pipeline - [ ] Create SQLite tables:
calendar_pending,calendar_events - [ ] Store pending appointment data when briefing is sent with calendar button
- [ ] Implement
handle_calendar_add()callback handler - [ ] Implement
handle_calendar_delete()βhandle_calendar_confirm_delete()flow - [ ] Implement
handle_calendar_retry()callback handler - [ ] Implement
handle_calendar_copy_details()callback handler - [ ] Implement
handle_calendar_edit()stub (toast: "Coming soon!") - [ ] Add calendar callback routing to
process_update()inhandler.py - [ ] Add conflict check before calendar insertion
- [ ] Build and test MarkdownV2 message templates
- [ ] Add 24-hour TTL cleanup for
calendar_pendingrows - [ ] Test: appointment with all fields (title, date, time, location)
- [ ] Test: appointment with date but no time (all-day event)
- [ ] Test: appointment with date + time but no end time (default 1 hour)
- [ ] Test: calendar API timeout β error state β retry
- [ ] Test: duplicate event detection
- [ ] Test: conflict detection before insertion
21. File References
| File | What to modify |
|---|---|
icarus/core/telegram/handler.py |
Add calendar callback routing, import new handlers |
icarus/core/handlers/calendar_actions.py |
NEW β All calendar callback handlers |
icarus/core/db/calendar_state.py |
NEW β SQLite CRUD for calendar_pending and calendar_events |
icarus/core/conflict_engine.py |
No changes (reuse detect_conflicts()) |
icarus/core/calendar_sync.py |
No changes (reuse create_event()) |
icarus/core/vision/pipeline.py |
Add appointment dict and briefing_id to response |
This spec is the UX contract. Socrates builds to this, Daedalus reviews the result.
β Daedalus π¨