From de0cede5da7eb40ce5c53974f5f15632a3440b62 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 08:19:32 -0700 Subject: [PATCH 1/4] test(cmux-next): internal page tabs draw their page's registry icon (red) Co-Authored-By: Claude Opus 5.5 --- .../InternalPageTabIconTests.swift | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift new file mode 100644 index 000000000000..c2c792b394d5 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift @@ -0,0 +1,30 @@ +import AppKit +import CmuxNextIcons +import CmuxNextTabs +import Testing +@testable import CmuxNextApp + +/// Internal page tabs (Settings, History, Diff, ...) draw their page's cmux +/// icon in the tab strip; a page with no registry icon (a third-party app's +/// pane) keeps its SF Symbol. +@MainActor @Suite struct InternalPageTabIconTests { + final class Provider: InternalPageProvider { + let icon: IconName? + init(icon: IconName?) { self.icon = icon } + var page: InternalPageID { InternalPageID(rawValue: "icon-test") } + var title: String { "Test" } + var symbol: String { "square" } + func makeView(for key: String, in window: WindowController?) -> NSView { NSView() } + } + + @Test func aPageWithARegistryIconDrawsIt() { + #expect(InternalPageTabStore.tabIcon(Provider(icon: .settings)) == .icon(.settings)) + #expect(InternalPageTabStore.tabIcon(Provider(icon: nil)) == .symbol("square")) + #expect(InternalPageTabStore.tabIcon(nil) == .icon(.placeholder)) + } + + @Test func filePagesNameTheirKind() { + #expect(FilePageKind.markdown.icon == .fileText) + #expect(FilePageKind.editor.icon == .code) + } +} From 27344a90070aedb6e7ea35915328bb152bbd99cc Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 08:20:25 -0700 Subject: [PATCH 2/4] fix(cmux-next): internal page tabs draw their page's registry icon Settings, App Store, History, Bookmarks, Feed, Keyboard Shortcuts, Passwords, Diff, Markdown and editor pages, CodeRouter and the changelog drew stock SF Symbols in the tab strip at text size. Each page now names its cmux icon (InternalPageProvider.icon), drawn filling the icon box like terminal and browser tabs; a third-party app's pane keeps its symbol. Co-Authored-By: Claude Opus 5.5 --- .../Sources/CmuxNextApp/Apps/AppPanePage.swift | 3 +++ .../Sources/CmuxNextApp/Apps/AppsService.swift | 2 ++ .../CmuxNextApp/Apps/CodeRouterPageTab.swift | 2 ++ .../Sources/CmuxNextApp/Feed/FeedPageService.swift | 2 ++ .../Focus/Keybindings/KeybindingsPage.swift | 2 ++ .../CmuxNextApp/InternalPages/InternalPage.swift | 6 +++++- .../InternalPages/InternalPageTabStore.swift | 13 ++++++++++--- .../CmuxNextApp/Pages/ChangelogPageTab.swift | 2 ++ .../CmuxNextApp/Pages/Diff/DiffPageService.swift | 2 ++ .../CmuxNextApp/Pages/Files/FilePageKind.swift | 8 ++++++++ .../CmuxNextApp/Pages/Files/FilePageService.swift | 2 ++ .../CmuxNextApp/Passwords/PasswordsPage.swift | 2 ++ .../Settings/SettingsWindowService.swift | 2 ++ .../CmuxNextApp/TopPages/BrowserAppTopPages.swift | 3 +++ 14 files changed, 47 insertions(+), 4 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppPanePage.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppPanePage.swift index 67fbccd62014..e5e2a5ed3705 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppPanePage.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppPanePage.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextApps +import CmuxNextIcons /// An app's own page (its pane implementation, `contributes.paneKinds` in /// manifest v1) as an internal page tab: page id `app:`, one tab per @@ -24,6 +25,8 @@ final class AppPanePage: InternalPageProvider { if case .symbol(let name)? = app?.manifest.icon { return name } return pane?.symbol ?? "app" } + /// The registry's generic app for an app with no symbol of its own. + var icon: IconName? { symbol == "app" ? .appGeneric : nil } private var app: InstalledApp? { apps.registry.app(appID) } /// The app's first pane implementation. diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift index bebc399b6be1..340659896567 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/AppsService.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextApps import CmuxNextControl import CmuxNextDaemon +import CmuxNextIcons import CmuxNextPages import Foundation import Synchronization @@ -196,6 +197,7 @@ extension AppsService: InternalPageProvider { var page: InternalPageID { .appStore } var title: String { AppStoreModel.title } var symbol: String { "bag" } + var icon: IconName? { .store } func makeView(for key: String, in window: WindowController?) -> NSView { if let page = PageFactory(services: services).appsWebPage(route: Self.storeRoute(appID: nil, installed: false)) { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/CodeRouterPageTab.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/CodeRouterPageTab.swift index 16f838fe5cdc..be366804f5fb 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/CodeRouterPageTab.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Apps/CodeRouterPageTab.swift @@ -1,4 +1,5 @@ import AppKit +import CmuxNextIcons import CmuxNextPages extension InternalPageID { @@ -21,6 +22,7 @@ final class CodeRouterPageTab: InternalPageProvider { var page: InternalPageID { .coderouter } var title: String { CodeRouterPageStrings.title } var symbol: String { "arrow.triangle.branch" } + var icon: IconName? { .accountRouted } func makeView(for key: String, in window: WindowController?) -> NSView { guard let services, let view = PageFactory(services: services).coderouterWebPage() else { return NSView() } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Feed/FeedPageService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Feed/FeedPageService.swift index f2f6d5c773d0..919ee7a0bcbc 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Feed/FeedPageService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Feed/FeedPageService.swift @@ -1,6 +1,7 @@ import AppKit import CmuxNextActions import CmuxNextFeed +import CmuxNextIcons extension InternalPageID { /// The wide inbox view of the one feed model. @@ -31,6 +32,7 @@ final class FeedPageService: InternalPageProvider { var page: InternalPageID { .inbox } var title: String { FeedHostView.paneTitle } var symbol: String { "tray" } + var icon: IconName? { .inbox } func makeView(for key: String, in window: WindowController?) -> NSView { guard let feed = services.feed else { return NSView() } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/Keybindings/KeybindingsPage.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/Keybindings/KeybindingsPage.swift index 95b872085850..c42484d727c2 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/Keybindings/KeybindingsPage.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/Keybindings/KeybindingsPage.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextActions +import CmuxNextIcons import CmuxNextPages import CmuxNextSettings @@ -35,6 +36,7 @@ final class KeybindingsPageService: InternalPageProvider { var page: InternalPageID { .keybindings } var title: String { KeybindingStrings.pageTitle } var symbol: String { "keyboard" } + var icon: IconName? { .keyboard } func makeView(for key: String, in window: WindowController?) -> NSView { let provider = KeybindingsPageProvider(services: services) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPage.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPage.swift index 3c09c120c7a9..e47a5446aeb4 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPage.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPage.swift @@ -1,4 +1,5 @@ import AppKit +import CmuxNextIcons /// Internal pages: cmux's own surfaces (Settings, Debug Settings, the App /// Store) shown as tabs in a pane instead of windows of their own. @@ -45,8 +46,10 @@ protocol InternalPageProvider: AnyObject { var page: InternalPageID { get } /// The tab title (localized). var title: String { get } - /// SF Symbol of the tab. + /// SF Symbol of the tab, drawn when `icon` is nil. var symbol: String { get } + /// The tab's cmux icon. Default: nil. + var icon: IconName? { get } /// A new view for the tab `key` (called once per tab, on first show). /// `window` is the main window the tab opens in, for its theme scope. func makeView(for key: String, in window: WindowController?) -> NSView @@ -58,6 +61,7 @@ protocol InternalPageProvider: AnyObject { } extension InternalPageProvider { + var icon: IconName? { nil } func tabClosed(_ key: String) {} func title(for key: String) -> String { title } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPageTabStore.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPageTabStore.swift index 92b6c534ff15..679fe86c78fa 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPageTabStore.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/InternalPages/InternalPageTabStore.swift @@ -1,7 +1,8 @@ import AppKit -import CmuxNextDaemon import CmuxNextActions import CmuxNextBridge +import CmuxNextDaemon +import CmuxNextIcons import CmuxNextTabs /// Which internal page tabs each pane lists, and their views @@ -98,7 +99,13 @@ final class InternalPageTabStore { func stripItem(_ key: String) -> StripTabItem { let provider = LocalPageTab.page(of: key).flatMap { providers[$0] } return StripTabItem(id: StripTabID(key), title: provider?.title(for: key) ?? "", subtitle: nil, - icon: .symbol(provider?.symbol ?? "square.dashed"), isBusy: false) + icon: Self.tabIcon(provider), isBusy: false) + } + + /// A page tab's strip icon: the page's cmux icon, else its SF Symbol. + static func tabIcon(_ provider: (any InternalPageProvider)?) -> TabIcon { + guard let provider else { return .icon(.placeholder) } + return provider.icon.map(TabIcon.icon) ?? .symbol(provider.symbol) } /// The tab's view, made on first show. @@ -124,7 +131,7 @@ final class InternalPageTabStore { /// A store page tab's strip title and icon, from the page it shows. func storeTabItem(_ tab: TabModel) -> (title: String, icon: TabIcon)? { guard let page = tab.page.map(InternalPageID.init(rawValue:)), let provider = providers[page] else { return nil } - return (provider.title(for: storeKeys[tab.id] ?? ""), .symbol(provider.symbol)) + return (provider.title(for: storeKeys[tab.id] ?? ""), Self.tabIcon(provider)) } /// The view of store page tab `tab` (its `page` source), made on first diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/ChangelogPageTab.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/ChangelogPageTab.swift index d8882ca93c32..93cfc39f89a5 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/ChangelogPageTab.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/ChangelogPageTab.swift @@ -1,4 +1,5 @@ import AppKit +import CmuxNextIcons import CmuxNextPages import CmuxNextUpdater @@ -20,6 +21,7 @@ final class ChangelogPageTab: InternalPageProvider { var page: InternalPageID { .changelog } var title: String { ChangelogPageStrings.title } var symbol: String { "sparkles" } + var icon: IconName? { .fileText } func makeView(for key: String, in window: WindowController?) -> NSView { guard let services, let view = PageFactory(services: services).changelogPage() else { return NSView() } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Diff/DiffPageService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Diff/DiffPageService.swift index 15ae76f22b43..dba7124ba48d 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Diff/DiffPageService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Diff/DiffPageService.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextBridge +import CmuxNextIcons import CmuxNextPages import CmuxNextSettings @@ -61,6 +62,7 @@ final class DiffPageService: InternalPageProvider { var page: InternalPageID { .diff } var title: String { DiffPageStrings.tabTitle } var symbol: String { "plusminus" } + var icon: IconName? { .diffFile } var recents: DiffRecents { recentsStore } func title(for key: String) -> String { tabs[key]?.repository?.name ?? title } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageKind.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageKind.swift index b8517b101897..0d4f32763084 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageKind.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageKind.swift @@ -1,4 +1,5 @@ import CmuxNextDesign +import CmuxNextIcons import CmuxNextPages import Foundation @@ -60,6 +61,13 @@ nonisolated enum FilePageKind: String, Sendable, CaseIterable { } } + var icon: IconName { + switch self { + case .markdown: .fileText + case .editor: .code + } + } + var title: String { switch self { case .markdown: FilePageStrings.markdownTitle diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageService.swift index 5417c6e0ae86..d2eadc8830e4 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/Files/FilePageService.swift @@ -1,6 +1,7 @@ import AppKit import CmuxNextBridge import CmuxNextDesign +import CmuxNextIcons import CmuxNextPages import CmuxNextSettings import Foundation @@ -59,6 +60,7 @@ final class FilePageService: InternalPageProvider { var page: InternalPageID { kind.page } var title: String { kind.title } var symbol: String { kind.symbol } + var icon: IconName? { kind.icon } func title(for key: String) -> String { tabs[key]?.file?.lastPathComponent ?? title } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Passwords/PasswordsPage.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Passwords/PasswordsPage.swift index 05b5b5afa82f..86b97888adb1 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Passwords/PasswordsPage.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Passwords/PasswordsPage.swift @@ -1,5 +1,6 @@ import AppKit import CmuxNextActions +import CmuxNextIcons import CmuxNextPages import Foundation @@ -46,6 +47,7 @@ final class PasswordsPageService: InternalPageProvider { var page: InternalPageID { .passwords } var title: String { PasswordStrings.pageTitle } var symbol: String { "key" } + var icon: IconName? { .securityLock } func makeView(for key: String, in window: WindowController?) -> NSView { let provider = PasswordsPageProvider( diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Settings/SettingsWindowService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Settings/SettingsWindowService.swift index 942d666185e8..ef0c5a51d6b8 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Settings/SettingsWindowService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Settings/SettingsWindowService.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextActions import CmuxNextDaemon import CmuxNextDesign +import CmuxNextIcons import CmuxNextPages import CmuxNextSettings import CmuxNextSettingsWindow @@ -96,6 +97,7 @@ final class SettingsWindowService: InternalPageProvider { var page: InternalPageID { .settings } var title: String { SettingsDeepLink.pageTitle } var symbol: String { "gearshape" } + var icon: IconName? { .settings } /// The kept page when no other tab shows it, else a new one (a second window's tab). func makeView(for key: String, in window: WindowController?) -> NSView { diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/TopPages/BrowserAppTopPages.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/TopPages/BrowserAppTopPages.swift index 062e7e320a3f..8eaffed0441a 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/TopPages/BrowserAppTopPages.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/TopPages/BrowserAppTopPages.swift @@ -2,6 +2,7 @@ import AppKit import CmuxNextBookmarks import CmuxNextBrowser import CmuxNextDaemon +import CmuxNextIcons extension InternalPageID { /// History as a top page (TOP-SECTION-ITEMS-ARE-PAGES Q3). @@ -23,6 +24,7 @@ final class HistoryTopPage: InternalPageProvider { var page: InternalPageID { .history } var title: String { HistoryPageStrings.title } var symbol: String { "clock" } + var icon: IconName? { .history } func makeView(for key: String, in window: WindowController?) -> NSView { guard let services else { return NSView() } @@ -52,6 +54,7 @@ final class BookmarksTopPage: InternalPageProvider { var page: InternalPageID { .bookmarks } var title: String { BookmarkStrings.pageTitle } var symbol: String { "book" } + var icon: IconName? { .bookmarkManager } func makeView(for key: String, in window: WindowController?) -> NSView { guard let services else { return NSView() } From 2121a1897ad7cdcf78f9f2d73acaf5f46e50a71b Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 09:06:16 -0700 Subject: [PATCH 3/4] test(cmux-next): cmux page addresses in browser tabs wear their page icon (red) Co-Authored-By: Claude Opus 5.5 --- .../InternalPageTabIconTests.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift index c2c792b394d5..85daa34f0cf2 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/InternalPageTabIconTests.swift @@ -27,4 +27,18 @@ import Testing #expect(FilePageKind.markdown.icon == .fileText) #expect(FilePageKind.editor.icon == .code) } + + /// cmux pages shown in a browser tab (History, Bookmarks, Agent activity, + /// a remote view) wear their page's icon, not the globe, and no throbber. + @Test func cmuxAddressesWearTheirPageIcon() { + let page = { (url: String) in BrowserTabIconState.resolve(isLoading: true, isDormant: false, favicon: nil, url: URL(string: url)) } + #expect(page("cmux://history") == .page(.history)) + #expect(page("cmux://bookmarks/") == .page(.bookmarkManager)) + #expect(page("cmux://agent-activity") == .page(.agentActivity)) + #expect(page("cmux://remote-view?host=a") == .page(.machineRemote)) + #expect(page("https://example.com") == .throbber) + var item = TabItem(id: TabID("h"), title: "History") + BrowserTabIconState.page(.history).apply(to: &item) + #expect(item.icon == .icon(.history)) + } } From aa7f244be66a4c681f3c859afcc15fe5ab98a935 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Tue, 6 Oct 2026 09:07:17 -0700 Subject: [PATCH 4/4] fix(cmux-next): cmux page addresses in browser tabs wear their page icon History, Bookmarks, Agent activity and remote-view tabs are browser tabs at cmux:// addresses, so they drew the globe (and the throbber while loading). They now draw their page's registry icon. Co-Authored-By: Claude Opus 5.5 --- .../Favicons/BrowserTabIconState.swift | 28 ++++++++++++++++--- .../Sources/CmuxNextApp/PaneController.swift | 11 ++++---- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Favicons/BrowserTabIconState.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Favicons/BrowserTabIconState.swift index c6f009b5894e..e40c57c48d79 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Favicons/BrowserTabIconState.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Favicons/BrowserTabIconState.swift @@ -1,21 +1,40 @@ +import CmuxNextAgentActivity +import CmuxNextBookmarks +import CmuxNextHistory import CmuxNextIcons +import CmuxNextRemoteView import CmuxNextTabs +import Foundation /// What a browser tab shows where its icon goes (Chromium's `TabIcon` -/// rule): the throbber while the page loads, else the page's favicon, else -/// the browser icon (no favicon yet, none, or its fetch failed). Pure. +/// rule): a cmux page's own icon, the throbber while the page loads, else +/// the page's favicon, else the browser icon (no favicon yet, none, or its +/// fetch failed). Pure. enum BrowserTabIconState: Equatable { case throbber case favicon(TabImage) case globe + /// A cmux page (`cmux://history`, ...) shown in the browser tab. + case page(IconName) /// `isLoading` is the live page's load state; a hibernated tab has no - /// live page, so it shows its favicon and never the throbber. - static func resolve(isLoading: Bool, isDormant: Bool, favicon: TabImage?) -> BrowserTabIconState { + /// live page, so it shows its favicon and never the throbber. `url` is + /// the tab's address: a cmux page's address wears that page's icon. + static func resolve(isLoading: Bool, isDormant: Bool, favicon: TabImage?, url: URL? = nil) -> BrowserTabIconState { + if let page = pageIcon(url) { return .page(page) } if isLoading, !isDormant { return .throbber } return favicon.map(BrowserTabIconState.favicon) ?? .globe } + /// The icon of the cmux page at `url`, nil for any other address. + static func pageIcon(_ url: URL?) -> IconName? { + if HistoryPageAddress.matches(url) { return .history } + if BookmarkPageAddress.matches(url) { return .bookmarkManager } + if AgentActivityPageAddress.matches(url) { return .agentActivity } + if url?.scheme?.lowercased() == RemoteViewTabRecord.scheme, url?.host()?.lowercased() == RemoteViewTabRecord.urlHost { return .machineRemote } + return nil + } + /// Applies this state to a strip item (the throbber is the strip's busy /// spinner in place of the icon; the browser icon is the item's default). func apply(to item: inout TabItem) { @@ -23,6 +42,7 @@ enum BrowserTabIconState: Equatable { case .throbber: item.isBusy = true case .favicon(let image): item.icon = .image(image) case .globe: item.icon = .icon(.browser) + case .page(let name): item.icon = .icon(name) } } } diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController.swift index c224b638a221..2eb02435be97 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/PaneController.swift @@ -142,7 +142,7 @@ final class PaneController: SurfacePresenter, PresentablePane { } else { item.profileBadge = services.browserProfiles.tabBadge(for: tab, workspaceID: workspaceID) } - browserIcon(key: tab.id, recordFavicon: incognito ? nil : tab.faviconURL).apply(to: &item) + browserIcon(key: tab.id, recordFavicon: incognito ? nil : tab.faviconURL, recordURL: tab.url).apply(to: &item) } return item } @@ -167,14 +167,15 @@ final class PaneController: SurfacePresenter, PresentablePane { generation: store.generation?.rawValue, surfaces: pane.tabs.map(\.surface.rawValue)) } - /// The favicon, throbber or globe of browser tab `key`: its live page's - /// load state and favicon, else the favicon its record names. - private func browserIcon(key: String, recordFavicon: String?) -> BrowserTabIconState { + /// The page icon, favicon, throbber or globe of browser tab `key`: its live page's + /// address, load state and favicon, else the address and favicon its record names. + private func browserIcon(key: String, recordFavicon: String?, recordURL: String? = nil) -> BrowserTabIconState { _ = services.cache.pageInstalls.revision let page = services.cache.existingBrowser(key)?.tab.state let address = page.map { $0.faviconURL?.absoluteString } ?? recordFavicon let image = services.favicons.image(for: address, profile: services.browserProfiles.engineProfile(forTab: key)) - return .resolve(isLoading: page?.isLoading ?? false, isDormant: services.cache.dormantTabs.contains(key), favicon: image) + let url = page?.url ?? recordURL.flatMap(URL.init(string:)) + return .resolve(isLoading: page?.isLoading ?? false, isDormant: services.cache.dormantTabs.contains(key), favicon: image, url: url) } /// Pushes daemon truth into the strip. `force` resets optimistic strip