sentienceapi 0.95.0__py3-none-any.whl

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.

Potentially problematic release.


This version of sentienceapi might be problematic. Click here for more details.

Files changed (82) hide show
  1. sentience/__init__.py +253 -0
  2. sentience/_extension_loader.py +195 -0
  3. sentience/action_executor.py +215 -0
  4. sentience/actions.py +1020 -0
  5. sentience/agent.py +1181 -0
  6. sentience/agent_config.py +46 -0
  7. sentience/agent_runtime.py +424 -0
  8. sentience/asserts/__init__.py +70 -0
  9. sentience/asserts/expect.py +621 -0
  10. sentience/asserts/query.py +383 -0
  11. sentience/async_api.py +108 -0
  12. sentience/backends/__init__.py +137 -0
  13. sentience/backends/actions.py +343 -0
  14. sentience/backends/browser_use_adapter.py +241 -0
  15. sentience/backends/cdp_backend.py +393 -0
  16. sentience/backends/exceptions.py +211 -0
  17. sentience/backends/playwright_backend.py +194 -0
  18. sentience/backends/protocol.py +216 -0
  19. sentience/backends/sentience_context.py +469 -0
  20. sentience/backends/snapshot.py +427 -0
  21. sentience/base_agent.py +196 -0
  22. sentience/browser.py +1215 -0
  23. sentience/browser_evaluator.py +299 -0
  24. sentience/canonicalization.py +207 -0
  25. sentience/cli.py +130 -0
  26. sentience/cloud_tracing.py +807 -0
  27. sentience/constants.py +6 -0
  28. sentience/conversational_agent.py +543 -0
  29. sentience/element_filter.py +136 -0
  30. sentience/expect.py +188 -0
  31. sentience/extension/background.js +104 -0
  32. sentience/extension/content.js +161 -0
  33. sentience/extension/injected_api.js +914 -0
  34. sentience/extension/manifest.json +36 -0
  35. sentience/extension/pkg/sentience_core.d.ts +51 -0
  36. sentience/extension/pkg/sentience_core.js +323 -0
  37. sentience/extension/pkg/sentience_core_bg.wasm +0 -0
  38. sentience/extension/pkg/sentience_core_bg.wasm.d.ts +10 -0
  39. sentience/extension/release.json +115 -0
  40. sentience/formatting.py +15 -0
  41. sentience/generator.py +202 -0
  42. sentience/inspector.py +367 -0
  43. sentience/llm_interaction_handler.py +191 -0
  44. sentience/llm_provider.py +875 -0
  45. sentience/llm_provider_utils.py +120 -0
  46. sentience/llm_response_builder.py +153 -0
  47. sentience/models.py +846 -0
  48. sentience/ordinal.py +280 -0
  49. sentience/overlay.py +222 -0
  50. sentience/protocols.py +228 -0
  51. sentience/query.py +303 -0
  52. sentience/read.py +188 -0
  53. sentience/recorder.py +589 -0
  54. sentience/schemas/trace_v1.json +335 -0
  55. sentience/screenshot.py +100 -0
  56. sentience/sentience_methods.py +86 -0
  57. sentience/snapshot.py +706 -0
  58. sentience/snapshot_diff.py +126 -0
  59. sentience/text_search.py +262 -0
  60. sentience/trace_event_builder.py +148 -0
  61. sentience/trace_file_manager.py +197 -0
  62. sentience/trace_indexing/__init__.py +27 -0
  63. sentience/trace_indexing/index_schema.py +199 -0
  64. sentience/trace_indexing/indexer.py +414 -0
  65. sentience/tracer_factory.py +322 -0
  66. sentience/tracing.py +449 -0
  67. sentience/utils/__init__.py +40 -0
  68. sentience/utils/browser.py +46 -0
  69. sentience/utils/element.py +257 -0
  70. sentience/utils/formatting.py +59 -0
  71. sentience/utils.py +296 -0
  72. sentience/verification.py +380 -0
  73. sentience/visual_agent.py +2058 -0
  74. sentience/wait.py +139 -0
  75. sentienceapi-0.95.0.dist-info/METADATA +984 -0
  76. sentienceapi-0.95.0.dist-info/RECORD +82 -0
  77. sentienceapi-0.95.0.dist-info/WHEEL +5 -0
  78. sentienceapi-0.95.0.dist-info/entry_points.txt +2 -0
  79. sentienceapi-0.95.0.dist-info/licenses/LICENSE +24 -0
  80. sentienceapi-0.95.0.dist-info/licenses/LICENSE-APACHE +201 -0
  81. sentienceapi-0.95.0.dist-info/licenses/LICENSE-MIT +21 -0
  82. sentienceapi-0.95.0.dist-info/top_level.txt +1 -0
sentience/expect.py ADDED
@@ -0,0 +1,188 @@
1
+ """
2
+ Expect/Assert functionality
3
+ """
4
+
5
+ import asyncio
6
+ import time
7
+
8
+ from .browser import AsyncSentienceBrowser, SentienceBrowser
9
+ from .models import Element
10
+ from .query import query
11
+ from .wait import wait_for, wait_for_async
12
+
13
+
14
+ class Expectation:
15
+ """Assertion helper for element expectations"""
16
+
17
+ def __init__(self, browser: SentienceBrowser, selector: str | dict):
18
+ self.browser = browser
19
+ self.selector = selector
20
+
21
+ def to_be_visible(self, timeout: float = 10.0) -> Element:
22
+ """Assert element is visible (exists and in viewport)"""
23
+ result = wait_for(self.browser, self.selector, timeout=timeout)
24
+
25
+ if not result.found:
26
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
27
+
28
+ element = result.element
29
+ if not element.in_viewport:
30
+ raise AssertionError(f"Element found but not visible in viewport: {self.selector}")
31
+
32
+ return element
33
+
34
+ def to_exist(self, timeout: float = 10.0) -> Element:
35
+ """Assert element exists"""
36
+ result = wait_for(self.browser, self.selector, timeout=timeout)
37
+
38
+ if not result.found:
39
+ raise AssertionError(f"Element does not exist: {self.selector} (timeout: {timeout}s)")
40
+
41
+ return result.element
42
+
43
+ def to_have_text(self, expected_text: str, timeout: float = 10.0) -> Element:
44
+ """Assert element has specific text"""
45
+ result = wait_for(self.browser, self.selector, timeout=timeout)
46
+
47
+ if not result.found:
48
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
49
+
50
+ element = result.element
51
+ if not element.text or expected_text not in element.text:
52
+ raise AssertionError(
53
+ f"Element text mismatch. Expected '{expected_text}', got '{element.text}'"
54
+ )
55
+
56
+ return element
57
+
58
+ def to_have_count(self, expected_count: int, timeout: float = 10.0) -> None:
59
+ """Assert selector matches exactly N elements"""
60
+ from .snapshot import snapshot
61
+
62
+ start_time = time.time()
63
+ while time.time() - start_time < timeout:
64
+ snap = snapshot(self.browser)
65
+ matches = query(snap, self.selector)
66
+
67
+ if len(matches) == expected_count:
68
+ return
69
+
70
+ time.sleep(0.25)
71
+
72
+ # Final check
73
+ snap = snapshot(self.browser)
74
+ matches = query(snap, self.selector)
75
+ actual_count = len(matches)
76
+
77
+ raise AssertionError(
78
+ f"Element count mismatch. Expected {expected_count}, got {actual_count}"
79
+ )
80
+
81
+
82
+ def expect(browser: SentienceBrowser, selector: str | dict) -> Expectation:
83
+ """
84
+ Create expectation helper for assertions
85
+
86
+ Args:
87
+ browser: SentienceBrowser instance
88
+ selector: String DSL or dict query
89
+
90
+ Returns:
91
+ Expectation helper
92
+ """
93
+ return Expectation(browser, selector)
94
+
95
+
96
+ class ExpectationAsync:
97
+ """Assertion helper for element expectations (async)"""
98
+
99
+ def __init__(self, browser: AsyncSentienceBrowser, selector: str | dict):
100
+ self.browser = browser
101
+ self.selector = selector
102
+
103
+ async def to_be_visible(self, timeout: float = 10.0) -> Element:
104
+ """Assert element is visible (exists and in viewport)"""
105
+ result = await wait_for_async(self.browser, self.selector, timeout=timeout)
106
+
107
+ if not result.found:
108
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
109
+
110
+ element = result.element
111
+ if not element.in_viewport:
112
+ raise AssertionError(f"Element found but not visible in viewport: {self.selector}")
113
+
114
+ return element
115
+
116
+ async def to_exist(self, timeout: float = 10.0) -> Element:
117
+ """Assert element exists"""
118
+ result = await wait_for_async(self.browser, self.selector, timeout=timeout)
119
+
120
+ if not result.found:
121
+ raise AssertionError(f"Element does not exist: {self.selector} (timeout: {timeout}s)")
122
+
123
+ return result.element
124
+
125
+ async def to_have_text(self, expected_text: str, timeout: float = 10.0) -> Element:
126
+ """Assert element has specific text"""
127
+ result = await wait_for_async(self.browser, self.selector, timeout=timeout)
128
+
129
+ if not result.found:
130
+ raise AssertionError(f"Element not found: {self.selector} (timeout: {timeout}s)")
131
+
132
+ element = result.element
133
+ if not element.text or expected_text not in element.text:
134
+ raise AssertionError(
135
+ f"Element text mismatch. Expected '{expected_text}', got '{element.text}'"
136
+ )
137
+
138
+ return element
139
+
140
+ async def to_have_count(self, expected_count: int, timeout: float = 10.0) -> None:
141
+ """Assert selector matches exactly N elements"""
142
+ from .snapshot import snapshot_async
143
+
144
+ start_time = time.time()
145
+ while time.time() - start_time < timeout:
146
+ snap = await snapshot_async(self.browser)
147
+ matches = query(snap, self.selector)
148
+
149
+ if len(matches) == expected_count:
150
+ return
151
+
152
+ await asyncio.sleep(0.25)
153
+
154
+ # Final check
155
+ snap = await snapshot_async(self.browser)
156
+ matches = query(snap, self.selector)
157
+ actual_count = len(matches)
158
+
159
+ raise AssertionError(
160
+ f"Element count mismatch. Expected {expected_count}, got {actual_count}"
161
+ )
162
+
163
+
164
+ def expect_async(browser: AsyncSentienceBrowser, selector: str | dict) -> ExpectationAsync:
165
+ """
166
+ Create expectation helper for assertions (async)
167
+
168
+ Args:
169
+ browser: AsyncSentienceBrowser instance
170
+ selector: String DSL or dict query
171
+
172
+ Returns:
173
+ ExpectationAsync helper
174
+
175
+ Example:
176
+ # Assert element is visible
177
+ element = await expect_async(browser, "role=button").to_be_visible()
178
+
179
+ # Assert element has text
180
+ element = await expect_async(browser, "h1").to_have_text("Welcome")
181
+
182
+ # Assert element exists
183
+ element = await expect_async(browser, "role=link").to_exist()
184
+
185
+ # Assert count
186
+ await expect_async(browser, "role=button").to_have_count(5)
187
+ """
188
+ return ExpectationAsync(browser, selector)
@@ -0,0 +1,104 @@
1
+ import init, { analyze_page_with_options, analyze_page, prune_for_api } from "../pkg/sentience_core.js";
2
+
3
+ let wasmReady = !1, wasmInitPromise = null;
4
+
5
+ async function initWASM() {
6
+ if (!wasmReady) return wasmInitPromise || (wasmInitPromise = (async () => {
7
+ try {
8
+ globalThis.js_click_element = () => {}, await init(), wasmReady = !0;
9
+ } catch (error) {
10
+ throw error;
11
+ }
12
+ })(), wasmInitPromise);
13
+ }
14
+
15
+ async function handleScreenshotCapture(_tabId, options = {}) {
16
+ try {
17
+ const {format: format = "png", quality: quality = 90} = options;
18
+ return await chrome.tabs.captureVisibleTab(null, {
19
+ format: format,
20
+ quality: quality
21
+ });
22
+ } catch (error) {
23
+ throw new Error(`Failed to capture screenshot: ${error.message}`);
24
+ }
25
+ }
26
+
27
+ async function handleSnapshotProcessing(rawData, options = {}) {
28
+ const startTime = performance.now();
29
+ try {
30
+ if (!Array.isArray(rawData)) throw new Error("rawData must be an array");
31
+ if (rawData.length > 1e4 && (rawData = rawData.slice(0, 1e4)), await initWASM(),
32
+ !wasmReady) throw new Error("WASM module not initialized");
33
+ let analyzedElements, prunedRawData;
34
+ try {
35
+ const wasmPromise = new Promise((resolve, reject) => {
36
+ try {
37
+ let result;
38
+ result = options.limit || options.filter ? analyze_page_with_options(rawData, options) : analyze_page(rawData),
39
+ resolve(result);
40
+ } catch (e) {
41
+ reject(e);
42
+ }
43
+ });
44
+ analyzedElements = await Promise.race([ wasmPromise, new Promise((_, reject) => setTimeout(() => reject(new Error("WASM processing timeout (>18s)")), 18e3)) ]);
45
+ } catch (e) {
46
+ const errorMsg = e.message || "Unknown WASM error";
47
+ throw new Error(`WASM analyze_page failed: ${errorMsg}`);
48
+ }
49
+ try {
50
+ prunedRawData = prune_for_api(rawData);
51
+ } catch (e) {
52
+ prunedRawData = rawData;
53
+ }
54
+ performance.now();
55
+ return {
56
+ elements: analyzedElements,
57
+ raw_elements: prunedRawData
58
+ };
59
+ } catch (error) {
60
+ performance.now();
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ initWASM().catch(err => {}), chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
66
+ try {
67
+ return "captureScreenshot" === request.action ? (handleScreenshotCapture(sender.tab.id, request.options).then(screenshot => {
68
+ sendResponse({
69
+ success: !0,
70
+ screenshot: screenshot
71
+ });
72
+ }).catch(error => {
73
+ sendResponse({
74
+ success: !1,
75
+ error: error.message || "Screenshot capture failed"
76
+ });
77
+ }), !0) : "processSnapshot" === request.action ? (handleSnapshotProcessing(request.rawData, request.options).then(result => {
78
+ sendResponse({
79
+ success: !0,
80
+ result: result
81
+ });
82
+ }).catch(error => {
83
+ sendResponse({
84
+ success: !1,
85
+ error: error.message || "Snapshot processing failed"
86
+ });
87
+ }), !0) : (sendResponse({
88
+ success: !1,
89
+ error: "Unknown action"
90
+ }), !1);
91
+ } catch (error) {
92
+ try {
93
+ sendResponse({
94
+ success: !1,
95
+ error: `Fatal error: ${error.message || "Unknown error"}`
96
+ });
97
+ } catch (e) {}
98
+ return !1;
99
+ }
100
+ }), self.addEventListener("error", event => {
101
+ event.preventDefault();
102
+ }), self.addEventListener("unhandledrejection", event => {
103
+ event.preventDefault();
104
+ });
@@ -0,0 +1,161 @@
1
+ !function() {
2
+ "use strict";
3
+ window, window.top;
4
+ document.documentElement.dataset.sentienceExtensionId = chrome.runtime.id, window.addEventListener("message", event => {
5
+ var data;
6
+ if (event.source === window) switch (event.data.type) {
7
+ case "SENTIENCE_SCREENSHOT_REQUEST":
8
+ data = event.data, chrome.runtime.sendMessage({
9
+ action: "captureScreenshot",
10
+ options: data.options
11
+ }, response => {
12
+ window.postMessage({
13
+ type: "SENTIENCE_SCREENSHOT_RESULT",
14
+ requestId: data.requestId,
15
+ screenshot: response?.success ? response.screenshot : null,
16
+ error: response?.error
17
+ }, "*");
18
+ });
19
+ break;
20
+
21
+ case "SENTIENCE_SNAPSHOT_REQUEST":
22
+ !function(data) {
23
+ const startTime = performance.now();
24
+ let responded = !1;
25
+ const timeoutId = setTimeout(() => {
26
+ if (!responded) {
27
+ responded = !0;
28
+ const duration = performance.now() - startTime;
29
+ window.postMessage({
30
+ type: "SENTIENCE_SNAPSHOT_RESULT",
31
+ requestId: data.requestId,
32
+ error: "WASM processing timeout - background script may be unresponsive",
33
+ duration: duration
34
+ }, "*");
35
+ }
36
+ }, 2e4);
37
+ try {
38
+ chrome.runtime.sendMessage({
39
+ action: "processSnapshot",
40
+ rawData: data.rawData,
41
+ options: data.options
42
+ }, response => {
43
+ if (responded) return;
44
+ responded = !0, clearTimeout(timeoutId);
45
+ const duration = performance.now() - startTime;
46
+ chrome.runtime.lastError ? window.postMessage({
47
+ type: "SENTIENCE_SNAPSHOT_RESULT",
48
+ requestId: data.requestId,
49
+ error: `Chrome runtime error: ${chrome.runtime.lastError.message}`,
50
+ duration: duration
51
+ }, "*") : response?.success ? window.postMessage({
52
+ type: "SENTIENCE_SNAPSHOT_RESULT",
53
+ requestId: data.requestId,
54
+ elements: response.result.elements,
55
+ raw_elements: response.result.raw_elements,
56
+ duration: duration
57
+ }, "*") : window.postMessage({
58
+ type: "SENTIENCE_SNAPSHOT_RESULT",
59
+ requestId: data.requestId,
60
+ error: response?.error || "Processing failed",
61
+ duration: duration
62
+ }, "*");
63
+ });
64
+ } catch (error) {
65
+ if (!responded) {
66
+ responded = !0, clearTimeout(timeoutId);
67
+ const duration = performance.now() - startTime;
68
+ window.postMessage({
69
+ type: "SENTIENCE_SNAPSHOT_RESULT",
70
+ requestId: data.requestId,
71
+ error: `Failed to send message: ${error.message}`,
72
+ duration: duration
73
+ }, "*");
74
+ }
75
+ }
76
+ }(event.data);
77
+ break;
78
+
79
+ case "SENTIENCE_SHOW_OVERLAY":
80
+ !function(data) {
81
+ const {elements: elements, targetElementId: targetElementId} = data;
82
+ if (!elements || !Array.isArray(elements)) return;
83
+ removeOverlay();
84
+ const host = document.createElement("div");
85
+ host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
86
+ document.body.appendChild(host);
87
+ const shadow = host.attachShadow({
88
+ mode: "closed"
89
+ }), maxImportance = Math.max(...elements.map(e => e.importance || 0), 1);
90
+ elements.forEach(element => {
91
+ const bbox = element.bbox;
92
+ if (!bbox) return;
93
+ const isTarget = element.id === targetElementId, isPrimary = element.visual_cues?.is_primary || !1, importance = element.importance || 0;
94
+ let color;
95
+ color = isTarget ? "#FF0000" : isPrimary ? "#0066FF" : "#00FF00";
96
+ const importanceRatio = maxImportance > 0 ? importance / maxImportance : .5, borderOpacity = isTarget ? 1 : isPrimary ? .9 : Math.max(.4, .5 + .5 * importanceRatio), fillOpacity = .2 * borderOpacity, borderWidth = isTarget ? 2 : isPrimary ? 1.5 : Math.max(.5, Math.round(2 * importanceRatio)), hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div");
97
+ if (box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px solid ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${borderOpacity};\n pointer-events: none;\n `,
98
+ importance > 0 || isPrimary) {
99
+ const badge = document.createElement("span");
100
+ badge.textContent = isPrimary ? `⭐${importance}` : `${importance}`, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
101
+ box.appendChild(badge);
102
+ }
103
+ if (isTarget) {
104
+ const targetIndicator = document.createElement("span");
105
+ targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
106
+ box.appendChild(targetIndicator);
107
+ }
108
+ shadow.appendChild(box);
109
+ }), overlayTimeout = setTimeout(() => {
110
+ removeOverlay();
111
+ }, 5e3);
112
+ }(event.data);
113
+ break;
114
+
115
+ case "SENTIENCE_CLEAR_OVERLAY":
116
+ removeOverlay();
117
+ break;
118
+
119
+ case "SENTIENCE_SHOW_GRID_OVERLAY":
120
+ !function(data) {
121
+ const {grids: grids, targetGridId: targetGridId} = data;
122
+ if (!grids || !Array.isArray(grids)) return;
123
+ removeOverlay();
124
+ const host = document.createElement("div");
125
+ host.id = OVERLAY_HOST_ID, host.style.cssText = "\n position: fixed !important;\n top: 0 !important;\n left: 0 !important;\n width: 100vw !important;\n height: 100vh !important;\n pointer-events: none !important;\n z-index: 2147483647 !important;\n margin: 0 !important;\n padding: 0 !important;\n ",
126
+ document.body.appendChild(host);
127
+ const shadow = host.attachShadow({
128
+ mode: "closed"
129
+ });
130
+ grids.forEach(grid => {
131
+ const bbox = grid.bbox;
132
+ if (!bbox) return;
133
+ const isTarget = grid.grid_id === targetGridId, isDominant = !0 === grid.is_dominant;
134
+ let color = "#9B59B6";
135
+ isTarget ? color = "#FF0000" : isDominant && (color = "#FF8C00");
136
+ const borderStyle = isTarget ? "solid" : "dashed", borderWidth = isTarget ? 3 : isDominant ? 2.5 : 2, opacity = isTarget ? 1 : isDominant ? .9 : .8, fillOpacity = .1 * opacity, hexOpacity = Math.round(255 * fillOpacity).toString(16).padStart(2, "0"), box = document.createElement("div");
137
+ box.style.cssText = `\n position: absolute;\n left: ${bbox.x}px;\n top: ${bbox.y}px;\n width: ${bbox.width}px;\n height: ${bbox.height}px;\n border: ${borderWidth}px ${borderStyle} ${color};\n background-color: ${color}${hexOpacity};\n box-sizing: border-box;\n opacity: ${opacity};\n pointer-events: none;\n `;
138
+ let labelText = grid.label ? `Grid ${grid.grid_id}: ${grid.label}` : `Grid ${grid.grid_id}`;
139
+ grid.is_dominant && (labelText = `⭐ ${labelText} (dominant)`);
140
+ const badge = document.createElement("span");
141
+ if (badge.textContent = labelText, badge.style.cssText = `\n position: absolute;\n top: -18px;\n left: 0;\n background: ${color};\n color: white;\n font-size: 11px;\n font-weight: bold;\n padding: 2px 6px;\n font-family: Arial, sans-serif;\n border-radius: 3px;\n opacity: 0.95;\n white-space: nowrap;\n pointer-events: none;\n `,
142
+ box.appendChild(badge), isTarget) {
143
+ const targetIndicator = document.createElement("span");
144
+ targetIndicator.textContent = "🎯", targetIndicator.style.cssText = "\n position: absolute;\n top: -18px;\n right: 0;\n font-size: 16px;\n pointer-events: none;\n ",
145
+ box.appendChild(targetIndicator);
146
+ }
147
+ shadow.appendChild(box);
148
+ }), overlayTimeout = setTimeout(() => {
149
+ removeOverlay();
150
+ }, 5e3);
151
+ }(event.data);
152
+ }
153
+ });
154
+ const OVERLAY_HOST_ID = "sentience-overlay-host";
155
+ let overlayTimeout = null;
156
+ function removeOverlay() {
157
+ const existing = document.getElementById(OVERLAY_HOST_ID);
158
+ existing && existing.remove(), overlayTimeout && (clearTimeout(overlayTimeout),
159
+ overlayTimeout = null);
160
+ }
161
+ }();