Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions domain/provider-presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,39 @@ export interface ProviderPresentation {
readonly darkMarkPath?: string;
readonly apiKeySetupUrl?: string;
readonly apiKeyGuide?: "elevenlabs" | "newapi";
/** Provider usage page with a stable public URL. Absent when none is known. */
readonly usagePageUrl?: string;
}

export const providerCatalog = {
chatgpt: {
displayName: "ChatGPT",
presentation: {
markPath: "/provider-marks/chatgpt.svg",
usagePageUrl: "https://chatgpt.com/codex/settings/usage",
},
},
claude: {
displayName: "Claude",
presentation: {
markPath: "/provider-marks/claude.svg",
usagePageUrl: "https://claude.ai/settings/usage",
},
},
kimi: {
displayName: "Kimi",
presentation: {
markPath: "/provider-marks/kimi.svg",
darkMarkPath: "/provider-marks/kimi-dark.svg",
usagePageUrl: "https://www.kimi.com/settings/subscription",
},
},
cursor: {
displayName: "Cursor",
presentation: {
markPath: "/provider-marks/cursor.svg",
darkMarkPath: "/provider-marks/cursor-dark.svg",
usagePageUrl: "https://cursor.com/dashboard/spending",
},
},
grok: {
Expand All @@ -45,12 +51,14 @@ export const providerCatalog = {
displayName: "Mistral",
presentation: {
markPath: "/provider-marks/mistral.svg",
usagePageUrl: "https://admin.mistral.ai/organization/usage",
},
},
perplexity: {
displayName: "Perplexity",
presentation: {
markPath: "/provider-marks/perplexity.svg",
usagePageUrl: "https://www.perplexity.ai/#settings/usage",
},
},
elevenlabs: {
Expand Down Expand Up @@ -125,6 +133,7 @@ export const providerCatalog = {
presentation: {
markPath: "/provider-marks/openai.svg",
apiKeySetupUrl: "https://platform.openai.com/api-keys",
usagePageUrl: "https://platform.openai.com/usage",
},
},
groqcloud: {
Expand Down
60 changes: 60 additions & 0 deletions entrypoints/sidepanel/Cockpit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1474,6 +1474,66 @@ describe("Cockpit", () => {
expect(screen.getByRole("button", { name: "Disconnect ChatGPT" })).toHaveFocus();
});

it("links provider detail to the plan usage page when that page has a stable URL", () => {
const state = createFixtureState(NOW);
const kimi = state.instances.find((instance) => instance.providerKind === "kimi");
if (kimi) {
state.instances.push({
...kimi,
id: "perplexity:default",
providerKind: "perplexity",
snapshot: kimi.snapshot
? { ...kimi.snapshot, providerKind: "perplexity" }
: undefined,
});
}

render(
<Cockpit
state={state}
now={NOW}
onDisplayModeChange={vi.fn()}
onRefresh={vi.fn()}
onConnectProvider={vi.fn()}
onDisconnectInstance={vi.fn()}
/>,
);

fireEvent.click(
screen.getByRole("button", { name: "Open ChatGPT details" }),
);
const usagePage = screen.getByRole("link", {
name: "Open the ChatGPT usage page",
});
expect(usagePage).toHaveAttribute(
"href",
"https://chatgpt.com/codex/settings/usage",
);
expect(usagePage).toHaveAttribute("target", "_blank");
expect(usagePage).toHaveAttribute("rel", "noopener noreferrer");
expect(usagePage).toHaveTextContent("Usage page");

fireEvent.click(screen.getByRole("button", { name: "Overview" }));
fireEvent.click(screen.getByRole("button", { name: "Open Kimi details" }));
expect(
screen.getByRole("link", { name: "Open the Kimi usage page" }),
).toHaveAttribute("href", "https://www.kimi.com/settings/subscription");

fireEvent.click(screen.getByRole("button", { name: "Overview" }));
fireEvent.click(
screen.getByRole("button", { name: "Open Perplexity details" }),
);
expect(
screen.getByRole("link", { name: "Open the Perplexity usage page" }),
).toHaveAttribute("href", "https://www.perplexity.ai/#settings/usage");

fireEvent.click(screen.getByRole("button", { name: "Overview" }));
fireEvent.click(screen.getByRole("button", { name: "Open Grok details" }));
expect(
screen.queryByRole("link", { name: /usage page/i }),
).not.toBeInTheDocument();
});

it("returns to Overview from Settings after disconnecting the provider that opened it", () => {
const onDisconnectInstance = vi.fn();
render(
Expand Down
1 change: 1 addition & 0 deletions entrypoints/sidepanel/Cockpit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,7 @@ export function providerView(
: undefined,
detailTables: snapshot?.detailTables,
now,
usagePageUrl: providerPresentation(provider.providerKind).usagePageUrl,
};
}

Expand Down
9 changes: 8 additions & 1 deletion entrypoints/sidepanel/components/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ export type IconName =
| "settings"
| "trash"
| "trending-down"
| "trending-up";
| "trending-up"
| "external";

export interface IconProps {
name: IconName;
Expand Down Expand Up @@ -83,6 +84,12 @@ export function Icon({ name, className = "" }: IconProps) {
<path {...common} d="M10 9v4" />
<circle cx="10" cy="6.5" r="1" fill="currentColor" />
</>
) : name === "external" ? (
<>
<path {...common} d="M8 4.5H4.5V15.5H15.5V12" />
<path {...common} d="M10.5 4.5H15.5V9.5" />
<path {...common} d="m9.2 10.8 6-6" />
</>
) : name === "code" ? (
<path {...common} d="m7.5 5-5 5 5 5m5-10 5 5-5 5" />
) : (
Expand Down
1 change: 1 addition & 0 deletions entrypoints/sidepanel/components/ProviderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface ProviderCardProps {
onClick: () => void;
};
headingLevel?: 2 | 3;
usagePageUrl?: string;
openDetailsFocusKey?: string;
onOpenDetails?: () => void;
onOpenHistory?: (metricId: string) => void;
Expand Down
13 changes: 13 additions & 0 deletions entrypoints/sidepanel/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1951,17 +1951,30 @@ section[aria-labelledby="overview-title"] .provider-list {
overflow-wrap: anywhere;
}

.detail-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

.detail-history-action {
display: inline-flex;
min-height: 44px;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 11px;
color: var(--text);
font-size: 12px;
font-weight: 600;
text-decoration: none;
}

a.detail-history-action:hover {
color: var(--text);
border-color: var(--quota);
}

.connection-surface__disconnect,
Expand Down
44 changes: 31 additions & 13 deletions entrypoints/sidepanel/views/ProviderDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,19 +265,37 @@ export function ProviderDetailView({
/>
) : null}

{firstWindow ? (
<button
className="detail-history-action"
type="button"
aria-label={l10n.t("providerDetail.openHistoryNamed", {
label: provider.instanceLabel,
})}
data-focus-key={`provider-history-${provider.instanceId}`}
onClick={() => onOpenHistory(provider.instanceId, firstWindow?.id)}
>
<Icon name="trending-up" />
{l10n.t("providerDetail.openHistory")}
</button>
{firstWindow || provider.usagePageUrl ? (
<div className="detail-actions">
{firstWindow ? (
<button
className="detail-history-action"
type="button"
aria-label={l10n.t("providerDetail.openHistoryNamed", {
label: provider.instanceLabel,
})}
data-focus-key={`provider-history-${provider.instanceId}`}
onClick={() => onOpenHistory(provider.instanceId, firstWindow.id)}
>
<Icon name="trending-up" />
{l10n.t("providerDetail.openHistory")}
</button>
) : null}
{provider.usagePageUrl ? (
<a
className="detail-history-action"
href={provider.usagePageUrl}
rel="noopener noreferrer"
target="_blank"
aria-label={l10n.t("providerDetail.openUsageNamed", {
label: provider.instanceLabel,
})}
>
<Icon name="external" />
{l10n.t("providerDetail.openUsage")}
</a>
) : null}
</div>
) : null}

<section className="connection-surface" aria-labelledby="connection-title">
Expand Down
2 changes: 2 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ providerDetail:
usageGroup: Usage group
openHistory: Open history
openHistoryNamed: "Open {label} history"
openUsage: Usage page
openUsageNamed: "Open the {label} usage page"
connectionTitle: Connection and capabilities
scheduledNoTab: Scheduled refresh reads the existing session without opening a tab.
disconnectExplanation: Disconnecting permanently deletes this provider's credentials, configuration, usage, and history.
Expand Down
2 changes: 2 additions & 0 deletions locales/zh_CN.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ providerDetail:
usageGroup: 用量组
openHistory: 打开历史
openHistoryNamed: "打开 {label} 的历史"
openUsage: 用量页面
openUsageNamed: "打开 {label} 的用量页面"
connectionTitle: 连接与能力
scheduledNoTab: 定时刷新会读取现有会话,不会打开标签页。
disconnectExplanation: 断开连接会永久删除该服务的凭据、配置、用量和历史。
Expand Down
8 changes: 8 additions & 0 deletions providers/catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe("provider catalog", () => {
darkMarkPath?: string;
apiKeySetupUrl?: string;
apiKeyGuide?: "elevenlabs" | "newapi";
usagePageUrl?: string;
};
const providerPresentation = (
catalog as typeof catalog & {
Expand All @@ -70,20 +71,24 @@ describe("provider catalog", () => {
{
providerId: "chatgpt",
markPath: "/provider-marks/chatgpt.svg",
usagePageUrl: "https://chatgpt.com/codex/settings/usage",
},
{
providerId: "claude",
markPath: "/provider-marks/claude.svg",
usagePageUrl: "https://claude.ai/settings/usage",
},
{
providerId: "kimi",
markPath: "/provider-marks/kimi.svg",
darkMarkPath: "/provider-marks/kimi-dark.svg",
usagePageUrl: "https://www.kimi.com/settings/subscription",
},
{
providerId: "cursor",
markPath: "/provider-marks/cursor.svg",
darkMarkPath: "/provider-marks/cursor-dark.svg",
usagePageUrl: "https://cursor.com/dashboard/spending",
},
{
providerId: "grok",
Expand All @@ -93,10 +98,12 @@ describe("provider catalog", () => {
{
providerId: "mistral",
markPath: "/provider-marks/mistral.svg",
usagePageUrl: "https://admin.mistral.ai/organization/usage",
},
{
providerId: "perplexity",
markPath: "/provider-marks/perplexity.svg",
usagePageUrl: "https://www.perplexity.ai/#settings/usage",
},
{
providerId: "elevenlabs",
Expand Down Expand Up @@ -149,6 +156,7 @@ describe("provider catalog", () => {
providerId: "openai",
markPath: "/provider-marks/openai.svg",
apiKeySetupUrl: "https://platform.openai.com/api-keys",
usagePageUrl: "https://platform.openai.com/usage",
},
{
providerId: "groqcloud",
Expand Down
Loading