Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,20 @@ import { EmptyState } from '@/components/empty-state';
import { PickerSheet } from '@/components/picker-sheet';
import { FindingFilterModal } from '@/components/security-agent/finding-filter-modal';
import {
clearSecurityFindingFilterBridge,
getSecurityFindingFilterBridge,
} from '@/lib/security-finding-filter-bridge';
SECURITY_FILTER_ROUTE_KEY,
securityFilterSlot,
useRouteRegistry,
} from '@/lib/route-registry';

export default function SecurityAgentFilterFindingsRoute() {
const router = useRouter();
const [bridge, setBridge] = useState(() => getSecurityFindingFilterBridge());
const [bridge, setBridge] = useState(() => securityFilterSlot.get(SECURITY_FILTER_ROUTE_KEY));
const { t } = useTranslation();
const [draft, setDraft] = useState<SecurityFindingFilters>(
() => getSecurityFindingFilterBridge()?.filters ?? DEFAULT_SECURITY_FINDING_FILTERS
() =>
securityFilterSlot.get(SECURITY_FILTER_ROUTE_KEY)?.filters ?? DEFAULT_SECURITY_FINDING_FILTERS
);
useRouteRegistry(SECURITY_FILTER_ROUTE_KEY);

const handleClose = useCallback(() => {
router.back();
Expand All @@ -35,11 +38,11 @@ export default function SecurityAgentFilterFindingsRoute() {

useFocusEffect(
useCallback(() => {
const nextBridge = getSecurityFindingFilterBridge();
const nextBridge = securityFilterSlot.get(SECURITY_FILTER_ROUTE_KEY);
setBridge(nextBridge);
setDraft(nextBridge?.filters ?? DEFAULT_SECURITY_FINDING_FILTERS);
return () => {
clearSecurityFindingFilterBridge();
securityFilterSlot.clear(SECURITY_FILTER_ROUTE_KEY);
};
}, [])
);
Expand Down
24 changes: 11 additions & 13 deletions apps/mobile/src/app/(app)/agent-chat/instance-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import { radioItemA11y } from '@/components/ui/radio-group';
import { Skeleton } from '@/components/ui/skeleton';
import { Text } from '@/components/ui/text';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import {
clearInstancePickerBridge,
getInstancePickerBridge,
type InstancePickerInstance,
} from '@/lib/picker-bridge';
import { type InstancePickerInstance } from '@/lib/picker-bridge';
import { instancePickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry';
import {
dedupeInstanceLabels,
type LabeledInstance,
Expand All @@ -34,8 +31,9 @@ export default function InstancePickerScreen() {
const colors = useThemeColors();
const { bottom } = useSafeAreaInsets();
const { t } = useTranslation();
const [bridge, setBridge] = useState(() => getInstancePickerBridge());
const [bridge, setBridge] = useState(() => instancePickerSlot.get(UNFENCED_ROUTE_KEY));
const bridgeRef = useRef(bridge);
useRouteRegistry(UNFENCED_ROUTE_KEY);

const closePicker = useCallback(() => {
router.back();
Expand Down Expand Up @@ -69,7 +67,7 @@ export default function InstancePickerScreen() {

useFocusEffect(
useCallback(() => {
const nextBridge = getInstancePickerBridge();
const nextBridge = instancePickerSlot.get(UNFENCED_ROUTE_KEY);
bridgeRef.current = nextBridge;
setBridge(nextBridge);
// kilocode_change - `refetchOnWindowFocus` only reacts to OS-level
Expand All @@ -80,8 +78,8 @@ export default function InstancePickerScreen() {
void refetchInstances();

return () => {
clearInstancePickerBridge();
bridgeRef.current = null;
instancePickerSlot.clear(UNFENCED_ROUTE_KEY);
bridgeRef.current = undefined;
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- refetchInstances is a stable react-query function identity; including it would re-run this effect on every render because react-query does not memoize it across renders.
}, [])
Expand All @@ -103,17 +101,17 @@ export default function InstancePickerScreen() {
const handleSelectCloudAgent = useCallback(() => {
void Haptics.selectionAsync();
bridgeRef.current?.onSelect(null);
clearInstancePickerBridge();
bridgeRef.current = null;
instancePickerSlot.clear(UNFENCED_ROUTE_KEY);
bridgeRef.current = undefined;
closePicker();
}, [closePicker]);

const handleSelectInstance = useCallback(
(instance: InstancePickerInstance) => {
void Haptics.selectionAsync();
bridgeRef.current?.onSelect(instance);
clearInstancePickerBridge();
bridgeRef.current = null;
instancePickerSlot.clear(UNFENCED_ROUTE_KEY);
bridgeRef.current = undefined;
closePicker();
},
[closePicker]
Expand Down
23 changes: 10 additions & 13 deletions apps/mobile/src/app/(app)/agent-chat/mode-picker.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Haptics from 'expo-haptics';
import { useRouter } from 'expo-router';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { Check } from '@/components/ui/icons';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { FlatList, Pressable, ScrollView, View } from 'react-native';
import { useTranslation } from 'react-i18next';

Expand All @@ -21,27 +21,24 @@ import {
import { PickerSheet } from '@/components/picker-sheet';
import { Text } from '@/components/ui/text';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { clearModePickerBridge, getModePickerBridge } from '@/lib/picker-bridge';
import { parseParam } from '@/lib/route-params';
import { modePickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry';

export default function ModePickerScreen() {
const router = useRouter();
const colors = useThemeColors();
const { t } = useTranslation();
// Lazy init reads the bridge synchronously on first render — no effect, no
const { routeKey: rawRouteKey } = useLocalSearchParams<{ routeKey?: string }>();
const routeKey = parseParam(rawRouteKey) ?? UNFENCED_ROUTE_KEY;
useRouteRegistry(routeKey);
// Lazy init reads the slot synchronously on first render — no effect, no
// "No options available" flash before a later effect populates state.
const [bridge] = useState(() => getModePickerBridge());

useEffect(
() => () => {
clearModePickerBridge();
},
[]
);
const [bridge] = useState(() => modePickerSlot.get(routeKey));
Comment thread
iscekic marked this conversation as resolved.

function handleSelect(mode: AgentMode) {
void Haptics.selectionAsync();
bridge?.onSelect(mode);
clearModePickerBridge();
modePickerSlot.clear(routeKey);
router.back();
}

Expand Down
17 changes: 9 additions & 8 deletions apps/mobile/src/app/(app)/agent-chat/repo-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EmptyState } from '@/components/empty-state';
import { PickerSheet } from '@/components/picker-sheet';
import { Text } from '@/components/ui/text';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { clearRepoPickerBridge, getRepoPickerBridge } from '@/lib/picker-bridge';
import { repoPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry';
import { filterRepoPickerOptions } from '@/lib/repo-picker-filter';

export default function RepoPickerScreen() {
Expand All @@ -19,24 +19,25 @@ export default function RepoPickerScreen() {
const { bottom } = useSafeAreaInsets();
const { t } = useTranslation();
const [search, setSearch] = useState('');
const [bridge, setBridge] = useState(() => getRepoPickerBridge());
const [bridge, setBridge] = useState(() => repoPickerSlot.get(UNFENCED_ROUTE_KEY));

const bridgeRef = useRef(bridge);
useRouteRegistry(UNFENCED_ROUTE_KEY);

const closePicker = useCallback(() => {
router.back();
}, [router]);

useFocusEffect(
useCallback(() => {
const nextBridge = getRepoPickerBridge();
const nextBridge = repoPickerSlot.get(UNFENCED_ROUTE_KEY);
bridgeRef.current = nextBridge;
setBridge(nextBridge);
setSearch('');

return () => {
clearRepoPickerBridge();
bridgeRef.current = null;
repoPickerSlot.clear(UNFENCED_ROUTE_KEY);
bridgeRef.current = undefined;
};
}, [])
);
Expand All @@ -50,8 +51,8 @@ export default function RepoPickerScreen() {
(repo: string) => {
void Haptics.selectionAsync();
bridgeRef.current?.onSelect(repo);
clearRepoPickerBridge();
bridgeRef.current = null;
repoPickerSlot.clear(UNFENCED_ROUTE_KEY);
bridgeRef.current = undefined;
closePicker();
},
[closePicker]
Expand All @@ -73,7 +74,7 @@ export default function RepoPickerScreen() {
<FlatList
className="flex-1 bg-background"
data={filtered}
keyExtractor={repo => repo.fullName}
keyExtractor={repo => `${repo.platform}:${repo.fullName}`}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
contentContainerStyle={{ paddingBottom: bottom }}
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/components/agents/instance-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { Pressable } from 'react-native';
import { Text } from '@/components/ui/text';
import { i18n } from '@/i18n';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { type InstancePickerInstance, setInstancePickerBridge } from '@/lib/picker-bridge';
import { type InstancePickerInstance } from '@/lib/picker-bridge';
import { instancePickerSlot, UNFENCED_ROUTE_KEY } from '@/lib/route-registry';
import { cn } from '@/lib/utils';

type InstanceSelectorProps = {
Expand Down Expand Up @@ -63,7 +64,7 @@ export function InstanceSelector({
if (!canOpenPicker) {
return;
}
setInstancePickerBridge({
instancePickerSlot.set(UNFENCED_ROUTE_KEY, {
instances,
currentValue: value,
onSelect: onChange,
Expand Down
11 changes: 7 additions & 4 deletions apps/mobile/src/components/agents/mode-selector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Pressable } from 'react-native';
import { type Href, useRouter } from 'expo-router';
import { type Href, useLocalSearchParams, useRouter } from 'expo-router';
import { useTranslation } from 'react-i18next';
import { ChevronDown } from '@/components/ui/icons';

Expand All @@ -14,7 +14,8 @@ import {
} from '@/components/agents/mode-normalize';
import { Text } from '@/components/ui/text';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import { setModePickerBridge } from '@/lib/picker-bridge';
import { parseParam } from '@/lib/route-params';
import { modePickerSlot, UNFENCED_ROUTE_KEY } from '@/lib/route-registry';
import { cn } from '@/lib/utils';

export type { AgentMode };
Expand All @@ -35,6 +36,7 @@ export function ModeSelector({
const router = useRouter();
const colors = useThemeColors();
const { t } = useTranslation();
const { 'session-id': rawSessionId } = useLocalSearchParams<{ 'session-id'?: string }>();
const selectedValue = normalizeAgentMode(value);
const customOptionsDeduped = dedupeCustomModeOptions(customOptions);
const selectedCustomOption = ensureSelectedCustomOption(customOptionsDeduped, selectedValue).find(
Expand All @@ -49,12 +51,13 @@ export function ModeSelector({
if (disabled) {
return;
}
setModePickerBridge({
const routeKey = parseParam(rawSessionId) ?? UNFENCED_ROUTE_KEY;
modePickerSlot.set(routeKey, {
currentValue: selectedValue,
onSelect: onChange,
customOptions,
});
router.push('/(app)/agent-chat/mode-picker' as Href);
router.push(`/(app)/agent-chat/mode-picker?routeKey=${encodeURIComponent(routeKey)}` as Href);
}

return (
Expand Down
24 changes: 12 additions & 12 deletions apps/mobile/src/components/agents/model-picker-content.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Haptics from 'expo-haptics';
import { useFocusEffect, useRouter } from 'expo-router';
import { useFocusEffect, useLocalSearchParams, useRouter } from 'expo-router';
import { AlertCircle, Info, Search, SearchX } from '@/components/ui/icons';
import { useCallback, useMemo, useRef, useState } from 'react';
import { FlatList, TextInput, View } from 'react-native';
Expand All @@ -18,12 +18,9 @@ import {
favoriteToggleAction,
type ModelPickerRow,
} from '@/lib/model-picker-rows';
import {
clearModelPickerBridge,
commitModelPickerSelection,
getModelPickerBridge,
resolveModelPickerSelection,
} from '@/lib/picker-bridge';
import { commitModelPickerSelection, resolveModelPickerSelection } from '@/lib/picker-bridge';
import { parseParam } from '@/lib/route-params';
import { modelPickerSlot, UNFENCED_ROUTE_KEY, useRouteRegistry } from '@/lib/route-registry';

export function ModelPickerContent() {
const router = useRouter();
Expand All @@ -32,8 +29,11 @@ export function ModelPickerContent() {
const { bottom } = useSafeAreaInsets();
const { favorites, favoritesError, addFavorite, removeFavorite } = useModelPreferences(undefined);
const favoriteIds = useMemo(() => new Set(favorites), [favorites]);
const { routeKey: rawRouteKey } = useLocalSearchParams<{ routeKey?: string }>();
const routeKey = parseParam(rawRouteKey) ?? UNFENCED_ROUTE_KEY;
useRouteRegistry(routeKey);
const [search, setSearch] = useState('');
const [bridge, setBridge] = useState(() => getModelPickerBridge());
const [bridge, setBridge] = useState(() => modelPickerSlot.get(routeKey));
const [selectedModel, setSelectedModel] = useState(bridge?.currentValue ?? '');
const [selectedVariant, setSelectedVariant] = useState(bridge?.currentVariant ?? '');
const bridgeRef = useRef(bridge);
Expand All @@ -48,7 +48,7 @@ export function ModelPickerContent() {

useFocusEffect(
useCallback(() => {
const nextBridge = getModelPickerBridge();
const nextBridge = modelPickerSlot.get(routeKey);
const nextModel = nextBridge?.currentValue ?? '';
const nextVariant = nextBridge?.currentVariant ?? '';

Expand All @@ -75,10 +75,10 @@ export function ModelPickerContent() {
selectedVariantRef.current
);
}
clearModelPickerBridge();
bridgeRef.current = null;
modelPickerSlot.clear(routeKey);
bridgeRef.current = undefined;
};
}, [])
}, [routeKey])
);

const rows = useMemo<ModelPickerRow[]>(
Expand Down
12 changes: 5 additions & 7 deletions apps/mobile/src/components/agents/model-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ import { type ModelOption, thinkingEffortLabel } from '@/lib/hooks/use-available
import { type SessionModelOption } from '@/lib/hooks/use-session-model-options';
import { modelPickerCostLabel } from '@/lib/model-cost';
import { useThemeColors } from '@/lib/hooks/use-theme-colors';
import {
type ModelPickerSelection,
type ModelPickerSelectionScope,
setModelPickerBridge,
} from '@/lib/picker-bridge';
import { type ModelPickerSelection, type ModelPickerSelectionScope } from '@/lib/picker-bridge';
import { modelPickerSlot } from '@/lib/route-registry';
import { cn } from '@/lib/utils';

import { modelSelectorBadges } from './model-selector-badges';
Expand Down Expand Up @@ -107,7 +104,8 @@ export function openModelPicker(
}
) {
const { options, value, variant, onSelect, selectionScope = UNFENCED_SELECTION_CONTEXT } = params;
setModelPickerBridge({
const routeKey = selectionScope.selectionScope.sessionId;
modelPickerSlot.set(routeKey, {
options: options.map(option => toSessionModelOption(option)),
currentValue: value,
currentVariant: variant,
Expand All @@ -117,7 +115,7 @@ export function openModelPicker(
onSelect(selection.option.id, selection.variant, selection);
},
});
router.push('/(app)/agent-chat/model-picker' as Href);
router.push(`/(app)/agent-chat/model-picker?routeKey=${encodeURIComponent(routeKey)}` as Href);
}

export function ModelSelector({
Expand Down
Loading