feat: improve agent stream view UI, fix history replay timestamps, and enhance planner/worker loop
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
import { View, Text, Pressable, Modal, RefreshControl, FlatList, type ListRenderItem } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
Modal,
|
||||
RefreshControl,
|
||||
FlatList,
|
||||
type ListRenderItem,
|
||||
} from "react-native";
|
||||
import { useCallback, useState, type ReactElement } from "react";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { buildAgentNavigationKey, startNavigationTiming } from "@/utils/navigation-timing";
|
||||
import {
|
||||
buildAgentNavigationKey,
|
||||
startNavigationTiming,
|
||||
} from "@/utils/navigation-timing";
|
||||
|
||||
interface AgentListProps {
|
||||
agents: AggregatedAgent[];
|
||||
@@ -30,7 +41,9 @@ export function AgentList({
|
||||
|
||||
// Get the methods for the specific server
|
||||
const methods = useSessionStore((state) =>
|
||||
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.methods : undefined
|
||||
actionAgent?.serverId
|
||||
? state.sessions[actionAgent.serverId]?.methods
|
||||
: undefined
|
||||
);
|
||||
const deleteAgent = methods?.deleteAgent;
|
||||
|
||||
@@ -94,7 +107,11 @@ export function AgentList({
|
||||
const isRunning = agent.status === "running";
|
||||
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||
const isSelected = selectedAgentId === agentKey;
|
||||
const statusColor = isRunning ? theme.colors.palette.blue[500] : agent.requiresAttention ? theme.colors.success : null;
|
||||
const statusColor = isRunning
|
||||
? theme.colors.palette.blue[500]
|
||||
: agent.requiresAttention
|
||||
? theme.colors.success
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -108,26 +125,28 @@ export function AgentList({
|
||||
onLongPress={() => handleAgentLongPress(agent)}
|
||||
>
|
||||
{({ hovered }) => (
|
||||
<View style={styles.agentContent}>
|
||||
<View style={styles.row}>
|
||||
{statusColor && (
|
||||
<View style={[styles.statusDot, { backgroundColor: statusColor }]} />
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
styles.agentTitle,
|
||||
(isSelected || hovered) && styles.agentTitleHighlighted,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.title || "New Agent"}
|
||||
<View style={styles.agentContent}>
|
||||
<View style={styles.row}>
|
||||
{statusColor && (
|
||||
<View
|
||||
style={[styles.statusDot, { backgroundColor: statusColor }]}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
style={[
|
||||
styles.agentTitle,
|
||||
(isSelected || hovered) && styles.agentTitleHighlighted,
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.title || "New Agent"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.secondaryRow} numberOfLines={1}>
|
||||
{agent.cwd.replace(/^\/(?:Users|home)\/[^/]+/, "~")} · {timeAgo}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
@@ -176,7 +195,10 @@ export function AgentList({
|
||||
onRequestClose={handleCloseActionSheet}
|
||||
>
|
||||
<View style={styles.sheetOverlay}>
|
||||
<Pressable style={styles.sheetBackdrop} onPress={handleCloseActionSheet} />
|
||||
<Pressable
|
||||
style={styles.sheetBackdrop}
|
||||
onPress={handleCloseActionSheet}
|
||||
/>
|
||||
<View style={styles.sheetContainer}>
|
||||
<View style={styles.sheetHandle} />
|
||||
<Text style={styles.sheetTitle}>
|
||||
@@ -195,7 +217,8 @@ export function AgentList({
|
||||
<Text
|
||||
style={[
|
||||
styles.sheetDeleteText,
|
||||
(!deleteAgent || isActionDaemonUnavailable) && styles.sheetDeleteTextDisabled,
|
||||
(!deleteAgent || isActionDaemonUnavailable) &&
|
||||
styles.sheetDeleteTextDisabled,
|
||||
]}
|
||||
>
|
||||
{isActionDaemonUnavailable ? "Host offline" : "Delete agent"}
|
||||
@@ -246,7 +269,7 @@ const styles = StyleSheet.create((theme) => ({
|
||||
row: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
gap: theme.spacing[0],
|
||||
},
|
||||
agentTitle: {
|
||||
flex: 1,
|
||||
@@ -255,10 +278,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
xs: "400",
|
||||
md: "300",
|
||||
},
|
||||
color: theme.colors.foregroundMuted,
|
||||
color: theme.colors.foreground,
|
||||
opacity: 0.8,
|
||||
},
|
||||
agentTitleHighlighted: {
|
||||
color: theme.colors.foreground,
|
||||
opacity: 1,
|
||||
},
|
||||
secondaryRow: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
|
||||
@@ -373,8 +373,6 @@ export function AgentStreamView({
|
||||
return null;
|
||||
}
|
||||
|
||||
const gap = getGapAbove(item, index);
|
||||
|
||||
// Check if this is the end of a turn (before a user message or end of stream when not running)
|
||||
// In inverted list: index-1 is the next item (newer in time)
|
||||
const nextItem = flatListData[index - 1];
|
||||
@@ -386,18 +384,19 @@ export function AgentStreamView({
|
||||
const getContent = () => collectTurnContent(index);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
stylesheet.streamItemWrapper,
|
||||
gap ? { marginBottom: gap } : null,
|
||||
]}
|
||||
>
|
||||
<View style={[stylesheet.streamItemWrapper]}>
|
||||
{content}
|
||||
{isEndOfTurn ? <TurnCopyButton getContent={getContent} /> : null}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[getGapAbove, renderStreamItemContent, flatListData, agent.status, collectTurnContent]
|
||||
[
|
||||
getGapAbove,
|
||||
renderStreamItemContent,
|
||||
flatListData,
|
||||
agent.status,
|
||||
collectTurnContent,
|
||||
]
|
||||
);
|
||||
|
||||
const pendingPermissionItems = useMemo(
|
||||
@@ -419,7 +418,10 @@ export function AgentStreamView({
|
||||
}
|
||||
const delta = Math.abs(totalCount - prevCount);
|
||||
streamItemCountRef.current = totalCount;
|
||||
if (totalCount < STREAM_ITEM_LOG_MIN_COUNT && delta < STREAM_ITEM_LOG_DELTA_THRESHOLD) {
|
||||
if (
|
||||
totalCount < STREAM_ITEM_LOG_MIN_COUNT &&
|
||||
delta < STREAM_ITEM_LOG_DELTA_THRESHOLD
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let userCount = 0;
|
||||
@@ -452,7 +454,10 @@ export function AgentStreamView({
|
||||
break;
|
||||
}
|
||||
}
|
||||
const metrics = totalCount >= STREAM_ITEM_LOG_MIN_COUNT ? measurePayload(streamItems) : null;
|
||||
const metrics =
|
||||
totalCount >= STREAM_ITEM_LOG_MIN_COUNT
|
||||
? measurePayload(streamItems)
|
||||
: null;
|
||||
perfLog(AGENT_STREAM_LOG_TAG, {
|
||||
event: "stream_items",
|
||||
agentId,
|
||||
@@ -945,11 +950,10 @@ function PermissionRequestCard({
|
||||
const handleResponse = useCallback(
|
||||
(response: AgentPermissionResponse) => {
|
||||
respondToPermission({
|
||||
agentId: permission.agentId,
|
||||
requestId: permission.request.id,
|
||||
response,
|
||||
})
|
||||
.catch((error) => {
|
||||
agentId: permission.agentId,
|
||||
requestId: permission.request.id,
|
||||
response,
|
||||
}).catch((error) => {
|
||||
console.error(
|
||||
"[PermissionRequestCard] Failed to respond to permission:",
|
||||
error
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { View, Text, Pressable, Animated } from "react-native";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
Animated,
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
@@ -31,6 +38,7 @@ import {
|
||||
Search,
|
||||
Brain,
|
||||
Copy,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react-native";
|
||||
import {
|
||||
StyleSheet,
|
||||
@@ -173,14 +181,11 @@ export const UserMessage = memo(function UserMessage({
|
||||
<View
|
||||
style={[
|
||||
userMessageStylesheet.container,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
userMessageStylesheet.containerSpacing,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
isFirstInGroup &&
|
||||
userMessageStylesheet.containerFirstInGroup,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
isLastInGroup &&
|
||||
userMessageStylesheet.containerLastInGroup,
|
||||
isFirstInGroup && { marginTop: theme.spacing[4] },
|
||||
isLastInGroup && { marginBottom: theme.spacing[4] },
|
||||
!isFirstInGroup || !isLastInGroup
|
||||
? { marginBottom: theme.spacing[1] }
|
||||
: undefined,
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
@@ -364,7 +369,7 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
label: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
flexShrink: 0,
|
||||
},
|
||||
secondaryLabel: {
|
||||
@@ -804,8 +809,7 @@ export const ActivityLog = memo(function ActivityLog({
|
||||
disabled={!isInteractive}
|
||||
style={[
|
||||
activityLogStylesheet.pressable,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
activityLogStylesheet.pressableSpacing,
|
||||
!resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
|
||||
config.bg,
|
||||
isInteractive && activityLogStylesheet.pressableActive,
|
||||
]}
|
||||
@@ -985,8 +989,7 @@ export const TodoListCard = memo(function TodoListCard({
|
||||
<View
|
||||
style={[
|
||||
todoListCardStylesheet.container,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
todoListCardStylesheet.containerSpacing,
|
||||
!resolvedDisableOuterSpacing && todoListCardStylesheet.containerSpacing,
|
||||
]}
|
||||
>
|
||||
<View style={todoListCardStylesheet.card}>
|
||||
@@ -1056,6 +1059,7 @@ interface ExpandableBadgeProps {
|
||||
secondaryLabel?: string;
|
||||
icon?: ComponentType<{ size?: number; color?: string }>;
|
||||
isExpanded: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
onToggle?: () => void;
|
||||
renderDetails?: () => ReactNode;
|
||||
isLoading?: boolean;
|
||||
@@ -1066,6 +1070,7 @@ interface ExpandableBadgeProps {
|
||||
|
||||
const ExpandableBadge = memo(function ExpandableBadge({
|
||||
label,
|
||||
style,
|
||||
secondaryLabel,
|
||||
icon,
|
||||
isExpanded,
|
||||
@@ -1118,7 +1123,7 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
const IconComponent = icon;
|
||||
const iconColor = isError
|
||||
? theme.colors.destructive
|
||||
: theme.colors.foreground;
|
||||
: theme.colors.mutedForeground;
|
||||
|
||||
let iconNode: ReactNode = null;
|
||||
if (isLoading) {
|
||||
@@ -1128,22 +1133,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
|
||||
</Animated.View>
|
||||
);
|
||||
} else if (isError) {
|
||||
iconNode = <X size={12} color={iconColor} />;
|
||||
iconNode = <TriangleAlertIcon size={12} color={iconColor} opacity={0.8} />;
|
||||
} else if (IconComponent) {
|
||||
iconNode = <IconComponent size={12} color={iconColor} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
expandableBadgeStylesheet.container,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
expandableBadgeStylesheet.containerSpacing,
|
||||
!resolvedDisableOuterSpacing &&
|
||||
isLastInSequence &&
|
||||
expandableBadgeStylesheet.containerLastInSequence,
|
||||
]}
|
||||
>
|
||||
<View style={[expandableBadgeStylesheet.container, style]}>
|
||||
<Pressable
|
||||
onPress={hasDetails ? onToggle : undefined}
|
||||
disabled={!hasDetails}
|
||||
@@ -1507,12 +1503,13 @@ export const ToolCall = memo(function ToolCall({
|
||||
hasDetails && !isMobile
|
||||
? renderDetails
|
||||
: hasDetails
|
||||
? () => null
|
||||
: undefined
|
||||
? () => null
|
||||
: undefined
|
||||
}
|
||||
isLoading={status === "executing"}
|
||||
isError={status === "failed"}
|
||||
isLastInSequence={isLastInSequence}
|
||||
style={isLastInSequence ? undefined : { marginBottom: theme.spacing[1] }}
|
||||
disableOuterSpacing={disableOuterSpacing}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -67,7 +67,9 @@ function updateFavicon(status: FaviconStatus, colorScheme: ColorScheme) {
|
||||
}
|
||||
|
||||
function getSystemColorScheme(): ColorScheme {
|
||||
if (typeof window === "undefined") return "dark";
|
||||
if (Platform.OS !== "web" || typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
||||
return "dark";
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ StyleSheet.configure({
|
||||
xl: 1200,
|
||||
},
|
||||
settings: {
|
||||
initialTheme: "dark",
|
||||
adaptiveThemes: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user