@womp/mcp-ui 0.1.9 → 0.1.10

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.10",
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
  /**
@@ -96,6 +96,8 @@ const MAX_LONGEST_CM = 60;
96
96
  const PANEL_DEFAULT_TINT = "#276ee2";
97
97
  /** The panel's own re-price debounce. */
98
98
  const REQUOTE_DEBOUNCE_MS = 500;
99
+ /** State push: faster than a re-quote (it prices nothing) but still one per gesture, not per keystroke. */
100
+ const SYNC_DEBOUNCE_MS = 300;
99
101
  const AXES: Axis[] = ["width", "height", "depth"];
100
102
 
101
103
  const state = {
@@ -129,6 +131,7 @@ const state = {
129
131
  /** Monotonic guard: a slow older quote must never overwrite a newer one. */
130
132
  let quoteSeq = 0;
131
133
  let debounceTimer: number | undefined;
134
+ let syncTimer: number | undefined;
132
135
 
133
136
  // --- helpers ---------------------------------------------------------------
134
137
 
@@ -169,6 +172,7 @@ function setQuoting(on: boolean) {
169
172
  function markDirty() {
170
173
  state.cartCheckoutUrl = "";
171
174
  state.preparedUrl = "";
175
+ scheduleSync();
172
176
  }
173
177
 
174
178
  /**
@@ -668,6 +672,48 @@ function scheduleRequote() {
668
672
  debounceTimer = window.setTimeout(() => void requote(), REQUOTE_DEBOUNCE_MS);
669
673
  }
670
674
 
675
+ /** "Tinted clear (resin-clear-tinted) · matte · #276ee2 (Pantone 2386 C) · 10.2 cm · x2" */
676
+ function describeSelection(): string {
677
+ const m = selectedMaterial();
678
+ const snapped = snappedColor();
679
+ const bits = [`${materialLabel(m)} (${state.material})`];
680
+ if (state.finish && m?.finishes?.length) bits.push(state.finish);
681
+ if (snapped) bits.push(`${snapped.hex} (Pantone ${snapped.code})`);
682
+ if (state.longestSideCm) bits.push(`${fmtDim(state.longestSideCm, state.unit)} ${state.unit} longest side`);
683
+ bits.push(`x${state.quantity}`);
684
+ return bits.join(" · ");
685
+ }
686
+
687
+ /**
688
+ * Publish what the card is showing — the whole point of the card being live.
689
+ *
690
+ * Its re-quotes are app-initiated calls whose results come back here and go
691
+ * nowhere else, so a configuration tuned for two minutes is invisible to the
692
+ * conversation: ask for it in chat and the model orders the settings it opened
693
+ * with. Two channels, deliberately: `sync_print_config` parks the selection on
694
+ * the server, which is what `add_to_cart` actually reads, and
695
+ * `updateModelContext` tells the model so it can talk about the right print
696
+ * (host-gated, and a no-op where it is unsupported).
697
+ *
698
+ * Failures are swallowed. A state push that does not land costs an out-of-date
699
+ * order note, not a broken card, and the user has the button right there.
700
+ */
701
+ function syncConfig() {
702
+ if (!state.source || state.staticPrintUrl || !state.material) return;
703
+ void callTool(card.app, "sync_print_config", baseArgs()).catch(() => {});
704
+ void pushModelContext(
705
+ card.app,
706
+ `The 3D print card in this conversation now shows: ${describeSelection()}. ` +
707
+ "These are the user's own selections on the card, later than anything said in chat. " +
708
+ "If they ask to order or add it to the cart, call add_to_cart with only the mesh source.",
709
+ );
710
+ }
711
+
712
+ function scheduleSync() {
713
+ if (syncTimer !== undefined) clearTimeout(syncTimer);
714
+ syncTimer = window.setTimeout(syncConfig, SYNC_DEBOUNCE_MS);
715
+ }
716
+
671
717
  async function addToCart() {
672
718
  const cta = $("cta") as HTMLButtonElement;
673
719
  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";