@omnivoice/page-agent 0.4.0 → 0.6.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.
@@ -0,0 +1,4870 @@
1
+ (function() {
2
+ "use strict";
3
+ try {
4
+ if (typeof document != "undefined") {
5
+ var elementStyle = document.createElement("style");
6
+ elementStyle.appendChild(document.createTextNode("._wrapper_1rq3n_1 {\n position: fixed;\n bottom: 100px;\n left: 50%;\n transform: translateX(-50%) translateY(20px);\n opacity: 0;\n z-index: 2147483642; /* 比 SimulatorMask 高一层 */\n box-sizing: border-box;\n\n overflow: visible;\n\n * {\n box-sizing: border-box;\n }\n\n --width: 360px;\n --height: 40px;\n --border-radius: 12px;\n\n --side-space: 12px; /* 控制栏两侧的间距 */\n --history-width: calc(var(--width) - var(--side-space) * 2);\n\n --color-1: rgb(57, 182, 255);\n --color-2: rgb(189, 69, 251);\n --color-3: rgb(255, 87, 51);\n --color-4: rgb(255, 214, 0);\n\n width: var(--width);\n height: var(--height);\n\n transition: all 0.3s ease-in-out;\n\n /* FORK: responsivo. O upstream só encolhia a largura; no celular o painel ainda ficava a\n 100px do rodapé (sobre a área do polegar e sobre a barra do navegador), com as mesmas\n margens laterais de desktop. Aqui ele encosta no rodapé respeitando a safe area, usa a\n largura quase inteira e limita a altura do histórico para não cobrir a página. */\n @media (max-width: 640px) {\n --width: calc(100vw - 16px);\n --side-space: 8px;\n width: var(--width);\n bottom: calc(12px + env(safe-area-inset-bottom, 0px));\n --history-max: min(60vh, calc(100dvh - 120px - var(--height)));\n }\n\n /* Quem prefere menos animação não deve receber transição de layout. */\n @media (prefers-reduced-motion: reduce) {\n transition: none;\n\n * {\n animation: none !important;\n transition: none !important;\n }\n }\n\n ._background_1rq3n_55 {\n position: absolute;\n inset: -2px -8px;\n border-radius: calc(var(--border-radius) + 4px);\n filter: blur(16px);\n overflow: hidden;\n /* mix-blend-mode: lighten; */\n /* display: none; */\n\n &::before {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n /* left: -100%; */\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-1),\n var(--color-2),\n var(--color-1)\n );\n animation: _mask-running_1rq3n_1 2s linear infinite;\n }\n &::after {\n content: '';\n z-index: -1;\n pointer-events: none;\n position: absolute;\n width: 100%;\n height: 100%;\n left: 0;\n top: 0;\n\n background-image: linear-gradient(\n to bottom left,\n var(--color-2),\n var(--color-1),\n var(--color-2)\n );\n animation: _mask-running_1rq3n_1 2s linear infinite;\n animation-delay: 1s;\n }\n }\n}\n\n@keyframes _mask-running_1rq3n_1 {\n from {\n transform: translateX(-100%);\n }\n to {\n transform: translateX(100%);\n }\n}\n\n/* 控制栏 */\n._header_1rq3n_115 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 12px;\n user-select: none;\n\n position: absolute;\n inset: 0;\n\n cursor: pointer;\n flex-shrink: 0; /* 防止 header 被压缩 */\n\n background: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(10px);\n border-radius: var(--border-radius);\n background-clip: padding-box;\n\n box-shadow:\n 0 0 0px 2px rgba(255, 255, 255, 0.4),\n 0 0 5px 1px rgba(255, 255, 255, 0.3);\n\n ._statusSection_1rq3n_137 {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-height: 24px; /* 确保垂直居中 */\n\n ._indicator_1rq3n_144 {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: rgba(255, 255, 255, 0.5);\n flex-shrink: 0;\n animation: none; /* 默认无动画 */\n\n /* 运行状态 - 有动画 */\n &._thinking_1rq3n_153 {\n background: rgb(57, 182, 255);\n animation: _pulse_1rq3n_1 0.8s ease-in-out infinite;\n }\n\n &._tool_executing_1rq3n_158 {\n background: rgb(189, 69, 251);\n animation: _pulse_1rq3n_1 0.6s ease-in-out infinite;\n }\n\n &._retry_1rq3n_163 {\n background: rgb(255, 214, 0);\n animation: _retryPulse_1rq3n_1 1s ease-in-out infinite;\n }\n\n /* 静止状态 - 无动画 */\n &._completed_1rq3n_169,\n &._input_1rq3n_170,\n &._output_1rq3n_171 {\n background: rgb(34, 197, 94);\n animation: none;\n }\n\n &._error_1rq3n_176 {\n background: rgb(239, 68, 68);\n animation: none;\n }\n }\n\n ._statusText_1rq3n_182 {\n color: white;\n font-size: 12px;\n line-height: 1;\n font-weight: 500;\n transition: all 0.3s ease-in-out;\n position: relative;\n overflow: hidden;\n display: flex;\n align-items: center;\n min-height: 24px; /* 确保垂直居中 */\n\n &._fadeOut_1rq3n_194 {\n animation: _statusTextFadeOut_1rq3n_1 0.3s ease forwards;\n }\n\n &._fadeIn_1rq3n_198 {\n animation: _statusTextFadeIn_1rq3n_1 0.3s ease forwards;\n }\n }\n }\n\n ._controls_1rq3n_204 {\n display: flex;\n align-items: center;\n gap: 4px;\n\n ._controlButton_1rq3n_209 {\n width: 24px;\n height: 24px;\n border: none;\n border-radius: 4px;\n background: rgba(255, 255, 255, 0.1);\n color: white;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n line-height: 1;\n\n &:hover {\n background: rgba(255, 255, 255, 0.2);\n }\n }\n\n ._stopButton_1rq3n_228 {\n background: rgba(239, 68, 68, 0.2);\n color: rgb(255, 41, 41);\n font-weight: 600;\n\n &:hover {\n background: rgba(239, 68, 68, 0.3);\n }\n }\n }\n}\n\n@keyframes _statusTextFadeIn_1rq3n_1 {\n 0% {\n opacity: 0;\n transform: translateY(5px);\n }\n 100% {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes _statusTextFadeOut_1rq3n_1 {\n 0% {\n opacity: 1;\n transform: translateY(0);\n }\n 100% {\n opacity: 0;\n transform: translateY(-5px);\n }\n}\n\n._historySectionWrapper_1rq3n_262 {\n position: absolute;\n width: var(--history-width);\n bottom: var(--height);\n left: var(--side-space);\n z-index: -2;\n\n padding-top: 0px;\n visibility: collapse;\n overflow: hidden;\n\n transition: all 0.2s;\n\n background: rgba(2, 0, 20, 0.5);\n /* background: rgba(186, 186, 186, 0.2); */\n backdrop-filter: blur(10px);\n\n text-shadow: 0 0 1px rgba(0, 0, 0, 0.2);\n\n border-top-left-radius: calc(var(--border-radius) + 4px);\n border-top-right-radius: calc(var(--border-radius) + 4px);\n\n /* border: 2px solid rgba(255, 255, 255, 0.8); */\n border: 2px solid rgba(255, 255, 255, 0.4);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.6);\n\n /* @media (prefers-color-scheme: dark) {\n box-shadow:\n 0 8px 32px 0 rgba(0, 0, 0, 0.85),\n 0 2px 12px 0 rgba(57, 182, 255, 0.1);\n } */\n\n ._expanded_1rq3n_294 & {\n padding-top: 8px;\n visibility: visible;\n }\n\n ._historySection_1rq3n_262 {\n position: relative;\n overflow-y: auto;\n overscroll-behavior: contain;\n scrollbar-width: none;\n max-height: 0;\n padding-inline: 8px;\n\n transition: max-height 0.2s;\n\n ._expanded_1rq3n_294 & {\n /* FORK: vira variável para o celular poder reaproveitar o espaço que o painel deixou de\n ocupar no rodapé; a reserva fixa de 200px era do layout de desktop. */\n max-height: var(--history-max, min(500px, calc(100vh - 200px - var(--height))));\n }\n\n /* FORK: componentes de mensagem redesenhados.\n O upstream usava lajota translúcida sobre a página do usuário, então o contraste do texto\n dependia do que estivesse atrás — sobre página clara o texto branco sumia. Aqui a bolha é\n OPACA, com par de cores fixo e contraste medido (o teste mede). O formato segue o de um\n aplicativo de mensagens: a fala da pessoa de um lado, a do agente do outro. */\n ._historyRow_1rq3n_320 {\n display: flex;\n margin-bottom: 8px;\n\n &:last-child {\n margin-bottom: 12px;\n }\n\n &._fromPessoa_1rq3n_328 {\n justify-content: flex-end;\n }\n\n &._fromAgente_1rq3n_332,\n &._fromSistema_1rq3n_333 {\n justify-content: flex-start;\n }\n }\n\n ._historyItem_1rq3n_338 {\n --bolha: #22222e;\n --bolha-texto: #ececf2;\n --bolha-meta: #a9a9bd;\n --bolha-realce: #7a8cff;\n\n max-width: 88%;\n min-width: 0;\n padding: 7px 10px 8px;\n border-radius: 12px;\n background: var(--bolha);\n color: var(--bolha-texto);\n font-size: 13px;\n line-height: 1.45;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);\n\n /* A bolha só precisa de um canto reto do lado de quem fala — é o que dá a leitura de\n conversa sem desenhar rabicho. */\n ._fromPessoa_1rq3n_328 & {\n --bolha: #2f4f74;\n --bolha-texto: #f2f6fb;\n --bolha-meta: #bcd0e6;\n border-bottom-right-radius: 4px;\n }\n\n ._fromAgente_1rq3n_332 &,\n ._fromSistema_1rq3n_333 & {\n border-bottom-left-radius: 4px;\n }\n\n /* O tipo não pinta o fundo inteiro: pintar derruba o contraste e some com a hierarquia.\n Ele colore só o ícone e o nome do autor, que é onde a informação faz falta. */\n &._output_1rq3n_171 {\n --bolha-realce: #58d68d;\n }\n\n &._observation_1rq3n_374 {\n --bolha-realce: #c49bff;\n }\n\n &._question_1rq3n_378 {\n --bolha-realce: #ffb067;\n }\n\n &._doneSuccess_1rq3n_382 {\n --bolha: #1d3b2a;\n --bolha-texto: #e3f8ec;\n --bolha-meta: #a9d8bd;\n --bolha-realce: #58d68d;\n font-weight: 500;\n }\n\n &._doneFailure_1rq3n_390,\n &._error_1rq3n_176 {\n --bolha: #412128;\n --bolha-texto: #fde8ec;\n --bolha-meta: #e3aeb8;\n --bolha-realce: #ff8095;\n }\n\n &._retry_1rq3n_163 {\n --bolha-realce: #ffd166;\n }\n\n ._historyHeader_1rq3n_402 {\n display: flex;\n align-items: center;\n gap: 6px;\n margin-bottom: 3px;\n color: var(--bolha-realce);\n }\n\n ._historyAuthor_1rq3n_410 {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.01em;\n }\n\n ._historyMeta_1rq3n_416 {\n margin-left: auto;\n font-size: 11px;\n color: var(--bolha-meta);\n white-space: nowrap;\n }\n\n /* O ícone é SVG, então o tamanho vem de uma caixa fixa e não do font-size — emoji herdava\n a métrica da fonte do sistema e saía desproporcional. É decorativo: o autor vai em texto\n ao lado, e o SVG carrega aria-hidden. */\n ._statusIcon_1rq3n_426 {\n flex-shrink: 0;\n width: 14px;\n height: 14px;\n line-height: 1;\n\n svg {\n width: 100%;\n height: 100%;\n display: block;\n }\n }\n\n ._historyContent_1rq3n_439 {\n /* FORK: `break-all` partia palavra no meio (\"corretamen/te\"). `anywhere` só quebra\n quando o token não cabe de jeito nenhum — uma URL longa, não uma palavra comum. */\n word-break: normal;\n overflow-wrap: anywhere;\n\n > span,\n ._reflectionLine_1rq3n_446 {\n white-space: pre-wrap;\n }\n }\n\n ._reflectionLines_1rq3n_451 {\n display: flex;\n flex-direction: column;\n gap: 7px;\n }\n\n ._reflectionLine_1rq3n_446 {\n display: block;\n }\n\n /* O rótulo substitui o emoji que antes dava o sentido da linha. */\n ._reflectionLabel_1rq3n_462 {\n display: block;\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.07em;\n text-transform: uppercase;\n color: var(--bolha-meta);\n margin-bottom: 1px;\n }\n }\n\n /* Em telas de alto contraste, a borda devolve o limite da bolha que a cor sozinha dá. */\n @media (prefers-contrast: more) {\n ._historyItem_1rq3n_338 {\n --bolha: #000000;\n --bolha-texto: #ffffff;\n --bolha-meta: #e0e0e0;\n border: 1px solid #ffffff;\n }\n }\n }\n}\n\n/* 动画关键帧 - 更快的闪烁 */\n@keyframes _pulse_1rq3n_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1);\n }\n 50% {\n opacity: 0.4;\n transform: scale(1.3);\n }\n}\n\n/* 重试动画 - 旋转脉冲 */\n@keyframes _retryPulse_1rq3n_1 {\n 0%,\n 100% {\n opacity: 1;\n transform: scale(1) rotate(0deg);\n }\n 25% {\n opacity: 0.6;\n transform: scale(1.2) rotate(90deg);\n }\n 50% {\n opacity: 0.8;\n transform: scale(1.1) rotate(180deg);\n }\n 75% {\n opacity: 0.6;\n transform: scale(1.2) rotate(270deg);\n }\n}\n\n/* 庆祝动画 */\n@keyframes _celebrate_1rq3n_1 {\n 0%,\n 100% {\n transform: scale(1);\n }\n 25% {\n transform: scale(1.2) rotate(-5deg);\n }\n 75% {\n transform: scale(1.2) rotate(5deg);\n }\n}\n\n/* done 卡片的光泽效果 */\n@keyframes _shimmer_1rq3n_1 {\n 0% {\n left: -100%;\n }\n 100% {\n left: 100%;\n }\n}\n\n/* 输入区域样式 */\n._inputSectionWrapper_1rq3n_544 {\n position: absolute;\n width: var(--history-width);\n top: var(--height);\n left: var(--side-space);\n z-index: -1;\n\n visibility: visible;\n overflow: hidden;\n\n height: 48px;\n\n transition: all 0.2s;\n\n background: rgba(186, 186, 186, 0.2);\n backdrop-filter: blur(10px);\n\n border-bottom-left-radius: calc(var(--border-radius) + 4px);\n border-bottom-right-radius: calc(var(--border-radius) + 4px);\n\n border: 2px solid rgba(255, 255, 255, 0.3);\n box-shadow: 0 1px 16px rgba(0, 0, 0, 0.4);\n\n &._hidden_1rq3n_567 {\n visibility: collapse;\n height: 0;\n }\n\n ._inputSection_1rq3n_544 {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 8px 8px;\n\n ._taskInput_1rq3n_578 {\n flex: 1;\n background: rgba(255, 255, 255, 0.4);\n border: 1px solid rgba(255, 255, 255, 0.3);\n border-radius: 10px;\n padding-inline: 10px;\n color: rgb(20, 20, 20);\n font-size: 12px;\n height: 28px;\n line-height: 1;\n outline: none;\n transition: all 0.2s ease;\n\n /* text-shadow: 0 0 2px rgba(255, 255, 255, 0.8); */\n\n /* border-color: rgba(57, 182, 255, 0.3); */\n\n &::placeholder {\n color: rgb(53, 53, 53);\n }\n\n &:focus {\n background: rgba(255, 255, 255, 0.8);\n border-color: rgba(57, 182, 255, 0.6);\n box-shadow: 0 0 0 2px rgba(57, 182, 255, 0.2);\n }\n }\n }\n}\n/* FORK: estilos do widget central (launcher + microfone). Injetados via CSS-in-JS — sem\n request de CSS separada, sem fontes externas. Pensado para mobile (alvo de toque 48px,\n safe-area) e para não brigar com a página host (z-index alto, cor própria). */\n\n.page-agent-widget-launcher {\n position: fixed;\n z-index: 2147483630;\n width: 48px;\n height: 48px;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: #111827;\n color: #f9fafb;\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.28);\n cursor: pointer;\n transition: transform 0.12s ease, box-shadow 0.12s ease, background 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n/* Safe-area em telas com notch/barras: o botão não fica preso na borda. */\n.page-agent-widget-launcher {\n right: calc(20px + env(safe-area-inset-right, 0px));\n bottom: calc(20px + env(safe-area-inset-bottom, 0px));\n}\n\n.page-agent-widget-launcher:hover {\n transform: translateY(-1px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.34);\n background: #1f2937;\n}\n\n.page-agent-widget-launcher:active {\n transform: translateY(0) scale(0.97);\n}\n\n.page-agent-widget-launcher:focus-visible {\n outline: 3px solid #60a5fa;\n outline-offset: 2px;\n}\n\n.page-agent-widget-launcher svg {\n display: block;\n}\n\n/* Botão de microfone, injetado ao lado do input de tarefa do painel. */\n.page-agent-voice-mic {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 34px;\n height: 34px;\n flex: 0 0 auto;\n margin-right: 8px;\n padding: 0;\n border: 1px solid rgba(148, 163, 184, 0.4);\n border-radius: 8px;\n background: rgba(148, 163, 184, 0.12);\n color: inherit;\n cursor: pointer;\n transition: background 0.12s ease, border-color 0.12s ease, transform 0.12s ease;\n -webkit-tap-highlight-color: transparent;\n}\n\n.page-agent-voice-mic:hover {\n background: rgba(148, 163, 184, 0.22);\n border-color: rgba(148, 163, 184, 0.6);\n}\n\n.page-agent-voice-mic:active {\n transform: scale(0.96);\n}\n\n.page-agent-voice-mic:focus-visible {\n outline: 2px solid #60a5fa;\n outline-offset: 1px;\n}\n\n.page-agent-voice-mic.is-listening {\n background: rgba(239, 68, 68, 0.16);\n border-color: rgba(239, 68, 68, 0.6);\n animation: page-agent-voice-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes page-agent-voice-pulse {\n 0%,\n 100% {\n box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.35);\n }\n 50% {\n box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .page-agent-voice-mic.is-listening {\n animation: none;\n }\n}"));
7
+ document.head.appendChild(elementStyle);
8
+ }
9
+ } catch (e) {
10
+ console.error("vite-plugin-css-injected-by-js", e);
11
+ }
12
+ })();
13
+ import * as z from "zod/v4";
14
+ import chalk from "chalk";
15
+ const InvokeErrorTypes = {
16
+ // Retryable
17
+ NETWORK_ERROR: "network_error",
18
+ // Network error, retry
19
+ RATE_LIMIT: "rate_limit",
20
+ // Rate limit, retry
21
+ SERVER_ERROR: "server_error",
22
+ // 5xx, retry
23
+ NO_TOOL_CALL: "no_tool_call",
24
+ // Model did not call tool
25
+ INVALID_TOOL_ARGS: "invalid_tool_args",
26
+ // Tool args don't match schema
27
+ TOOL_EXECUTION_ERROR: "tool_execution_error",
28
+ // Tool execution error
29
+ INVALID_RESPONSE: "invalid_response",
30
+ // Response body is not valid JSON
31
+ INVALID_SCHEMA: "invalid_schema",
32
+ // Response is valid JSON but doesn't match expected shape
33
+ UNKNOWN: "unknown",
34
+ // Non-retryable
35
+ CONFIG_ERROR: "config_error",
36
+ // Invalid local configuration or hook
37
+ AUTH_ERROR: "auth_error",
38
+ // Authentication failed
39
+ CONTEXT_LENGTH: "context_length",
40
+ // Prompt too long
41
+ CONTENT_FILTER: "content_filter"
42
+ // Content filtered
43
+ };
44
+ const RETRYABLE_TYPES = [
45
+ InvokeErrorTypes.NETWORK_ERROR,
46
+ InvokeErrorTypes.RATE_LIMIT,
47
+ InvokeErrorTypes.SERVER_ERROR,
48
+ InvokeErrorTypes.NO_TOOL_CALL,
49
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
50
+ InvokeErrorTypes.TOOL_EXECUTION_ERROR,
51
+ InvokeErrorTypes.INVALID_RESPONSE,
52
+ InvokeErrorTypes.INVALID_SCHEMA,
53
+ InvokeErrorTypes.UNKNOWN
54
+ ];
55
+ class InvokeError extends Error {
56
+ type;
57
+ retryable;
58
+ statusCode;
59
+ /* raw error (provided if this error is caused by another error) */
60
+ rawError;
61
+ /* raw response from the API (provided if this error is caused by an API calling) */
62
+ rawResponse;
63
+ constructor(type, message, rawError, rawResponse) {
64
+ super(message);
65
+ this.name = "InvokeError";
66
+ this.type = type;
67
+ this.retryable = RETRYABLE_TYPES.includes(type);
68
+ this.rawError = rawError;
69
+ this.rawResponse = rawResponse;
70
+ }
71
+ }
72
+ function debug(...args) {
73
+ console.debug(chalk.gray("[LLM]"), ...args);
74
+ }
75
+ function zodToOpenAITool(name, tool2) {
76
+ return {
77
+ type: "function",
78
+ function: {
79
+ name,
80
+ description: tool2.description,
81
+ parameters: z.toJSONSchema(tool2.inputSchema, { target: "openapi-3.0" })
82
+ }
83
+ };
84
+ }
85
+ function modelPatch(body, baseURL) {
86
+ const model = body.model || "";
87
+ if (!model) return body;
88
+ const provider = getProvider(baseURL);
89
+ const modelName = normalizeModelName(model);
90
+ if (modelName.startsWith("qwen")) {
91
+ if (provider === "openrouter" && modelName.startsWith("qwen38-max")) {
92
+ debug("Patch Qwen3.8-max on OpenRouter: reasoning_effort=low, remove tool_choice");
93
+ body.reasoning_effort = "low";
94
+ delete body.tool_choice;
95
+ } else {
96
+ debug("Patch Qwen: disable thinking");
97
+ body.enable_thinking = false;
98
+ }
99
+ if (body.temperature === void 0 && !/max|plus/.test(modelName)) {
100
+ debug("Patch Qwen: raise temperature to 1.0");
101
+ body.temperature = 1;
102
+ }
103
+ }
104
+ if (modelName.startsWith("deepseek")) {
105
+ debug("Patch DeepSeek: disable thinking, remove tool_choice");
106
+ body.thinking = { type: "disabled" };
107
+ delete body.tool_choice;
108
+ }
109
+ if (modelName.startsWith("gpt")) {
110
+ if (modelName.startsWith("gpt-5")) {
111
+ body.verbosity = "low";
112
+ }
113
+ if (modelName.includes("chat-latest")) {
114
+ debug("Patch chat-latest: omit reasoning_effort and temperature");
115
+ delete body.reasoning_effort;
116
+ delete body.temperature;
117
+ } else if (/^gpt-5[12](-|$)/.test(modelName)) {
118
+ debug("Patch GPT-5.1/5.2: reasoning_effort=none");
119
+ body.reasoning_effort = "none";
120
+ } else if (/^gpt-5(-|$)/.test(modelName)) {
121
+ debug("Patch GPT-5: reasoning_effort=minimal");
122
+ body.reasoning_effort = "minimal";
123
+ } else {
124
+ debug("Patch GPT: omit reasoning_effort");
125
+ delete body.reasoning_effort;
126
+ }
127
+ }
128
+ if (modelName.startsWith("claude")) {
129
+ if (/opus|sonnet|haiku/.test(modelName)) {
130
+ debug("Patch Claude: disable thinking");
131
+ body.thinking = { type: "disabled" };
132
+ if (provider !== "openrouter") {
133
+ if (body.tool_choice === "required") {
134
+ debug('Applying Claude patch: convert tool_choice "required" to { type: "any" }');
135
+ body.tool_choice = { type: "any" };
136
+ } else if (body.tool_choice?.function?.name) {
137
+ debug("Applying Claude patch: convert tool_choice format");
138
+ body.tool_choice = { type: "tool", name: body.tool_choice.function.name };
139
+ }
140
+ }
141
+ } else {
142
+ debug("Patch Claude: reasoning_effort=low");
143
+ body.reasoning_effort = "low";
144
+ delete body.tool_choice;
145
+ }
146
+ }
147
+ if (modelName.startsWith("gemini")) {
148
+ debug("Patch Gemini: reasoning_effort=low");
149
+ body.reasoning_effort = "low";
150
+ if (/^gemini-25(?!.*pro)/.test(modelName)) {
151
+ debug("Patch Gemini 2.5 non-Pro: reasoning_effort=none");
152
+ body.reasoning_effort = "none";
153
+ } else if (modelName.startsWith("gemini-35-flash") || modelName.startsWith("gemini-31-flash-lite") || modelName.startsWith("gemini-3-flash")) {
154
+ debug("Patch Gemini 3.x Flash/Lite: reasoning_effort=minimal");
155
+ body.reasoning_effort = "minimal";
156
+ }
157
+ }
158
+ if (modelName.startsWith("glm")) {
159
+ if (/^glm-5[3-9]/.test(modelName)) {
160
+ debug("Patch GLM 5.3+: reasoning_effort=low");
161
+ body.reasoning_effort = "low";
162
+ } else {
163
+ debug("Patch GLM: disable thinking");
164
+ body.thinking = { type: "disabled" };
165
+ }
166
+ }
167
+ if (modelName.startsWith("hy")) {
168
+ debug("Patch Hunyuan: disable thinking, reasoning_effort=low");
169
+ body.thinking = { type: "disabled" };
170
+ body.reasoning_effort = "low";
171
+ }
172
+ if (modelName.startsWith("grok")) {
173
+ if (/^grok-4-?3/.test(modelName)) {
174
+ debug("Patch Grok 4.3: reasoning_effort=none");
175
+ body.reasoning_effort = "none";
176
+ } else if (modelName.startsWith("grok-3-mini") || modelName.startsWith("grok-code-fast")) {
177
+ debug("Patch Grok mini/code: reasoning_effort=low");
178
+ body.reasoning_effort = "low";
179
+ }
180
+ }
181
+ if (modelName.startsWith("kimi")) {
182
+ if (modelName.startsWith("kimi-k3")) {
183
+ debug("Patch Kimi K3: use required tool choice, remove parallel tool calls");
184
+ delete body.parallel_tool_calls;
185
+ if (body.tool_choice?.function?.name) body.tool_choice = "required";
186
+ } else if (!modelName.includes("code")) {
187
+ debug("Patch Kimi: disable thinking");
188
+ body.thinking = { type: "disabled" };
189
+ }
190
+ }
191
+ if (modelName.startsWith("minimax")) {
192
+ debug("Patch MiniMax: remove parallel_tool_calls");
193
+ delete body.parallel_tool_calls;
194
+ if (modelName.includes("m3")) {
195
+ debug("Patch MiniMax: disable thinking");
196
+ body.thinking = { type: "disabled" };
197
+ }
198
+ }
199
+ if (provider === "openrouter") {
200
+ const reasoningEffort = body.reasoning_effort;
201
+ const reasoningDisabled = body.thinking?.type === "disabled" || body.enable_thinking === false || reasoningEffort === "none";
202
+ if (reasoningDisabled) {
203
+ body.reasoning = { enabled: false };
204
+ } else if (reasoningEffort) {
205
+ body.reasoning = { enabled: true, effort: reasoningEffort };
206
+ }
207
+ }
208
+ return body;
209
+ }
210
+ function normalizeModelName(modelName) {
211
+ let normalizedName = modelName.toLowerCase();
212
+ if (normalizedName.includes("/")) {
213
+ normalizedName = normalizedName.split("/")[1];
214
+ }
215
+ normalizedName = normalizedName.replace(/_/g, "");
216
+ normalizedName = normalizedName.replace(/\./g, "");
217
+ return normalizedName;
218
+ }
219
+ function getProvider(baseURL) {
220
+ if (!baseURL) return void 0;
221
+ try {
222
+ const url = new URL(baseURL);
223
+ const hostname = url.hostname;
224
+ if (hostname === "openrouter.ai") return "openrouter";
225
+ return void 0;
226
+ } catch (e) {
227
+ return void 0;
228
+ }
229
+ }
230
+ class OpenAIClient {
231
+ config;
232
+ fetch;
233
+ constructor(config) {
234
+ this.config = config;
235
+ this.fetch = config.customFetch;
236
+ }
237
+ async invoke(messages, tools2, abortSignal, options) {
238
+ abortSignal?.throwIfAborted();
239
+ const openaiTools = Object.entries(tools2).map(([name, t]) => zodToOpenAITool(name, t));
240
+ let toolChoice = "required";
241
+ if (options?.toolChoiceName && !this.config.disableNamedToolChoice) {
242
+ toolChoice = { type: "function", function: { name: options.toolChoiceName } };
243
+ }
244
+ const requestBody = {
245
+ model: this.config.model,
246
+ messages,
247
+ tools: openaiTools,
248
+ parallel_tool_calls: false,
249
+ tool_choice: toolChoice
250
+ };
251
+ if (this.config.temperature !== void 0) {
252
+ requestBody.temperature = this.config.temperature;
253
+ }
254
+ modelPatch(requestBody, this.config.baseURL);
255
+ let transformedBody;
256
+ try {
257
+ transformedBody = this.config.transformRequestBody(requestBody);
258
+ } catch (error2) {
259
+ throw new InvokeError(
260
+ InvokeErrorTypes.CONFIG_ERROR,
261
+ `transformRequestBody failed: ${error2.message}`,
262
+ error2
263
+ );
264
+ }
265
+ const finalRequestBody = transformedBody ?? requestBody;
266
+ let response;
267
+ try {
268
+ response = await this.fetch(`${this.config.baseURL}/chat/completions`, {
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ ...this.config.apiKey && { Authorization: `Bearer ${this.config.apiKey}` }
273
+ },
274
+ body: JSON.stringify(finalRequestBody),
275
+ signal: abortSignal
276
+ });
277
+ } catch (error2) {
278
+ if (error2?.name === "AbortError") throw error2;
279
+ console.error(error2);
280
+ throw new InvokeError(InvokeErrorTypes.NETWORK_ERROR, "Network request failed", error2);
281
+ }
282
+ if (!response.ok) {
283
+ let errorData;
284
+ try {
285
+ errorData = await response.json();
286
+ } catch (error2) {
287
+ if (error2?.name === "AbortError") throw error2;
288
+ }
289
+ const errorMessage = errorData?.error?.message || response.statusText;
290
+ if (response.status === 401 || response.status === 403) {
291
+ throw new InvokeError(
292
+ InvokeErrorTypes.AUTH_ERROR,
293
+ `Authentication failed: ${errorMessage}`,
294
+ errorData
295
+ );
296
+ }
297
+ if (response.status === 429) {
298
+ throw new InvokeError(
299
+ InvokeErrorTypes.RATE_LIMIT,
300
+ `Rate limit exceeded: ${errorMessage}`,
301
+ errorData
302
+ );
303
+ }
304
+ if (response.status >= 500) {
305
+ throw new InvokeError(
306
+ InvokeErrorTypes.SERVER_ERROR,
307
+ `Server error: ${errorMessage}`,
308
+ errorData
309
+ );
310
+ }
311
+ throw new InvokeError(
312
+ InvokeErrorTypes.UNKNOWN,
313
+ `HTTP ${response.status}: ${errorMessage}`,
314
+ errorData
315
+ );
316
+ }
317
+ let data;
318
+ try {
319
+ data = await response.json();
320
+ } catch (error2) {
321
+ if (error2?.name === "AbortError") throw error2;
322
+ throw new InvokeError(
323
+ InvokeErrorTypes.INVALID_RESPONSE,
324
+ "Response body is not valid JSON",
325
+ error2
326
+ );
327
+ }
328
+ const choice = data.choices?.[0];
329
+ if (!choice) {
330
+ throw new InvokeError(InvokeErrorTypes.INVALID_SCHEMA, "No choices in response", data);
331
+ }
332
+ switch (choice.finish_reason) {
333
+ case "tool_calls":
334
+ case "function_call":
335
+ // gemini
336
+ case "stop":
337
+ break;
338
+ case "length":
339
+ throw new InvokeError(
340
+ InvokeErrorTypes.CONTEXT_LENGTH,
341
+ "Response truncated: max tokens reached",
342
+ void 0,
343
+ data
344
+ );
345
+ case "content_filter":
346
+ throw new InvokeError(
347
+ InvokeErrorTypes.CONTENT_FILTER,
348
+ "Content filtered by safety system",
349
+ void 0,
350
+ data
351
+ );
352
+ default:
353
+ throw new InvokeError(
354
+ InvokeErrorTypes.INVALID_SCHEMA,
355
+ `Unexpected finish_reason: ${choice.finish_reason}`,
356
+ void 0,
357
+ data
358
+ );
359
+ }
360
+ const normalizedData = options?.normalizeResponse ? options.normalizeResponse(data) : data;
361
+ const normalizedChoice = normalizedData.choices?.[0];
362
+ const toolCallName = normalizedChoice?.message?.tool_calls?.[0]?.function?.name;
363
+ if (!toolCallName) {
364
+ throw new InvokeError(
365
+ InvokeErrorTypes.NO_TOOL_CALL,
366
+ "No tool call found in response",
367
+ void 0,
368
+ data
369
+ );
370
+ }
371
+ const tool2 = tools2[toolCallName];
372
+ if (!tool2) {
373
+ throw new InvokeError(
374
+ InvokeErrorTypes.UNKNOWN,
375
+ `Tool "${toolCallName}" not found in tools`,
376
+ void 0,
377
+ data
378
+ );
379
+ }
380
+ const argString = normalizedChoice.message?.tool_calls?.[0]?.function?.arguments;
381
+ if (!argString) {
382
+ throw new InvokeError(
383
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
384
+ "No tool call arguments found",
385
+ void 0,
386
+ data
387
+ );
388
+ }
389
+ let parsedArgs;
390
+ try {
391
+ parsedArgs = JSON.parse(argString);
392
+ } catch (error2) {
393
+ throw new InvokeError(
394
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
395
+ "Failed to parse tool arguments as JSON",
396
+ error2,
397
+ data
398
+ );
399
+ }
400
+ const validation = tool2.inputSchema.safeParse(parsedArgs);
401
+ if (!validation.success) {
402
+ console.error(z.prettifyError(validation.error));
403
+ throw new InvokeError(
404
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
405
+ "Tool arguments validation failed",
406
+ validation.error,
407
+ data
408
+ );
409
+ }
410
+ const toolInput = validation.data;
411
+ let toolResult;
412
+ try {
413
+ toolResult = await tool2.execute(toolInput);
414
+ } catch (error2) {
415
+ if (error2?.name === "AbortError") throw error2;
416
+ throw new InvokeError(
417
+ InvokeErrorTypes.TOOL_EXECUTION_ERROR,
418
+ `Tool execution failed: ${error2?.message}`,
419
+ error2,
420
+ data
421
+ );
422
+ }
423
+ return {
424
+ toolCall: {
425
+ name: toolCallName,
426
+ args: toolInput
427
+ },
428
+ toolResult,
429
+ usage: {
430
+ promptTokens: data.usage?.prompt_tokens ?? 0,
431
+ completionTokens: data.usage?.completion_tokens ?? 0,
432
+ totalTokens: data.usage?.total_tokens ?? 0,
433
+ cachedTokens: data.usage?.prompt_tokens_details?.cached_tokens,
434
+ reasoningTokens: data.usage?.completion_tokens_details?.reasoning_tokens
435
+ },
436
+ rawResponse: data,
437
+ rawRequest: finalRequestBody
438
+ };
439
+ }
440
+ }
441
+ class LLM extends EventTarget {
442
+ config;
443
+ client;
444
+ constructor(config) {
445
+ super();
446
+ this.config = parseLLMConfig(config);
447
+ this.client = new OpenAIClient(this.config);
448
+ }
449
+ /**
450
+ * - call llm api *once*
451
+ * - invoke tool call *once*
452
+ * - return the result of the tool
453
+ */
454
+ async invoke(messages, tools2, abortSignal, options) {
455
+ return await withRetry(async () => this.client.invoke(messages, tools2, abortSignal, options), {
456
+ maxRetries: this.config.maxRetries,
457
+ onRetry: (attempt, lastError) => {
458
+ this.dispatchEvent(
459
+ new CustomEvent("retry", {
460
+ detail: { attempt, maxAttempts: this.config.maxRetries, lastError }
461
+ })
462
+ );
463
+ }
464
+ });
465
+ }
466
+ }
467
+ async function withRetry(fn, settings) {
468
+ let attempt = 0;
469
+ while (true) {
470
+ try {
471
+ return await fn();
472
+ } catch (error2) {
473
+ if (error2?.name === "AbortError") throw error2;
474
+ if (error2 instanceof InvokeError && !error2.retryable) throw error2;
475
+ attempt++;
476
+ if (attempt > settings.maxRetries) throw error2;
477
+ console.debug("[LLM] retryable failure, will retry:", error2);
478
+ settings.onRetry(attempt, error2);
479
+ await new Promise((resolve) => setTimeout(resolve, 100));
480
+ }
481
+ }
482
+ }
483
+ function parseLLMConfig(config) {
484
+ if (!config.baseURL || !config.model) {
485
+ throw new Error(
486
+ "[PageAgent] LLM configuration required. Please provide: baseURL, model. See: https://alibaba.github.io/page-agent/docs/features/models"
487
+ );
488
+ }
489
+ if (config.temperature !== void 0) {
490
+ console.warn(
491
+ "[PageAgent] LLMConfig.temperature is deprecated and will be removed in a future version. Use transformRequestBody to set it only for models you have verified accept it."
492
+ );
493
+ }
494
+ return {
495
+ baseURL: config.baseURL,
496
+ model: config.model,
497
+ apiKey: config.apiKey || "",
498
+ temperature: config.temperature,
499
+ maxRetries: config.maxRetries ?? 2,
500
+ transformRequestBody: config.transformRequestBody ?? ((requestBody) => requestBody),
501
+ disableNamedToolChoice: config.disableNamedToolChoice ?? false,
502
+ customFetch: (config.customFetch ?? fetch).bind(globalThis)
503
+ // fetch will be illegal unless bound
504
+ };
505
+ }
506
+ const SYSTEM_PROMPT = 'You are an AI agent designed to operate in an iterative loop to automate browser tasks. Your ultimate goal is accomplishing the task provided in <user_request>.\n\n<intro>\nYou excel at following tasks:\n1. Navigating complex websites and extracting precise information\n2. Automating form submissions and interactive web actions\n3. Gathering and saving information \n4. Operate effectively in an agent loop\n5. Efficiently performing diverse web tasks\n</intro>\n\n<language_settings>\n- Default working language: **English**\n- Use the language that user is using. Return in user\'s language.\n</language_settings>\n\n<input>\nAt every step, your input will consist of: \n1. <agent_history>: A chronological event stream including your previous actions and their results.\n2. <agent_state>: Current <user_request> and <step_info>.\n3. <browser_state>: Current URL, interactive elements indexed for actions, and visible page content.\n</input>\n\n<agent_history>\nAgent history will be given as a list of step information as follows:\n\n<step_{step_number}>:\nEvaluation of Previous Step: Assessment of last action\nMemory: Your memory of this step\nNext Goal: Your goal for this step\nAction Results: Your actions and their results\n</step_{step_number}>\n\nand system messages wrapped in <sys> tag.\n</agent_history>\n\n<user_request>\nUSER REQUEST: This is your ultimate objective and always remains visible.\n- This has the highest priority. Make the user happy.\n- If the user request is very specific - then carefully follow each step and don\'t skip or hallucinate steps.\n- If the task is open ended you can plan yourself how to get it done.\n</user_request>\n\n<browser_state>\n1. Browser State will be given as:\n\nCurrent URL: URL of the page you are currently viewing.\nInteractive Elements: All interactive elements will be provided in format as [index]<type>text</type> where\n- index: Numeric identifier for interaction\n- type: HTML element type (button, input, etc.)\n- text: Element description\n\nExamples:\n[33]<div>User form</div>\n\\t*[35]<button aria-label=\'Submit form\'>Submit</button>\n\nNote that:\n- Only elements with numeric indexes in [] are interactive\n- (stacked) indentation (with \\t) is important and means that the element is a (html) child of the element above (with a lower index)\n- Elements tagged with `*[` are the new clickable elements that appeared on the website since the last step - if url has not changed.\n- Pure text elements without [] are not interactive.\n</browser_state>\n\n<browser_rules>\nStrictly follow these rules while using the browser and navigating the web:\n- Only interact with elements that have a numeric [index] assigned.\n- Only use indexes that are explicitly provided.\n- If the page changes after, for example, an input text action, analyze if you need to interact with new elements, e.g. selecting the right option from the list.\n- By default, only elements in the visible viewport are listed. Use scrolling actions if you suspect relevant content is offscreen which you need to interact with. Scroll ONLY if there are more pixels below or above the page.\n- You can scroll by a specific number of pages using the num_pages parameter (e.g., 0.5 for half page, 2.0 for two pages).\n- All the elements that are scrollable are marked with `data-scrollable` attribute. Including the scrollable distance in every directions. You can scroll *the element* in case some area are overflowed.\n- If a captcha appears, tell user you can not solve captcha. Finish the task and ask user to solve it.\n- If the page is not fully loaded, use the `wait` action.\n- Do not repeat one action for more than 3 times unless some conditions changed.\n- If you fill an input field and your action sequence is interrupted, most often something changed e.g. suggestions popped up under the field.\n- If the <user_request> includes specific page information such as product type, rating, price, location, etc., try to apply filters to be more efficient.\n- The <user_request> is the ultimate goal. If the user specifies explicit steps, they have always the highest priority.\n- If you input_text into a field, you might need to press enter, click the search button, or select from dropdown for completion.\n- Don\'t login into a page if you don\'t have to. Don\'t login if you don\'t have the credentials. \n- There are 2 types of tasks always first think which type of request you are dealing with:\n1. Very specific step by step instructions:\n- Follow them as very precise and don\'t skip steps. Try to complete everything as requested.\n2. Open ended tasks. Plan yourself, be creative in achieving them.\n- If you get stuck e.g. with logins or captcha in open-ended tasks you can re-evaluate the task and try alternative ways, e.g. sometimes accidentally login pops up, even though there some part of the page is accessible or you get some information via web search.\n</browser_rules>\n\n<capability>\n- You can only handle single page app. Do not jump out of current page.\n- Do not click on link if it will open in a new page (e.g., <a target="_blank">)\n- It is ok to fail the task.\n - User can be wrong. If the request of user is not achievable, inappropriate or you do not have enough information or tools to achieve it. Tell user to make a better request.\n - Webpage can be broken. All webpages or apps have bugs. Some bug will make it hard for your job. It\'s encouraged to tell user the problem of current page. Your feedbacks (including failing) are valuable for user.\n - Trying too hard can be harmful. Repeating some action back and forth or pushing for a complex procedure with little knowledge can cause unwanted results and harmful side-effects. User would rather you complete the task with a fail.\n- If you do not have knowledge for the current webpage or task. You must require user to give specific instructions and detailed steps.\n</capability>\n\n<task_completion_rules>\nYou must call the `done` action in one of three cases:\n- When you have fully completed the USER REQUEST.\n- When you reach the final allowed step (`max_steps`), even if the task is incomplete.\n- When you feel stuck or unable to solve user request. Or user request is not clear or contains inappropriate content.\n- If it is ABSOLUTELY IMPOSSIBLE to continue.\n\nThe `done` action is your opportunity to terminate and share your findings with the user.\n- Set `success` to `true` only if the full USER REQUEST has been completed with no missing components.\n- If any part of the request is missing, incomplete, or uncertain, set `success` to `false`.\n- You can use the `text` field of the `done` action to communicate your findings and to provide a coherent reply to the user and fulfill the USER REQUEST.\n- You are ONLY ALLOWED to call `done` as a single action. Don\'t call it together with other actions.\n- If the user asks for specified format, such as "return JSON with following structure", "return a list of format...", MAKE sure to use the right format in your answer.\n- If the user asks for a structured output, your `done` action\'s schema may be modified. Take this schema into account when solving the task!\n</task_completion_rules>\n\n<reasoning_rules>\nExhibit the following reasoning patterns to successfully achieve the <user_request>:\n\n- Reason about <agent_history> to track progress and context toward <user_request>.\n- Analyze the most recent "Next Goal" and "Action Result" in <agent_history> and clearly state what you previously tried to achieve.\n- Analyze all relevant items in <agent_history> and <browser_state> to understand your state.\n- Explicitly judge success/failure/uncertainty of the last action. Never assume an action succeeded just because it appears to be executed in your last step in <agent_history>. If the expected change is missing, mark the last action as failed (or uncertain) and plan a recovery.\n- Analyze whether you are stuck, e.g. when you repeat the same actions multiple times without any progress. Then consider alternative approaches e.g. scrolling for more context or ask user for help.\n- Ask user for help if you have any difficulty. Keep user in the loop.\n- If you see information relevant to <user_request>, plan saving the information to memory.\n- Always reason about the <user_request>. Make sure to carefully analyze the specific steps and information required. E.g. specific filters, specific form fields, specific information to search. Make sure to always compare the current trajectory with the user request and think carefully if thats how the user requested it.\n</reasoning_rules>\n\n<examples>\nHere are examples of good output patterns. Use them as reference but never copy them directly.\n\n<evaluation_examples>\n"evaluation_previous_goal": "Successfully navigated to the product page and found the target information. Verdict: Success"\n"evaluation_previous_goal": "Clicked the login button and user authentication form appeared. Verdict: Success"\n</evaluation_examples>\n\n<memory_examples>\n"memory": "Found many pending reports that need to be analyzed in the main page. Successfully processed the first 2 reports on quarterly sales data and moving on to inventory analysis and customer feedback reports."\n</memory_examples>\n\n<next_goal_examples>\n"next_goal": "Click on the \'Add to Cart\' button to proceed with the purchase flow."\n</next_goal_examples>\n</examples>\n\n<output>\n{\n "evaluation_previous_goal": "Concise one-sentence analysis of your last action. Clearly state success, failure, or uncertain.",\n "memory": "1-3 concise sentences of specific memory of this step and overall progress. You should put here everything that will help you track progress in future steps. Like counting pages visited, items found, etc.",\n "next_goal": "State the next immediate goal and action to achieve it, in one clear sentence.",\n "action":{\n "Action name": {// Action parameters}\n }\n}\n</output>\n';
507
+ const log = console.log.bind(console, chalk.yellow("[autoFixer]"));
508
+ function normalizeResponse(response, tools2) {
509
+ let resolvedArguments;
510
+ const choice = response.choices?.[0];
511
+ if (!choice) throw new Error("No choices in response");
512
+ const message = choice.message;
513
+ if (!message) throw new Error("No message in choice");
514
+ const toolCall = message.tool_calls?.[0];
515
+ if (toolCall?.function?.arguments) {
516
+ resolvedArguments = safeJsonParse(toolCall.function.arguments);
517
+ if (toolCall.function.name && toolCall.function.name !== "AgentOutput") {
518
+ log(`#1: fixing tool_call`);
519
+ resolvedArguments = { action: safeJsonParse(resolvedArguments) };
520
+ }
521
+ } else {
522
+ if (message.content) {
523
+ const content = message.content.trim();
524
+ const jsonInContent = retrieveJsonFromString(content);
525
+ if (jsonInContent) {
526
+ resolvedArguments = safeJsonParse(jsonInContent);
527
+ if (resolvedArguments?.name === "AgentOutput") {
528
+ log(`#2: fixing tool_call`);
529
+ resolvedArguments = safeJsonParse(resolvedArguments.arguments);
530
+ }
531
+ if (resolvedArguments?.type === "function") {
532
+ log(`#3: fixing tool_call`);
533
+ resolvedArguments = safeJsonParse(resolvedArguments.function.arguments);
534
+ }
535
+ if (!resolvedArguments?.action && !resolvedArguments?.evaluation_previous_goal && !resolvedArguments?.memory && !resolvedArguments?.next_goal && !resolvedArguments?.thinking) {
536
+ log(`#4: fixing tool_call`);
537
+ resolvedArguments = { action: safeJsonParse(resolvedArguments) };
538
+ }
539
+ } else {
540
+ throw new Error("No tool_call and the message content does not contain valid JSON");
541
+ }
542
+ } else {
543
+ throw new Error("No tool_call nor message content is present");
544
+ }
545
+ }
546
+ resolvedArguments = safeJsonParse(resolvedArguments);
547
+ if (resolvedArguments.action) {
548
+ resolvedArguments.action = safeJsonParse(resolvedArguments.action);
549
+ }
550
+ if (resolvedArguments.action && tools2) {
551
+ resolvedArguments.action = validateAction(resolvedArguments.action, tools2);
552
+ }
553
+ if (!resolvedArguments.action) {
554
+ log(`#5: fixing tool_call`);
555
+ resolvedArguments.action = { wait: { seconds: 1 } };
556
+ }
557
+ return {
558
+ ...response,
559
+ choices: [
560
+ {
561
+ ...choice,
562
+ message: {
563
+ ...message,
564
+ tool_calls: [
565
+ {
566
+ ...toolCall || {},
567
+ function: {
568
+ ...toolCall?.function || {},
569
+ name: "AgentOutput",
570
+ arguments: JSON.stringify(resolvedArguments)
571
+ }
572
+ }
573
+ ]
574
+ }
575
+ }
576
+ ]
577
+ };
578
+ }
579
+ function validateAction(action, tools2) {
580
+ if (typeof action !== "object" || action === null) return action;
581
+ const toolName = Object.keys(action)[0];
582
+ if (!toolName) return action;
583
+ const tool2 = tools2.get(toolName);
584
+ if (!tool2) {
585
+ const available = Array.from(tools2.keys()).join(", ");
586
+ throw new InvokeError(
587
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
588
+ `Unknown action "${toolName}". Available: ${available}`
589
+ );
590
+ }
591
+ let value = action[toolName];
592
+ const schema = tool2.inputSchema;
593
+ if (schema instanceof z.ZodObject && value !== null && typeof value !== "object") {
594
+ const requiredKey = Object.keys(schema.shape).find(
595
+ (k) => !schema.shape[k].safeParse(void 0).success
596
+ );
597
+ if (requiredKey) {
598
+ log(`coercing primitive action input for "${toolName}"`);
599
+ value = { [requiredKey]: value };
600
+ }
601
+ }
602
+ const result2 = schema.safeParse(value);
603
+ if (!result2.success) {
604
+ throw new InvokeError(
605
+ InvokeErrorTypes.INVALID_TOOL_ARGS,
606
+ `Invalid input for action "${toolName}": ${z.prettifyError(result2.error)}`
607
+ );
608
+ }
609
+ return { [toolName]: result2.data };
610
+ }
611
+ function safeJsonParse(input2) {
612
+ if (typeof input2 === "string") {
613
+ try {
614
+ return JSON.parse(input2.trim());
615
+ } catch {
616
+ return input2;
617
+ }
618
+ }
619
+ return input2;
620
+ }
621
+ function retrieveJsonFromString(str) {
622
+ try {
623
+ const json = /({[\s\S]*})/.exec(str) ?? [];
624
+ if (json.length === 0) {
625
+ return null;
626
+ }
627
+ return JSON.parse(json[0]);
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+ async function waitFor$1(seconds, signal2) {
633
+ if (!signal2) {
634
+ await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
635
+ return;
636
+ }
637
+ signal2.throwIfAborted();
638
+ await new Promise((resolve, reject) => {
639
+ const timer = setTimeout(() => {
640
+ signal2.removeEventListener("abort", onAbort);
641
+ resolve();
642
+ }, seconds * 1e3);
643
+ const onAbort = () => {
644
+ clearTimeout(timer);
645
+ reject(signal2.reason);
646
+ };
647
+ signal2.addEventListener("abort", onAbort, { once: true });
648
+ });
649
+ }
650
+ function truncate$1(text, maxLength) {
651
+ if (text.length > maxLength) {
652
+ return text.substring(0, maxLength) + "...";
653
+ }
654
+ return text;
655
+ }
656
+ function randomID(existingIDs) {
657
+ let id = Math.random().toString(36).substring(2, 11);
658
+ if (!existingIDs) {
659
+ return id;
660
+ }
661
+ const MAX_TRY = 1e3;
662
+ let tryCount = 0;
663
+ while (existingIDs.includes(id)) {
664
+ id = Math.random().toString(36).substring(2, 11);
665
+ tryCount++;
666
+ if (tryCount > MAX_TRY) {
667
+ throw new Error("randomID: too many tries");
668
+ }
669
+ }
670
+ return id;
671
+ }
672
+ const _global = globalThis;
673
+ if (!_global.__PAGE_AGENT_IDS__) {
674
+ _global.__PAGE_AGENT_IDS__ = [];
675
+ }
676
+ const ids = _global.__PAGE_AGENT_IDS__;
677
+ function uid() {
678
+ const id = randomID(ids);
679
+ ids.push(id);
680
+ return id;
681
+ }
682
+ const llmsTxtCache = /* @__PURE__ */ new Map();
683
+ async function fetchLlmsTxt(url) {
684
+ let origin;
685
+ try {
686
+ origin = new URL(url).origin;
687
+ } catch {
688
+ return null;
689
+ }
690
+ if (origin === "null") return null;
691
+ if (llmsTxtCache.has(origin)) return llmsTxtCache.get(origin);
692
+ const endpoint = `${origin}/llms.txt`;
693
+ let result2 = null;
694
+ try {
695
+ console.log(chalk.gray(`[llms.txt] Fetching ${endpoint}`));
696
+ const res = await fetch(endpoint, { signal: AbortSignal.timeout(3e3) });
697
+ if (res.ok) {
698
+ result2 = await res.text();
699
+ console.log(chalk.green(`[llms.txt] Found (${result2.length} chars)`));
700
+ if (result2.length > 1e3) {
701
+ console.log(chalk.yellow(`[llms.txt] Truncating to 1000 chars`));
702
+ result2 = truncate$1(result2, 1e3);
703
+ }
704
+ } else {
705
+ console.debug(chalk.gray(`[llms.txt] ${res.status} for ${endpoint}`));
706
+ }
707
+ } catch (e) {
708
+ console.debug(chalk.gray(`[llms.txt] not found for ${endpoint}`), e);
709
+ }
710
+ llmsTxtCache.set(origin, result2);
711
+ return result2;
712
+ }
713
+ function assert(condition, message, silent) {
714
+ if (!condition) {
715
+ const errorMessage = message ?? "Assertion failed";
716
+ console.error(chalk.red(`❌ assert: ${errorMessage}`));
717
+ throw new Error(errorMessage);
718
+ }
719
+ }
720
+ async function suppress(fn) {
721
+ try {
722
+ return await fn();
723
+ } catch (error2) {
724
+ console.error(error2);
725
+ return void 0;
726
+ }
727
+ }
728
+ function tool(options) {
729
+ return options;
730
+ }
731
+ const tools = /* @__PURE__ */ new Map();
732
+ tools.set(
733
+ "done",
734
+ tool({
735
+ description: "Complete task. Text is your final response to the user — keep it concise unless the user explicitly asks for detail.",
736
+ inputSchema: z.object({
737
+ text: z.string(),
738
+ success: z.boolean().default(true)
739
+ }),
740
+ execute: async function(input2) {
741
+ return Promise.resolve("Task completed");
742
+ }
743
+ })
744
+ );
745
+ tools.set(
746
+ "wait",
747
+ tool({
748
+ description: "Wait for x seconds. Can be used to wait until the page or data is fully loaded.",
749
+ inputSchema: z.object({
750
+ seconds: z.number().min(1).max(10).default(1)
751
+ }),
752
+ execute: async function(input2, { signal: signal2 }) {
753
+ const lastTimeUpdate = await this.pageController.getLastUpdateTime();
754
+ const secondsSinceLastUpdate = (Date.now() - lastTimeUpdate) / 1e3;
755
+ const actualWaitTime = Math.max(0, input2.seconds - secondsSinceLastUpdate);
756
+ console.log(`actualWaitTime: ${actualWaitTime} seconds`);
757
+ await waitFor$1(actualWaitTime, signal2);
758
+ const waitedSeconds = (secondsSinceLastUpdate + actualWaitTime).toFixed(2);
759
+ return `✅ Waited for ${waitedSeconds} seconds.`;
760
+ }
761
+ })
762
+ );
763
+ tools.set(
764
+ "ask_user",
765
+ tool({
766
+ description: "Ask the user a question and wait for their answer. Use this if you need more information or clarification.",
767
+ inputSchema: z.object({
768
+ question: z.string()
769
+ }),
770
+ execute: async function(input2, { signal: signal2 }) {
771
+ if (!this.onAskUser) {
772
+ throw new Error("ask_user tool requires onAskUser callback to be set");
773
+ }
774
+ const answer = await this.onAskUser(input2.question, { signal: signal2 });
775
+ return `User answered: ${answer}`;
776
+ }
777
+ })
778
+ );
779
+ tools.set(
780
+ "click_element_by_index",
781
+ tool({
782
+ description: "Click element by index",
783
+ inputSchema: z.object({
784
+ index: z.int().min(0)
785
+ }),
786
+ execute: async function(input2) {
787
+ const result2 = await this.pageController.clickElement(input2.index);
788
+ return result2.message;
789
+ }
790
+ })
791
+ );
792
+ tools.set(
793
+ "input_text",
794
+ tool({
795
+ description: "Click and type text into an interactive input element",
796
+ inputSchema: z.object({
797
+ index: z.int().min(0),
798
+ text: z.string()
799
+ }),
800
+ execute: async function(input2) {
801
+ const result2 = await this.pageController.inputText(input2.index, input2.text);
802
+ return result2.message;
803
+ }
804
+ })
805
+ );
806
+ tools.set(
807
+ "select_dropdown_option",
808
+ tool({
809
+ description: "Select dropdown option for interactive element index by the text of the option you want to select",
810
+ inputSchema: z.object({
811
+ index: z.int().min(0),
812
+ text: z.string()
813
+ }),
814
+ execute: async function(input2) {
815
+ const result2 = await this.pageController.selectOption(input2.index, input2.text);
816
+ return result2.message;
817
+ }
818
+ })
819
+ );
820
+ tools.set(
821
+ "scroll",
822
+ tool({
823
+ description: "Scroll vertically. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
824
+ inputSchema: z.object({
825
+ down: z.boolean().default(true),
826
+ num_pages: z.number().min(0).max(10).optional().default(0.1),
827
+ pixels: z.number().int().min(0).optional(),
828
+ index: z.number().int().min(0).optional()
829
+ }),
830
+ execute: async function(input2) {
831
+ const result2 = await this.pageController.scroll({
832
+ ...input2,
833
+ numPages: input2.num_pages
834
+ });
835
+ return result2.message;
836
+ }
837
+ })
838
+ );
839
+ tools.set(
840
+ "scroll_horizontally",
841
+ tool({
842
+ description: "Scroll horizontally. Without index: scrolls the document. With index: scrolls the container at that index (or its nearest scrollable ancestor). Use index of a data-scrollable element to scroll a specific area.",
843
+ inputSchema: z.object({
844
+ right: z.boolean().default(true),
845
+ pixels: z.number().int().min(0),
846
+ index: z.number().int().min(0).optional()
847
+ }),
848
+ execute: async function(input2) {
849
+ const result2 = await this.pageController.scrollHorizontally(input2);
850
+ return result2.message;
851
+ }
852
+ })
853
+ );
854
+ tools.set(
855
+ "execute_javascript",
856
+ tool({
857
+ description: "Execute JavaScript code on the current page. Supports async/await syntax. Use with caution! An `AbortSignal` named `signal` is available in scope: long-running async code MUST honor it (e.g. `await fetch(url, { signal })`, or `signal.throwIfAborted()` in loops)",
858
+ inputSchema: z.object({
859
+ script: z.string()
860
+ }),
861
+ execute: async function(input2, { signal: signal2 }) {
862
+ const result2 = await this.pageController.executeJavascript(input2.script, signal2);
863
+ signal2.throwIfAborted();
864
+ return result2.message;
865
+ }
866
+ })
867
+ );
868
+ class PageAgentCore extends EventTarget {
869
+ id = uid();
870
+ config;
871
+ tools;
872
+ /** PageController for DOM operations */
873
+ pageController;
874
+ task = "";
875
+ taskId = "";
876
+ /** History events */
877
+ history = [];
878
+ /** Whether this agent has been disposed */
879
+ disposed = false;
880
+ /**
881
+ * Called when the agent needs to ask the user questions.
882
+ * If unset, the `ask_user` tool will be disabled.
883
+ * Implementations should reject the promise when `signal` aborts.
884
+ * @example onAskUser: (q) => window.prompt(q) || ''
885
+ */
886
+ onAskUser;
887
+ #status = "idle";
888
+ #llm;
889
+ /**
890
+ * Task cancellation primitive: its signal reaches the LLM fetch, tools
891
+ * (via `ctx.signal`) and async callbacks. Aborted only by `stop`/`dispose`
892
+ * (during a task) or task setup, always WITHOUT a reason so `signal.reason`
893
+ * stays a standard `AbortError`.
894
+ */
895
+ #abortController = new AbortController();
896
+ #observations = [];
897
+ /** Resolves when the current run has fully settled. Awaited by `stop()`. */
898
+ #running = Promise.resolve();
899
+ #lastResult = null;
900
+ /** internal states during a single task execution */
901
+ #states = {
902
+ /** Accumulated wait time in seconds */
903
+ totalWaitTime: 0,
904
+ /** For detecting navigation */
905
+ lastURL: "",
906
+ /** Browser state */
907
+ browserState: null
908
+ };
909
+ constructor(config) {
910
+ super();
911
+ this.config = { ...config, maxSteps: config.maxSteps ?? 40 };
912
+ this.#llm = new LLM(this.config);
913
+ this.tools = new Map(tools);
914
+ this.pageController = config.pageController;
915
+ this.#llm.addEventListener("retry", (e) => {
916
+ const { attempt, maxAttempts, lastError } = e.detail;
917
+ this.#emitActivity({ type: "retrying", attempt, maxAttempts });
918
+ this.history.push({
919
+ type: "error",
920
+ message: String(lastError),
921
+ rawResponse: lastError.rawResponse
922
+ });
923
+ this.history.push({
924
+ type: "retry",
925
+ message: `LLM retry attempt ${attempt} of ${maxAttempts}`,
926
+ attempt,
927
+ maxAttempts
928
+ });
929
+ this.#emitHistoryChange();
930
+ });
931
+ if (this.config.customTools) {
932
+ for (const [name, tool2] of Object.entries(this.config.customTools)) {
933
+ if (tool2 === null) {
934
+ this.tools.delete(name);
935
+ continue;
936
+ }
937
+ this.tools.set(name, tool2);
938
+ }
939
+ }
940
+ if (!this.config.experimentalScriptExecutionTool) {
941
+ this.tools.delete("execute_javascript");
942
+ }
943
+ }
944
+ /** Get current agent status */
945
+ get status() {
946
+ return this.#status;
947
+ }
948
+ /** Result of the most recent run, or `null` before the first run completes. */
949
+ get lastResult() {
950
+ return this.#lastResult;
951
+ }
952
+ /** Emit statuschange event */
953
+ #emitStatusChange() {
954
+ this.dispatchEvent(new Event("statuschange"));
955
+ }
956
+ /** Emit historychange event */
957
+ #emitHistoryChange(pushHistoricalEvent) {
958
+ if (pushHistoricalEvent) this.history.push(pushHistoricalEvent);
959
+ this.dispatchEvent(new Event("historychange"));
960
+ }
961
+ /**
962
+ * Emit activity event - for transient UI feedback
963
+ * @param activity - Current agent activity
964
+ */
965
+ #emitActivity(activity) {
966
+ this.dispatchEvent(new CustomEvent("activity", { detail: activity }));
967
+ }
968
+ /** Update status and emit event */
969
+ #setStatus(status) {
970
+ if (this.#status !== status) {
971
+ this.#status = status;
972
+ this.#emitStatusChange();
973
+ }
974
+ }
975
+ /**
976
+ * Push an observation message to the history event stream.
977
+ * This will be visible in <agent_history> and remain persistent in memory across steps.
978
+ * @experimental @internal
979
+ * @note history change will be emitted before next step starts
980
+ */
981
+ pushObservation(content) {
982
+ this.#observations.push(content);
983
+ }
984
+ /**
985
+ * Stop the current task and wait until the run has fully settled (including lifecycle hooks).
986
+ * @note never await .stop() in a lifecycle hook.
987
+ */
988
+ async stop() {
989
+ if (this.#status !== "running") return;
990
+ this.#abortController.abort();
991
+ await this.#running;
992
+ }
993
+ /**
994
+ * external errors (pre-checks/config/hooks) will threw;
995
+ * agent errors will be caught and added to history, and return a failed result
996
+ */
997
+ async execute(task) {
998
+ if (this.disposed) throw new Error("PageAgent has been disposed. Create a new instance.");
999
+ if (this.#status === "running") throw new Error("A task is already running.");
1000
+ if (!task) throw new Error("Task is required");
1001
+ this.task = task;
1002
+ this.taskId = uid();
1003
+ this.history = [];
1004
+ this.#observations = [];
1005
+ this.#states = { totalWaitTime: 0, lastURL: "", browserState: null };
1006
+ this.#abortController = new AbortController();
1007
+ const signal2 = this.#abortController.signal;
1008
+ let resolveRunning;
1009
+ this.#running = new Promise((r) => resolveRunning = r);
1010
+ this.#setStatus("running");
1011
+ this.#emitHistoryChange();
1012
+ if (!this.onAskUser) this.tools.delete("ask_user");
1013
+ const onBeforeStep = this.config.onBeforeStep;
1014
+ const onAfterStep = this.config.onAfterStep;
1015
+ const onBeforeTask = this.config.onBeforeTask;
1016
+ const onAfterTask = this.config.onAfterTask;
1017
+ const stepDelay = this.config.stepDelay ?? 0.4;
1018
+ const maxSteps = this.config.maxSteps;
1019
+ let step = 0;
1020
+ let taskResult;
1021
+ let finalStatus = "error";
1022
+ await suppress(() => this.pageController.showMask());
1023
+ try {
1024
+ await onBeforeTask?.(this);
1025
+ while (true) {
1026
+ await onBeforeStep?.(this, step);
1027
+ try {
1028
+ console.group(`step: ${step}`);
1029
+ if (step > 0) await waitFor$1(stepDelay, signal2);
1030
+ signal2.throwIfAborted();
1031
+ console.log(chalk.blue.bold("👀 Observing..."));
1032
+ this.#states.browserState = await this.pageController.getBrowserState();
1033
+ await this.#handleObservations(step);
1034
+ const messages = [
1035
+ { role: "system", content: this.#getSystemPrompt() },
1036
+ { role: "user", content: await this.#assembleUserPrompt() }
1037
+ ];
1038
+ const macroTool = { AgentOutput: this.#packMacroTool() };
1039
+ console.log(chalk.blue.bold("🧠 Thinking..."));
1040
+ this.#emitActivity({ type: "thinking" });
1041
+ const result2 = await this.#llm.invoke(messages, macroTool, signal2, {
1042
+ toolChoiceName: "AgentOutput",
1043
+ normalizeResponse: (res) => normalizeResponse(res, this.tools)
1044
+ });
1045
+ const macroResult = result2.toolResult;
1046
+ const input2 = macroResult.input;
1047
+ const output2 = macroResult.output;
1048
+ const reflection = {
1049
+ evaluation_previous_goal: input2.evaluation_previous_goal,
1050
+ memory: input2.memory,
1051
+ next_goal: input2.next_goal
1052
+ };
1053
+ const actionName = Object.keys(input2.action)[0];
1054
+ const action = {
1055
+ name: actionName,
1056
+ input: input2.action[actionName],
1057
+ output: output2
1058
+ };
1059
+ this.#emitHistoryChange({
1060
+ type: "step",
1061
+ stepIndex: step,
1062
+ reflection,
1063
+ action,
1064
+ usage: result2.usage,
1065
+ rawResponse: result2.rawResponse,
1066
+ rawRequest: result2.rawRequest
1067
+ });
1068
+ if (actionName === "done") {
1069
+ const success = action.input?.success ?? false;
1070
+ const data = action.input?.text || "no text provided";
1071
+ console.log(chalk.green.bold("Task completed"), success, data);
1072
+ taskResult = { success, data, history: this.history };
1073
+ this.#lastResult = taskResult;
1074
+ finalStatus = "completed";
1075
+ break;
1076
+ }
1077
+ } catch (error2) {
1078
+ const isAbortError = error2?.name === "AbortError";
1079
+ if (!isAbortError) console.error("Task failed", error2);
1080
+ const message = isAbortError ? "Tarefa interrompida" : String(error2);
1081
+ this.#emitActivity({ type: "error", message });
1082
+ this.#emitHistoryChange({ type: "error", message, rawResponse: error2 });
1083
+ taskResult = { success: false, data: message, history: this.history };
1084
+ this.#lastResult = taskResult;
1085
+ finalStatus = isAbortError ? "stopped" : "error";
1086
+ break;
1087
+ } finally {
1088
+ console.groupEnd();
1089
+ await onAfterStep?.(this, this.history);
1090
+ }
1091
+ step++;
1092
+ if (step > maxSteps) {
1093
+ const message = "Step count exceeded maximum limit";
1094
+ console.error(message);
1095
+ this.#emitActivity({ type: "error", message });
1096
+ this.#emitHistoryChange({ type: "error", message });
1097
+ taskResult = { success: false, data: message, history: this.history };
1098
+ this.#lastResult = taskResult;
1099
+ finalStatus = "error";
1100
+ break;
1101
+ }
1102
+ }
1103
+ await onAfterTask?.(this, taskResult);
1104
+ return taskResult;
1105
+ } catch (error2) {
1106
+ this.#emitActivity({ type: "error", message: String(error2) });
1107
+ finalStatus = "error";
1108
+ throw error2;
1109
+ } finally {
1110
+ await suppress(() => this.pageController.cleanUpHighlights());
1111
+ await suppress(() => this.pageController.hideMask());
1112
+ this.#abortController.abort();
1113
+ resolveRunning();
1114
+ this.#setStatus(finalStatus);
1115
+ }
1116
+ }
1117
+ /**
1118
+ * Merge all tools into a single MacroTool with the following input:
1119
+ * - thinking: string
1120
+ * - evaluation_previous_goal: string
1121
+ * - memory: string
1122
+ * - next_goal: string
1123
+ * - action: { toolName: toolInput }
1124
+ * where action must be selected from tools defined in this.tools
1125
+ */
1126
+ #packMacroTool() {
1127
+ const tools2 = this.tools;
1128
+ const actionSchemas = Array.from(tools2.entries()).map(([toolName, tool2]) => {
1129
+ return z.object({ [toolName]: tool2.inputSchema }).describe(tool2.description);
1130
+ });
1131
+ const actionSchema = z.union(actionSchemas);
1132
+ const macroToolSchema = z.object({
1133
+ // thinking: z.string().optional(),
1134
+ evaluation_previous_goal: z.string().optional(),
1135
+ memory: z.string().optional(),
1136
+ next_goal: z.string().optional(),
1137
+ action: actionSchema
1138
+ });
1139
+ return {
1140
+ description: "You MUST call this tool every step!",
1141
+ inputSchema: macroToolSchema,
1142
+ execute: async (input2) => {
1143
+ const signal2 = this.#abortController.signal;
1144
+ signal2.throwIfAborted();
1145
+ console.log(chalk.blue.bold("MacroTool input"), input2);
1146
+ const action = input2.action;
1147
+ const toolName = Object.keys(action)[0];
1148
+ const toolInput = action[toolName];
1149
+ const reflectionLines2 = [];
1150
+ if (input2.evaluation_previous_goal)
1151
+ reflectionLines2.push(`✅: ${input2.evaluation_previous_goal}`);
1152
+ if (input2.memory) reflectionLines2.push(`💾: ${input2.memory}`);
1153
+ if (input2.next_goal) reflectionLines2.push(`🎯: ${input2.next_goal}`);
1154
+ const reflectionText = reflectionLines2.length > 0 ? reflectionLines2.join("\n") : "";
1155
+ if (reflectionText) {
1156
+ console.log(reflectionText);
1157
+ }
1158
+ const tool2 = tools2.get(toolName);
1159
+ assert(tool2, `Tool ${toolName} not found`);
1160
+ console.log(chalk.blue.bold(`Executing tool: ${toolName}`), toolInput);
1161
+ this.#emitActivity({ type: "executing", tool: toolName, input: toolInput });
1162
+ const startTime = Date.now();
1163
+ const result2 = await tool2.execute.bind(this)(toolInput, { signal: signal2 });
1164
+ signal2.throwIfAborted();
1165
+ const duration = Date.now() - startTime;
1166
+ console.log(chalk.green.bold(`Tool (${toolName}) executed for ${duration}ms`), result2);
1167
+ this.#emitActivity({
1168
+ type: "executed",
1169
+ tool: toolName,
1170
+ input: toolInput,
1171
+ output: result2,
1172
+ duration
1173
+ });
1174
+ if (toolName === "wait") {
1175
+ this.#states.totalWaitTime += toolInput?.seconds || 0;
1176
+ } else {
1177
+ this.#states.totalWaitTime = 0;
1178
+ }
1179
+ return {
1180
+ input: input2,
1181
+ output: result2
1182
+ };
1183
+ }
1184
+ };
1185
+ }
1186
+ /**
1187
+ * Get system prompt, dynamically replace language settings based on configured language
1188
+ */
1189
+ #getSystemPrompt() {
1190
+ if (this.config.customSystemPrompt) {
1191
+ return this.config.customSystemPrompt;
1192
+ }
1193
+ const targetLanguage = this.config.language === "pt-BR" ? "Portuguese (Brazil)" : this.config.language === "zh-CN" ? "Chinese" : "English";
1194
+ const systemPrompt = SYSTEM_PROMPT.replace(
1195
+ /Default working language: \*\*.*?\*\*/,
1196
+ `Default working language: **${targetLanguage}**`
1197
+ );
1198
+ return systemPrompt;
1199
+ }
1200
+ /**
1201
+ * Get instructions from config
1202
+ */
1203
+ async #getInstructions() {
1204
+ const { instructions, experimentalLlmsTxt } = this.config;
1205
+ const systemInstructions = instructions?.system?.trim();
1206
+ let pageInstructions;
1207
+ const url = this.#states.browserState?.url || "";
1208
+ if (instructions?.getPageInstructions && url) {
1209
+ try {
1210
+ pageInstructions = instructions.getPageInstructions(url)?.trim();
1211
+ } catch (error2) {
1212
+ console.error(
1213
+ chalk.red("[PageAgent] Failed to execute getPageInstructions callback:"),
1214
+ error2
1215
+ );
1216
+ }
1217
+ }
1218
+ const llmsTxt = experimentalLlmsTxt && url ? await fetchLlmsTxt(url) : void 0;
1219
+ if (!systemInstructions && !pageInstructions && !llmsTxt) return "";
1220
+ let result2 = "<instructions>\n";
1221
+ if (systemInstructions) {
1222
+ result2 += `<system_instructions>
1223
+ ${systemInstructions}
1224
+ </system_instructions>
1225
+ `;
1226
+ }
1227
+ if (pageInstructions) {
1228
+ result2 += `<page_instructions>
1229
+ ${pageInstructions}
1230
+ </page_instructions>
1231
+ `;
1232
+ }
1233
+ if (llmsTxt) {
1234
+ result2 += `<llms_txt>
1235
+ ${llmsTxt}
1236
+ </llms_txt>
1237
+ `;
1238
+ }
1239
+ result2 += "</instructions>\n\n";
1240
+ return result2;
1241
+ }
1242
+ /**
1243
+ * Generate system observations before each step
1244
+ * @todo loop detection
1245
+ * @todo console error
1246
+ */
1247
+ async #handleObservations(step) {
1248
+ if (this.#states.totalWaitTime >= 3) {
1249
+ this.pushObservation(
1250
+ `You have waited ${this.#states.totalWaitTime} seconds accumulatively. DO NOT wait any longer unless you have a good reason.`
1251
+ );
1252
+ }
1253
+ const currentURL = this.#states.browserState?.url || "";
1254
+ if (currentURL !== this.#states.lastURL) {
1255
+ this.pushObservation(`A página navegou para → ${currentURL}`);
1256
+ this.#states.lastURL = currentURL;
1257
+ await waitFor$1(0.5);
1258
+ }
1259
+ const remaining = this.config.maxSteps - step;
1260
+ if (remaining === 5) {
1261
+ this.pushObservation(
1262
+ `⚠️ Only ${remaining} steps remaining. Consider wrapping up or calling done with partial results.`
1263
+ );
1264
+ } else if (remaining === 2) {
1265
+ this.pushObservation(
1266
+ `⚠️ Critical: Only ${remaining} steps left! You must finish the task or call done immediately.`
1267
+ );
1268
+ }
1269
+ if (this.#observations.length > 0) {
1270
+ for (const content of this.#observations) {
1271
+ this.history.push({ type: "observation", content });
1272
+ console.log(chalk.cyan("Observation:"), content);
1273
+ }
1274
+ this.#observations = [];
1275
+ this.#emitHistoryChange();
1276
+ }
1277
+ }
1278
+ async #assembleUserPrompt() {
1279
+ const browserState = this.#states.browserState;
1280
+ let prompt = "";
1281
+ prompt += await this.#getInstructions();
1282
+ const stepCount = this.history.filter((e) => e.type === "step").length;
1283
+ prompt += "<agent_state>\n";
1284
+ prompt += "<user_request>\n";
1285
+ prompt += `${this.task}
1286
+ `;
1287
+ prompt += "</user_request>\n";
1288
+ prompt += "<step_info>\n";
1289
+ prompt += `Step ${stepCount + 1} of ${this.config.maxSteps} max possible steps
1290
+ `;
1291
+ prompt += `Current time: ${(/* @__PURE__ */ new Date()).toLocaleString()}
1292
+ `;
1293
+ prompt += "</step_info>\n";
1294
+ prompt += "</agent_state>\n\n";
1295
+ prompt += "<agent_history>\n";
1296
+ let stepIndex = 0;
1297
+ for (const event of this.history) {
1298
+ if (event.type === "step") {
1299
+ stepIndex++;
1300
+ prompt += `<step_${stepIndex}>
1301
+ `;
1302
+ prompt += `Evaluation of Previous Step: ${event.reflection.evaluation_previous_goal}
1303
+ `;
1304
+ prompt += `Memory: ${event.reflection.memory}
1305
+ `;
1306
+ prompt += `Next Goal: ${event.reflection.next_goal}
1307
+ `;
1308
+ prompt += `Action Results: ${event.action.output}
1309
+ `;
1310
+ prompt += `</step_${stepIndex}>
1311
+ `;
1312
+ } else if (event.type === "observation") {
1313
+ prompt += `<sys>${event.content}</sys>
1314
+ `;
1315
+ } else if (event.type === "user_takeover") {
1316
+ prompt += `<sys>User took over control and made changes to the page</sys>
1317
+ `;
1318
+ } else if (event.type === "error") ;
1319
+ }
1320
+ prompt += "</agent_history>\n\n";
1321
+ let pageContent = browserState.content;
1322
+ if (this.config.transformPageContent) {
1323
+ pageContent = await this.config.transformPageContent(pageContent);
1324
+ }
1325
+ prompt += "<browser_state>\n";
1326
+ prompt += browserState.header + "\n";
1327
+ prompt += pageContent + "\n";
1328
+ prompt += browserState.footer + "\n\n";
1329
+ prompt += "</browser_state>\n\n";
1330
+ return prompt;
1331
+ }
1332
+ dispose() {
1333
+ console.log("Disposing PageAgent...");
1334
+ this.disposed = true;
1335
+ this.pageController.dispose();
1336
+ this.#abortController.abort();
1337
+ this.dispatchEvent(new Event("dispose"));
1338
+ this.config.onDispose?.(this);
1339
+ }
1340
+ }
1341
+ function isHTMLElement(el) {
1342
+ return !!el && el.nodeType === 1;
1343
+ }
1344
+ function isInputElement(el) {
1345
+ return el?.nodeType === 1 && el.tagName === "INPUT";
1346
+ }
1347
+ function isTextAreaElement(el) {
1348
+ return el?.nodeType === 1 && el.tagName === "TEXTAREA";
1349
+ }
1350
+ function isSelectElement(el) {
1351
+ return el?.nodeType === 1 && el.tagName === "SELECT";
1352
+ }
1353
+ function isAnchorElement(el) {
1354
+ return el?.nodeType === 1 && el.tagName === "A";
1355
+ }
1356
+ function getIframeOffset(element) {
1357
+ const frame = element.ownerDocument.defaultView?.frameElement;
1358
+ if (!frame) return { x: 0, y: 0 };
1359
+ const rect = frame.getBoundingClientRect();
1360
+ return { x: rect.left, y: rect.top };
1361
+ }
1362
+ function getNativeValueSetter(element) {
1363
+ return Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), "value").set;
1364
+ }
1365
+ async function waitFor(seconds) {
1366
+ await new Promise((resolve) => setTimeout(resolve, seconds * 1e3));
1367
+ }
1368
+ async function movePointerToElement(element, x, y) {
1369
+ const offset = getIframeOffset(element);
1370
+ window.dispatchEvent(
1371
+ new CustomEvent("PageAgent::MovePointerTo", {
1372
+ detail: { x: x + offset.x, y: y + offset.y }
1373
+ })
1374
+ );
1375
+ await waitFor(0.3);
1376
+ }
1377
+ async function clickPointer() {
1378
+ window.dispatchEvent(new CustomEvent("PageAgent::ClickPointer"));
1379
+ }
1380
+ async function enablePassThrough() {
1381
+ window.dispatchEvent(new CustomEvent("PageAgent::EnablePassThrough"));
1382
+ }
1383
+ async function disablePassThrough() {
1384
+ window.dispatchEvent(new CustomEvent("PageAgent::DisablePassThrough"));
1385
+ }
1386
+ function getElementByIndex(selectorMap, index) {
1387
+ const interactiveNode = selectorMap.get(index);
1388
+ if (!interactiveNode) {
1389
+ throw new Error(`No interactive element found at index ${index}`);
1390
+ }
1391
+ const element = interactiveNode.ref;
1392
+ if (!element) {
1393
+ throw new Error(`Element at index ${index} does not have a reference`);
1394
+ }
1395
+ if (!isHTMLElement(element)) {
1396
+ throw new Error(`Element at index ${index} is not an HTMLElement`);
1397
+ }
1398
+ return element;
1399
+ }
1400
+ let lastClickedElement = null;
1401
+ function blurLastClickedElement() {
1402
+ if (lastClickedElement) {
1403
+ lastClickedElement.dispatchEvent(new PointerEvent("pointerout", { bubbles: true }));
1404
+ lastClickedElement.dispatchEvent(new PointerEvent("pointerleave", { bubbles: false }));
1405
+ lastClickedElement.dispatchEvent(new MouseEvent("mouseout", { bubbles: true }));
1406
+ lastClickedElement.dispatchEvent(new MouseEvent("mouseleave", { bubbles: false }));
1407
+ lastClickedElement.blur();
1408
+ lastClickedElement = null;
1409
+ }
1410
+ }
1411
+ async function clickElement(element) {
1412
+ blurLastClickedElement();
1413
+ lastClickedElement = element;
1414
+ await scrollIntoViewIfNeeded(element);
1415
+ const frame = element.ownerDocument.defaultView?.frameElement;
1416
+ if (frame) await scrollIntoViewIfNeeded(frame);
1417
+ const rect = element.getBoundingClientRect();
1418
+ const x = rect.left + rect.width / 2;
1419
+ const y = rect.top + rect.height / 2;
1420
+ await movePointerToElement(element, x, y);
1421
+ await clickPointer();
1422
+ await waitFor(0.1);
1423
+ const doc = element.ownerDocument;
1424
+ await enablePassThrough();
1425
+ const hitTarget = doc.elementFromPoint(x, y);
1426
+ await disablePassThrough();
1427
+ const target = hitTarget instanceof HTMLElement && element.contains(hitTarget) ? hitTarget : element;
1428
+ const pointerOpts = {
1429
+ bubbles: true,
1430
+ cancelable: true,
1431
+ clientX: x,
1432
+ clientY: y,
1433
+ pointerType: "mouse"
1434
+ };
1435
+ const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 };
1436
+ target.dispatchEvent(new PointerEvent("pointerover", pointerOpts));
1437
+ target.dispatchEvent(new PointerEvent("pointerenter", { ...pointerOpts, bubbles: false }));
1438
+ target.dispatchEvent(new MouseEvent("mouseover", mouseOpts));
1439
+ target.dispatchEvent(new MouseEvent("mouseenter", { ...mouseOpts, bubbles: false }));
1440
+ target.dispatchEvent(new PointerEvent("pointerdown", pointerOpts));
1441
+ target.dispatchEvent(new MouseEvent("mousedown", mouseOpts));
1442
+ element.focus({ preventScroll: true });
1443
+ target.dispatchEvent(new PointerEvent("pointerup", pointerOpts));
1444
+ target.dispatchEvent(new MouseEvent("mouseup", mouseOpts));
1445
+ target.click();
1446
+ await waitFor(0.2);
1447
+ }
1448
+ async function inputTextElement(element, text) {
1449
+ const isContentEditable = element.isContentEditable;
1450
+ if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {
1451
+ throw new Error("Element is not an input, textarea, or contenteditable");
1452
+ }
1453
+ await clickElement(element);
1454
+ if (isContentEditable) {
1455
+ if (element.dispatchEvent(
1456
+ new InputEvent("beforeinput", {
1457
+ bubbles: true,
1458
+ cancelable: true,
1459
+ inputType: "deleteContent"
1460
+ })
1461
+ )) {
1462
+ element.innerText = "";
1463
+ element.dispatchEvent(
1464
+ new InputEvent("input", {
1465
+ bubbles: true,
1466
+ inputType: "deleteContent"
1467
+ })
1468
+ );
1469
+ }
1470
+ if (element.dispatchEvent(
1471
+ new InputEvent("beforeinput", {
1472
+ bubbles: true,
1473
+ cancelable: true,
1474
+ inputType: "insertText",
1475
+ data: text
1476
+ })
1477
+ )) {
1478
+ element.innerText = text;
1479
+ element.dispatchEvent(
1480
+ new InputEvent("input", {
1481
+ bubbles: true,
1482
+ inputType: "insertText",
1483
+ data: text
1484
+ })
1485
+ );
1486
+ }
1487
+ const planASucceeded = element.innerText.trim() === text.trim();
1488
+ if (!planASucceeded) {
1489
+ element.focus();
1490
+ const doc = element.ownerDocument;
1491
+ const selection = (doc.defaultView || window).getSelection();
1492
+ const range = doc.createRange();
1493
+ range.selectNodeContents(element);
1494
+ selection?.removeAllRanges();
1495
+ selection?.addRange(range);
1496
+ doc.execCommand("delete", false);
1497
+ doc.execCommand("insertText", false, text);
1498
+ }
1499
+ element.dispatchEvent(new Event("change", { bubbles: true }));
1500
+ element.blur();
1501
+ } else {
1502
+ getNativeValueSetter(element).call(element, text);
1503
+ }
1504
+ if (!isContentEditable) {
1505
+ element.dispatchEvent(new Event("input", { bubbles: true }));
1506
+ }
1507
+ await waitFor(0.1);
1508
+ }
1509
+ async function selectOptionElement(selectElement, optionText) {
1510
+ if (!isSelectElement(selectElement)) {
1511
+ throw new Error("Element is not a select element");
1512
+ }
1513
+ const options = Array.from(selectElement.options);
1514
+ const option = options.find((opt) => opt.textContent?.trim() === optionText.trim());
1515
+ if (!option) {
1516
+ throw new Error(`Option with text "${optionText}" not found in select element`);
1517
+ }
1518
+ selectElement.value = option.value;
1519
+ selectElement.dispatchEvent(new Event("change", { bubbles: true }));
1520
+ await waitFor(0.1);
1521
+ }
1522
+ async function scrollIntoViewIfNeeded(element) {
1523
+ const el = element;
1524
+ if (typeof el.scrollIntoViewIfNeeded === "function") {
1525
+ el.scrollIntoViewIfNeeded();
1526
+ } else {
1527
+ element.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
1528
+ }
1529
+ }
1530
+ async function scrollVertically(scroll_amount, element) {
1531
+ if (element) {
1532
+ const targetElement = element;
1533
+ let currentElement = targetElement;
1534
+ let scrollSuccess = false;
1535
+ let scrolledElement = null;
1536
+ let scrollDelta = 0;
1537
+ let attempts = 0;
1538
+ const dy2 = scroll_amount;
1539
+ while (currentElement && attempts < 10) {
1540
+ const computedStyle = window.getComputedStyle(currentElement);
1541
+ const hasScrollableY = /(auto|scroll|overlay)/.test(computedStyle.overflowY) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
1542
+ const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight;
1543
+ if (hasScrollableY && canScrollVertically) {
1544
+ const beforeScroll = currentElement.scrollTop;
1545
+ const maxScroll = currentElement.scrollHeight - currentElement.clientHeight;
1546
+ let scrollAmount = dy2 / 3;
1547
+ if (scrollAmount > 0) {
1548
+ scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
1549
+ } else {
1550
+ scrollAmount = Math.max(scrollAmount, -beforeScroll);
1551
+ }
1552
+ currentElement.scrollTop = beforeScroll + scrollAmount;
1553
+ const afterScroll = currentElement.scrollTop;
1554
+ const actualScrollDelta = afterScroll - beforeScroll;
1555
+ if (Math.abs(actualScrollDelta) > 0.5) {
1556
+ scrollSuccess = true;
1557
+ scrolledElement = currentElement;
1558
+ scrollDelta = actualScrollDelta;
1559
+ break;
1560
+ }
1561
+ }
1562
+ if (currentElement === document.body || currentElement === document.documentElement) {
1563
+ break;
1564
+ }
1565
+ currentElement = currentElement.parentElement;
1566
+ attempts++;
1567
+ }
1568
+ if (scrollSuccess) {
1569
+ return `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`;
1570
+ } else {
1571
+ return `No scrollable container found for element (${targetElement.tagName})`;
1572
+ }
1573
+ }
1574
+ const dy = scroll_amount;
1575
+ const bigEnough = (el2) => el2.clientHeight >= window.innerHeight * 0.5;
1576
+ const canScroll = (el2) => Boolean(
1577
+ el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowY) && el2.scrollHeight > el2.clientHeight && bigEnough(el2)
1578
+ );
1579
+ let el = document.activeElement;
1580
+ while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
1581
+ el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
1582
+ if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
1583
+ const scrollBefore = window.scrollY;
1584
+ const scrollMax = document.documentElement.scrollHeight - window.innerHeight;
1585
+ window.scrollBy(0, dy);
1586
+ const scrollAfter = window.scrollY;
1587
+ const scrolled = scrollAfter - scrollBefore;
1588
+ if (Math.abs(scrolled) < 1) {
1589
+ return dy > 0 ? `⚠️ Already at the bottom of the page, cannot scroll down further.` : `⚠️ Already at the top of the page, cannot scroll up further.`;
1590
+ }
1591
+ const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
1592
+ const reachedTop = dy < 0 && scrollAfter <= 1;
1593
+ if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`;
1594
+ if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`;
1595
+ return `✅ Scrolled page by ${scrolled}px.`;
1596
+ } else {
1597
+ const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
1598
+ console.log(`[PageController] ${warningMsg}`);
1599
+ const scrollBefore = el.scrollTop;
1600
+ const scrollMax = el.scrollHeight - el.clientHeight;
1601
+ el.scrollBy({ top: dy, behavior: "smooth" });
1602
+ await waitFor(0.1);
1603
+ const scrollAfter = el.scrollTop;
1604
+ const scrolled = scrollAfter - scrollBefore;
1605
+ if (Math.abs(scrolled) < 1) {
1606
+ return dy > 0 ? `⚠️ ${warningMsg} Already at the bottom of container (${el.tagName}), cannot scroll down further.` : `⚠️ ${warningMsg} Already at the top of container (${el.tagName}), cannot scroll up further.`;
1607
+ }
1608
+ const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1;
1609
+ const reachedTop = dy < 0 && scrollAfter <= 1;
1610
+ if (reachedBottom)
1611
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the bottom.`;
1612
+ if (reachedTop)
1613
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the top.`;
1614
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px.`;
1615
+ }
1616
+ }
1617
+ async function scrollHorizontally(scroll_amount, element) {
1618
+ if (element) {
1619
+ const targetElement = element;
1620
+ let currentElement = targetElement;
1621
+ let scrollSuccess = false;
1622
+ let scrolledElement = null;
1623
+ let scrollDelta = 0;
1624
+ let attempts = 0;
1625
+ const dx2 = scroll_amount;
1626
+ while (currentElement && attempts < 10) {
1627
+ const computedStyle = window.getComputedStyle(currentElement);
1628
+ const hasScrollableX = /(auto|scroll|overlay)/.test(computedStyle.overflowX) || computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== "auto" || computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== "auto";
1629
+ const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth;
1630
+ if (hasScrollableX && canScrollHorizontally) {
1631
+ const beforeScroll = currentElement.scrollLeft;
1632
+ const maxScroll = currentElement.scrollWidth - currentElement.clientWidth;
1633
+ let scrollAmount = dx2 / 3;
1634
+ if (scrollAmount > 0) {
1635
+ scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll);
1636
+ } else {
1637
+ scrollAmount = Math.max(scrollAmount, -beforeScroll);
1638
+ }
1639
+ currentElement.scrollLeft = beforeScroll + scrollAmount;
1640
+ const afterScroll = currentElement.scrollLeft;
1641
+ const actualScrollDelta = afterScroll - beforeScroll;
1642
+ if (Math.abs(actualScrollDelta) > 0.5) {
1643
+ scrollSuccess = true;
1644
+ scrolledElement = currentElement;
1645
+ scrollDelta = actualScrollDelta;
1646
+ break;
1647
+ }
1648
+ }
1649
+ if (currentElement === document.body || currentElement === document.documentElement) {
1650
+ break;
1651
+ }
1652
+ currentElement = currentElement.parentElement;
1653
+ attempts++;
1654
+ }
1655
+ if (scrollSuccess) {
1656
+ return `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`;
1657
+ } else {
1658
+ return `No horizontally scrollable container found for element (${targetElement.tagName})`;
1659
+ }
1660
+ }
1661
+ const dx = scroll_amount;
1662
+ const bigEnough = (el2) => el2.clientWidth >= window.innerWidth * 0.5;
1663
+ const canScroll = (el2) => Boolean(
1664
+ el2 && /(auto|scroll|overlay)/.test(getComputedStyle(el2).overflowX) && el2.scrollWidth > el2.clientWidth && bigEnough(el2)
1665
+ );
1666
+ let el = document.activeElement;
1667
+ while (el && !canScroll(el) && el !== document.body) el = el.parentElement;
1668
+ el = canScroll(el) ? el : Array.from(document.querySelectorAll("*")).find(canScroll) || document.scrollingElement || document.documentElement;
1669
+ if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
1670
+ const scrollBefore = window.scrollX;
1671
+ const scrollMax = document.documentElement.scrollWidth - window.innerWidth;
1672
+ window.scrollBy(dx, 0);
1673
+ const scrollAfter = window.scrollX;
1674
+ const scrolled = scrollAfter - scrollBefore;
1675
+ if (Math.abs(scrolled) < 1) {
1676
+ return dx > 0 ? `⚠️ Already at the right edge of the page, cannot scroll right further.` : `⚠️ Already at the left edge of the page, cannot scroll left further.`;
1677
+ }
1678
+ const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
1679
+ const reachedLeft = dx < 0 && scrollAfter <= 1;
1680
+ if (reachedRight)
1681
+ return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`;
1682
+ if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`;
1683
+ return `✅ Scrolled page horizontally by ${scrolled}px.`;
1684
+ } else {
1685
+ const warningMsg = `The document is not scrollable. Falling back to container scroll.`;
1686
+ console.log(`[PageController] ${warningMsg}`);
1687
+ const scrollBefore = el.scrollLeft;
1688
+ const scrollMax = el.scrollWidth - el.clientWidth;
1689
+ el.scrollBy({ left: dx, behavior: "smooth" });
1690
+ await waitFor(0.1);
1691
+ const scrollAfter = el.scrollLeft;
1692
+ const scrolled = scrollAfter - scrollBefore;
1693
+ if (Math.abs(scrolled) < 1) {
1694
+ return dx > 0 ? `⚠️ ${warningMsg} Already at the right edge of container (${el.tagName}), cannot scroll right further.` : `⚠️ ${warningMsg} Already at the left edge of container (${el.tagName}), cannot scroll left further.`;
1695
+ }
1696
+ const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1;
1697
+ const reachedLeft = dx < 0 && scrollAfter <= 1;
1698
+ if (reachedRight)
1699
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the right edge.`;
1700
+ if (reachedLeft)
1701
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the left edge.`;
1702
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) horizontally by ${scrolled}px.`;
1703
+ }
1704
+ }
1705
+ const domTree = (args = {
1706
+ doHighlightElements: true,
1707
+ focusHighlightIndex: -1,
1708
+ viewportExpansion: 0,
1709
+ debugMode: false,
1710
+ /**
1711
+ * @edit
1712
+ */
1713
+ /** @type {Element[]} */
1714
+ interactiveBlacklist: [],
1715
+ /** @type {Element[]} */
1716
+ interactiveWhitelist: [],
1717
+ highlightOpacity: 0.1,
1718
+ highlightLabelOpacity: 0.5
1719
+ }) => {
1720
+ const { interactiveBlacklist, interactiveWhitelist, highlightOpacity, highlightLabelOpacity } = args;
1721
+ const { doHighlightElements, focusHighlightIndex, viewportExpansion, debugMode } = args;
1722
+ let highlightIndex = 0;
1723
+ const extraData = /* @__PURE__ */ new WeakMap();
1724
+ function addExtraData(element, data) {
1725
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return;
1726
+ extraData.set(element, { ...extraData.get(element), ...data });
1727
+ }
1728
+ const DOM_CACHE = {
1729
+ boundingRects: /* @__PURE__ */ new WeakMap(),
1730
+ clientRects: /* @__PURE__ */ new WeakMap(),
1731
+ computedStyles: /* @__PURE__ */ new WeakMap(),
1732
+ clearCache: () => {
1733
+ DOM_CACHE.boundingRects = /* @__PURE__ */ new WeakMap();
1734
+ DOM_CACHE.clientRects = /* @__PURE__ */ new WeakMap();
1735
+ DOM_CACHE.computedStyles = /* @__PURE__ */ new WeakMap();
1736
+ }
1737
+ };
1738
+ function getCachedBoundingRect(element) {
1739
+ if (!element) return null;
1740
+ if (DOM_CACHE.boundingRects.has(element)) {
1741
+ return DOM_CACHE.boundingRects.get(element);
1742
+ }
1743
+ const rect = element.getBoundingClientRect();
1744
+ if (rect) {
1745
+ DOM_CACHE.boundingRects.set(element, rect);
1746
+ }
1747
+ return rect;
1748
+ }
1749
+ function getCachedComputedStyle(element) {
1750
+ if (!element) return null;
1751
+ if (DOM_CACHE.computedStyles.has(element)) {
1752
+ return DOM_CACHE.computedStyles.get(element);
1753
+ }
1754
+ const style = window.getComputedStyle(element);
1755
+ if (style) {
1756
+ DOM_CACHE.computedStyles.set(element, style);
1757
+ }
1758
+ return style;
1759
+ }
1760
+ function getCachedClientRects(element) {
1761
+ if (!element) return null;
1762
+ if (DOM_CACHE.clientRects.has(element)) {
1763
+ return DOM_CACHE.clientRects.get(element);
1764
+ }
1765
+ const rects = element.getClientRects();
1766
+ if (rects) {
1767
+ DOM_CACHE.clientRects.set(element, rects);
1768
+ }
1769
+ return rects;
1770
+ }
1771
+ const DOM_HASH_MAP = {};
1772
+ const ID = { current: 0 };
1773
+ const HIGHLIGHT_CONTAINER_ID = "playwright-highlight-container";
1774
+ function highlightElement(element, index, parentIframe = null) {
1775
+ if (!element) return index;
1776
+ const overlays = [];
1777
+ let label = null;
1778
+ let labelWidth = 20;
1779
+ let labelHeight = 16;
1780
+ let cleanupFn = null;
1781
+ try {
1782
+ let container = document.getElementById(HIGHLIGHT_CONTAINER_ID);
1783
+ if (!container) {
1784
+ container = document.createElement("div");
1785
+ container.id = HIGHLIGHT_CONTAINER_ID;
1786
+ container.style.position = "fixed";
1787
+ container.style.pointerEvents = "none";
1788
+ container.style.top = "0";
1789
+ container.style.left = "0";
1790
+ container.style.width = "100%";
1791
+ container.style.height = "100%";
1792
+ container.style.zIndex = "2147483640";
1793
+ container.style.backgroundColor = "transparent";
1794
+ document.body.appendChild(container);
1795
+ }
1796
+ const rects = element.getClientRects();
1797
+ if (!rects || rects.length === 0) return index;
1798
+ const colors = [
1799
+ "#FF0000",
1800
+ "#00FF00",
1801
+ "#0000FF",
1802
+ "#FFA500",
1803
+ "#800080",
1804
+ "#008080",
1805
+ "#FF69B4",
1806
+ "#4B0082",
1807
+ "#FF4500",
1808
+ "#2E8B57",
1809
+ "#DC143C",
1810
+ "#4682B4"
1811
+ ];
1812
+ const colorIndex = index % colors.length;
1813
+ let baseColor = colors[colorIndex];
1814
+ const backgroundColor = baseColor + Math.floor(highlightOpacity * 255).toString(16).padStart(2, "0");
1815
+ baseColor = baseColor + Math.floor(highlightLabelOpacity * 255).toString(16).padStart(2, "0");
1816
+ let iframeOffset = { x: 0, y: 0 };
1817
+ if (parentIframe) {
1818
+ const iframeRect = parentIframe.getBoundingClientRect();
1819
+ iframeOffset.x = iframeRect.left;
1820
+ iframeOffset.y = iframeRect.top;
1821
+ }
1822
+ const fragment = document.createDocumentFragment();
1823
+ for (const rect of rects) {
1824
+ if (rect.width === 0 || rect.height === 0) continue;
1825
+ const overlay = document.createElement("div");
1826
+ overlay.style.position = "fixed";
1827
+ overlay.style.border = `2px solid ${baseColor}`;
1828
+ overlay.style.backgroundColor = backgroundColor;
1829
+ overlay.style.pointerEvents = "none";
1830
+ overlay.style.boxSizing = "border-box";
1831
+ const top = rect.top + iframeOffset.y;
1832
+ const left = rect.left + iframeOffset.x;
1833
+ overlay.style.top = `${top}px`;
1834
+ overlay.style.left = `${left}px`;
1835
+ overlay.style.width = `${rect.width}px`;
1836
+ overlay.style.height = `${rect.height}px`;
1837
+ fragment.appendChild(overlay);
1838
+ overlays.push({ element: overlay, initialRect: rect });
1839
+ }
1840
+ const firstRect = rects[0];
1841
+ label = document.createElement("div");
1842
+ label.className = "playwright-highlight-label";
1843
+ label.style.position = "fixed";
1844
+ label.style.background = baseColor;
1845
+ label.style.color = "white";
1846
+ label.style.padding = "1px 4px";
1847
+ label.style.borderRadius = "4px";
1848
+ label.style.fontSize = `${Math.min(12, Math.max(8, firstRect.height / 2))}px`;
1849
+ label.textContent = index.toString();
1850
+ labelWidth = label.offsetWidth > 0 ? label.offsetWidth : labelWidth;
1851
+ labelHeight = label.offsetHeight > 0 ? label.offsetHeight : labelHeight;
1852
+ const firstRectTop = firstRect.top + iframeOffset.y;
1853
+ const firstRectLeft = firstRect.left + iframeOffset.x;
1854
+ let labelTop = firstRectTop + 2;
1855
+ let labelLeft = firstRectLeft + firstRect.width - labelWidth - 2;
1856
+ if (firstRect.width < labelWidth + 4 || firstRect.height < labelHeight + 4) {
1857
+ labelTop = firstRectTop - labelHeight - 2;
1858
+ labelLeft = firstRectLeft + firstRect.width - labelWidth;
1859
+ if (labelLeft < iframeOffset.x) labelLeft = firstRectLeft;
1860
+ }
1861
+ labelTop = Math.max(0, Math.min(labelTop, window.innerHeight - labelHeight));
1862
+ labelLeft = Math.max(0, Math.min(labelLeft, window.innerWidth - labelWidth));
1863
+ label.style.top = `${labelTop}px`;
1864
+ label.style.left = `${labelLeft}px`;
1865
+ fragment.appendChild(label);
1866
+ const updatePositions = () => {
1867
+ const newRects = element.getClientRects();
1868
+ let newIframeOffset = { x: 0, y: 0 };
1869
+ if (parentIframe) {
1870
+ const iframeRect = parentIframe.getBoundingClientRect();
1871
+ newIframeOffset.x = iframeRect.left;
1872
+ newIframeOffset.y = iframeRect.top;
1873
+ }
1874
+ overlays.forEach((overlayData, i) => {
1875
+ if (i < newRects.length) {
1876
+ const newRect = newRects[i];
1877
+ const newTop = newRect.top + newIframeOffset.y;
1878
+ const newLeft = newRect.left + newIframeOffset.x;
1879
+ overlayData.element.style.top = `${newTop}px`;
1880
+ overlayData.element.style.left = `${newLeft}px`;
1881
+ overlayData.element.style.width = `${newRect.width}px`;
1882
+ overlayData.element.style.height = `${newRect.height}px`;
1883
+ overlayData.element.style.display = newRect.width === 0 || newRect.height === 0 ? "none" : "block";
1884
+ } else {
1885
+ overlayData.element.style.display = "none";
1886
+ }
1887
+ });
1888
+ if (newRects.length < overlays.length) {
1889
+ for (let i = newRects.length; i < overlays.length; i++) {
1890
+ overlays[i].element.style.display = "none";
1891
+ }
1892
+ }
1893
+ if (label && newRects.length > 0) {
1894
+ const firstNewRect = newRects[0];
1895
+ const firstNewRectTop = firstNewRect.top + newIframeOffset.y;
1896
+ const firstNewRectLeft = firstNewRect.left + newIframeOffset.x;
1897
+ let newLabelTop = firstNewRectTop + 2;
1898
+ let newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth - 2;
1899
+ if (firstNewRect.width < labelWidth + 4 || firstNewRect.height < labelHeight + 4) {
1900
+ newLabelTop = firstNewRectTop - labelHeight - 2;
1901
+ newLabelLeft = firstNewRectLeft + firstNewRect.width - labelWidth;
1902
+ if (newLabelLeft < newIframeOffset.x) newLabelLeft = firstNewRectLeft;
1903
+ }
1904
+ newLabelTop = Math.max(0, Math.min(newLabelTop, window.innerHeight - labelHeight));
1905
+ newLabelLeft = Math.max(0, Math.min(newLabelLeft, window.innerWidth - labelWidth));
1906
+ label.style.top = `${newLabelTop}px`;
1907
+ label.style.left = `${newLabelLeft}px`;
1908
+ label.style.display = "block";
1909
+ } else if (label) {
1910
+ label.style.display = "none";
1911
+ }
1912
+ };
1913
+ const throttleFunction = (func, delay) => {
1914
+ let lastCall = 0;
1915
+ return (...args2) => {
1916
+ const now = performance.now();
1917
+ if (now - lastCall < delay) return;
1918
+ lastCall = now;
1919
+ return func(...args2);
1920
+ };
1921
+ };
1922
+ const throttledUpdatePositions = throttleFunction(updatePositions, 16);
1923
+ window.addEventListener("scroll", throttledUpdatePositions, true);
1924
+ window.addEventListener("resize", throttledUpdatePositions);
1925
+ cleanupFn = () => {
1926
+ window.removeEventListener("scroll", throttledUpdatePositions, true);
1927
+ window.removeEventListener("resize", throttledUpdatePositions);
1928
+ overlays.forEach((overlay) => overlay.element.remove());
1929
+ if (label) label.remove();
1930
+ };
1931
+ container.appendChild(fragment);
1932
+ return index + 1;
1933
+ } finally {
1934
+ if (cleanupFn) {
1935
+ (window._highlightCleanupFunctions = window._highlightCleanupFunctions || []).push(
1936
+ cleanupFn
1937
+ );
1938
+ }
1939
+ }
1940
+ }
1941
+ function isScrollableElement(element) {
1942
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
1943
+ return null;
1944
+ }
1945
+ const style = getCachedComputedStyle(element);
1946
+ if (!style) return null;
1947
+ const display = style.display;
1948
+ if (display === "inline" || display === "inline-block") {
1949
+ return null;
1950
+ }
1951
+ const overflowX = style.overflowX;
1952
+ const overflowY = style.overflowY;
1953
+ const hasScrollbarSignal = style.scrollbarWidth && style.scrollbarWidth !== "auto" || style.scrollbarGutter && style.scrollbarGutter !== "auto";
1954
+ const scrollableX = overflowX === "auto" || overflowX === "scroll";
1955
+ const scrollableY = overflowY === "auto" || overflowY === "scroll";
1956
+ if (!scrollableX && !scrollableY && !hasScrollbarSignal) {
1957
+ return null;
1958
+ }
1959
+ const scrollWidth = element.scrollWidth - element.clientWidth;
1960
+ const scrollHeight = element.scrollHeight - element.clientHeight;
1961
+ const threshold = 4;
1962
+ if (scrollWidth < threshold && scrollHeight < threshold) {
1963
+ return null;
1964
+ }
1965
+ if (!scrollableY && !hasScrollbarSignal && scrollWidth < threshold) {
1966
+ return null;
1967
+ }
1968
+ if (!scrollableX && !hasScrollbarSignal && scrollHeight < threshold) {
1969
+ return null;
1970
+ }
1971
+ const distanceToTop = element.scrollTop;
1972
+ const distanceToLeft = element.scrollLeft;
1973
+ const distanceToRight = element.scrollWidth - element.clientWidth - element.scrollLeft;
1974
+ const distanceToBottom = element.scrollHeight - element.clientHeight - element.scrollTop;
1975
+ const scrollData = {
1976
+ top: distanceToTop,
1977
+ right: distanceToRight,
1978
+ bottom: distanceToBottom,
1979
+ left: distanceToLeft
1980
+ };
1981
+ addExtraData(element, {
1982
+ scrollable: true,
1983
+ scrollData
1984
+ });
1985
+ return scrollData;
1986
+ }
1987
+ function isTextNodeVisible(textNode) {
1988
+ try {
1989
+ if (viewportExpansion === -1) {
1990
+ const parentElement2 = textNode.parentElement;
1991
+ if (!parentElement2) return false;
1992
+ try {
1993
+ return parentElement2.checkVisibility({
1994
+ checkOpacity: true,
1995
+ checkVisibilityCSS: true
1996
+ });
1997
+ } catch (e) {
1998
+ const style = window.getComputedStyle(parentElement2);
1999
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
2000
+ }
2001
+ }
2002
+ const range = document.createRange();
2003
+ range.selectNodeContents(textNode);
2004
+ const rects = range.getClientRects();
2005
+ if (!rects || rects.length === 0) {
2006
+ return false;
2007
+ }
2008
+ let isAnyRectVisible = false;
2009
+ let isAnyRectInViewport = false;
2010
+ for (const rect of rects) {
2011
+ if (rect.width > 0 && rect.height > 0) {
2012
+ isAnyRectVisible = true;
2013
+ if (!(rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
2014
+ isAnyRectInViewport = true;
2015
+ break;
2016
+ }
2017
+ }
2018
+ }
2019
+ if (!isAnyRectVisible || !isAnyRectInViewport) {
2020
+ return false;
2021
+ }
2022
+ const parentElement = textNode.parentElement;
2023
+ if (!parentElement) return false;
2024
+ try {
2025
+ return parentElement.checkVisibility({
2026
+ checkOpacity: true,
2027
+ checkVisibilityCSS: true
2028
+ });
2029
+ } catch (e) {
2030
+ const style = window.getComputedStyle(parentElement);
2031
+ return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0";
2032
+ }
2033
+ } catch (e) {
2034
+ console.warn("Error checking text node visibility:", e);
2035
+ return false;
2036
+ }
2037
+ }
2038
+ function isElementAccepted(element) {
2039
+ if (!element || !element.tagName) return false;
2040
+ const alwaysAccept = /* @__PURE__ */ new Set([
2041
+ "body",
2042
+ "div",
2043
+ "main",
2044
+ "article",
2045
+ "section",
2046
+ "nav",
2047
+ "header",
2048
+ "footer"
2049
+ ]);
2050
+ const tagName = element.tagName.toLowerCase();
2051
+ if (alwaysAccept.has(tagName)) return true;
2052
+ const leafElementDenyList = /* @__PURE__ */ new Set([
2053
+ "svg",
2054
+ "script",
2055
+ "style",
2056
+ "link",
2057
+ "meta",
2058
+ "noscript",
2059
+ "template"
2060
+ ]);
2061
+ return !leafElementDenyList.has(tagName);
2062
+ }
2063
+ function isElementVisible(element) {
2064
+ const style = getCachedComputedStyle(element);
2065
+ return element.offsetWidth > 0 && element.offsetHeight > 0 && style?.visibility !== "hidden" && style?.display !== "none";
2066
+ }
2067
+ function isInteractiveElement(element) {
2068
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
2069
+ return false;
2070
+ }
2071
+ if (interactiveBlacklist.includes(element)) {
2072
+ return false;
2073
+ }
2074
+ if (interactiveWhitelist.includes(element)) {
2075
+ return true;
2076
+ }
2077
+ const tagName = element.tagName.toLowerCase();
2078
+ const style = getCachedComputedStyle(element);
2079
+ const interactiveCursors = /* @__PURE__ */ new Set([
2080
+ "pointer",
2081
+ // Link/clickable elements
2082
+ "move",
2083
+ // Movable elements
2084
+ "text",
2085
+ // Text selection
2086
+ "grab",
2087
+ // Grabbable elements
2088
+ "grabbing",
2089
+ // Currently grabbing
2090
+ "cell",
2091
+ // Table cell selection
2092
+ "copy",
2093
+ // Copy operation
2094
+ "alias",
2095
+ // Alias creation
2096
+ "all-scroll",
2097
+ // Scrollable content
2098
+ "col-resize",
2099
+ // Column resize
2100
+ "context-menu",
2101
+ // Context menu available
2102
+ "crosshair",
2103
+ // Precise selection
2104
+ "e-resize",
2105
+ // East resize
2106
+ "ew-resize",
2107
+ // East-west resize
2108
+ "help",
2109
+ // Help available
2110
+ "n-resize",
2111
+ // North resize
2112
+ "ne-resize",
2113
+ // Northeast resize
2114
+ "nesw-resize",
2115
+ // Northeast-southwest resize
2116
+ "ns-resize",
2117
+ // North-south resize
2118
+ "nw-resize",
2119
+ // Northwest resize
2120
+ "nwse-resize",
2121
+ // Northwest-southeast resize
2122
+ "row-resize",
2123
+ // Row resize
2124
+ "s-resize",
2125
+ // South resize
2126
+ "se-resize",
2127
+ // Southeast resize
2128
+ "sw-resize",
2129
+ // Southwest resize
2130
+ "vertical-text",
2131
+ // Vertical text selection
2132
+ "w-resize",
2133
+ // West resize
2134
+ "zoom-in",
2135
+ // Zoom in
2136
+ "zoom-out"
2137
+ // Zoom out
2138
+ ]);
2139
+ const nonInteractiveCursors = /* @__PURE__ */ new Set([
2140
+ "not-allowed",
2141
+ // Action not allowed
2142
+ "no-drop",
2143
+ // Drop not allowed
2144
+ "wait",
2145
+ // Processing
2146
+ "progress",
2147
+ // In progress
2148
+ "initial",
2149
+ // Initial value
2150
+ "inherit"
2151
+ // Inherited value
2152
+ //? Let's just include all potentially clickable elements that are not specifically blocked
2153
+ // 'none', // No cursor
2154
+ // 'default', // Default cursor
2155
+ // 'auto', // Browser default
2156
+ ]);
2157
+ function doesElementHaveInteractivePointer(element2) {
2158
+ if (element2.tagName.toLowerCase() === "html") return false;
2159
+ if (style?.cursor && interactiveCursors.has(style.cursor)) return true;
2160
+ return false;
2161
+ }
2162
+ let isInteractiveCursor = doesElementHaveInteractivePointer(element);
2163
+ if (isInteractiveCursor) {
2164
+ return true;
2165
+ }
2166
+ const interactiveElements = /* @__PURE__ */ new Set([
2167
+ "a",
2168
+ // Links
2169
+ "button",
2170
+ // Buttons
2171
+ "input",
2172
+ // All input types (text, checkbox, radio, etc.)
2173
+ "select",
2174
+ // Dropdown menus
2175
+ "textarea",
2176
+ // Text areas
2177
+ "details",
2178
+ // Expandable details
2179
+ "summary",
2180
+ // Summary element (clickable part of details)
2181
+ "label",
2182
+ // Form labels (often clickable)
2183
+ "option",
2184
+ // Select options
2185
+ "optgroup",
2186
+ // Option groups
2187
+ "fieldset",
2188
+ // Form fieldsets (can be interactive with legend)
2189
+ "legend"
2190
+ // Fieldset legends
2191
+ ]);
2192
+ const explicitDisableTags = /* @__PURE__ */ new Set([
2193
+ "disabled",
2194
+ // Standard disabled attribute
2195
+ // 'aria-disabled', // ARIA disabled state
2196
+ "readonly"
2197
+ // Read-only state
2198
+ // 'aria-readonly', // ARIA read-only state
2199
+ // 'aria-hidden', // Hidden from accessibility
2200
+ // 'hidden', // Hidden attribute
2201
+ // 'inert', // Inert attribute
2202
+ // 'aria-inert', // ARIA inert state
2203
+ // 'tabindex="-1"', // Removed from tab order
2204
+ // 'aria-hidden="true"' // Hidden from screen readers
2205
+ ]);
2206
+ if (interactiveElements.has(tagName)) {
2207
+ if (style?.cursor && nonInteractiveCursors.has(style.cursor)) {
2208
+ return false;
2209
+ }
2210
+ for (const disableTag of explicitDisableTags) {
2211
+ if (element.hasAttribute(disableTag) || element.getAttribute(disableTag) === "true" || element.getAttribute(disableTag) === "") {
2212
+ return false;
2213
+ }
2214
+ }
2215
+ if (element.disabled) {
2216
+ return false;
2217
+ }
2218
+ if (element.readOnly) {
2219
+ return false;
2220
+ }
2221
+ if (element.inert) {
2222
+ return false;
2223
+ }
2224
+ return true;
2225
+ }
2226
+ const role = element.getAttribute("role");
2227
+ const ariaRole = element.getAttribute("aria-role");
2228
+ if (element.getAttribute("contenteditable") === "true" || element.isContentEditable) {
2229
+ return true;
2230
+ }
2231
+ if (element.classList && (element.classList.contains("button") || element.classList.contains("dropdown-toggle") || element.getAttribute("data-index") || element.getAttribute("data-toggle") === "dropdown" || element.getAttribute("aria-haspopup") === "true")) {
2232
+ return true;
2233
+ }
2234
+ const interactiveRoles = /* @__PURE__ */ new Set([
2235
+ "button",
2236
+ // Directly clickable element
2237
+ // 'link', // Clickable link
2238
+ "menu",
2239
+ // Menu container (ARIA menus)
2240
+ "menubar",
2241
+ // Menu bar container
2242
+ "menuitem",
2243
+ // Clickable menu item
2244
+ "menuitemradio",
2245
+ // Radio-style menu item (selectable)
2246
+ "menuitemcheckbox",
2247
+ // Checkbox-style menu item (toggleable)
2248
+ "radio",
2249
+ // Radio button (selectable)
2250
+ "checkbox",
2251
+ // Checkbox (toggleable)
2252
+ "tab",
2253
+ // Tab (clickable to switch content)
2254
+ "switch",
2255
+ // Toggle switch (clickable to change state)
2256
+ "slider",
2257
+ // Slider control (draggable)
2258
+ "spinbutton",
2259
+ // Number input with up/down controls
2260
+ "combobox",
2261
+ // Dropdown with text input
2262
+ "searchbox",
2263
+ // Search input field
2264
+ "textbox",
2265
+ // Text input field
2266
+ "listbox",
2267
+ // Selectable list
2268
+ "option",
2269
+ // Selectable option in a list
2270
+ "scrollbar"
2271
+ // Scrollable control
2272
+ ]);
2273
+ const hasInteractiveRole = interactiveElements.has(tagName) || role && interactiveRoles.has(role) || ariaRole && interactiveRoles.has(ariaRole);
2274
+ if (hasInteractiveRole) return true;
2275
+ try {
2276
+ if (typeof getEventListeners === "function") {
2277
+ const listeners = getEventListeners(element);
2278
+ const mouseEvents = ["click", "mousedown", "mouseup", "dblclick"];
2279
+ for (const eventType of mouseEvents) {
2280
+ if (listeners[eventType] && listeners[eventType].length > 0) {
2281
+ return true;
2282
+ }
2283
+ }
2284
+ }
2285
+ const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
2286
+ if (typeof getEventListenersForNode === "function") {
2287
+ const listeners = getEventListenersForNode(element);
2288
+ const interactionEvents = [
2289
+ "click",
2290
+ "mousedown",
2291
+ "mouseup",
2292
+ "keydown",
2293
+ "keyup",
2294
+ "submit",
2295
+ "change",
2296
+ "input",
2297
+ "focus",
2298
+ "blur"
2299
+ ];
2300
+ for (const eventType of interactionEvents) {
2301
+ for (const listener of listeners) {
2302
+ if (listener.type === eventType) {
2303
+ return true;
2304
+ }
2305
+ }
2306
+ }
2307
+ }
2308
+ const commonMouseAttrs = ["onclick", "onmousedown", "onmouseup", "ondblclick"];
2309
+ for (const attr of commonMouseAttrs) {
2310
+ if (element.hasAttribute(attr) || typeof element[attr] === "function") {
2311
+ return true;
2312
+ }
2313
+ }
2314
+ } catch (e) {
2315
+ }
2316
+ if (isScrollableElement(element)) {
2317
+ return true;
2318
+ }
2319
+ return false;
2320
+ }
2321
+ function isTopElement(element) {
2322
+ if (viewportExpansion === -1) {
2323
+ return true;
2324
+ }
2325
+ const rects = getCachedClientRects(element);
2326
+ if (!rects || rects.length === 0) {
2327
+ return false;
2328
+ }
2329
+ let isAnyRectInViewport = false;
2330
+ for (const rect2 of rects) {
2331
+ if (rect2.width > 0 && rect2.height > 0 && !// Only check non-empty rects
2332
+ (rect2.bottom < -viewportExpansion || rect2.top > window.innerHeight + viewportExpansion || rect2.right < -viewportExpansion || rect2.left > window.innerWidth + viewportExpansion)) {
2333
+ isAnyRectInViewport = true;
2334
+ break;
2335
+ }
2336
+ }
2337
+ if (!isAnyRectInViewport) {
2338
+ return false;
2339
+ }
2340
+ let doc = element.ownerDocument;
2341
+ if (doc !== window.document) {
2342
+ return true;
2343
+ }
2344
+ let rect = Array.from(rects).find((r) => r.width > 0 && r.height > 0);
2345
+ if (!rect) {
2346
+ return false;
2347
+ }
2348
+ const shadowRoot = element.getRootNode();
2349
+ if (shadowRoot instanceof ShadowRoot) {
2350
+ const centerX = rect.left + rect.width / 2;
2351
+ const centerY = rect.top + rect.height / 2;
2352
+ try {
2353
+ const topEl = shadowRoot.elementFromPoint(centerX, centerY);
2354
+ if (!topEl) return false;
2355
+ let current = topEl;
2356
+ while (current && current !== shadowRoot) {
2357
+ if (current === element) return true;
2358
+ current = current.parentElement;
2359
+ }
2360
+ return false;
2361
+ } catch (e) {
2362
+ return true;
2363
+ }
2364
+ }
2365
+ const margin = 5;
2366
+ const checkPoints = [
2367
+ // Initially only this was used, but it was not enough
2368
+ { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
2369
+ { x: rect.left + margin, y: rect.top + margin },
2370
+ // top left
2371
+ // { x: rect.right - margin, y: rect.top + margin }, // top right
2372
+ // { x: rect.left + margin, y: rect.bottom - margin }, // bottom left
2373
+ { x: rect.right - margin, y: rect.bottom - margin }
2374
+ // bottom right
2375
+ ];
2376
+ return checkPoints.some(({ x, y }) => {
2377
+ try {
2378
+ const topEl = document.elementFromPoint(x, y);
2379
+ if (!topEl) return false;
2380
+ let current = topEl;
2381
+ while (current && current !== document.documentElement) {
2382
+ if (current === element) return true;
2383
+ current = current.parentElement;
2384
+ }
2385
+ return false;
2386
+ } catch (e) {
2387
+ return true;
2388
+ }
2389
+ });
2390
+ }
2391
+ function isInExpandedViewport(element, viewportExpansion2) {
2392
+ if (viewportExpansion2 === -1) {
2393
+ return true;
2394
+ }
2395
+ const rects = element.getClientRects();
2396
+ if (!rects || rects.length === 0) {
2397
+ const boundingRect = getCachedBoundingRect(element);
2398
+ if (!boundingRect || boundingRect.width === 0 || boundingRect.height === 0) {
2399
+ return false;
2400
+ }
2401
+ return !(boundingRect.bottom < -viewportExpansion2 || boundingRect.top > window.innerHeight + viewportExpansion2 || boundingRect.right < -viewportExpansion2 || boundingRect.left > window.innerWidth + viewportExpansion2);
2402
+ }
2403
+ for (const rect of rects) {
2404
+ if (rect.width === 0 || rect.height === 0) continue;
2405
+ if (!(rect.bottom < -viewportExpansion2 || rect.top > window.innerHeight + viewportExpansion2 || rect.right < -viewportExpansion2 || rect.left > window.innerWidth + viewportExpansion2)) {
2406
+ return true;
2407
+ }
2408
+ }
2409
+ return false;
2410
+ }
2411
+ const INTERACTIVE_ARIA_ATTRS = [
2412
+ "aria-expanded",
2413
+ "aria-checked",
2414
+ "aria-selected",
2415
+ "aria-pressed",
2416
+ "aria-haspopup",
2417
+ "aria-controls",
2418
+ "aria-owns",
2419
+ "aria-activedescendant",
2420
+ "aria-valuenow",
2421
+ "aria-valuetext",
2422
+ "aria-valuemax",
2423
+ "aria-valuemin",
2424
+ "aria-autocomplete"
2425
+ ];
2426
+ function hasInteractiveAria(el) {
2427
+ for (let i = 0; i < INTERACTIVE_ARIA_ATTRS.length; i++) {
2428
+ if (el.hasAttribute(INTERACTIVE_ARIA_ATTRS[i])) return true;
2429
+ }
2430
+ return false;
2431
+ }
2432
+ function isInteractiveCandidate(element) {
2433
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
2434
+ const tagName = element.tagName.toLowerCase();
2435
+ const interactiveElements = /* @__PURE__ */ new Set([
2436
+ "a",
2437
+ "button",
2438
+ "input",
2439
+ "select",
2440
+ "textarea",
2441
+ "details",
2442
+ "summary",
2443
+ "label"
2444
+ ]);
2445
+ if (interactiveElements.has(tagName)) return true;
2446
+ const hasQuickInteractiveAttr = element.hasAttribute("onclick") || element.hasAttribute("role") || element.hasAttribute("tabindex") || hasInteractiveAria(element) || element.hasAttribute("data-action") || element.getAttribute("contenteditable") === "true";
2447
+ return hasQuickInteractiveAttr;
2448
+ }
2449
+ const DISTINCT_INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
2450
+ "a",
2451
+ "button",
2452
+ "input",
2453
+ "select",
2454
+ "textarea",
2455
+ "summary",
2456
+ "details",
2457
+ "label",
2458
+ "option",
2459
+ "li"
2460
+ ]);
2461
+ const DISTINCT_INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
2462
+ "button",
2463
+ "link",
2464
+ "menuitem",
2465
+ "menuitemradio",
2466
+ "menuitemcheckbox",
2467
+ "radio",
2468
+ "checkbox",
2469
+ "tab",
2470
+ "switch",
2471
+ "slider",
2472
+ "spinbutton",
2473
+ "combobox",
2474
+ "searchbox",
2475
+ "textbox",
2476
+ "listbox",
2477
+ "listitem",
2478
+ "treeitem",
2479
+ "row",
2480
+ "option",
2481
+ "scrollbar"
2482
+ ]);
2483
+ function isHeuristicallyInteractive(element) {
2484
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) return false;
2485
+ if (!isElementVisible(element)) return false;
2486
+ const hasInteractiveAttributes = element.hasAttribute("role") || element.hasAttribute("tabindex") || element.hasAttribute("onclick") || typeof element.onclick === "function";
2487
+ const hasInteractiveClass = /\b(btn|clickable|menu|item|entry|link)\b/i.test(
2488
+ element.className || ""
2489
+ );
2490
+ const isInKnownContainer = Boolean(
2491
+ element.closest('button,a,[role="button"],.menu,.dropdown,.list,.toolbar')
2492
+ );
2493
+ const hasVisibleChildren = [...element.children].some(isElementVisible);
2494
+ const isParentBody = element.parentElement && element.parentElement.isSameNode(document.body);
2495
+ return (isInteractiveElement(element) || hasInteractiveAttributes || hasInteractiveClass) && hasVisibleChildren && isInKnownContainer && !isParentBody;
2496
+ }
2497
+ function isElementDistinctInteraction(element) {
2498
+ if (!element || element.nodeType !== Node.ELEMENT_NODE) {
2499
+ return false;
2500
+ }
2501
+ const tagName = element.tagName.toLowerCase();
2502
+ const role = element.getAttribute("role");
2503
+ if (tagName === "iframe") {
2504
+ return true;
2505
+ }
2506
+ if (DISTINCT_INTERACTIVE_TAGS.has(tagName)) {
2507
+ return true;
2508
+ }
2509
+ if (role && DISTINCT_INTERACTIVE_ROLES.has(role)) {
2510
+ return true;
2511
+ }
2512
+ if (element.isContentEditable || element.getAttribute("contenteditable") === "true") {
2513
+ return true;
2514
+ }
2515
+ if (element.hasAttribute("data-testid") || element.hasAttribute("data-cy") || element.hasAttribute("data-test")) {
2516
+ return true;
2517
+ }
2518
+ if (element.hasAttribute("onclick") || typeof element.onclick === "function") {
2519
+ return true;
2520
+ }
2521
+ if (hasInteractiveAria(element)) {
2522
+ return true;
2523
+ }
2524
+ try {
2525
+ const getEventListenersForNode = element?.ownerDocument?.defaultView?.getEventListenersForNode || window.getEventListenersForNode;
2526
+ if (typeof getEventListenersForNode === "function") {
2527
+ const listeners = getEventListenersForNode(element);
2528
+ const interactionEvents = [
2529
+ "click",
2530
+ "mousedown",
2531
+ "mouseup",
2532
+ "keydown",
2533
+ "keyup",
2534
+ "submit",
2535
+ "change",
2536
+ "input",
2537
+ "focus",
2538
+ "blur"
2539
+ ];
2540
+ for (const eventType of interactionEvents) {
2541
+ for (const listener of listeners) {
2542
+ if (listener.type === eventType) {
2543
+ return true;
2544
+ }
2545
+ }
2546
+ }
2547
+ }
2548
+ const commonEventAttrs = [
2549
+ "onmousedown",
2550
+ "onmouseup",
2551
+ "onkeydown",
2552
+ "onkeyup",
2553
+ "onsubmit",
2554
+ "onchange",
2555
+ "oninput",
2556
+ "onfocus",
2557
+ "onblur"
2558
+ ];
2559
+ if (commonEventAttrs.some((attr) => element.hasAttribute(attr))) {
2560
+ return true;
2561
+ }
2562
+ } catch (e) {
2563
+ }
2564
+ if (isHeuristicallyInteractive(element)) {
2565
+ return true;
2566
+ }
2567
+ if (extraData.get(element)?.scrollable) {
2568
+ return true;
2569
+ }
2570
+ return false;
2571
+ }
2572
+ function handleHighlighting(nodeData, node, parentIframe, isParentHighlighted) {
2573
+ if (!nodeData.isInteractive) return false;
2574
+ let shouldHighlight = false;
2575
+ if (!isParentHighlighted) {
2576
+ shouldHighlight = true;
2577
+ } else {
2578
+ if (isElementDistinctInteraction(node)) {
2579
+ shouldHighlight = true;
2580
+ } else {
2581
+ shouldHighlight = false;
2582
+ }
2583
+ }
2584
+ if (shouldHighlight) {
2585
+ nodeData.isInViewport = isInExpandedViewport(node, viewportExpansion);
2586
+ if (nodeData.isInViewport || viewportExpansion === -1) {
2587
+ nodeData.highlightIndex = highlightIndex++;
2588
+ if (doHighlightElements) {
2589
+ if (focusHighlightIndex >= 0) {
2590
+ if (focusHighlightIndex === nodeData.highlightIndex) {
2591
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
2592
+ }
2593
+ } else {
2594
+ highlightElement(node, nodeData.highlightIndex, parentIframe);
2595
+ }
2596
+ return true;
2597
+ }
2598
+ }
2599
+ }
2600
+ return false;
2601
+ }
2602
+ function buildDomTree(node, parentIframe = null, isParentHighlighted = false) {
2603
+ if (!node || node.id === HIGHLIGHT_CONTAINER_ID || node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
2604
+ return null;
2605
+ }
2606
+ if (!node || node.id === HIGHLIGHT_CONTAINER_ID) {
2607
+ return null;
2608
+ }
2609
+ if (node.dataset?.browserUseIgnore === "true" || node.dataset?.pageAgentIgnore === "true") {
2610
+ return null;
2611
+ }
2612
+ if (node.getAttribute && node.getAttribute("aria-hidden") === "true") {
2613
+ return null;
2614
+ }
2615
+ if (node === document.body) {
2616
+ const nodeData2 = {
2617
+ tagName: "body",
2618
+ attributes: {},
2619
+ xpath: "/body",
2620
+ children: []
2621
+ };
2622
+ for (const child of node.childNodes) {
2623
+ const domElement = buildDomTree(child, parentIframe, false);
2624
+ if (domElement) nodeData2.children.push(domElement);
2625
+ }
2626
+ const id2 = `${ID.current++}`;
2627
+ DOM_HASH_MAP[id2] = nodeData2;
2628
+ return id2;
2629
+ }
2630
+ if (node.nodeType !== Node.ELEMENT_NODE && node.nodeType !== Node.TEXT_NODE) {
2631
+ return null;
2632
+ }
2633
+ if (node.nodeType === Node.TEXT_NODE) {
2634
+ const textContent = node.textContent?.trim();
2635
+ if (!textContent) {
2636
+ return null;
2637
+ }
2638
+ const parentElement = node.parentElement;
2639
+ if (!parentElement || parentElement.tagName.toLowerCase() === "script") {
2640
+ return null;
2641
+ }
2642
+ const id2 = `${ID.current++}`;
2643
+ DOM_HASH_MAP[id2] = {
2644
+ type: "TEXT_NODE",
2645
+ text: textContent,
2646
+ isVisible: isTextNodeVisible(node)
2647
+ };
2648
+ return id2;
2649
+ }
2650
+ if (node.nodeType === Node.ELEMENT_NODE && !isElementAccepted(node)) {
2651
+ return null;
2652
+ }
2653
+ if (viewportExpansion !== -1 && !node.shadowRoot) {
2654
+ const rect = getCachedBoundingRect(node);
2655
+ const style = getCachedComputedStyle(node);
2656
+ const isFixedOrSticky = style && (style.position === "fixed" || style.position === "sticky");
2657
+ const hasSize = node.offsetWidth > 0 || node.offsetHeight > 0;
2658
+ if (!rect || !isFixedOrSticky && !hasSize && (rect.bottom < -viewportExpansion || rect.top > window.innerHeight + viewportExpansion || rect.right < -viewportExpansion || rect.left > window.innerWidth + viewportExpansion)) {
2659
+ return null;
2660
+ }
2661
+ }
2662
+ const nodeData = {
2663
+ tagName: node.tagName.toLowerCase(),
2664
+ attributes: {},
2665
+ /**
2666
+ * @edit no need for xpath
2667
+ */
2668
+ // xpath: getXPathTree(node, true),
2669
+ children: []
2670
+ };
2671
+ if (isInteractiveCandidate(node) || node.tagName.toLowerCase() === "iframe" || node.tagName.toLowerCase() === "body") {
2672
+ const attributeNames = node.getAttributeNames?.() || [];
2673
+ for (const name of attributeNames) {
2674
+ const value = node.getAttribute(name);
2675
+ nodeData.attributes[name] = value;
2676
+ }
2677
+ if (node.tagName.toLowerCase() === "input" && (node.type === "checkbox" || node.type === "radio")) {
2678
+ nodeData.attributes.checked = node.checked ? "true" : "false";
2679
+ }
2680
+ }
2681
+ let nodeWasHighlighted = false;
2682
+ if (node.nodeType === Node.ELEMENT_NODE) {
2683
+ nodeData.isVisible = isElementVisible(node);
2684
+ if (nodeData.isVisible) {
2685
+ nodeData.isTopElement = isTopElement(node);
2686
+ const role = node.getAttribute("role");
2687
+ const isMenuContainer = role === "menu" || role === "menubar" || role === "listbox";
2688
+ if (nodeData.isTopElement || isMenuContainer) {
2689
+ nodeData.isInteractive = isInteractiveElement(node);
2690
+ nodeWasHighlighted = handleHighlighting(nodeData, node, parentIframe, isParentHighlighted);
2691
+ nodeData.ref = node;
2692
+ if (nodeData.isInteractive && Object.keys(nodeData.attributes).length === 0) {
2693
+ const attributeNames = node.getAttributeNames?.() || [];
2694
+ for (const name of attributeNames) {
2695
+ const value = node.getAttribute(name);
2696
+ nodeData.attributes[name] = value;
2697
+ }
2698
+ }
2699
+ }
2700
+ }
2701
+ }
2702
+ if (node.tagName) {
2703
+ const tagName = node.tagName.toLowerCase();
2704
+ if (tagName === "iframe") {
2705
+ try {
2706
+ const iframeDoc = node.contentDocument || node.contentWindow?.document;
2707
+ if (iframeDoc) {
2708
+ for (const child of iframeDoc.childNodes) {
2709
+ const domElement = buildDomTree(child, node, false);
2710
+ if (domElement) nodeData.children.push(domElement);
2711
+ }
2712
+ }
2713
+ } catch (e) {
2714
+ console.warn("Unable to access iframe:", e);
2715
+ }
2716
+ } else if (node.isContentEditable || node.getAttribute("contenteditable") === "true" || node.id === "tinymce" || node.classList.contains("mce-content-body") || tagName === "body" && node.getAttribute("data-id")?.startsWith("mce_")) {
2717
+ for (const child of node.childNodes) {
2718
+ const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
2719
+ if (domElement) nodeData.children.push(domElement);
2720
+ }
2721
+ } else {
2722
+ if (node.shadowRoot) {
2723
+ nodeData.shadowRoot = true;
2724
+ for (const child of node.shadowRoot.childNodes) {
2725
+ const domElement = buildDomTree(child, parentIframe, nodeWasHighlighted);
2726
+ if (domElement) nodeData.children.push(domElement);
2727
+ }
2728
+ }
2729
+ for (const child of node.childNodes) {
2730
+ const passHighlightStatusToChild = nodeWasHighlighted || isParentHighlighted;
2731
+ const domElement = buildDomTree(child, parentIframe, passHighlightStatusToChild);
2732
+ if (domElement) nodeData.children.push(domElement);
2733
+ }
2734
+ }
2735
+ }
2736
+ if (nodeData.tagName === "a" && nodeData.children.length === 0 && !nodeData.attributes.href) {
2737
+ const rect = getCachedBoundingRect(node);
2738
+ const hasSize = rect && rect.width > 0 && rect.height > 0 || node.offsetWidth > 0 || node.offsetHeight > 0;
2739
+ if (!hasSize) {
2740
+ return null;
2741
+ }
2742
+ }
2743
+ nodeData.extra = extraData.get(node) || null;
2744
+ const id = `${ID.current++}`;
2745
+ DOM_HASH_MAP[id] = nodeData;
2746
+ return id;
2747
+ }
2748
+ const rootId = buildDomTree(document.body);
2749
+ DOM_CACHE.clearCache();
2750
+ return { rootId, map: DOM_HASH_MAP };
2751
+ };
2752
+ const DEFAULT_VIEWPORT_EXPANSION = -1;
2753
+ function resolveViewportExpansion(viewportExpansion) {
2754
+ return viewportExpansion ?? DEFAULT_VIEWPORT_EXPANSION;
2755
+ }
2756
+ const SEMANTIC_TAGS = /* @__PURE__ */ new Set([
2757
+ "nav",
2758
+ "menu",
2759
+ // 'main',
2760
+ "header",
2761
+ "footer",
2762
+ "aside",
2763
+ // 'article',
2764
+ // 'form',
2765
+ "dialog"
2766
+ ]);
2767
+ const newElementsCache = /* @__PURE__ */ new WeakMap();
2768
+ function getFlatTree(config) {
2769
+ const viewportExpansion = resolveViewportExpansion(config.viewportExpansion);
2770
+ const interactiveBlacklist = [];
2771
+ for (const item of config.interactiveBlacklist || []) {
2772
+ if (typeof item === "function") {
2773
+ interactiveBlacklist.push(item());
2774
+ } else {
2775
+ interactiveBlacklist.push(item);
2776
+ }
2777
+ }
2778
+ const interactiveWhitelist = [];
2779
+ for (const item of config.interactiveWhitelist || []) {
2780
+ if (typeof item === "function") {
2781
+ interactiveWhitelist.push(item());
2782
+ } else {
2783
+ interactiveWhitelist.push(item);
2784
+ }
2785
+ }
2786
+ const elements = domTree({
2787
+ doHighlightElements: true,
2788
+ debugMode: true,
2789
+ focusHighlightIndex: -1,
2790
+ viewportExpansion,
2791
+ interactiveBlacklist,
2792
+ interactiveWhitelist,
2793
+ highlightOpacity: config.highlightOpacity ?? 0,
2794
+ highlightLabelOpacity: config.highlightLabelOpacity ?? 0.1
2795
+ });
2796
+ const currentUrl = window.location.href;
2797
+ for (const nodeId in elements.map) {
2798
+ const node = elements.map[nodeId];
2799
+ if (node.isInteractive && node.ref) {
2800
+ const ref = node.ref;
2801
+ if (!newElementsCache.has(ref)) {
2802
+ newElementsCache.set(ref, currentUrl);
2803
+ node.isNew = true;
2804
+ }
2805
+ }
2806
+ }
2807
+ return elements;
2808
+ }
2809
+ const globRegexCache = /* @__PURE__ */ new Map();
2810
+ function globToRegex(pattern) {
2811
+ let regex = globRegexCache.get(pattern);
2812
+ if (!regex) {
2813
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2814
+ regex = new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
2815
+ globRegexCache.set(pattern, regex);
2816
+ }
2817
+ return regex;
2818
+ }
2819
+ function matchAttributes(attrs, patterns) {
2820
+ const result2 = {};
2821
+ for (const pattern of patterns) {
2822
+ if (pattern.includes("*")) {
2823
+ const regex = globToRegex(pattern);
2824
+ for (const key of Object.keys(attrs)) {
2825
+ if (regex.test(key) && attrs[key].trim()) {
2826
+ result2[key] = attrs[key].trim();
2827
+ }
2828
+ }
2829
+ } else {
2830
+ const value = attrs[pattern];
2831
+ if (value && value.trim()) {
2832
+ result2[pattern] = value.trim();
2833
+ }
2834
+ }
2835
+ }
2836
+ return result2;
2837
+ }
2838
+ function flatTreeToString(flatTree, includeAttributes = [], keepSemanticTags = false) {
2839
+ const DEFAULT_INCLUDE_ATTRIBUTES = [
2840
+ "title",
2841
+ "type",
2842
+ "checked",
2843
+ "name",
2844
+ "role",
2845
+ "value",
2846
+ "placeholder",
2847
+ "data-date-format",
2848
+ "alt",
2849
+ "aria-label",
2850
+ "aria-expanded",
2851
+ "data-state",
2852
+ "aria-checked",
2853
+ // @edit added for better form handling
2854
+ "id",
2855
+ "for",
2856
+ // for jump check
2857
+ "target",
2858
+ // absolute position dropdown menu
2859
+ "aria-haspopup",
2860
+ "aria-controls",
2861
+ "aria-owns",
2862
+ // content editable
2863
+ "contenteditable"
2864
+ ];
2865
+ const includeAttrs = [...includeAttributes, ...DEFAULT_INCLUDE_ATTRIBUTES];
2866
+ const capTextLength = (text, maxLength) => {
2867
+ if (text.length > maxLength) {
2868
+ return text.substring(0, maxLength) + "...";
2869
+ }
2870
+ return text;
2871
+ };
2872
+ const buildTreeNode = (nodeId) => {
2873
+ const node = flatTree.map[nodeId];
2874
+ if (!node) return null;
2875
+ if (node.type === "TEXT_NODE") {
2876
+ const textNode = node;
2877
+ return {
2878
+ type: "text",
2879
+ text: textNode.text,
2880
+ isVisible: textNode.isVisible,
2881
+ parent: null,
2882
+ children: []
2883
+ };
2884
+ } else {
2885
+ const elementNode = node;
2886
+ const children = [];
2887
+ if (elementNode.children) {
2888
+ for (const childId of elementNode.children) {
2889
+ const child = buildTreeNode(childId);
2890
+ if (child) {
2891
+ child.parent = null;
2892
+ children.push(child);
2893
+ }
2894
+ }
2895
+ }
2896
+ return {
2897
+ type: "element",
2898
+ tagName: elementNode.tagName,
2899
+ attributes: elementNode.attributes ?? {},
2900
+ isVisible: elementNode.isVisible ?? false,
2901
+ isInteractive: elementNode.isInteractive ?? false,
2902
+ isTopElement: elementNode.isTopElement ?? false,
2903
+ isNew: elementNode.isNew ?? false,
2904
+ highlightIndex: elementNode.highlightIndex,
2905
+ parent: null,
2906
+ children,
2907
+ extra: elementNode.extra ?? {}
2908
+ };
2909
+ }
2910
+ };
2911
+ const setParentReferences = (node, parent = null) => {
2912
+ node.parent = parent;
2913
+ for (const child of node.children) {
2914
+ setParentReferences(child, node);
2915
+ }
2916
+ };
2917
+ const rootNode = buildTreeNode(flatTree.rootId);
2918
+ if (!rootNode) return "";
2919
+ setParentReferences(rootNode);
2920
+ const hasParentWithHighlightIndex = (node) => {
2921
+ let current = node.parent;
2922
+ while (current) {
2923
+ if (current.type === "element" && current.highlightIndex !== void 0) {
2924
+ return true;
2925
+ }
2926
+ current = current.parent;
2927
+ }
2928
+ return false;
2929
+ };
2930
+ const processNode = (node, depth, result22) => {
2931
+ let nextDepth = depth;
2932
+ const depthStr = " ".repeat(depth);
2933
+ if (node.type === "element") {
2934
+ const isSemantic = keepSemanticTags && node.tagName && SEMANTIC_TAGS.has(node.tagName);
2935
+ if (node.highlightIndex !== void 0) {
2936
+ nextDepth += 1;
2937
+ const text = getAllTextTillNextClickableElement(node);
2938
+ let attributesHtmlStr = "";
2939
+ if (includeAttrs.length > 0 && node.attributes) {
2940
+ const attributesToInclude = matchAttributes(node.attributes, includeAttrs);
2941
+ const keys = Object.keys(attributesToInclude);
2942
+ if (keys.length > 1) {
2943
+ const keysToRemove = /* @__PURE__ */ new Set();
2944
+ const seenValues = {};
2945
+ for (const key of keys) {
2946
+ const value = attributesToInclude[key];
2947
+ if (value.length > 5) {
2948
+ if (value in seenValues) {
2949
+ keysToRemove.add(key);
2950
+ } else {
2951
+ seenValues[value] = key;
2952
+ }
2953
+ }
2954
+ }
2955
+ for (const key of keysToRemove) {
2956
+ delete attributesToInclude[key];
2957
+ }
2958
+ }
2959
+ if (attributesToInclude.role === node.tagName) {
2960
+ delete attributesToInclude.role;
2961
+ }
2962
+ const attrsToRemoveIfTextMatches = ["aria-label", "placeholder", "title"];
2963
+ for (const attr of attrsToRemoveIfTextMatches) {
2964
+ if (attributesToInclude[attr] && attributesToInclude[attr].toLowerCase().trim() === text.toLowerCase().trim()) {
2965
+ delete attributesToInclude[attr];
2966
+ }
2967
+ }
2968
+ if (Object.keys(attributesToInclude).length > 0) {
2969
+ attributesHtmlStr = Object.entries(attributesToInclude).map(([key, value]) => `${key}=${capTextLength(value, 20)}`).join(" ");
2970
+ }
2971
+ }
2972
+ const highlightIndicator = node.isNew ? `*[${node.highlightIndex}]` : `[${node.highlightIndex}]`;
2973
+ let line = `${depthStr}${highlightIndicator}<${node.tagName ?? ""}`;
2974
+ if (attributesHtmlStr) {
2975
+ line += ` ${attributesHtmlStr}`;
2976
+ }
2977
+ if (node.extra) {
2978
+ if (node.extra.scrollable) {
2979
+ let scrollDataText = "";
2980
+ if (node.extra.scrollData?.left)
2981
+ scrollDataText += `left=${node.extra.scrollData.left}, `;
2982
+ if (node.extra.scrollData?.top) scrollDataText += `top=${node.extra.scrollData.top}, `;
2983
+ if (node.extra.scrollData?.right)
2984
+ scrollDataText += `right=${node.extra.scrollData.right}, `;
2985
+ if (node.extra.scrollData?.bottom)
2986
+ scrollDataText += `bottom=${node.extra.scrollData.bottom}`;
2987
+ line += ` data-scrollable="${scrollDataText}"`;
2988
+ }
2989
+ }
2990
+ if (text) {
2991
+ const trimmedText = text.trim();
2992
+ if (!attributesHtmlStr) {
2993
+ line += " ";
2994
+ }
2995
+ line += `>${trimmedText}`;
2996
+ } else if (!attributesHtmlStr) {
2997
+ line += " ";
2998
+ }
2999
+ line += " />";
3000
+ result22.push(line);
3001
+ }
3002
+ const emitSemantic = isSemantic && node.highlightIndex === void 0;
3003
+ const mark = emitSemantic ? result22.length : -1;
3004
+ if (emitSemantic) {
3005
+ result22.push(`${depthStr}<${node.tagName}>`);
3006
+ nextDepth += 1;
3007
+ }
3008
+ for (const child of node.children) {
3009
+ processNode(child, nextDepth, result22);
3010
+ }
3011
+ if (emitSemantic) {
3012
+ if (result22.length === mark + 1) {
3013
+ result22.pop();
3014
+ } else {
3015
+ result22.push(`${depthStr}</${node.tagName}>`);
3016
+ }
3017
+ }
3018
+ } else if (node.type === "text") {
3019
+ if (hasParentWithHighlightIndex(node)) {
3020
+ return;
3021
+ }
3022
+ if (node.parent && node.parent.type === "element" && node.parent.isVisible && node.parent.isTopElement) {
3023
+ result22.push(`${depthStr}${node.text ?? ""}`);
3024
+ }
3025
+ }
3026
+ };
3027
+ const result2 = [];
3028
+ processNode(rootNode, 0, result2);
3029
+ return result2.join("\n");
3030
+ }
3031
+ const getAllTextTillNextClickableElement = (node, maxDepth = -1) => {
3032
+ const textParts = [];
3033
+ const collectText = (currentNode, currentDepth) => {
3034
+ if (maxDepth !== -1 && currentDepth > maxDepth) {
3035
+ return;
3036
+ }
3037
+ if (currentNode.type === "element" && currentNode !== node && currentNode.highlightIndex !== void 0) {
3038
+ return;
3039
+ }
3040
+ if (currentNode.type === "text" && currentNode.text) {
3041
+ textParts.push(currentNode.text);
3042
+ } else if (currentNode.type === "element") {
3043
+ for (const child of currentNode.children) {
3044
+ collectText(child, currentDepth + 1);
3045
+ }
3046
+ }
3047
+ };
3048
+ collectText(node, 0);
3049
+ return textParts.join("\n").trim();
3050
+ };
3051
+ function getSelectorMap(flatTree) {
3052
+ const selectorMap = /* @__PURE__ */ new Map();
3053
+ const keys = Object.keys(flatTree.map);
3054
+ for (const key of keys) {
3055
+ const node = flatTree.map[key];
3056
+ if (node.isInteractive && typeof node.highlightIndex === "number") {
3057
+ selectorMap.set(node.highlightIndex, node);
3058
+ }
3059
+ }
3060
+ return selectorMap;
3061
+ }
3062
+ function getElementTextMap(simplifiedHTML) {
3063
+ const lines = simplifiedHTML.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
3064
+ const elementTextMap = /* @__PURE__ */ new Map();
3065
+ for (const line of lines) {
3066
+ const regex = /^\[(\d+)\]<[^>]+>([^<]*)/;
3067
+ const match = regex.exec(line);
3068
+ if (match) {
3069
+ const index = parseInt(match[1], 10);
3070
+ elementTextMap.set(index, line);
3071
+ }
3072
+ }
3073
+ return elementTextMap;
3074
+ }
3075
+ function cleanUpHighlights() {
3076
+ const cleanupFunctions = window._highlightCleanupFunctions || [];
3077
+ for (const cleanup of cleanupFunctions) {
3078
+ if (typeof cleanup === "function") {
3079
+ cleanup();
3080
+ }
3081
+ }
3082
+ window._highlightCleanupFunctions = [];
3083
+ }
3084
+ window.addEventListener("popstate", () => {
3085
+ cleanUpHighlights();
3086
+ });
3087
+ window.addEventListener("hashchange", () => {
3088
+ cleanUpHighlights();
3089
+ });
3090
+ window.addEventListener("beforeunload", () => {
3091
+ cleanUpHighlights();
3092
+ });
3093
+ const navigation = window.navigation;
3094
+ if (navigation && typeof navigation.addEventListener === "function") {
3095
+ navigation.addEventListener("navigate", () => {
3096
+ cleanUpHighlights();
3097
+ });
3098
+ } else {
3099
+ let currentUrl = window.location.href;
3100
+ setInterval(() => {
3101
+ if (window.location.href !== currentUrl) {
3102
+ currentUrl = window.location.href;
3103
+ cleanUpHighlights();
3104
+ }
3105
+ }, 500);
3106
+ }
3107
+ function getPageInfo() {
3108
+ const viewport_width = window.innerWidth;
3109
+ const viewport_height = window.innerHeight;
3110
+ const page_width = Math.max(document.documentElement.scrollWidth, document.body.scrollWidth || 0);
3111
+ const page_height = Math.max(
3112
+ document.documentElement.scrollHeight,
3113
+ document.body.scrollHeight || 0
3114
+ );
3115
+ const scroll_x = window.scrollX || window.pageXOffset || document.documentElement.scrollLeft || 0;
3116
+ const scroll_y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;
3117
+ const pixels_below = Math.max(0, page_height - (window.innerHeight + scroll_y));
3118
+ const pixels_right = Math.max(0, page_width - (window.innerWidth + scroll_x));
3119
+ return {
3120
+ // Current viewport dimensions
3121
+ viewport_width,
3122
+ viewport_height,
3123
+ // Total page dimensions
3124
+ page_width,
3125
+ page_height,
3126
+ // Current scroll position
3127
+ scroll_x,
3128
+ scroll_y,
3129
+ pixels_above: scroll_y,
3130
+ pixels_below,
3131
+ pages_above: viewport_height > 0 ? scroll_y / viewport_height : 0,
3132
+ pages_below: viewport_height > 0 ? pixels_below / viewport_height : 0,
3133
+ total_pages: viewport_height > 0 ? page_height / viewport_height : 0,
3134
+ current_page_position: scroll_y / Math.max(1, page_height - viewport_height),
3135
+ pixels_left: scroll_x,
3136
+ pixels_right
3137
+ };
3138
+ }
3139
+ function patchReact(pageController) {
3140
+ const reactRootElements = document.querySelectorAll(
3141
+ '[data-reactroot], [data-reactid], [data-react-checksum], #root, #app, [id^="root-"], [id^="app-"], #adex-wrapper, #adex-root'
3142
+ );
3143
+ for (const element of reactRootElements) {
3144
+ element.setAttribute("data-page-agent-not-interactive", "true");
3145
+ }
3146
+ }
3147
+ class PageController extends EventTarget {
3148
+ config;
3149
+ /** Corresponds to eval_page in browser-use */
3150
+ flatTree = null;
3151
+ /**
3152
+ * All highlighted index-mapped interactive elements
3153
+ * Corresponds to DOMState.selector_map in browser-use
3154
+ */
3155
+ selectorMap = /* @__PURE__ */ new Map();
3156
+ /** Index -> element text description mapping */
3157
+ elementTextMap = /* @__PURE__ */ new Map();
3158
+ /**
3159
+ * Simplified HTML for LLM consumption.
3160
+ * Corresponds to clickable_elements_to_string in browser-use
3161
+ */
3162
+ simplifiedHTML = "<EMPTY>";
3163
+ /** last time the tree was updated */
3164
+ lastTimeUpdate = 0;
3165
+ /** Whether the tree has been indexed at least once */
3166
+ isIndexed = false;
3167
+ /** Visual mask overlay for blocking user interaction during automation */
3168
+ mask = null;
3169
+ maskReady = null;
3170
+ constructor(config = {}) {
3171
+ super();
3172
+ this.config = config;
3173
+ patchReact();
3174
+ if (config.enableMask) this.initMask();
3175
+ }
3176
+ /**
3177
+ * FORK: expõe, somente leitura, a árvore achatada da última `updateTree()`.
3178
+ * O perfilador da página precisa descrever os mesmos elementos que o agente vê, e `flatTree`
3179
+ * é privado (o pacote nem exporta o namespace `dom`). O campo continua privado: quem lê
3180
+ * não pode substituir a árvore do agente. Ver vendor/UPSTREAM.md.
3181
+ */
3182
+ readFlatTree() {
3183
+ return this.flatTree;
3184
+ }
3185
+ /**
3186
+ * FORK: expõe, somente leitura, o mapa índice -> elemento interativo da última `updateTree()`.
3187
+ * Mesmo motivo de `readFlatTree()`; o tipo `ReadonlyMap` impede `set`/`delete` por quem lê.
3188
+ */
3189
+ readSelectorMap() {
3190
+ return this.selectorMap;
3191
+ }
3192
+ /**
3193
+ * Initialize mask asynchronously (dynamic import to avoid CSS loading in Node)
3194
+ */
3195
+ initMask() {
3196
+ if (this.maskReady !== null) return;
3197
+ this.maskReady = (async () => {
3198
+ const { SimulatorMask } = await import("./SimulatorMask-DIoKZt1z.js");
3199
+ this.mask = new SimulatorMask();
3200
+ })();
3201
+ }
3202
+ // ======= State Queries =======
3203
+ /**
3204
+ * Get current page URL
3205
+ */
3206
+ async getCurrentUrl() {
3207
+ return window.location.href;
3208
+ }
3209
+ /**
3210
+ * Get last tree update timestamp
3211
+ */
3212
+ async getLastUpdateTime() {
3213
+ return this.lastTimeUpdate;
3214
+ }
3215
+ /**
3216
+ * Get structured browser state for LLM consumption.
3217
+ * Automatically calls updateTree() to refresh the DOM state.
3218
+ */
3219
+ async getBrowserState() {
3220
+ const url = window.location.href;
3221
+ const title = document.title;
3222
+ const pi = getPageInfo();
3223
+ const viewportExpansion = resolveViewportExpansion(this.config.viewportExpansion);
3224
+ await this.updateTree();
3225
+ const content = this.simplifiedHTML;
3226
+ const titleLine = `Current Page: [${title}](${url})`;
3227
+ const pageInfoLine = `Page info: ${pi.viewport_width}x${pi.viewport_height}px viewport, ${pi.page_width}x${pi.page_height}px total page size, ${pi.pages_above.toFixed(1)} pages above, ${pi.pages_below.toFixed(1)} pages below, ${pi.total_pages.toFixed(1)} total pages, at ${(pi.current_page_position * 100).toFixed(0)}% of page`;
3228
+ const elementsLabel = viewportExpansion === -1 ? "Interactive elements from top layer of the current page (full page):" : "Interactive elements from top layer of the current page inside the viewport:";
3229
+ const hasContentAbove = pi.pixels_above > 4;
3230
+ const scrollHintAbove = hasContentAbove && viewportExpansion !== -1 ? `... ${pi.pixels_above} pixels above (${pi.pages_above.toFixed(1)} pages) - scroll to see more ...` : "[Start of page]";
3231
+ const header2 = `${titleLine}
3232
+ ${pageInfoLine}
3233
+
3234
+ ${elementsLabel}
3235
+
3236
+ ${scrollHintAbove}`;
3237
+ const hasContentBelow = pi.pixels_below > 4;
3238
+ const footer = hasContentBelow && viewportExpansion !== -1 ? `... ${pi.pixels_below} pixels below (${pi.pages_below.toFixed(1)} pages) - scroll to see more ...` : "[End of page]";
3239
+ return { url, title, header: header2, content, footer };
3240
+ }
3241
+ // ======= DOM Tree Operations =======
3242
+ /**
3243
+ * Update DOM tree, returns simplified HTML for LLM.
3244
+ * This is the main method to refresh the page state.
3245
+ * Automatically bypasses mask during DOM extraction if enabled.
3246
+ */
3247
+ async updateTree() {
3248
+ this.dispatchEvent(new Event("beforeUpdate"));
3249
+ this.lastTimeUpdate = Date.now();
3250
+ if (this.mask) {
3251
+ this.mask.wrapper.style.pointerEvents = "none";
3252
+ }
3253
+ cleanUpHighlights();
3254
+ const blacklist = [
3255
+ ...this.config.interactiveBlacklist || [],
3256
+ ...Array.from(document.querySelectorAll("[data-page-agent-not-interactive]"))
3257
+ ];
3258
+ this.flatTree = getFlatTree({
3259
+ ...this.config,
3260
+ interactiveBlacklist: blacklist
3261
+ });
3262
+ this.simplifiedHTML = flatTreeToString(
3263
+ this.flatTree,
3264
+ this.config.includeAttributes,
3265
+ this.config.keepSemanticTags
3266
+ );
3267
+ this.selectorMap.clear();
3268
+ this.selectorMap = getSelectorMap(this.flatTree);
3269
+ this.elementTextMap.clear();
3270
+ this.elementTextMap = getElementTextMap(this.simplifiedHTML);
3271
+ this.isIndexed = true;
3272
+ if (this.mask) {
3273
+ this.mask.wrapper.style.pointerEvents = "auto";
3274
+ }
3275
+ this.dispatchEvent(new Event("afterUpdate"));
3276
+ return this.simplifiedHTML;
3277
+ }
3278
+ /**
3279
+ * Clean up all element highlights
3280
+ */
3281
+ async cleanUpHighlights() {
3282
+ console.log("[PageController] cleanUpHighlights");
3283
+ cleanUpHighlights();
3284
+ }
3285
+ // ======= Element Actions =======
3286
+ /**
3287
+ * Ensure the tree has been indexed before any index-based operation.
3288
+ * Throws if updateTree() hasn't been called yet.
3289
+ */
3290
+ assertIndexed() {
3291
+ if (!this.isIndexed) {
3292
+ throw new Error("DOM tree not indexed yet. Can not perform actions on elements.");
3293
+ }
3294
+ }
3295
+ /**
3296
+ * Click element by index
3297
+ */
3298
+ async clickElement(index) {
3299
+ try {
3300
+ this.assertIndexed();
3301
+ const element = getElementByIndex(this.selectorMap, index);
3302
+ const elemText = this.elementTextMap.get(index);
3303
+ await clickElement(element);
3304
+ if (isAnchorElement(element) && element.target === "_blank") {
3305
+ return {
3306
+ success: true,
3307
+ message: `OK: elemento (${elemText ?? index}) clicado. ATENÇÃO: o link abriu em uma nova aba.`
3308
+ };
3309
+ }
3310
+ return {
3311
+ success: true,
3312
+ message: `OK: elemento (${elemText ?? index}) clicado.`
3313
+ };
3314
+ } catch (error2) {
3315
+ return {
3316
+ success: false,
3317
+ message: `ERRO: não foi possível clicar no elemento: ${error2}`
3318
+ };
3319
+ }
3320
+ }
3321
+ /**
3322
+ * Input text into element by index
3323
+ */
3324
+ async inputText(index, text) {
3325
+ try {
3326
+ this.assertIndexed();
3327
+ const element = getElementByIndex(this.selectorMap, index);
3328
+ const elemText = this.elementTextMap.get(index);
3329
+ await inputTextElement(element, text);
3330
+ return {
3331
+ success: true,
3332
+ message: `OK: texto (${text}) digitado no elemento (${elemText ?? index}).`
3333
+ };
3334
+ } catch (error2) {
3335
+ return {
3336
+ success: false,
3337
+ message: `ERRO: não foi possível digitar o texto: ${error2}`
3338
+ };
3339
+ }
3340
+ }
3341
+ /**
3342
+ * Select dropdown option by index and option text
3343
+ */
3344
+ async selectOption(index, optionText) {
3345
+ try {
3346
+ this.assertIndexed();
3347
+ const element = getElementByIndex(this.selectorMap, index);
3348
+ const elemText = this.elementTextMap.get(index);
3349
+ await selectOptionElement(element, optionText);
3350
+ return {
3351
+ success: true,
3352
+ message: `OK: opção (${optionText}) escolhida no elemento (${elemText ?? index}).`
3353
+ };
3354
+ } catch (error2) {
3355
+ return {
3356
+ success: false,
3357
+ message: `ERRO: não foi possível escolher a opção: ${error2}`
3358
+ };
3359
+ }
3360
+ }
3361
+ /**
3362
+ * Scroll vertically
3363
+ */
3364
+ async scroll(options) {
3365
+ try {
3366
+ const { down, numPages, pixels, index } = options;
3367
+ this.assertIndexed();
3368
+ const scrollAmount = (pixels ?? numPages * window.innerHeight) * (down ? 1 : -1);
3369
+ const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
3370
+ const message = await scrollVertically(scrollAmount, element);
3371
+ return {
3372
+ success: true,
3373
+ message
3374
+ };
3375
+ } catch (error2) {
3376
+ return {
3377
+ success: false,
3378
+ message: `ERRO: não foi possível rolar a página: ${error2}`
3379
+ };
3380
+ }
3381
+ }
3382
+ /**
3383
+ * Scroll horizontally
3384
+ */
3385
+ async scrollHorizontally(options) {
3386
+ try {
3387
+ const { right, pixels, index } = options;
3388
+ this.assertIndexed();
3389
+ const scrollAmount = pixels * (right ? 1 : -1);
3390
+ const element = index !== void 0 ? getElementByIndex(this.selectorMap, index) : null;
3391
+ const message = await scrollHorizontally(scrollAmount, element);
3392
+ return {
3393
+ success: true,
3394
+ message
3395
+ };
3396
+ } catch (error2) {
3397
+ return {
3398
+ success: false,
3399
+ message: `ERRO: não foi possível rolar na horizontal: ${error2}`
3400
+ };
3401
+ }
3402
+ }
3403
+ /**
3404
+ * Execute arbitrary JavaScript on the page.
3405
+ * The optional `signal` is exposed to the script scope so cooperative code
3406
+ * can abort promptly when the task is stopped.
3407
+ */
3408
+ async executeJavascript(script, signal) {
3409
+ try {
3410
+ const asyncFunction = eval(`(async (signal) => { ${script} })`);
3411
+ const result = await asyncFunction(signal);
3412
+ return {
3413
+ success: true,
3414
+ message: `OK: JavaScript executado. Resultado: ${result}`
3415
+ };
3416
+ } catch (error2) {
3417
+ return {
3418
+ success: false,
3419
+ message: `ERRO: falha ao executar JavaScript: ${error2}`
3420
+ };
3421
+ }
3422
+ }
3423
+ // ======= Mask Operations =======
3424
+ /**
3425
+ * Show the visual mask overlay.
3426
+ * Only works after mask is setup.
3427
+ */
3428
+ async showMask() {
3429
+ await this.maskReady;
3430
+ this.mask?.show();
3431
+ }
3432
+ /**
3433
+ * Hide the visual mask overlay.
3434
+ * Only works after mask is setup.
3435
+ */
3436
+ async hideMask() {
3437
+ await this.maskReady;
3438
+ this.mask?.hide();
3439
+ }
3440
+ /**
3441
+ * Dispose and clean up resources
3442
+ */
3443
+ dispose() {
3444
+ cleanUpHighlights();
3445
+ this.flatTree = null;
3446
+ this.selectorMap.clear();
3447
+ this.elementTextMap.clear();
3448
+ this.simplifiedHTML = "<EMPTY>";
3449
+ this.isIndexed = false;
3450
+ this.mask?.dispose();
3451
+ this.mask = null;
3452
+ }
3453
+ }
3454
+ const enUS = {
3455
+ ui: {
3456
+ panel: {
3457
+ ready: "Ready",
3458
+ thinking: "Thinking...",
3459
+ taskInput: "Enter new task, describe steps in detail, press Enter to submit",
3460
+ userAnswerPrompt: "Please answer the question above, press Enter to submit",
3461
+ taskTerminated: "Task terminated",
3462
+ taskCompleted: "Task completed",
3463
+ userAnswer: "User answer: {{input}}",
3464
+ question: "Question: {{question}}",
3465
+ waitingPlaceholder: "Waiting for task to start...",
3466
+ stop: "Stop",
3467
+ close: "Close",
3468
+ expand: "Expand history",
3469
+ collapse: "Collapse history",
3470
+ step: "Step {{number}}",
3471
+ // FORK: textos que estavam cravados em inglês dentro do Panel.ts.
3472
+ userTakeover: "User takeover",
3473
+ retrying: "Retrying",
3474
+ errorLabel: "Error",
3475
+ reflectionEvaluation: "Assessment",
3476
+ reflectionMemory: "Memory",
3477
+ reflectionNextGoal: "Next goal",
3478
+ // FORK: nome de quem fala em cada mensagem, e rótulos das regiões.
3479
+ authorPerson: "You",
3480
+ authorAgent: "Agent",
3481
+ authorReasoning: "Reasoning",
3482
+ authorAction: "Action",
3483
+ authorResult: "Result",
3484
+ authorPage: "Page",
3485
+ authorSystem: "System",
3486
+ historyLabel: "Agent activity history",
3487
+ panelLabel: "Agent panel"
3488
+ },
3489
+ tools: {
3490
+ clicking: "Clicking element [{{index}}]...",
3491
+ inputting: "Inputting text to element [{{index}}]...",
3492
+ selecting: 'Selecting option "{{text}}"...',
3493
+ scrolling: "Scrolling page...",
3494
+ waiting: "Waiting {{seconds}} seconds...",
3495
+ askingUser: "Asking user...",
3496
+ done: "Task done",
3497
+ clicked: "Clicked element [{{index}}]",
3498
+ inputted: 'Inputted text "{{text}}"',
3499
+ selected: 'Selected option "{{text}}"',
3500
+ scrolled: "Page scrolled",
3501
+ waited: "Wait completed",
3502
+ executing: "Executing {{toolName}}...",
3503
+ resultSuccess: "success",
3504
+ resultFailure: "failed",
3505
+ resultError: "error"
3506
+ },
3507
+ errors: {
3508
+ elementNotFound: "No interactive element found at index {{index}}",
3509
+ taskRequired: "Task description is required",
3510
+ executionFailed: "Task execution failed",
3511
+ notInputElement: "Element is not an input or textarea",
3512
+ notSelectElement: "Element is not a select element",
3513
+ optionNotFound: 'Option "{{text}}" not found'
3514
+ }
3515
+ }
3516
+ };
3517
+ const ptBR = {
3518
+ ui: {
3519
+ panel: {
3520
+ ready: "Pronto",
3521
+ thinking: "Pensando...",
3522
+ taskInput: "Descreva a próxima tarefa com os passos em detalhe e tecle Enter",
3523
+ userAnswerPrompt: "Responda à pergunta acima e tecle Enter",
3524
+ taskTerminated: "Tarefa interrompida",
3525
+ taskCompleted: "Tarefa concluída",
3526
+ userAnswer: "Resposta: {{input}}",
3527
+ question: "Pergunta: {{question}}",
3528
+ waitingPlaceholder: "Aguardando o início da tarefa...",
3529
+ stop: "Parar",
3530
+ close: "Fechar",
3531
+ expand: "Abrir histórico",
3532
+ collapse: "Fechar histórico",
3533
+ step: "Passo {{number}}",
3534
+ userTakeover: "A pessoa assumiu o controle",
3535
+ retrying: "Tentando de novo",
3536
+ errorLabel: "Erro",
3537
+ reflectionEvaluation: "Avaliação",
3538
+ reflectionMemory: "Memória",
3539
+ reflectionNextGoal: "Próximo passo",
3540
+ authorPerson: "Você",
3541
+ authorAgent: "Agente",
3542
+ authorReasoning: "Raciocínio",
3543
+ authorAction: "Ação",
3544
+ authorResult: "Resultado",
3545
+ authorPage: "Página",
3546
+ authorSystem: "Sistema",
3547
+ historyLabel: "Histórico de atividade do agente",
3548
+ panelLabel: "Painel do agente"
3549
+ },
3550
+ tools: {
3551
+ clicking: "Clicando no elemento [{{index}}]...",
3552
+ inputting: "Digitando no elemento [{{index}}]...",
3553
+ selecting: 'Escolhendo a opção "{{text}}"...',
3554
+ scrolling: "Rolando a página...",
3555
+ waiting: "Aguardando {{seconds}} segundos...",
3556
+ askingUser: "Perguntando à pessoa...",
3557
+ done: "Tarefa encerrada",
3558
+ clicked: "Elemento [{{index}}] clicado",
3559
+ inputted: 'Texto "{{text}}" digitado',
3560
+ selected: 'Opção "{{text}}" escolhida',
3561
+ scrolled: "Página rolada",
3562
+ waited: "Espera concluída",
3563
+ executing: "Executando {{toolName}}...",
3564
+ resultSuccess: "sucesso",
3565
+ resultFailure: "falha",
3566
+ resultError: "erro"
3567
+ },
3568
+ errors: {
3569
+ elementNotFound: "Nenhum elemento interativo no índice {{index}}",
3570
+ taskRequired: "A descrição da tarefa é obrigatória",
3571
+ executionFailed: "A execução da tarefa falhou",
3572
+ notInputElement: "O elemento não é um campo de texto",
3573
+ notSelectElement: "O elemento não é uma lista de seleção",
3574
+ optionNotFound: 'Opção "{{text}}" não encontrada'
3575
+ }
3576
+ }
3577
+ };
3578
+ const locales = {
3579
+ "en-US": enUS,
3580
+ "pt-BR": ptBR
3581
+ };
3582
+ class I18n {
3583
+ language;
3584
+ translations;
3585
+ /**
3586
+ * FORK: aceita qualquer identificador de língua, porque o corpo já trata o caso desconhecido
3587
+ * caindo em 'en-US'. O tipo estreito contradizia essa checagem: ou ela era morta, ou o tipo
3588
+ * mentia. O núcleo conhece línguas que o painel ainda não traduz, e isso é normal.
3589
+ */
3590
+ constructor(language = "en-US") {
3591
+ this.language = language in locales ? language : "en-US";
3592
+ this.translations = locales[this.language];
3593
+ }
3594
+ // 类型安全的翻译方法
3595
+ t(key, params) {
3596
+ const value = this.getNestedValue(this.translations, key);
3597
+ if (!value) {
3598
+ console.warn(`Translation key "${key}" not found for language "${this.language}"`);
3599
+ return key;
3600
+ }
3601
+ if (params) {
3602
+ return this.interpolate(value, params);
3603
+ }
3604
+ return value;
3605
+ }
3606
+ getNestedValue(obj, path) {
3607
+ return path.split(".").reduce((current, key) => current?.[key], obj);
3608
+ }
3609
+ interpolate(template, params) {
3610
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
3611
+ return params[key] != null ? params[key].toString() : match;
3612
+ });
3613
+ }
3614
+ getLanguage() {
3615
+ return this.language;
3616
+ }
3617
+ }
3618
+ function truncate(text, maxLength) {
3619
+ if (text.length > maxLength) {
3620
+ return text.substring(0, maxLength) + "...";
3621
+ }
3622
+ return text;
3623
+ }
3624
+ function escapeHtml(text) {
3625
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3626
+ }
3627
+ const wrapper = "_wrapper_1rq3n_1";
3628
+ const background = "_background_1rq3n_55";
3629
+ const header = "_header_1rq3n_115";
3630
+ const pulse = "_pulse_1rq3n_1";
3631
+ const retryPulse = "_retryPulse_1rq3n_1";
3632
+ const statusTextFadeOut = "_statusTextFadeOut_1rq3n_1";
3633
+ const statusTextFadeIn = "_statusTextFadeIn_1rq3n_1";
3634
+ const statusSection = "_statusSection_1rq3n_137";
3635
+ const indicator = "_indicator_1rq3n_144";
3636
+ const thinking = "_thinking_1rq3n_153";
3637
+ const tool_executing = "_tool_executing_1rq3n_158";
3638
+ const retry = "_retry_1rq3n_163";
3639
+ const completed = "_completed_1rq3n_169";
3640
+ const input = "_input_1rq3n_170";
3641
+ const output = "_output_1rq3n_171";
3642
+ const error = "_error_1rq3n_176";
3643
+ const statusText = "_statusText_1rq3n_182";
3644
+ const fadeOut = "_fadeOut_1rq3n_194";
3645
+ const fadeIn = "_fadeIn_1rq3n_198";
3646
+ const controls = "_controls_1rq3n_204";
3647
+ const controlButton = "_controlButton_1rq3n_209";
3648
+ const stopButton = "_stopButton_1rq3n_228";
3649
+ const historySectionWrapper = "_historySectionWrapper_1rq3n_262";
3650
+ const expanded = "_expanded_1rq3n_294";
3651
+ const historySection = "_historySection_1rq3n_262";
3652
+ const historyRow = "_historyRow_1rq3n_320";
3653
+ const fromPessoa = "_fromPessoa_1rq3n_328";
3654
+ const fromAgente = "_fromAgente_1rq3n_332";
3655
+ const fromSistema = "_fromSistema_1rq3n_333";
3656
+ const historyItem = "_historyItem_1rq3n_338";
3657
+ const observation = "_observation_1rq3n_374";
3658
+ const question = "_question_1rq3n_378";
3659
+ const doneSuccess = "_doneSuccess_1rq3n_382";
3660
+ const doneFailure = "_doneFailure_1rq3n_390";
3661
+ const historyHeader = "_historyHeader_1rq3n_402";
3662
+ const historyAuthor = "_historyAuthor_1rq3n_410";
3663
+ const historyMeta = "_historyMeta_1rq3n_416";
3664
+ const statusIcon = "_statusIcon_1rq3n_426";
3665
+ const historyContent = "_historyContent_1rq3n_439";
3666
+ const reflectionLine = "_reflectionLine_1rq3n_446";
3667
+ const reflectionLines = "_reflectionLines_1rq3n_451";
3668
+ const reflectionLabel = "_reflectionLabel_1rq3n_462";
3669
+ const inputSectionWrapper = "_inputSectionWrapper_1rq3n_544";
3670
+ const hidden = "_hidden_1rq3n_567";
3671
+ const inputSection = "_inputSection_1rq3n_544";
3672
+ const taskInput = "_taskInput_1rq3n_578";
3673
+ const celebrate = "_celebrate_1rq3n_1";
3674
+ const shimmer = "_shimmer_1rq3n_1";
3675
+ const styles = {
3676
+ wrapper,
3677
+ "mask-running": "_mask-running_1rq3n_1",
3678
+ background,
3679
+ header,
3680
+ pulse,
3681
+ retryPulse,
3682
+ statusTextFadeOut,
3683
+ statusTextFadeIn,
3684
+ statusSection,
3685
+ indicator,
3686
+ thinking,
3687
+ tool_executing,
3688
+ retry,
3689
+ completed,
3690
+ input,
3691
+ output,
3692
+ error,
3693
+ statusText,
3694
+ fadeOut,
3695
+ fadeIn,
3696
+ controls,
3697
+ controlButton,
3698
+ stopButton,
3699
+ historySectionWrapper,
3700
+ expanded,
3701
+ historySection,
3702
+ historyRow,
3703
+ fromPessoa,
3704
+ fromAgente,
3705
+ fromSistema,
3706
+ historyItem,
3707
+ observation,
3708
+ question,
3709
+ doneSuccess,
3710
+ doneFailure,
3711
+ historyHeader,
3712
+ historyAuthor,
3713
+ historyMeta,
3714
+ statusIcon,
3715
+ historyContent,
3716
+ reflectionLine,
3717
+ reflectionLines,
3718
+ reflectionLabel,
3719
+ inputSectionWrapper,
3720
+ hidden,
3721
+ inputSection,
3722
+ taskInput,
3723
+ celebrate,
3724
+ shimmer
3725
+ };
3726
+ function createCard({ icon, content, meta, type, from = "agente", author }) {
3727
+ const typeClass = type ? styles[type] : "";
3728
+ const fromClass = from === "pessoa" ? styles.fromPessoa : from === "sistema" ? styles.fromSistema : styles.fromAgente;
3729
+ const contentHtml = Array.isArray(content) ? `<div class="${styles.reflectionLines}">${content.map(
3730
+ (line) => `<span class="${styles.reflectionLine}"><span class="${styles.reflectionLabel}">${escapeHtml(line.label)}</span>${escapeHtml(line.text)}</span>`
3731
+ ).join("")}</div>` : `<span>${escapeHtml(content)}</span>`;
3732
+ const cabecalho = `
3733
+ <div class="${styles.historyHeader}">
3734
+ <span class="${styles.statusIcon}">${icon}</span>
3735
+ <span class="${styles.historyAuthor}" data-autor="${from}">${escapeHtml(author || "")}</span>
3736
+ ${meta ? `<span class="${styles.historyMeta}">${escapeHtml(meta)}</span>` : ""}
3737
+ </div>`;
3738
+ return `
3739
+ <div class="${styles.historyRow} ${fromClass}">
3740
+ <div class="${styles.historyItem} ${typeClass}" data-mensagem="${from}">
3741
+ ${cabecalho}
3742
+ <div class="${styles.historyContent}" data-conteudo>${contentHtml}</div>
3743
+ </div>
3744
+ </div>
3745
+ `;
3746
+ }
3747
+ function createReflectionLines(reflection, labels) {
3748
+ const lines = [];
3749
+ if (reflection.evaluation_previous_goal) {
3750
+ lines.push({ label: labels.evaluation, text: reflection.evaluation_previous_goal });
3751
+ }
3752
+ if (reflection.memory) {
3753
+ lines.push({ label: labels.memory, text: reflection.memory });
3754
+ }
3755
+ if (reflection.next_goal) {
3756
+ lines.push({ label: labels.nextGoal, text: reflection.next_goal });
3757
+ }
3758
+ return lines;
3759
+ }
3760
+ const svg = (paths) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths}</svg>`;
3761
+ const iconTask = svg('<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4"/><path d="M12 3v2M12 19v2M3 12h2M19 12h2"/>');
3762
+ const iconThinking = svg('<path d="M12 3l1.8 4.2L18 9l-4.2 1.8L12 15l-1.8-4.2L6 9l4.2-1.8z"/><path d="M18 15l.9 2.1L21 18l-2.1.9L18 21l-.9-2.1L15 18l2.1-.9z"/>');
3763
+ const iconObservation = svg('<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6z"/><circle cx="12" cy="12" r="2.5"/>');
3764
+ const iconUser = svg('<circle cx="12" cy="8" r="3.5"/><path d="M5 20c0-3.3 3.1-5.5 7-5.5s7 2.2 7 5.5"/>');
3765
+ const iconRetry = svg('<path d="M20 11a8 8 0 1 0-2.3 6.3"/><path d="M20 5v6h-6"/>');
3766
+ const iconError = svg('<circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.2h.01"/>');
3767
+ const iconDone = svg('<circle cx="12" cy="12" r="9"/><path d="M8.2 12.3l2.6 2.6 5-5.2"/>');
3768
+ const iconQuestion = svg('<circle cx="12" cy="12" r="9"/><path d="M9.6 9.4a2.5 2.5 0 1 1 3.3 2.4c-.6.2-.9.8-.9 1.4v.3"/><path d="M12 16.6h.01"/>');
3769
+ const iconAnswer = svg('<path d="M20 14.5a2.5 2.5 0 0 1-2.5 2.5H9l-5 3.5V6.5A2.5 2.5 0 0 1 6.5 4h11A2.5 2.5 0 0 1 20 6.5z"/>');
3770
+ const iconTool = svg('<path d="M5 7.5l3.5 3.5L5 14.5"/><path d="M11.5 15.5H19"/><rect x="2.5" y="3.5" width="19" height="17" rx="2.5"/>');
3771
+ class Panel {
3772
+ #wrapper;
3773
+ #indicator;
3774
+ #statusText;
3775
+ #historySection;
3776
+ #expandButton;
3777
+ #actionButton;
3778
+ #inputSection;
3779
+ #taskInput;
3780
+ #agent;
3781
+ #config;
3782
+ #isExpanded = false;
3783
+ #i18n;
3784
+ #userAnswerResolver = null;
3785
+ #isWaitingForUserAnswer = false;
3786
+ #headerUpdateTimer = null;
3787
+ #pendingHeaderText = null;
3788
+ #isAnimating = false;
3789
+ // Event handlers (bound for removal)
3790
+ #onStatusChange = () => this.#handleStatusChange();
3791
+ #onHistoryChange = () => this.#handleHistoryChange();
3792
+ #onActivity = (e) => this.#handleActivity(e.detail);
3793
+ #onAgentDispose = () => this.dispose();
3794
+ get wrapper() {
3795
+ return this.#wrapper;
3796
+ }
3797
+ /**
3798
+ * Create a Panel bound to an agent
3799
+ * @param agent - Agent instance that implements PanelAgentAdapter
3800
+ * @param config - Optional panel configuration
3801
+ */
3802
+ constructor(agent, config = {}) {
3803
+ this.#agent = agent;
3804
+ this.#config = config;
3805
+ this.#i18n = new I18n(config.language ?? "en-US");
3806
+ this.#agent.onAskUser = (question2, options) => this.#askUser(question2, options?.signal);
3807
+ this.#wrapper = this.#createWrapper();
3808
+ this.#indicator = this.#wrapper.querySelector(`.${styles.indicator}`);
3809
+ this.#statusText = this.#wrapper.querySelector(`.${styles.statusText}`);
3810
+ this.#historySection = this.#wrapper.querySelector(`.${styles.historySection}`);
3811
+ this.#expandButton = this.#wrapper.querySelector(`.${styles.expandButton}`);
3812
+ this.#actionButton = this.#wrapper.querySelector(`.${styles.stopButton}`);
3813
+ this.#inputSection = this.#wrapper.querySelector(`.${styles.inputSectionWrapper}`);
3814
+ this.#taskInput = this.#wrapper.querySelector(`.${styles.taskInput}`);
3815
+ this.#agent.addEventListener("statuschange", this.#onStatusChange);
3816
+ this.#agent.addEventListener("historychange", this.#onHistoryChange);
3817
+ this.#agent.addEventListener("activity", this.#onActivity);
3818
+ this.#agent.addEventListener("dispose", this.#onAgentDispose);
3819
+ this.#setupEventListeners();
3820
+ this.#startHeaderUpdateLoop();
3821
+ this.#showInputArea();
3822
+ this.hide();
3823
+ }
3824
+ // ========== Agent event handlers ==========
3825
+ /** Handle agent status change */
3826
+ #handleStatusChange() {
3827
+ const status = this.#agent.status;
3828
+ const failed = status === "completed" && this.#agent.lastResult?.success === false;
3829
+ this.#updateStatusIndicator(failed ? "error" : status);
3830
+ if (status === "running") {
3831
+ this.#actionButton.textContent = "■";
3832
+ this.#actionButton.title = this.#i18n.t("ui.panel.stop");
3833
+ } else {
3834
+ this.#actionButton.textContent = "X";
3835
+ this.#actionButton.title = this.#i18n.t("ui.panel.close");
3836
+ }
3837
+ if (status === "running") {
3838
+ this.show();
3839
+ this.#hideInputArea();
3840
+ }
3841
+ if (status === "completed" || status === "error" || status === "stopped") {
3842
+ if (!this.#isExpanded) {
3843
+ this.#expand();
3844
+ }
3845
+ if (this.#shouldShowInputArea()) {
3846
+ this.#showInputArea();
3847
+ }
3848
+ }
3849
+ }
3850
+ /** Handle agent history change - re-render history list from agent.history */
3851
+ #handleHistoryChange() {
3852
+ this.#renderHistory();
3853
+ }
3854
+ /**
3855
+ * Handle agent activity - transient state for immediate UI feedback
3856
+ * Activity events are NOT persisted in history, only used for header bar updates
3857
+ */
3858
+ #handleActivity(activity) {
3859
+ switch (activity.type) {
3860
+ case "thinking":
3861
+ this.#pendingHeaderText = this.#i18n.t("ui.panel.thinking");
3862
+ this.#updateStatusIndicator("thinking");
3863
+ break;
3864
+ case "executing":
3865
+ this.#pendingHeaderText = this.#getToolExecutingText(activity.tool, activity.input);
3866
+ this.#updateStatusIndicator("executing");
3867
+ break;
3868
+ case "executed":
3869
+ this.#pendingHeaderText = truncate(activity.output, 50);
3870
+ break;
3871
+ case "retrying":
3872
+ this.#pendingHeaderText = `Retrying (${activity.attempt}/${activity.maxAttempts})`;
3873
+ this.#updateStatusIndicator("retrying");
3874
+ break;
3875
+ case "error":
3876
+ this.#pendingHeaderText = truncate(activity.message, 50);
3877
+ this.#updateStatusIndicator("error");
3878
+ break;
3879
+ }
3880
+ }
3881
+ /**
3882
+ * Ask for user input (internal, called by agent via onAskUser).
3883
+ * Rejects when `signal` aborts (task stopped or disposed), cleaning up the
3884
+ * question card and pending state so the agent loop can settle.
3885
+ */
3886
+ #askUser(question2, signal2) {
3887
+ return new Promise((resolve, reject) => {
3888
+ this.#isWaitingForUserAnswer = true;
3889
+ this.#userAnswerResolver = resolve;
3890
+ if (!this.#isExpanded) {
3891
+ this.#expand();
3892
+ }
3893
+ const tempCard = document.createElement("div");
3894
+ tempCard.innerHTML = createCard({
3895
+ icon: iconQuestion,
3896
+ content: this.#i18n.t("ui.panel.question", { question: question2 }),
3897
+ type: "question",
3898
+ author: this.#i18n.t("ui.panel.authorAgent")
3899
+ });
3900
+ const cardElement = tempCard.firstElementChild;
3901
+ cardElement.setAttribute("data-temp-card", "true");
3902
+ this.#historySection.appendChild(cardElement);
3903
+ this.#scrollToBottom();
3904
+ this.#showInputArea(this.#i18n.t("ui.panel.userAnswerPrompt"));
3905
+ signal2?.addEventListener(
3906
+ "abort",
3907
+ () => {
3908
+ this.#removeTempCards();
3909
+ this.#isWaitingForUserAnswer = false;
3910
+ this.#userAnswerResolver = null;
3911
+ reject(signal2.reason);
3912
+ },
3913
+ { once: true }
3914
+ );
3915
+ });
3916
+ }
3917
+ /** Remove temporary question cards (only direct children for safety) */
3918
+ #removeTempCards() {
3919
+ Array.from(this.#historySection.children).forEach((child) => {
3920
+ if (child.getAttribute("data-temp-card") === "true") {
3921
+ child.remove();
3922
+ }
3923
+ });
3924
+ }
3925
+ // ========== Public control methods ==========
3926
+ show() {
3927
+ this.wrapper.style.display = "block";
3928
+ this.wrapper.getBoundingClientRect();
3929
+ this.wrapper.style.opacity = "1";
3930
+ this.wrapper.style.transform = "translateX(-50%) translateY(0)";
3931
+ }
3932
+ hide() {
3933
+ this.wrapper.style.opacity = "0";
3934
+ this.wrapper.style.transform = "translateX(-50%) translateY(20px)";
3935
+ this.wrapper.style.display = "none";
3936
+ }
3937
+ reset() {
3938
+ this.#statusText.textContent = this.#i18n.t("ui.panel.ready");
3939
+ this.#updateStatusIndicator("thinking");
3940
+ this.#renderHistory();
3941
+ this.#collapse();
3942
+ this.#isWaitingForUserAnswer = false;
3943
+ this.#userAnswerResolver = null;
3944
+ this.#showInputArea();
3945
+ }
3946
+ expand() {
3947
+ this.#expand();
3948
+ }
3949
+ collapse() {
3950
+ this.#collapse();
3951
+ }
3952
+ /**
3953
+ * Dispose panel and clean up event listeners
3954
+ */
3955
+ dispose() {
3956
+ this.#agent.removeEventListener("statuschange", this.#onStatusChange);
3957
+ this.#agent.removeEventListener("historychange", this.#onHistoryChange);
3958
+ this.#agent.removeEventListener("activity", this.#onActivity);
3959
+ this.#agent.removeEventListener("dispose", this.#onAgentDispose);
3960
+ this.#isWaitingForUserAnswer = false;
3961
+ this.#stopHeaderUpdateLoop();
3962
+ this.wrapper.remove();
3963
+ }
3964
+ // ========== Private methods ==========
3965
+ #getToolExecutingText(toolName, args) {
3966
+ const a = args;
3967
+ switch (toolName) {
3968
+ case "click_element_by_index":
3969
+ return this.#i18n.t("ui.tools.clicking", { index: a.index });
3970
+ case "input_text":
3971
+ return this.#i18n.t("ui.tools.inputting", { index: a.index });
3972
+ case "select_dropdown_option":
3973
+ return this.#i18n.t("ui.tools.selecting", { text: a.text });
3974
+ case "scroll":
3975
+ return this.#i18n.t("ui.tools.scrolling");
3976
+ case "wait":
3977
+ return this.#i18n.t("ui.tools.waiting", { seconds: a.seconds });
3978
+ case "ask_user":
3979
+ return this.#i18n.t("ui.tools.askingUser");
3980
+ case "done":
3981
+ return this.#i18n.t("ui.tools.done");
3982
+ default:
3983
+ return this.#i18n.t("ui.tools.executing", { toolName });
3984
+ }
3985
+ }
3986
+ /**
3987
+ * Action button handler: stop when running, close (dispose) when idle
3988
+ */
3989
+ #handleActionButton() {
3990
+ if (this.#agent.status === "running") {
3991
+ this.#agent.stop();
3992
+ } else {
3993
+ this.#agent.dispose();
3994
+ }
3995
+ }
3996
+ /**
3997
+ * Submit task
3998
+ */
3999
+ #submitTask() {
4000
+ const input2 = this.#taskInput.value.trim();
4001
+ if (!input2) return;
4002
+ this.#hideInputArea();
4003
+ if (this.#isWaitingForUserAnswer) {
4004
+ this.#handleUserAnswer(input2);
4005
+ } else {
4006
+ this.#agent.execute(input2);
4007
+ }
4008
+ }
4009
+ /**
4010
+ * Handle user answer
4011
+ */
4012
+ #handleUserAnswer(input2) {
4013
+ this.#removeTempCards();
4014
+ this.#isWaitingForUserAnswer = false;
4015
+ if (this.#userAnswerResolver) {
4016
+ this.#userAnswerResolver(input2);
4017
+ this.#userAnswerResolver = null;
4018
+ }
4019
+ }
4020
+ /**
4021
+ * Show input area
4022
+ */
4023
+ #showInputArea(placeholder) {
4024
+ this.#taskInput.value = "";
4025
+ this.#taskInput.placeholder = placeholder || this.#i18n.t("ui.panel.taskInput");
4026
+ this.#inputSection.classList.remove(styles.hidden);
4027
+ setTimeout(() => {
4028
+ this.#taskInput.focus();
4029
+ }, 100);
4030
+ }
4031
+ /**
4032
+ * Hide input area
4033
+ */
4034
+ #hideInputArea() {
4035
+ this.#inputSection.classList.add(styles.hidden);
4036
+ }
4037
+ /**
4038
+ * Check if input area should be shown
4039
+ */
4040
+ #shouldShowInputArea() {
4041
+ if (this.#isWaitingForUserAnswer) return true;
4042
+ const history = this.#agent.history;
4043
+ if (history.length === 0) {
4044
+ return true;
4045
+ }
4046
+ const status = this.#agent.status;
4047
+ const isTaskEnded = status === "completed" || status === "error" || status === "stopped";
4048
+ if (isTaskEnded) {
4049
+ return this.#config.promptForNextTask ?? true;
4050
+ }
4051
+ return false;
4052
+ }
4053
+ #createWrapper() {
4054
+ const taskInputMaxLength = 1e3;
4055
+ const wrapper2 = document.createElement("div");
4056
+ wrapper2.id = "page-agent-runtime_agent-panel";
4057
+ wrapper2.className = styles.wrapper;
4058
+ wrapper2.setAttribute("data-browser-use-ignore", "true");
4059
+ wrapper2.setAttribute("data-page-agent-ignore", "true");
4060
+ wrapper2.setAttribute("role", "complementary");
4061
+ wrapper2.setAttribute("aria-label", this.#i18n.t("ui.panel.panelLabel"));
4062
+ wrapper2.innerHTML = `
4063
+ <div class="${styles.background}"></div>
4064
+ <div class="${styles.historySectionWrapper}">
4065
+ <!-- FORK: role=log + aria-live faz o leitor de tela anunciar cada mensagem nova sem
4066
+ roubar o foco de quem está usando a página. aria-relevant=additions evita
4067
+ reanunciar o histórico inteiro a cada render. -->
4068
+ <div
4069
+ class="${styles.historySection}"
4070
+ role="log"
4071
+ aria-live="polite"
4072
+ aria-relevant="additions"
4073
+ aria-label="${this.#i18n.t("ui.panel.historyLabel")}"
4074
+ >
4075
+ <div class="${styles.historyRow} ${styles.fromSistema}">
4076
+ <div class="${styles.historyItem}" data-mensagem="sistema">
4077
+ <div class="${styles.historyHeader}">
4078
+ <span class="${styles.statusIcon}">${iconThinking}</span>
4079
+ <span class="${styles.historyAuthor}" data-autor="sistema">${this.#i18n.t("ui.panel.authorSystem")}</span>
4080
+ </div>
4081
+ <div class="${styles.historyContent}" data-conteudo><span>${this.#i18n.t("ui.panel.waitingPlaceholder")}</span></div>
4082
+ </div>
4083
+ </div>
4084
+ </div>
4085
+ </div>
4086
+ <div class="${styles.header}">
4087
+ <div class="${styles.statusSection}">
4088
+ <div class="${styles.indicator} ${styles.thinking}"></div>
4089
+ <div class="${styles.statusText}">${this.#i18n.t("ui.panel.ready")}</div>
4090
+ </div>
4091
+ <div class="${styles.controls}">
4092
+ <button class="${styles.controlButton} ${styles.expandButton}" title="${this.#i18n.t("ui.panel.expand")}">
4093
+ ▼
4094
+ </button>
4095
+ <button class="${styles.controlButton} ${styles.stopButton}" title="${this.#i18n.t("ui.panel.close")}">
4096
+ X
4097
+ </button>
4098
+ </div>
4099
+ </div>
4100
+ <div class="${styles.inputSectionWrapper} ${styles.hidden}">
4101
+ <div class="${styles.inputSection}">
4102
+ <input
4103
+ type="text"
4104
+ class="${styles.taskInput}"
4105
+ maxlength="${taskInputMaxLength}"
4106
+ />
4107
+ </div>
4108
+ </div>
4109
+ `;
4110
+ document.body.appendChild(wrapper2);
4111
+ return wrapper2;
4112
+ }
4113
+ #setupEventListeners() {
4114
+ const header2 = this.wrapper.querySelector(`.${styles.header}`);
4115
+ header2.addEventListener("click", (e) => {
4116
+ if (e.target.closest(`.${styles.controlButton}`)) {
4117
+ return;
4118
+ }
4119
+ this.#toggle();
4120
+ });
4121
+ this.#expandButton.addEventListener("click", (e) => {
4122
+ e.stopPropagation();
4123
+ this.#toggle();
4124
+ });
4125
+ this.#actionButton.addEventListener("click", (e) => {
4126
+ e.stopPropagation();
4127
+ this.#handleActionButton();
4128
+ });
4129
+ this.#taskInput.addEventListener("keydown", (e) => {
4130
+ if (e.isComposing) return;
4131
+ if (e.key === "Enter") {
4132
+ e.preventDefault();
4133
+ this.#submitTask();
4134
+ }
4135
+ });
4136
+ this.#inputSection.addEventListener("click", (e) => {
4137
+ e.stopPropagation();
4138
+ });
4139
+ }
4140
+ #toggle() {
4141
+ if (this.#isExpanded) {
4142
+ this.#collapse();
4143
+ } else {
4144
+ this.#expand();
4145
+ }
4146
+ }
4147
+ #expand() {
4148
+ this.#isExpanded = true;
4149
+ this.wrapper.classList.add(styles.expanded);
4150
+ this.#expandButton.textContent = "▲";
4151
+ }
4152
+ #collapse() {
4153
+ this.#isExpanded = false;
4154
+ this.wrapper.classList.remove(styles.expanded);
4155
+ this.#expandButton.textContent = "▼";
4156
+ }
4157
+ /**
4158
+ * Start periodic header update loop
4159
+ */
4160
+ #startHeaderUpdateLoop() {
4161
+ this.#headerUpdateTimer = setInterval(() => {
4162
+ this.#checkAndUpdateHeader();
4163
+ }, 450);
4164
+ }
4165
+ /**
4166
+ * Stop periodic header update loop
4167
+ */
4168
+ #stopHeaderUpdateLoop() {
4169
+ if (this.#headerUpdateTimer) {
4170
+ clearInterval(this.#headerUpdateTimer);
4171
+ this.#headerUpdateTimer = null;
4172
+ }
4173
+ }
4174
+ /**
4175
+ * Check if header needs update and trigger animation if not currently animating
4176
+ */
4177
+ #checkAndUpdateHeader() {
4178
+ if (!this.#pendingHeaderText || this.#isAnimating) {
4179
+ return;
4180
+ }
4181
+ if (this.#statusText.textContent === this.#pendingHeaderText) {
4182
+ this.#pendingHeaderText = null;
4183
+ return;
4184
+ }
4185
+ const textToShow = this.#pendingHeaderText;
4186
+ this.#pendingHeaderText = null;
4187
+ this.#animateTextChange(textToShow);
4188
+ }
4189
+ /**
4190
+ * Animate text change with fade out/in effect
4191
+ */
4192
+ #animateTextChange(newText) {
4193
+ this.#isAnimating = true;
4194
+ this.#statusText.classList.add(styles.fadeOut);
4195
+ setTimeout(() => {
4196
+ this.#statusText.textContent = newText;
4197
+ this.#statusText.classList.remove(styles.fadeOut);
4198
+ this.#statusText.classList.add(styles.fadeIn);
4199
+ setTimeout(() => {
4200
+ this.#statusText.classList.remove(styles.fadeIn);
4201
+ this.#isAnimating = false;
4202
+ }, 300);
4203
+ }, 150);
4204
+ }
4205
+ #updateStatusIndicator(type) {
4206
+ const variant = type === "running" ? "thinking" : type;
4207
+ this.#indicator.className = styles.indicator;
4208
+ if (variant !== "idle" && variant !== "stopped") {
4209
+ this.#indicator.classList.add(styles[variant]);
4210
+ }
4211
+ }
4212
+ #scrollToBottom() {
4213
+ setTimeout(() => {
4214
+ this.#historySection.scrollTop = this.#historySection.scrollHeight;
4215
+ }, 0);
4216
+ }
4217
+ /**
4218
+ * Render history directly from agent.history
4219
+ *
4220
+ * Renders:
4221
+ * 1. Task (first item, from agent.task)
4222
+ * 2. Reflection cards (evaluation, memory, next_goal)
4223
+ * 3. Tool execution with output
4224
+ * 4. Observations
4225
+ */
4226
+ #renderHistory() {
4227
+ const items = [];
4228
+ const task = this.#agent.task;
4229
+ if (task) {
4230
+ items.push(this.#createTaskCard(task));
4231
+ }
4232
+ const history = this.#agent.history;
4233
+ for (const event of history) {
4234
+ items.push(...this.#createHistoryCards(event));
4235
+ }
4236
+ this.#historySection.innerHTML = items.join("");
4237
+ this.#scrollToBottom();
4238
+ }
4239
+ #createTaskCard(task) {
4240
+ return createCard({
4241
+ icon: iconTask,
4242
+ content: task,
4243
+ type: "input",
4244
+ from: "pessoa",
4245
+ author: this.#i18n.t("ui.panel.authorPerson")
4246
+ });
4247
+ }
4248
+ /** Create cards for a history event */
4249
+ #createHistoryCards(event) {
4250
+ const cards = [];
4251
+ const meta = event.type === "step" && event.stepIndex !== void 0 ? this.#i18n.t("ui.panel.step", {
4252
+ number: (event.stepIndex + 1).toString()
4253
+ }) : void 0;
4254
+ if (event.type === "step") {
4255
+ if (event.reflection) {
4256
+ const lines = createReflectionLines(event.reflection, {
4257
+ evaluation: this.#i18n.t("ui.panel.reflectionEvaluation"),
4258
+ memory: this.#i18n.t("ui.panel.reflectionMemory"),
4259
+ nextGoal: this.#i18n.t("ui.panel.reflectionNextGoal")
4260
+ });
4261
+ if (lines.length > 0) {
4262
+ cards.push(
4263
+ createCard({
4264
+ icon: iconThinking,
4265
+ content: lines,
4266
+ meta,
4267
+ author: this.#i18n.t("ui.panel.authorReasoning")
4268
+ })
4269
+ );
4270
+ }
4271
+ }
4272
+ const action = event.action;
4273
+ if (action) {
4274
+ cards.push(...this.#createActionCards(action, meta));
4275
+ }
4276
+ } else if (event.type === "observation") {
4277
+ cards.push(
4278
+ createCard({
4279
+ icon: iconObservation,
4280
+ content: event.content || "",
4281
+ meta,
4282
+ type: "observation",
4283
+ author: this.#i18n.t("ui.panel.authorPage")
4284
+ })
4285
+ );
4286
+ } else if (event.type === "user_takeover") {
4287
+ cards.push(
4288
+ createCard({
4289
+ icon: iconUser,
4290
+ content: this.#i18n.t("ui.panel.userTakeover"),
4291
+ meta,
4292
+ type: "input",
4293
+ from: "pessoa",
4294
+ author: this.#i18n.t("ui.panel.authorPerson")
4295
+ })
4296
+ );
4297
+ } else if (event.type === "retry") {
4298
+ const retryInfo = `${event.message || this.#i18n.t("ui.panel.retrying")} (${event.attempt}/${event.maxAttempts})`;
4299
+ cards.push(
4300
+ createCard({
4301
+ icon: iconRetry,
4302
+ content: retryInfo,
4303
+ meta,
4304
+ type: "observation",
4305
+ from: "sistema",
4306
+ author: this.#i18n.t("ui.panel.authorSystem")
4307
+ })
4308
+ );
4309
+ } else if (event.type === "error") {
4310
+ cards.push(
4311
+ createCard({
4312
+ icon: iconError,
4313
+ content: event.message || this.#i18n.t("ui.panel.errorLabel"),
4314
+ meta,
4315
+ type: "observation",
4316
+ from: "sistema",
4317
+ author: this.#i18n.t("ui.panel.authorSystem")
4318
+ })
4319
+ );
4320
+ }
4321
+ return cards;
4322
+ }
4323
+ /** Create cards for an action */
4324
+ #createActionCards(action, meta) {
4325
+ const cards = [];
4326
+ if (action.name === "done") {
4327
+ const input2 = action.input;
4328
+ const text = input2.text || action.output || "";
4329
+ if (text) {
4330
+ cards.push(
4331
+ createCard({
4332
+ icon: iconDone,
4333
+ content: text,
4334
+ meta,
4335
+ type: "output",
4336
+ author: this.#i18n.t("ui.panel.authorAgent")
4337
+ })
4338
+ );
4339
+ }
4340
+ } else if (action.name === "ask_user") {
4341
+ const input2 = action.input;
4342
+ const answer = action.output.replace(/^User answered:\s*/i, "");
4343
+ cards.push(
4344
+ createCard({
4345
+ icon: iconQuestion,
4346
+ content: this.#i18n.t("ui.panel.question", { question: input2.question || "" }),
4347
+ meta,
4348
+ type: "question",
4349
+ author: this.#i18n.t("ui.panel.authorAgent")
4350
+ })
4351
+ );
4352
+ cards.push(
4353
+ createCard({
4354
+ icon: iconAnswer,
4355
+ content: this.#i18n.t("ui.panel.userAnswer", { input: answer }),
4356
+ meta,
4357
+ type: "input",
4358
+ from: "pessoa",
4359
+ author: this.#i18n.t("ui.panel.authorPerson")
4360
+ })
4361
+ );
4362
+ } else {
4363
+ const toolText = this.#getToolExecutingText(action.name, action.input);
4364
+ cards.push(
4365
+ createCard({ icon: iconTool, content: toolText, meta, author: this.#i18n.t("ui.panel.authorAction") })
4366
+ );
4367
+ if (action.output?.length > 0) {
4368
+ cards.push(
4369
+ createCard({
4370
+ icon: iconTool,
4371
+ content: action.output,
4372
+ meta,
4373
+ type: "output",
4374
+ author: this.#i18n.t("ui.panel.authorResult")
4375
+ })
4376
+ );
4377
+ }
4378
+ }
4379
+ return cards;
4380
+ }
4381
+ }
4382
+ class PageAgent extends PageAgentCore {
4383
+ panel;
4384
+ constructor(config) {
4385
+ const pageController = new PageController({
4386
+ ...config,
4387
+ enableMask: config.enableMask ?? true
4388
+ });
4389
+ super({ ...config, pageController });
4390
+ this.panel = new Panel(this, {
4391
+ language: config.language,
4392
+ promptForNextTask: config.promptForNextTask
4393
+ });
4394
+ }
4395
+ }
4396
+ function getRecognitionCtor() {
4397
+ const w = globalThis;
4398
+ return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
4399
+ }
4400
+ const STATUS_LISTENING = "listening";
4401
+ const STATUS_SPEAKING = "speaking";
4402
+ const STATUS_IDLE = "idle";
4403
+ function createVoice(options = {}) {
4404
+ const lang = options.lang ?? "pt-BR";
4405
+ const rate = options.rate ?? 1;
4406
+ const pitch = options.pitch ?? 1;
4407
+ const inputEnabled = options.input ?? true;
4408
+ const outputEnabled = options.output ?? false;
4409
+ const listenTimeoutMs = options.listenTimeoutMs ?? 3e4;
4410
+ const Recognition = inputEnabled ? getRecognitionCtor() : null;
4411
+ const synth = typeof window !== "undefined" ? window.speechSynthesis : void 0;
4412
+ const supported = {
4413
+ input: Boolean(inputEnabled && Recognition),
4414
+ output: Boolean(outputEnabled && synth)
4415
+ };
4416
+ let status = STATUS_IDLE;
4417
+ const statusListeners = /* @__PURE__ */ new Set();
4418
+ function setStatus(next) {
4419
+ if (status === next) return;
4420
+ status = next;
4421
+ statusListeners.forEach((cb) => {
4422
+ try {
4423
+ cb(next);
4424
+ } catch {
4425
+ }
4426
+ });
4427
+ }
4428
+ let activeRecognition = null;
4429
+ let currentVoice = null;
4430
+ if (synth) {
4431
+ const pickVoice = () => {
4432
+ if (options.voiceURI) {
4433
+ currentVoice = synth.getVoices().find((v) => v.voiceURI === options.voiceURI) ?? null;
4434
+ } else {
4435
+ const voices = synth.getVoices();
4436
+ const prefix = lang.split("-")[0].toLowerCase();
4437
+ currentVoice = voices.find((v) => v.lang?.toLowerCase() === lang.toLowerCase()) ?? voices.find((v) => v.lang?.toLowerCase().startsWith(prefix)) ?? null;
4438
+ }
4439
+ };
4440
+ pickVoice();
4441
+ synth.onvoiceschanged = pickVoice;
4442
+ }
4443
+ function speak(text, opts) {
4444
+ if (!synth || !outputEnabled) return;
4445
+ const clean = (text ?? "").trim();
4446
+ if (!clean) return;
4447
+ if (opts?.interrupt !== false) {
4448
+ synth.cancel();
4449
+ }
4450
+ const utterance = new SpeechSynthesisUtterance(clean);
4451
+ utterance.lang = lang;
4452
+ utterance.rate = rate;
4453
+ utterance.pitch = pitch;
4454
+ if (currentVoice) utterance.voice = currentVoice;
4455
+ utterance.onend = () => setStatus(STATUS_IDLE);
4456
+ utterance.onerror = () => setStatus(STATUS_IDLE);
4457
+ setStatus(STATUS_SPEAKING);
4458
+ synth.speak(utterance);
4459
+ }
4460
+ function listen() {
4461
+ if (!Recognition) {
4462
+ return Promise.reject(new Error("Voz de entrada não suportada neste navegador"));
4463
+ }
4464
+ return new Promise((resolve, reject) => {
4465
+ const recognition = new Recognition();
4466
+ activeRecognition = recognition;
4467
+ let finalText = "";
4468
+ let settled = false;
4469
+ let timer;
4470
+ const finish = (fn) => {
4471
+ if (settled) return;
4472
+ settled = true;
4473
+ if (timer) clearTimeout(timer);
4474
+ if (activeRecognition === recognition) activeRecognition = null;
4475
+ setStatus(STATUS_IDLE);
4476
+ fn();
4477
+ };
4478
+ timer = setTimeout(() => {
4479
+ try {
4480
+ recognition.stop();
4481
+ } catch {
4482
+ }
4483
+ finish(() => reject(new Error("Tempo de escuta esgotado")));
4484
+ }, listenTimeoutMs);
4485
+ recognition.lang = lang;
4486
+ recognition.continuous = false;
4487
+ recognition.interimResults = true;
4488
+ recognition.maxAlternatives = 1;
4489
+ recognition.onstart = () => setStatus(STATUS_LISTENING);
4490
+ recognition.onresult = (event) => {
4491
+ for (let i = event.resultIndex; i < event.results.length; i++) {
4492
+ const result2 = event.results[i];
4493
+ if (result2?.isFinal) finalText = result2[0]?.transcript ?? finalText;
4494
+ }
4495
+ };
4496
+ recognition.onerror = (event) => {
4497
+ const code = event?.error;
4498
+ if (code === "no-speech" || code === "aborted") {
4499
+ finish(() => resolve(finalText.trim()));
4500
+ return;
4501
+ }
4502
+ finish(() => reject(new Error(`Erro de reconhecimento de voz: ${code ?? "desconhecido"}`)));
4503
+ };
4504
+ recognition.onend = () => {
4505
+ finish(() => resolve(finalText.trim()));
4506
+ };
4507
+ try {
4508
+ recognition.start();
4509
+ } catch (error2) {
4510
+ finish(() => reject(error2 instanceof Error ? error2 : new Error(String(error2))));
4511
+ }
4512
+ });
4513
+ }
4514
+ return {
4515
+ supported,
4516
+ listen,
4517
+ stopListening() {
4518
+ try {
4519
+ activeRecognition?.stop();
4520
+ } catch {
4521
+ }
4522
+ activeRecognition = null;
4523
+ setStatus(STATUS_IDLE);
4524
+ },
4525
+ speak,
4526
+ stopSpeaking() {
4527
+ if (!synth) return;
4528
+ synth.cancel();
4529
+ setStatus(STATUS_IDLE);
4530
+ },
4531
+ onStatus(cb) {
4532
+ statusListeners.add(cb);
4533
+ return () => statusListeners.delete(cb);
4534
+ }
4535
+ };
4536
+ }
4537
+ const DEFAULTS = {
4538
+ language: "pt-BR",
4539
+ maxSteps: 30,
4540
+ enableMask: true,
4541
+ viewportExpansion: 0,
4542
+ promptForNextTask: true,
4543
+ position: "bottom-right",
4544
+ launcherLabel: "Abrir assistente da página",
4545
+ open: false
4546
+ };
4547
+ const LAUNCHER_ICON = '<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l1.9 4.6L18.5 9l-4.6 1.9L12 15.5l-1.9-4.6L5.5 9l4.6-1.4L12 3z"/><path d="M19 14l.8 2 2 .8-2 .8-.8 2-.8-2-2-.8 2-.8.8-2z"/></svg>';
4548
+ const MIC_ICON = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="3" width="6" height="11" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3"/></svg>';
4549
+ function positionCss(position) {
4550
+ switch (position) {
4551
+ case "bottom-left":
4552
+ return { bottom: "20px", left: "20px", right: "auto", top: "auto" };
4553
+ case "top-right":
4554
+ return { top: "20px", right: "20px", bottom: "auto", left: "auto" };
4555
+ case "top-left":
4556
+ return { top: "20px", left: "20px", bottom: "auto", right: "auto" };
4557
+ default:
4558
+ return { bottom: "20px", right: "20px", top: "auto", left: "auto" };
4559
+ }
4560
+ }
4561
+ function logHookError(name, error2) {
4562
+ console.error(`[page-agent-widget] gancho ${name} falhou (a tarefa segue):`, error2);
4563
+ }
4564
+ function mergeExtensions(host, extensions) {
4565
+ const customTools = { ...host.customTools };
4566
+ for (const extension of extensions) Object.assign(customTools, extension.customTools);
4567
+ const system = [host.instructions?.system, ...extensions.map((e) => e.instructions?.system)].map((text) => text?.trim()).filter((text) => Boolean(text)).join("\n\n");
4568
+ const pageProviders = [host.instructions?.getPageInstructions, ...extensions.map((e) => e.instructions?.getPageInstructions)].filter(
4569
+ (fn) => typeof fn === "function"
4570
+ );
4571
+ const getPageInstructions = pageProviders.length ? (url) => {
4572
+ const parts = [];
4573
+ for (const provide of pageProviders) {
4574
+ try {
4575
+ const text = provide(url)?.trim();
4576
+ if (text) parts.push(text);
4577
+ } catch (error2) {
4578
+ logHookError("getPageInstructions", error2);
4579
+ }
4580
+ }
4581
+ return parts.join("\n\n") || void 0;
4582
+ } : void 0;
4583
+ const sources = [host.hooks, ...extensions.map((e) => e.hooks)].filter((h) => Boolean(h));
4584
+ const chain = (name) => {
4585
+ const fns = sources.map((source) => source[name]).filter((fn) => typeof fn === "function");
4586
+ if (!fns.length) return void 0;
4587
+ return (async (...args) => {
4588
+ for (const fn of fns) {
4589
+ try {
4590
+ await fn(...args);
4591
+ } catch (error2) {
4592
+ logHookError(String(name), error2);
4593
+ }
4594
+ }
4595
+ });
4596
+ };
4597
+ const transforms = sources.map((source) => source.transformPageContent).filter((fn) => typeof fn === "function");
4598
+ const transformPageContent = transforms.length ? async (content) => {
4599
+ let current = content;
4600
+ for (const transform of transforms) {
4601
+ try {
4602
+ current = await transform(current);
4603
+ } catch (error2) {
4604
+ logHookError("transformPageContent", error2);
4605
+ }
4606
+ }
4607
+ return current;
4608
+ } : void 0;
4609
+ return {
4610
+ customTools,
4611
+ instructions: system || getPageInstructions ? { ...system ? { system } : {}, ...getPageInstructions ? { getPageInstructions } : {} } : void 0,
4612
+ onBeforeStep: chain("onBeforeStep"),
4613
+ onAfterStep: chain("onAfterStep"),
4614
+ onBeforeTask: chain("onBeforeTask"),
4615
+ onAfterTask: chain("onAfterTask"),
4616
+ onDispose: chain("onDispose"),
4617
+ transformPageContent
4618
+ };
4619
+ }
4620
+ function mountWidget$1(options = {}) {
4621
+ const position = options.position ?? DEFAULTS.position;
4622
+ const launcherLabel = options.launcherLabel ?? DEFAULTS.launcherLabel;
4623
+ const open = options.open ?? DEFAULTS.open;
4624
+ let agent = null;
4625
+ let voice = null;
4626
+ let micButton = null;
4627
+ let listening = false;
4628
+ let configPromise = null;
4629
+ async function ensureConfig() {
4630
+ if (configPromise) return configPromise;
4631
+ if (!options.configUrl) return;
4632
+ configPromise = (async () => {
4633
+ const res = await fetch(options.configUrl, { headers: { accept: "application/json" } });
4634
+ if (!res.ok) throw new Error(`mountWidget: configUrl respondeu ${res.status} — o gateway está no ar?`);
4635
+ const server = await res.json();
4636
+ for (const key of ["model", "language", "maxSteps", "promptForNextTask", "instructions", "voice", "brain", "debug"]) {
4637
+ const value = server[key];
4638
+ if (value !== void 0 && value !== null) {
4639
+ options[key] = value;
4640
+ }
4641
+ }
4642
+ })();
4643
+ return configPromise;
4644
+ }
4645
+ function ensureVoice() {
4646
+ if (!voice) voice = createVoice(options.voice ?? {});
4647
+ return voice;
4648
+ }
4649
+ async function ensureAgent() {
4650
+ if (agent && !agent.disposed) return agent;
4651
+ await ensureConfig();
4652
+ const v = ensureVoice();
4653
+ const model = options.model;
4654
+ const baseURL = options.baseURL;
4655
+ if (!model || !baseURL) {
4656
+ throw new Error(
4657
+ options.configUrl ? "mountWidget: o gateway não enviou `model` e/ou falta `baseURL` no front. Confira AGENT_MODEL no gateway e a baseURL do widget." : "mountWidget: configure `model` e `baseURL` (config central do LLM) antes de abrir o agente."
4658
+ );
4659
+ }
4660
+ const resolved = options.resolveExtensions ? await options.resolveExtensions(options) : [];
4661
+ const merged = mergeExtensions(options, [...options.extensions ?? [], ...resolved]);
4662
+ agent = new PageAgent({
4663
+ model,
4664
+ baseURL,
4665
+ apiKey: options.apiKey,
4666
+ transformRequestBody: options.transformRequestBody,
4667
+ customFetch: options.customFetch,
4668
+ maxRetries: options.maxRetries,
4669
+ language: options.language ?? DEFAULTS.language,
4670
+ maxSteps: options.maxSteps ?? DEFAULTS.maxSteps,
4671
+ enableMask: options.enableMask ?? DEFAULTS.enableMask,
4672
+ viewportExpansion: options.viewportExpansion ?? DEFAULTS.viewportExpansion,
4673
+ instructions: merged.instructions,
4674
+ customTools: merged.customTools,
4675
+ onBeforeStep: merged.onBeforeStep,
4676
+ onAfterStep: merged.onAfterStep,
4677
+ onBeforeTask: merged.onBeforeTask,
4678
+ onAfterTask: merged.onAfterTask,
4679
+ onDispose: merged.onDispose,
4680
+ transformPageContent: merged.transformPageContent,
4681
+ promptForNextTask: options.promptForNextTask ?? DEFAULTS.promptForNextTask
4682
+ });
4683
+ wireVoiceOutput(agent, v);
4684
+ injectMicButton(agent, v);
4685
+ return agent;
4686
+ }
4687
+ function wireVoiceOutput(a, v) {
4688
+ const speakOnComplete = options.voice?.speakOnComplete ?? true;
4689
+ const speakSteps = options.voice?.speakSteps ?? false;
4690
+ const onStatus = () => {
4691
+ const status = a.status;
4692
+ const result2 = a.lastResult;
4693
+ if (status === "completed" || status === "error") {
4694
+ if (speakOnComplete && v.supported.output) {
4695
+ if (status === "error") {
4696
+ v.speak("A tarefa encontrou um problema e foi parada.");
4697
+ } else if (result2?.success) {
4698
+ v.speak("Tarefa concluída com sucesso.");
4699
+ } else {
4700
+ v.speak("A tarefa terminou, mas o resultado não foi confirmado.");
4701
+ }
4702
+ }
4703
+ return;
4704
+ }
4705
+ if (speakSteps && status === "running" && v.supported.output) {
4706
+ const lastStep = [...a.history].reverse().find((h) => h.type === "step");
4707
+ const goal = lastStep?.reflection?.next_goal;
4708
+ if (goal) v.speak(goal, { interrupt: true });
4709
+ }
4710
+ };
4711
+ a.addEventListener("statuschange", onStatus);
4712
+ a.addEventListener("activity", (event) => {
4713
+ const detail = event.detail;
4714
+ if (detail?.type === "error" && detail.message && v.supported.output) {
4715
+ v.speak(detail.message, { interrupt: true });
4716
+ }
4717
+ });
4718
+ }
4719
+ function injectMicButton(a, v) {
4720
+ const wrapper2 = a.panel.wrapper;
4721
+ const input2 = wrapper2.querySelector("input");
4722
+ if (!input2 || !v.supported.input) return;
4723
+ if (wrapper2.querySelector('[data-page-agent-voice="mic"]')) return;
4724
+ if (micButton) micButton.remove();
4725
+ micButton = document.createElement("button");
4726
+ micButton.type = "button";
4727
+ micButton.className = "page-agent-voice-mic";
4728
+ micButton.setAttribute("data-page-agent-voice", "mic");
4729
+ micButton.setAttribute("aria-label", "Ditar tarefa por voz");
4730
+ micButton.title = "Ditar tarefa por voz";
4731
+ micButton.innerHTML = MIC_ICON;
4732
+ micButton.style.display = "inline-flex";
4733
+ const host = input2.parentElement ?? wrapper2;
4734
+ host.insertBefore(micButton, input2);
4735
+ v.onStatus((s) => {
4736
+ micButton?.classList.toggle("is-listening", s === "listening");
4737
+ });
4738
+ micButton.addEventListener("click", async () => {
4739
+ if (listening) {
4740
+ v.stopListening();
4741
+ return;
4742
+ }
4743
+ listening = true;
4744
+ try {
4745
+ const transcript = await v.listen();
4746
+ if (transcript) {
4747
+ input2.value = transcript;
4748
+ input2.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true }));
4749
+ }
4750
+ } catch (error2) {
4751
+ input2.setAttribute("placeholder", error2 instanceof Error ? error2.message : "Não foi possível ouvir; digite a tarefa.");
4752
+ setTimeout(() => input2.removeAttribute("placeholder"), 6e3);
4753
+ } finally {
4754
+ listening = false;
4755
+ }
4756
+ });
4757
+ }
4758
+ const launcher = document.createElement("button");
4759
+ launcher.type = "button";
4760
+ launcher.className = "page-agent-widget-launcher";
4761
+ launcher.setAttribute("aria-label", launcherLabel);
4762
+ launcher.title = launcherLabel;
4763
+ launcher.innerHTML = LAUNCHER_ICON;
4764
+ const pos = positionCss(position);
4765
+ Object.assign(launcher.style, pos);
4766
+ let panelVisible = false;
4767
+ async function openPanel(silent = false) {
4768
+ try {
4769
+ const a = await ensureAgent();
4770
+ a.panel.show();
4771
+ panelVisible = true;
4772
+ } catch (error2) {
4773
+ if (silent) console.error("[page-agent-widget] não abriu o agente:", error2);
4774
+ else throw error2;
4775
+ }
4776
+ }
4777
+ function closePanel() {
4778
+ agent?.panel.hide?.();
4779
+ panelVisible = false;
4780
+ }
4781
+ launcher.addEventListener("click", () => {
4782
+ if (panelVisible) {
4783
+ closePanel();
4784
+ } else {
4785
+ void openPanel(true);
4786
+ }
4787
+ });
4788
+ document.body.appendChild(launcher);
4789
+ if (open) {
4790
+ void openPanel(true);
4791
+ }
4792
+ const widget = {
4793
+ get agent() {
4794
+ return agent;
4795
+ },
4796
+ get voice() {
4797
+ return voice;
4798
+ },
4799
+ launcher,
4800
+ ready() {
4801
+ return ensureAgent();
4802
+ },
4803
+ async run(task) {
4804
+ const a = await ensureAgent();
4805
+ return await a.execute(task);
4806
+ },
4807
+ async stop() {
4808
+ if (agent) await agent.stop();
4809
+ },
4810
+ show() {
4811
+ return openPanel(false);
4812
+ },
4813
+ hide() {
4814
+ closePanel();
4815
+ },
4816
+ toggle() {
4817
+ launcher.click();
4818
+ },
4819
+ setConfig(patch) {
4820
+ Object.assign(options, patch);
4821
+ if (agent) {
4822
+ agent.dispose();
4823
+ agent = null;
4824
+ }
4825
+ if (panelVisible) {
4826
+ panelVisible = false;
4827
+ void openPanel(true);
4828
+ }
4829
+ },
4830
+ unmount() {
4831
+ voice?.stopSpeaking();
4832
+ voice?.stopListening();
4833
+ agent?.dispose();
4834
+ agent = null;
4835
+ voice = null;
4836
+ micButton?.remove();
4837
+ micButton = null;
4838
+ launcher.remove();
4839
+ }
4840
+ };
4841
+ return widget;
4842
+ }
4843
+ function mountWidget(options = {}) {
4844
+ const { brain: brainOption, ...rest } = options;
4845
+ const previous = rest.resolveExtensions;
4846
+ return mountWidget$1({
4847
+ ...rest,
4848
+ brain: Boolean(brainOption),
4849
+ resolveExtensions: async (resolved) => {
4850
+ const extensions = previous ? [...await previous(resolved)] : [];
4851
+ if (resolved.brain) {
4852
+ const { createBrainExtension } = await import("./brain-extension-DDuKTYrJ.js").then((n) => n.d);
4853
+ extensions.push(createBrainExtension(typeof brainOption === "object" ? brainOption : {}));
4854
+ }
4855
+ if (resolved.debug) {
4856
+ const { createDebugExtension } = await import("./debug-extension-D0P_sNkc.js");
4857
+ extensions.push(createDebugExtension());
4858
+ }
4859
+ return extensions;
4860
+ }
4861
+ });
4862
+ }
4863
+ export {
4864
+ PageAgent,
4865
+ PageAgentCore,
4866
+ createVoice,
4867
+ mountWidget,
4868
+ tool
4869
+ };
4870
+ //# sourceMappingURL=page-agent.js.map