Simplify project filter to single selection and persist sidebar order
This commit is contained in:
@@ -16,7 +16,7 @@ import Animated, {
|
|||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Plus, Settings, Users } from "lucide-react-native";
|
import { Plus, ListFilter, Settings, Users } from "lucide-react-native";
|
||||||
import { router, usePathname } from "expo-router";
|
import { router, usePathname } from "expo-router";
|
||||||
import { usePanelStore } from "@/stores/panel-store";
|
import { usePanelStore } from "@/stores/panel-store";
|
||||||
import { SidebarAgentList } from "./sidebar-agent-list";
|
import { SidebarAgentList } from "./sidebar-agent-list";
|
||||||
@@ -119,12 +119,14 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
);
|
);
|
||||||
const hostTriggerRef = useRef<View>(null);
|
const hostTriggerRef = useRef<View>(null);
|
||||||
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
|
const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
|
||||||
|
const projectFilterAnchorRef = useRef<View>(null);
|
||||||
|
const [isProjectFilterOpen, setIsProjectFilterOpen] = useState(false);
|
||||||
|
|
||||||
// Derive isOpen from the unified panel state
|
// Derive isOpen from the unified panel state
|
||||||
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
|
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
|
||||||
const [selectedProjectFilterKeys, setSelectedProjectFilterKeys] = useState<
|
const [selectedProjectFilterKey, setSelectedProjectFilterKey] = useState<string | null>(
|
||||||
string[]
|
null
|
||||||
>([]);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
entries,
|
entries,
|
||||||
@@ -135,10 +137,10 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
refreshAll,
|
refreshAll,
|
||||||
} = useSidebarAgentsList({
|
} = useSidebarAgentsList({
|
||||||
serverId: activeServerId,
|
serverId: activeServerId,
|
||||||
selectedProjectFilterKeys,
|
selectedProjectFilterKey,
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedProjectFilterKeys([]);
|
setSelectedProjectFilterKey(null);
|
||||||
}, [activeServerId]);
|
}, [activeServerId]);
|
||||||
const {
|
const {
|
||||||
translateX,
|
translateX,
|
||||||
@@ -393,6 +395,27 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{projectFilterOptions.length > 0 ? (
|
||||||
|
<Pressable
|
||||||
|
ref={projectFilterAnchorRef}
|
||||||
|
style={styles.filterButton}
|
||||||
|
onPress={() => setIsProjectFilterOpen(true)}
|
||||||
|
>
|
||||||
|
{({ hovered }) => (
|
||||||
|
<>
|
||||||
|
<ListFilter
|
||||||
|
size={theme.iconSize.md}
|
||||||
|
color={
|
||||||
|
(selectedProjectFilterKey || hovered)
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selectedProjectFilterKey ? <View style={styles.filterActiveDot} /> : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -402,10 +425,14 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
) : (
|
) : (
|
||||||
<SidebarAgentList
|
<SidebarAgentList
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
serverId={activeServerId}
|
||||||
entries={entries}
|
entries={entries}
|
||||||
projectFilterOptions={projectFilterOptions}
|
projectFilterOptions={projectFilterOptions}
|
||||||
selectedProjectFilterKeys={selectedProjectFilterKeys}
|
selectedProjectFilterKey={selectedProjectFilterKey}
|
||||||
onSelectedProjectFilterKeysChange={setSelectedProjectFilterKeys}
|
onSelectedProjectFilterKeyChange={setSelectedProjectFilterKey}
|
||||||
|
isProjectFilterOpen={isProjectFilterOpen}
|
||||||
|
onProjectFilterOpenChange={setIsProjectFilterOpen}
|
||||||
|
projectFilterAnchorRef={projectFilterAnchorRef}
|
||||||
isRefreshing={isManualRefresh && isRevalidating}
|
isRefreshing={isManualRefresh && isRevalidating}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
listFooterComponent={listFooterComponent}
|
listFooterComponent={listFooterComponent}
|
||||||
@@ -500,8 +527,11 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
<View style={[styles.desktopSidebar, { width: DESKTOP_SIDEBAR_WIDTH }]}>
|
||||||
|
{trafficLightPadding.top > 0 ? (
|
||||||
|
<View style={{ height: trafficLightPadding.top }} {...dragHandlers} />
|
||||||
|
) : null}
|
||||||
<View
|
<View
|
||||||
style={[styles.sidebarHeader, { paddingLeft: theme.spacing[2] + trafficLightPadding.left }]}
|
style={styles.sidebarHeader}
|
||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
>
|
>
|
||||||
<View style={styles.sidebarHeaderRow}>
|
<View style={styles.sidebarHeaderRow}>
|
||||||
@@ -516,10 +546,31 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
size={theme.iconSize.md}
|
size={theme.iconSize.md}
|
||||||
color={hovered ? theme.colors.foreground : theme.colors.foregroundMuted}
|
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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
{projectFilterOptions.length > 0 ? (
|
||||||
|
<Pressable
|
||||||
|
ref={projectFilterAnchorRef}
|
||||||
|
style={styles.filterButton}
|
||||||
|
onPress={() => setIsProjectFilterOpen(true)}
|
||||||
|
>
|
||||||
|
{({ hovered }) => (
|
||||||
|
<>
|
||||||
|
<ListFilter
|
||||||
|
size={theme.iconSize.md}
|
||||||
|
color={
|
||||||
|
(selectedProjectFilterKey || hovered)
|
||||||
|
? theme.colors.foreground
|
||||||
|
: theme.colors.foregroundMuted
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{selectedProjectFilterKey ? <View style={styles.filterActiveDot} /> : null}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@@ -529,10 +580,14 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
|||||||
) : (
|
) : (
|
||||||
<SidebarAgentList
|
<SidebarAgentList
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
serverId={activeServerId}
|
||||||
entries={entries}
|
entries={entries}
|
||||||
projectFilterOptions={projectFilterOptions}
|
projectFilterOptions={projectFilterOptions}
|
||||||
selectedProjectFilterKeys={selectedProjectFilterKeys}
|
selectedProjectFilterKey={selectedProjectFilterKey}
|
||||||
onSelectedProjectFilterKeysChange={setSelectedProjectFilterKeys}
|
onSelectedProjectFilterKeyChange={setSelectedProjectFilterKey}
|
||||||
|
isProjectFilterOpen={isProjectFilterOpen}
|
||||||
|
onProjectFilterOpenChange={setIsProjectFilterOpen}
|
||||||
|
projectFilterAnchorRef={projectFilterAnchorRef}
|
||||||
isRefreshing={isManualRefresh && isRevalidating}
|
isRefreshing={isManualRefresh && isRevalidating}
|
||||||
onRefresh={handleRefresh}
|
onRefresh={handleRefresh}
|
||||||
listFooterComponent={listFooterComponent}
|
listFooterComponent={listFooterComponent}
|
||||||
@@ -655,7 +710,7 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
sidebarHeaderRow: {
|
sidebarHeaderRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "space-between",
|
||||||
gap: theme.spacing[2],
|
gap: theme.spacing[2],
|
||||||
},
|
},
|
||||||
newAgentButton: {
|
newAgentButton: {
|
||||||
@@ -666,6 +721,23 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
paddingHorizontal: theme.spacing[1],
|
paddingHorizontal: theme.spacing[1],
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
},
|
},
|
||||||
|
filterButton: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingVertical: theme.spacing[1],
|
||||||
|
paddingHorizontal: theme.spacing[1],
|
||||||
|
position: "relative",
|
||||||
|
},
|
||||||
|
filterActiveDot: {
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: theme.borderRadius.full,
|
||||||
|
backgroundColor: theme.colors.borderAccent,
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
right: -2,
|
||||||
|
},
|
||||||
newAgentButtonHovered: {},
|
newAgentButtonHovered: {},
|
||||||
newAgentButtonText: {
|
newAgentButtonText: {
|
||||||
fontSize: theme.fontSize.base,
|
fontSize: theme.fontSize.base,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
useUnistyles,
|
useUnistyles,
|
||||||
} from "react-native-unistyles";
|
} from "react-native-unistyles";
|
||||||
import { type GestureType } from "react-native-gesture-handler";
|
import { type GestureType } from "react-native-gesture-handler";
|
||||||
import { Archive, Check, ChevronDown } from "lucide-react-native";
|
import { Archive, Check } from "lucide-react-native";
|
||||||
import {
|
import {
|
||||||
DraggableList,
|
DraggableList,
|
||||||
type DraggableRenderItemInfo,
|
type DraggableRenderItemInfo,
|
||||||
@@ -55,6 +55,7 @@ import { parseRepoNameFromRemoteUrl } from "@/utils/agent-grouping";
|
|||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
import { isSidebarActiveAgent } from "@/utils/sidebar-agent-state";
|
import { isSidebarActiveAgent } from "@/utils/sidebar-agent-state";
|
||||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||||
|
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
|
||||||
|
|
||||||
type EntryData = SidebarAgentListEntry;
|
type EntryData = SidebarAgentListEntry;
|
||||||
|
|
||||||
@@ -69,8 +70,12 @@ interface SidebarAgentListProps {
|
|||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
entries: SidebarAgentListEntry[];
|
entries: SidebarAgentListEntry[];
|
||||||
projectFilterOptions: SidebarProjectFilterOption[];
|
projectFilterOptions: SidebarProjectFilterOption[];
|
||||||
selectedProjectFilterKeys: string[];
|
selectedProjectFilterKey: string | null;
|
||||||
onSelectedProjectFilterKeysChange: (keys: string[]) => void;
|
onSelectedProjectFilterKeyChange: (key: string | null) => void;
|
||||||
|
isProjectFilterOpen: boolean;
|
||||||
|
onProjectFilterOpenChange: (open: boolean) => void;
|
||||||
|
projectFilterAnchorRef: MutableRefObject<View | null>;
|
||||||
|
serverId: string | null;
|
||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
selectedAgentId?: string;
|
selectedAgentId?: string;
|
||||||
@@ -85,7 +90,7 @@ interface ProjectFilterOptionRowProps {
|
|||||||
selected: boolean;
|
selected: boolean;
|
||||||
iconDataUri: string | null;
|
iconDataUri: string | null;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
onToggle: (projectKey: string) => void;
|
onSelect: (projectKey: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ProjectFilterOptionRow({
|
function ProjectFilterOptionRow({
|
||||||
@@ -93,18 +98,17 @@ function ProjectFilterOptionRow({
|
|||||||
selected,
|
selected,
|
||||||
iconDataUri,
|
iconDataUri,
|
||||||
displayName,
|
displayName,
|
||||||
onToggle,
|
onSelect,
|
||||||
}: ProjectFilterOptionRowProps) {
|
}: ProjectFilterOptionRowProps) {
|
||||||
const { theme } = useUnistyles();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={({ pressed, hovered = false }) => [
|
style={({ pressed, hovered = false }) => [
|
||||||
styles.filterOption,
|
styles.filterOption,
|
||||||
|
selected && styles.filterOptionSelected,
|
||||||
hovered && styles.filterOptionHovered,
|
hovered && styles.filterOptionHovered,
|
||||||
pressed && styles.filterOptionPressed,
|
pressed && styles.filterOptionPressed,
|
||||||
]}
|
]}
|
||||||
onPress={() => onToggle(option.projectKey)}
|
onPress={() => onSelect(option.projectKey)}
|
||||||
>
|
>
|
||||||
<View style={styles.filterOptionLeft}>
|
<View style={styles.filterOptionLeft}>
|
||||||
{iconDataUri ? (
|
{iconDataUri ? (
|
||||||
@@ -128,7 +132,6 @@ 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={theme.iconSize.sm} color={theme.colors.foregroundMuted} /> : null}
|
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
);
|
);
|
||||||
@@ -390,33 +393,6 @@ function deriveShortcutIndexByAgentKey(sidebarShortcutAgentKeys: string[]) {
|
|||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSelectedProjectLabel(input: {
|
|
||||||
selectedProjectFilterKeys: string[];
|
|
||||||
projectFilterOptions: SidebarProjectFilterOption[];
|
|
||||||
}): string {
|
|
||||||
if (input.selectedProjectFilterKeys.length === 0) {
|
|
||||||
return "Project";
|
|
||||||
}
|
|
||||||
if (input.selectedProjectFilterKeys.length === 1) {
|
|
||||||
const selected = input.projectFilterOptions.find(
|
|
||||||
(option) => option.projectKey === input.selectedProjectFilterKeys[0]
|
|
||||||
);
|
|
||||||
if (selected) {
|
|
||||||
return deriveProjectDisplayName({
|
|
||||||
projectKey: selected.projectKey,
|
|
||||||
projectName: selected.projectName,
|
|
||||||
remoteUrl: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return deriveProjectDisplayName({
|
|
||||||
projectKey: input.selectedProjectFilterKeys[0] ?? "",
|
|
||||||
projectName: "",
|
|
||||||
remoteUrl: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return "Projects";
|
|
||||||
}
|
|
||||||
|
|
||||||
function deriveProjectDisplayName(input: {
|
function deriveProjectDisplayName(input: {
|
||||||
projectKey: string;
|
projectKey: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
@@ -456,8 +432,12 @@ export function SidebarAgentList({
|
|||||||
isOpen = true,
|
isOpen = true,
|
||||||
entries,
|
entries,
|
||||||
projectFilterOptions,
|
projectFilterOptions,
|
||||||
selectedProjectFilterKeys,
|
selectedProjectFilterKey,
|
||||||
onSelectedProjectFilterKeysChange,
|
onSelectedProjectFilterKeyChange,
|
||||||
|
isProjectFilterOpen,
|
||||||
|
onProjectFilterOpenChange,
|
||||||
|
projectFilterAnchorRef,
|
||||||
|
serverId,
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
@@ -470,12 +450,13 @@ export function SidebarAgentList({
|
|||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
const showDesktopWebScrollbar = Platform.OS === "web" && !isMobile;
|
||||||
const [isProjectFilterOpen, setIsProjectFilterOpen] = useState(false);
|
|
||||||
const projectFilterAnchorRef = useRef<View>(null);
|
|
||||||
|
|
||||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||||
const [selectedBatchKeys, setSelectedBatchKeys] = useState<Set<string>>(new Set());
|
const [selectedBatchKeys, setSelectedBatchKeys] = useState<Set<string>>(new Set());
|
||||||
const { archiveAgent, isArchivingAgent } = useArchiveAgent();
|
const { archiveAgent, isArchivingAgent } = useArchiveAgent();
|
||||||
|
const getSidebarOrder = useSidebarOrderStore((state) => state.getOrder);
|
||||||
|
const setSidebarOrder = useSidebarOrderStore((state) => state.setOrder);
|
||||||
|
const removeSidebarOrderEntry = useSidebarOrderStore((state) => state.remove);
|
||||||
|
|
||||||
const altDown = useKeyboardShortcutsStore((s) => s.altDown);
|
const altDown = useKeyboardShortcutsStore((s) => s.altDown);
|
||||||
const cmdOrCtrlDown = useKeyboardShortcutsStore((s) => s.cmdOrCtrlDown);
|
const cmdOrCtrlDown = useKeyboardShortcutsStore((s) => s.cmdOrCtrlDown);
|
||||||
@@ -489,19 +470,6 @@ export function SidebarAgentList({
|
|||||||
[sidebarShortcutAgentKeys]
|
[sidebarShortcutAgentKeys]
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedProjectLabel = useMemo(
|
|
||||||
() => resolveSelectedProjectLabel({ selectedProjectFilterKeys, projectFilterOptions }),
|
|
||||||
[projectFilterOptions, selectedProjectFilterKeys]
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedProjectOption = useMemo(() => {
|
|
||||||
if (selectedProjectFilterKeys.length !== 1) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
projectFilterOptions.find((option) => option.projectKey === selectedProjectFilterKeys[0]) ?? null
|
|
||||||
);
|
|
||||||
}, [projectFilterOptions, selectedProjectFilterKeys]);
|
|
||||||
const showProjectFilters = projectFilterOptions.length > 0;
|
const showProjectFilters = projectFilterOptions.length > 0;
|
||||||
|
|
||||||
const projectIconRequests = useMemo(() => {
|
const projectIconRequests = useMemo(() => {
|
||||||
@@ -592,29 +560,18 @@ export function SidebarAgentList({
|
|||||||
return map;
|
return map;
|
||||||
}, [entries, projectIconByQueryKey, projectFilterOptions]);
|
}, [entries, projectIconByQueryKey, projectFilterOptions]);
|
||||||
|
|
||||||
const selectedProjectIconUri = useMemo(() => {
|
const handleSelectProject = useCallback(
|
||||||
if (!selectedProjectOption) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return projectIconByProjectKey.get(selectedProjectOption.projectKey) ?? null;
|
|
||||||
}, [projectIconByProjectKey, selectedProjectOption]);
|
|
||||||
|
|
||||||
const handleToggleProject = useCallback(
|
|
||||||
(projectKey: string) => {
|
(projectKey: string) => {
|
||||||
const next = new Set(selectedProjectFilterKeys);
|
onSelectedProjectFilterKeyChange(
|
||||||
if (next.has(projectKey)) {
|
selectedProjectFilterKey === projectKey ? null : projectKey
|
||||||
next.delete(projectKey);
|
);
|
||||||
} else {
|
|
||||||
next.add(projectKey);
|
|
||||||
}
|
|
||||||
onSelectedProjectFilterKeysChange(Array.from(next));
|
|
||||||
},
|
},
|
||||||
[onSelectedProjectFilterKeysChange, selectedProjectFilterKeys]
|
[onSelectedProjectFilterKeyChange, selectedProjectFilterKey]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearProjectFilter = useCallback(() => {
|
const handleClearProjectFilter = useCallback(() => {
|
||||||
onSelectedProjectFilterKeysChange([]);
|
onSelectedProjectFilterKeyChange(null);
|
||||||
}, [onSelectedProjectFilterKeysChange]);
|
}, [onSelectedProjectFilterKeyChange]);
|
||||||
|
|
||||||
const handleAgentPress = useCallback(
|
const handleAgentPress = useCallback(
|
||||||
(entry: SidebarAgentListEntry) => {
|
(entry: SidebarAgentListEntry) => {
|
||||||
@@ -659,11 +616,18 @@ export function SidebarAgentList({
|
|||||||
await archiveAgent({
|
await archiveAgent({
|
||||||
serverId: entry.agent.serverId,
|
serverId: entry.agent.serverId,
|
||||||
agentId: entry.agent.id,
|
agentId: entry.agent.id,
|
||||||
}).catch((error) => {
|
})
|
||||||
console.warn("[archive_agent] failed", error);
|
.then(() => {
|
||||||
});
|
removeSidebarOrderEntry(
|
||||||
|
entry.agent.serverId,
|
||||||
|
`${entry.agent.serverId}:${entry.agent.id}`
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("[archive_agent] failed", error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[archiveAgent]
|
[archiveAgent, removeSidebarOrderEntry]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleArchiveBatch = useCallback(() => {
|
const handleArchiveBatch = useCallback(() => {
|
||||||
@@ -682,9 +646,13 @@ export function SidebarAgentList({
|
|||||||
archiveAgent({
|
archiveAgent({
|
||||||
serverId: parsed.serverId,
|
serverId: parsed.serverId,
|
||||||
agentId: parsed.agentId,
|
agentId: parsed.agentId,
|
||||||
}).catch((error) => {
|
|
||||||
console.warn("[archive_agent_batch] failed", { key, error });
|
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
removeSidebarOrderEntry(parsed.serverId, key);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn("[archive_agent_batch] failed", { key, error });
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,7 +660,7 @@ export function SidebarAgentList({
|
|||||||
setSelectedBatchKeys(new Set());
|
setSelectedBatchKeys(new Set());
|
||||||
setIsSelectionMode(false);
|
setIsSelectionMode(false);
|
||||||
});
|
});
|
||||||
}, [archiveAgent, selectedBatchKeys]);
|
}, [archiveAgent, removeSidebarOrderEntry, selectedBatchKeys]);
|
||||||
|
|
||||||
const handleSelectionBack = useCallback(() => {
|
const handleSelectionBack = useCallback(() => {
|
||||||
setSelectedBatchKeys(new Set());
|
setSelectedBatchKeys(new Set());
|
||||||
@@ -759,102 +727,68 @@ export function SidebarAgentList({
|
|||||||
(entry: EntryData) => `${entry.agent.serverId}:${entry.agent.id}`,
|
(entry: EntryData) => `${entry.agent.serverId}:${entry.agent.id}`,
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(reorderedEntries: SidebarAgentListEntry[]) => {
|
||||||
|
if (!serverId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reorderedKeys = reorderedEntries.map(
|
||||||
|
(entry) => `${entry.agent.serverId}:${entry.agent.id}`
|
||||||
|
);
|
||||||
|
const reorderedSet = new Set(reorderedKeys);
|
||||||
|
const remainder = getSidebarOrder(serverId).filter((key) => !reorderedSet.has(key));
|
||||||
|
setSidebarOrder(serverId, [...reorderedKeys, ...remainder]);
|
||||||
|
},
|
||||||
|
[getSidebarOrder, serverId, setSidebarOrder]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{showProjectFilters ? (
|
{showProjectFilters ? (
|
||||||
<View style={styles.filtersRow}>
|
<Combobox
|
||||||
<Pressable
|
options={[]}
|
||||||
ref={projectFilterAnchorRef}
|
value=""
|
||||||
style={({ hovered = false, pressed }) => [
|
onSelect={() => {}}
|
||||||
styles.filterTrigger,
|
title="Filter by project"
|
||||||
(selectedProjectFilterKeys.length > 0 || hovered || pressed) &&
|
placeholder="Search projects"
|
||||||
styles.filterTriggerActive,
|
searchPlaceholder="Search projects"
|
||||||
]}
|
desktopPlacement="bottom-start"
|
||||||
onPress={() => setIsProjectFilterOpen(true)}
|
open={isProjectFilterOpen}
|
||||||
>
|
onOpenChange={onProjectFilterOpenChange}
|
||||||
{({ hovered = false, pressed }) => {
|
anchorRef={projectFilterAnchorRef}
|
||||||
const isInteracting = hovered || pressed;
|
>
|
||||||
const showActiveForeground =
|
<View style={styles.filterOptionsList}>
|
||||||
selectedProjectFilterKeys.length > 0 || isInteracting;
|
<Pressable
|
||||||
return (
|
style={({ pressed, hovered = false }) => [
|
||||||
<>
|
styles.filterOption,
|
||||||
{selectedProjectFilterKeys.length === 1 && selectedProjectIconUri ? (
|
!selectedProjectFilterKey && styles.filterOptionSelected,
|
||||||
<Image
|
hovered && styles.filterOptionHovered,
|
||||||
source={{
|
pressed && styles.filterOptionPressed,
|
||||||
uri: selectedProjectIconUri,
|
]}
|
||||||
}}
|
onPress={handleClearProjectFilter}
|
||||||
style={styles.selectedProjectIcon}
|
>
|
||||||
/>
|
<Text style={styles.filterOptionLabel}>All projects</Text>
|
||||||
) : selectedProjectFilterKeys.length > 1 ? (
|
|
||||||
<View style={styles.projectCountBadge}>
|
|
||||||
<Text style={styles.projectCountBadgeText}>
|
|
||||||
{selectedProjectFilterKeys.length}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.filterTriggerText,
|
|
||||||
!showActiveForeground && styles.filterTriggerTextMuted,
|
|
||||||
]}
|
|
||||||
numberOfLines={1}
|
|
||||||
>
|
|
||||||
{selectedProjectLabel}
|
|
||||||
</Text>
|
|
||||||
<ChevronDown
|
|
||||||
size={theme.iconSize.sm}
|
|
||||||
color={
|
|
||||||
showActiveForeground
|
|
||||||
? theme.colors.foreground
|
|
||||||
: theme.colors.foregroundMuted
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</Pressable>
|
|
||||||
|
|
||||||
{selectedProjectFilterKeys.length > 0 ? (
|
|
||||||
<Pressable style={styles.clearFilterButton} onPress={handleClearProjectFilter}>
|
|
||||||
<Text style={styles.clearFilterText}>Clear</Text>
|
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : null}
|
{projectFilterOptions.length === 0 ? (
|
||||||
|
<Text style={styles.filterEmptyText}>No projects</Text>
|
||||||
<Combobox
|
) : (
|
||||||
options={[]}
|
projectFilterOptions.map((option) => (
|
||||||
value=""
|
<ProjectFilterOptionRow
|
||||||
onSelect={() => {}}
|
key={option.projectKey}
|
||||||
title="Filter by project"
|
option={option}
|
||||||
placeholder="Search projects"
|
selected={selectedProjectFilterKey === option.projectKey}
|
||||||
searchPlaceholder="Search projects"
|
iconDataUri={projectIconByProjectKey.get(option.projectKey) ?? null}
|
||||||
desktopPlacement="bottom-start"
|
displayName={deriveProjectDisplayName({
|
||||||
open={isProjectFilterOpen}
|
projectKey: option.projectKey,
|
||||||
onOpenChange={setIsProjectFilterOpen}
|
projectName: option.projectName,
|
||||||
anchorRef={projectFilterAnchorRef}
|
remoteUrl: null,
|
||||||
>
|
})}
|
||||||
<View style={styles.filterOptionsList}>
|
onSelect={handleSelectProject}
|
||||||
{projectFilterOptions.length === 0 ? (
|
/>
|
||||||
<Text style={styles.filterEmptyText}>No projects</Text>
|
))
|
||||||
) : (
|
)}
|
||||||
projectFilterOptions.map((option) => (
|
</View>
|
||||||
<ProjectFilterOptionRow
|
</Combobox>
|
||||||
key={option.projectKey}
|
|
||||||
option={option}
|
|
||||||
selected={selectedProjectFilterKeys.includes(option.projectKey)}
|
|
||||||
iconDataUri={projectIconByProjectKey.get(option.projectKey) ?? null}
|
|
||||||
displayName={deriveProjectDisplayName({
|
|
||||||
projectKey: option.projectKey,
|
|
||||||
projectName: option.projectName,
|
|
||||||
remoteUrl: null,
|
|
||||||
})}
|
|
||||||
onToggle={handleToggleProject}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Combobox>
|
|
||||||
</View>
|
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<DraggableList
|
<DraggableList
|
||||||
@@ -867,7 +801,7 @@ export function SidebarAgentList({
|
|||||||
testID="sidebar-agent-list-scroll"
|
testID="sidebar-agent-list-scroll"
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
renderItem={renderRow}
|
renderItem={renderRow}
|
||||||
onDragEnd={() => {}}
|
onDragEnd={handleDragEnd}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
enableDesktopWebScrollbar={showDesktopWebScrollbar}
|
enableDesktopWebScrollbar={showDesktopWebScrollbar}
|
||||||
ListFooterComponent={listFooterComponent}
|
ListFooterComponent={listFooterComponent}
|
||||||
@@ -902,64 +836,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
filtersRow: {
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingTop: theme.spacing[2],
|
|
||||||
paddingBottom: theme.spacing[2],
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
filterTrigger: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
maxWidth: 260,
|
|
||||||
borderWidth: 1,
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingVertical: theme.spacing[1],
|
|
||||||
backgroundColor: theme.colors.surface1,
|
|
||||||
},
|
|
||||||
filterTriggerActive: {
|
|
||||||
borderColor: theme.colors.borderAccent,
|
|
||||||
},
|
|
||||||
filterTriggerText: {
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
maxWidth: 180,
|
|
||||||
},
|
|
||||||
filterTriggerTextMuted: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
},
|
|
||||||
selectedProjectIcon: {
|
|
||||||
width: theme.iconSize.sm,
|
|
||||||
height: theme.iconSize.sm,
|
|
||||||
borderRadius: theme.borderRadius.sm,
|
|
||||||
},
|
|
||||||
projectCountBadge: {
|
|
||||||
minWidth: theme.iconSize.md,
|
|
||||||
height: theme.iconSize.md,
|
|
||||||
borderRadius: theme.borderRadius.full,
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
paddingHorizontal: 4,
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
},
|
|
||||||
projectCountBadgeText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
},
|
|
||||||
clearFilterButton: {
|
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
paddingVertical: theme.spacing[1],
|
|
||||||
},
|
|
||||||
clearFilterText: {
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
fontSize: theme.fontSize.sm,
|
|
||||||
},
|
|
||||||
filterOptionsList: {
|
filterOptionsList: {
|
||||||
gap: theme.spacing[1],
|
gap: theme.spacing[1],
|
||||||
},
|
},
|
||||||
@@ -983,6 +859,9 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
filterOptionPressed: {
|
filterOptionPressed: {
|
||||||
backgroundColor: theme.colors.surface2,
|
backgroundColor: theme.colors.surface2,
|
||||||
},
|
},
|
||||||
|
filterOptionSelected: {
|
||||||
|
backgroundColor: theme.colors.surface2,
|
||||||
|
},
|
||||||
filterOptionLeft: {
|
filterOptionLeft: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import {
|
|||||||
} from "@/runtime/host-runtime";
|
} from "@/runtime/host-runtime";
|
||||||
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import {
|
import {
|
||||||
deriveSidebarStateBucket,
|
|
||||||
isSidebarActiveAgent,
|
isSidebarActiveAgent,
|
||||||
} from "@/utils/sidebar-agent-state";
|
} from "@/utils/sidebar-agent-state";
|
||||||
import type { ProjectPlacementPayload } from "@server/shared/messages";
|
import type { ProjectPlacementPayload } from "@server/shared/messages";
|
||||||
import { resolveProjectPlacement } from "@/utils/project-placement";
|
import { resolveProjectPlacement } from "@/utils/project-placement";
|
||||||
|
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
|
||||||
|
|
||||||
const SIDEBAR_DONE_FILL_TARGET = 50;
|
const SIDEBAR_DONE_FILL_TARGET = 50;
|
||||||
|
const EMPTY_ORDER: string[] = [];
|
||||||
|
|
||||||
export interface SidebarProjectFilterOption {
|
export interface SidebarProjectFilterOption {
|
||||||
projectKey: string;
|
projectKey: string;
|
||||||
@@ -38,25 +39,23 @@ export interface SidebarAgentsListResult {
|
|||||||
refreshAll: () => void;
|
refreshAll: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareByLastActivityDesc(
|
function compareByCreatedAtDesc(
|
||||||
left: SidebarAgentListEntry,
|
left: SidebarAgentListEntry,
|
||||||
right: SidebarAgentListEntry
|
right: SidebarAgentListEntry
|
||||||
): number {
|
): number {
|
||||||
return right.agent.lastActivityAt.getTime() - left.agent.lastActivityAt.getTime();
|
const createdDelta = right.agent.createdAt.getTime() - left.agent.createdAt.getTime();
|
||||||
}
|
if (createdDelta !== 0) {
|
||||||
|
return createdDelta;
|
||||||
|
}
|
||||||
|
|
||||||
function compareByTitleAsc(
|
|
||||||
left: SidebarAgentListEntry,
|
|
||||||
right: SidebarAgentListEntry
|
|
||||||
): number {
|
|
||||||
const leftTitle = (left.agent.title?.trim() || "New agent").toLocaleLowerCase();
|
const leftTitle = (left.agent.title?.trim() || "New agent").toLocaleLowerCase();
|
||||||
const rightTitle = (right.agent.title?.trim() || "New agent").toLocaleLowerCase();
|
const rightTitle = (right.agent.title?.trim() || "New agent").toLocaleLowerCase();
|
||||||
const titleCmp = leftTitle.localeCompare(rightTitle, undefined, {
|
const titleDelta = leftTitle.localeCompare(rightTitle, undefined, {
|
||||||
numeric: true,
|
numeric: true,
|
||||||
sensitivity: "base",
|
sensitivity: "base",
|
||||||
});
|
});
|
||||||
if (titleCmp !== 0) {
|
if (titleDelta !== 0) {
|
||||||
return titleCmp;
|
return titleDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
return left.agent.id.localeCompare(right.agent.id, undefined, {
|
return left.agent.id.localeCompare(right.agent.id, undefined, {
|
||||||
@@ -65,50 +64,66 @@ function compareByTitleAsc(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function applySidebarDefaultOrdering(
|
function toSidebarAgentKey(entry: SidebarAgentListEntry): string {
|
||||||
entries: SidebarAgentListEntry[]
|
return `${entry.agent.serverId}:${entry.agent.id}`;
|
||||||
): { entries: SidebarAgentListEntry[]; hasMore: boolean } {
|
}
|
||||||
const needsInput: SidebarAgentListEntry[] = [];
|
|
||||||
const failed: SidebarAgentListEntry[] = [];
|
|
||||||
const running: SidebarAgentListEntry[] = [];
|
|
||||||
const attention: SidebarAgentListEntry[] = [];
|
|
||||||
const done: SidebarAgentListEntry[] = [];
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
function applySidebarUserOrdering(input: {
|
||||||
const bucket = deriveSidebarStateBucket({
|
entries: SidebarAgentListEntry[];
|
||||||
|
order: string[];
|
||||||
|
}): { entries: SidebarAgentListEntry[]; hasMore: boolean } {
|
||||||
|
const entryKeySet = new Set(input.entries.map((entry) => toSidebarAgentKey(entry)));
|
||||||
|
const prunedOrder = input.order.filter((key) => entryKeySet.has(key));
|
||||||
|
const knownOrderSet = new Set(prunedOrder);
|
||||||
|
const newEntries = input.entries
|
||||||
|
.filter((entry) => !knownOrderSet.has(toSidebarAgentKey(entry)))
|
||||||
|
.sort(compareByCreatedAtDesc);
|
||||||
|
const effectiveOrder = [
|
||||||
|
...newEntries.map((entry) => toSidebarAgentKey(entry)),
|
||||||
|
...prunedOrder,
|
||||||
|
];
|
||||||
|
const orderIndexByKey = new Map<string, number>();
|
||||||
|
for (let index = 0; index < effectiveOrder.length; index += 1) {
|
||||||
|
orderIndexByKey.set(effectiveOrder[index] ?? "", index);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = [...input.entries].sort((left, right) => {
|
||||||
|
const leftOrder = orderIndexByKey.get(toSidebarAgentKey(left));
|
||||||
|
const rightOrder = orderIndexByKey.get(toSidebarAgentKey(right));
|
||||||
|
|
||||||
|
if (leftOrder === undefined && rightOrder === undefined) {
|
||||||
|
return compareByCreatedAtDesc(left, right);
|
||||||
|
}
|
||||||
|
if (leftOrder === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (rightOrder === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return leftOrder - rightOrder;
|
||||||
|
});
|
||||||
|
|
||||||
|
const active: SidebarAgentListEntry[] = [];
|
||||||
|
const done: SidebarAgentListEntry[] = [];
|
||||||
|
for (const entry of sorted) {
|
||||||
|
const isActive = isSidebarActiveAgent({
|
||||||
status: entry.agent.status,
|
status: entry.agent.status,
|
||||||
pendingPermissionCount: entry.agent.pendingPermissionCount,
|
pendingPermissionCount: entry.agent.pendingPermissionCount,
|
||||||
requiresAttention: entry.agent.requiresAttention,
|
requiresAttention: entry.agent.requiresAttention,
|
||||||
attentionReason: entry.agent.attentionReason,
|
attentionReason: entry.agent.attentionReason,
|
||||||
});
|
});
|
||||||
if (bucket === "needs_input") {
|
if (isActive) {
|
||||||
needsInput.push(entry);
|
active.push(entry);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (bucket === "failed") {
|
|
||||||
failed.push(entry);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (bucket === "running") {
|
|
||||||
running.push(entry);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (bucket === "attention") {
|
|
||||||
attention.push(entry);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
done.push(entry);
|
done.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
needsInput.sort(compareByLastActivityDesc);
|
|
||||||
failed.sort(compareByLastActivityDesc);
|
|
||||||
running.sort(compareByTitleAsc);
|
|
||||||
attention.sort(compareByLastActivityDesc);
|
|
||||||
done.sort(compareByLastActivityDesc);
|
|
||||||
|
|
||||||
const active = [...needsInput, ...failed, ...running, ...attention];
|
|
||||||
if (active.length >= SIDEBAR_DONE_FILL_TARGET) {
|
if (active.length >= SIDEBAR_DONE_FILL_TARGET) {
|
||||||
return { entries: active, hasMore: done.length > 0 };
|
return {
|
||||||
|
entries: active,
|
||||||
|
hasMore: done.length > 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const remainingDoneSlots = SIDEBAR_DONE_FILL_TARGET - active.length;
|
const remainingDoneSlots = SIDEBAR_DONE_FILL_TARGET - active.length;
|
||||||
@@ -147,7 +162,7 @@ function toAggregatedAgent(params: {
|
|||||||
|
|
||||||
export function useSidebarAgentsList(options?: {
|
export function useSidebarAgentsList(options?: {
|
||||||
serverId?: string | null;
|
serverId?: string | null;
|
||||||
selectedProjectFilterKeys?: string[];
|
selectedProjectFilterKey?: string | null;
|
||||||
}): SidebarAgentsListResult {
|
}): SidebarAgentsListResult {
|
||||||
const { daemons } = useDaemonRegistry();
|
const { daemons } = useDaemonRegistry();
|
||||||
const runtime = getHostRuntimeStore();
|
const runtime = getHostRuntimeStore();
|
||||||
@@ -171,14 +186,16 @@ export function useSidebarAgentsList(options?: {
|
|||||||
return daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
|
return daemons.find((daemon) => daemon.serverId === serverId)?.label ?? serverId;
|
||||||
}, [daemonLabelSignature, serverId]);
|
}, [daemonLabelSignature, serverId]);
|
||||||
|
|
||||||
const selectedProjectFilterKeys = useMemo(
|
const selectedProjectFilterKey = useMemo(() => {
|
||||||
() =>
|
const value = options?.selectedProjectFilterKey;
|
||||||
new Set(
|
if (typeof value !== "string") {
|
||||||
(options?.selectedProjectFilterKeys ?? [])
|
return null;
|
||||||
.map((item) => item.trim())
|
}
|
||||||
.filter((item) => item.length > 0)
|
const trimmed = value.trim();
|
||||||
),
|
return trimmed.length > 0 ? trimmed : null;
|
||||||
[options?.selectedProjectFilterKeys]
|
}, [options?.selectedProjectFilterKey]);
|
||||||
|
const persistedOrder = useSidebarOrderStore((state) =>
|
||||||
|
serverId ? state.orderByServerId[serverId] ?? EMPTY_ORDER : EMPTY_ORDER
|
||||||
);
|
);
|
||||||
|
|
||||||
const isActive = Boolean(serverId);
|
const isActive = Boolean(serverId);
|
||||||
@@ -213,7 +230,12 @@ export function useSidebarAgentsList(options?: {
|
|||||||
const [connectionStatus = "idle", directoryStatus = "idle"] =
|
const [connectionStatus = "idle", directoryStatus = "idle"] =
|
||||||
runtimeStatusSignature.split(":", 2);
|
runtimeStatusSignature.split(":", 2);
|
||||||
|
|
||||||
const { entries, projectFilterOptions, hasAnyData, hasMoreEntries } = useMemo(() => {
|
const {
|
||||||
|
entries,
|
||||||
|
projectFilterOptions,
|
||||||
|
hasAnyData,
|
||||||
|
hasMoreEntries,
|
||||||
|
} = useMemo(() => {
|
||||||
if (!isActive || !serverId || !liveAgents) {
|
if (!isActive || !serverId || !liveAgents) {
|
||||||
return {
|
return {
|
||||||
entries: [] as SidebarAgentListEntry[],
|
entries: [] as SidebarAgentListEntry[],
|
||||||
@@ -282,14 +304,15 @@ export function useSidebarAgentsList(options?: {
|
|||||||
pushEntry({ agent, project });
|
pushEntry({ agent, project });
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredEntries =
|
const filteredEntries = selectedProjectFilterKey
|
||||||
selectedProjectFilterKeys.size > 0
|
? mergedEntries.filter(
|
||||||
? mergedEntries.filter((entry) =>
|
(entry) => entry.project.projectKey === selectedProjectFilterKey
|
||||||
selectedProjectFilterKeys.has(entry.project.projectKey)
|
)
|
||||||
)
|
: mergedEntries;
|
||||||
: mergedEntries;
|
const ordered = applySidebarUserOrdering({
|
||||||
|
entries: filteredEntries,
|
||||||
const ordered = applySidebarDefaultOrdering(filteredEntries);
|
order: persistedOrder,
|
||||||
|
});
|
||||||
const options = Array.from(byProject.values()).sort((left, right) => {
|
const options = Array.from(byProject.values()).sort((left, right) => {
|
||||||
if (left.activeCount !== right.activeCount) {
|
if (left.activeCount !== right.activeCount) {
|
||||||
return right.activeCount - left.activeCount;
|
return right.activeCount - left.activeCount;
|
||||||
@@ -297,14 +320,21 @@ export function useSidebarAgentsList(options?: {
|
|||||||
return left.projectName.localeCompare(right.projectName);
|
return left.projectName.localeCompare(right.projectName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = {
|
return {
|
||||||
entries: ordered.entries,
|
entries: ordered.entries,
|
||||||
projectFilterOptions: options,
|
projectFilterOptions: options,
|
||||||
hasAnyData: ordered.entries.length > 0,
|
hasAnyData: mergedEntries.length > 0,
|
||||||
hasMoreEntries: ordered.hasMore,
|
hasMoreEntries: ordered.hasMore,
|
||||||
};
|
};
|
||||||
return result;
|
}, [
|
||||||
}, [agentLastActivity, isActive, liveAgents, selectedProjectFilterKeys, serverId, serverLabel]);
|
agentLastActivity,
|
||||||
|
isActive,
|
||||||
|
liveAgents,
|
||||||
|
persistedOrder,
|
||||||
|
selectedProjectFilterKey,
|
||||||
|
serverId,
|
||||||
|
serverLabel,
|
||||||
|
]);
|
||||||
|
|
||||||
const refreshAll = useCallback(() => {
|
const refreshAll = useCallback(() => {
|
||||||
if (!isActive || !serverId || connectionStatus !== "online") {
|
if (!isActive || !serverId || connectionStatus !== "online") {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import { create } from "zustand";
|
||||||
|
import { createJSONStorage, persist } from "zustand/middleware";
|
||||||
|
|
||||||
|
interface SidebarOrderStoreState {
|
||||||
|
orderByServerId: Record<string, string[]>;
|
||||||
|
getOrder: (serverId: string) => string[];
|
||||||
|
setOrder: (serverId: string, keys: string[]) => void;
|
||||||
|
insertAtTop: (serverId: string, key: string) => void;
|
||||||
|
remove: (serverId: string, key: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeKeys(keys: string[]): string[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const normalized: string[] = [];
|
||||||
|
|
||||||
|
for (const rawKey of keys) {
|
||||||
|
const key = rawKey.trim();
|
||||||
|
if (!key || seen.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seen.add(key);
|
||||||
|
normalized.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSidebarOrderStore = create<SidebarOrderStoreState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
orderByServerId: {},
|
||||||
|
getOrder: (serverId) => {
|
||||||
|
const key = serverId.trim();
|
||||||
|
if (!key) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return get().orderByServerId[key] ?? [];
|
||||||
|
},
|
||||||
|
setOrder: (serverId, keys) => {
|
||||||
|
const key = serverId.trim();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizeKeys(keys);
|
||||||
|
set((state) => ({
|
||||||
|
orderByServerId: {
|
||||||
|
...state.orderByServerId,
|
||||||
|
[key]: normalized,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
insertAtTop: (serverId, rawKey) => {
|
||||||
|
const serverKey = serverId.trim();
|
||||||
|
const entryKey = rawKey.trim();
|
||||||
|
if (!serverKey || !entryKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set((state) => {
|
||||||
|
const current = state.orderByServerId[serverKey] ?? [];
|
||||||
|
const next = [entryKey, ...current.filter((key) => key !== entryKey)];
|
||||||
|
return {
|
||||||
|
orderByServerId: {
|
||||||
|
...state.orderByServerId,
|
||||||
|
[serverKey]: next,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
remove: (serverId, rawKey) => {
|
||||||
|
const serverKey = serverId.trim();
|
||||||
|
const entryKey = rawKey.trim();
|
||||||
|
if (!serverKey || !entryKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set((state) => {
|
||||||
|
const current = state.orderByServerId[serverKey] ?? [];
|
||||||
|
const next = current.filter((key) => key !== entryKey);
|
||||||
|
if (next.length === current.length) {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
orderByServerId: {
|
||||||
|
...state.orderByServerId,
|
||||||
|
[serverKey]: next,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: "sidebar-agent-order",
|
||||||
|
storage: createJSONStorage(() => AsyncStorage),
|
||||||
|
partialize: (state) => ({
|
||||||
|
orderByServerId: state.orderByServerId,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user