diff --git a/apps/app/src/components/sidebar/SidebarUpdatesBadge.test.tsx b/apps/app/src/components/sidebar/SidebarUpdatesBadge.test.tsx index 586d9575c..776b06ec3 100644 --- a/apps/app/src/components/sidebar/SidebarUpdatesBadge.test.tsx +++ b/apps/app/src/components/sidebar/SidebarUpdatesBadge.test.tsx @@ -52,6 +52,34 @@ function providerIssue( }; } +function missingInstallIssue( + provider: ProviderCliKey, + displayName: string, +): ProviderCliIssue { + return { + provider, + status: { + displayName, + executableName: provider, + executablePath: null, + installed: false, + installSource: "notInstalled", + currentVersion: null, + latestVersion: "1.1.0", + minimumSupportedVersion: null, + npmPackageName: `@example/${provider}`, + npmGlobalPackageVersion: null, + installAction: null, + needsUpdate: false, + versionUnsupported: false, + }, + action: null, + title: `${displayName} CLI not installed`, + description: `Install ${displayName} so bb can start ${displayName} sessions.`, + fingerprint: `${provider}:missing:1.1.0`, + }; +} + function host(id: string): Host { return { id, @@ -137,6 +165,33 @@ describe("SidebarUpdatesBadge", () => { ).toBe("Claude Code update available"); }); + it("renders no provider chip when a CLI is not installed", () => { + renderBadge({ + machines: [ + machine({ + issues: [missingInstallIssue("claudeCode", "Claude Code")], + }), + ], + }); + + expect(screen.queryByTestId("sidebar-updates-badge-providers")).toBeNull(); + expect(screen.queryByTestId("sidebar-updates-badge-bb")).toBeNull(); + }); + + it("still shows the bb chip when the only provider issue is a missing CLI", () => { + renderBadge({ + appUpdateAvailable: true, + machines: [ + machine({ + issues: [missingInstallIssue("codex", "Codex")], + }), + ], + }); + + expect(screen.getByTestId("sidebar-updates-badge-bb")).toBeTruthy(); + expect(screen.queryByTestId("sidebar-updates-badge-providers")).toBeNull(); + }); + it("renders one mark per provider in a stable order when the same CLI is stale on several machines", () => { renderBadge({ appUpdateAvailable: true, diff --git a/apps/app/src/components/sidebar/SidebarUpdatesBadge.tsx b/apps/app/src/components/sidebar/SidebarUpdatesBadge.tsx index 46c253b75..56f97a42b 100644 --- a/apps/app/src/components/sidebar/SidebarUpdatesBadge.tsx +++ b/apps/app/src/components/sidebar/SidebarUpdatesBadge.tsx @@ -57,6 +57,10 @@ interface StaleProvider { * protocol) and the agent CLIs, which carry their own brand marks so it is * clear which agent is stale without hovering. Both chips open the * consolidated Settings → Updates view. + * + * A CLI that is not installed at all is not an update and gets no chip here: + * there is no installed version to stale against, and the Settings → Updates + * page already surfaces the install prompt for it. */ export function SidebarUpdatesBadge({ onNavigate }: SidebarUpdatesBadgeProps) { const inventory = useUpdateInventory(); @@ -70,9 +74,14 @@ export function SidebarUpdatesBadge({ onNavigate }: SidebarUpdatesBadgeProps) { stuckDaemonCount; // One mark per provider, even when the same CLI is stale on several machines. + // Missing CLIs are install prompts, not updates: skip them so the chip never + // claims an update is available for a CLI that isn't installed. const staleProvidersByKey = new Map(); for (const machine of inventory.machines) { for (const issue of machine.issues) { + if (!issue.status.installed) { + continue; + } if (!staleProvidersByKey.has(issue.provider)) { staleProvidersByKey.set(issue.provider, { provider: issue.provider,