@ggui-ai/ui-visual-tester 0.23.0 → 0.24.0

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.
@@ -28577,6 +28577,7 @@
28577
28577
  buildWireConfig: () => buildWireConfig,
28578
28578
  connectionSource: () => connectionSource,
28579
28579
  useAction: () => useAction,
28580
+ useActionSpent: () => useActionSpent,
28580
28581
  useApp: () => useApp,
28581
28582
  useAuth: () => useAuth,
28582
28583
  useContract: () => useContract,
@@ -28667,8 +28668,19 @@
28667
28668
  }, [actionName, dispatch, onDispatchSuppressed]);
28668
28669
  }
28669
28670
 
28670
- // ../wire/dist/useGguiContext.js
28671
+ // ../wire/dist/action-spent.js
28671
28672
  var import_react35 = __toESM(require_react(), 1);
28673
+ var ActionSpentContext = (0, import_react35.createContext)(null);
28674
+ var noSubscription = () => () => void 0;
28675
+ function useActionSpent(actionName) {
28676
+ const source = (0, import_react35.useContext)(ActionSpentContext);
28677
+ const subscribe = (0, import_react35.useCallback)((listener) => source === null ? noSubscription() : source.subscribe(listener), [source]);
28678
+ const read = (0, import_react35.useCallback)(() => source === null ? false : source.isSpent(actionName), [source, actionName]);
28679
+ return (0, import_react35.useSyncExternalStore)(subscribe, read, read);
28680
+ }
28681
+
28682
+ // ../wire/dist/useGguiContext.js
28683
+ var import_react36 = __toESM(require_react(), 1);
28672
28684
  function useGguiContext(slotName) {
28673
28685
  const contextName = slotName.length === 0 ? "Context" : slotName.charAt(0).toUpperCase() + slotName.slice(1) + "Context";
28674
28686
  const registry = globalThis.__ggui__?.contexts;
@@ -28676,19 +28688,19 @@
28676
28688
  if (!ctx) {
28677
28689
  throw new Error(`[ggui] useGguiContext('${slotName}'): no Context registered. The contract must declare 'contextSpec.${slotName}' AND the runtime must have synthesized the per-slot React Context at boot. If you see this in production code generated by the LLM, the boilerplate's destructure line drifted from the contract.`);
28678
28690
  }
28679
- return (0, import_react35.useContext)(ctx);
28691
+ return (0, import_react36.useContext)(ctx);
28680
28692
  }
28681
28693
 
28682
28694
  // ../wire/dist/useStream.js
28683
- var import_react36 = __toESM(require_react(), 1);
28695
+ var import_react37 = __toESM(require_react(), 1);
28684
28696
  function useStream(channelName) {
28685
28697
  const { subscribe } = useWireContext();
28686
- const [latest, setLatest] = (0, import_react36.useState)(null);
28687
- const [all, setAll] = (0, import_react36.useState)([]);
28688
- const [isComplete, setIsComplete] = (0, import_react36.useState)(false);
28689
- const allRef = (0, import_react36.useRef)(all);
28698
+ const [latest, setLatest] = (0, import_react37.useState)(null);
28699
+ const [all, setAll] = (0, import_react37.useState)([]);
28700
+ const [isComplete, setIsComplete] = (0, import_react37.useState)(false);
28701
+ const allRef = (0, import_react37.useRef)(all);
28690
28702
  allRef.current = all;
28691
- (0, import_react36.useEffect)(() => {
28703
+ (0, import_react37.useEffect)(() => {
28692
28704
  return subscribe(channelName, (delivery) => {
28693
28705
  const typed = delivery.payload;
28694
28706
  setLatest(typed);
@@ -28726,7 +28738,7 @@
28726
28738
  }
28727
28739
 
28728
28740
  // ../wire/dist/useRender.js
28729
- var import_react37 = __toESM(require_react(), 1);
28741
+ var import_react38 = __toESM(require_react(), 1);
28730
28742
 
28731
28743
  // ../wire/dist/connection-store.js
28732
28744
  var ConnectionWriterConflictError = class extends Error {
@@ -28823,7 +28835,7 @@
28823
28835
  // ../wire/dist/useRender.js
28824
28836
  function useRender() {
28825
28837
  const { render } = useWireContext();
28826
- const isConnected = (0, import_react37.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28838
+ const isConnected = (0, import_react38.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28827
28839
  return { sessionId: render.sessionId, isConnected };
28828
28840
  }
28829
28841
 
@@ -29407,7 +29419,24 @@
29407
29419
  });
29408
29420
  const validateEnvelope = opts.validateEnvelope ?? validateOutboundActionEnvelope;
29409
29421
  const spentOneShots = /* @__PURE__ */ new Set();
29422
+ const isSpent = (actionName) => opts.getActiveActionSpec()?.[actionName]?.oneShot === true && (spentOneShots.has(actionName) || opts.getSpentOneShots?.()?.includes(actionName) === true);
29423
+ const spentListeners = /* @__PURE__ */ new Set();
29424
+ const notifySpent = () => {
29425
+ for (const listener of [...spentListeners])
29426
+ listener();
29427
+ };
29428
+ const actionSpent = {
29429
+ isSpent,
29430
+ subscribe: (listener) => {
29431
+ spentListeners.add(listener);
29432
+ return () => {
29433
+ spentListeners.delete(listener);
29434
+ };
29435
+ }
29436
+ };
29437
+ opts.spentInputsChanged?.(notifySpent);
29410
29438
  return {
29439
+ actionSpent,
29411
29440
  app: opts.app,
29412
29441
  render: opts.render,
29413
29442
  auth: opts.auth,
@@ -29417,8 +29446,7 @@
29417
29446
  if (actionSpec === void 0)
29418
29447
  opts.onActionSpecAbsent?.(actionName);
29419
29448
  const isOneShot = actionSpec?.[actionName]?.oneShot === true;
29420
- const spent = spentOneShots.has(actionName) || opts.getSpentOneShots?.()?.includes(actionName) === true;
29421
- if (isOneShot && spent) {
29449
+ if (isSpent(actionName)) {
29422
29450
  console.warn(`[ggui] dispatch('${actionName}') suppressed: this action is declared oneShot and already fired for this render. A second gesture is NOT sent to the agent \u2014 the submitted control is spent for the render's lifetime. If the action is meant to repeat, remove \`oneShot\` from its contract entry.`);
29423
29451
  opts.onDispatchSuppressed?.({
29424
29452
  reason: "one-shot-spent",
@@ -29443,8 +29471,10 @@
29443
29471
  opts.onViolation(new ClientContractViolationError("outbound-action", result.violations));
29444
29472
  return;
29445
29473
  }
29446
- if (isOneShot)
29474
+ if (isOneShot && !spentOneShots.has(actionName)) {
29447
29475
  spentOneShots.add(actionName);
29476
+ notifySpent();
29477
+ }
29448
29478
  opts.emitEnvelope(envelope);
29449
29479
  },
29450
29480
  subscribe: (channelName, handler) => {
@@ -32439,8 +32469,8 @@
32439
32469
 
32440
32470
  // ../design/dist/themes/ThemeProvider.js
32441
32471
  var import_jsx_runtime58 = __toESM(require_jsx_runtime(), 1);
32442
- var import_react38 = __toESM(require_react(), 1);
32443
- var ThemeContext = (0, import_react38.createContext)(null);
32472
+ var import_react39 = __toESM(require_react(), 1);
32473
+ var ThemeContext = (0, import_react39.createContext)(null);
32444
32474
 
32445
32475
  // ../design/dist/rendering/css-tokens.js
32446
32476
  var EXPANDED_FRAME = { outerRadiusVar: "--ggui-shape-radius-xl", insetPx: 16 };
@@ -32459,6 +32489,11 @@
32459
32489
  "useGguiContext",
32460
32490
  "useContract",
32461
32491
  "useWireContext",
32492
+ // ggui#1223: a `oneShot` action's spent state, as data. It is served here
32493
+ // one release BEFORE the triad teaches it, because a card that imports a name
32494
+ // its runtime's shim lacks fails at module link. A card generated to use it
32495
+ // must never meet a runtime without it.
32496
+ "useActionSpent",
32462
32497
  // Provider component
32463
32498
  "GguiWireProvider"
32464
32499
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-visual-tester",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Behavioral validator for ggui-generated component code. Renders a component in real Chromium via Playwright, clicks each action declared in its contract, and verifies the action actually does something — mutating bound state or dispatching to the agent. Catches buttons wired to no-op handlers that structural validators miss. Playwright is an optional peer dependency, injected by the caller.",
5
5
  "keywords": [
6
6
  "ggui",
@@ -53,9 +53,9 @@
53
53
  "dependencies": {
54
54
  "react": "^19.0.0",
55
55
  "react-dom": "^19.0.0",
56
- "@ggui-ai/design": "0.23.0",
57
- "@ggui-ai/wire": "0.23.0",
58
- "@ggui-ai/protocol": "0.23.0"
56
+ "@ggui-ai/design": "0.24.0",
57
+ "@ggui-ai/protocol": "0.24.0",
58
+ "@ggui-ai/wire": "0.24.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@playwright/test": "^1.60.0",