@cairnvibe/sdk 0.2.10 → 0.2.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/dist/runtime-scan.js +42 -8
- package/package.json +1 -1
- package/src/runtime-scan.ts +43 -8
package/dist/runtime-scan.js
CHANGED
|
@@ -3,11 +3,9 @@
|
|
|
3
3
|
// the build-time manifest, and not limited to elements a developer
|
|
4
4
|
// remembered to tag with data-ai. This is what lets the agent address a
|
|
5
5
|
// dynamically-rendered row (a session card, a list item) it was never told
|
|
6
|
-
// about ahead of time
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// with no semantic role is invisible to this, same limit the existing
|
|
10
|
-
// ladder already has.
|
|
6
|
+
// about ahead of time — AND a plain `<div onClick>` styled as a button (see
|
|
7
|
+
// hasClickHandler below), which a selector alone could never catch, since
|
|
8
|
+
// React never writes an "onclick" HTML attribute for a JSX onClick prop.
|
|
11
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
10
|
exports.scanInteractiveElements = scanInteractiveElements;
|
|
13
11
|
exports.createLiveElementRegistry = createLiveElementRegistry;
|
|
@@ -75,13 +73,41 @@ function labelFor(el) {
|
|
|
75
73
|
const trimmed = raw.replace(/\s+/g, " ").trim();
|
|
76
74
|
return trimmed.length > MAX_LABEL_LENGTH ? `${trimmed.slice(0, MAX_LABEL_LENGTH - 1)}…` : trimmed;
|
|
77
75
|
}
|
|
78
|
-
function roleFor(el) {
|
|
76
|
+
function roleFor(el, nonSemanticClickable = false) {
|
|
79
77
|
if (el.getAttribute("role"))
|
|
80
78
|
return el.getAttribute("role");
|
|
81
79
|
if (isFormField(el))
|
|
82
80
|
return "input";
|
|
81
|
+
// A real tag name ("div") tells the model nothing useful about a plain
|
|
82
|
+
// `<div onClick>` — "clickable" is what actually matters to it here.
|
|
83
|
+
if (nonSemanticClickable)
|
|
84
|
+
return "clickable";
|
|
83
85
|
return el.tagName.toLowerCase();
|
|
84
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* True for an element with a real click handler that CANDIDATE_SELECTOR's
|
|
89
|
+
* semantic tags/roles can't catch — a `<div onClick>` or similar, common in
|
|
90
|
+
* component libraries that style a div/span as a button instead of using a
|
|
91
|
+
* real <button>. `el.onclick` covers the plain-JS `el.onclick = fn` case;
|
|
92
|
+
* the __reactProps$/__reactEventHandlers$ scan covers React's synthetic
|
|
93
|
+
* `onClick` prop, which never appears as an "onclick" HTML attribute a CSS
|
|
94
|
+
* selector could match. `addEventListener`-only handlers (React-free code
|
|
95
|
+
* calling it directly) aren't introspectable from page JS at all — an
|
|
96
|
+
* honest, known gap, not attempted here. Whichever way it's wired,
|
|
97
|
+
* el.click() (verb-executor.ts's click execution) still fires it correctly
|
|
98
|
+
* either way — a real click() call dispatches a genuine bubbling
|
|
99
|
+
* MouseEvent, exactly what both a native `onclick` and React's delegated
|
|
100
|
+
* listener are listening for.
|
|
101
|
+
*/
|
|
102
|
+
function hasClickHandler(el) {
|
|
103
|
+
if (typeof el.onclick === "function")
|
|
104
|
+
return true;
|
|
105
|
+
const reactPropsKey = Object.keys(el).find((k) => k.startsWith("__reactProps$") || k.startsWith("__reactEventHandlers$"));
|
|
106
|
+
if (!reactPropsKey)
|
|
107
|
+
return false;
|
|
108
|
+
const props = el[reactPropsKey];
|
|
109
|
+
return typeof props?.onClick === "function";
|
|
110
|
+
}
|
|
85
111
|
/**
|
|
86
112
|
* Scans the live DOM for interactive elements, on screen right now or just
|
|
87
113
|
* off it (see viewportDistance) — anything rendered on the page at all, not
|
|
@@ -98,7 +124,15 @@ function scanInteractiveElements(root = document) {
|
|
|
98
124
|
let counter = 0;
|
|
99
125
|
if (typeof document === "undefined")
|
|
100
126
|
return { elements, byId };
|
|
101
|
-
const
|
|
127
|
+
const semanticCandidates = Array.from(root.querySelectorAll(CANDIDATE_SELECTOR)).filter(isRendered);
|
|
128
|
+
const semanticSet = new Set(semanticCandidates);
|
|
129
|
+
// A second pass, only over what the selector-based pass didn't already
|
|
130
|
+
// catch — a plain `<div onClick>`/`<span onClick>` with no semantic tag
|
|
131
|
+
// or role at all (see hasClickHandler's doc comment for why a selector
|
|
132
|
+
// alone can't find these).
|
|
133
|
+
const nonSemanticCandidates = Array.from(root.querySelectorAll("*")).filter((el) => !semanticSet.has(el) && isRendered(el) && hasClickHandler(el));
|
|
134
|
+
const nonSemanticSet = new Set(nonSemanticCandidates);
|
|
135
|
+
const candidates = [...semanticCandidates, ...nonSemanticCandidates];
|
|
102
136
|
candidates.sort((a, b) => viewportDistance(a) - viewportDistance(b));
|
|
103
137
|
for (const el of candidates) {
|
|
104
138
|
if (elements.length >= MAX_ELEMENTS)
|
|
@@ -111,7 +145,7 @@ function scanInteractiveElements(root = document) {
|
|
|
111
145
|
if (!label)
|
|
112
146
|
continue; // nothing to address it by — skip rather than send an empty label
|
|
113
147
|
byId.set(id, el);
|
|
114
|
-
elements.push({ id, role: roleFor(el), label });
|
|
148
|
+
elements.push({ id, role: roleFor(el, nonSemanticSet.has(el)), label });
|
|
115
149
|
}
|
|
116
150
|
return { elements, byId };
|
|
117
151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cairnvibe/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "In-app AI copilot — <Copilot/> for React/Next.js, <cairn-widget> for any framework — plus the server handlers and realtime voice relay behind them.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": { "access": "public" },
|
package/src/runtime-scan.ts
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
// the build-time manifest, and not limited to elements a developer
|
|
3
3
|
// remembered to tag with data-ai. This is what lets the agent address a
|
|
4
4
|
// dynamically-rendered row (a session card, a list item) it was never told
|
|
5
|
-
// about ahead of time
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// with no semantic role is invisible to this, same limit the existing
|
|
9
|
-
// ladder already has.
|
|
5
|
+
// about ahead of time — AND a plain `<div onClick>` styled as a button (see
|
|
6
|
+
// hasClickHandler below), which a selector alone could never catch, since
|
|
7
|
+
// React never writes an "onclick" HTML attribute for a JSX onClick prop.
|
|
10
8
|
|
|
11
9
|
import type { LiveElement } from "@cairnvibe/core";
|
|
12
10
|
|
|
@@ -85,12 +83,38 @@ function labelFor(el: HTMLElement): string {
|
|
|
85
83
|
return trimmed.length > MAX_LABEL_LENGTH ? `${trimmed.slice(0, MAX_LABEL_LENGTH - 1)}…` : trimmed;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
function roleFor(el: HTMLElement): string {
|
|
86
|
+
function roleFor(el: HTMLElement, nonSemanticClickable = false): string {
|
|
89
87
|
if (el.getAttribute("role")) return el.getAttribute("role")!;
|
|
90
88
|
if (isFormField(el)) return "input";
|
|
89
|
+
// A real tag name ("div") tells the model nothing useful about a plain
|
|
90
|
+
// `<div onClick>` — "clickable" is what actually matters to it here.
|
|
91
|
+
if (nonSemanticClickable) return "clickable";
|
|
91
92
|
return el.tagName.toLowerCase();
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* True for an element with a real click handler that CANDIDATE_SELECTOR's
|
|
97
|
+
* semantic tags/roles can't catch — a `<div onClick>` or similar, common in
|
|
98
|
+
* component libraries that style a div/span as a button instead of using a
|
|
99
|
+
* real <button>. `el.onclick` covers the plain-JS `el.onclick = fn` case;
|
|
100
|
+
* the __reactProps$/__reactEventHandlers$ scan covers React's synthetic
|
|
101
|
+
* `onClick` prop, which never appears as an "onclick" HTML attribute a CSS
|
|
102
|
+
* selector could match. `addEventListener`-only handlers (React-free code
|
|
103
|
+
* calling it directly) aren't introspectable from page JS at all — an
|
|
104
|
+
* honest, known gap, not attempted here. Whichever way it's wired,
|
|
105
|
+
* el.click() (verb-executor.ts's click execution) still fires it correctly
|
|
106
|
+
* either way — a real click() call dispatches a genuine bubbling
|
|
107
|
+
* MouseEvent, exactly what both a native `onclick` and React's delegated
|
|
108
|
+
* listener are listening for.
|
|
109
|
+
*/
|
|
110
|
+
function hasClickHandler(el: HTMLElement): boolean {
|
|
111
|
+
if (typeof (el as unknown as { onclick?: unknown }).onclick === "function") return true;
|
|
112
|
+
const reactPropsKey = Object.keys(el).find((k) => k.startsWith("__reactProps$") || k.startsWith("__reactEventHandlers$"));
|
|
113
|
+
if (!reactPropsKey) return false;
|
|
114
|
+
const props = (el as unknown as Record<string, { onClick?: unknown } | undefined>)[reactPropsKey];
|
|
115
|
+
return typeof props?.onClick === "function";
|
|
116
|
+
}
|
|
117
|
+
|
|
94
118
|
/**
|
|
95
119
|
* Scans the live DOM for interactive elements, on screen right now or just
|
|
96
120
|
* off it (see viewportDistance) — anything rendered on the page at all, not
|
|
@@ -108,7 +132,18 @@ export function scanInteractiveElements(root: ParentNode = document): LiveScan {
|
|
|
108
132
|
|
|
109
133
|
if (typeof document === "undefined") return { elements, byId };
|
|
110
134
|
|
|
111
|
-
const
|
|
135
|
+
const semanticCandidates = Array.from(root.querySelectorAll<HTMLElement>(CANDIDATE_SELECTOR)).filter(isRendered);
|
|
136
|
+
const semanticSet = new Set(semanticCandidates);
|
|
137
|
+
// A second pass, only over what the selector-based pass didn't already
|
|
138
|
+
// catch — a plain `<div onClick>`/`<span onClick>` with no semantic tag
|
|
139
|
+
// or role at all (see hasClickHandler's doc comment for why a selector
|
|
140
|
+
// alone can't find these).
|
|
141
|
+
const nonSemanticCandidates = Array.from(root.querySelectorAll<HTMLElement>("*")).filter(
|
|
142
|
+
(el) => !semanticSet.has(el) && isRendered(el) && hasClickHandler(el),
|
|
143
|
+
);
|
|
144
|
+
const nonSemanticSet = new Set(nonSemanticCandidates);
|
|
145
|
+
|
|
146
|
+
const candidates = [...semanticCandidates, ...nonSemanticCandidates];
|
|
112
147
|
candidates.sort((a, b) => viewportDistance(a) - viewportDistance(b));
|
|
113
148
|
|
|
114
149
|
for (const el of candidates) {
|
|
@@ -122,7 +157,7 @@ export function scanInteractiveElements(root: ParentNode = document): LiveScan {
|
|
|
122
157
|
if (!label) continue; // nothing to address it by — skip rather than send an empty label
|
|
123
158
|
|
|
124
159
|
byId.set(id, el);
|
|
125
|
-
elements.push({ id, role: roleFor(el), label });
|
|
160
|
+
elements.push({ id, role: roleFor(el, nonSemanticSet.has(el)), label });
|
|
126
161
|
}
|
|
127
162
|
|
|
128
163
|
return { elements, byId };
|