From 42fb56890b00cfce4f8e64de9bde1bea53b21dec Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 21 Jan 2026 15:13:34 +0700 Subject: [PATCH] Match thinking badge behavior to tool badge --- .../app/src/components/agent-stream-view.tsx | 113 +++++------ .../src/components/agent-thought-content.tsx | 95 +++++++++ packages/app/src/components/message.tsx | 168 ++-------------- .../components/orchestrator-messages-view.tsx | 151 ++++++++------- .../app/src/components/thinking-sheet.tsx | 182 ++++++++++++++++++ 5 files changed, 431 insertions(+), 278 deletions(-) create mode 100644 packages/app/src/components/agent-thought-content.tsx create mode 100644 packages/app/src/components/thinking-sheet.tsx diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 83526b410..c7136e166 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -49,6 +49,7 @@ import type { DaemonClientV2 } from "@server/client/daemon-client-v2"; import { parseToolCallDisplay } from "@/utils/tool-call-parsers"; import { ToolCallDetailsContent } from "./tool-call-details"; import { ToolCallSheetProvider } from "./tool-call-sheet"; +import { ThinkingSheetProvider } from "./thinking-sheet"; import { createMarkdownStyles } from "@/styles/markdown-styles"; import { MAX_CONTENT_WIDTH } from "@/constants/layout"; import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf"; @@ -532,62 +533,64 @@ export function AgentStreamView({ return ( - - - item.id} - contentContainerStyle={{ - paddingVertical: 0, - flexGrow: 1, - }} - style={stylesheet.list} - onScroll={handleScroll} - scrollEventThrottle={16} - ListEmptyComponent={ - - - Start chatting with this agent... - - - } - ListHeaderComponent={listHeaderComponent} - extraData={flatListExtraData} - maintainVisibleContentPosition={ - // Disable when streaming and user is at bottom - we handle auto-scroll ourselves - agent.status === "running" && isNearBottom - ? undefined - : { minIndexForVisible: 0, autoscrollToTopThreshold: 40 } - } - initialNumToRender={12} - windowSize={10} - inverted - /> - + + + + item.id} + contentContainerStyle={{ + paddingVertical: 0, + flexGrow: 1, + }} + style={stylesheet.list} + onScroll={handleScroll} + scrollEventThrottle={16} + ListEmptyComponent={ + + + Start chatting with this agent... + + + } + ListHeaderComponent={listHeaderComponent} + extraData={flatListExtraData} + maintainVisibleContentPosition={ + // Disable when streaming and user is at bottom - we handle auto-scroll ourselves + agent.status === "running" && isNearBottom + ? undefined + : { minIndexForVisible: 0, autoscrollToTopThreshold: 40 } + } + initialNumToRender={12} + windowSize={10} + inverted + /> + - {/* Scroll to bottom button */} - {!isNearBottom && ( - - - - - - - - )} - + {/* Scroll to bottom button */} + {!isNearBottom && ( + + + + + + + + )} + + ); } diff --git a/packages/app/src/components/agent-thought-content.tsx b/packages/app/src/components/agent-thought-content.tsx new file mode 100644 index 000000000..1515b548d --- /dev/null +++ b/packages/app/src/components/agent-thought-content.tsx @@ -0,0 +1,95 @@ +import { memo, useMemo } from "react"; +import { Text, View } from "react-native"; +import Markdown from "react-native-markdown-display"; +import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { createCompactMarkdownStyles } from "@/styles/markdown-styles"; + +export const AgentThoughtContent = memo(function AgentThoughtContent({ + message, +}: { + message: string; +}) { + const { theme } = useUnistyles(); + const markdownContent = useMemo(() => message?.trim() ?? "", [message]); + const markdownStyles = useMemo( + () => createCompactMarkdownStyles(theme), + [theme] + ); + + const markdownRules = useMemo(() => { + return { + text: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => ( + + {node.content} + + ), + textgroup: (node: any, children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => ( + + {children} + + ), + code_block: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => ( + + {node.content} + + ), + fence: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => ( + + {node.content} + + ), + code_inline: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => ( + + {node.content} + + ), + bullet_list: (node: any, children: any[], _parent: any, styles: any) => ( + + {children} + + ), + ordered_list: (node: any, children: any[], _parent: any, styles: any) => ( + + {children} + + ), + list_item: (node: any, children: any[], parent: any, styles: any) => { + const isOrdered = parent?.type === "ordered_list"; + const index = parent?.children?.indexOf(node) ?? 0; + const bullet = isOrdered ? `${index + 1}.` : "•"; + const iconStyle = isOrdered ? styles.ordered_list_icon : styles.bullet_list_icon; + const contentStyle = isOrdered + ? styles.ordered_list_content + : styles.bullet_list_content; + + return ( + + {bullet} + + {children} + + + ); + }, + }; + }, []); + + if (!markdownContent) { + return No captured thinking; + } + + return ( + + {markdownContent} + + ); +}); + +const styles = StyleSheet.create((theme) => ({ + emptyText: { + color: theme.colors.foregroundMuted, + fontSize: theme.fontSize.xs, + fontStyle: "italic" as const, + }, +})); + diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 162662566..537577f25 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -48,7 +48,6 @@ import { import { baseColors, theme } from "@/styles/theme"; import { createMarkdownStyles, - createCompactMarkdownStyles, } from "@/styles/markdown-styles"; import { Colors, Fonts } from "@/constants/theme"; import * as Clipboard from "expo-clipboard"; @@ -57,6 +56,8 @@ import { extractPrincipalParam } from "@/utils/tool-call-parsers"; import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf"; import { resolveToolCallPreview } from "./tool-call-preview"; import { useToolCallSheet } from "./tool-call-sheet"; +import { useThinkingSheet } from "./thinking-sheet"; +import { AgentThoughtContent } from "./agent-thought-content"; import { ToolCallDetailsContent, useToolCallDetails, @@ -1194,172 +1195,41 @@ const ExpandableBadge = memo(function ExpandableBadge({ ); }); -const agentThoughtStylesheet = StyleSheet.create((theme) => ({ - emptyText: { - color: theme.colors.foregroundMuted, - fontSize: theme.fontSize.xs, - fontStyle: "italic" as const, - }, -})); - export const AgentThoughtMessage = memo(function AgentThoughtMessage({ message, status = "ready", isLastInSequence = false, disableOuterSpacing, }: AgentThoughtMessageProps) { - const { theme } = useUnistyles(); + const { openThinking } = useThinkingSheet(); const [isExpanded, setIsExpanded] = useState(false); - const markdownContent = useMemo(() => message?.trim() ?? "", [message]); - const markdownStyles = useMemo( - () => createCompactMarkdownStyles(theme), - [theme] - ); - const toggle = useCallback(() => { + const isMobile = + UnistylesRuntime.breakpoint === "xs" || + UnistylesRuntime.breakpoint === "sm"; + + const handleToggle = useCallback(() => { + if (isMobile) { + openThinking({ message, status }); + return; + } setIsExpanded((prev) => !prev); - }, []); - - const markdownRules = useMemo(() => { - return { - text: ( - node: any, - _children: ReactNode[], - _parent: any, - styles: any, - inheritedStyles: any = {} - ) => ( - - {node.content} - - ), - textgroup: ( - node: any, - children: ReactNode[], - _parent: any, - styles: any, - inheritedStyles: any = {} - ) => ( - - {children} - - ), - code_block: ( - node: any, - _children: ReactNode[], - _parent: any, - styles: any, - inheritedStyles: any = {} - ) => ( - - {node.content} - - ), - fence: ( - node: any, - _children: ReactNode[], - _parent: any, - styles: any, - inheritedStyles: any = {} - ) => ( - - {node.content} - - ), - code_inline: ( - node: any, - _children: ReactNode[], - _parent: any, - styles: any, - inheritedStyles: any = {} - ) => ( - - {node.content} - - ), - bullet_list: ( - node: any, - children: ReactNode[], - _parent: any, - styles: any - ) => ( - - {children} - - ), - ordered_list: ( - node: any, - children: ReactNode[], - _parent: any, - styles: any - ) => ( - - {children} - - ), - list_item: ( - node: any, - children: ReactNode[], - parent: any, - styles: any - ) => { - const isOrdered = parent?.type === "ordered_list"; - const index = parent?.children?.indexOf(node) ?? 0; - const bullet = isOrdered ? `${index + 1}.` : "•"; - const iconStyle = isOrdered - ? styles.ordered_list_icon - : styles.bullet_list_icon; - const contentStyle = isOrdered - ? styles.ordered_list_content - : styles.bullet_list_content; - - return ( - - {bullet} - - {children} - - - ); - }, - }; - }, []); + }, [isMobile, openThinking, message, status]); const renderDetails = useCallback(() => { - if (!markdownContent) { - return ( - - No captured thinking - - ); - } - return ( - - {markdownContent} - - ); - }, [markdownContent, markdownRules, markdownStyles]); + return ; + }, [message]); return ( null : renderDetails} isLoading={status !== "ready"} isLastInSequence={isLastInSequence} + style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }} disableOuterSpacing={disableOuterSpacing} /> ); diff --git a/packages/app/src/components/orchestrator-messages-view.tsx b/packages/app/src/components/orchestrator-messages-view.tsx index 0fadaa911..c3598f389 100644 --- a/packages/app/src/components/orchestrator-messages-view.tsx +++ b/packages/app/src/components/orchestrator-messages-view.tsx @@ -8,6 +8,7 @@ import { ToolCall, } from "@/components/message"; import { ToolCallSheetProvider } from "@/components/tool-call-sheet"; +import { ThinkingSheetProvider } from "@/components/thinking-sheet"; import type { MessageEntry } from "@/contexts/session-context"; interface OrchestratorMessagesViewProps { @@ -21,86 +22,88 @@ export const OrchestratorMessagesView = forwardRef - - {messages.map((msg) => { - if (msg.type === "user") { - return ( - - ); - } + + + {messages.map((msg) => { + if (msg.type === "user") { + return ( + + ); + } - if (msg.type === "assistant") { - return ( - - ); - } + if (msg.type === "assistant") { + return ( + + ); + } - if (msg.type === "activity") { - return ( - - ); - } + if (msg.type === "activity") { + return ( + + ); + } - if (msg.type === "artifact") { - return ( - - ); - } + if (msg.type === "artifact") { + return ( + + ); + } - if (msg.type === "tool_call") { - return ( - - ); - } + if (msg.type === "tool_call") { + return ( + + ); + } - return null; - })} + return null; + })} - {/* Streaming assistant message */} - {currentAssistantMessage && ( - - )} - + {/* Streaming assistant message */} + {currentAssistantMessage && ( + + )} + + ); } diff --git a/packages/app/src/components/thinking-sheet.tsx b/packages/app/src/components/thinking-sheet.tsx new file mode 100644 index 000000000..d61646464 --- /dev/null +++ b/packages/app/src/components/thinking-sheet.tsx @@ -0,0 +1,182 @@ +import React, { + createContext, + useCallback, + useContext, + useMemo, + useRef, + ReactNode, +} from "react"; +import { View, Text, Pressable } from "react-native"; +import { StyleSheet } from "react-native-unistyles"; +import { + BottomSheetModal, + BottomSheetScrollView, + BottomSheetBackdrop, +} from "@gorhom/bottom-sheet"; +import { Brain, X } from "lucide-react-native"; +import type { ThoughtStatus } from "@/types/stream"; +import { AgentThoughtContent } from "./agent-thought-content"; + +export interface ThinkingSheetData { + message: string; + status?: ThoughtStatus; +} + +interface ThinkingSheetContextValue { + openThinking: (data: ThinkingSheetData) => void; + closeThinking: () => void; +} + +const ThinkingSheetContext = createContext(null); + +export function useThinkingSheet(): ThinkingSheetContextValue { + const context = useContext(ThinkingSheetContext); + if (!context) { + throw new Error("useThinkingSheet must be used within a ThinkingSheetProvider"); + } + return context; +} + +export function ThinkingSheetProvider({ children }: { children: ReactNode }) { + const bottomSheetRef = useRef(null); + const [sheetData, setSheetData] = React.useState(null); + + const snapPoints = useMemo(() => ["60%", "95%"], []); + + const openThinking = useCallback((data: ThinkingSheetData) => { + setSheetData(data); + bottomSheetRef.current?.present(); + }, []); + + const closeThinking = useCallback(() => { + bottomSheetRef.current?.dismiss(); + }, []); + + const handleSheetChange = useCallback((index: number) => { + if (index === -1) { + setSheetData(null); + } + }, []); + + const renderBackdrop = useCallback( + (props: React.ComponentProps) => ( + + ), + [] + ); + + const contextValue = useMemo( + () => ({ openThinking, closeThinking }), + [openThinking, closeThinking] + ); + + return ( + + {children} + + {sheetData && ( + + )} + + + ); +} + +function ThinkingSheetContent({ + data, + onClose, +}: { + data: ThinkingSheetData; + onClose: () => void; +}) { + const { message } = data; + return ( + + + + + + Thinking + + + + + + + + + + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + sheetBackground: { + backgroundColor: theme.colors.surface2, + }, + handleIndicator: { + backgroundColor: theme.colors.palette.zinc[600], + }, + container: { + flex: 1, + backgroundColor: theme.colors.surface2, + }, + header: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: theme.spacing[4], + paddingVertical: theme.spacing[3], + borderBottomWidth: theme.borderWidth[1], + borderBottomColor: theme.colors.border, + }, + headerLeft: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[2], + flex: 1, + }, + headerIcon: { + color: theme.colors.foreground, + }, + headerTitle: { + fontSize: theme.fontSize.lg, + fontWeight: theme.fontWeight.semibold, + color: theme.colors.foreground, + flex: 1, + }, + closeButton: { + padding: theme.spacing[2], + }, + closeIcon: { + color: theme.colors.foregroundMuted, + }, + content: { + flex: 1, + backgroundColor: theme.colors.surface2, + }, + contentContainer: { + paddingHorizontal: theme.spacing[4], + paddingTop: theme.spacing[4], + paddingBottom: theme.spacing[8], + }, +}));