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],
+ },
+}));