feat(voice): implement turn-detection and real-time audio streaming

This commit is contained in:
Mohamed Boudra
2026-03-14 13:46:10 +07:00
parent c45b414476
commit fc7369384b
43 changed files with 3367 additions and 1476 deletions
@@ -17,6 +17,7 @@ import {
deriveWorkspaceTabPresentation,
WorkspaceTabIcon,
} from "@/screens/workspace/workspace-tab-presentation";
import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import { encodeFilePathForPathSegment } from "@/utils/host-routes";
import type { Agent } from "@/stores/session-store";
@@ -163,9 +164,19 @@ export function WorkspaceDesktopTabsRow({
: presentation.label;
const contextMenuTestId = `workspace-tab-context-${tab.key}`;
const isFirstTab = index === 0;
const isLastTab = index === tabs.length - 1;
const isOnlyTab = tabs.length <= 1;
const menuEntries = buildWorkspaceTabMenuEntries({
surface: "desktop",
tab,
index,
tabCount: tabs.length,
menuTestIDBase: contextMenuTestId,
onCopyResumeCommand,
onCopyAgentId,
onCloseTab,
onCloseTabsBefore: onCloseTabsToLeft,
onCloseTabsAfter: onCloseTabsToRight,
onCloseOtherTabs,
});
return (
<ContextMenu key={tab.key}>
@@ -285,64 +296,21 @@ export function WorkspaceDesktopTabsRow({
</Tooltip>
<ContextMenuContent align="start" width={DROPDOWN_WIDTH} testID={contextMenuTestId}>
{tab.kind === "agent" ? (
<>
{menuEntries.map((entry) =>
entry.kind === "separator" ? (
<ContextMenuSeparator key={entry.key} />
) : (
<ContextMenuItem
testID={`${contextMenuTestId}-copy-resume-command`}
onSelect={() => {
void onCopyResumeCommand(tab.agentId);
}}
key={entry.key}
testID={entry.testID}
disabled={entry.disabled}
destructive={entry.destructive}
onSelect={entry.onSelect}
>
Copy resume command
{entry.label}
</ContextMenuItem>
<ContextMenuItem
testID={`${contextMenuTestId}-copy-agent-id`}
onSelect={() => {
void onCopyAgentId(tab.agentId);
}}
>
Copy agent id
</ContextMenuItem>
</>
) : null}
<ContextMenuSeparator />
<ContextMenuItem
testID={`${contextMenuTestId}-close-left`}
disabled={isFirstTab}
onSelect={() => {
void onCloseTabsToLeft(tab.tabId);
}}
>
Close to the left
</ContextMenuItem>
<ContextMenuItem
testID={`${contextMenuTestId}-close-right`}
disabled={isLastTab}
onSelect={() => {
void onCloseTabsToRight(tab.tabId);
}}
>
Close to the right
</ContextMenuItem>
<ContextMenuItem
testID={`${contextMenuTestId}-close-others`}
disabled={isOnlyTab}
onSelect={() => {
void onCloseOtherTabs(tab.tabId);
}}
>
Close other tabs
</ContextMenuItem>
<ContextMenuItem
testID={`${contextMenuTestId}-close`}
onSelect={() => {
void onCloseTab(tab.tabId);
}}
>
Close
</ContextMenuItem>
)
)}
</ContextMenuContent>
</ContextMenu>
);
@@ -31,6 +31,7 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
@@ -84,6 +85,7 @@ import {
WorkspaceTabIcon,
WorkspaceTabOptionRow,
} from "@/screens/workspace/workspace-tab-presentation";
import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
import {
resolveWorkspaceHeader,
@@ -143,6 +145,7 @@ function buildOpenIntentKey(input: {
}
type MobileWorkspaceTabSwitcherProps = {
tabs: WorkspaceTabDescriptor[];
activeTabKey: string;
activeTabLabel: string;
activeTabPresentation: WorkspaceTabPresentation | null;
@@ -151,9 +154,16 @@ type MobileWorkspaceTabSwitcherProps = {
tabPresentationsByKey: Map<string, WorkspaceTabPresentation>;
onSelectSwitcherTab: (key: string) => void;
onSelectNewTabOption: (key: typeof NEW_TAB_AGENT_OPTION_ID) => void;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCloseTab: (tabId: string) => Promise<void> | void;
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
};
const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
tabs,
activeTabKey,
activeTabLabel,
activeTabPresentation,
@@ -162,15 +172,29 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
tabPresentationsByKey,
onSelectSwitcherTab,
onSelectNewTabOption,
onCopyResumeCommand,
onCopyAgentId,
onCloseTab,
onCloseTabsAbove,
onCloseTabsBelow,
onCloseOtherTabs,
}: MobileWorkspaceTabSwitcherProps) {
const { theme } = useUnistyles();
const [isOpen, setIsOpen] = useState(false);
const anchorRef = useRef<View>(null);
const tabIndexByKey = useMemo(() => {
const map = new Map<string, number>();
tabs.forEach((tab, index) => {
map.set(tab.key, index);
});
return map;
}, [tabs]);
return (
<View style={styles.mobileTabsRow} testID="workspace-tabs-row">
<Pressable
ref={anchorRef}
testID="workspace-tab-switcher-trigger"
style={({ hovered, pressed }) => [
styles.switcherTrigger,
(hovered || pressed || isOpen) && styles.switcherTriggerActive,
@@ -242,12 +266,71 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
const presentation =
tabPresentationsByKey.get(option.id) ??
deriveWorkspaceTabPresentation({ tab });
const tabIndex = tabIndexByKey.get(tab.key) ?? -1;
const menuTestIDBase = `workspace-tab-menu-${tab.key}`;
const menuEntries =
tabIndex < 0
? []
: buildWorkspaceTabMenuEntries({
surface: "mobile",
tab,
index: tabIndex,
tabCount: tabs.length,
menuTestIDBase,
onCopyResumeCommand,
onCopyAgentId,
onCloseTab,
onCloseTabsBefore: onCloseTabsAbove,
onCloseTabsAfter: onCloseTabsBelow,
onCloseOtherTabs,
});
return (
<WorkspaceTabOptionRow
presentation={presentation}
selected={selected}
active={active}
onPress={onPress}
trailingAccessory={
<DropdownMenu>
<DropdownMenuTrigger
testID={`${menuTestIDBase}-trigger`}
accessibilityRole="button"
accessibilityLabel={`Open menu for ${presentation.label}`}
hitSlop={8}
style={({ open, pressed }) => [
styles.mobileTabMenuTrigger,
(open || pressed) && styles.mobileTabMenuTriggerActive,
]}
>
<Ellipsis
size={theme.iconSize.sm}
color={theme.colors.foregroundMuted}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
side="bottom"
align="end"
width={220}
testID={menuTestIDBase}
>
{menuEntries.map((entry) =>
entry.kind === "separator" ? (
<DropdownMenuSeparator key={entry.key} />
) : (
<DropdownMenuItem
key={entry.key}
testID={entry.testID}
disabled={entry.disabled}
destructive={entry.destructive}
onSelect={entry.onSelect}
>
{entry.label}
</DropdownMenuItem>
)
)}
</DropdownMenuContent>
</DropdownMenu>
}
/>
);
}}
@@ -587,6 +670,29 @@ function WorkspaceScreenContent({
[normalizedServerId, normalizedWorkspaceId, openIntent]
);
const openWorkspaceDraftTab = useCallback(
(input?: { draftId?: string; focus?: boolean }) => {
if (!normalizedServerId || !normalizedWorkspaceId) {
return null;
}
const tabId = openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId: trimNonEmpty(input?.draftId) ?? generateDraftId(),
});
if (tabId && input?.focus !== false) {
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
}
return tabId;
},
[focusTab, normalizedServerId, normalizedWorkspaceId, openDraftTab]
);
useEffect(() => {
if (!currentOpenIntentKey) {
if (resolvedOpenIntentKey !== null) {
@@ -619,24 +725,11 @@ function WorkspaceScreenContent({
if (openIntent.kind === "draft") {
const draftId = openIntent.draftId.trim();
const tabId =
draftId === "new"
? openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId: generateDraftId(),
})
: openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId,
});
const tabId = openWorkspaceDraftTab({
...(draftId === "new" ? {} : { draftId }),
focus: true,
});
if (tabId) {
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
setResolvedOpenIntentKey(intentKey);
}
return;
@@ -653,18 +746,12 @@ function WorkspaceScreenContent({
: { kind: "file", path: openIntent.path },
});
if (tabId) {
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
});
setResolvedOpenIntentKey(intentKey);
}
}, [
currentOpenIntentKey,
focusTab,
openDraftTab,
openIntent,
openWorkspaceDraftTab,
openOrFocusTab,
persistenceKey,
normalizedServerId,
@@ -821,6 +908,10 @@ function WorkspaceScreenContent({
if (!persistenceKey) {
return;
}
if (openIntent) {
emptyWorkspaceSeedRef.current = null;
return;
}
if (workspaceAgents.length > 0 || terminals.length > 0) {
emptyWorkspaceSeedRef.current = null;
return;
@@ -834,20 +925,12 @@ function WorkspaceScreenContent({
return;
}
emptyWorkspaceSeedRef.current = workspaceKey;
const draftId = generateDraftId();
const tabId = openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId,
});
if (tabId) {
navigateToTabId(tabId);
}
openWorkspaceDraftTab();
}, [
navigateToTabId,
normalizedServerId,
normalizedWorkspaceId,
openDraftTab,
openIntent,
openWorkspaceDraftTab,
persistenceKey,
terminals.length,
tabs.length,
@@ -924,19 +1007,8 @@ function WorkspaceScreenContent({
}, [activeTabPresentation]);
const handleCreateDraftTab = useCallback(() => {
if (!normalizedServerId || !normalizedWorkspaceId) {
return;
}
const draftId = generateDraftId();
const tabId = openDraftTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
draftId,
});
if (tabId) {
navigateToTabId(tabId);
}
}, [navigateToTabId, normalizedServerId, normalizedWorkspaceId, openDraftTab]);
openWorkspaceDraftTab();
}, [openWorkspaceDraftTab]);
const handleCreateTerminal = useCallback(() => {
if (createTerminalMutation.isPending) {
@@ -1375,29 +1447,23 @@ function WorkspaceScreenContent({
}
if (target.kind === "draft") {
const tabId = activeTabId ?? target.draftId;
return (
<WorkspaceDraftAgentTab
key={`${normalizedServerId}:${normalizedWorkspaceId}:${tabId}`}
serverId={normalizedServerId}
workspaceId={normalizedWorkspaceId}
tabId={activeTabId ?? target.draftId}
tabId={tabId}
draftId={target.draftId}
onOpenWorkspaceFile={handleOpenFileFromChat}
onCreated={(agentSnapshot) => {
const tabId = activeTabId ?? target.draftId;
const normalized = normalizeAgentSnapshot(agentSnapshot, normalizedServerId);
const nextTabId = retargetWorkspaceTab({
retargetWorkspaceTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId,
target: { kind: "agent", agentId: agentSnapshot.id },
});
if (nextTabId) {
focusTab({
serverId: normalizedServerId,
workspaceId: normalizedWorkspaceId,
tabId: nextTabId,
});
}
useSessionStore.getState().setAgents(normalizedServerId, (prev) => {
const next = new Map(prev);
next.set(agentSnapshot.id, normalized);
@@ -1635,6 +1701,7 @@ function WorkspaceScreenContent({
{isMobile ? (
<MobileWorkspaceTabSwitcher
tabs={tabs}
activeTabKey={activeTabKey}
activeTabLabel={activeTabLabel}
activeTabPresentation={activeTabPresentation}
@@ -1643,6 +1710,12 @@ function WorkspaceScreenContent({
tabPresentationsByKey={tabPresentationsByKey}
onSelectSwitcherTab={handleSelectSwitcherTab}
onSelectNewTabOption={handleSelectNewTabOption}
onCopyResumeCommand={handleCopyResumeCommand}
onCopyAgentId={handleCopyAgentId}
onCloseTab={handleCloseTabById}
onCloseTabsAbove={handleCloseTabsToLeft}
onCloseTabsBelow={handleCloseTabsToRight}
onCloseOtherTabs={handleCloseOtherTabs}
/>
) : (
<WorkspaceDesktopTabsRow
@@ -1868,6 +1941,16 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
mobileTabMenuTrigger: {
width: 28,
height: 28,
borderRadius: theme.borderRadius.md,
alignItems: "center",
justifyContent: "center",
},
mobileTabMenuTriggerActive: {
backgroundColor: theme.colors.surface2,
},
tabsContainer: {
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
@@ -0,0 +1,107 @@
import { describe, expect, it, vi } from "vitest";
import { buildWorkspaceTabMenuEntries } from "@/screens/workspace/workspace-tab-menu";
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
function createAgentTab(): WorkspaceTabDescriptor {
return {
key: "agent_123",
tabId: "agent_123",
kind: "agent",
agentId: "agent-123",
provider: "codex",
label: "Agent 123",
subtitle: "/repo",
titleState: "ready",
};
}
describe("buildWorkspaceTabMenuEntries", () => {
it("uses desktop tab ordering labels for desktop menus", () => {
const onCopyResumeCommand = vi.fn();
const onCopyAgentId = vi.fn();
const onCloseTab = vi.fn();
const onCloseTabsBefore = vi.fn();
const onCloseTabsAfter = vi.fn();
const onCloseOtherTabs = vi.fn();
const entries = buildWorkspaceTabMenuEntries({
surface: "desktop",
tab: createAgentTab(),
index: 1,
tabCount: 3,
menuTestIDBase: "workspace-tab-context-agent_123",
onCopyResumeCommand,
onCopyAgentId,
onCloseTab,
onCloseTabsBefore,
onCloseTabsAfter,
onCloseOtherTabs,
});
expect(
entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)
).toEqual([
"Copy resume command",
"Copy agent id",
"Close to the left",
"Close to the right",
"Close other tabs",
"Close",
]);
});
it("uses stacked ordering labels for mobile menus", () => {
const entries = buildWorkspaceTabMenuEntries({
surface: "mobile",
tab: createAgentTab(),
index: 1,
tabCount: 3,
menuTestIDBase: "workspace-tab-menu-agent_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCloseTab: vi.fn(),
onCloseTabsBefore: vi.fn(),
onCloseTabsAfter: vi.fn(),
onCloseOtherTabs: vi.fn(),
});
expect(
entries.filter((entry) => entry.kind === "item").map((entry) => entry.label)
).toEqual([
"Copy resume command",
"Copy agent id",
"Close tabs above",
"Close tabs below",
"Close other tabs",
"Close",
]);
});
it("omits agent copy actions for non-agent tabs", () => {
const entries = buildWorkspaceTabMenuEntries({
surface: "mobile",
tab: {
key: "draft_123",
tabId: "draft_123",
kind: "draft",
draftId: "draft_123",
label: "New agent",
subtitle: "/repo",
},
index: 0,
tabCount: 1,
menuTestIDBase: "workspace-tab-menu-draft_123",
onCopyResumeCommand: vi.fn(),
onCopyAgentId: vi.fn(),
onCloseTab: vi.fn(),
onCloseTabsBefore: vi.fn(),
onCloseTabsAfter: vi.fn(),
onCloseOtherTabs: vi.fn(),
});
expect(entries.some((entry) => entry.kind === "item" && entry.label === "Copy agent id")).toBe(
false
);
expect(entries.some((entry) => entry.kind === "separator")).toBe(false);
});
});
@@ -0,0 +1,137 @@
import type { WorkspaceTabDescriptor } from "@/screens/workspace/workspace-tabs-types";
export type WorkspaceTabMenuSurface = "desktop" | "mobile";
export type WorkspaceTabMenuEntry =
| {
kind: "item";
key: string;
label: string;
disabled?: boolean;
destructive?: boolean;
testID: string;
onSelect: () => void;
}
| {
kind: "separator";
key: string;
};
interface BuildWorkspaceTabMenuEntriesInput {
surface: WorkspaceTabMenuSurface;
tab: WorkspaceTabDescriptor;
index: number;
tabCount: number;
menuTestIDBase: string;
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
onCopyAgentId: (agentId: string) => Promise<void> | void;
onCloseTab: (tabId: string) => Promise<void> | void;
onCloseTabsBefore: (tabId: string) => Promise<void> | void;
onCloseTabsAfter: (tabId: string) => Promise<void> | void;
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
}
function buildCloseBeforeLabel(surface: WorkspaceTabMenuSurface): string {
return surface === "mobile" ? "Close tabs above" : "Close to the left";
}
function buildCloseAfterLabel(surface: WorkspaceTabMenuSurface): string {
return surface === "mobile" ? "Close tabs below" : "Close to the right";
}
function buildCloseBeforeTestIDSuffix(surface: WorkspaceTabMenuSurface): string {
return surface === "mobile" ? "close-above" : "close-left";
}
function buildCloseAfterTestIDSuffix(surface: WorkspaceTabMenuSurface): string {
return surface === "mobile" ? "close-below" : "close-right";
}
export function buildWorkspaceTabMenuEntries(
input: BuildWorkspaceTabMenuEntriesInput
): WorkspaceTabMenuEntry[] {
const {
surface,
tab,
index,
tabCount,
menuTestIDBase,
onCopyResumeCommand,
onCopyAgentId,
onCloseTab,
onCloseTabsBefore,
onCloseTabsAfter,
onCloseOtherTabs,
} = input;
const isFirstTab = index === 0;
const isLastTab = index === tabCount - 1;
const isOnlyTab = tabCount <= 1;
const entries: WorkspaceTabMenuEntry[] = [];
if (tab.kind === "agent") {
entries.push({
kind: "item",
key: "copy-resume-command",
label: "Copy resume command",
testID: `${menuTestIDBase}-copy-resume-command`,
onSelect: () => {
void onCopyResumeCommand(tab.agentId);
},
});
entries.push({
kind: "item",
key: "copy-agent-id",
label: "Copy agent id",
testID: `${menuTestIDBase}-copy-agent-id`,
onSelect: () => {
void onCopyAgentId(tab.agentId);
},
});
entries.push({
kind: "separator",
key: "copy-separator",
});
}
entries.push({
kind: "item",
key: "close-before",
label: buildCloseBeforeLabel(surface),
disabled: isFirstTab,
testID: `${menuTestIDBase}-${buildCloseBeforeTestIDSuffix(surface)}`,
onSelect: () => {
void onCloseTabsBefore(tab.tabId);
},
});
entries.push({
kind: "item",
key: "close-after",
label: buildCloseAfterLabel(surface),
disabled: isLastTab,
testID: `${menuTestIDBase}-${buildCloseAfterTestIDSuffix(surface)}`,
onSelect: () => {
void onCloseTabsAfter(tab.tabId);
},
});
entries.push({
kind: "item",
key: "close-others",
label: "Close other tabs",
disabled: isOnlyTab,
testID: `${menuTestIDBase}-close-others`,
onSelect: () => {
void onCloseOtherTabs(tab.tabId);
},
});
entries.push({
kind: "item",
key: "close",
label: "Close",
testID: `${menuTestIDBase}-close`,
onSelect: () => {
void onCloseTab(tab.tabId);
},
});
return entries;
}
@@ -104,6 +104,7 @@ describe("deriveWorkspaceTabModel", () => {
});
expect(model.tabs.map((tab) => tab.descriptor.kind)).toEqual(["draft", "agent", "file"]);
expect(model.tabs[0]?.descriptor.label).toBe("New Agent");
});
it("applies stored order and appends newly-derived tabs deterministically", () => {
@@ -169,8 +169,8 @@ export function deriveWorkspaceTabModel(input: {
tabId: tab.tabId,
kind: "draft",
draftId: tab.target.draftId,
label: "Draft",
subtitle: "Draft",
label: "New Agent",
subtitle: "New Agent",
},
});
continue;
@@ -1,4 +1,4 @@
import type { ReactElement } from "react";
import type { ReactElement, ReactNode } from "react";
import { Pressable, Text, View } from "react-native";
import { Bot, Check, FileText, Pencil, Terminal } from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -124,6 +124,7 @@ type WorkspaceTabOptionRowProps = {
selected: boolean;
active: boolean;
onPress: () => void;
trailingAccessory?: ReactNode;
};
export function WorkspaceTabOptionRow({
@@ -131,30 +132,36 @@ export function WorkspaceTabOptionRow({
selected,
active,
onPress,
trailingAccessory,
}: WorkspaceTabOptionRowProps): ReactElement {
const { theme } = useUnistyles();
return (
<Pressable
onPress={onPress}
style={({ hovered = false, pressed }) => [
styles.optionRow,
(hovered || pressed || active) && styles.optionRowActive,
]}
>
<View style={styles.optionLeadingSlot}>
<WorkspaceTabIcon presentation={presentation} active={selected || active} />
</View>
<View style={styles.optionContent}>
<Text numberOfLines={1} style={styles.optionLabel}>
{presentation.titleState === "loading" ? "Loading..." : presentation.label}
</Text>
</View>
<View style={[styles.optionRow, active && styles.optionRowActive]}>
<Pressable
onPress={onPress}
style={({ hovered = false, pressed }) => [
styles.optionMainPressable,
(hovered || pressed || active) && styles.optionRowActive,
]}
>
<View style={styles.optionLeadingSlot}>
<WorkspaceTabIcon presentation={presentation} active={selected || active} />
</View>
<View style={styles.optionContent}>
<Text numberOfLines={1} style={styles.optionLabel}>
{presentation.titleState === "loading" ? "Loading..." : presentation.label}
</Text>
</View>
</Pressable>
{selected ? (
<View style={styles.optionTrailingSlot}>
<Check size={16} color={theme.colors.foregroundMuted} />
</View>
) : null}
</Pressable>
{trailingAccessory ? (
<View style={styles.optionTrailingAccessorySlot}>{trailingAccessory}</View>
) : null}
</View>
);
}
@@ -178,12 +185,21 @@ const styles = StyleSheet.create((theme) => ({
alignItems: "center",
minHeight: 36,
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
paddingHorizontal: theme.spacing[1],
paddingVertical: theme.spacing[1],
borderRadius: 0,
marginHorizontal: theme.spacing[1],
marginBottom: theme.spacing[1],
},
optionMainPressable: {
flex: 1,
minWidth: 0,
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[2],
},
optionRowActive: {
backgroundColor: theme.colors.surface1,
},
@@ -204,6 +220,9 @@ const styles = StyleSheet.create((theme) => ({
width: 16,
alignItems: "center",
justifyContent: "center",
marginLeft: "auto",
},
optionTrailingAccessorySlot: {
alignItems: "center",
justifyContent: "center",
},
}));