fix: note delete guard, contact_frequency migration, calendar & chat improvements#154
fix: note delete guard, contact_frequency migration, calendar & chat improvements#154
Conversation
…urrent changes - Add isNull(orgId) guard to solo-mode note deletion (prevents ex-member access) - Add contact_frequency table migration (version 6) to mail db plugin - Include concurrent agent changes: AGENTS.md updates, integrations sidebar, compose editor
✅ Deploy Preview for agent-native-fw ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
agent-native-mail | 4657722 | Commit Preview URL Branch Preview URL |
Apr 06 2026, 11:28 PM |
There was a problem hiding this comment.
Builder has reviewed your changes and found 3 potential issues.
Review Details
PR #154 Review — fix: note delete guard, contact_frequency migration, calendar & chat improvements
This PR bundles four distinct changes: (1) a security fix to prevent ex-members from deleting org-scoped notes in solo mode, (2) a missing DB migration for the contact_frequency table from PR #153, (3) calendar click-to-create events with inline quick-editing in day/week views, and (4) GFM markdown support and UI polish in the agent chat. A significant portion of the diff is mechanical (browser dialogs → shadcn AlertDialog/Popover, AGENTS.md updates across templates). Risk: Standard.
The note delete guard is correct. Adding isNull(orgId) in solo-mode properly prevents users outside an org context from deleting notes that still carry an orgId, closing the bypass. No issues found.
The contact_frequency migration has a Postgres compatibility bug. The raw SQL uses INTEGER NOT NULL for last_contacted_at. In Postgres, INTEGER is 32-bit (max ~2.1 billion). JavaScript Date.now() timestamps (~1.7 trillion) overflow this type, causing a fatal numeric value out of range error on insert. The framework's intType() helper exists precisely for this and should be used here.
The calendar quick-edit flow has two UX bugs confirmed by all three review agents. First, clicking a time slot immediately creates an event in the backend, then sets quickEditEventId to the real server ID in onSuccess — but the real event isn't in the React Query cache until onSettled triggers an async refetch. During that gap the inline editor unmounts and any typed text is lost. Second, QuickEditInput.onBlur calls onSave rather than onCancel, so clicking away without typing silently leaves a permanent "(No title)" event in the database. The intended cancel-and-delete path is only reachable via Escape.
Browser testing: 🧪 Will run after this review — PR touches calendar and mail UI components.
Code review by Builder.io
There was a problem hiding this comment.
Browser testing: 3/12 passed
Test Results: 3/12 passed ⚠️
✅ TC-01: Calendar app loads and day/week view is accessible without errors (succeeded)
URLs tested: http://localhost:8082/
Evidence: 1 screenshot captured
❌ TC-02: Click-to-create: Click on empty time slot creates inline editor on calendar grid (failed)
Steps: 1. Navigated to calendar day view. 2. Attempted to click on empty time slots. 3. Observed network requests creating events. 4. Checked console for errors and found RangeError.
Failure: assertion_failed — When clicking on time slots in day/week view, the code attempts to create events but encounters "RangeError: Invalid time value" errors in the console. Network requests show POST /api/events succeeding, but the events don't appear on the calendar. The inline editor component should display but doesn't due to the time parsing bug.
Console errors: Uncaught RangeError: Invalid time value (appeared twice during testing)
⚠️ TC-03: Click-to-create: Typing title and pressing Enter saves event with that title (couldnt_verify)
Failure: not_applicable — Skipped — click-to-create feature (TC-02) is broken due to RangeError in time value parsing. Cannot test saving event with title if inline editor doesn't appear.
⚠️ TC-04: Click-to-create: Pressing Escape cancels without creating event (couldnt_verify)
Failure: not_applicable — Skipped — click-to-create feature (TC-02) is broken due to RangeError in time value parsing. Cannot test Escape behavior if inline editor doesn't appear.
⚠️ TC-05: Click-to-create: Clicking away without typing should not create phantom '(No title)' event (couldnt_verify)
Failure: not_applicable — Skipped — click-to-create feature (TC-02) is broken due to RangeError in time value parsing. Cannot test click-away behavior if inline editor doesn't appear.
✅ TC-06: Calendar delete dialog: Deleting recurring event shows proper modal dialog (not browser confirm) (succeeded)
Evidence: 1 screenshot captured
✅ TC-07: Calendar create event dialog: Dialog accepts default start/end times from time slot click (succeeded)
Evidence: 1 screenshot captured
⚠️ TC-08: Calendar week view: Click-to-create works in week view same as day view (couldnt_verify)
Failure: not_applicable — Skipped — week view click-to-create depends on TC-02 which is broken by RangeError. Time parsing bug blocks all click-to-create testing.
❌ TC-09: No console errors during click-to-create, delete, or create event operations (failed)
Failure: assertion_failed — Browser console shows "Uncaught RangeError: Invalid time value" errors when interacting with calendar time slots. These errors block click-to-create functionality and prevent events from displaying properly on the calendar.
Console errors: RangeError: Invalid time value (appeared 2+ times)
⚠️ TC-10: Mail: Alias delete button shows proper confirmation dialog (not browser confirm) (couldnt_verify)
Failure: timeout — Ran out of test time. Mail app at http://localhost:8085 not verified. Test would require navigating to mail settings, finding alias section, and verifying delete dialog appears as modal instead of window.confirm.
⚠️ TC-11: Mail: Link insert in compose shows proper dialog (not window.prompt) (couldnt_verify)
Failure: timeout — Ran out of test time. Mail app compose link dialog would require opening mail compose, clicking link button, and verifying proper dialog appears instead of window.prompt.
⚠️ TC-12: Calendar: Rapid clicks on time slots don't create duplicate events or leave orphans (couldnt_verify)
Failure: not_applicable — Skipped — rapid click edge case depends on TC-02 (click-to-create) which is broken. Cannot test duplicate/orphan behavior if click-to-create doesn't work.
Details
PR #154 calendar changes partially implemented - delete dialog uses proper modal, but click-to-create feature is broken by time parsing RangeError
…t changes - Fix framework-request-handler: shadow event.path with instance property instead of trying to set getter-only prototype property (H3 >=1.15) - Fix nitro/runtime imports with ts-expect-error (virtual module) - Include concurrent agent changes: run-manager, tiptap composer, calendar helpers
- Replace INTEGER with intType() in contact_frequency migration to prevent 32-bit overflow on Postgres timestamps - Export intType from @agent-native/core/db - Fix empty QuickEditInput onBlur leaving permanent "(No title)" events in DayView and WeekView — now calls onCancel when title is empty - Fix quick-edit input blinking closed by synchronously updating React Query cache in onSuccess before setting quickEditEventId
…edit improvements
There was a problem hiding this comment.
Builder has reviewed your changes and found 2 potential issues.
Review Details
PR #154 — Incremental Review (Round 3)
All three previously-flagged issues are now resolved:
- ✅ Postgres overflow (
db.ts):intType()now used correctly — confirmed safe sincegetDialect()readsDATABASE_URLat module load time before migrations run. - ✅ Quick-edit input race (
CalendarView.tsx):onSuccessnow synchronously swaps the optimistic cache entry viaqueryClient.setQueriesDatabefore callingsetQuickEditEventId(realEvent.id)— editor stays mounted. - ✅ Phantom "(No title)" events (
DayView.tsx,WeekView.tsx):onBlurnow correctly routes toonCancelwhen value is empty.
Two new medium issues found in the additional files shipped alongside the fixes:
🟡 Escape-during-flight still leaks an event: When the user presses Escape before the createEvent POST resolves, handleQuickEditCancel(tempId) calls deleteEvent with the temp ID (which doesn't exist on the server, so it silently fails), then createEvent.onSuccess fires unconditionally and calls setQuickEditEventId(realEvent.id) — re-opening the editor the user just dismissed. The real server event is never cleaned up. Fix: use a cancellation ref checked in onSuccess.
🟡 TiptapComposer draft key collision: The hardcoded "an-composer-draft" key is shared across all simultaneously-mounted TiptapComposer instances. In MultiTabAssistantChat, inactive tabs stay mounted (hidden via display:none), so opening a new tab's composer will restore the most recent draft from whichever tab last typed — bleeding drafts across threads.
🧪 Browser testing: Will run after this review — PR touches calendar and mail UI.
Code review by Builder.io
| onSuccess: () => { | ||
| setUndoAction(undo); | ||
| toast("Event updated", { | ||
| action: { label: "Undo", onClick: undo }, |
There was a problem hiding this comment.
🟡 Escape-during-flight re-opens editor and leaks the created event
When Escape is pressed before the createEvent POST resolves, handleQuickEditCancel(tempId) fires deleteEvent with the temp ID (rejected by the server — temp IDs don't exist), then onSuccess unconditionally calls setQuickEditEventId(realEvent.id), re-opening the editor the user just dismissed. The server event is never deleted. Fix: add a cancelledRef checked in onSuccess — if cancelled, delete the real ID instead of re-opening.
React with 👍 or 👎 to help me improve.
| }, []); | ||
|
|
||
| // Persist draft to localStorage so hot-reloads don't lose the prompt | ||
| const DRAFT_KEY = "an-composer-draft"; |
There was a problem hiding this comment.
🟡 Hardcoded draft key causes cross-tab draft contamination
DRAFT_KEY = "an-composer-draft" is shared by all TiptapComposer instances. MultiTabAssistantChat keeps inactive tabs mounted via display:none, so when a new tab's composer mounts its onCreate restores whatever the last active tab was typing — bleeding drafts across threads. Fix: accept a draftKey prop (e.g. threadId) and derive the key from it.
React with 👍 or 👎 to help me improve.
There was a problem hiding this comment.
Builder has reviewed your changes and found 3 potential issues.
Review Details
PR #154 Incremental Review — Calendar Undo & QuickEdit Enhancements
This update adds undo support to calendar event operations (delete, create, move) via a new use-undo.ts module-level stack, enhances QuickEditInput with a two-stage Delete key discard flow, adds optimistic updates to useDeleteEvent, and introduces a two-stage Cmd+A to the content editor. Risk: Standard.
Previously reported issues — all resolved ✅
All three comments from the earlier review (Postgres INTEGER overflow, quick-edit cache race, onBlur phantom events) were fixed and those threads have been resolved.
New issues found in this update:
🔴 Delete key + Enter silently discards typed event titles — pressing Delete on a non-empty QuickEditInput puts it into confirmDelete mode, but the mode indicator (placeholder text) is invisible while text is present. A user who presses Delete to edit their title then presses Enter to save will instead delete the newly created event with no warning.
🔴 Delete undo recreates a stripped copy instead of restoring the original — the undo snapshot captures only 7 basic fields (title, description, location, start, end, allDay, color) and calls createEvent.mutate. For Google Calendar events this creates a new standalone event, permanently discarding attendees, recurrence rules, Meet links, RSVP state, and the original Google event ID. Undo for a removeOnly delete (non-organizer declining an invite) creates a new personal event instead of re-accepting.
🧪 Browser testing: Will run after this review — PR touches calendar UI.
Code review by Builder.io
| onKeyDown={(e) => { | ||
| if (e.key === "Enter") { | ||
| e.preventDefault(); | ||
| if (confirmDelete) { | ||
| onCancel(eventId); | ||
| } else { | ||
| onSave(eventId, value); |
There was a problem hiding this comment.
🔴 Delete key + Enter silently deletes events with typed titles
When the user types a title and presses Delete (forward-delete), confirmDelete is set but the placeholder hint is hidden behind the text — so the mode change is invisible. Pressing Enter immediately after then calls onCancel(eventId) instead of saving, silently discarding the newly created event. The same issue exists in WeekView.tsx. Consider reserving the two-stage discard only for when the input is already empty, or using a visible UI affordance (border change, short toast) instead of altering the placeholder.
React with 👍 or 👎 to help me improve.
| // Snapshot for undo | ||
| const snapshot = { ...ev }; | ||
| const undo = () => { | ||
| createEvent.mutate({ | ||
| title: snapshot.title, | ||
| description: snapshot.description ?? "", | ||
| location: snapshot.location ?? "", | ||
| start: snapshot.start, | ||
| end: snapshot.end, | ||
| allDay: snapshot.allDay ?? false, | ||
| color: snapshot.color, |
There was a problem hiding this comment.
🔴 Delete undo recreates a stripped copy, not the original event
The undo closure calls createEvent.mutate with only 7 basic fields, creating a brand-new local event. For Google Calendar events this permanently discards attendees, recurrence rules, Meet/conferencing links, RSVP state, and the original googleEventId. For removeOnly deletes (non-organizer) it creates a new personal event instead of re-accepting the invite. Consider disabling undo for Google-synced or recurring events until a proper restore path exists.
React with 👍 or 👎 to help me improve.
| /** Push an undo action onto the stack. */ | ||
| export function setUndoAction(action: UndoEntry) { | ||
| undoStack.push(action); | ||
| notify(); |
There was a problem hiding this comment.
🟡 Undo stack grows unbounded; stale actions can fire long after toast expires
Each delete/move/create pushes to undoStack with no maximum size or expiry. If a Cmd+Z shortcut is wired to runUndo(), it can silently execute an undo action created hours earlier. Cap the stack (e.g., if (undoStack.length > 10) undoStack.shift()) and consider clearing it when the toast duration expires.
React with 👍 or 👎 to help me improve.
- Remove dangerous Delete-key-to-discard in QuickEditInput (was invisible when text was present, Enter would silently delete the event) - Delete undo now preserves full event snapshot (attendees, recurrence, conferenceData, etc.) instead of just 7 basic fields - Cap undo stack at 20 entries to prevent unbounded growth
There was a problem hiding this comment.
Browser testing: 4/10 passed
Test Results: 4/10 passed ⚠️
✅ TC-01: Load calendar at http://localhost:8082/ and verify it displays in day or week view (succeeded)
Evidence: 1 screenshot captured
⚠️ TC-02: Click on an empty time slot in day/week view and verify inline event input appears (couldnt_verify)
Steps: 1. Navigated to calendar at http://localhost:8082/
2. Switched to Day view (Wednesday, April 8, 2026)
3. Clicked on multiple time slots (9 AM, 10 AM areas)
4. Attempted via JavaScript to simulate clicks on the calendar grid overlay
5. Verified click handler exists in DOM but events not visually displayed
Failure: not_applicable — The calendar application does not appear to display inline event creation in day/week views in the current environment. All event creation was successful via the dialog-based 'New Event' button instead. The QuickEdit component exists in the code but was not triggered by time slot clicks in testing.
⚠️ TC-03: Type event title in inline input and press Enter; verify event is created and undo toast appears (couldnt_verify)
Failure: not_applicable — As with TC-02, the inline event creation flow was not observable in the UI. Event creation via dialog succeeded with undo toast confirmed (TC-07)."
⚠️ TC-04: Click the 'Undo' button in the toast; verify the event disappears from the calendar (couldnt_verify)
Failure: not_applicable — This test case depends on successfully triggering the inline event creation flow (TC-03), which was not observable in the testing environment."
⚠️ TC-05: Click on an empty time slot, then press Escape immediately without typing; verify no event is created (couldnt_verify)
Failure: not_applicable — Depends on inline event creation being observable (TC-02). The inline input component exists in code but was not triggered in testing environment."
⚠️ TC-06: Click on an empty time slot, do NOT type anything, then click elsewhere on calendar; verify no event remains (couldnt_verify)
Failure: not_applicable — Depends on inline event creation flow (TC-02). Not applicable in this environment where inline input was not observable."
✅ TC-07: Create event via 'New Event' button in dialog: set title 'Team Meeting', verify toast with Undo button appears and is styled as blue text link (succeeded)
Evidence: 1 screenshot captured
✅ TC-08: Create event, then click on it and delete it; verify delete toast with Undo button appears and is styled correctly (succeeded)
Evidence: 1 screenshot captured
✅ TC-09: Verify no JavaScript console errors during all above interactions (succeeded)
Evidence: 1 screenshot captured
⚠️ TC-10: Test rapid interactions: create event with inline edit, save title, then move it via drag/resize; verify all undo toasts appear correctly (couldnt_verify)
Steps: 1. Created events via dialog multiple times
2. Verified event creation API calls succeeded3. Confirmed toast notifications appeared4. Could not test inline creation or drag operations as those flows were not triggered
5. Console remained clean throughout operations"
Failure: not_applicable — This test depends on inline event creation (TC-02) and drag-and-drop operations which were not fully testable in the current environment. Dialog-based creation works with undo toasts (TC-07, TC-08), but the full rapid interaction scenario could not be executed."
Details
Thorough test session completed for calendar undo and QuickEdit features (PR #154). 3/10 test cases succeeded. 6/10 couldnt_verify due to environment limitations with inline event creation not being observable. 1/10 succeeded with console verification.
- Remove dangerous Delete-key-to-discard in QuickEditInput (was invisible when text was present, Enter would silently delete the event) - Delete undo now preserves full event snapshot (attendees, recurrence, conferenceData, etc.) instead of just 7 basic fields - Cap undo stack at 20 entries to prevent unbounded growth
…ed stack (#155) * fix(calendar): address PR #154 review feedback - Remove dangerous Delete-key-to-discard in QuickEditInput (was invisible when text was present, Enter would silently delete the event) - Delete undo now preserves full event snapshot (attendees, recurrence, conferenceData, etc.) instead of just 7 basic fields - Cap undo stack at 20 entries to prevent unbounded growth * fix: calendar quick-edit cleanup, agent panel & content editor improvements * feat(calendar): extract QuickEditPopover component + compose toolbar improvements * fix: AgentPanel loading stuck on error, agents saved as shared scope - Fix setLoading(false) never called on non-OK response (try→finally) - Save agents with shared:true so they're discoverable for @-mentions in production - Include concurrent mail template changes * fix: stop mail nitro task spam + integrations plugin crash on auto-mount - Remove Nitro scheduled tasks from mail vite.config (the setInterval-based scheduler in server plugins already handles job processing; Nitro tasks don't resolve in dev mode and spam ERR_MODULE_NOT_FOUND every 30s) - Fix integrations plugin crash when h3App is undefined (happens during auto-mount via framework request handler's fake nitroApp shim) * fix: 9 bugs + add slash commands and DEVELOPMENT.md Bugs fixed: - Calendar: event creation now shows full detail popover with editable title that auto-saves on dismiss, works in both popover and sidebar mode - Analytics: remove catch-all redirect that masked SSR errors and caused dashboard flash-then-redirect - Forms: fix open-in-new-tab by returning Response object instead of using H3 headers (Vite 8 SSR compat); remove broken publicFormSSR plugin - Integrations: refactor plugin to use getH3App() so routes actually mount - @-tagging: preserve query strings in framework request handler so mention search and resource scope params work - Files sidebar: same query string fix enables proper resource fetching - Backdrop filter: add -webkit- prefix and cssTarget for Safari compat - Cloudflare cold starts: add Smart Placement to all wrangler configs - Recruiting: add OrgSwitcher component to sidebar for multi-tenancy Features: - Add /clear, /new, /history, /help slash commands to agent chat - Add DEVELOPMENT.md with setup, workspace structure, and env var docs * fix: address PR review feedback — blur listener leak, toolbar dismiss, mail jobs - ComposeBubbleToolbar: use named handleBlur ref so editor.off actually removes the listener; add setTimeout check so link/AI inputs don't dismiss the toolbar when autoFocus fires - Add mail-jobs server plugin with setInterval scheduler for processJobs (snooze, scheduled-send) and processAutomations, replacing the Nitro tasks that were removed in f3df24c * fix: update mail-jobs plugin imports after tasks/ dir removed in main * ci: retrigger Cloudflare builds * fix: remove Smart Placement from slides worker (incompatible with CF config) * fix: createRequire patch regex fails on minified $ in variable names The CF Workers deploy patch for createRequire used (\w+) to capture the aliased variable name, but minifiers like Rolldown produce names like L$n where $ is valid in JS identifiers but not matched by \w. Changed to ([\w$]+) so the patch catches these and stubs them out.
Summary
isNull(orgId)to prevent ex-members from deleting org-scoped notescontact_frequencytable (was missing from PR feat(mail): smart contact sorting, snooze filtering, calendar fixes #153)Test plan
🤖 Generated with Claude Code