Skip to content

feat(calendar): change repeating events, create events, repeat rules and guests → 3.155.0-alpha (C9b-1b + C10–C12 of #884) - #961

Merged
BBYU26 merged 14 commits into
chore/calendar-series-dialog-partsfrom
feat/calendar-series-dialog
Oct 22, 2026
Merged

BBYU26 merged 14 commits into
chore/calendar-series-dialog-partsfrom
feat/calendar-series-dialog

Conversation

@BBYU26

@BBYU26 BBYU26 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #884. This PR now also carries C10a–C12c. On 2026-10-22 the stack above it (#962–#967, #969–#973) was merged into this branch, so merging this PR lands all of it: 3.149.0-alpha → 3.155.0-alpha. The branch tip has exactly the same content as #973's tip, which passed every check. Stacked on #960: the base is chore/calendar-series-dialog-parts, and there's no closing link while the base isn't main.

Already merged into this branch

Each PR has its own description, reviews and live tests:


C9b-1b (this PR's own change)

What changes for you

Saving or deleting an occurrence of a repeating event opens the "which occurrences?" dialog, as Google Calendar does: This event, This and following events or All events. Each row says what choosing it would do, and nothing is sent until you press Save or Delete. "This event" is picked for you, and focus starts on Back, so pressing Enter on open can't choose.

  • Editor: the dialog opens over the editor, and the save sends exactly the changes and token the choices were worked out for. A later answer never replaces an earlier one. Words PM says once (for example "PM closed before it finished changing this repeating event") stay as a notice whichever way you leave, and focus goes back to Save.
  • Delete: the same dialog. It reopens once if the backend needs a choice. If PM and Google disagree about whether the event repeats, it tells you to refresh. A delete that leaves some occurrences in Google shows them again on the next read.
  • Repeating events you were invited to stay read-only until C14 (invitations).

Backend

  • Recurring is gone.
  • An unscoped save or delete of an occurrence answers NeedsChoice after the read-only check and before any write.
  • An unscoped delete of a series row is refused before it's held.
  • Release builds stop refusing scoped writes and settling. The choice token is still required.

Decisions taken while you were away (recommendations from the spec)

  1. Two PRs: chore(calendar): the repeating-event dialog's parts, not yet shown → 3.148.9-alpha (C9b-1a of #884) #960 (the parts, nothing visible), then this one, so main never shows Edit on a repeating event without the dialog.
  2. No automatic Finish: every send stays something you pressed.
  3. Only the selected row's warnings show, under the radios.

Reviews

  • Round 1 (four lenses, each finding checked by a skeptic) kept 13: one medium (an untested fallback in the editor) and 12 lows, all fixed in e0909da:
    • a late "choose" from an earlier delete could reopen over another dialog;
    • an older mirror read landing late could show a deleted event again;
    • a settle could close a dialog opened after it;
    • the editor's rebase after the choices could fail silently;
    • the What's New wording;
    • and missing tests.
  • Round 2 kept 3 lows, fixed in f42032a:
    • a delete answered "choose" after the Calendar tab closed now leaves its notice;
    • the delete dialog never reopens over any open dialog or the event popover;
    • tests for useMirrorReads' unmount and failure paths, and both hooks' tests run under StrictMode.
  • Round 3 (on the round-2 fixes) kept nothing.

Checks

The full just check passes at f42032a: 2,109 Rust tests and 2,108 frontend tests.

Live tests for you

docs/calendar-live-tests-2026-10-10.md, section "Repeating events from the dialog (C9b-1b)", D1–D13, on the dev build against the test calendar.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 20, 2026 00:00
@BBYU26 BBYU26 added enhancement New feature or request rust Pull requests that update rust code labels Oct 20, 2026
@BBYU26 BBYU26 self-assigned this Oct 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

BBYU26 and others added 3 commits October 21, 2026 16:22
…currences → 3.149.0-alpha (C9b-1b of #884)

Repeating events you organise become editable from PM. Saving or
deleting an occurrence opens the "which occurrences?" dialog (This
event / This and following events / All events), nested in the editor,
with the save sending the very changes and token the choices were asked
for. A stale answer never replaces a later one, words said once are kept
as a notice on every way out, and focus returns to Save. Deletes ask the
same, reopen once when the backend wants a choice, and say to refresh
when PM and Google disagree. A delete that leaves occurrences in Google
shows them again on the read its settle starts.

Backend: Recurring is gone; an unscoped save or delete of an occurrence
answers NeedsChoice after the read-only gate and before any write, an
unscoped delete of a series row is refused before it's held, and
release builds stop refusing scoped writes and settling (the choice
token is still required there).

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A delete answered "choose" reopens its dialog once, but never over a
  delete dialog or the editor opened meanwhile; a notice says to delete
  again instead (F1, F6). The reopen-once routing moves into
  useDeleteDialog and is tested (F8). A choice the plan changed under
  again says so in its own words (F11).
- Only the newest read of the mirror applies or releases hidden rows, so
  an older read landing late can't show a deleted event again (F4). The
  settle reload moves into useMirrorReads, which CalendarView calls and
  the tests drive (F12).
- A settle that ends after its delete dialog was closed closes nothing
  more (F5).
- A delete whose choices say Google's copy doesn't repeat says to
  refresh at once, as the delete would only be asked for a choice (F7).
- The editor's rebase after the choices catches a failed read, holds
  Save while it runs and puts focus back on it; a conflict's failed
  rebase no longer marks the save as possibly in Google (F2). Tests pin
  the not_repeating save, its loop guard, a failed choices read and a
  refused conflict's rebase (F3, F9).
- What's New says "without guests", how each kind of save is changed
  back, and drops the promise that nothing shows twice (F10); a stale
  test doc names row_repeats (F13).

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A delete answered "choose" after the Calendar tab unmounted now leaves its
notice in the store (a StrictMode-safe mounted ref), and the dialog never
reopens over any open dialog (isAnyDialogOpen at answer time) or the event
popover. Tests cover useMirrorReads' unmount guard and failure path, and
both hooks' tests run under StrictMode.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@BBYU26
BBYU26 force-pushed the feat/calendar-series-dialog branch from 9afff68 to 77b584b Compare October 21, 2026 15:37
BBYU26 and others added 11 commits October 22, 2026 03:19
…ly → 3.149.1-alpha (C10a) (#962)

* chore(calendar): the create command and its safeguards, dev builds only → 3.149.1-alpha (C10a of #884)

The create command goes to Google under an id PM chose, as a one-step plan
on the series machine: a lost answer is looked for, looked for again a
second later, and only then posted once more under the same id. An answer
PM never got is held under a retry token of PM's own (never the event's id),
and a retry looks before it sends, refuses a changed draft or calendar, and
caps the posts under one id at three. Google's copy lands as a created row,
or not at all when it starts outside the months PM mirrors. Release builds
refuse the command until the editor's create mode ships (C10b); nothing in
the app changes yet. The default-length storage and overview field land
here; reading it from Google is C10b's.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C10a

- A new event found cancelled on the second look after a lost answer
  now reads as gone, as on the first look, and the exhaustive answer
  test pins that a copy Google showed always settles the run.
- A retry never sends a new event again into a calendar PM stopped
  syncing; it still looks for the earlier one and keeps the token.
- Tests pin the held create lapsing through the command's own prune, a
  re-send starting the eight hours over, a re-send meeting a cancelled
  copy, and a running retry's claim before its take.
- The words no longer promise a dropped connection can't make a second
  copy (Google can't always catch a repeated id), and the 'changed'
  refusal says to look in Google Calendar before creating it again.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C10a

- A retry refused because PM stopped syncing the calendar now says the
  earlier send may have landed, to look in Google Calendar or choose the
  calendar and Check again; the token stays held.
- Comments on the create path no longer say a re-send under the same id
  can't make a second copy (Google can't always catch a repeated id).
- What's New drops "under the same id".

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…a (C10b) (#963)

* feat(calendar): create events in your Google calendars → 3.150.0-alpha (C10b of #884)

"+ New event" in the calendar header, and the c key, open the full editor in
create mode: a calendar picker (writable calendars on editing-on accounts,
grouped by account), the last calendar created in as the default, Google's
defaultEventLength per account (read at connect and at each sync), and
notices for a start outside the synced months or a hidden calendar. Save,
Ctrl/Cmd+S and Enter in the title share one saving guard; an answer that
never arrived locks the draft as sent and offers Check again under the
same retry token, and outlives the editor as a notice. Release builds no
longer refuse the create command.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C10b

- A held new event whose token ends unproven (lost, a changed draft, or
  made and since deleted) is never called "nothing saved": the discard
  question repeats what PM knows, and closing leaves those words as a
  notice. A later successful save clears them.
- Tests for a Check again that ends after the editor has gone (answered,
  and with no answer at all) and for one with no answer while it's open.
- Enter or Ctrl+S that is part of composing text (isComposing, or WebKit's
  key code 229) saves nothing; a held key's repeats on Ctrl+S, the title
  or Save send nothing more.
- Save checks the discard question, a save already out and a missing zone
  before blurring anything; a save refused for a problem puts focus back
  where it was. EventEditor's Save does the same.
- The calendar list stays inside the editor's dialog (Popover gains
  withinDialog), puts focus on the calendar picked when it opens, and
  hands focus back to its button on Tab, Enter and Escape. A click on a
  row's name or dot picks and closes. It closes for good when disabled.
- The bar's Close is disabled while a save is out (Dialog gains
  closeDisabled), in both editors.
- With no zone, the editor says a new event can't be saved.
- The calendar's write notices sit under an open dialog's scrim.
- The header's "why not" restarts and is said again on another click;
  tests for its timing, its reset, the button's secondary variant, the
  title's focus on open and a calendar change alone asking to discard.
- createReasonFor, the header's "why not", is pure and tested.
- A default event length read during a disconnect is never stored: the
  check is a tested helper, used at connect and at each sync.
- Comments on the encrypted calendar_ui blob name the last calendar an
  event was created in beside the hidden set.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C10b

- A date a field couldn't read now stands until that field is given a
  date, typed or picked: a blur, a time or the other date never ends it,
  so putting focus back on the field after a refused save can't let the
  next save go out on the old date. Each half of the event keeps its own
  refusal, in both editors.
- Notices sit strictly above the floating briefing and below any
  dialog's scrim; a test checks both bounds.
- A test pins that only a held token ending without proof leaves a
  notice; a fresh save that fails outright says nothing was saved.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C10b

- A date a field couldn't read is said naming that field (Start date,
  End date, First day or Last day, by the event's kind), and says to
  type or pick that date again. The line sits under the date fields; the
  field is marked wrong and described by it, and the same text refused
  in both fields gives a line for each. A save the refusal stops puts
  focus on that field (the start's first), in both editors; any other
  refused save still leaves focus where it was.
- Tests pin that closing with only a refused date standing asks before
  discarding, in both editors.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(calendar): each refused date's line is its own field's (C10b round 4)

The same-text tests in both editors now check that the two refusal lines have different ids and that each date field is described by the line that names it.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… as you type → 3.151.0-alpha (C10c) (#964)

* feat(calendar): quick create, and the new event shows on the calendar as you type → 3.151.0-alpha (C10c of #884)

Shift+C opens a small, non-modal quick-create box under "+ New event": type a
title and press Enter to create it; the date, the times, all day and the
calendar can be changed there, and More options hands the draft (and any
retry token held) to the full editor without sending anything. A click
outside or Escape closes it only while the title is empty; a click in a
picker it opened, or on its ghost, isn't outside. A titled draft that goes
with the Calendar tab (a click in the sidebar), nothing sent, is handed back,
and the box opens on it again when the calendar does. It registers as a
dialog layer, so the calendar's keys, the swipe and Ctrl+K stand down, and
while a save is out nothing sends, closes or hands over again. A date its
field couldn't read is said under it, naming the field, which is marked
wrong; it refuses every save until the field is given a date, a save it
stops puts focus there, and More options carries the refusal on.

The draft is drawn on the grid as a dashed ghost in its calendar's colour
(week, day, month and agenda; not in the Terminal look), and a saved one
stays until a read of the mirror that started after the save lands. A screen
reader names it as the new event and how far it has got: not saved yet,
saving, or one PM may already have made. In Month it takes the top lane over
its own days only, so bands in other weeks keep their lanes.
Month packs the weeks the ghost touches with it on top and every other week
as if it weren't there, so a band that runs into its days keeps its lane
elsewhere and changes lane only at the edge of the ghost's weeks.

Escape beside a non-modal layer (isKeyForDialogOver): a modal dialog always
owns the key, and a non-modal layer such as quick create only a key pressed
inside it. So a popover, or Settings' model menu, open beside quick create
closes on Escape pressed in its panel, on its trigger or on the page, and
Escape on the page still leaves fullscreen. Popover panels are marked with
data-popover-panel.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(calendar): quick-create tests pass in any locale, and wait for the calendar's keys (C10c)

The Agenda and Month tests named rows by a fixed 24-hour time, but the views say the time in the reader's own clock (formatClock), which on CI's runner reads 09:00 AM. They now build the names with that helper, as whenText.test already does. CalendarView.test's mount() also waits for the calendars' reads and the key rebinding, as C11b's does, so Shift+C can no longer arrive first and open nothing.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…fers to move its milestones → 3.152.0-alpha (C9b-2) (#965)

* feat(calendar): the popover says how an event repeats, and a split offers to move its milestones → 3.152.0-alpha (C9b-2 of #884)

A repeating Google event's details now say how it repeats, in Google
Calendar's words, read from Google's copy of the series (a GET only,
kept a while per series and forgotten by PM's own writes to it).

After a "this and following" change lands, each unmet milestone linked
to the event and dated from that day on (read before the first send)
is offered a move to the new part, with Keep it there beside Move and
Move back for 8 seconds after. The move is compare-and-set, so an old
offer never undoes a change made since. A new part PM can't name says
which milestones stayed. set_milestone_event now nudges the briefing
and refuses an id no milestone has.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C9b-2

- F1: both claim drops (InFlight, HeldClaim) end in one release_claims,
  which forgets the series' words while the claims' lock is held, so an
  ask never finds a series let go with its words still kept. A source
  test pins both drops and that order.
- F2/F12: the popover's re-point test answers the shown event first and
  the older one after; the shown words stay.
- F3: a test fills the summary cache to its cap: the stale go first,
  then the one read longest ago, and only that one.
- F4: a split offers a linked milestone by the occurrence its date comes
  from (that occurrence's place in the series against the split), not
  by its date alone; with no row on its date, the date decides. The
  earlier part's last day is read the same way. Tested both ways round.
- F5: the landing decides which milestones stay unasked by what it saw
  still on the earlier part, not by slot days against moved dates; a
  cleanup whose answer was lost, or a landing that couldn't read Google,
  asks. Tested with a moved occurrence left behind, a lost cancel that
  landed, and a lost cancel with a stale landing.
- F6: set_milestone_event's body is a relink core, tested for both
  refusals (and that neither changes anything); a source test pins the
  briefing nudge after it.
- F7: tests for a new part deleted before its one GET, the user's zone
  deciding every day, and the earlier part's last day when an
  occurrence is left behind.
- F8: Move back takes no press in its first half second, nor a
  double-click's second click, nor a held Enter's repeats.
- F9: the Move back and Keep it there names start with the words they
  show; a test checks every move button.
- F10: a test dismisses a moved notice during its Move back window; it
  stays gone.
- F11: the re-point test checks focus goes into the panel.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C9b-2

- R2-F1: which milestones a split offers goes back to the rule Bobby decided
  (DECISIONS 17-10 #2): a milestone dated on or after the day the change
  starts, in the user's zone, is asked about; an earlier date is never asked,
  even when its occurrence goes to the new part. The only exception is the
  adopted C1-L9 one: a milestone on an occurrence left on the earlier part.
  milestones::linked_on_or_after is back; the occurrence slots now only serve
  that exception. The words in saveChoices.ts and help.ts ("on or after this
  day") are true again, and the doc comments in dto.rs, types.ts and
  useEventWrites.ts say the same.
- R2-F2: a cleanup step whose answer was lost now ends Unanswered, apart from
  Skipped (never sent, or proved not done, including Finish's changed
  branch). Ran::skipped still counts both, so the mirror and the left-behind
  words are unchanged; Ran::not_done counts only Skipped. When the landing
  can't read Google, a milestone on an occurrence whose cancel surely never
  landed isn't offered; one whose cancel's answer was lost still is.
- R2-F3: the left-behind test also runs for a user in Honolulu, where the
  occurrence's day differs from the series' London day, so matching
  milestones by the series' zone fails it.
- R2-F4/F5: release_claims lets go of the claims' lock before it forgets the
  series' words, as the CalendarEditState lock order says; the writes count
  covers a keep in between. The source test now pins that order.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C9b-2

- R3-F1: the day a split offers milestones from is the day the user splits
  from as they see it: the earlier of the occurrence's place in the series and
  the day it shows on, in the user's zone. That's the "this day" the dialog
  names. A split from the 19-10 occurrence moved to Sunday 18-10 now offers
  "Launch" on 18-10, where the split carries that occurrence. A split from one
  moved later still goes from its place in the series. The left-behind
  exception still compares occurrence slots with the split point.
- R3-F2: a test for the branch where the landing reads Google. The cancel's
  answer is lost, Google still lists the occurrence on the earlier part, and
  "Launch" isn't offered. Working this out from what PM knows it never sent
  fails the test.
- R3-F3: a landed split now sends today, in the user's zone, beside the earlier
  part's last day. The offer and the stayed notice say "which ended on D, and
  shows as overdue" for a day before today, and "whose last day is D, and shows
  as overdue after that" for today or later.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(calendar): pin the user's zone in a split's milestone days (C9b-2 round 4)

The test host's today now follows the zone it is asked for, and two Honolulu tests fail if the
shown day, the offer's today or the resolving today is read in the series' zone.
Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nd can create a repeating event → 3.152.1-alpha (C11a) (#966)

* chore(calendar): repeat rules in words and on the wire, and the backend can create a repeating event → 3.152.1-alpha (C11a of #884)

A repeat rule from the editor's set now has a wire form the webview can
hold, and two pure commands give the repeat menu its ready-made rules and a
rule's words, with the day its first occurrence falls on: "Weekly on Monday
and Wednesday" chosen on a Tuesday starts on the Wednesday, and a first
occurrence whose start or end the clocks skip is refused in words. A rule
that never happens is refused before the rrule crate is asked.

The create command takes a rule: the event starts on the first day the rule
makes, its end moved with it, with one rule line and its zone on both timed
nodes. Google answers with the repeating event itself, which is never a
mirror row, so its occurrences are listed (three looks at most) and landed
as created rows, or said to be outside the months PM mirrors. A repeating
create counts as series-wide to a dev build's allow-list, and an empty rule
is no rule wherever PM asks. Nothing in the app sends a rule yet.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C11a

- A test now covers the listing loop's stop condition when a new
  repeating event's first look is incomplete: page 1 arrives with a next
  page, page 2 is refused with a 503, and the second look a second later
  lists all three occurrences and lands them. If the "complete" half of
  the stop condition is removed, the loop stops after that first look
  and this test fails.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(calendar): new events can repeat → 3.153.0-alpha (C11b of #884)

The full editor's create mode and quick create gain a Repeat row: Google's
ready-made rules for the start date, and Custom… (every N days, weeks, months
or years; weekdays, a day or numbered weekday of the month; ending never, on a
date or after N times). The backend's words say where a rule really starts when
it moves the start, why it can't be saved, and the zone it repeats in; every way
to save waits for them. A rule's own day follows a moved start date. Custom…
acts only on a committed pick; from quick create it hands over to the full
editor with the dialog open. Also steadies CalendarView.test's mount, whose
keys could reach the view before its calendars did.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C11b

The Repeat menu only ever shows the rule the draft holds, so a save sends
what it shows. While a key steps the closed select, Custom… is turned off
for that moment, so the step passes over it instead of landing on it; it
comes back on by a timer rather than on the key coming up, which a menu the
key opened can swallow. Custom… opens on any pick from the list: a click,
or Enter in the list the keyboard opened. Alt+Down, and Up and Down on a
Mac, open the list rather than step, so Custom… stays on offer there.
Space is not a step either.

The note before Save no longer says a repeating event that starts before
the synced months won't show here: the backend lands its occurrences
inside them, so it says only those will show. The Repeat select is never
wider than its row, and the When fields can be narrower than their widest
content. The Repeat row holds back its refusal while the When fields
already refuse a time the clocks skip, so it is said once.

New tests: arrowing to the last rule and saving sends exactly that rule;
a pick of Custom… right after arrowing opens it; Space, Enter, Alt+Down
and a Mac's arrows leave Custom… on offer; the moved-start line stays away
when the start didn't move; and Check again (and Ctrl+S) on a held retry
send exactly the held body, rule included, while the repeat's words are
still to come.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C11b

The Repeat menu is no longer a native select. WebKit and Chromium track a
select's changes differently, so a step could land on a row without PM
hearing of it: the menu could show a rule the draft didn't hold, or open
Custom… on an arrow key. It is now a button that always names the rule the
draft holds, opening a list inside the editor's dialog (or quick create's
box), as the calendar list does: the rules as radios in Google's order, then
Custom… as a button of its own. Opening puts focus on the rule picked; the
arrow keys move the choice and the button follows; a click or Enter picks
and closes; Tab goes from the rules to Custom… and out of the list to the
button; Escape closes only the list. Custom… opens on a click, Enter or
Space, never on an arrow key. A rule of its own keeps its row while the list
is open, so an arrow key passing over it can come back to it. Disabled for a
save, the list closes. A long rule's words wrap on the button, which is never
wider than its row. Quick create keeps its smaller button, and its Custom…
still carries on in the full editor.

The round-1 step and timer handling, and its tests, are gone. A click on a
Mac doesn't focus a button, so the Repeat and calendar buttons take focus on
a click, and Escape in their lists hands it back there.

New tests: the button always names the rule held; the arrow keys change the
rule and the button, and never open Custom…; a change the editor doesn't take
leaves the button as it was; Enter and Space on Custom… open it, as a click
does; Tab in and out of the list; Escape closes only the list, in both
editors; Ctrl+S straight after arrowing sends exactly the rule the button
names; the list sits inside the dialog and the box; disabled closes it;
presets arriving while it's open; a long rule wraps.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C11b

When the ready-made rules arrive while the Repeat list is open, the row
focus was on could go with them: a rule shows as its own row while they
load, and becomes its preset's row once they come. Focus then fell out of
the list while it stayed open, so the arrow keys did nothing and, in quick
create, Tab jumped to the top of the page. Focus now goes back to the rule
picked whenever the rules change and focus is no longer in the list; if it
is still in the list (on Custom…, say), it stays where it is.

The tests now check that the Repeat and calendar lists are mounted in the
dialog itself, not just somewhere inside it. A list left inside the button's
box would be cut off by the dialog's scrolling body, and no test noticed
when the prop that lifts it out was removed.

New tests: focus follows the rule when the rules arrive while the list is
open; focus on Custom… stays there when they arrive; the Repeat list is
mounted in the dialog and placed against the window; both lists in both
editors are mounted in the dialog or box itself.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t sent → 3.153.1-alpha (C11c of #884) (#969)

* chore(calendar): the repeating-event planners take a new rule, not yet sent → 3.153.1-alpha (C11c of #884)

The series planners now take a new repeat rule or "Does not repeat" for
every occurrence or from one on: the rule is rebuilt at its own start,
deleted days and changed occurrences follow it where they still fall,
it never repeats past a later part, and stopping keeps the first live
occurrence as one event. Changing how it repeats is refused for a single
occurrence and, for now, for a one-off event. Landing handles a series
that became one event, and the save choices word the new warnings.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C11c

A deleted extra date stays deleted when a new rule keeps it: the rebuild
writes its EXDATE again, for every occurrence and from one on.

A yearly-on-its-date rule picked with the first occurrence moved earlier
starts where it was put, not a year later.

A "this and following" row that changes every occurrence words its
warnings as All, so it never speaks of a new part that isn't made.

"Does not repeat" for every occurrence keeps the hedge for the other
occurrences changed on their own, and says the kept one moves when the
draft moved this occurrence to another day.

A new daily rule with a moved day asks nothing about which days it
repeats on; where it starts says it.

The carry of an occurrence's own time onto a new part, the stop's own
time and the all-day later-part check now have tests, and the refusal
when nothing is left to keep points to Google Calendar instead of a
choice that is refused.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C11c, part 2

A series that is itself a part of one changed "this and following"
before, by PM's note or Google's id, is held to where its own rule ends
now: a new rule for it, or from one of its occurrences on, never repeats
on a day after its last occurrence, since PM can't list the parts of the
series it came from. One that repeats for ever has no part after it.

A new rule keeps an occurrence deleted or changed on its own on its own
day wherever the rule still makes that day. Only a day it doesn't make
moves with the day the draft moved, and never onto a day the old series
had an occurrence of its own. The occurrence opened still goes where the
draft put it, and an extra date kept takes its own changes where it is.

"This and following" into another time zone measures the part before in
the series' own zone, so the new part can't start on the day it still
reaches.

A lost answer to a new rule or to "Does not repeat" says what may be half
done instead of speaking of moved occurrences. A record of "Does not
repeat" from one occurrence on, left from before a restart, says to
delete the event that doesn't repeat, and that occurrences changed on
their own may still show.

The notice after a save that left its event outside the months PM syncs
says so, warns, and drops "edit it again"; "this and following" with
"Does not repeat" is worded as one event that doesn't repeat.

The rule paths' own guards (R2 on the occurrence and on the series,
guests, formatting, the new part's id, what the kept occurrence carries,
an unreadable line on a stop) now have tests.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): C11c's What's New doesn't promise stopping a repeat

C11d keeps "Does not repeat" on a repeating event for dev builds until
the live probe records what Google does with occurrences changed on
their own, so the next update can't stop a repeat yet.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C11c

A new rule keeps the slot the draft gives the occurrence opened for that
occurrence alone: an occurrence deleted on that day writes no EXDATE
there, and one changed on its own there is counted as lost rather than
carried over it, for every occurrence and from this one on.

A move into another time zone reads each occurrence's day, and how far
the day moved, in the zone it moves into, so a move that keeps every
moment keeps each deletion and each change at its own moment.

With an old rule PM can only show as text, a deletion that would move
with the days onto a day the new rule makes is refused in words, never
dropped, since PM can't tell whether that day had an occurrence of its
own.

A part whose rule PM can only show as text, and that repeats for ever,
has no later part, so a new rule for it is offered again.

"This and following" picked on the first occurrence saves every
occurrence, and its notice now says so; with "Does not repeat" it names
the day of the one event left. The scope and token sent are unchanged.

An event saved outside the months PM syncs is said to be outside even
when PM's calendar couldn't be written or the re-fetch never showed the
change, and the notice keeps saying the calendar will catch up after
the next refresh, since the old occurrences still show until then.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C11c

A new rule for every occurrence, opened on an extra date that is kept,
no longer takes the rule's slot that day for it: the extra date stays
where it is, so an occurrence deleted at the rule's time that day stays
deleted.

A move into another time zone now reads each old occurrence's day as its
day in the series' own zone, moved on as far as the occurrence opened
moves between the two zones. Both rules repeat by the day on the clock in
their own zone, so across a clock change each occurrence keeps its place
in the week: a deletion stays on its own occurrence, never on the live
one beside it, and a change goes onto its own. The same reading places
the one event "Does not repeat" keeps, where a new rule starts, and how
far a day move takes every occurrence, for every occurrence and from
this one on. Whether the old series had an occurrence of its own on a
day is read on that day in its own zone.

A deletion PM can't place is named by its day in the series' own zone.

A Finish that lands outside the months PM syncs now says the calendar
will catch up after the next refresh, as a save does, and warns: one
helper words a saved outcome's warnings for both.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(calendar): review round 4 of C11c

A timed series made all-day is read by its own wall days, never by the
all-day start's zone. Weekly on Monday at 00:30 in London, Monday 12-10
deleted, Monday 19-10 made all-day and "Weekly on Sunday and Monday":
for every occurrence the EXDATE stays on 12-10 and nothing warns that
the repeat day changes; from this one on, Monday 26-10's own change goes
onto the new part's Monday 26-10. Days::new comparing the zones for
every kind of start moves both onto the Sunday before.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…884) (#970)

* feat(calendar): change how an event repeats → 3.154.0-alpha (C11d of #884)

The editor's Repeat row now works on existing events. A one-off given a
repeat saves alone: its times and rule go to Google started on the first
day the rule makes, its occurrences land in place of its row, and an
answer that never arrives is read by meaning. On an occurrence, a new
repeat or "Does not repeat" asks which occurrences, "This event" refused;
the event's own rule shows in its own words and never holds a save.
Every occurrence can now move between all-day and timed, or into another
time zone, rebuilt from the event's own rule. Release builds hold back
"Does not repeat" on a repeating event until live probe P-R2.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C11d

A change between all-day and timed, or into another time zone, for every
occurrence now refuses a repeating event whose first occurrence isn't on its
own rule, as a move does: rebuilt from the rule, that first occurrence would
go and, with a count, one more would come at the end.

The one-off new part of a "Does not repeat from here on" change PM couldn't
finish now waits for that change, as a repeating new part does: held, being
sent or settled, or left in the journal from before a restart.

A one-off given a repeat with its time left alone is saved in the zone the
editor showed it in (the calendar's, else this computer's, kept with the
session); only with no zone anywhere is it refused, in words about the event.

The refusals for a change of kind or zone with a day move fit the "this and
following" row too, and an occurrence already moved on its own gets words of
its own instead of naming a move the user never made.

The Repeat list keeps the event's own rule as a row once it closes on another,
as Google does, so picking it again sends nothing. Switching All day on or off
goes through the same path as any time change, so a picked rule follows the
date it lands on. The saved notice after "Does not repeat" says one event is
left, and a save PM's calendar won't show points to Google Calendar instead of
saying to edit it again.

New tests cover the all-day to timed rebuild both ways and in a one-off given a
repeat, the times check on a lost answer, and the zone note for a rule PM can
only show.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C11d

A "Does not repeat" left from before a restart is now tested on its
one-off new part's own editor for every reading: an original never cut
refuses in the stop's words with only reads sent and keeps the record,
and an original whose repeat changed in Google since is said once and
forgotten, so the next save goes through.

Switching every occurrence between all-day and timed, or into another
time zone, on a repeating event whose first day is off its own rule now
refuses in words of its own, naming no move the user never made.

The stop's restart words end with making the change again rather than
trying again, since the event being saved may be the one to delete.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C11d

A one-off made to repeat whose series starts outside the months PM syncs
no longer ignores a failed removal of its old row: when PM can't take the
row off its calendar, the save says the event is outside and that PM's
calendar will catch up after the next refresh, and the next sync redoes
the calendar in full.

A new test pins how a change of time zone sits on the reworked day
handling underneath: weekly on Monday at 01:00 in London, put in New York
at 01:00 on the same Monday, stays on Mondays for every occurrence and
from this one on; kept at the same moment, which is Sunday in New York,
it is refused in words, since the rule's own day would have to move.

Test fixes: three test stand-ins for the passphrase strength check now
answer in its real shape (the meter reads its warning and suggestions,
which were missing), and the ghost-state test for a failed new event
waits for the ghost to hear of the failure rather than reading it the
moment the words appear.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 4 of C11d

Moving every occurrence of a series into another zone at the same time on
the same day asked whether to move every occurrence a day, and said the
series' start moved. Weekly on Monday at 01:00 in London, moved to 01:00
in New York on the same Monday: what is sent was right, but the save
choices asked "Move every occurrence a day later?" (18-10 to 19-10) and
said the start moves from 04-10 to 05-10, because both were read in New
York's days, where Monday 01:00 in London is still Sunday.

Both are now raised only when a day moves on the series' own clock as
well as in the new zone's days, and are worded in the series' own days,
as the dialog reads them. A move into another zone at the same moment
still raises neither, and a real move to another day with a new rule
still raises both: Monday to Tuesday, and 05-10 to 06-10.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 5 of C11d

"Does not repeat" for every occurrence no longer says the first occurrence
moves when it is put in another zone at the same time on the same day past
midnight. As round 4 did for a new rule, a day moves only when it moves on the
series' own clock too, and the warning is worded in the series' own days.

Custom... opened at a later occurrence of a repeating event now checks the end
date against the event's first day instead of this occurrence's, so every
occurrence can end before the one opened, as in Google Calendar. The choices
still say whether each can. The occurrence's date still drives the weekday,
month and year labels.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…wn → 3.154.1-alpha (C12a of #884) (#971)

* chore(calendar): guests and update emails on the backend, not yet shown → 3.154.1-alpha (C12a of #884)

The backend can now change an event's guest list as changes to Google's
own list, keeping every guest's reply, comment, extra guests and room
flag, and works out who a change or delete would email. A save or delete
that would email someone and has no Send / Don't send answer for those
numbers is asked about with only a read sent; one that emails nobody
sends none. Repeating-event plans stamp each step with the answer where
it reaches someone, bind who they email into the choice token, and never
run unanswered. The editor says it rather than looping when asked.
Events with guests stay read-only until the lift.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C12a

A repeating-event write that sends a guest list keeps the user's guest
changes on its step. After a 412 the list is built again from Google's
copy then, so a reply, comment or extra guests that landed meanwhile are
not overwritten. A list Google cut short since is a conflict.

Under Send, a resend after a 412 stops when Google's copy would now email
anyone else, or not someone it would have. The next save asks again with
the new numbers, as a one-off's second round does. Under Don't send,
nobody is emailed. The resend test that pinned the old behaviour is
updated, and deletes of the whole series or one occurrence are covered.

Guests already on Google's list are matched by address and no longer held
to PM's address format, so a guest Google accepted (a team calendar with a
long name) can be taken off or made optional.

A one-off delete asks before the hold only when the row shows guests and
nobody answered. The numbers are compared at the send against Google's
copy, because the mirror can't say a list was cut short.

The plan keeps the calendar's own addresses, so the 412 rule and the
counts agree on who the user is. A test covers an account address on the
list without Google's self mark.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(calendar): review round 2 of C12a

The guest list sent once more after a 412 is rebuilt from Google's list,
and that rebuilt write must email whom the user chose. The run test now
plans the guest change under every answer (all, none, external only) and
asserts the first write carries that sendUpdates and the rebuilt write
carries the same one, so a fixed or dropped answer on the resend fails.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…t shown → 3.154.2-alpha (C12b of #884) (#972)

* chore(calendar): the guest list and the update-email question, not yet shown → 3.154.2-alpha (C12b of #884)

The editor's guest pieces, built and tested but not mounted: the address
check, replies as glyphs, suggestions from people already in the user's
Google and Outlook calendars (never a subscription), who a change would
email, and the words of the question. The draft holds guests and the
three guest permissions, sends changes to Google's list, never the list,
and merges a guest list Google changed meanwhile. GuestField, NotifyChoice
and NotifyPrompt are new; ConfirmDialog gains confirmDisabled and
initialFocus; the which-occurrences dialog asks Send or Don't send for a
row that would email guests, with no answer picked.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C12b

Suggestions know the user by every account's own address (from the
calendar's source, since Outlook never marks the user) and by Google's
self mark on the primary calendar only. A colleague whose calendar is
shared into the account is now suggested like anyone else.

The organiser's address now comes to the editor with Google's copy,
whether or not the guest list carries it. The guest list never sends a
change to it, matching the backend's check by flags and by address. The
guest box won't add it either: it says "That's the organiser, who is on
it already." Before, the whole save was refused.

A guest added back gets their entry from Google's list as it is now, the
rebased base, or comes back as a new guest. Before, they came back as
they were when taken off. Removing a guest moves focus to the next row's
remove button, else the one before, else the box. Keys sent during IME
composition (isComposing or key code 229) are left to the input method.
A long refused text wraps. Optional is filled when on and has an --ink4
edge when off. Under Modify event, the other two permissions are drawn
checked whatever Google stored.

Pasting splits Outlook's unquoted "Last, First <addr>" lists, and each
address comes back once.

After a conflict, the banner names the guests whose change was dropped
instead of calling the merged list Google's version. The rebase now
returns those names.

The email question's backend note no longer says the user chose before,
since it can be the first question.

New tests cover replaying a guest removal and a required-again change
onto Google's list. The delete-table row the backend doesn't have now
has a test of its own.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C12b

Guest suggestions no longer offer the user's own other calendars. On a
calendar the user made ("Work"), the entry Google marks as the calendar's
own is that calendar's group address, so it now counts as the user, as on
the primary calendar. A colleague's calendar shared into the account keeps
its owner suggested.

The guest field treats the organiser's address as fixed even when Google
sent the list without its marks: the row shows "Organiser" and offers no
Optional or remove, matching what the save sends.

The Optional toggle has a visible disabled look: its words dim to --ink4
and, when on, the accent fill gives way to the surface, as the remove
button beside it dims.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ails them → 3.155.0-alpha (C12c of #884) (#973)

* feat(calendar): edit events with guests, and choose whether Google emails them → 3.155.0-alpha (C12c of #884)

Events you organise with guests are editable now: the gate's guest refusal
and its series flags are gone, the guest list sits in the editor, and every
save or delete that would email someone asks Send or Don't send first, with
no answer picked, for one-offs and repeating events alike. A change held
part-way after a Send says which settles email guests. Help, the settings
line and What's New say so.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 1 of C12c

After the choices say an event no longer repeats, the one-event save that
follows can be asked whether Google emails the guests. That question now
opens with the backend's numbers: the editor counts it on its own, so the
"doesn't repeat" before it no longer turns it into a banner no Save could
get past.

Closing the editor with an address it couldn't read in the guest box asks
"Discard your changes?" first, as a date it couldn't read does, instead of
dropping what was typed.

A delete dialog the backend reopens for guests says the event wasn't
deleted. When nobody had answered, it says the event has guests now; when
an answer came for other numbers, it uses the editor question's neutral
words, that PM checked with Google before deleting and these are the
numbers it found.

The editor's guest suggestions are a required prop, so a caller that
leaves them out fails the type check. A Calendar tab test covers the
suggestions, and a delete reopened with the backend's numbers and the
account Google sends from.

The held-delete test now settles with what the backend sends: no "again"
when nobody answered, "again" when an answer came for other numbers. The
reopened dialog carries it, and so does the notice when something else is
open.

Two backend tests cover what C12a's review fixes do on a series with
guests: a guest list sent again after a 412 keeps a reply that landed
meanwhile, and a delete from the editor answered for Google's newer list
is held and deleted rather than asked again with PM's older count.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 2 of C12c

A delete the backend reopens for guests nobody had answered about no
longer says the event "has guests now". When Google removed the guests
after the last sync, the backend asks from PM's older row, so that was
false. The dialog now says "It wasn't deleted. PM found guests who would
be told. Choose whether Google emails them.", which is true either way.

The notice shown when another dialog is open said the same thing and now
says "PM found guests who would be told. Delete it again to choose
whether Google emails them."

Nothing else changes: the send still checks Google's copy, so nobody is
emailed who shouldn't be.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(calendar): review round 3 of C12c

A guest on an occurrence that a mixed plan cancels was told they would get
an update, while Google emails them a cancellation. Example: a weekly series
with no series guests and an occurrence on 26-10 with a guest of its own;
"This and following" from 19-10 with "Does not repeat" cuts the series and
cancels 26-10, and the question said "1 guest already invited will get an
update."

Reach now keeps who only a delete or a cancel reaches: a merge unions both
sides and drops anyone a write other than a cancel reaches on either side.
The guest notice carries that count as told_cancelled (never among updated,
none when the whole change cancels), and the question gives it its own line,
"1 guest will be told an occurrence is cancelled.", beside the update line.
A plain delete and a delete's plan keep their wording, everyone told it's
cancelled; an invitation-only question is no longer asked when someone is
told of a cancel.

Tests: rule.rs's a_rule_change_that_reaches_an_occurrence_with_guests_asks_first
asserts the new count; attendees.rs adds
a_guest_only_a_cancel_reaches_is_told_of_the_cancel (both merge orders, a
second write reaching the same guest, two cancels of one guest, taken off as
well, plain delete and delete's plan); guests.test.ts adds the wording, the
question and the delete sum.

Part of #884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@BBYU26 BBYU26 changed the title feat(calendar): change and delete repeating events, choosing which occurrences → 3.149.0-alpha (C9b-1b) feat(calendar): change repeating events, create events, repeat rules and guests → 3.155.0-alpha (C9b-1b + C10–C12 of #884) Oct 22, 2026
@BBYU26
BBYU26 added this pull request to stack #975 October 22, 2026 02:51
@BBYU26
BBYU26 merged commit abe5133 into main Oct 22, 2026
44 of 47 checks passed
@BBYU26
BBYU26 deleted the feat/calendar-series-dialog branch October 22, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request rust Pull requests that update rust code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants