refactor: extract explorer open gesture and standardize icon sizes
This commit is contained in:
@@ -52,6 +52,17 @@ import {
|
|||||||
import { getTauri } from "@/utils/tauri";
|
import { getTauri } from "@/utils/tauri";
|
||||||
|
|
||||||
polyfillCrypto();
|
polyfillCrypto();
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logLeftSidebarOpenGesture(
|
||||||
|
event: string,
|
||||||
|
details: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[LeftSidebarOpenGesture] ${event}`, details);
|
||||||
|
}
|
||||||
|
|
||||||
function PushNotificationRouter() {
|
function PushNotificationRouter() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -212,6 +223,10 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
})
|
})
|
||||||
.onStart(() => {
|
.onStart(() => {
|
||||||
isGesturing.value = true;
|
isGesturing.value = true;
|
||||||
|
runOnJS(logLeftSidebarOpenGesture)("start", {
|
||||||
|
mobileView,
|
||||||
|
openGestureEnabled,
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.onUpdate((event) => {
|
.onUpdate((event) => {
|
||||||
// Start from closed position (-windowWidth) and move towards 0
|
// Start from closed position (-windowWidth) and move towards 0
|
||||||
@@ -228,6 +243,13 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
isGesturing.value = false;
|
isGesturing.value = false;
|
||||||
// Open if dragged more than 1/3 of sidebar or fast swipe
|
// Open if dragged more than 1/3 of sidebar or fast swipe
|
||||||
const shouldOpen = event.translationX > windowWidth / 3 || event.velocityX > 500;
|
const shouldOpen = event.translationX > windowWidth / 3 || event.velocityX > 500;
|
||||||
|
runOnJS(logLeftSidebarOpenGesture)("end", {
|
||||||
|
translationX: event.translationX,
|
||||||
|
velocityX: event.velocityX,
|
||||||
|
shouldOpen,
|
||||||
|
mobileView,
|
||||||
|
openGestureEnabled,
|
||||||
|
});
|
||||||
if (shouldOpen) {
|
if (shouldOpen) {
|
||||||
animateToOpen();
|
animateToOpen();
|
||||||
runOnJS(openAgentList)();
|
runOnJS(openAgentList)();
|
||||||
@@ -246,6 +268,7 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
|
|||||||
animateToOpen,
|
animateToOpen,
|
||||||
animateToClose,
|
animateToClose,
|
||||||
openAgentList,
|
openAgentList,
|
||||||
|
mobileView,
|
||||||
isGesturing,
|
isGesturing,
|
||||||
horizontalScroll?.isAnyScrolledRight,
|
horizontalScroll?.isAnyScrolledRight,
|
||||||
touchStartX,
|
touchStartX,
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export function DropdownField({
|
|||||||
>
|
>
|
||||||
{value || placeholder}
|
{value || placeholder}
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronDown size={16} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronDown size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
||||||
@@ -196,7 +196,7 @@ export function SelectField({
|
|||||||
{value || placeholder || "Select..."}
|
{value || placeholder || "Select..."}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<ChevronRight size={20} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronRight size={defaultTheme.iconSize.lg} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
{errorMessage ? <Text style={styles.errorText}>{errorMessage}</Text> : null}
|
||||||
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
{warningMessage ? <Text style={styles.warningText}>{warningMessage}</Text> : null}
|
||||||
@@ -289,7 +289,7 @@ export function DropdownSheet({
|
|||||||
hitSlop={10}
|
hitSlop={10}
|
||||||
testID="dropdown-sheet-close"
|
testID="dropdown-sheet-close"
|
||||||
>
|
>
|
||||||
<X size={18} color={defaultTheme.colors.foregroundMuted} />
|
<X size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<BottomSheetScrollView
|
<BottomSheetScrollView
|
||||||
@@ -477,7 +477,7 @@ export function FormSelectTrigger({
|
|||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<ChevronDown size={16} color={defaultTheme.colors.foregroundMuted} />
|
<ChevronDown size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -571,7 +571,7 @@ export function AgentConfigRow({
|
|||||||
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
|
placeholder={providerOptions.length > 0 ? "Select..." : "No providers available"}
|
||||||
disabled={disabled || providerOptions.length === 0}
|
disabled={disabled || providerOptions.length === 0}
|
||||||
onSelect={onSelectProvider}
|
onSelect={onSelectProvider}
|
||||||
icon={<Bot size={16} color={defaultTheme.colors.foregroundMuted} />}
|
icon={<Bot size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -585,7 +585,7 @@ export function AgentConfigRow({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
isLoading={isModelLoading}
|
isLoading={isModelLoading}
|
||||||
onSelect={onSelectModel}
|
onSelect={onSelectModel}
|
||||||
icon={<Brain size={16} color={defaultTheme.colors.foregroundMuted} />}
|
icon={<Brain size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -598,7 +598,7 @@ export function AgentConfigRow({
|
|||||||
placeholder="Default"
|
placeholder="Default"
|
||||||
disabled={disabled || modeOptions.length === 0}
|
disabled={disabled || modeOptions.length === 0}
|
||||||
onSelect={onSelectMode}
|
onSelect={onSelectMode}
|
||||||
icon={<Shield size={16} color={defaultTheme.colors.foregroundMuted} />}
|
icon={<Shield size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -612,7 +612,7 @@ export function AgentConfigRow({
|
|||||||
placeholder="Select..."
|
placeholder="Select..."
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onSelect={onSelectThinkingOption}
|
onSelect={onSelectThinkingOption}
|
||||||
icon={<Brain size={16} color={defaultTheme.colors.foregroundMuted} />}
|
icon={<Brain size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -1120,16 +1120,16 @@ export function GitOptionsSection({
|
|||||||
onSubmitEditing={handleConfirmEdit}
|
onSubmitEditing={handleConfirmEdit}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={handleConfirmEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
<Pressable onPress={handleConfirmEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
||||||
<Check size={16} color={defaultTheme.colors.palette.green[500]} />
|
<Check size={defaultTheme.iconSize.md} color={defaultTheme.colors.palette.green[500]} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
<Pressable onPress={handleCancelEdit} hitSlop={8} style={styles.baseBranchIconButton}>
|
||||||
<X size={16} color={defaultTheme.colors.foregroundMuted} />
|
<X size={defaultTheme.iconSize.md} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
<Pressable onPress={handleStartEdit} style={styles.baseBranchValueRow}>
|
||||||
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
<Text style={styles.baseBranchValue}>{displayBranch}</Text>
|
||||||
<Pencil size={14} color={defaultTheme.colors.foregroundMuted} />
|
<Pencil size={defaultTheme.iconSize.sm} color={defaultTheme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -562,7 +562,7 @@ export function AgentInputArea({
|
|||||||
{isCancellingAgent ? (
|
{isCancellingAgent ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<ActivityIndicator size="small" color="white" />
|
||||||
) : (
|
) : (
|
||||||
<Square size={18} color="white" fill="white" />
|
<Square size={theme.iconSize.lg} color="white" fill="white" />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
@@ -591,7 +591,7 @@ export function AgentInputArea({
|
|||||||
{voice?.isVoiceSwitching ? (
|
{voice?.isVoiceSwitching ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<ActivityIndicator size="small" color="white" />
|
||||||
) : (
|
) : (
|
||||||
<AudioLines size={18} color={theme.colors.foreground} />
|
<AudioLines size={theme.iconSize.lg} color={theme.colors.foreground} />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
@@ -628,13 +628,13 @@ export function AgentInputArea({
|
|||||||
onPress={() => handleEditQueuedMessage(item.id)}
|
onPress={() => handleEditQueuedMessage(item.id)}
|
||||||
style={styles.queueActionButton}
|
style={styles.queueActionButton}
|
||||||
>
|
>
|
||||||
<Pencil size={14} color={theme.colors.foreground} />
|
<Pencil size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => handleSendQueuedNow(item.id)}
|
onPress={() => handleSendQueuedNow(item.id)}
|
||||||
style={[styles.queueActionButton, styles.queueSendButton]}
|
style={[styles.queueActionButton, styles.queueSendButton]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={14} color="white" />
|
<ArrowUp size={theme.iconSize.sm} color="white" />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
agent.currentModeId ||
|
agent.currentModeId ||
|
||||||
"default"}
|
"default"}
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="top"
|
side="top"
|
||||||
@@ -157,7 +157,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
testID="agent-model-selector"
|
testID="agent-model-selector"
|
||||||
>
|
>
|
||||||
<Text style={styles.modeBadgeText}>{displayModel}</Text>
|
<Text style={styles.modeBadgeText}>{displayModel}</Text>
|
||||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="top"
|
side="top"
|
||||||
@@ -199,9 +199,13 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
accessibilityLabel="Select thinking option"
|
accessibilityLabel="Select thinking option"
|
||||||
testID="agent-thinking-selector"
|
testID="agent-thinking-selector"
|
||||||
>
|
>
|
||||||
<Brain size={12} color={theme.colors.foregroundMuted} style={{ marginTop: 1 }} />
|
<Brain
|
||||||
|
size={theme.iconSize.xs}
|
||||||
|
color={theme.colors.foregroundMuted}
|
||||||
|
style={{ marginTop: 1 }}
|
||||||
|
/>
|
||||||
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
<Text style={styles.modeBadgeText}>{displayThinking}</Text>
|
||||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
side="top"
|
side="top"
|
||||||
@@ -249,7 +253,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
accessibilityLabel="Agent preferences"
|
accessibilityLabel="Agent preferences"
|
||||||
testID="agent-preferences-button"
|
testID="agent-preferences-button"
|
||||||
>
|
>
|
||||||
<SlidersHorizontal size={20} color={theme.colors.foreground} />
|
<SlidersHorizontal size={theme.iconSize.lg} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<AdaptiveModalSheet
|
<AdaptiveModalSheet
|
||||||
@@ -271,7 +275,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
testID="agent-preferences-mode"
|
testID="agent-preferences-mode"
|
||||||
>
|
>
|
||||||
<Text style={styles.sheetSelectText}>{displayMode}</Text>
|
<Text style={styles.sheetSelectText}>{displayMode}</Text>
|
||||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="top" align="start">
|
<DropdownMenuContent side="top" align="start">
|
||||||
{agent.availableModes.map((mode) => {
|
{agent.availableModes.map((mode) => {
|
||||||
@@ -303,7 +307,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
testID="agent-preferences-model"
|
testID="agent-preferences-model"
|
||||||
>
|
>
|
||||||
<Text style={styles.sheetSelectText}>{displayModel}</Text>
|
<Text style={styles.sheetSelectText}>{displayModel}</Text>
|
||||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="top" align="start">
|
<DropdownMenuContent side="top" align="start">
|
||||||
{models?.map((model) => {
|
{models?.map((model) => {
|
||||||
@@ -342,7 +346,7 @@ export function AgentStatusBar({ agentId, serverId }: AgentStatusBarProps) {
|
|||||||
testID="agent-preferences-thinking"
|
testID="agent-preferences-thinking"
|
||||||
>
|
>
|
||||||
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
<Text style={styles.sheetSelectText}>{displayThinking}</Text>
|
||||||
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="top" align="start">
|
<DropdownMenuContent side="top" align="start">
|
||||||
{thinkingOptions.map((opt) => {
|
{thinkingOptions.map((opt) => {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export function DictationControls({
|
|||||||
accessibilityLabel="Start voice dictation"
|
accessibilityLabel="Start voice dictation"
|
||||||
style={[styles.micButton, disabled && styles.buttonDisabled]}
|
style={[styles.micButton, disabled && styles.buttonDisabled]}
|
||||||
>
|
>
|
||||||
<Mic size={16} color={theme.colors.foreground} />
|
<Mic size={theme.iconSize.md} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export function DictationControls({
|
|||||||
actionsDisabled && !isFailed ? styles.buttonDisabled : undefined,
|
actionsDisabled && !isFailed ? styles.buttonDisabled : undefined,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<X size={14} color={theme.colors.foreground} />
|
<X size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{actionsDisabled ? (
|
{actionsDisabled ? (
|
||||||
<View style={styles.loadingContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
@@ -100,7 +100,7 @@ export function DictationControls({
|
|||||||
accessibilityLabel="Retry dictation"
|
accessibilityLabel="Retry dictation"
|
||||||
style={[styles.actionButton, styles.actionButtonConfirm]}
|
style={[styles.actionButton, styles.actionButtonConfirm]}
|
||||||
>
|
>
|
||||||
<RefreshCcw size={14} color={theme.colors.surface0} />
|
<RefreshCcw size={theme.iconSize.sm} color={theme.colors.surface0} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -109,14 +109,14 @@ export function DictationControls({
|
|||||||
accessibilityLabel="Insert transcription"
|
accessibilityLabel="Insert transcription"
|
||||||
style={[styles.actionButton, styles.actionButtonSecondary]}
|
style={[styles.actionButton, styles.actionButtonSecondary]}
|
||||||
>
|
>
|
||||||
<Check size={14} color={theme.colors.foreground} />
|
<Check size={theme.iconSize.sm} color={theme.colors.foreground} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onAcceptAndSend}
|
onPress={onAcceptAndSend}
|
||||||
accessibilityLabel="Insert transcription and send"
|
accessibilityLabel="Insert transcription and send"
|
||||||
style={[styles.actionButton, styles.actionButtonConfirm]}
|
style={[styles.actionButton, styles.actionButtonConfirm]}
|
||||||
>
|
>
|
||||||
<ArrowUp size={14} color={theme.colors.surface0} />
|
<ArrowUp size={theme.iconSize.sm} color={theme.colors.surface0} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -167,7 +167,7 @@ export function DictationOverlay({
|
|||||||
actionsDisabled && !isFailed && overlayStyles.buttonDisabled,
|
actionsDisabled && !isFailed && overlayStyles.buttonDisabled,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<X size={20} color={theme.colors.palette.white} strokeWidth={2.5} />
|
<X size={theme.iconSize.lg} color={theme.colors.palette.white} strokeWidth={2.5} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<View style={overlayStyles.centerContainer}>
|
<View style={overlayStyles.centerContainer}>
|
||||||
@@ -219,7 +219,7 @@ export function DictationOverlay({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<RefreshCcw
|
<RefreshCcw
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={theme.colors.accent}
|
color={theme.colors.accent}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
@@ -234,7 +234,7 @@ export function DictationOverlay({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Pencil
|
<Pencil
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={theme.colors.palette.white}
|
color={theme.colors.palette.white}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
@@ -247,7 +247,7 @@ export function DictationOverlay({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<ArrowUp
|
<ArrowUp
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={theme.colors.accent}
|
color={theme.colors.accent}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,6 +24,14 @@ import { TerminalPane } from "./terminal-pane";
|
|||||||
|
|
||||||
const MIN_CHAT_WIDTH = 400;
|
const MIN_CHAT_WIDTH = 400;
|
||||||
const IOS_KEYBOARD_INSET_MIN_HEIGHT = 120;
|
const IOS_KEYBOARD_INSET_MIN_HEIGHT = 120;
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logExplorerSidebar(event: string, details: Record<string, unknown>): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[ExplorerSidebar] ${event}`, details);
|
||||||
|
}
|
||||||
|
|
||||||
function resolveKeyboardShift(rawHeight: number, inset: number): number {
|
function resolveKeyboardShift(rawHeight: number, inset: number): number {
|
||||||
"worklet";
|
"worklet";
|
||||||
@@ -94,9 +102,18 @@ export function ExplorerSidebar({ serverId, agentId, cwd, isGit }: ExplorerSideb
|
|||||||
const startWidthRef = useRef(explorerWidth);
|
const startWidthRef = useRef(explorerWidth);
|
||||||
const resizeWidth = useSharedValue(explorerWidth);
|
const resizeWidth = useSharedValue(explorerWidth);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(
|
||||||
closeToAgent();
|
(reason: string) => {
|
||||||
}, [closeToAgent]);
|
logExplorerSidebar("handleClose", {
|
||||||
|
reason,
|
||||||
|
isOpen,
|
||||||
|
mobileView,
|
||||||
|
desktopFileExplorerOpen,
|
||||||
|
});
|
||||||
|
closeToAgent();
|
||||||
|
},
|
||||||
|
[closeToAgent, desktopFileExplorerOpen, isOpen, mobileView]
|
||||||
|
);
|
||||||
|
|
||||||
const enableSidebarCloseGesture = isMobile && isOpen;
|
const enableSidebarCloseGesture = isMobile && isOpen;
|
||||||
|
|
||||||
@@ -165,9 +182,15 @@ export function ExplorerSidebar({ serverId, agentId, cwd, isGit }: ExplorerSideb
|
|||||||
isGesturing.value = false;
|
isGesturing.value = false;
|
||||||
const shouldClose =
|
const shouldClose =
|
||||||
event.translationX > windowWidth / 3 || event.velocityX > 500;
|
event.translationX > windowWidth / 3 || event.velocityX > 500;
|
||||||
|
runOnJS(logExplorerSidebar)("closeGestureEnd", {
|
||||||
|
translationX: event.translationX,
|
||||||
|
velocityX: event.velocityX,
|
||||||
|
shouldClose,
|
||||||
|
windowWidth,
|
||||||
|
});
|
||||||
if (shouldClose) {
|
if (shouldClose) {
|
||||||
animateToClose();
|
animateToClose();
|
||||||
runOnJS(handleClose)();
|
runOnJS(handleClose)("swipe-close-gesture");
|
||||||
} else {
|
} else {
|
||||||
animateToOpen();
|
animateToOpen();
|
||||||
}
|
}
|
||||||
@@ -249,7 +272,10 @@ export function ExplorerSidebar({ serverId, agentId, cwd, isGit }: ExplorerSideb
|
|||||||
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
|
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
|
||||||
{/* Backdrop */}
|
{/* Backdrop */}
|
||||||
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]}>
|
<Animated.View style={[styles.backdrop, backdropAnimatedStyle]}>
|
||||||
<Pressable style={styles.backdropPressable} onPress={handleClose} />
|
<Pressable
|
||||||
|
style={styles.backdropPressable}
|
||||||
|
onPress={() => handleClose("backdrop-press")}
|
||||||
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
<GestureDetector gesture={closeGesture} touchAction="pan-y">
|
||||||
@@ -265,7 +291,7 @@ export function ExplorerSidebar({ serverId, agentId, cwd, isGit }: ExplorerSideb
|
|||||||
<SidebarContent
|
<SidebarContent
|
||||||
activeTab={explorerTab}
|
activeTab={explorerTab}
|
||||||
onTabPress={handleTabPress}
|
onTabPress={handleTabPress}
|
||||||
onClose={handleClose}
|
onClose={() => handleClose("header-close-button")}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
cwd={cwd}
|
cwd={cwd}
|
||||||
@@ -298,7 +324,7 @@ export function ExplorerSidebar({ serverId, agentId, cwd, isGit }: ExplorerSideb
|
|||||||
<SidebarContent
|
<SidebarContent
|
||||||
activeTab={explorerTab}
|
activeTab={explorerTab}
|
||||||
onTabPress={handleTabPress}
|
onTabPress={handleTabPress}
|
||||||
onClose={handleClose}
|
onClose={() => handleClose("desktop-close-button")}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
agentId={agentId}
|
agentId={agentId}
|
||||||
cwd={cwd}
|
cwd={cwd}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function BackHeader({
|
|||||||
onPress={onBack ?? (() => router.back())}
|
onPress={onBack ?? (() => router.back())}
|
||||||
style={styles.backButton}
|
style={styles.backButton}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} color={theme.colors.foregroundMuted} />
|
<ArrowLeft size={theme.iconSize.lg} color={theme.colors.foregroundMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{title && (
|
{title && (
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function SidebarMenuToggle({
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<MobileMenuIcon color={menuIconColor} />
|
<MobileMenuIcon color={menuIconColor} />
|
||||||
) : (
|
) : (
|
||||||
<PanelLeft size={16} color={menuIconColor} />
|
<PanelLeft size={theme.iconSize.md} color={menuIconColor} />
|
||||||
)}
|
)}
|
||||||
</HeaderToggleButton>
|
</HeaderToggleButton>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -645,7 +645,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
(hovered || !IS_WEB) && styles.removeImageButtonVisible,
|
(hovered || !IS_WEB) && styles.removeImageButtonVisible,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<X size={16} color="white" />
|
<X size={theme.iconSize.md} color="white" />
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -703,7 +703,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]}
|
style={[styles.attachButton, (!isConnected || disabled) && styles.buttonDisabled]}
|
||||||
>
|
>
|
||||||
<Paperclip size={20} color={theme.colors.foreground} />
|
<Paperclip size={theme.iconSize.lg} color={theme.colors.foreground} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
<Text style={styles.tooltipText}>Attach images</Text>
|
<Text style={styles.tooltipText}>Attach images</Text>
|
||||||
@@ -736,11 +736,11 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isDictating ? (
|
{isDictating ? (
|
||||||
<Square size={14} color="white" fill="white" />
|
<Square size={theme.iconSize.lg} color="white" fill="white" />
|
||||||
) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? (
|
) : isRealtimeVoiceForCurrentAgent && voice?.isMuted ? (
|
||||||
<MicOff size={20} color={theme.colors.foreground} />
|
<MicOff size={theme.iconSize.lg} color={theme.colors.foreground} />
|
||||||
) : (
|
) : (
|
||||||
<Mic size={20} color={theme.colors.foreground} />
|
<Mic size={theme.iconSize.lg} color={theme.colors.foreground} />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
@@ -769,7 +769,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]}
|
style={[styles.queueButton, (!isConnected || disabled) && styles.buttonDisabled]}
|
||||||
>
|
>
|
||||||
<Plus size={20} color="white" />
|
<Plus size={theme.iconSize.lg} color="white" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
<View style={styles.tooltipRow}>
|
<View style={styles.tooltipRow}>
|
||||||
@@ -791,7 +791,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(funct
|
|||||||
{isSubmitLoading ? (
|
{isSubmitLoading ? (
|
||||||
<ActivityIndicator size="small" color="white" />
|
<ActivityIndicator size="small" color="white" />
|
||||||
) : (
|
) : (
|
||||||
<ArrowUp size={20} color="white" />
|
<ArrowUp size={theme.iconSize.lg} color="white" />
|
||||||
)}
|
)}
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="top" align="center" offset={8}>
|
<TooltipContent side="top" align="center" offset={8}>
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ export function RealtimeVoiceOverlay({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<MicOff size={20} color={theme.colors.palette.white} strokeWidth={2.5} />
|
<MicOff size={theme.iconSize.lg} color={theme.colors.palette.white} strokeWidth={2.5} />
|
||||||
) : (
|
) : (
|
||||||
<Mic size={20} color={theme.colors.foreground} strokeWidth={2.5} />
|
<Mic size={theme.iconSize.lg} color={theme.colors.foreground} strokeWidth={2.5} />
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ export function RealtimeVoiceOverlay({
|
|||||||
<ActivityIndicator size="small" color={theme.colors.palette.white} />
|
<ActivityIndicator size="small" color={theme.colors.palette.white} />
|
||||||
) : (
|
) : (
|
||||||
<Square
|
<Square
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={theme.colors.palette.white}
|
color={theme.colors.palette.white}
|
||||||
fill={theme.colors.palette.white}
|
fill={theme.colors.palette.white}
|
||||||
strokeWidth={2.5}
|
strokeWidth={2.5}
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ function ProjectFilterOptionRow({
|
|||||||
{option.activeCount > 0 ? (
|
{option.activeCount > 0 ? (
|
||||||
<Text style={styles.filterOptionCount}>{option.activeCount}</Text>
|
<Text style={styles.filterOptionCount}>{option.activeCount}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
{selected ? <Check size={14} color={theme.colors.foregroundMuted} /> : null}
|
{selected ? <Check size={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> : null}
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
@@ -247,7 +247,7 @@ function SidebarAgentRow({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{isBatchSelected ? (
|
{isBatchSelected ? (
|
||||||
<Check size={12} color={theme.colors.primaryForeground} />
|
<Check size={theme.iconSize.xs} color={theme.colors.primaryForeground} />
|
||||||
) : null}
|
) : null}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
@@ -301,10 +301,10 @@ function SidebarAgentRow({
|
|||||||
isArchiving ? (
|
isArchiving ? (
|
||||||
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
||||||
) : isArchiveConfirmVisible ? (
|
) : isArchiveConfirmVisible ? (
|
||||||
<Check size={12} color={theme.colors.foreground} />
|
<Check size={theme.iconSize.xs} color={theme.colors.foreground} />
|
||||||
) : (
|
) : (
|
||||||
<Archive
|
<Archive
|
||||||
size={12}
|
size={theme.iconSize.xs}
|
||||||
color={
|
color={
|
||||||
archiveHovered
|
archiveHovered
|
||||||
? theme.colors.foreground
|
? theme.colors.foreground
|
||||||
@@ -785,7 +785,7 @@ export function SidebarAgentList({
|
|||||||
{selectedProjectLabel}
|
{selectedProjectLabel}
|
||||||
</Text>
|
</Text>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
size={14}
|
size={theme.iconSize.sm}
|
||||||
color={
|
color={
|
||||||
showActiveForeground
|
showActiveForeground
|
||||||
? theme.colors.foreground
|
? theme.colors.foreground
|
||||||
@@ -916,13 +916,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
selectedProjectIcon: {
|
selectedProjectIcon: {
|
||||||
width: 14,
|
width: theme.iconSize.sm,
|
||||||
height: 14,
|
height: theme.iconSize.sm,
|
||||||
borderRadius: theme.borderRadius.sm,
|
borderRadius: theme.borderRadius.sm,
|
||||||
},
|
},
|
||||||
projectCountBadge: {
|
projectCountBadge: {
|
||||||
minWidth: 16,
|
minWidth: theme.iconSize.md,
|
||||||
height: 16,
|
height: theme.iconSize.md,
|
||||||
borderRadius: theme.borderRadius.full,
|
borderRadius: theme.borderRadius.full,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
@@ -987,13 +987,13 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
},
|
},
|
||||||
projectIcon: {
|
projectIcon: {
|
||||||
width: 14,
|
width: theme.iconSize.sm,
|
||||||
height: 14,
|
height: theme.iconSize.sm,
|
||||||
borderRadius: theme.borderRadius.sm,
|
borderRadius: theme.borderRadius.sm,
|
||||||
},
|
},
|
||||||
projectIconFallback: {
|
projectIconFallback: {
|
||||||
width: 14,
|
width: theme.iconSize.sm,
|
||||||
height: 14,
|
height: theme.iconSize.sm,
|
||||||
borderRadius: theme.borderRadius.sm,
|
borderRadius: theme.borderRadius.sm,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
@@ -1039,8 +1039,8 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
minHeight: 20,
|
minHeight: 20,
|
||||||
},
|
},
|
||||||
checkbox: {
|
checkbox: {
|
||||||
width: 16,
|
width: theme.iconSize.md,
|
||||||
height: 16,
|
height: theme.iconSize.md,
|
||||||
borderRadius: theme.borderRadius.sm,
|
borderRadius: theme.borderRadius.sm,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: theme.colors.border,
|
borderColor: theme.colors.border,
|
||||||
@@ -1100,8 +1100,8 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
agentMetaProjectIcon: {
|
agentMetaProjectIcon: {
|
||||||
width: 14,
|
width: theme.iconSize.sm,
|
||||||
height: 14,
|
height: theme.iconSize.sm,
|
||||||
borderRadius: theme.borderRadius.sm,
|
borderRadius: theme.borderRadius.sm,
|
||||||
},
|
},
|
||||||
agentMetaProjectIconInactive: {
|
agentMetaProjectIconInactive: {
|
||||||
|
|||||||
@@ -356,7 +356,10 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<>
|
<>
|
||||||
<Plus size={18} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
|
<Plus
|
||||||
|
size={theme.iconSize.md}
|
||||||
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
<Text style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}>New agent</Text>
|
<Text style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}>New agent</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -418,7 +421,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<Users
|
<Users
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -435,7 +438,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<Settings
|
<Settings
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -479,7 +482,10 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<>
|
<>
|
||||||
<Plus size={18} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
|
<Plus
|
||||||
|
size={theme.iconSize.md}
|
||||||
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
<Text style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}>New agent</Text>
|
<Text style={[styles.newAgentButtonText, hovered && styles.newAgentButtonTextHovered]}>New agent</Text>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -539,7 +545,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<Users
|
<Users
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -555,7 +561,10 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
|||||||
onPress={handleSettingsDesktop}
|
onPress={handleSettingsDesktop}
|
||||||
>
|
>
|
||||||
{({ hovered }) => (
|
{({ hovered }) => (
|
||||||
<Settings size={20} color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted} />
|
<Settings
|
||||||
|
size={theme.iconSize.lg}
|
||||||
|
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export function SegmentedControl<T extends string>({
|
|||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd;
|
const segmentSizeStyle = size === "sm" ? styles.segmentSm : styles.segmentMd;
|
||||||
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
|
const labelSizeStyle = size === "sm" ? styles.labelSm : styles.labelMd;
|
||||||
const iconSize = size === "sm" ? 14 : 16;
|
const iconSize = size === "sm" ? theme.iconSize.sm : theme.iconSize.md;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.container, style]} testID={testID}>
|
<View style={[styles.container, style]} testID={testID}>
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ import { usePanelStore } from "@/stores/panel-store";
|
|||||||
|
|
||||||
const ANIMATION_DURATION = 220;
|
const ANIMATION_DURATION = 220;
|
||||||
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
const ANIMATION_EASING = Easing.bezier(0.25, 0.1, 0.25, 1);
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logExplorerAnimation(
|
||||||
|
event: string,
|
||||||
|
details: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[ExplorerAnimation] ${event}`, details);
|
||||||
|
}
|
||||||
|
|
||||||
interface ExplorerSidebarAnimationContextValue {
|
interface ExplorerSidebarAnimationContextValue {
|
||||||
translateX: SharedValue<number>;
|
translateX: SharedValue<number>;
|
||||||
@@ -50,13 +61,28 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
|||||||
if (prevIsOpen.current === isOpen) {
|
if (prevIsOpen.current === isOpen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const previousIsOpen = prevIsOpen.current;
|
||||||
prevIsOpen.current = isOpen;
|
prevIsOpen.current = isOpen;
|
||||||
|
|
||||||
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
|
// Don't animate if we're in the middle of a gesture - the gesture handler will handle it
|
||||||
if (isGesturing.value) {
|
if (isGesturing.value) {
|
||||||
|
logExplorerAnimation("sync-skipped-during-gesture", {
|
||||||
|
previousIsOpen,
|
||||||
|
nextIsOpen: isOpen,
|
||||||
|
mobileView,
|
||||||
|
desktopFileExplorerOpen,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logExplorerAnimation("sync-state-change", {
|
||||||
|
previousIsOpen,
|
||||||
|
nextIsOpen: isOpen,
|
||||||
|
mobileView,
|
||||||
|
desktopFileExplorerOpen,
|
||||||
|
windowWidth,
|
||||||
|
});
|
||||||
|
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
translateX.value = withTiming(0, {
|
translateX.value = withTiming(0, {
|
||||||
duration: ANIMATION_DURATION,
|
duration: ANIMATION_DURATION,
|
||||||
@@ -76,7 +102,15 @@ export function ExplorerSidebarAnimationProvider({ children }: { children: React
|
|||||||
easing: ANIMATION_EASING,
|
easing: ANIMATION_EASING,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isOpen, translateX, backdropOpacity, windowWidth, isGesturing]);
|
}, [
|
||||||
|
isOpen,
|
||||||
|
translateX,
|
||||||
|
backdropOpacity,
|
||||||
|
windowWidth,
|
||||||
|
isGesturing,
|
||||||
|
mobileView,
|
||||||
|
desktopFileExplorerOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
const animateToOpen = () => {
|
const animateToOpen = () => {
|
||||||
"worklet";
|
"worklet";
|
||||||
|
|||||||
@@ -51,4 +51,19 @@ describe("useArchiveAgent", () => {
|
|||||||
})
|
})
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes an archived agent from cached list payloads", () => {
|
||||||
|
const payload = {
|
||||||
|
entries: [
|
||||||
|
{ agent: { id: "agent-1" } },
|
||||||
|
{ agent: { id: "agent-2" } },
|
||||||
|
],
|
||||||
|
pageInfo: { hasMore: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = __private__.removeAgentFromListPayload(payload, "agent-1");
|
||||||
|
|
||||||
|
expect(next.entries).toEqual([{ agent: { id: "agent-2" } }]);
|
||||||
|
expect(next.pageInfo).toEqual({ hasMore: false });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ interface IsAgentArchivingInput extends ArchiveAgentInput {
|
|||||||
queryClient: QueryClient;
|
queryClient: QueryClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AgentsListQueryData {
|
||||||
|
entries?: Array<{ agent?: { id?: string | null } | null } | null>;
|
||||||
|
}
|
||||||
|
|
||||||
function toArchiveKey(input: ArchiveAgentInput): string {
|
function toArchiveKey(input: ArchiveAgentInput): string {
|
||||||
const serverId = input.serverId.trim();
|
const serverId = input.serverId.trim();
|
||||||
const agentId = input.agentId.trim();
|
const agentId = input.agentId.trim();
|
||||||
@@ -73,6 +77,66 @@ function isAgentArchiving(input: IsAgentArchivingInput): boolean {
|
|||||||
return Boolean(readPendingState(input.queryClient)[key]);
|
return Boolean(readPendingState(input.queryClient)[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeAgentFromListPayload<T extends AgentsListQueryData | undefined>(
|
||||||
|
payload: T,
|
||||||
|
agentId: string
|
||||||
|
): T {
|
||||||
|
if (!payload || !Array.isArray(payload.entries) || !agentId) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
const filtered = payload.entries.filter((entry) => entry?.agent?.id !== agentId);
|
||||||
|
if (filtered.length === payload.entries.length) {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
entries: filtered,
|
||||||
|
} as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAgentFromCachedLists(
|
||||||
|
queryClient: QueryClient,
|
||||||
|
input: ArchiveAgentInput
|
||||||
|
): void {
|
||||||
|
const agentId = input.agentId.trim();
|
||||||
|
if (!agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.setQueryData<AgentsListQueryData | undefined>(
|
||||||
|
["sidebarAgentsList", input.serverId],
|
||||||
|
(current) => removeAgentFromListPayload(current, agentId)
|
||||||
|
);
|
||||||
|
queryClient.setQueryData<AgentsListQueryData | undefined>(
|
||||||
|
["allAgents", input.serverId],
|
||||||
|
(current) => removeAgentFromListPayload(current, agentId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: string }): void {
|
||||||
|
const archivedAt = new Date(input.archivedAt);
|
||||||
|
if (Number.isNaN(archivedAt.getTime())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAgents = useSessionStore.getState().setAgents;
|
||||||
|
setAgents(input.serverId, (prev) => {
|
||||||
|
const existing = prev.get(input.agentId);
|
||||||
|
if (!existing) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
if (existing.archivedAt && existing.archivedAt.getTime() === archivedAt.getTime()) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.set(input.agentId, {
|
||||||
|
...existing,
|
||||||
|
archivedAt,
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
|
export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
|
||||||
setAgentArchiving({
|
setAgentArchiving({
|
||||||
...input,
|
...input,
|
||||||
@@ -92,12 +156,12 @@ export function useArchiveAgent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const archiveMutation = useMutation({
|
const archiveMutation = useMutation({
|
||||||
mutationFn: async (input: ArchiveAgentInput) => {
|
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
|
||||||
const client = useSessionStore.getState().sessions[input.serverId]?.client ?? null;
|
const client = useSessionStore.getState().sessions[input.serverId]?.client ?? null;
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon client not available");
|
throw new Error("Daemon client not available");
|
||||||
}
|
}
|
||||||
await client.archiveAgent(input.agentId);
|
return await client.archiveAgent(input.agentId);
|
||||||
},
|
},
|
||||||
onMutate: (input) => {
|
onMutate: (input) => {
|
||||||
setAgentArchiving({
|
setAgentArchiving({
|
||||||
@@ -107,7 +171,21 @@ export function useArchiveAgent() {
|
|||||||
isArchiving: true,
|
isArchiving: true,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onError: (_error, input) => {
|
onSuccess: (result, input) => {
|
||||||
|
markAgentArchivedInStore({
|
||||||
|
serverId: input.serverId,
|
||||||
|
agentId: input.agentId,
|
||||||
|
archivedAt: result.archivedAt,
|
||||||
|
});
|
||||||
|
removeAgentFromCachedLists(queryClient, input);
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ["sidebarAgentsList", input.serverId],
|
||||||
|
});
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: ["allAgents", input.serverId],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: (_result, _error, input) => {
|
||||||
clearArchiveAgentPending({
|
clearArchiveAgentPending({
|
||||||
queryClient,
|
queryClient,
|
||||||
serverId: input.serverId,
|
serverId: input.serverId,
|
||||||
@@ -154,4 +232,5 @@ export const __private__ = {
|
|||||||
readPendingState,
|
readPendingState,
|
||||||
setAgentArchiving,
|
setAgentArchiving,
|
||||||
isAgentArchiving,
|
isAgentArchiving,
|
||||||
|
removeAgentFromListPayload,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { Gesture } from "react-native-gesture-handler";
|
||||||
|
import {
|
||||||
|
Extrapolation,
|
||||||
|
interpolate,
|
||||||
|
runOnJS,
|
||||||
|
useSharedValue,
|
||||||
|
} from "react-native-reanimated";
|
||||||
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
|
|
||||||
|
interface UseExplorerOpenGestureParams {
|
||||||
|
enabled: boolean;
|
||||||
|
onOpen: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logExplorerOpenGesture(
|
||||||
|
event: string,
|
||||||
|
details: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[ExplorerOpenGesture] ${event}`, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useExplorerOpenGesture({
|
||||||
|
enabled,
|
||||||
|
onOpen,
|
||||||
|
}: UseExplorerOpenGestureParams) {
|
||||||
|
const {
|
||||||
|
translateX,
|
||||||
|
backdropOpacity,
|
||||||
|
windowWidth,
|
||||||
|
animateToOpen,
|
||||||
|
animateToClose,
|
||||||
|
isGesturing,
|
||||||
|
} = useExplorerSidebarAnimation();
|
||||||
|
const touchStartX = useSharedValue(0);
|
||||||
|
const touchStartY = useSharedValue(0);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() =>
|
||||||
|
Gesture.Pan()
|
||||||
|
.enabled(enabled)
|
||||||
|
.manualActivation(true)
|
||||||
|
.onTouchesDown((event) => {
|
||||||
|
const touch = event.changedTouches[0];
|
||||||
|
if (!touch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
touchStartX.value = touch.absoluteX;
|
||||||
|
touchStartY.value = touch.absoluteY;
|
||||||
|
})
|
||||||
|
.onTouchesMove((event, stateManager) => {
|
||||||
|
const touch = event.changedTouches[0];
|
||||||
|
if (!touch || event.numberOfTouches !== 1) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaX = touch.absoluteX - touchStartX.value;
|
||||||
|
const deltaY = touch.absoluteY - touchStartY.value;
|
||||||
|
const absDeltaX = Math.abs(deltaX);
|
||||||
|
const absDeltaY = Math.abs(deltaY);
|
||||||
|
|
||||||
|
// Fail quickly on rightward or clearly vertical intent.
|
||||||
|
if (deltaX >= 10) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate only on intentional leftward movement.
|
||||||
|
if (deltaX <= -15 && absDeltaX > absDeltaY) {
|
||||||
|
stateManager.activate();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onStart(() => {
|
||||||
|
isGesturing.value = true;
|
||||||
|
runOnJS(logExplorerOpenGesture)("start", { enabled });
|
||||||
|
})
|
||||||
|
.onUpdate((event) => {
|
||||||
|
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
||||||
|
const newTranslateX = Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(windowWidth, windowWidth + event.translationX)
|
||||||
|
);
|
||||||
|
translateX.value = newTranslateX;
|
||||||
|
backdropOpacity.value = interpolate(
|
||||||
|
newTranslateX,
|
||||||
|
[windowWidth, 0],
|
||||||
|
[0, 1],
|
||||||
|
Extrapolation.CLAMP
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.onEnd((event) => {
|
||||||
|
isGesturing.value = false;
|
||||||
|
const shouldOpenByPosition = translateX.value < (windowWidth * 2) / 3;
|
||||||
|
const shouldOpenByVelocity = event.velocityX < -500;
|
||||||
|
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
|
||||||
|
runOnJS(logExplorerOpenGesture)("end", {
|
||||||
|
translationX: event.translationX,
|
||||||
|
velocityX: event.velocityX,
|
||||||
|
panelTranslateX: translateX.value,
|
||||||
|
windowWidth,
|
||||||
|
shouldOpenByPosition,
|
||||||
|
shouldOpenByVelocity,
|
||||||
|
shouldOpen,
|
||||||
|
});
|
||||||
|
if (shouldOpen) {
|
||||||
|
animateToOpen();
|
||||||
|
runOnJS(onOpen)();
|
||||||
|
} else {
|
||||||
|
animateToClose();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.onFinalize(() => {
|
||||||
|
isGesturing.value = false;
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
enabled,
|
||||||
|
windowWidth,
|
||||||
|
translateX,
|
||||||
|
backdropOpacity,
|
||||||
|
animateToOpen,
|
||||||
|
animateToClose,
|
||||||
|
isGesturing,
|
||||||
|
onOpen,
|
||||||
|
touchStartX,
|
||||||
|
touchStartY,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,11 +17,8 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import ReanimatedAnimated, {
|
import ReanimatedAnimated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
runOnJS,
|
|
||||||
interpolate,
|
|
||||||
Extrapolation,
|
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { GestureDetector } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import {
|
import {
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
@@ -41,7 +38,6 @@ import { FileDropZone } from "@/components/file-drop-zone";
|
|||||||
import type { ImageAttachment } from "@/components/message-input";
|
import type { ImageAttachment } from "@/components/message-input";
|
||||||
import {
|
import {
|
||||||
ExplorerSidebarAnimationProvider,
|
ExplorerSidebarAnimationProvider,
|
||||||
useExplorerSidebarAnimation,
|
|
||||||
} from "@/contexts/explorer-sidebar-animation-context";
|
} from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
@@ -76,6 +72,7 @@ import {
|
|||||||
import { mergePendingCreateImages } from "@/utils/pending-create-images";
|
import { mergePendingCreateImages } from "@/utils/pending-create-images";
|
||||||
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||||
import type { FetchAgentsEntry } from "@server/client/daemon-client";
|
import type { FetchAgentsEntry } from "@server/client/daemon-client";
|
||||||
|
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -90,6 +87,14 @@ const DROPDOWN_WIDTH = 220;
|
|||||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||||
const RECONNECT_NOTICE_DELAY_MS = 10_000;
|
const RECONNECT_NOTICE_DELAY_MS = 10_000;
|
||||||
const CONNECTED_NOTICE_DURATION_MS = 2_500;
|
const CONNECTED_NOTICE_DURATION_MS = 2_500;
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logAgentExplorer(event: string, details: Record<string, unknown>): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[AgentExplorer] ${event}`, details);
|
||||||
|
}
|
||||||
|
|
||||||
export function AgentReadyScreen({
|
export function AgentReadyScreen({
|
||||||
serverId,
|
serverId,
|
||||||
@@ -324,31 +329,27 @@ function AgentScreenContent({
|
|||||||
}, [resolveCachedCheckoutIsGit, resolvedAgentId, checkout?.isGit, serverId]);
|
}, [resolveCachedCheckoutIsGit, resolvedAgentId, checkout?.isGit, serverId]);
|
||||||
const openExplorerForActiveCheckout = useCallback(() => {
|
const openExplorerForActiveCheckout = useCallback(() => {
|
||||||
const checkoutContext = resolveCurrentExplorerCheckout();
|
const checkoutContext = resolveCurrentExplorerCheckout();
|
||||||
|
logAgentExplorer("openExplorerForActiveCheckout", {
|
||||||
|
hasCheckoutContext: Boolean(checkoutContext),
|
||||||
|
checkoutContext,
|
||||||
|
});
|
||||||
if (checkoutContext) {
|
if (checkoutContext) {
|
||||||
activateExplorerTabForCheckout(checkoutContext);
|
activateExplorerTabForCheckout(checkoutContext);
|
||||||
}
|
}
|
||||||
openFileExplorer();
|
openFileExplorer();
|
||||||
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
||||||
const handleToggleExplorer = useCallback(() => {
|
const handleToggleExplorer = useCallback(() => {
|
||||||
|
logAgentExplorer("handleToggleExplorer", {
|
||||||
|
isExplorerOpen,
|
||||||
|
mobileView,
|
||||||
|
isMobile,
|
||||||
|
});
|
||||||
if (isExplorerOpen) {
|
if (isExplorerOpen) {
|
||||||
toggleFileExplorer();
|
toggleFileExplorer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openExplorerForActiveCheckout();
|
openExplorerForActiveCheckout();
|
||||||
}, [isExplorerOpen, openExplorerForActiveCheckout, toggleFileExplorer]);
|
}, [isExplorerOpen, isMobile, mobileView, openExplorerForActiveCheckout, toggleFileExplorer]);
|
||||||
|
|
||||||
const {
|
|
||||||
translateX: explorerTranslateX,
|
|
||||||
backdropOpacity: explorerBackdropOpacity,
|
|
||||||
windowWidth: explorerWindowWidth,
|
|
||||||
animateToOpen: animateExplorerToOpen,
|
|
||||||
animateToClose: animateExplorerToClose,
|
|
||||||
isGesturing: isExplorerGesturing,
|
|
||||||
} = useExplorerSidebarAnimation();
|
|
||||||
const handleOpenExplorerFromGesture = useCallback(() => {
|
|
||||||
openExplorerForActiveCheckout();
|
|
||||||
animateExplorerToOpen();
|
|
||||||
}, [animateExplorerToOpen, openExplorerForActiveCheckout]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web") {
|
if (Platform.OS !== "web") {
|
||||||
@@ -360,53 +361,10 @@ function AgentScreenContent({
|
|||||||
}, [serverId, agentId]);
|
}, [serverId, agentId]);
|
||||||
|
|
||||||
// Swipe-left gesture to open explorer sidebar on mobile
|
// Swipe-left gesture to open explorer sidebar on mobile
|
||||||
const explorerOpenGesture = useMemo(
|
const explorerOpenGesture = useExplorerOpenGesture({
|
||||||
() =>
|
enabled: isMobile && mobileView === "agent",
|
||||||
Gesture.Pan()
|
onOpen: openExplorerForActiveCheckout,
|
||||||
.enabled(isMobile && !isExplorerOpen)
|
});
|
||||||
// Only activate after 15px horizontal movement to the left (negative)
|
|
||||||
.activeOffsetX(-15)
|
|
||||||
// Fail if 10px vertical movement happens first (allow vertical scroll)
|
|
||||||
.failOffsetY([-10, 10])
|
|
||||||
.onStart(() => {
|
|
||||||
isExplorerGesturing.value = true;
|
|
||||||
})
|
|
||||||
.onUpdate((event) => {
|
|
||||||
// Right sidebar: start from closed position (+windowWidth) and move towards 0
|
|
||||||
// Swiping left means negative translationX
|
|
||||||
const newTranslateX = Math.max(0, explorerWindowWidth + event.translationX);
|
|
||||||
explorerTranslateX.value = newTranslateX;
|
|
||||||
explorerBackdropOpacity.value = interpolate(
|
|
||||||
newTranslateX,
|
|
||||||
[explorerWindowWidth, 0],
|
|
||||||
[0, 1],
|
|
||||||
Extrapolation.CLAMP
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.onEnd((event) => {
|
|
||||||
isExplorerGesturing.value = false;
|
|
||||||
// Open if dragged more than 1/3 of window or fast swipe left
|
|
||||||
const shouldOpen = event.translationX < -explorerWindowWidth / 3 || event.velocityX < -500;
|
|
||||||
if (shouldOpen) {
|
|
||||||
runOnJS(handleOpenExplorerFromGesture)();
|
|
||||||
} else {
|
|
||||||
animateExplorerToClose();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onFinalize(() => {
|
|
||||||
isExplorerGesturing.value = false;
|
|
||||||
}),
|
|
||||||
[
|
|
||||||
isMobile,
|
|
||||||
isExplorerOpen,
|
|
||||||
explorerWindowWidth,
|
|
||||||
explorerTranslateX,
|
|
||||||
explorerBackdropOpacity,
|
|
||||||
animateExplorerToClose,
|
|
||||||
handleOpenExplorerFromGesture,
|
|
||||||
isExplorerGesturing,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle hardware back button - close explorer sidebar first, then navigate back
|
// Handle hardware back button - close explorer sidebar first, then navigate back
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -947,7 +905,7 @@ function AgentScreenContent({
|
|||||||
await Clipboard.setStringAsync(value);
|
await Clipboard.setStringAsync(value);
|
||||||
toast.show(`Copied ${label}`, {
|
toast.show(`Copied ${label}`, {
|
||||||
variant: "success",
|
variant: "success",
|
||||||
icon: <CheckCircle2 size={16} color={theme.colors.primary} />,
|
icon: <CheckCircle2 size={theme.iconSize.md} color={theme.colors.primary} />,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Copy failed");
|
toast.error("Copy failed");
|
||||||
@@ -1015,7 +973,7 @@ function AgentScreenContent({
|
|||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
checkout?.isGit ? (
|
checkout?.isGit ? (
|
||||||
<GitBranch
|
<GitBranch
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={
|
color={
|
||||||
isExplorerOpen
|
isExplorerOpen
|
||||||
? theme.colors.foreground
|
? theme.colors.foreground
|
||||||
@@ -1024,7 +982,7 @@ function AgentScreenContent({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Folder
|
<Folder
|
||||||
size={20}
|
size={theme.iconSize.lg}
|
||||||
color={
|
color={
|
||||||
isExplorerOpen
|
isExplorerOpen
|
||||||
? theme.colors.foreground
|
? theme.colors.foreground
|
||||||
@@ -1034,7 +992,7 @@ function AgentScreenContent({
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<PanelRight
|
<PanelRight
|
||||||
size={16}
|
size={theme.iconSize.md}
|
||||||
color={
|
color={
|
||||||
isExplorerOpen
|
isExplorerOpen
|
||||||
? theme.colors.foreground
|
? theme.colors.foreground
|
||||||
@@ -1051,7 +1009,10 @@ function AgentScreenContent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
|
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
|
||||||
<MoreVertical size={isMobile ? 20 : 16} color={theme.colors.foregroundMuted} />
|
<MoreVertical
|
||||||
|
size={isMobile ? theme.iconSize.lg : theme.iconSize.md}
|
||||||
|
color={theme.colors.foregroundMuted}
|
||||||
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
|
||||||
<View style={styles.menuMetaContainer}>
|
<View style={styles.menuMetaContainer}>
|
||||||
@@ -1170,7 +1131,7 @@ function AgentScreenContent({
|
|||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
leading={<RotateCcw size={16} color={theme.colors.foreground} />}
|
leading={<RotateCcw size={theme.iconSize.md} color={theme.colors.foreground} />}
|
||||||
disabled={isInitializing || shouldBlockForHistorySync}
|
disabled={isInitializing || shouldBlockForHistorySync}
|
||||||
trailing={
|
trailing={
|
||||||
isInitializing ? (
|
isInitializing ? (
|
||||||
@@ -1202,7 +1163,7 @@ function AgentScreenContent({
|
|||||||
>
|
>
|
||||||
{showConnectedNotice ? (
|
{showConnectedNotice ? (
|
||||||
<CheckCircle2
|
<CheckCircle2
|
||||||
size={14}
|
size={theme.iconSize.sm}
|
||||||
color={theme.colors.palette.green[600]}
|
color={theme.colors.palette.green[600]}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import { createNameId } from 'mnemonic-id'
|
|||||||
import type { ImageAttachment } from '@/components/message-input'
|
import type { ImageAttachment } from '@/components/message-input'
|
||||||
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
|
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router'
|
import { useLocalSearchParams, useRouter } from 'expo-router'
|
||||||
|
import { useIsFocused } from '@react-navigation/native'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'
|
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'
|
||||||
import { Gesture, GestureDetector } from 'react-native-gesture-handler'
|
import { GestureDetector } from 'react-native-gesture-handler'
|
||||||
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
import Animated, { useAnimatedStyle, useSharedValue } from 'react-native-reanimated'
|
||||||
import { Folder, GitBranch, PanelRight } from 'lucide-react-native'
|
import { Folder, GitBranch, PanelRight } from 'lucide-react-native'
|
||||||
import { SidebarMenuToggle } from '@/components/headers/menu-header'
|
import { SidebarMenuToggle } from '@/components/headers/menu-header'
|
||||||
import { HeaderToggleButton } from '@/components/headers/header-toggle-button'
|
import { HeaderToggleButton } from '@/components/headers/header-toggle-button'
|
||||||
@@ -31,6 +32,7 @@ import { buildBranchComboOptions, normalizeBranchOptionName } from '@/utils/bran
|
|||||||
import { shortenPath } from '@/utils/shorten-path'
|
import { shortenPath } from '@/utils/shorten-path'
|
||||||
import { collectAgentWorkingDirectorySuggestions } from '@/utils/agent-working-directory-suggestions'
|
import { collectAgentWorkingDirectorySuggestions } from '@/utils/agent-working-directory-suggestions'
|
||||||
import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-suggestions'
|
import { buildWorkingDirectorySuggestions } from '@/utils/working-directory-suggestions'
|
||||||
|
import { useExplorerOpenGesture } from '@/hooks/use-explorer-open-gesture'
|
||||||
import { useSessionStore } from '@/stores/session-store'
|
import { useSessionStore } from '@/stores/session-store'
|
||||||
import { useCreateFlowStore } from '@/stores/create-flow-store'
|
import { useCreateFlowStore } from '@/stores/create-flow-store'
|
||||||
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
|
import { ExplorerSidebarAnimationProvider } from '@/contexts/explorer-sidebar-animation-context'
|
||||||
@@ -131,6 +133,7 @@ function DraftAgentScreenContent({
|
|||||||
onCreateFlowActiveChange,
|
onCreateFlowActiveChange,
|
||||||
forcedServerId,
|
forcedServerId,
|
||||||
}: DraftAgentScreenProps = {}) {
|
}: DraftAgentScreenProps = {}) {
|
||||||
|
const isFocused = useIsFocused()
|
||||||
const { theme } = useUnistyles()
|
const { theme } = useUnistyles()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
@@ -713,26 +716,10 @@ function DraftAgentScreenContent({
|
|||||||
}
|
}
|
||||||
openExplorerForDraftCheckout()
|
openExplorerForDraftCheckout()
|
||||||
}, [canOpenExplorer, isExplorerOpen, openExplorerForDraftCheckout, toggleFileExplorer])
|
}, [canOpenExplorer, isExplorerOpen, openExplorerForDraftCheckout, toggleFileExplorer])
|
||||||
const handleOpenExplorerFromGesture = useCallback(() => {
|
const explorerOpenGesture = useExplorerOpenGesture({
|
||||||
if (!canOpenExplorer) {
|
enabled: isMobile && mobileView === 'agent' && canOpenExplorer,
|
||||||
return
|
onOpen: openExplorerForDraftCheckout,
|
||||||
}
|
})
|
||||||
openExplorerForDraftCheckout()
|
|
||||||
}, [canOpenExplorer, openExplorerForDraftCheckout])
|
|
||||||
const explorerOpenGesture = useMemo(
|
|
||||||
() =>
|
|
||||||
Gesture.Pan()
|
|
||||||
.enabled(isMobile && !isExplorerOpen && canOpenExplorer)
|
|
||||||
.activeOffsetX(-15)
|
|
||||||
.failOffsetY([-10, 10])
|
|
||||||
.onEnd((event) => {
|
|
||||||
const shouldOpen = event.translationX < -80 || event.velocityX < -500
|
|
||||||
if (shouldOpen) {
|
|
||||||
runOnJS(handleOpenExplorerFromGesture)()
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
[canOpenExplorer, handleOpenExplorerFromGesture, isExplorerOpen, isMobile]
|
|
||||||
)
|
|
||||||
const hasWorkingDirectorySearch = debouncedWorkingDirSearchQuery.length > 0
|
const hasWorkingDirectorySearch = debouncedWorkingDirSearchQuery.length > 0
|
||||||
const workingDirSearchError =
|
const workingDirSearchError =
|
||||||
directorySuggestionsQuery.error instanceof Error
|
directorySuggestionsQuery.error instanceof Error
|
||||||
@@ -926,20 +913,23 @@ function DraftAgentScreenContent({
|
|||||||
workingDir,
|
workingDir,
|
||||||
])
|
])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isFocused) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setActiveExplorerCheckout(draftExplorerCheckout)
|
setActiveExplorerCheckout(draftExplorerCheckout)
|
||||||
}, [draftExplorerCheckout, setActiveExplorerCheckout])
|
}, [draftExplorerCheckout, isFocused, setActiveExplorerCheckout])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!draftExplorerCheckout) {
|
if (!isFocused || !draftExplorerCheckout) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
activateExplorerTabForCheckout(draftExplorerCheckout)
|
activateExplorerTabForCheckout(draftExplorerCheckout)
|
||||||
}, [activateExplorerTabForCheckout, draftExplorerCheckout])
|
}, [activateExplorerTabForCheckout, draftExplorerCheckout, isFocused])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (canOpenExplorer || !isExplorerOpen) {
|
if (!isFocused || canOpenExplorer || !isExplorerOpen) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
closeFileExplorer()
|
closeFileExplorer()
|
||||||
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen])
|
}, [canOpenExplorer, closeFileExplorer, isExplorerOpen, isFocused])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
setActiveExplorerCheckout(null)
|
setActiveExplorerCheckout(null)
|
||||||
@@ -1146,7 +1136,7 @@ function DraftAgentScreenContent({
|
|||||||
accessibilityState={{ expanded: isExplorerOpen }}
|
accessibilityState={{ expanded: isExplorerOpen }}
|
||||||
>
|
>
|
||||||
<PanelRight
|
<PanelRight
|
||||||
size={16}
|
size={theme.iconSize.md}
|
||||||
color={isExplorerOpen ? theme.colors.foreground : theme.colors.foregroundMuted}
|
color={isExplorerOpen ? theme.colors.foreground : theme.colors.foregroundMuted}
|
||||||
/>
|
/>
|
||||||
</HeaderToggleButton>
|
</HeaderToggleButton>
|
||||||
@@ -1179,7 +1169,7 @@ function DraftAgentScreenContent({
|
|||||||
value={displayWorkingDir}
|
value={displayWorkingDir}
|
||||||
placeholder="/path/to/project"
|
placeholder="/path/to/project"
|
||||||
onPress={() => setIsWorkingDirOpen(true)}
|
onPress={() => setIsWorkingDirOpen(true)}
|
||||||
icon={<Folder size={16} color={theme.colors.foregroundMuted} />}
|
icon={<Folder size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
valueEllipsizeMode="middle"
|
valueEllipsizeMode="middle"
|
||||||
testID="working-directory-select"
|
testID="working-directory-select"
|
||||||
@@ -1226,7 +1216,7 @@ function DraftAgentScreenContent({
|
|||||||
value={worktreeTriggerValue}
|
value={worktreeTriggerValue}
|
||||||
placeholder="Select worktree"
|
placeholder="Select worktree"
|
||||||
onPress={() => setIsWorktreePickerOpen(true)}
|
onPress={() => setIsWorktreePickerOpen(true)}
|
||||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
icon={<GitBranch size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
valueEllipsizeMode="middle"
|
valueEllipsizeMode="middle"
|
||||||
/>
|
/>
|
||||||
@@ -1239,7 +1229,7 @@ function DraftAgentScreenContent({
|
|||||||
placeholder="From branch"
|
placeholder="From branch"
|
||||||
onPress={() => setIsBranchOpen(true)}
|
onPress={() => setIsBranchOpen(true)}
|
||||||
disabled={repoInfoStatus === 'loading'}
|
disabled={repoInfoStatus === 'loading'}
|
||||||
icon={<GitBranch size={16} color={theme.colors.foregroundMuted} />}
|
icon={<GitBranch size={theme.iconSize.md} color={theme.colors.foregroundMuted} />}
|
||||||
showLabel={false}
|
showLabel={false}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -51,6 +51,23 @@ export const DEFAULT_EXPLORER_FILES_SPLIT_RATIO = 0.38;
|
|||||||
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
|
export const MIN_EXPLORER_FILES_SPLIT_RATIO = 0.2;
|
||||||
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
|
export const MAX_EXPLORER_FILES_SPLIT_RATIO = 0.8;
|
||||||
|
|
||||||
|
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||||
|
|
||||||
|
function logPanelTransition(
|
||||||
|
action: string,
|
||||||
|
details: Record<string, unknown>
|
||||||
|
): void {
|
||||||
|
if (!IS_DEV) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const stack =
|
||||||
|
new Error().stack
|
||||||
|
?.split("\n")
|
||||||
|
.slice(2, 8)
|
||||||
|
.join("\n") ?? "stack unavailable";
|
||||||
|
console.log(`[PanelStore] ${action}`, details, stack);
|
||||||
|
}
|
||||||
|
|
||||||
interface PanelState {
|
interface PanelState {
|
||||||
// Mobile: which panel is currently shown
|
// Mobile: which panel is currently shown
|
||||||
mobileView: MobilePanelView;
|
mobileView: MobilePanelView;
|
||||||
@@ -134,53 +151,102 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
explorerFilesSplitRatio: DEFAULT_EXPLORER_FILES_SPLIT_RATIO,
|
||||||
|
|
||||||
openAgentList: () =>
|
openAgentList: () =>
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
mobileView: "agent-list",
|
const nextState = {
|
||||||
desktop: { ...state.desktop, agentListOpen: true },
|
mobileView: "agent-list" as const,
|
||||||
})),
|
desktop: { ...state.desktop, agentListOpen: true },
|
||||||
|
};
|
||||||
|
logPanelTransition("openAgentList", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextState.mobileView,
|
||||||
|
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
||||||
|
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
||||||
|
});
|
||||||
|
return nextState;
|
||||||
|
}),
|
||||||
|
|
||||||
openFileExplorer: () =>
|
openFileExplorer: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
||||||
return {
|
const nextMobileView: MobilePanelView = "file-explorer";
|
||||||
mobileView: "file-explorer",
|
const nextDesktop = { ...state.desktop, fileExplorerOpen: true };
|
||||||
desktop: { ...state.desktop, fileExplorerOpen: true },
|
const nextState = {
|
||||||
|
mobileView: nextMobileView,
|
||||||
|
desktop: nextDesktop,
|
||||||
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
...(resolvedTab ? { explorerTab: resolvedTab } : {}),
|
||||||
};
|
};
|
||||||
|
logPanelTransition("openFileExplorer", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextMobileView,
|
||||||
|
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
||||||
|
toDesktopFileExplorerOpen: nextDesktop.fileExplorerOpen,
|
||||||
|
resolvedTab: resolvedTab ?? null,
|
||||||
|
activeCheckout: state.activeExplorerCheckout,
|
||||||
|
});
|
||||||
|
return nextState;
|
||||||
}),
|
}),
|
||||||
closeFileExplorer: () =>
|
closeFileExplorer: () =>
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
mobileView: state.mobileView === "file-explorer" ? "agent" : state.mobileView,
|
const nextState = {
|
||||||
desktop: {
|
mobileView:
|
||||||
...state.desktop,
|
state.mobileView === "file-explorer" ? ("agent" as const) : state.mobileView,
|
||||||
fileExplorerOpen: false,
|
desktop: {
|
||||||
},
|
...state.desktop,
|
||||||
})),
|
fileExplorerOpen: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
logPanelTransition("closeFileExplorer", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextState.mobileView,
|
||||||
|
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
||||||
|
toDesktopFileExplorerOpen: nextState.desktop.fileExplorerOpen,
|
||||||
|
});
|
||||||
|
return nextState;
|
||||||
|
}),
|
||||||
|
|
||||||
closeToAgent: () =>
|
closeToAgent: () =>
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
mobileView: "agent",
|
const nextState = {
|
||||||
// On desktop, closing depends on which panel triggered it
|
mobileView: "agent" as const,
|
||||||
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
// On desktop, closing depends on which panel triggered it
|
||||||
desktop: {
|
// This is called when closing via gesture/backdrop, so we close the currently active mobile panel
|
||||||
agentListOpen:
|
desktop: {
|
||||||
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
agentListOpen:
|
||||||
fileExplorerOpen:
|
state.mobileView === "agent-list" ? false : state.desktop.agentListOpen,
|
||||||
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
fileExplorerOpen:
|
||||||
},
|
state.mobileView === "file-explorer" ? false : state.desktop.fileExplorerOpen,
|
||||||
})),
|
},
|
||||||
|
};
|
||||||
|
logPanelTransition("closeToAgent", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextState.mobileView,
|
||||||
|
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
||||||
|
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
||||||
|
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
||||||
|
toDesktopFileExplorerOpen: nextState.desktop.fileExplorerOpen,
|
||||||
|
});
|
||||||
|
return nextState;
|
||||||
|
}),
|
||||||
|
|
||||||
toggleAgentList: () =>
|
toggleAgentList: () =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
// Mobile: toggle between agent and agent-list
|
// Mobile: toggle between agent and agent-list
|
||||||
const newMobileView = state.mobileView === "agent-list" ? "agent" : "agent-list";
|
const newMobileView: MobilePanelView =
|
||||||
return {
|
state.mobileView === "agent-list" ? "agent" : "agent-list";
|
||||||
|
const nextState = {
|
||||||
mobileView: newMobileView,
|
mobileView: newMobileView,
|
||||||
desktop: {
|
desktop: {
|
||||||
...state.desktop,
|
...state.desktop,
|
||||||
agentListOpen: !state.desktop.agentListOpen,
|
agentListOpen: !state.desktop.agentListOpen,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
logPanelTransition("toggleAgentList", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextState.mobileView,
|
||||||
|
fromDesktopAgentListOpen: state.desktop.agentListOpen,
|
||||||
|
toDesktopAgentListOpen: nextState.desktop.agentListOpen,
|
||||||
|
});
|
||||||
|
return nextState;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
toggleFileExplorer: () =>
|
toggleFileExplorer: () =>
|
||||||
@@ -188,19 +254,35 @@ export const usePanelStore = create<PanelState>()(
|
|||||||
// Mobile: toggle between agent and file-explorer
|
// Mobile: toggle between agent and file-explorer
|
||||||
const willOpenMobile = state.mobileView !== "file-explorer";
|
const willOpenMobile = state.mobileView !== "file-explorer";
|
||||||
const willOpenDesktop = !state.desktop.fileExplorerOpen;
|
const willOpenDesktop = !state.desktop.fileExplorerOpen;
|
||||||
const nextState: Partial<PanelState> = {
|
const nextMobileView: MobilePanelView = willOpenMobile
|
||||||
mobileView: willOpenMobile ? "file-explorer" : "agent",
|
? "file-explorer"
|
||||||
desktop: {
|
: "agent";
|
||||||
...state.desktop,
|
const nextDesktop = {
|
||||||
fileExplorerOpen: willOpenDesktop,
|
...state.desktop,
|
||||||
},
|
fileExplorerOpen: willOpenDesktop,
|
||||||
};
|
};
|
||||||
|
const nextState: Pick<PanelState, "mobileView" | "desktop"> &
|
||||||
|
Partial<Pick<PanelState, "explorerTab">> = {
|
||||||
|
mobileView: nextMobileView,
|
||||||
|
desktop: nextDesktop,
|
||||||
|
};
|
||||||
|
let resolvedTab: ExplorerTab | null = null;
|
||||||
if (willOpenMobile || willOpenDesktop) {
|
if (willOpenMobile || willOpenDesktop) {
|
||||||
const resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
resolvedTab = resolveExplorerTabFromActiveCheckout(state);
|
||||||
if (resolvedTab) {
|
if (resolvedTab) {
|
||||||
nextState.explorerTab = resolvedTab;
|
nextState.explorerTab = resolvedTab;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logPanelTransition("toggleFileExplorer", {
|
||||||
|
fromMobileView: state.mobileView,
|
||||||
|
toMobileView: nextMobileView,
|
||||||
|
fromDesktopFileExplorerOpen: state.desktop.fileExplorerOpen,
|
||||||
|
toDesktopFileExplorerOpen: nextDesktop.fileExplorerOpen,
|
||||||
|
willOpenMobile,
|
||||||
|
willOpenDesktop,
|
||||||
|
resolvedTab: resolvedTab ?? null,
|
||||||
|
activeCheckout: state.activeExplorerCheckout,
|
||||||
|
});
|
||||||
return nextState;
|
return nextState;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -215,6 +215,13 @@ const commonTheme = {
|
|||||||
"4xl": 34,
|
"4xl": 34,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
iconSize: {
|
||||||
|
xs: 12,
|
||||||
|
sm: 14,
|
||||||
|
md: 16,
|
||||||
|
lg: 20,
|
||||||
|
},
|
||||||
|
|
||||||
fontWeight: {
|
fontWeight: {
|
||||||
normal: "normal" as const,
|
normal: "normal" as const,
|
||||||
medium: "500" as const,
|
medium: "500" as const,
|
||||||
|
|||||||
@@ -168,6 +168,34 @@ describe("daemon client E2E", () => {
|
|||||||
rmSync(cwd, { recursive: true, force: true });
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
test("archives agents and excludes them from default listings", async () => {
|
||||||
|
const cwd = tmpCwd();
|
||||||
|
try {
|
||||||
|
const created = await ctx.client.createAgent({
|
||||||
|
config: {
|
||||||
|
...getFullAccessConfig("codex"),
|
||||||
|
cwd,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await ctx.client.archiveAgent(created.id);
|
||||||
|
|
||||||
|
const active = await ctx.client.fetchAgents();
|
||||||
|
expect(active.entries.some((entry) => entry.agent.id === created.id)).toBe(false);
|
||||||
|
|
||||||
|
const withArchived = await ctx.client.fetchAgents({
|
||||||
|
filter: { includeArchived: true },
|
||||||
|
});
|
||||||
|
expect(withArchived.entries.some((entry) => entry.agent.id === created.id)).toBe(true);
|
||||||
|
} finally {
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
test("returns rpc error when archiving an unknown agent id", async () => {
|
||||||
|
await expect(ctx.client.archiveAgent(randomUUID())).rejects.toThrow();
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
test("returns home-scoped directory suggestions", async () => {
|
test("returns home-scoped directory suggestions", async () => {
|
||||||
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
|
const insideHomeDir = mkdtempSync(path.join(homedir(), "paseo-dir-suggestion-"));
|
||||||
const outsideHomeDir = mkdtempSync(path.join(tmpdir(), "paseo-dir-suggestion-outside-"));
|
const outsideHomeDir = mkdtempSync(path.join(tmpdir(), "paseo-dir-suggestion-outside-"));
|
||||||
|
|||||||
@@ -1675,22 +1675,32 @@ export class Session {
|
|||||||
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`)
|
this.sessionLogger.info({ agentId }, `Archiving agent ${agentId}`)
|
||||||
|
|
||||||
const archivedAt = new Date().toISOString()
|
const archivedAt = new Date().toISOString()
|
||||||
let archivedRecord: StoredAgentRecord | null = null
|
|
||||||
|
|
||||||
try {
|
const existing = await this.agentStorage.get(agentId)
|
||||||
const existing = await this.agentStorage.get(agentId)
|
let archivedRecord: StoredAgentRecord | null = existing
|
||||||
if (existing) {
|
if (!archivedRecord) {
|
||||||
archivedRecord = {
|
const liveAgent = this.agentManager.getAgent(agentId)
|
||||||
...existing,
|
if (!liveAgent) {
|
||||||
archivedAt,
|
throw new Error(`Agent not found: ${agentId}`)
|
||||||
}
|
}
|
||||||
await this.agentStorage.upsert(archivedRecord)
|
|
||||||
|
await this.agentStorage.applySnapshot(liveAgent, {
|
||||||
|
title: liveAgent.config.title ?? null,
|
||||||
|
internal: liveAgent.internal,
|
||||||
|
})
|
||||||
|
archivedRecord = await this.agentStorage.get(agentId)
|
||||||
|
if (!archivedRecord) {
|
||||||
|
throw new Error(`Agent not found in storage after snapshot: ${agentId}`)
|
||||||
}
|
}
|
||||||
this.agentManager.notifyAgentState(agentId)
|
|
||||||
} catch (error: any) {
|
|
||||||
this.sessionLogger.error({ err: error, agentId }, `Failed to archive agent ${agentId}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
archivedRecord = {
|
||||||
|
...archivedRecord,
|
||||||
|
archivedAt,
|
||||||
|
}
|
||||||
|
await this.agentStorage.upsert(archivedRecord)
|
||||||
|
this.agentManager.notifyAgentState(agentId)
|
||||||
|
|
||||||
this.emit({
|
this.emit({
|
||||||
type: 'agent_archived',
|
type: 'agent_archived',
|
||||||
payload: {
|
payload: {
|
||||||
@@ -1700,13 +1710,11 @@ export class Session {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (archivedRecord) {
|
await this.maybeArchiveWorktreeAfterLastAgentArchived({
|
||||||
await this.maybeArchiveWorktreeAfterLastAgentArchived({
|
archivedAgentId: agentId,
|
||||||
archivedAgentId: agentId,
|
archivedAgentCwd: archivedRecord.cwd,
|
||||||
archivedAgentCwd: archivedRecord.cwd,
|
requestId,
|
||||||
requestId,
|
})
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleUpdateAgentRequest(
|
private async handleUpdateAgentRequest(
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ describe("createWorktree", () => {
|
|||||||
expect(ownership.repoRoot).toBe(repoDir);
|
expect(ownership.repoRoot).toBe(repoDir);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("treats non-git directories as non-worktrees without throwing", async () => {
|
||||||
|
const nonGitDir = join(tempDir, "not-a-repo");
|
||||||
|
execSync(`mkdir -p ${nonGitDir}`);
|
||||||
|
|
||||||
|
const ownership = await isPaseoOwnedWorktreeCwd(nonGitDir, { paseoHome });
|
||||||
|
|
||||||
|
expect(ownership.allowed).toBe(false);
|
||||||
|
expect(ownership.worktreePath).toBe(realpathSync(nonGitDir));
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a worktree with a new branch", async () => {
|
it("creates a worktree with a new branch", async () => {
|
||||||
const projectHash = await deriveWorktreeProjectHash(repoDir);
|
const projectHash = await deriveWorktreeProjectHash(repoDir);
|
||||||
const result = await createWorktree({
|
const result = await createWorktree({
|
||||||
|
|||||||
@@ -692,7 +692,15 @@ export async function isPaseoOwnedWorktreeCwd(
|
|||||||
cwd: string,
|
cwd: string,
|
||||||
options?: { paseoHome?: string }
|
options?: { paseoHome?: string }
|
||||||
): Promise<PaseoWorktreeOwnership> {
|
): Promise<PaseoWorktreeOwnership> {
|
||||||
const gitCommonDir = await getGitCommonDir(cwd);
|
let gitCommonDir: string;
|
||||||
|
try {
|
||||||
|
gitCommonDir = await getGitCommonDir(cwd);
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
worktreePath: normalizePathForOwnership(cwd),
|
||||||
|
};
|
||||||
|
}
|
||||||
const repoRoot = resolveRepoRootFromGitCommonDir(gitCommonDir);
|
const repoRoot = resolveRepoRootFromGitCommonDir(gitCommonDir);
|
||||||
const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome);
|
const worktreesRoot = await getPaseoWorktreesRoot(cwd, options?.paseoHome);
|
||||||
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
|
const resolvedRoot = normalizePathForOwnership(worktreesRoot) + sep;
|
||||||
|
|||||||
Reference in New Issue
Block a user