@copilotkit/learning 0.0.1 → 1.80.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.
- package/README.md +63 -3
- package/dist/index.cjs +335 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +6 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +335 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/scripts/check-size.mjs +2 -2
- package/src/__tests__/clicks.test.ts +499 -1
- package/src/__tests__/collector.test.ts +152 -0
- package/src/__tests__/inputs.test.ts +400 -1
- package/src/__tests__/redact.test.ts +4 -2
- package/src/__tests__/trajectory-collector.test.ts +105 -0
- package/src/clicks.ts +308 -3
- package/src/collector.ts +11 -5
- package/src/inputs.ts +167 -19
- package/src/redact.ts +7 -2
- package/src/trajectory-collector.ts +16 -2
- package/src/types.ts +6 -0
- package/CHANGELOG.md +0 -9
package/src/inputs.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import { describeTarget, readControlValue } from "./clicks";
|
|
2
2
|
import { createRedactor } from "./redact";
|
|
3
3
|
import type { Redactor } from "./redact";
|
|
4
|
-
import
|
|
4
|
+
import { REDACTED } from "./types";
|
|
5
|
+
import type { ClickTarget, Emit } from "./types";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
const TEXT_DEBOUNCE_MS = 300;
|
|
8
|
+
const TEXT_INPUT_TYPES = new Set([
|
|
9
|
+
"text",
|
|
10
|
+
"search",
|
|
11
|
+
"tel",
|
|
12
|
+
"url",
|
|
13
|
+
"email",
|
|
14
|
+
"password",
|
|
15
|
+
"number",
|
|
16
|
+
]);
|
|
17
|
+
type ControlValue = ReturnType<typeof readControlValue>;
|
|
18
|
+
interface Edit {
|
|
19
|
+
state: ControlValue;
|
|
20
|
+
payload: {
|
|
21
|
+
eventType: string;
|
|
22
|
+
target: Omit<ClickTarget, "input">;
|
|
23
|
+
url: string;
|
|
24
|
+
route: string;
|
|
25
|
+
};
|
|
26
|
+
timer?: ReturnType<typeof setTimeout>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Records settled user edits without interfering with the host app's controls. */
|
|
7
30
|
export function installInputCapture(params: {
|
|
8
31
|
emit: Emit;
|
|
9
32
|
isTrusted?: (event: Event) => boolean;
|
|
@@ -14,29 +37,154 @@ export function installInputCapture(params: {
|
|
|
14
37
|
isTrusted = (event: Event) => event.isTrusted,
|
|
15
38
|
redact = createRedactor(),
|
|
16
39
|
} = params;
|
|
17
|
-
|
|
40
|
+
let active = true;
|
|
41
|
+
const pending = new Map<Element, Edit>();
|
|
42
|
+
// A native change can echo a settled input on blur. Consume that one echo;
|
|
43
|
+
// every fresh trusted input is a new edit, even if its redacted value matches.
|
|
44
|
+
const changeEcho = new WeakMap<Element, string>();
|
|
45
|
+
const composing = new WeakMap<Element, boolean>();
|
|
46
|
+
|
|
47
|
+
const cancel = (element: Element) => {
|
|
48
|
+
clearTimeout(pending.get(element)?.timer);
|
|
49
|
+
pending.delete(element);
|
|
50
|
+
};
|
|
51
|
+
const deliver = (element: Element, edit: Edit) => {
|
|
18
52
|
try {
|
|
19
|
-
if
|
|
20
|
-
|
|
21
|
-
if (!(element
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
// Recheck privacy, but retain the observed value and route even if a host
|
|
54
|
+
// handler reset or removed the control before this trailing delivery.
|
|
55
|
+
if (!active || describeTarget(element, redact) === null) return;
|
|
56
|
+
// A host can reset the live value before making this field sensitive.
|
|
57
|
+
// Drop the old snapshot: mirrored text/attributes may contain its value.
|
|
58
|
+
if (redact.isPassword(element) && edit.state.value !== REDACTED) return;
|
|
59
|
+
if (edit.payload.eventType === "input") {
|
|
60
|
+
changeEcho.set(element, JSON.stringify(edit.state));
|
|
61
|
+
}
|
|
62
|
+
edit.payload.url = redact.url(edit.payload.url);
|
|
63
|
+
edit.payload.target.text = redact.url(edit.payload.target.text);
|
|
64
|
+
edit.payload.target.attributes = Object.fromEntries(
|
|
65
|
+
Object.entries(edit.payload.target.attributes).map(([key, value]) => [
|
|
66
|
+
key,
|
|
67
|
+
redact.url(value),
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
emit("input", edit.payload);
|
|
71
|
+
} catch {
|
|
72
|
+
// Capture must never break an edit or a later action in the host app.
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const flush = () => {
|
|
76
|
+
// Clear before callbacks: an emit can synchronously stop or restart capture.
|
|
77
|
+
const edits = [...pending];
|
|
78
|
+
for (const [element] of edits) cancel(element);
|
|
79
|
+
for (const [element, edit] of edits) deliver(element, edit);
|
|
80
|
+
};
|
|
81
|
+
const observe = (element: Element, eventType: string, settle: boolean) => {
|
|
82
|
+
if (pending.size > 0 && !pending.has(element)) flush();
|
|
83
|
+
if (!active) return;
|
|
84
|
+
const described = describeTarget(element, redact);
|
|
85
|
+
if (described === null) {
|
|
86
|
+
cancel(element);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const state = readControlValue(element, redact);
|
|
90
|
+
if (!("value" in state)) return;
|
|
91
|
+
const text =
|
|
92
|
+
element instanceof HTMLTextAreaElement ||
|
|
93
|
+
(element instanceof HTMLInputElement &&
|
|
94
|
+
TEXT_INPUT_TYPES.has(element.type)) ||
|
|
95
|
+
element.matches('[contenteditable]:not([contenteditable="false"])');
|
|
96
|
+
if (!text) flush();
|
|
97
|
+
const signature = JSON.stringify(state);
|
|
98
|
+
const echo =
|
|
99
|
+
eventType === "change" &&
|
|
100
|
+
(signature === JSON.stringify(pending.get(element)?.state) ||
|
|
101
|
+
signature === changeEcho.get(element));
|
|
102
|
+
changeEcho.delete(element);
|
|
103
|
+
if (echo) {
|
|
104
|
+
flush();
|
|
105
|
+
changeEcho.delete(element);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
cancel(element);
|
|
109
|
+
const edit: Edit = {
|
|
110
|
+
state,
|
|
111
|
+
payload: {
|
|
112
|
+
eventType,
|
|
113
|
+
target: { ...described.target, ...state },
|
|
29
114
|
url: redact.url(location.href),
|
|
30
115
|
route: location.pathname,
|
|
31
|
-
}
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
if (!text) {
|
|
119
|
+
deliver(element, edit);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (settle) {
|
|
123
|
+
edit.timer = setTimeout(() => {
|
|
124
|
+
cancel(element);
|
|
125
|
+
deliver(element, edit);
|
|
126
|
+
}, TEXT_DEBOUNCE_MS);
|
|
127
|
+
}
|
|
128
|
+
pending.set(element, edit);
|
|
129
|
+
};
|
|
130
|
+
const onEdit = (event: Event) => {
|
|
131
|
+
try {
|
|
132
|
+
if (!active || !isTrusted(event)) return;
|
|
133
|
+
const [element] = event.composedPath();
|
|
134
|
+
if (!(element instanceof Element)) return;
|
|
135
|
+
if (event.type === "compositionstart") {
|
|
136
|
+
composing.set(element, true);
|
|
137
|
+
clearTimeout(pending.get(element)?.timer);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (event.type === "compositionend") composing.set(element, false);
|
|
141
|
+
// Keep a draft during composition for explicit action/stop flushes. An
|
|
142
|
+
// end event takes precedence over a late input's stale isComposing flag.
|
|
143
|
+
const inComposition =
|
|
144
|
+
composing.get(element) === true ||
|
|
145
|
+
(composing.get(element) !== false &&
|
|
146
|
+
"isComposing" in event &&
|
|
147
|
+
event.isComposing === true);
|
|
148
|
+
observe(
|
|
149
|
+
element,
|
|
150
|
+
event.type === "compositionend" ? "input" : event.type,
|
|
151
|
+
!inComposition,
|
|
152
|
+
);
|
|
153
|
+
if (event.type === "change") flush();
|
|
32
154
|
} catch {
|
|
33
155
|
// Capture must never break an edit in the host app.
|
|
34
156
|
}
|
|
35
157
|
};
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
158
|
+
const onAction = (event: Event) => {
|
|
159
|
+
if (event.type !== "pagehide" && !isTrusted(event)) return;
|
|
160
|
+
if (event.type === "keydown") {
|
|
161
|
+
const [element] = event.composedPath();
|
|
162
|
+
// Window capture runs before no-form send handlers can reset the control.
|
|
163
|
+
// IME completion can precede its final keydown, which still reports 229.
|
|
164
|
+
if (
|
|
165
|
+
!(event instanceof KeyboardEvent) ||
|
|
166
|
+
event.key !== "Enter" ||
|
|
167
|
+
event.isComposing ||
|
|
168
|
+
event.keyCode === 229 ||
|
|
169
|
+
(element instanceof Element && composing.get(element) === true)
|
|
170
|
+
)
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
flush();
|
|
174
|
+
};
|
|
175
|
+
const editTypes = ["input", "change", "compositionstart", "compositionend"];
|
|
176
|
+
const actionTypes = ["click", "submit", "focusout", "pagehide", "keydown"];
|
|
177
|
+
for (const type of editTypes)
|
|
178
|
+
window.addEventListener(type, onEdit, { capture: true, passive: true });
|
|
179
|
+
for (const type of actionTypes)
|
|
180
|
+
window.addEventListener(type, onAction, { capture: true, passive: true });
|
|
181
|
+
const uninstall = () => {
|
|
182
|
+
active = false;
|
|
183
|
+
for (const element of pending.keys()) cancel(element);
|
|
184
|
+
for (const type of editTypes)
|
|
185
|
+
window.removeEventListener(type, onEdit, true);
|
|
186
|
+
for (const type of actionTypes)
|
|
187
|
+
window.removeEventListener(type, onAction, true);
|
|
41
188
|
};
|
|
189
|
+
return Object.assign(uninstall, { flush });
|
|
42
190
|
}
|
package/src/redact.ts
CHANGED
|
@@ -148,8 +148,11 @@ export function createRedactor() {
|
|
|
148
148
|
seen.add(record.target as Element);
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
/**
|
|
152
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Replaces remembered values, raw and in their JSON and URL encodings, plus
|
|
153
|
+
* `normalize(value)` when given and at least 4 characters long.
|
|
154
|
+
*/
|
|
155
|
+
const scrub = (text: string, normalize?: (value: string) => string) => {
|
|
153
156
|
for (const value of [...remembered.values()].sort(
|
|
154
157
|
(a, b) => b.length - a.length,
|
|
155
158
|
))
|
|
@@ -158,6 +161,7 @@ export function createRedactor() {
|
|
|
158
161
|
JSON.stringify(value).slice(1, -1),
|
|
159
162
|
encodeURIComponent(value),
|
|
160
163
|
new URLSearchParams({ v: value }).toString().slice(2),
|
|
164
|
+
...[normalize?.(value) ?? ""].filter((normal) => normal.length >= 4),
|
|
161
165
|
])
|
|
162
166
|
text = /^\d+$/.test(form)
|
|
163
167
|
? // One-time codes must not match inside longer numbers.
|
|
@@ -191,6 +195,7 @@ export function createRedactor() {
|
|
|
191
195
|
|
|
192
196
|
return {
|
|
193
197
|
isPassword,
|
|
198
|
+
scrub,
|
|
194
199
|
/**
|
|
195
200
|
* Tracks password fields for the session: existing ones, `type` toggles away
|
|
196
201
|
* from "password", and typed values even when input capture is off.
|
|
@@ -53,7 +53,9 @@ export function createTrajectoryCollector(
|
|
|
53
53
|
},
|
|
54
54
|
): TrajectoryCollector {
|
|
55
55
|
let active = false;
|
|
56
|
+
let generation = 0;
|
|
56
57
|
let uninstalls: (() => void)[] = [];
|
|
58
|
+
let flushInputs: (() => void) | undefined;
|
|
57
59
|
|
|
58
60
|
const report = (code: string, message: string) => {
|
|
59
61
|
try {
|
|
@@ -67,6 +69,9 @@ export function createTrajectoryCollector(
|
|
|
67
69
|
|
|
68
70
|
const record = (name: string, value: JsonValue) => {
|
|
69
71
|
if (!active) return;
|
|
72
|
+
const current = generation;
|
|
73
|
+
if (name !== "input" && name !== "network") flushInputs?.();
|
|
74
|
+
if (!active || generation !== current) return;
|
|
70
75
|
let payload: TrajectoryEvent;
|
|
71
76
|
try {
|
|
72
77
|
const event: TrajectoryEvent = {
|
|
@@ -77,7 +82,7 @@ export function createTrajectoryCollector(
|
|
|
77
82
|
};
|
|
78
83
|
const kept =
|
|
79
84
|
options.beforeSend === undefined ? event : options.beforeSend(event);
|
|
80
|
-
if (kept === null || !active) return;
|
|
85
|
+
if (kept === null || !active || generation !== current) return;
|
|
81
86
|
if (
|
|
82
87
|
kept.type !== "CUSTOM" ||
|
|
83
88
|
typeof kept.name !== "string" ||
|
|
@@ -130,6 +135,12 @@ export function createTrajectoryCollector(
|
|
|
130
135
|
};
|
|
131
136
|
|
|
132
137
|
const stop = () => {
|
|
138
|
+
const current = generation;
|
|
139
|
+
const flush = flushInputs;
|
|
140
|
+
flushInputs = undefined;
|
|
141
|
+
flush?.();
|
|
142
|
+
if (generation !== current) return;
|
|
143
|
+
generation++;
|
|
133
144
|
active = false;
|
|
134
145
|
const installed = uninstalls;
|
|
135
146
|
uninstalls = [];
|
|
@@ -140,6 +151,7 @@ export function createTrajectoryCollector(
|
|
|
140
151
|
start() {
|
|
141
152
|
if (active || typeof window === "undefined") return;
|
|
142
153
|
active = true;
|
|
154
|
+
generation++;
|
|
143
155
|
// Native callers can emit developer events without browser observers.
|
|
144
156
|
if (typeof window.addEventListener !== "function") return;
|
|
145
157
|
// One per session: fields seen and values typed are forgotten on stop().
|
|
@@ -155,7 +167,9 @@ export function createTrajectoryCollector(
|
|
|
155
167
|
uninstalls.push(installNavigationCapture({ emit: capture, redact }));
|
|
156
168
|
}
|
|
157
169
|
if (options.capture?.inputs !== false) {
|
|
158
|
-
|
|
170
|
+
const inputs = installInputCapture({ emit: capture, redact });
|
|
171
|
+
flushInputs = inputs.flush;
|
|
172
|
+
uninstalls.push(inputs);
|
|
159
173
|
}
|
|
160
174
|
if (options.capture?.network !== false) {
|
|
161
175
|
uninstalls.push(
|
package/src/types.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface ClickTarget {
|
|
|
49
49
|
role: string | null;
|
|
50
50
|
action: string | null;
|
|
51
51
|
text: string;
|
|
52
|
+
/**
|
|
53
|
+
* Short, cleaned name of the clicked control (click events only). Comes from
|
|
54
|
+
* `aria-labelledby`, `aria-label`, an input caption or `alt`, `title`, or a
|
|
55
|
+
* control's own text. Omitted when no safe name exists.
|
|
56
|
+
*/
|
|
57
|
+
label?: string;
|
|
52
58
|
attributes: Record<string, string>;
|
|
53
59
|
value?: string;
|
|
54
60
|
checked?: boolean;
|
package/CHANGELOG.md
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# Changelog — learning lane
|
|
2
|
-
|
|
3
|
-
`@copilotkit/learning` versions independently of the monorepo packages
|
|
4
|
-
(`scopes.learning` in `release.config.json`).
|
|
5
|
-
|
|
6
|
-
`release / create-pr` prepends a section here for each release, and
|
|
7
|
-
`release / publish` reads that section back as the GitHub Release body.
|
|
8
|
-
|
|
9
|
-
Entries begin with the first independent release cut after this file was added.
|