mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
feat: show agent working indicator in stream
This commit is contained in:
@@ -2,7 +2,6 @@ import { View, Text, Pressable } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { ChevronDown } from "lucide-react-native";
|
||||
import { useSession } from "@/contexts/session-context";
|
||||
import type { Agent } from "@/contexts/session-context";
|
||||
import { useState } from "react";
|
||||
import { ModeSelectorModal } from "./mode-selector-modal";
|
||||
|
||||
@@ -25,23 +24,6 @@ export function AgentStatusBar({ agentId }: AgentStatusBarProps) {
|
||||
setAgentMode(agentId, modeId);
|
||||
}
|
||||
|
||||
function getStatusColor(status: Agent["status"]): string {
|
||||
switch (status) {
|
||||
case "initializing":
|
||||
return theme.colors.palette.orange[500];
|
||||
case "idle":
|
||||
return theme.colors.palette.green[500];
|
||||
case "running":
|
||||
return theme.colors.palette.blue[500];
|
||||
case "error":
|
||||
return theme.colors.palette.red[500];
|
||||
case "closed":
|
||||
return theme.colors.palette.gray[500];
|
||||
default:
|
||||
return theme.colors.mutedForeground;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{/* Agent Mode Badge */}
|
||||
@@ -62,14 +44,6 @@ export function AgentStatusBar({ agentId }: AgentStatusBarProps) {
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{/* Agent Status Indicator - just a dot */}
|
||||
<View
|
||||
style={[
|
||||
styles.statusDot,
|
||||
{ backgroundColor: getStatusColor(agent.status) },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Mode selector modal */}
|
||||
<ModeSelectorModal
|
||||
visible={showModeSelector}
|
||||
@@ -105,19 +79,4 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
textTransform: "capitalize",
|
||||
},
|
||||
statusIndicator: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
statusDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
statusText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -14,7 +14,7 @@ import Markdown from "react-native-markdown-display";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { BottomSheetModal } from "@gorhom/bottom-sheet";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import Animated, { FadeIn, FadeOut, cancelAnimation, useAnimatedStyle, useSharedValue, withDelay, withRepeat, withSequence, withTiming } from "react-native-reanimated";
|
||||
import { ChevronDown } from "lucide-react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import {
|
||||
@@ -310,10 +310,48 @@ export function AgentStreamView({
|
||||
[pendingPermissions, agentId]
|
||||
);
|
||||
|
||||
const showWorkingIndicator = agent.status === "running";
|
||||
|
||||
const listHeaderComponent = useMemo(() => {
|
||||
if (pendingPermissionItems.length === 0 && !showWorkingIndicator) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={stylesheet.listHeaderContent}>
|
||||
{pendingPermissionItems.length > 0 ? (
|
||||
<View style={stylesheet.permissionsContainer}>
|
||||
{pendingPermissionItems.map((permission) => (
|
||||
<PermissionRequestCard
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{showWorkingIndicator ? (
|
||||
<View style={stylesheet.workingIndicatorWrapper}>
|
||||
<WorkingIndicator />
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}, [onPermissionResponse, pendingPermissionItems, showWorkingIndicator]);
|
||||
|
||||
const flatListData = useMemo(() => {
|
||||
return [...streamItems].reverse();
|
||||
}, [streamItems]);
|
||||
|
||||
const flatListExtraData = useMemo(
|
||||
() => ({
|
||||
pendingPermissionCount: pendingPermissionItems.length,
|
||||
showWorkingIndicator,
|
||||
}),
|
||||
[pendingPermissionItems.length, showWorkingIndicator]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={stylesheet.container}>
|
||||
<FlatList
|
||||
@@ -339,20 +377,8 @@ export function AgentStreamView({
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
ListHeaderComponent={
|
||||
pendingPermissionItems.length > 0 ? (
|
||||
<View style={stylesheet.permissionsContainer}>
|
||||
{pendingPermissionItems.map((permission) => (
|
||||
<PermissionRequestCard
|
||||
key={permission.key}
|
||||
permission={permission}
|
||||
onResponse={onPermissionResponse}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
) : null
|
||||
}
|
||||
extraData={pendingPermissionItems.length}
|
||||
ListHeaderComponent={listHeaderComponent}
|
||||
extraData={flatListExtraData}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
autoscrollToTopThreshold: 40,
|
||||
@@ -432,6 +458,62 @@ function normalizeInlinePath(rawPath: string):
|
||||
};
|
||||
}
|
||||
|
||||
function WorkingIndicator() {
|
||||
const dotOne = useSharedValue(0);
|
||||
const dotTwo = useSharedValue(0);
|
||||
const dotThree = useSharedValue(0);
|
||||
|
||||
useEffect(() => {
|
||||
const sharedValues = [dotOne, dotTwo, dotThree];
|
||||
sharedValues.forEach((value, index) => {
|
||||
value.value = withDelay(
|
||||
index * 120,
|
||||
withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 320 }),
|
||||
withTiming(0, { duration: 320 })
|
||||
),
|
||||
-1,
|
||||
true
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
return () => {
|
||||
sharedValues.forEach((value) => {
|
||||
cancelAnimation(value);
|
||||
value.value = 0;
|
||||
});
|
||||
};
|
||||
}, [dotOne, dotTwo, dotThree]);
|
||||
|
||||
const dotOneStyle = useAnimatedStyle(() => ({
|
||||
opacity: 0.3 + dotOne.value * 0.7,
|
||||
transform: [{ translateY: dotOne.value * -4 }],
|
||||
}));
|
||||
|
||||
const dotTwoStyle = useAnimatedStyle(() => ({
|
||||
opacity: 0.3 + dotTwo.value * 0.7,
|
||||
transform: [{ translateY: dotTwo.value * -4 }],
|
||||
}));
|
||||
|
||||
const dotThreeStyle = useAnimatedStyle(() => ({
|
||||
opacity: 0.3 + dotThree.value * 0.7,
|
||||
transform: [{ translateY: dotThree.value * -4 }],
|
||||
}));
|
||||
|
||||
return (
|
||||
<View style={stylesheet.workingIndicatorBubble}>
|
||||
<Text style={stylesheet.workingIndicatorText}>Working</Text>
|
||||
<View style={stylesheet.workingDotsRow}>
|
||||
<Animated.View style={[stylesheet.workingDot, dotOneStyle]} />
|
||||
<Animated.View style={[stylesheet.workingDot, dotTwoStyle]} />
|
||||
<Animated.View style={[stylesheet.workingDot, dotThreeStyle]} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
// Permission Request Card Component
|
||||
function PermissionRequestCard({
|
||||
permission,
|
||||
@@ -754,6 +836,39 @@ const stylesheet = StyleSheet.create((theme) => ({
|
||||
permissionsContainer: {
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
listHeaderContent: {
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
workingIndicatorWrapper: {
|
||||
alignItems: "center",
|
||||
},
|
||||
workingIndicatorBubble: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.card,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
workingIndicatorText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
workingDotsRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
workingDot: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: theme.colors.mutedForeground,
|
||||
},
|
||||
invertedWrapper: {
|
||||
transform: [{ scaleY: -1 }],
|
||||
width: "100%",
|
||||
|
||||
3
plan.md
3
plan.md
@@ -80,4 +80,5 @@
|
||||
- Replaced the text-based pill with a compact circular stop control that reuses the Square media glyph (plus accessibility labels), making the interrupt action visually distinct without impacting the other pending AgentInput tweaks.
|
||||
- [x] When the agent input has text and we’re not in the cancellable state, replace both the mic (Dictate) and Realtime buttons with a single Send button—only surface Dictate/Realtime when the input is empty or the agent is running.
|
||||
- `packages/app/src/components/agent-input-area.tsx` now shows a lone Send control when text/images are queued while keeping Dictate + Realtime visible only when the input is empty or the agent is actively running.
|
||||
- [ ] Remove the agent status pill next to the permission selector; instead show a “working” indicator inside the chat scroll view itself (three bouncing dots animation) whenever the agent is busy.
|
||||
- [x] Remove the agent status pill next to the permission selector; instead show a “working” indicator inside the chat scroll view itself (three bouncing dots animation) whenever the agent is busy.
|
||||
- Dropped the old status dot from `AgentStatusBar` (the permission/mode selector now stands alone) and introduced an in-stream "Working" chip inside `AgentStreamView` that renders three bouncing dots via Reanimated whenever the agent reports `status === "running"`, so busy turns surface directly in the chat timeline.
|
||||
|
||||
Reference in New Issue
Block a user