@womp/mcp-ui 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/mcp-ui",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "MCP Apps widgets for Womp's MCP server — interactive cards rendered inline in Claude and other MCP hosts.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
package/src/print/main.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $, callTool, createCard, openLink, type GenerationResult } from "../shared/bridge";
1
+ import { $, callTool, createCard, openLink, pushModelContext, type GenerationResult } from "../shared/bridge";
2
2
  import { expandHexInput, nearestPantone, normalizeHex, pantoneChips } from "./pantone";
3
3
 
4
4
  /**
@@ -13,6 +13,14 @@ import { expandHexInput, nearestPantone, normalizeHex, pantoneChips } from "./pa
13
13
  * struck through. What the panel does to the 3D viewport (drain holes, wall
14
14
  * thickness, X-ray) stays on womp.com, which is where Hollow hands off.
15
15
  *
16
+ * WHICH of those the user actually sees is the backend's decision, and v1 shows
17
+ * fewer: `gc.mcp.printMaterials` sends only the materials chat sells (Clear and
18
+ * Tinted clear — so the Enterprise disclosure has nothing to open) and
19
+ * `gc.mcp.printHollowPricing` withholds the quote's `hollow` row, which is the
20
+ * only thing the Print style section is rendered from. Both are data, so
21
+ * nothing here needed changing — do not hardcode either, or turning them back
22
+ * on in console will not reach the card.
23
+ *
16
24
  * The card re-quotes THROUGH THE HOST by calling `estimate_print` itself, so
17
25
  * tuning a print never round-trips through the conversation. The model seeds
18
26
  * the starting settings simply by calling the tool with them: the host
@@ -96,6 +104,8 @@ const MAX_LONGEST_CM = 60;
96
104
  const PANEL_DEFAULT_TINT = "#276ee2";
97
105
  /** The panel's own re-price debounce. */
98
106
  const REQUOTE_DEBOUNCE_MS = 500;
107
+ /** State push: faster than a re-quote (it prices nothing) but still one per gesture, not per keystroke. */
108
+ const SYNC_DEBOUNCE_MS = 300;
99
109
  const AXES: Axis[] = ["width", "height", "depth"];
100
110
 
101
111
  const state = {
@@ -129,6 +139,7 @@ const state = {
129
139
  /** Monotonic guard: a slow older quote must never overwrite a newer one. */
130
140
  let quoteSeq = 0;
131
141
  let debounceTimer: number | undefined;
142
+ let syncTimer: number | undefined;
132
143
 
133
144
  // --- helpers ---------------------------------------------------------------
134
145
 
@@ -169,6 +180,7 @@ function setQuoting(on: boolean) {
169
180
  function markDirty() {
170
181
  state.cartCheckoutUrl = "";
171
182
  state.preparedUrl = "";
183
+ scheduleSync();
172
184
  }
173
185
 
174
186
  /**
@@ -668,6 +680,48 @@ function scheduleRequote() {
668
680
  debounceTimer = window.setTimeout(() => void requote(), REQUOTE_DEBOUNCE_MS);
669
681
  }
670
682
 
683
+ /** "Tinted clear (resin-clear-tinted) · matte · #276ee2 (Pantone 2386 C) · 10.2 cm · x2" */
684
+ function describeSelection(): string {
685
+ const m = selectedMaterial();
686
+ const snapped = snappedColor();
687
+ const bits = [`${materialLabel(m)} (${state.material})`];
688
+ if (state.finish && m?.finishes?.length) bits.push(state.finish);
689
+ if (snapped) bits.push(`${snapped.hex} (Pantone ${snapped.code})`);
690
+ if (state.longestSideCm) bits.push(`${fmtDim(state.longestSideCm, state.unit)} ${state.unit} longest side`);
691
+ bits.push(`x${state.quantity}`);
692
+ return bits.join(" · ");
693
+ }
694
+
695
+ /**
696
+ * Publish what the card is showing — the whole point of the card being live.
697
+ *
698
+ * Its re-quotes are app-initiated calls whose results come back here and go
699
+ * nowhere else, so a configuration tuned for two minutes is invisible to the
700
+ * conversation: ask for it in chat and the model orders the settings it opened
701
+ * with. Two channels, deliberately: `sync_print_config` parks the selection on
702
+ * the server, which is what `add_to_cart` actually reads, and
703
+ * `updateModelContext` tells the model so it can talk about the right print
704
+ * (host-gated, and a no-op where it is unsupported).
705
+ *
706
+ * Failures are swallowed. A state push that does not land costs an out-of-date
707
+ * order note, not a broken card, and the user has the button right there.
708
+ */
709
+ function syncConfig() {
710
+ if (!state.source || state.staticPrintUrl || !state.material) return;
711
+ void callTool(card.app, "sync_print_config", baseArgs()).catch(() => {});
712
+ void pushModelContext(
713
+ card.app,
714
+ `The 3D print card in this conversation now shows: ${describeSelection()}. ` +
715
+ "These are the user's own selections on the card, later than anything said in chat. " +
716
+ "If they ask to order or add it to the cart, call add_to_cart with only the mesh source.",
717
+ );
718
+ }
719
+
720
+ function scheduleSync() {
721
+ if (syncTimer !== undefined) clearTimeout(syncTimer);
722
+ syncTimer = window.setTimeout(syncConfig, SYNC_DEBOUNCE_MS);
723
+ }
724
+
671
725
  async function addToCart() {
672
726
  const cta = $("cta") as HTMLButtonElement;
673
727
  cta.disabled = true;
@@ -202,6 +202,29 @@ export async function callTool(app: App, name: string, args: Record<string, unkn
202
202
  return app.request({ method: "tools/call", params: { name, arguments: args } }, CallToolResultSchema);
203
203
  }
204
204
 
205
+ /**
206
+ * Push what the card is showing into the model's context for the next turn.
207
+ *
208
+ * A card's own tool calls are announced to nobody: the host proxies them and
209
+ * hands the result back to the iframe, so nothing the user changes here reaches
210
+ * the conversation. `ui/update-model-context` is the channel for that — the
211
+ * host holds the last update and gives it to the model on the next user
212
+ * message. It is host-gated, and Claude web was reported to drop the payload
213
+ * silently, so this is a bonus that makes the model DESCRIBE the right print,
214
+ * never the thing correctness rests on: the server holds the configuration too.
215
+ *
216
+ * No-ops (rather than throws) when the host does not advertise the capability.
217
+ */
218
+ export async function pushModelContext(app: App, text: string) {
219
+ const supported = (app.getHostCapabilities() as { updateModelContext?: unknown } | undefined)?.updateModelContext;
220
+ if (!supported) return;
221
+ try {
222
+ await app.updateModelContext({ content: [{ type: "text", text }] });
223
+ } catch {
224
+ // A host that rejects the update is not a card the user can do nothing with.
225
+ }
226
+ }
227
+
205
228
  /** Toggle fullscreen, trusting the host's answer over our request. */
206
229
  export function toggleFullscreen(app: App) {
207
230
  const isFull = document.documentElement.dataset.displayMode === "fullscreen";