diff --git a/bun.lock b/bun.lock index 3fab909..047092b 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "contextvm-site", "dependencies": { "@contextvm/mcp-sdk": "^1.30.0", - "@contextvm/sdk": "^0.13.6", + "@contextvm/sdk": "^0.13.7", "@sjsf/ajv8-validator": "^3.3.2", "@sjsf/form": "^3.3.2", "@sjsf/shadcn4-theme": "^3.3.2", @@ -102,7 +102,7 @@ "@contextvm/mcp-sdk": ["@contextvm/mcp-sdk@1.30.0", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "cross-spawn": "^7.0.5", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" } }, "sha512-Z4xyxxxtKnczEgVzLagaUdT1EtoVzc1bjr6kpIfTlw7mP5ESAzXeDUAMO9leIwituphz2ELE6ad9BjW7blDjjw=="], - "@contextvm/sdk": ["@contextvm/sdk@0.13.6", "", { "dependencies": { "@contextvm/mcp-sdk": "^1.30.0", "@noble/hashes": "^2.2.0", "applesauce-relay": "^6.2.1", "canonicalize": "^2.1.0", "nostr-tools": "^2.23.9", "pino": "^10.3.1", "rxjs": "^7.8.2", "ws": "^8.20.0", "zod": "^4.4.3" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-m67xpauk6w2YD4R/wDBdNBqNcPbXPgtbKx/gUS3/90UBJEyT9/Mt4/hGh9B/nX9y07HT+LUAfS9BC/0wYJWdhg=="], + "@contextvm/sdk": ["@contextvm/sdk@0.13.7", "", { "dependencies": { "@contextvm/mcp-sdk": "^1.30.0", "@noble/hashes": "^2.2.0", "applesauce-relay": "^6.2.1", "canonicalize": "^2.1.0", "nostr-tools": "^2.23.9", "pino": "^10.3.1", "rxjs": "^7.8.2", "ws": "^8.20.0", "zod": "^4.4.3" }, "peerDependencies": { "typescript": "^5.9.3" } }, "sha512-Y4fKGVYJzOTHM0bNuLE9+HzDWvr+pCmQgrsn4Wh2pT73ThwNIhvLOWw/GJygNPOZnQSQ3h3v46efwYto/g/oBQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], diff --git a/package.json b/package.json index a581dca..ada4ba3 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ }, "dependencies": { "@contextvm/mcp-sdk": "^1.30.0", - "@contextvm/sdk": "^0.13.6", + "@contextvm/sdk": "^0.13.7", "@sjsf/ajv8-validator": "^3.3.2", "@sjsf/form": "^3.3.2", "@sjsf/shadcn4-theme": "^3.3.2", diff --git a/src/lib/components/PromptGetForm.svelte b/src/lib/components/PromptGetForm.svelte index 547e808..a1f2aa6 100644 --- a/src/lib/components/PromptGetForm.svelte +++ b/src/lib/components/PromptGetForm.svelte @@ -19,6 +19,11 @@ import { activeAccount } from '$lib/services/accountManager.svelte'; import * as Alert from '$lib/components/ui/alert/index.js'; import PaymentStatusPanel from '$lib/components/PaymentStatusPanel.svelte'; + import PaymentErrorCard from '$lib/components/chat/PaymentErrorCard.svelte'; + import { + extractExplicitGatingError, + type ExplicitGatingError + } from '$lib/services/payments/payment-errors'; import { paymentNotificationsService } from '$lib/services/payments/payment-notifications.svelte'; import { findCapTagForPrompt, @@ -43,6 +48,7 @@ // Form state let formResult = $state(null); let formError = $state(null); + let gatingError = $state(null); let showResult = $state(false); // Collapsible state @@ -98,6 +104,7 @@ // Ensure payment panel is visible for subsequent calls. paymentOpen = true; paymentNotificationsService.clearServer(serverPubkey); + gatingError = null; open = true; try { if (!connectionState.connected) { @@ -111,6 +118,12 @@ formResult = result; showResult = true; } catch (error) { + const explicitGatingError = extractExplicitGatingError(error); + if (explicitGatingError) { + gatingError = explicitGatingError; + return; + } + formError = error instanceof Error ? error.message : 'Failed to get prompt'; } finally { loading = false; @@ -123,6 +136,7 @@ form.reset(new Event('reset')); formResult = null; formError = null; + gatingError = null; showResult = false; paymentNotificationsService.clearServer(serverPubkey); paymentOpen = true; @@ -160,6 +174,11 @@ class="data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 overflow-hidden" >
+ {#if gatingError} +
+ +
+ {/if} {#if paymentState}
diff --git a/src/lib/components/ResourceReadForm.svelte b/src/lib/components/ResourceReadForm.svelte index 2892381..90b678f 100644 --- a/src/lib/components/ResourceReadForm.svelte +++ b/src/lib/components/ResourceReadForm.svelte @@ -12,6 +12,11 @@ import { activeAccount } from '$lib/services/accountManager.svelte'; import * as Alert from '$lib/components/ui/alert/index.js'; import PaymentStatusPanel from '$lib/components/PaymentStatusPanel.svelte'; + import PaymentErrorCard from '$lib/components/chat/PaymentErrorCard.svelte'; + import { + extractExplicitGatingError, + type ExplicitGatingError + } from '$lib/services/payments/payment-errors'; import { paymentNotificationsService } from '$lib/services/payments/payment-notifications.svelte'; import { findCapTagForResource, @@ -36,6 +41,7 @@ // Form state let formResult = $state(null); let formError = $state(null); + let gatingError = $state(null); let showResult = $state(false); // Collapsible state @@ -80,6 +86,7 @@ // Ensure payment panel is visible for subsequent reads. paymentOpen = true; paymentNotificationsService.clearServer(serverPubkey); + gatingError = null; open = true; try { if (!connectionState.connected) { @@ -93,6 +100,12 @@ formResult = result; showResult = true; } catch (error) { + const explicitGatingError = extractExplicitGatingError(error); + if (explicitGatingError) { + gatingError = explicitGatingError; + return; + } + formError = error instanceof Error ? error.message : 'Failed to read resource'; } finally { loading = false; @@ -106,6 +119,7 @@ form.reset(new Event('reset')); formResult = null; formError = null; + gatingError = null; showResult = false; paymentNotificationsService.clearServer(serverPubkey); paymentOpen = true; @@ -140,6 +154,11 @@ class="data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 overflow-hidden" >
+ {#if gatingError} +
+ +
+ {/if} {#if paymentState}
diff --git a/src/lib/components/ToolCallForm.svelte b/src/lib/components/ToolCallForm.svelte index ec7c5eb..601ff10 100644 --- a/src/lib/components/ToolCallForm.svelte +++ b/src/lib/components/ToolCallForm.svelte @@ -13,6 +13,11 @@ import { activeAccount } from '$lib/services/accountManager.svelte'; import * as Alert from '$lib/components/ui/alert/index.js'; import PaymentStatusPanel from '$lib/components/PaymentStatusPanel.svelte'; + import PaymentErrorCard from '$lib/components/chat/PaymentErrorCard.svelte'; + import { + extractExplicitGatingError, + type ExplicitGatingError + } from '$lib/services/payments/payment-errors'; import { paymentNotificationsService } from '$lib/services/payments/payment-notifications.svelte'; import { findCapTagForTool, @@ -36,6 +41,7 @@ // Form state let formResult = $state(null); let formError = $state(null); + let gatingError = $state(null); let showResult = $state(false); let paymentState = $derived.by(() => paymentNotificationsService.getLatestForServer(serverPubkey) @@ -73,6 +79,7 @@ // Ensure the payment UI is visible for subsequent calls. paymentOpen = true; paymentNotificationsService.clearServer(serverPubkey); + gatingError = null; try { if (!connectionState.connected) { await mcpClientService.getClient(serverPubkey); @@ -92,6 +99,14 @@ showResult = true; loading = false; } catch (error) { + const explicitGatingError = extractExplicitGatingError(error); + if (explicitGatingError) { + gatingError = explicitGatingError; + open = true; + loading = false; + return; + } + formError = error instanceof Error ? error.message : 'Failed to call tool'; loading = false; } @@ -104,6 +119,7 @@ form.reset(new Event('reset')); formResult = null; formError = null; + gatingError = null; showResult = false; paymentNotificationsService.clearServer(serverPubkey); // Keep payment panel visible for the next invocation. @@ -165,6 +181,11 @@ class="data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0 overflow-hidden" >
+ {#if gatingError} +
+ +
+ {/if} {#if paymentState}
diff --git a/src/lib/components/chat/PaymentErrorCard.svelte b/src/lib/components/chat/PaymentErrorCard.svelte new file mode 100644 index 0000000..845c1a7 --- /dev/null +++ b/src/lib/components/chat/PaymentErrorCard.svelte @@ -0,0 +1,113 @@ + + +
+
+ + {title} + + + {error.code === PAYMENT_REQUIRED_ERROR_CODE + ? PAYMENT_REQUIRED_ERROR_CODE + : PAYMENT_PENDING_ERROR_CODE} + + {#if retryAfter !== null} + + Retry after {retryAfter}s + + {/if} +
+ + {#if instructions} +

{instructions}

+ {:else if error.message} +

{error.message}

+ {/if} + + {#if paymentOptions.length > 0} +
+ {#each paymentOptions as option, i (i)} + {@const amount = option.amount} + {@const pmi = typeof option.pmi === 'string' ? option.pmi : null} + {@const payReq = typeof option.pay_req === 'string' ? option.pay_req : null} + +
0 ? 'mt-3 border-t border-border/50 pt-3' : ''}> +
+ {#if amount !== undefined} + Amount + {amount} + {/if} + {#if pmi} + PMI + {pmi} + {/if} +
+ + {#if payReq} + + + Payment Request + QR + + + +
+
{payReq}
+ +
+
+ + +
+ +
+
+
+ {/if} +
+ {/each} +
+ {/if} +
diff --git a/src/lib/components/chat/ToolCallCard.svelte b/src/lib/components/chat/ToolCallCard.svelte index 9f5aebc..be04456 100644 --- a/src/lib/components/chat/ToolCallCard.svelte +++ b/src/lib/components/chat/ToolCallCard.svelte @@ -1,5 +1,8 @@ @@ -295,7 +333,34 @@ {/if}
-
+
+
+
+

Payment mode

+

+ {paymentModeCaption} +

+ {#if modeMismatch} +

+ Server does not support explicit gating — fell back to transparent. +

+ {/if} +
+
+ + {explicitGatingEnabled ? 'Explicit gating' : 'Transparent'} + + +
+
{#if connectionState.connected}