Skip to content

feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 7)#4871

Open
justincrich wants to merge 28 commits into
superset-sh:mainfrom
justincrich:chat-mobile-sprint-1-molecules
Open

feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 7)#4871
justincrich wants to merge 28 commits into
superset-sh:mainfrom
justincrich:chat-mobile-sprint-1-molecules

Conversation

@justincrich
Copy link
Copy Markdown
Contributor

@justincrich justincrich commented May 23, 2026

Summary

Wave 2 of the chat-mobile UI build: 19 molecules composed from the wave-1 atoms (PR #4870) and vendor primitives. Each lives under `apps/mobile/components/{Name}/` with co-located `.tsx`, `.stories.tsx` (Storybook `Molecules/` prefix + full argTypes controls), and `index.ts`.

Composer cluster (UC-COMP):

  • PickerTrigger, ComposerSettingsButton, ComposerRow, SlashCommandOption, ModelPickerOption, ThinkingLevelOption

Chrome cluster:

  • AppHeader, ModalHeader, CodeBlock, Banner

Pause cluster (UC-PAUSE):

  • PendingApprovalCard, ApprovalFooter, SuggestedAnswerPill, PendingActionPill

Render cluster (UC-RENDER):

  • AssistantMessageHead, UserMessageBubble, ToolCallCard, CollapsedBlock, ScrollBackButton

Flips `molecules` gate to `passed` for both mobile platforms. Also includes ComposerRow refinements (single-tone bg, slash icon, Claude iOS layout).

Stack position — 4 of 5

Depends on: PR #4870 (atoms) → PR #4875 (ported) → PR #4874 (tooling)

# Branch Status
PR 1 `chat-mobile-sprint-1-tooling` #4874
PR 2 `chat-mobile-sprint-1-ported` #4875
PR 3 `chat-mobile-sprint-1-atoms` #4870
PR 4 (this) `chat-mobile-sprint-1-molecules` 19 molecules
PR 5 `chat-mobile-sprint-1-organisms` 10 organisms (depends on PR 4)

This branch contains PRs 1–3's commits + this PR's 3 commits. After PRs 1–3 merge, this will be rebased onto fresh `main` and the diff will shrink to molecules-only.

Test plan

  • `cd apps/mobile && bun storybook`
  • Navigate `Molecules/` section — all 19 molecule stories render
  • Composer molecules: open ComposerRow story, toggle variant (idle / typing / streaming / sending) — Send button + textarea state changes
  • Pause cluster: PendingApprovalCard / ApprovalFooter — toggle state controls
  • Render cluster: ToolCallCard — verify status variants (running / done / pending / error / neutral)
  • `bun run typecheck` passes

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive Storybook integration for mobile app component library with 50+ UI components.
    • Implemented visual component documentation and interactive testing environment.
    • Added font management and theme support across all mobile components.
  • Chores

    • Updated build configuration to support Storybook development workflow.
    • Added development dependencies for on-device Storybook controls and actions.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 23, 2026

📝 Walkthrough

Walkthrough

Adds on-device Storybook to the mobile app, gates boot via env flag, updates Metro, adds fonts/splash handling, and introduces numerous React Native UI components with corresponding stories and barrel exports.

Changes

Storybook integration and UI components

Layer / File(s) Summary
End-to-end Storybook wiring, app/config changes, components, stories, barrels
apps/mobile/.rnstorybook/*, apps/mobile/app/_layout.tsx, apps/mobile/metro.config.js, apps/mobile/package.json, apps/mobile/components/**/*, apps/mobile/screens/**/*
Storybook runtime, preview/decorators, router contexts, app splash/fonts and Storybook toggle, Metro wrapper, scripts/deps, and a comprehensive component library with stories and barrels.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

A rabbit taps its tiny keys,
Spinning tales of knobs and trees—
Stories bloom on mobile night,
UI constellations bright.
Hop by hop through themed delight,
Carrots cached, controls in sight.
Ship the stack—ears up, all right! 🥕✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

@capy-ai
Copy link
Copy Markdown

capy-ai Bot commented May 23, 2026

Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews.

@stage-review
Copy link
Copy Markdown

stage-review Bot commented May 23, 2026

@justincrich justincrich marked this pull request as draft May 23, 2026 00:03
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 148 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mobile/app/_layout.tsx">

<violation number="1" location="apps/mobile/app/_layout.tsx:34">
P1: Font load errors are ignored, which can leave the app permanently stuck on the splash screen when fonts fail to load.</violation>
</file>

<file name="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx">

<violation number="1" location="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx:82">
P2: `detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

Comment thread apps/mobile/app/_layout.tsx Outdated
@@ -0,0 +1,133 @@
import { Check, type LucideIcon, Target, X } from "lucide-react-native";
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.

P2: detailed mode claims internal scrolling but uses plain Text — content exceeding max-h-32 is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx, line 82:

<comment>`detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</comment>

<file context>
@@ -0,0 +1,133 @@
+	className,
+	...props
+}: PendingApprovalCardProps) {
+	const cfg = STATE[state];
+	const displayTitle = cfg.titleOverride ?? title;
+	const isResolving = state === "resolving";
</file context>

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (5)
apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx (1)

84-91: 💤 Low value

Consider using accessibilityRole="status" instead of "alert".

The status segment uses accessibilityRole="alert", but "alert" is semantically intended for important, time-sensitive information requiring immediate user attention. This status indicator (streaming, thinking, completed, etc.) is informational rather than critical. React Native supports "status" which better represents ongoing state information that updates dynamically.

♿ Proposed accessibility refinement
 {showStatus ? (
   <View
-    accessibilityRole="alert"
+    accessibilityRole="status"
     accessibilityLiveRegion="polite"
🤖 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 `@apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx` around
lines 84 - 91, The accessibilityRole on the status container is currently set to
"alert" but should be "status" because this component conveys non-critical,
dynamic state; update the View in AssistantMessageHead (the element with
accessibilityRole and accessibilityLiveRegion) to use accessibilityRole="status"
(keep accessibilityLiveRegion="polite" and the existing className/variant logic
unchanged) so screen readers treat it as ongoing informational status instead of
an urgent alert.
apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx (1)

86-89: ⚡ Quick win

Avoid opacity-based state styling for state signaling.

Line 86 and Line 88 use opacity for interaction/disabled state, which conflicts with the semantic-color state model documented for this control. Prefer semantic bg/border/text state classes instead.

♻️ Proposed adjustment
-				"flex-row items-center gap-1.5 h-7 px-3 rounded-full border border-border bg-card active:opacity-70",
+				"flex-row items-center gap-1.5 h-7 px-3 rounded-full border border-border bg-card active:bg-accent",
 				isOpen && "bg-accent",
-				disabled && "opacity-50",
+				disabled && "bg-muted border-muted",
 				className,
🤖 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 `@apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx`
around lines 86 - 89, The button currently uses opacity classes for
interaction/disabled states in ComposerSettingsButton (the JSX that composes the
className array referencing isOpen, disabled, and className), which conflicts
with the semantic-color model; replace opacity-based classes (active:opacity-70
and opacity-50) with semantic state classes instead (for example: use an active
state background/border like active:bg-accent/active:border-accent or
active:bg-accent-600 and a disabled semantic treatment such as
bg-muted/border-muted/text-muted or disabled:bg-muted/disabled:border-muted),
and ensure the isOpen branch still applies the intended accent background
(bg-accent) while removing opacity manipulations so the control uses semantic
bg/border/text classes for both active and disabled states.
apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx (1)

32-45: ⚡ Quick win

Consider merging className instead of allowing override.

The ghost variant applies bg-transparent border-border styling (line 32-33), but because {...props} is spread after the explicit className prop, any className passed by the caller will override the ghost styling entirely. This could break the ghost variant's visual appearance.

Consider using cn() to merge the ghost styles with any caller-provided className:

className={cn(
  variant === "ghost" && "bg-transparent border-border",
  props.className
)}

Then remove className from the spread to avoid duplication.

🤖 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 `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx` around
lines 32 - 45, The ghost variant's className on SuggestedAnswerPill is being
overridden by the spread {...props}; update the JSX to merge the ghost styles
with any caller className using the project's cn utility and pass that merged
value to Pill (e.g., compute className = cn(variant === "ghost" &&
"bg-transparent border-border", props.className)) and then remove className from
the {...props} spread so callers cannot fully override the ghost styling while
still allowing additional classes.
apps/mobile/components/ToastBase/ToastBase.tsx (1)

136-136: ⚡ Quick win

Consider truncating long body text in accessibilityLabel.

When body is used as the fallback accessibilityLabel, verbose toast messages may create overly long screen reader announcements. Consider truncating or summarizing long body text for better accessibility.

🤖 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 `@apps/mobile/components/ToastBase/ToastBase.tsx` at line 136, The
accessibilityLabel prop currently falls back to title ?? body which can pass
long verbose strings to screen readers; update the fallback logic in ToastBase
(the accessibilityLabel assignment) to truncate or summarize body when used as
the fallback (e.g., limit to a reasonable character count or words and append an
ellipsis) so that if title is missing the computed accessibilityLabel uses a
shortened version of body rather than the full text. Ensure the truncation
function is applied only when using body as the fallback and keep title
unchanged when present.
apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx (1)

40-40: ⚡ Quick win

Consider defensive handling for empty organization name.

If activeOrgName is an empty string, charAt(0) returns an empty string, resulting in an empty avatar fallback. While Storybook args default to "Superset", adding a fallback would make the component more robust.

🛡️ Suggested defensive approach
-const orgInitial = activeOrgName.charAt(0).toUpperCase();
+const orgInitial = activeOrgName.charAt(0).toUpperCase() || '?';
🤖 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
`@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
at line 40, The code computes orgInitial from activeOrgName using
activeOrgName.charAt(0), which yields an empty string for an empty name; update
the logic around orgInitial in OrgDropdown.stories.tsx to defensively handle
empty or falsy activeOrgName (e.g., check activeOrgName truthiness or length and
fall back to a default character like "?" or the first char of a known default)
so orgInitial is always a single visible character when rendering the avatar.
🤖 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 `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx`:
- Around line 80-114: The three ActivityIndicator instances inside
ApprovalFooter (the ones shown in the decline/approve/always Button render
branches controlled by resolving === "decline"/"approve"/"always") are using
className="text-..." which doesn't reliably set spinner color; replace className
with the ActivityIndicator prop color or colorClassName and supply the proper
color tokens (e.g., white or an accent/foreground token) for each case, removing
the text-* className so the spinner renders with the intended color.

In `@apps/mobile/components/CodeBlock/CodeBlock.tsx`:
- Around line 42-46: The handleCopy function schedules setCopied(false) with
setTimeout but never clears the timer, risking setState on an unmounted
component; store the timeout id (e.g., in a ref like copyTimeoutRef) when
calling setTimeout inside handleCopy, clear any existing timeout before creating
a new one, and add a useEffect cleanup that clears copyTimeoutRef.current on
unmount to prevent calling setCopied after the component is gone.

In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 87-105: The current unmount logic in PendingActionPill relies on
reading Reanimated shared values (opacity.value) which don't trigger React
renders, so the component never re-checks the `if (!visible && opacity.value ===
0) return null;` after the fade completes; fix by switching to a React
state-driven unmount or Reanimated entering/exiting: replace the manual
shared-value + useEffect flow (opacity, translateY, withTiming) with
Reanimated's layout/entering/exiting animations on the Animated.View (or use
withTiming's callback with runOnJS to call a local state setter such as
setIsMounted) so that when the animation finishes you call runOnJS(() =>
setIsMounted(false)) and use that isMounted state (instead of reading
opacity.value) to return null; update the render guard to check the React state
(isMounted && visible) and remove the shared-value read in the render path.

In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx`:
- Around line 81-87: In SlashCommandOption, ensure the Pressable's
accessibilityState.disabled mirrors actual interactivity by using the combined
condition (disabled || isLoading) instead of the current disabled ?? false;
update accessibilityState.disabled to reflect both props so screen readers see
the item as disabled when isLoading is true (keep accessibilityState.busy mapped
to isLoading and selected mapped to isHighlighted).

In `@apps/mobile/components/ToastBase/ToastBase.tsx`:
- Line 149: The ActivityIndicator usage in ToastBase (ActivityIndicator element)
incorrectly passes a Tailwind-derived CSS class via the className prop
(mapping.iconColor); replace this by resolving the actual color value/token from
the Tailwind class (or a theme mapping) and pass it to the ActivityIndicator
using its color prop (e.g., set color={resolvedColor}) instead of className,
ensuring mapping.iconColor is converted to a valid color string before use.

In `@apps/mobile/components/ToolCallCard/ToolCallCard.tsx`:
- Around line 121-124: In ToolCallCard replace the unsupported className prop on
the ActivityIndicator: remove className="text-state-live-fg" and pass the
spinner color via the color prop instead (e.g.,
color={theme.colors['state-live-fg']} or a hex/string literal); ensure you
obtain the theme color (via existing useTheme / styles / constants) or hardcode
the hex for state-live-fg so the ActivityIndicator renders the intended color.

In `@apps/mobile/design/manifest.json`:
- Line 90: The manifest's molecule build-plan entries currently have "status":
"deferred" for some molecules while other entries and the scope notes indicate
Wave 2 is complete; update the offending "status" fields (the molecule
build-plan entries that are out of sync) to "passed" so the molecule statuses
are consistent with the completed gates and the other entries in the manifest.

---

Nitpick comments:
In `@apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx`:
- Around line 84-91: The accessibilityRole on the status container is currently
set to "alert" but should be "status" because this component conveys
non-critical, dynamic state; update the View in AssistantMessageHead (the
element with accessibilityRole and accessibilityLiveRegion) to use
accessibilityRole="status" (keep accessibilityLiveRegion="polite" and the
existing className/variant logic unchanged) so screen readers treat it as
ongoing informational status instead of an urgent alert.

In `@apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx`:
- Around line 86-89: The button currently uses opacity classes for
interaction/disabled states in ComposerSettingsButton (the JSX that composes the
className array referencing isOpen, disabled, and className), which conflicts
with the semantic-color model; replace opacity-based classes (active:opacity-70
and opacity-50) with semantic state classes instead (for example: use an active
state background/border like active:bg-accent/active:border-accent or
active:bg-accent-600 and a disabled semantic treatment such as
bg-muted/border-muted/text-muted or disabled:bg-muted/disabled:border-muted),
and ensure the isOpen branch still applies the intended accent background
(bg-accent) while removing opacity manipulations so the control uses semantic
bg/border/text classes for both active and disabled states.

In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx`:
- Around line 32-45: The ghost variant's className on SuggestedAnswerPill is
being overridden by the spread {...props}; update the JSX to merge the ghost
styles with any caller className using the project's cn utility and pass that
merged value to Pill (e.g., compute className = cn(variant === "ghost" &&
"bg-transparent border-border", props.className)) and then remove className from
the {...props} spread so callers cannot fully override the ghost styling while
still allowing additional classes.

In `@apps/mobile/components/ToastBase/ToastBase.tsx`:
- Line 136: The accessibilityLabel prop currently falls back to title ?? body
which can pass long verbose strings to screen readers; update the fallback logic
in ToastBase (the accessibilityLabel assignment) to truncate or summarize body
when used as the fallback (e.g., limit to a reasonable character count or words
and append an ellipsis) so that if title is missing the computed
accessibilityLabel uses a shortened version of body rather than the full text.
Ensure the truncation function is applied only when using body as the fallback
and keep title unchanged when present.

In
`@apps/mobile/screens/`(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx:
- Line 40: The code computes orgInitial from activeOrgName using
activeOrgName.charAt(0), which yields an empty string for an empty name; update
the logic around orgInitial in OrgDropdown.stories.tsx to defensively handle
empty or falsy activeOrgName (e.g., check activeOrgName truthiness or length and
fall back to a default character like "?" or the first char of a known default)
so orgInitial is always a single visible character when rendering the avatar.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1412f4a1-6863-49ed-8710-313650c8235d

📥 Commits

Reviewing files that changed from the base of the PR and between c7883c7 and 353396c.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (147)
  • apps/mobile/.rnstorybook/.gitignore
  • apps/mobile/.rnstorybook/index.tsx
  • apps/mobile/.rnstorybook/main.js
  • apps/mobile/.rnstorybook/preview.tsx
  • apps/mobile/.rnstorybook/stories/DesignSystem/Colors.stories.tsx
  • apps/mobile/.rnstorybook/stories/DesignSystem/Icons.stories.tsx
  • apps/mobile/.rnstorybook/stories/DesignSystem/Spacing.stories.tsx
  • apps/mobile/.rnstorybook/stories/DesignSystem/Typography.stories.tsx
  • apps/mobile/app/_layout.tsx
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/AppHeader/AppHeader.tsx
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/Banner/Banner.stories.tsx
  • apps/mobile/components/Banner/Banner.tsx
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/CodeBlock/CodeBlock.stories.tsx
  • apps/mobile/components/CodeBlock/CodeBlock.tsx
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsx
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.tsx
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/ComposerRow/ComposerRow.stories.tsx
  • apps/mobile/components/ComposerRow/ComposerRow.tsx
  • apps/mobile/components/ComposerRow/index.ts
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsx
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/FabBase/FabBase.stories.tsx
  • apps/mobile/components/FabBase/FabBase.tsx
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/HelloWorld/HelloWorld.stories.tsx
  • apps/mobile/components/HelloWorld/HelloWorld.tsx
  • apps/mobile/components/HelloWorld/index.ts
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsx
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsx
  • apps/mobile/components/HitTargetWrapper/index.ts
  • apps/mobile/components/IconButton/IconButton.stories.tsx
  • apps/mobile/components/IconButton/IconButton.tsx
  • apps/mobile/components/IconButton/index.ts
  • apps/mobile/components/ModalHeader/ModalHeader.stories.tsx
  • apps/mobile/components/ModalHeader/ModalHeader.tsx
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsx
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/PendingActionPill/PendingActionPill.tsx
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsx
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/PickerTrigger/PickerTrigger.stories.tsx
  • apps/mobile/components/PickerTrigger/PickerTrigger.tsx
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/Pill/Pill.stories.tsx
  • apps/mobile/components/Pill/Pill.tsx
  • apps/mobile/components/Pill/index.ts
  • apps/mobile/components/ProgressDots/ProgressDots.stories.tsx
  • apps/mobile/components/ProgressDots/ProgressDots.tsx
  • apps/mobile/components/ProgressDots/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.tsx
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/ScrollFade/ScrollFade.stories.tsx
  • apps/mobile/components/ScrollFade/ScrollFade.tsx
  • apps/mobile/components/ScrollFade/index.ts
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsx
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/StatusDot/StatusDot.stories.tsx
  • apps/mobile/components/StatusDot/StatusDot.tsx
  • apps/mobile/components/StatusDot/index.ts
  • apps/mobile/components/StreamingCursor/StreamingCursor.stories.tsx
  • apps/mobile/components/StreamingCursor/StreamingCursor.tsx
  • apps/mobile/components/StreamingCursor/index.ts
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsx
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsx
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsx
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/ToastBase/ToastBase.stories.tsx
  • apps/mobile/components/ToastBase/ToastBase.tsx
  • apps/mobile/components/ToastBase/index.ts
  • apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
  • apps/mobile/components/ToolStatusRule/index.ts
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/components/ui/accordion.stories.tsx
  • apps/mobile/components/ui/alert-dialog.stories.tsx
  • apps/mobile/components/ui/alert.stories.tsx
  • apps/mobile/components/ui/aspect-ratio.stories.tsx
  • apps/mobile/components/ui/avatar.stories.tsx
  • apps/mobile/components/ui/badge.stories.tsx
  • apps/mobile/components/ui/button.stories.tsx
  • apps/mobile/components/ui/card.stories.tsx
  • apps/mobile/components/ui/checkbox.stories.tsx
  • apps/mobile/components/ui/collapsible.stories.tsx
  • apps/mobile/components/ui/context-menu.stories.tsx
  • apps/mobile/components/ui/dialog.stories.tsx
  • apps/mobile/components/ui/dropdown-menu.stories.tsx
  • apps/mobile/components/ui/hover-card.stories.tsx
  • apps/mobile/components/ui/icon.stories.tsx
  • apps/mobile/components/ui/input.stories.tsx
  • apps/mobile/components/ui/label.stories.tsx
  • apps/mobile/components/ui/menubar.stories.tsx
  • apps/mobile/components/ui/popover.stories.tsx
  • apps/mobile/components/ui/progress.stories.tsx
  • apps/mobile/components/ui/radio-group.stories.tsx
  • apps/mobile/components/ui/select.stories.tsx
  • apps/mobile/components/ui/separator.stories.tsx
  • apps/mobile/components/ui/skeleton.stories.tsx
  • apps/mobile/components/ui/switch.stories.tsx
  • apps/mobile/components/ui/tabs.stories.tsx
  • apps/mobile/components/ui/text.stories.tsx
  • apps/mobile/components/ui/textarea.stories.tsx
  • apps/mobile/components/ui/toggle-group.stories.tsx
  • apps/mobile/components/ui/toggle.stories.tsx
  • apps/mobile/components/ui/tooltip.stories.tsx
  • apps/mobile/design/manifest.json
  • apps/mobile/global.css
  • apps/mobile/lib/theme.ts
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
  • apps/mobile/screens/AUDIT.md

Comment on lines +80 to +114
{resolving === "decline" ? (
<ActivityIndicator size="small" className="text-white" />
) : (
<Text>Decline</Text>
)}
</Button>
<Button
variant="default"
size="default"
className="flex-1 h-touch-min"
onPress={onApprove}
accessibilityLabel="Approve tool action"
>
{resolving === "approve" ? (
<ActivityIndicator
size="small"
className="text-primary-foreground"
/>
) : (
<Text>Approve</Text>
)}
</Button>
<Button
variant="ghost"
size="default"
className="flex-1 h-touch-min"
onPress={onAlways}
accessibilityLabel="Always allow this tool category"
>
{resolving === "always" ? (
<ActivityIndicator size="small" className="text-foreground" />
) : (
<Text>Always</Text>
)}
</Button>
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify ActivityIndicator API in React Native

rg -n "import.*ActivityIndicator" apps/mobile/components -A5 | head -20

Repository: superset-sh/superset

Length of output: 1925


🌐 Web query:

Does React Native ActivityIndicator support className prop for styling or does it use a color prop?

💡 Result:

React Native’s built-in ActivityIndicator does not support a className prop for styling. Instead, it exposes a color prop to control the spinner’s foreground color, and it can accept normal View-style props via the style prop. The official React Native docs list supported props for ActivityIndicator and include a color prop (“The foreground color of the spinner”) but no className prop. [1] So, for React Native ActivityIndicator, use: - color={...} to change spinner color [1] - style={...} (inherited View props) for other styling needs like layout/positioning [1] If you see className mentioned, that’s coming from a non-core system (e.g., a library that adds className support such as NativeWind/NativeWind bindings), not from React Native’s ActivityIndicator itself. [2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,180p' apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | nl -ba | sed -n '60,140p'

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the exact section containing the three ActivityIndicator usages
nl -ba apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '60,140p'

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Show the exact section containing the three ActivityIndicator usages
nl -ba apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '60,140p'

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx | sed -n '70,130p'

Repository: superset-sh/superset

Length of output: 1599


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# 1) Find how className is supported for RN components (NativeWind / Tailwind RN)
rg -n --hidden -S "nativewind|tailwind-rn|nativewindcss|unwind|NativeWind" apps/mobile | head -50 || true

# 2) Find other ActivityIndicator usages in mobile to see if color prop is used
rg -n "ActivityIndicator" apps/mobile/components -S

Repository: superset-sh/superset

Length of output: 2769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show ToastBase ActivityIndicator usage around the reported line
sed -n '120,190p' apps/mobile/components/ToastBase/ToastBase.tsx

# Show IconButton ActivityIndicator usage around the reported lines
sed -n '90,170p' apps/mobile/components/IconButton/IconButton.tsx

# Show FabBase ActivityIndicator usage around the reported lines
sed -n '140,220p' apps/mobile/components/FabBase/FabBase.tsx

# Show ToolCallCard ActivityIndicator usage around the reported lines
sed -n '90,160p' apps/mobile/components/ToolCallCard/ToolCallCard.tsx

Repository: superset-sh/superset

Length of output: 5986


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Any ActivityIndicator with explicit `color` prop in the mobile app
rg -n "ActivityIndicator[^>]*color=" apps/mobile | head -50 || true

# Check ActivityIndicator className usage pattern
rg -n "ActivityIndicator[^>]*className=" apps/mobile | head -50

# Look for NativeWind / Tailwind-RN config in the mobile app
rg -n "nativewind|tailwind-rn|babel-plugin|tw=" apps/mobile . | head -100 || true

# Look for Tailwind / NativeWind config files at repo root (common locations)
ls -la | head -50

Repository: superset-sh/superset

Length of output: 19889


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --hidden -S "nativewind|tailwind-rn|unwind|nativewindcss" . | head -100

Repository: superset-sh/superset

Length of output: 591


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Look for any known RN Tailwind/className adapters or config files
rg -n --hidden -S "uniwind|NativeWindStyleSheet|nativewind|tailwindcss-react-native|react-native-unwind|unwind" apps/mobile packages plugins scripts . | head -100

# Also search for Tailwind config usage
rg -n --hidden -S "tailwind.config|twConfig|tw=" apps/mobile . | head -100

# Find how Text/Button components implement className (to infer whether RN className transformer exists)
rg -n "function .*\\b(className)\\b" apps/mobile/components -S | head -50

Repository: superset-sh/superset

Length of output: 26230


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "ActivityIndicator" apps/mobile/uniwind-types.d.ts apps/mobile/uniwind-env.d.ts | head -50

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Search for any ActivityIndicator-related typing overrides
rg -n "ActivityIndicator" apps/mobile/uniwind-types.d.ts apps/mobile/uniwind-env.d.ts || true

# Search for whether uniwind adds/extends className on RN component types
rg -n "className" apps/mobile/uniwind-types.d.ts | head -50

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
set -euo pipefail
wc -l apps/mobile/uniwind-types.d.ts
head -120 apps/mobile/uniwind-types.d.ts

Repository: superset-sh/superset

Length of output: 346


🌐 Web query:

uniwind ActivityIndicator className color prop text-*

💡 Result:

In Uniwind (React Native), for ActivityIndicator: 1) className controls style, so Tailwind text-* classes (e.g., text-red-500) apply to styles mapped from className (and not to the spinner “foreground” color). 2) The spinner’s foreground color is set via the color prop, which in Uniwind must be provided through colorClassName with an accent-* prefix. Correct (Uniwind): Wrong (Uniwind): (because color is a non-style color prop, so using className with text-* does not set ActivityIndicator’s color; Uniwind requires an accent-* prefixed {propName}ClassName such as colorClassName for the color prop) [1][2][3]. Uniwind docs explicitly state that for “non-style props (like color)”, you must use the accent- prefix via colorClassName, and that since 1.6.2 passing a className without accent- to a color prop logs a warning [1]. The Uniwind SKILL notes reiterate that for ActivityIndicator, colorClassName maps to the color prop and requires accent-* [2], and that className maps to style [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "ActivityIndicator[^>]*colorClassName" apps/mobile | head -50

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Inspect ToastBase variant mapping where ActivityIndicator className is sourced
rg -n "iconColor|variantMapping" apps/mobile/components/ToastBase/ToastBase.tsx
sed -n '1,220p' apps/mobile/components/ToastBase/ToastBase.tsx

# Inspect IconButton iconColorByVariant where ActivityIndicator className is sourced
rg -n "iconColorByVariant" apps/mobile/components/IconButton/IconButton.tsx
sed -n '1,220p' apps/mobile/components/IconButton/IconButton.tsx

# Inspect FabBase iconColorByVariant where ActivityIndicator className is sourced
rg -n "iconColorByVariant" apps/mobile/components/FabBase/FabBase.tsx
sed -n '1,260p' apps/mobile/components/FabBase/FabBase.tsx

Repository: superset-sh/superset

Length of output: 14547


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Find any usage of uniwind colorClassName / accent-*
rg -n "colorClassName\\s*=" apps/mobile | head -50 || true
rg -n "accent-" apps/mobile/components apps/mobile/screens apps/mobile/hooks | head -50 || true

Repository: superset-sh/superset

Length of output: 5110


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -ლა apps/mobile | head -50
ls -ლა apps/mobile | rg "tailwind" || true
cat apps/mobile/tailwind.config.js 2>/dev/null || true
cat apps/mobile/tailwind.config.ts 2>/dev/null || true
fd tailwind.config -t f apps/mobile -a 2>/dev/null || true

Repository: superset-sh/superset

Length of output: 128


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ls -la apps/mobile | head -50
fd tailwind.config -t f apps/mobile || true
fd tailwind.config -t f . | head -20
cat apps/mobile/tailwind.config.js 2>/dev/null || true
cat apps/mobile/tailwind.config.ts 2>/dev/null || true

Repository: superset-sh/superset

Length of output: 1620


Fix ActivityIndicator spinner color in ApprovalFooter

ActivityIndicator’s spinner foreground color is controlled via color/Uniwind’s colorClassName; the current className="text-..." on the spinners won’t reliably apply the intended white/foreground colors.

Update the three ActivityIndicator usages (decline/approve/always) to use color or colorClassName (accent-* tokens) instead of className.

🤖 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 `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx` around lines 80 -
114, The three ActivityIndicator instances inside ApprovalFooter (the ones shown
in the decline/approve/always Button render branches controlled by resolving ===
"decline"/"approve"/"always") are using className="text-..." which doesn't
reliably set spinner color; replace className with the ActivityIndicator prop
color or colorClassName and supply the proper color tokens (e.g., white or an
accent/foreground token) for each case, removing the text-* className so the
spinner renders with the intended color.

Comment on lines +42 to +46
const handleCopy = () => {
onCopy?.(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clean up setTimeout to prevent setState on unmounted component.

The setTimeout at line 45 is not cleaned up. If the component unmounts before 1500ms elapses, React will attempt to call setCopied(false) on an unmounted component, causing warnings and a potential memory leak.

🔧 Proposed fix using useEffect cleanup
-export function CodeBlock({
+import { useEffect, useRef } from "react";
+
+export function CodeBlock({
   code,
   language,
   onCopy,
   overflow,
   bare,
   className,
   ...props
 }: CodeBlockProps) {
   const [copied, setCopied] = useState(false);
+  const timeoutRef = useRef<NodeJS.Timeout>();
 
   const handleCopy = () => {
     onCopy?.(code);
     setCopied(true);
-    setTimeout(() => setCopied(false), 1500);
+    timeoutRef.current = setTimeout(() => setCopied(false), 1500);
   };
+
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) {
+        clearTimeout(timeoutRef.current);
+      }
+    };
+  }, []);
🤖 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 `@apps/mobile/components/CodeBlock/CodeBlock.tsx` around lines 42 - 46, The
handleCopy function schedules setCopied(false) with setTimeout but never clears
the timer, risking setState on an unmounted component; store the timeout id
(e.g., in a ref like copyTimeoutRef) when calling setTimeout inside handleCopy,
clear any existing timeout before creating a new one, and add a useEffect
cleanup that clears copyTimeoutRef.current on unmount to prevent calling
setCopied after the component is gone.

Comment on lines +87 to +105
const opacity = useSharedValue(visible ? 1 : 0);
const translateY = useSharedValue(visible ? 0 : 8);

useEffect(() => {
opacity.value = withTiming(visible ? 1 : 0, { duration: 200 });
translateY.value = withTiming(visible ? 0 : 8, { duration: 200 });
}, [opacity, translateY, visible]);

const animatedStyle = useAnimatedStyle(
() => ({
opacity: opacity.value,
transform: [{ translateY: translateY.value }],
}),
[opacity, translateY],
);

if (!visible && opacity.value === 0) {
return null;
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Shared value reads don't trigger re-renders; unmount condition won't work as intended.

Lines 103-105 attempt to return null when !visible && opacity.value === 0, but this has a critical flaw:

  • When visible becomes false, the useEffect starts animating opacity.value from 1 → 0 over 200ms
  • However, shared values don't trigger React re-renders when they change
  • The condition is checked during render, but after the animation completes there's no re-render to re-check it
  • Result: The component continues to render (just invisible) even after the fade-out animation finishes
🔧 Recommended fix using Reanimated entering/exiting

Replace manual shared value + useEffect approach with Reanimated's built-in entering/exiting animations, which handle unmounting automatically:

 import Animated, {
-	useAnimatedStyle,
-	useSharedValue,
-	withTiming,
+	FadeIn,
+	FadeOut,
 } from "react-native-reanimated";

 export function PendingActionPill({
 	// ... props
 }: PendingActionPillProps) {
-	const opacity = useSharedValue(visible ? 1 : 0);
-	const translateY = useSharedValue(visible ? 0 : 8);
-
-	useEffect(() => {
-		opacity.value = withTiming(visible ? 1 : 0, { duration: 200 });
-		translateY.value = withTiming(visible ? 0 : 8, { duration: 200 });
-	}, [opacity, translateY, visible]);
-
-	const animatedStyle = useAnimatedStyle(
-		() => ({
-			opacity: opacity.value,
-			transform: [{ translateY: translateY.value }],
-		}),
-		[opacity, translateY],
-	);
-
-	if (!visible && opacity.value === 0) {
+	if (!visible) {
 		return null;
 	}

 	return (
-		<Animated.View style={animatedStyle}>
+		<Animated.View
+			entering={FadeIn.duration(200)}
+			exiting={FadeOut.duration(200)}
+		>

Alternatively, if you need the translateY animation, use a finishing callback with runOnJS to update local state and trigger unmount.

🤖 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 `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines
87 - 105, The current unmount logic in PendingActionPill relies on reading
Reanimated shared values (opacity.value) which don't trigger React renders, so
the component never re-checks the `if (!visible && opacity.value === 0) return
null;` after the fade completes; fix by switching to a React state-driven
unmount or Reanimated entering/exiting: replace the manual shared-value +
useEffect flow (opacity, translateY, withTiming) with Reanimated's
layout/entering/exiting animations on the Animated.View (or use withTiming's
callback with runOnJS to call a local state setter such as setIsMounted) so that
when the animation finishes you call runOnJS(() => setIsMounted(false)) and use
that isMounted state (instead of reading opacity.value) to return null; update
the render guard to check the React state (isMounted && visible) and remove the
shared-value read in the render path.

Comment on lines +81 to +87
accessibilityState={{
selected: isHighlighted ?? false,
disabled: disabled ?? false,
busy: isLoading ?? false,
}}
disabled={disabled || isLoading}
className={cn(
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Synchronize accessibility disabled state with actual interactivity.

Pressable becomes non-interactive when isLoading is true, but accessibilityState.disabled can still announce it as enabled.

Suggested fix
 export function SlashCommandOption({
   name,
   description,
   source = "builtin",
   icon,
   isHighlighted,
   isLoading,
   className,
   disabled,
   ...props
 }: SlashCommandOptionProps) {
+  const isDisabled = Boolean(disabled || isLoading);
   return (
     <Pressable
       accessibilityRole="button"
       accessibilityLabel={`${name} — ${description}`}
       accessibilityState={{
         selected: isHighlighted ?? false,
-        disabled: disabled ?? false,
+        disabled: isDisabled,
         busy: isLoading ?? false,
       }}
-      disabled={disabled || isLoading}
+      disabled={isDisabled}
       className={cn(
         slashCommandOptionVariants({ isHighlighted, isLoading }),
         disabled && "opacity-40",
         className,
       )}
🤖 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 `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx` around
lines 81 - 87, In SlashCommandOption, ensure the Pressable's
accessibilityState.disabled mirrors actual interactivity by using the combined
condition (disabled || isLoading) instead of the current disabled ?? false;
update accessibilityState.disabled to reflect both props so screen readers see
the item as disabled when isLoading is true (keep accessibilityState.busy mapped
to isLoading and selected mapped to isHighlighted).

>
<View className={cn(isStacked ? "pt-0.5" : "")}>
{variant === "loading" ? (
<ActivityIndicator size="small" className={mapping.iconColor} />
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

ActivityIndicator does not support className prop.

React Native's ActivityIndicator does not accept a className prop for styling. The color should be set via the color prop instead. This line will not apply the intended color styling.

🐛 Proposed fix
-					{variant === "loading" ? (
-						<ActivityIndicator size="small" className={mapping.iconColor} />
-					) : (
+					{variant === "loading" ? (
+						<ActivityIndicator size="small" color={mapping.iconColor} />
+					) : (

Note: You'll need to resolve the actual color value from the Tailwind class. Consider extracting color values or using a direct color string/token.

🤖 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 `@apps/mobile/components/ToastBase/ToastBase.tsx` at line 149, The
ActivityIndicator usage in ToastBase (ActivityIndicator element) incorrectly
passes a Tailwind-derived CSS class via the className prop (mapping.iconColor);
replace this by resolving the actual color value/token from the Tailwind class
(or a theme mapping) and pass it to the ActivityIndicator using its color prop
(e.g., set color={resolvedColor}) instead of className, ensuring
mapping.iconColor is converted to a valid color string before use.

Comment on lines +121 to +124
<ActivityIndicator
size="small"
className="text-state-live-fg"
/>
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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

ActivityIndicator does not support className prop.

React Native's ActivityIndicator does not accept a className prop. Use the color prop instead to set the spinner color. This will not apply the intended text-state-live-fg color.

🐛 Proposed fix
 							{cfg.showSpinner ? (
-								<ActivityIndicator
-									size="small"
-									className="text-state-live-fg"
-								/>
+								<ActivityIndicator
+									size="small"
+									color="`#50a878`"
+								/>
 							) : null}

Note: Replace the hex color with the appropriate theme color value for state-live-fg.

🤖 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 `@apps/mobile/components/ToolCallCard/ToolCallCard.tsx` around lines 121 - 124,
In ToolCallCard replace the unsupported className prop on the ActivityIndicator:
remove className="text-state-live-fg" and pass the spinner color via the color
prop instead (e.g., color={theme.colors['state-live-fg']} or a hex/string
literal); ensure you obtain the theme color (via existing useTheme / styles /
constants) or hardcode the hex for state-live-fg so the ActivityIndicator
renders the intended color.

Comment thread apps/mobile/design/manifest.json Outdated
]
},
"molecules": {
"status": "deferred",
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align molecule build-plan status with completed gates.

Line 90 and Line 142 still mark molecules as deferred, while Line 45 and Line 135 set molecules to passed and the scope notes describe Wave 2 as completed. This inconsistency can cause tooling to read stale progress state.

Also applies to: 142-142

🤖 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 `@apps/mobile/design/manifest.json` at line 90, The manifest's molecule
build-plan entries currently have "status": "deferred" for some molecules while
other entries and the scope notes indicate Wave 2 is complete; update the
offending "status" fields (the molecule build-plan entries that are out of sync)
to "passed" so the molecule statuses are consistent with the completed gates and
the other entries in the manifest.

Cherry-picked from f4f2a687b (originally on the deleted chat-mobile-ui-elements
branch). Adds Storybook 9 native sandbox env-gated on EXPO_PUBLIC_STORYBOOK,
Design System token stories (Colors/Typography/Spacing/Icons reading existing
global.css tokens via className), and a HelloWorld reference component.

Manifest updated with structured `constraints` block:
  - preserve_theme: lib/theme.ts, global.css, uniwind-env.d.ts, uniwind-types.d.ts
    are canonical — scaffold and later phases must not overwrite.
  - wireframes_are_reference_only: PRD ASCII wireframes describe structural
    intent only; use frontend-design skill for high-fidelity during build.

Gates: discover/target/equip/scaffold = passed on mobile-ios and mobile-android.
Next: /pixel-perfect:build --platform mobile-ios (or mobile-android).
Adds storybook 9 + addon-ondevice-actions/controls + @storybook/react-native
devDeps in apps/mobile to match the cherry-picked package.json. Also picks
up apps/desktop 1.10.3 -> 1.11.0 from main.
…Phase 0)

Execute the token migration described in plans/chat-mobile-plan/14-token-migration-audit.md.
Path A: keep flat shadcn key names (--color-*) for rn-reusables CLI compatibility; swap
values to desktop ember warm palette + add chat-domain extensions (state palette, domain
tokens, fonts, touch-target spacing).

Vendor react-native-reusables components in apps/mobile/components/ui/* are not touched —
they read tokens at runtime and cascade automatically against the new values, per the
"vendor libraries + style overrides only" rule.

- apps/mobile/global.css: rewrite under Tailwind 4 @theme + uniwind @variant. Warm-neutral
  ramp (#151110 background / #201e1c card / #2a2827 secondary in dark; #ffffff / warm-tinted
  light grays in light). Ember accent #e07850 (hsl(17 69% 60%)) as --color-primary in both
  themes. Add state palette (live/warning/danger/success/neutral × fg/bg) and chat domain
  tokens (streaming-cursor, tool-rule). Pre-compute oklch literals as hsl for RN safety;
  omit color-mix() hover/pressed (rn-reusables handles interaction via opacity/scale).
- apps/mobile/lib/theme.ts: mirror global.css key-for-key. NAV_THEME.primary now resolves
  to ember (was inverted-neutral); NAV_THEME.notification stays destructive per audit §4.
  Add stateXxx + streamingCursor / toolRule + fontBody / fontMono.
- apps/mobile/app/_layout.tsx: wire Geist + Geist Mono via @expo-google-fonts/geist with
  SplashScreen.preventAutoHideAsync gate. Storybook + production both wait for fonts.
- apps/mobile/.rnstorybook/stories/DesignSystem/Colors.stories.tsx: extend with State
  palette + Domain tokens sections.
- apps/mobile/.rnstorybook/stories/DesignSystem/Typography.stories.tsx: add Font families
  section demonstrating all 4 Geist weights + Geist Mono weights via fontFamily prop.
- apps/mobile/design/manifest.json: bump to v5.1.0. Vibe rewritten to ember. Narrow
  preserve_theme.paths to uniwind machinery only (global.css + lib/theme.ts no longer
  locked). Add vendor_components_immutable constraint (per the new rule). Add
  tokens_source pointer to designs/tokens/tokens.css. Add fonts tool spec to both
  platform entries.
- apps/mobile/package.json + bun.lock: add @expo-google-fonts/geist 0.4.2,
  @expo-google-fonts/geist-mono 0.4.2, expo-font ~56, expo-splash-screen ~56.

Verified: bun typecheck passes (exit 0), biome check passes on touched files.
Storybook v9 react-native does not auto-detect AsyncStorage when
`shouldPersistSelection: true` — it expects the consumer to pass a
`storage` adapter explicitly. Without one, the persistence layer attempts
to call `.getItem()` on `undefined` and throws:

  TypeError: Cannot read property 'getItem' of undefined

This appears on first launch when Storybook tries to read the last-selected
story.

Fix: pass `{ getItem: AsyncStorage.getItem, setItem: AsyncStorage.setItem }`
from `@react-native-async-storage/async-storage` (already a dep). Storybook
now persists story selection across launches without erroring.
Storybook 9.x's `instrumenter` (transitively via @storybook/addon-ondevice-
controls + @storybook/addon-ondevice-actions) pulls in `tinyrainbow`, which
requires Node's built-in `tty` module. Metro cannot bundle it, surfacing as:

    ERROR  Unable to resolve module tty from
    .../storybook@9.1.20/.../instrumenter/index.cjs

Fix: shim `tty` to an empty module via `resolver.resolveRequest`. Returning
`{ type: "empty" }` is Metro's built-in pattern for Node built-ins it does
not bundle (same trick used for `fs`, `path`, etc. in RN bundles).

Pre-existing infra issue surfaced during pixel-perfect Wave-1 atom
verification; unrelated to Wave-1 atom additions.
Several screen modules (OrganizationHeaderButton, AuthenticatedTabBar,
TabBarAccessory, OrgDropdown, MoreMenuScreen, SettingsScreen, etc.) call
`useRouter`/`useNavigation` hooks that require an active expo-router
NavigationContainer. Storybook 9's `addon-ondevice-controls` eagerly
evaluates each story's render function during `createPreparedStoryMapping`,
which throws "Couldn't find an UnhandledLinkingContext context" outside a
running navigator.

The affected stories already comment-acknowledge they're "not renderable
in Storybook isolation" — they exist mainly as documentation surfaces.
Commenting out the `../screens/**/*.stories.?(ts|tsx|js|jsx)` glob in
.rnstorybook/main.js stops Storybook from pulling them into the bundle.

To restore screen stories later, uncomment the glob AND add a nav-mock
decorator to preview.tsx that provides UnhandledLinkingContext + a stub
useRouter (or wrap the story tree in a real NavigationContainer with mock
state).

Pre-existing infra issue surfaced during pixel-perfect Wave-1 atom
verification; unrelated to Wave-1 atom additions.
Pre-existing screen modules (OrganizationHeaderButton, AuthenticatedTabBar,
TabBarAccessory, OrgDropdown, MoreMenuScreen, SettingsScreen, etc.) call
`useRouter`/`useNavigation` hooks. Storybook 9's `addon-ondevice-controls`
eagerly evaluates each story's render path during `createPreparedStoryMapping`,
throwing "Couldn't find an UnhandledLinkingContext context" outside a
running NavigationContainer.

Fix: wrap the preview decorator chain in `<NavigationContainer>` from
`expo-router/react-navigation`. That sets up `UnhandledLinkingContext`,
`LinkingContext`, `LocaleDirContext`, and the base navigation state — the
same contexts expo-router's `<ExpoRoot>` provides in the real app.

Using the expo-router sub-path (`expo-router/react-navigation`) instead of
`@react-navigation/native` directly so we ride on apps/mobile's already-
declared expo-router dep — avoids a phantom-dep on `@react-navigation/native`
(which is only present transitively in the bun store).

Reverts the prior commit `dedf3dd56` that hid screens stories from main.js.
Screens stories now render correctly in Storybook isolation.
… prep

Tried wrapping the storybook preview chain in `<NavigationContainer>` from
`expo-router/react-navigation` (commit 0e805a1). Did not resolve the
`UnhandledLinkingContext` error.

Root cause: Storybook 9 RN's `loadStory` (called inside
`createPreparedStoryMapping`) does eager module + render-fn evaluation
BEFORE preview decorators apply. The screen placeholder stories
transitively import `useTheme` → `lib/theme.ts` → `expo-router/react-navigation`,
which during prep calls `useLinking` → reads the default
`UnhandledLinkingContext` value's getter → throws "Couldn't find an
UnhandledLinkingContext context."

Decorators in preview.tsx wrap render-time only, not prep-time. So the
NavigationContainer wrapper is structurally unable to fix this chain.

Re-exclude `../screens/**/*.stories.?(ts|tsx)` (same as the earlier
dedf3dd commit). Keep the NavigationContainer in preview.tsx as defense
for any future story that DOES route through decorators and needs nav
context (e.g. a future composer molecule that uses `<Link>`).

Long-term restoration of screen stories requires decoupling them from
`lib/theme.ts` (mirror the pattern used in `components/ScrollFade/ScrollFade.tsx`)
or moving them under `expo-router/testing-library`'s `renderRouter`.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from 353396c to 43f6299 Compare May 23, 2026 00:15
@justincrich justincrich changed the title feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 3 of 4) feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 5) May 23, 2026
Unblock storybook RN prep for views that transitively import expo-router
by providing a self-contained StorybookRouterProvider wrapping the
preview-time linking contexts.

- Add StorybookRouterProvider that wraps PreviewRouteContext +
  LinkingContext + UnhandledLinkingContext so views (including AskUserSheet
  from REMED-009) can render in Storybook without crashing on
  "Couldn't find an UnhandledLinkingContext context."
- Update preview.tsx + index.tsx to wrap stories in
  StorybookRouterProvider; remove the prior "do not import nav modules"
  ban (now properly worked-around at runtime, not at module-load time).
- metro.config.js teaches metro to resolve the new router/ subpath +
  storybook config files in app bundling.

Follow-up to ae4494c (REMED-009 task commit).
@justincrich justincrich changed the title feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 5) feat(mobile): wave-2 chat-view molecules — 19 molecules (Sprint 1 / PR 4 of 7) May 24, 2026
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from a0edf5e to 7d64168 Compare May 24, 2026 17:11
@justincrich justincrich marked this pull request as ready for review May 24, 2026 21:49
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

2 issues found across 172 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx">

<violation number="1" location="apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx:82">
P2: `detailed` mode claims internal scrolling but uses plain `Text` — content exceeding `max-h-32` is clipped with no scroll capability, hindering users from reviewing full tool arguments in an approval UI.</violation>
</file>

<file name="apps/mobile/components/IconButton/IconButton.tsx">

<violation number="1" location="apps/mobile/components/IconButton/IconButton.tsx:110">
P2: accessibilityState can be overridden by caller-supplied props due to spread ordering</violation>
</file>

<file name="apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx">

<violation number="1" location="apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx:87">
P2: Disabled state is not propagated to the nested `RadioGroupItem`, so tapping the radio control itself can still select the option even when the outer row is disabled.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 100 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

return (
<Button
accessibilityLabel={accessibilityLabel}
accessibilityState={{ disabled: isDisabled, busy: loading ?? 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.

P2: accessibilityState can be overridden by caller-supplied props due to spread ordering

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/IconButton/IconButton.tsx, line 110:

<comment>accessibilityState can be overridden by caller-supplied props due to spread ordering</comment>

<file context>
@@ -0,0 +1,142 @@
+	return (
+		<Button
+			accessibilityLabel={accessibilityLabel}
+			accessibilityState={{ disabled: isDisabled, busy: loading ?? false }}
+			disabled={isDisabled}
+			variant={vendorVariantByOurVariant[resolvedVariant]}
</file context>

)}
{...props}
>
<RadioGroupItem value={value} aria-label={label} />
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.

P2: Disabled state is not propagated to the nested RadioGroupItem, so tapping the radio control itself can still select the option even when the outer row is disabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx, line 87:

<comment>Disabled state is not propagated to the nested `RadioGroupItem`, so tapping the radio control itself can still select the option even when the outer row is disabled.</comment>

<file context>
@@ -0,0 +1,102 @@
+			)}
+			{...props}
+		>
+			<RadioGroupItem value={value} aria-label={label} />
+			<View className="flex-1 flex-row items-center gap-2">
+				<Text className="flex-1 font-medium text-foreground" numberOfLines={1}>
</file context>
Suggested change
<RadioGroupItem value={value} aria-label={label} />
<RadioGroupItem value={value} aria-label={label} disabled={disabled} />

- Revert global.css and theme.ts to reactnativereusables default
  theme (stock shadcn/ui keys + values, no custom ember palette)
- Remove DesignSystem gallery stories (Colors, Icons, Spacing, Typography)
- Remove HelloWorld scaffold component
- Remove design/manifest.json (superseded by Storybook)
- Trim comments in global.css and main.js
- Pin expo-font and expo-splash-screen to exact versions
- Move tty stub from metro.config.js to Storybook module mock
- Handle useFonts error so splash screen never stalls
Matches global.css and theme.ts exactly to origin/main — no custom
theme changes in this PR, only Storybook tooling additions.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from 17beb7a to 4cdc402 Compare May 26, 2026 17:57
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

♻️ Duplicate comments (2)
apps/mobile/components/CodeBlock/CodeBlock.tsx (1)

42-46: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Clean up setTimeout to prevent setState on unmounted component.

The setTimeout at line 45 is not cleaned up. If the component unmounts before 1500ms elapses, React will attempt to call setCopied(false) on an unmounted component, causing warnings and a potential memory leak.

🔧 Proposed fix using useEffect cleanup
+import { useEffect, useRef } from "react";
 import { Check, Copy } from "lucide-react-native";
-import { useState } from "react";
+import { useState } from "react";
 import { ScrollView, View, type ViewProps } from "react-native";
 
 export function CodeBlock({
   code,
   language,
   onCopy,
   overflow,
   bare,
   className,
   ...props
 }: CodeBlockProps) {
   const [copied, setCopied] = useState(false);
+  const timeoutRef = useRef<NodeJS.Timeout>();
 
   const handleCopy = () => {
     onCopy?.(code);
     setCopied(true);
-    setTimeout(() => setCopied(false), 1500);
+    if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    timeoutRef.current = setTimeout(() => setCopied(false), 1500);
   };
+
+  useEffect(() => {
+    return () => {
+      if (timeoutRef.current) clearTimeout(timeoutRef.current);
+    };
+  }, []);
🤖 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 `@apps/mobile/components/CodeBlock/CodeBlock.tsx` around lines 42 - 46, The
setTimeout in handleCopy uses setCopied(false) after 1500ms but isn't cleared on
unmount, so store the timer id (e.g., in a timerRef) when calling setTimeout
inside handleCopy and clear it in a useEffect cleanup (or clearTimeout in a
cleanup function returned from useEffect) to avoid calling setState on an
unmounted component; update CodeBlock's handleCopy, add a ref like timerRef and
a useEffect that clears timerRef.current on unmount and after clearing to
prevent leaks.
apps/mobile/components/PendingActionPill/PendingActionPill.tsx (1)

103-105: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unmount condition is tied to a non-reactive value.

At Line 103, unmounting depends on opacity.value, which won’t trigger a React re-render when the timing animation finishes. This can leave the component mounted at 0 opacity. Use React state for mount/unmount and update it from the animation completion callback.

Proposed fix
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
 import { Pressable, type PressableProps } from "react-native";
 import Animated, {
+	runOnJS,
 	useAnimatedStyle,
 	useSharedValue,
 	withTiming,
 } from "react-native-reanimated";
@@
 }: PendingActionPillProps) {
+	const [isMounted, setIsMounted] = useState(visible);
 	const cfg = KIND[kind];
@@
 	const opacity = useSharedValue(visible ? 1 : 0);
 	const translateY = useSharedValue(visible ? 0 : 8);

 	useEffect(() => {
-		opacity.value = withTiming(visible ? 1 : 0, { duration: 200 });
+		if (visible) {
+			setIsMounted(true);
+		}
+		opacity.value = withTiming(visible ? 1 : 0, { duration: 200 }, (finished) => {
+			if (finished && !visible) {
+				runOnJS(setIsMounted)(false);
+			}
+		});
 		translateY.value = withTiming(visible ? 0 : 8, { duration: 200 });
 	}, [opacity, translateY, visible]);
@@
-	if (!visible && opacity.value === 0) {
+	if (!isMounted) {
 		return null;
 	}
🤖 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 `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines
103 - 105, The component currently unmounts based on a non-reactive Reanimated
value (opacity.value) inside the PendingActionPill render, so the React tree
won’t update when the animation completes; replace that logic with React state
(e.g. useState mounted/isMounted) and update that state from the animation
completion callback (use runOnJS or the withTiming onEnd callback tied to the
opacity animation) so when visible becomes false you animate opacity and then
call the JS callback to set mounted=false to actually unmount; update the
conditional that returns null to check visible and the new state instead of
opacity.value.
🤖 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 `@apps/mobile/.rnstorybook/main.js`:
- Around line 3-6: The Storybook `stories` array in
apps/mobile/.rnstorybook/main.js omits the app's screen stories; add a glob for
the screens (e.g. "../screens/**/*.stories.?(ts|tsx|js|jsx)" or
"./screens/**/*.stories.?(ts|tsx|js|jsx)" depending on relative layout) to the
existing stories array so apps/mobile/screens/**/*.stories.* are discovered;
update the `stories` array entry near the existing
"./stories/**/*.stories.?(ts|tsx|js|jsx)" and
"../components/**/*.stories.?(ts|tsx|js|jsx)" entries.

In `@apps/mobile/components/AppHeader/AppHeader.tsx`:
- Around line 37-43: The header currently renders tappable back/action controls
even when callbacks are undefined; update AppHeader so the back and actions
buttons are only rendered as interactive when their respective handlers (onBack,
onActions) are provided — otherwise render a non-interactive element or a
disabled button (no onPress) and ensure accessibilityLabel remains present but
not actionable; locate the back button and actions button render logic in
AppHeader (props: showBack, onBack, backAccessibilityLabel and showActions,
onActions, actionsAccessibilityLabel, actionsIcon) and conditionally attach
onPress/interactive wrappers or set disabled/pointerEvents accordingly to
prevent no-op taps.
- Line 67: In AppHeader replace the fragile "w-1" spacer Views with a
fixed-width spacer that matches the header icon button slot (use the same width
used by the IconButton component or the shared ICON_BUTTON_SIZE constant), so
the title stays centered when a side control is hidden; update both spacer
occurrences around the title (the two View elements currently using
className="w-1") to use that exact fixed width via className or inline style
referencing the shared button size.

In `@apps/mobile/components/IconButton/IconButton.tsx`:
- Around line 125-139: The ActivityIndicator inside IconButton should use the
native color prop instead of passing a NativeWind className; update the loading
branch in the IconButton component to map resolvedVariant via the existing
iconColorByVariant (or a new color mapping) and pass that string into
ActivityIndicator's color prop rather than className, keeping the Icon branch
unchanged (Icon, iconSizeBySize, iconClassName remain as-is).

In `@apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx`:
- Around line 55-57: The detailed prop is documented to enable internal
scrolling but the detailed branch only caps height without overflow; update the
JSX that renders the args/preview for the detailed mode (the component/element
using the detailed?: boolean prop — e.g., the Args/args preview node inside
PendingApprovalCard) to wrap the preview text in a scroll container when
detailed is true and apply a maxHeight (~120px) plus overflowY: 'auto' (or
overflow: 'auto') so long previews scroll internally; implement this by adding a
wrapper div / styled component around the same preview markup that currently
sets height constraints and move the max-height + overflow styles there for the
detailed branch only.

In `@apps/mobile/components/Pill/Pill.tsx`:
- Around line 141-174: The dismiss Pressable currently calls onDismiss directly
which allows the tap to bubble to the outer Pressable (onPress) when
resolvedInteractive is true; change the inner Pressable's onPress to accept the
press event and call event.stopPropagation() (or event.stopPropagation?.())
before invoking onDismiss (e.g., onPress={(e) => { e.stopPropagation?.();
onDismiss?.(); }}), so the dismiss action does not trigger the parent Pill
onPress; update the inner Pressable in Pill.tsx where onDismiss is used and
ensure the event parameter type is compatible if needed.

In `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx`:
- Around line 32-46: The computed className for SuggestedAnswerPill (variable
className) can be overridden by spreading {...props}; update SuggestedAnswerPill
to merge className with any incoming props.className before passing to Pill,
e.g. derive a finalClassName by combining the local className (when variant ===
"ghost") and props.className (using a class merge helper or simple join) and
pass finalClassName to Pill instead of passing className and {...props} that may
overwrite it; keep the rest of the props spread as-is and ensure you reference
the className variable and the Pill component props in the change.

In `@apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx`:
- Around line 35-46: The running and pending style objects in ToolStatusRule use
iOS-only shadow props (shadowOpacity/shadowRadius/shadowOffset) which don't
produce the same glow on Android; update the running and pending styles to
include Android-compatible fallbacks (e.g., add elevation values and/or
Platform.select to set elevation for Android) and optionally provide a boxShadow
alternative when New Architecture is enabled; target the running and pending
style objects in ToolStatusRule to make the change so Android shows a comparable
glow.

In `@apps/mobile/components/ui/textarea.stories.tsx`:
- Around line 2-18: TextareaShowcase initializes local state with
useState(initialValue) so changes to the Storybook arg initialValue aren't
reflected; update the component to watch for changes to the initialValue prop
(the initialValue arg/control) and call setValue when it changes (e.g., add a
useEffect that has initialValue in its dependency array and updates value via
setValue), keeping existing state updates from user input intact; locate the
TextareaShowcase function and its useState(initialValue) to add this effect.

In `@apps/mobile/components/ui/toggle.stories.tsx`:
- Around line 3-20: The ToggleShowcase component currently initializes local
state with useState(initialPressed) but doesn't resync when story args change;
update ToggleShowcase to watch initialPressed and call
setPressed(initialPressed) via a useEffect so the local pressed state follows
changes to the initialPressed prop, leaving the Toggle props (pressed and
onPressedChange) unchanged; reference ToggleShowcase, initialPressed, pressed,
setPressed, useState, useEffect, and the Toggle component to locate where to add
the effect.

In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx`:
- Around line 70-80: The retry Button is rendered even when there is no onRetry
handler, producing a non-functional CTA; update the UserMessageBubble render
logic to only show the retry control when both failed is true and onRetry is
provided (e.g., guard rendering of the Button/Text by checking failed &&
onRetry), and ensure the onRetry prop on the component is optional in its type
so callers aren't required to pass it; locate the conditional using failed and
the Button in UserMessageBubble to apply this guard.

---

Duplicate comments:
In `@apps/mobile/components/CodeBlock/CodeBlock.tsx`:
- Around line 42-46: The setTimeout in handleCopy uses setCopied(false) after
1500ms but isn't cleared on unmount, so store the timer id (e.g., in a timerRef)
when calling setTimeout inside handleCopy and clear it in a useEffect cleanup
(or clearTimeout in a cleanup function returned from useEffect) to avoid calling
setState on an unmounted component; update CodeBlock's handleCopy, add a ref
like timerRef and a useEffect that clears timerRef.current on unmount and after
clearing to prevent leaks.

In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 103-105: The component currently unmounts based on a non-reactive
Reanimated value (opacity.value) inside the PendingActionPill render, so the
React tree won’t update when the animation completes; replace that logic with
React state (e.g. useState mounted/isMounted) and update that state from the
animation completion callback (use runOnJS or the withTiming onEnd callback tied
to the opacity animation) so when visible becomes false you animate opacity and
then call the JS callback to set mounted=false to actually unmount; update the
conditional that returns null to check visible and the new state instead of
opacity.value.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d87ee91f-2d1b-4ef2-8a8e-8f1ce002b1b2

📥 Commits

Reviewing files that changed from the base of the PR and between 353396c and 4cdc402.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (141)
  • apps/mobile/.rnstorybook/.gitignore
  • apps/mobile/.rnstorybook/StorybookRouterProvider.tsx
  • apps/mobile/.rnstorybook/index.tsx
  • apps/mobile/.rnstorybook/main.js
  • apps/mobile/.rnstorybook/mocks/tty.js
  • apps/mobile/.rnstorybook/preview.tsx
  • apps/mobile/.rnstorybook/router/LinkingContext.ts
  • apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
  • apps/mobile/app/_layout.tsx
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/AppHeader/AppHeader.tsx
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/Banner/Banner.stories.tsx
  • apps/mobile/components/Banner/Banner.tsx
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/CodeBlock/CodeBlock.stories.tsx
  • apps/mobile/components/CodeBlock/CodeBlock.tsx
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsx
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.tsx
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/ComposerRow/ComposerRow.stories.tsx
  • apps/mobile/components/ComposerRow/ComposerRow.tsx
  • apps/mobile/components/ComposerRow/index.ts
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsx
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/FabBase/FabBase.stories.tsx
  • apps/mobile/components/FabBase/FabBase.tsx
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsx
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsx
  • apps/mobile/components/HitTargetWrapper/index.ts
  • apps/mobile/components/IconButton/IconButton.stories.tsx
  • apps/mobile/components/IconButton/IconButton.tsx
  • apps/mobile/components/IconButton/index.ts
  • apps/mobile/components/ModalHeader/ModalHeader.stories.tsx
  • apps/mobile/components/ModalHeader/ModalHeader.tsx
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsx
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/PendingActionPill/PendingActionPill.tsx
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsx
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/PickerTrigger/PickerTrigger.stories.tsx
  • apps/mobile/components/PickerTrigger/PickerTrigger.tsx
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/Pill/Pill.stories.tsx
  • apps/mobile/components/Pill/Pill.tsx
  • apps/mobile/components/Pill/index.ts
  • apps/mobile/components/ProgressDots/ProgressDots.stories.tsx
  • apps/mobile/components/ProgressDots/ProgressDots.tsx
  • apps/mobile/components/ProgressDots/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.tsx
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/ScrollFade/ScrollFade.stories.tsx
  • apps/mobile/components/ScrollFade/ScrollFade.tsx
  • apps/mobile/components/ScrollFade/index.ts
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsx
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/StatusDot/StatusDot.stories.tsx
  • apps/mobile/components/StatusDot/StatusDot.tsx
  • apps/mobile/components/StatusDot/index.ts
  • apps/mobile/components/StreamingCursor/StreamingCursor.stories.tsx
  • apps/mobile/components/StreamingCursor/StreamingCursor.tsx
  • apps/mobile/components/StreamingCursor/index.ts
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsx
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsx
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsx
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/ToastBase/ToastBase.stories.tsx
  • apps/mobile/components/ToastBase/ToastBase.tsx
  • apps/mobile/components/ToastBase/index.ts
  • apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
  • apps/mobile/components/ToolStatusRule/index.ts
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/components/ui/accordion.stories.tsx
  • apps/mobile/components/ui/alert-dialog.stories.tsx
  • apps/mobile/components/ui/alert.stories.tsx
  • apps/mobile/components/ui/aspect-ratio.stories.tsx
  • apps/mobile/components/ui/avatar.stories.tsx
  • apps/mobile/components/ui/badge.stories.tsx
  • apps/mobile/components/ui/button.stories.tsx
  • apps/mobile/components/ui/card.stories.tsx
  • apps/mobile/components/ui/checkbox.stories.tsx
  • apps/mobile/components/ui/collapsible.stories.tsx
  • apps/mobile/components/ui/context-menu.stories.tsx
  • apps/mobile/components/ui/dialog.stories.tsx
  • apps/mobile/components/ui/dropdown-menu.stories.tsx
  • apps/mobile/components/ui/hover-card.stories.tsx
  • apps/mobile/components/ui/icon.stories.tsx
  • apps/mobile/components/ui/input.stories.tsx
  • apps/mobile/components/ui/label.stories.tsx
  • apps/mobile/components/ui/menubar.stories.tsx
  • apps/mobile/components/ui/popover.stories.tsx
  • apps/mobile/components/ui/progress.stories.tsx
  • apps/mobile/components/ui/radio-group.stories.tsx
  • apps/mobile/components/ui/select.stories.tsx
  • apps/mobile/components/ui/separator.stories.tsx
  • apps/mobile/components/ui/skeleton.stories.tsx
  • apps/mobile/components/ui/switch.stories.tsx
  • apps/mobile/components/ui/tabs.stories.tsx
  • apps/mobile/components/ui/text.stories.tsx
  • apps/mobile/components/ui/textarea.stories.tsx
  • apps/mobile/components/ui/toggle-group.stories.tsx
  • apps/mobile/components/ui/toggle.stories.tsx
  • apps/mobile/components/ui/tooltip.stories.tsx
  • apps/mobile/metro.config.js
  • apps/mobile/package.json
  • apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
  • apps/mobile/screens/AUDIT.md
✅ Files skipped from review due to trivial changes (28)
  • apps/mobile/.rnstorybook/.gitignore
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/.rnstorybook/mocks/tty.js
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/.rnstorybook/router/UnhandledLinkingContext.ts
  • apps/mobile/components/ToolStatusRule/index.ts
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/.rnstorybook/router/LinkingContext.ts
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/screens/AUDIT.md
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/components/ToastBase/index.ts
  • apps/mobile/components/StatusDot/StatusDot.stories.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ui/dialog.stories.tsx
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/ApprovalFooter/index.ts

Comment on lines +3 to +6
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
],
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Include screens stories in Storybook discovery.

The current stories globs skip apps/mobile/screens/**/*.stories.*, so those stories in this cohort won’t load.

Suggested diff
 const main = {
 	stories: [
 		"./stories/**/*.stories.?(ts|tsx|js|jsx)",
 		"../components/**/*.stories.?(ts|tsx|js|jsx)",
+		"../screens/**/*.stories.?(ts|tsx|js|jsx)",
 	],
📝 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
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
],
stories: [
"./stories/**/*.stories.?(ts|tsx|js|jsx)",
"../components/**/*.stories.?(ts|tsx|js|jsx)",
"../screens/**/*.stories.?(ts|tsx|js|jsx)",
],
🤖 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 `@apps/mobile/.rnstorybook/main.js` around lines 3 - 6, The Storybook `stories`
array in apps/mobile/.rnstorybook/main.js omits the app's screen stories; add a
glob for the screens (e.g. "../screens/**/*.stories.?(ts|tsx|js|jsx)" or
"./screens/**/*.stories.?(ts|tsx|js|jsx)" depending on relative layout) to the
existing stories array so apps/mobile/screens/**/*.stories.* are discovered;
update the `stories` array entry near the existing
"./stories/**/*.stories.?(ts|tsx|js|jsx)" and
"../components/**/*.stories.?(ts|tsx|js|jsx)" entries.

Comment on lines +37 to +43
showBack = true,
onBack,
backAccessibilityLabel = "Back to sessions",
showActions = true,
onActions,
actionsAccessibilityLabel = "Session actions",
actionsIcon = MoreVertical,
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent no-op header actions when callbacks are missing.

Both action buttons can render as tappable controls even when onBack/onActions are undefined, which creates dead interactions.

Suggested diff
-	showBack = true,
+	showBack = true,
 	onBack,
@@
-	showActions = true,
+	showActions = true,
 	onActions,
@@
-			{showBack ? (
+			{showBack && onBack ? (
 				<IconButton
@@
-			{showActions ? (
+			{showActions && onActions ? (
 				<IconButton

Also applies to: 58-65, 86-93

🤖 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 `@apps/mobile/components/AppHeader/AppHeader.tsx` around lines 37 - 43, The
header currently renders tappable back/action controls even when callbacks are
undefined; update AppHeader so the back and actions buttons are only rendered as
interactive when their respective handlers (onBack, onActions) are provided —
otherwise render a non-interactive element or a disabled button (no onPress) and
ensure accessibilityLabel remains present but not actionable; locate the back
button and actions button render logic in AppHeader (props: showBack, onBack,
backAccessibilityLabel and showActions, onActions, actionsAccessibilityLabel,
actionsIcon) and conditionally attach onPress/interactive wrappers or set
disabled/pointerEvents accordingly to prevent no-op taps.

onPress={onBack}
/>
) : (
<View className="w-1" />
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use fixed-width spacers matching icon-button width.

w-1 placeholders can shift title alignment when one side control is hidden. Use a stable spacer width equal to the button slot.

Suggested diff
-				<View className="w-1" />
+				<View className="w-touch-min" />
@@
-				<View className="w-1" />
+				<View className="w-touch-min" />

Also applies to: 95-95

🤖 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 `@apps/mobile/components/AppHeader/AppHeader.tsx` at line 67, In AppHeader
replace the fragile "w-1" spacer Views with a fixed-width spacer that matches
the header icon button slot (use the same width used by the IconButton component
or the shared ICON_BUTTON_SIZE constant), so the title stays centered when a
side control is hidden; update both spacer occurrences around the title (the two
View elements currently using className="w-1") to use that exact fixed width via
className or inline style referencing the shared button size.

Comment on lines +125 to +139
{loading ? (
<ActivityIndicator
size="small"
className={iconColorByVariant[resolvedVariant]}
/>
) : (
<Icon
as={icon}
className={cn(
iconSizeBySize[resolvedSize],
iconColorByVariant[resolvedVariant],
iconClassName,
)}
/>
)}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use native color prop instead of className for ActivityIndicator.

React Native's ActivityIndicator expects a color prop (hex or named color string), not className-based styling. The NativeWind className approach on line 128 may not reliably set the spinner color across variants.

🎨 Proposed fix to use native color prop

Define a color mapping:

 const iconColorByVariant: Record<IconButtonVariant, string> = {
 	ghost: "text-foreground",
 	soft: "text-foreground",
 	primary: "text-primary-foreground",
 	neutral: "text-background",
 	destructive: "text-white",
 };
+
+const spinnerColorByVariant: Record<IconButtonVariant, string> = {
+	ghost: "`#000000`", // or theme color variable
+	soft: "`#000000`",
+	primary: "`#ffffff`",
+	neutral: "`#ffffff`",
+	destructive: "`#ffffff`",
+};

Then use it:

 		{loading ? (
 			<ActivityIndicator
 				size="small"
-				className={iconColorByVariant[resolvedVariant]}
+				color={spinnerColorByVariant[resolvedVariant]}
 			/>
 		) : (

Alternatively, if your theme system provides color tokens, extract them programmatically rather than using className.

🤖 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 `@apps/mobile/components/IconButton/IconButton.tsx` around lines 125 - 139, The
ActivityIndicator inside IconButton should use the native color prop instead of
passing a NativeWind className; update the loading branch in the IconButton
component to map resolvedVariant via the existing iconColorByVariant (or a new
color mapping) and pass that string into ActivityIndicator's color prop rather
than className, keeping the Icon branch unchanged (Icon, iconSizeBySize,
iconClassName remain as-is).

Comment on lines +55 to +57
/** Internal scroll on the args preview when multi-line (≤120pt tall). */
detailed?: boolean;
};
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

detailed mode doesn’t implement the documented internal scroll.

Line 55 promises internal scrolling, but Lines 119-125 only cap text height. Add a scroll container for the detailed branch so long previews remain readable.

Proposed fix
-import { View, type ViewProps } from "react-native";
+import { ScrollView, View, type ViewProps } from "react-native";
@@
 				{argsPreview ? (
 					<>
 						<Separator />
-						<Text
-							className={cn(
-								"font-mono text-xs text-foreground",
-								detailed && "max-h-32",
-							)}
-							numberOfLines={detailed ? undefined : 3}
-						>
-							{argsPreview}
-						</Text>
+						{detailed ? (
+							<ScrollView className="max-h-32" nestedScrollEnabled>
+								<Text className="font-mono text-xs text-foreground">
+									{argsPreview}
+								</Text>
+							</ScrollView>
+						) : (
+							<Text className="font-mono text-xs text-foreground" numberOfLines={3}>
+								{argsPreview}
+							</Text>
+						)}
 					</>
 				) : null}

Also applies to: 119-125

🤖 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 `@apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx` around
lines 55 - 57, The detailed prop is documented to enable internal scrolling but
the detailed branch only caps height without overflow; update the JSX that
renders the args/preview for the detailed mode (the component/element using the
detailed?: boolean prop — e.g., the Args/args preview node inside
PendingApprovalCard) to wrap the preview text in a scroll container when
detailed is true and apply a maxHeight (~120px) plus overflowY: 'auto' (or
overflow: 'auto') so long previews scroll internally; implement this by adding a
wrapper div / styled component around the same preview markup that currently
sets height constraints and move the max-height + overflow styles there for the
detailed branch only.

Comment on lines +32 to +46
const className =
variant === "ghost" ? "bg-transparent border-border" : undefined;

return (
<Pill
label={text}
variant={pillVariant}
size="md"
interactive
onPress={onPress}
disabled={disabled}
accessibilityLabel={`Use suggested answer: ${text}`}
className={className}
{...props}
/>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve ghost styles when consumer passes className

At Line 45, {...props} can override Line 44’s computed className, so variant="ghost" styles may be dropped when callers pass their own class. Merge classes explicitly instead.

Suggested fix
 import { Pill, type PillProps } from "`@/components/Pill`";
+import { cn } from "`@/lib/utils`";
@@
 export function SuggestedAnswerPill({
 	text,
 	variant = "default",
 	onPress,
 	disabled,
+	className: userClassName,
 	...props
 }: SuggestedAnswerPillProps) {
 	const pillVariant: PillProps["variant"] =
 		variant === "accent" ? "accent" : "default";
-	const className =
+	const variantClassName =
 		variant === "ghost" ? "bg-transparent border-border" : undefined;
@@
 			onPress={onPress}
 			disabled={disabled}
 			accessibilityLabel={`Use suggested answer: ${text}`}
-			className={className}
+			className={cn(variantClassName, userClassName)}
 			{...props}
 		/>
 	);
 }
🤖 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 `@apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx` around
lines 32 - 46, The computed className for SuggestedAnswerPill (variable
className) can be overridden by spreading {...props}; update SuggestedAnswerPill
to merge className with any incoming props.className before passing to Pill,
e.g. derive a finalClassName by combining the local className (when variant ===
"ghost") and props.className (using a class merge helper or simple join) and
pass finalClassName to Pill instead of passing className and {...props} that may
overwrite it; keep the rest of the props spread as-is and ensure you reference
the className variable and the Pill component props in the change.

Comment on lines +35 to +46
running: {
shadowColor: "rgba(80, 168, 120, 0.6)",
shadowOpacity: 1,
shadowRadius: 6,
shadowOffset: { width: 0, height: 0 },
},
pending: {
shadowColor: "rgba(212, 168, 75, 0.6)",
shadowOpacity: 1,
shadowRadius: 4,
shadowOffset: { width: 0, height: 0 },
},
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In React Native 0.85.x, which shadow style props are supported on Android (shadowOpacity, shadowRadius, shadowOffset), and is boxShadow the recommended cross-platform alternative?

💡 Result:

In React Native 0.85, the legacy shadow style props (shadowOpacity, shadowRadius, shadowOffset) remain effectively iOS-only [1][2][3]. While shadowColor is supported on both platforms [1][3], the other three props do not function on Android [1][3]. boxShadow is the recommended, modern, and cross-platform alternative for handling shadows in React Native [1][4]. Key details regarding boxShadow in React Native 0.85: - It is a spec-compliant implementation of the web style prop [1]. - It is supported on both iOS and Android [1]. - It is available exclusively under the New Architecture [5]. - Unlike legacy shadow props, boxShadow supports more complex features such as inset shadows and multiple shadow layers [5][1]. - For Android specifically, boxShadow has version-dependent support: outset shadows are supported on Android 9+, and inset shadows are supported on Android 10+ [5]. If you are not using the New Architecture or require support for older Android versions where boxShadow is unavailable, the traditional approach remains using the elevation prop for Android, often combined with conditional platform styling to apply shadow* props on iOS [6][7][2].

Citations:


Fix Android glow styling for running/pending in ToolStatusRule

In React Native, the legacy shadow props you’re using (shadowOpacity, shadowRadius, shadowOffset) are effectively iOS-only on Android; only shadowColor is supported there—so the glow will likely not match on Android. Add an Android fallback (e.g., elevation) and/or switch to boxShadow for cross-platform shadows (noting boxShadow support is tied to the New Architecture).

🤖 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 `@apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx` around lines 35 -
46, The running and pending style objects in ToolStatusRule use iOS-only shadow
props (shadowOpacity/shadowRadius/shadowOffset) which don't produce the same
glow on Android; update the running and pending styles to include
Android-compatible fallbacks (e.g., add elevation values and/or Platform.select
to set elevation for Android) and optionally provide a boxShadow alternative
when New Architecture is enabled; target the running and pending style objects
in ToolStatusRule to make the change so Android shows a comparable glow.

Comment on lines +2 to +18
import { useState } from "react";
import { View } from "react-native";
import { Textarea } from "@/components/ui/textarea";

function TextareaShowcase({
placeholder,
editable,
initialValue,
numberOfLines,
}: {
placeholder: string;
editable: boolean;
initialValue: string;
numberOfLines: number;
}) {
const [value, setValue] = useState(initialValue);
return (
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and display the relevant Storybook story file with line numbers
FILE="apps/mobile/components/ui/textarea.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "==== $FILE (head/tail excerpt) ===="
wc -l "$FILE"
sed -n '1,200p' "$FILE" | nl -ba | sed -n '1,220p'

Repository: superset-sh/superset

Length of output: 253


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/textarea.stories.tsx"
wc -l "$FILE"
sed -n '1,220p' "$FILE" | nl -ba | sed -n '1,220p'

Repository: superset-sh/superset

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/textarea.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi
sed -n '1,120p' "$FILE" | nl -ba
sed -n '120,260p' "$FILE" | nl -ba

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
echo "----- BEGIN -----"
sed -n '1,220p' "$FILE" | nl -ba
echo "----- END -----"

Repository: superset-sh/superset

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
sed -n '1,220p' "$FILE" | nl -ba

Repository: superset-sh/superset

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
wc -l "$FILE"
sed -n '1,260p' "$FILE" | nl -ba

Repository: superset-sh/superset

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
sed -n '1,260p' "$FILE" | nl -ba

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"
[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }
sed -n '1,120p' "$FILE" | nl -ba

Repository: superset-sh/superset

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="apps/mobile/components/ui/textarea.stories.tsx"

[ -f "$FILE" ] || { echo "Missing $FILE" >&2; exit 1; }

echo "=== File: $FILE ==="
wc -l "$FILE"

echo "=== Lines 1-120 (awk line numbers) ==="
awk '{printf "%5d\t%s\n", NR, $0}' "$FILE" | sed -n '1,120p'

echo "=== Context around TextareaShowcase ==="
rg -n "function TextareaShowcase|TextareaShowcase|initialValue|useState\\(initialValue\\)" "$FILE" || true

echo "=== Context around default export / meta / args ==="
rg -n "export default|args|argTypes|render" "$FILE" || true

Repository: superset-sh/superset

Length of output: 3572


Sync Storybook initialValue into local state in TextareaShowcase.

TextareaShowcase uses useState(initialValue) only once, so updates to the Storybook initialValue args/control won’t update the textarea value.

Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [value, setValue] = useState(initialValue);
+	useEffect(() => {
+		setValue(initialValue);
+	}, [initialValue]);
📝 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
import { useState } from "react";
import { View } from "react-native";
import { Textarea } from "@/components/ui/textarea";
function TextareaShowcase({
placeholder,
editable,
initialValue,
numberOfLines,
}: {
placeholder: string;
editable: boolean;
initialValue: string;
numberOfLines: number;
}) {
const [value, setValue] = useState(initialValue);
return (
import { useEffect, useState } from "react";
import { View } from "react-native";
import { Textarea } from "`@/components/ui/textarea`";
function TextareaShowcase({
placeholder,
editable,
initialValue,
numberOfLines,
}: {
placeholder: string;
editable: boolean;
initialValue: string;
numberOfLines: number;
}) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
return (
🤖 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 `@apps/mobile/components/ui/textarea.stories.tsx` around lines 2 - 18,
TextareaShowcase initializes local state with useState(initialValue) so changes
to the Storybook arg initialValue aren't reflected; update the component to
watch for changes to the initialValue prop (the initialValue arg/control) and
call setValue when it changes (e.g., add a useEffect that has initialValue in
its dependency array and updates value via setValue), keeping existing state
updates from user input intact; locate the TextareaShowcase function and its
useState(initialValue) to add this effect.

Comment on lines +3 to +20
import { useState } from "react";
import { Text } from "@/components/ui/text";
import { Toggle, ToggleIcon } from "@/components/ui/toggle";

function ToggleShowcase({
variant,
withIcon,
label,
initialPressed,
}: {
variant: "default" | "outline";
withIcon: boolean;
label: string;
initialPressed: boolean;
}) {
const [pressed, setPressed] = useState(initialPressed);
return (
<Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the full toggle story file with line numbers
FILE="apps/mobile/components/ui/toggle.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== $FILE (lines) ==="
nl -ba "$FILE" | sed -n '1,220p'

# Search for similar "initialPressed" usage in the repo (to cross-check pattern)
rg -n "initialPressed|pressedChange|onPressedChange|ToggleShowcase" -S apps/mobile/components/ui || true

Repository: superset-sh/superset

Length of output: 187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/toggle.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "=== $FILE (first 220 lines, with line numbers via cat -n) ==="
cat -n "$FILE" | sed -n '1,220p'

echo "=== rg: initialPressed / args wiring (file + nearby components/ui stories) ==="
rg -n "initialPressed|args|ToggleShowcase|onPressedChange|pressed" -S "$FILE" || true

echo "=== rg: other stories for the same pattern (pressed state from args) ==="
rg -n "setPressed\\(|useState\\(initialPressed\\)|onPressedChange\\=\\{set.*\\}|useEffect\\(\\(\\) => \\{\\s*set.*Pressed|initialPressed\\:" -S apps/mobile/components/ui || true

Repository: superset-sh/superset

Length of output: 3205


Resync pressed when initialPressed story args change.
useState(initialPressed) only uses initialPressed on the initial mount; updating Storybook controls for initialPressed won’t update the rendered toggle state.

Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [pressed, setPressed] = useState(initialPressed);
+	useEffect(() => {
+		setPressed(initialPressed);
+	}, [initialPressed]);
📝 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
import { useState } from "react";
import { Text } from "@/components/ui/text";
import { Toggle, ToggleIcon } from "@/components/ui/toggle";
function ToggleShowcase({
variant,
withIcon,
label,
initialPressed,
}: {
variant: "default" | "outline";
withIcon: boolean;
label: string;
initialPressed: boolean;
}) {
const [pressed, setPressed] = useState(initialPressed);
return (
<Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}>
import { useEffect, useState } from "react";
import { Text } from "`@/components/ui/text`";
import { Toggle, ToggleIcon } from "`@/components/ui/toggle`";
function ToggleShowcase({
variant,
withIcon,
label,
initialPressed,
}: {
variant: "default" | "outline";
withIcon: boolean;
label: string;
initialPressed: boolean;
}) {
const [pressed, setPressed] = useState(initialPressed);
useEffect(() => {
setPressed(initialPressed);
}, [initialPressed]);
return (
<Toggle variant={variant} pressed={pressed} onPressedChange={setPressed}>
🤖 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 `@apps/mobile/components/ui/toggle.stories.tsx` around lines 3 - 20, The
ToggleShowcase component currently initializes local state with
useState(initialPressed) but doesn't resync when story args change; update
ToggleShowcase to watch initialPressed and call setPressed(initialPressed) via a
useEffect so the local pressed state follows changes to the initialPressed prop,
leaving the Toggle props (pressed and onPressedChange) unchanged; reference
ToggleShowcase, initialPressed, pressed, setPressed, useState, useEffect, and
the Toggle component to locate where to add the effect.

Comment on lines +70 to +80
{failed ? (
<>
<Text className="text-xs text-state-danger-fg">Failed to send</Text>
<Button
size="sm"
variant="link"
className="h-auto px-1"
onPress={onRetry}
>
<Text className="text-xs text-primary">Retry</Text>
</Button>
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid rendering a non-functional Retry action when onRetry is missing.

On Lines 73-80, failed shows a retry control even if no callback is provided, creating an inert CTA in an error state.

💡 Proposed fix
 				{failed ? (
 					<>
 						<Text className="text-xs text-state-danger-fg">Failed to send</Text>
-						<Button
-							size="sm"
-							variant="link"
-							className="h-auto px-1"
-							onPress={onRetry}
-						>
-							<Text className="text-xs text-primary">Retry</Text>
-						</Button>
+						{onRetry ? (
+							<Button
+								size="sm"
+								variant="link"
+								className="h-auto px-1"
+								onPress={onRetry}
+							>
+								<Text className="text-xs text-primary">Retry</Text>
+							</Button>
+						) : null}
 					</>
 				) : timestamp ? (
📝 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
{failed ? (
<>
<Text className="text-xs text-state-danger-fg">Failed to send</Text>
<Button
size="sm"
variant="link"
className="h-auto px-1"
onPress={onRetry}
>
<Text className="text-xs text-primary">Retry</Text>
</Button>
{failed ? (
<>
<Text className="text-xs text-state-danger-fg">Failed to send</Text>
{onRetry ? (
<Button
size="sm"
variant="link"
className="h-auto px-1"
onPress={onRetry}
>
<Text className="text-xs text-primary">Retry</Text>
</Button>
) : null}
</>
) : timestamp ? (
🤖 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 `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx` around lines
70 - 80, The retry Button is rendered even when there is no onRetry handler,
producing a non-functional CTA; update the UserMessageBubble render logic to
only show the retry control when both failed is true and onRetry is provided
(e.g., guard rendering of the Button/Text by checking failed && onRetry), and
ensure the onRetry prop on the component is optional in its type so callers
aren't required to pass it; locate the conditional using failed and the Button
in UserMessageBubble to apply this guard.

- Remove duplicate NavigationContainer from preview.tsx decorator.
  StorybookRouterProvider already provides one. SDK 56 blocks direct
  @react-navigation/native imports inside the expo-router tree.
- Add Welcome.stories.tsx so Storybook has at least one story on the
  tooling branch (prevents EmptyIndexError crash).
…migration)

Migration verification surface for the ember theme rewrite. Every primitive in
apps/mobile/components/ui/* now has a sidecar Components/{Primitive} story so
the live app components are visually inspectable on iOS Simulator + Android
Emulator under both light and dark ember themes.

Per the vendor-immutable rule, no primitive source file is edited — the stories
import the real components the app already uses. Discovered token-bypass
divergences (shadow-black/5, bg-black/50 backdrops, text-white in destructive
variants) are documented in apps/mobile/components/ui/AUDIT.md with the
upstream-PR resolution path. Local source edits to vendor primitives are
explicitly not the fix.

- 28 stories: accordion, alert, alert-dialog, aspect-ratio, avatar, badge,
  button, card, checkbox, collapsible, context-menu, dialog, dropdown-menu,
  hover-card, icon (catalog with 22 chat-domain lucide icons), input, label,
  menubar, popover, progress, radio-group, select, separator, skeleton, switch,
  tabs, text, textarea, toggle, toggle-group, tooltip
- AUDIT.md: 3 token-bypass findings catalogued, accepted as upstream-PR work
- .rnstorybook/preview.tsx: PortalHost added to decorator so primitives that
  render via portal (Popover, Dialog, AlertDialog, DropdownMenu, ContextMenu,
  Menubar, Select, Tooltip, HoverCard) work in Storybook
- Story conventions match the workflow spec: title='Components/{Name}',
  argTypes wired for all controllable props, realistic chat-domain mock data
  (model names, session statuses, branch · host disambiguation)

Verified: bun typecheck (exit 0), biome check (no warnings).

This is the migration verification layer for Sprint 01 Phase 0 (token rewrite,
2c21dbc). Pixel-perfect manifest atoms gate stays pending — the gate flips
only after the human reviewer walks the Storybook inventory on simulator.
…udit + stories)

Completes the theme-migration story coverage for live first-party app
components in apps/mobile/screens/**/components/. Per the audit pass:

CRITICAL fixes (active bugs under ember):

- OrganizationHeaderButton.tsx:25 — replaced hardcoded `hsl(240 5% 64.9%)`
  (OLD cool-neutral palette) with `theme.mutedForeground` (warm). Chevron now
  renders correctly under the warm-neutral surface ramp.

MEDIUM fixes (token bypass / wrong source):

- SocialButton.tsx — replaced `useColorScheme()` + hardcoded `"white"/"black"`
  icon color with `useTheme().foreground`. Previously the icon read the OS
  appearance setting (which may disagree with `Uniwind.setTheme()`); now tracks
  the active app theme. Google brand colors retained per brand-asset policy.

DOCUMENTED exceptions:

- SocialButton Google icon brand colors (#4285F4 / #34A853 / #FBBC05 / #EA4335)
  — kept as official Google brand assets.
- OrganizationSwitcherSheet forces `colorScheme: dark` in SwiftUI environment.
  Documented as design choice; revisit if app supports system light/dark.

Story coverage (8 stories) — sidecar to source per existing convention:

- OrganizationAvatar — fully presentational; matrix story for sizes 20/28/36/48/64
- SocialButton — both providers; brand-color verification
- DevSignInButton — base render only
- OrganizationHeaderButton — *Preview wrapper renders inner content* (real
  component lives inside expo-router Stack.Toolbar — not isolatable)
- OrganizationSwitcherSheet — *Inner content* + NativeSheet iOS-only story
  (real sheet uses @expo/ui/swift-ui — Android renders disclaimer)
- OrgDropdown — *Preview wrapper* with mock orgs (real reads authClient +
  Electric collections — not isolatable)
- TabBarAccessory — *Preview wrapper* with mock org (real reads useOrganizations)
- AuthenticatedTabBar — placeholder documenting integration dependencies
  (TabBarView SwiftUI bridge + expo-router/ui Tabs context + Electric)

Storybook glob — extended `.rnstorybook/main.js` to include
`../screens/**/*.stories.?(ts|tsx|js|jsx)` so screen-component stories are
picked up by sb-rn-get-stories.

Audit doc — apps/mobile/screens/AUDIT.md catalogs all 8 components, the 2
fixes applied, the 2 documented exceptions, the 6 clean components, and the
isolation caveats per integration-heavy component.

This completes the migration of the live app component set to the ember theme.
The Storybook walkthrough on iOS Simulator + Android Emulator is the
verification surface.

Verified: bun typecheck (exit 0), biome check (clean).
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from 4cdc402 to 0f891ea Compare May 26, 2026 19:08
…s PR)

The Welcome story was added in PR superset-sh#4874 (tooling) to give Storybook at
least one entry — otherwise the empty stories index threw an
EmptyIndexError on the tooling branch in isolation.

This PR brings real component stories (8 first-party + 28 vendor
primitives), so the placeholder is no longer needed.
Adds 5 chat-specific atom components + stories as foundation for the chat
mobile sprint. Token-driven (state palette, ember accent), 44pt touch targets,
no hardcoded values. These do NOT yet match the designs/atoms/{name}/ HTML
mockups exactly — a follow-up pass will refine each against the design spec
before chat screens consume them.

Why "foundation" not "migration": these are new components, not migration of
existing live app components to the new theme. They were started during the
build pass but the user redirected scope to migration-first; committing as
foundation rather than discarding so the work survives for future refinement.

- components/IconButton/ — Pressable + Icon wrapper with 44pt min hit target,
  5 variants (default/primary/secondary/ghost/destructive), 3 sizes.
  Real-design refinement TODO: match designs/atoms/icon-button/README.md
  variant taxonomy (--ghost is default per spec, also needs --soft and
  --neutral variants; pill shape modifier).
- components/Pill/ — chat-domain pill (model chip, mode chip, suggested answer)
  with 6 variants (default/selected/warning/danger/success/live) and
  interactive/non-interactive split.
  Real-design refinement TODO: match designs/atoms/pill/README.md — needs
  6 variants per spec (default/strong/accent/live/warning/danger), 3 sizes
  with monospace + uppercase modifiers, separate dismiss button.
- components/StatusDot/ — single colored circle (live/warning/danger/success/
  neutral + ember). 3 sizes.
  Real-design refinement TODO: match designs/atoms/status-dot/README.md —
  6px/8px/10px exact sizes (sm 8px is default), live variant needs pulse
  animation, warning needs box-shadow ring.
- components/StreamingCursor/ — Reanimated ▌ blink. Configurable duration +
  color. accessibilityElementsHidden per spec.
  Real-design refinement TODO: match designs/atoms/streaming-cursor/README.md
  — 2px width × 1em height with 6px glow shadow, 1s steps(2) animation,
  --default / --steady / --paused variants.
- components/ToolStatusRule/ — 3px (default) left rule with running/completed/
  failed/pending/neutral/ember variants.
  Real-design refinement TODO: match designs/atoms/tool-status-rule/README.md
  — 5 status variants (--running/--done/--pending/--error/--neutral), needs
  --vertical/--horizontal orientation, glow shadow on running + pending.

All atoms have Components/{Name} stories with argTypes wired. Verified:
bun typecheck (exit 0), biome check (no warnings).

ScrollFade and HitTargetWrapper from the original 7-atom plan are NOT
included — they were not started. Will be built fresh from designs/atoms/
spec when chat screens need them.
Phase 4b BUILD PLAN written to apps/mobile/design/manifest.json (plan
gate → passed for mobile-ios + mobile-android). Scope cut to chat-view
per designs/AUDIT.md; sessions-list/nav surfaces deferred.

Refined against design specs (designs/atoms/<name>/README.md):
  - IconButton  → 5 variants × 4 sizes × 2 shapes; composes vendor Button
  - Pill        → 6 variants × 3 sizes; mono/uppercase mods + dismiss
                  ✕ button; composes vendor Badge
  - StatusDot   → xs/sm/md (6/8/10px); live-pulse + warning-ring halos,
                  reduced-motion via AccessibilityInfo
  - StreamingCursor → default/steady/paused variants; steps(2) emulation;
                      reduced-motion fallback
  - ToolStatusRule  → 5 status variants × vertical/horizontal; glow on
                      running + pending; fixed 3px per spec

Created:
  - FabBase          — 56/64pt circular FAB; live-ring pulse halo;
                       composes existing IconButton/Pressable patterns
  - HitTargetWrapper — 44pt invisible tap zone; square/circle shapes;
                       debug-outline modifier
  - ProgressDots     — 3-dot staggered pulse; 4 variants × 3 sizes;
                       reduced-motion fallback
  - ScrollFade       — theme-aware gradient (expo-linear-gradient);
                       top/bottom × 3 surfaces × 3 sizes; 120ms hide
                       transition via Reanimated
  - ToastBase        — 5 variants × inline/stacked; composes
                       ToolStatusRule + Icon + Text + HitTargetWrapper

Composition priority observed: existing internal components (vendor
Button/Badge/Icon/Text + first-party atoms) → rn-primitives → custom.
NativeWind className preferred over inline style where reasonable; inline
style retained only for Reanimated animated values and RN custom-colored
shadows.

Stories use Components/ prefix per pixel-perfect convention; all props
wired to argTypes controls with descriptive options.

Deps:
  - apps/mobile: + expo-linear-gradient@56.0.4 (latest stable for SDK 56)

Verification:
  - bun run typecheck (apps/mobile): clean
  - bunx biome check (touched files): clean
ScrollFade imported THEME from @/lib/theme to pick the gradient opaque
stop. lib/theme.ts transitively imports DarkTheme/DefaultTheme from
expo-router/react-navigation, which pulls in UnhandledLinkingContext.
Storybook RN does not wrap stories in expo-router's NavigationContainer,
so any story that touches ScrollFade crashed with:

    ERROR  [Error: Couldn't find an UnhandledLinkingContext context.]

Fix: inline the 3 surface color values (page/soft/overlay × light/dark)
locally in ScrollFade.tsx. Keep in sync with --color-background /
--color-card / --color-popover in global.css. Decouples ScrollFade from
the nav-coupled theme module.
…ents out of args

Two prep-time blockers cleared:

1. preview.tsx imported NavigationContainer from expo-router/react-navigation.
   Storybook 9 RN evaluates preview.tsx during createPreparedStoryMapping
   (before decorators apply). Loading expo-router's nav module reads the
   default UnhandledLinkingContext value, whose getters throw "Couldn't find
   an UnhandledLinkingContext context."
   Decorators apply at render time, not prep time — so the wrapping I added
   in commit 0e805a1 was structurally unable to fix the chain. Worse, it
   BECAME the trigger.
   Fix: remove the nav import + decorator from preview.tsx. Keep only the
   neutral View + PortalHost wrapper. Documented why nav can't live here.

2. ToastBase WithAction + StackedWithCTA stories put `<Button>` React
   elements directly in `args.action`. Storybook tries to JSON-serialize
   args to persist control state; React elements have circular refs that
   trigger "cycle in arg" warnings repeatedly.
   Fix: move the action element into the story's `render` function. Args
   stay JSON-serializable; the render fn supplies the live React element at
   render time.

Verified: bundler running from apps/mobile cwd; full bundle compiles 21MB
HTTP 200; sim screenshot shows DesignSystem "Semantic pairs" story
rendering (background/foreground, card/card-foreground swatches visible).
No UnhandledLinkingContext error in fresh bundler log; no cycle warning
in fresh re-bundles.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from 0f891ea to 3c130de Compare May 26, 2026 19:09
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 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 `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx`:
- Around line 80-113: The spinner rendering currently ignores the disabled prop
and shows ActivityIndicator whenever resolving matches a button; update the
three spinner branches inside the Button components (the decline button, the
approve button using onApprove, and the always button using onAlways) to only
render the ActivityIndicator when resolving matches AND disabled is false (i.e.,
gate each spinner with !disabled && resolving === "…"). Ensure the fallback Text
labels remain unchanged.

In `@apps/mobile/components/FabBase/FabBase.tsx`:
- Around line 164-167: The ActivityIndicator in FabBase is being tinted via
className but Uniwind controls spinner tint via color or colorClassName; in the
FabBase component replace the className={iconColorByVariant[resolvedVariant]}
prop on ActivityIndicator with the appropriate tint prop (either
color={iconColorByVariant[resolvedVariant]} if the map contains a valid color
string or colorClassName={iconColorByVariant[resolvedVariant]} if it contains
Uniwind accent-* tokens) so the spinner tint is applied reliably.

In `@apps/mobile/components/ModalHeader/ModalHeader.tsx`:
- Around line 52-58: The close IconButton in ModalHeader is rendered even when
onClose is undefined, producing an interactive-looking control that does
nothing; update ModalHeader to only render the IconButton (icon={X},
accessibilityLabel={closeAccessibilityLabel}, variant="ghost", size="md",
onPress={onClose}) when the onClose prop is provided (or else render nothing/ a
non-interactive decorative element) so the close control is not shown as a
no-op.

In `@apps/mobile/components/ScrollFade/ScrollFade.tsx`:
- Around line 106-118: The props spread on the Animated.View in ScrollFade is
applied last, allowing callers to override the decorative-only contract
(pointerEvents="none", accessibilityElementsHidden,
importantForAccessibility="no-hide-descendants"); update ScrollFade so the
component-controlled accessibility/interaction props cannot be overridden by
consumers—either move {...props} before the fixed props on the Animated.View or
explicitly omit/filter pointerEvents, accessibilityElementsHidden, and
importantForAccessibility from incoming props inside the ScrollFade component,
ensuring those three values remain enforced.

In `@apps/mobile/components/StreamingCursor/StreamingCursor.tsx`:
- Around line 76-95: The computed blink phase lengths can become negative when
resolvedDuration is very small (so half - 1 < 0); update the useEffect logic
around resolvedDuration/half to sanitize/clamp durations before building the
withSequence timings: either clamp resolvedDuration to a safe minimum (e.g.,
ensure resolvedDuration >= 2) or compute phase durations like const halfMinusOne
= Math.max(0, half - 1) and use those non-negative values in the withTiming
calls; update references in the same block using resolvedDuration, half,
opacity, and shouldAnimate so no timing argument is negative.
- Around line 59-79: The initial reduceMotion state defaults to false causing a
flash for users who prefer reduced motion; change the initialization and logic
so the component assumes reduced-motion (non-animated) until AccessibilityInfo
resolves. Specifically, update the useState for reduceMotion so it defaults to
"true" (or to a tri-state and treat null as reduced) and keep the useEffect that
calls AccessibilityInfo.isReduceMotionEnabled() and subscribes via
AccessibilityInfo.addEventListener to update reduceMotion; ensure shouldAnimate
(which uses variant, resolvedDuration, durationMs, and variantDurationMs) only
returns true when reduceMotion is explicitly false so animations are suppressed
until the preference is known.

In `@apps/mobile/components/ui/checkbox.stories.tsx`:
- Around line 2-17: CheckboxShowcase currently uses useState(initialChecked)
which only reads the prop on first render, so when the Storybook control updates
initialChecked the UI stays out of sync; update CheckboxShowcase to import and
use useEffect and add a useEffect(() => setChecked(initialChecked),
[initialChecked]) so the local checked state is resynced whenever the prop
changes (referencing the CheckboxShowcase function, checked/setChecked state,
and initialChecked prop).

In `@apps/mobile/components/ui/collapsible.stories.tsx`:
- Line 3: The story initializes open once from initialOpen so changing the
Storybook control doesn't update it; inside the story component that declares
const [open, setOpen] = useState(initialOpen) add a useEffect that runs when
initialOpen changes: useEffect(() => setOpen(initialOpen), [initialOpen]) so the
open state is kept in sync with the initialOpen control (refer to the
initialOpen, open, setOpen state declarations in the story).

In `@apps/mobile/components/ui/input.stories.tsx`:
- Around line 2-3: The story initializes local state via useState(initialValue)
so changing the `initialValue` story arg after mount doesn’t update the input;
add a useEffect that watches `initialValue` and calls `setValue(initialValue)`
to keep `value` in sync. Update the story component where `initialValue`,
`value`, and `setValue` are declared (and replicate the same effect for the
other story instance mentioned) so the rendered input reflects control changes.

In `@apps/mobile/components/ui/switch.stories.tsx`:
- Around line 2-3: The story initializes local checked state with
useState(initialChecked) which only seeds once, so the rendered Switch won't
update when the Storybook arg initialChecked changes; update the story to watch
initialChecked (via useEffect) and call setChecked(initialChecked) whenever
initialChecked changes (or convert to a controlled pattern that directly uses
initialChecked), referencing the useState(initialChecked) call and the
checked/setChecked variables in the story to locate where to apply the fix.

In `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx`:
- Around line 59-64: The Pressable in UserMessageBubble currently always exposes
button semantics even when onLongPress is undefined; update the render for the
Pressable (or its wrapper) so accessibilityRole, accessibilityLabel and
accessibilityHint are only set when the onLongPress prop exists: check the
onLongPress value and conditionally pass those accessibility props (or omit
them) so the bubble is not announced as an actionable button unless onLongPress
is provided.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ae09b795-bd51-4685-9801-847b11b3e78f

📥 Commits

Reviewing files that changed from the base of the PR and between 4cdc402 and 3c130de.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (132)
  • apps/mobile/.rnstorybook/preview.tsx
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/AppHeader/AppHeader.tsx
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/Banner/Banner.stories.tsx
  • apps/mobile/components/Banner/Banner.tsx
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/CodeBlock/CodeBlock.stories.tsx
  • apps/mobile/components/CodeBlock/CodeBlock.tsx
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsx
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.tsx
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/ComposerRow/ComposerRow.stories.tsx
  • apps/mobile/components/ComposerRow/ComposerRow.tsx
  • apps/mobile/components/ComposerRow/index.ts
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsx
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/FabBase/FabBase.stories.tsx
  • apps/mobile/components/FabBase/FabBase.tsx
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.stories.tsx
  • apps/mobile/components/HitTargetWrapper/HitTargetWrapper.tsx
  • apps/mobile/components/HitTargetWrapper/index.ts
  • apps/mobile/components/IconButton/IconButton.stories.tsx
  • apps/mobile/components/IconButton/IconButton.tsx
  • apps/mobile/components/IconButton/index.ts
  • apps/mobile/components/ModalHeader/ModalHeader.stories.tsx
  • apps/mobile/components/ModalHeader/ModalHeader.tsx
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsx
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/PendingActionPill/PendingActionPill.tsx
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsx
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/PickerTrigger/PickerTrigger.stories.tsx
  • apps/mobile/components/PickerTrigger/PickerTrigger.tsx
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/Pill/Pill.stories.tsx
  • apps/mobile/components/Pill/Pill.tsx
  • apps/mobile/components/Pill/index.ts
  • apps/mobile/components/ProgressDots/ProgressDots.stories.tsx
  • apps/mobile/components/ProgressDots/ProgressDots.tsx
  • apps/mobile/components/ProgressDots/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.tsx
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/ScrollFade/ScrollFade.stories.tsx
  • apps/mobile/components/ScrollFade/ScrollFade.tsx
  • apps/mobile/components/ScrollFade/index.ts
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsx
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/StatusDot/StatusDot.stories.tsx
  • apps/mobile/components/StatusDot/StatusDot.tsx
  • apps/mobile/components/StatusDot/index.ts
  • apps/mobile/components/StreamingCursor/StreamingCursor.stories.tsx
  • apps/mobile/components/StreamingCursor/StreamingCursor.tsx
  • apps/mobile/components/StreamingCursor/index.ts
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsx
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsx
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsx
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/ToastBase/ToastBase.stories.tsx
  • apps/mobile/components/ToastBase/ToastBase.tsx
  • apps/mobile/components/ToastBase/index.ts
  • apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.stories.tsx
  • apps/mobile/components/ToolStatusRule/ToolStatusRule.tsx
  • apps/mobile/components/ToolStatusRule/index.ts
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/components/ui/accordion.stories.tsx
  • apps/mobile/components/ui/alert-dialog.stories.tsx
  • apps/mobile/components/ui/alert.stories.tsx
  • apps/mobile/components/ui/aspect-ratio.stories.tsx
  • apps/mobile/components/ui/avatar.stories.tsx
  • apps/mobile/components/ui/badge.stories.tsx
  • apps/mobile/components/ui/button.stories.tsx
  • apps/mobile/components/ui/card.stories.tsx
  • apps/mobile/components/ui/checkbox.stories.tsx
  • apps/mobile/components/ui/collapsible.stories.tsx
  • apps/mobile/components/ui/context-menu.stories.tsx
  • apps/mobile/components/ui/dialog.stories.tsx
  • apps/mobile/components/ui/dropdown-menu.stories.tsx
  • apps/mobile/components/ui/hover-card.stories.tsx
  • apps/mobile/components/ui/icon.stories.tsx
  • apps/mobile/components/ui/input.stories.tsx
  • apps/mobile/components/ui/label.stories.tsx
  • apps/mobile/components/ui/menubar.stories.tsx
  • apps/mobile/components/ui/popover.stories.tsx
  • apps/mobile/components/ui/progress.stories.tsx
  • apps/mobile/components/ui/radio-group.stories.tsx
  • apps/mobile/components/ui/select.stories.tsx
  • apps/mobile/components/ui/separator.stories.tsx
  • apps/mobile/components/ui/skeleton.stories.tsx
  • apps/mobile/components/ui/switch.stories.tsx
  • apps/mobile/components/ui/tabs.stories.tsx
  • apps/mobile/components/ui/text.stories.tsx
  • apps/mobile/components/ui/textarea.stories.tsx
  • apps/mobile/components/ui/toggle-group.stories.tsx
  • apps/mobile/components/ui/toggle.stories.tsx
  • apps/mobile/components/ui/tooltip.stories.tsx
  • apps/mobile/package.json
  • apps/mobile/screens/(auth)/sign-in/components/DevSignInButton/DevSignInButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.stories.tsx
  • apps/mobile/screens/(auth)/sign-in/components/SocialButton/SocialButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationHeaderButton/OrganizationHeaderButton.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/OrganizationSwitcherSheet.stories.tsx
  • apps/mobile/screens/(authenticated)/(home)/workspaces/components/OrganizationSwitcherSheet/components/OrganizationAvatar/OrganizationAvatar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/AuthenticatedTabBar/AuthenticatedTabBar.stories.tsx
  • apps/mobile/screens/(authenticated)/components/OrgDropdown/OrgDropdown.stories.tsx
  • apps/mobile/screens/(authenticated)/components/TabBarAccessory/TabBarAccessory.stories.tsx
  • apps/mobile/screens/AUDIT.md
✅ Files skipped from review due to trivial changes (21)
  • apps/mobile/components/ProgressDots/index.ts
  • apps/mobile/components/ScrollFade/index.ts
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/UserMessageBubble/index.ts
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/FabBase/index.ts
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/HitTargetWrapper/index.ts
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/ui/AUDIT.md
  • apps/mobile/screens/AUDIT.md
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/ComposerRow/index.ts

Comment on lines +80 to +113
{resolving === "decline" ? (
<ActivityIndicator size="small" className="text-white" />
) : (
<Text>Decline</Text>
)}
</Button>
<Button
variant="default"
size="default"
className="flex-1 h-touch-min"
onPress={onApprove}
accessibilityLabel="Approve tool action"
>
{resolving === "approve" ? (
<ActivityIndicator
size="small"
className="text-primary-foreground"
/>
) : (
<Text>Approve</Text>
)}
</Button>
<Button
variant="ghost"
size="default"
className="flex-1 h-touch-min"
onPress={onAlways}
accessibilityLabel="Always allow this tool category"
>
{resolving === "always" ? (
<ActivityIndicator size="small" className="text-foreground" />
) : (
<Text>Always</Text>
)}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Suppress resolving spinners when disabled is true

disabled is documented as “no spinner”, but current branches still show a spinner if resolving is set. Gate spinner rendering with !disabled.

Proposed fix
-						{resolving === "decline" ? (
+						{!disabled && resolving === "decline" ? (
 							<ActivityIndicator size="small" className="text-white" />
 						) : (
 							<Text>Decline</Text>
 						)}
@@
-						{resolving === "approve" ? (
+						{!disabled && resolving === "approve" ? (
 							<ActivityIndicator
 								size="small"
 								className="text-primary-foreground"
 							/>
 						) : (
 							<Text>Approve</Text>
 						)}
@@
-						{resolving === "always" ? (
+						{!disabled && resolving === "always" ? (
 							<ActivityIndicator size="small" className="text-foreground" />
 						) : (
 							<Text>Always</Text>
 						)}
📝 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
{resolving === "decline" ? (
<ActivityIndicator size="small" className="text-white" />
) : (
<Text>Decline</Text>
)}
</Button>
<Button
variant="default"
size="default"
className="flex-1 h-touch-min"
onPress={onApprove}
accessibilityLabel="Approve tool action"
>
{resolving === "approve" ? (
<ActivityIndicator
size="small"
className="text-primary-foreground"
/>
) : (
<Text>Approve</Text>
)}
</Button>
<Button
variant="ghost"
size="default"
className="flex-1 h-touch-min"
onPress={onAlways}
accessibilityLabel="Always allow this tool category"
>
{resolving === "always" ? (
<ActivityIndicator size="small" className="text-foreground" />
) : (
<Text>Always</Text>
)}
{!disabled && resolving === "decline" ? (
<ActivityIndicator size="small" className="text-white" />
) : (
<Text>Decline</Text>
)}
</Button>
<Button
variant="default"
size="default"
className="flex-1 h-touch-min"
onPress={onApprove}
accessibilityLabel="Approve tool action"
>
{!disabled && resolving === "approve" ? (
<ActivityIndicator
size="small"
className="text-primary-foreground"
/>
) : (
<Text>Approve</Text>
)}
</Button>
<Button
variant="ghost"
size="default"
className="flex-1 h-touch-min"
onPress={onAlways}
accessibilityLabel="Always allow this tool category"
>
{!disabled && resolving === "always" ? (
<ActivityIndicator size="small" className="text-foreground" />
) : (
<Text>Always</Text>
)}
🤖 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 `@apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx` around lines 80 -
113, The spinner rendering currently ignores the disabled prop and shows
ActivityIndicator whenever resolving matches a button; update the three spinner
branches inside the Button components (the decline button, the approve button
using onApprove, and the always button using onAlways) to only render the
ActivityIndicator when resolving matches AND disabled is false (i.e., gate each
spinner with !disabled && resolving === "…"). Ensure the fallback Text labels
remain unchanged.

Comment on lines +164 to +167
<ActivityIndicator
size="small"
className={iconColorByVariant[resolvedVariant]}
/>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In React Native 0.85.x, does ActivityIndicator support a classNameprop, or should spinner tint be set only via thecolor prop?

💡 Result:

In React Native 0.85.x, the built-in ActivityIndicator component does not support a className prop [1]. You must set the spinner's color using the color prop [2][1]. While the standard React Native ActivityIndicator does not natively accept a className prop, some third-party styling libraries (such as certain Tailwind-based wrappers) may introduce a className prop that maps to the underlying style or color properties [3][4]. However, in the standard component API, the color of the spinner is strictly controlled via the color prop [2][1][5]. Top results: [2], [1], [3], [4]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/FabBase/FabBase.tsx"
# Show imports + the spinner block area
sed -n '1,120p' "$FILE"
echo "----"
sed -n '130,220p' "$FILE"

Repository: superset-sh/superset

Length of output: 4936


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find all ActivityIndicator usages and see whether className/color is used
rg -n "ActivityIndicator" -S . | head -n 200

echo "----"
# 2) Check for NativeWind/babel setup hints (className support in RN)
rg -n "nativewind|NativeWind|tailwind-react-native|babel-plugin-nativewind|nativewind" -S . | head -n 200

echo "----"
# 3) Specifically search for className usage on ActivityIndicator
rg -n "ActivityIndicator[^\n]*\n(?:.*\n)*.*className\s*=" -S apps packages . || true

Repository: superset-sh/superset

Length of output: 3147


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/mobile/components/ToastBase/ToastBase.tsx
echo "----"
sed -n '1,170p' apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
echo "----"
sed -n '80,170p' apps/mobile/components/IconButton/IconButton.tsx
echo "----"
sed -n '1,220p' apps/mobile/components/ToolCallCard/ToolCallCard.tsx
echo "----"
# Search for Tailwind/RN styling integration (NativeWind / uniwind)
rg -n "uniwind|nativewind|tailwind-react-native|babel-plugin-nativewind|tailwindcss|postcss|nativewindui" -S . | head -n 200

Repository: superset-sh/superset

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Check the uniwind/nativewind types augmentation file
ls -la apps/mobile | head
echo "----"
ls -la apps/mobile | rg -n "uniwind|nativewind|tailwind" || true

echo "----"
# biome.json references apps/mobile/uniwind-types.d.ts
if [ -f "apps/mobile/uniwind-types.d.ts" ]; then
  sed -n '1,200p' apps/mobile/uniwind-types.d.ts
fi

echo "----"
# 2) Focused grep for uniwind/nativewind integration only under apps/mobile
rg -n --hidden "uniwind|NativeWind|nativewind|tailwindcss|babel-plugin|className" apps/mobile | head -n 200

echo "----"
# 3) Check whether any file defines/uses ActivityIndicator color prop patterns
rg -n "ActivityIndicator[\s\S]{0,120}color\s*=" apps/mobile | head -n 50 || true

Repository: superset-sh/superset

Length of output: 25558


🌐 Web query:

uniwind ActivityIndicator className tint color prop

💡 Result:

In Uniwind, you cannot use the standard className prop to set the ActivityIndicator's color, because the color property is not part of the component's style object [1]. Instead, you must use the colorClassName prop with the accent- prefix [2][1]. Usage Example: Key Rules: 1. className maps only to the style prop [2][1]. 2. Non-style color props (like color, tintColor, etc.) require a corresponding *ClassName prop (e.g., colorClassName, tintColorClassName) [1][3][4]. 3. You must use the accent- prefix for these colorClassName props [2][1]. Since Uniwind version 1.6.2, passing a class without this prefix to a color prop will trigger a development warning [2][5].

Citations:


🌐 Web query:

NativeWind/uniwind ActivityIndicator className text color

💡 Result:

To change the color of an ActivityIndicator when using NativeWind or Uniwind, you should not use the className prop for the color itself, as color is a component-specific prop rather than a style attribute in React Native [1][2]. In NativeWind and Uniwind, the approach differs slightly based on the library: Uniwind (from the creators of Unistyles) Uniwind explicitly separates style props from functional props like color [2]. 1. Use the className prop for layout-related styles (e.g., margins, padding) [2]. 2. Use the colorClassName prop (suffixed with "ClassName") to map to the ActivityIndicator color prop [2]. 3. Prefix the color utility class with accent- (e.g., accent-blue-500) [2]. Example: NativeWind NativeWind typically maps styles directly, but for the ActivityIndicator, the color property is handled as a standard React Native prop [1]. 1. Use the native color prop directly to set the color value [1]. 2. If you are using a theme or custom colors, pass them into the color prop directly [1]. Example: If you are building a wrapper component in either library, you can use a higher-order component (such as withUniwind) or a custom wrapper to automatically map color-related props to their corresponding *ClassName variants [3]. Summary of key distinctions: - Uniwind: Requires the colorClassName prop and the accent- prefix [2]. - NativeWind: Typically uses the standard React Native color prop [1].

Citations:


🌐 Web query:

react-native ActivityIndicator className uniwind types

💡 Result:

In Uniwind, the React Native ActivityIndicator component supports the className prop directly out of the box, as do all core React Native components [1]. For styling the ActivityIndicator, Uniwind utilizes the following convention: - className: Maps to the standard style prop, allowing you to use regular Tailwind utility classes for layout or spacing (e.g., className="m-4") [2][1]. - colorClassName: A specialized prop for handling the indicator's color. You must use the accent- prefix for these values (e.g., colorClassName="accent-blue-500") [2]. Passing a className without this prefix to a color-related prop will trigger a development console warning [2]. Because Uniwind provides native support for these props, you do not need to manually wrap the ActivityIndicator component in a styling utility [1]. TypeScript support is included automatically as part of the Uniwind integration with your React Native project [1][3]. Example usage: [2]

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "ActivityIndicator[^{\n]*\b(colorClassName|tintColor|color)\s*=" apps/mobile | head -n 200
rg -n "colorClassName" apps/mobile | head -n 200
rg -n "tintColorClassName|accent-" apps/mobile | head -n 200

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for accent- usage anywhere in apps/mobile
rg -n "accent-" apps/mobile | head -n 200

echo "----"
# Look for colorClassName usage in the whole repo (just in case outside apps/mobile)
rg -n "colorClassName" . | head -n 200

echo "----"
# Look for ActivityIndicator color usage patterns that might exist elsewhere
rg -n "ActivityIndicator[^\n]*\b(colorClassName|color=|tintColor)\b" . | head -n 200 || true

Repository: superset-sh/superset

Length of output: 5261


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find tailwind/uniwind config that defines colors
fd -a "tailwind.config.*" apps/mobile -t f -d 3 || true
fd -a "uniwind.config.*" apps/mobile -t f -d 3 || true
ls -la apps/mobile | head

echo "----"
# Search for specific tokens used in iconColorByVariant
rg -n "primary-foreground|background\\b|foreground\\b" apps/mobile -g"*.ts" -g"*.tsx" -g"*.js" -g"*.css" | head -n 200

echo "----"
# Search for any "accent-" color class definitions in configs
rg -n "accent-" apps/mobile -g"*.ts" -g"*.tsx" -g"*.js" -g"*.css" | head -n 200

echo "----"
# Search for ActivityIndicator-specific props usage
rg -n "colorClassName|tintColorClassName" apps/mobile -g"*.ts" -g"*.tsx" | head -n 200

Repository: superset-sh/superset

Length of output: 30355


Tint ActivityIndicator via color/colorClassName, not className

apps/mobile/components/FabBase/FabBase.tsx passes className={iconColorByVariant[resolvedVariant]} to react-native’s ActivityIndicator. In this codebase’s Uniwind setup, spinner tint is controlled by color or Uniwind’s colorClassName (accent-*), not the style-only className, so the tint won’t be reliable. Update the prop used for spinner tint accordingly.

🤖 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 `@apps/mobile/components/FabBase/FabBase.tsx` around lines 164 - 167, The
ActivityIndicator in FabBase is being tinted via className but Uniwind controls
spinner tint via color or colorClassName; in the FabBase component replace the
className={iconColorByVariant[resolvedVariant]} prop on ActivityIndicator with
the appropriate tint prop (either color={iconColorByVariant[resolvedVariant]} if
the map contains a valid color string or
colorClassName={iconColorByVariant[resolvedVariant]} if it contains Uniwind
accent-* tokens) so the spinner tint is applied reliably.

Comment on lines +52 to +58
<IconButton
icon={X}
accessibilityLabel={closeAccessibilityLabel}
variant="ghost"
size="md"
onPress={onClose}
/>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid rendering a no-op close control when onClose is undefined.

When onClose is missing, the close icon is still interactive-looking but does nothing.

🛠️ Proposed fix
 			<IconButton
 				icon={X}
 				accessibilityLabel={closeAccessibilityLabel}
 				variant="ghost"
 				size="md"
 				onPress={onClose}
+				disabled={!onClose}
 			/>
📝 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
<IconButton
icon={X}
accessibilityLabel={closeAccessibilityLabel}
variant="ghost"
size="md"
onPress={onClose}
/>
<IconButton
icon={X}
accessibilityLabel={closeAccessibilityLabel}
variant="ghost"
size="md"
onPress={onClose}
disabled={!onClose}
/>
🤖 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 `@apps/mobile/components/ModalHeader/ModalHeader.tsx` around lines 52 - 58, The
close IconButton in ModalHeader is rendered even when onClose is undefined,
producing an interactive-looking control that does nothing; update ModalHeader
to only render the IconButton (icon={X},
accessibilityLabel={closeAccessibilityLabel}, variant="ghost", size="md",
onPress={onClose}) when the onClose prop is provided (or else render nothing/ a
non-interactive decorative element) so the close control is not shown as a
no-op.

Comment on lines +106 to +118
<Animated.View
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={[animatedStyle, style]}
className={cn(
"absolute left-0 right-0 z-[5]",
directionClass[direction],
sizeClass[size],
className,
)}
{...props}
>
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent overriding the decorative-only accessibility/interaction contract.

{...props} is applied last, so callers can override pointerEvents="none" and hidden-accessibility props. This breaks the component’s stated “decorative only” behavior.

Suggested fix
 	return (
 		<Animated.View
+			{...props}
 			accessibilityElementsHidden
 			importantForAccessibility="no-hide-descendants"
 			pointerEvents="none"
 			style={[animatedStyle, style]}
 			className={cn(
 				"absolute left-0 right-0 z-[5]",
 				directionClass[direction],
 				sizeClass[size],
 				className,
 			)}
-			{...props}
 		>
📝 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
<Animated.View
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={[animatedStyle, style]}
className={cn(
"absolute left-0 right-0 z-[5]",
directionClass[direction],
sizeClass[size],
className,
)}
{...props}
>
<Animated.View
{...props}
accessibilityElementsHidden
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={[animatedStyle, style]}
className={cn(
"absolute left-0 right-0 z-[5]",
directionClass[direction],
sizeClass[size],
className,
)}
>
🤖 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 `@apps/mobile/components/ScrollFade/ScrollFade.tsx` around lines 106 - 118, The
props spread on the Animated.View in ScrollFade is applied last, allowing
callers to override the decorative-only contract (pointerEvents="none",
accessibilityElementsHidden, importantForAccessibility="no-hide-descendants");
update ScrollFade so the component-controlled accessibility/interaction props
cannot be overridden by consumers—either move {...props} before the fixed props
on the Animated.View or explicitly omit/filter pointerEvents,
accessibilityElementsHidden, and importantForAccessibility from incoming props
inside the ScrollFade component, ensuring those three values remain enforced.

Comment on lines +59 to +79
const [reduceMotion, setReduceMotion] = useState(false);

useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
if (mounted) setReduceMotion(enabled);
});
const sub = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
(enabled) => setReduceMotion(enabled),
);
return () => {
mounted = false;
sub.remove();
};
}, []);

const resolvedDuration = durationMs ?? variantDurationMs[variant];
const shouldAnimate =
variant !== "steady" && resolvedDuration > 0 && !reduceMotion;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor reduced-motion preference from first render.

On Line 59, reduceMotion starts as false, so users with reduced motion enabled can still see a brief blink before the async check resolves. Default to a non-animated state until preference is known.

Proposed fix
-	const [reduceMotion, setReduceMotion] = useState(false);
+	const [reduceMotion, setReduceMotion] = useState<boolean | null>(null);
@@
-	const shouldAnimate =
-		variant !== "steady" && resolvedDuration > 0 && !reduceMotion;
+	const shouldAnimate =
+		reduceMotion === false && variant !== "steady" && resolvedDuration > 0;
📝 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
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
if (mounted) setReduceMotion(enabled);
});
const sub = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
(enabled) => setReduceMotion(enabled),
);
return () => {
mounted = false;
sub.remove();
};
}, []);
const resolvedDuration = durationMs ?? variantDurationMs[variant];
const shouldAnimate =
variant !== "steady" && resolvedDuration > 0 && !reduceMotion;
const [reduceMotion, setReduceMotion] = useState<boolean | null>(null);
useEffect(() => {
let mounted = true;
AccessibilityInfo.isReduceMotionEnabled().then((enabled) => {
if (mounted) setReduceMotion(enabled);
});
const sub = AccessibilityInfo.addEventListener(
"reduceMotionChanged",
(enabled) => setReduceMotion(enabled),
);
return () => {
mounted = false;
sub.remove();
};
}, []);
const resolvedDuration = durationMs ?? variantDurationMs[variant];
const shouldAnimate =
reduceMotion === false && variant !== "steady" && resolvedDuration > 0;
🤖 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 `@apps/mobile/components/StreamingCursor/StreamingCursor.tsx` around lines 59 -
79, The initial reduceMotion state defaults to false causing a flash for users
who prefer reduced motion; change the initialization and logic so the component
assumes reduced-motion (non-animated) until AccessibilityInfo resolves.
Specifically, update the useState for reduceMotion so it defaults to "true" (or
to a tri-state and treat null as reduced) and keep the useEffect that calls
AccessibilityInfo.isReduceMotionEnabled() and subscribes via
AccessibilityInfo.addEventListener to update reduceMotion; ensure shouldAnimate
(which uses variant, resolvedDuration, durationMs, and variantDurationMs) only
returns true when reduceMotion is explicitly false so animations are suppressed
until the preference is known.

Comment on lines +2 to +17
import { useState } from "react";
import { View } from "react-native";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";

function CheckboxShowcase({
initialChecked,
disabled,
label,
}: {
initialChecked: boolean;
disabled: boolean;
label: string;
}) {
const [checked, setChecked] = useState(initialChecked);
return (
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/checkbox.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "---- $FILE (head) ----"
sed -n '1,120p' "$FILE" | cat -n

echo "---- grep for initialChecked / checked state usage ----"
rg -n "initialChecked|useState\(|setChecked|useEffect" "$FILE" || true

Repository: superset-sh/superset

Length of output: 2315


Resync local checked state when initialChecked changes (so Storybook control updates don’t leave the UI out of sync).

File: apps/mobile/components/ui/checkbox.stories.tsx (lines 2-17)

useState(initialChecked) only reads the arg on initial mount; updating initialChecked won’t update checked.

Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [checked, setChecked] = useState(initialChecked);
+	useEffect(() => {
+		setChecked(initialChecked);
+	}, [initialChecked]);
📝 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
import { useState } from "react";
import { View } from "react-native";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
function CheckboxShowcase({
initialChecked,
disabled,
label,
}: {
initialChecked: boolean;
disabled: boolean;
label: string;
}) {
const [checked, setChecked] = useState(initialChecked);
return (
import { useEffect, useState } from "react";
import { View } from "react-native";
import { Checkbox } from "`@/components/ui/checkbox`";
import { Label } from "`@/components/ui/label`";
function CheckboxShowcase({
initialChecked,
disabled,
label,
}: {
initialChecked: boolean;
disabled: boolean;
label: string;
}) {
const [checked, setChecked] = useState(initialChecked);
useEffect(() => {
setChecked(initialChecked);
}, [initialChecked]);
return (
🤖 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 `@apps/mobile/components/ui/checkbox.stories.tsx` around lines 2 - 17,
CheckboxShowcase currently uses useState(initialChecked) which only reads the
prop on first render, so when the Storybook control updates initialChecked the
UI stays out of sync; update CheckboxShowcase to import and use useEffect and
add a useEffect(() => setChecked(initialChecked), [initialChecked]) so the local
checked state is resynced whenever the prop changes (referencing the
CheckboxShowcase function, checked/setChecked state, and initialChecked prop).

@@ -0,0 +1,81 @@
import type { Meta, StoryObj } from "@storybook/react-native";
import { ChevronDown, ChevronRight } from "lucide-react-native";
import { useState } from "react";
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Sync open when initialOpen control changes.

open is only initialized once, so the initialOpen Storybook control stops affecting the story after mount (Line 22).

🔧 Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [open, setOpen] = useState(initialOpen);
+	useEffect(() => {
+		setOpen(initialOpen);
+	}, [initialOpen]);
+
 	return (

Also applies to: 22-26

🤖 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 `@apps/mobile/components/ui/collapsible.stories.tsx` at line 3, The story
initializes open once from initialOpen so changing the Storybook control doesn't
update it; inside the story component that declares const [open, setOpen] =
useState(initialOpen) add a useEffect that runs when initialOpen changes:
useEffect(() => setOpen(initialOpen), [initialOpen]) so the open state is kept
in sync with the initialOpen control (refer to the initialOpen, open, setOpen
state declarations in the story).

Comment on lines +2 to +3
import { useState } from "react";
import { View } from "react-native";
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and show the relevant Storybook file
FILE="apps/mobile/components/ui/input.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "---- $FILE (head) ----"
sed -n '1,120p' "$FILE" | nl -ba

echo "---- grep for initialValue/value state patterns ----"
rg -n "initialValue|useState\\(|value\\s*," "$FILE" || true

echo "---- grep for Storybook args usage ----"
rg -n "args\\.|argTypes|render\\(|StoryObj|Meta" "$FILE" || true

Repository: superset-sh/superset

Length of output: 187


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/input.stories.tsx"
test -f "$FILE"

echo "---- $FILE (first 220 lines, with line numbers via awk) ----"
awk '{printf "%4d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'

echo "---- rg: initialValue/value patterns ----"
rg -n "initialValue|useState\\(|value\\s*=" "$FILE" || true

echo "---- rg: story args / render function patterns ----"
rg -n "args\\.|argTypes|render\\(|StoryObj|parameters|controls" "$FILE" || true

Repository: superset-sh/superset

Length of output: 3013


Keep value state in sync with initialValue story args.

initialValue is only used to initialize value via useState(initialValue), so changing the initialValue control won’t update the rendered input after mount. Add an effect to update value when initialValue changes.

Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [value, setValue] = useState(initialValue);
+	useEffect(() => {
+		setValue(initialValue);
+	}, [initialValue]);

Also applies to: 18-19

🤖 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 `@apps/mobile/components/ui/input.stories.tsx` around lines 2 - 3, The story
initializes local state via useState(initialValue) so changing the
`initialValue` story arg after mount doesn’t update the input; add a useEffect
that watches `initialValue` and calls `setValue(initialValue)` to keep `value`
in sync. Update the story component where `initialValue`, `value`, and
`setValue` are declared (and replicate the same effect for the other story
instance mentioned) so the rendered input reflects control changes.

Comment on lines +2 to +3
import { useState } from "react";
import { View } from "react-native";
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant story file with line numbers
FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "== $FILE =="
nl -ba "$FILE" | sed -n '1,200p'

Repository: superset-sh/superset

Length of output: 177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

nl -ba "$FILE" | sed -n '1,220p'

Repository: superset-sh/superset

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/ui/switch.stories.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

echo "== $FILE =="
cat -n "$FILE" | sed -n '1,240p'

Repository: superset-sh/superset

Length of output: 2140


Sync checked state with the initialChecked Storybook arg

useState(initialChecked) only seeds on the first render, so changing the Storybook control for initialChecked won’t update the rendered Switch.

Proposed fix
-import { useState } from "react";
+import { useEffect, useState } from "react";
@@
 	const [checked, setChecked] = useState(initialChecked);
+	useEffect(() => {
+		setChecked(initialChecked);
+	}, [initialChecked]);
🤖 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 `@apps/mobile/components/ui/switch.stories.tsx` around lines 2 - 3, The story
initializes local checked state with useState(initialChecked) which only seeds
once, so the rendered Switch won't update when the Storybook arg initialChecked
changes; update the story to watch initialChecked (via useEffect) and call
setChecked(initialChecked) whenever initialChecked changes (or convert to a
controlled pattern that directly uses initialChecked), referencing the
useState(initialChecked) call and the checked/setChecked variables in the story
to locate where to apply the fix.

Comment on lines +59 to +64
<Pressable
accessibilityRole="button"
accessibilityLabel="Long-press to copy or share"
accessibilityHint="Opens the message context menu"
onLongPress={onLongPress}
delayLongPress={400}
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant portion of the file with line numbers
FILE="apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "=== File: $FILE (lines 1-140) ==="
nl -ba "$FILE" | sed -n '1,140p'

echo
echo "=== File: $FILE (lines 40-110) ==="
nl -ba "$FILE" | sed -n '40,110p'

echo
echo "=== Type definition for UserMessageBubbleProps (if present) ==="
rg -n "type UserMessageBubbleProps|interface UserMessageBubbleProps|UserMessageBubbleProps" "$FILE" || true

Repository: superset-sh/superset

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx"
[ -f "$FILE" ] || { echo "Missing file: $FILE"; exit 1; }

echo "=== File: $FILE (lines 1-160) ==="
cat -n "$FILE" | sed -n '1,160p'

echo
echo "=== Type definition for UserMessageBubbleProps (if present) ==="
rg -n "type UserMessageBubbleProps|interface UserMessageBubbleProps|UserMessageBubbleProps" "$FILE" || true

echo
echo "=== onLongPress usage and related props in file ==="
rg -n "onLongPress|accessibilityRole|accessibilityLabel|accessibilityHint|Long-press to copy or share|context menu" "$FILE" || true

Repository: superset-sh/superset

Length of output: 3818


Review comment

File: apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
Lines: 59-64

Snippet showing the final state of code at these lines

The below code does not show removed or replaced code.

			<Pressable
				accessibilityRole="button"
				accessibilityLabel="Long-press to copy or share"
				accessibilityHint="Opens the message context menu"
				onLongPress={onLongPress}
				delayLongPress={400}

Comment

Only expose button semantics when onLongPress exists.

onLongPress is optional, but the bubble is always announced as an actionable button with a context-menu hint.

Proposed fix
 export function UserMessageBubble({
@@
 }: UserMessageBubbleProps) {
+	const hasLongPressAction = Boolean(onLongPress);
 	return (
@@
 			<Pressable
-				accessibilityRole="button"
-				accessibilityLabel="Long-press to copy or share"
-				accessibilityHint="Opens the message context menu"
+				accessibilityRole={hasLongPressAction ? "button" : undefined}
+				accessibilityLabel={
+					hasLongPressAction ? "Long-press to copy or share" : undefined
+				}
+				accessibilityHint={
+					hasLongPressAction ? "Opens the message context menu" : undefined
+				}
 				onLongPress={onLongPress}
🤖 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 `@apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx` around lines
59 - 64, The Pressable in UserMessageBubble currently always exposes button
semantics even when onLongPress is undefined; update the render for the
Pressable (or its wrapper) so accessibilityRole, accessibilityLabel and
accessibilityHint are only set when the onLongPress prop exists: check the
onLongPress value and conditionally pass those accessibility props (or omit
them) so the bubble is not announced as an actionable button unless onLongPress
is provided.

…e behavior

HitTargetWrapper was a 44pt Pressable wrapper with no consumers beyond
ToastBase's dismiss button. IconButton already provides the same 44pt
touch target plus hitSlop, variants, sizes, shapes, and loading state.

ToastBase dismiss now uses IconButton (variant=ghost, shape=pill, size=xs)
instead of HitTargetWrapper + Icon composition.
…gate

Wave 2 of the pixel-perfect chat-view build. 19 molecules added under
apps/mobile/components/ (folder/folder + index.ts + stories), composing
the Wave-1 atoms + vendor primitives (Button, Badge, Avatar, RadioGroup,
Textarea, Collapsible, Separator, Icon, Text).

Composer cluster (replaces deprecated picker-toolbar pattern per desktop
PR superset-sh#4866 / SUPER-755 unified-settings-menu directive):
  - PickerTrigger          — kept as building block for internal use
  - ComposerSettingsButton — NEW: single pill with [Shield][Model][Brain]
                              icons; tap opens composer-settings bottom
                              sheet (sheet itself is organism, deferred
                              to Wave 3 — needs sheet primitive added)
  - ComposerRow            — Textarea + send/stop button + settings pill;
                              4 state variants (idle/typing/streaming/sending)

Popover row items (rendered inside settings sheet or slash-command popover):
  - SlashCommandOption · ModelPickerOption · ThinkingLevelOption

Chrome:
  - AppHeader   — leading back + centered title/subtitle + trailing actions
  - ModalHeader — leading ✕ + title + optional trailing action; simple variant

Render surfaces:
  - CodeBlock          — language label + Copy IconButton + Separator + mono body
  - Banner             — 4 variants × 2 shapes; top horizontal ToolStatusRule
  - PendingApprovalCard — UC-PAUSE-01 inline pending card; 4 states
  - ApprovalFooter     — UC-PAUSE-01 sticky footer; Decline/Approve/Always
                          ordering for one-handed UX (deviation documented)
  - SuggestedAnswerPill — UC-PAUSE-02 ask_user pill; composes Pill (accent/default/ghost)
  - PendingActionPill  — UC-PAUSE-04 floating pill; 3 kinds × Reanimated FadeIn/Out
  - AssistantMessageHead — UC-RENDER-01 head; 5 status variants
  - UserMessageBubble  — UC-RENDER-01 user message; 3 variants + failed state
  - ToolCallCard       — UC-RENDER-04 tool invocation; 5 status variants
  - CollapsedBlock     — UC-RENDER-05/06 plan/reasoning/subagent; vendor Collapsible
  - ScrollBackButton   — UC-RENDER-07 floating FAB + new-messages dot badge

Composition priority observed: existing internal Wave-1 atoms (Pill,
IconButton, ToolStatusRule, StatusDot, ProgressDots, FabBase, etc.)
+ vendor components/ui/* (Button, Badge, Avatar, RadioGroup, Collapsible,
Separator, Icon, Text, Textarea) → custom. Reanimated used only for
animated values; otherwise NativeWind className.

Each story uses Molecules/ prefix per pixel-perfect convention. All
props wired to argTypes controls.

Stale: ComposerToolbar (popover-per-trigger pattern) was prototyped then
deleted per user feedback — desktop PR superset-sh#4866 collapses 3 sibling pills
into a single trigger menu, and the equivalent mobile pattern is a
bottom sheet behind one button (ComposerSettingsButton).

Verification:
  - bun run typecheck (apps/mobile): clean
  - bunx biome check (touched files): clean
  - Wave 1 storybook bundle still loads (no regressions to atom stories)

Gates flipped (apps/mobile/design/manifest.json):
  mobile-ios:      gates.molecules → passed (phase=molecules)
  mobile-android:  gates.molecules → passed (shared source tree)

Wave 3 (screens) deferred behind user confirmation.
Replace the stacked layout (settings pill on its own row above textarea)
with the Claude iOS pattern: single rounded container with the textarea
on top and an action toolbar inside the same chrome below.

Toolbar order (mirrors Claude reference):
  LEFT:  [+] commands button → [Shield/Model/Brain] settings pill
  RIGHT: send / stop / progress-dots (state-driven swap)

Behavior per variant is unchanged:
  - idle:      Send disabled; toolbar fully enabled
  - typing:    Send active (primary ember)
  - streaming: Stop (destructive); toolbar buttons disabled
  - sending:   ProgressDots replaces Send in 44pt slot; toolbar buttons disabled

API:
  + Added `onCommandsPress?: () => void` + `commandsAccessibilityLabel?: string`
  - Settings prop unchanged. Send/stop/value/onChangeText/variant/placeholder kept.
  - When `onCommandsPress` is omitted the `+` button is hidden — keeps the
    API surface optional for hosts that haven't wired a slash-command
    popover yet.

Composition: composes vendor Textarea (inner editable, borderless) +
first-party IconButton (commands `+` ghost sm; send/stop primary/
destructive pill md) + ComposerSettingsButton + ProgressDots. The outer
container owns the border + bg-card; the textarea is transparent inside.

Out of scope (organisms / Wave 3):
  - Bottom-sheet behind ComposerSettingsButton
  - Slash-command popover behind the `+` button

Verification:
  - bun run typecheck: clean
  - bunx biome check apps/mobile/components/ComposerRow: clean
  - Storybook RN on iOS 26.5 sim: bundle compiles HTTP 200 (22MB, 5249 mod);
    Molecules/ComposerRow story renders new layout; bottom toolbar shows
    [+] [○ Sonnet 4.6] on left, ember send circle on right; sample text
    rendered in textarea above. Screenshot in plan-mode reference confirms.

Per plan: ~/.claude/plans/i-feel-we-need-clever-plum.md
…tching commands circle

User feedback on the first Claude-style pass:
1. The input body was two-tone — vendor Textarea has `dark:bg-input/30` +
   `shadow-sm` chrome that wins over my `bg-transparent` in dark mode,
   creating a darker rectangle inside the outer card surface.
2. The commands icon should be `/` (slash-command intent) not `+`
   (attachments).
3. The commands button should match the settings pill in size/border —
   a circle so the option remains visually open when the layout grows.

Fixes:
  - Textarea className: `bg-transparent dark:bg-transparent border-0
    rounded-none shadow-none` to fully suppress vendor chrome. Outer
    container owns border + bg-card so composer renders as one tonal
    block end-to-end.
  - Replaced `Plus` import with `Slash` from lucide-react-native.
  - Commands IconButton: `size="xs" shape="pill"` (28×28 circle, matches
    ComposerSettingsButton's h-7 height) + `variant="soft"` + explicit
    `border border-border` className to mirror the settings pill border
    treatment.

Verification:
  - bun run typecheck: clean
  - bunx biome check apps/mobile/components/ComposerRow: clean
  - Storybook RN on iOS 26.5 sim: Molecules/ComposerRow/Typing renders
    one-tone rounded container; bottom toolbar shows `[/]` circle button
    + `[○ Sonnet 4.6 🧠]` settings pill on left, ember send circle on
    right. Verified via xcrun simctl screenshot.
@justincrich justincrich force-pushed the chat-mobile-sprint-1-molecules branch from 3c130de to a98b606 Compare May 26, 2026 21:01
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (4)
apps/mobile/components/AppHeader/AppHeader.tsx (2)

58-65: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Back/actions can still render as no-op tappable controls.

Buttons are interactive even when onBack/onActions is missing, causing dead interactions.

Also applies to: 86-93

🤖 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 `@apps/mobile/components/AppHeader/AppHeader.tsx` around lines 58 - 65, The
header currently renders interactive IconButton components for back/actions even
when their handlers are undefined (see showBack/IconButton with
ArrowLeft/backAccessibilityLabel and the actions block using onActions),
resulting in tappable no-ops; change the rendering so that when onBack or
onActions is falsy you either render a non-interactive element (e.g., a static
icon container) or set the IconButton to a disabled/non-pressable state and
remove the onPress prop, and ensure accessibility reflects disabled state (so
replace the IconButton's onPress when onBack/onActions is absent and apply a
disabled flag or render a plain View for both the back (showBack) and actions
sections).

67-67: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use stable-width spacer slots for title centering.

w-1 placeholders can shift title centering when one side control is hidden.

Also applies to: 95-95

🤖 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 `@apps/mobile/components/AppHeader/AppHeader.tsx` at line 67, The title
centering breaks because the small spacers (<View className="w-1" />) shrink
when side controls hide; in AppHeader.tsx replace those variable tiny spacers
with stable equal-width spacer slots (e.g., a fixed width/mindWidth view used on
both sides of the title) so the left and right spacer elements that flank the
title always occupy the same width regardless of control visibility; update the
spacer instances currently at the same places (the View with className "w-1"
used around the title) to use a fixed-width style/class so centering remains
stable.
apps/mobile/components/PendingActionPill/PendingActionPill.tsx (1)

87-105: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Unmount condition relies on shared-value reads that won’t re-render React.

if (!visible && opacity.value === 0) can stay mounted after fade-out because shared-value updates are not React state updates.

In react-native-reanimated v4, do SharedValue updates trigger React component re-renders, and what is the recommended pattern for unmount-after-exit animations?
🤖 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 `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx` around lines
87 - 105, The unmount check reads the Reanimated SharedValue (opacity.value)
which does not trigger React re-renders, so the component may not unmount after
the fade-out; change to set a React state flag when the animation finishes:
start the animations in the existing useEffect and either (A) set a setTimeout
matching the 200ms duration to call setIsMounted(false) when visible becomes
false, or (B) use useAnimatedReaction on the shared value (opacity) and call
runOnJS to set a React state like setShouldRender(false) once opacity.value ===
0 and visible is false; then use that React state (e.g., shouldRender) in the
render check instead of reading opacity.value directly.
apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx (1)

81-87: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align accessibilityState.disabled with actual disabled behavior.

The control is non-interactive when isLoading is true, but accessibilityState.disabled currently ignores that path.

Proposed fix
 export function SlashCommandOption({
   name,
   description,
   source = "builtin",
   icon,
   isHighlighted,
   isLoading,
   className,
   disabled,
   ...props
 }: SlashCommandOptionProps) {
+  const isDisabled = Boolean(disabled || isLoading);
   return (
     <Pressable
       accessibilityRole="button"
       accessibilityLabel={`${name} — ${description}`}
       accessibilityState={{
         selected: isHighlighted ?? false,
-        disabled: disabled ?? false,
+        disabled: isDisabled,
         busy: isLoading ?? false,
       }}
-      disabled={disabled || isLoading}
+      disabled={isDisabled}
       className={cn(
         slashCommandOptionVariants({ isHighlighted, isLoading }),
         disabled && "opacity-40",
         className,
       )}
🤖 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 `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx` around
lines 81 - 87, Update the accessibilityState.disabled value so it reflects the
actual non-interactive condition by including isLoading; specifically, in the
SlashCommandOption component change the accessibilityState entry for disabled to
use the same combined expression as the disabled prop (disabled || isLoading) so
the accessibility state matches the rendered behavior.
🤖 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 `@apps/mobile/components/ComposerRow/ComposerRow.tsx`:
- Around line 125-131: The Stop/Send IconButton controls are interactive even
when their handlers are undefined; update the ComposerRow rendering logic (where
IconButton is used for Stop with prop onStop and for Send with prop onSend) to
guard against missing handlers by making the buttons non-interactive—either pass
a disabled flag (or set variant/shape to a non-interactive state) and omit
onPress when onStop/onSend is falsy, or render a non-button placeholder; ensure
you change both the Stop IconButton block (uses onStop) and the Send IconButton
block (uses onSend) so they are only clickable when the corresponding handler
exists.

---

Duplicate comments:
In `@apps/mobile/components/AppHeader/AppHeader.tsx`:
- Around line 58-65: The header currently renders interactive IconButton
components for back/actions even when their handlers are undefined (see
showBack/IconButton with ArrowLeft/backAccessibilityLabel and the actions block
using onActions), resulting in tappable no-ops; change the rendering so that
when onBack or onActions is falsy you either render a non-interactive element
(e.g., a static icon container) or set the IconButton to a
disabled/non-pressable state and remove the onPress prop, and ensure
accessibility reflects disabled state (so replace the IconButton's onPress when
onBack/onActions is absent and apply a disabled flag or render a plain View for
both the back (showBack) and actions sections).
- Line 67: The title centering breaks because the small spacers (<View
className="w-1" />) shrink when side controls hide; in AppHeader.tsx replace
those variable tiny spacers with stable equal-width spacer slots (e.g., a fixed
width/mindWidth view used on both sides of the title) so the left and right
spacer elements that flank the title always occupy the same width regardless of
control visibility; update the spacer instances currently at the same places
(the View with className "w-1" used around the title) to use a fixed-width
style/class so centering remains stable.

In `@apps/mobile/components/PendingActionPill/PendingActionPill.tsx`:
- Around line 87-105: The unmount check reads the Reanimated SharedValue
(opacity.value) which does not trigger React re-renders, so the component may
not unmount after the fade-out; change to set a React state flag when the
animation finishes: start the animations in the existing useEffect and either
(A) set a setTimeout matching the 200ms duration to call setIsMounted(false)
when visible becomes false, or (B) use useAnimatedReaction on the shared value
(opacity) and call runOnJS to set a React state like setShouldRender(false) once
opacity.value === 0 and visible is false; then use that React state (e.g.,
shouldRender) in the render check instead of reading opacity.value directly.

In `@apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx`:
- Around line 81-87: Update the accessibilityState.disabled value so it reflects
the actual non-interactive condition by including isLoading; specifically, in
the SlashCommandOption component change the accessibilityState entry for
disabled to use the same combined expression as the disabled prop (disabled ||
isLoading) so the accessibility state matches the rendered behavior.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5ca1fe4d-309c-431e-82d2-7b665b84b23b

📥 Commits

Reviewing files that changed from the base of the PR and between 3c130de and a98b606.

📒 Files selected for processing (59)
  • apps/mobile/components/AppHeader/AppHeader.stories.tsx
  • apps/mobile/components/AppHeader/AppHeader.tsx
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.stories.tsx
  • apps/mobile/components/ApprovalFooter/ApprovalFooter.tsx
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.stories.tsx
  • apps/mobile/components/AssistantMessageHead/AssistantMessageHead.tsx
  • apps/mobile/components/AssistantMessageHead/index.ts
  • apps/mobile/components/Banner/Banner.stories.tsx
  • apps/mobile/components/Banner/Banner.tsx
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/CodeBlock/CodeBlock.stories.tsx
  • apps/mobile/components/CodeBlock/CodeBlock.tsx
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.stories.tsx
  • apps/mobile/components/CollapsedBlock/CollapsedBlock.tsx
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/ComposerRow/ComposerRow.stories.tsx
  • apps/mobile/components/ComposerRow/ComposerRow.tsx
  • apps/mobile/components/ComposerRow/index.ts
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.stories.tsx
  • apps/mobile/components/ComposerSettingsButton/ComposerSettingsButton.tsx
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/ModalHeader/ModalHeader.stories.tsx
  • apps/mobile/components/ModalHeader/ModalHeader.tsx
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.stories.tsx
  • apps/mobile/components/ModelPickerOption/ModelPickerOption.tsx
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/PendingActionPill/PendingActionPill.stories.tsx
  • apps/mobile/components/PendingActionPill/PendingActionPill.tsx
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.stories.tsx
  • apps/mobile/components/PendingApprovalCard/PendingApprovalCard.tsx
  • apps/mobile/components/PendingApprovalCard/index.ts
  • apps/mobile/components/PickerTrigger/PickerTrigger.stories.tsx
  • apps/mobile/components/PickerTrigger/PickerTrigger.tsx
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.stories.tsx
  • apps/mobile/components/ScrollBackButton/ScrollBackButton.tsx
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.stories.tsx
  • apps/mobile/components/SlashCommandOption/SlashCommandOption.tsx
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.stories.tsx
  • apps/mobile/components/SuggestedAnswerPill/SuggestedAnswerPill.tsx
  • apps/mobile/components/SuggestedAnswerPill/index.ts
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.stories.tsx
  • apps/mobile/components/ThinkingLevelOption/ThinkingLevelOption.tsx
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/ToastBase/ToastBase.stories.tsx
  • apps/mobile/components/ToastBase/ToastBase.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.stories.tsx
  • apps/mobile/components/ToolCallCard/ToolCallCard.tsx
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.stories.tsx
  • apps/mobile/components/UserMessageBubble/UserMessageBubble.tsx
  • apps/mobile/components/UserMessageBubble/index.ts
✅ Files skipped from review due to trivial changes (15)
  • apps/mobile/components/SlashCommandOption/index.ts
  • apps/mobile/components/ComposerSettingsButton/index.ts
  • apps/mobile/components/ModalHeader/index.ts
  • apps/mobile/components/CodeBlock/index.ts
  • apps/mobile/components/Banner/index.ts
  • apps/mobile/components/AppHeader/index.ts
  • apps/mobile/components/ModelPickerOption/index.ts
  • apps/mobile/components/ToolCallCard/index.ts
  • apps/mobile/components/ScrollBackButton/index.ts
  • apps/mobile/components/ApprovalFooter/index.ts
  • apps/mobile/components/ThinkingLevelOption/index.ts
  • apps/mobile/components/PickerTrigger/index.ts
  • apps/mobile/components/CollapsedBlock/index.ts
  • apps/mobile/components/PendingActionPill/index.ts
  • apps/mobile/components/AssistantMessageHead/index.ts

Comment on lines +125 to +131
<IconButton
icon={Square}
accessibilityLabel="Stop streaming"
variant="destructive"
shape="pill"
onPress={onStop}
/>
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent dead-tap Send/Stop actions when handlers are missing.

Stop and Send are rendered interactive even if onStop/onSend is undefined, which creates no-op controls.

Suggested fix
 					) : variant === "streaming" ? (
 						<IconButton
 							icon={Square}
 							accessibilityLabel="Stop streaming"
 							variant="destructive"
 							shape="pill"
 							onPress={onStop}
+							disabled={!onStop}
 						/>
 					) : (
 						<IconButton
 							icon={Send}
 							accessibilityLabel="Send message"
 							variant="primary"
 							shape="pill"
 							onPress={onSend}
-							disabled={variant === "idle"}
+							disabled={variant === "idle" || !onSend}
 						/>
 					)}

Also applies to: 133-140

🤖 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 `@apps/mobile/components/ComposerRow/ComposerRow.tsx` around lines 125 - 131,
The Stop/Send IconButton controls are interactive even when their handlers are
undefined; update the ComposerRow rendering logic (where IconButton is used for
Stop with prop onStop and for Send with prop onSend) to guard against missing
handlers by making the buttons non-interactive—either pass a disabled flag (or
set variant/shape to a non-interactive state) and omit onPress when
onStop/onSend is falsy, or render a non-button placeholder; ensure you change
both the Stop IconButton block (uses onStop) and the Send IconButton block (uses
onSend) so they are only clickable when the corresponding handler exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant