mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -1,187 +0,0 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Copy, Info } from "lucide-react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { AdaptiveModalSheet } from "@/components/adaptive-modal-sheet";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
|
||||
export type AgentDetailsSheetProps = {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
agentId: string;
|
||||
persistenceSessionId: string | null;
|
||||
};
|
||||
|
||||
export function AgentDetailsSheet({
|
||||
visible,
|
||||
onClose,
|
||||
agentId,
|
||||
persistenceSessionId,
|
||||
}: AgentDetailsSheetProps) {
|
||||
const toast = useToast();
|
||||
|
||||
const details = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: "Agent ID",
|
||||
value: agentId,
|
||||
testID: "agent-details-agent-id",
|
||||
copyLabel: "agent id",
|
||||
},
|
||||
{
|
||||
label: "Persistence session ID",
|
||||
value: persistenceSessionId,
|
||||
testID: "agent-details-persistence-session-id",
|
||||
copyLabel: "session id",
|
||||
},
|
||||
],
|
||||
[agentId, persistenceSessionId]
|
||||
);
|
||||
|
||||
const handleCopy = useCallback(
|
||||
async (value: string | null, label: string) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
await Clipboard.setStringAsync(value);
|
||||
toast.copied(label);
|
||||
} catch {
|
||||
toast.error("Copy failed");
|
||||
}
|
||||
},
|
||||
[toast]
|
||||
);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
title="Agent details"
|
||||
visible={visible}
|
||||
onClose={onClose}
|
||||
testID="agent-details-sheet"
|
||||
snapPoints={["45%", "70%"]}
|
||||
>
|
||||
{details.map((row) => (
|
||||
<DetailRow
|
||||
key={row.label}
|
||||
label={row.label}
|
||||
value={row.value}
|
||||
onCopy={() => void handleCopy(row.value, row.copyLabel)}
|
||||
testID={row.testID}
|
||||
disabled={!row.value}
|
||||
/>
|
||||
))}
|
||||
</AdaptiveModalSheet>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailRow({
|
||||
label,
|
||||
value,
|
||||
onCopy,
|
||||
disabled,
|
||||
testID,
|
||||
}: {
|
||||
label: string;
|
||||
value: string | null;
|
||||
onCopy: () => void;
|
||||
disabled?: boolean;
|
||||
testID: string;
|
||||
}) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${label} copy`}
|
||||
accessibilityHint={disabled ? "Not available" : "Copies to clipboard"}
|
||||
disabled={disabled}
|
||||
onPress={onCopy}
|
||||
style={({ pressed }) => [
|
||||
styles.row,
|
||||
disabled ? styles.rowDisabled : null,
|
||||
pressed && !disabled ? styles.rowPressed : null,
|
||||
]}
|
||||
>
|
||||
<View style={styles.rowTop}>
|
||||
<View style={styles.labelRow}>
|
||||
<Info size={16} color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.label}>{label}</Text>
|
||||
</View>
|
||||
<View style={styles.copyPill}>
|
||||
<Copy size={14} color={theme.colors.foregroundMuted} />
|
||||
<Text style={styles.copyText}>Copy</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
testID={`${testID}-value`}
|
||||
style={[styles.value, !value ? styles.valueEmpty : null]}
|
||||
numberOfLines={2}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{value ?? "Not available"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
row: {
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.surface2,
|
||||
backgroundColor: theme.colors.surface1,
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[4],
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
rowPressed: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
rowDisabled: {
|
||||
opacity: 0.65,
|
||||
},
|
||||
rowTop: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
labelRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
flex: 1,
|
||||
},
|
||||
label: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
flex: 1,
|
||||
},
|
||||
copyPill: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.full,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
copyText: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
value: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontFamily: Fonts.mono,
|
||||
},
|
||||
valueEmpty: {
|
||||
color: theme.colors.foregroundMuted,
|
||||
fontFamily: Fonts.sans,
|
||||
},
|
||||
}));
|
||||
@@ -424,12 +424,20 @@ export function FileExplorerPane({ serverId, agentId }: FileExplorerPaneProps) {
|
||||
<DropdownMenuContent align="end" width={220}>
|
||||
<View style={styles.contextMetaBlock}>
|
||||
<View style={styles.contextMetaRow}>
|
||||
<Text style={styles.contextMetaLabel}>Size</Text>
|
||||
<Text style={styles.contextMetaValue}>{formatFileSize({ size: entry.size })}</Text>
|
||||
<Text style={styles.contextMetaLabel} numberOfLines={1}>
|
||||
Size
|
||||
</Text>
|
||||
<Text style={styles.contextMetaValue} numberOfLines={1} ellipsizeMode="tail">
|
||||
{formatFileSize({ size: entry.size })}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.contextMetaRow}>
|
||||
<Text style={styles.contextMetaLabel}>Modified</Text>
|
||||
<Text style={styles.contextMetaValue}>{formatTimeAgo(new Date(entry.modifiedAt))}</Text>
|
||||
<Text style={styles.contextMetaLabel} numberOfLines={1}>
|
||||
Modified
|
||||
</Text>
|
||||
<Text style={styles.contextMetaValue} numberOfLines={1} ellipsizeMode="tail">
|
||||
{formatTimeAgo(new Date(entry.modifiedAt))}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<DropdownMenuSeparator />
|
||||
@@ -1132,11 +1140,15 @@ const styles = StyleSheet.create((theme) => ({
|
||||
contextMetaLabel: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
flexShrink: 0,
|
||||
},
|
||||
contextMetaValue: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
},
|
||||
previewHeaderText: {
|
||||
flex: 1,
|
||||
|
||||
@@ -15,7 +15,19 @@ import { ScrollView, type ScrollView as ScrollViewType } from "react-native-gest
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as Linking from "expo-linking";
|
||||
import { Archive, ChevronDown, ChevronRight, GitBranch, MoreVertical, ListChevronsDownUp, ListChevronsUpDown } from "lucide-react-native";
|
||||
import {
|
||||
Archive,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
GitBranch,
|
||||
GitCommitHorizontal,
|
||||
GitMerge,
|
||||
ListChevronsDownUp,
|
||||
ListChevronsUpDown,
|
||||
MoreVertical,
|
||||
RefreshCcw,
|
||||
Upload,
|
||||
} from "lucide-react-native";
|
||||
import { useCheckoutGitActionsStore } from "@/stores/checkout-git-actions-store";
|
||||
import {
|
||||
useCheckoutDiffQuery,
|
||||
@@ -37,6 +49,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
type ActionStatus,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { GitHubIcon } from "@/components/icons/github-icon";
|
||||
|
||||
// =============================================================================
|
||||
// Git Actions Data Structure
|
||||
@@ -856,6 +869,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
successLabel: "Committed",
|
||||
disabled: commitDisabled,
|
||||
status: commitStatus,
|
||||
icon: <GitCommitHorizontal size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: handleCommit,
|
||||
});
|
||||
|
||||
@@ -869,6 +883,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
disabled: pushDisabled,
|
||||
status: pushStatus,
|
||||
description: !hasRemote ? "No remote configured" : undefined,
|
||||
icon: <Upload size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: handlePush,
|
||||
});
|
||||
}
|
||||
@@ -883,6 +898,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
successLabel: "View PR",
|
||||
disabled: false,
|
||||
status: "idle",
|
||||
icon: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: () => openURLInNewTab(prUrl),
|
||||
});
|
||||
}
|
||||
@@ -896,6 +912,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
successLabel: "PR Created",
|
||||
disabled: prDisabled,
|
||||
status: prCreateStatus,
|
||||
icon: <GitHubIcon size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: handleCreatePr,
|
||||
});
|
||||
}
|
||||
@@ -910,6 +927,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
disabled: mergeDisabled,
|
||||
status: mergeStatus,
|
||||
description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
|
||||
icon: <GitMerge size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: handleMergeBranch,
|
||||
});
|
||||
}
|
||||
@@ -924,6 +942,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
disabled: mergeFromBaseDisabled,
|
||||
status: mergeFromBaseStatus,
|
||||
description: hasUncommittedChanges ? "Requires clean working tree" : undefined,
|
||||
icon: <RefreshCcw size={16} color={theme.colors.foregroundMuted} />,
|
||||
handler: handleMergeFromBase,
|
||||
});
|
||||
}
|
||||
@@ -994,6 +1013,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
commitDisabled, pushDisabled, prDisabled, mergeDisabled, mergeFromBaseDisabled, archiveDisabled,
|
||||
commitStatus, pushStatus, prCreateStatus, mergeStatus, mergeFromBaseStatus, archiveStatus,
|
||||
handleCommit, handlePush, handleCreatePr, handleMergeBranch, handleMergeFromBase, handleArchiveWorktree,
|
||||
theme.colors.foregroundMuted,
|
||||
]);
|
||||
|
||||
// Helper to get display label based on status
|
||||
@@ -1033,7 +1053,10 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
{gitActions.primary.status === "pending" ? (
|
||||
<ActivityIndicator size="small" color={theme.colors.foreground} style={styles.splitButtonSpinner} />
|
||||
) : (
|
||||
<Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text>
|
||||
<View style={styles.splitButtonContent}>
|
||||
{gitActions.primary.icon}
|
||||
<Text style={styles.splitButtonText}>{getActionDisplayLabel(gitActions.primary)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
{gitActions.secondary.length > 0 ? (
|
||||
@@ -1054,6 +1077,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
{needsSeparator && index > 0 ? <DropdownMenuSeparator /> : null}
|
||||
<DropdownMenuItem
|
||||
testID={`changes-menu-${action.id}`}
|
||||
leading={action.icon}
|
||||
disabled={action.disabled}
|
||||
status={action.status}
|
||||
pendingLabel={action.pendingLabel}
|
||||
@@ -1077,7 +1101,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
<DropdownMenuTrigger
|
||||
testID="changes-overflow-menu"
|
||||
hitSlop={8}
|
||||
style={styles.iconButton}
|
||||
style={[styles.iconButton, styles.overflowMenuButton]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="More actions"
|
||||
>
|
||||
@@ -1280,6 +1304,12 @@ const styles = StyleSheet.create((theme) => ({
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
splitButtonContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
splitButtonSpinner: {
|
||||
height: theme.fontSize.xs * 1.5,
|
||||
width: theme.fontSize.xs * 1.5,
|
||||
@@ -1298,6 +1328,9 @@ const styles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "center",
|
||||
borderRadius: theme.borderRadius.md,
|
||||
},
|
||||
overflowMenuButton: {
|
||||
marginRight: -theme.spacing[2],
|
||||
},
|
||||
menuOverlay: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
14
packages/app/src/components/icons/github-icon.tsx
Normal file
14
packages/app/src/components/icons/github-icon.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
interface GitHubIconProps {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export function GitHubIcon({ size = 16, color = "currentColor" }: GitHubIconProps) {
|
||||
return (
|
||||
<Svg width={size} height={size} viewBox="0 -0.5 25 25" fill={color}>
|
||||
<Path d="m12.301 0h.093c2.242 0 4.34.613 6.137 1.68l-.055-.031c1.871 1.094 3.386 2.609 4.449 4.422l.031.058c1.04 1.769 1.654 3.896 1.654 6.166 0 5.406-3.483 10-8.327 11.658l-.087.026c-.063.02-.135.031-.209.031-.162 0-.312-.054-.433-.144l.002.001c-.128-.115-.208-.281-.208-.466 0-.005 0-.01 0-.014v.001q0-.048.008-1.226t.008-2.154c.007-.075.011-.161.011-.249 0-.792-.323-1.508-.844-2.025.618-.061 1.176-.163 1.718-.305l-.076.017c.573-.16 1.073-.373 1.537-.642l-.031.017c.508-.28.938-.636 1.292-1.058l.006-.007c.372-.476.663-1.036.84-1.645l.009-.035c.209-.683.329-1.468.329-2.281 0-.045 0-.091-.001-.136v.007c0-.022.001-.047.001-.072 0-1.248-.482-2.383-1.269-3.23l.003.003c.168-.44.265-.948.265-1.479 0-.649-.145-1.263-.404-1.814l.011.026c-.115-.022-.246-.035-.381-.035-.334 0-.649.078-.929.216l.012-.005c-.568.21-1.054.448-1.512.726l.038-.022-.609.384c-.922-.264-1.981-.416-3.075-.416s-2.153.152-3.157.436l.081-.02q-.256-.176-.681-.433c-.373-.214-.814-.421-1.272-.595l-.066-.022c-.293-.154-.64-.244-1.009-.244-.124 0-.246.01-.364.03l.013-.002c-.248.524-.393 1.139-.393 1.788 0 .531.097 1.04.275 1.509l-.01-.029c-.785.844-1.266 1.979-1.266 3.227 0 .025 0 .051.001.076v-.004c-.001.039-.001.084-.001.13 0 .809.12 1.591.344 2.327l-.015-.057c.189.643.476 1.202.85 1.693l-.009-.013c.354.435.782.793 1.267 1.062l.022.011c.432.252.933.465 1.46.614l.046.011c.466.125 1.024.227 1.595.284l.046.004c-.431.428-.718 1-.784 1.638l-.001.012c-.207.101-.448.183-.699.236l-.021.004c-.256.051-.549.08-.85.08-.022 0-.044 0-.066 0h.003c-.394-.008-.756-.136-1.055-.348l.006.004c-.371-.259-.671-.595-.881-.986l-.007-.015c-.198-.336-.459-.614-.768-.827l-.009-.006c-.225-.169-.49-.301-.776-.38l-.016-.004-.32-.048c-.023-.002-.05-.003-.077-.003-.14 0-.273.028-.394.077l.007-.003q-.128.072-.08.184c.039.086.087.16.145.225l-.001-.001c.061.072.13.135.205.19l.003.002.112.08c.283.148.516.354.693.603l.004.006c.191.237.359.505.494.792l.01.024.16.368c.135.402.38.738.7.981l.005.004c.3.234.662.402 1.057.478l.016.002c.33.064.714.104 1.106.112h.007c.045.002.097.002.15.002.261 0 .517-.021.767-.062l-.027.004.368-.064q0 .609.008 1.418t.008.873v.014c0 .185-.08.351-.208.466h-.001c-.119.089-.268.143-.431.143-.075 0-.147-.011-.214-.032l.005.001c-4.929-1.689-8.409-6.283-8.409-11.69 0-2.268.612-4.393 1.681-6.219l-.032.058c1.094-1.871 2.609-3.386 4.422-4.449l.058-.031c1.739-1.034 3.835-1.645 6.073-1.645h.098-.005zm-7.64 17.666q.048-.112-.112-.192-.16-.048-.208.032-.048.112.112.192.144.096.208-.032zm.497.545q.112-.08-.032-.256-.16-.144-.256-.048-.112.08.032.256.159.157.256.047zm.48.72q.144-.112 0-.304-.128-.208-.272-.096-.144.08 0 .288t.272.112zm.672.673q.128-.128-.064-.304-.192-.192-.32-.048-.144.128.064.304.192.192.32.044zm.913.4q.048-.176-.208-.256-.24-.064-.304.112t.208.24q.24.097.304-.096zm1.009.08q0-.208-.272-.176-.256 0-.256.176 0 .208.272.176.256.001.256-.175zm.929-.16q-.032-.176-.288-.144-.256.048-.224.24t.288.128.225-.224z" />
|
||||
</Svg>
|
||||
);
|
||||
}
|
||||
@@ -25,11 +25,11 @@ import { CheckCircle2, AlertTriangle } from "lucide-react-native";
|
||||
type ToastVariant = "default" | "success" | "error";
|
||||
|
||||
export type ToastShowOptions = {
|
||||
icon?: ReactNode;
|
||||
variant?: ToastVariant;
|
||||
durationMs?: number;
|
||||
/**
|
||||
* On Android we prefer the OS toast by default.
|
||||
* Set to false to force the in-app toast.
|
||||
* Set to true to use OS toast on Android.
|
||||
*/
|
||||
nativeAndroid?: boolean;
|
||||
testID?: string;
|
||||
@@ -37,14 +37,16 @@ export type ToastShowOptions = {
|
||||
|
||||
type ToastState = {
|
||||
id: number;
|
||||
message: string;
|
||||
content: ReactNode;
|
||||
nativeMessage: string | null;
|
||||
icon?: ReactNode;
|
||||
variant: ToastVariant;
|
||||
durationMs: number;
|
||||
testID?: string;
|
||||
};
|
||||
|
||||
export type ToastApi = {
|
||||
show: (message: string, options?: ToastShowOptions) => void;
|
||||
show: (content: ReactNode, options?: ToastShowOptions) => void;
|
||||
copied: (label?: string) => void;
|
||||
error: (message: string) => void;
|
||||
};
|
||||
@@ -66,23 +68,26 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const idRef = useRef(0);
|
||||
|
||||
const show = useCallback(
|
||||
(message: string, options?: ToastShowOptions) => {
|
||||
const resolvedMessage = message.trim();
|
||||
if (!resolvedMessage) return;
|
||||
(content: ReactNode, options?: ToastShowOptions) => {
|
||||
const nativeMessage =
|
||||
typeof content === "string"
|
||||
? content.trim()
|
||||
: null;
|
||||
if (!content || nativeMessage === "") return;
|
||||
|
||||
const variant = options?.variant ?? "default";
|
||||
const durationMs = options?.durationMs ?? DEFAULT_DURATION_MS;
|
||||
const nativeAndroid = options?.nativeAndroid ?? true;
|
||||
const nativeAndroid = options?.nativeAndroid ?? false;
|
||||
|
||||
if (Platform.OS === "android" && nativeAndroid) {
|
||||
if (Platform.OS === "android" && nativeAndroid && nativeMessage) {
|
||||
const duration =
|
||||
durationMs <= 2500
|
||||
? ToastAndroid.SHORT
|
||||
: ToastAndroid.LONG;
|
||||
ToastAndroid.showWithGravity(
|
||||
resolvedMessage,
|
||||
nativeMessage,
|
||||
duration,
|
||||
ToastAndroid.BOTTOM
|
||||
ToastAndroid.TOP
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +95,9 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
idRef.current += 1;
|
||||
setToast({
|
||||
id: idRef.current,
|
||||
message: resolvedMessage,
|
||||
content,
|
||||
nativeMessage,
|
||||
icon: options?.icon,
|
||||
variant,
|
||||
durationMs,
|
||||
testID: options?.testID,
|
||||
@@ -103,7 +110,10 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
() => ({
|
||||
show,
|
||||
copied: (label?: string) =>
|
||||
show(label ? `Copied ${label}` : "Copied", { variant: "success" }),
|
||||
show(label ? `Copied ${label}` : "Copied", {
|
||||
variant: "success",
|
||||
icon: <CheckCircle2 size={18} />,
|
||||
}),
|
||||
error: (message: string) => show(message, { variant: "error", durationMs: 3200 }),
|
||||
}),
|
||||
[show]
|
||||
@@ -127,7 +137,7 @@ function ToastViewport({
|
||||
const { theme } = useUnistyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const opacity = useRef(new Animated.Value(0)).current;
|
||||
const translateY = useRef(new Animated.Value(8)).current;
|
||||
const translateY = useRef(new Animated.Value(-8)).current;
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearTimer = useCallback(() => {
|
||||
@@ -147,7 +157,7 @@ function ToastViewport({
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(translateY, {
|
||||
toValue: 8,
|
||||
toValue: -8,
|
||||
duration: 140,
|
||||
easing: Easing.out(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
@@ -163,13 +173,13 @@ function ToastViewport({
|
||||
if (!toast) {
|
||||
clearTimer();
|
||||
opacity.setValue(0);
|
||||
translateY.setValue(8);
|
||||
translateY.setValue(-8);
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimer();
|
||||
opacity.setValue(0);
|
||||
translateY.setValue(8);
|
||||
translateY.setValue(-8);
|
||||
|
||||
Animated.parallel([
|
||||
Animated.timing(opacity, {
|
||||
@@ -200,11 +210,13 @@ function ToastViewport({
|
||||
}
|
||||
|
||||
const icon =
|
||||
toast.variant === "success" ? (
|
||||
toast.icon ?? (
|
||||
toast.variant === "success" ? (
|
||||
<CheckCircle2 size={18} color={theme.colors.primary} />
|
||||
) : toast.variant === "error" ? (
|
||||
<AlertTriangle size={18} color={theme.colors.destructive} />
|
||||
) : null;
|
||||
) : null
|
||||
);
|
||||
|
||||
const content = (
|
||||
<View style={styles.container} pointerEvents="box-none">
|
||||
@@ -212,8 +224,10 @@ function ToastViewport({
|
||||
testID={toast.testID ?? "app-toast"}
|
||||
style={[
|
||||
styles.toast,
|
||||
toast.variant === "success" ? styles.toastSuccess : null,
|
||||
toast.variant === "error" ? styles.toastError : null,
|
||||
{
|
||||
marginBottom: theme.spacing[4] + insets.bottom,
|
||||
marginTop: theme.spacing[2] + insets.top,
|
||||
opacity,
|
||||
transform: [{ translateY }],
|
||||
},
|
||||
@@ -221,16 +235,22 @@ function ToastViewport({
|
||||
accessibilityRole="alert"
|
||||
>
|
||||
{icon ? <View style={styles.iconSlot}>{icon}</View> : null}
|
||||
<Text
|
||||
testID="app-toast-message"
|
||||
style={[
|
||||
styles.message,
|
||||
toast.variant === "error" ? styles.messageError : null,
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{toast.message}
|
||||
</Text>
|
||||
{typeof toast.content === "string" ? (
|
||||
<Text
|
||||
testID="app-toast-message"
|
||||
style={[
|
||||
styles.message,
|
||||
toast.variant === "error" ? styles.messageError : null,
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{toast.content}
|
||||
</Text>
|
||||
) : (
|
||||
<View testID="app-toast-message" style={styles.contentSlot}>
|
||||
{toast.content}
|
||||
</View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
@@ -248,18 +268,18 @@ const styles = StyleSheet.create((theme) => ({
|
||||
position: "absolute",
|
||||
left: theme.spacing[4],
|
||||
right: theme.spacing[4],
|
||||
bottom: 0,
|
||||
top: 0,
|
||||
zIndex: OVERLAY_Z.toast,
|
||||
alignItems: "center",
|
||||
},
|
||||
toast: {
|
||||
width: "100%",
|
||||
maxWidth: 520,
|
||||
alignSelf: "center",
|
||||
maxWidth: "92%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[3],
|
||||
backgroundColor: theme.colors.surface2,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
backgroundColor: theme.colors.surface0,
|
||||
borderRadius: theme.borderRadius.full,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
paddingVertical: theme.spacing[3],
|
||||
@@ -270,15 +290,25 @@ const styles = StyleSheet.create((theme) => ({
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
toastSuccess: {
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
toastError: {
|
||||
borderColor: theme.colors.destructive,
|
||||
},
|
||||
iconSlot: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
contentSlot: {
|
||||
flexShrink: 1,
|
||||
minWidth: 0,
|
||||
},
|
||||
message: {
|
||||
flex: 1,
|
||||
flexShrink: 1,
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
messageError: {
|
||||
color: theme.colors.foreground,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
AppState,
|
||||
} from "react-native";
|
||||
import { useRouter } from "expo-router";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
@@ -28,14 +29,13 @@ import {
|
||||
Folder,
|
||||
RotateCcw,
|
||||
PanelRight,
|
||||
Info,
|
||||
CheckCircle2,
|
||||
} from "lucide-react-native";
|
||||
import { MenuHeader } from "@/components/headers/menu-header";
|
||||
import { BackHeader } from "@/components/headers/back-header";
|
||||
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
|
||||
import { AgentStreamView } from "@/components/agent-stream-view";
|
||||
import { AgentInputArea } from "@/components/agent-input-area";
|
||||
import { AgentDetailsSheet } from "@/components/agent-details-sheet";
|
||||
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
||||
import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import type { ImageAttachment } from "@/components/message-input";
|
||||
@@ -63,6 +63,7 @@ import { shortenPath } from "@/utils/shorten-path";
|
||||
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
|
||||
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
||||
import { useAgentInitialization } from "@/hooks/use-agent-initialization";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -178,9 +179,9 @@ function AgentScreenContent({
|
||||
agentId,
|
||||
}: AgentScreenContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const toast = useToast();
|
||||
const insets = useSafeAreaInsets();
|
||||
const router = useRouter();
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||
|
||||
@@ -471,6 +472,11 @@ function AgentScreenContent({
|
||||
}, [resolvedAgentId, serverId, shouldUseOptimisticStream]);
|
||||
|
||||
const effectiveAgent = agent ?? placeholderAgent;
|
||||
const providerLabel = (effectiveAgent?.provider ?? "Provider").replace(/^\w/, (m) =>
|
||||
m.toUpperCase()
|
||||
);
|
||||
const providerSessionId =
|
||||
effectiveAgent?.runtimeInfo?.sessionId ?? effectiveAgent?.persistence?.sessionId ?? null;
|
||||
|
||||
// Header subtitle: project path + branch (matching agent list row format)
|
||||
const headerProjectPath = effectiveAgent
|
||||
@@ -584,16 +590,6 @@ function AgentScreenContent({
|
||||
}
|
||||
}, [resolvedAgentId, agent?.status, client]);
|
||||
|
||||
const handleViewChanges = useCallback(() => {
|
||||
setExplorerTab("changes");
|
||||
openFileExplorer();
|
||||
}, [setExplorerTab, openFileExplorer]);
|
||||
|
||||
const handleBrowseFiles = useCallback(() => {
|
||||
setExplorerTab("files");
|
||||
openFileExplorer();
|
||||
}, [setExplorerTab, openFileExplorer]);
|
||||
|
||||
const handleRefreshAgent = useCallback(() => {
|
||||
if (!resolvedAgentId) {
|
||||
return;
|
||||
@@ -603,6 +599,24 @@ function AgentScreenContent({
|
||||
});
|
||||
}, [resolvedAgentId, refreshAgent]);
|
||||
|
||||
const handleCopyMeta = useCallback(
|
||||
async (label: string, value: string | null | undefined) => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Clipboard.setStringAsync(value);
|
||||
toast.show(`Copied ${label}`, {
|
||||
variant: "success",
|
||||
icon: <CheckCircle2 size={16} color={theme.colors.primary} />,
|
||||
});
|
||||
} catch {
|
||||
toast.error("Copy failed");
|
||||
}
|
||||
},
|
||||
[theme.colors.primary, toast]
|
||||
);
|
||||
|
||||
if (!effectiveAgent) {
|
||||
return (
|
||||
<View style={styles.container} testID="agent-not-found">
|
||||
@@ -677,19 +691,39 @@ function AgentScreenContent({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
||||
<View style={styles.menuMetaContainer}>
|
||||
<View style={styles.menuMetaRow}>
|
||||
<Text style={styles.menuMetaLabel}>Directory</Text>
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
void handleCopyMeta("Directory", effectiveAgent.cwd);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Directory
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.menuMetaValue}
|
||||
numberOfLines={2}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{shortenPath(effectiveAgent.cwd)}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<View style={styles.menuMetaRow}>
|
||||
<Text style={styles.menuMetaLabel}>Model</Text>
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
void handleCopyMeta("Model", modelDisplayValue);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Model
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.menuMetaValue}
|
||||
numberOfLines={1}
|
||||
@@ -697,48 +731,80 @@ function AgentScreenContent({
|
||||
>
|
||||
{modelDisplayValue}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{checkout?.isGit && checkout.currentBranch && checkout.currentBranch !== "HEAD" ? (
|
||||
<View style={styles.menuMetaRow}>
|
||||
<Text style={styles.menuMetaLabel}>Branch</Text>
|
||||
<View style={styles.menuMetaValueRow}>
|
||||
{checkoutStatusQuery.isFetching ? (
|
||||
<>
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
<Text style={styles.menuMetaPendingText}>Fetching…</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text
|
||||
style={styles.menuMetaValue}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{checkout.currentBranch}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
if (checkoutStatusQuery.isFetching) {
|
||||
return;
|
||||
}
|
||||
void handleCopyMeta("Branch", checkout.currentBranch);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Branch
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.menuMetaValue}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{checkoutStatusQuery.isFetching ? "Fetching…" : checkout.currentBranch}
|
||||
</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
(hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
onPress={() => {
|
||||
void handleCopyMeta("Paseo ID", effectiveAgent.id);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
Paseo ID
|
||||
</Text>
|
||||
<Text
|
||||
style={styles.menuMetaValue}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{effectiveAgent.id}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Pressable
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.menuMetaRow,
|
||||
providerSessionId && (hovered || pressed) && styles.menuMetaRowActive,
|
||||
]}
|
||||
disabled={!providerSessionId}
|
||||
onPress={() => {
|
||||
void handleCopyMeta(`${providerLabel} ID`, providerSessionId);
|
||||
}}
|
||||
>
|
||||
<Text style={styles.menuMetaLabel} numberOfLines={1}>
|
||||
{providerLabel} ID
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.menuMetaValue, !providerSessionId && styles.menuMetaValueError]}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="middle"
|
||||
>
|
||||
{providerSessionId ?? "Not available"}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
leading={<GitBranch size={16} color={theme.colors.foreground} />}
|
||||
onSelect={handleViewChanges}
|
||||
>
|
||||
View changes
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
leading={<Folder size={16} color={theme.colors.foreground} />}
|
||||
onSelect={handleBrowseFiles}
|
||||
>
|
||||
Browse files
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
leading={<RotateCcw size={16} color={theme.colors.foreground} />}
|
||||
disabled={isInitializing}
|
||||
@@ -755,16 +821,6 @@ function AgentScreenContent({
|
||||
>
|
||||
{isInitializing ? "Refreshing..." : "Refresh"}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem
|
||||
testID="agent-menu-details"
|
||||
leading={<Info size={16} color={theme.colors.foreground} />}
|
||||
onSelect={() => setDetailsOpen(true)}
|
||||
>
|
||||
Details
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</View>
|
||||
@@ -797,13 +853,6 @@ function AgentScreenContent({
|
||||
</View>
|
||||
</FileDropZone>
|
||||
|
||||
<AgentDetailsSheet
|
||||
visible={detailsOpen}
|
||||
onClose={() => setDetailsOpen(false)}
|
||||
agentId={effectiveAgent.id}
|
||||
persistenceSessionId={effectiveAgent.persistence?.sessionId ?? null}
|
||||
/>
|
||||
|
||||
{/* Explorer Sidebar - Desktop: inline, Mobile: overlay */}
|
||||
{!isMobile && isExplorerOpen && resolvedAgentId && (
|
||||
<ExplorerSidebar serverId={serverId} agentId={resolvedAgentId} cwd={effectiveAgent.cwd} />
|
||||
@@ -978,31 +1027,30 @@ const styles = StyleSheet.create((theme) => ({
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
},
|
||||
menuMetaContainer: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
gap: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
menuMetaRow: {
|
||||
gap: theme.spacing[1],
|
||||
minHeight: 32,
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
menuMetaRowActive: {
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
menuMetaLabel: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
letterSpacing: 0.5,
|
||||
textTransform: "uppercase",
|
||||
flexShrink: 0,
|
||||
},
|
||||
menuMetaValue: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
menuMetaValueRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
menuMetaPendingText: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
color: theme.colors.foregroundMuted,
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
textAlign: "right",
|
||||
},
|
||||
menuMetaValueError: {
|
||||
color: theme.colors.destructive,
|
||||
|
||||
Reference in New Issue
Block a user