diff --git a/apps/desktop/src/components/layout/AppTabBar.vue b/apps/desktop/src/components/layout/AppTabBar.vue index 52304057f7..74266cff79 100644 --- a/apps/desktop/src/components/layout/AppTabBar.vue +++ b/apps/desktop/src/components/layout/AppTabBar.vue @@ -703,26 +703,27 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul @mousemove="handleTabDragTarget($event, tab)" @mouseleave="tabDrag.clearTarget(tab.id)" > - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + {{ tabTitleText(tab) }} @@ -911,26 +913,27 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul @mousemove="handleTabDragTarget($event, tab)" @mouseleave="tabDrag.clearTarget(tab.id)" > - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - + + + + {{ tabTitleText(tab) }} diff --git a/apps/desktop/src/components/layout/TabExecutionStatus.vue b/apps/desktop/src/components/layout/TabExecutionStatus.vue index 9fe5d3a081..93c4be3702 100644 --- a/apps/desktop/src/components/layout/TabExecutionStatus.vue +++ b/apps/desktop/src/components/layout/TabExecutionStatus.vue @@ -21,11 +21,12 @@ const status = computed(() => { v-if="status" data-tab-execution-status role="status" - class="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full bg-background/80" + class="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center" :class="status === 'cancelling' ? 'text-amber-600 dark:text-amber-400' : 'text-blue-600 dark:text-blue-400'" :aria-label="t(queryExecutionLabelKey(tab))" :title="t(queryExecutionLabelKey(tab))" > - + diff --git a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts index 54aa122b78..6d1468ff22 100644 --- a/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/AppTabBar.spec.ts @@ -121,8 +121,9 @@ describe("AppTabBar overflow search", () => { }); describe("AppTabBar query execution status", () => { - it("renders one shared status indicator in regular, pinned, and both overflow surfaces", () => { + it("replaces the icon through one shared status component in every tab surface", () => { expect(tabBarSource).toContain('import TabExecutionStatus from "@/components/layout/TabExecutionStatus.vue";'); - expect(tabBarSource.match(//g)).toHaveLength(4); + expect(tabBarSource.match(//g)).toHaveLength(4); + expect(tabBarSource.match(/<\/TabExecutionStatus>/g)).toHaveLength(4); }); }); diff --git a/apps/desktop/src/components/layout/__tests__/TabExecutionStatus.spec.ts b/apps/desktop/src/components/layout/__tests__/TabExecutionStatus.spec.ts index ff995fb3e6..8873cd5c31 100644 --- a/apps/desktop/src/components/layout/__tests__/TabExecutionStatus.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/TabExecutionStatus.spec.ts @@ -10,12 +10,12 @@ vi.mock("vue-i18n", () => ({ const mountedApps: ReturnType[] = []; -function mountStatus(initial: { mode: "query" | "data"; isExecuting: boolean; isCancelling?: boolean }) { +function mountStatus(initial: { mode: "query" | "data"; isExecuting: boolean; isCancelling?: boolean }, withFallback = false) { const state = reactive(initial); const root = document.createElement("div"); document.body.appendChild(root); const app = createApp({ - setup: () => () => h(TabExecutionStatus, { tab: state }), + setup: () => () => h(TabExecutionStatus, { tab: state }, withFallback ? { default: () => h("span", { "data-tab-icon": "" }) } : undefined), }); mountedApps.push(app); app.mount(root); @@ -48,6 +48,22 @@ describe("TabExecutionStatus", () => { expect(status?.className).toContain("text-amber-600"); }); + it("replaces the tab icon while the query is executing", async () => { + const { root, state } = mountStatus({ mode: "query", isExecuting: false }, true); + await nextTick(); + expect(root.querySelector("[data-tab-icon]")).not.toBeNull(); + + state.isExecuting = true; + await nextTick(); + expect(root.querySelector("[data-tab-icon]")).toBeNull(); + expect(root.querySelector("[data-tab-execution-status]")).not.toBeNull(); + + state.isExecuting = false; + await nextTick(); + expect(root.querySelector("[data-tab-execution-status]")).toBeNull(); + expect(root.querySelector("[data-tab-icon]")).not.toBeNull(); + }); + it.each(["success", "error", "cancelled"])("disappears as soon as %s completes", async () => { const { root, state } = mountStatus({ mode: "query", isExecuting: true, isCancelling: true }); await nextTick();