Skip to content

Commit

Permalink
Merge pull request #3869 from continuedev/dallin/unselectable-loading…
Browse files Browse the repository at this point in the history
…-item

Submenu loading items - unselectable Loading item + abort controllers
RomneyDa authored Jan 28, 2025
2 parents e625b24 + 4ba2aef commit a5d01c6
Showing 2 changed files with 197 additions and 151 deletions.
164 changes: 93 additions & 71 deletions gui/src/components/mainInput/MentionList.tsx
Original file line number Diff line number Diff line change
@@ -155,7 +155,6 @@ const ItemDiv = styled.div`
text-align: left;
width: 100%;
color: ${vscForeground};
cursor: pointer;
&.is-selected {
background-color: ${vscListActiveBackground};
@@ -203,6 +202,9 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
const [querySubmenuItem, setQuerySubmenuItem] = useState<
ComboBoxItem | undefined
>(undefined);
const [loadingSubmenuItem, setLoadingSubmenuItem] = useState<
ComboBoxItem | undefined
>(undefined);

const [allItems, setAllItems] = useState<ComboBoxItem[]>([]);

@@ -245,8 +247,8 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
description: "Create a new .prompt file",
});
}

setAllItems(items);
setLoadingSubmenuItem(items.find((item) => item.id === "loading"));
setAllItems(items.filter((item) => item.id !== "loading"));
}, [subMenuTitle, props.items, props.editor]);

const queryInputRef = useRef<HTMLTextAreaElement>(null);
@@ -403,78 +405,98 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
) : (
<>
{subMenuTitle && <ItemDiv className="mb-2">{subMenuTitle}</ItemDiv>}
{loadingSubmenuItem && (
<ItemDiv>
<span className="flex w-full items-center justify-between">
<div className="flex items-center justify-center">
<DropdownIcon item={loadingSubmenuItem} className="mr-2" />
<span>{loadingSubmenuItem.title}</span>
{" "}
</div>
<span
style={{
color: lightGray,
float: "right",
textAlign: "right",
minWidth: "30px",
}}
className="ml-2 flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap text-xs"
>
{loadingSubmenuItem.description}
</span>
</span>
</ItemDiv>
)}
{allItems.length ? (
allItems.map((item, index) => (
<ItemDiv
as="button"
ref={(el) => (itemRefs.current[index] = el)}
className={`item ${
index === selectedIndex ? "is-selected" : ""
}`}
key={index}
onClick={() => selectItem(index)}
onMouseEnter={() => setSelectedIndex(index)}
data-testid="context-provider-dropdown-item"
>
<span className="flex w-full items-center justify-between">
<div className="flex items-center justify-center">
{showFileIconForItem(item) && (
<FileIcon
height="20px"
width="20px"
filename={item.description}
/>
)}
{!showFileIconForItem(item) && (
<>
<DropdownIcon item={item} className="mr-2" />
</>
)}
<span title={item.id}>{item.title}</span>
{" "}
</div>
<span
style={{
color: lightGray,
float: "right",
textAlign: "right",
opacity: index !== selectedIndex ? 0 : 1,
minWidth: "30px",
}}
className="ml-2 flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap"
>
{item.description}
{item.type === "contextProvider" &&
item.contextProvider?.type === "submenu" && (
<ArrowRightIcon
className="ml-2 flex-shrink-0"
width="1.2em"
height="1.2em"
allItems.map((item, index) => {
const isSelected = index === selectedIndex;
return (
<ItemDiv
as="button"
ref={(el) => (itemRefs.current[index] = el)}
className={`item cursor-pointer ${isSelected ? "is-selected" : ""}`}
key={index}
onClick={() => selectItem(index)}
onMouseEnter={() => setSelectedIndex(index)}
data-testid="context-provider-dropdown-item"
>
<span className="flex w-full items-center justify-between">
<div className="flex items-center justify-center">
{showFileIconForItem(item) ? (
<FileIcon
height="20px"
width="20px"
filename={item.description}
/>
) : (
<DropdownIcon item={item} className="mr-2" />
)}
{item.subActions?.map((subAction) => {
const Icon = getIconFromDropdownItem(
subAction.icon,
"action",
);
return (
<HeaderButtonWithToolTip
onClick={(e) => {
subAction.action(item);
e.stopPropagation();
e.preventDefault();
props.onClose();
}}
text={undefined}
>
<Icon width="1.2em" height="1.2em" />
</HeaderButtonWithToolTip>
);
})}
<span title={item.id}>{item.title}</span>
{" "}
</div>
<span
style={{
color: lightGray,
float: "right",
textAlign: "right",
opacity: isSelected ? 1 : 0,
minWidth: "30px",
}}
className="ml-2 flex items-center overflow-hidden overflow-ellipsis whitespace-nowrap text-xs"
>
{item.description}
{item.type === "contextProvider" &&
item.contextProvider?.type === "submenu" && (
<ArrowRightIcon
className="ml-2 flex-shrink-0"
width="1.2em"
height="1.2em"
/>
)}
{item.subActions?.map((subAction) => {
const Icon = getIconFromDropdownItem(
subAction.icon,
"action",
);
return (
<HeaderButtonWithToolTip
onClick={(e) => {
subAction.action(item);
e.stopPropagation();
e.preventDefault();
props.onClose();
}}
text={undefined}
>
<Icon width="1.2em" height="1.2em" />
</HeaderButtonWithToolTip>
);
})}
</span>
</span>
</span>
</ItemDiv>
))
</ItemDiv>
);
})
) : (
<ItemDiv className="item">No results</ItemDiv>
)}
184 changes: 104 additions & 80 deletions gui/src/context/SubmenuContextProviders.tsx
Original file line number Diff line number Diff line change
@@ -116,7 +116,10 @@ export const SubmenuContextProvidersProvider = ({
};
}, [ideMessenger]);

const providersLoading = useRef(new Set<string>()).current;
const providersLoading = useRef(new Set<ContextProviderName>()).current;
const abortControllers = useRef(
new Map<ContextProviderName, AbortController>(),
).current;

const getSubmenuContextItems = useCallback(
(
@@ -217,94 +220,116 @@ export const SubmenuContextProvidersProvider = ({
);

const loadSubmenuItems = useCallback(
(providers: "dependsOnIndexing" | "all" | ContextProviderName[]) => {
submenuContextProviders.forEach(
async (description: ContextProviderDescription) => {
try {
if (providersLoading.has(description.title)) {
return;
}
providersLoading.add(description.title);

const refreshProvider =
providers === "all"
? true
: providers === "dependsOnIndexing"
? description.dependsOnIndexing
: providers.includes(description.title);

if (!refreshProvider) {
return;
}
async (providers: "dependsOnIndexing" | "all" | ContextProviderName[]) => {
await Promise.allSettled(
submenuContextProviders.map(
async (description: ContextProviderDescription) => {
const controller = new AbortController();
try {
const refreshProvider =
providers === "all"
? true
: providers === "dependsOnIndexing"
? description.dependsOnIndexing
: providers.includes(description.title);

if (!refreshProvider) {
if (providers === "dependsOnIndexing") {
console.debug(
`Skipping ${description.title} provider due to disabled indexing`,
);
}
return;
}

if (description.dependsOnIndexing && disableIndexing) {
console.debug(
`Skipping ${description.title} provider due to disabled indexing`,
// Submenu loading requests cancel existing requests
abortControllers.get(description.title)?.abort();
abortControllers.set(description.title, controller);
providersLoading.add(description.title);
console.log(
`Refreshing items for ${description.title} submenu provider`,
);
const result = await ideMessenger.request(
"context/loadSubmenuItems",
{
title: description.title,
},
);
return;
}
const result = await ideMessenger.request(
"context/loadSubmenuItems",
{
title: description.title,
},
);

if (result.status === "error") {
throw new Error(result.error);
}
const submenuItems = result.content;
const providerTitle = description.title;

const itemsWithProvider = submenuItems.map((item) => ({
...item,
providerTitle,
}));
// IMPORTANT - the controller only prevents invalid loading state
// But does not cancel using data from the request
// Could uncomment this to truly cancel the request
// if (controller.signal.aborted) {
// return console.debug(
// `${description.title} provider loading aborted`,
// );
// }

if (result.status === "error") {
throw new Error(result.error);
}
const submenuItems = result.content;
const providerTitle = description.title;

const minisearch = new MiniSearch<ContextSubmenuItemWithProvider>({
fields: ["title", "description"],
storeFields: ["id", "title", "description", "providerTitle"],
});
const itemsWithProvider = submenuItems.map((item) => ({
...item,
providerTitle,
}));

minisearch.addAll(
submenuItems.map((item) => ({ ...item, providerTitle })),
);
const minisearch = new MiniSearch<ContextSubmenuItemWithProvider>(
{
fields: ["title", "description"],
storeFields: ["id", "title", "description", "providerTitle"],
},
);

setMinisearches((prev) => ({
...prev,
[providerTitle]: minisearch,
}));
minisearch.addAll(
submenuItems.map((item) => ({ ...item, providerTitle })),
);

if (providerTitle === "file") {
setFallbackResults((prev) => ({
...prev,
file: deduplicateArray(
[...lastOpenFilesRef.current, ...(prev.file ?? [])],
(a, b) => a.id === b.id,
),
}));
} else {
setFallbackResults((prev) => ({
setMinisearches((prev) => ({
...prev,
[providerTitle]: itemsWithProvider,
[providerTitle]: minisearch,
}));

if (providerTitle === "file") {
setFallbackResults((prev) => ({
...prev,
file: deduplicateArray(
[...lastOpenFilesRef.current, ...(prev.file ?? [])],
(a, b) => a.id === b.id,
),
}));
} else {
setFallbackResults((prev) => ({
...prev,
[providerTitle]: itemsWithProvider,
}));
}
} catch (error) {
console.error(
`Error loading items for ${description.title}:`,
error,
);
console.error(
"Error details:",
JSON.stringify(error, Object.getOwnPropertyNames(error)),
);
} finally {
if (!controller.signal.aborted) {
providersLoading.delete(description.title);
}
}
} catch (error) {
console.error(
`Error loading items for ${description.title}:`,
error,
);
console.error(
"Error details:",
JSON.stringify(error, Object.getOwnPropertyNames(error)),
);
} finally {
providersLoading.delete(description.title);
}
},
},
),
);
},
[submenuContextProviders, disableIndexing, providersLoading],
[
submenuContextProviders,
disableIndexing,
providersLoading,
abortControllers,
],
);

useWebviewListener(
@@ -318,14 +343,13 @@ export const SubmenuContextProvidersProvider = ({
// Reload all submenu items on the initial config load
// TODO - could refresh on any change
const initialLoad = useRef(false);
const config = useAppSelector((store) => store.config.config);
useEffect(() => {
if (!config?.contextProviders?.length || initialLoad.current) {
if (!submenuContextProviders.length || initialLoad.current) {
return;
}
void loadSubmenuItems("all");
initialLoad.current = true;
}, [loadSubmenuItems, config, initialLoad]);
}, [loadSubmenuItems, submenuContextProviders, initialLoad]);

return (
<SubmenuContextProvidersContext.Provider

0 comments on commit a5d01c6

Please sign in to comment.