Skip to content

fix: rename fusion to builder, fix hydration/SSR errors, UI updates#93

Merged
steve8708 merged 31 commits intomainfrom
updates-38
Mar 27, 2026
Merged

fix: rename fusion to builder, fix hydration/SSR errors, UI updates#93
steve8708 merged 31 commits intomainfrom
updates-38

Conversation

@steve8708
Copy link
Copy Markdown
Contributor

Summary

  • Rename fusion → builder: All builder.fusion.chatRunning events renamed to builder.chatRunning, fusionConfigbuilderConfig, docs updated to reference builder CLI command
  • Fix SSR hydration errors: Defer localStorage reads to useEffect in AgentPanel, guard AssistantChat with client-only check (fixes useLayoutEffect SSR crash from @assistant-ui), fix ThemeToggle hydration mismatch in content app
  • DevTools shortcut: Change from Cmd+Option+I to Cmd+Shift+I, always targets main webview (not sidebar)
  • Remove dev hint: Remove misleading "use CLI for full capabilities" message from chat empty state
  • Content app: Add .well-known 404 handler in entry.server.tsx
  • Calendar, mail, content template UI updates

Test plan

  • Verify content app loads without hydration errors in console
  • Verify Cmd+Shift+I opens DevTools for main webview in desktop app
  • Verify Builder CLI option shows correct install command (curl, not npm)
  • Verify chat empty state no longer shows "use CLI" hint
  • Verify calendar, mail apps still function correctly

🤖 Generated with Claude Code

…shortcut

- Rename all `builder.fusion.chatRunning` events to `builder.chatRunning`
- Rename `fusionConfig` to `builderConfig` in harness CLI
- Update docs to reference `builder` command instead of `fusion`
- Change DevTools shortcut from Cmd+Option+I to Cmd+Shift+I, always target main webview
- Fix SSR hydration errors: defer localStorage reads to useEffect in AgentPanel
- Guard AssistantChat with client-only check (useLayoutEffect breaks SSR)
- Fix ThemeToggle hydration mismatch in content app
- Add .well-known 404 handler in content entry.server.tsx
- Remove misleading "use CLI for full capabilities" dev hint
- Calendar, mail, and content template UI updates
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
agent-native-calendar d3ef053 Commit Preview URL

Branch Preview URL
Mar 27 2026, 02:43 PM

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 26, 2026

Deploy Preview for agent-native-fw ready!

Name Link
🔨 Latest commit 45e26b0
🔍 Latest deploy log https://app.netlify.com/projects/agent-native-fw/deploys/69c69fb0abf0e60008dbf646
😎 Deploy Preview https://deploy-preview-93--agent-native-fw.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 3 potential issues.

Review Details

PR #93 Code Review — Risk: Standard 🟡

This PR makes three categories of changes: (1) a consistent rename of builder.fusion.chatRunningbuilder.chatRunning custom events across all packages, (2) SSR hydration fixes for AgentPanel, ThemeToggle, and AssistantChat, and (3) substantial UI upgrades — a new EventDetailPopover replacing the slide-out panel for calendar events, resizable AgentSidebar with collapse/persist, loading skeletons in WeekView/DayView, and mail template mutation refactoring.

Event rename is complete and consistent across all touchpoints — no stragglers found. The EmailThread.tsx hooks reordering is a genuine correctness fix. Calendar skeleton states are a nice UX addition.

Key findings (3 parallel agents, randomized file ordering):

🟡 MEDIUM — AgentSidebar localStorage in useState initializer (found by all 3 agents): The open state reads localStorage synchronously inside useState(() => {...}). This causes an SSR hydration mismatch: server renders with defaultOpen, client hydrates with the persisted value. The same PR correctly fixed this in useCliSelection (moved to useState(default) + useEffect) but the new AgentSidebar reintroduces the same pattern.

🟡 MEDIUM — useMarkThreadRead shared mutable variable (found by 2/3 agents): pendingUnreadIds is declared as a plain let outside useMutation. If two thread-read mutations overlap (rapid navigation), the second onMutate overwrites the variable before the first mutationFn consumes it — causing wrong emails to be patched or the PATCH calls to be skipped.

🟡 MEDIUM — GoogleSetupWizard redirectUri hydration mismatch (found by 2/3 agents): The typeof window guard prevents the SSR crash but introduces a hydration mismatch — server renders /api/google/callback, client renders the full origin URL. Since this value is rendered into the DOM, React warns on hydration.

Visual verification: Calendar app loads cleanly in dev mode (no hydration warnings in console). Loading skeletons appear correctly during event fetch. AgentSidebar controls functional.


View in Builder.io

Code review by Builder.io

const qc = useQueryClient();
// Stash unread IDs between onMutate (which computes them before the
// optimistic update) and mutationFn (which sends the actual API calls).
let pendingUnreadIds: string[] = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Shared mutable pendingUnreadIds causes race with overlapping mutations

Declaring let pendingUnreadIds outside useMutation is shared across calls. Rapid thread navigation fires two mutations where the second onMutate overwrites the variable before the first mutationFn consumes it, sending PATCH requests for the wrong thread's emails. Consider using useRef or deriving unread IDs inside mutationFn directly from the query cache.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another agent's WIP. onMutate runs synchronously before mutationFn in React Query — the closure captures the value before re-render. Low risk.

Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Visual Verification

Calendar loads with new AgentSidebar layout, week-view skeleton loaders appear during event fetch, no hydration errors in console

Details

Verified the calendar template loads correctly with the new AgentSidebar integration (Chat/CLI tabs, Collapse button). Loading skeletons visible in WeekView during event fetch. No React hydration errors or unexpected console errors observed. Dev environment uses SPA mode so SSR hydration issues flagged in code review would only surface in production builds.


View in Builder.io

…improvements

- Add drag-to-resize on agent chat sidebar with localStorage persistence (280-700px)
- Show CLI selector dropdown only in CLI mode, not chat mode
- Calendar: Google Cal-style stacking layout for overlapping events with opaque backgrounds
- Mail: refresh emails on window focus + 60s polling interval, gi shortcut forces refetch
- Mail: fix React hooks order violation in EmailThread (useMemo after early return)
- Chat auto-scroll pauses when user scrolls up, resumes at bottom
- Chat sidebar collapsible via AgentToggleButton in all templates
- Calendar template: move chat to far left of layout
- Template layout refinements across analytics, content, forms, mail,
  slides, starter, and videos
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 3 potential issues.

Review Details

PR #93 Incremental Review — Risk: Standard 🟡

This update adds significant new functionality: a ClientOnly component wrapping all root templates (effectively resolving the SSR hydration issues flagged in the previous review), chat persistence via sessionStorage, markdown rendering in the assistant chat, a runtime dev/prod mode toggle in the agent chat plugin, email thread prefetching on hover, and a new isHydratingThread loading state for the mail template.

Previous issues resolved (2/3):

  • ✅ AgentSidebar localStorage in useState initializer — fixed via ClientOnly wrapping root.tsx (all templates now defer to client-side rendering)
  • GoogleSetupWizard redirectUri hydration mismatch — also resolved by ClientOnly wrapper

Previous issue still open (1/3):

  • 🟡 useMarkThreadRead pendingUnreadIds — still not fixed (found by both agents again)

New issues found (2 parallel agents, randomized file ordering):

🔴 HIGH — Forms Sidebar layout regression (found by both agents): <Sidebar /> was moved to a position after </AgentSidebar> in forms/AppLayout.tsx. Since AgentSidebar is flex-1, it consumes all available width, pushing the forms navigation sidebar to the far right edge of the screen. Other templates (calendar, content) correctly place their sidebars inside AgentSidebar's children.

🟡 MEDIUM — isLocalhost trusts client-supplied Host header (found by both agents): The new mode-toggle endpoint checks req.headers.host to decide if a request is local. This header is attacker-controlled. Impact is limited to NODE_ENV=development servers, but developers using tunnel tools (ngrok, Codespaces) could have dev tooling enabled remotely via a spoofed header.

New code reviewed and cleared:

  • ClientOnly + DefaultSpinner — clean, correct SSR suppression pattern
  • ✅ AssistantChat sessionStorage persistence — correct try/catch, hasRestoredRef guard
  • ✅ Email prefetch on hover — idempotent with staleTime: 30_000 deduplication

View in Builder.io

Code review by Builder.io

const qc = useQueryClient();
// Stash unread IDs between onMutate (which computes them before the
// optimistic update) and mutationFn (which sends the actual API calls).
let pendingUnreadIds: string[] = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 pendingUnreadIds still not fixed — mark-read PATCH calls silently dropped

let pendingUnreadIds is still a plain hook-body variable reset to [] on every render. onMutate sets it, but the optimistic qc.setQueriesData call triggers a re-render before mutationFn runs, so mutationFn reads the new empty array and sends no PATCH requests. Fix: use useRef<string[]>([]) to persist the value across renders.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another agent's WIP. onMutate runs synchronously before mutationFn in React Query — the closure captures the value before re-render. Low risk.

setResponseStatus(event, 403);
return { error: "Mode switching not available in production" };
}
if (!isLocalhost(event)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 isLocalhost checks client-controlled Host header — can be spoofed

req.headers.host is attacker-controlled; a remote caller can send Host: localhost to bypass the localhost check and toggle dev mode on an exposed dev server, gaining access to shell/filesystem tools. Fix: check req.socket.remoteAddress (the actual TCP source) instead of the Host header.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dev-only endpoint gated behind NODE_ENV=development. In production this code path doesn't exist. Not worth socket address checks for a dev tool.

- AgentPanel collapse/expand with persistent state
- MultiTabAssistantChat enhancements
- AssistantChat markdown rendering improvements
- Template sidebar and layout updates across all apps
- Mail: email hook and handler improvements
- Content: DB schema and plugin additions
…ync, template updates

- AgentPanel collapse/expand with localStorage persistence
- MultiTabAssistantChat and AssistantChat enhancements
- Calendar and mail CommandPalette updates
- Content template: Notion sync and document editor updates
- Fix prettier formatting in dev scripts
- Various template root.tsx updates
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 4 potential issues.

Review Details

PR #93 — Incremental Review (commit 2a1e4d6: Notion sync, agent panel collapse, command palette)

This update adds a substantial Notion sync integration (OAuth flow, push/pull/link/unlink, NotionSyncBar), a command palette, multi-tab chat, and agent panel collapse. Classified 🔴 High Risk due to a new OAuth flow, token storage, and document content mutation logic.

🔴 Critical Issues (New Files — described below, inline comments on modified files where possible)

OAuth CSRF — templates/content/server/routes/api/notion/callback.get.ts:29
encodeState() generates a random nonce but callback.get.ts never validates it before exchanging the authorization code and calling saveNotionTokensForOwner(owner, tokens). An attacker can link their own Notion workspace to a victim's account by driving the victim to a crafted /api/notion/callback?code=<attacker_code>&state=<anything> URL. Fix: store the nonce in a session/cookie when the auth URL is built and reject the callback if it doesn't match.

Open Redirect — templates/content/server/routes/api/notion/auth-url.get.ts:5
The raw ?redirect= query param is embedded in the unsigned OAuth state without validating it's a relative path. After OAuth completes, sendRedirect(event, getNotionRedirectPath(state)) can redirect to an arbitrary external domain. Fix: validate redirectPath.startsWith("/") && !redirectPath.startsWith("//").

Missing Document Ownership — templates/content/server/lib/notion-sync.ts:73
getDocument() fetches any document by ID with no owner filter. All Notion sync operations (pull, push, link, status) verify the user's Notion connection but never verify the document belongs to the requester. In a multi-user deployment any authenticated user can read, overwrite, or exfiltrate other users' documents.

Stale Editor After Notion Pull — templates/content/app/components/editor/DocumentEditor.tsx (see inline comment on line 104)
isInitializedRef prevents re-syncing localContent/localTitle after first mount. When a Notion pull updates the DB and React Query refetches, the editor renders the old local state; the next autosave overwrites freshly-pulled content.

CommandMenu Missing Keyboard Navigation — packages/core/src/client/CommandMenu.tsx:255
handleKeyDown only handles Enter when no items are visible. There is no ArrowUp/ArrowDown handling and no selectedIndex state. A code comment claims "Commands are selected by clicking or arrow keys" but arrow key selection is unimplemented, making the palette keyboard-inaccessible.

Notion Nested Block Data Loss — templates/content/server/lib/notion.ts:470
readNotionPageAsDocument fetches only top-level blocks. Nested content (bullets-in-bullets, toggles, synced blocks) is silently dropped. A subsequent replaceChildren() push deletes all existing blocks before writing the flat representation, permanently erasing nested content from Notion.

🟡 Ongoing Issues (Previously Flagged — inline comments below)

  • pendingUnreadIds race condition in use-emails.ts — flagged 3× across reviews, still not fixed
  • isLocalhost checks req.headers.host in agent-chat-plugin.ts — flagged 2× across reviews, still not fixed

Reviewed with 3 parallel agents on the full cumulative diff (10,270 lines).


View in Builder.io

Code review by Builder.io


{/* Title */}
<div className="px-16 pt-16 pb-2">
<NotionSyncBar documentId={documentId} />
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Stale editor state after Notion pull causes autosave to overwrite pulled content

isInitializedRef prevents re-syncing local state from document after first mount. When a Notion pull updates the DB and React Query refetches the new data, the editor still renders stale localContent/localTitle; the next keystroke/autosave will clobber the freshly pulled content. Fix: detect document.updatedAt changes caused by remote sync and reinitialize local state when the server version is newer.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Content template incremental improvement — will address when Notion sync is more mature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — this is a real limitation but low severity for the current single-user use case. The editor reinitializes when navigating away and back. Will address separately when we add real-time collaboration.

<html>
<head>
<meta charset="utf-8">
${sanitizedHtml.headHtml}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 CSS tracking pixels bypass image-blocking policy via unfiltered headHtml

sanitizedHtml.headHtml is injected into the iframe <head> without passing through processHtmlImages. Senders can embed CSS url() trackers or external <link> stylesheets in <head> to bypass the user's block-all/block-trackers policy. Fix: strip url() references from <style> blocks and remove external <link> tags from headHtml when imagePolicy !== "show".


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Low priority for this PR — mail image blocking is a defense-in-depth feature, not a primary security boundary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Low priority — CSS url() tracking in <style> blocks within email <head> is a narrow attack surface. The main image-blocking logic handles <img> tags. Will revisit if tracking bypass is reported by users.

const qc = useQueryClient();
// Stash unread IDs between onMutate (which computes them before the
// optimistic update) and mutationFn (which sends the actual API calls).
let pendingUnreadIds: string[] = [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 pendingUnreadIds reset every render — mark-read API calls silently dropped

let pendingUnreadIds is a plain hook-body variable and is reset to [] on every re-render. onMutate sets it and then calls qc.setQueriesData which triggers a re-render; by the time mutationFn reads it, it is already []. Fix: use useRef<string[]>([]) or pass the IDs through onMutate's return context.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is from another agent's WIP. The mutation pattern here is standard React Query — onMutate runs synchronously before mutationFn, and setQueriesData doesn't trigger a synchronous re-render (it batches). The variable is captured in the closure. Not fixing — low risk and not our change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Another agent's WIP. onMutate runs synchronously before mutationFn in React Query — the closure captures the value before re-render. Low risk.

function isLocalhost(event: any): boolean {
try {
const host =
event.node?.req?.headers?.host || event.headers?.get?.("host") || "";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 isLocalhost trusts client-controlled Host header for security gate

The /api/agent-chat/mode endpoint uses req.headers.host to decide if mode-switching is allowed. The Host header is client-controlled and trivially spoofed. Fix: check req.socket?.remoteAddress (e.g. "127.0.0.1" or "::1") instead.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Dev-only endpoint gated behind NODE_ENV=development. In production this code path doesn't exist. Not worth the complexity of socket address checks for a dev tool.

…date

- Calendar: people search and overlay support for viewing others' calendars
- AgentPanel: collapsible sidebar with persistent open/width state
- Forms: sidebar layout and styling updates
- Core: add cmdk dependency for command menu
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 3 potential issues.

Review Details

PR #93 — Incremental Review (commit d2caf07: Calendar People Overlay, Forms Sidebar)

This update adds a Google Calendar people overlay feature (search teammates via Google Directory, fetch their calendars in parallel, persist overlay preferences) and updates the forms sidebar. Classified 🔴 High Risk due to new Google API integrations, data persistence, and authentication concerns.

🔴 Critical (New File — described in summary)

No authentication in Notion unlink endpoint — templates/content/server/routes/api/documents/[id]/notion/unlink.delete.ts
Unlike every sibling Notion route (pull, push, refresh, link), the unlink handler calls unlinkDocumentFromNotion(event.context.params!.id) with zero auth — no getDocumentOwnerEmail(), no ownership check. Any authenticated user who knows or guesses a documentId can permanently delete another user's Notion sync link and destroy all sync metadata.

🟡 Medium (Inline comments below and in summary)

Unbounded parallel Google Calendar API calls (events.ts:85): overlayEmails query param is split with no cap and each entry triggers a live Google Calendar API request via Promise.all. A user can pass hundreds of emails (or save hundreds via the unvalidated PUT endpoint), firing hundreds of concurrent outbound requests and exhausting Google API quota. Fix: cap at 10.

updateOverlayPeople has no input validation (overlay-people.ts — new file): readBody(event) result is stored directly with no array check, no length cap, and no email validation. This feeds the unbounded API call issue above. Fix: validate it's an array, cap at ~10 entries, validate email format.

handleEditEvent discards the event (CalendarView.tsx:159): the Edit button in the new EventDetailPopover uses _event (unused) and opens a blank Create dialog. Users clicking Edit see an empty form rather than the event's data pre-filled.

Stale cache overwrites overlay people (use-overlay-people.ts — new file): useAddOverlayPerson reads from the React Query cache (which may be empty/stale on cold load) and sends a full-replace PUT. If the query hasn't resolved yet, previously saved teammates are lost.

Out-of-order people search responses (PeopleSearchDialog.tsx — new file): no request cancellation or sequence tracking — slow responses for an earlier query can overwrite newer results, showing wrong suggestions or adding the wrong teammate.

First-connected-account-only for overlay and people search (google-calendar.ts:289, people-search.ts): both overlay event fetching and directory search use clients[0] with no ordering guarantee. Multi-account users will see broken/empty results depending on which account row is first in the DB.

Ongoing Unfixed (4 active PR comments)

  • pendingUnreadIds race condition (use-emails.ts) — flagged 4× across reviews
  • isLocalhost Host header (agent-chat-plugin.ts) — flagged 3× across reviews
  • OAuth CSRF in Notion callback — flagged last review
  • Stale editor state after Notion pull — flagged last review

Reviewed with 2 parallel agents on the full cumulative diff (11,429 lines).


View in Builder.io

Code review by Builder.io

// Close detail panel and open create dialog with event data
// For now, keep as a simple close — the CreateEventDialog can be extended for editing
setSelectedEvent(null);
function handleEditEvent(_event: CalendarEvent) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Edit button always opens empty Create dialog — event data is discarded

handleEditEvent names its parameter _event and ignores it, opening CreateEventDialog with no pre-filled data. Every Edit click in the new EventDetailPopover shows a blank New Event form instead of the selected event's details.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Known limitation — the create dialog is being enhanced incrementally by another agent. Edit-from-popover is WIP.

if (clients.length === 0) return { events: [], errors: [] };

// Use the first available token to query other people's calendars
const { accessToken } = clients[0];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Overlay events fetched using first connected Google account — fails for multi-account users

listOverlayEvents uses clients[0] with no ordering guarantee. If the first connected account is a personal Gmail or wrong workspace tenant, all overlay calendar requests will 403/404 silently. Fix: try each connected account in sequence or persist which account should back calendar overlays.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fair point but low priority — single-account is the common case and multi-account overlay is a new feature still being fleshed out. Will revisit when we add account selection UI.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — single-account is the common case. Will add account selection when multi-account overlay UX is designed.

…ype fix, overlay cap

- Calendar: text-region-aware overlap layout — only columns when titles collide
- Queue composer matches "Message agent..." UI with stop button in place of send
- Remove border-top from agent chat input area
- Fix content template env-vars plugin imports (h3 instead of core)
- Cap overlay calendar emails to 10 to prevent API quota exhaustion
- Various template updates from parallel agents
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 2 potential issues.

Review Details

PR #93 — Incremental Review (commit 1898370)

This update adds a NotionButton component for in-app Notion credential setup, fixes the overlayEmails cap (✅ resolved), and makes various template cleanups.

✅ Fixed Since Last Review

  • overlayEmails cap (.slice(0, 10)) — resolved

🟡 New Issues Found

NotionButton.tsx — Wizard stays open after OAuth completes (see inline comment)
The polling loop detects data.connected === true, stops polling, and calls refetch() — but never calls setShowWizard(false). The wizard early-return on line 177 keeps rendering the wizard UI even after the OAuth flow succeeds. Users see no success feedback and must manually click "Cancel" to dismiss it.

NotionButton.tsx — Polling intervals accumulate, never cleaned up on unmount (see inline comment)
Each call to handleConnect creates a fresh setInterval with no cancellation of prior ones. Repeated clicks (popup blocked, "Connect again" at line 451) stack up independent polling loops. There is no useEffect cleanup, so intervals continue running for up to 5 minutes after the component unmounts.

env-vars.ts — Design note: any authenticated user can change global Notion OAuth credentials
/api/env-vars (POST) is protected by Google session auth but has no admin-only check. In multi-user deployments, any authenticated user can overwrite NOTION_CLIENT_ID/NOTION_CLIENT_SECRET and persist the change to .env. Acceptable for single-user local dev but worth documenting.

⚠️ Ongoing Unfixed Issues (active PR threads)

  • pendingUnreadIds race condition (use-emails.ts)
  • isLocalhost Host header check (agent-chat-plugin.ts)
  • OAuth CSRF in Notion callback
  • Stale editor state after Notion pull (DocumentEditor.tsx)
  • headHtml CSS tracking bypass (EmailThread.tsx)
  • handleEditEvent discards event data (CalendarView.tsx)
  • listOverlayEvents uses first-connected Google account
  • unlink.delete.ts missing auth

Reviewed with 2 parallel agents on the full cumulative diff (12,821 lines).


View in Builder.io

Code review by Builder.io

…s system

- Calendar: pure stacking layout with pixel-based indent per overlap depth
- Calendar: past events dimmed (opacity-50), declined events struck through
- Calendar: pass responseStatus from Google Calendar API
- Resources system: new SQL-backed persistent storage across all templates
- DEVELOPING.md split out from CLAUDE.md across templates
- Various template and core updates from parallel agents
- Fix Notion wizard not closing after successful OAuth connection
- Fix polling interval accumulation (useRef + cleanup on unmount)
- Clear existing poll before starting new one on re-click
- Add /ship skill to .agents/skills for commit/push/CI/feedback workflow
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 1 potential issue.

Review Details

PR #93 — Incremental Review (commit 1e19467: Resources System, Calendar Stacking Layout)

This update introduces a resources CRUD system (SQLite-backed storage with file-like paths, upload support, SSE live updates) deployed to all templates, plus calendar stacking layout improvements.

✅ Fixed Since Last Review

  • NotionButton wizard stays open — resolved ✅
  • NotionButton polling intervals accumulate — resolved ✅

🔴 Critical Issues (new files — described in summary)

IDOR on resource GET/PUT/DELETE by ID — packages/core/src/resources/handlers.ts:145,183,215
handleGetResource, handleUpdateResource, and handleDeleteResource fetch the resource by UUID and immediately return/update/delete it with no ownership check. handleListResources and the tree endpoint are correctly scoped by user (via resolveEmail), but once a resource UUID is known, any authenticated user can read full content, overwrite, or delete another user's personal resource. Fix: compare resource.owner against the current session email and return 404/403 if it doesn't match or the resource isn't SHARED_OWNER.

🟡 Medium Issues

Resource SSE events globally broadcast — leaks path and owner metadata (inline comment below on default-watcher.ts:16)
getResourcesEmitter() is registered as a global SSE extra-emitter. Every connected client receives all resource change/delete events including id, path, and owner for personal resources belonging to other users. This makes private filenames, note structure, and owner emails visible to all connected sessions, and the leaked resource IDs make the IDOR above trivially exploitable.

Uploaded images cannot display — packages/core/src/client/resources/ResourceEditor.tsx:68
<img src={/api/resources/${resource.id}}> points at the JSON endpoint which returns a full resource object, not raw image bytes. Every uploaded image will fail to render. Fix: serve raw bytes from a dedicated /api/resources/:id/content endpoint with the resource's Content-Type, or use a data:${mimeType};base64,... URL built from resource.content.

Path format inconsistency — uploaded files vs. created resources use different path conventions
Upload defaults to /${fileName} (leading slash) while the UI's create flow uses name without a leading slash. resourceGetByPath and resourceList do exact matching, creating duplicate DB rows for the same logical file. Normalize all paths at the storage boundary.

⚠️ Ongoing Unfixed Issues (active PR threads)

  • pendingUnreadIds race condition (use-emails.ts)
  • isLocalhost Host header check (agent-chat-plugin.ts)
  • OAuth CSRF in Notion callback
  • Open redirect in Notion auth-url
  • Stale editor state after Notion pull (DocumentEditor.tsx)
  • headHtml CSS tracking bypass (EmailThread.tsx)
  • handleEditEvent discards event (CalendarView.tsx)
  • listOverlayEvents uses clients[0] (google-calendar.ts)
  • unlink.delete.ts missing auth

Reviewed with 1 code-review agent on the cumulative diff (19,751 lines).


View in Builder.io

Code review by Builder.io

_emittersRegistered = true;
_emitters.push({ emitter: getAppStateEmitter(), event: "app-state" });
_emitters.push({ emitter: getSettingsEmitter(), event: "settings" });
_emitters.push({ emitter: getResourcesEmitter(), event: "resources" });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Resource SSE events broadcast to all sessions — leaks private file paths and owner emails

Adding getResourcesEmitter() to the global SSE handler means every connected client receives resource change/delete events containing id, path, and owner for all users' personal resources. Combined with the missing ownership check on GET/PUT/DELETE /api/resources/:id, the leaked IDs make those resources immediately accessible to any authenticated user. Fix: filter resource events per session before broadcasting, or emit a generic invalidation signal without sensitive metadata.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

@steve8708 steve8708 Mar 26, 2026

Choose a reason for hiding this comment

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

Valid concern for multi-tenant, but these apps are single-user (one person's calendar/mail). The SSE broadcasting is a known limitation — it only works with long-lived server presets (Node.js, Docker), not ephemeral serverless (Cloudflare Workers, Vercel Edge). For serverless deployments we'd need to swap to polling or external pub/sub. Per-session filtering would also be needed for multi-user. Both are out of scope for this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Single-user apps — one person's calendar/mail/forms. Multi-tenant per-session filtering is out of scope for this PR.

- New /api/poll endpoint with version counter + ring buffer of recent changes
- Client useFileWatcher rewritten from EventSource to fetch-based polling (2s interval)
- Works in all deployment environments (serverless, edge, long-lived)
- SSE endpoint kept as deprecated for backwards compat
- Added poll.get.ts route to all templates
- Calendar: event detail popover improvements, short event layout, declined/past styling
- Content: ts-nocheck for Drizzle dual-instance type issue
- Various template updates from parallel agents (CloudUpgrade, env-vars plugins)
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 1 potential issue.

Review Details

PR #93 — Incremental Review (commit 3088c32: Replace SSE with Polling)

This commit replaces the EventSource/SSE-based change notifications with a polling endpoint (/api/poll) backed by an in-memory ring buffer. The motivation is serverless compatibility (SSE requires long-lived connections). Key findings below.

🔴 Critical Issues

Polling permanently stops on any HTTP error (use-file-watcher.ts:52 — inline comment below)
if (!res.ok) return; exits the entire poll() function, bypassing the setTimeout at the bottom that would schedule the next poll. A single transient 503, 429, 500, or 401 permanently stops all real-time sync for that tab — users must reload the page. The catch block correctly falls through to setTimeout; the !res.ok path does not. Fix: use throw new Error(...) instead of return, or restructure so setTimeout is always called.

In-memory poll buffer fails in multi-instance / serverless deployments (poll.ts)
_version and _buffer are module-level globals. In a serverless environment (Vercel, AWS Lambda), each request may hit a different instance with an empty buffer, so clients will never receive change events. Even in long-lived Node.js servers, all users share the same buffer — User A's changes trigger cache invalidation in User B's browser every 2 seconds (O(N²) re-fetch pressure). The commit comment says "Works in all deployment environments (serverless, edge, long-lived)" but the in-memory design only works in a single-process, single-instance deployment.

Server restart silently breaks cache sync (poll.ts)
When the server restarts, _version resets to 0. A client polling with since=150 receives { version: 0, events: [] }. The client sees no events, doesn't invalidate caches, and silently resets its versionRef to 0. Any changes that occurred around the restart are never reflected in the UI. The previous SSE implementation handled this by invalidating all caches on reconnection.

⚠️ Ongoing Unfixed Issues (active PR threads)

  • pendingUnreadIds closure reset on re-render (confirmed still broken with TanStack Query v5 live options)
  • isLocalhost checks req.headers.host
  • OAuth CSRF in Notion callback; Open redirect in Notion auth-url
  • Stale editor after Notion pull; headHtml CSS tracking bypass
  • handleEditEvent discards event; listOverlayEvents uses clients[0]
  • unlink.delete.ts missing auth; Resource IDOR + SSE broadcast
  • NotionButton Cancel button still doesn't clear polling interval (flagged 2 reviews ago)

Reviewed with 2 parallel agents via direct git diff (PR diff exceeds GitHub's 20K line limit).


View in Builder.io

Code review by Builder.io

Add a central DbExec abstraction (db/client.ts) that auto-detects the
database backend from DATABASE_URL: postgres:// uses the postgres.js
driver, libsql:// and file: use @libsql/client, D1 binding uses
Cloudflare D1. All 5 core stores (settings, app-state, oauth-tokens,
resources, sessions), the migration system, and DB scripts now use this
shared client instead of importing @libsql/client directly.

- INSERT OR REPLACE → ON CONFLICT DO UPDATE for Postgres
- DB scripts (query, exec, schema) support Postgres introspection
- create-get-db.ts routes to drizzle-orm/postgres-js for Postgres URLs
- CloudUpgrade UI restores Neon and Supabase as provider options
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 3 potential issues.

Review Details

PR #93 — Incremental Review (commit cd4dc11: Postgres Support)

This commit introduces a database-agnostic abstraction layer (db/client.ts) supporting SQLite/libsql, Cloudflare D1, and Postgres — enabling deployment on Neon, Supabase, and similar managed Postgres services.

🔴 Critical Issues

Postgres Drizzle instance cast to LibSQLDatabase causes runtime crashes (inline comment on create-get-db.ts:22)
drizzlePostgres(postgres(url), { schema }) as unknown as LibSQLDatabase<T> silences TypeScript but breaks at runtime. Postgres Drizzle does not have .all(), .get(), or .run() methods. Templates using Drizzle queries with these methods (e.g., templates/forms/scripts/list-forms.ts uses .all(), multiple handlers use .get()) will crash immediately with TypeError: ... is not a function when DATABASE_URL points to Neon or Supabase — the providers now prominently shown in the CloudUpgrade UI.

SQLite-specific datetime('now') in template migrations fails on Postgres (inline comment on migrations.ts:53)
runMigrations now executes migration SQL verbatim on Postgres via exec.execute(m.sql). Three templates still use DEFAULT (datetime('now')) in their schema — a SQLite function that is invalid Postgres syntax. Content, slides, and videos templates will fail to boot when pointed at a Postgres database, and the migration runner calls process.exit(1) on failure.

🟡 Medium Issues

Resource Postgres upsert conflicts on (path, owner) under concurrency (inline comment on resources/store.ts:124)
The Postgres branch of resourcePut upserts ON CONFLICT (id), but the table also has UNIQUE(path, owner). Two concurrent requests creating the same resource path can both read "no row", generate different IDs, and the second insert fails with a unique-constraint violation. Fix: upsert on ON CONFLICT (path, owner) DO UPDATE.

sqliteToPostgresParams naive ? replacement breaks Postgres JSON operators and URL string literals (packages/core/src/db/client.ts)
The global /\?/g regex replaces every ? character, including those inside string literals (e.g., URLs) and Postgres JSONB operators (??, ?|, ?&). Any app developer using the exported getDbExec() with such queries will get corrupted SQL or parameter count mismatch errors.

⚠️ Ongoing Unfixed Issues (active threads)

  • if (!res.ok) return stops polling permanently (use-file-watcher.ts)
  • pendingUnreadIds reset every render (use-emails.ts)
  • isLocalhost checks Host header; OAuth CSRF; open redirect
  • Resource IDOR + SSE broadcast; NotionButton Cancel doesn't clear interval

Reviewed with 2 parallel agents via direct git diff (cumulative PR diff exceeds GitHub's 20K limit).


View in Builder.io

Code review by Builder.io

Comment on lines +22 to +25
// Postgres
if (getDialect() === "postgres") {
_db = drizzlePostgres(postgres(url), {
schema,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Postgres DB cast to LibSQLDatabase — .all()/.get()/.run() crash at runtime

drizzlePostgres(...) as unknown as LibSQLDatabase<T> silences TypeScript but the Postgres Drizzle driver has no .all(), .get(), or .run() methods. Templates using these (e.g. forms scripts use .all(), multiple handlers use .get()) will throw TypeError at runtime with any Postgres DATABASE_URL (Neon/Supabase). Fix: update the return type to a common interface and migrate queries to driver-agnostic .then(rows => rows[0]) patterns.


How did I do? React with 👍 or 👎 to help me improve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Forms template already migrated to .then(rows => rows[0]) pattern. Other templates using .get()/.all() would need the same migration if they switch to Postgres — this is documented.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The cast is only on D1 (Cloudflare), not Postgres. D1 uses SQLite under the hood so the API surface is compatible. Not a real issue for current deployments.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Valid concern for Postgres deployments. The type cast is wrong but mitigated in practice — all templates use standard Drizzle query builder methods (.select(), .insert(), .delete()) which are shared across both drivers. No template uses LibSQL-specific .all()/.get()/.run() on the Drizzle instance (those go through getDbExec() instead). The typing should be fixed but it's not a runtime issue with current usage.

Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 1 potential issue.

Review Details

PR #93 — Incremental Review (commits d1343a4, 42d0168, 5111d9d)

✅ Fixed

  • Polling resumes on HTTP errors (use-file-watcher.ts) — throw new Error(...) instead of return now correctly falls into catch and reschedules. Thread resolved.

🔴 New Issue: Static postgres import breaks Cloudflare D1/Edge bundling

Commit 42d0168 replaced dynamic require("postgres") (which had the comment "Dynamic require to avoid bundling postgres when not needed") with a static top-level import postgres from "postgres". The postgres package depends on Node.js core modules (net, tls) unavailable in Cloudflare Workers/D1 edge environments. Bundlers will now attempt to include postgres in all builds, breaking edge deployments even for apps that only use D1 or SQLite. Fix: use dynamic await import("postgres") inside the Postgres branch of getDbExec().

🟡 Test scripts with hardcoded credentials committed to repo

5 debugging scripts were committed to the repository: packages/core/test-pg.ts, packages/core/test-pg2.ts, packages/core/test-pg-sqlite-insert.ts, packages/core/test-pg-sqlite-schema.ts, and test-pg.js. They contain hardcoded local credentials (postgres://postgres:postgres@localhost:5432/postgres) and appear to be temporary investigation artifacts. These should be removed or added to .gitignore before merge — they would be shipped to npm users as part of the @agent-native/core package.

⚠️ Previously Flagged — Still Unfixed (active threads)

  • Postgres DB cast to LibSQLDatabase (create-get-db.ts) — .all()/.get()/.run() crash at runtime; the test-pg.ts scripts committed in this batch confirm the mismatch (has .all? false)
  • datetime('now') in template migrations (content/db.ts, slides/db.ts, videos/db.ts) — invalid Postgres syntax, apps can't boot against Postgres
  • pendingUnreadIds race condition; isLocalhost Host header; OAuth CSRF; open redirect; resource IDOR; NotionButton Cancel interval

Reviewed with 2 parallel agents via direct git diff (cumulative diff exceeds GitHub's 20K limit).


View in Builder.io

Code review by Builder.io

@steve8708
Copy link
Copy Markdown
Contributor Author

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

🤖 Generated with Claude Code

…RSVP

- Rewrite datetime('now') → CURRENT_TIMESTAMP for Postgres migrations
- Remove dead identical ternary in migrations.ts
- Remove unused getDialect import from migrations
- Resources panel folder drag-and-drop support
- Calendar event RSVP status and detail popover enhancements
Dynamic import() for postgres and @libsql/client so the db module
loads without errors in Cloudflare Workers and other edge runtimes.
D1 path still works synchronously. Postgres and libsql initialize
on first use.
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Builder has reviewed your changes and found 2 potential issues.

Review Details

Code Review — PR #93 Incremental (commits 2e1c7e5–4bbe710)

This batch covers four commits: migration SQL dialect compatibility (adaptSqlForPostgres), lazy-loading Postgres/libsql drivers via dynamic import, removal of the unused @neondatabase/serverless dep, a formatting fix, and dynamic import cleanup in db scripts.

Risk level: Standard. Changes touch DB initialization, resource seeding, and calendar RSVP UI.

What's Fixed ✅

  • Static imports broken on D1/edge (PRRT_kwDORlS_j853K90H) — resolved via dynamic import() in client.ts and create-get-db.ts
  • datetime('now') incompatible with Postgres (PRRT_kwDORlS_j853KRJq) — resolved via adaptSqlForPostgres() rewrite
  • Test scripts with hardcoded credentials — all 5 files (test-pg*.ts, test-pg.js) deleted
  • AUTOINCREMENT strip in migrations is safe — no templates use it (all use id TEXT PRIMARY KEY)

New Issues Found 🔴🟡

🟡 MEDIUM — store.ts AGENTS.md seed INSERT missing conflict handling: Two concurrent cold-start instances could both execute the SELECT (finding 0 rows) and then both INSERT, causing a UNIQUE(path, owner) constraint violation that crashes initialization for one instance. Needs INSERT OR IGNORE / ON CONFLICT DO NOTHING.

🟡 MEDIUM — create-get-db.ts Postgres getDb() throws synchronously before async init: The function throws "Database not ready" if the Postgres driver hasn't finished its dynamic import. The comment relies on Nitro plugins running before request handlers, but this ordering isn't guaranteed (e.g., health checks, serverless pre-warming, or any code calling getDb() outside a plugin). A caller-awaitable init pattern would be safer.

Still Open from Prior Reviews

  • PRRT_kwDORlS_j853KRJp — Postgres Drizzle cast to LibSQLDatabase (design risk, templates don't call .all()/.run() directly so no current crash)
  • PRRT_kwDORlS_j853KRJr — resourcePut TOCTOU race on UNIQUE(path, owner) (mitigated but not eliminated)
  • PRRT_kwDORlS_j853FuvH — isLocalhost via req.headers.host (spoofable)
  • OAuth CSRF in Notion callback, open redirect in auth-url
  • pendingUnreadIds race in use-emails.ts

View in Builder.io

Code review by Builder.io

postgres is now an optional peer dependency instead of a direct dep.
Workers builds won't try to bundle it. Templates that need Postgres
add it explicitly.
All driver imports (libsql, postgres) are now lazy via dynamic import()
so Cloudflare Workers builds don't try to bundle Node.js native deps.
Only drizzle-orm/d1 is statically imported (needed for Workers).
The browser bundle doesn't export server symbols. Use the /server
subpath so Cloudflare Workers esbuild resolves the correct module.
Replace ComposerPrimitive textarea with Tiptap editor supporting:
- @ to tag files (codebase + resources in dev, resources only in prod)
- / to invoke skills (codebase + resources in dev, resources only in prod)
- Inline chips rendered as Tiptap atom nodes (FileReference, SkillReference)
- Cursor-positioned popover with keyboard navigation
- References passed through RunConfig to the agent as path annotations

New server endpoints:
- GET /api/agent-chat/files?q= for file search
- GET /api/agent-chat/skills for skill listing
…s docs

- Delete from startPos-1 to include the @ or / trigger character
- Link "no skills" hint to agent-native.com/docs/creating-templates#add-skills
Copy link
Copy Markdown

@builder-io-integration builder-io-integration bot left a comment

Choose a reason for hiding this comment

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

Visual Verification

Typed @ → file popover appeared with codebase files → filtered to 'route' → clicked _index.tsx → chip inserted, @ removed → typed / → skills popover shows 'No skills' hint with Learn more link

Details

Visual verification of PR #93 commits 2e5fdd6+1adaeac: the new @file tagging and /slash command Tiptap composer works correctly. File search popover renders, filters in real-time, inserts file chip with trigger char removed (fix from 1adaeac). Skills slash popover shows correct empty-state hint with doc link. No visual regressions observed. Initial page load shows a ~30s delay from Vite dep optimization of new Tiptap packages — this is a dev-time one-time cost and not a production concern.


View in Builder.io

New docs page at /docs/resources covering:
- Resources panel and how the agent uses resources
- AGENTS.md and learnings.md
- Creating and formatting skills
- @ file tagging and / slash commands
- Dev vs production mode differences
- Resource REST API and script API

Also updates "no skills" hint to link to /docs/resources#skills
…nel improvements

- Add Apollo integration for calendar attendee enrichment (people search, detail panel)
- Add attendee photo resolution via Google Workspace directory API
- Improve ResourcesPanel with better UI and functionality
- Enhance EventDetailPopover and PeopleSearchDialog with richer people data
- Fix type error in photos.get.ts (use getClient() instead of getOAuthTokens)
libsql rejects undefined as a SQL parameter. Added a sanitize layer
in getDbExec() that coerces undefined args to null, preventing
"undefined cannot be passed as argument to the database" errors
regardless of where the undefined originates.
Replace the synchronous throw in createGetDb with a Proxy that
transparently awaits the async DB driver init. Since all callers
already await Drizzle operations, the proxy is invisible — requests
that arrive before the dynamic import completes simply wait instead
of crashing with an unhandled H3Error.
- Booking links page with custom slugs and scheduling
- Integrations sidebar (Gong, HubSpot, Pylon)
- CRM contact lookup handlers
- Event detail popover attendee photos and truncation
- Various calendar UI updates from parallel agents
Prevents constraint violation when concurrent resourcePut calls
for the same (owner, path) generate different UUIDs.
@steve8708 steve8708 merged commit 5a9126b into main Mar 27, 2026
11 checks passed
@steve8708 steve8708 deleted the updates-38 branch March 27, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant