From 9c705659a07f4adb2a665a02855e0591541acc36 Mon Sep 17 00:00:00 2001 From: brsbl <57682038+brsbl@users.noreply.github.com> Date: Fri, 9 Oct 2026 05:16:36 +0000 Subject: [PATCH 1/2] Show plugin display names in file opener menus --- .../plugin/ExperimentalFileLinkMenu.tsx | 4 ++- .../FileOpenersSettingsSection.test.tsx | 32 +++++++++++++++---- .../settings/FileOpenersSettingsSection.tsx | 6 ++-- apps/app/src/lib/plugin-logos.ts | 9 +++++- .../views/thread-detail/ThreadDetailView.tsx | 5 ++- 5 files changed, 45 insertions(+), 11 deletions(-) diff --git a/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx b/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx index 0a1546ccb98..6fa8133aed0 100644 --- a/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx +++ b/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx @@ -15,6 +15,7 @@ import { getExperimentalFileLocationStart, } from "@/lib/live-file-navigation"; import { usePluginSlots } from "@/lib/plugin-slots"; +import { usePluginDisplayNames } from "@/lib/plugin-logos"; function getFileExtension(path: string): string | null { const name = getFileBasename(path); @@ -38,6 +39,7 @@ export function ExperimentalFileLinkMenu({ : {}), }); const { fileOpeners } = usePluginSlots(); + const getPluginName = usePluginDisplayNames(); const extension = getFileExtension(intent.target.path); const matchingOpeners = extension === null @@ -74,7 +76,7 @@ export function ExperimentalFileLinkMenu({ }) } > - {opener.title} + {getPluginName(opener.pluginId)} ({opener.title}) ))} diff --git a/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx b/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx index 86521c02d71..4e3b07c9ef3 100644 --- a/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx +++ b/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx @@ -9,15 +9,34 @@ import { import { BUILT_IN_FILE_OPENER_PREFERENCE } from "@/lib/file-opener-preference"; import { FileOpenersSettingsSection } from "./FileOpenersSettingsSection"; import { makePluginRegistrationSet } from "@/test/fixtures/plugins"; +import { + resetPluginLogoStoreForTest, + setPluginLogoUrls, +} from "@/lib/plugin-logos"; function registerNotesOpener() { + setPluginLogoUrls( + new Map([ + [ + "simple-notes", + { + displayName: "Docs", + icon: null, + compactIconUrl: null, + logoUrl: null, + logoDarkUrl: null, + icons: new Map(), + }, + ], + ]), + ); setPluginSlotRegistrations( - "notes", + "simple-notes", makePluginRegistrationSet({ fileOpeners: [ { - id: "editor", - title: "Notes editor", + id: "docs", + title: "Markdown", extensions: ["md", "mdx"], component: () => null, }, @@ -30,6 +49,7 @@ afterEach(() => { cleanup(); window.localStorage.clear(); resetPluginSlotStoreForTest(); + resetPluginLogoStoreForTest(); }); describe("FileOpenersSettingsSection", () => { @@ -42,15 +62,15 @@ describe("FileOpenersSettingsSection", () => { const trigger = screen.getByRole("button", { name: "Default opener for .md files", }); - expect(trigger.textContent).toContain("Automatic"); + expect(trigger.textContent).toContain("Automatic (Docs)"); await selectOption(trigger, /Built-in preview/u); expect(storedPreference()).toEqual({ md: BUILT_IN_FILE_OPENER_PREFERENCE, }); - await selectOption(trigger, /Notes editor \(notes\)/u); - expect(storedPreference()).toEqual({ md: "notes:editor" }); + await selectOption(trigger, /Docs \(Markdown\)/u); + expect(storedPreference()).toEqual({ md: "simple-notes:docs" }); await selectOption(trigger, /Automatic/u); expect(storedPreference()).toEqual({}); diff --git a/apps/app/src/components/settings/FileOpenersSettingsSection.tsx b/apps/app/src/components/settings/FileOpenersSettingsSection.tsx index 51369e18e28..2d63a1f3177 100644 --- a/apps/app/src/components/settings/FileOpenersSettingsSection.tsx +++ b/apps/app/src/components/settings/FileOpenersSettingsSection.tsx @@ -18,6 +18,7 @@ import { useFileOpenerPreference, } from "@/lib/file-opener-preference"; import { usePluginSlots, type PluginFileOpenerSlot } from "@/lib/plugin-slots"; +import { usePluginDisplayNames } from "@/lib/plugin-logos"; import { cn } from "@bb/shared-ui/lib/utils"; import { SETTINGS_DROPDOWN_CONTENT_CLASS, @@ -84,18 +85,19 @@ function ExtensionOpenerControl({ openers: PluginFileOpenerSlot[]; preference: string; }) { + const getPluginName = usePluginDisplayNames(); const automaticOpener = openers[0]; if (automaticOpener === undefined) return null; const options = [ { key: AUTOMATIC_FILE_OPENER_PREFERENCE, - label: `Automatic (${automaticOpener.title})`, + label: `Automatic (${getPluginName(automaticOpener.pluginId)})`, }, { key: BUILT_IN_FILE_OPENER_PREFERENCE, label: BUILTIN_LABEL }, ...openers.map((opener) => ({ key: buildFileOpenerRef(opener), - label: `${opener.title} (${opener.pluginId})`, + label: `${getPluginName(opener.pluginId)} (${opener.title})`, })), ]; const selected = diff --git a/apps/app/src/lib/plugin-logos.ts b/apps/app/src/lib/plugin-logos.ts index 9ead7a00cb4..915a45f89fe 100644 --- a/apps/app/src/lib/plugin-logos.ts +++ b/apps/app/src/lib/plugin-logos.ts @@ -50,12 +50,19 @@ export function usePluginCompactBranding( } export function usePluginDisplayName(pluginId: string): string { + return usePluginDisplayNames()(pluginId); +} + +export function usePluginDisplayNames(): (pluginId: string) => string { const entries = useSyncExternalStore( subscribePluginLogos, getPluginLogoUrls, getPluginLogoUrls, ); - return entries.get(pluginId)?.displayName ?? pluginId; + return useCallback( + (pluginId: string) => entries.get(pluginId)?.displayName ?? pluginId, + [entries], + ); } export function usePluginIconUrl( diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx index 01bdb9bd97e..ec7b817d956 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx @@ -202,6 +202,7 @@ import { createFileOpenerOriginalTab } from "@/components/plugin/file-opener-tab import { PluginThreadPanelNavigationProvider } from "@/components/plugin/plugin-thread-panel-navigation"; import { ThreadTimelineNavigationProvider } from "@/components/thread/timeline/ThreadTimelineNavigationContext"; import { usePluginSlots } from "@/lib/plugin-slots"; +import { usePluginDisplayNames } from "@/lib/plugin-logos"; import { getFileExtension } from "@/lib/plugin-slot-resolvers"; import { Icon } from "@bb/shared-ui/icon"; import { getBbDesktopInfo, isDesktopBrowserAvailable } from "@/lib/bb-desktop"; @@ -681,6 +682,7 @@ function ThreadDetailViewInternal( fileOpeners: pluginFileOpeners, threadPanelActions: pluginThreadPanelActions, } = usePluginSlots(); + const getPluginName = usePluginDisplayNames(); useThreadOpenFileSignal({ threadId, environmentId: thread?.environmentId, @@ -2080,7 +2082,7 @@ function ThreadDetailViewInternal( }, ...matching.map((opener) => ({ id: `${opener.pluginId}:${opener.id}`, - label: `Open with ${opener.title}`, + label: `Open with ${getPluginName(opener.pluginId)} (${opener.title})`, onSelect: () => { handleOpenTimelineLocalFileLink(link, { viewer: { pluginId: opener.pluginId, openerId: opener.id }, @@ -2121,6 +2123,7 @@ function ThreadDetailViewInternal( handleOpenTimelineLocalFileLink, openPathInFileTarget, pluginFileOpeners, + getPluginName, ], ); const handleOpenFilePreview = useCallback( From 74ba862d223e9409a0b97560911a06d9649a30a8 Mon Sep 17 00:00:00 2001 From: brsbl <57682038+brsbl@users.noreply.github.com> Date: Mon, 19 Oct 2026 22:51:32 +0000 Subject: [PATCH 2/2] Show the plugin name once when a file opener's title repeats it Co-Authored-By: Claude Opus 5.5 --- .../plugin/ExperimentalFileLinkMenu.tsx | 10 ++++- .../FileOpenersSettingsSection.test.tsx | 44 +++++++++++++++++++ .../settings/FileOpenersSettingsSection.tsx | 10 ++++- apps/app/src/lib/plugin-logos.ts | 9 ++++ .../views/thread-detail/ThreadDetailView.tsx | 7 ++- 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx b/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx index 6fa8133aed0..794e163ee2e 100644 --- a/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx +++ b/apps/app/src/components/plugin/ExperimentalFileLinkMenu.tsx @@ -15,7 +15,10 @@ import { getExperimentalFileLocationStart, } from "@/lib/live-file-navigation"; import { usePluginSlots } from "@/lib/plugin-slots"; -import { usePluginDisplayNames } from "@/lib/plugin-logos"; +import { + formatFileOpenerLabel, + usePluginDisplayNames, +} from "@/lib/plugin-logos"; function getFileExtension(path: string): string | null { const name = getFileBasename(path); @@ -76,7 +79,10 @@ export function ExperimentalFileLinkMenu({ }) } > - {getPluginName(opener.pluginId)} ({opener.title}) + {formatFileOpenerLabel( + getPluginName(opener.pluginId), + opener.title, + )} ))} diff --git a/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx b/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx index 4e3b07c9ef3..76a888ea82b 100644 --- a/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx +++ b/apps/app/src/components/settings/FileOpenersSettingsSection.test.tsx @@ -75,6 +75,50 @@ describe("FileOpenersSettingsSection", () => { await selectOption(trigger, /Automatic/u); expect(storedPreference()).toEqual({}); }); + + it("shows the plugin name once when the opener title repeats it", async () => { + setPluginLogoUrls( + new Map([ + [ + "monaco-editor", + { + displayName: "File Editor", + icon: null, + compactIconUrl: null, + logoUrl: null, + logoDarkUrl: null, + icons: new Map(), + }, + ], + ]), + ); + setPluginSlotRegistrations( + "monaco-editor", + makePluginRegistrationSet({ + fileOpeners: [ + { + id: "editor", + title: "File Editor", + extensions: ["ts"], + component: () => null, + }, + ], + }), + ); + render(); + + const trigger = screen.getByRole("button", { + name: "Default opener for .ts files", + }); + fireEvent.pointerDown(trigger, { button: 0 }); + const items = await screen.findAllByRole("menuitem"); + expect(items.map((item) => item.textContent)).toContain("File Editor"); + expect( + items.some((item) => + item.textContent?.includes("File Editor (File Editor)"), + ), + ).toBe(false); + }); }); async function selectOption(trigger: HTMLElement, name: RegExp) { diff --git a/apps/app/src/components/settings/FileOpenersSettingsSection.tsx b/apps/app/src/components/settings/FileOpenersSettingsSection.tsx index 2d63a1f3177..007d034c97c 100644 --- a/apps/app/src/components/settings/FileOpenersSettingsSection.tsx +++ b/apps/app/src/components/settings/FileOpenersSettingsSection.tsx @@ -18,7 +18,10 @@ import { useFileOpenerPreference, } from "@/lib/file-opener-preference"; import { usePluginSlots, type PluginFileOpenerSlot } from "@/lib/plugin-slots"; -import { usePluginDisplayNames } from "@/lib/plugin-logos"; +import { + formatFileOpenerLabel, + usePluginDisplayNames, +} from "@/lib/plugin-logos"; import { cn } from "@bb/shared-ui/lib/utils"; import { SETTINGS_DROPDOWN_CONTENT_CLASS, @@ -97,7 +100,10 @@ function ExtensionOpenerControl({ { key: BUILT_IN_FILE_OPENER_PREFERENCE, label: BUILTIN_LABEL }, ...openers.map((opener) => ({ key: buildFileOpenerRef(opener), - label: `${getPluginName(opener.pluginId)} (${opener.title})`, + label: formatFileOpenerLabel( + getPluginName(opener.pluginId), + opener.title, + ), })), ]; const selected = diff --git a/apps/app/src/lib/plugin-logos.ts b/apps/app/src/lib/plugin-logos.ts index 915a45f89fe..7f71a459afb 100644 --- a/apps/app/src/lib/plugin-logos.ts +++ b/apps/app/src/lib/plugin-logos.ts @@ -65,6 +65,15 @@ export function usePluginDisplayNames(): (pluginId: string) => string { ); } +export function formatFileOpenerLabel( + pluginName: string, + openerTitle: string, +): string { + return pluginName.trim().toLowerCase() === openerTitle.trim().toLowerCase() + ? pluginName + : `${pluginName} (${openerTitle})`; +} + export function usePluginIconUrl( glyph: string | undefined, ): string | undefined { diff --git a/apps/app/src/views/thread-detail/ThreadDetailView.tsx b/apps/app/src/views/thread-detail/ThreadDetailView.tsx index cdf3b4436c4..64082c06ba6 100644 --- a/apps/app/src/views/thread-detail/ThreadDetailView.tsx +++ b/apps/app/src/views/thread-detail/ThreadDetailView.tsx @@ -210,7 +210,10 @@ import { type OpenAttachmentRequest, } from "@/components/secondary-panel/AttachmentOpenerContext"; import { usePluginSlots } from "@/lib/plugin-slots"; -import { usePluginDisplayNames } from "@/lib/plugin-logos"; +import { + formatFileOpenerLabel, + usePluginDisplayNames, +} from "@/lib/plugin-logos"; import { getFileExtension } from "@/lib/plugin-slot-resolvers"; import { Icon } from "@bb/shared-ui/icon"; import { getBbDesktopInfo, isDesktopBrowserAvailable } from "@/lib/bb-desktop"; @@ -2136,7 +2139,7 @@ function ThreadDetailViewInternal( }, ...matching.map((opener) => ({ id: `${opener.pluginId}:${opener.id}`, - label: `Open with ${getPluginName(opener.pluginId)} (${opener.title})`, + label: `Open with ${formatFileOpenerLabel(getPluginName(opener.pluginId), opener.title)}`, onSelect: () => { handleOpenTimelineLocalFileLink(link, { viewer: { pluginId: opener.pluginId, openerId: opener.id },