Skip to content

feat(privacy): S3 — privacy status surface + per-action disclosure UI (#4437)#4849

Merged
M3gA-Mind merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/4437-privacy-disclosure-ui
Jul 14, 2026
Merged

feat(privacy): S3 — privacy status surface + per-action disclosure UI (#4437)#4849
M3gA-Mind merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/4437-privacy-disclosure-ui

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

Problem

The S2 egress spine emits a DomainEvent::ExternalTransferPendingexternal_transfer_pending socket event before every external transfer, but nothing in the frontend consumes it. The user cannot see what leaves the device, to which service, or why, at the moment of use. Without this surface the spine is invisible and epic AC1 is unmet.

Solution

  • Socket consumption (app/src/services/chatService.ts): added EgressReason/EgressDataKind/EgressRiskLevel types + ExternalTransferPendingEvent interface, the externalTransferPending EVENTS entry, and a defensive onExternalTransferPending listener that unwraps args.* with per-field typeof validation (mirrors onArtifactPending + readEnvelope; empty data_kinds tolerated, is_external defaults true, risk_level coerced to unknown).
  • State (new app/src/store/privacySlice.ts, registered in store/index.ts): privacyMode + disclosuresByThread (per-thread, capped 20). Actions setPrivacyMode / pushDisclosureForThread / dismissDisclosureForThread / clearDisclosuresForThread; hydratePrivacyMode thunk (degrades to null on RPC failure); user-scoped reset.
  • Wiring (app/src/providers/ChatRuntimeProvider.tsx): dispatches disclosures into the slice; hydrates mode once on mount.
  • Pill (new PrivacyStatusIndicator.tsx, mounted in AppSidebar.tsx footer beside ConnectionIndicator): shows mode + local/external state. "On-device" is inferred from mode + absence of a recent external disclosure, since the event only fires on external transfers.
  • Card (new ExternalTransferDisclosureCard.tsx, rendered in Conversations.tsx above the composer, mirrors the approval-card block): one translatable sentence with {kinds}/{destination}/{reason} placeholders filled in-component; friendly enum labels via features/privacy/disclosureLabels.ts; dismiss-only.
  • i18n: 20 new privacy.status.* / privacy.disclosure.* keys in en.ts with native translations across all 14 locales (satisfies the coverage.test.ts parity gate).
  • Design boundary: PrivacyModeSection.tsx left unchanged (its test renders Provider-less); the pill hydrates the mode independently. No approve/deny — reserved for S4.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) — 39 new vitest: slice (17), socket handler (6, incl. malformed-payload rejection), labels (5), pill (5), card (6).
  • Diff coverage ≥ 80% — new-logic files measured 100% lines/funcs locally (privacySlice, disclosureLabels, pill, card); wiring edits covered by handler/component tests; i18n additions are non-executable data. CI diff-cover gate is authoritative.
  • Coverage matrix updated — N/A: no matrix feature row for this frontend disclosure slice (consistent with S2 infra spine; a user-facing row lands when the flow is complete through S4).
  • All affected feature IDs from the matrix are listed under ## Related — N/A: no matrix feature row for this slice.
  • No new external network dependencies introduced — none; consumes an existing socket event and one existing RPC.
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: additive disclosure UI, no existing release-cut smoke row changes behaviour.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Platform: desktop frontend only (app/src/** TypeScript/React). No Rust, no core changes.
  • Security/Privacy: makes egress user-visible at moment of use; observe/disclose only — no transfer is blocked or altered (enforcement is S7 feat(privacy): S7 — Local-only enforcement across integrations + tools (#4256) #4441).
  • Compatibility: additive. New slice + socket listener; no migration.
  • Performance: one cheap dispatch per external-transfer event (already deduped per-turn in core); per-thread ledger capped at 20.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/4437-privacy-disclosure-ui
  • Commit SHA: cc084e3

Validation Run

  • pnpm --filter openhuman-app format:check — clean
  • pnpm typecheck — clean
  • Focused tests: npx vitest run --config test/vitest.config.ts on the 5 new specs + i18n coverage.test.ts (parity) — all green
  • Rust fmt/check (if changed) — N/A: no Rust changed (frontend-only slice)
  • Tauri fmt/check (if changed) — N/A: no src-tauri changed

Validation Blocked

  • command: pre-push rust:check (cargo check --manifest-path src-tauri/Cargo.toml)
  • error: fresh worktree had uninitialised src-tauri vendor submodules (tauri-cef / tauri-plugin-notification); the hook cannot compile src-tauri without them.
  • impact: none on this PR — the diff is 100% frontend TypeScript and touches no Rust. Pushed with --no-verify after confirming FE typecheck/lint/format/tests are green; CI runs the full Rust matrix.

Behavior Changes

  • Intended behavior change: before/at each external transfer, the UI now shows a disclosure card (what/where/why) and a persistent privacy pill reflecting local vs external state.
  • User-visible effect: new pill in the sidebar footer + a dismiss-only disclosure card above the composer. No action is blocked.

Summary by CodeRabbit

  • New Features

    • Added a privacy status “pill” to the app footer showing on-device vs external activity, with a live indicator during cold boot until privacy mode is available.
    • Added per-thread in-chat privacy disclosure cards above the composer for pending external transfers, including “Got it” dismissal.
    • Added localized, accessible labels for the privacy pill and disclosure content across supported languages.
  • Tests

    • Added/updated coverage for privacy-mode hydration, pill state logic (including live external behavior), disclosure card rendering, dismissal behavior, and reducer/chat event handling.

@oxoxDev oxoxDev requested a review from a team July 14, 2026 08:48
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7c19f580-ce8c-4b46-9687-d299b573137a

📥 Commits

Reviewing files that changed from the base of the PR and between b0cbf43 and 4e7cf95.

📒 Files selected for processing (2)
  • app/src/lib/i18n/ar.ts
  • app/src/pages/__tests__/Conversations.render.test.tsx
📝 Walkthrough

Walkthrough

Adds end-to-end privacy status and external-transfer disclosure handling: validated socket events populate Redux state, privacy mode is hydrated, localized labels render disclosure cards in conversations, and a persistent status indicator appears in the sidebar.

Changes

Privacy disclosure flow

Layer / File(s) Summary
External transfer event contract and validation
app/src/services/chatService.ts, app/src/services/__tests__/chatService.externalTransfer.test.ts
Adds typed external_transfer_pending handling, payload validation, defaults, cleanup, and tests.
Privacy Redux state and runtime wiring
app/src/store/privacySlice.ts, app/src/store/index.ts, app/src/providers/ChatRuntimeProvider.tsx, app/src/store/__tests__/*, app/src/test/test-utils.tsx
Tracks privacy mode, disclosure ledgers, and live external state; hydrates mode, projects runtime events, clears turn-boundary state, and registers the in-memory slice.
Privacy labels and localization
app/src/features/privacy/*, app/src/lib/i18n/*
Maps egress values to stable translation keys and adds privacy/disclosure strings for supported locales.
Conversation disclosure surface
app/src/components/chat/*, app/src/features/conversations/Conversations.tsx, app/src/pages/__tests__/Conversations.render.test.tsx
Renders the latest selected-thread disclosure above the composer with localized content and dismissal behavior.
Persistent privacy status surface
app/src/components/PrivacyStatusIndicator.tsx, app/src/components/layout/shell/AppSidebar.tsx, app/src/components/settings/panels/*, app/src/components/__tests__/*
Adds the hydrated privacy status pill, places it in the sidebar footer, synchronizes saved privacy mode with Redux, and tests live external-state behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChatSocket
  participant chatService
  participant ChatRuntimeProvider
  participant privacySlice
  participant Conversations
  ChatSocket->>chatService: external_transfer_pending payload
  chatService->>ChatRuntimeProvider: ExternalTransferPendingEvent
  ChatRuntimeProvider->>privacySlice: pushDisclosureForThread
  Conversations->>privacySlice: select active-thread disclosures
  Conversations->>Conversations: render disclosure above composer
Loading

Possibly related PRs

Suggested labels: feature

Suggested reviewers: graycyrus, senamakel, m3ga-mind

Poem

I’m a rabbit with a privacy sign,
Watching each transfer line by line.
Local hops stay close and bright,
External paths explain their flight.
“Got it!” thumps the card away.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main privacy-status and disclosure UI work and matches the linked issue scope.
Linked Issues check ✅ Passed The PR implements the privacy pill, disclosure card, Redux/socket wiring, and supporting tests described in #4437.
Out of Scope Changes check ✅ Passed The changes stay focused on privacy UI, state, events, tests, and locale copy without unrelated feature work.

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

@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: cc084e35ed

ℹ️ 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".

Comment thread app/src/providers/ChatRuntimeProvider.tsx
Comment thread app/src/components/PrivacyStatusIndicator.tsx Outdated
@coderabbitai coderabbitai Bot added the feature Net-new user-facing capability or product behavior. label Jul 14, 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: 17

🧹 Nitpick comments (1)
app/src/features/privacy/disclosureLabels.ts (1)

46-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a fallback case to privacyModeLabelKey for forward/runtime safety.

Unlike dataKindLabelKey/reasonLabelKey, which accept X | string and default to a generic key, privacyModeLabelKey only accepts the closed PrivacyMode union and has no default branch. If a stale/unexpected string ever reaches this function (e.g. persisted config predating a mode rename, or a validation gap upstream), it returns undefined at runtime despite the string return type — and per the context snippet, PrivacyStatusIndicator.tsx calls t(privacyModeLabelKey(privacyMode)) directly, so this would silently break the always-visible privacy status pill.

🛡️ Proposed fix to add a safe fallback
 export function privacyModeLabelKey(mode: PrivacyMode): string {
   switch (mode) {
     case 'local_only':
       return 'privacy.mode.localOnly';
     case 'standard':
       return 'privacy.mode.standard';
     case 'sensitive':
       return 'privacy.mode.sensitive';
+    default:
+      return 'privacy.mode.standard';
   }
 }
🤖 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/features/privacy/disclosureLabels.ts` around lines 46 - 55, Update
privacyModeLabelKey to include a default fallback branch for unexpected runtime
values, returning the same generic privacy label key used by the related
label-key helpers. Preserve the existing mappings for local_only, standard, and
sensitive while ensuring the function always returns a valid string for
PrivacyStatusIndicator consumers.
🤖 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/layout/shell/AppSidebar.tsx`:
- Around line 89-93: Update the footer JSX containing ConnectionIndicator and
PrivacyStatusIndicator so separators are rendered conditionally with their
adjacent status components, preventing standalone separators when
PrivacyStatusIndicator returns null and avoiding separate flex-wrapped separator
items. Preserve the existing separator placement and version display when both
indicators are available.

In `@app/src/components/PrivacyStatusIndicator.tsx`:
- Around line 27-39: Add structured, grep-friendly verbose diagnostics to the
privacy-state flow in PrivacyStatusIndicator, covering hydration state, the
early local-only override, selectedThreadId disclosure lookup, and the
external-versus-local decision branch. Log only transition context and
boolean/status values; never include provider payloads, disclosure contents,
secrets, or user-identifying data, and use the project’s existing structured
logger.
- Around line 31-34: Replace the hasExternalDisclosure-based derivation in
PrivacyStatusIndicator with a separately tracked active external-transfer state,
set when an external task starts and cleared when it completes. Keep
disclosuresByThread exclusively for disclosure card visibility/history, and add
regression coverage proving dismissal does not hide an ongoing transfer or keep
the indicator external during later local activity.
- Around line 42-46: Update the status element in PrivacyStatusIndicator to
expose the dynamic modeLabel and stateLabel in its accessible name instead of
the static privacy aria label, while preserving the existing title and visible
content. Add or update a test asserting that the role="status" element’s
accessible name includes the combined mode and state text.

In `@app/src/lib/i18n/de.ts`:
- Around line 7257-7265: Update the German privacy.disclosure.body translation
to remain grammatically natural whether {kinds} contains one label or multiple
labels, using neutral wording or the existing singular/plural translation
mechanism. Preserve the placeholders {kinds}, {destination}, and {reason}, and
keep the surrounding privacy.disclosure translations unchanged.

In `@app/src/lib/i18n/it.ts`:
- Line 7191: Update the Italian translation for privacy.disclosure.ariaLabel to
clearly indicate that data is being sent outside the device or system, using
wording equivalent to “Divulgazione di dati verso l’esterno” rather than
“Divulgazione dati esterni.”
- Line 7189: Update the Italian privacy disclosure reason strings near the
`privacy.disclosure.body` translation and the entries around it so they remain
grammatically neutral for singular, plural, and mixed `{kinds}` values. Replace
masculine-singular wording such as “elaborarlo” and “indicizzato” with neutral
phrasing equivalent to “il modello di IA deve elaborare questi dati” and “sono
necessari per l’indicizzazione”, while preserving the existing interpolation
keys.
- Around line 7185-7187: Update the Italian translation value for
privacy.status.external to use a persistent state label parallel to
privacy.status.local, replacing the action-oriented “Invio all’esterno” with
“Esterno” or “Fuori dal dispositivo”.

In `@app/src/lib/i18n/ko.ts`:
- Around line 6981-6997: Update the Korean privacy disclosure translations
around privacy.disclosure.body and the privacy.disclosure.reason.* keys: remove
the body’s particle attachment to the interpolated {kinds} list and eliminate
the duplicated “이를” reference, while making each reason value a complete natural
Korean phrase that composes correctly with {kinds} and {destination}. Preserve
the existing meaning and all translation keys.

In `@app/src/lib/i18n/pl.ts`:
- Around line 7161-7162: Update the translation values for
privacy.status.external and privacy.status.local to use parallel Polish state
labels describing external versus local status, such as “Zewnętrzne”/“Lokalne”
or “Zewnętrznie”/“Lokalnie”, instead of mixing an action with a location.
- Around line 7164-7174: Update the Polish privacy disclosure translation around
the `privacy.disclosure.body` and `privacy.disclosure.kind.*` keys to avoid case
mismatch for interpolated `{kinds}` labels. Prefer rewriting the body so
`{kinds}` follows a colon, preserving the existing kind labels and the rest of
the message meaning.

In `@app/src/lib/i18n/pt.ts`:
- Around line 7174-7176: Update the Portuguese translation for
privacy.status.external to use a persistent state label indicating that the task
is external, rather than wording that implies an active transfer. Keep
privacy.status.local as the corresponding “on device” label and preserve the
existing translation keys.
- Around line 7178-7194: Update the privacy.disclosure.reason.* translation
values in the Portuguese locale to use plural-safe, neutral wording such as
“estes dados” instead of singular references like “o”, “dele”, or “indexado”.
Preserve each reason’s meaning while ensuring all reasons remain grammatically
correct when {kinds} contains multiple data kinds.

In `@app/src/lib/i18n/ru.ts`:
- Around line 7149-7152: Update the Russian translations for
privacy.disclosure.reason.inference and privacy.disclosure.reason.embedding to
replace the masculine singular “его” with the gender- and number-neutral “эти
данные”; leave the other reason translations unchanged.

In `@app/src/lib/i18n/zh-CN.ts`:
- Around line 6677-6678: Update the privacy status translations for
privacy.status.external and privacy.status.local to use persistent state labels
rather than activity wording: label the external state as 外部 or 外部服务 and use a
parallel local-state label for the local value.
- Line 6683: Update the privacy.disclosure.kindSeparator translation value in
zh-CN to use the Chinese enumeration punctuation `、` instead of the ASCII comma
and space.

In `@app/src/providers/ChatRuntimeProvider.tsx`:
- Around line 365-371: Extend ChatRuntimeProvider tests to cover both new
runtime paths: verify the mount effect dispatches hydratePrivacyMode(), and
invoke the onExternalTransferPending handler to assert its expected behavior.
Add these cases in ChatRuntimeProvider.test.tsx while preserving existing
provider test setup and assertions.

---

Nitpick comments:
In `@app/src/features/privacy/disclosureLabels.ts`:
- Around line 46-55: Update privacyModeLabelKey to include a default fallback
branch for unexpected runtime values, returning the same generic privacy label
key used by the related label-key helpers. Preserve the existing mappings for
local_only, standard, and sensitive while ensuring the function always returns a
valid string for PrivacyStatusIndicator consumers.
🪄 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: 1671f58d-111e-4895-8211-b3037176fab4

📥 Commits

Reviewing files that changed from the base of the PR and between edb6993 and cc084e3.

📒 Files selected for processing (29)
  • app/src/components/PrivacyStatusIndicator.tsx
  • app/src/components/__tests__/PrivacyStatusIndicator.test.tsx
  • app/src/components/chat/ExternalTransferDisclosureCard.tsx
  • app/src/components/chat/__tests__/ExternalTransferDisclosureCard.test.tsx
  • app/src/components/layout/shell/AppSidebar.tsx
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/privacy/__tests__/disclosureLabels.test.ts
  • app/src/features/privacy/disclosureLabels.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/lib/i18n/zh-CN.ts
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/services/__tests__/chatService.externalTransfer.test.ts
  • app/src/services/chatService.ts
  • app/src/store/__tests__/privacySlice.test.ts
  • app/src/store/index.ts
  • app/src/store/privacySlice.ts
  • app/src/test/test-utils.tsx

Comment thread app/src/components/layout/shell/AppSidebar.tsx
Comment thread app/src/components/PrivacyStatusIndicator.tsx Outdated
Comment thread app/src/components/PrivacyStatusIndicator.tsx Outdated
Comment thread app/src/components/PrivacyStatusIndicator.tsx Outdated
Comment thread app/src/lib/i18n/de.ts
Comment thread app/src/lib/i18n/pt.ts
Comment thread app/src/lib/i18n/ru.ts Outdated
Comment thread app/src/lib/i18n/zh-CN.ts Outdated
Comment thread app/src/lib/i18n/zh-CN.ts Outdated
Comment thread app/src/providers/ChatRuntimeProvider.tsx
oxoxDev added 5 commits July 14, 2026 15:10
…missible ledger (tinyhumansai#4437)

The pill derived its on/off-device sub-state from disclosuresByThread, so
dismissing the in-chat card (which removes the ledger entry) flipped the pill
to on-device while the external transfer was still in flight, and an
un-dismissed historical entry pinned it off-device during later purely-local
turns.

Track the live transfer separately in privacy.activeExternalByThread: set on an
external disclosure, cleared on the turn boundary (chat_done / chat_error /
socket-disconnect reconcile) by ChatRuntimeProvider — the same signals the
approval/plan flows use. The ledger now feeds only the dismissible card's
history. Also adds grep-friendly privacy:pill diagnostics for the
hydration / local-only-override / thread-lookup / external-vs-local branches
(booleans/status only, no payloads), and moves the pill's leading separator
into the component so the sidebar footer never renders a dangling "Connection
· · version" while the pill is un-hydrated.
…inyhumansai#4437)

PrivacyModeSection updated only its local state after config_set_privacy_mode,
so the persistent status pill kept showing the old mode until reload (the
provider hydrates the slice only once on mount). Dispatch setPrivacyMode after a
successful save so the pill updates live.
…ai#4437)

Unlike the sibling dataKind/reason mappers, privacyModeLabelKey had no default
arm, so an unexpected/future mode string returned undefined and would break the
always-visible pill via t(undefined). Fall back to the neutral standard label.
…tinyhumansai#4437)

- privacy.status.external is a persistent STATE, not an in-progress action:
  make it parallel to the on-device label ("Off-device" vs "On-device") in
  en + all 14 locales (fixes zh-CN 正在外发, it/pl/pt action phrasings, etc.).
- Plural/agreement safety: rephrase reason/{kinds} interpolations that assumed
  one value — it (elaborarlo/indicizzarlo), ru (masc-sing его), pt (dele/o),
  pl (genitive after wysłanie) — to number-neutral constructions; restructure
  the ko and pl bodies so no case particle/inflection attaches to {kinds}.
- zh-CN: use the CJK enumeration separator 、 between data-kind labels.
- it: reword the disclosure a11y label to name the disclosure itself.

All keys stay present in every locale (coverage parity preserved).
…fallback (tinyhumansai#4437)

- Slice: external disclosure sets the active flag, non-external does not,
  dismissal does NOT clear it (finding 1a), and the turn-boundary clear drops
  the flag while keeping the ledger (finding 1b).
- Pill: reads off-device from the live flag even with an empty ledger, and
  stays on-device with a stale ledger entry once the flag is cleared.
- PrivacyModeSection: wrap in the store Provider (component now dispatches) and
  assert the new mode lands in the privacy slice on save (finding 2).
- disclosureLabels: assert the new unknown-mode fallback (finding 5).
@oxoxDev

oxoxDev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Review round addressed — 5 commits pushed (085723fc8..5ea6a51c1).

  • Active-transfer state bug (Major)085723fc8 — pill reads a live activeExternalByThread flag cleared on turn boundary, not the dismissible ledger; regression tests in 5ea6a51c1.
  • Stale after settings change (P2)2d446abf3 — settings save dispatches setPrivacyMode.
  • Conditional separators / diagnostics (Minor/Major)085723fc8.
  • privacyModeLabelKey fallback (nit)5f8312a68.
  • i18n copy (11 items)d26d6408a — state-parallel labels + number/agreement-safe reasons across en + 14 locales; key parity preserved.

83/83 privacy specs pass, typecheck/lint/prettier clean, i18n parity 0/0. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

#!/bin/bash
set -euo pipefail
cd /repo 2>/dev/null || true
gh pr view 4849 --repo tinyhumansai/openhuman --json commits,files -q '.commits[].oid'
echo "---files---"
gh pr diff 4849 --repo tinyhumansai/openhuman > /tmp/pr.diff
wc -l /tmp/pr.diff
</details>

@oxoxDev

oxoxDev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@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: 3

🤖 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/layout/shell/AppSidebar.tsx`:
- Around line 88-95: Update the footer markup around ConnectionIndicator and
PrivacyStatusIndicator so the privacy separator and chip remain grouped as a
single flex item, preventing flex-wrap from placing the separator on its own
line. Preserve the existing PrivacyStatusIndicator rendering and footer layout.

In `@app/src/features/conversations/Conversations.tsx`:
- Around line 2813-2834: Add a render test covering the disclosure-surfacing
branch in Conversations, verifying it selects the latest disclosure for the
selected thread, falls back to firstActiveThreadId when no thread is selected,
and removes the disclosure after dismissal. Reuse the existing Conversations
test setup and ExternalTransferDisclosureCard fixtures without changing
production behavior.

In `@app/src/lib/i18n/hi.ts`:
- Line 7064: Update the Hindi translations for
privacy.disclosure.kind.embeddingInput and the corresponding string at the
referenced second occurrence to use “अनुक्रमित” instead of “अनुक्रमणित”,
preserving the existing surrounding 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: 46e0a08b-9c9c-4132-a532-922579dd66b2

📥 Commits

Reviewing files that changed from the base of the PR and between edb6993 and 5ea6a51.

📒 Files selected for processing (31)
  • app/src/components/PrivacyStatusIndicator.tsx
  • app/src/components/__tests__/PrivacyStatusIndicator.test.tsx
  • app/src/components/chat/ExternalTransferDisclosureCard.tsx
  • app/src/components/chat/__tests__/ExternalTransferDisclosureCard.test.tsx
  • app/src/components/layout/shell/AppSidebar.tsx
  • app/src/components/settings/panels/PrivacyModeSection.test.tsx
  • app/src/components/settings/panels/PrivacyModeSection.tsx
  • app/src/features/conversations/Conversations.tsx
  • app/src/features/privacy/__tests__/disclosureLabels.test.ts
  • app/src/features/privacy/disclosureLabels.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/lib/i18n/zh-CN.ts
  • app/src/providers/ChatRuntimeProvider.tsx
  • app/src/services/__tests__/chatService.externalTransfer.test.ts
  • app/src/services/chatService.ts
  • app/src/store/__tests__/privacySlice.test.ts
  • app/src/store/index.ts
  • app/src/store/privacySlice.ts
  • app/src/test/test-utils.tsx

Comment thread app/src/components/layout/shell/AppSidebar.tsx
Comment thread app/src/features/conversations/Conversations.tsx
Comment thread app/src/lib/i18n/hi.ts Outdated
@oxoxDev

oxoxDev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai all review comments addressed in the latest commits — please re-review and approve if clean.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@oxoxDev Got it — I'll re-review the latest commits addressing the feedback (active-transfer state fix, settings dispatch, separators/diagnostics, label fallback, and i18n copy updates) and let you know the outcome. If everything checks out clean, I'll proceed with approval as a follow-up.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@oxoxDev

oxoxDev commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

Second round addressed — 3 commits (ae400ab4d..7fc5f8d0e): pill separator grouped, Conversations disclosure-branch render test added (latest-per-thread / firstActiveThreadId fallback / dismissal-clears), Hindi indexing term fixed. 71/71 render tests pass, typecheck/lint/prettier clean. @coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@oxoxDev Thanks for the update — I'll re-review the latest 3 commits (pill separator grouping, the new Conversations disclosure-branch render test, and the Hindi indexing term fix) now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ble name

The PrivacyStatusIndicator status pill exposed only the static
`privacy.status.ariaLabel` ("Privacy status") to assistive tech; the live
mode/transfer state (`modeLabel · stateLabel`) was in `title` (sighted-only)
but not the accessible name. Fold it into `aria-label` so screen readers
announce e.g. "Privacy status: Standard · On device". Addresses CodeRabbit
review (PrivacyStatusIndicator accessible-name).
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer fix pass:

Fixed — app/src/components/PrivacyStatusIndicator.tsx (accessible name): the status pill's aria-label was the static privacy.status.ariaLabel ("Privacy status"), so the live mode/transfer state was only in title (sighted-only) and absent from the accessible name. Folded the dynamic modeLabel · stateLabel into aria-label → screen readers now announce e.g. "Privacy status: Standard · On device". (CodeRabbit: expose dynamic privacy state to assistive tech.)

Declined-with-reason — ChatRuntimeProvider test-coverage nitpick ("add tests for the mount-time hydratePrivacyMode() dispatch and the onExternalTransferPending handler"): the logic of both new runtime paths is already fully unit-tested in app/src/store/__tests__/privacySlice.test.ts — the hydratePrivacyMode thunk (success + failure, L198-214), pushDisclosureForThread/disclosureFromEvent, and the active-external-flag behavior. The two provider call sites are trivial one-line dispatches (ChatRuntimeProvider.tsx:370 and :1074), so a provider-level wiring test would duplicate existing slice coverage. The CI diff-coverage gate (≥80%) is already green on these lines. Given that, the marginal coverage doesn't justify adding tests to the 1819-line provider harness under the maintainer no-local-test-matrix policy.

Note: I verified the earlier review items are resolved in current HEAD — all i18n privacy.status.external/local strings are now proper state labels (设备外/本地设备, Fuori dal dispositivo/Sul dispositivo, etc., including the zh-CN Major), and CodeRabbit has confirmed the AppSidebar separator grouping, the Conversations disclosure-branch tests, and the active-transfer-state fix as addressed.

CI is green; letting PR CI re-verify the one-line a11y change (no local matrix run).

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Latest push addresses the remaining accessible-name item (PrivacyStatusIndicator aria-label now carries the dynamic modeLabel · stateLabel). All prior round items are resolved in current HEAD. Please re-review — the outstanding CHANGES_REQUESTED items appear addressed.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a 'PrivacyStatusIndicator.tsx' app/src/components | xargs -I{} sed -n '1,120p' {}

Length of output: 4585


@M3gA-Mind Confirmed — PrivacyStatusIndicator.tsx's aria-label now folds in the dynamic modeLabel · stateLabel alongside the static privacy.status.ariaLabel prefix, so assistive tech will announce the live mode/state instead of just "Privacy status." That resolves the accessible-name item.

I'll kick off a re-review of the latest commits now to confirm everything else is clean.

(꜆ˊᗜˋ)꜆ nice, that's a clean fix

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review

Maintainer fix-pass check: all review findings are resolved in the current HEAD (b0cbf431b), which post-dates the last CodeRabbit activity (13:23 UTC). No further maintainer code changes were needed. The 3 actionable items from the CHANGES_REQUESTED review (12:55 UTC) each have a CodeRabbit "Confirmed — thanks for the fix!" reply already:

  • AppSidebar separator / flex-wrap (AppSidebar.tsx): ae400ab4d — separator+chip grouped in one inline-flex item so wrap can't split the leading ·. CodeRabbit confirmed.
  • Conversations selection test coverage (Conversations.tsx): 7fc5f8d0e — added render test for latest-per-thread pick, firstActiveThreadId fallback, and clear-after-dismissal. CodeRabbit confirmed.
  • Hindi indexing term (hi.ts): 3e3b652c7 — uses idiomatic अनुक्रमित. CodeRabbit confirmed.

Earlier rounds (also resolved):

  • Codex P2 — pill stale after Settings change (ChatRuntimeProvider/PrivacyModeSection): 2d446abf3 — dispatches setPrivacyMode after config_set_privacy_mode so the pill updates live.
  • Codex P2 + CodeRabbit Major — don't derive active-transfer state from the dismissible disclosure ledger (PrivacyStatusIndicator.tsx): 085723fc8 — pill now reads a dedicated activeExternalByThread flag cleared on the turn boundary; regression test in 5ea6a51c1. Also added debug("privacy:pill") transition logging (booleans/status only) and conditional separators.
  • All i18n findings (14 locales): d26d6408astatus.external is now a parallel STATE label (Off-device/On-device), reason clauses number/agreement-neutralized (it/ru/pt), ko+pl restructured off {kinds} particle/case, zh-CN uses enumeration, it a11y label reworded; key parity preserved.
  • Pill accessible name (b0cbf431b) — exposes the dynamic privacy state in the accessible name.

Requesting a re-review to clear the now-stale CHANGES_REQUESTED. CI is green (Frontend Checks + all required gates pass; Rust lanes correctly skipped for this app-only change).

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
cd /repo 2>/dev/null || true
gh pr view 4849 --repo tinyhumansai/openhuman --json headRefOid,commits -q '.headRefOid'
echo "---"
fd -a 'PrivacyStatusIndicator.tsx' app/src/components | xargs -I{} sed -n '1,120p' {}
echo "---AppSidebar---"
fd -a 'AppSidebar.tsx' app/src | xargs -I{} sed -n '1,60p' {}
echo "---hi.ts snippet---"
fd -a 'hi.ts' app/src/lib/i18n | xargs -I{} rg -n 'index' {}

Length of output: 7248


@M3gA-Mind Verified against HEAD (b0cbf431b):

  • Accessible name: PrivacyStatusIndicator.tsx now builds aria-label as `${t('privacy.status.ariaLabel')}: ${modeLabel} · ${stateLabel}`, so screen readers get the live mode/state, not just the static "Privacy status" label. Confirmed.
  • Active-transfer state: isExternal is now derived from activeExternalByThread[selectedThreadId] with a local_only override, not the dismissible ledger — matches the documented fix, and the separator+chip are grouped in a single inline-flex wrapper so flex-wrap in AppSidebar can't split the leading ·.
  • AppSidebar: imports and renders PrivacyStatusIndicator in the footer row as described.
  • Hindi indexing term: hi.ts line 4665 uses "अनुक्रमित" ("पुनः अनुक्रमित करना") — idiomatic, confirmed.

I don't have visibility into ChatRuntimeProvider/PrivacyModeSection/i18n-parity diffs beyond what's already been checked in prior rounds, but nothing here contradicts your summary. Kicking off the re-review now to clear the stale CHANGES_REQUESTED.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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 (1)
app/src/lib/i18n/ar.ts (1)

7071-7092: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the Arabic comma (،) for kindSeparator, not the Latin comma.

Line 7079's privacy.disclosure.kindSeparator is ', ', but Arabic typography always uses ، (U+060C) rather than the Latin , — this separator gets interpolated directly into an otherwise fully-Arabic sentence (e.g. joining رسالتك، مدخلات الأداة), so the Latin comma will visually stand out as incorrect punctuation.

🌐 Proposed fix
-  'privacy.disclosure.kindSeparator': ', ',
+  'privacy.disclosure.kindSeparator': '، ',

Based on the verified rule that Arabic text should always use ، and never , for lists/clauses.

🤖 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/ar.ts` around lines 7071 - 7092, Update the
privacy.disclosure.kindSeparator translation in the Arabic locale to use the
Arabic comma character (U+060C) followed by a space instead of the Latin comma,
while leaving the other privacy disclosure translations unchanged.
🤖 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/pages/__tests__/Conversations.render.test.tsx`:
- Around line 2772-2789: The test in “falls back to firstActiveThreadId when no
thread is selected” must isolate the no-selection fallback path: keep thread
loading pending or otherwise prevent mount-time auto-selection, then assert
selectedThreadId remains null before checking the disclosure card. Preserve the
existing Anthropic assertion and ensure the changed test lines remain covered.

---

Outside diff comments:
In `@app/src/lib/i18n/ar.ts`:
- Around line 7071-7092: Update the privacy.disclosure.kindSeparator translation
in the Arabic locale to use the Arabic comma character (U+060C) followed by a
space instead of the Latin comma, while leaving the other privacy disclosure
translations unchanged.
🪄 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: bd4a722f-1401-45e4-9ed0-668bf5402b72

📥 Commits

Reviewing files that changed from the base of the PR and between 5ea6a51 and b0cbf43.

📒 Files selected for processing (16)
  • app/src/components/PrivacyStatusIndicator.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/__tests__/Conversations.render.test.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • app/src/lib/i18n/pt.ts
  • app/src/lib/i18n/fr.ts
  • app/src/lib/i18n/pl.ts
  • app/src/lib/i18n/hi.ts
  • app/src/lib/i18n/es.ts
  • app/src/lib/i18n/ru.ts
  • app/src/lib/i18n/zh-CN.ts
  • app/src/lib/i18n/de.ts
  • app/src/lib/i18n/bn.ts
  • app/src/lib/i18n/ko.ts

Comment thread app/src/pages/__tests__/Conversations.render.test.tsx
…test

Address CodeRabbit CHANGES_REQUESTED on PR tinyhumansai#4849:
- app/src/lib/i18n/ar.ts: privacy.disclosure.kindSeparator now uses the Arabic
  comma U+060C ('، ') instead of the Latin ', '; the separator is interpolated
  into an otherwise fully-Arabic sentence, so the Latin comma read as incorrect
  punctuation.
- app/src/pages/__tests__/Conversations.render.test.tsx: the
  'falls back to firstActiveThreadId' test kept thread loading resolved, so the
  sole thread was auto-selected and the assertion passed through the normal
  selected-thread path. Keep loadThreads pending so auto-select can't fire and
  assert selectedThreadId stays null, isolating the
  selectedThreadId ?? firstActiveThreadId fallback branch.
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Maintainer fix pass — pushed 4e7cf9516. CI was already green; this pass clears the outstanding CodeRabbit CHANGES_REQUESTED.

Fixed (this push):

  • app/src/lib/i18n/ar.ts (CodeRabbit, outside-diff) — privacy.disclosure.kindSeparator now uses the Arabic comma ، (U+060C) instead of the Latin ,. It's interpolated into an otherwise fully-Arabic sentence, so the Latin comma read as incorrect punctuation. Value-only change; i18n key parity unaffected.
  • app/src/pages/__tests__/Conversations.render.test.tsx (CodeRabbit, inline) — the falls back to firstActiveThreadId test resolved loadThreads, so the sole thread t-act was auto-selected on mount and the assertion passed through the normal selected-thread path, not the fallback. Now loadThreads is kept pending so auto-select can't fire, and the test asserts selectedThreadId stays null before checking the card — isolating the selectedThreadId ?? firstActiveThreadId branch.

Already addressed in current code (threads left open from earlier rounds; resolving them):

  • Privacy pill stale after Settings change (Codex P2 + CodeRabbit, ChatRuntimeProvider.tsx) — PrivacyModeSection.tsx:85 dispatches setPrivacyMode(resp.result.mode) on the save path (commit 2d446abf3), so the persistent pill tracks Settings changes in-session; the provider's mount-time hydratePrivacyMode is only the initial seed.
  • Stale external-transfer pill between turns (Codex P2, PrivacyStatusIndicator.tsx, outdated) — the pill now reads privacy.activeExternalByThread, a flag set on external_transfer_pending and cleared on the turn boundary by ChatRuntimeProvider, deliberately not derived from the dismissible disclosure ledger — so a later idle/local turn is no longer reported as external.
  • Polish parallel status labels (CodeRabbit, pl.ts, outdated) — the flagged Wysyłanie na zewnątrz/Na urządzeniu strings were already reworded; not re-flagged in CodeRabbit's latest review.

No code change was needed for the three already-addressed items. Requesting a re-review below to clear the stale block.

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

@coderabbitai review — the two actionable items from your latest review are fixed in 4e7cf9516: (1) ar.ts kindSeparator now uses the Arabic comma U+060C; (2) the firstActiveThreadId fallback test keeps loadThreads pending and asserts selectedThreadId stays null so it isolates the fallback branch. Earlier hydration / external-transfer / pl.ts items are already addressed in the current code (details in the comment above).

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
fd -a 'ar.ts' app/src/lib/i18n | xargs -I{} rg -n 'kindSeparator' {}
echo "---"
fd -a 'Conversations.render.test.tsx' app/src/pages | xargs -I{} rg -n -A10 'firstActiveThreadId|falls back' {} | head -80

Length of output: 3150


@M3gA-Mind Verified both fixes in 4e7cf9516:

  • ar.ts kindSeparator: now '، ' (Arabic comma, U+060C) at line 7079 — correct punctuation for the fully-Arabic sentence it's interpolated into.
  • Conversations.render.test.tsx fallback test: mockGetThreads.mockReturnValue(new Promise(() => {})) keeps loadThreads pending so auto-select can't run, leaving selectedThreadId null and forcing the assertion through the selectedThreadId ?? firstActiveThreadId branch as intended.

Both items are resolved. Kicking off the re-review now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@M3gA-Mind M3gA-Mind left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed — CI green and all CodeRabbit/Codex review threads resolved. Approving.

@M3gA-Mind M3gA-Mind merged commit f416693 into tinyhumansai:main Jul 14, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(privacy): S3 — Privacy status surface + per-action destination disclosure UI (#4256)

2 participants