Match thinking badge behavior to tool badge
This commit is contained in:
@@ -49,6 +49,7 @@ import type { DaemonClientV2 } from "@server/client/daemon-client-v2";
|
|||||||
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
|
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
|
||||||
import { ToolCallDetailsContent } from "./tool-call-details";
|
import { ToolCallDetailsContent } from "./tool-call-details";
|
||||||
import { ToolCallSheetProvider } from "./tool-call-sheet";
|
import { ToolCallSheetProvider } from "./tool-call-sheet";
|
||||||
|
import { ThinkingSheetProvider } from "./thinking-sheet";
|
||||||
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
import { createMarkdownStyles } from "@/styles/markdown-styles";
|
||||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||||
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
import { isPerfLoggingEnabled, measurePayload, perfLog } from "@/utils/perf";
|
||||||
@@ -532,62 +533,64 @@ export function AgentStreamView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolCallSheetProvider>
|
<ToolCallSheetProvider>
|
||||||
<View style={stylesheet.container}>
|
<ThinkingSheetProvider>
|
||||||
<MessageOuterSpacingProvider disableOuterSpacing>
|
<View style={stylesheet.container}>
|
||||||
<FlatList
|
<MessageOuterSpacingProvider disableOuterSpacing>
|
||||||
ref={flatListRef}
|
<FlatList
|
||||||
data={flatListData}
|
ref={flatListRef}
|
||||||
renderItem={renderStreamItem}
|
data={flatListData}
|
||||||
keyExtractor={(item) => item.id}
|
renderItem={renderStreamItem}
|
||||||
contentContainerStyle={{
|
keyExtractor={(item) => item.id}
|
||||||
paddingVertical: 0,
|
contentContainerStyle={{
|
||||||
flexGrow: 1,
|
paddingVertical: 0,
|
||||||
}}
|
flexGrow: 1,
|
||||||
style={stylesheet.list}
|
}}
|
||||||
onScroll={handleScroll}
|
style={stylesheet.list}
|
||||||
scrollEventThrottle={16}
|
onScroll={handleScroll}
|
||||||
ListEmptyComponent={
|
scrollEventThrottle={16}
|
||||||
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
ListEmptyComponent={
|
||||||
<Text style={stylesheet.emptyStateText}>
|
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
|
||||||
Start chatting with this agent...
|
<Text style={stylesheet.emptyStateText}>
|
||||||
</Text>
|
Start chatting with this agent...
|
||||||
</View>
|
</Text>
|
||||||
}
|
</View>
|
||||||
ListHeaderComponent={listHeaderComponent}
|
}
|
||||||
extraData={flatListExtraData}
|
ListHeaderComponent={listHeaderComponent}
|
||||||
maintainVisibleContentPosition={
|
extraData={flatListExtraData}
|
||||||
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
|
maintainVisibleContentPosition={
|
||||||
agent.status === "running" && isNearBottom
|
// Disable when streaming and user is at bottom - we handle auto-scroll ourselves
|
||||||
? undefined
|
agent.status === "running" && isNearBottom
|
||||||
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
|
? undefined
|
||||||
}
|
: { minIndexForVisible: 0, autoscrollToTopThreshold: 40 }
|
||||||
initialNumToRender={12}
|
}
|
||||||
windowSize={10}
|
initialNumToRender={12}
|
||||||
inverted
|
windowSize={10}
|
||||||
/>
|
inverted
|
||||||
</MessageOuterSpacingProvider>
|
/>
|
||||||
|
</MessageOuterSpacingProvider>
|
||||||
|
|
||||||
{/* Scroll to bottom button */}
|
{/* Scroll to bottom button */}
|
||||||
{!isNearBottom && (
|
{!isNearBottom && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={stylesheet.scrollToBottomContainer}
|
style={stylesheet.scrollToBottomContainer}
|
||||||
entering={scrollIndicatorFadeIn}
|
entering={scrollIndicatorFadeIn}
|
||||||
exiting={scrollIndicatorFadeOut}
|
exiting={scrollIndicatorFadeOut}
|
||||||
>
|
>
|
||||||
<View style={stylesheet.scrollToBottomInner}>
|
<View style={stylesheet.scrollToBottomInner}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={stylesheet.scrollToBottomButton}
|
style={stylesheet.scrollToBottomButton}
|
||||||
onPress={scrollToBottom}
|
onPress={scrollToBottom}
|
||||||
>
|
>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={24}
|
size={24}
|
||||||
color={stylesheet.scrollToBottomIcon.color}
|
color={stylesheet.scrollToBottomIcon.color}
|
||||||
/>
|
/>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
</ThinkingSheetProvider>
|
||||||
</ToolCallSheetProvider>
|
</ToolCallSheetProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {}) => (
|
||||||
|
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
||||||
|
{node.content}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
textgroup: (node: any, children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||||
|
<Text key={node.key} style={[inheritedStyles, styles.textgroup]}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
code_block: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||||
|
<Text key={node.key} style={[inheritedStyles, styles.code_block]}>
|
||||||
|
{node.content}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
fence: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||||
|
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
||||||
|
{node.content}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
code_inline: (node: any, _children: any[], _parent: any, styles: any, inheritedStyles: any = {}) => (
|
||||||
|
<Text key={node.key} style={[inheritedStyles, styles.code_inline]}>
|
||||||
|
{node.content}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
bullet_list: (node: any, children: any[], _parent: any, styles: any) => (
|
||||||
|
<View key={node.key} style={styles.bullet_list}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
ordered_list: (node: any, children: any[], _parent: any, styles: any) => (
|
||||||
|
<View key={node.key} style={styles.ordered_list}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
),
|
||||||
|
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 (
|
||||||
|
<View key={node.key} style={styles.list_item}>
|
||||||
|
<Text style={iconStyle}>{bullet}</Text>
|
||||||
|
<View style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!markdownContent) {
|
||||||
|
return <Text style={styles.emptyText}>No captured thinking</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Markdown style={markdownStyles} rules={markdownRules}>
|
||||||
|
{markdownContent}
|
||||||
|
</Markdown>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const styles = StyleSheet.create((theme) => ({
|
||||||
|
emptyText: {
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
fontSize: theme.fontSize.xs,
|
||||||
|
fontStyle: "italic" as const,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
@@ -48,7 +48,6 @@ import {
|
|||||||
import { baseColors, theme } from "@/styles/theme";
|
import { baseColors, theme } from "@/styles/theme";
|
||||||
import {
|
import {
|
||||||
createMarkdownStyles,
|
createMarkdownStyles,
|
||||||
createCompactMarkdownStyles,
|
|
||||||
} from "@/styles/markdown-styles";
|
} from "@/styles/markdown-styles";
|
||||||
import { Colors, Fonts } from "@/constants/theme";
|
import { Colors, Fonts } from "@/constants/theme";
|
||||||
import * as Clipboard from "expo-clipboard";
|
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 { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
|
||||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||||
import { useToolCallSheet } from "./tool-call-sheet";
|
import { useToolCallSheet } from "./tool-call-sheet";
|
||||||
|
import { useThinkingSheet } from "./thinking-sheet";
|
||||||
|
import { AgentThoughtContent } from "./agent-thought-content";
|
||||||
import {
|
import {
|
||||||
ToolCallDetailsContent,
|
ToolCallDetailsContent,
|
||||||
useToolCallDetails,
|
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({
|
export const AgentThoughtMessage = memo(function AgentThoughtMessage({
|
||||||
message,
|
message,
|
||||||
status = "ready",
|
status = "ready",
|
||||||
isLastInSequence = false,
|
isLastInSequence = false,
|
||||||
disableOuterSpacing,
|
disableOuterSpacing,
|
||||||
}: AgentThoughtMessageProps) {
|
}: AgentThoughtMessageProps) {
|
||||||
const { theme } = useUnistyles();
|
const { openThinking } = useThinkingSheet();
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
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);
|
setIsExpanded((prev) => !prev);
|
||||||
}, []);
|
}, [isMobile, openThinking, message, status]);
|
||||||
|
|
||||||
const markdownRules = useMemo(() => {
|
|
||||||
return {
|
|
||||||
text: (
|
|
||||||
node: any,
|
|
||||||
_children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any,
|
|
||||||
inheritedStyles: any = {}
|
|
||||||
) => (
|
|
||||||
<Text key={node.key} style={[inheritedStyles, styles.text]}>
|
|
||||||
{node.content}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
textgroup: (
|
|
||||||
node: any,
|
|
||||||
children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any,
|
|
||||||
inheritedStyles: any = {}
|
|
||||||
) => (
|
|
||||||
<Text
|
|
||||||
key={node.key}
|
|
||||||
style={[inheritedStyles, styles.textgroup]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
code_block: (
|
|
||||||
node: any,
|
|
||||||
_children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any,
|
|
||||||
inheritedStyles: any = {}
|
|
||||||
) => (
|
|
||||||
<Text
|
|
||||||
key={node.key}
|
|
||||||
style={[inheritedStyles, styles.code_block]}
|
|
||||||
>
|
|
||||||
{node.content}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
fence: (
|
|
||||||
node: any,
|
|
||||||
_children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any,
|
|
||||||
inheritedStyles: any = {}
|
|
||||||
) => (
|
|
||||||
<Text key={node.key} style={[inheritedStyles, styles.fence]}>
|
|
||||||
{node.content}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
code_inline: (
|
|
||||||
node: any,
|
|
||||||
_children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any,
|
|
||||||
inheritedStyles: any = {}
|
|
||||||
) => (
|
|
||||||
<Text
|
|
||||||
key={node.key}
|
|
||||||
style={[inheritedStyles, styles.code_inline]}
|
|
||||||
>
|
|
||||||
{node.content}
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
bullet_list: (
|
|
||||||
node: any,
|
|
||||||
children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any
|
|
||||||
) => (
|
|
||||||
<View key={node.key} style={styles.bullet_list}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
ordered_list: (
|
|
||||||
node: any,
|
|
||||||
children: ReactNode[],
|
|
||||||
_parent: any,
|
|
||||||
styles: any
|
|
||||||
) => (
|
|
||||||
<View key={node.key} style={styles.ordered_list}>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
),
|
|
||||||
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 (
|
|
||||||
<View key={node.key} style={styles.list_item}>
|
|
||||||
<Text style={iconStyle}>{bullet}</Text>
|
|
||||||
<View
|
|
||||||
style={[contentStyle, { flex: 1, flexShrink: 1, minWidth: 0 }]}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderDetails = useCallback(() => {
|
const renderDetails = useCallback(() => {
|
||||||
if (!markdownContent) {
|
return <AgentThoughtContent message={message} />;
|
||||||
return (
|
}, [message]);
|
||||||
<Text style={agentThoughtStylesheet.emptyText}>
|
|
||||||
No captured thinking
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Markdown style={markdownStyles} rules={markdownRules}>
|
|
||||||
{markdownContent}
|
|
||||||
</Markdown>
|
|
||||||
);
|
|
||||||
}, [markdownContent, markdownRules, markdownStyles]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableBadge
|
<ExpandableBadge
|
||||||
label="Thinking"
|
label="Thinking"
|
||||||
icon={status === "ready" ? Brain : undefined}
|
icon={status === "ready" ? Brain : undefined}
|
||||||
isExpanded={isExpanded}
|
isExpanded={!isMobile && isExpanded}
|
||||||
onToggle={toggle}
|
onToggle={handleToggle}
|
||||||
renderDetails={renderDetails}
|
renderDetails={isMobile ? () => null : renderDetails}
|
||||||
isLoading={status !== "ready"}
|
isLoading={status !== "ready"}
|
||||||
isLastInSequence={isLastInSequence}
|
isLastInSequence={isLastInSequence}
|
||||||
|
style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
|
||||||
disableOuterSpacing={disableOuterSpacing}
|
disableOuterSpacing={disableOuterSpacing}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
ToolCall,
|
ToolCall,
|
||||||
} from "@/components/message";
|
} from "@/components/message";
|
||||||
import { ToolCallSheetProvider } from "@/components/tool-call-sheet";
|
import { ToolCallSheetProvider } from "@/components/tool-call-sheet";
|
||||||
|
import { ThinkingSheetProvider } from "@/components/thinking-sheet";
|
||||||
import type { MessageEntry } from "@/contexts/session-context";
|
import type { MessageEntry } from "@/contexts/session-context";
|
||||||
|
|
||||||
interface OrchestratorMessagesViewProps {
|
interface OrchestratorMessagesViewProps {
|
||||||
@@ -21,86 +22,88 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolCallSheetProvider>
|
<ToolCallSheetProvider>
|
||||||
<ScrollView
|
<ThinkingSheetProvider>
|
||||||
ref={ref}
|
<ScrollView
|
||||||
style={styles.scrollView}
|
ref={ref}
|
||||||
contentContainerStyle={styles.scrollContent}
|
style={styles.scrollView}
|
||||||
keyboardShouldPersistTaps="handled"
|
contentContainerStyle={styles.scrollContent}
|
||||||
keyboardDismissMode="on-drag"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
keyboardDismissMode="on-drag"
|
||||||
{messages.map((msg) => {
|
>
|
||||||
if (msg.type === "user") {
|
{messages.map((msg) => {
|
||||||
return (
|
if (msg.type === "user") {
|
||||||
<UserMessage
|
return (
|
||||||
key={msg.id}
|
<UserMessage
|
||||||
message={msg.message}
|
key={msg.id}
|
||||||
timestamp={msg.timestamp}
|
message={msg.message}
|
||||||
/>
|
timestamp={msg.timestamp}
|
||||||
);
|
/>
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (msg.type === "assistant") {
|
if (msg.type === "assistant") {
|
||||||
return (
|
return (
|
||||||
<AssistantMessage
|
<AssistantMessage
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
message={msg.message}
|
message={msg.message}
|
||||||
timestamp={msg.timestamp}
|
timestamp={msg.timestamp}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "activity") {
|
if (msg.type === "activity") {
|
||||||
return (
|
return (
|
||||||
<ActivityLog
|
<ActivityLog
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
type={msg.activityType}
|
type={msg.activityType}
|
||||||
message={msg.message}
|
message={msg.message}
|
||||||
timestamp={msg.timestamp}
|
timestamp={msg.timestamp}
|
||||||
metadata={msg.metadata}
|
metadata={msg.metadata}
|
||||||
onArtifactClick={onArtifactClick}
|
onArtifactClick={onArtifactClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "artifact") {
|
if (msg.type === "artifact") {
|
||||||
return (
|
return (
|
||||||
<ActivityLog
|
<ActivityLog
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
type="artifact"
|
type="artifact"
|
||||||
message=""
|
message=""
|
||||||
timestamp={msg.timestamp}
|
timestamp={msg.timestamp}
|
||||||
artifactId={msg.artifactId}
|
artifactId={msg.artifactId}
|
||||||
artifactType={msg.artifactType}
|
artifactType={msg.artifactType}
|
||||||
title={msg.title}
|
title={msg.title}
|
||||||
onArtifactClick={onArtifactClick}
|
onArtifactClick={onArtifactClick}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "tool_call") {
|
if (msg.type === "tool_call") {
|
||||||
return (
|
return (
|
||||||
<ToolCall
|
<ToolCall
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
toolName={msg.toolName}
|
toolName={msg.toolName}
|
||||||
args={msg.args}
|
args={msg.args}
|
||||||
result={msg.result}
|
result={msg.result}
|
||||||
error={msg.error}
|
error={msg.error}
|
||||||
status={msg.status}
|
status={msg.status}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Streaming assistant message */}
|
{/* Streaming assistant message */}
|
||||||
{currentAssistantMessage && (
|
{currentAssistantMessage && (
|
||||||
<AssistantMessage
|
<AssistantMessage
|
||||||
message={currentAssistantMessage}
|
message={currentAssistantMessage}
|
||||||
timestamp={Date.now()}
|
timestamp={Date.now()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</ThinkingSheetProvider>
|
||||||
</ToolCallSheetProvider>
|
</ToolCallSheetProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<ThinkingSheetContextValue | null>(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<BottomSheetModal>(null);
|
||||||
|
const [sheetData, setSheetData] = React.useState<ThinkingSheetData | null>(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<typeof BottomSheetBackdrop>) => (
|
||||||
|
<BottomSheetBackdrop
|
||||||
|
{...props}
|
||||||
|
disappearsOnIndex={-1}
|
||||||
|
appearsOnIndex={0}
|
||||||
|
opacity={0.5}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({ openThinking, closeThinking }),
|
||||||
|
[openThinking, closeThinking]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThinkingSheetContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
<BottomSheetModal
|
||||||
|
ref={bottomSheetRef}
|
||||||
|
snapPoints={snapPoints}
|
||||||
|
index={0}
|
||||||
|
enableDynamicSizing={false}
|
||||||
|
onChange={handleSheetChange}
|
||||||
|
backdropComponent={renderBackdrop}
|
||||||
|
enablePanDownToClose
|
||||||
|
backgroundStyle={styles.sheetBackground}
|
||||||
|
handleIndicatorStyle={styles.handleIndicator}
|
||||||
|
>
|
||||||
|
{sheetData && (
|
||||||
|
<ThinkingSheetContent data={sheetData} onClose={closeThinking} />
|
||||||
|
)}
|
||||||
|
</BottomSheetModal>
|
||||||
|
</ThinkingSheetContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ThinkingSheetContent({
|
||||||
|
data,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
data: ThinkingSheetData;
|
||||||
|
onClose: () => void;
|
||||||
|
}) {
|
||||||
|
const { message } = data;
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.headerLeft}>
|
||||||
|
<Brain size={20} color={styles.headerIcon.color} />
|
||||||
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||||
|
Thinking
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||||
|
<X size={20} color={styles.closeIcon.color} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<BottomSheetScrollView
|
||||||
|
style={styles.content}
|
||||||
|
contentContainerStyle={styles.contentContainer}
|
||||||
|
>
|
||||||
|
<AgentThoughtContent message={message} />
|
||||||
|
</BottomSheetScrollView>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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],
|
||||||
|
},
|
||||||
|
}));
|
||||||
Reference in New Issue
Block a user