diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue
index e37c74ff6c..2c8ab72930 100644
--- a/apps/desktop/src/App.vue
+++ b/apps/desktop/src/App.vue
@@ -494,6 +494,15 @@ const isAiPanelMaximized = ref(false);
const isHistoryPanelMaximized = ref(false);
const isZenMode = ref(false);
const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true");
+const sqlLibraryLocateRequest = ref<{ id: number; fileId: string } | null>(null);
+let sqlLibraryLocateRequestId = 0;
+watch(
+ showSqlLibraryPanel,
+ (open) => {
+ if (!open) sqlLibraryLocateRequest.value = null;
+ },
+ { flush: "sync" },
+);
const sqlFileLocateRequest = ref<{ id: number; path: string } | null>(null);
let sqlFileLocateRequestId = 0;
const showSqlFilePanel = ref(safeLocalStorageGet("dbx-sql-file-panel-open") === "true");
@@ -3740,6 +3749,17 @@ function finishSqlFileLocate(requestId: number) {
if (sqlFileLocateRequest.value?.id === requestId) sqlFileLocateRequest.value = null;
}
+function locateSqlLibraryFile(tab: QueryTab) {
+ if (!tab.savedSqlId || isDetachedWindowContext) return;
+ isZenMode.value = false;
+ openRightSidebarPanel("sqlLibrary");
+ sqlLibraryLocateRequest.value = { id: ++sqlLibraryLocateRequestId, fileId: tab.savedSqlId };
+}
+
+function finishSqlLibraryLocate(requestId: number) {
+ if (sqlLibraryLocateRequest.value?.id === requestId) sqlLibraryLocateRequest.value = null;
+}
+
async function locateTabInSidebar(tab: QueryTab) {
setSidebarOpen(true);
await nextTick();
@@ -5586,6 +5606,7 @@ onUnmounted(() => {
:content-suppressed="activeTab?.mode === 'plugin-workbench'"
@locate-tab="locateTabInSidebar"
@locate-sql-file="locateSqlFile"
+ @locate-sql-library-file="locateSqlLibraryFile"
@close-tab="
(tabId: string) => {
if (tabId === queryStore.activeTabId) void closeActiveSurface();
@@ -5819,7 +5840,7 @@ onUnmounted(() => {
diff --git a/apps/desktop/src/components/layout/EditorGroup.vue b/apps/desktop/src/components/layout/EditorGroup.vue
index 83f2ef7ebf..e7e0030983 100644
--- a/apps/desktop/src/components/layout/EditorGroup.vue
+++ b/apps/desktop/src/components/layout/EditorGroup.vue
@@ -192,6 +192,7 @@ const groupExecutableSql = computed(() => {
@activate-tab="$emit('activate-tab', $event)"
@locate-tab="$emit('locate-tab', $event)"
@locate-sql-file="$emit('locate-sql-file', $event)"
+ @locate-sql-library-file="$emit('locate-sql-library-file', $event)"
@toggle-zen-mode="$emit('toggle-zen-mode')"
@start-resize="$emit('start-resize', $event)"
@toggle-collapse="$emit('toggle-collapse')"
diff --git a/apps/desktop/src/components/layout/EditorGroupTabBar.vue b/apps/desktop/src/components/layout/EditorGroupTabBar.vue
index 3466839d1c..0338fb24c9 100644
--- a/apps/desktop/src/components/layout/EditorGroupTabBar.vue
+++ b/apps/desktop/src/components/layout/EditorGroupTabBar.vue
@@ -145,6 +145,7 @@ const emit = defineEmits<{
"activate-tab": [tabId: string];
"locate-tab": [tab: QueryTab];
"locate-sql-file": [tab: QueryTab];
+ "locate-sql-library-file": [tab: QueryTab];
"toggle-zen-mode": [];
"start-resize": [event: PointerEvent];
"toggle-collapse": [];
@@ -1705,6 +1706,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
visible: !!queryTabExternalFilePath(tab),
action: () => emit("locate-sql-file", tab),
},
+ {
+ label: t("sqlFileTree.locateActiveFile"),
+ icon: Crosshair,
+ visible: !!tab.savedSqlId,
+ action: () => emit("locate-sql-library-file", tab),
+ },
{ label: "", separator: true },
createPinTabMenuItem({
label: tab.pinned ? t("contextMenu.unpinTab") : t("contextMenu.pinTab"),
diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue
index ca2008d087..172a148bd6 100644
--- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue
+++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue
@@ -78,8 +78,13 @@ const queryStore = useQueryStore();
const settingsStore = useSettingsStore();
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
+const props = defineProps<{
+ locateRequest?: { id: number; fileId: string } | null;
+}>();
+
const emit = defineEmits<{
close: [];
+ "locate-handled": [id: number];
}>();
const UNFILED_DROP_TARGET_ID = "__sql-library-unfiled__";
@@ -541,8 +546,8 @@ const activeSavedSqlId = computed(() => queryStore.tabs.find((tab) => tab.id ===
const libraryTree = ref(null);
const currentSavedSqlFile = computed(() => (activeSavedSqlId.value ? savedSqlStore.getFile(activeSavedSqlId.value) : undefined));
-async function locateCurrentFile() {
- const file = currentSavedSqlFile.value;
+async function locateFile(fileId: string) {
+ const file = savedSqlStore.getFile(fileId);
if (!file) return;
if (searchInputPending.value || !treeRows.value.some((row) => row.type === "file" && row.file.id === file.id)) {
searchInput.value?.clear();
@@ -565,6 +570,25 @@ async function locateCurrentFile() {
row?.scrollIntoView({ block: "center", inline: "nearest" });
}
+async function locateCurrentFile() {
+ const file = currentSavedSqlFile.value;
+ if (!file) return;
+ await locateFile(file.id);
+}
+
+watch(
+ [() => savedSqlStore.loadState, () => props.locateRequest],
+ async ([loadState, request]) => {
+ if (!request || loadState === "loading") return;
+ try {
+ await locateFile(request.fileId);
+ } finally {
+ emit("locate-handled", request.id);
+ }
+ },
+ { immediate: true, flush: "post" },
+);
+
const hasCurrentSavedSqlExecutionTarget = computed(() => {
const activeTab = queryStore.tabs.find((tab) => tab.id === queryStore.activeTabId);
const target = savedSqlExecutionTargetFromTab(activeTab);
@@ -1611,6 +1635,12 @@ function showDropAfter(targetId: string) {
function showDropInside(targetId: string) {
return dragState.active && dragState.targetId === targetId && dragState.dropPosition === "inside";
}
+
+defineExpose({
+ locateFile,
+ locateCurrentFile,
+ searchInput,
+});
diff --git a/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts
index c9924e6b55..4ec9865c8c 100644
--- a/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts
+++ b/apps/desktop/src/components/layout/__tests__/EditorGroupTabBar.behavior.spec.ts
@@ -126,7 +126,8 @@ function mountBar(
fullTabTitle: "Full title",
compactTabTitle: "Compact title",
},
- sidebar: { locateActiveTab: "Locate" },
+ sidebar: { locateActiveTab: "Locate", locateSqlFile: "Locate in SQL file list" },
+ sqlFileTree: { locateActiveFile: "Locate current file" },
},
},
}),
@@ -831,6 +832,37 @@ describe("EditorGroupTabBar behavior", () => {
app.unmount();
host.remove();
});
+
+ it("shows locate current file menu item for saved sql tab and emits locate-sql-library-file", async () => {
+ const store = useQueryStore();
+ const queryTabId = store.createTab("pg-1", "app", "Query 1", "query");
+ const savedSqlTabId = store.createTab("pg-1", "app", "Saved Query", "query");
+ store.tabs.find((tab) => tab.id === savedSqlTabId)!.savedSqlId = "saved-sql-1";
+ const mainGroup = store.groups[0];
+
+ const onLocateSqlLibraryFile = vi.fn();
+ const { app, host } = mountBar(mainGroup.id, [queryTabId, savedSqlTabId], queryTabId, pinia, undefined, undefined, undefined, undefined, { "onLocate-sql-library-file": onLocateSqlLibraryFile });
+ await settle();
+
+ const rawMenuFor = (tabId: string) => {
+ const pill = tabPill(host, tabId);
+ const stub = pill.closest(".ctx-menu-stub") as any;
+ return (stub?.__ctxMenuItems ?? []) as Array<{ label: string; action?: () => void; visible?: boolean }>;
+ };
+
+ const regularLocateItem = rawMenuFor(queryTabId).find((item) => item.label === "Locate current file");
+ expect(regularLocateItem?.visible).toBe(false);
+
+ const savedSqlLocateItem = rawMenuFor(savedSqlTabId).find((item) => item.label === "Locate current file");
+ expect(savedSqlLocateItem?.visible).toBe(true);
+
+ savedSqlLocateItem?.action?.();
+ expect(onLocateSqlLibraryFile).toHaveBeenCalledTimes(1);
+ expect(onLocateSqlLibraryFile).toHaveBeenCalledWith(expect.objectContaining({ id: savedSqlTabId, savedSqlId: "saved-sql-1" }));
+
+ app.unmount();
+ host.remove();
+ });
});
describe("active tab reveal scrolling (issue #12010)", () => {
diff --git a/apps/desktop/src/components/layout/__tests__/SqlLibraryPanel.search.spec.ts b/apps/desktop/src/components/layout/__tests__/SqlLibraryPanel.search.spec.ts
index 21e318a454..33b83bbd60 100644
--- a/apps/desktop/src/components/layout/__tests__/SqlLibraryPanel.search.spec.ts
+++ b/apps/desktop/src/components/layout/__tests__/SqlLibraryPanel.search.spec.ts
@@ -174,4 +174,34 @@ describe("SQL library locate and search preferences", () => {
await nextTick();
expect(button.disabled).toBe(true);
});
+
+ it("handles locateRequest prop, uncollapses ancestors, scrolls into view and emits locate-handled", async () => {
+ const onLocateHandled = vi.fn();
+ const pinia = createPinia();
+ setActivePinia(pinia);
+ const store = useSavedSqlStore();
+ store.loadState = "loaded";
+ store.folders = [
+ { id: "root", name: "Root", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
+ { id: "child", parentFolderId: "root", name: "Child", connectionId: "", createdAt: timestamp, updatedAt: timestamp },
+ ];
+ store.files = [file("unrelated", "unrelated.sql", "SELECT 1"), { ...file("nested-file", "nested.sql", "SELECT 42"), folderId: "child" }];
+ vi.spyOn(store, "ensureAllFilesLoaded").mockResolvedValue(undefined);
+
+ const scroll = vi.spyOn(HTMLElement.prototype, "scrollIntoView").mockImplementation(() => {});
+ const host = document.createElement("div");
+ document.body.appendChild(host);
+ const app = createApp(SqlLibraryPanel, {
+ locateRequest: { id: 42, fileId: "nested-file" },
+ "onLocate-handled": onLocateHandled,
+ });
+ app.use(pinia);
+ app.use(i18n);
+ app.mount(host);
+ mounted.push({ app, host });
+
+ await vi.waitFor(() => expect(onLocateHandled).toHaveBeenCalledWith(42));
+ expect(visibleFiles(host)).toContain("nested-file");
+ expect(scroll.mock.instances.at(-1)).toBe(host.querySelector('[data-sql-library-file="nested-file"]'));
+ });
});
diff --git a/apps/desktop/src/components/layout/querySurfaces.ts b/apps/desktop/src/components/layout/querySurfaces.ts
index 39fd9d49bd..06ef0acb24 100644
--- a/apps/desktop/src/components/layout/querySurfaces.ts
+++ b/apps/desktop/src/components/layout/querySurfaces.ts
@@ -85,6 +85,7 @@ export interface ContentAreaSurfaceEmits {
closeTab: [tabId: string];
"locate-tab": [tab: QueryTab];
"locate-sql-file": [tab: QueryTab];
+ "locate-sql-library-file": [tab: QueryTab];
"update:activeOutputView": [tabId: string, value: TabOutputView];
fixWithAi: [tabId: string, errorMessage: string];
sendSelectionToAi: [tabId: string, sql: string];
diff --git a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts
index a062765edd..8ff6576e3b 100644
--- a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts
+++ b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts
@@ -10,6 +10,7 @@ export const contentSurfaceEventNames = [
"closeTab",
"locate-tab",
"locate-sql-file",
+ "locate-sql-library-file",
"update:activeOutputView",
"fixWithAi",
"sendSelectionToAi",