From b3f1607c84ea5b65415129067c4fae6fc30e5294 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Wed, 21 Oct 2026 12:01:27 -0700 Subject: [PATCH] Remove directional tab-close actions and tracking (#31599) * Remove directional tab-close menu actions * Remove retired directional-close translations * Remove retired directional-close PostHog tracking * Preserve legacy app-open schema compatibility without emitting samples --- .../mobile-bulk-close-sheet-actions.ts | 36 ++-- .../mobile-tab-close-selection.test.ts | 20 +- .../src/session/mobile-tab-close-selection.ts | 19 +- .../telemetry/client-tab-close-budget.test.ts | 181 ------------------ src/main/telemetry/client.test.ts | 9 +- src/main/telemetry/client.ts | 37 ---- src/main/telemetry/tab-close-sampling.test.ts | 23 --- src/main/telemetry/tab-close-sampling.ts | 8 - .../src/components/tab-bar/BrowserTab.tsx | 28 +-- .../tab-bar/EditorFileTabContextMenu.test.tsx | 6 +- .../tab-bar/EditorFileTabContextMenu.tsx | 32 +--- .../tab-bar/SortableTabContextMenu.test.tsx | 28 +-- .../tab-bar/SortableTabContextMenu.tsx | 29 +-- .../useTabGroupCloseScopeCommands.test.ts | 158 --------------- .../useTabGroupCloseScopeCommands.ts | 23 +-- src/renderer/src/i18n/locales/en.json | 12 +- src/renderer/src/i18n/locales/es.json | 12 +- src/renderer/src/i18n/locales/fr.json | 12 +- src/renderer/src/i18n/locales/ja.json | 12 +- src/renderer/src/i18n/locales/ko.json | 12 +- src/renderer/src/i18n/locales/zh.json | 12 +- src/shared/telemetry-app-event-schemas.ts | 4 +- src/shared/telemetry-event-registry.ts | 2 - .../telemetry-tab-event-schemas.test.ts | 39 ---- src/shared/telemetry-tab-event-schemas.ts | 15 -- 25 files changed, 59 insertions(+), 710 deletions(-) delete mode 100644 src/main/telemetry/client-tab-close-budget.test.ts delete mode 100644 src/main/telemetry/tab-close-sampling.test.ts delete mode 100644 src/main/telemetry/tab-close-sampling.ts delete mode 100644 src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.ts delete mode 100644 src/shared/telemetry-tab-event-schemas.test.ts delete mode 100644 src/shared/telemetry-tab-event-schemas.ts diff --git a/mobile/src/session/mobile-bulk-close-sheet-actions.ts b/mobile/src/session/mobile-bulk-close-sheet-actions.ts index bdce5323a8f..00121c07d88 100644 --- a/mobile/src/session/mobile-bulk-close-sheet-actions.ts +++ b/mobile/src/session/mobile-bulk-close-sheet-actions.ts @@ -1,10 +1,6 @@ import type { MarkdownDocState, MobileSessionTab } from './mobile-session-route-types' import type { ActionSheetAction } from '../components/ActionSheetModal' -import { - BULK_TAB_CLOSE_ACTIONS, - selectBulkCloseTabs, - type BulkTabCloseMode -} from './mobile-tab-close-selection' +import { selectBulkCloseTabs } from './mobile-tab-close-selection' /** Session-route state the bulk close orchestration reads and drives. */ type BulkCloseSheetDeps = { @@ -16,14 +12,14 @@ type BulkCloseSheetDeps = { } /** - * Builds the long-press bulk-close entries (Close Others / Left) shared + * Builds the long-press bulk-close entries (Close Others) shared * by every session tab sheet. Lives outside the session route to keep the * orchestration out of its max-lines budget; anchors are passed by tab id so * sheets never need the full tab object. */ export function createBulkCloseSheetActions(deps: BulkCloseSheetDeps) { - const selectClosable = (anchorTabId: string, mode: BulkTabCloseMode) => - selectBulkCloseTabs(deps.sessionTabsRef.current, anchorTabId, mode).filter((candidate) => { + const selectClosable = (anchorTabId: string) => + selectBulkCloseTabs(deps.sessionTabsRef.current, anchorTabId).filter((candidate) => { if (candidate.type !== 'markdown') { return true } @@ -33,8 +29,8 @@ export function createBulkCloseSheetActions(deps: BulkCloseSheetDeps) { return !(doc?.status === 'ready' && doc.isDirty) }) - const bulkClose = async (anchor: MobileSessionTab, mode: BulkTabCloseMode) => { - const targets = selectClosable(anchor.id, mode) + const bulkClose = async (anchor: MobileSessionTab) => { + const targets = selectClosable(anchor.id) const activeWasTargeted = targets.some( (candidate) => candidate.id === deps.activeSessionTabIdRef.current ) @@ -53,19 +49,19 @@ export function createBulkCloseSheetActions(deps: BulkCloseSheetDeps) { anchorTabId == null ? undefined : deps.sessionTabsRef.current.find((candidate) => candidate.id === anchorTabId) - if (!anchor) { + if (!anchor || selectClosable(anchor.id).length === 0) { return [] } - return BULK_TAB_CLOSE_ACTIONS.filter( - ({ mode }) => selectClosable(anchor.id, mode).length > 0 - ).map(({ mode, label }) => ({ - label, - destructive: true, - onPress: () => { - dismiss() - void bulkClose(anchor, mode) + return [ + { + label: 'Close Other Tabs', + destructive: true, + onPress: () => { + dismiss() + void bulkClose(anchor) + } } - })) + ] } } diff --git a/mobile/src/session/mobile-tab-close-selection.test.ts b/mobile/src/session/mobile-tab-close-selection.test.ts index cac9f181a32..c90c1c79f48 100644 --- a/mobile/src/session/mobile-tab-close-selection.test.ts +++ b/mobile/src/session/mobile-tab-close-selection.test.ts @@ -10,31 +10,21 @@ const tab = (id: string, isDirty?: boolean, isPinned?: boolean) => ({ describe('selectBulkCloseTabs', () => { const tabs = [tab('a'), tab('b'), tab('c'), tab('d')] - it('selects every tab except the anchor for mode "others"', () => { - expect(selectBulkCloseTabs(tabs, 'b', 'others').map((t) => t.id)).toEqual(['a', 'c', 'd']) - }) - - it('selects tabs before the anchor for mode "left"', () => { - expect(selectBulkCloseTabs(tabs, 'c', 'left').map((t) => t.id)).toEqual(['a', 'b']) - }) - - it('returns empty when the anchor is at the left edge', () => { - expect(selectBulkCloseTabs(tabs, 'a', 'left')).toEqual([]) + it('selects every tab except the anchor', () => { + expect(selectBulkCloseTabs(tabs, 'b').map((t) => t.id)).toEqual(['a', 'c', 'd']) }) it('returns empty when the anchor is not in the list', () => { - expect(selectBulkCloseTabs(tabs, 'missing', 'others')).toEqual([]) + expect(selectBulkCloseTabs(tabs, 'missing')).toEqual([]) }) it('skips dirty tabs so unsaved edits survive a bulk close', () => { const withDirty = [tab('a', true), tab('b'), tab('c', false), tab('d')] - expect(selectBulkCloseTabs(withDirty, 'd', 'left').map((t) => t.id)).toEqual(['b', 'c']) - expect(selectBulkCloseTabs(withDirty, 'b', 'others').map((t) => t.id)).toEqual(['c', 'd']) + expect(selectBulkCloseTabs(withDirty, 'b').map((t) => t.id)).toEqual(['c', 'd']) }) it('skips pinned tabs', () => { const withPinned = [tab('a', undefined, true), tab('b'), tab('c', undefined, true), tab('d')] - expect(selectBulkCloseTabs(withPinned, 'd', 'left').map((t) => t.id)).toEqual(['b']) - expect(selectBulkCloseTabs(withPinned, 'b', 'others').map((t) => t.id)).toEqual(['d']) + expect(selectBulkCloseTabs(withPinned, 'b').map((t) => t.id)).toEqual(['d']) }) }) diff --git a/mobile/src/session/mobile-tab-close-selection.ts b/mobile/src/session/mobile-tab-close-selection.ts index 11829e0cb3b..1cba116bed7 100644 --- a/mobile/src/session/mobile-tab-close-selection.ts +++ b/mobile/src/session/mobile-tab-close-selection.ts @@ -1,11 +1,3 @@ -export type BulkTabCloseMode = 'others' | 'left' - -/** Long-press sheet entries, in display order. */ -export const BULK_TAB_CLOSE_ACTIONS: { mode: BulkTabCloseMode; label: string }[] = [ - { mode: 'others', label: 'Close Other Tabs' }, - { mode: 'left', label: 'Close Tabs to the Left' } -] - type BulkClosableTab = { id: string isDirty?: boolean @@ -13,23 +5,18 @@ type BulkClosableTab = { } /** - * Pick the tabs a long-press bulk close ("Close Other Tabs" / "Close Tabs to - * the Left") should target, in strip order relative to the pressed tab. + * Pick the other tabs a long-press bulk close should target, in strip order. * Dirty documents are skipped — mobile has no save prompt on close, so bulk * closing must never silently discard unsaved edits. */ export function selectBulkCloseTabs( tabs: readonly T[], - anchorTabId: string, - mode: BulkTabCloseMode + anchorTabId: string ): T[] { const anchorIndex = tabs.findIndex((tab) => tab.id === anchorTabId) if (anchorIndex === -1) { return [] } - const candidates = - mode === 'others' - ? tabs.filter((_, index) => index !== anchorIndex) - : tabs.slice(0, anchorIndex) + const candidates = tabs.filter((_, index) => index !== anchorIndex) return candidates.filter((tab) => tab.isDirty !== true && tab.isPinned !== true) } diff --git a/src/main/telemetry/client-tab-close-budget.test.ts b/src/main/telemetry/client-tab-close-budget.test.ts deleted file mode 100644 index 94acb43767a..00000000000 --- a/src/main/telemetry/client-tab-close-budget.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { EventProps } from '../../shared/telemetry-events' -import { - _setCommonPropsForTests, - initTelemetry, - setOptIn, - track, - trackAppOpenedOnce -} from './client' -import { - BASE_COMMON, - cleanupTelemetryClientTest, - setupTelemetryClientTest, - type TelemetryClientTestState -} from './client-test-harness' - -const left: EventProps<'tabs_directional_close_requested'> = { - direction: 'left', - tab_type: 'terminal', - surface: 'workspace', - eligible_tab_count: 3 -} -const right: EventProps<'tabs_directional_close_requested'> = { ...left, direction: 'right' } - -describe('directional tab close event budget', () => { - let state: TelemetryClientTestState - - beforeEach(() => { - state = setupTelemetryClientTest() - _setCommonPropsForTests({ - ...BASE_COMMON, - install_id: '00000000-0000-4000-8000-000000000013' - }) - }) - afterEach(() => { - cleanupTelemetryClientTest(state.envStash) - vi.useRealTimers() - }) - - it('captures at most two events across tab types, surfaces, and token-bucket refills', () => { - vi.useFakeTimers() - expect(track('tabs_directional_close_requested', left)).toBe(true) - expect(track('tabs_directional_close_requested', right)).toBe(true) - - for (let index = 0; index < 100; index++) { - vi.advanceTimersByTime(60_000) - expect( - track('tabs_directional_close_requested', { - ...left, - tab_type: 'browser', - surface: 'floating' - }) - ).toBe(false) - expect(track('tabs_directional_close_requested', right)).toBe(false) - } - - expect(state.mock.capture).toHaveBeenCalledTimes(2) - }) - - it('does not let repeat clicks exhaust the other direction or the global event budget', () => { - track('tabs_directional_close_requested', left) - for (let index = 0; index < 1500; index++) { - track('tabs_directional_close_requested', left) - } - - expect(track('tabs_directional_close_requested', right)).toBe(true) - expect(track('app_opened', {})).toBe(true) - expect(state.mock.capture).toHaveBeenCalledTimes(3) - }) - - it('allows each direction again after a new app session starts', () => { - track('tabs_directional_close_requested', left) - track('tabs_directional_close_requested', right) - initTelemetry(state.store) - - expect(track('tabs_directional_close_requested', left)).toBe(true) - expect(track('tabs_directional_close_requested', right)).toBe(true) - expect(state.mock.capture).toHaveBeenCalledTimes(4) - }) - - it('does not spend the direction allowance while telemetry is disabled', () => { - state.settings.telemetry!.optedIn = false - expect(track('tabs_directional_close_requested', left)).toBe(false) - state.settings.telemetry!.optedIn = true - - expect(track('tabs_directional_close_requested', left)).toBe(true) - expect(state.mock.capture).toHaveBeenCalledTimes(1) - }) - - it('does not spend the direction allowance on an invalid payload', () => { - expect(track('tabs_directional_close_requested', { ...left, eligible_tab_count: -1 })).toBe( - false - ) - - expect(track('tabs_directional_close_requested', left)).toBe(true) - expect(state.mock.capture).toHaveBeenCalledTimes(1) - }) - - it.each([true, false])( - 'includes settings opt-in in the denominator (sampled: %s)', - async (sampled) => { - if (!sampled) { - _setCommonPropsForTests(BASE_COMMON) - } - state.settings.telemetry!.optedIn = false - trackAppOpenedOnce() - expect(track('tabs_directional_close_requested', left)).toBe(false) - expect(state.mock.capture).not.toHaveBeenCalled() - - await setOptIn('settings', true) - expect(track('tabs_directional_close_requested', left)).toBe(sampled) - await setOptIn('settings', false) - await setOptIn('settings', true) - trackAppOpenedOnce() - expect(track('tabs_directional_close_requested', left)).toBe(false) - - const captures = state.mock.capture.mock.calls.map(([capture]) => capture) - const opened = captures.filter((capture) => capture.event === 'app_opened') - expect(opened).toHaveLength(1) - expect(opened[0].properties).toMatchObject({ - tab_close_sampled: sampled, - tab_close_sample_rate: 0.1, - session_id: BASE_COMMON.session_id - }) - expect(captures[0]).toEqual(opened[0]) - const closes = captures.filter( - (capture) => capture.event === 'tabs_directional_close_requested' - ) - expect(closes).toHaveLength(sampled ? 1 : 0) - if (sampled) { - expect(closes[0].properties.session_id).toBe(opened[0].properties.session_id) - } - } - ) - - it('does not consume the denominator while an environment consent override blocks opt-in', async () => { - process.env.DO_NOT_TRACK = '1' - await setOptIn('settings', true) - expect(track('tabs_directional_close_requested', left)).toBe(false) - expect(state.mock.capture).not.toHaveBeenCalled() - - delete process.env.DO_NOT_TRACK - await setOptIn('settings', true) - expect(track('tabs_directional_close_requested', left)).toBe(true) - expect(state.mock.capture.mock.calls.map(([capture]) => capture.event)).toEqual([ - 'app_opened', - 'telemetry_opted_in', - 'tabs_directional_close_requested' - ]) - }) - - it('marks sampled installs on the existing app-open event without adding an event', () => { - track('app_opened', { tab_close_sampled: false }) - track('tabs_directional_close_requested', left) - - expect(state.mock.capture).toHaveBeenCalledTimes(2) - expect(state.mock.capture.mock.calls[0]?.[0].properties).toMatchObject({ - tab_close_sampled: true, - tab_close_sample_rate: 0.1 - }) - expect(state.mock.capture.mock.calls[1]?.[0].properties).toMatchObject({ sample_rate: 0.1 }) - }) - - it('excludes unsampled installs while leaving other analytics enabled', () => { - _setCommonPropsForTests(BASE_COMMON) - - for (let index = 0; index < 1500; index++) { - expect(track('tabs_directional_close_requested', left)).toBe(false) - expect(track('tabs_directional_close_requested', right)).toBe(false) - } - expect(track('app_opened', { tab_close_sampled: true })).toBe(true) - expect( - track('workspace_created', { source: 'command_palette', from_existing_branch: false }) - ).toBe(true) - expect(state.mock.capture).toHaveBeenCalledTimes(2) - expect(state.mock.capture.mock.calls[0]?.[0].properties).toMatchObject({ - tab_close_sampled: false, - tab_close_sample_rate: 0.1 - }) - }) -}) diff --git a/src/main/telemetry/client.test.ts b/src/main/telemetry/client.test.ts index 405de93442a..c862da64810 100644 --- a/src/main/telemetry/client.test.ts +++ b/src/main/telemetry/client.test.ts @@ -2,7 +2,6 @@ // tests pin capture payload drift, shutdown gating, burst caps, consent order, // and the per-session event ceiling. import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { CommonProps } from '../../shared/telemetry-events' import type { Store } from '../persistence' import { _setShuttingDownForTests, track, trackAppOpenedOnce } from './client' import { @@ -34,10 +33,10 @@ describe('track()', () => { const call = mock.capture.mock.calls[0]![0] expect(call.event).toBe('app_opened') expect(call.distinctId).toBe(BASE_COMMON.install_id) - expect(call.properties.$process_person_profile).toBe(false) - for (const key of Object.keys(BASE_COMMON) as (keyof CommonProps)[]) { - expect(call.properties[key]).toBe(BASE_COMMON[key]) - } + expect(call.properties).toEqual({ + ...BASE_COMMON, + $process_person_profile: false + }) }) // Drift-check: the full set of keys on the capture payload is bounded by diff --git a/src/main/telemetry/client.ts b/src/main/telemetry/client.ts index 18cb3959bd0..da9c7eb41fd 100644 --- a/src/main/telemetry/client.ts +++ b/src/main/telemetry/client.ts @@ -10,12 +10,10 @@ import { arch as osArch, platform as osPlatform, release as osRelease } from 'no import { getAppEnvironment } from '../../shared/app-environment' import { PostHog } from 'posthog-node' import type { CommonProps, EventName, EventProps, OptInVia } from '../../shared/telemetry-events' -import { TAB_CLOSE_TELEMETRY_SAMPLE_RATE } from '../../shared/telemetry-tab-event-schemas' import type { Store } from '../persistence' import { consumeBurstToken, resetBurstCapsForSession } from './burst-cap' import { getCohortAtEmit } from './cohort-classifier' import { resolveConsent, type ConsentState } from './consent' -import { isInTabCloseTelemetrySample } from './tab-close-sampling' import { commonPropsSchema, validate } from './validator' // Compile-time feature flag, independent of the build-identity gate — both must be satisfied to transmit. @@ -41,7 +39,6 @@ const IS_OFFICIAL_BUILD: boolean = let posthog: PostHog | null = null let sessionId: string | null = null let commonProps: CommonProps | null = null -let tabCloseSampled = false let shuttingDown = false let storeRef: Store | null = null @@ -52,8 +49,6 @@ let testTransportEnabled = false // First-launch `app_opened` gate: no events transmit until the banner resolves; keep mark+emit atomic. let appOpenedTrackedThisSession = false -// Main owns this budget so additional windows cannot multiply billed tab-action events. -const trackedTabCloseDirections = new Set<'left' | 'right'>() function buildCommonProps(installId: string, sid: string, channel: 'stable' | 'rc'): CommonProps { // Don't truncate here; the validator's `.max(64)` is authoritative, so an over-long string drops rather than being silently masked. @@ -75,7 +70,6 @@ export function initTelemetry(store: Store): void { shuttingDown = false // Reset per session: the "no app_opened until banner resolution" invariant is per-launch, not per-install. appOpenedTrackedThisSession = false - trackedTabCloseDirections.clear() if (!TELEMETRY_ENABLED || !IS_OFFICIAL_BUILD) { return @@ -105,7 +99,6 @@ export function initTelemetry(store: Store): void { commonProps = null return } - tabCloseSampled = isInTabCloseTelemetrySample(commonProps.install_id) posthog = new PostHog(WRITE_KEY as string, { host: 'https://us.i.posthog.com', @@ -193,21 +186,6 @@ export function track(name: N, props: EventProps): boole return false } - const tabCloseDirection = - name === 'tabs_directional_close_requested' && - props !== null && - typeof props === 'object' && - 'direction' in props && - (props.direction === 'left' || props.direction === 'right') - ? props.direction - : null - if (tabCloseDirection && trackedTabCloseDirections.has(tabCloseDirection)) { - return false - } - if (name === 'tabs_directional_close_requested' && !tabCloseSampled) { - return false - } - // (2) Burst cap before consent: the O(1) cap drops floods before the costly settings read, so a compromised opted-out renderer can't burn CPU. if (!consumeBurstToken(name)) { return false @@ -224,16 +202,6 @@ export function track(name: N, props: EventProps): boole if (!result.ok) { return false } - const samplingProps = - name === 'app_opened' - ? { - tab_close_sampled: tabCloseSampled, - tab_close_sample_rate: TAB_CLOSE_TELEMETRY_SAMPLE_RATE - } - : name === 'tabs_directional_close_requested' - ? { sample_rate: TAB_CLOSE_TELEMETRY_SAMPLE_RATE } - : {} - // (5) Capture. `$process_person_profile: false` stops posthog-node creating a person per install_id (no init-time equivalent). posthog.capture({ distinctId: commonProps.install_id, @@ -241,13 +209,9 @@ export function track(name: N, props: EventProps): boole properties: { ...commonProps, ...result.props, - ...samplingProps, $process_person_profile: false } }) - if (tabCloseDirection) { - trackedTabCloseDirections.add(tabCloseDirection) - } return true } @@ -361,7 +325,6 @@ export function _setPostHogClientForTests(client: PostHog | null): void { export function _setCommonPropsForTests(props: CommonProps | null): void { commonProps = props - tabCloseSampled = props !== null && isInTabCloseTelemetrySample(props.install_id) } export function _setStoreForTests(store: Store | null): void { diff --git a/src/main/telemetry/tab-close-sampling.test.ts b/src/main/telemetry/tab-close-sampling.test.ts deleted file mode 100644 index 8618bec5659..00000000000 --- a/src/main/telemetry/tab-close-sampling.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { isInTabCloseTelemetrySample } from './tab-close-sampling' - -describe('tab close telemetry sampling', () => { - it('keeps known anonymous installs consistently inside or outside the sample', () => { - for (let index = 0; index < 10; index++) { - expect(isInTabCloseTelemetrySample('00000000-0000-4000-8000-000000000013')).toBe(true) - expect(isInTabCloseTelemetrySample('00000000-0000-4000-8000-000000000000')).toBe(false) - } - }) - - it('selects about ten percent of a fixed set of installs', () => { - let sampled = 0 - for (let index = 0; index < 10_000; index++) { - const installId = `00000000-0000-4000-8000-${String(index).padStart(12, '0')}` - if (isInTabCloseTelemetrySample(installId)) { - sampled++ - } - } - expect(sampled).toBeGreaterThan(900) - expect(sampled).toBeLessThan(1100) - }) -}) diff --git a/src/main/telemetry/tab-close-sampling.ts b/src/main/telemetry/tab-close-sampling.ts deleted file mode 100644 index 90ad66700e1..00000000000 --- a/src/main/telemetry/tab-close-sampling.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createHash } from 'node:crypto' -import { TAB_CLOSE_TELEMETRY_SAMPLE_RATE } from '../../shared/telemetry-tab-event-schemas' - -export function isInTabCloseTelemetrySample(installId: string): boolean { - // Keep the namespace stable so cohort membership survives app restarts and updates. - const hash = createHash('sha256').update('tabs-directional-close:v1:').update(installId).digest() - return hash.readUInt32BE(0) / 2 ** 32 < TAB_CLOSE_TELEMETRY_SAMPLE_RATE -} diff --git a/src/renderer/src/components/tab-bar/BrowserTab.tsx b/src/renderer/src/components/tab-bar/BrowserTab.tsx index b36bba5b3a7..54e62826aa4 100644 --- a/src/renderer/src/components/tab-bar/BrowserTab.tsx +++ b/src/renderer/src/components/tab-bar/BrowserTab.tsx @@ -1,15 +1,6 @@ import { useEffect, useState } from 'react' import { useSortable } from '@dnd-kit/sortable' -import { - X, - ExternalLink, - Copy, - CopyX, - Pin, - PinOff, - PanelLeftClose, - PanelRightClose -} from 'lucide-react' +import { X, ExternalLink, Copy, CopyX, Pin, PinOff } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -76,13 +67,10 @@ export default function BrowserTab({ isActive, isPinned, hasTabsToRight, - hasTabsToLeft, tabCount, onActivate, onClose, onCloseOthers, - onCloseToRight, - onCloseToLeft, onCloseSplitPane, onDuplicate, onTogglePin, @@ -297,20 +285,6 @@ export default function BrowserTab({ {translate('components.tab.bar.BrowserTab.closeOthers', 'Close Others')} - - - {translate( - 'auto.components.tab.bar.BrowserTab.9dd880bd56', - 'Close All Tabs To The Right' - )} - - - - {translate( - 'components.tab.bar.BrowserTab.closeTabsToLeft', - 'Close All Tabs To The Left' - )} - void window.api.shell.openUrl(openInBrowserUrl)} diff --git a/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx b/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx index dfd44832701..7ecd43abeb2 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.test.tsx @@ -360,15 +360,15 @@ describe('EditorFileTabContextMenu close-all shortcut', () => { expect(findElementsByType(tree, 'DropdownMenuShortcut')).toHaveLength(3) }) - it('renders Close Others and both directional close items', async () => { + it('keeps Close Others without directional close items', async () => { const tree = expandNode(await renderMenu()) const labels = findElementsByType(tree, 'DropdownMenuItem').map((item) => extractText(item.props.children) ) expect(labels).toContain('Close Others') - expect(labels.some((label) => label.includes('Close All Tabs To The Right'))).toBe(true) - expect(labels.some((label) => label.includes('Close All Tabs To The Left'))).toBe(true) + expect(labels.some((label) => label.includes('Close All Tabs To The Right'))).toBe(false) + expect(labels.some((label) => label.includes('Close All Tabs To The Left'))).toBe(false) }) it('hides the shortcut chip when close-all is unassigned', async () => { diff --git a/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.tsx b/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.tsx index fd207fbe046..45576a33dcf 100644 --- a/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.tsx +++ b/src/renderer/src/components/tab-bar/EditorFileTabContextMenu.tsx @@ -1,16 +1,4 @@ -import { - Copy, - CopyX, - ExternalLink, - Eye, - ListX, - PanelLeftClose, - PanelRightClose, - Pencil, - Pin, - PinOff, - X -} from 'lucide-react' +import { Copy, CopyX, ExternalLink, Eye, ListX, Pencil, Pin, PinOff, X } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -82,8 +70,6 @@ export function EditorFileTabContextMenu({ groupId, isPinned, isRenaming, - hasTabsToRight, - hasTabsToLeft, tabCount, canRename, canShowMarkdownPreview, @@ -97,8 +83,6 @@ export function EditorFileTabContextMenu({ onClose, onCloseOthers, onCloseAll, - onCloseToRight, - onCloseToLeft, onCloseSplitPane, onOpenMarkdownPreview }: EditorFileTabContextMenuProps): React.JSX.Element { @@ -182,20 +166,6 @@ export function EditorFileTabContextMenu({ {closeAllShortcut} ) : null} - - - {translate( - 'auto.components.tab.bar.EditorFileTabContextMenu.e5ff31ccaf', - 'Close All Tabs To The Right' - )} - - - - {translate( - 'components.tab.bar.EditorFileTabContextMenu.closeTabsToLeft', - 'Close All Tabs To The Left' - )} - {isVirtualEditorFile(file) ? null : } {canShowMarkdownPreview ? ( diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx index 8f1a5a5b42d..7953fe660e6 100644 --- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx +++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx @@ -352,34 +352,14 @@ describe('SortableTabContextMenu', () => { expect(container.textContent).not.toContain('Split terminal') }) - it('routes the directional close actions to their handlers with the tab id', () => { + it('keeps Close Others available without directional close entries', () => { const onCloseOthers = vi.fn() - const onCloseToRight = vi.fn() - const onCloseToLeft = vi.fn() - const { container } = renderMenu({ - onCloseOthers, - onCloseToRight, - onCloseToLeft - }) + const { container } = renderMenu({ onCloseOthers }) act(() => getButton(container, 'Close Others').click()) expect(onCloseOthers).toHaveBeenCalledWith('term-1') - - act(() => getButton(container, 'Close All Tabs To The Right').click()) - expect(onCloseToRight).toHaveBeenCalledWith('term-1') - - act(() => getButton(container, 'Close All Tabs To The Left').click()) - expect(onCloseToLeft).toHaveBeenCalledWith('term-1') - }) - - it('disables directional closes when no tabs exist on that side', () => { - const { container } = renderMenu({ - hasTabsToLeft: false, - hasTabsToRight: false - }) - - expect(getButton(container, 'Close All Tabs To The Left').disabled).toBe(true) - expect(getButton(container, 'Close All Tabs To The Right').disabled).toBe(true) + expect(container.textContent).not.toContain('Close All Tabs To The Left') + expect(container.textContent).not.toContain('Close All Tabs To The Right') }) it('hides move-tab split actions for a single-tab group', () => { diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx index 20bdc025529..f71ebfdb35f 100644 --- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx +++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx @@ -1,13 +1,4 @@ -import { - PanelLeftClose, - PanelRightClose, - Pin, - PinOff, - Pencil, - SquareTerminal, - X, - ListX -} from 'lucide-react' +import { Pin, PinOff, Pencil, SquareTerminal, X, ListX } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -135,8 +126,6 @@ export function SortableTabContextMenu({ open, point, tabCount, - hasTabsToRight, - hasTabsToLeft, isPinned, onOpenChange, onSessionSwitchComplete, @@ -144,8 +133,6 @@ export function SortableTabContextMenu({ onActivate, onClose, onCloseOthers, - onCloseToRight, - onCloseToLeft, onCloseSplitPane, onRenameOpen, onSetTabColor, @@ -228,20 +215,6 @@ export function SortableTabContextMenu({ {translate('auto.components.tab.bar.SortableTabContextMenu.8d16f9cd30', 'Close Others')} - onCloseToRight(tab.id)} disabled={!hasTabsToRight}> - - {translate( - 'auto.components.tab.bar.SortableTabContextMenu.c1ee099c7e', - 'Close All Tabs To The Right' - )} - - onCloseToLeft(tab.id)} disabled={!hasTabsToLeft}> - - {translate( - 'components.tab.bar.SortableTabContextMenu.closeTabsToLeft', - 'Close All Tabs To The Left' - )} - diff --git a/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.ts b/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.ts deleted file mode 100644 index a25e25d2732..00000000000 --- a/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -// @vitest-environment happy-dom -import { act, renderHook } from '@testing-library/react' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import type { Tab, TabContentType, TabGroup } from '../../../../shared/tab-types' -import { useTabGroupCloseScopeCommands } from './useTabGroupCloseScopeCommands' - -const mocks = vi.hoisted(() => { - const state: { - groupsByWorktree: Record - unifiedTabsByWorktree: Record - } = { groupsByWorktree: {}, unifiedTabsByWorktree: {} } - return { track: vi.fn(), closeEmptyGroup: vi.fn(), state } -}) - -vi.mock('@/lib/telemetry', () => ({ track: mocks.track })) -vi.mock('../../store', () => ({ - useAppStore: Object.assign( - (selector: (state: { closeEmptyGroup: () => void }) => unknown) => selector(mocks), - { getState: () => mocks.state } - ) -})) -vi.mock('../confirmation-dialog-context', () => ({ - useConfirmationDialog: () => async () => true -})) -vi.mock('./workspace-emptied-reaction', () => ({ - captureWorkspaceEmptiedReaction: vi.fn(() => vi.fn()) -})) - -function tab(id: string, contentType: TabContentType = 'terminal', isPinned = false): Tab { - return { - id, - entityId: id, - contentType, - groupId: 'group-1', - worktreeId: 'worktree-1', - label: 'private tab title', - customLabel: null, - color: null, - sortOrder: 0, - createdAt: 0, - isPinned - } -} - -function commands({ - worktreeId = 'worktree-1', - contentType = 'terminal', - closeMany = vi.fn() -}: { - worktreeId?: string - contentType?: TabContentType - closeMany?: (ids: string[]) => void -} = {}) { - const group: TabGroup = { - id: 'group-1', - worktreeId, - activeTabId: 'selected', - tabOrder: ['left', 'pinned-left', 'selected', 'right', 'pinned-right', 'missing', 'last'] - } - // Storage order differs from the visible order; pinned and missing tabs must not count. - const groupTabs = [ - tab('last'), - tab('selected', contentType), - tab('pinned-left', 'terminal', true), - tab('pinned-right', 'terminal', true), - tab('right'), - tab('left') - ] - mocks.state = { - groupsByWorktree: { [worktreeId]: [group] }, - unifiedTabsByWorktree: { [worktreeId]: groupTabs } - } - const hook = renderHook(() => - useTabGroupCloseScopeCommands({ - groupId: group.id, - worktreeId, - closeItem: vi.fn(), - closeMany - }) - ) - return { ...hook.result.current, closeMany } -} - -beforeEach(() => vi.clearAllMocks()) - -describe('directional tab close telemetry', () => { - it.each(['left', 'right'] as const)( - 'counts one %s request before asking to close tabs', - async (direction) => { - const expectedIds = direction === 'left' ? ['left'] : ['right', 'last'] - const closeMany = vi.fn((ids: string[]) => { - expect(mocks.track).toHaveBeenCalledExactlyOnceWith('tabs_directional_close_requested', { - direction, - tab_type: 'terminal', - surface: 'workspace', - eligible_tab_count: expectedIds.length - }) - expect(ids).toEqual(expectedIds) - // The close flow can leave these tabs open after a cancelled save prompt. - }) - const scope = commands({ closeMany }) - - await act(async () => { - if (direction === 'left') { - scope.closeToLeft('selected') - } else { - scope.closeToRight('selected') - } - }) - - expect(closeMany).toHaveBeenCalledExactlyOnceWith(expectedIds) - } - ) - - it.each([ - ['terminal', 'terminal'], - ['editor', 'editor'], - ['diff', 'editor'], - ['conflict-review', 'editor'], - ['check-details', 'editor'], - ['agent-session', 'agent-session'], - ['browser', 'browser'], - ['simulator', 'simulator'] - ] as const)('reports %s tabs as %s in the floating panel', (contentType, tabType) => { - commands({ worktreeId: FLOATING_TERMINAL_WORKTREE_ID, contentType }).closeToLeft('selected') - - expect(mocks.track).toHaveBeenCalledExactlyOnceWith('tabs_directional_close_requested', { - direction: 'left', - tab_type: tabType, - surface: 'floating', - eligible_tab_count: 1 - }) - }) - - it('records an empty selection as a request with no eligible tabs', () => { - const scope = commands() - scope.closeToLeft('left') - - expect(mocks.track).toHaveBeenCalledExactlyOnceWith('tabs_directional_close_requested', { - direction: 'left', - tab_type: 'terminal', - surface: 'workspace', - eligible_tab_count: 0 - }) - expect(scope.closeMany).not.toHaveBeenCalled() - }) - - it('does not count stale tab ids or unrelated close commands', () => { - const scope = commands() - scope.closeToLeft('stale') - scope.closeToRight('stale') - scope.closeOthers('selected') - scope.closeAllEditorTabsInGroup() - - expect(mocks.track).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.ts b/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.ts index 000b75213c6..0f6615b040b 100644 --- a/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.ts +++ b/src/renderer/src/components/tab-group/useTabGroupCloseScopeCommands.ts @@ -1,7 +1,4 @@ import { useCallback } from 'react' -import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants' -import { toVisibleTabType } from '../../../../shared/tab-types' -import { track } from '@/lib/telemetry' import { useAppStore } from '../../store' import { captureWorkspaceEmptiedReaction } from './workspace-emptied-reaction' import { isEditorTabContentType } from '@/store/slices/editor/tabs/editor-tab-content-type' @@ -93,27 +90,9 @@ export function useTabGroupCloseScopeCommands({ const closeInDirection = useCallback( (itemId: string, direction: 'left' | 'right') => { - const state = useAppStore.getState() - const group = (state.groupsByWorktree[worktreeId] ?? []).find( - (candidate) => candidate.id === groupId - ) - if (!group?.tabOrder.includes(itemId)) { - return - } - const item = (state.unifiedTabsByWorktree[worktreeId] ?? []).find( - (candidate) => candidate.groupId === groupId && candidate.id === itemId - ) - if (item) { - track('tabs_directional_close_requested', { - direction, - tab_type: toVisibleTabType(item.contentType), - surface: worktreeId === FLOATING_TERMINAL_WORKTREE_ID ? 'floating' : 'workspace', - eligible_tab_count: selectTabIds(direction, itemId).length - }) - } confirmBulkClose(direction, () => selectTabIds(direction, itemId), closeMany) }, - [closeMany, confirmBulkClose, groupId, selectTabIds, worktreeId] + [closeMany, confirmBulkClose, selectTabIds] ) const closeToRight = useCallback( diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 5c930772860..a447e67e853 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -3778,7 +3778,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "Open In Browser", - "9dd880bd56": "Close All Tabs To The Right", "1611a1324b": "Close", "5d6e89891f": "Duplicate Tab", "966feb9ad5": "Split Right", @@ -3795,7 +3794,6 @@ "52ce4f4605": "Copy Relative Path", "5b85754786": "Copy Path", "bfd5797ef4": "Open Markdown Preview", - "e5ff31ccaf": "Close All Tabs To The Right", "ba1369dd24": "Close All Editor Tabs", "1ba8492c5b": "Close", "68cc610e7f": "Rename", @@ -3839,7 +3837,6 @@ "SortableTabContextMenu": { "35e8892fd0": "Tab Color", "2f697b3c31": "Change Title", - "c1ee099c7e": "Close All Tabs To The Right", "8d16f9cd30": "Close Others", "89359a36f7": "Close", "21132389e9": "Split Right", @@ -19452,16 +19449,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "Continue with Terminal Agent", - "switchToChatView": "Continue in Chat UI", - "closeTabsToLeft": "Close All Tabs To The Left" + "switchToChatView": "Continue in Chat UI" }, "BrowserTab": { - "closeOthers": "Close Others", - "closeTabsToLeft": "Close All Tabs To The Left" + "closeOthers": "Close Others" }, "EditorFileTabContextMenu": { - "closeOthers": "Close Others", - "closeTabsToLeft": "Close All Tabs To The Left" + "closeOthers": "Close Others" }, "CopyTabIdMenuItem": { "copied": "Tab ID copied", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 98818a7aff6..d60b8d2d5ef 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -3510,7 +3510,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "Abrir en el navegador", - "9dd880bd56": "Cerrar todas las pestañas a la derecha", "1611a1324b": "Cerrar", "5d6e89891f": "Duplicar pestaña", "966feb9ad5": "Dividir a la derecha", @@ -3527,7 +3526,6 @@ "52ce4f4605": "Copiar ruta relativa", "5b85754786": "Copiar ruta", "bfd5797ef4": "Abrir vista previa de Markdown", - "e5ff31ccaf": "Cerrar todas las pestañas a la derecha", "ba1369dd24": "Cerrar todas las pestañas del editor", "1ba8492c5b": "Cerrar", "68cc610e7f": "Renombrar", @@ -3560,7 +3558,6 @@ "SortableTabContextMenu": { "35e8892fd0": "Color de pestaña", "2f697b3c31": "Cambiar título", - "c1ee099c7e": "Cerrar todas las pestañas a la derecha", "8d16f9cd30": "Cerrar otras", "89359a36f7": "Cerrar", "21132389e9": "Dividir a la derecha", @@ -19079,16 +19076,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "Continuar con el agente de terminal", - "switchToChatView": "Continuar en la interfaz de chat", - "closeTabsToLeft": "Cerrar todas las pestañas a la izquierda" + "switchToChatView": "Continuar en la interfaz de chat" }, "BrowserTab": { - "closeOthers": "Cerrar otras", - "closeTabsToLeft": "Cerrar todas las pestañas a la izquierda" + "closeOthers": "Cerrar otras" }, "EditorFileTabContextMenu": { - "closeOthers": "Cerrar otras", - "closeTabsToLeft": "Cerrar todas las pestañas a la izquierda" + "closeOthers": "Cerrar otras" }, "CopyTabIdMenuItem": { "copied": "ID de pestaña copiado", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index a2bc424f814..f240a4cc818 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -3623,7 +3623,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "Ouvrir dans le navigateur", - "9dd880bd56": "Fermer tous les onglets à droite", "1611a1324b": "Fermer", "5d6e89891f": "Dupliquer l'onglet", "966feb9ad5": "Scinder vers la droite", @@ -3640,7 +3639,6 @@ "52ce4f4605": "Copier le chemin relatif", "5b85754786": "Copier le chemin", "bfd5797ef4": "Ouvrir l'aperçu Markdown", - "e5ff31ccaf": "Fermer tous les onglets à droite", "ba1369dd24": "Fermer tous les onglets de l'éditeur", "1ba8492c5b": "Fermer", "68cc610e7f": "Renommer", @@ -3673,7 +3671,6 @@ "SortableTabContextMenu": { "35e8892fd0": "Couleur de l'onglet", "2f697b3c31": "Modifier le titre", - "c1ee099c7e": "Fermer tous les onglets à droite", "8d16f9cd30": "Fermer les autres", "89359a36f7": "Fermer", "21132389e9": "Scinder vers la droite", @@ -19188,16 +19185,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "Continuer avec l’agent de terminal", - "switchToChatView": "Continuer dans l’interface de chat", - "closeTabsToLeft": "Fermer tous les onglets à gauche" + "switchToChatView": "Continuer dans l’interface de chat" }, "BrowserTab": { - "closeOthers": "Fermer les autres", - "closeTabsToLeft": "Fermer tous les onglets à gauche" + "closeOthers": "Fermer les autres" }, "EditorFileTabContextMenu": { - "closeOthers": "Fermer les autres", - "closeTabsToLeft": "Fermer tous les onglets à gauche" + "closeOthers": "Fermer les autres" }, "CopyTabIdMenuItem": { "copied": "ID de l'onglet copié", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 41e222a3656..c9864d59f75 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -3515,7 +3515,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "ブラウザで開く", - "9dd880bd56": "右側のすべてのタブを閉じる", "1611a1324b": "閉じる", "5d6e89891f": "タブの複製", "966feb9ad5": "右に分割", @@ -3532,7 +3531,6 @@ "52ce4f4605": "相対パスをコピー", "5b85754786": "パスのコピー", "bfd5797ef4": "Markdown プレビューを開く", - "e5ff31ccaf": "右側のすべてのタブを閉じる", "ba1369dd24": "すべてのエディターのタブを閉じる", "1ba8492c5b": "閉じる", "68cc610e7f": "名前を変更する", @@ -3565,7 +3563,6 @@ "SortableTabContextMenu": { "35e8892fd0": "タブの色", "2f697b3c31": "タイトルの変更", - "c1ee099c7e": "右側のすべてのタブを閉じる", "8d16f9cd30": "その他を閉じる", "89359a36f7": "閉じる", "21132389e9": "右に分割", @@ -19119,16 +19116,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "ターミナル Agent で続ける", - "switchToChatView": "チャット UI で続ける", - "closeTabsToLeft": "左側のすべてのタブを閉じる" + "switchToChatView": "チャット UI で続ける" }, "BrowserTab": { - "closeOthers": "その他を閉じる", - "closeTabsToLeft": "左側のすべてのタブを閉じる" + "closeOthers": "その他を閉じる" }, "EditorFileTabContextMenu": { - "closeOthers": "その他を閉じる", - "closeTabsToLeft": "左側のすべてのタブを閉じる" + "closeOthers": "その他を閉じる" }, "CopyTabIdMenuItem": { "copied": "タブ ID をコピーしました", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 49dbd4abb3b..d1e94b05464 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -3515,7 +3515,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "브라우저에서 열기", - "9dd880bd56": "오른쪽의 모든 탭 닫기", "1611a1324b": "닫기", "5d6e89891f": "탭 복제", "966feb9ad5": "오른쪽 분할", @@ -3532,7 +3531,6 @@ "52ce4f4605": "상대 경로 복사", "5b85754786": "경로 복사", "bfd5797ef4": "Markdown 미리보기 열기", - "e5ff31ccaf": "오른쪽의 모든 탭 닫기", "ba1369dd24": "모든 편집기 탭 닫기", "1ba8492c5b": "닫기", "68cc610e7f": "이름 바꾸기", @@ -3565,7 +3563,6 @@ "SortableTabContextMenu": { "35e8892fd0": "탭 색상", "2f697b3c31": "제목 변경", - "c1ee099c7e": "오른쪽의 모든 탭 닫기", "8d16f9cd30": "다른 탭 닫기", "89359a36f7": "닫기", "21132389e9": "오른쪽 분할", @@ -19119,16 +19116,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "터미널 에이전트로 계속하기", - "switchToChatView": "채팅 UI에서 계속하기", - "closeTabsToLeft": "왼쪽의 모든 탭 닫기" + "switchToChatView": "채팅 UI에서 계속하기" }, "BrowserTab": { - "closeOthers": "다른 탭 닫기", - "closeTabsToLeft": "왼쪽의 모든 탭 닫기" + "closeOthers": "다른 탭 닫기" }, "EditorFileTabContextMenu": { - "closeOthers": "다른 탭 닫기", - "closeTabsToLeft": "왼쪽의 모든 탭 닫기" + "closeOthers": "다른 탭 닫기" }, "CopyTabIdMenuItem": { "copied": "탭 ID 복사됨", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 9ef22a9d046..4f8a25a6191 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -3515,7 +3515,6 @@ "bar": { "BrowserTab": { "6e0bc8f3a8": "在浏览器中打开", - "9dd880bd56": "关闭右侧的所有选项卡", "1611a1324b": "关闭", "5d6e89891f": "复制选项卡", "966feb9ad5": "向右拆分", @@ -3532,7 +3531,6 @@ "52ce4f4605": "复制相对路径", "5b85754786": "复制路径", "bfd5797ef4": "打开 Markdown 预览", - "e5ff31ccaf": "关闭右侧的所有选项卡", "ba1369dd24": "关闭所有编辑器选项卡", "1ba8492c5b": "关闭", "68cc610e7f": "重命名", @@ -3565,7 +3563,6 @@ "SortableTabContextMenu": { "35e8892fd0": "标签颜色", "2f697b3c31": "更改标题", - "c1ee099c7e": "关闭右侧的所有选项卡", "8d16f9cd30": "关闭其他", "89359a36f7": "关闭", "21132389e9": "向右拆分", @@ -19084,16 +19081,13 @@ "bar": { "SortableTabContextMenu": { "switchToTerminalView": "使用终端智能体继续", - "switchToChatView": "在聊天界面中继续", - "closeTabsToLeft": "关闭左侧的所有选项卡" + "switchToChatView": "在聊天界面中继续" }, "BrowserTab": { - "closeOthers": "关闭其他", - "closeTabsToLeft": "关闭左侧的所有选项卡" + "closeOthers": "关闭其他" }, "EditorFileTabContextMenu": { - "closeOthers": "关闭其他", - "closeTabsToLeft": "关闭左侧的所有选项卡" + "closeOthers": "关闭其他" }, "CopyTabIdMenuItem": { "copied": "已复制标签页 ID", diff --git a/src/shared/telemetry-app-event-schemas.ts b/src/shared/telemetry-app-event-schemas.ts index 8732a6297d4..a108af4b373 100644 --- a/src/shared/telemetry-app-event-schemas.ts +++ b/src/shared/telemetry-app-event-schemas.ts @@ -21,7 +21,6 @@ import { workspaceSourceSchema } from './telemetry-property-schemas' import { workspaceCreatedTimingProperties } from './telemetry-workspace-create-schemas' -import { TAB_CLOSE_TELEMETRY_SAMPLE_RATE } from './telemetry-tab-event-schemas' // ── Per-event schemas ─────────────────────────────────────────────────── @@ -31,8 +30,9 @@ export const nthRepoAddedSchema = z.number().int().nonnegative().optional() export const appOpenedSchema = z .object({ nth_repo_added: nthRepoAddedSchema, + // Accept retired sampling fields from older callers; current builds no longer emit them. tab_close_sampled: z.boolean().optional(), - tab_close_sample_rate: z.literal(TAB_CLOSE_TELEMETRY_SAMPLE_RATE).optional() + tab_close_sample_rate: z.literal(0.1).optional() }) .strict() diff --git a/src/shared/telemetry-event-registry.ts b/src/shared/telemetry-event-registry.ts index ea9d26c0a20..53623c232ef 100644 --- a/src/shared/telemetry-event-registry.ts +++ b/src/shared/telemetry-event-registry.ts @@ -93,7 +93,6 @@ import { workspaceCreateFailedSchema } from './telemetry-repository-event-schemas' import { sshRemoteRuntimeResolvedSchema } from './telemetry-ssh-runtime-event-schemas' -import { tabsDirectionalCloseRequestedSchema } from './telemetry-tab-event-schemas' import { sshHostServerConversionSchema, sshHostServerDecidedSchema, @@ -197,7 +196,6 @@ export const eventSchemas = { setup_guide_closed: setupGuideClosedSchema, setup_guide_step_completed: setupGuideStepCompletedSchema, terminal_pane_split: terminalPaneSplitSchema, - tabs_directional_close_requested: tabsDirectionalCloseRequestedSchema, editor_external_change_conflict_shown: editorExternalChangeConflictShownSchema, editor_external_change_conflict_action: editorExternalChangeConflictActionSchema, diff --git a/src/shared/telemetry-tab-event-schemas.test.ts b/src/shared/telemetry-tab-event-schemas.test.ts deleted file mode 100644 index 71c6dc781df..00000000000 --- a/src/shared/telemetry-tab-event-schemas.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { eventSchemas } from './telemetry-events' - -const valid = { - direction: 'left', - tab_type: 'terminal', - surface: 'workspace', - eligible_tab_count: 2 -} - -describe('tabs_directional_close_requested schema', () => { - it('accepts requests, including empty selections', () => { - for (const direction of ['left', 'right']) { - for (const surface of ['workspace', 'floating']) { - for (const eligible_tab_count of [0, 2]) { - const props = { ...valid, direction, surface, eligible_tab_count } - expect(eventSchemas.tabs_directional_close_requested.parse(props)).toEqual(props) - } - } - } - }) - - it.each([ - { direction: 'others' }, - { tab_type: 'private title' }, - { surface: 'unknown' }, - { eligible_tab_count: -1 }, - { eligible_tab_count: 1.5 }, - { eligible_tab_count: '2' }, - { sample_rate: 1 }, - { tab_id: '/private/file.ts' }, - { title: 'private title' }, - { worktree_id: 'private workspace' } - ])('rejects invalid values and private fields: %j', (extra) => { - expect( - eventSchemas.tabs_directional_close_requested.safeParse({ ...valid, ...extra }).success - ).toBe(false) - }) -}) diff --git a/src/shared/telemetry-tab-event-schemas.ts b/src/shared/telemetry-tab-event-schemas.ts deleted file mode 100644 index 975a45c7bb6..00000000000 --- a/src/shared/telemetry-tab-event-schemas.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod' - -export const TAB_CLOSE_TELEMETRY_SAMPLE_RATE = 0.1 - -// First request per direction per app session, including empty selections and cancelled save prompts. -// Properties describe that first request; event counts measure session adoption, not click frequency. -export const tabsDirectionalCloseRequestedSchema = z - .object({ - direction: z.enum(['left', 'right']), - tab_type: z.enum(['terminal', 'editor', 'agent-session', 'browser', 'simulator']), - surface: z.enum(['workspace', 'floating']), - eligible_tab_count: z.number().int().min(0), - sample_rate: z.literal(TAB_CLOSE_TELEMETRY_SAMPLE_RATE).optional() - }) - .strict()