Marketplace UI plumbing: wrappers, singleton tab, settings section, view shell

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 08:48:06 -07:00
co-authored by Claude Opus 5.5
parent d419d0a6b4
commit d23d0a44c5
21 changed files with 964 additions and 9 deletions
@@ -0,0 +1,42 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import MarketplaceSettings from "./MarketplaceSettings";
import { useAppState, MARKETPLACE_TAB_KEY } from "../../store/appState";
import type { AppSettings } from "../../lib/types";
const listMarketplaceUpdates = vi.fn();
vi.mock("../../lib/tauri-commands", () => ({
listMarketplaceUpdates: () => listMarketplaceUpdates(),
}));
describe("MarketplaceSettings", () => {
beforeEach(() => {
vi.clearAllMocks();
useAppState.setState({
tabOrder: [],
activeTabKey: null,
appSettings: {
marketplaces: [{ id: "m1", name: "Starter", url: "https://x/y.git", branch: null, account_id: null }],
global_marketplace_installs: [
{ marketplace_id: "m1", kind: "agent", key: "a", commit: "a".repeat(40) },
{ marketplace_id: "m1", kind: "hook", key: "h", commit: "a".repeat(40) },
],
marketplace_accounts: [],
} as unknown as AppSettings,
});
listMarketplaceUpdates.mockResolvedValue([
{ item: { marketplace_id: "m1", kind: "agent", key: "a" }, pinned: "a".repeat(40), head: "b".repeat(40) },
]);
});
it("summarises and opens the Marketplace tab", async () => {
render(<MarketplaceSettings />);
expect(screen.getByTestId("marketplace-summary")).toHaveTextContent("1 marketplace");
expect(screen.getByTestId("marketplace-summary")).toHaveTextContent("2 installed for all projects");
await waitFor(() =>
expect(screen.getByTestId("marketplace-summary")).toHaveTextContent("1 update available"),
);
fireEvent.click(screen.getByRole("button", { name: "Open Marketplace" }));
expect(useAppState.getState().activeTabKey).toBe(MARKETPLACE_TAB_KEY);
});
});
@@ -0,0 +1,48 @@
import { useEffect, useState } from "react";
import { useAppState } from "../../store/appState";
import { listMarketplaceUpdates } from "../../lib/tauri-commands";
import Button from "../ui/Button";
const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`;
export default function MarketplaceSettings() {
const appSettings = useAppState((s) => s.appSettings);
const openMarketplace = useAppState((s) => s.openMarketplace);
const [updateCount, setUpdateCount] = useState<number | null>(null);
useEffect(() => {
let cancelled = false;
listMarketplaceUpdates()
.then((u) => {
if (!cancelled) setUpdateCount(u.length);
})
.catch(() => {
if (!cancelled) setUpdateCount(null);
});
return () => {
cancelled = true;
};
}, [appSettings?.marketplaces.length]);
const marketplaces = appSettings?.marketplaces.length ?? 0;
const globalInstalls = appSettings?.global_marketplace_installs.length ?? 0;
return (
<div className="space-y-2">
<p data-testid="marketplace-summary" className="text-xs text-[var(--text-secondary)] leading-snug">
{plural(marketplaces, "marketplace", "marketplaces")} ·{" "}
{globalInstalls} installed for all projects
{updateCount !== null && updateCount > 0 && (
<> · {plural(updateCount, "update available", "updates available")}</>
)}
</p>
<p className="text-xs text-[var(--text-secondary)] leading-snug">
Agents, skills, commands, hooks and plugins from git repositories, installed for all
projects or per project. Changes apply to new Claude sessions.
</p>
<Button size="md" variant="secondary" onClick={() => openMarketplace()}>
Open Marketplace
</Button>
</div>
);
}
@@ -20,6 +20,7 @@ import { resolveTerminalGpuRendering } from "../../lib/terminalRenderer";
import WebTerminalSettings from "./WebTerminalSettings";
import SttSettings from "./SttSettings";
import SharedAuthSettings from "./SharedAuthSettings";
import MarketplaceSettings from "./MarketplaceSettings";
import CertificateSettings from "./CertificateSettings";
import ExportSettingsModal from "./ExportSettingsModal";
import ImportSettingsModal from "./ImportSettingsModal";
@@ -171,6 +172,10 @@ export default function SettingsPanel() {
<SharedAuthSettings />
</AccordionSection>
<AccordionSection id="marketplace" title="Marketplace" defaultOpen={false}>
<MarketplaceSettings />
</AccordionSection>
<AccordionSection id="backends" title="Backends" defaultOpen={false}>
<AwsSettings />
<div className="pt-3 border-t border-[var(--border-color)]" />