From 5b3f88948d87016d0e93cdc471539fb355c52f4b Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Wed, 22 Apr 2026 14:53:52 +0530 Subject: [PATCH 1/5] fix: dedupe getExplorer API call via Zustand store Both explorer-component.jsx and chat-ai/Body.jsx were independently calling explorerSvc.getExplorer(projectId), causing a duplicate GET /explorer request every time the chat drawer opened. Introduce a minimal Zustand store (explorer-store.js) that holds the shared response. explorer-component.jsx becomes the single writer (updates on every successful fetch, clears on project switch), and Body.jsx reads explorerData from the store to populate prompt autocomplete instead of issuing its own fetch. getDbExplorer in Body.jsx is unchanged. Co-Authored-By: Claude Opus 4.7 --- frontend/src/ide/chat-ai/Body.jsx | 28 +++++++++---------- .../src/ide/explorer/explorer-component.jsx | 14 +++++++++- frontend/src/store/explorer-store.js | 16 +++++++++++ 3 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 frontend/src/store/explorer-store.js diff --git a/frontend/src/ide/chat-ai/Body.jsx b/frontend/src/ide/chat-ai/Body.jsx index 9b08de47..9196385e 100644 --- a/frontend/src/ide/chat-ai/Body.jsx +++ b/frontend/src/ide/chat-ai/Body.jsx @@ -6,6 +6,7 @@ import { ExistingChat } from "./ExistingChat"; import { NewChat } from "./NewChat"; import { useChatAIService } from "./services"; import { useProjectStore } from "../../store/project-store"; +import { useExplorerStore } from "../../store/explorer-store"; import { useNotificationService } from "../../service/notification-service"; import { explorerService } from "../explorer/explorer-service"; @@ -73,6 +74,7 @@ const Body = function Body({ }); const explorerSvc = useRef(explorerService()).current; const { projectId } = useProjectStore(); + const explorerData = useExplorerStore((state) => state.explorerData); const { postChatPrompt, getChatIntents, getChatLlmModels } = useChatAIService(); @@ -217,23 +219,19 @@ const Body = function Body({ .catch(() => { console.error("Failed to fetch database schemas"); }); - - // fetch models & seeds -> update as soon as ready - explorerSvc - .getExplorer(projectId) - .then((res) => { - const children = res?.data?.children || []; - setPromptAutoComplete((prev) => ({ - ...prev, - modelsData: children[0] || {}, - seedsData: children[1] || {}, - })); - }) - .catch(() => { - console.error("Failed to fetch models and seeds"); - }); }, [projectId, isChatDrawerOpen, explorerSvc]); + // Mirror shared explorer data (fetched by explorer-component) into the + // prompt autocomplete shape consumed by NewChat / InputPrompt. + useEffect(() => { + const children = explorerData || []; + setPromptAutoComplete((prev) => ({ + ...prev, + modelsData: children[0] || {}, + seedsData: children[1] || {}, + })); + }, [explorerData]); + const triggerGetChatMessagesApi = useCallback(() => { setIsGetChatMessages(true); }, []); diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 2a0471ae..dc4b8382 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -63,6 +63,7 @@ import "../ide-layout.css"; import { useNotificationService } from "../../service/notification-service.js"; import { SpinnerLoader } from "../../widgets/spinner_loader/index.js"; import { useRefreshModelsStore } from "../../store/refresh-models-store.js"; +import { useExplorerStore } from "../../store/explorer-store.js"; import { LinearScale } from "../../base/icons"; // Static sort options for model explorer @@ -176,6 +177,10 @@ const IdeExplorer = ({ const currentSchema = useProjectStore((state) => state.currentSchema); const setCurrentSchema = useProjectStore((state) => state.setCurrentSchema); const setSchemaList = useProjectStore((state) => state.setSchemaList); + const setExplorerData = useExplorerStore((state) => state.setExplorerData); + const clearExplorerData = useExplorerStore( + (state) => state.clearExplorerData + ); // Reset currentSchema on unmount to prevent stale data useEffect(() => { @@ -1245,11 +1250,17 @@ const IdeExplorer = ({ ); useEffect(() => { - if (schemaMenu) { + if (schemaMenu?.length) { getExplorer(projectId); } }, [schemaMenu, currentSchema]); + // Clear shared explorer data on project switch so other consumers + // (e.g. chat autocomplete) don't momentarily read the previous project's tree. + useEffect(() => { + clearExplorerData(); + }, [projectId]); + function getExplorer(projectId) { if (!projectId) return; setLoading(true); @@ -1275,6 +1286,7 @@ const IdeExplorer = ({ }); transformTree(treeData); setTreeData(treeData); + setExplorerData(treeData); setCachedLists((prev) => ({ ...prev, diff --git a/frontend/src/store/explorer-store.js b/frontend/src/store/explorer-store.js new file mode 100644 index 00000000..10400d82 --- /dev/null +++ b/frontend/src/store/explorer-store.js @@ -0,0 +1,16 @@ +import { create } from "zustand"; + +/** + * Explorer Store + * Holds the shared response of explorerSvc.getExplorer(projectId) + * so multiple consumers (explorer tree, chat autocomplete) don't refetch. + * Owner of writes: frontend/src/ide/explorer/explorer-component.jsx + */ +const useExplorerStore = create((set) => ({ + // res.data.children from /explorer API — array where [0]=models, [1]=seeds + explorerData: null, + setExplorerData: (data) => set({ explorerData: data }), + clearExplorerData: () => set({ explorerData: null }), +})); + +export { useExplorerStore }; From 49076af0b05e0a0e9544857975a69ac4ea07eb24 Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Wed, 22 Apr 2026 14:54:27 +0530 Subject: [PATCH 2/5] fix: dedupe getDbExplorer API call via the same Zustand store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend useExplorerStore with a dbExplorerData slice so chat-ai/Body.jsx stops fetching /db_explorer independently. Previously, opening the chat drawer triggered a second identical GET /db_explorer alongside the one already issued by explorer-component.jsx on mount. explorer-component.jsx becomes the single writer for both slices (getExplorer and getDbExplorer wrappers), and the existing project-switch clear effect now resets both slices via an expanded clearExplorerData. Body.jsx subscribes to the store and mirrors dbExplorerData into the existing promptAutoComplete.dbData shape — no direct explorerService usage remains in chat-ai/Body.jsx (explorerService import, useRef import, and the explorerSvc ref were dropped). Also updated the PR description to cover both refactors in one PR. Co-Authored-By: Claude Opus 4.7 --- frontend/src/ide/chat-ai/Body.jsx | 29 ++++++------------- .../src/ide/explorer/explorer-component.jsx | 4 +++ frontend/src/store/explorer-store.js | 10 +++++-- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/frontend/src/ide/chat-ai/Body.jsx b/frontend/src/ide/chat-ai/Body.jsx index 9196385e..c80ce852 100644 --- a/frontend/src/ide/chat-ai/Body.jsx +++ b/frontend/src/ide/chat-ai/Body.jsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useRef } from "react"; +import { useState, useCallback, useEffect } from "react"; import PropTypes from "prop-types"; import { useSocketService } from "../../service/socket-service"; @@ -8,7 +8,6 @@ import { useChatAIService } from "./services"; import { useProjectStore } from "../../store/project-store"; import { useExplorerStore } from "../../store/explorer-store"; import { useNotificationService } from "../../service/notification-service"; -import { explorerService } from "../explorer/explorer-service"; // Cloud-only: fetch token balance (unavailable in OSS — import fails gracefully) let getTokenBalance = null; @@ -72,9 +71,9 @@ const Body = function Body({ seedsData: {}, dbData: {}, }); - const explorerSvc = useRef(explorerService()).current; const { projectId } = useProjectStore(); const explorerData = useExplorerStore((state) => state.explorerData); + const dbExplorerData = useExplorerStore((state) => state.dbExplorerData); const { postChatPrompt, getChatIntents, getChatLlmModels } = useChatAIService(); @@ -204,23 +203,6 @@ const Body = function Body({ }); }, [selectedChatId, chatMessages.length, realTokenBalance, notify]); - useEffect(() => { - if (!projectId || !isChatDrawerOpen) return; - - // fetch database schemas -> update immediately when ready - explorerSvc - .getDbExplorer(projectId) - .then((res) => { - setPromptAutoComplete((prev) => ({ - ...prev, - dbData: res?.data || {}, - })); - }) - .catch(() => { - console.error("Failed to fetch database schemas"); - }); - }, [projectId, isChatDrawerOpen, explorerSvc]); - // Mirror shared explorer data (fetched by explorer-component) into the // prompt autocomplete shape consumed by NewChat / InputPrompt. useEffect(() => { @@ -232,6 +214,13 @@ const Body = function Body({ })); }, [explorerData]); + useEffect(() => { + setPromptAutoComplete((prev) => ({ + ...prev, + dbData: dbExplorerData || {}, + })); + }, [dbExplorerData]); + const triggerGetChatMessagesApi = useCallback(() => { setIsGetChatMessages(true); }, []); diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index dc4b8382..934b2459 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -178,6 +178,9 @@ const IdeExplorer = ({ const setCurrentSchema = useProjectStore((state) => state.setCurrentSchema); const setSchemaList = useProjectStore((state) => state.setSchemaList); const setExplorerData = useExplorerStore((state) => state.setExplorerData); + const setDbExplorerData = useExplorerStore( + (state) => state.setDbExplorerData + ); const clearExplorerData = useExplorerStore( (state) => state.clearExplorerData ); @@ -1311,6 +1314,7 @@ const IdeExplorer = ({ const treeData = res.data; const mappedData = mapIconsToTreeData([treeData]); setDBExplorer(mappedData); + setDbExplorerData(treeData); setCachedLists((prev) => ({ ...prev, 2: generateList([treeData]), // Correct key diff --git a/frontend/src/store/explorer-store.js b/frontend/src/store/explorer-store.js index 10400d82..41f945a8 100644 --- a/frontend/src/store/explorer-store.js +++ b/frontend/src/store/explorer-store.js @@ -2,15 +2,19 @@ import { create } from "zustand"; /** * Explorer Store - * Holds the shared response of explorerSvc.getExplorer(projectId) - * so multiple consumers (explorer tree, chat autocomplete) don't refetch. + * Holds the shared responses of explorerSvc.getExplorer(projectId) and + * explorerSvc.getDbExplorer(projectId) so multiple consumers (explorer + * tree, chat autocomplete) don't refetch. * Owner of writes: frontend/src/ide/explorer/explorer-component.jsx */ const useExplorerStore = create((set) => ({ // res.data.children from /explorer API — array where [0]=models, [1]=seeds explorerData: null, + // res.data from /db_explorer API — single DB tree object + dbExplorerData: null, setExplorerData: (data) => set({ explorerData: data }), - clearExplorerData: () => set({ explorerData: null }), + setDbExplorerData: (data) => set({ dbExplorerData: data }), + clearExplorerData: () => set({ explorerData: null, dbExplorerData: null }), })); export { useExplorerStore }; From b0af8f7889a6693962c9a4e57bfe3ae6a90eb249 Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Wed, 22 Apr 2026 15:12:05 +0530 Subject: [PATCH 3/5] fix: address review feedback on explorer dedup refactor Three review-driven tweaks from the first round of review on this branch: - explorer-component.jsx: publish the raw /explorer response to useExplorerStore (via rawTreeDataRef.current) before the in-place mutations by sortModels, applyModelDecorations, and transformTree begin. The store now matches its documented contract and Body.jsx's autocomplete receives the untransformed children shape it did before the dedup refactor. - explorer-component.jsx: add clearExplorerData to the dependency array of the project-switch clear effect. Zustand action refs are stable in practice, so effect cadence is unchanged, but the deps array is now exhaustive and future-proof against a non-stable refactor. - Body.jsx: expand the short comment above the mirror effects into a block that documents why this component is read-only and lists the escape hatches (fetch fallback here or a loading state in InputPrompt) to reach for if the "explorer mounts before chat drawer" invariant ever breaks. Co-Authored-By: Claude Opus 4.7 --- frontend/src/ide/chat-ai/Body.jsx | 9 +++++++-- frontend/src/ide/explorer/explorer-component.jsx | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/ide/chat-ai/Body.jsx b/frontend/src/ide/chat-ai/Body.jsx index c80ce852..9ce04bdf 100644 --- a/frontend/src/ide/chat-ai/Body.jsx +++ b/frontend/src/ide/chat-ai/Body.jsx @@ -203,8 +203,13 @@ const Body = function Body({ }); }, [selectedChatId, chatMessages.length, realTokenBalance, notify]); - // Mirror shared explorer data (fetched by explorer-component) into the - // prompt autocomplete shape consumed by NewChat / InputPrompt. + // Autocomplete data is mirrored passively from useExplorerStore, which is + // populated exclusively by explorer-component.jsx. This component deliberately + // does NOT fetch on its own — the IDE layout mounts the explorer before the + // chat drawer opens, so the store is populated by the time autocomplete is + // triggered. If that invariant ever breaks (lazy-loaded explorer, standalone + // chat route, explorer fetch failure), add a fetch fallback here or a + // loading/unavailable state in the InputPrompt autocomplete UI. useEffect(() => { const children = explorerData || []; setPromptAutoComplete((prev) => ({ diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 934b2459..90bc0fb0 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -1262,7 +1262,7 @@ const IdeExplorer = ({ // (e.g. chat autocomplete) don't momentarily read the previous project's tree. useEffect(() => { clearExplorerData(); - }, [projectId]); + }, [projectId, clearExplorerData]); function getExplorer(projectId) { if (!projectId) return; @@ -1272,6 +1272,9 @@ const IdeExplorer = ({ .then((res) => { const treeData = res.data.children; rawTreeDataRef.current = JSON.parse(JSON.stringify(treeData)); + // Publish the raw (pre-mutation) shape to the shared store so + // consumers like chat-ai/Body.jsx get the untransformed children. + setExplorerData(rawTreeDataRef.current); // Apply sort and decorations to no_code models BEFORE transformTree // so that _isChild flag is set when className is assigned treeData.forEach((node) => { @@ -1289,7 +1292,6 @@ const IdeExplorer = ({ }); transformTree(treeData); setTreeData(treeData); - setExplorerData(treeData); setCachedLists((prev) => ({ ...prev, From b54eedf8570edccedbc6465a7e3413bd017b091c Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Wed, 22 Apr 2026 15:24:22 +0530 Subject: [PATCH 4/5] fix: use null-sentinel for schemaMenu to gate explorer fetch correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior `schemaMenu?.length` guard on the schema/schema-change useEffect conflated "schemas not loaded yet" (schemaMenu === []) with "loaded but empty" (same []). A reviewer correctly flagged that a project with no DB connection would have the fetch silently suppressed. Fix: - Initialise schemaMenu as null (sentinel for "not yet loaded") instead of []. - Revert the effect guard to `if (schemaMenu)` so it fires the moment schemaMenu transitions from null to an array, regardless of whether that array has items. - Add null-safe guards — `(schemaMenu || [])` — at the JSX consumers (`.map`, `.length` comparisons) that now need to handle the pre-load null state. During loading, seed-related actions remain disabled and the schema dropdown is empty, both the correct UX. - Add an inline comment above the effect explaining the sentinel so this design choice is visible at the call site. Net result: the original optimisation (skip the redundant mount-time fetch while schemaMenu is still []) is preserved, and the reviewer's no-DB regression is fixed. Co-Authored-By: Claude Opus 4.7 --- .../src/ide/explorer/explorer-component.jsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 90bc0fb0..96247caf 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -202,7 +202,7 @@ const IdeExplorer = ({ const [openNameModal, setOpenNameModal] = useState(false); const [newSchemaName, setNewSchemaName] = useState(""); const [isSchemaModalOpen, setIsSchemaModalOpen] = useState(false); - const [schemaMenu, setSchemaMenu] = useState([]); + const [schemaMenu, setSchemaMenu] = useState(null); const [dbExplorer, setDBExplorer] = useState([]); const [activeMenu, setActiveMenu] = useState(""); const [dbLoading, setDbLoading] = useState(false); @@ -984,7 +984,7 @@ const IdeExplorer = ({ <> ({ + items: (schemaMenu || []).map((el) => ({ ...el, label: el.key === "add-new-schema" ? ( @@ -1022,7 +1022,7 @@ const IdeExplorer = ({ ? "Run Seed Disabled - Please select a schema" : previewTimeTravel ? "Run Seed Disabled - Time travel mode active" - : schemaMenu.length <= 1 + : (schemaMenu || []).length <= 1 ? "Run Seed Disabled - No schemas available" : "Run Seed" } @@ -1043,7 +1043,7 @@ const IdeExplorer = ({ disabled={ previewTimeTravel || !currentSchema || - schemaMenu.length <= 1 + (schemaMenu || []).length <= 1 } > @@ -1126,7 +1126,7 @@ const IdeExplorer = ({ if ( !previewTimeTravel && currentSchema && - schemaMenu.length > 1 + (schemaMenu || []).length > 1 ) { handleSeedIconClick(event, child.title); } @@ -1135,7 +1135,7 @@ const IdeExplorer = ({ previewTimeTravel || seedRunningRef.current || !currentSchema || - schemaMenu.length <= 1 + (schemaMenu || []).length <= 1 ? "seed-icon-disabled" : "" }`} @@ -1252,8 +1252,12 @@ const IdeExplorer = ({ [modelSortBy, handleModelSort] ); + // schemaMenu starts as null; becomes an array (possibly empty) after + // getSchemas resolves. Gating on truthiness skips the redundant mount-time + // fetch while still firing for projects whose schema list is legitimately + // empty. useEffect(() => { - if (schemaMenu?.length) { + if (schemaMenu) { getExplorer(projectId); } }, [schemaMenu, currentSchema]); From 6ff56658dc11c4bf20ddd20408b3844c31875ccf Mon Sep 17 00:00:00 2001 From: Tahier Hussain Date: Fri, 24 Apr 2026 17:20:44 +0530 Subject: [PATCH 5/5] fix: only clear explorer store on actual project change The project-switch clear effect was also firing on initial mount (and on any remount of explorer-component within the same session), which briefly wiped valid store data and could leave chat autocomplete empty until the next fetch resolved. Gate the clear with a prevProjectIdRef so clearExplorerData() only fires when projectId actually changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/ide/explorer/explorer-component.jsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/src/ide/explorer/explorer-component.jsx b/frontend/src/ide/explorer/explorer-component.jsx index 96247caf..21414009 100644 --- a/frontend/src/ide/explorer/explorer-component.jsx +++ b/frontend/src/ide/explorer/explorer-component.jsx @@ -1264,8 +1264,14 @@ const IdeExplorer = ({ // Clear shared explorer data on project switch so other consumers // (e.g. chat autocomplete) don't momentarily read the previous project's tree. + // Ref-gated so the clear does NOT fire on initial mount / remount within the + // same project — only when projectId actually changes. + const prevProjectIdRef = useRef(projectId); useEffect(() => { - clearExplorerData(); + if (prevProjectIdRef.current !== projectId) { + clearExplorerData(); + prevProjectIdRef.current = projectId; + } }, [projectId, clearExplorerData]); function getExplorer(projectId) {