Files
Panama/docs/superpowers/plans/2026-08-17-calendar-agenda.md
T

34 KiB
Raw Blame History

Calendar Agenda Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build the approved A · Daybook date menu using the workstation's existing Evolution Data Server calendars, with a quiet upcoming-event capsule and GNOME Calendar handoff.

Architecture: A Python/GObject bridge queries enabled EDS calendar sources through EDataServer and ECal, expands recurring instances, and streams normalized JSON snapshots. A Quickshell singleton owns that stream and exposes date/event selectors to a two-column Daybook, while GNOME Calendar remains responsible for accounts and event editing.

Tech Stack: Python 3.14, PyGObject, Evolution Data Server 3.60 (EDataServer, ECal, ICalGLib), Quickshell 0.3/QML, Gio D-Bus, Bash contract tests, jq, Hyprland 0.56

Spec: docs/superpowers/specs/2026-08-17-calendar-agenda-design.md

Global Constraints

  • Use only enabled EDS event sources; never read authentication extensions or credential stores.
  • GNOME Calendar owns account configuration and event editing.
  • Live event content must never be written to logs, settings, test fixtures, screenshots committed to the repository, or documentation.
  • The bridge may emit event content only to its Quickshell consumer over stdout.
  • Descriptions and attendees never enter the bridge output; descriptions may only be inspected to locate an allowed HTTPS meeting URL.
  • Allowed meeting providers are Google Meet, Zoom, Microsoft Teams, Webex, and Jitsi.
  • Calendar failures must not break notifications, media, weather, or the static month grid.
  • The bar capsule appears from 15 minutes before through 5 minutes after a timed event starts; all-day events never produce it.
  • No calendar toasts, sounds, pulse, shimmer, or continuously repainting animation.
  • No test may create, modify, or delete a real calendar event.
  • Wi-Fi QR sharing is removed from Panama's roadmap and package guidance.

File map

Calendar data boundary

  • config/dot/quickshell/scripts/calendar-agenda — executable Python bridge; the only code that imports EDS libraries.
  • config/dot/quickshell/services/CalendarAgenda.qml — singleton state consumed by shell UI; supervises the bridge and owns date-derived selectors.
  • tests/quickshell/calendar_agenda_bridge_test.py — pure normalization and URL-policy tests using synthetic components only.
  • tests/quickshell/calendar-agenda-helper-contract.sh — executable/probe/live-query contract that redacts live content.
  • tests/quickshell/calendar-agenda-contract.sh — live Quickshell IPC/state contract using synthetic in-memory fixtures.

Daybook presentation

  • config/dot/quickshell/modules/datemenu/AgendaEvent.qml — one event row.
  • config/dot/quickshell/modules/datemenu/AgendaPane.qml — Up Next, all-day section, timed agenda, empty/error states, and notification summary.
  • config/dot/quickshell/modules/datemenu/NotificationSummary.qml — quiet bridge from Agenda to the existing grouped notification list.
  • config/dot/quickshell/modules/datemenu/DateMenuPanel.qml — approved two-column layout and agenda/notification routing.
  • config/dot/quickshell/modules/datemenu/CalendarGrid.qml — date selection, adjacent-month navigation, and event dots.
  • config/dot/quickshell/modules/datemenu/DateMenu.qml — panel sizing, preparation, and notification read-state behavior.
  • config/dot/quickshell/modules/datemenu/qmldir — explicit registrations for new components.

Shell and bar integration

  • config/dot/quickshell/services/ShellState.qmldateMenuPage and explicit Agenda/Notifications open semantics.
  • config/dot/quickshell/modules/bar/Clock.qml — clock opens Agenda rather than an undifferentiated notification overlay.
  • config/dot/quickshell/modules/bar/CalendarIndicator.qml — quiet 15-minute capsule.
  • config/dot/quickshell/modules/bar/Bar.qml — mounts the capsule in the right status area.
  • config/dot/quickshell/shell.qml — calendar IPC fixture/diagnostics surface and notification-page routing.

Documentation

  • config/dot/hypr/DESKTOP-PARITY.md — calendar becomes Live and Wi-Fi QR is removed.
  • config/dot/hypr/README.md — EDS/GNOME Calendar handoff and capsule behavior.
  • setup/packages/hyprland-packages — remove qrencode if it is present only for the abandoned Wi-Fi QR feature.

Task 1: EDS calendar bridge

Files:

  • Create: config/dot/quickshell/scripts/calendar-agenda
  • Create: tests/quickshell/calendar_agenda_bridge_test.py
  • Create: tests/quickshell/calendar-agenda-helper-contract.sh

Interfaces:

  • Consumes: enabled sources returned by EDataServer.SourceRegistry.list_enabled(EDataServer.SOURCE_EXTENSION_CALENDAR).

  • Produces: calendar-agenda probe, calendar-agenda query START END, and calendar-agenda watch START END.

  • Produces: one-line JSON snapshots with ok, generatedAt, sources, events, and errors.

  • Produces event objects with exactly id, sourceId, uid, summary, start, end, allDay, location, and joinUrl.

  • Step 1: Write failing pure bridge tests

Create tests/quickshell/calendar_agenda_bridge_test.py with import-by-path and synthetic policy cases:

import importlib.machinery
import importlib.util
import pathlib
import unittest
from unittest.mock import Mock

ROOT = pathlib.Path(__file__).resolve().parents[2]
HELPER = ROOT / "config/dot/quickshell/scripts/calendar-agenda"

loader = importlib.machinery.SourceFileLoader("calendar_agenda", str(HELPER))
spec = importlib.util.spec_from_loader(loader.name, loader)
calendar_agenda = importlib.util.module_from_spec(spec)
loader.exec_module(calendar_agenda)


class CalendarAgendaBridgeTest(unittest.TestCase):
    def test_meeting_url_allowlist(self):
        values = ["Planning: https://meet.google.com/abc-defg-hij"]
        self.assertEqual(calendar_agenda.find_join_url(values), values[0].split()[-1])
        self.assertEqual(calendar_agenda.find_join_url(["https://example.com/private"]), "")
        self.assertEqual(calendar_agenda.find_join_url(["http://zoom.us/j/123"]), "")

    def test_stable_instance_id_includes_recurrence(self):
        self.assertEqual(
            calendar_agenda.stable_event_id("source", "uid", "20260817T133000Z"),
            "source:uid:20260817T133000Z",
        )

    def test_snapshot_sort_order(self):
        events = [
            {"id": "b", "start": 20, "end": 30, "allDay": False},
            {"id": "a", "start": 10, "end": 20, "allDay": True},
        ]
        self.assertEqual([event["id"] for event in calendar_agenda.sort_events(events)], ["a", "b"])

    def test_normalizes_an_all_day_recurrence_instance(self):
        component_id = Mock()
        component_id.get_uid.return_value = "uid"
        component_id.get_rid.return_value = "20260817"
        summary = Mock()
        summary.get_value.return_value = "Synthetic event"
        component = Mock()
        component.get_id.return_value = component_id
        component.get_summary.return_value = summary
        component.get_location.return_value = ""
        component.get_url.return_value = None
        component.get_descriptions.return_value = []
        start = Mock()
        start.as_timet.return_value = 1786924800
        start.is_date.return_value = True
        end = Mock()
        end.as_timet.return_value = 1787011200
        end.is_date.return_value = True

        event = calendar_agenda.normalize_instance(
            {"id": "source", "name": "Synthetic", "color": "#82aaff"},
            component,
            start,
            end,
        )
        self.assertTrue(event["allDay"])
        self.assertEqual(event["id"], "source:uid:20260817")


if __name__ == "__main__":
    unittest.main()
  • Step 2: Run the unit test and verify RED

Run:

python3 tests/quickshell/calendar_agenda_bridge_test.py

Expected: FAIL because config/dot/quickshell/scripts/calendar-agenda does not exist.

  • Step 3: Implement the bridge's pure contract and CLI boundary

Create config/dot/quickshell/scripts/calendar-agenda with a Python shebang and no top-level execution on import. Begin with these pure helpers:

MEETING_HOST_SUFFIXES = (
    "meet.google.com",
    "zoom.us",
    "teams.microsoft.com",
    "teams.live.com",
    "webex.com",
    "meet.jit.si",
)

def stable_event_id(source_id: str, uid: str, recurrence_id: str) -> str:
    return f"{source_id}:{uid}:{recurrence_id}"

def sort_events(events: list[dict[str, object]]) -> list[dict[str, object]]:
    return sorted(events, key=lambda event: (not bool(event["allDay"]), int(event["start"]), str(event["id"])))

def is_allowed_meeting_host(host: str) -> bool:
    normalized = host.lower().rstrip(".")
    return any(normalized == suffix or normalized.endswith("." + suffix) for suffix in MEETING_HOST_SUFFIXES)

find_join_url must parse URLs with urllib.parse.urlsplit, require https, lowercase the hostname, and match either an exact allowed host or a subdomain boundary (host == suffix or host.endswith("." + suffix)). Strip trailing sentence punctuation before parsing.

Then implement these exact public boundaries: find_join_url(values: list[str]) -> str, normalize_instance(source_meta, component, instance_start, instance_end) -> dict[str, object], probe() -> dict[str, object], collect_snapshot(start: int, end: int) -> dict[str, object], watch(start: int, end: int) -> int, and main(argv: list[str]) -> int.

Import GI with exact version requirements:

import gi
gi.require_version("ECal", "2.0")
gi.require_version("EDataServer", "1.2")
from gi.repository import ECal, EDataServer, Gio, GLib

For each enabled source:

client = ECal.Client.connect_sync(source, ECal.ClientSourceType.EVENTS, 5, None)
client.generate_instances_sync(start, end, None, on_instance, None)

The callback signature is (component, instance_start, instance_end, cancellable, callback_data). Convert callback times with as_timet(). Use component.get_id().get_uid() and .get_rid(), component.get_summary().get_value(), component.get_location(), component.get_url(), and component.get_descriptions().

Use source metadata from source.get_uid(), source.get_display_name(), and the calendar extension's get_color(). Fall back to #82aaff only when color is absent or invalid. Catch failures per source and append a generic {sourceId, code: "unavailable"} error without raw exception text.

watch must emit the initial snapshot, subscribe to EDS source-registry changes and GNOME CalendarServer invalidation signals when available, debounce refreshes by 350 ms, and emit a fallback snapshot every 600 seconds. Failure to subscribe to GNOME CalendarServer must not stop periodic EDS reads.

Write each snapshot with json.dumps(snapshot, separators=(",", ":")) + "\n" and immediately flush stdout. Never print event data to stderr.

Make the helper executable:

chmod +x config/dot/quickshell/scripts/calendar-agenda
  • Step 4: Run pure tests and verify GREEN

Run:

python3 tests/quickshell/calendar_agenda_bridge_test.py

Expected: four tests PASS.

  • Step 5: Add the redacted live helper contract

Create tests/quickshell/calendar-agenda-helper-contract.sh with:

#!/usr/bin/env bash
set -euo pipefail

fail() { printf 'calendar agenda helper contract: %s\n' "$1" >&2; exit 1; }
project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$project_root/config/dot/quickshell/scripts/calendar-agenda"

[[ -x "$helper" ]] || fail 'helper is missing or not executable'
probe="$($helper probe)"
jq -e '.eds == true and .sourceRegistry == true and .enabledSources > 0' <<<"$probe" >/dev/null \
    || fail 'EDS readiness probe failed'

range_start="$(date +%s)"
range_end="$((range_start + 1209600))"
snapshot="$($helper query "$range_start" "$range_end")"
jq -e '
  .ok == true and
  (.sources | type == "array" and length > 0) and
  (.events | type == "array") and
  ([.events[] | keys_unsorted - ["id","sourceId","uid","summary","start","end","allDay","location","joinUrl"]] | all(length == 0))
' <<<"$snapshot" >/dev/null || fail 'live snapshot shape is invalid'

printf 'calendar agenda helper contract: PASS (%s sources, %s events; contents redacted)\n' \
  "$(jq '.sources | length' <<<"$snapshot")" "$(jq '.events | length' <<<"$snapshot")"

Make it executable and run it. Expected: PASS with counts only; no source names, summaries, locations, or URLs in terminal output.

  • Step 6: Commit the bridge
git add config/dot/quickshell/scripts/calendar-agenda \
  tests/quickshell/calendar_agenda_bridge_test.py \
  tests/quickshell/calendar-agenda-helper-contract.sh
git commit -m "Add the EDS calendar bridge"

Task 2: Quickshell calendar service and deterministic contract

Files:

  • Create: config/dot/quickshell/services/CalendarAgenda.qml
  • Create: tests/quickshell/calendar-agenda-contract.sh
  • Modify: config/dot/quickshell/shell.qml

Interfaces:

  • Consumes: calendar-agenda watch START END newline-delimited snapshots.

  • Produces: singleton properties phase, available, sources, events, selectedDate, selectedDateKey, selectedEvents, nextEvent, nextEventForSelectedDate, rangeStart, rangeEnd, capsuleVisible, and capsuleText.

  • Produces: functions selectDate(date), selectIsoDate(dateString), setVisibleMonth(year, month), eventsForDate(date), markersForDate(date), openEvent(event), openCalendar(date), join(event), refresh(), applyFixture(name), and clearFixture().

  • Produces IPC target calendar-agenda with fixture, reset, open, select, refresh, and status.

  • Step 1: Write the failing live service contract

Create tests/quickshell/calendar-agenda-contract.sh that first asserts the IPC target, loads fixture daybook, and validates only synthetic state:

#!/usr/bin/env bash
set -euo pipefail

fail() { printf 'calendar agenda contract: %s\n' "$1" >&2; exit 1; }
cleanup() {
  qs ipc call calendar-agenda reset >/dev/null 2>&1 || true
  qs ipc call calendar-agenda close >/dev/null 2>&1 || true
}
trap cleanup EXIT

qs ipc show | rg -q '^target calendar-agenda$' || fail 'calendar IPC target is missing'
qs ipc call calendar-agenda fixture daybook >/dev/null
status="$(qs ipc call calendar-agenda status)"
jq -e '.fixture == true and .sourceCount == 3 and .eventCount == 5 and .selectedDate == "2026-08-17"' \
  <<<"$status" >/dev/null || fail 'daybook fixture state is malformed'

qs ipc call calendar-agenda select 2026-08-18 >/dev/null
jq -e '.selectedDate == "2026-08-18" and .selectedEventCount == 1' \
  <<<"$(qs ipc call calendar-agenda status)" >/dev/null || fail 'date selection did not update events'

qs ipc call calendar-agenda reset >/dev/null
jq -e '.fixture == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
  || fail 'fixture reset did not restore live mode'
printf 'calendar agenda contract: PASS\n'
  • Step 2: Run the contract and verify RED

Run tests/quickshell/calendar-agenda-contract.sh.

Expected: FAIL with calendar IPC target is missing.

  • Step 3: Implement CalendarAgenda.qml stream supervision

Create the singleton with pragma Singleton, Quickshell, Quickshell.Io, and QtQuick imports. Use a streaming parser, not StdioCollector:

Process {
    id: watchProc
    command: [root.helperPath, "watch", String(root.rangeStart), String(root.rangeEnd)]
    running: !root.fixtureMode && root.rangeStart > 0
    stdout: SplitParser {
        onRead: data => root.consumeSnapshot(data)
    }
    onExited: (code, status) => {
        if (!root.fixtureMode)
            restartTimer.restart();
    }
}

The restart timer uses 2 seconds and does not loop while fixture mode is active. consumeSnapshot(data) parses one line, rejects snapshots without ok === true, assigns arrays atomically, and sets phase to ready or degraded when errors.length > 0.

Implement date keys in local time with Qt.formatDate(new Date(epoch * 1000), "yyyy-MM-dd"). For multi-day/all-day markers, treat end as exclusive and include each local date touched before end - 1.

setVisibleMonth(year, month) computes a range from seven days before the first day through seven days after the final 42-cell grid day. Only restart the bridge when the requested range falls outside the loaded range.

nextEvent excludes all-day and ended events. capsuleVisible is true when start - now is at most 900 seconds and at least -300 seconds. capsuleText is Now after start, otherwise the ceiling minute count plus m.

applyFixture("daybook") must install three synthetic sources and five synthetic events spanning August 1718, 2026. Use clearly synthetic labels (Fixture standup, Fixture planning, Fixture focus, Fixture dinner, Fixture tomorrow) and no real account or event data.

  • Step 4: Add calendar IPC without exposing live content

Add to shell.qml:

IpcHandler {
    target: "calendar-agenda"
    function fixture(name: string): void { CalendarAgenda.applyFixture(name); }
    function reset(): void { CalendarAgenda.clearFixture(); }
    function refresh(): void { CalendarAgenda.refresh(); }
    function open(): void { ShellState.openDateMenu("agenda"); }
    function close(): void { ShellState.close(); }
    function select(date: string): void { CalendarAgenda.selectIsoDate(date); }
    function status(): string {
        return JSON.stringify({
            phase: CalendarAgenda.phase,
            fixture: CalendarAgenda.fixtureMode,
            sourceCount: CalendarAgenda.sources.length,
            eventCount: CalendarAgenda.events.length,
            selectedDate: CalendarAgenda.selectedDateKey,
            selectedEventCount: CalendarAgenda.selectedEvents.length,
            capsuleVisible: CalendarAgenda.capsuleVisible,
            capsuleText: CalendarAgenda.capsuleText
        });
    }
}

Do not include titles, source names, locations, identifiers, or URLs in status output.

  • Step 5: Run service contracts and verify GREEN

Allow Quickshell to hot reload, then run:

tests/quickshell/calendar-agenda-contract.sh
tests/quickshell/calendar-agenda-helper-contract.sh
qs log -n 80 | rg -i 'calendar|error|warn'

Expected: both contracts PASS; no calendar QML error or bridge restart loop.

  • Step 6: Commit the service
git add config/dot/quickshell/services/CalendarAgenda.qml \
  config/dot/quickshell/shell.qml \
  tests/quickshell/calendar-agenda-contract.sh
git commit -m "Add live calendar state to Quickshell"

Task 3: Explicit date-menu routing and interactive month grid

Files:

  • Modify: config/dot/quickshell/services/ShellState.qml
  • Modify: config/dot/quickshell/modules/bar/Clock.qml
  • Modify: config/dot/quickshell/shell.qml
  • Modify: config/dot/quickshell/modules/datemenu/CalendarGrid.qml
  • Modify: tests/quickshell/calendar-agenda-contract.sh

Interfaces:

  • Consumes: CalendarAgenda.selectDate(date) and CalendarAgenda.markersForDate(date).

  • Produces: ShellState.dateMenuPage, ShellState.toggleDateMenu(page), and ShellState.openDateMenu(page).

  • Produces from CalendarGrid: signals dateActivated(date) and visibleMonthChanged(year, month).

  • Step 1: Extend the failing contract with page-routing assertions

Append fixture assertions:

qs ipc call calendar-agenda open >/dev/null
jq -e '.open == true and .page == "agenda"' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
  || fail 'calendar open did not route to Agenda'

qs ipc call notifications open >/dev/null
jq -e '.open == true and .page == "notifications"' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
  || fail 'notification open did not route to Notifications'

Update the calendar status response to include only open: ShellState.notificationsOpen and page: ShellState.dateMenuPage in addition to the non-sensitive fields from Task 2.

  • Step 2: Run the contract and verify RED

Expected: FAIL because dateMenuPage and notification open() do not exist.

  • Step 3: Implement explicit ShellState routing

Add:

property string dateMenuPage: "agenda"

function openDateMenu(page: string): void {
    root.dateMenuPage = page === "notifications" ? "notifications" : "agenda";
    root.activeOverlay = "notifications";
}

function toggleDateMenu(page: string): void {
    const target = page === "notifications" ? "notifications" : "agenda";
    if (root.activeOverlay === "notifications" && root.dateMenuPage === target) {
        root.activeOverlay = "";
        return;
    }
    root.dateMenuPage = target;
    root.activeOverlay = "notifications";
}

Change Clock activation to ShellState.toggleDateMenu("agenda"). Change notification IPC toggle() to ShellState.toggleDateMenu("notifications"), add open() using openDateMenu("notifications"), and leave Super+B routed through that IPC target.

  • Step 4: Make CalendarGrid date-aware and event-aware

Replace numeric-only cell objects with full local Date values and add:

signal dateActivated(date date)
signal visibleMonthChanged(int year, int month)
property date selectedDate: CalendarAgenda.selectedDate

function activateCell(cell): void {
    if (!cell.current) {
        root.viewYear = cell.date.getFullYear();
        root.viewMonth = cell.date.getMonth();
    }
    root.dateActivated(cell.date);
}

Every cell receives markers: CalendarAgenda.markersForDate(cell.date).slice(0, 3). Render dots in a centered row beneath the day number with each marker's source color. Today remains solid accent; selected non-today uses Theme.alpha(Theme.accent, 0.18) plus accent text. Add a hover surface only now that cells are actionable.

Month changes call visibleMonthChanged(viewYear, viewMonth). Adjacent-month date activation changes month and selection in one click.

  • Step 5: Verify routing and grid contracts

Run:

tests/quickshell/calendar-agenda-contract.sh
rg -q 'markersForDate' config/dot/quickshell/modules/datemenu/CalendarGrid.qml
rg -q 'dateActivated' config/dot/quickshell/modules/datemenu/CalendarGrid.qml

Expected: contract PASS and both interaction hooks present.

  • Step 6: Commit routing and grid behavior
git add config/dot/quickshell/services/ShellState.qml \
  config/dot/quickshell/modules/bar/Clock.qml \
  config/dot/quickshell/shell.qml \
  config/dot/quickshell/modules/datemenu/CalendarGrid.qml \
  tests/quickshell/calendar-agenda-contract.sh
git commit -m "Route the interactive Panama date menu"

Task 4: Build the approved A · Daybook panel

Files:

  • Create: config/dot/quickshell/modules/datemenu/AgendaEvent.qml
  • Create: config/dot/quickshell/modules/datemenu/AgendaPane.qml
  • Create: config/dot/quickshell/modules/datemenu/NotificationSummary.qml
  • Modify: config/dot/quickshell/modules/datemenu/DateMenuPanel.qml
  • Modify: config/dot/quickshell/modules/datemenu/DateMenu.qml
  • Modify: config/dot/quickshell/modules/datemenu/MediaCard.qml
  • Modify: config/dot/quickshell/modules/datemenu/qmldir
  • Modify: tests/quickshell/calendar-agenda-contract.sh

Interfaces:

  • Consumes: CalendarAgenda.selectedEvents, nextEventForSelectedDate, phase, and action functions.

  • Consumes: ShellState.dateMenuPage, Notifs.hasNotifications, and existing NotificationList.

  • Produces: a 760-pixel two-column date menu with left width 310 and independently bounded right content.

  • Step 1: Add failing structural and live-surface assertions

Extend the contract to assert the loaded layer and approved components:

hyprctl layers | rg -q 'namespace: qs-popover-datemenu' || fail 'Daybook layer did not map'
rg -q 'implicitWidth: 760' config/dot/quickshell/modules/datemenu/DateMenu.qml \
  || fail 'Daybook is not the approved width'
for component in AgendaPane AgendaEvent NotificationSummary; do
  rg -q "${component} 1.0 ${component}.qml" config/dot/quickshell/modules/datemenu/qmldir \
    || fail "$component is not registered"
done

Run the contract and expect failure on width or missing components.

  • Step 2: Implement focused event and summary components

AgendaEvent.qml accepts property var event, property bool past, and signals activated and joinRequested. It renders a 48-pixel tabular time column, 3-pixel source-color rail, elided summary, source/location metadata, and optional Join pill. All-day rows replace time with All day.

NotificationSummary.qml is visible only when Notifs.hasNotifications; it displays history count and emits activated. It never marks notifications read by itself.

AgendaPane.qml renders exactly one of:

  • loading copy (Loading calendars…);
  • unavailable copy plus Open Calendar;
  • empty-day copy plus Open Calendar;
  • Up Next plus the selected day's ordered list.

The Up Next card is omitted for selected dates other than today and when no future timed event exists. Use ScrollColumn with a maximum content height derived from the panel's available height. Event clicks call CalendarAgenda.openEvent(event); Join calls CalendarAgenda.join(event).

  • Step 3: Recompose DateMenuPanel as Daybook

Keep the existing header styling and Prism container, but replace the single Column with this concrete structure:

Column {
    id: frame
    spacing: 0
    Item {
        id: header
        width: parent.width
        height: 42
    }
    Rectangle { width: parent.width; height: 1; color: Theme.alpha(Theme.fg, 0.1) }
    Row {
        width: parent.width
        Item {
            id: leftColumn
            width: 310
            height: rightColumn.height
        }
        Rectangle {
            width: 1
            height: rightColumn.height
            color: Theme.alpha(Theme.fg, 0.1)
        }
        Item {
            id: rightColumn
            width: frame.width - 311
            height: Math.max(438, activeContent.implicitHeight)
            property Item activeContent: ShellState.dateMenuPage === "agenda" ? agendaPane : notificationList
            AgendaPane { visible: ShellState.dateMenuPage === "agenda" }
            NotificationList { visible: ShellState.dateMenuPage === "notifications" }
        }
    }
}

Populate leftColumn inline in DateMenuPanel.qml with a Column containing CalendarGrid, conditional MediaCard, and WeatherCard. Populate header with the existing date text and pill controls. Bind date activation to CalendarAgenda.selectDate(date) and month changes to CalendarAgenda.setVisibleMonth(year, month).

When Agenda's notification summary activates, set ShellState.dateMenuPage = "notifications" and call Notifs.markAllRead(). Notification view header exposes Back to agenda, DND, and Clear All. Returning to agenda retains selected date and month.

Resize MediaCard typography/controls responsively for the 310-pixel column; do not duplicate its playback logic.

  • Step 4: Update DateMenu lifecycle and bounds

Set DateMenu.implicitWidth: 760. Bound height to the active screen's available geometry, with the panel content choosing a maximum right-column scroll height instead of overflowing the monitor.

On opening:

CalendarAgenda.refresh();
if (ShellState.dateMenuPage === "notifications")
    Notifs.markAllRead();
panel.prepare(ShellState.dateMenuPage);

Add a connection that marks notifications read when an already-open panel switches into Notifications. Opening from the clock must not mark them read.

  • Step 5: Verify fixture visuals on the real layer surface

Run:

qs ipc call calendar-agenda fixture daybook
qs ipc call calendar-agenda open
tests/quickshell/calendar-agenda-contract.sh

Capture a temporary full-screen screenshot with grim, inspect the Daybook at original detail, and verify:

  • no clipped text at 1.5 scale;
  • month and agenda are simultaneously visible;
  • right content has no unexplained empty glass;
  • three source colors remain restrained;
  • notification summary is quieter than Up Next;
  • no continuously repainting animation.

Close the panel and reset fixture mode after inspection. Do not add screenshots to git.

  • Step 6: Commit the Daybook
git add config/dot/quickshell/modules/datemenu \
  tests/quickshell/calendar-agenda-contract.sh
git commit -m "Build the Prism calendar Daybook"

Task 5: Quiet upcoming-event capsule

Files:

  • Create: config/dot/quickshell/modules/bar/CalendarIndicator.qml
  • Modify: config/dot/quickshell/modules/bar/Bar.qml
  • Modify: config/dot/quickshell/services/CalendarAgenda.qml
  • Modify: tests/quickshell/calendar-agenda-contract.sh

Interfaces:

  • Consumes: CalendarAgenda.capsuleVisible, capsuleText, and nextEvent.

  • Produces: a right-side Pill showing calendar glyph plus 12m or Now.

  • Produces: applyFixture("upcoming") and applyFixture("outside-window") for boundary tests.

  • Step 1: Add failing capsule boundary assertions

Append:

qs ipc call calendar-agenda fixture upcoming >/dev/null
jq -e '.capsuleVisible == true and .capsuleText == "12m"' \
  <<<"$(qs ipc call calendar-agenda status)" >/dev/null || fail 'upcoming capsule window is wrong'

qs ipc call calendar-agenda fixture outside-window >/dev/null
jq -e '.capsuleVisible == false' <<<"$(qs ipc call calendar-agenda status)" >/dev/null \
  || fail 'capsule appears outside the quiet window'

Run and expect RED because those fixtures do not exist.

  • Step 2: Implement deterministic timing fixtures and capsule state

Add fixtureNow to CalendarAgenda. Production uses Date.now() / 1000; fixture modes use their explicit clock. upcoming creates one timed event exactly 12 minutes ahead. outside-window creates one 20 minutes ahead plus an all-day event. Verify all-day never wins nextEvent.

Use a one-minute Timer only while events are loaded to update relative time. Midnight refresh remains separate. There is no sub-minute repaint.

  • Step 3: Build CalendarIndicator and mount it in Bar

Use Pill with visible: CalendarAgenda.capsuleVisible, horizontal padding 8, a calendar Nerd Font glyph, and tabular relative text. onActivated opens Agenda. Import QtQuick.Controls and use ToolTip.visible: root.hovered && root.visible with the next event's title; the title must not otherwise appear in the bar.

Mount the indicator at the beginning of Bar's right row, before ActivityIndicator, so it does not shift the centered clock.

Use a single 120 ms opacity/width entrance tied to visibility. No looping animation.

  • Step 4: Run capsule and full shell contracts
tests/quickshell/calendar-agenda-contract.sh
for test in tests/quickshell/*.sh; do "$test"; done

Expected: calendar fixture boundaries PASS and every existing Quickshell contract remains green.

  • Step 5: Commit the capsule
git add config/dot/quickshell/modules/bar/CalendarIndicator.qml \
  config/dot/quickshell/modules/bar/Bar.qml \
  config/dot/quickshell/services/CalendarAgenda.qml \
  tests/quickshell/calendar-agenda-contract.sh
git commit -m "Add the quiet calendar capsule"

Task 6: Documentation, live EDS verification, and delivery

Files:

  • Modify: config/dot/hypr/DESKTOP-PARITY.md
  • Modify: config/dot/hypr/README.md
  • Modify: setup/packages/hyprland-packages
  • Modify: docs/superpowers/plans/2026-08-17-calendar-agenda.md

Interfaces:

  • Consumes: completed helper, service, Daybook, capsule, and test suite.

  • Produces: documented live behavior and a pushed sequence of reviewed commits.

  • Step 1: Update parity and operator documentation

In DESKTOP-PARITY:

  • change Calendar Agenda to Live with EDS/Google/iCloud/Nextcloud wording;
  • remove Wi-Fi QR from remaining work;
  • remove qrencode from the privileged package command;
  • keep KDE Connect, Home Assistant, printer UI, and RustDesk lock-screen validation as future work.

In Hyprland README document:

  • clock opens Agenda;
  • Super+B opens Notifications inside the same Daybook;
  • event clicks hand off to GNOME Calendar;
  • account management remains in GNOME Calendar/Online Accounts;
  • the bar capsule's 15-minute to +5-minute window.

Remove qrencode from setup/packages/hyprland-packages only if rg -n qrencode . confirms no remaining runtime consumer.

  • Step 2: Run static verification
bash -n tests/quickshell/*.sh config/dot/quickshell/scripts/screen-intelligence
python3 -c 'compile(open("config/dot/quickshell/scripts/calendar-agenda", encoding="utf-8").read(), "calendar-agenda", "exec")'
python3 tests/quickshell/calendar_agenda_bridge_test.py
desktop-file-validate config/local/share/applications/*.desktop
Hyprland --verify-config
git diff --check

Expected: every command exits 0 and Hyprland reports config ok.

  • Step 3: Run every Quickshell contract
for test in tests/quickshell/*.sh; do
  "$test"
done

Expected: all prior contracts plus both calendar contracts report PASS.

  • Step 4: Verify the live calendar without leaking content

Reset fixture mode and refresh:

qs ipc call calendar-agenda reset
qs ipc call calendar-agenda refresh

Poll status until phase is ready or degraded. Assert sourceCount > 0; report only source/event counts. Open Agenda from the clock path, inspect the real panel locally, test one date containing events, and confirm event clicks launch GNOME Calendar. Do not capture or print a screenshot containing real event details.

Test a Join action only when a recognized URL is already present; opening that URL is an external navigation and requires an explicit click in the local UI. Do not fabricate or modify an event for testing.

  • Step 5: Review logs and the complete diff
qs log -n 250 | rg -i 'calendar|qml|error|warn'
git status --short
git diff --stat 5248883..HEAD
git diff --check 5248883..HEAD

Expected: no calendar QML errors, no bridge restart loop, no secrets, and only calendar/planned-roadmap changes beyond the already-approved desktop snapshot.

  • Step 6: Mark this plan complete and commit documentation

Mark completed checkboxes only after their commands actually passed, then:

git add config/dot/hypr/DESKTOP-PARITY.md \
  config/dot/hypr/README.md \
  setup/packages/hyprland-packages \
  docs/superpowers/plans/2026-08-17-calendar-agenda.md
git commit -m "Document the Panama calendar agenda"
  • Step 7: Push without rewriting history
git status --short
git push origin main

Expected: clean worktree and main advances on origin. If Gitea still returns HTTP 502, preserve all commits locally, report the remote outage, and do not force-push or change remote history.