Skip to content

feat(meet): redesign Meetings — platform selector + history master-detail#4308

Merged
graycyrus merged 22 commits into
tinyhumansai:mainfrom
graycyrus:feat/meetings-redesign
Jun 30, 2026
Merged

feat(meet): redesign Meetings — platform selector + history master-detail#4308
graycyrus merged 22 commits into
tinyhumansai:mainfrom
graycyrus:feat/meetings-redesign

Conversation

@graycyrus

@graycyrus graycyrus commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Issues

Closes #4275 — Redesign Meetings UI (multi-platform, auto-fill, beta banner)
Closes #4302 — Support non-Google-Meet links (Zoom / Teams / Webex platform selector)
Closes #4301 — Forward detected meeting platform in the bot:join payload

Advances #3505 (Meeting Assistant epic) — proactive auto-join (calendar watch_calendar → heartbeat), notes & summaries surfaced.
Related #4305 — surfaces auto_summarize_policy ("Post-call summary") in the new Meeting Defaults drawer.
Related #4304 — active-meeting view shows live bot replies/captions; full live-transcript console is a deferred follow-up (not closed by this PR).

Summary

Full redesign of the Meetings feature ("Send OpenHuman to a meeting"). The meetings tab becomes a full-width, multi-platform workspace and finally surfaces the calendar + transcript/summary data the backend already captures. Shipped in four reviewable phases (one commit each).

Phase 1A — Composer + platform selector

  • New app/src/components/meetings/ tree: MeetingsPage, MeetComposer, PlatformChips, ActiveMeetingBanner, meetingUtils.
  • Platform is no longer hardcoded — Google Meet / Zoom / Teams / Webex chips drive the URL placeholder, the display-name auto-fill source, and the join payload (backend already supports all four via Recall.ai). Chips are a pure selector; no account connection required to send the bot.
  • MeetingBotsCard is now a back-compat re-export shim.

Phase 1B — History master-detail + rich transcript

  • Replaces the flat expandable recent-calls list with a searchable, platform-filtered, date-grouped call rail beside a lazy-loading detail pane.
  • TranscriptViewer parses the [MM:SS] [Name] line prefix into timestamped, speaker-labeled, role-colored lines, with Copy + Download.
  • ActionItemChecklist surfaces executable action items with a "Run with OpenHuman" action. Old RecentCallsSection removed.

Phase 2 — Upcoming table + meet_list_upcoming RPC

  • Full-width Upcoming table between composer and history: relative/absolute time, title, platform (inferred from the conferencing link), invitee count, an Auto/Ask/Skip join-policy toggle, and a Join-now action on imminent meetings.
  • New openhuman.meet_list_upcoming JSON-RPC fetches upcoming events via the existing Composio GOOGLECALENDAR_EVENTS_LIST path (reused, not a heartbeat refactor), extracts the join link from hangoutLink / conferenceData / description, infers the platform, and returns typed records.

Phase 3 — Join-policy persistence + Defaults drawer

  • The Auto/Ask/Skip policy now persists and drives auto-join, resolved per-event override > per-platform default > global default.
  • Backend: new meeting_event_policies SQLite table + meet_set_event_policy / meet_get_event_policies RPCs; platform_auto_join_policies map added to MeetConfig (serde-defaulted, backward compatible); resolve_effective_join_policy feeds both meet_list_upcoming and the calendar auto-join candidate handler (policy source only — the scheduler is untouched; the heartbeat planner threads the calendar_event_id through).
  • Frontend: MeetDefaultsDrawer (gear button) for global + per-platform defaults; the Upcoming toggle persists via setEventPolicy with optimistic update + revert.

Notes / follow-ups

  • "Run with OpenHuman" navigates to /chat without prefilling the action text yet (no trivial prefill mechanism) — TODO left inline.
  • Upcoming is Google Calendar–sourced; Outlook is a natural follow-up.
  • Stacks on the name-autofill change (merged to main).

Test plan

  • pnpm typecheck clean
  • Frontend unit tests pass (1178)
  • GGML_NATIVE=OFF cargo check clean
  • Rust unit + json_rpc_e2e policy round-trip pass
  • pnpm i18n:check parity clean across all 14 locales (new keys fully translated)
  • Lint clean (no new errors; pre-existing warnings unchanged)
  • Build verified locally (app builds & launches); full CI build/E2E runs on this PR

Summary by CodeRabbit

  • New Features
    • Added a full Meetings experience: upcoming meetings table, meeting history rail + detail pane, transcript viewing (copy/download), and a redesigned meeting composer with platform selection (including Webex).
    • Added “Meeting Defaults” to configure global and per-platform auto-join (auto/ask/skip), with an optional calendar-watch hint; join behavior can also vary per meeting.
  • Bug Fixes
    • Improved reliability with retry and stale-response protection across history and upcoming flows, plus stricter meeting-link validation and safer join-policy updates.

Prefill 'Your name in this meeting' from the Composio account that best
matches the meeting platform (own account -> mailbox -> blank). Composio
exposes only an account email, so derive a 'First Last' display name from
the local part. Only prefills while the field is untouched, so it never
clobbers manual input.
Phase 1A of the meetings redesign. Replace the single-platform inline
form with a hero composer + Google Meet/Zoom/Teams/Webex platform chips,
un-hardcoding the platform so the selection flows through to the join
payload, URL placeholder, and display-name auto-fill source. Extract the
new components into app/src/components/meetings/ (MeetingsPage,
MeetComposer, PlatformChips, ActiveMeetingBanner, meetingUtils);
MeetingBotsCard becomes a back-compat re-export shim. The chips are a
pure selector — no connection is required to send the bot.
Phase 1B of the meetings redesign. Replace the flat expandable recent-
calls list with a master-detail history view: a searchable, platform-
filtered, date-grouped call rail beside a detail pane that lazy-loads
each call's summary and transcript. Add a rich TranscriptViewer that
parses the [MM:SS] [Name] line prefix into timestamped, speaker-labeled,
role-colored lines with copy/download, and an ActionItemChecklist that
surfaces executable items with a Run-with-OpenHuman action. Infer the
platform from the meeting URL for per-row icons. Removes the old
RecentCallsSection (now only used here).
# Conflicts:
#	app/src/components/skills/MeetingBotsCard.tsx
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds upcoming-meeting retrieval, per-event and per-platform join-policy handling, strict meeting URL validation, and a redesigned meetings UI with composer, active banner, upcoming table, history views, defaults drawer, and translated meeting-bot strings.

Changes

Meetings Phase 3: Upcoming Meetings, Policy Overrides, and UI Redesign

Layer / File(s) Summary
Config schema and event policy storage
src/openhuman/config/schema/meet.rs, src/openhuman/config/ops/ui.rs, src/openhuman/config/schemas/helpers.rs, src/openhuman/config/schemas/controllers.rs, app/src/utils/tauriCommands/config.ts, src/openhuman/agent_meetings/store.rs
Adds platform_auto_join_policies to meet settings shapes, persists per-event policy overrides in SQLite, and wires get/update handlers to read and write the new data.
Meeting URL validation and calendar join policy resolution
src/openhuman/agent_meetings/ops.rs, src/openhuman/agent_meetings/calendar.rs, src/openhuman/subconscious/heartbeat/planner/mod.rs
Adds strict meeting-host allowlisting, shared URL and event-id extraction helpers, tiered join-policy resolution, calendar candidate hardening, platform-aware auto-join payloads, and planner wiring for watch-calendar handling.
Upcoming-meeting RPCs and schemas
src/openhuman/agent_meetings/types.rs, src/openhuman/agent_meetings/upcoming.rs, src/openhuman/agent_meetings/ops.rs, src/openhuman/agent_meetings/schemas.rs, src/core/all.rs, tests/json_rpc_e2e.rs
Adds upcoming-meeting request/response types, a Composio-backed upcoming-meeting fetcher, RPC handlers for listing and policy management, schema registration, and JSON-RPC end-to-end coverage.
Meeting service helpers and shared utilities
app/src/components/composio/toolkitMeta.tsx, app/src/components/meetings/meetingUtils.ts, app/src/components/meetings/useUpcomingMeetings.ts, app/src/services/meetCallService.ts, app/src/services/__tests__/meetCallService.test.ts, app/src/components/meetings/__tests__/meetingUtils.test.ts, app/src/components/meetings/__tests__/useUpcomingMeetings.test.ts
Adds transcript parsing, upcoming-meeting service calls, shared meeting platform metadata, display-name resolution, platform inference, Composio logo export, and the polling hook used by the meetings UI.
Join policy and platform selector primitives
app/src/components/meetings/JoinPolicyToggle.tsx, app/src/components/meetings/PlatformChips.tsx, app/src/components/meetings/__tests__/JoinPolicyToggle.test.tsx, app/src/components/meetings/__tests__/PlatformChips.test.tsx
Adds the join-policy radio toggle and the platform chip selector with logo fallback, keyboard handling, ARIA state, and component tests.
Composer, active banner, and meetings page orchestration
app/src/components/meetings/MeetComposer.tsx, app/src/components/meetings/ActiveMeetingBanner.tsx, app/src/components/meetings/MeetingsPage.tsx, app/src/components/meetings/__tests__/MeetComposer.test.tsx, app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx, app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
Adds the redesigned meeting composer, the active meeting banner, and the meetings page that switches between them, fetches watch-calendar hints, opens defaults settings, and composes upcoming and history sections.
Upcoming meetings table and defaults drawer
app/src/components/meetings/UpcomingTable.tsx, app/src/components/meetings/MeetDefaultsDrawer.tsx, app/src/components/meetings/__tests__/UpcomingTable.test.tsx, app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx, app/src/components/settings/panels/__tests__/MeetingSettingsPanel.test.tsx
Adds the upcoming meetings table with grouping, filtering, join actions, optimistic join-policy updates, and watch-calendar hints, plus the defaults drawer for global and per-platform meeting settings.
Call history rail and detail views
app/src/components/meetings/TranscriptViewer.tsx, app/src/components/meetings/ActionItemChecklist.tsx, app/src/components/meetings/HistoryDetail.tsx, app/src/components/meetings/HistoryRail.tsx, app/src/components/meetings/HistorySection.tsx, app/src/components/meetings/__tests__/HistoryDetail.test.tsx, app/src/components/meetings/__tests__/HistorySection.test.tsx, app/src/components/meetings/__tests__/HistoryRail.test.tsx, app/src/components/meetings/__tests__/ActionItemChecklist.test.tsx, app/src/components/meetings/__tests__/TranscriptViewer.test.tsx
Adds the action-item checklist, transcript viewer, history detail pane, history rail, and history section orchestration, plus the associated component tests.
Skills page integration, MeetingBotsCard shim, and translations
app/src/components/skills/MeetingBotsCard.tsx, app/src/pages/Skills.tsx, app/src/pages/__tests__/Skills.meetings-tab.test.tsx, app/src/lib/i18n/*.ts
Replaces the old MeetingBotsCard implementation with a MeetingsPage shim, updates the Skills page and tab test to use MeetingsPage, and adds Webex plus meetings UI strings across the locale files.

Possibly related issues

Suggested labels

feature

Poem

🐇 I hopped through tabs where meetings bloom,
With Webex strings to light the room.
Policies twirl in three-tier grace,
While banners, rails, and tables race.
A bunny grin, a click, a chime—
New meeting paths are in their prime.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main Meetings redesign, including the platform selector and history master-detail layout.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Comment @coderabbitai help to get the list of available commands.

Phase 2 of the meetings redesign. Add a full-width Upcoming table between
the composer and history that lists calendar meetings with a conferencing
link, the platform inferred from that link, invitee count, a relative/
absolute start time, and a per-meeting Auto/Ask/Skip join-policy toggle
(local state this phase; persistence lands in Phase 3). A Join-now action
appears on imminent meetings.

Backend: new openhuman.meet_list_upcoming JSON-RPC fetches upcoming events
via the existing Composio GOOGLECALENDAR_EVENTS_LIST path (reused, not a
heartbeat refactor), extracts the join link from hangoutLink /
conferenceData / description, infers the platform, and returns typed
UpcomingMeeting records with the effective global join policy. Wired
through agent_meetings ops/schemas + json_rpc_e2e coverage.
Phase 3 (final) of the meetings redesign. Make the Auto/Ask/Skip join
policy persist and actually drive auto-join, with resolution order
per-event override > per-platform default > global default.

Backend: new meeting_event_policies SQLite table + meet_set_event_policy
/ meet_get_event_policies RPCs; add platform_auto_join_policies map to
MeetConfig (serde-defaulted, backward compatible); resolve_effective_join_policy
feeds both meet_list_upcoming (effective per-row policy) and the calendar
auto-join candidate handler (policy source only — scheduler untouched);
the heartbeat planner now threads the calendar_event_id through.

Frontend: MeetDefaultsDrawer (gear button) for global + per-platform
defaults and the existing listen-only/summarize/ingest settings; the
Upcoming table's policy toggle persists via setEventPolicy with optimistic
update + revert, and shows auto/ask hints per row.
The Upcoming table used bg-surface/50 + border-line/50, letting the
dotted page background bleed through. Match the composer card styling
(rounded-2xl border border-line bg-surface shadow-soft) so it reads as a
solid card.
History rail: replace the full-width platform <select> with a compact,
icon-only dropdown placed to the left of the search bar — it shows just
the selected platform's logo when closed and the icon + name for each
option when open. Reuse inferPlatformFromUrl for the per-row icons.

History: auto-select the first (most recent) visible call so the detail
pane is populated by default and re-snaps when the active filter hides
the current selection.
RECENT CALLS rendered directly on the page, so the dotted background bled
through. Wrap it in the same rounded-2xl border bg-surface shadow-soft
card used by the composer and Upcoming sections.
- Optimistic join-policy revert no longer clobbers a newer concurrent
  change: revert only if the current value still equals the optimistic
  value this call set (UpcomingTable).
- Route relative-time labels through i18n (skills.meetingBots.relative.*)
  instead of hardcoded English in formatWhen/formatRelativeTime; real
  translations added to all 14 locales.
- Upcoming poll no longer flips back to the loading skeleton on each 60s
  background tick (only initial load + manual refresh show loading).
- Dedupe MascotMeetPlatform into a MeetingPlatform alias.
- Single source of truth for the Composio logo URL: export composioLogoUrl
  and have platformLogoUrl delegate to it.
- SECURITY: strict host validation on the calendar auto-join path —
  is_meeting_url now parses the URL and matches ALLOWED_HOSTS by exact
  host or proper dot-suffix (rejects meet.google.com.attacker.com), and
  handle_calendar_meeting_candidate validates before any bot:join.
- Include in-progress meetings in the upcoming list (keep while
  effective_end >= now && start <= window_end) so ongoing meetings and
  their Join-now action no longer vanish at start time.
- Consistent calendar-event id across UI, heartbeat, and webhook paths
  (id -> eventId -> icalUID), so a per-event skip/auto override is honored
  on the live trigger path; warn-log on unkeyable payloads.
- Distinct synthetic dedup key (meet_url@start) for id-less events so they
  no longer collapse to a single 'unknown' row.
- Respect the caller's limit for GOOGLECALENDAR_EVENTS_LIST maxResults.
- Batch per-event policy lookups (one connection/query) instead of opening
  a fresh SQLite connection + migration per meeting.
- Consolidate the triplicated URL/host/extraction helpers into ops.rs.
Set "platform" in the auto-join bot:join payload so Zoom/Teams/Webex
meetings route to the right provider instead of defaulting to Google
Meet. Resolved via the strict infer_platform_from_url (same host
validation as the manual-join path; unknown host falls back to gmeet).

Folds in the remaining unique change from tinyhumansai#4307 (superseded by this
branch's host-detection consolidation) without its loose substring host
match.
@graycyrus graycyrus marked this pull request as ready for review June 30, 2026 07:10
@graycyrus graycyrus requested a review from a team June 30, 2026 07:10

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 06f51fa1f7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"connectionId": conn.id,
"timeMin": now.to_rfc3339(),
"timeMax": end_window.to_rfc3339(),
"maxResults": max_results,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not cap the calendar page before link filtering

Using the caller's display limit as Google Calendar maxResults means the RPC only inspects the first N calendar events, but the conferencing-link filter runs later in try_extract_meeting. For a calendar where the first 20 events in the 8-hour window are reminders/OOO/no-link items and a Meet/Zoom call is event 21, the Upcoming table incorrectly omits that valid meeting and its join-policy controls. Fetch a larger page or paginate until enough meeting-link events have been collected after filtering.

Useful? React with 👍 / 👎.

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.

Fixed in f4a718f — page_size now fetches the API max (100) BEFORE the conferencing-link filter, so link-bearing events behind reminders/OOO aren't dropped; truncate(limit) still applies post-filter.

Comment on lines +91 to +95
log('[detail] no summary yet, scheduling retry in 2000ms for', record.request_id);
const timer = setTimeout(() => {
log('[detail] retrying detail load for', record.request_id);
void loadDetail(record.request_id);
}, 2000);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop retrying missing summaries indefinitely

This effect schedules another load every time the detail returns loaded without a summary, so any older call where summary generation failed or was never written will keep hitting meet_agent_get_call_detail every 2 seconds for as long as the detail pane stays open. The comment says this should retry once; track that a retry has already been attempted for the selected request_id or bound the polling.

Useful? React with 👍 / 👎.

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.

Fixed in 4ca9a78 — retry now fires at most once per request_id (retriedForRef set), no more 2s polling loop.

Comment on lines +60 to +63
const result = await getMeetCallDetail(requestId);
log('[detail] loaded detail for', requestId, 'hasSummary=%s', hasSummaryDetail(result));
setDetail(result);
setStatus('loaded');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore stale call-detail responses

When the user switches between calls quickly, an earlier getMeetCallDetail request can resolve after a later selection and overwrite detail/status for the currently displayed record, showing the wrong transcript or summary under the new call header. Check that the resolved requestId still matches the current selection before calling setDetail and setStatus.

Useful? React with 👍 / 👎.

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.

Fixed in 4ca9a78 — stale getMeetCallDetail responses are ignored via a latestRequestIdRef guard before setDetail/setStatus.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/agent_meetings/ops.rs (1)

400-408: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject plaintext meeting URLs.

These helpers still accept http:// links. Because they gate both calendar auto-join and upcoming-meeting extraction, a downgraded URL can be surfaced or auto-joined before any redirect, leaking meeting tokens over cleartext transport. Restrict this path to HTTPS only.

Suggested fix
-    if url.scheme() != "https" && url.scheme() != "http" {
+    if url.scheme() != "https" {
         return Err(format!(
             "invalid meeting URL: scheme `{}` not allowed",
             url.scheme()
@@
-            matches!(u.scheme(), "http" | "https")
+            u.scheme() == "https"
                 && u.host_str().map(host_is_allowed).unwrap_or(false)
@@
-            (matches!(parsed.scheme(), "http" | "https")
+            (parsed.scheme() == "https"
                 && parsed.host_str().map(host_is_allowed).unwrap_or(false))
             .then(|| parsed.to_string())

Also applies to: 433-478

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/ops.rs` around lines 400 - 408, The meeting URL
validation and related helpers still allow plaintext http links, which must be
rejected so only secure meeting URLs are accepted. Update validate_meeting_url
and the other affected meeting-link parsing paths in the same area to enforce
HTTPS-only by checking the parsed Url scheme and returning an error for anything
other than https. Use the existing validate_meeting_url symbol as the main entry
point and apply the same restriction consistently to the other helper(s)
referenced in the diff so calendar auto-join and meeting extraction cannot pass
through cleartext URLs.
🟡 Minor comments (9)
app/src/components/meetings/JoinPolicyToggle.tsx-36-72 (1)

36-72: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add keyboard roving navigation here.
role="radiogroup"/role="radio" should behave as a single tab stop with arrow-key movement, but these buttons are all separate tab stops and have no Left/Right/Up/Down handling. Switching to native radios or adding roving tabindex would close the gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/JoinPolicyToggle.tsx` around lines 36 - 72, The
JoinPolicyToggle radiogroup currently uses separate button tab stops and lacks
arrow-key navigation, so update the component to behave like a single radio
group. In JoinPolicyToggle, implement roving tabindex (or switch to native radio
inputs) so only the active option is tabbable, and add Left/Right/Up/Down key
handling to move selection through SEGMENTS and call onChange. Keep the existing
role="radiogroup" and role="radio" semantics aligned with the active value.
app/src/components/meetings/MeetComposer.tsx-147-161 (1)

147-161: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Trim the meeting URL before the RPC call.

The button-enable logic already treats whitespace-only padding as invalid, but the submit path still sends the raw meetUrl into joinMeetViaBackendBot(...). A pasted URL like " https://zoom.us/j/123 " will reach backend validation with the spaces intact and fail even though the form allowed submission.

Suggested fix
   const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
     event.preventDefault();
     setError(null);
     setSubmitting(true);
     hasSubmittedRef.current = true;
+    const normalizedMeetUrl = meetUrl.trim();
     const meetingId = crypto.randomUUID();
     log('[composer] submit platform=%s active=%s correlationId=%s', platform, !listenOnly, meetingId);
     try {
@@
       await joinMeetViaBackendBot({
-        meetUrl,
+        meetUrl: normalizedMeetUrl,
         displayName: agentName,
@@
       });
       // RPC was accepted — transition the UI to the joining / active banner.
-      dispatch(setBackendMeetJoining({ meetUrl: meetUrl.trim(), meetingId, listenOnly }));
+      dispatch(setBackendMeetJoining({ meetUrl: normalizedMeetUrl, meetingId, listenOnly }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/MeetComposer.tsx` around lines 147 - 161, The
submit path in MeetComposer.tsx is passing the untrimmed meetUrl into
joinMeetViaBackendBot, which can break backend validation for pasted URLs with
surrounding spaces. Trim meetUrl before the RPC call and use the trimmed value
consistently for the join request and the setBackendMeetJoining dispatch, while
keeping the existing validation behavior intact.
app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx-92-105 (1)

92-105: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unawaited waitFor makes these assertions non-enforcing.

it(...) here is synchronous and the waitFor(...) promise is neither awaited nor returned, so the assertions inside it run on a detached microtask and can never fail the test — it passes even if "Close" never appears. The same pattern exists at Lines 174-176 in the mascot-transition test.

Since setBackendMeetLeft updates the store synchronously, you can assert directly without waitFor:

💚 Proposed fix
     store.dispatch(setBackendMeetLeft({ reason: 'done' }));

-    // After ended, "Leave" disappears and "Close" appears
-    waitFor(() => {
-      expect(screen.queryByRole('button', { name: /leave/i })).not.toBeInTheDocument();
-      expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
-    });
+    // After ended, "Leave" disappears and "Close" appears
+    expect(screen.queryByRole('button', { name: /leave/i })).not.toBeInTheDocument();
+    expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();

Apply the same change to the mascot-transition assertion at Lines 174-176.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx` around
lines 92 - 105, The ended-state checks in ActiveMeetingBanner.test.tsx are not
enforced because waitFor is used without await/return, so the assertions can
pass silently even when the UI does not update; remove the unnecessary waitFor
and assert directly after dispatching setBackendMeetLeft, and apply the same
synchronous assertion fix in the mascot-transition test that uses the same
pattern.
app/src/lib/i18n/id.ts-5169-5169 (1)

5169-5169: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the platform name as the product brand.

Google Temui reads like a literal translation; the UI should preserve the brand name (Google Meet) for consistency with the other platform labels.

Suggested fix
-  'skills.meetingBots.platforms.gmeet': 'Google Temui',
+  'skills.meetingBots.platforms.gmeet': 'Google Meet',
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/lib/i18n/id.ts` at line 5169, Update the translation entry for
skills.meetingBots.platforms.gmeet in the i18n/id.ts mapping so it preserves the
product brand name instead of using a literal translation. Change the displayed
label from the translated form to the brand-consistent “Google Meet”, matching
the style used by the other platform labels in this locale file.
src/openhuman/agent_meetings/upcoming.rs-325-345 (1)

325-345: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the same 1-hour fallback in the window check.

When end.dateTime is missing, Line 336 falls back to start_utc, but Lines 343-345 still serialize a synthetic 1-hour end. In-progress meetings without an end timestamp are therefore filtered out even though the returned payload assumes they last an hour.

Suggested fix
-    let effective_end = end_utc.unwrap_or(start_utc);
+    let effective_end =
+        end_utc.unwrap_or_else(|| start_utc + chrono::Duration::hours(1));
     if effective_end < start_window || start_utc > end_window {
         return None;
     }
@@
-    let end_ms = end_utc
-        .map(|dt| dt.timestamp_millis().max(0) as u64)
-        .unwrap_or_else(|| start_ms + 3_600_000); // Default 1-hour duration
+    let end_ms = effective_end.timestamp_millis().max(0) as u64;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/upcoming.rs` around lines 325 - 345, The window
check in the upcoming-meetings filter is using start_utc as the fallback end,
which conflicts with the 1-hour synthetic end used later in the same routine.
Update the in-progress meeting check in the upcoming.rs parsing logic to use the
same 1-hour fallback as the end_ms serialization path (the code around end_utc,
effective_end, and the interval check) so events without end.dateTime are not
dropped prematurely.
src/openhuman/agent_meetings/schemas.rs-305-309 (1)

305-309: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Update the join_policy schema description to match the effective-policy contract.

handle_list_upcoming now applies per-event and per-platform overrides before returning meetings, so this schema text should not say join_policy is only the global default.

Suggested wording update
-                      organizer, and the global auto-join policy as the default join_policy.",
+                      organizer, and the effective join_policy after per-event, per-platform, \
+                      and global defaults are resolved.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/schemas.rs` around lines 305 - 309, The
`join_policy` description in `handle_list_upcoming`/`ListUpcoming` still says it
is only the global auto-join default, but the returned value now reflects the
effective policy after per-event and per-platform overrides. Update the schema
text in `schemas.rs` so it clearly states that `join_policy` is the resolved
effective policy used for each meeting, not just the global default.
app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx-87-95 (1)

87-95: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the if (...) guards from these assertions.

Both tests become false positives if the queried element disappears: the branch just never runs, so the suite still passes when the platform override select or backdrop stops rendering. Assert those elements exist first, then interact with them.

Also applies to: 103-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx` around
lines 87 - 95, The tests in MeetDefaultsDrawer.test.tsx are silently skipping
critical interactions because of the conditional guards around the combobox
assertions. Remove the if checks, explicitly assert the expected
selects/backdrop exist first using the relevant screen queries in the affected
test cases, and then perform the fireEvent.change/click and waitFor assertions
so failures surface when the UI stops rendering those elements.
app/src/components/meetings/HistorySection.tsx-24-63 (1)

24-63: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Group history by local day, not UTC.

todayKey() / yesterdayKey() use UTC calendar boundaries, but these buckets are rendered as user-facing “Today” / “Yesterday” labels. For users outside UTC, late-night calls can land in the wrong group even though their displayed times are local. Use local date parts, or the same timezone basis as the rendered timestamps, for grouping.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/HistorySection.tsx` around lines 24 - 63, The
history grouping logic in groupRecords uses utcDayKey(), todayKey(), and
yesterdayKey(), which buckets calls by UTC instead of the user’s local day.
Update the day-key helpers to use local date boundaries (or the same timezone
basis as the displayed timestamps) and keep groupRecords comparing against those
local keys so calls fall under the correct Today/Yesterday labels.
app/src/components/meetings/ActionItemChecklist.tsx-25-30 (1)

25-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Checkbox state leaks between different meetings.

checked is keyed only by array index and never reset when items changes. If a user checks item 0 in one call and then opens another call, item 0 in the new checklist will render as checked too. Scope this cosmetic state to the current meeting/item identity instead of raw indices.

Also applies to: 56-57

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/ActionItemChecklist.tsx` around lines 25 - 30,
The checklist checkbox state in ActionItemChecklist leaks across meetings
because checked is keyed only by array index and is never reset when items
changes. Update the state in ActionItemChecklist so it is scoped to the current
meeting/item identity instead of raw indices, and make handleCheck use a stable
per-item key or reset the checked map when the items prop changes. Also update
the rendering logic around checked so each checklist instance reflects only its
own items.
🧹 Nitpick comments (2)
app/src/services/meetCallService.ts (1)

417-430: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Narrow these RPC types to the actual platform/join-policy unions.

UpcomingTable immediately casts these fields back to MeetingPlatform / JoinPolicy, so typos from mocks or future callers will compile and only fail at runtime. Keeping the frontend types aligned with the Rust RPC contract here removes those unsafe casts.

Suggested diff
+export type MeetingJoinPolicy = 'auto' | 'ask' | 'skip';
+
 export interface UpcomingMeeting {
   calendar_event_id: string;
   title: string;
   /** Unix milliseconds */
   start_time_ms: number;
   /** Unix milliseconds */
   end_time_ms: number;
   meet_url: string | null;
   /** Platform slug: "gmeet" | "zoom" | "teams" | "webex" */
-  platform: string | null;
+  platform: MeetingPlatform | null;
   participant_count: number | null;
   organizer: string | null;
   /** "auto" | "ask" | "skip" — local UI state only this phase */
-  join_policy: string;
+  join_policy: MeetingJoinPolicy;
   calendar_source: string;
 }
@@
 interface CoreGetEventPoliciesResponse {
   ok: boolean;
-  policies: Record<string, string>;
+  policies: Record<string, MeetingJoinPolicy>;
 }
@@
 export async function getEventPolicies(
   calendarEventIds: string[]
-): Promise<Record<string, string>> {
+): Promise<Record<string, MeetingJoinPolicy>> {

Also applies to: 468-470, 494-505

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/services/meetCallService.ts` around lines 417 - 430, The RPC-facing
types in UpcomingMeeting are too loose because platform and join_policy are
typed as string, even though UpcomingTable immediately narrows them back to
MeetingPlatform and JoinPolicy. Update UpcomingMeeting (and the related RPC
shapes used by UpcomingTable/MeetCallService) to use the actual MeetingPlatform
and JoinPolicy unions instead of plain strings, so invalid values are rejected
at compile time and the frontend stays aligned with the Rust contract.
app/src/utils/tauriCommands/config.ts (1)

793-805: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Use a platform-key union for platform_auto_join_policies.

The backend only accepts gmeet | zoom | teams | webex keys, but Record<string, ...> lets invalid keys compile and pushes the failure to runtime. A partial map keyed by the supported platforms will keep this TS contract honest.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/utils/tauriCommands/config.ts` around lines 793 - 805, The
`MeetSettingsUpdate` contract currently types `platform_auto_join_policies` as
`Record<string, MeetAutoJoinPolicy>`, which allows unsupported keys to compile.
Update this field to use a platform-key union for the supported platforms
(`gmeet`, `zoom`, `teams`, `webex`) so the TypeScript type matches the backend
contract, and keep the change aligned with the existing `MeetSettingsUpdate`
interface and its related auto-join policy types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/meetings/HistoryDetail.tsx`:
- Around line 56-68: The loadDetail flow in HistoryDetail can apply stale
results when getMeetCallDetail resolves out of order after a newer request has
started. Update loadDetail to track the latest requestId (or use cancellation)
and, before calling setDetail, setStatus('loaded'), or setting
loadedForRef.current, verify the response still matches the current request.
Keep the current request guarded using the existing loadDetail function and
loadedForRef reference so older completions are ignored.
- Around line 86-97: The retry effect in HistoryDetail.tsx is re-scheduling
forever whenever a loaded meeting still has no summary. Add a per-request_id
guard in the HistoryDetail component around the useEffect that checks
status/detail/record?.request_id so loadDetail only gets called once for each
request_id, and ensure the timer setup is skipped after that first retry.
- Around line 71-84: The reset logic in HistoryDetail’s useEffect is triggering
the repo’s react-hooks/set-state-in-effect rule because it synchronously calls
setStatus and setDetail inside the effect body. Move the “clear on record
change” behavior out of this effect in HistoryDetail—either key the component by
record?.request_id or derive the displayed detail/status from the current record
so no state reset is needed here, while keeping loadDetail(record.request_id)
responsible only for loading.

In `@app/src/components/meetings/HistorySection.tsx`:
- Around line 145-152: The selection in HistorySection is left stale when
filteredRecords becomes empty because the useEffect exits early and preserves
selectedCallId, which can hide the mobile rail and trap the user on stale
details. Update the selection logic in HistorySection so the active call is
derived from filteredRecords (or explicitly cleared) instead of being patched
inside useEffect; prefer computing the selected item from the current
filteredRecords/selection state or resetting it through the existing selection
flow in the component rather than calling setSelectedCallId directly in the
effect.

In `@app/src/components/meetings/MeetComposer.tsx`:
- Around line 89-92: The MeetComposer effects are synchronously setting state
inside useEffect, which violates the repo’s react-hooks/set-state-in-effect rule
and will fail CI. Refactor the responsive prefill and error/submitting handling
so state is derived outside the effect or updated from the relevant
event/transition source instead of directly in the effect body. Use the
MeetComposer logic around resolvedDisplayName, setRespondTo, setError, and
setSubmitting to locate and remove these synchronous effect-based state updates.

In `@app/src/components/meetings/MeetDefaultsDrawer.tsx`:
- Around line 108-113: The initial-load failure path in MeetDefaultsDrawer
should not fall back to the editable form backed by default state; instead, keep
the live settings controls hidden or disabled until openhumanGetMeetSettings()
succeeds and a real snapshot is loaded. Update the loading/error flow around the
load effect and the form rendering so the normal save path cannot run from stale
defaults, and ensure any state used by the submit handlers remains gated by the
successful load state.
- Around line 122-143: The shared persistSeqRef guard in MeetDefaultsDrawer’s
persist helper is causing unrelated setting saves to cancel or hide each other’s
failures because each handler only sends a partial patch. Update the save flow
in persist and the individual setting handlers so request tracking is scoped per
setting/field (or replace the global sequence check with a refetch/reconcile
step after each save), ensuring a failed write for one control cannot be dropped
by a later success from another control.

In `@app/src/components/meetings/meetingUtils.ts`:
- Around line 119-125: The hostname matching in inferPlatformFromUrl is too
loose because it uses includes(), which can falsely पहचानise lookalike hosts as
meeting platforms. Update the checks in inferPlatformFromUrl to use exact
hostname comparison or dot-boundary suffix matching for meet.google.com,
zoom.us, teams.microsoft.com, and webex.com, so only real platform hosts are
labeled and used by the join-payload fallback.

In `@app/src/components/meetings/UpcomingTable.tsx`:
- Around line 141-143: Use a single effectivePlatform helper in UpcomingTable so
rendering, filtering, and filter-option generation all use the same platform
value. Right now filterMeetings and the filter list rely on meeting.platform
while row rendering falls back to inferPlatformFromUrl(meet_url), causing
null-platform meetings to display differently than they filter. Update the logic
around filterMeetings, the row rendering path, and the platform option
aggregation to reuse that helper everywhere.

In `@app/src/components/meetings/useUpcomingMeetings.ts`:
- Around line 44-78: Guard `fetchMeetings` in `useUpcomingMeetings` against
overlapping/out-of-order responses, since `mountedRef` only prevents updates
after unmount. Add a request/version token (or similar latest-request check)
inside `fetchMeetings` and only call `setMeetings`, `setError`, and
`setLoading(false)` for the most recent invocation triggered by the `useEffect`
poll, `refresh()`, or `lookaheadMinutes`/`limit` changes.

In `@src/openhuman/agent_meetings/calendar.rs`:
- Line 302: The MeetingSession records created in
handle_calendar_meeting_candidate are dropping the resolved calendar_event_id by
still initializing it as None. Update both MeetingSession initializer paths in
this function so they persist calendar_event_id.clone() instead, matching the
new argument already used for policy resolution. Keep the change aligned across
both the auto-join and prompt session branches so the persisted session retains
the calendar-event association.

In `@src/openhuman/agent_meetings/ops.rs`:
- Around line 497-509: The helper extract_calendar_event_id_from_payload
currently checks the wrapper payload before payload["data"], which can cause
mismatched override keys when the envelope has its own top-level id. Update this
function to prefer the nested event object under data first, and only fall back
to the root payload afterward, while still using extract_calendar_event_id for
the actual id/eventId/icalUID selection.

In `@src/openhuman/agent_meetings/store.rs`:
- Around line 30-34: The persisted meeting policy values are currently
unrestricted, so invalid strings can be stored and later returned by
handle_get_event_policies and resolve_effective_join_policy_with_overrides.
Update the meeting_event_policies schema to add a DB-level CHECK on policy, and
tighten set_event_policy so it validates the incoming value against the allowed
join policy set before writing. Use the existing policy-handling paths as the
source of truth and reject any invalid value before insert/update.

In `@src/openhuman/agent_meetings/upcoming.rs`:
- Around line 81-94: The upcoming-meetings flow is swallowing upstream calendar
errors by turning them into empty vectors and treating that as “no meetings.”
Update `fetch_connections`, `fetch_events_for_connection`, and the caller logic
in `upcoming` so Composio/Google failures are propagated as errors instead of
returning `Vec::new()`, and only return an empty list when the provider truly
has no data. Make sure the early return after `calendar_connections.is_empty()`
is reserved for the real “no calendar connected” case, and keep the same
error-handling behavior consistent in the other affected paths referenced by
`fetch_connections` and `fetch_events_for_connection`.

In `@src/openhuman/config/schemas/controllers.rs`:
- Around line 650-662: The update_meet_settings parser in
controllers::update_meet_settings currently validates only policy values and
allows any platform key to be persisted. Add key validation in the raw_map
iteration so only the supported platform keys (gmeet, zoom, teams, webex) are
accepted before inserting into parsed, and return an error plus warn log for any
unsupported key; keep the existing AutoJoinPolicy matching intact and apply the
check in the same loop where policy_str is parsed.

---

Outside diff comments:
In `@src/openhuman/agent_meetings/ops.rs`:
- Around line 400-408: The meeting URL validation and related helpers still
allow plaintext http links, which must be rejected so only secure meeting URLs
are accepted. Update validate_meeting_url and the other affected meeting-link
parsing paths in the same area to enforce HTTPS-only by checking the parsed Url
scheme and returning an error for anything other than https. Use the existing
validate_meeting_url symbol as the main entry point and apply the same
restriction consistently to the other helper(s) referenced in the diff so
calendar auto-join and meeting extraction cannot pass through cleartext URLs.

---

Minor comments:
In `@app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx`:
- Around line 92-105: The ended-state checks in ActiveMeetingBanner.test.tsx are
not enforced because waitFor is used without await/return, so the assertions can
pass silently even when the UI does not update; remove the unnecessary waitFor
and assert directly after dispatching setBackendMeetLeft, and apply the same
synchronous assertion fix in the mascot-transition test that uses the same
pattern.

In `@app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx`:
- Around line 87-95: The tests in MeetDefaultsDrawer.test.tsx are silently
skipping critical interactions because of the conditional guards around the
combobox assertions. Remove the if checks, explicitly assert the expected
selects/backdrop exist first using the relevant screen queries in the affected
test cases, and then perform the fireEvent.change/click and waitFor assertions
so failures surface when the UI stops rendering those elements.

In `@app/src/components/meetings/ActionItemChecklist.tsx`:
- Around line 25-30: The checklist checkbox state in ActionItemChecklist leaks
across meetings because checked is keyed only by array index and is never reset
when items changes. Update the state in ActionItemChecklist so it is scoped to
the current meeting/item identity instead of raw indices, and make handleCheck
use a stable per-item key or reset the checked map when the items prop changes.
Also update the rendering logic around checked so each checklist instance
reflects only its own items.

In `@app/src/components/meetings/HistorySection.tsx`:
- Around line 24-63: The history grouping logic in groupRecords uses
utcDayKey(), todayKey(), and yesterdayKey(), which buckets calls by UTC instead
of the user’s local day. Update the day-key helpers to use local date boundaries
(or the same timezone basis as the displayed timestamps) and keep groupRecords
comparing against those local keys so calls fall under the correct
Today/Yesterday labels.

In `@app/src/components/meetings/JoinPolicyToggle.tsx`:
- Around line 36-72: The JoinPolicyToggle radiogroup currently uses separate
button tab stops and lacks arrow-key navigation, so update the component to
behave like a single radio group. In JoinPolicyToggle, implement roving tabindex
(or switch to native radio inputs) so only the active option is tabbable, and
add Left/Right/Up/Down key handling to move selection through SEGMENTS and call
onChange. Keep the existing role="radiogroup" and role="radio" semantics aligned
with the active value.

In `@app/src/components/meetings/MeetComposer.tsx`:
- Around line 147-161: The submit path in MeetComposer.tsx is passing the
untrimmed meetUrl into joinMeetViaBackendBot, which can break backend validation
for pasted URLs with surrounding spaces. Trim meetUrl before the RPC call and
use the trimmed value consistently for the join request and the
setBackendMeetJoining dispatch, while keeping the existing validation behavior
intact.

In `@app/src/lib/i18n/id.ts`:
- Line 5169: Update the translation entry for skills.meetingBots.platforms.gmeet
in the i18n/id.ts mapping so it preserves the product brand name instead of
using a literal translation. Change the displayed label from the translated form
to the brand-consistent “Google Meet”, matching the style used by the other
platform labels in this locale file.

In `@src/openhuman/agent_meetings/schemas.rs`:
- Around line 305-309: The `join_policy` description in
`handle_list_upcoming`/`ListUpcoming` still says it is only the global auto-join
default, but the returned value now reflects the effective policy after
per-event and per-platform overrides. Update the schema text in `schemas.rs` so
it clearly states that `join_policy` is the resolved effective policy used for
each meeting, not just the global default.

In `@src/openhuman/agent_meetings/upcoming.rs`:
- Around line 325-345: The window check in the upcoming-meetings filter is using
start_utc as the fallback end, which conflicts with the 1-hour synthetic end
used later in the same routine. Update the in-progress meeting check in the
upcoming.rs parsing logic to use the same 1-hour fallback as the end_ms
serialization path (the code around end_utc, effective_end, and the interval
check) so events without end.dateTime are not dropped prematurely.

---

Nitpick comments:
In `@app/src/services/meetCallService.ts`:
- Around line 417-430: The RPC-facing types in UpcomingMeeting are too loose
because platform and join_policy are typed as string, even though UpcomingTable
immediately narrows them back to MeetingPlatform and JoinPolicy. Update
UpcomingMeeting (and the related RPC shapes used by
UpcomingTable/MeetCallService) to use the actual MeetingPlatform and JoinPolicy
unions instead of plain strings, so invalid values are rejected at compile time
and the frontend stays aligned with the Rust contract.

In `@app/src/utils/tauriCommands/config.ts`:
- Around line 793-805: The `MeetSettingsUpdate` contract currently types
`platform_auto_join_policies` as `Record<string, MeetAutoJoinPolicy>`, which
allows unsupported keys to compile. Update this field to use a platform-key
union for the supported platforms (`gmeet`, `zoom`, `teams`, `webex`) so the
TypeScript type matches the backend contract, and keep the change aligned with
the existing `MeetSettingsUpdate` interface and its related auto-join policy
types.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b0b2909-601a-4b1a-a793-168770dfbdf7

📥 Commits

Reviewing files that changed from the base of the PR and between 300d70a and 06f51fa.

📒 Files selected for processing (65)
  • app/src/components/composio/toolkitMeta.tsx
  • app/src/components/meetings/ActionItemChecklist.tsx
  • app/src/components/meetings/ActiveMeetingBanner.tsx
  • app/src/components/meetings/HistoryDetail.tsx
  • app/src/components/meetings/HistoryRail.tsx
  • app/src/components/meetings/HistorySection.tsx
  • app/src/components/meetings/JoinPolicyToggle.tsx
  • app/src/components/meetings/MeetComposer.tsx
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/MeetingsPage.tsx
  • app/src/components/meetings/PlatformChips.tsx
  • app/src/components/meetings/TranscriptViewer.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/ActionItemChecklist.test.tsx
  • app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx
  • app/src/components/meetings/__tests__/HistoryDetail.test.tsx
  • app/src/components/meetings/__tests__/HistoryRail.test.tsx
  • app/src/components/meetings/__tests__/HistorySection.test.tsx
  • app/src/components/meetings/__tests__/JoinPolicyToggle.test.tsx
  • app/src/components/meetings/__tests__/MeetComposer.test.tsx
  • app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
  • app/src/components/meetings/__tests__/PlatformChips.test.tsx
  • app/src/components/meetings/__tests__/TranscriptViewer.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/meetings/__tests__/meetingUtils.test.ts
  • app/src/components/meetings/__tests__/useUpcomingMeetings.test.ts
  • app/src/components/meetings/meetingUtils.ts
  • app/src/components/meetings/useUpcomingMeetings.ts
  • app/src/components/skills/MeetingBotsCard.tsx
  • app/src/components/skills/RecentCallsSection.test.tsx
  • app/src/components/skills/RecentCallsSection.tsx
  • app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/pages/Skills.tsx
  • app/src/pages/__tests__/Skills.meetings-tab.test.tsx
  • app/src/services/__tests__/meetCallService.test.ts
  • app/src/services/meetCallService.ts
  • app/src/utils/tauriCommands/config.ts
  • src/core/all.rs
  • src/openhuman/agent_meetings/calendar.rs
  • src/openhuman/agent_meetings/mod.rs
  • src/openhuman/agent_meetings/ops.rs
  • src/openhuman/agent_meetings/schemas.rs
  • src/openhuman/agent_meetings/store.rs
  • src/openhuman/agent_meetings/types.rs
  • src/openhuman/agent_meetings/upcoming.rs
  • src/openhuman/config/ops/ui.rs
  • src/openhuman/config/schema/meet.rs
  • src/openhuman/config/schemas/controllers.rs
  • src/openhuman/config/schemas/helpers.rs
  • src/openhuman/subconscious/heartbeat/planner/mod.rs
  • tests/json_rpc_e2e.rs
💤 Files with no reviewable changes (2)
  • app/src/components/skills/RecentCallsSection.test.tsx
  • app/src/components/skills/RecentCallsSection.tsx

Comment thread app/src/components/meetings/HistoryDetail.tsx
Comment thread app/src/components/meetings/HistoryDetail.tsx
Comment thread app/src/components/meetings/HistoryDetail.tsx Outdated
Comment thread app/src/components/meetings/HistorySection.tsx Outdated
Comment thread app/src/components/meetings/MeetComposer.tsx Outdated
Comment thread src/openhuman/agent_meetings/calendar.rs
Comment thread src/openhuman/agent_meetings/ops.rs Outdated
Comment thread src/openhuman/agent_meetings/store.rs
Comment thread src/openhuman/agent_meetings/upcoming.rs Outdated
Comment thread src/openhuman/config/schemas/controllers.rs Outdated
…calendar

The calendar auto-join / ask-to-join engine was gated behind
heartbeat.notify_meetings (a reminder-notifications toggle, off by
default), so setting a meeting's join policy to Auto/Ask silently did
nothing. Introduce meet.watch_calendar as a dedicated master switch:

- Planner polls the calendar + runs auto-join when notify_meetings OR
  watch_calendar is on; in watch-calendar-only mode it suppresses the
  plain reminder card (auto-join/ask still fire), so joining is decoupled
  from reminder notifications.
- watch_calendar (serde-default false) threaded through the meet-settings
  get/update RPC.
- Defaults drawer gains a top master switch; the Upcoming table shows an
  inline hint when watching is off so the Auto/Ask toggles aren't silently
  inert.
@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/openhuman/subconscious/heartbeat/planner/mod.rs (2)

168-170: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don’t pass fallback event IDs into per-event policy resolution.

source_event_id can be the collector fallback "calendar-event" when the payload has no stable id/eventId/icalUID. Passing it as Some(...) lets unrelated no-id events share a per-event override key, which can incorrectly change auto-join behavior. Pass None for fallback IDs or preserve the original optional calendar ID separately.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/subconscious/heartbeat/planner/mod.rs` around lines 168 - 170,
Do not pass the collector fallback value from `event.source_event_id` into
per-event policy resolution, because that makes unrelated no-id events share the
same override key. In the planner code that feeds `meet_set_event_policy` and
the per-event policy tier, only forward a stable calendar identifier as
`Some(...)`; if `source_event_id` is the fallback `"calendar-event"`, pass
`None` instead or keep the original optional calendar ID separate so fallback
events do not pick up shared overrides.

45-64: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Move the new planner behavior out of mod.rs.

This adds watch-calendar gating, policy forwarding, and suppressed-card dispatch logic directly in mod.rs. Please move the implementation into a sibling planner module and keep mod.rs to exports/wiring. As per coding guidelines, src/openhuman/**/mod.rs: “mod.rs files should be export-focused only: mod/pub mod, pub use, and controller schema wiring; no business logic.”

Also applies to: 168-170, 195-209

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/subconscious/heartbeat/planner/mod.rs` around lines 45 - 64,
The new heartbeat planner behavior is too much business logic in mod.rs; move
the watch-calendar gating, policy forwarding, and suppressed-card dispatch into
a sibling planner module, and keep mod.rs limited to exports/wiring. Locate the
logic in the heartbeat planner entrypoint and related helpers such as the
planner function that collects reminders/meetings, then extract that
implementation into the appropriate sibling module while preserving the existing
public surface via mod/pub mod and pub use. Ensure mod.rs only re-exports the
planner pieces and controller/schema wiring, with no event-planning logic
remaining.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx`:
- Around line 150-160: The rollback test in MeetDefaultsDrawer is too weak
because it can pass while never observing the optimistic update. Update the test
around the watch-my-calendar switch to use a deferred rejected promise for
updateMock, then assert the switch becomes checked after the click before
triggering the rejection and verifying it rolls back to unchecked; use the
existing test helpers and the switch queried by its accessible name to keep it
robust.

---

Outside diff comments:
In `@src/openhuman/subconscious/heartbeat/planner/mod.rs`:
- Around line 168-170: Do not pass the collector fallback value from
`event.source_event_id` into per-event policy resolution, because that makes
unrelated no-id events share the same override key. In the planner code that
feeds `meet_set_event_policy` and the per-event policy tier, only forward a
stable calendar identifier as `Some(...)`; if `source_event_id` is the fallback
`"calendar-event"`, pass `None` instead or keep the original optional calendar
ID separate so fallback events do not pick up shared overrides.
- Around line 45-64: The new heartbeat planner behavior is too much business
logic in mod.rs; move the watch-calendar gating, policy forwarding, and
suppressed-card dispatch into a sibling planner module, and keep mod.rs limited
to exports/wiring. Locate the logic in the heartbeat planner entrypoint and
related helpers such as the planner function that collects reminders/meetings,
then extract that implementation into the appropriate sibling module while
preserving the existing public surface via mod/pub mod and pub use. Ensure
mod.rs only re-exports the planner pieces and controller/schema wiring, with no
event-planning logic remaining.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: aaf6473a-0577-4c3a-a64f-f0851a3e5cd4

📥 Commits

Reviewing files that changed from the base of the PR and between 06f51fa and 2a492cc.

📒 Files selected for processing (26)
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/MeetingsPage.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/settings/panels/__tests__/MeetingSettingsPanel.test.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/utils/tauriCommands/config.ts
  • src/openhuman/config/ops/ui.rs
  • src/openhuman/config/schema/meet.rs
  • src/openhuman/config/schemas/controllers.rs
  • src/openhuman/config/schemas/helpers.rs
  • src/openhuman/subconscious/heartbeat/planner/mod.rs
✅ Files skipped from review due to trivial changes (4)
  • app/src/components/settings/panels/tests/MeetingSettingsPanel.test.tsx
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/bn.ts
🚧 Files skipped from review as they are similar to previous changes (20)
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/id.ts
  • app/src/components/meetings/MeetingsPage.tsx
  • app/src/lib/i18n/de.ts
  • src/openhuman/config/schema/meet.rs
  • app/src/lib/i18n/hi.ts
  • app/src/utils/tauriCommands/config.ts
  • app/src/lib/i18n/en.ts
  • src/openhuman/config/ops/ui.rs
  • app/src/lib/i18n/ko.ts
  • src/openhuman/config/schemas/controllers.rs
  • app/src/lib/i18n/zh-CN.ts
  • src/openhuman/config/schemas/helpers.rs
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/lib/i18n/pt.ts
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/it.ts
  • app/src/components/meetings/tests/UpcomingTable.test.tsx
  • app/src/lib/i18n/ar.ts

Comment thread app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
- HistoryDetail: ignore out-of-order getMeetCallDetail responses (guard by
  latest request id); retry missing summaries at most once per call;
  remove reset-in-effect (remount via key) to satisfy set-state-in-effect.
- HistorySection: clear selection when a filter empties the list.
- MeetComposer: derive the prefilled display name at render instead of in
  an effect (satisfies react-hooks/set-state-in-effect), behavior preserved.
- meetingUtils: inferPlatformFromUrl parses the hostname and matches by
  exact/dot-suffix instead of substring (no spoofed-host match).
- useUpcomingMeetings: ignore stale out-of-order fetch responses.
- UpcomingTable: filter by the effective (inferred) platform shown per row.
- upcoming.rs: fetch the full API page (maxResults=100) BEFORE the
  conferencing-link filter, so a Meet/Zoom event behind reminders/OOO in
  the first N isn't silently dropped; still truncate(limit) post-filter.
- upcoming.rs: propagate genuine Composio fetch failures (Result) instead
  of swallowing them as an empty list; no-connection still returns empty.
- calendar.rs: persist the resolved calendar_event_id on the auto-join /
  ask MeetingSession so per-event policy + dedup can key off it.
- ops.rs: extract_calendar_event_id_from_payload prefers the nested
  data.* event id over the wrapper's id (events.list vs webhook shapes).
- store.rs: set_event_policy rejects values outside auto|ask|skip.
- controllers.rs: update_meet_settings rejects unknown platform slugs in
  platform_auto_join_policies (allowlist gmeet|zoom|teams|webex).
# Conflicts:
#	app/src/components/skills/MeetingBotsCard.tsx
main added resolveMeetingBotMascotId (legacy-mascot allowlist so manifest-
only mascot ids fall back to the legacy color) to the old MeetingBotsCard,
which our shim dropped. Port it into meetingUtils + MeetComposer so the
backend bot still gets a recognized mascot id. Drop the obsolete
RecentCallsSection test cases that auto-merged from main (that surface is
now HistorySection, covered by its own specs) and add unit tests for the
ported resolver.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/meetings/__tests__/HistoryDetail.test.tsx`:
- Around line 252-305: The stale-transcript test in HistoryDetail.test.tsx is
not validating actual transcript content because the current TranscriptViewer
mock only exposes a test id/count. Update the mock used by HistoryDetail so it
either renders each transcript line’s content or otherwise records the received
props, then change the assertion in the rapid record switch test to verify the
late transcript content remains and that the stale early content is not
rendered. Use the TranscriptViewer mock and the HistoryDetail/record switch test
case to locate the fix.

In `@src/openhuman/agent_meetings/upcoming.rs`:
- Around line 265-275: The fallback error text in the GOOGLECALENDAR_EVENTS_LIST
failure path is contradictory and should match the `r.successful == false`
branch. Update the `detail` handling in `upcoming.rs` so the default string used
by the `tracing::warn!` call and the returned `Err(...)` in the `Ok(r)` arm of
the upcoming meetings flow reflects an unsuccessful response consistently, using
the same symbols (`r.error`, `detail`, `conn.id`, and the `"[meet:upcoming]
GOOGLECALENDAR_EVENTS_LIST..."` message) to locate and fix the fallback text.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee0a9631-0114-47df-b58b-bd153709a311

📥 Commits

Reviewing files that changed from the base of the PR and between 2a492cc and 8949a7b.

📒 Files selected for processing (31)
  • app/src/components/meetings/HistoryDetail.tsx
  • app/src/components/meetings/HistorySection.tsx
  • app/src/components/meetings/MeetComposer.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/HistoryDetail.test.tsx
  • app/src/components/meetings/__tests__/HistorySection.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/meetings/__tests__/meetingUtils.test.ts
  • app/src/components/meetings/__tests__/useUpcomingMeetings.test.ts
  • app/src/components/meetings/meetingUtils.ts
  • app/src/components/meetings/useUpcomingMeetings.ts
  • app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • src/openhuman/agent_meetings/calendar.rs
  • src/openhuman/agent_meetings/ops.rs
  • src/openhuman/agent_meetings/store.rs
  • src/openhuman/agent_meetings/upcoming.rs
  • src/openhuman/config/schemas/controllers.rs
✅ Files skipped from review due to trivial changes (7)
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/es.ts
🚧 Files skipped from review as they are similar to previous changes (15)
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/fr.ts
  • src/openhuman/agent_meetings/store.rs
  • app/src/lib/i18n/it.ts
  • app/src/components/meetings/tests/UpcomingTable.test.tsx
  • app/src/lib/i18n/bn.ts
  • app/src/components/meetings/MeetComposer.tsx
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/hi.ts
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/HistorySection.tsx
  • app/src/components/meetings/HistoryDetail.tsx
  • src/openhuman/agent_meetings/calendar.rs
  • src/openhuman/agent_meetings/ops.rs

Comment on lines +252 to +305
it('shows the correct transcript and ignores stale content after rapid record switch', async () => {
let resolveEarly!: (v: MeetCallDetail) => void;

const earlyDetail: MeetCallDetail = {
request_id: 'req-early',
summary: null,
transcript: [{ role: 'participant', content: 'EARLY STALE CONTENT' }],
};
const lateDetail: MeetCallDetail = {
request_id: 'req-late',
summary: null,
transcript: [{ role: 'participant', content: 'LATE FRESH CONTENT' }],
};

getMeetCallDetailMock.mockImplementation((reqId: string) => {
if (reqId === 'req-early') {
return new Promise(r => { resolveEarly = r; });
}
return Promise.resolve(lateDetail);
});

const earlyRecord: MeetCallRecord = {
...record,
request_id: 'req-early',
meet_url: 'https://meet.google.com/early-abc',
};
const lateRecord: MeetCallRecord = {
...record,
request_id: 'req-late',
meet_url: 'https://meet.google.com/late-xyz',
};

const { rerender } = renderWithProviders(<HistoryDetail record={earlyRecord} />);

// Wait for the early fetch to start.
await waitFor(() => expect(getMeetCallDetailMock).toHaveBeenCalledWith('req-early'));

// Switch while early is still in-flight.
rerender(<HistoryDetail record={lateRecord} />);

// Late (current) record's transcript should appear.
await waitFor(() => {
expect(screen.getByTestId('transcript')).toBeInTheDocument();
});

// Deliver the stale early response.
await act(async () => {
resolveEarly(earlyDetail);
await Promise.resolve();
});

// Stale early content must not replace the live late content.
expect(screen.queryByText('EARLY STALE CONTENT')).toBeNull();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The stale-transcript assertion is currently vacuous.

This file’s TranscriptViewer mock only renders a test id/count, so Line 304 stays null even if stale transcript lines are passed through. Please assert the mock props or have the mock render line.content so this test actually catches stale transcript overwrites.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/__tests__/HistoryDetail.test.tsx` around lines
252 - 305, The stale-transcript test in HistoryDetail.test.tsx is not validating
actual transcript content because the current TranscriptViewer mock only exposes
a test id/count. Update the mock used by HistoryDetail so it either renders each
transcript line’s content or otherwise records the received props, then change
the assertion in the rapid record switch test to verify the late transcript
content remains and that the stale early content is not rendered. Use the
TranscriptViewer mock and the HistoryDetail/record switch test case to locate
the fix.

Comment on lines +265 to +275
Ok(r) => {
let detail = r.error.as_deref().unwrap_or("unsuccessful=true");
tracing::warn!(
connection_id = %conn.id,
error = %detail,
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST returned unsuccessful"
);
Err(format!(
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST unsuccessful for connection {}: {detail}",
conn.id
))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the contradictory fallback error text.

Line 266 is in the r.successful == false branch, but the fallback message says unsuccessful=true. That string can bubble into the returned Err(...), so logs or UI will describe the failure state incorrectly.

Proposed fix
-            let detail = r.error.as_deref().unwrap_or("unsuccessful=true");
+            let detail = r.error.as_deref().unwrap_or("successful=false");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Ok(r) => {
let detail = r.error.as_deref().unwrap_or("unsuccessful=true");
tracing::warn!(
connection_id = %conn.id,
error = %detail,
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST returned unsuccessful"
);
Err(format!(
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST unsuccessful for connection {}: {detail}",
conn.id
))
Ok(r) => {
let detail = r.error.as_deref().unwrap_or("successful=false");
tracing::warn!(
connection_id = %conn.id,
error = %detail,
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST returned unsuccessful"
);
Err(format!(
"[meet:upcoming] GOOGLECALENDAR_EVENTS_LIST unsuccessful for connection {}: {detail}",
conn.id
))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent_meetings/upcoming.rs` around lines 265 - 275, The
fallback error text in the GOOGLECALENDAR_EVENTS_LIST failure path is
contradictory and should match the `r.successful == false` branch. Update the
`detail` handling in `upcoming.rs` so the default string used by the
`tracing::warn!` call and the returned `Err(...)` in the `Ok(r)` arm of the
upcoming meetings flow reflects an unsuccessful response consistently, using the
same symbols (`r.error`, `detail`, `conn.id`, and the `"[meet:upcoming]
GOOGLECALENDAR_EVENTS_LIST..."` message) to locate and fix the fallback text.

- Don't render the editable controls after a failed initial settings load:
  track loaded/loadError and show an error + retry instead, so a save can't
  push a partial patch built from hard-coded defaults (which could wipe
  platform_auto_join_policies).
- Replace the single global persistSeqRef with a per-setting-key sequence
  map so a failed save for one control is no longer masked by a successful
  save of another; superseded responses for the same setting are still
  ignored.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx`:
- Around line 229-247: The optimistic rollback test for the auto-join setting is
too weak because it only checks the final reverted value in
MeetDefaultsDrawer.test.tsx. In the test around the updateMock / rejectAutoJoin
flow, first assert that the combobox actually changes to the optimistic value
('always') after fireEvent.change and before rejecting, then keep the existing
revert assertion to confirm it returns to 'ask_each_time'. Use the existing
selects[0], rejectAutoJoin, and screen.getAllByRole('combobox') references to
place the added assertion in the same test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ea7b678-8e16-4eb9-b333-d8abd14c5d05

📥 Commits

Reviewing files that changed from the base of the PR and between 8949a7b and 5ad303d.

📒 Files selected for processing (2)
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/src/components/meetings/MeetDefaultsDrawer.tsx

Comment on lines +229 to +247
// Optimistically change setting A (auto_join_policy: 'ask_each_time' → 'always')
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[0], { target: { value: 'always' } });

// Optimistically change setting B (listen_only: true → false)
const listenSwitch = screen.getByRole('switch', { name: /listen.only/i });
fireEvent.click(listenSwitch);

// Wait for both saves to have been dispatched
await waitFor(() => expect(updateMock).toHaveBeenCalledTimes(2));

// Save B (listen_only) has already resolved successfully; now reject save A
rejectAutoJoin(new Error('network error'));

// Setting A should revert to its original value
await waitFor(() => {
const autoJoinSelect = screen.getAllByRole('combobox')[0];
expect(autoJoinSelect).toHaveValue('ask_each_time');
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the optimistic value before rejecting so the revert assertion is meaningful.

Setting A's original value after load is 'ask_each_time', and the final assertion at Line 246 also expects 'ask_each_time'. Because the start state and the post-revert state are identical, this test can pass even if the optimistic change → revert path never actually executed. Assert that selects[0] becomes 'always' after the change (and before rejectAutoJoin), so the revert genuinely demonstrates a state transition. Setting B (Lines 234-235, 250-251) already avoids this since true → false differs from its start state.

This is the same weakness pattern previously flagged on the watch-calendar rollback test (lines 150-160).

💚 Suggested hardening
     const selects = screen.getAllByRole('combobox');
     fireEvent.change(selects[0], { target: { value: 'always' } });
+    // Confirm the optimistic update actually applied before we reject.
+    expect(screen.getAllByRole('combobox')[0]).toHaveValue('always');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Optimistically change setting A (auto_join_policy: 'ask_each_time' → 'always')
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[0], { target: { value: 'always' } });
// Optimistically change setting B (listen_only: true → false)
const listenSwitch = screen.getByRole('switch', { name: /listen.only/i });
fireEvent.click(listenSwitch);
// Wait for both saves to have been dispatched
await waitFor(() => expect(updateMock).toHaveBeenCalledTimes(2));
// Save B (listen_only) has already resolved successfully; now reject save A
rejectAutoJoin(new Error('network error'));
// Setting A should revert to its original value
await waitFor(() => {
const autoJoinSelect = screen.getAllByRole('combobox')[0];
expect(autoJoinSelect).toHaveValue('ask_each_time');
});
// Optimistically change setting A (auto_join_policy: 'ask_each_time' → 'always')
const selects = screen.getAllByRole('combobox');
fireEvent.change(selects[0], { target: { value: 'always' } });
// Confirm the optimistic update actually applied before we reject.
expect(screen.getAllByRole('combobox')[0]).toHaveValue('always');
// Optimistically change setting B (listen_only: true → false)
const listenSwitch = screen.getByRole('switch', { name: /listen.only/i });
fireEvent.click(listenSwitch);
// Wait for both saves to have been dispatched
await waitFor(() => expect(updateMock).toHaveBeenCalledTimes(2));
// Save B (listen_only) has already resolved successfully; now reject save A
rejectAutoJoin(new Error('network error'));
// Setting A should revert to its original value
await waitFor(() => {
const autoJoinSelect = screen.getAllByRole('combobox')[0];
expect(autoJoinSelect).toHaveValue('ask_each_time');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx` around
lines 229 - 247, The optimistic rollback test for the auto-join setting is too
weak because it only checks the final reverted value in
MeetDefaultsDrawer.test.tsx. In the test around the updateMock / rejectAutoJoin
flow, first assert that the combobox actually changes to the optimistic value
('always') after fireEvent.change and before rejecting, then keep the existing
revert assertion to confirm it returns to 'ask_each_time'. Use the existing
selects[0], rejectAutoJoin, and screen.getAllByRole('combobox') references to
place the added assertion in the same test.

…et-settings RPC schema

The update_meet_settings controller schema (schema_defs.rs) only listed
auto_join_policy/listen_only_default/etc., so core's validate_params
rejected watch_calendar (and platform_auto_join_policies) as an 'unknown
param' BEFORE the handler ran — even though the Rust struct + handler
accepted them. Declare both in the update inputs and get outputs so the
Defaults drawer can actually persist them.
The review-fix agents ran cargo check / eslint but not cargo fmt /
prettier, leaving formatting that fails the Rust Quality + Frontend
Checks CI lanes. Reformat the affected agent_meetings + config Rust and
the app/src/components/meetings + i18n files. No logic changes.
@coderabbitai coderabbitai Bot removed the feature Net-new user-facing capability or product behavior. label Jun 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/config/schemas/schema_defs.rs`:
- Around line 500-510: The meet settings response schema is treating optional
fields as plain types even though `handle_get_meet_settings` serializes unset
`Option` values as null. Update the relevant `FieldSchema` entries in the schema
definition so the `platform_auto_join_policies` and `watch_calendar` fields use
`TypeSchema::Option(...)` around their current types, keeping the existing field
names and comments intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da0a0474-8fab-4956-94bb-a007a342f10d

📥 Commits

Reviewing files that changed from the base of the PR and between 5ad303d and eb73804.

📒 Files selected for processing (48)
  • app/src/components/meetings/ActionItemChecklist.tsx
  • app/src/components/meetings/HistoryDetail.tsx
  • app/src/components/meetings/HistoryRail.tsx
  • app/src/components/meetings/HistorySection.tsx
  • app/src/components/meetings/JoinPolicyToggle.tsx
  • app/src/components/meetings/MeetComposer.tsx
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/MeetingsPage.tsx
  • app/src/components/meetings/PlatformChips.tsx
  • app/src/components/meetings/TranscriptViewer.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • app/src/components/meetings/__tests__/ActionItemChecklist.test.tsx
  • app/src/components/meetings/__tests__/ActiveMeetingBanner.test.tsx
  • app/src/components/meetings/__tests__/HistoryDetail.test.tsx
  • app/src/components/meetings/__tests__/HistoryRail.test.tsx
  • app/src/components/meetings/__tests__/HistorySection.test.tsx
  • app/src/components/meetings/__tests__/JoinPolicyToggle.test.tsx
  • app/src/components/meetings/__tests__/MeetComposer.test.tsx
  • app/src/components/meetings/__tests__/MeetDefaultsDrawer.test.tsx
  • app/src/components/meetings/__tests__/TranscriptViewer.test.tsx
  • app/src/components/meetings/__tests__/UpcomingTable.test.tsx
  • app/src/components/meetings/__tests__/useUpcomingMeetings.test.ts
  • app/src/components/meetings/useUpcomingMeetings.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/en.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/id.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/ru.ts
  • app/src/pages/Skills.tsx
  • app/src/services/__tests__/meetCallService.test.ts
  • src/openhuman/agent_meetings/calendar.rs
  • src/openhuman/agent_meetings/ops.rs
  • src/openhuman/agent_meetings/schemas.rs
  • src/openhuman/agent_meetings/store.rs
  • src/openhuman/agent_meetings/upcoming.rs
  • src/openhuman/config/schema/meet.rs
  • src/openhuman/config/schemas/controllers.rs
  • src/openhuman/config/schemas/schema_defs.rs
  • src/openhuman/subconscious/heartbeat/planner/mod.rs
  • tests/json_rpc_e2e.rs
✅ Files skipped from review due to trivial changes (8)
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/ko.ts
  • app/src/lib/i18n/ar.ts
  • app/src/lib/i18n/it.ts
  • app/src/lib/i18n/pt.ts
🚧 Files skipped from review as they are similar to previous changes (39)
  • app/src/components/meetings/tests/HistoryRail.test.tsx
  • app/src/components/meetings/tests/JoinPolicyToggle.test.tsx
  • app/src/pages/Skills.tsx
  • app/src/components/meetings/TranscriptViewer.tsx
  • app/src/lib/i18n/de.ts
  • app/src/components/meetings/tests/TranscriptViewer.test.tsx
  • app/src/components/meetings/JoinPolicyToggle.tsx
  • app/src/components/meetings/HistorySection.tsx
  • app/src/components/meetings/ActionItemChecklist.tsx
  • app/src/lib/i18n/hi.ts
  • src/openhuman/config/schema/meet.rs
  • app/src/components/meetings/MeetingsPage.tsx
  • tests/json_rpc_e2e.rs
  • app/src/lib/i18n/id.ts
  • app/src/components/meetings/HistoryRail.tsx
  • app/src/lib/i18n/en.ts
  • app/src/components/meetings/useUpcomingMeetings.ts
  • app/src/components/meetings/tests/ActionItemChecklist.test.tsx
  • app/src/components/meetings/PlatformChips.tsx
  • app/src/lib/i18n/fr.ts
  • src/openhuman/subconscious/heartbeat/planner/mod.rs
  • app/src/components/meetings/tests/ActiveMeetingBanner.test.tsx
  • app/src/components/meetings/tests/HistorySection.test.tsx
  • src/openhuman/agent_meetings/schemas.rs
  • src/openhuman/agent_meetings/store.rs
  • app/src/components/meetings/tests/HistoryDetail.test.tsx
  • app/src/services/tests/meetCallService.test.ts
  • app/src/components/meetings/HistoryDetail.tsx
  • app/src/components/meetings/MeetComposer.tsx
  • app/src/components/meetings/MeetDefaultsDrawer.tsx
  • app/src/components/meetings/tests/MeetDefaultsDrawer.test.tsx
  • src/openhuman/config/schemas/controllers.rs
  • src/openhuman/agent_meetings/ops.rs
  • app/src/components/meetings/tests/useUpcomingMeetings.test.ts
  • src/openhuman/agent_meetings/upcoming.rs
  • app/src/components/meetings/tests/MeetComposer.test.tsx
  • app/src/components/meetings/UpcomingTable.tsx
  • src/openhuman/agent_meetings/calendar.rs
  • app/src/components/meetings/tests/UpcomingTable.test.tsx

Comment on lines +500 to +510
FieldSchema {
name: "platform_auto_join_policies",
ty: TypeSchema::Json,
comment: "Per-platform auto-join overrides keyed by platform slug.",
required: false,
},
FieldSchema {
name: "watch_calendar",
ty: TypeSchema::Bool,
comment: "Whether the heartbeat watches the calendar to drive auto-join / ask.",
required: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Schema type / validator definitions =="
rg -n -C3 'enum TypeSchema|struct FieldSchema|required:\s*false|TypeSchema::Option|fn .*validate|validate_.*schema|ControllerSchema' src/openhuman -g '!**/target/**'

echo
echo "== Other optional output-field patterns in schema_defs.rs =="
rg -n -C2 'outputs: vec!\[|required:\s*false|TypeSchema::Option' src/openhuman/config/schemas/schema_defs.rs

echo
echo "== Meet-settings handler / payload construction =="
rg -n -C4 '"get_meet_settings"|platform_auto_join_policies|watch_calendar' src/openhuman -g '!**/target/**'

Repository: tinyhumansai/openhuman

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant file outlines =="
ast-grep outline src/openhuman/config/schemas/schema_defs.rs --view expanded | sed -n '1,220p'
echo
ast-grep outline src/openhuman/core/*.rs --view expanded 2>/dev/null | sed -n '1,220p' || true
echo
echo "== Search for TypeSchema definition and validation =="
rg -n -C3 'enum TypeSchema|struct FieldSchema|impl .*TypeSchema|validate.*TypeSchema|required:\s*false|Option\(Box::new\(TypeSchema::Json\)\)|Option\(Box::new\(TypeSchema::Bool\)\)' src -g '!**/target/**'

Repository: tinyhumansai/openhuman

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Relevant file outline: schema_defs.rs =="
ast-grep outline src/openhuman/config/schemas/schema_defs.rs --view expanded | sed -n '1,240p'

echo
echo "== TypeSchema / validator definitions =="
rg -n -C3 'enum TypeSchema|struct FieldSchema|impl .*TypeSchema|validate.*TypeSchema|required:\s*false|Option\(Box::new\(TypeSchema::Json\)\)|Option\(Box::new\(TypeSchema::Bool\)\)' src -g '!**/target/**'

Repository: tinyhumansai/openhuman

Length of output: 50379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find core schema types =="
fd -a '.*(schema|controller|type).*\.rs' src/openhuman src | sed -n '1,120p'

echo
echo "== Search for TypeSchema and FieldSchema definitions only =="
rg -n -C2 'enum TypeSchema|struct FieldSchema|type TypeSchema|struct ControllerSchema|required:\s*false' src/openhuman/core src/openhuman -g '!**/target/**' | head -n 200

echo
echo "== Search for output validation / serialization path =="
rg -n -C2 'outputs: vec|output_field\(|validate.*output|validate.*schema|required.*output|serde_json::Value::Null|null' src/openhuman -g '!**/target/**' | head -n 250

Repository: tinyhumansai/openhuman

Length of output: 22571


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Focused search: schema types and validation =="
rg -n -C2 'enum TypeSchema|struct FieldSchema|struct ControllerSchema|required:\s*false|TypeSchema::Option|serde_json::Value::Null|null' src/openhuman \
  -g '!**/target/**' \
  -g '*.rs' | head -n 250

Repository: tinyhumansai/openhuman

Length of output: 17686


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate schema_defs.rs fields around get_meet_settings =="
sed -n '480,515p' src/openhuman/config/schemas/schema_defs.rs | cat -n

echo
echo "== Locate the corresponding handler / response construction =="
rg -n -C4 'get_meet_settings|platform_auto_join_policies|watch_calendar' src/openhuman -g '!**/target/**' -g '*.rs' | head -n 120

echo
echo "== Search for validation semantics around required false / null =="
rg -n -C3 'required:\s*false|skip_serializing_if|Option<|Null|may be null|omit' src/openhuman -g '!**/target/**' -g '*.rs' | head -n 200

Repository: tinyhumansai/openhuman

Length of output: 25265


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Meet settings helper structs =="
sed -n '100,170p' src/openhuman/config/schemas/helpers.rs | cat -n

echo
echo "== get_meet_settings handler =="
rg -n -C6 'handle_get_meet_settings|get_meet_settings' src/openhuman/config/schemas/controllers.rs

echo
echo "== Serialization hints in config schema files =="
rg -n -C2 'skip_serializing_if|serde::Serialize|Option<|serde_json::to_value|json!\(' src/openhuman/config/schemas src/openhuman/config -g '*.rs' | head -n 200

Repository: tinyhumansai/openhuman

Length of output: 20011


Align the meet settings response schema with nullable fields
handle_get_meet_settings serializes these values directly from Option fields, so unset settings become null. required: false is not enough here; wrap both outputs in TypeSchema::Option(...) so the validator matches the actual payload.
src/openhuman/config/schemas/schema_defs.rs:500-510

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/config/schemas/schema_defs.rs` around lines 500 - 510, The meet
settings response schema is treating optional fields as plain types even though
`handle_get_meet_settings` serializes unset `Option` values as null. Update the
relevant `FieldSchema` entries in the schema definition so the
`platform_auto_join_policies` and `watch_calendar` fields use
`TypeSchema::Option(...)` around their current types, keeping the existing field
names and comments intact.

run_event_planner_tick_for_config short-circuited the planner unless a
notify_* category was on — so watch_calendar (which only gates the inner
planner) never actually let calendar collection / auto-join run. Add
watch_calendar to this outer gate too, matching evaluate_and_dispatch, so
enabling 'Watch my calendar' actually drives auto-join.
The redesign replaced the single-platform inline form with a platform
selector, so the spec's 'Zoom/Teams absent' assertions are now wrong.
Assert the four platform radio chips are present instead.
@graycyrus

Copy link
Copy Markdown
Contributor Author

CI note: the Rust Core Coverage failure is pre-existing and unrelated to this PR

The only red lane is Rust Core Coverage (cargo-llvm-cov), which aborts on a stack overflow in:

openhuman::cron::scheduler::tests::run_agent_job_returns_error_without_provider_key

Every meetings-related lane is green: Rust Quality (fmt/clippy) ✅ · Rust E2E ✅ · Frontend Checks ✅ · E2E (Playwright) ✅ (the latter fixed in 84f86dcc7 for the multi-platform composer), plus all soft checks.

Tracking the cron overflow separately; it should not block this PR on its own merit.

@graycyrus graycyrus merged commit 5853aaa into tinyhumansai:main Jun 30, 2026
12 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant