diff --git a/src/App.vue b/src/App.vue
index 882b427bb..f4ec56c8b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -5,6 +5,13 @@ import { DatabaseZap, FilePlus2, Play, Loader2, X, Globe, Moon, Sun, Upload, Dow
import { Splitpanes, Pane } from "splitpanes";
import "splitpanes/dist/splitpanes.css";
import { Button } from "@/components/ui/button";
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuTrigger,
+} from "@/components/ui/context-menu";
import {
Select,
SelectContent,
@@ -701,34 +708,62 @@ async function setupFileDrop() {
-
-
-
{{ tabDisplayTitle(tab) }}
-
-
+
+
+
+
{{ tabDisplayTitle(tab) }}
+
+
+
+
+ {{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}
+
-
-
{{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}
-
-
-
+
+
+
+
+
+
+ {{ tab.pinned ? t('contextMenu.unpin') : t('contextMenu.pin') }}
+
+
+
+
+ {{ t('contextMenu.closeTab') }}
+
+
+
+ {{ t('contextMenu.closeOtherTabs') }}
+
+
+
+ {{ t('contextMenu.closeAllTabs') }}
+
+
+
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 382882aec..f9cc1a82a 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -170,6 +170,9 @@ export default {
refreshChildren: "Refresh",
pin: "Pin",
unpin: "Unpin",
+ closeTab: "Close Tab",
+ closeOtherTabs: "Close Other Tabs",
+ closeAllTabs: "Close All Tabs",
copyName: "Copy Name",
},
tree: {
diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts
index 517af5099..a6b13dff2 100644
--- a/src/i18n/locales/zh-CN.ts
+++ b/src/i18n/locales/zh-CN.ts
@@ -168,6 +168,9 @@ export default {
refreshChildren: "刷新",
pin: "置顶",
unpin: "取消置顶",
+ closeTab: "关闭标签页",
+ closeOtherTabs: "关闭其他标签页",
+ closeAllTabs: "关闭全部标签页",
copyName: "复制名称",
},
tree: {
diff --git a/src/lib/tabCloseActions.ts b/src/lib/tabCloseActions.ts
new file mode 100644
index 000000000..71ad1ea14
--- /dev/null
+++ b/src/lib/tabCloseActions.ts
@@ -0,0 +1,26 @@
+export interface TabLike {
+ id: string;
+}
+
+export interface TabCloseState
{
+ tabs: T[];
+ activeTabId: string | null;
+}
+
+export function closeOtherTabsState(
+ tabs: readonly T[],
+ activeTabId: string | null,
+ targetTabId: string,
+): TabCloseState {
+ const target = tabs.find((tab) => tab.id === targetTabId);
+ if (!target) return { tabs: [...tabs], activeTabId };
+
+ return { tabs: [target], activeTabId: target.id };
+}
+
+export function closeAllTabsState(
+ _tabs: readonly T[],
+ _activeTabId: string | null,
+): TabCloseState {
+ return { tabs: [], activeTabId: null };
+}
diff --git a/src/stores/queryStore.ts b/src/stores/queryStore.ts
index 91e35d0eb..5109506fd 100644
--- a/src/stores/queryStore.ts
+++ b/src/stores/queryStore.ts
@@ -2,6 +2,7 @@ import { defineStore } from "pinia";
import { ref } from "vue";
import type { QueryTab } from "@/types/database";
import { orderPinnedFirst } from "@/lib/pinnedItems";
+import { closeAllTabsState, closeOtherTabsState } from "@/lib/tabCloseActions";
import * as api from "@/lib/tauri";
export const useQueryStore = defineStore("query", () => {
@@ -46,6 +47,18 @@ export const useQueryStore = defineStore("query", () => {
}
}
+ function closeOtherTabs(id: string) {
+ const next = closeOtherTabsState(tabs.value, activeTabId.value, id);
+ tabs.value = next.tabs;
+ activeTabId.value = next.activeTabId;
+ }
+
+ function closeAllTabs() {
+ const next = closeAllTabsState(tabs.value, activeTabId.value);
+ tabs.value = next.tabs;
+ activeTabId.value = next.activeTabId;
+ }
+
function updateSql(id: string, sql: string) {
const tab = tabs.value.find((t) => t.id === id);
if (tab) tab.sql = sql;
@@ -123,6 +136,8 @@ export const useQueryStore = defineStore("query", () => {
activeTabId,
createTab,
closeTab,
+ closeOtherTabs,
+ closeAllTabs,
updateSql,
togglePinnedTab,
updateDatabase,
diff --git a/tests/tabCloseActions.test.ts b/tests/tabCloseActions.test.ts
new file mode 100644
index 000000000..4d2075bce
--- /dev/null
+++ b/tests/tabCloseActions.test.ts
@@ -0,0 +1,29 @@
+import { strict as assert } from "node:assert";
+import test from "node:test";
+import { closeAllTabsState, closeOtherTabsState } from "../src/lib/tabCloseActions.js";
+
+test("close other tabs keeps the target tab and makes it active", () => {
+ const tabs = [{ id: "a" }, { id: "b" }, { id: "c" }];
+
+ const result = closeOtherTabsState(tabs, "a", "b");
+
+ assert.deepEqual(result.tabs.map((tab) => tab.id), ["b"]);
+ assert.equal(result.activeTabId, "b");
+ assert.deepEqual(tabs.map((tab) => tab.id), ["a", "b", "c"]);
+});
+
+test("close other tabs is a no-op when the target tab is missing", () => {
+ const tabs = [{ id: "a" }, { id: "b" }];
+
+ const result = closeOtherTabsState(tabs, "a", "missing");
+
+ assert.deepEqual(result.tabs.map((tab) => tab.id), ["a", "b"]);
+ assert.equal(result.activeTabId, "a");
+});
+
+test("close all tabs clears every tab and active tab", () => {
+ const result = closeAllTabsState([{ id: "a" }, { id: "b" }], "a");
+
+ assert.deepEqual(result.tabs, []);
+ assert.equal(result.activeTabId, null);
+});