@ggui-ai/ui-visual-tester 0.23.0 → 0.25.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,8 @@
28577
28577
  buildWireConfig: () => buildWireConfig,
28578
28578
  connectionSource: () => connectionSource,
28579
28579
  useAction: () => useAction,
28580
+ useActionPending: () => useActionPending,
28581
+ useActionSpent: () => useActionSpent,
28580
28582
  useApp: () => useApp,
28581
28583
  useAuth: () => useAuth,
28582
28584
  useContract: () => useContract,
@@ -28667,8 +28669,30 @@
28667
28669
  }, [actionName, dispatch, onDispatchSuppressed]);
28668
28670
  }
28669
28671
 
28670
- // ../wire/dist/useGguiContext.js
28672
+ // ../wire/dist/action-spent.js
28671
28673
  var import_react35 = __toESM(require_react(), 1);
28674
+ var ActionSpentContext = (0, import_react35.createContext)(null);
28675
+ var noSubscription = () => () => void 0;
28676
+ function useActionSpent(actionName) {
28677
+ const source = (0, import_react35.useContext)(ActionSpentContext);
28678
+ const subscribe = (0, import_react35.useCallback)((listener) => source === null ? noSubscription() : source.subscribe(listener), [source]);
28679
+ const read = (0, import_react35.useCallback)(() => source === null ? false : source.isSpent(actionName), [source, actionName]);
28680
+ return (0, import_react35.useSyncExternalStore)(subscribe, read, read);
28681
+ }
28682
+
28683
+ // ../wire/dist/action-pending.js
28684
+ var import_react36 = __toESM(require_react(), 1);
28685
+ var ActionPendingContext = (0, import_react36.createContext)(null);
28686
+ var noSubscription2 = () => () => void 0;
28687
+ function useActionPending(actionName) {
28688
+ const source = (0, import_react36.useContext)(ActionPendingContext);
28689
+ const subscribe = (0, import_react36.useCallback)((listener) => source === null ? noSubscription2() : source.subscribe(listener), [source]);
28690
+ const read = (0, import_react36.useCallback)(() => source === null ? false : source.isPending(actionName), [source, actionName]);
28691
+ return (0, import_react36.useSyncExternalStore)(subscribe, read, read);
28692
+ }
28693
+
28694
+ // ../wire/dist/useGguiContext.js
28695
+ var import_react37 = __toESM(require_react(), 1);
28672
28696
  function useGguiContext(slotName) {
28673
28697
  const contextName = slotName.length === 0 ? "Context" : slotName.charAt(0).toUpperCase() + slotName.slice(1) + "Context";
28674
28698
  const registry = globalThis.__ggui__?.contexts;
@@ -28676,19 +28700,19 @@
28676
28700
  if (!ctx) {
28677
28701
  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
28702
  }
28679
- return (0, import_react35.useContext)(ctx);
28703
+ return (0, import_react37.useContext)(ctx);
28680
28704
  }
28681
28705
 
28682
28706
  // ../wire/dist/useStream.js
28683
- var import_react36 = __toESM(require_react(), 1);
28707
+ var import_react38 = __toESM(require_react(), 1);
28684
28708
  function useStream(channelName) {
28685
28709
  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);
28710
+ const [latest, setLatest] = (0, import_react38.useState)(null);
28711
+ const [all, setAll] = (0, import_react38.useState)([]);
28712
+ const [isComplete, setIsComplete] = (0, import_react38.useState)(false);
28713
+ const allRef = (0, import_react38.useRef)(all);
28690
28714
  allRef.current = all;
28691
- (0, import_react36.useEffect)(() => {
28715
+ (0, import_react38.useEffect)(() => {
28692
28716
  return subscribe(channelName, (delivery) => {
28693
28717
  const typed = delivery.payload;
28694
28718
  setLatest(typed);
@@ -28726,7 +28750,7 @@
28726
28750
  }
28727
28751
 
28728
28752
  // ../wire/dist/useRender.js
28729
- var import_react37 = __toESM(require_react(), 1);
28753
+ var import_react39 = __toESM(require_react(), 1);
28730
28754
 
28731
28755
  // ../wire/dist/connection-store.js
28732
28756
  var ConnectionWriterConflictError = class extends Error {
@@ -28823,7 +28847,7 @@
28823
28847
  // ../wire/dist/useRender.js
28824
28848
  function useRender() {
28825
28849
  const { render } = useWireContext();
28826
- const isConnected = (0, import_react37.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28850
+ const isConnected = (0, import_react39.useSyncExternalStore)(connectionSource.subscribe, connectionSource.getSnapshot, connectionSource.getSnapshot);
28827
28851
  return { sessionId: render.sessionId, isConnected };
28828
28852
  }
28829
28853
 
@@ -29399,6 +29423,7 @@
29399
29423
  }
29400
29424
  }
29401
29425
  };
29426
+ var DEFAULT_ACTION_PENDING_BOUND_MS = 2e4;
29402
29427
  function buildWireConfig(opts) {
29403
29428
  let internalSeq = 0;
29404
29429
  const nextClientSeq = opts.nextClientSeq ?? (() => {
@@ -29407,7 +29432,59 @@
29407
29432
  });
29408
29433
  const validateEnvelope = opts.validateEnvelope ?? validateOutboundActionEnvelope;
29409
29434
  const spentOneShots = /* @__PURE__ */ new Set();
29435
+ const isSpent = (actionName) => opts.getActiveActionSpec()?.[actionName]?.oneShot === true && (spentOneShots.has(actionName) || opts.getSpentOneShots?.()?.includes(actionName) === true);
29436
+ const spentListeners = /* @__PURE__ */ new Set();
29437
+ const notifySpent = () => {
29438
+ for (const listener of [...spentListeners])
29439
+ listener();
29440
+ };
29441
+ const actionSpent = {
29442
+ isSpent,
29443
+ subscribe: (listener) => {
29444
+ spentListeners.add(listener);
29445
+ return () => {
29446
+ spentListeners.delete(listener);
29447
+ };
29448
+ }
29449
+ };
29450
+ opts.spentInputsChanged?.(notifySpent);
29451
+ const pendingBoundMs = opts.actionPendingBoundMs ?? DEFAULT_ACTION_PENDING_BOUND_MS;
29452
+ const pending = /* @__PURE__ */ new Map();
29453
+ const pendingListeners = /* @__PURE__ */ new Set();
29454
+ const notifyPending = () => {
29455
+ for (const listener of [...pendingListeners])
29456
+ listener();
29457
+ };
29458
+ const markPending = (actionName) => {
29459
+ const previous = pending.get(actionName);
29460
+ if (previous !== void 0)
29461
+ clearTimeout(previous);
29462
+ pending.set(actionName, setTimeout(() => {
29463
+ pending.delete(actionName);
29464
+ notifyPending();
29465
+ }, pendingBoundMs));
29466
+ notifyPending();
29467
+ };
29468
+ const actionPending = {
29469
+ isPending: (actionName) => pending.has(actionName),
29470
+ subscribe: (listener) => {
29471
+ pendingListeners.add(listener);
29472
+ return () => {
29473
+ pendingListeners.delete(listener);
29474
+ };
29475
+ }
29476
+ };
29477
+ opts.pendingInputsChanged?.(() => {
29478
+ if (pending.size === 0)
29479
+ return;
29480
+ for (const timer of pending.values())
29481
+ clearTimeout(timer);
29482
+ pending.clear();
29483
+ notifyPending();
29484
+ });
29410
29485
  return {
29486
+ actionSpent,
29487
+ actionPending,
29411
29488
  app: opts.app,
29412
29489
  render: opts.render,
29413
29490
  auth: opts.auth,
@@ -29417,8 +29494,7 @@
29417
29494
  if (actionSpec === void 0)
29418
29495
  opts.onActionSpecAbsent?.(actionName);
29419
29496
  const isOneShot = actionSpec?.[actionName]?.oneShot === true;
29420
- const spent = spentOneShots.has(actionName) || opts.getSpentOneShots?.()?.includes(actionName) === true;
29421
- if (isOneShot && spent) {
29497
+ if (isSpent(actionName)) {
29422
29498
  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
29499
  opts.onDispatchSuppressed?.({
29424
29500
  reason: "one-shot-spent",
@@ -29443,9 +29519,12 @@
29443
29519
  opts.onViolation(new ClientContractViolationError("outbound-action", result.violations));
29444
29520
  return;
29445
29521
  }
29446
- if (isOneShot)
29522
+ if (isOneShot && !spentOneShots.has(actionName)) {
29447
29523
  spentOneShots.add(actionName);
29524
+ notifySpent();
29525
+ }
29448
29526
  opts.emitEnvelope(envelope);
29527
+ markPending(actionName);
29449
29528
  },
29450
29529
  subscribe: (channelName, handler) => {
29451
29530
  return opts.streamBus.subscribe(channelName, (env) => {
@@ -32439,12 +32518,18 @@
32439
32518
 
32440
32519
  // ../design/dist/themes/ThemeProvider.js
32441
32520
  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);
32521
+ var import_react40 = __toESM(require_react(), 1);
32522
+ var ThemeContext = (0, import_react40.createContext)(null);
32444
32523
 
32445
32524
  // ../design/dist/rendering/css-tokens.js
32446
32525
  var EXPANDED_FRAME = { outerRadiusVar: "--ggui-shape-radius-xl", insetPx: 16 };
32447
32526
  var EXPANDED_FRAME_INNER_RADIUS = `max(0px, calc(var(${EXPANDED_FRAME.outerRadiusVar}) - ${EXPANDED_FRAME.insetPx}px))`;
32527
+ var EXPANDED_FRAME_CHROME = {
32528
+ radius: `var(${EXPANDED_FRAME.outerRadiusVar})`,
32529
+ hairline: "color-mix(in srgb, var(--ggui-color-onGround) 6%, transparent)",
32530
+ shadowVar: "--ggui-shape-shadow-sm",
32531
+ ground: "var(--ggui-color-neutral-50)"
32532
+ };
32448
32533
 
32449
32534
  // ../design/dist/rendering/rewrite-imports.js
32450
32535
  var WIRE_EXPORTS = [
@@ -32459,6 +32544,15 @@
32459
32544
  "useGguiContext",
32460
32545
  "useContract",
32461
32546
  "useWireContext",
32547
+ // ggui#1223: a `oneShot` action's spent state, as data. It is served here
32548
+ // one release BEFORE the triad teaches it, because a card that imports a name
32549
+ // its runtime's shim lacks fails at module link. A card generated to use it
32550
+ // must never meet a runtime without it.
32551
+ "useActionSpent",
32552
+ // ggui#1398: whether a dispatched action is waiting for the agent's answer,
32553
+ // as data. Served one release before the triad teaches it, for the same
32554
+ // reason as `useActionSpent` above.
32555
+ "useActionPending",
32462
32556
  // Provider component
32463
32557
  "GguiWireProvider"
32464
32558
  ];
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.25.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/protocol": "0.25.0",
57
+ "@ggui-ai/wire": "0.25.0",
58
+ "@ggui-ai/design": "0.25.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@playwright/test": "^1.60.0",