Update files
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
View,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import { shortenPath } from "@/utils/shorten-path";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
|
||||
function agentKey(agent: Pick<AggregatedAgent, "serverId" | "id">): string {
|
||||
return `${agent.serverId}:${agent.id}`;
|
||||
}
|
||||
|
||||
function isMatch(agent: AggregatedAgent, query: string): boolean {
|
||||
if (!query) return true;
|
||||
const q = query.toLowerCase();
|
||||
const title = (agent.title ?? "New agent").toLowerCase();
|
||||
const cwd = agent.cwd.toLowerCase();
|
||||
const host = agent.serverLabel.toLowerCase();
|
||||
return title.includes(q) || cwd.includes(q) || host.includes(q);
|
||||
}
|
||||
|
||||
function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
|
||||
const leftAttention = left.requiresAttention ? 1 : 0;
|
||||
const rightAttention = right.requiresAttention ? 1 : 0;
|
||||
if (leftAttention !== rightAttention) return rightAttention - leftAttention;
|
||||
|
||||
const leftRunning = left.status === "running" ? 1 : 0;
|
||||
const rightRunning = right.status === "running" ? 1 : 0;
|
||||
if (leftRunning !== rightRunning) return rightRunning - leftRunning;
|
||||
|
||||
return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
|
||||
}
|
||||
|
||||
export function CommandCenter() {
|
||||
const { theme } = useUnistyles();
|
||||
const pathname = usePathname();
|
||||
const { agents } = useAggregatedAgents();
|
||||
const open = useKeyboardNavStore((s) => s.commandCenterOpen);
|
||||
const setOpen = useKeyboardNavStore((s) => s.setCommandCenterOpen);
|
||||
const inputRef = useRef<TextInput>(null);
|
||||
const [query, setQuery] = useState("");
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
|
||||
const results = useMemo(() => {
|
||||
const filtered = agents.filter((agent) => isMatch(agent, query));
|
||||
filtered.sort(sortAgents);
|
||||
return filtered;
|
||||
}, [agents, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setQuery("");
|
||||
setActiveIndex(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = setTimeout(() => {
|
||||
inputRef.current?.focus();
|
||||
}, 0);
|
||||
return () => clearTimeout(id);
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (activeIndex >= results.length) {
|
||||
setActiveIndex(results.length > 0 ? results.length - 1 : 0);
|
||||
}
|
||||
}, [activeIndex, open, results.length]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(agent: AggregatedAgent) => {
|
||||
const session = useSessionStore.getState().sessions[agent.serverId];
|
||||
session?.client?.clearAgentAttention(agent.id);
|
||||
|
||||
const shouldReplace = pathname.startsWith("/agent/");
|
||||
const navigate = shouldReplace ? router.replace : router.push;
|
||||
|
||||
setOpen(false);
|
||||
navigate(`/agent/${agent.serverId}/${agent.id}` as any);
|
||||
},
|
||||
[pathname, setOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
const handler = (event: KeyboardEvent) => {
|
||||
const key = event.key;
|
||||
if (key !== "ArrowDown" && key !== "ArrowUp" && key !== "Enter" && key !== "Escape") {
|
||||
return;
|
||||
}
|
||||
if (key === "Escape") {
|
||||
event.preventDefault();
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
if (key === "Enter") {
|
||||
if (results.length === 0) return;
|
||||
event.preventDefault();
|
||||
const index = Math.max(0, Math.min(activeIndex, results.length - 1));
|
||||
handleSelect(results[index]!);
|
||||
return;
|
||||
}
|
||||
if (key === "ArrowDown" || key === "ArrowUp") {
|
||||
if (results.length === 0) return;
|
||||
event.preventDefault();
|
||||
setActiveIndex((current) => {
|
||||
const delta = key === "ArrowDown" ? 1 : -1;
|
||||
const next = current + delta;
|
||||
if (next < 0) return results.length - 1;
|
||||
if (next >= results.length) return 0;
|
||||
return next;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// react-native-web can stop propagation on key events, so listen in capture phase.
|
||||
window.addEventListener("keydown", handler, true);
|
||||
return () => window.removeEventListener("keydown", handler, true);
|
||||
}, [activeIndex, handleClose, handleSelect, open, results]);
|
||||
|
||||
if (Platform.OS !== "web") return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={open}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<Pressable style={styles.backdrop} onPress={handleClose} />
|
||||
|
||||
<View style={[styles.panel, { borderColor: theme.colors.border, backgroundColor: theme.colors.surface0 }]}>
|
||||
<View style={[styles.header, { borderBottomColor: theme.colors.border }]}>
|
||||
<TextInput
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
placeholder="Search agents…"
|
||||
placeholderTextColor={theme.colors.foregroundMuted}
|
||||
style={[
|
||||
styles.input,
|
||||
{ color: theme.colors.foreground },
|
||||
]}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
autoFocus
|
||||
/>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.results}
|
||||
contentContainerStyle={styles.resultsContent}
|
||||
keyboardShouldPersistTaps="always"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{results.length === 0 ? (
|
||||
<Text style={[styles.emptyText, { color: theme.colors.foregroundMuted }]}>
|
||||
No matches
|
||||
</Text>
|
||||
) : (
|
||||
results.map((agent, index) => {
|
||||
const active = index === activeIndex;
|
||||
return (
|
||||
<Pressable
|
||||
key={agentKey(agent)}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.row,
|
||||
(hovered || pressed || active) && {
|
||||
backgroundColor: theme.colors.surface1,
|
||||
},
|
||||
]}
|
||||
onPress={() => handleSelect(agent)}
|
||||
>
|
||||
<View style={styles.rowContent}>
|
||||
<Text
|
||||
style={[styles.title, { color: theme.colors.foreground }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.title || "New agent"}
|
||||
</Text>
|
||||
<Text
|
||||
style={[styles.subtitle, { color: theme.colors.foregroundMuted }]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{agent.serverLabel} · {shortenPath(agent.cwd)} · {formatTimeAgo(agent.lastActivityAt)}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
justifyContent: "flex-start",
|
||||
alignItems: "center",
|
||||
paddingTop: theme.spacing[12],
|
||||
},
|
||||
backdrop: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
},
|
||||
panel: {
|
||||
width: 640,
|
||||
maxWidth: "92%",
|
||||
maxHeight: "80%",
|
||||
borderWidth: 1,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
overflow: "hidden",
|
||||
shadowColor: "#000",
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 24,
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
borderBottomWidth: 1,
|
||||
},
|
||||
input: {
|
||||
fontSize: theme.fontSize.lg,
|
||||
paddingVertical: theme.spacing[1],
|
||||
outlineStyle: "none",
|
||||
} as any,
|
||||
results: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
resultsContent: {
|
||||
paddingVertical: theme.spacing[2],
|
||||
},
|
||||
row: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[3],
|
||||
},
|
||||
rowContent: {
|
||||
gap: 2,
|
||||
},
|
||||
title: {
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
emptyText: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[4],
|
||||
fontSize: theme.fontSize.base,
|
||||
},
|
||||
}));
|
||||
@@ -17,25 +17,20 @@ import {
|
||||
import { router, usePathname } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { type GestureType } from "react-native-gesture-handler";
|
||||
import { useQueries, useQueryClient, type UseQueryOptions } from "@tanstack/react-query";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Archive, ChevronDown, ChevronRight, Plus } from "lucide-react-native";
|
||||
import {
|
||||
DraggableList,
|
||||
type DraggableRenderItemInfo,
|
||||
} from "./draggable-list";
|
||||
import { formatTimeAgo } from "@/utils/time";
|
||||
import {
|
||||
groupAgents,
|
||||
parseRepoNameFromRemoteUrl,
|
||||
parseRepoShortNameFromRemoteUrl,
|
||||
} from "@/utils/agent-grouping";
|
||||
import { parseRepoNameFromRemoteUrl, parseRepoShortNameFromRemoteUrl } from "@/utils/agent-grouping";
|
||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import {
|
||||
type CheckoutStatusPayload,
|
||||
CHECKOUT_STATUS_STALE_TIME,
|
||||
checkoutStatusQueryKey,
|
||||
useCheckoutStatusCacheOnly,
|
||||
CHECKOUT_STATUS_STALE_TIME,
|
||||
} from "@/hooks/use-checkout-status-query";
|
||||
import {
|
||||
buildAgentNavigationKey,
|
||||
@@ -43,22 +38,14 @@ import {
|
||||
} from "@/utils/navigation-timing";
|
||||
import {
|
||||
useSectionOrderStore,
|
||||
sortProjectsByStoredOrder,
|
||||
} from "@/stores/section-order-store";
|
||||
import { useProjectIconQuery } from "@/hooks/use-project-icon-query";
|
||||
import { useSidebarAgentSections, type SidebarSectionData } from "@/hooks/use-sidebar-agent-sections";
|
||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||
import { getIsTauri } from "@/constants/layout";
|
||||
|
||||
interface SectionData {
|
||||
key: string;
|
||||
projectKey: string;
|
||||
title: string;
|
||||
agents: AggregatedAgent[];
|
||||
/** For project sections, the first agent's serverId (to lookup checkout status) */
|
||||
firstAgentServerId?: string;
|
||||
/** For project sections, the first agent's id (to lookup checkout status) */
|
||||
firstAgentId?: string;
|
||||
/** Working directory for the project (from first agent) */
|
||||
workingDir?: string;
|
||||
}
|
||||
type SectionData = SidebarSectionData;
|
||||
|
||||
interface GroupedAgentListProps {
|
||||
agents: AggregatedAgent[];
|
||||
@@ -209,9 +196,24 @@ export function GroupedAgentList({
|
||||
const queryClient = useQueryClient();
|
||||
const insets = useSafeAreaInsets();
|
||||
const [actionAgent, setActionAgent] = useState<AggregatedAgent | null>(null);
|
||||
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
|
||||
const collapsedProjectKeys = useSidebarCollapsedSectionsStore((s) => s.collapsedProjectKeys);
|
||||
const toggleProjectCollapsed = useSidebarCollapsedSectionsStore((s) => s.toggleProjectCollapsed);
|
||||
|
||||
const altDown = useKeyboardNavStore((s) => s.altDown);
|
||||
const cmdOrCtrlDown = useKeyboardNavStore((s) => s.cmdOrCtrlDown);
|
||||
const sidebarShortcutAgentKeys = useKeyboardNavStore((s) => s.sidebarShortcutAgentKeys);
|
||||
const isTauri = getIsTauri();
|
||||
const showShortcutBadges = altDown || (isTauri && cmdOrCtrlDown);
|
||||
const shortcutIndexByAgentKey = useMemo(() => {
|
||||
const map = new Map<string, number>();
|
||||
for (let i = 0; i < sidebarShortcutAgentKeys.length; i++) {
|
||||
const key = sidebarShortcutAgentKeys[i];
|
||||
if (!key) continue;
|
||||
map.set(key, i + 1);
|
||||
}
|
||||
return map;
|
||||
}, [sidebarShortcutAgentKeys]);
|
||||
|
||||
const actionClient = useSessionStore((state) =>
|
||||
actionAgent?.serverId ? state.sessions[actionAgent.serverId]?.client ?? null : null
|
||||
@@ -265,18 +267,6 @@ export function GroupedAgentList({
|
||||
setActionAgent(null);
|
||||
}, [actionAgent, actionClient]);
|
||||
|
||||
const toggleSection = useCallback((sectionKey: string) => {
|
||||
setCollapsedSections((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(sectionKey)) {
|
||||
next.delete(sectionKey);
|
||||
} else {
|
||||
next.add(sectionKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleCreateAgentInProject = useCallback(
|
||||
(workingDir: string) => {
|
||||
onAgentSelect?.();
|
||||
@@ -315,87 +305,10 @@ export function GroupedAgentList({
|
||||
}
|
||||
}, [agents, queryClient]);
|
||||
|
||||
// Subscribe to checkout status cache entries so project grouping can react
|
||||
// to remote URL updates (e.g. git worktrees in different directories).
|
||||
const checkoutCacheQueries = useQueries({
|
||||
queries: agents.map(
|
||||
(agent): UseQueryOptions<CheckoutStatusPayload> => ({
|
||||
queryKey: checkoutStatusQueryKey(agent.serverId, agent.cwd),
|
||||
enabled: false,
|
||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||
queryFn: async (): Promise<CheckoutStatusPayload> => {
|
||||
throw new Error("checkout status cache-only query should not run");
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const remoteUrlByAgentKey = useMemo(() => {
|
||||
const result = new Map<string, string | null>();
|
||||
for (let i = 0; i < agents.length; i++) {
|
||||
const agent = agents[i];
|
||||
if (!agent) {
|
||||
continue;
|
||||
}
|
||||
const checkout = checkoutCacheQueries[i]?.data ?? null;
|
||||
const remoteUrl = checkout?.remoteUrl ?? null;
|
||||
result.set(`${agent.serverId}:${agent.id}`, remoteUrl);
|
||||
}
|
||||
return result;
|
||||
}, [agents, checkoutCacheQueries]);
|
||||
|
||||
// Section order from store
|
||||
const projectOrder = useSectionOrderStore((state) => state.projectOrder);
|
||||
const setProjectOrder = useSectionOrderStore((state) => state.setProjectOrder);
|
||||
|
||||
// Group agents
|
||||
const { activeGroups } = useMemo(
|
||||
() =>
|
||||
groupAgents(agents, {
|
||||
getRemoteUrl: (agent) =>
|
||||
remoteUrlByAgentKey.get(`${agent.serverId}:${agent.id}`) ?? null,
|
||||
}),
|
||||
[agents, remoteUrlByAgentKey]
|
||||
);
|
||||
|
||||
// Sort groups by persisted order
|
||||
const sortedGroups = useMemo(
|
||||
() => sortProjectsByStoredOrder(activeGroups, projectOrder),
|
||||
[activeGroups, projectOrder]
|
||||
);
|
||||
|
||||
// Build sections for DraggableFlatList
|
||||
const sections: SectionData[] = useMemo(() => {
|
||||
const result: SectionData[] = [];
|
||||
|
||||
for (const group of sortedGroups) {
|
||||
const sectionKey = `project:${group.projectKey}`;
|
||||
const firstAgent = group.agents[0];
|
||||
result.push({
|
||||
key: sectionKey,
|
||||
projectKey: group.projectKey,
|
||||
title: group.projectName,
|
||||
agents: group.agents,
|
||||
firstAgentServerId: firstAgent?.serverId,
|
||||
firstAgentId: firstAgent?.id,
|
||||
workingDir: firstAgent?.cwd,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [sortedGroups]);
|
||||
|
||||
// Sync section order when new projects appear
|
||||
useEffect(() => {
|
||||
const currentKeys = sections.map((s) => s.projectKey);
|
||||
const storedKeys = new Set(projectOrder);
|
||||
const newKeys = currentKeys.filter((key) => !storedKeys.has(key));
|
||||
|
||||
if (newKeys.length > 0) {
|
||||
// Add new projects at the end of the stored order
|
||||
setProjectOrder([...projectOrder, ...newKeys]);
|
||||
}
|
||||
}, [sections, projectOrder, setProjectOrder]);
|
||||
const sections: SectionData[] = useSidebarAgentSections(agents);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(newData: SectionData[]) => {
|
||||
@@ -424,6 +337,8 @@ export function GroupedAgentList({
|
||||
const agentKey = `${agent.serverId}:${agent.id}`;
|
||||
const isSelected = selectedAgentId === agentKey;
|
||||
const isRunning = agent.status === "running";
|
||||
const shortcutNumber =
|
||||
showShortcutBadges ? (shortcutIndexByAgentKey.get(agentKey) ?? null) : null;
|
||||
const statusColor = isRunning
|
||||
? theme.colors.palette.blue[500]
|
||||
: agent.requiresAttention
|
||||
@@ -476,7 +391,13 @@ export function GroupedAgentList({
|
||||
>
|
||||
{agent.title || "New agent"}
|
||||
</Text>
|
||||
{isHovered && canArchive ? (
|
||||
{shortcutNumber !== null ? (
|
||||
<View style={styles.branchBadge}>
|
||||
<Text style={styles.branchBadgeText} numberOfLines={1}>
|
||||
{shortcutNumber}
|
||||
</Text>
|
||||
</View>
|
||||
) : isHovered && canArchive ? (
|
||||
<Pressable
|
||||
style={styles.branchBadge}
|
||||
onPress={(e) => handleArchiveAgent(e, agent)}
|
||||
@@ -512,6 +433,8 @@ export function GroupedAgentList({
|
||||
handleAgentPress,
|
||||
handleArchiveAgent,
|
||||
selectedAgentId,
|
||||
showShortcutBadges,
|
||||
shortcutIndexByAgentKey,
|
||||
theme.colors.foreground,
|
||||
theme.colors.foregroundMuted,
|
||||
theme.colors.palette.blue,
|
||||
@@ -521,14 +444,14 @@ export function GroupedAgentList({
|
||||
|
||||
const renderSection = useCallback(
|
||||
({ item: section, drag, isActive }: DraggableRenderItemInfo<SectionData>) => {
|
||||
const isCollapsed = collapsedSections.has(section.key);
|
||||
const isCollapsed = collapsedProjectKeys.has(section.projectKey);
|
||||
|
||||
return (
|
||||
<View style={[styles.sectionContainer, isActive && styles.sectionDragging]}>
|
||||
<SectionHeader
|
||||
section={section}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggle={() => toggleSection(section.key)}
|
||||
onToggle={() => toggleProjectCollapsed(section.projectKey)}
|
||||
onCreateAgent={handleCreateAgentInProject}
|
||||
onDrag={drag}
|
||||
isDragging={isActive}
|
||||
@@ -540,7 +463,7 @@ export function GroupedAgentList({
|
||||
</View>
|
||||
);
|
||||
},
|
||||
[AgentListRow, collapsedSections, handleCreateAgentInProject, toggleSection]
|
||||
[AgentListRow, collapsedProjectKeys, handleCreateAgentInProject, toggleProjectCollapsed]
|
||||
);
|
||||
|
||||
const keyExtractor = useCallback(
|
||||
|
||||
@@ -19,6 +19,10 @@ import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-wind
|
||||
import { useVoice } from "@/contexts/voice-context";
|
||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||
import { VoicePanel } from "./voice-panel";
|
||||
import { useSidebarAgentSections } from "@/hooks/use-sidebar-agent-sections";
|
||||
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
|
||||
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
|
||||
import { deriveSidebarShortcutAgentKeys } from "@/utils/sidebar-shortcuts";
|
||||
|
||||
const DESKTOP_SIDEBAR_WIDTH = 320;
|
||||
const SIDEBAR_AGENT_LIMIT = 15;
|
||||
@@ -83,6 +87,17 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
|
||||
[sortedAgents]
|
||||
);
|
||||
|
||||
const sidebarSections = useSidebarAgentSections(limitedAgents);
|
||||
const collapsedProjectKeys = useSidebarCollapsedSectionsStore((s) => s.collapsedProjectKeys);
|
||||
const setSidebarShortcutAgentKeys = useKeyboardNavStore((s) => s.setSidebarShortcutAgentKeys);
|
||||
const sidebarShortcutAgentKeys = useMemo(() => {
|
||||
return deriveSidebarShortcutAgentKeys(sidebarSections, collapsedProjectKeys, 9);
|
||||
}, [collapsedProjectKeys, sidebarSections]);
|
||||
|
||||
useEffect(() => {
|
||||
setSidebarShortcutAgentKeys(sidebarShortcutAgentKeys);
|
||||
}, [setSidebarShortcutAgentKeys, sidebarShortcutAgentKeys]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
closeToAgent();
|
||||
}, [closeToAgent]);
|
||||
|
||||
Reference in New Issue
Block a user