@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/README.md +45 -6
- package/dist/index.js +7 -7
- package/dist/print/index.html +7 -7
- package/package.json +1 -1
- package/src/print/main.ts +55 -1
- package/src/shared/bridge.ts +23 -0
package/package.json
CHANGED
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;
|
package/src/shared/bridge.ts
CHANGED
|
@@ -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";
|