@capagents/forgeqe 0.0.0-stage → 1.0.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.
Files changed (220) hide show
  1. package/HOW_TO_USE.md +1639 -0
  2. package/README.md +338 -2
  3. package/bin/forgeqe.d.ts +3 -0
  4. package/bin/forgeqe.js +188 -0
  5. package/docs/ARCHITECTURE.md +408 -0
  6. package/docs/security-data-flow.html +1870 -0
  7. package/docs/user-guide.html +2094 -0
  8. package/evals/domain/access-role-change.yaml +37 -0
  9. package/evals/domain/banking-payment-transfer.yaml +41 -0
  10. package/evals/domain/gdpr-account-erasure.yaml +30 -0
  11. package/evals/domain/insurance-claim-fnol.yaml +30 -0
  12. package/evals/domain/insurance-policy-renewal.yaml +37 -0
  13. package/evals/domain/lending-loan-application.yaml +36 -0
  14. package/forgeqe-architecture.html +718 -0
  15. package/forgeqe.example.yaml +203 -0
  16. package/knowledge/README.md +18 -0
  17. package/knowledge/business-rules.md +10 -0
  18. package/knowledge/compliance.md +16 -0
  19. package/knowledge/environments.md +5 -0
  20. package/knowledge/glossary.md +5 -0
  21. package/knowledge/product.md +16 -0
  22. package/knowledge/roles.md +9 -0
  23. package/knowledge/test-data.md +7 -0
  24. package/llms.example.json +51 -0
  25. package/package.json +63 -4
  26. package/prompts/README.md +34 -0
  27. package/prompts/api-scenarios.md +21 -0
  28. package/prompts/api-tests.md +39 -0
  29. package/prompts/brain.md +166 -0
  30. package/prompts/browser-capture.md +7 -0
  31. package/prompts/browser-case-report.md +7 -0
  32. package/prompts/browser-checks.md +14 -0
  33. package/prompts/browser-efficiency.md +7 -0
  34. package/prompts/browser-error-recovery.md +15 -0
  35. package/prompts/browser-extra.md +5 -0
  36. package/prompts/browser-goal-verification.md +13 -0
  37. package/prompts/browser-skill-select.md +6 -0
  38. package/prompts/domain-rule.md +13 -0
  39. package/prompts/eval-judge.md +19 -0
  40. package/prompts/explore-charters.md +27 -0
  41. package/prompts/find-tests.md +28 -0
  42. package/prompts/heal.md +40 -0
  43. package/prompts/pr-review.md +29 -0
  44. package/prompts/skill-rule.md +6 -0
  45. package/prompts/test-agent.md +34 -0
  46. package/prompts/test-reviewer.md +30 -0
  47. package/prompts/test-rules.md +28 -0
  48. package/py/forgeqe_engine/__init__.py +3 -0
  49. package/py/forgeqe_engine/__main__.py +3 -0
  50. package/py/forgeqe_engine/brand.py +453 -0
  51. package/py/forgeqe_engine/brand_assets.py +6 -0
  52. package/py/forgeqe_engine/browser.py +196 -0
  53. package/py/forgeqe_engine/capture.py +538 -0
  54. package/py/forgeqe_engine/checks.py +98 -0
  55. package/py/forgeqe_engine/cli.py +188 -0
  56. package/py/forgeqe_engine/diagnostics.py +609 -0
  57. package/py/forgeqe_engine/llm.py +195 -0
  58. package/py/forgeqe_engine/logins.py +251 -0
  59. package/py/forgeqe_engine/loops.py +224 -0
  60. package/py/forgeqe_engine/memory.py +1004 -0
  61. package/py/forgeqe_engine/network.py +244 -0
  62. package/py/forgeqe_engine/prompts.py +20 -0
  63. package/py/forgeqe_engine/replay.py +240 -0
  64. package/py/forgeqe_engine/report.py +173 -0
  65. package/py/forgeqe_engine/service.py +1335 -0
  66. package/py/forgeqe_engine/settings.py +93 -0
  67. package/py/forgeqe_engine/skills.py +88 -0
  68. package/py/forgeqe_engine/summary.py +239 -0
  69. package/py/forgeqe_engine/urls.py +81 -0
  70. package/py/forgeqe_engine/workflow_yaml.py +689 -0
  71. package/skills/dotnet-nunit-playwright/SKILL.md +34 -0
  72. package/skills/dynamics-365/SKILL.md +21 -0
  73. package/skills/playwright-typescript/SKILL.md +35 -0
  74. package/skills/writing-skills/SKILL.md +30 -0
  75. package/src/api/plan.ts +246 -0
  76. package/src/api/run.ts +204 -0
  77. package/src/api/scenario.ts +502 -0
  78. package/src/api/schema.ts +170 -0
  79. package/src/api/spec.ts +624 -0
  80. package/src/api/store.ts +232 -0
  81. package/src/api/traffic.ts +281 -0
  82. package/src/api/types.ts +163 -0
  83. package/src/api/write.ts +272 -0
  84. package/src/auth/signin.ts +494 -0
  85. package/src/auth/store.ts +170 -0
  86. package/src/brain/index.ts +189 -0
  87. package/src/brain/prompt.ts +19 -0
  88. package/src/capabilities/docs.ts +101 -0
  89. package/src/capabilities/guide.ts +455 -0
  90. package/src/capabilities/registry.ts +343 -0
  91. package/src/capabilities/render.ts +106 -0
  92. package/src/capabilities/status.ts +137 -0
  93. package/src/card.ts +11 -0
  94. package/src/cases/ado.ts +325 -0
  95. package/src/cases/api.ts +117 -0
  96. package/src/cases/bugs.ts +223 -0
  97. package/src/cases/confluence.ts +111 -0
  98. package/src/cases/context.ts +223 -0
  99. package/src/cases/dashboards.ts +137 -0
  100. package/src/cases/failure.ts +97 -0
  101. package/src/cases/files.ts +295 -0
  102. package/src/cases/goal.ts +53 -0
  103. package/src/cases/http.ts +111 -0
  104. package/src/cases/index.ts +131 -0
  105. package/src/cases/jira.ts +122 -0
  106. package/src/cases/qtest.ts +108 -0
  107. package/src/cases/quality.ts +190 -0
  108. package/src/cases/rag.ts +100 -0
  109. package/src/cases/readiness.ts +284 -0
  110. package/src/cases/save.ts +149 -0
  111. package/src/cases/sprints.ts +148 -0
  112. package/src/cases/testrail.ts +176 -0
  113. package/src/cases/types.ts +107 -0
  114. package/src/cases/wiki.ts +105 -0
  115. package/src/cases/xray.ts +206 -0
  116. package/src/cases/zephyr.ts +153 -0
  117. package/src/childEnv.ts +86 -0
  118. package/src/cli/args.ts +114 -0
  119. package/src/cli/help.ts +167 -0
  120. package/src/cli/index.ts +1142 -0
  121. package/src/cli/terminal.ts +187 -0
  122. package/src/code/areas.ts +337 -0
  123. package/src/code/attention.ts +214 -0
  124. package/src/code/find.ts +540 -0
  125. package/src/code/handoff.ts +541 -0
  126. package/src/code/history.ts +170 -0
  127. package/src/code/impact.ts +231 -0
  128. package/src/code/index.ts +34 -0
  129. package/src/code/livebrowser.ts +67 -0
  130. package/src/code/loop.ts +603 -0
  131. package/src/code/notes.ts +75 -0
  132. package/src/code/profile.ts +226 -0
  133. package/src/code/prompt.ts +303 -0
  134. package/src/code/prreview.ts +649 -0
  135. package/src/code/prtests.ts +103 -0
  136. package/src/code/replay.ts +772 -0
  137. package/src/code/review.ts +202 -0
  138. package/src/code/runtime.ts +781 -0
  139. package/src/code/slices.ts +159 -0
  140. package/src/code/testrun.ts +187 -0
  141. package/src/code/traceability.ts +100 -0
  142. package/src/code/worktree.ts +153 -0
  143. package/src/commands.ts +101 -0
  144. package/src/config.ts +970 -0
  145. package/src/detail.ts +65 -0
  146. package/src/domain.ts +19 -0
  147. package/src/engine/bridge.ts +359 -0
  148. package/src/engine/mock.ts +102 -0
  149. package/src/engine/setup.ts +298 -0
  150. package/src/entra.ts +154 -0
  151. package/src/env.ts +24 -0
  152. package/src/evals/domain.ts +338 -0
  153. package/src/explore/charters.ts +151 -0
  154. package/src/explore/run.ts +280 -0
  155. package/src/explore/story.ts +106 -0
  156. package/src/guard.ts +51 -0
  157. package/src/heal/loop.ts +357 -0
  158. package/src/heal/prompt.ts +143 -0
  159. package/src/heal/report.ts +95 -0
  160. package/src/index.ts +12 -0
  161. package/src/init.ts +102 -0
  162. package/src/knowledge.ts +114 -0
  163. package/src/legacyEnv.ts +9 -0
  164. package/src/llm.ts +61 -0
  165. package/src/locale.ts +25 -0
  166. package/src/logins.ts +148 -0
  167. package/src/managed.ts +101 -0
  168. package/src/mcp/apitools.ts +335 -0
  169. package/src/mcp/captools.ts +25 -0
  170. package/src/mcp/casetools.ts +223 -0
  171. package/src/mcp/commands.ts +421 -0
  172. package/src/mcp/exploretools.ts +129 -0
  173. package/src/mcp/external.ts +172 -0
  174. package/src/mcp/git.ts +78 -0
  175. package/src/mcp/notifytools.ts +49 -0
  176. package/src/mcp/prtools.ts +261 -0
  177. package/src/mcp/qualitytools.ts +154 -0
  178. package/src/mcp/repotests.ts +286 -0
  179. package/src/mcp/server.ts +234 -0
  180. package/src/mcp/tools.ts +810 -0
  181. package/src/memory.ts +250 -0
  182. package/src/money.ts +39 -0
  183. package/src/monitor/alerts.ts +94 -0
  184. package/src/monitor/background.ts +68 -0
  185. package/src/monitor/run.ts +263 -0
  186. package/src/monitor/state.ts +105 -0
  187. package/src/notify/events.ts +144 -0
  188. package/src/notify/notice.ts +177 -0
  189. package/src/prices.ts +175 -0
  190. package/src/print.ts +54 -0
  191. package/src/privacy.ts +35 -0
  192. package/src/prompts.ts +165 -0
  193. package/src/quality/budgets.ts +249 -0
  194. package/src/quality/history.ts +149 -0
  195. package/src/quality/visual.ts +201 -0
  196. package/src/report/data.ts +475 -0
  197. package/src/report/html.ts +106 -0
  198. package/src/report/template.html +84 -0
  199. package/src/run.ts +243 -0
  200. package/src/sessionLog.ts +216 -0
  201. package/src/sessionLogins.ts +94 -0
  202. package/src/skills/registry.ts +217 -0
  203. package/src/skills/scaffold.ts +65 -0
  204. package/src/skills/select.ts +37 -0
  205. package/src/skills/view.ts +44 -0
  206. package/src/suite/report.ts +135 -0
  207. package/src/suite/run.ts +303 -0
  208. package/src/summary.ts +188 -0
  209. package/src/tools.ts +130 -0
  210. package/src/tui/App.tsx +983 -0
  211. package/src/tui/clipboard.ts +36 -0
  212. package/src/tui/components.tsx +263 -0
  213. package/src/tui/dialogs.ts +65 -0
  214. package/src/tui/main.tsx +20 -0
  215. package/src/tui/text.ts +28 -0
  216. package/src/tui/theme.ts +104 -0
  217. package/src/tui/transcript.tsx +344 -0
  218. package/src/types.ts +146 -0
  219. package/src/update.ts +311 -0
  220. package/src/usage.ts +158 -0
@@ -0,0 +1,3 @@
1
+ """ForgeQE browser engine: the library's Agent plus recovery prompts, loop breaker, and workflow export."""
2
+
3
+ BROWSER_USE_VERSION = "0.13.10"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,453 @@
1
+ """
2
+ ForgeQE's look in the browser it drives, when the browser is visible:
3
+
4
+ - a ForgeQE start page on blank tabs, in place of the library's (which loads a remote logo)
5
+ - on every page, an overlay: a glow round the edge, a bar along the bottom with the step being run, and a lock on the
6
+ person's mouse and keyboard while the agent thinks, lifted while it acts so its own clicks and typing get through
7
+
8
+ The overlay is left out of the page the model reads (the library's exclude attribute), hidden from screen readers and the
9
+ accessibility checks (aria-hidden, shadow DOM, outside <body>), and taken out of a screenshot only while its pixels are
10
+ captured. The step bar stays on screen while the agent clicks, types and scrolls. Nothing in it loads a URL, so a page's
11
+ Content-Security-Policy has nothing to block or log.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import asyncio
16
+ import json
17
+ import logging
18
+ import os
19
+ import re
20
+ from typing import Any, Dict, Optional, Tuple
21
+ from urllib.parse import urlparse
22
+
23
+ from .brand_assets import ICON_PNG, MARK_RGBA, MARK_SIZE
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ OVERLAY_TAG = "forgeqe-overlay"
28
+ START_ID = "forgeqe-start"
29
+ CDP_TIMEOUT = 3.0
30
+
31
+ _OVERLAYS: Dict[int, "Overlay"] = {}
32
+
33
+ # Runs in every top-level page (and on the pages open when the overlay starts). Starts unlocked: a page that loads in
34
+ # the middle of the agent's actions must not block the rest of them. Overlay.update() locks it once the step is over.
35
+ OVERLAY_JS = r"""(() => {
36
+ if (window.top !== window) return;
37
+ if (window.__forgeqeOverlay) { window.__forgeqeOverlay.mount(); return; }
38
+ const CFG = __CONFIG__;
39
+ const MODES = {
40
+ thinking: ['Thinking', 'rgba(62,196,255,.16)', '#8fdcff'],
41
+ acting: ['Acting', 'rgba(52,211,153,.18)', '#6ee7b7'],
42
+ replaying: ['From memory', 'rgba(167,139,250,.22)', '#c4b5fd'],
43
+ waiting: ['Your turn', 'rgba(251,191,36,.18)', '#fcd34d'],
44
+ };
45
+ // hide counts screenshots in progress. The bar stays up through clicks, typing and scrolling; only a shot takes it down.
46
+ const state = { text: CFG.text, mode: CFG.mode, lock: false, hide: 0 };
47
+ let host = null, textEl, chipEl, lockEl, blocker, restores = 0, restoreAt = 0;
48
+
49
+ const el = (tag, css, text) => { const e = document.createElement(tag); e.style.cssText = css; if (text) e.textContent = text; return e; };
50
+ const locked = () => state.lock && !state.hide;
51
+
52
+ function paintMark(canvas) {
53
+ try {
54
+ const n = CFG.markSize, bin = atob(CFG.mark), px = new Uint8ClampedArray(bin.length);
55
+ for (let i = 0; i < bin.length; i++) px[i] = bin.charCodeAt(i);
56
+ canvas.width = canvas.height = n;
57
+ canvas.getContext('2d').putImageData(new ImageData(px, n, n), 0, 0);
58
+ } catch (_) {}
59
+ }
60
+
61
+ function build() {
62
+ host = document.createElement(CFG.tag);
63
+ host.setAttribute('data-browser-use-exclude', 'true');
64
+ host.setAttribute('aria-hidden', 'true');
65
+ // Top layer, so a click that scrolls the page or sets a transform on it cannot carry the bar off screen.
66
+ try { host.setAttribute('popover', 'manual'); } catch (_) {}
67
+ host.style.cssText = 'all:initial;';
68
+ const root = host.attachShadow({ mode: 'open' });
69
+ const css = document.createElement('style');
70
+ css.textContent = '*{pointer-events:none!important;}';
71
+ root.append(css);
72
+ const glow = el('div', 'position:fixed;inset:0;pointer-events:none;border-radius:6px;' +
73
+ 'box-shadow:inset 0 0 0 2px rgba(50,210,234,.85),inset 0 0 34px 5px rgba(23,87,194,.4);');
74
+ try { glow.animate([{ opacity: .45 }, { opacity: 1 }], { duration: 1600, direction: 'alternate', iterations: Infinity, easing: 'ease-in-out' }); } catch (_) {}
75
+ blocker = el('div', 'position:fixed;inset:0;background:transparent;');
76
+ const bar = el('div', 'position:fixed;left:0;right:0;bottom:0;box-sizing:border-box;display:flex;align-items:center;gap:12px;' +
77
+ 'padding:9px 16px;background:rgba(15,27,61,.96);color:#f4f8ff;border-top:1px solid rgba(50,210,234,.5);' +
78
+ 'box-shadow:0 -8px 24px rgba(0,0,0,.28);font:500 13px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;' +
79
+ 'user-select:none;-webkit-user-select:none;');
80
+ const mark = el('canvas', 'width:26px;height:26px;border-radius:6px;flex:none;display:block;');
81
+ paintMark(mark);
82
+ const word = el('span', 'flex:none;font-weight:800;font-size:15px;letter-spacing:-.02em;');
83
+ word.append(el('span', 'color:#f4f8ff', 'Forge'), el('span', 'color:#3ec4ff', 'QE'));
84
+ const sep = el('span', 'flex:none;width:1px;height:18px;background:rgba(255,255,255,.22);');
85
+ textEl = el('span', 'flex:1 1 auto;min-width:0;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;' +
86
+ 'word-break:break-word;color:rgba(244,248,255,.92);');
87
+ chipEl = el('span', 'flex:none;padding:3px 10px;border-radius:999px;font-size:12px;font-weight:600;white-space:nowrap;');
88
+ lockEl = el('span', 'flex:none;font-size:12px;color:rgba(244,248,255,.55);white-space:nowrap;', 'Mouse and keyboard locked');
89
+ bar.append(mark, word, sep, textEl, chipEl, lockEl);
90
+ root.append(glow, blocker, bar);
91
+ }
92
+
93
+ function apply() {
94
+ const hidden = state.hide > 0;
95
+ host.style.setProperty('position', 'fixed', 'important');
96
+ host.style.setProperty('inset', '0', 'important');
97
+ host.style.setProperty('margin', '0', 'important');
98
+ host.style.setProperty('transform', 'none', 'important');
99
+ host.style.setProperty('contain', 'none', 'important');
100
+ host.style.setProperty('z-index', '2147483647', 'important');
101
+ host.style.setProperty('pointer-events', 'none', 'important');
102
+ host.style.setProperty('visibility', hidden ? 'hidden' : 'visible', 'important');
103
+ host.style.setProperty('display', hidden ? 'none' : 'block', 'important');
104
+ try {
105
+ if (hidden) { if (host.hidePopover && host.matches(':popover-open')) host.hidePopover(); }
106
+ else if (host.showPopover && !host.matches(':popover-open')) host.showPopover();
107
+ } catch (_) {}
108
+ textEl.textContent = state.text || '';
109
+ const m = MODES[state.mode] || MODES.thinking;
110
+ chipEl.textContent = m[0];
111
+ chipEl.style.background = m[1];
112
+ chipEl.style.color = m[2];
113
+ blocker.style.setProperty('pointer-events', locked() ? 'auto' : 'none', 'important');
114
+ blocker.style.cursor = locked() ? 'not-allowed' : 'default';
115
+ lockEl.style.display = locked() ? '' : 'none';
116
+ const start = document.getElementById(CFG.startId);
117
+ if (start) start.style.visibility = hidden ? 'hidden' : 'visible';
118
+ }
119
+
120
+ function mount() {
121
+ const parent = document.documentElement;
122
+ if (!parent) return;
123
+ if (!host) build();
124
+ if (host.parentNode !== parent) parent.appendChild(host);
125
+ watch();
126
+ apply();
127
+ }
128
+
129
+ // A click can rewrite the document. Put the bar back, but stop if the page removes it in a loop.
130
+ function restore() {
131
+ const now = Date.now();
132
+ if (now - restoreAt > 1000) { restores = 0; restoreAt = now; }
133
+ if (restores > 8) return;
134
+ restores++;
135
+ mount();
136
+ }
137
+
138
+ const keep = new MutationObserver(() => { if (host && !host.isConnected && !state.hide) restore(); });
139
+ function watch() {
140
+ keep.disconnect();
141
+ const parent = document.documentElement;
142
+ if (parent) keep.observe(parent, { childList: true });
143
+ keep.observe(document, { childList: true });
144
+ }
145
+
146
+ // Registered before the page's own scripts, so the page never sees a person's input while the agent works.
147
+ const block = (e) => { if (e.isTrusted && locked()) { e.preventDefault(); e.stopImmediatePropagation(); } };
148
+ for (const t of ['click', 'dblclick', 'auxclick', 'contextmenu', 'mousedown', 'mouseup', 'pointerdown', 'pointerup',
149
+ 'touchstart', 'touchend', 'wheel', 'dragstart', 'drop', 'keydown', 'keyup', 'keypress', 'beforeinput', 'paste']) {
150
+ window.addEventListener(t, block, { capture: true, passive: false });
151
+ }
152
+
153
+ window.__forgeqeOverlay = {
154
+ mount,
155
+ set(s) { Object.assign(state, s); if (state.hide < 0) state.hide = 0; mount(); },
156
+ hide() { state.hide++; mount(); },
157
+ show() { state.hide = Math.max(0, state.hide - 1); mount(); },
158
+ };
159
+ if (document.documentElement) mount();
160
+ document.addEventListener('DOMContentLoaded', mount, { once: true });
161
+ })()"""
162
+
163
+ START_JS = r"""(() => {
164
+ if (window.top !== window || location.href !== 'about:blank' || document.getElementById(__ID__)) return;
165
+ const el = (tag, css, text) => { const e = document.createElement(tag); e.style.cssText = css; if (text) e.textContent = text; return e; };
166
+ document.title = 'ForgeQE';
167
+ const icon = document.createElement('link');
168
+ icon.rel = 'icon';
169
+ icon.href = __ICON__;
170
+ (document.head || document.documentElement).appendChild(icon);
171
+ const page = el('div', 'position:fixed;inset:0;z-index:2147483646;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;' +
172
+ 'background:radial-gradient(circle at 50% 38%,#1b3a7a 0%,#0f1b3d 58%,#0a1330 100%);' +
173
+ 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;user-select:none;-webkit-user-select:none;');
174
+ page.id = __ID__;
175
+ page.setAttribute('data-browser-use-exclude', 'true');
176
+ page.setAttribute('aria-hidden', 'true');
177
+ const img = el('img', 'width:128px;height:128px;border-radius:30px;box-shadow:0 18px 50px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.14);');
178
+ img.src = __ICON__;
179
+ img.alt = '';
180
+ const word = el('div', 'font-size:46px;font-weight:800;letter-spacing:-.03em;line-height:1;');
181
+ word.append(el('span', 'color:#f4f8ff', 'Forge'), el('span', 'color:#3ec4ff', 'QE'));
182
+ const pill = el('div', 'display:flex;align-items:center;gap:10px;padding:8px 16px;border-radius:999px;background:rgba(255,255,255,.08);' +
183
+ 'border:1px solid rgba(62,196,255,.3);color:rgba(244,248,255,.85);font-size:13px;font-weight:600;');
184
+ const dot = el('span', 'width:8px;height:8px;border-radius:50%;background:#3ec4ff;box-shadow:0 0 10px #3ec4ff;');
185
+ try { dot.animate([{ opacity: .3 }, { opacity: 1 }], { duration: 900, direction: 'alternate', iterations: Infinity }); } catch (_) {}
186
+ pill.append(dot, document.createTextNode(__TEXT__));
187
+ page.append(img, word, pill);
188
+ (document.body || document.documentElement).appendChild(page);
189
+ })()"""
190
+
191
+ _SET_JS = "(() => { const o = window.__forgeqeOverlay; if (o) o.set(%s); })()"
192
+ _HIDDEN_JS = (
193
+ "(() => { const o = window.__forgeqeOverlay; if (o) o.%s(); "
194
+ "const s = document.getElementById('" + START_ID + "'); if (s && !o) s.style.visibility = '%s'; })()"
195
+ )
196
+
197
+
198
+ class Overlay:
199
+ """The overlay for one browser. Every call is best effort: a failure is logged, never raised into the run."""
200
+
201
+ def __init__(self, browser: Any, *, lock: bool = True, text: str = "ForgeQE is driving this browser") -> None:
202
+ self.browser = browser
203
+ self.lock = lock
204
+ self.state: Dict[str, Any] = {"text": text, "mode": "thinking", "lock": False}
205
+ # target id -> (script id, source) of the script each page runs on load
206
+ self._installed: Dict[str, Tuple[str, str]] = {}
207
+ _OVERLAYS[id(browser)] = self
208
+
209
+ def _script(self) -> str:
210
+ """The overlay as a page loaded now should first show it: current text and mode, unlocked."""
211
+ cfg = {"tag": OVERLAY_TAG, "startId": START_ID, "text": self.state["text"], "mode": self.state["mode"], "mark": MARK_RGBA, "markSize": MARK_SIZE}
212
+ return OVERLAY_JS.replace("__CONFIG__", json.dumps(cfg))
213
+
214
+ async def thinking(self, text: Optional[str] = None) -> None:
215
+ await self.update(text=text, mode="thinking", lock=self.lock)
216
+
217
+ async def acting(self, text: str, *, mode: str = "acting") -> None:
218
+ await self.update(text=text, mode=mode, lock=False)
219
+
220
+ async def waiting(self, text: str) -> None:
221
+ await self.update(text=text, mode="waiting", lock=False)
222
+
223
+ async def update(self, **changes: Any) -> None:
224
+ self.state.update({k: v for k, v in changes.items() if v is not None})
225
+ await self._each_page(_SET_JS % json.dumps(self.state), install=True)
226
+
227
+ async def hide(self) -> None:
228
+ await self._each_page(_HIDDEN_JS % ("hide", "hidden"), install=False)
229
+
230
+ async def show(self) -> None:
231
+ await self._each_page(_HIDDEN_JS % ("show", "visible"), install=False)
232
+
233
+ async def start_page(self, target_id: str) -> None:
234
+ start = (
235
+ START_JS.replace("__ID__", json.dumps(START_ID))
236
+ .replace("__ICON__", json.dumps(f"data:image/png;base64,{ICON_PNG}"))
237
+ .replace("__TEXT__", json.dumps("Starting the browser…"))
238
+ )
239
+ try:
240
+ cdp = await self.browser.get_or_create_cdp_session(target_id=target_id, focus=False)
241
+ await self._evaluate(cdp, start)
242
+ except Exception:
243
+ logger.debug("start page not shown", exc_info=True)
244
+
245
+ def close(self) -> None:
246
+ _OVERLAYS.pop(id(self.browser), None)
247
+
248
+ async def _each_page(self, expression: str, *, install: bool) -> None:
249
+ try:
250
+ targets = await asyncio.wait_for(self.browser._cdp_get_all_pages(), timeout=CDP_TIMEOUT)
251
+ except Exception:
252
+ logger.debug("overlay: pages not listed", exc_info=True)
253
+ return
254
+ script = self._script() if install else ""
255
+ for t in targets or []:
256
+ target_id = t.get("targetId")
257
+ try:
258
+ cdp = await asyncio.wait_for(self.browser.get_or_create_cdp_session(target_id=target_id, focus=False), timeout=CDP_TIMEOUT)
259
+ if install:
260
+ await self._install(cdp, target_id, script)
261
+ await self._evaluate(cdp, expression)
262
+ except Exception:
263
+ logger.debug("overlay: page %s not updated", target_id, exc_info=True)
264
+
265
+ async def _install(self, cdp: Any, target_id: str, script: str) -> None:
266
+ old = self._installed.get(target_id)
267
+ if old and old[1] == script:
268
+ return
269
+ send = cdp.cdp_client.send.Page
270
+ if old:
271
+ await asyncio.wait_for(send.removeScriptToEvaluateOnNewDocument(params={"identifier": old[0]}, session_id=cdp.session_id), timeout=CDP_TIMEOUT)
272
+ added = await asyncio.wait_for(send.addScriptToEvaluateOnNewDocument(params={"source": script}, session_id=cdp.session_id), timeout=CDP_TIMEOUT)
273
+ self._installed[target_id] = (str(added.get("identifier") or ""), script)
274
+ if not old:
275
+ await self._evaluate(cdp, script)
276
+
277
+ @staticmethod
278
+ async def _evaluate(cdp: Any, expression: str) -> None:
279
+ await asyncio.wait_for(
280
+ cdp.cdp_client.send.Runtime.evaluate(params={"expression": expression, "awaitPromise": False}, session_id=cdp.session_id),
281
+ timeout=CDP_TIMEOUT,
282
+ )
283
+
284
+
285
+ def install() -> None:
286
+ """
287
+ Patch the library once per process: blank tabs get the ForgeQE start page (or nothing, with no overlay) instead of
288
+ the library's remote logo, and a screenshot capture hides the overlay only while the pixels are taken.
289
+ """
290
+ try:
291
+ from browser_use.browser.watchdogs.aboutblank_watchdog import AboutBlankWatchdog
292
+ except ImportError:
293
+ return
294
+
295
+ async def start_page(self: Any, target_id: Any, *args: Any, **kwargs: Any) -> None:
296
+ overlay = _OVERLAYS.get(id(self.browser_session))
297
+ if overlay is not None:
298
+ await overlay.start_page(target_id)
299
+
300
+ AboutBlankWatchdog._show_dvd_screensaver_loading_animation_cdp = start_page # type: ignore[method-assign]
301
+
302
+ try:
303
+ from cdp_use.cdp.page.library import PageClient
304
+ except ImportError:
305
+ return
306
+
307
+ original_capture = PageClient.captureScreenshot
308
+ if getattr(original_capture, "__forgeqe__", False):
309
+ return
310
+
311
+ async def captureScreenshot(page: Any, params: Any = None, session_id: Optional[str] = None) -> Any:
312
+ # Hide for the capture itself, not for the slow work around it, so the step bar stays up while the agent acts.
313
+ overlays = [o for o in list(_OVERLAYS.values()) if o is not None]
314
+ for overlay in overlays:
315
+ await overlay.hide()
316
+ try:
317
+ return await original_capture(page, params=params, session_id=session_id)
318
+ finally:
319
+ for overlay in overlays:
320
+ await overlay.show()
321
+
322
+ captureScreenshot.__forgeqe__ = True # type: ignore[attr-defined]
323
+ PageClient.captureScreenshot = captureScreenshot # type: ignore[method-assign]
324
+
325
+
326
+ def _quoted(value: Any, n: int = 50) -> str:
327
+ s = re.sub(r"\s+", " ", str(value or "")).strip()
328
+ return f"“{s if len(s) <= n else s[: n - 1] + '…'}”"
329
+
330
+
331
+ def _page_url(url: Any) -> str:
332
+ try:
333
+ u = urlparse(str(url or ""))
334
+ except ValueError:
335
+ return str(url or "")
336
+ if not u.hostname:
337
+ return str(url or "")
338
+ host = u.hostname[4:] if u.hostname.startswith("www.") else u.hostname
339
+ path = "" if u.path in ("", "/") else u.path
340
+ s = host + path
341
+ return s if len(s) <= 60 else s[:59] + "…"
342
+
343
+
344
+ # CSS selector pieces find_elements is given, as the words for what it looks through.
345
+ _SELECTOR_WORDS = (
346
+ (r"\bbutton\b|role=['\"]?button", "buttons"),
347
+ (r"(^|[\s,>+~])a(\b|\[|\.|#|:)|role=['\"]?link", "links"),
348
+ (r"\binput\b|\btextarea\b|role=['\"]?textbox", "input fields"),
349
+ (r"\bselect\b|\boption\b|role=['\"]?(combobox|listbox|option)", "dropdowns"),
350
+ (r"\btable\b|\btr\b|\btd\b|\bth\b|role=['\"]?(row|cell|grid)", "table rows"),
351
+ (r"\bli\b|\bul\b|\bol\b|role=['\"]?(list|listitem)", "list items"),
352
+ (r"\bh[1-6]\b|role=['\"]?heading", "headings"),
353
+ (r"\bimg\b|role=['\"]?img", "images"),
354
+ (r"\bform\b", "forms"),
355
+ (r"\blabel\b", "labels"),
356
+ )
357
+
358
+
359
+ def _selector_words(selector: Any) -> str:
360
+ sel = str(selector or "")
361
+ words = [w for pattern, w in _SELECTOR_WORDS if re.search(pattern, sel, re.I)]
362
+ if not words:
363
+ return "the elements on the page"
364
+ return "the page's " + (words[0] if len(words) == 1 else ", ".join(words[:-1]) + " and " + words[-1])
365
+
366
+
367
+ def plain_action(name: str, params: Dict[str, Any], label: str = "") -> str:
368
+ """What one library action does, in words a person reads at a glance: `Clicking “Flights”`."""
369
+ p = params if isinstance(params, dict) else {}
370
+ target = f" {_quoted(label)}" if label else ""
371
+ if name in ("navigate", "go_to_url"):
372
+ return f"Opening {_page_url(p.get('url'))}" + (" in a new tab" if p.get("new_tab") else "")
373
+ if name == "search":
374
+ return f"Searching the web for {_quoted(p.get('query'))}"
375
+ if name in ("input", "input_text"):
376
+ text = str(p.get("text") or "")
377
+ if p.get("text_env") or p.get("totp_env") or "<secret>" in text:
378
+ return f"Filling in{target or ' a field'}"
379
+ if not text:
380
+ return f"Clearing{target or ' a field'}"
381
+ return f"Typing {_quoted(text, 40)}" + (f" into{target}" if target else "")
382
+ if name == "click":
383
+ return f"Clicking{target}" if target else "Clicking on the page"
384
+ if name == "send_keys":
385
+ return f"Pressing {p.get('keys') or 'a key'}"
386
+ if name == "scroll":
387
+ direction = "down" if p.get("down", True) else "up"
388
+ return f"Scrolling {direction}" + (f" in{target}" if target else "")
389
+ if name == "find_text":
390
+ return f"Scrolling to {_quoted(p.get('text'))}"
391
+ if name == "record_check":
392
+ return f"Checking {_quoted(p.get('text'))} on the page"
393
+ if name == "go_back":
394
+ return "Going back to the previous page"
395
+ if name == "wait":
396
+ return f"Waiting {p.get('seconds') or 3}s for the page to settle"
397
+ if name == "extract":
398
+ return f"Reading the page: {str(p.get('query') or 'its content').strip()[:90]}"
399
+ if name == "search_page":
400
+ return f"Looking for {_quoted(p.get('pattern'))} on the page"
401
+ if name == "find_elements":
402
+ return f"Looking through {_selector_words(p.get('selector'))}"
403
+ if name == "dropdown_options":
404
+ return f"Checking the options in{target or ' a dropdown'}"
405
+ if name == "select_dropdown":
406
+ return f"Choosing {_quoted(p.get('text'))}" + (f" in{target}" if target else "")
407
+ if name in ("switch", "switch_tab"):
408
+ return "Switching to another tab"
409
+ if name in ("close", "close_tab"):
410
+ return "Closing a tab"
411
+ if name == "upload_file":
412
+ return f"Uploading {os.path.basename(str(p.get('path') or 'a file'))}" + (f" to{target}" if target else "")
413
+ if name == "screenshot":
414
+ return "Taking a screenshot"
415
+ if name == "save_as_pdf":
416
+ return "Saving the page as a PDF"
417
+ if name in ("write_file", "replace_file"):
418
+ return "Writing down what it found"
419
+ if name == "read_file":
420
+ return "Reading its notes"
421
+ if name == "evaluate":
422
+ return "Running a script on the page"
423
+ if name == "done":
424
+ return "Checking the result and finishing"
425
+ return (name.replace("_", " ").capitalize() + target).strip()
426
+
427
+
428
+ # "Next immediate goal: open the sort menu." / "The next immediate goal is to open the sort menu."
429
+ _NEXT_GOAL = re.compile(r"next\s+(?:immediate\s+)?(?:goal|step)\s*(?:is\s+)?(?:to\s+)?[:\-–]?\s*(.+)", re.I | re.S)
430
+
431
+
432
+ def step_intent(payload: Dict[str, Any]) -> str:
433
+ """What the step is for, in one sentence: next_goal, or the next goal the model wrote into its memory."""
434
+ goal = str(payload.get("next_goal") or "").strip()
435
+ if not goal:
436
+ m = _NEXT_GOAL.search(str(payload.get("memory") or ""))
437
+ goal = m.group(1).strip() if m else ""
438
+ goal = re.split(r"(?<=[.!?])\s+", re.sub(r"\s+", " ", goal))[0].rstrip(".").strip()
439
+ if len(goal) > 150:
440
+ goal = goal[:149].rstrip() + "…"
441
+ return goal[:1].upper() + goal[1:]
442
+
443
+
444
+ def step_text(payload: Dict[str, Any]) -> str:
445
+ """The bar's line for an agent step: what it is for, then what it is doing, in plain words."""
446
+ doing = [a for a in payload.get("plain_actions") or [] if a][:3]
447
+ text = f"Step {payload.get('step_number')}"
448
+ intent = step_intent(payload)
449
+ if intent:
450
+ text += f" · {intent}"
451
+ if doing:
452
+ text += " · " + ", then ".join([doing[0]] + [d[:1].lower() + d[1:] for d in doing[1:]])
453
+ return text[:300]