mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-21 11:26:39 +00:00
feat(mobile-voice): add swipeable prompt history pager in transcription panel
Swipe right-to-left on the transcription area to browse previous prompts sent in the current session. Includes haptic feedback on page snap, auto-snap to live page on record start, and layout-measured page width for correct alignment.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
View,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
FlatList,
|
||||
Modal,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
@@ -38,7 +39,12 @@ import { fetch as expoFetch } from "expo/fetch"
|
||||
import { buildPermissionCardModel } from "@/lib/pending-permissions"
|
||||
import { unregisterRelayDevice } from "@/lib/relay-client"
|
||||
import { useMdnsDiscovery } from "@/hooks/use-mdns-discovery"
|
||||
import { useMonitoring, type MonitorJob, type PermissionDecision } from "@/hooks/use-monitoring"
|
||||
import {
|
||||
useMonitoring,
|
||||
type MonitorJob,
|
||||
type PermissionDecision,
|
||||
type PromptHistoryEntry,
|
||||
} from "@/hooks/use-monitoring"
|
||||
import { DEFAULT_RELAY_URL, looksLikeLocalHost, useServerSessions } from "@/hooks/use-server-sessions"
|
||||
import { ensureNotificationPermissions, getDevicePushToken } from "@/notifications/monitoring-notifications"
|
||||
|
||||
@@ -728,6 +734,8 @@ export default function DictationScreen() {
|
||||
const scanLockRef = useRef(false)
|
||||
const pairProbeRunRef = useRef(0)
|
||||
const whisperRestoredRef = useRef(false)
|
||||
const promptPagerRef = useRef<FlatList<PromptHistoryEntry | "live">>(null)
|
||||
const promptPagerPageRef = useRef(-1)
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
setDropdownMode("none")
|
||||
@@ -766,13 +774,17 @@ export default function DictationScreen() {
|
||||
activePermissionRequest,
|
||||
devicePushToken,
|
||||
latestAssistantContext,
|
||||
latestPromptText,
|
||||
latestAssistantResponse,
|
||||
monitorJob,
|
||||
monitorStatus,
|
||||
pendingPermissionCount,
|
||||
promptHistory,
|
||||
respondingPermissionID,
|
||||
respondToPermission,
|
||||
setDevicePushToken,
|
||||
setLatestPromptText,
|
||||
setPromptHistory,
|
||||
setMonitorStatus,
|
||||
} = useMonitoring({
|
||||
completePlayer,
|
||||
@@ -1766,6 +1778,8 @@ export default function DictationScreen() {
|
||||
throw new Error(`Prompt request failed (${response.status})`)
|
||||
}
|
||||
|
||||
setLatestPromptText(text)
|
||||
|
||||
const nextJob: MonitorJob = {
|
||||
id: `job-${Date.now()}`,
|
||||
sessionID: session.id,
|
||||
@@ -1813,6 +1827,7 @@ export default function DictationScreen() {
|
||||
isSending,
|
||||
serversRef,
|
||||
setMonitorStatus,
|
||||
setLatestPromptText,
|
||||
sendOutProgress,
|
||||
sendPlayer,
|
||||
transcribedText,
|
||||
@@ -1828,6 +1843,14 @@ export default function DictationScreen() {
|
||||
setDropdownMode("none")
|
||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light).catch(() => {})
|
||||
isHoldingRef.current = true
|
||||
// Snap pager to live page (index 0) so user sees their transcription
|
||||
if (promptPagerRef.current) {
|
||||
try {
|
||||
promptPagerRef.current.scrollToIndex({ index: 0, animated: true })
|
||||
} catch {
|
||||
// FlatList may not have items yet
|
||||
}
|
||||
}
|
||||
void startRecording()
|
||||
}, [startRecording])
|
||||
|
||||
@@ -1910,6 +1933,32 @@ export default function DictationScreen() {
|
||||
const isReplyingToActivePermission =
|
||||
activePermissionRequest !== null && respondingPermissionID === activePermissionRequest.id
|
||||
const displayedTranscript = isSending ? "" : transcribedText
|
||||
const [transcriptionPanelWidth, setTranscriptionPanelWidth] = useState(0)
|
||||
const handleTranscriptionPanelLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
setTranscriptionPanelWidth(e.nativeEvent.layout.width)
|
||||
}, [])
|
||||
const pagerPageWidth = transcriptionPanelWidth || 1
|
||||
|
||||
// Prompt history pager: "live" at index 0 (leftmost), then history newest-first to the right.
|
||||
// Swipe right-to-left to browse older prompts, swipe left-to-right to return to live.
|
||||
const promptPagerData = useMemo<(PromptHistoryEntry | "live")[]>(
|
||||
() => (promptHistory.length > 0 ? ["live" as const, ...[...promptHistory].reverse()] : []),
|
||||
[promptHistory],
|
||||
)
|
||||
const promptPagerKeyExtractor = useCallback(
|
||||
(item: PromptHistoryEntry | "live") => (item === "live" ? "live" : item.userMessageID),
|
||||
[],
|
||||
)
|
||||
const handlePromptPagerSnap = useCallback(
|
||||
(e: { nativeEvent: { contentOffset: { x: number } } }) => {
|
||||
const pageIndex = Math.round(e.nativeEvent.contentOffset.x / pagerPageWidth)
|
||||
if (pageIndex !== promptPagerPageRef.current) {
|
||||
promptPagerPageRef.current = pageIndex
|
||||
void Haptics.selectionAsync().catch(() => {})
|
||||
}
|
||||
},
|
||||
[pagerPageWidth],
|
||||
)
|
||||
const isDropdownOpen = dropdownMode !== "none"
|
||||
const effectiveDropdownMode = isDropdownOpen ? dropdownMode : dropdownRenderMode
|
||||
const isCreatingSession = sessionCreateMode !== null
|
||||
@@ -2768,7 +2817,7 @@ export default function DictationScreen() {
|
||||
body: "Control only listens while you hold the record button.",
|
||||
primaryLabel: microphonePermissionState === "pending" ? "Requesting microphone access..." : "Continue",
|
||||
primaryDisabled: microphonePermissionState === "pending",
|
||||
secondaryLabel: "Continue without granting",
|
||||
secondaryLabel: undefined,
|
||||
visualTag: "MIC",
|
||||
visualSurfaceStyle: styles.onboardingVisualSurfaceMic,
|
||||
visualOrbStyle: styles.onboardingVisualOrbMic,
|
||||
@@ -2779,7 +2828,7 @@ export default function DictationScreen() {
|
||||
body: "Get alerts when your OpenCode run finishes, fails, or needs your attention.",
|
||||
primaryLabel: notificationPermissionState === "pending" ? "Requesting notification access..." : "Continue",
|
||||
primaryDisabled: notificationPermissionState === "pending",
|
||||
secondaryLabel: "Continue without granting",
|
||||
secondaryLabel: undefined,
|
||||
visualTag: "PUSH",
|
||||
visualSurfaceStyle: styles.onboardingVisualSurfaceNotifications,
|
||||
visualOrbStyle: styles.onboardingVisualOrbNotifications,
|
||||
@@ -2790,7 +2839,7 @@ export default function DictationScreen() {
|
||||
body: "This lets Control discover your machine on the same network.",
|
||||
primaryLabel: localNetworkPermissionState === "pending" ? "Requesting local network access..." : "Continue",
|
||||
primaryDisabled: localNetworkPermissionState === "pending",
|
||||
secondaryLabel: "Continue without granting",
|
||||
secondaryLabel: undefined,
|
||||
visualTag: "LAN",
|
||||
visualSurfaceStyle: styles.onboardingVisualSurfaceNetwork,
|
||||
visualOrbStyle: styles.onboardingVisualOrbNetwork,
|
||||
@@ -2918,19 +2967,21 @@ export default function DictationScreen() {
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (clampedOnboardingStep < onboardingStepCount - 1) {
|
||||
setOnboardingStep((step) => Math.min(step + 1, onboardingStepCount - 1))
|
||||
return
|
||||
}
|
||||
{onboardingSecondaryLabel ? (
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
if (clampedOnboardingStep < onboardingStepCount - 1) {
|
||||
setOnboardingStep((step) => Math.min(step + 1, onboardingStepCount - 1))
|
||||
return
|
||||
}
|
||||
|
||||
completeOnboarding(false)
|
||||
}}
|
||||
style={({ pressed }) => [styles.onboardingSecondaryButton, pressed && styles.clearButtonPressed]}
|
||||
>
|
||||
<Text style={styles.onboardingSecondaryText}>{onboardingSecondaryLabel}</Text>
|
||||
</Pressable>
|
||||
completeOnboarding(false)
|
||||
}}
|
||||
style={({ pressed }) => [styles.onboardingSecondaryButton, pressed && styles.clearButtonPressed]}
|
||||
>
|
||||
<Text style={styles.onboardingSecondaryText}>{onboardingSecondaryLabel}</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
@@ -3335,7 +3386,7 @@ export default function DictationScreen() {
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
<View style={styles.transcriptionPanel}>
|
||||
<View style={styles.transcriptionPanel} onLayout={handleTranscriptionPanelLayout}>
|
||||
<View style={styles.transcriptionTopActions} pointerEvents="box-none">
|
||||
<Pressable
|
||||
onPress={handleOpenWhisperSettings}
|
||||
@@ -3364,20 +3415,66 @@ export default function DictationScreen() {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.transcriptionScroll}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
{promptPagerData.length > 1 ? (
|
||||
<FlatList
|
||||
ref={promptPagerRef}
|
||||
data={promptPagerData}
|
||||
keyExtractor={promptPagerKeyExtractor}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
bounces={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={handlePromptPagerSnap}
|
||||
initialScrollIndex={0}
|
||||
getItemLayout={(_data, index) => ({
|
||||
length: pagerPageWidth,
|
||||
offset: pagerPageWidth * index,
|
||||
index,
|
||||
})}
|
||||
style={styles.transcriptionScroll}
|
||||
renderItem={({ item }) =>
|
||||
item === "live" ? (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={{ width: pagerPageWidth }}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={{ width: pagerPageWidth }}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
>
|
||||
<Text style={styles.promptHistoryLabel}>Previous prompt</Text>
|
||||
<Text style={styles.promptHistoryText}>{item.promptText}</Text>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.transcriptionScroll}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
style={[styles.waveformBoxesRow, animatedWaveformRowStyle]}
|
||||
@@ -3451,7 +3548,7 @@ export default function DictationScreen() {
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<View style={styles.transcriptionPanel}>
|
||||
<View style={styles.transcriptionPanel} onLayout={handleTranscriptionPanelLayout}>
|
||||
<View style={styles.transcriptionTopActions} pointerEvents="box-none">
|
||||
<Pressable
|
||||
onPress={handleOpenWhisperSettings}
|
||||
@@ -3480,20 +3577,59 @@ export default function DictationScreen() {
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.transcriptionScroll}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
{promptPagerData.length > 1 ? (
|
||||
<FlatList
|
||||
ref={promptPagerRef}
|
||||
data={promptPagerData}
|
||||
keyExtractor={promptPagerKeyExtractor}
|
||||
horizontal
|
||||
pagingEnabled
|
||||
bounces={false}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
onMomentumScrollEnd={handlePromptPagerSnap}
|
||||
initialScrollIndex={0}
|
||||
getItemLayout={(_data, index) => ({ length: pagerPageWidth, offset: pagerPageWidth * index, index })}
|
||||
style={styles.transcriptionScroll}
|
||||
renderItem={({ item }) =>
|
||||
item === "live" ? (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={{ width: pagerPageWidth }}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<ScrollView style={{ width: pagerPageWidth }} contentContainerStyle={styles.transcriptionContent}>
|
||||
<Text style={styles.promptHistoryLabel}>Previous prompt</Text>
|
||||
<Text style={styles.promptHistoryText}>{item.promptText}</Text>
|
||||
</ScrollView>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
style={styles.transcriptionScroll}
|
||||
contentContainerStyle={styles.transcriptionContent}
|
||||
onContentSizeChange={() => scrollViewRef.current?.scrollToEnd({ animated: true })}
|
||||
>
|
||||
<Animated.View style={animatedTranscriptSendStyle}>
|
||||
{displayedTranscript ? (
|
||||
<Text style={styles.transcriptionText}>{displayedTranscript}</Text>
|
||||
) : isSending ? null : (
|
||||
<Text style={styles.placeholderText}>Your transcription will appear here…</Text>
|
||||
)}
|
||||
</Animated.View>
|
||||
</ScrollView>
|
||||
)}
|
||||
|
||||
<Animated.View
|
||||
style={[styles.waveformBoxesRow, animatedWaveformRowStyle]}
|
||||
@@ -4784,8 +4920,23 @@ const styles = StyleSheet.create({
|
||||
right: 10,
|
||||
zIndex: 4,
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
promptHistoryLabel: {
|
||||
color: "#6B7A99",
|
||||
fontSize: 13,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 0.6,
|
||||
textTransform: "uppercase",
|
||||
marginBottom: 8,
|
||||
},
|
||||
promptHistoryText: {
|
||||
fontSize: 24,
|
||||
fontWeight: "500",
|
||||
lineHeight: 34,
|
||||
color: "#8B96AD",
|
||||
},
|
||||
modelErrorBadge: {
|
||||
alignSelf: "flex-start",
|
||||
marginLeft: 14,
|
||||
|
||||
@@ -40,6 +40,11 @@ export type MonitorJob = {
|
||||
|
||||
export type PermissionDecision = "once" | "always" | "reject"
|
||||
|
||||
export type PromptHistoryEntry = {
|
||||
promptText: string
|
||||
userMessageID: string
|
||||
}
|
||||
|
||||
type SessionRuntimeStatus = "idle" | "busy" | "retry"
|
||||
|
||||
type PermissionPromptState = "idle" | "pending" | "granted" | "denied"
|
||||
@@ -121,6 +126,8 @@ export function useMonitoring({
|
||||
const [monitorJob, setMonitorJob] = useState<MonitorJob | null>(null)
|
||||
const [monitorStatus, setMonitorStatus] = useState("")
|
||||
const [latestAssistantResponse, setLatestAssistantResponse] = useState("")
|
||||
const [latestPromptText, setLatestPromptText] = useState("")
|
||||
const [promptHistory, setPromptHistory] = useState<PromptHistoryEntry[]>([])
|
||||
const [latestAssistantContext, setLatestAssistantContext] = useState<LatestAssistantContext | null>(null)
|
||||
const [pendingPermissions, setPendingPermissions] = useState<PendingPermissionRequest[]>([])
|
||||
const [replyingPermissionID, setReplyingPermissionID] = useState<string | null>(null)
|
||||
@@ -250,10 +257,14 @@ export function useMonitoring({
|
||||
|
||||
const payload = (await response.json()) as unknown
|
||||
const latest = findLatestAssistantCompletion(payload)
|
||||
const promptText = findLatestUserPrompt(payload)
|
||||
const history = buildPromptHistory(payload)
|
||||
|
||||
if (latestAssistantRequestRef.current !== requestID) return
|
||||
if (activeSessionIdRef.current !== sessionID) return
|
||||
setLatestAssistantResponse(latest.text)
|
||||
setLatestPromptText(promptText)
|
||||
setPromptHistory(history)
|
||||
setLatestAssistantContext(latest.context)
|
||||
if (latest.text) {
|
||||
setAgentStateDismissed(false)
|
||||
@@ -262,6 +273,8 @@ export function useMonitoring({
|
||||
if (latestAssistantRequestRef.current !== requestID) return
|
||||
if (activeSessionIdRef.current !== sessionID) return
|
||||
setLatestAssistantResponse("")
|
||||
setLatestPromptText("")
|
||||
setPromptHistory([])
|
||||
setLatestAssistantContext(null)
|
||||
}
|
||||
},
|
||||
@@ -446,6 +459,8 @@ export function useMonitoring({
|
||||
|
||||
useEffect(() => {
|
||||
setLatestAssistantResponse("")
|
||||
setLatestPromptText("")
|
||||
setPromptHistory([])
|
||||
setLatestAssistantContext(null)
|
||||
setPendingPermissions([])
|
||||
setAgentStateDismissed(false)
|
||||
@@ -790,6 +805,10 @@ export function useMonitoring({
|
||||
monitorJob,
|
||||
monitorStatus,
|
||||
setMonitorStatus,
|
||||
latestPromptText,
|
||||
setLatestPromptText,
|
||||
promptHistory,
|
||||
setPromptHistory,
|
||||
latestAssistantResponse,
|
||||
latestAssistantContext,
|
||||
activePermissionRequest,
|
||||
@@ -839,12 +858,76 @@ function cleanSessionText(text: string): string {
|
||||
return cleanTranscriptText(text).trimStart()
|
||||
}
|
||||
|
||||
function extractMessageText(parts: SessionMessagePart[]): string {
|
||||
const textParts: string[] = []
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part || part.type !== "text" || typeof part.text !== "string") continue
|
||||
|
||||
const text = cleanSessionText(part.text)
|
||||
if (text.length > 0) {
|
||||
textParts.push(text)
|
||||
}
|
||||
}
|
||||
|
||||
return textParts.join("\n\n")
|
||||
}
|
||||
|
||||
function maybeString(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > 0 ? trimmed : null
|
||||
}
|
||||
|
||||
function buildPromptHistory(payload: unknown): PromptHistoryEntry[] {
|
||||
if (!Array.isArray(payload)) return []
|
||||
|
||||
const entries: PromptHistoryEntry[] = []
|
||||
|
||||
for (const candidate of payload) {
|
||||
const msg = candidate as SessionMessagePayload
|
||||
if (!msg || typeof msg !== "object") continue
|
||||
|
||||
const info = msg.info as SessionMessageInfo
|
||||
if (!info || typeof info !== "object") continue
|
||||
if (info.role !== "user") continue
|
||||
|
||||
const id = (info as { id?: unknown }).id
|
||||
if (typeof id !== "string") continue
|
||||
|
||||
const parts = Array.isArray(msg.parts) ? (msg.parts as SessionMessagePart[]) : []
|
||||
const text = extractMessageText(parts)
|
||||
if (text.length === 0) continue
|
||||
|
||||
entries.push({ promptText: text, userMessageID: id })
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
function findLatestUserPrompt(payload: unknown): string {
|
||||
if (!Array.isArray(payload)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
for (let index = payload.length - 1; index >= 0; index -= 1) {
|
||||
const candidate = payload[index] as SessionMessagePayload
|
||||
if (!candidate || typeof candidate !== "object") continue
|
||||
|
||||
const info = candidate.info as SessionMessageInfo
|
||||
if (!info || typeof info !== "object") continue
|
||||
if (info.role !== "user") continue
|
||||
|
||||
const parts = Array.isArray(candidate.parts) ? (candidate.parts as SessionMessagePart[]) : []
|
||||
const text = extractMessageText(parts)
|
||||
if (text.length > 0) {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function extractAssistantContext(info: SessionMessageInfo): LatestAssistantContext | null {
|
||||
const providerID = maybeString(info.providerID)
|
||||
const modelID = maybeString(info.modelID)
|
||||
@@ -887,11 +970,7 @@ function findLatestAssistantCompletion(payload: unknown): LatestAssistantSnapsho
|
||||
const context = extractAssistantContext(info)
|
||||
|
||||
const parts = Array.isArray(candidate.parts) ? (candidate.parts as SessionMessagePart[]) : []
|
||||
const text = parts
|
||||
.filter((part) => part && part.type === "text" && typeof part.text === "string")
|
||||
.map((part) => cleanSessionText(part.text as string))
|
||||
.filter((part) => part.length > 0)
|
||||
.join("\n\n")
|
||||
const text = extractMessageText(parts)
|
||||
|
||||
if (text.length > 0 || context) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user