55 View ,
66 Pressable ,
77 ScrollView ,
8+ FlatList ,
89 Modal ,
910 Alert ,
1011 ActivityIndicator ,
@@ -38,7 +39,12 @@ import { fetch as expoFetch } from "expo/fetch"
3839import { buildPermissionCardModel } from "@/lib/pending-permissions"
3940import { unregisterRelayDevice } from "@/lib/relay-client"
4041import { useMdnsDiscovery } from "@/hooks/use-mdns-discovery"
41- import { useMonitoring , type MonitorJob , type PermissionDecision } from "@/hooks/use-monitoring"
42+ import {
43+ useMonitoring ,
44+ type MonitorJob ,
45+ type PermissionDecision ,
46+ type PromptHistoryEntry ,
47+ } from "@/hooks/use-monitoring"
4248import { DEFAULT_RELAY_URL , looksLikeLocalHost , useServerSessions } from "@/hooks/use-server-sessions"
4349import { ensureNotificationPermissions , getDevicePushToken } from "@/notifications/monitoring-notifications"
4450
@@ -728,6 +734,8 @@ export default function DictationScreen() {
728734 const scanLockRef = useRef ( false )
729735 const pairProbeRunRef = useRef ( 0 )
730736 const whisperRestoredRef = useRef ( false )
737+ const promptPagerRef = useRef < FlatList < PromptHistoryEntry | "live" > > ( null )
738+ const promptPagerPageRef = useRef ( - 1 )
731739
732740 const closeDropdown = useCallback ( ( ) => {
733741 setDropdownMode ( "none" )
@@ -766,13 +774,17 @@ export default function DictationScreen() {
766774 activePermissionRequest,
767775 devicePushToken,
768776 latestAssistantContext,
777+ latestPromptText,
769778 latestAssistantResponse,
770779 monitorJob,
771780 monitorStatus,
772781 pendingPermissionCount,
782+ promptHistory,
773783 respondingPermissionID,
774784 respondToPermission,
775785 setDevicePushToken,
786+ setLatestPromptText,
787+ setPromptHistory,
776788 setMonitorStatus,
777789 } = useMonitoring ( {
778790 completePlayer,
@@ -1766,6 +1778,8 @@ export default function DictationScreen() {
17661778 throw new Error ( `Prompt request failed (${ response . status } )` )
17671779 }
17681780
1781+ setLatestPromptText ( text )
1782+
17691783 const nextJob : MonitorJob = {
17701784 id : `job-${ Date . now ( ) } ` ,
17711785 sessionID : session . id ,
@@ -1813,6 +1827,7 @@ export default function DictationScreen() {
18131827 isSending ,
18141828 serversRef ,
18151829 setMonitorStatus ,
1830+ setLatestPromptText ,
18161831 sendOutProgress ,
18171832 sendPlayer ,
18181833 transcribedText ,
@@ -1828,6 +1843,14 @@ export default function DictationScreen() {
18281843 setDropdownMode ( "none" )
18291844 void Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) . catch ( ( ) => { } )
18301845 isHoldingRef . current = true
1846+ // Snap pager to live page (index 0) so user sees their transcription
1847+ if ( promptPagerRef . current ) {
1848+ try {
1849+ promptPagerRef . current . scrollToIndex ( { index : 0 , animated : true } )
1850+ } catch {
1851+ // FlatList may not have items yet
1852+ }
1853+ }
18311854 void startRecording ( )
18321855 } , [ startRecording ] )
18331856
@@ -1910,6 +1933,32 @@ export default function DictationScreen() {
19101933 const isReplyingToActivePermission =
19111934 activePermissionRequest !== null && respondingPermissionID === activePermissionRequest . id
19121935 const displayedTranscript = isSending ? "" : transcribedText
1936+ const [ transcriptionPanelWidth , setTranscriptionPanelWidth ] = useState ( 0 )
1937+ const handleTranscriptionPanelLayout = useCallback ( ( e : LayoutChangeEvent ) => {
1938+ setTranscriptionPanelWidth ( e . nativeEvent . layout . width )
1939+ } , [ ] )
1940+ const pagerPageWidth = transcriptionPanelWidth || 1
1941+
1942+ // Prompt history pager: "live" at index 0 (leftmost), then history newest-first to the right.
1943+ // Swipe right-to-left to browse older prompts, swipe left-to-right to return to live.
1944+ const promptPagerData = useMemo < ( PromptHistoryEntry | "live" ) [ ] > (
1945+ ( ) => ( promptHistory . length > 0 ? [ "live" as const , ...[ ...promptHistory ] . reverse ( ) ] : [ ] ) ,
1946+ [ promptHistory ] ,
1947+ )
1948+ const promptPagerKeyExtractor = useCallback (
1949+ ( item : PromptHistoryEntry | "live" ) => ( item === "live" ? "live" : item . userMessageID ) ,
1950+ [ ] ,
1951+ )
1952+ const handlePromptPagerSnap = useCallback (
1953+ ( e : { nativeEvent : { contentOffset : { x : number } } } ) => {
1954+ const pageIndex = Math . round ( e . nativeEvent . contentOffset . x / pagerPageWidth )
1955+ if ( pageIndex !== promptPagerPageRef . current ) {
1956+ promptPagerPageRef . current = pageIndex
1957+ void Haptics . selectionAsync ( ) . catch ( ( ) => { } )
1958+ }
1959+ } ,
1960+ [ pagerPageWidth ] ,
1961+ )
19131962 const isDropdownOpen = dropdownMode !== "none"
19141963 const effectiveDropdownMode = isDropdownOpen ? dropdownMode : dropdownRenderMode
19151964 const isCreatingSession = sessionCreateMode !== null
@@ -2768,7 +2817,7 @@ export default function DictationScreen() {
27682817 body : "Control only listens while you hold the record button." ,
27692818 primaryLabel : microphonePermissionState === "pending" ? "Requesting microphone access..." : "Continue" ,
27702819 primaryDisabled : microphonePermissionState === "pending" ,
2771- secondaryLabel : "Continue without granting" ,
2820+ secondaryLabel : undefined ,
27722821 visualTag : "MIC" ,
27732822 visualSurfaceStyle : styles . onboardingVisualSurfaceMic ,
27742823 visualOrbStyle : styles . onboardingVisualOrbMic ,
@@ -2779,7 +2828,7 @@ export default function DictationScreen() {
27792828 body : "Get alerts when your OpenCode run finishes, fails, or needs your attention." ,
27802829 primaryLabel : notificationPermissionState === "pending" ? "Requesting notification access..." : "Continue" ,
27812830 primaryDisabled : notificationPermissionState === "pending" ,
2782- secondaryLabel : "Continue without granting" ,
2831+ secondaryLabel : undefined ,
27832832 visualTag : "PUSH" ,
27842833 visualSurfaceStyle : styles . onboardingVisualSurfaceNotifications ,
27852834 visualOrbStyle : styles . onboardingVisualOrbNotifications ,
@@ -2790,7 +2839,7 @@ export default function DictationScreen() {
27902839 body : "This lets Control discover your machine on the same network." ,
27912840 primaryLabel : localNetworkPermissionState === "pending" ? "Requesting local network access..." : "Continue" ,
27922841 primaryDisabled : localNetworkPermissionState === "pending" ,
2793- secondaryLabel : "Continue without granting" ,
2842+ secondaryLabel : undefined ,
27942843 visualTag : "LAN" ,
27952844 visualSurfaceStyle : styles . onboardingVisualSurfaceNetwork ,
27962845 visualOrbStyle : styles . onboardingVisualOrbNetwork ,
@@ -2918,19 +2967,21 @@ export default function DictationScreen() {
29182967 />
29192968 </ Pressable >
29202969
2921- < Pressable
2922- onPress = { ( ) => {
2923- if ( clampedOnboardingStep < onboardingStepCount - 1 ) {
2924- setOnboardingStep ( ( step ) => Math . min ( step + 1 , onboardingStepCount - 1 ) )
2925- return
2926- }
2927-
2928- completeOnboarding ( false )
2929- } }
2930- style = { ( { pressed } ) => [ styles . onboardingSecondaryButton , pressed && styles . clearButtonPressed ] }
2931- >
2932- < Text style = { styles . onboardingSecondaryText } > { onboardingSecondaryLabel } </ Text >
2933- </ Pressable >
2970+ { onboardingSecondaryLabel ? (
2971+ < Pressable
2972+ onPress = { ( ) => {
2973+ if ( clampedOnboardingStep < onboardingStepCount - 1 ) {
2974+ setOnboardingStep ( ( step ) => Math . min ( step + 1 , onboardingStepCount - 1 ) )
2975+ return
2976+ }
2977+
2978+ completeOnboarding ( false )
2979+ } }
2980+ style = { ( { pressed } ) => [ styles . onboardingSecondaryButton , pressed && styles . clearButtonPressed ] }
2981+ >
2982+ < Text style = { styles . onboardingSecondaryText } > { onboardingSecondaryLabel } </ Text >
2983+ </ Pressable >
2984+ ) : null }
29342985 </ View >
29352986 </ View >
29362987 </ SafeAreaView >
@@ -3335,7 +3386,7 @@ export default function DictationScreen() {
33353386 </ ScrollView >
33363387 </ View >
33373388
3338- < View style = { styles . transcriptionPanel } >
3389+ < View style = { styles . transcriptionPanel } onLayout = { handleTranscriptionPanelLayout } >
33393390 < View style = { styles . transcriptionTopActions } pointerEvents = "box-none" >
33403391 < Pressable
33413392 onPress = { handleOpenWhisperSettings }
@@ -3364,20 +3415,66 @@ export default function DictationScreen() {
33643415 </ View >
33653416 ) : null }
33663417
3367- < ScrollView
3368- ref = { scrollViewRef }
3369- style = { styles . transcriptionScroll }
3370- contentContainerStyle = { styles . transcriptionContent }
3371- onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3372- >
3373- < Animated . View style = { animatedTranscriptSendStyle } >
3374- { displayedTranscript ? (
3375- < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3376- ) : isSending ? null : (
3377- < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3378- ) }
3379- </ Animated . View >
3380- </ ScrollView >
3418+ { promptPagerData . length > 1 ? (
3419+ < FlatList
3420+ ref = { promptPagerRef }
3421+ data = { promptPagerData }
3422+ keyExtractor = { promptPagerKeyExtractor }
3423+ horizontal
3424+ pagingEnabled
3425+ bounces = { false }
3426+ showsHorizontalScrollIndicator = { false }
3427+ onMomentumScrollEnd = { handlePromptPagerSnap }
3428+ initialScrollIndex = { 0 }
3429+ getItemLayout = { ( _data , index ) => ( {
3430+ length : pagerPageWidth ,
3431+ offset : pagerPageWidth * index ,
3432+ index,
3433+ } ) }
3434+ style = { styles . transcriptionScroll }
3435+ renderItem = { ( { item } ) =>
3436+ item === "live" ? (
3437+ < ScrollView
3438+ ref = { scrollViewRef }
3439+ style = { { width : pagerPageWidth } }
3440+ contentContainerStyle = { styles . transcriptionContent }
3441+ onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3442+ >
3443+ < Animated . View style = { animatedTranscriptSendStyle } >
3444+ { displayedTranscript ? (
3445+ < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3446+ ) : isSending ? null : (
3447+ < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3448+ ) }
3449+ </ Animated . View >
3450+ </ ScrollView >
3451+ ) : (
3452+ < ScrollView
3453+ style = { { width : pagerPageWidth } }
3454+ contentContainerStyle = { styles . transcriptionContent }
3455+ >
3456+ < Text style = { styles . promptHistoryLabel } > Previous prompt</ Text >
3457+ < Text style = { styles . promptHistoryText } > { item . promptText } </ Text >
3458+ </ ScrollView >
3459+ )
3460+ }
3461+ />
3462+ ) : (
3463+ < ScrollView
3464+ ref = { scrollViewRef }
3465+ style = { styles . transcriptionScroll }
3466+ contentContainerStyle = { styles . transcriptionContent }
3467+ onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3468+ >
3469+ < Animated . View style = { animatedTranscriptSendStyle } >
3470+ { displayedTranscript ? (
3471+ < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3472+ ) : isSending ? null : (
3473+ < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3474+ ) }
3475+ </ Animated . View >
3476+ </ ScrollView >
3477+ ) }
33813478
33823479 < Animated . View
33833480 style = { [ styles . waveformBoxesRow , animatedWaveformRowStyle ] }
@@ -3451,7 +3548,7 @@ export default function DictationScreen() {
34513548 ) : null }
34523549 </ >
34533550 ) : (
3454- < View style = { styles . transcriptionPanel } >
3551+ < View style = { styles . transcriptionPanel } onLayout = { handleTranscriptionPanelLayout } >
34553552 < View style = { styles . transcriptionTopActions } pointerEvents = "box-none" >
34563553 < Pressable
34573554 onPress = { handleOpenWhisperSettings }
@@ -3480,20 +3577,59 @@ export default function DictationScreen() {
34803577 </ View >
34813578 ) : null }
34823579
3483- < ScrollView
3484- ref = { scrollViewRef }
3485- style = { styles . transcriptionScroll }
3486- contentContainerStyle = { styles . transcriptionContent }
3487- onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3488- >
3489- < Animated . View style = { animatedTranscriptSendStyle } >
3490- { displayedTranscript ? (
3491- < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3492- ) : isSending ? null : (
3493- < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3494- ) }
3495- </ Animated . View >
3496- </ ScrollView >
3580+ { promptPagerData . length > 1 ? (
3581+ < FlatList
3582+ ref = { promptPagerRef }
3583+ data = { promptPagerData }
3584+ keyExtractor = { promptPagerKeyExtractor }
3585+ horizontal
3586+ pagingEnabled
3587+ bounces = { false }
3588+ showsHorizontalScrollIndicator = { false }
3589+ onMomentumScrollEnd = { handlePromptPagerSnap }
3590+ initialScrollIndex = { 0 }
3591+ getItemLayout = { ( _data , index ) => ( { length : pagerPageWidth , offset : pagerPageWidth * index , index } ) }
3592+ style = { styles . transcriptionScroll }
3593+ renderItem = { ( { item } ) =>
3594+ item === "live" ? (
3595+ < ScrollView
3596+ ref = { scrollViewRef }
3597+ style = { { width : pagerPageWidth } }
3598+ contentContainerStyle = { styles . transcriptionContent }
3599+ onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3600+ >
3601+ < Animated . View style = { animatedTranscriptSendStyle } >
3602+ { displayedTranscript ? (
3603+ < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3604+ ) : isSending ? null : (
3605+ < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3606+ ) }
3607+ </ Animated . View >
3608+ </ ScrollView >
3609+ ) : (
3610+ < ScrollView style = { { width : pagerPageWidth } } contentContainerStyle = { styles . transcriptionContent } >
3611+ < Text style = { styles . promptHistoryLabel } > Previous prompt</ Text >
3612+ < Text style = { styles . promptHistoryText } > { item . promptText } </ Text >
3613+ </ ScrollView >
3614+ )
3615+ }
3616+ />
3617+ ) : (
3618+ < ScrollView
3619+ ref = { scrollViewRef }
3620+ style = { styles . transcriptionScroll }
3621+ contentContainerStyle = { styles . transcriptionContent }
3622+ onContentSizeChange = { ( ) => scrollViewRef . current ?. scrollToEnd ( { animated : true } ) }
3623+ >
3624+ < Animated . View style = { animatedTranscriptSendStyle } >
3625+ { displayedTranscript ? (
3626+ < Text style = { styles . transcriptionText } > { displayedTranscript } </ Text >
3627+ ) : isSending ? null : (
3628+ < Text style = { styles . placeholderText } > Your transcription will appear here…</ Text >
3629+ ) }
3630+ </ Animated . View >
3631+ </ ScrollView >
3632+ ) }
34973633
34983634 < Animated . View
34993635 style = { [ styles . waveformBoxesRow , animatedWaveformRowStyle ] }
@@ -4784,8 +4920,23 @@ const styles = StyleSheet.create({
47844920 right : 10 ,
47854921 zIndex : 4 ,
47864922 flexDirection : "row" ,
4923+ alignItems : "flex-start" ,
47874924 justifyContent : "space-between" ,
47884925 } ,
4926+ promptHistoryLabel : {
4927+ color : "#6B7A99" ,
4928+ fontSize : 13 ,
4929+ fontWeight : "700" ,
4930+ letterSpacing : 0.6 ,
4931+ textTransform : "uppercase" ,
4932+ marginBottom : 8 ,
4933+ } ,
4934+ promptHistoryText : {
4935+ fontSize : 24 ,
4936+ fontWeight : "500" ,
4937+ lineHeight : 34 ,
4938+ color : "#8B96AD" ,
4939+ } ,
47894940 modelErrorBadge : {
47904941 alignSelf : "flex-start" ,
47914942 marginLeft : 14 ,
0 commit comments