From 72f76f0b6d48dca5ac4bcf746fc38ba450149689 Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:23:39 +0200 Subject: [PATCH 1/9] fix: deduplicate tool count fallback --- client/src/components/Panels/utilities.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/src/components/Panels/utilities.ts b/client/src/components/Panels/utilities.ts index 38485291fd9..98e9423bea3 100644 --- a/client/src/components/Panels/utilities.ts +++ b/client/src/components/Panels/utilities.ts @@ -67,6 +67,24 @@ export function getUniqueToolIdsInPanel(panel: Record | n return toolIds; } +type CountableTool = Pick & Partial>; + +export function getVersionlessToolId(toolId: string, tool: CountableTool | undefined) { + if (tool?.version && toolId.endsWith(`/${tool.version}`)) { + return toolId.slice(0, -tool.version.length - 1); + } + return toolId; +} + +export function countUniqueToolsInList(tools: Record | T[]) { + const toolEntries = Array.isArray(tools) ? tools.map((tool) => [tool.id, tool] as const) : Object.entries(tools); + const toolIds = new Set(); + for (const [toolId, tool] of toolEntries) { + toolIds.add(getVersionlessToolId(toolId, tool)); + } + return toolIds.size; +} + export function countUniqueToolsInPanel(panel: Record | null | undefined, fallbackCount = 0) { return getUniqueToolIdsInPanel(panel).size || fallbackCount; } From c11a284d6f5440def8b2ee7377c915157f54933a Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:23:45 +0200 Subject: [PATCH 2/9] test: cover versioned tool count fallback --- client/src/components/Panels/utilities.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/components/Panels/utilities.test.ts b/client/src/components/Panels/utilities.test.ts index cb770e1844e..3b892f4ad03 100644 --- a/client/src/components/Panels/utilities.test.ts +++ b/client/src/components/Panels/utilities.test.ts @@ -4,6 +4,7 @@ import toolsListInPanelUntyped from "@/components/ToolsView/testData/toolsListIn import { describe, expect, it } from "vitest"; import { + countUniqueToolsInList, createSortedResultPanel, createWhooshQuery, determineWidth, @@ -98,6 +99,12 @@ describe("test helpers in tool searching utilities", () => { expect(createWhooshQuery({ section: '"Get Data"' })).toEqual('(section:("Get Data"))'); }); + it("counts versioned tools once when falling back to the full tools list", async () => { + const firstTool = toolsList[0]!; + const toolsWithOlderVersion = [...toolsList, { ...firstTool, id: `${firstTool.id}/0.9`, version: "0.9" }]; + expect(countUniqueToolsInList(toolsWithOlderVersion)).toBe(toolsList.length); + }); + it("lowercases tag clauses to match the Whoosh field analyzer", async () => { // Whoosh indexes tool_tags lowercased; lowercase the search clause too. expect(createWhooshQuery({ tag: ["Get Data"] })).toEqual('(tool_tags:("get data"))'); From 4949d66c4a9ecc46c8955437713e63c81e54dd96 Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:23:50 +0200 Subject: [PATCH 3/9] fix: deduplicate tool panel count fallback --- client/src/components/Panels/ToolPanel.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue index eaab5725850..57de05c1884 100644 --- a/client/src/components/Panels/ToolPanel.vue +++ b/client/src/components/Panels/ToolPanel.vue @@ -9,7 +9,7 @@ import localize from "@/utils/localization"; import { errorMessageAsString } from "@/utils/simple-error"; import { MY_PANEL_VIEW_ID } from "./panelViews"; -import { countUniqueToolsInPanel } from "./utilities"; +import { countUniqueToolsInList, countUniqueToolsInPanel } from "./utilities"; import LoadingSpan from "../LoadingSpan.vue"; import ActivityPanel from "./ActivityPanel.vue"; @@ -43,7 +43,7 @@ const headerToolSections = computed(() => { return currentToolSections.value; }); const toolsCount = computed(() => - countUniqueToolsInPanel(headerToolSections.value, Object.keys(toolsById.value).length), + countUniqueToolsInPanel(headerToolSections.value, countUniqueToolsInList(toolsById.value)), ); function formatToolsCount(count: number) { From 13341c2d55deb5b8ecbba74c1abb8756c5890fb2 Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:23:55 +0200 Subject: [PATCH 4/9] fix: deduplicate toolbox count fallback --- client/src/components/Panels/ToolBox.vue | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/src/components/Panels/ToolBox.vue b/client/src/components/Panels/ToolBox.vue index 556b50e9ac5..6a77c03f6c4 100644 --- a/client/src/components/Panels/ToolBox.vue +++ b/client/src/components/Panels/ToolBox.vue @@ -17,6 +17,7 @@ import { buildToolEntries, buildToolLabel, buildToolSection, + countUniqueToolsInList, countUniqueToolsInPanel, FAVORITES_KEYS, filterPanelByToolIds, @@ -180,6 +181,8 @@ const defaultSectionsById = computed | null>(() => return Object.keys(validSections).length > 0 ? validSections : null; }); +const myToolsDefaultSectionsById = computed(() => defaultSectionsById.value || localSectionsById.value); + // Use composable for favorites and recent tools — we only need the bits that // drive the search-results split (favorites get their own section in mixed // results). The full My-Tools landing state lives inside `MyToolsLanding.vue`. @@ -189,7 +192,10 @@ const { favoritesCollapsed, favoriteToolIdSet, recentToolIdsToShowSet } = useToo const { favoriteResults, nonFavoriteResults, hasMixedResults } = useFavoriteSearchResults(results, favoriteToolIdSet); const toolsCount = computed(() => - countUniqueToolsInPanel(defaultSectionsById.value || localSectionsById.value, toolsList.value.length), + countUniqueToolsInPanel( + defaultSectionsById.value || localSectionsById.value, + countUniqueToolsInList(toolsList.value), + ), ); const resultsSet = computed(() => new Set(results.value)); @@ -436,7 +442,7 @@ function onLabelToggle(labelId: string) { Date: Mon, 6 Jul 2026 13:23:59 +0200 Subject: [PATCH 5/9] test: cover My Tools default count --- .../src/components/Panels/ToolPanel.test.ts | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/client/src/components/Panels/ToolPanel.test.ts b/client/src/components/Panels/ToolPanel.test.ts index 29f153244e7..946a28ecf24 100644 --- a/client/src/components/Panels/ToolPanel.test.ts +++ b/client/src/components/Panels/ToolPanel.test.ts @@ -13,7 +13,7 @@ import { useUserLocalStorage } from "@/composables/userLocalStorage"; import { type ToolPanelItem, useToolStore } from "@/stores/toolStore"; import viewsListJson from "./testData/viewsList.json"; -import { getUniqueToolIdsInPanel } from "./utilities"; +import { countUniqueToolsInList, getUniqueToolIdsInPanel } from "./utilities"; import ToolPanel from "./ToolPanel.vue"; @@ -38,7 +38,7 @@ const DEFAULT_VIEW_ID = "default"; const PANEL_VIEW_ERR_MSG = "Error loading panel view"; const firstTool = toolsList[0]; -const toolsListWithExtraVersion = [...toolsList, { ...firstTool, id: `${firstTool?.id}/older-version` }]; +const toolsListWithExtraVersion = [...toolsList, { ...firstTool, id: `${firstTool?.id}/0.9`, version: "0.9" }]; vi.mock("@/composables/config"); @@ -91,6 +91,7 @@ describe("ToolPanel", () => { failDefault: boolean = false, captureToolsRequest?: (url: URL) => void, panelResponses: Record = {}, + defaultPanelView: string = DEFAULT_VIEW_ID, ) { server.use( http.untyped.get("/api/tools", ({ request }) => { @@ -102,7 +103,7 @@ describe("ToolPanel", () => { return HttpResponse.json([]); }), http.untyped.get(TEST_PANELS_URI, () => { - return HttpResponse.json({ default_panel_view: DEFAULT_VIEW_ID, views: viewsList }); + return HttpResponse.json({ default_panel_view: defaultPanelView, views: viewsList }); }), http.get("/api/users/{user_id}", ({ response }) => { return response(200).json(getFakeRegisteredUser()); @@ -274,6 +275,31 @@ describe("ToolPanel", () => { expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`); }); + it("does not show the raw installed-version count when My Tools is the backend default view", async () => { + storePanelView(""); + const wrapper = await createWrapper( + "", + false, + undefined, + { + my_panel: { + favorites: { + model_class: "ToolSection", + id: "favorites", + name: "Favorites", + tools: [], + }, + }, + }, + "my_panel", + ); + const count = countUniqueToolsInList(toolsListWithExtraVersion); + const formatted = count < 1000 ? `${count}` : `${Math.floor(count / 1000)}k+`; + const discoverButton = wrapper.find('[data-description="toolbox discover tools"]'); + expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`); + expect(discoverButton.text()).not.toBe(`Discover ${toolsListWithExtraVersion.length} Tools`); + }); + it("does not request tool tags during default tool panel startup", async () => { let toolsRequestUrl: URL | undefined; let toolTagsRequested = false; From 402b5b8dd88c34e02667de3e85506626c5fc9c0e Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:54:17 +0200 Subject: [PATCH 6/9] fix: preload default sections for My Tools --- client/src/stores/toolStore.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/client/src/stores/toolStore.ts b/client/src/stores/toolStore.ts index ea3baf9427d..7aaf2fecfaa 100644 --- a/client/src/stores/toolStore.ts +++ b/client/src/stores/toolStore.ts @@ -109,6 +109,8 @@ const MY_PANEL_VIEW: Panel = { searchable: true, }; +const DEFAULT_PANEL_VIEW_ID = "default"; + export const useToolStore = defineStore("toolStore", () => { const currentPanelView: Ref = useUserLocalStorage("tool-store-view", ""); const defaultPanelView: Ref = ref(""); @@ -369,8 +371,16 @@ export const useToolStore = defineStore("toolStore", () => { async function setPanel(panelView: string) { try { - if (panelView === MY_PANEL_VIEW_ID && defaultPanelView.value && panelView !== defaultPanelView.value) { - await fetchToolSections(defaultPanelView.value); + if (panelView === MY_PANEL_VIEW_ID) { + const sectionedPanelView = + defaultPanelView.value && defaultPanelView.value !== MY_PANEL_VIEW_ID + ? defaultPanelView.value + : panels.value[DEFAULT_PANEL_VIEW_ID] + ? DEFAULT_PANEL_VIEW_ID + : null; + if (sectionedPanelView) { + await fetchToolSections(sectionedPanelView); + } } await fetchToolSections(panelView); currentPanelView.value = panelView; From f9d4f70f03b9a143f65a1374e017c6935004e082 Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Mon, 6 Jul 2026 13:54:29 +0200 Subject: [PATCH 7/9] test: cover My Tools default section preload --- .../src/components/Panels/ToolPanel.test.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/client/src/components/Panels/ToolPanel.test.ts b/client/src/components/Panels/ToolPanel.test.ts index 946a28ecf24..c1b35aa1a64 100644 --- a/client/src/components/Panels/ToolPanel.test.ts +++ b/client/src/components/Panels/ToolPanel.test.ts @@ -92,6 +92,7 @@ describe("ToolPanel", () => { captureToolsRequest?: (url: URL) => void, panelResponses: Record = {}, defaultPanelView: string = DEFAULT_VIEW_ID, + capturePanelRequest?: (panelView: string) => void, ) { server.use( http.untyped.get("/api/tools", ({ request }) => { @@ -134,6 +135,7 @@ describe("ToolPanel", () => { server.use( http.untyped.get(/\/api\/tool_panels\/.*/, ({ request }) => { const panelView = new URL(request.url).pathname.split("/").pop() || ""; + capturePanelRequest?.(panelView); return HttpResponse.json(panelResponses[panelView] ?? toolsListInPanel); }), ); @@ -300,6 +302,31 @@ describe("ToolPanel", () => { expect(discoverButton.text()).not.toBe(`Discover ${toolsListWithExtraVersion.length} Tools`); }); + it("loads default panel sections when My Tools is the backend default view", async () => { + storePanelView(""); + const requestedPanels: string[] = []; + await createWrapper( + "", + false, + undefined, + { + my_panel: { + favorites: { + model_class: "ToolSection", + id: "favorites", + name: "Favorites", + tools: [], + }, + }, + }, + "my_panel", + (panelView) => requestedPanels.push(panelView), + ); + + expect(requestedPanels).toContain(DEFAULT_VIEW_ID); + expect(requestedPanels).toContain("my_panel"); + }); + it("does not request tool tags during default tool panel startup", async () => { let toolsRequestUrl: URL | undefined; let toolTagsRequested = false; From 7edfc58f43a55cfda5e8d266a40c1dea80774d20 Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:52:23 +0200 Subject: [PATCH 8/9] fix: align My Tools header section source --- client/src/components/Panels/ToolPanel.vue | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue index 57de05c1884..67b39b3e37c 100644 --- a/client/src/components/Panels/ToolPanel.vue +++ b/client/src/components/Panels/ToolPanel.vue @@ -36,9 +36,17 @@ const isMyPanel = computed(() => currentPanelView.value === MY_PANEL_VIEW_ID); const errorMessage = ref(""); const panelsFetched = ref(false); const showFavorites = ref(false); +const defaultToolSections = computed(() => { + return ( + toolSections.value.default || + (defaultPanelView.value && defaultPanelView.value !== MY_PANEL_VIEW_ID + ? toolSections.value[defaultPanelView.value] + : null) + ); +}); const headerToolSections = computed(() => { if (isMyPanel.value) { - return toolSections.value[defaultPanelView.value] || toolSections.value.default || currentToolSections.value; + return defaultToolSections.value || currentToolSections.value; } return currentToolSections.value; }); From bd81afbb745922b68f2b3ee0c16e0c0768bd908b Mon Sep 17 00:00:00 2001 From: Alireza Heidari <8046843+itisAliRH@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:52:41 +0200 Subject: [PATCH 9/9] test: cover My Tools header section source --- .../src/components/Panels/ToolPanel.test.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/client/src/components/Panels/ToolPanel.test.ts b/client/src/components/Panels/ToolPanel.test.ts index c1b35aa1a64..b37c01ee2a6 100644 --- a/client/src/components/Panels/ToolPanel.test.ts +++ b/client/src/components/Panels/ToolPanel.test.ts @@ -13,7 +13,7 @@ import { useUserLocalStorage } from "@/composables/userLocalStorage"; import { type ToolPanelItem, useToolStore } from "@/stores/toolStore"; import viewsListJson from "./testData/viewsList.json"; -import { countUniqueToolsInList, getUniqueToolIdsInPanel } from "./utilities"; +import { getUniqueToolIdsInPanel } from "./utilities"; import ToolPanel from "./ToolPanel.vue"; @@ -277,7 +277,7 @@ describe("ToolPanel", () => { expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`); }); - it("does not show the raw installed-version count when My Tools is the backend default view", async () => { + it("uses the loaded default panel count when My Tools is the backend default view", async () => { storePanelView(""); const wrapper = await createWrapper( "", @@ -295,13 +295,42 @@ describe("ToolPanel", () => { }, "my_panel", ); - const count = countUniqueToolsInList(toolsListWithExtraVersion); + const count = getUniqueToolIdsInPanel(toolsListInPanel).size; const formatted = count < 1000 ? `${count}` : `${Math.floor(count / 1000)}k+`; const discoverButton = wrapper.find('[data-description="toolbox discover tools"]'); expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`); expect(discoverButton.text()).not.toBe(`Discover ${toolsListWithExtraVersion.length} Tools`); }); + it("ignores My Tools panel contents for the header count when My Tools is the backend default view", async () => { + storePanelView(""); + const firstToolId = firstTool?.id; + if (!firstToolId) { + throw new Error("Expected at least one tool fixture"); + } + const wrapper = await createWrapper( + "", + false, + undefined, + { + my_panel: { + favorites: { + model_class: "ToolSection", + id: "favorites", + name: "Favorites", + tools: [firstToolId], + }, + }, + }, + "my_panel", + ); + const count = getUniqueToolIdsInPanel(toolsListInPanel).size; + const formatted = count < 1000 ? `${count}` : `${Math.floor(count / 1000)}k+`; + const discoverButton = wrapper.find('[data-description="toolbox discover tools"]'); + expect(discoverButton.text()).toBe(`Discover ${formatted} Tools`); + expect(discoverButton.text()).not.toBe("Discover 1 Tools"); + }); + it("loads default panel sections when My Tools is the backend default view", async () => { storePanelView(""); const requestedPanels: string[] = [];