@classic-homes/page-agent-core 0.2.0 → 0.2.1

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 CHANGED
@@ -47,10 +47,14 @@ ignoreSelector })` and `executePageCall(call)`, the single entry point for
47
47
  the model's tool calls. `PAGE_TOOL_NAMES` and `PAGE_TOOLS` (plain-JSON
48
48
  input shapes) let the agent server mirror the contract exactly.
49
49
  - **Visibility**: `measureVisibility(el)` says what of a target the user can
50
- see: clipped to the viewport and its scroll boxes, cut where a sticky
51
- header / footer / dock covers it (hit-tested with
52
- `document.elementsFromPoint`, the layer skipped), and `covered` behind a
53
- modal dialog or `inert`. Bindings draw only `visible` and hide (not
50
+ see: clipped to the viewport and its scroll boxes, cut where a strip
51
+ covers one side of it (a sticky header / footer / dock, or a menu over one
52
+ end: a cover spanning the target's visible part along one axis and
53
+ reaching past an edge on the other, viewport-wide or not; cuts combine),
54
+ drawn over smaller covers (hit-tested with `document.elementsFromPoint`
55
+ at a 3 x 3 grid plus a probe inside each edge, the layer skipped), and
56
+ `covered` under a cover that hides most of it, behind a modal dialog or in
57
+ `inert`. Bindings draw only `visible` and hide (not
54
58
  remove) effects while it is null. The maths is the pure
55
59
  `computeVisibility({ rect, viewport, clips, behindModal, coverAt })`.
56
60
  - **Collapsed tab rows**: `collapsedTabMenu(tab)` and the
@@ -158,7 +162,8 @@ target) and a live region (`.chpa-sr`). Override `--chpa-accent` (default
158
162
  `#6d28d9`) and `--chpa-z` (default `2147482000`, below the Access
159
163
  Inspector). The layer stays on top so targets inside dialogs can be pointed
160
164
  at; covers are handled by drawing only the visible part (an outline cut by
161
- a header carries `data-cut="top"` and is left open on that side). Dark mode follows `.dark` / `[data-theme='dark']`; reduced
165
+ a header carries `data-cut="top"`, one cut by a menu over its right end
166
+ `data-cut="right"`, and is left open on that side). Dark mode follows `.dark` / `[data-theme='dark']`; reduced
162
167
  motion drops the glide and pulse.
163
168
 
164
169
  ## Testing
package/dist/index.d.ts CHANGED
@@ -265,7 +265,7 @@ declare function placeLabel(rect: RectLike, size: Size, viewport: Size, opts?: {
265
265
  declare function pointerTarget(rect: RectLike, viewport: Size, margin?: number): Point;
266
266
  /** Overlap of two boxes; null when they do not overlap. */
267
267
  declare function intersectBoxes(a: RectLike, b: RectLike): Box | null;
268
- /** Which sides of a target are cut off (by clipping, the viewport or a covering band). */
268
+ /** Which sides of a target are cut off (by clipping, the viewport or a covering strip). */
269
269
  interface CutSides {
270
270
  top: boolean;
271
271
  right: boolean;
@@ -289,14 +289,18 @@ interface TargetVisibility {
289
289
  visible: Box | null;
290
290
  /**
291
291
  * The area the target is shown in: the viewport, narrowed by clipping
292
- * ancestors and by edge bands (sticky header, footer, side dock) that
293
- * cover it. Captions are kept inside it. Null when there is none.
292
+ * ancestors and by the strips (sticky header, footer, side dock, a menu
293
+ * over one end) that cut it. Captions are kept inside it. Null when there
294
+ * is none.
294
295
  */
295
296
  bounds: Box | null;
296
297
  cut: CutSides;
297
298
  /** With `covered`: a modal dialog / inert page, or another element on top. */
298
299
  coveredBy?: 'modal' | 'element';
299
- /** Edge bands that cover part (or all) of the target, as measured. */
300
+ /**
301
+ * Strips that cut the target (or a header-like cover over all of it), as
302
+ * measured: sticky headers and footers, docks, menus over one side.
303
+ */
300
304
  bands: Box[];
301
305
  }
302
306
  interface VisibilityInput {
@@ -312,7 +316,11 @@ interface VisibilityInput {
312
316
  * viewport point, or null when the target itself (or nothing) is.
313
317
  */
314
318
  coverAt?: (x: number, y: number) => RectLike | null;
315
- /** Sample grid per side (default 3, so 9 points). */
319
+ /**
320
+ * Sample grid per side (default 3, so 9 points), plus one probe just
321
+ * inside the middle of each edge of the visible part (see
322
+ * {@link computeVisibility}).
323
+ */
316
324
  grid?: number;
317
325
  }
318
326
  /**
@@ -321,13 +329,25 @@ interface VisibilityInput {
321
329
  *
322
330
  * Occlusion rules, chosen to be robust with coarse sampling:
323
331
  * - something that covers the whole visible part (a dialog backdrop) hides it;
324
- * - an edge band (a box spanning the visible part's full width or height and
325
- * reaching past one edge, like a sticky header) is cut off exactly at the
326
- * band's edge, so the effect stops where the header starts;
327
- * - anything else on top (a floating button, a small popover) hides the
328
- * target only when it covers most of the remaining sample points, and is
329
- * otherwise drawn over: clipping around arbitrary shapes is not worth the
330
- * wrong guesses.
332
+ * - a side strip, a cover that spans the visible part's full height and
333
+ * reaches past its left or right edge (a side dock, a menu over the end of
334
+ * a heading), or its full width and past its top or bottom edge (a sticky
335
+ * header or footer), cuts the target off exactly at the cover's edge, so
336
+ * the effect stops where the cover starts. The cover does not have to
337
+ * span the viewport, and cuts on several sides combine;
338
+ * - anything else on top (a floating button over a corner, a popover over
339
+ * the middle) hides the target only when it covers most of the grid
340
+ * points left uncut, and is otherwise drawn over: clipping around
341
+ * arbitrary shapes is not worth the wrong guesses.
342
+ *
343
+ * Sampling: a `grid` x `grid` set of points over the visible part (weighs
344
+ * covers for the "most of it" rule), plus a probe just inside the middle of
345
+ * each edge (finds side strips the grid falls short of, such as a menu over
346
+ * the last tenth of a wide heading: any strip of an edge's full length
347
+ * contains that edge's midpoint). After each cut the new edges are probed
348
+ * again, so a strip that only spans the part below a header is found too.
349
+ * Edge probes only find strips; they never count towards "most of it".
350
+ * 9 + 4 hit tests for an unobstructed target, 4 more per cut.
331
351
  */
332
352
  declare function computeVisibility(input: VisibilityInput): TargetVisibility;
333
353
 
@@ -581,7 +601,7 @@ declare function isBehindModal(el: Element): boolean;
581
601
  * docks) hit-tested. See {@link computeVisibility} for the rules. Style
582
602
  * checks (`hidden`, `display: none`, …) are not repeated here; callers
583
603
  * check {@link isRendered} first. Cheap enough per animation frame for a
584
- * handful of effects (9 hit tests each).
604
+ * handful of effects (13 hit tests each, 4 more per cut side; reads only).
585
605
  */
586
606
  declare function measureVisibility(el: Element): TargetVisibility;
587
607
 
package/dist/index.js CHANGED
@@ -211,6 +211,8 @@ function pointerTarget(rect, viewport, margin = 12) {
211
211
  }
212
212
  var EDGE = 1;
213
213
  var MIN_VISIBLE = 4;
214
+ var PROBE_INSET = 2;
215
+ var MAX_TRIM_ROUNDS = 6;
214
216
  var viewportBox = (viewport) => ({
215
217
  top: 0,
216
218
  left: 0,
@@ -270,11 +272,24 @@ function computeVisibility(input) {
270
272
  }
271
273
  }
272
274
  const covers = [];
273
- for (const s of samples) {
274
- if (s.cover && !covers.some((c) => sameBox(c, s.cover))) covers.push(s.cover);
275
- }
275
+ const addCover = (cover) => {
276
+ if (cover && !covers.some((c) => sameBox(c, cover))) covers.push(cover);
277
+ };
278
+ for (const s of samples) addCover(s.cover);
279
+ const probed = /* @__PURE__ */ new Set();
280
+ const probeEdges = (box) => {
281
+ for (const [x, y] of edgeProbes(box)) {
282
+ const key = `${Math.round(x * 2)},${Math.round(y * 2)}`;
283
+ if (probed.has(key)) continue;
284
+ probed.add(key);
285
+ const hit = input.coverAt(x, y);
286
+ const cover = hit ? toBox(hit) : null;
287
+ addCover(cover && containsPoint(cover, x, y) ? cover : null);
288
+ }
289
+ };
290
+ probeEdges(visible);
276
291
  let trimmed = true;
277
- while (trimmed && visible) {
292
+ for (let round = 0; trimmed && visible && round < MAX_TRIM_ROUNDS; round++) {
278
293
  trimmed = false;
279
294
  for (const cover of covers) {
280
295
  if (bands.includes(cover) || !visible || !intersectBoxes(cover, visible)) continue;
@@ -293,6 +308,9 @@ function computeVisibility(input) {
293
308
  bands.push(cover);
294
309
  trimmed = true;
295
310
  }
311
+ if (trimmed && visible && visible.width >= MIN_VISIBLE && visible.height >= MIN_VISIBLE) {
312
+ probeEdges(visible);
313
+ }
296
314
  }
297
315
  if (!visible || visible.width < MIN_VISIBLE || visible.height < MIN_VISIBLE) {
298
316
  return result("covered", null, { coveredBy: "element", bands });
@@ -307,6 +325,18 @@ function computeVisibility(input) {
307
325
  const whole = !cut.top && !cut.right && !cut.bottom && !cut.left;
308
326
  return { state: whole ? "visible" : "partial", rect, visible, bounds, cut, bands };
309
327
  }
328
+ function edgeProbes(box) {
329
+ const dx = Math.min(PROBE_INSET, box.width / 4);
330
+ const dy = Math.min(PROBE_INSET, box.height / 4);
331
+ const cx = box.left + box.width / 2;
332
+ const cy = box.top + box.height / 2;
333
+ return [
334
+ [cx, box.top + dy],
335
+ [box.left + box.width - dx, cy],
336
+ [cx, box.top + box.height - dy],
337
+ [box.left + dx, cy]
338
+ ];
339
+ }
310
340
  function sameBox(a, b) {
311
341
  return a.top === b.top && a.left === b.left && a.width === b.width && a.height === b.height;
312
342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/page-agent-core",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Framework-agnostic page layer for an in-app AI agent: named targets, safe target resolution, a page snapshot, highlight / spotlight / pointer effects and one schema-validated executor for model tool calls",
5
5
  "type": "module",
6
6
  "module": "dist/index.js",