diff --git a/server/routers/virtualApiKey/listMyVirtualApiKeys.ts b/server/routers/virtualApiKey/listMyVirtualApiKeys.ts
index 46531560a..b1a724468 100644
--- a/server/routers/virtualApiKey/listMyVirtualApiKeys.ts
+++ b/server/routers/virtualApiKey/listMyVirtualApiKeys.ts
@@ -20,6 +20,7 @@ import {
toPublicVirtualApiKey
} from "@server/lib/virtualApiKey";
import type { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types";
+import { formatPublicResourceAccess } from "@server/routers/launcher/formatLauncherAccess";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -125,11 +126,19 @@ export async function listMyVirtualApiKeys(
let resourceId: number | undefined;
let resourceName: string | undefined;
+ let resourceNiceId: string | undefined;
+ let resourceAccessUrl: string | null = null;
if (resourceGuid) {
const [resource] = await db
.select({
resourceId: resources.resourceId,
- name: resources.name
+ name: resources.name,
+ niceId: resources.niceId,
+ mode: resources.mode,
+ fullDomain: resources.fullDomain,
+ ssl: resources.ssl,
+ proxyPort: resources.proxyPort,
+ wildcard: resources.wildcard
})
.from(resources)
.where(
@@ -151,6 +160,14 @@ export async function listMyVirtualApiKeys(
resourceId = resource.resourceId;
resourceName = resource.name;
+ resourceNiceId = resource.niceId;
+ resourceAccessUrl = formatPublicResourceAccess({
+ mode: resource.mode ?? "",
+ fullDomain: resource.fullDomain,
+ ssl: resource.ssl,
+ proxyPort: resource.proxyPort,
+ wildcard: resource.wildcard
+ }).accessUrl;
}
const [user] = await db
@@ -223,7 +240,9 @@ export async function listMyVirtualApiKeys(
manualKeys: manualRows.map((row) =>
toKeyWithResources(row, resourceIdsByKey)
),
- ...(resourceName !== undefined ? { resourceName } : {})
+ ...(resourceName !== undefined ? { resourceName } : {}),
+ ...(resourceNiceId !== undefined ? { resourceNiceId } : {}),
+ ...(resourceNiceId !== undefined ? { resourceAccessUrl } : {})
},
success: true,
error: false,
diff --git a/server/routers/virtualApiKey/types.ts b/server/routers/virtualApiKey/types.ts
index e6a387446..9b528a9cd 100644
--- a/server/routers/virtualApiKey/types.ts
+++ b/server/routers/virtualApiKey/types.ts
@@ -23,6 +23,8 @@ export type ListMyVirtualApiKeysResponse = {
userKey: VirtualApiKeyWithResources;
manualKeys: VirtualApiKeyWithResources[];
resourceName?: string | null;
+ resourceNiceId?: string | null;
+ resourceAccessUrl?: string | null;
};
export type GetMyVirtualApiKeyResponse = {
diff --git a/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx b/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx
index dd673ac85..ef3e18ee9 100644
--- a/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx
+++ b/src/app/[orgId]/resource/[resourceGuid]/keys/page.tsx
@@ -113,7 +113,12 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
launcherMode
showViewAsAdmin={isAdminOrOwner}
>
-
+
);
diff --git a/src/components/UserVirtualApiKeys.tsx b/src/components/UserVirtualApiKeys.tsx
index fa0bf0eb8..6f06382a2 100644
--- a/src/components/UserVirtualApiKeys.tsx
+++ b/src/components/UserVirtualApiKeys.tsx
@@ -26,6 +26,10 @@ import { formatVirtualApiKeyPreview } from "@app/lib/virtualApiKeyFormat";
type UserVirtualApiKeysProps = {
orgId: string;
initialData: ListMyVirtualApiKeysResponse;
+ /** The resource's niceId, when this page is scoped to a single resource. */
+ resourceNiceId?: string;
+ /** The resource's real access URL, when known; falls back to a placeholder otherwise. */
+ endpoint?: string;
};
function OwnedKeySecret({
@@ -163,7 +167,9 @@ function ManualKeyRow({
export default function UserVirtualApiKeys({
orgId,
- initialData
+ initialData,
+ resourceNiceId,
+ endpoint
}: UserVirtualApiKeysProps) {
const t = useTranslations();
const resourceName = initialData.resourceName;
@@ -217,11 +223,12 @@ export default function UserVirtualApiKeys({
>
diff --git a/src/components/ai-client-config/AiClientConfigCard.tsx b/src/components/ai-client-config/AiClientConfigCard.tsx
index 709e7ac33..8b6747896 100644
--- a/src/components/ai-client-config/AiClientConfigCard.tsx
+++ b/src/components/ai-client-config/AiClientConfigCard.tsx
@@ -40,6 +40,7 @@ type AiClientConfigCardProps = {
keyAuth: AiClientAuthInput;
description: string;
iconSrc: { light: string; dark: string };
+ resourceNiceId?: string;
};
export function AiClientConfigCard({
@@ -48,7 +49,8 @@ export function AiClientConfigCard({
endpoint,
keyAuth,
description,
- iconSrc
+ iconSrc,
+ resourceNiceId
}: AiClientConfigCardProps) {
const t = useTranslations();
const { theme } = useTheme();
@@ -94,16 +96,23 @@ export function AiClientConfigCard({
const guide = useMemo(() => {
if (keyAuth.mode === "keyless") {
- return buildAiClientGuide(clientId, endpoint, { mode: "keyless" });
+ return buildAiClientGuide(
+ clientId,
+ endpoint,
+ { mode: "keyless" },
+ resourceNiceId
+ );
}
if (revealedKey === null) {
return null;
}
- return buildAiClientGuide(clientId, endpoint, {
- mode: "keyed",
- key: revealedKey
- });
- }, [clientId, endpoint, keyAuth.mode, revealedKey]);
+ return buildAiClientGuide(
+ clientId,
+ endpoint,
+ { mode: "keyed", key: revealedKey },
+ resourceNiceId
+ );
+ }, [clientId, endpoint, keyAuth.mode, revealedKey, resourceNiceId]);
const preset =
guide?.presets.find((p) => p.id === presetId) ?? guide?.presets[0];
diff --git a/src/components/ai-client-config/AiClientConfigSection.tsx b/src/components/ai-client-config/AiClientConfigSection.tsx
index 5998bf615..364022e7f 100644
--- a/src/components/ai-client-config/AiClientConfigSection.tsx
+++ b/src/components/ai-client-config/AiClientConfigSection.tsx
@@ -27,6 +27,12 @@ type AiClientConfigSectionProps = {
*/
layout?: "wide" | "compact";
className?: string;
+ /**
+ * The resource's niceId, when known, so the displayed CLI commands can
+ * include `--resource ` and let `pangolin configure` skip its
+ * own resource picker.
+ */
+ resourceNiceId?: string;
};
// Each logo file is named for its own color, not the theme it belongs on, so
@@ -54,7 +60,8 @@ export function AiClientConfigSection({
endpoint,
auth,
layout = "compact",
- className
+ className,
+ resourceNiceId
}: AiClientConfigSectionProps) {
const t = useTranslations();
const isWide = layout === "wide";
@@ -93,6 +100,7 @@ export function AiClientConfigSection({
keyAuth={auth}
description={descriptions[clientId]}
iconSrc={CLIENT_LOGOS[clientId]}
+ resourceNiceId={resourceNiceId}
/>
))}
diff --git a/src/components/resource-launcher/LauncherResourcePanel.tsx b/src/components/resource-launcher/LauncherResourcePanel.tsx
index 83f4ac14f..e35d9dce6 100644
--- a/src/components/resource-launcher/LauncherResourcePanel.tsx
+++ b/src/components/resource-launcher/LauncherResourcePanel.tsx
@@ -351,6 +351,7 @@ function PublicResourceDetails({
mode: "keyed",
getKeyText: getAiKeyCopyText
}}
+ resourceNiceId={launcherResource.niceId}
/>
) : null}
>
@@ -428,6 +429,7 @@ function PrivateResourceDetails({
>
) : null}
diff --git a/src/lib/aiClientConfig.ts b/src/lib/aiClientConfig.ts
index 83cca4d6d..9107d62b9 100644
--- a/src/lib/aiClientConfig.ts
+++ b/src/lib/aiClientConfig.ts
@@ -59,12 +59,17 @@ function block(
function buildCli(
clientArg: "claude" | "codex" | "opencode",
- auth: AiClientAuth
+ auth: AiClientAuth,
+ resourceNiceId?: string
): AiCliCommands {
+ const resourceFlag = resourceNiceId
+ ? ` --resource ${resourceNiceId}`
+ : "";
+
const configure: AiConfigBlock = {
id: `cli-configure-${clientArg}`,
label: "Configure",
- displayText: `pangolin configure ${clientArg}`
+ displayText: `pangolin configure ${clientArg}${resourceFlag}`
};
if (auth.mode !== "keyed") {
@@ -76,12 +81,16 @@ function buildCli(
configureWithKey: {
id: `cli-configure-key-${clientArg}`,
label: "Configure with an API key",
- displayText: `pangolin configure ${clientArg} ${auth.key}`
+ displayText: `pangolin configure ${clientArg} ${auth.key}${resourceFlag}`
}
};
}
-function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
+function buildClaudeGuide(
+ endpoint: string,
+ auth: AiClientAuth,
+ resourceNiceId?: string
+): AiClientGuide {
const defaultSettings = block(
"claude-default-settings",
"~/.claude/settings.json",
@@ -168,7 +177,7 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
return {
id: "claude",
name: AI_CLIENT_NAMES.claude,
- cli: buildCli("claude", auth),
+ cli: buildCli("claude", auth, resourceNiceId),
presets: [
{
id: "default",
@@ -194,7 +203,11 @@ function buildClaudeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
};
}
-function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
+function buildCodexGuide(
+ endpoint: string,
+ auth: AiClientAuth,
+ resourceNiceId?: string
+): AiClientGuide {
const settings = block(
"codex-settings",
"~/.codex/config.toml",
@@ -224,7 +237,7 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
return {
id: "codex",
name: AI_CLIENT_NAMES.codex,
- cli: buildCli("codex", auth),
+ cli: buildCli("codex", auth, resourceNiceId),
presets: [
{
id: "default",
@@ -235,7 +248,11 @@ function buildCodexGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
};
}
-function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
+function buildOpencodeGuide(
+ endpoint: string,
+ auth: AiClientAuth,
+ resourceNiceId?: string
+): AiClientGuide {
const config = block(
"opencode-config",
"Step 1: opencode.json",
@@ -273,7 +290,7 @@ function buildOpencodeGuide(endpoint: string, auth: AiClientAuth): AiClientGuide
return {
id: "opencode",
name: AI_CLIENT_NAMES.opencode,
- cli: buildCli("opencode", auth),
+ cli: buildCli("opencode", auth, resourceNiceId),
presets: [
{
id: "default",
@@ -318,7 +335,11 @@ function buildCursorGuide(endpoint: string, auth: AiClientAuth): AiClientGuide {
const GUIDE_BUILDERS: Record<
AiClientId,
- (endpoint: string, auth: AiClientAuth) => AiClientGuide
+ (
+ endpoint: string,
+ auth: AiClientAuth,
+ resourceNiceId?: string
+ ) => AiClientGuide
> = {
claude: buildClaudeGuide,
codex: buildCodexGuide,
@@ -329,7 +350,8 @@ const GUIDE_BUILDERS: Record<
export function buildAiClientGuide(
clientId: AiClientId,
endpoint: string,
- auth: AiClientAuth
+ auth: AiClientAuth,
+ resourceNiceId?: string
): AiClientGuide {
- return GUIDE_BUILDERS[clientId](endpoint, auth);
+ return GUIDE_BUILDERS[clientId](endpoint, auth, resourceNiceId);
}