hunter-mate 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/assets/extension/background/{service-worker.20261002143654.js → service-worker.20261015103433.js} +868 -142
  2. package/dist/assets/extension/background/service-worker.js +868 -142
  3. package/dist/assets/extension/build-info.json +1 -1
  4. package/dist/assets/extension/content/control-runtime.js +1028 -0
  5. package/dist/assets/extension/content/index.js +37 -2
  6. package/dist/assets/extension/manifest.json +4 -3
  7. package/dist/assets/extension/offscreen/offscreen.html +1 -1
  8. package/dist/assets/extension/offscreen/offscreen.js +2 -2
  9. package/dist/assets/extension/package.json +1 -1
  10. package/dist/assets/extension/sidepanel/index.css +1 -1
  11. package/dist/assets/extension/sidepanel/index.html +1 -1
  12. package/dist/assets/extension/sidepanel/index.js +23 -14
  13. package/dist/assets/server/chunks/{chunk-IXYSL3NT.mjs → chunk-BLXHCSZD.mjs} +8 -10
  14. package/dist/assets/server/chunks/{script-rule-ANR2J7E6.mjs → script-rule-ZJN6CEYX.mjs} +1 -1
  15. package/dist/assets/server/main.mjs +11292 -4848
  16. package/dist/index.js +1075 -365
  17. package/dist/web/dist/404.html +1 -1
  18. package/dist/web/dist/_next/static/2fPtjfAzNbYkpIoAQOsP3/_buildManifest.js +1 -0
  19. package/dist/web/dist/_next/static/{N_4Eb07x7RNgd3O5ubXrJ → 2fPtjfAzNbYkpIoAQOsP3}/_ssgManifest.js +1 -1
  20. package/dist/web/dist/_next/static/chunks/105-dcd8f6d5867c8995.js +32 -0
  21. package/dist/web/dist/_next/static/chunks/166.b263e636ff66212f.js +1 -0
  22. package/dist/web/dist/_next/static/chunks/1e6d5a0c.75afa2d319272f42.js +15 -0
  23. package/dist/web/dist/_next/static/chunks/555-e2a871b9d2eccb03.js +12 -0
  24. package/dist/web/dist/_next/static/chunks/589-f984bff035a1cb85.js +1 -0
  25. package/dist/web/dist/_next/static/chunks/671.ae431f885d6c7f0c.js +1 -0
  26. package/dist/web/dist/_next/static/chunks/686-9f419b70feea9470.js +1 -0
  27. package/dist/web/dist/_next/static/chunks/852-248884963a986600.js +1 -0
  28. package/dist/web/dist/_next/static/chunks/865-0ed23dd43c0832a2.js +1 -0
  29. package/dist/web/dist/_next/static/chunks/935-7be3e60dd8bc29d2.js +5 -0
  30. package/dist/web/dist/_next/static/chunks/954-fb0212fe71f60a52.js +1 -0
  31. package/dist/web/dist/_next/static/chunks/a7067448-e416655baf443992.js +1 -0
  32. package/dist/web/dist/_next/static/chunks/app/_not-found/page-5b7249eb3c9a6cb9.js +1 -0
  33. package/dist/web/dist/_next/static/chunks/app/admin/data/page-14698e9c4ddaa6d9.js +1 -0
  34. package/dist/web/dist/_next/static/chunks/app/auth/verify/page-05304d5803008d61.js +1 -0
  35. package/dist/web/dist/_next/static/chunks/app/companies/page-ec27ce9ab3610221.js +1 -0
  36. package/dist/web/dist/_next/static/chunks/app/company/admin/page-c2584183bc4b34d1.js +1 -0
  37. package/dist/web/dist/_next/static/chunks/app/entity/page-08755f96102f0565.js +1 -0
  38. package/dist/web/dist/_next/static/chunks/app/jobs/page-4175a90f1b42f87d.js +1 -0
  39. package/dist/web/dist/_next/static/chunks/app/layout-af39afc7866e74db.js +1 -0
  40. package/dist/web/dist/_next/static/chunks/app/logs/page-be50a4e10b1a0024.js +1 -0
  41. package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-cf445b0cb8c2cb0a.js +1 -0
  42. package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-c7ff0d79b6a75915.js +1 -0
  43. package/dist/web/dist/_next/static/chunks/app/page-bd282e1e5f0965f1.js +1 -0
  44. package/dist/web/dist/_next/static/chunks/app/parselog/page-9814e75d6900a743.js +1 -0
  45. package/dist/web/dist/_next/static/chunks/app/recordings/page-441016638b43c4f8.js +1 -0
  46. package/dist/web/dist/_next/static/chunks/app/resource-usage/page-f6fa06bfe21bcd32.js +1 -0
  47. package/dist/web/dist/_next/static/chunks/app/shared/[token]/page-c9c5042a2d07c0bc.js +1 -0
  48. package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-f54cf2f0058035ca.js +1 -0
  49. package/dist/web/dist/_next/static/chunks/app/talents/page-c0a0c250eb5f5459.js +1 -0
  50. package/dist/web/dist/_next/static/chunks/app/team/insights/page-8b3934df3e292368.js +1 -0
  51. package/dist/web/dist/_next/static/chunks/framework-8aa939434277315a.js +1 -0
  52. package/dist/web/dist/_next/static/chunks/main-8a2c75d6c43385dd.js +1 -0
  53. package/dist/web/dist/_next/static/chunks/main-app-f496bd51a73ef9f3.js +1 -0
  54. package/dist/web/dist/_next/static/chunks/pages/_app-037e1cd81d653e68.js +4 -0
  55. package/dist/web/dist/_next/static/chunks/pages/_error-59095d3b83385d1f.js +1 -0
  56. package/dist/web/dist/_next/static/chunks/{webpack-cf21e951181fd345.js → webpack-54e74ccc9e9451ee.js} +1 -1
  57. package/dist/web/dist/_next/static/css/bd072fee69ae8474.css +3 -0
  58. package/dist/web/dist/admin/data.html +1 -1
  59. package/dist/web/dist/admin/data.txt +8 -6
  60. package/dist/web/dist/auth/verify.html +1 -1
  61. package/dist/web/dist/auth/verify.txt +8 -6
  62. package/dist/web/dist/companies.html +1 -1
  63. package/dist/web/dist/companies.txt +9 -6
  64. package/dist/web/dist/company/admin.html +1 -1
  65. package/dist/web/dist/company/admin.txt +8 -6
  66. package/dist/web/dist/entity.html +1 -1
  67. package/dist/web/dist/entity.txt +9 -7
  68. package/dist/web/dist/index.html +1 -1
  69. package/dist/web/dist/index.txt +9 -7
  70. package/dist/web/dist/jobs.html +1 -1
  71. package/dist/web/dist/jobs.txt +9 -7
  72. package/dist/web/dist/logs.html +1 -1
  73. package/dist/web/dist/logs.txt +9 -7
  74. package/dist/web/dist/matching/jobs/placeholder/talents.html +1 -1
  75. package/dist/web/dist/matching/jobs/placeholder/talents.txt +8 -6
  76. package/dist/web/dist/matching/portraits/placeholder/jobs.html +1 -1
  77. package/dist/web/dist/matching/portraits/placeholder/jobs.txt +8 -6
  78. package/dist/web/dist/parselog.html +1 -1
  79. package/dist/web/dist/parselog.txt +9 -7
  80. package/dist/web/dist/recordings.html +1 -1
  81. package/dist/web/dist/recordings.txt +9 -7
  82. package/dist/web/dist/resource-usage.html +1 -1
  83. package/dist/web/dist/resource-usage.txt +9 -7
  84. package/dist/web/dist/shared/placeholder.html +1 -0
  85. package/dist/web/dist/shared/placeholder.txt +10 -0
  86. package/dist/web/dist/snapshots/placeholder/agent-prompt.html +1 -1
  87. package/dist/web/dist/snapshots/placeholder/agent-prompt.txt +8 -6
  88. package/dist/web/dist/talents.html +1 -1
  89. package/dist/web/dist/talents.txt +8 -6
  90. package/dist/web/dist/team/insights.html +1 -0
  91. package/dist/web/dist/team/insights.txt +9 -0
  92. package/package.json +1 -1
  93. package/dist/web/dist/_next/static/N_4Eb07x7RNgd3O5ubXrJ/_buildManifest.js +0 -1
  94. package/dist/web/dist/_next/static/chunks/166.f30be223f694f8ee.js +0 -1
  95. package/dist/web/dist/_next/static/chunks/170-b66b6f731381943e.js +0 -1
  96. package/dist/web/dist/_next/static/chunks/1e6d5a0c.0488932e17ecdf24.js +0 -15
  97. package/dist/web/dist/_next/static/chunks/589-5e99e548a0ee6bac.js +0 -1
  98. package/dist/web/dist/_next/static/chunks/671.3b8e9cc96b25b6ea.js +0 -1
  99. package/dist/web/dist/_next/static/chunks/686-ade57c57a1737818.js +0 -1
  100. package/dist/web/dist/_next/static/chunks/935-bb4adb3868af4cd4.js +0 -5
  101. package/dist/web/dist/_next/static/chunks/a7067448-d3882c7e74c162bf.js +0 -1
  102. package/dist/web/dist/_next/static/chunks/app/_not-found/page-cf5ab201496f0914.js +0 -1
  103. package/dist/web/dist/_next/static/chunks/app/admin/data/page-37e01dad70306bf3.js +0 -1
  104. package/dist/web/dist/_next/static/chunks/app/auth/verify/page-38fa28a48f76defa.js +0 -1
  105. package/dist/web/dist/_next/static/chunks/app/companies/page-985002eea5b295d0.js +0 -1
  106. package/dist/web/dist/_next/static/chunks/app/company/admin/page-8c7ecc92bef09e92.js +0 -1
  107. package/dist/web/dist/_next/static/chunks/app/entity/page-dc9664fb628e2d08.js +0 -1
  108. package/dist/web/dist/_next/static/chunks/app/jobs/page-e39b3e25f0a81254.js +0 -1
  109. package/dist/web/dist/_next/static/chunks/app/layout-d094dc654772bf32.js +0 -1
  110. package/dist/web/dist/_next/static/chunks/app/logs/page-ccc6bb863cf77e56.js +0 -1
  111. package/dist/web/dist/_next/static/chunks/app/matching/jobs/[id]/talents/page-936f6190c968d20b.js +0 -1
  112. package/dist/web/dist/_next/static/chunks/app/matching/portraits/[id]/jobs/page-ea44a2236bd53cae.js +0 -1
  113. package/dist/web/dist/_next/static/chunks/app/page-41d8e008a9411221.js +0 -1
  114. package/dist/web/dist/_next/static/chunks/app/parselog/page-3f756bbd580f337e.js +0 -1
  115. package/dist/web/dist/_next/static/chunks/app/recordings/page-ffd507b12e67f2db.js +0 -1
  116. package/dist/web/dist/_next/static/chunks/app/resource-usage/page-3b8bd27536a7889a.js +0 -1
  117. package/dist/web/dist/_next/static/chunks/app/snapshots/[id]/agent-prompt/page-c89b32aa2ef509f5.js +0 -1
  118. package/dist/web/dist/_next/static/chunks/app/talents/page-8e0b4072df138fa3.js +0 -1
  119. package/dist/web/dist/_next/static/chunks/framework-b3a558b17a070965.js +0 -1
  120. package/dist/web/dist/_next/static/chunks/main-app-7e48cd6f6a954fec.js +0 -1
  121. package/dist/web/dist/_next/static/chunks/main-ead3ac50aa4bc931.js +0 -1
  122. package/dist/web/dist/_next/static/chunks/pages/_app-97da3522926feed4.js +0 -4
  123. package/dist/web/dist/_next/static/chunks/pages/_error-17756f67e0a11350.js +0 -1
  124. package/dist/web/dist/_next/static/css/1dd88e02ef1af7b9.css +0 -3
@@ -1,3 +1,325 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __commonJS = (cb, mod) => function __require() {
11
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
12
+ };
13
+ var __export = (target, all) => {
14
+ for (var name in all)
15
+ __defProp(target, name, { get: all[name], enumerable: true });
16
+ };
17
+ var __copyProps = (to, from, except, desc) => {
18
+ if (from && typeof from === "object" || typeof from === "function") {
19
+ for (let key of __getOwnPropNames(from))
20
+ if (!__hasOwnProp.call(to, key) && key !== except)
21
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
22
+ }
23
+ return to;
24
+ };
25
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
26
+ // If the importer is in node compatibility mode or this is not an ESM
27
+ // file that has been converted to a CommonJS file using a Babel-
28
+ // compatible transform (i.e. "__esModule" has not been set), then set
29
+ // "default" to the CommonJS "module.exports" for node compatibility.
30
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
31
+ mod
32
+ ));
33
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
+
35
+ // ../shared/dist/types.js
36
+ var require_types = __commonJS({
37
+ "../shared/dist/types.js"(exports) {
38
+ "use strict";
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ }
41
+ });
42
+
43
+ // ../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/redactor.js
44
+ var redactor_exports = {};
45
+ __export(redactor_exports, {
46
+ DEFAULT_HEADER_KEYS: () => DEFAULT_HEADER_KEYS,
47
+ DEFAULT_QUERY_KEYS: () => DEFAULT_QUERY_KEYS,
48
+ createRedactor: () => createRedactor
49
+ });
50
+ function createRedactor(opts = {}) {
51
+ const placeholder = opts.placeholder ?? "[REDACTED]";
52
+ const matchMode = opts.matchMode ?? "exact";
53
+ const headerKeys = [
54
+ ...opts.useDefaultHeaderKeys === false ? [] : DEFAULT_HEADER_KEYS,
55
+ ...opts.sensitiveHeaderKeys ?? []
56
+ ];
57
+ const queryKeys = [
58
+ ...opts.useDefaultQueryKeys === false ? [] : DEFAULT_QUERY_KEYS,
59
+ ...opts.sensitiveQueryKeys ?? []
60
+ ];
61
+ const bodyMode = opts.bodyMode ?? "json";
62
+ function hit(keys, key) {
63
+ const lower = key.toLowerCase();
64
+ return matchMode === "includes" ? keys.some((s) => lower.includes(s)) : keys.includes(lower);
65
+ }
66
+ function redactHeaders(headers) {
67
+ if (!headers)
68
+ return void 0;
69
+ const result = {};
70
+ for (const [key, value] of Object.entries(headers)) {
71
+ result[key] = hit(headerKeys, key) ? placeholder : value;
72
+ }
73
+ return result;
74
+ }
75
+ function redactUrlQuery(url) {
76
+ try {
77
+ const u = new URL(url);
78
+ let changed = false;
79
+ for (const key of u.searchParams.keys()) {
80
+ if (hit(queryKeys, key)) {
81
+ u.searchParams.set(key, placeholder);
82
+ changed = true;
83
+ }
84
+ }
85
+ return changed ? u.toString() : url;
86
+ } catch {
87
+ return url;
88
+ }
89
+ }
90
+ function redactBodyJson(body) {
91
+ const patterns = [
92
+ /"password"\s*:\s*"[^"]*"/gi,
93
+ /"access_token"\s*:\s*"[^"]*"/gi,
94
+ /"refresh_token"\s*:\s*"[^"]*"/gi,
95
+ /"api_key"\s*:\s*"[^"]*"/gi,
96
+ /"token"\s*:\s*"[^"]*"/gi
97
+ ];
98
+ let result = body;
99
+ for (const p of patterns) {
100
+ result = result.replace(p, (match) => {
101
+ const key = match.split(":")[0];
102
+ return `${key}: "${placeholder}"`;
103
+ });
104
+ }
105
+ return result;
106
+ }
107
+ function redactBodyLoose(body) {
108
+ let result = body.replace(/(password|token|secret|api[_-]?key)\s*[:=]\s*[^\s&]+/gi, `$1=${placeholder}`);
109
+ for (const [pattern, replacement] of [PHONE_RULE, EMAIL_RULE, ...opts.extraBodyRules ?? []]) {
110
+ result = result.replace(pattern, replacement);
111
+ }
112
+ return result;
113
+ }
114
+ function redactBody(body) {
115
+ if (!body)
116
+ return void 0;
117
+ return bodyMode === "loose" ? redactBodyLoose(body) : redactBodyJson(body);
118
+ }
119
+ function redactText(text) {
120
+ let result = text.replace(PHONE_RULE[0], PHONE_RULE[1]).replace(EMAIL_RULE[0], EMAIL_RULE[1]);
121
+ for (const [pattern, replacement] of opts.extraBodyRules ?? []) {
122
+ result = result.replace(pattern, replacement);
123
+ }
124
+ return result;
125
+ }
126
+ return { redactHeaders, redactUrlQuery, redactBody, redactText };
127
+ }
128
+ var DEFAULT_HEADER_KEYS, DEFAULT_QUERY_KEYS, PHONE_RULE, EMAIL_RULE;
129
+ var init_redactor = __esm({
130
+ "../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/redactor.js"() {
131
+ DEFAULT_HEADER_KEYS = [
132
+ "authorization",
133
+ "cookie",
134
+ "set-cookie",
135
+ "x-api-key",
136
+ "x-auth-token",
137
+ "access_token",
138
+ "refresh_token",
139
+ "password",
140
+ "api_key",
141
+ "apikey",
142
+ "secret",
143
+ "token"
144
+ ];
145
+ DEFAULT_QUERY_KEYS = [
146
+ "access_token",
147
+ "refresh_token",
148
+ "token",
149
+ "api_key",
150
+ "apikey",
151
+ "key",
152
+ "password",
153
+ "secret",
154
+ "code"
155
+ ];
156
+ PHONE_RULE = [/\b\d{11}\b/g, "***PHONE***"];
157
+ EMAIL_RULE = [
158
+ /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
159
+ "***EMAIL***"
160
+ ];
161
+ }
162
+ });
163
+
164
+ // ../shared/dist/redactor.js
165
+ var require_redactor = __commonJS({
166
+ "../shared/dist/redactor.js"(exports) {
167
+ "use strict";
168
+ Object.defineProperty(exports, "__esModule", { value: true });
169
+ exports.redactText = exports.redactBody = exports.redactUrlQuery = exports.redactHeaders = void 0;
170
+ var redactor_1 = (init_redactor(), __toCommonJS(redactor_exports));
171
+ var redactor = (0, redactor_1.createRedactor)({
172
+ placeholder: "***",
173
+ matchMode: "includes",
174
+ bodyMode: "loose",
175
+ // 完全替换 kit 默认清单:保持 hunter-mate 原有行为
176
+ // (kit 默认 query 清单含 'key',includes 模式会误伤 'monkey' 之类子串)
177
+ useDefaultHeaderKeys: false,
178
+ sensitiveHeaderKeys: [
179
+ "authorization",
180
+ "cookie",
181
+ "x-auth-token",
182
+ "token",
183
+ "api-key",
184
+ "apikey",
185
+ "password",
186
+ "secret"
187
+ ],
188
+ useDefaultQueryKeys: false,
189
+ sensitiveQueryKeys: [
190
+ "token",
191
+ "access_token",
192
+ "refresh_token",
193
+ "api_key",
194
+ "apikey",
195
+ "password",
196
+ "secret",
197
+ "code"
198
+ ]
199
+ });
200
+ exports.redactHeaders = redactor.redactHeaders;
201
+ exports.redactUrlQuery = redactor.redactUrlQuery;
202
+ exports.redactBody = redactor.redactBody;
203
+ exports.redactText = redactor.redactText;
204
+ }
205
+ });
206
+
207
+ // ../shared/dist/hbr.js
208
+ var require_hbr = __commonJS({
209
+ "../shared/dist/hbr.js"(exports) {
210
+ "use strict";
211
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
212
+ if (k2 === void 0) k2 = k;
213
+ var desc = Object.getOwnPropertyDescriptor(m, k);
214
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
215
+ desc = { enumerable: true, get: function() {
216
+ return m[k];
217
+ } };
218
+ }
219
+ Object.defineProperty(o, k2, desc);
220
+ } : function(o, m, k, k2) {
221
+ if (k2 === void 0) k2 = k;
222
+ o[k2] = m[k];
223
+ });
224
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
225
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
226
+ };
227
+ Object.defineProperty(exports, "__esModule", { value: true });
228
+ exports.createHbrEvent = createHbrEvent;
229
+ exports.createNetworkEvent = createNetworkEvent;
230
+ exports.createConsoleEvent = createConsoleEvent;
231
+ exports.createResumeEvent = createResumeEvent;
232
+ exports.createActionEvent = createActionEvent;
233
+ function createHbrEvent(type, sessionId, payload, options) {
234
+ return {
235
+ type,
236
+ sessionId,
237
+ timestamp: options?.timestamp ?? Date.now(),
238
+ privacy: options?.privacy ?? "public",
239
+ payload,
240
+ sourceUrl: options?.sourceUrl,
241
+ tabId: options?.tabId
242
+ };
243
+ }
244
+ function createNetworkEvent(sessionId, data, privacy = "redacted") {
245
+ return createHbrEvent("network", sessionId, data, { privacy });
246
+ }
247
+ function createConsoleEvent(sessionId, data, privacy = "private") {
248
+ return createHbrEvent("console", sessionId, data, { privacy });
249
+ }
250
+ function createResumeEvent(sessionId, data, privacy = "private") {
251
+ return createHbrEvent("resume", sessionId, data, { privacy });
252
+ }
253
+ function createActionEvent(sessionId, data, privacy = "public") {
254
+ return createHbrEvent("action", sessionId, data, { privacy });
255
+ }
256
+ __exportStar(require_types(), exports);
257
+ __exportStar(require_redactor(), exports);
258
+ }
259
+ });
260
+
261
+ // ../shared/dist/internal-url.js
262
+ var require_internal_url = __commonJS({
263
+ "../shared/dist/internal-url.js"(exports) {
264
+ "use strict";
265
+ Object.defineProperty(exports, "__esModule", { value: true });
266
+ exports.isHarnessInternalHostname = isHarnessInternalHostname2;
267
+ exports.isHarnessInternalUrl = isHarnessInternalUrl;
268
+ var INTERNAL_HOST_PATTERNS = [
269
+ "dev-ops.hiredchina.com"
270
+ ];
271
+ function isHarnessInternalHostname2(hostname) {
272
+ const host = (hostname || "").toLowerCase().replace(/\.$/, "");
273
+ if (!host)
274
+ return false;
275
+ return INTERNAL_HOST_PATTERNS.some((p) => {
276
+ if (p.startsWith("*.")) {
277
+ const root = p.slice(2);
278
+ return host === root || host.endsWith(`.${root}`);
279
+ }
280
+ return host === p;
281
+ });
282
+ }
283
+ function isHarnessInternalUrl(url) {
284
+ if (!url)
285
+ return false;
286
+ try {
287
+ return isHarnessInternalHostname2(new URL(url).hostname);
288
+ } catch {
289
+ return false;
290
+ }
291
+ }
292
+ }
293
+ });
294
+
295
+ // ../shared/dist/index.js
296
+ var require_dist = __commonJS({
297
+ "../shared/dist/index.js"(exports) {
298
+ "use strict";
299
+ var __createBinding = exports && exports.__createBinding || (Object.create ? function(o, m, k, k2) {
300
+ if (k2 === void 0) k2 = k;
301
+ var desc = Object.getOwnPropertyDescriptor(m, k);
302
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
303
+ desc = { enumerable: true, get: function() {
304
+ return m[k];
305
+ } };
306
+ }
307
+ Object.defineProperty(o, k2, desc);
308
+ } : function(o, m, k, k2) {
309
+ if (k2 === void 0) k2 = k;
310
+ o[k2] = m[k];
311
+ });
312
+ var __exportStar = exports && exports.__exportStar || function(m, exports2) {
313
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
314
+ };
315
+ Object.defineProperty(exports, "__esModule", { value: true });
316
+ __exportStar(require_types(), exports);
317
+ __exportStar(require_redactor(), exports);
318
+ __exportStar(require_hbr(), exports);
319
+ __exportStar(require_internal_url(), exports);
320
+ }
321
+ });
322
+
1
323
  // src/shared/messaging.ts
2
324
  var SERVER_PORT = 34571;
3
325
  var WS_URL = `ws://localhost:${SERVER_PORT}/ws`;
@@ -571,6 +893,27 @@ function runControl(command, args) {
571
893
  if ("errorEnvelope" in r) return r.errorEnvelope;
572
894
  const { el, matches_n, match_level, target } = r;
573
895
  el.scrollIntoView({ block: "center" });
896
+ if (a.full === true) {
897
+ const rect = el.getBoundingClientRect();
898
+ const opts = {
899
+ bubbles: true,
900
+ cancelable: true,
901
+ view: window,
902
+ clientX: rect.x + rect.width / 2,
903
+ clientY: rect.y + rect.height / 2,
904
+ button: 0,
905
+ buttons: 1,
906
+ pointerId: 1,
907
+ pointerType: "mouse",
908
+ isPrimary: true
909
+ };
910
+ el.dispatchEvent(new PointerEvent("pointerdown", opts));
911
+ el.dispatchEvent(new MouseEvent("mousedown", opts));
912
+ el.dispatchEvent(new PointerEvent("pointerup", { ...opts, buttons: 0 }));
913
+ el.dispatchEvent(new MouseEvent("mouseup", { ...opts, buttons: 0 }));
914
+ el.dispatchEvent(new MouseEvent("click", { ...opts, buttons: 0 }));
915
+ return { clicked: true, full: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };
916
+ }
574
917
  el.click();
575
918
  return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };
576
919
  }
@@ -999,10 +1342,13 @@ globalThis.__cpRunControl = runControl;
999
1342
  globalThis.__cpRuntimeVersion = RUNTIME_VERSION;
1000
1343
 
1001
1344
  // src/generated/control-runtime-source.ts
1002
- var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/control-runtime.ts\n function getRefStore() {\n const g = globalThis;\n if (!g.__cpRefs) {\n g.__cpRefs = { counter: 1, refs: /* @__PURE__ */ new Map(), cssPaths: /* @__PURE__ */ new Map() };\n }\n return g.__cpRefs;\n }\n function allocRef(el) {\n const store = getRefStore();\n for (const [ref2, existing] of store.refs) {\n if (existing === el) return ref2;\n }\n const ref = store.counter++;\n store.refs.set(ref, el);\n store.cssPaths.set(ref, uniqueCssPath(el));\n return ref;\n }\n function uniqueCssPath(el) {\n if (el.id) return `#${CSS.escape(el.id)}`;\n const parts = [];\n let cur = el;\n while (cur && cur !== document.body && cur !== document.documentElement) {\n let sel = cur.tagName.toLowerCase();\n if (cur.id) {\n parts.unshift(`#${CSS.escape(cur.id)}`);\n break;\n }\n const parent = cur.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);\n if (sameTag.length > 1) {\n const idx = sameTag.indexOf(cur) + 1;\n sel += `:nth-of-type(${idx})`;\n }\n }\n parts.unshift(sel);\n cur = parent;\n }\n return parts.join(" > ");\n }\n function err(code, message, extra) {\n return { error: { code, message, ...extra || {} } };\n }\n var ERROR_CODES = {\n NOT_FOUND: "not_found",\n STALE_REF: "stale_ref",\n INVALID_SELECTOR: "invalid_selector",\n SELECTOR_NOT_FOUND: "selector_not_found",\n SELECTOR_AMBIGUOUS: "selector_ambiguous",\n SELECTOR_NTH_OUT_OF_RANGE: "selector_nth_out_of_range",\n OPTION_NOT_FOUND: "option_not_found",\n NOT_A_SELECT: "not_a_select"\n };\n var IMPLICIT_ROLE = {\n a: "link",\n button: "button",\n select: "combobox",\n textarea: "textbox",\n h1: "heading",\n h2: "heading",\n h3: "heading",\n h4: "heading",\n h5: "heading",\n h6: "heading",\n img: "img",\n nav: "navigation",\n ul: "list",\n ol: "list",\n li: "listitem",\n main: "main",\n article: "article",\n aside: "complementary",\n header: "banner",\n footer: "contentinfo",\n form: "form",\n table: "table",\n summary: "button",\n option: "option",\n output: "status"\n };\n function roleOf(el) {\n const explicit = el.getAttribute("role");\n if (explicit) return explicit.split(/\\s+/)[0];\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const type = el.type || "text";\n if (type === "hidden") return null;\n if (type === "checkbox") return "checkbox";\n if (type === "radio") return "radio";\n if (type === "button" || type === "submit" || type === "reset") return "button";\n if (type === "range") return "slider";\n return "textbox";\n }\n if (tag === "progress") return "progressbar";\n if (tag === "meter") return "meter";\n return IMPLICIT_ROLE[tag] ?? null;\n }\n function accessibleName(el) {\n const aria = el.getAttribute("aria-label");\n if (aria) return aria.trim();\n const labelledby = el.getAttribute("aria-labelledby");\n if (labelledby) {\n const parts = labelledby.split(/\\s+/).map((id2) => document.getElementById(id2)?.textContent?.trim() || "");\n const joined = parts.filter(Boolean).join(" ");\n if (joined) return joined.slice(0, 120);\n }\n const id = el.getAttribute("id");\n if (id) {\n const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);\n if (label?.textContent?.trim()) return label.textContent.trim().slice(0, 120);\n }\n if (el.tagName.toLowerCase() === "input") {\n const type = el.type;\n if (type === "button" || type === "submit" || type === "reset") {\n const v = el.value;\n if (v) return v;\n }\n }\n const alt = el.getAttribute("alt");\n if (alt) return alt;\n const title = el.getAttribute("title");\n if (title) return title;\n const placeholder = el.getAttribute("placeholder");\n if (placeholder) return placeholder;\n return (el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 120);\n }\n function isVisible(el) {\n if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return false;\n const html = el;\n if (html.hasAttribute("hidden")) return false;\n if (html.closest("[hidden]")) return false;\n const style = window.getComputedStyle(html);\n if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;\n if (el instanceof HTMLElement && el.offsetParent === null) {\n const pos = style.position;\n if (pos !== "fixed" && pos !== "sticky") return false;\n }\n return true;\n }\n var INTERACTIVE_SELECTOR = "a[href], button, input, textarea, select, summary, [role], [contenteditable=\'\'], [contenteditable=\'true\'], [onclick], [tabindex]";\n function isInteractive(el) {\n return el.matches(INTERACTIVE_SELECTOR);\n }\n function testidSelector(testid) {\n return `[data-testid="${CSS.escape(testid)}"], [data-test-id="${CSS.escape(testid)}"], [data-test="${CSS.escape(testid)}"], [data-cy="${CSS.escape(testid)}"]`;\n }\n function resolveTargetRich(sel) {\n const targetDesc = sel.ref != null ? String(sel.ref) : sel.css ?? (sel.role || sel.name || sel.label || sel.testid || sel.text ? `${sel.role ? `role=${sel.role} ` : ""}${sel.name ? `name=${sel.name} ` : ""}${sel.label ? `label=${sel.label} ` : ""}${sel.testid ? `testid=${sel.testid} ` : ""}${sel.text ? `text=${sel.text}` : ""}`.trim() : "");\n if (sel.ref != null) {\n const store = getRefStore();\n const el = store.refs.get(sel.ref);\n if (el && document.contains(el) && isVisible(el)) {\n return { el, matches_n: 1, match_level: "exact", target: targetDesc };\n }\n if (el && !document.contains(el)) {\n const cssPath = store.cssPaths.get(sel.ref);\n if (cssPath) {\n const re = document.querySelector(cssPath);\n if (re && isVisible(re)) {\n store.refs.set(sel.ref, re);\n return { el: re, matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n }\n return { errorEnvelope: err(ERROR_CODES.STALE_REF, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n return { errorEnvelope: err(ERROR_CODES.NOT_FOUND, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n let candidates = [];\n if (sel.css) {\n try {\n candidates = Array.from(document.querySelectorAll(sel.css));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: sel.css }) };\n }\n if (candidates.length === 0) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: sel.css, candidates: suggestCandidates() }) };\n }\n } else if (sel.testid) {\n try {\n candidates = Array.from(document.querySelectorAll(testidSelector(sel.testid)));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.", { testid: sel.testid }) };\n }\n } else if (sel.role || sel.name || sel.label || sel.text) {\n const wantRole = sel.role ? sel.role.toLowerCase() : null;\n const wantName = sel.name ? sel.name.toLowerCase() : null;\n const wantText = sel.text ? sel.text.toLowerCase() : null;\n let labelTargets = [];\n if (sel.label) {\n const lower = sel.label.toLowerCase();\n labelTargets = Array.from(document.querySelectorAll("label")).filter((l) => (l.textContent || "").trim().toLowerCase().includes(lower)).map((l) => {\n const forId = l.getAttribute("for");\n if (forId) return document.getElementById(forId);\n return l.querySelector("input, select, textarea");\n }).filter((x) => !!x);\n }\n const all = Array.from(document.querySelectorAll("*"));\n candidates = all.filter((el) => {\n if (!isVisible(el)) return false;\n const role = roleOf(el);\n if (wantRole && (!role || role.toLowerCase() !== wantRole)) return false;\n if (wantName) {\n const name = accessibleName(el).toLowerCase();\n if (!name.includes(wantName)) return false;\n }\n if (wantText) {\n const t = (el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();\n if (!t.includes(wantText)) return false;\n }\n return true;\n });\n if (labelTargets.length) {\n const set = new Set(labelTargets);\n candidates = labelTargets.concat(candidates.filter((c) => !set.has(c)));\n }\n if (candidates.length === 0) {\n return {\n errorEnvelope: err(\n ERROR_CODES.SELECTOR_NOT_FOUND,\n "Semantic locator matches 0 elements.",\n { role: sel.role, name: sel.name, label: sel.label, text: sel.text, candidates: suggestCandidates() }\n )\n };\n }\n } else {\n return { errorEnvelope: err("no_target", "Provide a numeric ref, CSS selector, or semantic locator (--role/--name/--label/--text/--testid).") };\n }\n const visible = candidates.filter(isVisible);\n const pool = visible.length ? visible : candidates;\n if (sel.nth != null) {\n if (sel.nth < 0 || sel.nth >= pool.length) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NTH_OUT_OF_RANGE, `--nth ${sel.nth} beyond match count ${pool.length}.`) };\n }\n const el = pool[sel.nth];\n return { el, matches_n: pool.length, match_level: "reidentified", target: targetDesc };\n }\n if (pool.length > 1) {\n return {\n errorEnvelope: err(ERROR_CODES.SELECTOR_AMBIGUOUS, `CSS matches ${pool.length} elements and no --nth given.`, {\n matches_n: pool.length,\n candidates: pool.slice(0, 5).map((el) => uniqueCssPath(el))\n })\n };\n }\n return { el: pool[0], matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n function suggestCandidates() {\n const store = getRefStore();\n const recent = Array.from(store.refs.entries()).slice(-5).map(([, el]) => uniqueCssPath(el));\n return recent;\n }\n function compoundOf(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const input = el;\n const type = input.type;\n if (["date", "time", "datetime-local", "month", "week"].includes(type)) {\n const formatMap = {\n date: "YYYY-MM-DD",\n time: "HH:mm",\n "datetime-local": "YYYY-MM-DDTHH:mm",\n month: "YYYY-MM",\n week: "YYYY-Www"\n };\n return {\n control: type,\n format: formatMap[type] ?? type,\n current: input.value || "",\n min: input.min || null,\n max: input.max || null\n };\n }\n if (type === "file") {\n const current = Array.from(input.files ?? []).map((f) => f.name);\n return { control: "file", multiple: input.multiple, current, accept: input.accept || null };\n }\n return null;\n }\n if (tag === "select") {\n const select = el;\n const options = Array.from(select.options).slice(0, 50).map((o) => ({\n label: (o.textContent || "").trim(),\n value: o.value,\n selected: o.selected\n }));\n return {\n control: "select",\n multiple: select.multiple,\n current: select.selectedOptions[0] ? (select.selectedOptions[0].textContent || "").trim() : "",\n options,\n options_total: select.options.length\n };\n }\n return null;\n }\n var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "meta", "link", "head", "base", "template", "path", "circle", "rect", "g", "defs", "polygon", "line"]);\n function describeElement(el, ref) {\n const tag = el.tagName.toLowerCase();\n const role = el.getAttribute("role") ?? null;\n const name = accessibleName(el);\n const parts = [];\n if (ref != null) parts.push(`[${ref}]`);\n parts.push(tag);\n if (role && role !== IMPLICIT_ROLE[tag]) parts.push(`role=${role}`);\n if (tag === "input") {\n const input = el;\n if (input.type && input.type !== "text") parts.push(`[${input.type}]`);\n if (input.disabled) parts.push("(disabled)");\n }\n if (name) parts.push(JSON.stringify(name.length > 60 ? name.slice(0, 57) + "\\u2026" : name));\n if (tag === "a") {\n const href = el.getAttribute("href");\n if (href) parts.push(`\\u2192 ${href.slice(0, 50)}`);\n }\n if (el.disabled) parts.push("(disabled)");\n return parts.join(" ");\n }\n function buildStateTree() {\n const lines = [];\n const compounds = {};\n const budget = { nodes: 0, chars: 0, truncated: false };\n const MAX_NODES = 400;\n const MAX_CHARS = 9e3;\n let hiddenInteractive = 0;\n function emit(depth, text) {\n const line = " ".repeat(Math.min(depth, 12)) + text;\n lines.push(line);\n budget.chars += line.length;\n }\n function walk(el, depth) {\n if (budget.nodes >= MAX_NODES || budget.chars >= MAX_CHARS) {\n budget.truncated = true;\n return;\n }\n const tag = el.tagName.toLowerCase();\n if (SKIP_TAGS.has(tag)) return;\n budget.nodes++;\n const visible = isVisible(el);\n const interactive = isInteractive(el);\n if (interactive) {\n if (visible) {\n const ref = allocRef(el);\n emit(depth, describeElement(el, ref));\n const compound = compoundOf(el);\n if (compound) compounds[String(ref)] = compound;\n } else {\n hiddenInteractive++;\n return;\n }\n } else if (visible) {\n if (/^h[1-6]$/.test(tag)) {\n const t = accessibleName(el);\n if (t) emit(depth, `${"#".repeat(Number(tag[1]))} ${t}`);\n } else if (tag === "p" || tag === "li" || tag === "td" || tag === "th" || tag === "figcaption" || tag === "blockquote") {\n const ownText = ownTextOf(el);\n if (ownText) emit(depth, ownText.slice(0, 100));\n } else if (tag === "img") {\n const alt = el.getAttribute("alt") || "";\n if (alt) emit(depth, `img ${JSON.stringify(alt.slice(0, 60))}`);\n }\n }\n if (depth < 14) {\n for (const child of Array.from(el.children)) {\n walk(child, depth + 1);\n if (budget.truncated) return;\n }\n }\n }\n function ownTextOf(el) {\n let text = "";\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";\n }\n return text.replace(/\\s+/g, " ").trim();\n }\n const root = document.querySelector("main") || document.querySelector("#root") || document.body;\n emit(0, `# ${document.title}`);\n walk(root, 0);\n const scrollable = document.documentElement.scrollHeight > window.innerHeight + 200;\n if (scrollable) {\n emit(0, "\\u2195 page is scrollable \\u2014 run cp.scroll down to reveal more");\n }\n if (hiddenInteractive > 0) {\n emit(0, `\\u2139 ${hiddenInteractive} hidden interactive element(s) not tagged`);\n }\n if (budget.truncated) {\n emit(0, "\\u2026 (snapshot truncated by budget \\u2014 narrow with a selector or scroll)");\n }\n return {\n url: location.href,\n title: document.title,\n text: lines.join("\\n"),\n compounds,\n hints: { scroll: scrollable, hidden_interactive: hiddenInteractive },\n truncated: budget.truncated\n };\n }\n function runControl(command, args) {\n const a = args || {};\n function getVisibleText(el) {\n return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 120);\n }\n function getAttrs(el) {\n const attrs = {};\n for (const name of ["id", "class", "name", "type", "href", "src", "placeholder", "value", "role", "aria-label", "data-testid"]) {\n attrs[name] = el.getAttribute(name);\n }\n return attrs;\n }\n function sel() {\n return {\n ref: a.ref,\n css: a.css,\n nth: a.nth,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n };\n }\n function findElements(findArgs) {\n const limit = findArgs.limit ?? 20;\n const textMax = findArgs["text-max"] ?? 120;\n let elements = [];\n if (findArgs.css) {\n try {\n elements = Array.from(document.querySelectorAll(findArgs.css));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: findArgs.css });\n }\n } else if (findArgs.testid) {\n try {\n elements = Array.from(document.querySelectorAll(testidSelector(findArgs.testid)));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.");\n }\n } else if (findArgs.role || findArgs.name || findArgs.label) {\n const r = resolveTargetRich({ role: findArgs.role, name: findArgs.name, label: findArgs.label, text: findArgs.text });\n if ("errorEnvelope" in r) return r.errorEnvelope;\n elements = document.querySelectorAll("*").length ? semanticCandidates(findArgs) : [];\n } else if (findArgs.text) {\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const lower = findArgs.text.toLowerCase();\n const seen = /* @__PURE__ */ new Set();\n let node;\n while (node = walker.nextNode()) {\n if (node.textContent?.toLowerCase().includes(lower)) {\n const el = node.parentElement;\n if (el && !seen.has(el)) {\n seen.add(el);\n elements.push(el);\n if (elements.length >= limit) break;\n }\n }\n }\n } else {\n elements = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));\n }\n const out = elements.slice(0, limit).map((el, i) => {\n const ref = allocRef(el);\n return {\n nth: i,\n ref,\n tag: el.tagName.toLowerCase(),\n role: roleOf(el),\n text: getVisibleText(el).slice(0, textMax),\n attrs: getAttrs(el),\n visible: isVisible(el),\n compound: compoundOf(el) ?? void 0\n };\n });\n return out;\n }\n function semanticCandidates(findArgs) {\n const wantRole = findArgs.role ? findArgs.role.toLowerCase() : null;\n const wantName = findArgs.name ? findArgs.name.toLowerCase() : null;\n const wantText = findArgs.text ? findArgs.text.toLowerCase() : null;\n const all = Array.from(document.querySelectorAll("*"));\n return all.filter((el) => {\n if (!isVisible(el)) return false;\n if (wantRole) {\n const role = roleOf(el);\n if (!role || role.toLowerCase() !== wantRole) return false;\n }\n if (wantName && !accessibleName(el).toLowerCase().includes(wantName)) return false;\n if (wantText && !(el.textContent || "").toLowerCase().includes(wantText)) return false;\n return true;\n });\n }\n function pageState() {\n return {\n url: location.href,\n title: document.title,\n width: window.innerWidth,\n height: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio\n };\n }\n function goto(gotoArgs) {\n if (!gotoArgs.url) return { error: "url_required" };\n let sameUrl = false;\n try {\n sameUrl = new URL(gotoArgs.url, window.location.href).href === window.location.href;\n } catch {\n sameUrl = false;\n }\n if (sameUrl) {\n if (gotoArgs.reload === true) {\n window.location.reload();\n return { navigated: true, reloaded: true, url: gotoArgs.url };\n }\n return { navigated: false, reused: true, url: gotoArgs.url };\n }\n window.location.href = gotoArgs.url;\n return { navigated: true, url: gotoArgs.url };\n }\n function evalScript(evalArgs) {\n if (!evalArgs.script) return { error: "script_required" };\n try {\n const fn = new Function(evalArgs.script);\n const result = fn();\n return { result };\n } catch (e) {\n return { error: String(e) };\n }\n }\n function fillElement(fArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) {\n const isContentEditable = el.isContentEditable;\n if (isContentEditable) {\n el.innerText = fArgs.text ?? "";\n } else {\n input.value = fArgs.text ?? "";\n }\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n const actual = isContentEditable ? el.innerText : input.value;\n return actual === (fArgs.text ?? "") ? { filled: true, verified: true, text: fArgs.text, actual, target, matches_n, match_level } : err("verify_failed", "Input value diverged after fill (controlled component?).", { actual });\n }\n return err("invalid_type", "fill only supports input, textarea, and contenteditable.");\n }\n function typeElement(tArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n el.focus({ preventScroll: true });\n input.value = tArgs.text ?? "";\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n let autocomplete = false;\n try {\n autocomplete = !!document.querySelector("[role=\'listbox\'], [role=\'combobox\'] [role=\'listbox\'], datalist, .ant-select-dropdown:not([style*=\'hidden\']), [id^=\'listbox\']");\n } catch {\n }\n return { typed: true, text: tArgs.text, target, matches_n, match_level, autocomplete };\n }\n function clickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.scrollIntoView({ block: "center" });\n el.click();\n return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };\n }\n function selectElement(sArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n if (el.tagName !== "SELECT") {\n return err(ERROR_CODES.NOT_A_SELECT, "select was called on a non-<select> element.", { tag: el.tagName.toLowerCase() });\n }\n const select = el;\n const want = sArgs.label ?? sArgs.value ?? sArgs.option;\n if (!want) return err("option_required", "Provide label or value.");\n const byLabel = Array.from(select.options).find((o) => (o.textContent || "").trim() === want);\n const byValue = Array.from(select.options).find((o) => o.value === want);\n const opt = byLabel ?? byValue;\n if (!opt) {\n const available = Array.from(select.options).slice(0, 50).map((o) => (o.textContent || "").trim());\n return err(ERROR_CODES.OPTION_NOT_FOUND, "Option not found.", { available });\n }\n select.value = opt.value;\n select.dispatchEvent(new Event("input", { bubbles: true }));\n select.dispatchEvent(new Event("change", { bubbles: true }));\n return { selected: true, value: select.value, target, matches_n, match_level };\n }\n function pressKeys(kArgs) {\n const keyCombo = kArgs.keys ?? kArgs.key ?? "";\n if (!keyCombo) return { error: "keys_required" };\n const comboParts = keyCombo.split("+").map((s) => s.trim());\n const main = comboParts[comboParts.length - 1];\n const ctrl = comboParts.includes("Control") || comboParts.includes("Ctrl") || comboParts.includes("Meta") || comboParts.includes("Cmd");\n const alt = comboParts.includes("Alt");\n const shift = comboParts.includes("Shift");\n const target = document.activeElement ?? document.body;\n const opts = { key: main, bubbles: true, cancelable: true, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: comboParts.includes("Meta") || comboParts.includes("Cmd") };\n target.dispatchEvent(new KeyboardEvent("keydown", opts));\n target.dispatchEvent(new KeyboardEvent("keypress", opts));\n target.dispatchEvent(new KeyboardEvent("keyup", opts));\n return { pressed: true, key: keyCombo };\n }\n function hoverElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const rect = el.getBoundingClientRect();\n const opts = { bubbles: true, cancelable: true, clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2 };\n el.dispatchEvent(new MouseEvent("mouseover", opts));\n el.dispatchEvent(new MouseEvent("mousemove", opts));\n return { hovered: true, target, matches_n, match_level };\n }\n function focusElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.focus({ preventScroll: true });\n return { focused: true, target, matches_n, match_level };\n }\n function dblclickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));\n return { dblclicked: true, target, matches_n, match_level };\n }\n function setChecked(want) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n const isAria = input.type !== "checkbox" && input.type !== "radio" && el.getAttribute("aria-checked") != null;\n if (!isAria && input.type !== "checkbox" && input.type !== "radio") {\n return err("invalid_type", "Target is not a checkbox/radio/aria-checked control.", { tag: el.tagName.toLowerCase() });\n }\n if (isAria) {\n el.setAttribute("aria-checked", String(want));\n el.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed: true, target, matches_n, match_level, kind: "aria" };\n }\n if (input.type === "radio" && !want) {\n return err("invalid_type", "Radio buttons cannot be unchecked directly; select another radio in the group.");\n }\n const changed = input.checked !== want;\n input.checked = want;\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed, target, matches_n, match_level, kind: input.type };\n }\n function scrollElement(sArgs) {\n const dir = sArgs.direction ?? "down";\n const amount = sArgs.amount ?? 500;\n if (dir !== "up" && dir !== "down") return { error: "invalid_direction" };\n const dy = dir === "down" ? amount : -amount;\n window.scrollBy({ top: dy, behavior: "instant" });\n return {\n scrolled: true,\n direction: dir,\n amount,\n scrollY: Math.round(window.scrollY),\n scrollHeight: document.documentElement.scrollHeight\n };\n }\n function backNav() {\n history.back();\n return { back: true };\n }\n function htmlJsonTree(el, depth, maxDepth, childrenMax, textMax, truncated) {\n const children = Array.from(el.children);\n const ownText = (el.textContent || "").replace(/\\s+/g, " ").trim();\n const node = {\n tag: el.tagName.toLowerCase(),\n attrs: getAttrs(el),\n text: ownText.length > textMax ? (ownText.slice(0, textMax), truncated.text_truncated = (truncated.text_truncated || 0) + 1, ownText.slice(0, textMax)) : ownText,\n children: [],\n compound: compoundOf(el) ?? void 0\n };\n if (depth < maxDepth) {\n let kids = children;\n if (children.length > childrenMax) {\n truncated.children_dropped = (truncated.children_dropped || 0) + (children.length - childrenMax);\n kids = children.slice(0, childrenMax);\n }\n node.children = kids.map((c) => htmlJsonTree(c, depth + 1, maxDepth, childrenMax, textMax, truncated));\n } else if (children.length) {\n truncated.depth = (truncated.depth || 0) + 1;\n }\n return node;\n }\n function getElement(gArgs) {\n const what = gArgs.what ?? "text";\n if (what === "title") return { value: document.title, matches_n: 1, match_level: "exact" };\n if (what === "url") return { value: location.href, matches_n: 1, match_level: "exact" };\n if (what === "html") {\n if (gArgs["as"] === "json") {\n const root2 = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root2) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n const truncated = {};\n const tree = htmlJsonTree(root2, 0, gArgs.depth ?? 6, gArgs["children-max"] ?? 20, gArgs["text-max"] ?? 200, truncated);\n return {\n value: tree,\n truncated: Object.keys(truncated).length ? truncated : void 0\n };\n }\n const root = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n let html = root.outerHTML;\n if (gArgs.max && html.length > gArgs.max) html = html.slice(0, gArgs.max) + "\\u2026";\n return { value: html };\n }\n const r = resolveTargetRich(gArgs);\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level } = r;\n switch (what) {\n case "text": {\n const t = getVisibleText(el);\n return { value: t.length > 2e3 ? t.slice(0, 2e3) : t, matches_n, match_level };\n }\n case "value":\n return { value: el.value ?? el.innerText ?? null, matches_n, match_level };\n case "attributes":\n return { value: getAttrs(el), matches_n, match_level };\n default:\n return { error: `unknown_what: ${what}` };\n }\n }\n function extractMarkdown(eArgs) {\n const chunkSize = eArgs["chunk-size"] ?? 0;\n const start = eArgs.start ?? 0;\n const scope = eArgs.selector ? document.querySelector(eArgs.selector) : document.querySelector("main") || document.querySelector("article") || document.body;\n if (!scope) return { error: "scope_not_found" };\n const parts = [];\n const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);\n let node;\n while (node = walker.nextNode()) {\n const el = node;\n const tag = el.tagName.toLowerCase();\n const text = getVisibleText(el);\n if (!text) continue;\n if (tag === "h1") parts.push(`# ${text}`);\n else if (tag === "h2") parts.push(`## ${text}`);\n else if (tag === "h3") parts.push(`### ${text}`);\n else if (tag === "p" || tag === "li") parts.push(text);\n else if (tag === "a") parts.push(`[${text}](${el.getAttribute("href") ?? ""})`);\n }\n const full = parts.join("\\n\\n");\n const sliced = chunkSize > 0 ? full.slice(start, start + chunkSize) : full.slice(start);\n return {\n url: location.href,\n title: document.title,\n selector: eArgs.selector ?? "auto",\n total_chars: full.length,\n chunk_size: chunkSize,\n start,\n end: start + sliced.length,\n next_start_char: start + sliced.length < full.length ? start + sliced.length : null,\n content: sliced\n };\n }\n function listFrames() {\n const frames = Array.from(document.querySelectorAll("iframe")).map((f, index) => {\n const src = f.getAttribute("src") ?? "";\n let crossOrigin = false;\n try {\n if (src && /^https?:/.test(src)) crossOrigin = new URL(src, location.href).origin !== location.origin;\n } catch {\n }\n return { index, url: src, name: f.getAttribute("name") ?? "", cross_origin: crossOrigin };\n });\n return { frames };\n }\n function screenshotMeta() {\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n url: location.href,\n title: document.title\n };\n }\n function startTitleMarquee(tArgs) {\n const g = globalThis;\n if (g.__cpMarquee) {\n clearInterval(g.__cpMarquee.interval);\n } else {\n g.__cpMarquee = { original: document.title, interval: 0 };\n }\n const prefix = tArgs.text ?? "\\u{1F916}";\n const base = g.__cpMarquee.original;\n const full = `${prefix} ${base}`;\n const speed = Math.max(100, Math.min(2e3, tArgs.speed ?? 300));\n let pos = 0;\n const tick = () => {\n const display = full.slice(pos) + " " + full.slice(0, pos);\n document.title = display.slice(0, 60);\n pos = (pos + 1) % (full.length + 1 || 1);\n };\n tick();\n g.__cpMarquee.interval = window.setInterval(tick, speed);\n return { started: true, original: base, prefix, speed };\n }\n function stopTitleMarquee() {\n const g = globalThis;\n const state = g.__cpMarquee;\n if (!state) return { restored: false, reason: "no_active_marquee" };\n clearInterval(state.interval);\n document.title = state.original;\n delete g.__cpMarquee;\n return { restored: true, original: state.original };\n }\n function networkEntriesLegacy(nArgs) {\n const all = performance.getEntriesByType("resource").map((e) => ({\n url: e.name,\n type: e.initiatorType ?? "unknown",\n duration: Math.round(e.duration)\n }));\n let entries = all;\n if (nArgs.filter) {\n const keywords = nArgs.filter.split(",").map((s) => s.trim().toLowerCase());\n entries = all.filter((e) => keywords.some((kw) => e.url.toLowerCase().includes(kw)));\n }\n return { entries, count: entries.length, source: "performance-api" };\n }\n async function waitForCondition(wArgs) {\n const timeout = wArgs.timeout ?? 1e4;\n if (wArgs.time != null) {\n await new Promise((res) => setTimeout(res, Math.min(wArgs.time * 1e3, 3e4)));\n return { waited: true, kind: "time", seconds: wArgs.time };\n }\n const deadline = Date.now() + timeout;\n const pollInterval = 200;\n if (wArgs.selector) {\n while (Date.now() < deadline) {\n if (document.querySelector(wArgs.selector)) return { waited: true, kind: "selector", selector: wArgs.selector, elapsedMs: timeout - (deadline - Date.now()) };\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Selector not found within ${timeout}ms.`, { selector: wArgs.selector });\n }\n if (wArgs.text) {\n const lower = wArgs.text.toLowerCase();\n while (Date.now() < deadline) {\n if ((document.body.textContent || "").toLowerCase().includes(lower)) {\n return { waited: true, kind: "text", text: wArgs.text, elapsedMs: timeout - (deadline - Date.now()) };\n }\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Text not found within ${timeout}ms.`, { text: wArgs.text });\n }\n return { error: "no_condition" };\n }\n function uploadFiles(uArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (input.type !== "file") return err("invalid_type", "Target is not an input[type=file].", { tag: el.tagName.toLowerCase() });\n try {\n const dt = new DataTransfer();\n for (const name of uArgs.files ?? []) {\n dt.items.add(new File([""], name.split("/").pop() || name));\n }\n input.files = dt.files;\n } catch {\n }\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return {\n uploaded: true,\n files: uArgs.files ?? [],\n file_names: (uArgs.files ?? []).map((f) => f.split("/").pop()),\n target,\n matches_n,\n match_level,\n multiple: input.multiple,\n accept: input.accept || null\n };\n }\n function dragElements(dArgs) {\n const r1 = resolveTargetRich(sel());\n if ("errorEnvelope" in r1) return r1.errorEnvelope;\n const r2 = resolveTargetRich({ ref: dArgs.ref2, css: dArgs.css2 });\n if ("errorEnvelope" in r2) return r2.errorEnvelope;\n const src = r1.el;\n const tgt = r2.el;\n const make = (type) => {\n const r1rect = src.getBoundingClientRect();\n return new DragEvent(type, { bubbles: true, cancelable: true, clientX: r1rect.x + r1rect.width / 2, clientY: r1rect.y + r1rect.height / 2 });\n };\n src.dispatchEvent(make("dragstart"));\n tgt.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));\n tgt.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true }));\n tgt.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true }));\n src.dispatchEvent(new DragEvent("dragend", { bubbles: true }));\n return { dragged: true, source: String(sel().ref ?? sel().css ?? ""), target: String(dArgs.ref2 ?? dArgs.css2 ?? ""), source_matches_n: r1.matches_n, target_matches_n: r2.matches_n };\n }\n switch (command) {\n case "cp.state":\n return a.tree === false ? pageState() : buildStateTree();\n case "cp.snapshot":\n return buildStateTree();\n case "cp.find":\n return findElements({\n css: a.css,\n text: a.text,\n limit: a.limit,\n role: a.role,\n name: a.name,\n label: a.label,\n testid: a.testid,\n "text-max": a["text-max"]\n });\n case "cp.click":\n return clickElement();\n case "cp.type":\n return typeElement({ text: a.text });\n case "cp.goto":\n return goto({ url: a.url });\n case "cp.eval":\n return evalScript({ script: a.script });\n case "cp.fill":\n return fillElement({ text: a.text });\n case "cp.select":\n return selectElement({ label: a.label, value: a.value, option: a.option });\n case "cp.keys":\n return pressKeys({ keys: a.keys, key: a.key });\n case "cp.hover":\n return hoverElement();\n case "cp.focus":\n return focusElement();\n case "cp.dblclick":\n return dblclickElement();\n case "cp.check":\n return setChecked(true);\n case "cp.uncheck":\n return setChecked(false);\n case "cp.scroll":\n return scrollElement({ direction: a.direction, amount: a.amount });\n case "cp.back":\n return backNav();\n case "cp.get":\n return getElement({\n what: a.what,\n css: a.css,\n ref: a.ref,\n nth: a.nth,\n "as": a["as"],\n depth: a.depth,\n "children-max": a["children-max"],\n "text-max": a["text-max"],\n max: a.max,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n });\n case "cp.extract":\n return extractMarkdown({\n "chunk-size": a["chunk-size"],\n start: a.start,\n selector: a.selector\n });\n case "cp.frames":\n return listFrames();\n case "cp.screenshot":\n return screenshotMeta();\n case "cp.network":\n return networkEntriesLegacy({ filter: a.filter });\n case "cp.wait":\n return waitForCondition({\n selector: a.selector,\n text: a.text,\n time: a.time,\n timeout: a.timeout\n });\n case "cp.upload":\n return uploadFiles({ files: a.files });\n case "cp.drag":\n return dragElements({ ref2: a.ref2, css2: a.css2 });\n case "cp.title.marquee":\n return startTitleMarquee({ text: a.text, speed: a.speed });\n case "cp.title.restore":\n return stopTitleMarquee();\n default:\n return { error: `unknown_command: ${command}` };\n }\n }\n var RUNTIME_VERSION = "0.3.0-opencli-parity";\n globalThis.__cpRunControl = runControl;\n globalThis.__cpRuntimeVersion = RUNTIME_VERSION;\n})();\n';
1345
+ var control_runtime_source_default = '"use strict";\n(() => {\n // src/shared/control-runtime.ts\n function getRefStore() {\n const g = globalThis;\n if (!g.__cpRefs) {\n g.__cpRefs = { counter: 1, refs: /* @__PURE__ */ new Map(), cssPaths: /* @__PURE__ */ new Map() };\n }\n return g.__cpRefs;\n }\n function allocRef(el) {\n const store = getRefStore();\n for (const [ref2, existing] of store.refs) {\n if (existing === el) return ref2;\n }\n const ref = store.counter++;\n store.refs.set(ref, el);\n store.cssPaths.set(ref, uniqueCssPath(el));\n return ref;\n }\n function uniqueCssPath(el) {\n if (el.id) return `#${CSS.escape(el.id)}`;\n const parts = [];\n let cur = el;\n while (cur && cur !== document.body && cur !== document.documentElement) {\n let sel = cur.tagName.toLowerCase();\n if (cur.id) {\n parts.unshift(`#${CSS.escape(cur.id)}`);\n break;\n }\n const parent = cur.parentElement;\n if (parent) {\n const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);\n if (sameTag.length > 1) {\n const idx = sameTag.indexOf(cur) + 1;\n sel += `:nth-of-type(${idx})`;\n }\n }\n parts.unshift(sel);\n cur = parent;\n }\n return parts.join(" > ");\n }\n function err(code, message, extra) {\n return { error: { code, message, ...extra || {} } };\n }\n var ERROR_CODES = {\n NOT_FOUND: "not_found",\n STALE_REF: "stale_ref",\n INVALID_SELECTOR: "invalid_selector",\n SELECTOR_NOT_FOUND: "selector_not_found",\n SELECTOR_AMBIGUOUS: "selector_ambiguous",\n SELECTOR_NTH_OUT_OF_RANGE: "selector_nth_out_of_range",\n OPTION_NOT_FOUND: "option_not_found",\n NOT_A_SELECT: "not_a_select"\n };\n var IMPLICIT_ROLE = {\n a: "link",\n button: "button",\n select: "combobox",\n textarea: "textbox",\n h1: "heading",\n h2: "heading",\n h3: "heading",\n h4: "heading",\n h5: "heading",\n h6: "heading",\n img: "img",\n nav: "navigation",\n ul: "list",\n ol: "list",\n li: "listitem",\n main: "main",\n article: "article",\n aside: "complementary",\n header: "banner",\n footer: "contentinfo",\n form: "form",\n table: "table",\n summary: "button",\n option: "option",\n output: "status"\n };\n function roleOf(el) {\n const explicit = el.getAttribute("role");\n if (explicit) return explicit.split(/\\s+/)[0];\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const type = el.type || "text";\n if (type === "hidden") return null;\n if (type === "checkbox") return "checkbox";\n if (type === "radio") return "radio";\n if (type === "button" || type === "submit" || type === "reset") return "button";\n if (type === "range") return "slider";\n return "textbox";\n }\n if (tag === "progress") return "progressbar";\n if (tag === "meter") return "meter";\n return IMPLICIT_ROLE[tag] ?? null;\n }\n function accessibleName(el) {\n const aria = el.getAttribute("aria-label");\n if (aria) return aria.trim();\n const labelledby = el.getAttribute("aria-labelledby");\n if (labelledby) {\n const parts = labelledby.split(/\\s+/).map((id2) => document.getElementById(id2)?.textContent?.trim() || "");\n const joined = parts.filter(Boolean).join(" ");\n if (joined) return joined.slice(0, 120);\n }\n const id = el.getAttribute("id");\n if (id) {\n const label = document.querySelector(`label[for="${CSS.escape(id)}"]`);\n if (label?.textContent?.trim()) return label.textContent.trim().slice(0, 120);\n }\n if (el.tagName.toLowerCase() === "input") {\n const type = el.type;\n if (type === "button" || type === "submit" || type === "reset") {\n const v = el.value;\n if (v) return v;\n }\n }\n const alt = el.getAttribute("alt");\n if (alt) return alt;\n const title = el.getAttribute("title");\n if (title) return title;\n const placeholder = el.getAttribute("placeholder");\n if (placeholder) return placeholder;\n return (el.textContent || "").replace(/\\s+/g, " ").trim().slice(0, 120);\n }\n function isVisible(el) {\n if (!(el instanceof HTMLElement) && !(el instanceof SVGElement)) return false;\n const html = el;\n if (html.hasAttribute("hidden")) return false;\n if (html.closest("[hidden]")) return false;\n const style = window.getComputedStyle(html);\n if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") return false;\n if (el instanceof HTMLElement && el.offsetParent === null) {\n const pos = style.position;\n if (pos !== "fixed" && pos !== "sticky") return false;\n }\n return true;\n }\n var INTERACTIVE_SELECTOR = "a[href], button, input, textarea, select, summary, [role], [contenteditable=\'\'], [contenteditable=\'true\'], [onclick], [tabindex]";\n function isInteractive(el) {\n return el.matches(INTERACTIVE_SELECTOR);\n }\n function testidSelector(testid) {\n return `[data-testid="${CSS.escape(testid)}"], [data-test-id="${CSS.escape(testid)}"], [data-test="${CSS.escape(testid)}"], [data-cy="${CSS.escape(testid)}"]`;\n }\n function resolveTargetRich(sel) {\n const targetDesc = sel.ref != null ? String(sel.ref) : sel.css ?? (sel.role || sel.name || sel.label || sel.testid || sel.text ? `${sel.role ? `role=${sel.role} ` : ""}${sel.name ? `name=${sel.name} ` : ""}${sel.label ? `label=${sel.label} ` : ""}${sel.testid ? `testid=${sel.testid} ` : ""}${sel.text ? `text=${sel.text}` : ""}`.trim() : "");\n if (sel.ref != null) {\n const store = getRefStore();\n const el = store.refs.get(sel.ref);\n if (el && document.contains(el) && isVisible(el)) {\n return { el, matches_n: 1, match_level: "exact", target: targetDesc };\n }\n if (el && !document.contains(el)) {\n const cssPath = store.cssPaths.get(sel.ref);\n if (cssPath) {\n const re = document.querySelector(cssPath);\n if (re && isVisible(re)) {\n store.refs.set(sel.ref, re);\n return { el: re, matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n }\n return { errorEnvelope: err(ERROR_CODES.STALE_REF, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n return { errorEnvelope: err(ERROR_CODES.NOT_FOUND, "Numeric ref is no longer in the DOM. Re-run state.") };\n }\n let candidates = [];\n if (sel.css) {\n try {\n candidates = Array.from(document.querySelectorAll(sel.css));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: sel.css }) };\n }\n if (candidates.length === 0) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: sel.css, candidates: suggestCandidates() }) };\n }\n } else if (sel.testid) {\n try {\n candidates = Array.from(document.querySelectorAll(testidSelector(sel.testid)));\n } catch {\n return { errorEnvelope: err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.", { testid: sel.testid }) };\n }\n } else if (sel.role || sel.name || sel.label || sel.text) {\n const wantRole = sel.role ? sel.role.toLowerCase() : null;\n const wantName = sel.name ? sel.name.toLowerCase() : null;\n const wantText = sel.text ? sel.text.toLowerCase() : null;\n let labelTargets = [];\n if (sel.label) {\n const lower = sel.label.toLowerCase();\n labelTargets = Array.from(document.querySelectorAll("label")).filter((l) => (l.textContent || "").trim().toLowerCase().includes(lower)).map((l) => {\n const forId = l.getAttribute("for");\n if (forId) return document.getElementById(forId);\n return l.querySelector("input, select, textarea");\n }).filter((x) => !!x);\n }\n const all = Array.from(document.querySelectorAll("*"));\n candidates = all.filter((el) => {\n if (!isVisible(el)) return false;\n const role = roleOf(el);\n if (wantRole && (!role || role.toLowerCase() !== wantRole)) return false;\n if (wantName) {\n const name = accessibleName(el).toLowerCase();\n if (!name.includes(wantName)) return false;\n }\n if (wantText) {\n const t = (el.textContent || "").replace(/\\s+/g, " ").trim().toLowerCase();\n if (!t.includes(wantText)) return false;\n }\n return true;\n });\n if (labelTargets.length) {\n const set = new Set(labelTargets);\n candidates = labelTargets.concat(candidates.filter((c) => !set.has(c)));\n }\n if (candidates.length === 0) {\n return {\n errorEnvelope: err(\n ERROR_CODES.SELECTOR_NOT_FOUND,\n "Semantic locator matches 0 elements.",\n { role: sel.role, name: sel.name, label: sel.label, text: sel.text, candidates: suggestCandidates() }\n )\n };\n }\n } else {\n return { errorEnvelope: err("no_target", "Provide a numeric ref, CSS selector, or semantic locator (--role/--name/--label/--text/--testid).") };\n }\n const visible = candidates.filter(isVisible);\n const pool = visible.length ? visible : candidates;\n if (sel.nth != null) {\n if (sel.nth < 0 || sel.nth >= pool.length) {\n return { errorEnvelope: err(ERROR_CODES.SELECTOR_NTH_OUT_OF_RANGE, `--nth ${sel.nth} beyond match count ${pool.length}.`) };\n }\n const el = pool[sel.nth];\n return { el, matches_n: pool.length, match_level: "reidentified", target: targetDesc };\n }\n if (pool.length > 1) {\n return {\n errorEnvelope: err(ERROR_CODES.SELECTOR_AMBIGUOUS, `CSS matches ${pool.length} elements and no --nth given.`, {\n matches_n: pool.length,\n candidates: pool.slice(0, 5).map((el) => uniqueCssPath(el))\n })\n };\n }\n return { el: pool[0], matches_n: 1, match_level: "reidentified", target: targetDesc };\n }\n function suggestCandidates() {\n const store = getRefStore();\n const recent = Array.from(store.refs.entries()).slice(-5).map(([, el]) => uniqueCssPath(el));\n return recent;\n }\n function compoundOf(el) {\n const tag = el.tagName.toLowerCase();\n if (tag === "input") {\n const input = el;\n const type = input.type;\n if (["date", "time", "datetime-local", "month", "week"].includes(type)) {\n const formatMap = {\n date: "YYYY-MM-DD",\n time: "HH:mm",\n "datetime-local": "YYYY-MM-DDTHH:mm",\n month: "YYYY-MM",\n week: "YYYY-Www"\n };\n return {\n control: type,\n format: formatMap[type] ?? type,\n current: input.value || "",\n min: input.min || null,\n max: input.max || null\n };\n }\n if (type === "file") {\n const current = Array.from(input.files ?? []).map((f) => f.name);\n return { control: "file", multiple: input.multiple, current, accept: input.accept || null };\n }\n return null;\n }\n if (tag === "select") {\n const select = el;\n const options = Array.from(select.options).slice(0, 50).map((o) => ({\n label: (o.textContent || "").trim(),\n value: o.value,\n selected: o.selected\n }));\n return {\n control: "select",\n multiple: select.multiple,\n current: select.selectedOptions[0] ? (select.selectedOptions[0].textContent || "").trim() : "",\n options,\n options_total: select.options.length\n };\n }\n return null;\n }\n var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "style", "noscript", "meta", "link", "head", "base", "template", "path", "circle", "rect", "g", "defs", "polygon", "line"]);\n function describeElement(el, ref) {\n const tag = el.tagName.toLowerCase();\n const role = el.getAttribute("role") ?? null;\n const name = accessibleName(el);\n const parts = [];\n if (ref != null) parts.push(`[${ref}]`);\n parts.push(tag);\n if (role && role !== IMPLICIT_ROLE[tag]) parts.push(`role=${role}`);\n if (tag === "input") {\n const input = el;\n if (input.type && input.type !== "text") parts.push(`[${input.type}]`);\n if (input.disabled) parts.push("(disabled)");\n }\n if (name) parts.push(JSON.stringify(name.length > 60 ? name.slice(0, 57) + "\\u2026" : name));\n if (tag === "a") {\n const href = el.getAttribute("href");\n if (href) parts.push(`\\u2192 ${href.slice(0, 50)}`);\n }\n if (el.disabled) parts.push("(disabled)");\n return parts.join(" ");\n }\n function buildStateTree() {\n const lines = [];\n const compounds = {};\n const budget = { nodes: 0, chars: 0, truncated: false };\n const MAX_NODES = 400;\n const MAX_CHARS = 9e3;\n let hiddenInteractive = 0;\n function emit(depth, text) {\n const line = " ".repeat(Math.min(depth, 12)) + text;\n lines.push(line);\n budget.chars += line.length;\n }\n function walk(el, depth) {\n if (budget.nodes >= MAX_NODES || budget.chars >= MAX_CHARS) {\n budget.truncated = true;\n return;\n }\n const tag = el.tagName.toLowerCase();\n if (SKIP_TAGS.has(tag)) return;\n budget.nodes++;\n const visible = isVisible(el);\n const interactive = isInteractive(el);\n if (interactive) {\n if (visible) {\n const ref = allocRef(el);\n emit(depth, describeElement(el, ref));\n const compound = compoundOf(el);\n if (compound) compounds[String(ref)] = compound;\n } else {\n hiddenInteractive++;\n return;\n }\n } else if (visible) {\n if (/^h[1-6]$/.test(tag)) {\n const t = accessibleName(el);\n if (t) emit(depth, `${"#".repeat(Number(tag[1]))} ${t}`);\n } else if (tag === "p" || tag === "li" || tag === "td" || tag === "th" || tag === "figcaption" || tag === "blockquote") {\n const ownText = ownTextOf(el);\n if (ownText) emit(depth, ownText.slice(0, 100));\n } else if (tag === "img") {\n const alt = el.getAttribute("alt") || "";\n if (alt) emit(depth, `img ${JSON.stringify(alt.slice(0, 60))}`);\n }\n }\n if (depth < 14) {\n for (const child of Array.from(el.children)) {\n walk(child, depth + 1);\n if (budget.truncated) return;\n }\n }\n }\n function ownTextOf(el) {\n let text = "";\n for (const node of Array.from(el.childNodes)) {\n if (node.nodeType === Node.TEXT_NODE) text += node.textContent || "";\n }\n return text.replace(/\\s+/g, " ").trim();\n }\n const root = document.querySelector("main") || document.querySelector("#root") || document.body;\n emit(0, `# ${document.title}`);\n walk(root, 0);\n const scrollable = document.documentElement.scrollHeight > window.innerHeight + 200;\n if (scrollable) {\n emit(0, "\\u2195 page is scrollable \\u2014 run cp.scroll down to reveal more");\n }\n if (hiddenInteractive > 0) {\n emit(0, `\\u2139 ${hiddenInteractive} hidden interactive element(s) not tagged`);\n }\n if (budget.truncated) {\n emit(0, "\\u2026 (snapshot truncated by budget \\u2014 narrow with a selector or scroll)");\n }\n return {\n url: location.href,\n title: document.title,\n text: lines.join("\\n"),\n compounds,\n hints: { scroll: scrollable, hidden_interactive: hiddenInteractive },\n truncated: budget.truncated\n };\n }\n function runControl(command, args) {\n const a = args || {};\n function getVisibleText(el) {\n return (el.textContent || "").trim().replace(/\\s+/g, " ").slice(0, 120);\n }\n function getAttrs(el) {\n const attrs = {};\n for (const name of ["id", "class", "name", "type", "href", "src", "placeholder", "value", "role", "aria-label", "data-testid"]) {\n attrs[name] = el.getAttribute(name);\n }\n return attrs;\n }\n function sel() {\n return {\n ref: a.ref,\n css: a.css,\n nth: a.nth,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n };\n }\n function findElements(findArgs) {\n const limit = findArgs.limit ?? 20;\n const textMax = findArgs["text-max"] ?? 120;\n let elements = [];\n if (findArgs.css) {\n try {\n elements = Array.from(document.querySelectorAll(findArgs.css));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "CSS was rejected by querySelectorAll.", { selector: findArgs.css });\n }\n } else if (findArgs.testid) {\n try {\n elements = Array.from(document.querySelectorAll(testidSelector(findArgs.testid)));\n } catch {\n return err(ERROR_CODES.INVALID_SELECTOR, "Invalid testid.");\n }\n } else if (findArgs.role || findArgs.name || findArgs.label) {\n const r = resolveTargetRich({ role: findArgs.role, name: findArgs.name, label: findArgs.label, text: findArgs.text });\n if ("errorEnvelope" in r) return r.errorEnvelope;\n elements = document.querySelectorAll("*").length ? semanticCandidates(findArgs) : [];\n } else if (findArgs.text) {\n const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n const lower = findArgs.text.toLowerCase();\n const seen = /* @__PURE__ */ new Set();\n let node;\n while (node = walker.nextNode()) {\n if (node.textContent?.toLowerCase().includes(lower)) {\n const el = node.parentElement;\n if (el && !seen.has(el)) {\n seen.add(el);\n elements.push(el);\n if (elements.length >= limit) break;\n }\n }\n }\n } else {\n elements = Array.from(document.querySelectorAll(INTERACTIVE_SELECTOR));\n }\n const out = elements.slice(0, limit).map((el, i) => {\n const ref = allocRef(el);\n return {\n nth: i,\n ref,\n tag: el.tagName.toLowerCase(),\n role: roleOf(el),\n text: getVisibleText(el).slice(0, textMax),\n attrs: getAttrs(el),\n visible: isVisible(el),\n compound: compoundOf(el) ?? void 0\n };\n });\n return out;\n }\n function semanticCandidates(findArgs) {\n const wantRole = findArgs.role ? findArgs.role.toLowerCase() : null;\n const wantName = findArgs.name ? findArgs.name.toLowerCase() : null;\n const wantText = findArgs.text ? findArgs.text.toLowerCase() : null;\n const all = Array.from(document.querySelectorAll("*"));\n return all.filter((el) => {\n if (!isVisible(el)) return false;\n if (wantRole) {\n const role = roleOf(el);\n if (!role || role.toLowerCase() !== wantRole) return false;\n }\n if (wantName && !accessibleName(el).toLowerCase().includes(wantName)) return false;\n if (wantText && !(el.textContent || "").toLowerCase().includes(wantText)) return false;\n return true;\n });\n }\n function pageState() {\n return {\n url: location.href,\n title: document.title,\n width: window.innerWidth,\n height: window.innerHeight,\n devicePixelRatio: window.devicePixelRatio\n };\n }\n function goto(gotoArgs) {\n if (!gotoArgs.url) return { error: "url_required" };\n let sameUrl = false;\n try {\n sameUrl = new URL(gotoArgs.url, window.location.href).href === window.location.href;\n } catch {\n sameUrl = false;\n }\n if (sameUrl) {\n if (gotoArgs.reload === true) {\n window.location.reload();\n return { navigated: true, reloaded: true, url: gotoArgs.url };\n }\n return { navigated: false, reused: true, url: gotoArgs.url };\n }\n window.location.href = gotoArgs.url;\n return { navigated: true, url: gotoArgs.url };\n }\n function evalScript(evalArgs) {\n if (!evalArgs.script) return { error: "script_required" };\n try {\n const fn = new Function(evalArgs.script);\n const result = fn();\n return { result };\n } catch (e) {\n return { error: String(e) };\n }\n }\n function fillElement(fArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable) {\n const isContentEditable = el.isContentEditable;\n if (isContentEditable) {\n el.innerText = fArgs.text ?? "";\n } else {\n input.value = fArgs.text ?? "";\n }\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n const actual = isContentEditable ? el.innerText : input.value;\n return actual === (fArgs.text ?? "") ? { filled: true, verified: true, text: fArgs.text, actual, target, matches_n, match_level } : err("verify_failed", "Input value diverged after fill (controlled component?).", { actual });\n }\n return err("invalid_type", "fill only supports input, textarea, and contenteditable.");\n }\n function typeElement(tArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n el.focus({ preventScroll: true });\n input.value = tArgs.text ?? "";\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n let autocomplete = false;\n try {\n autocomplete = !!document.querySelector("[role=\'listbox\'], [role=\'combobox\'] [role=\'listbox\'], datalist, .ant-select-dropdown:not([style*=\'hidden\']), [id^=\'listbox\']");\n } catch {\n }\n return { typed: true, text: tArgs.text, target, matches_n, match_level, autocomplete };\n }\n function clickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.scrollIntoView({ block: "center" });\n if (a.full === true) {\n const rect = el.getBoundingClientRect();\n const opts = {\n bubbles: true,\n cancelable: true,\n view: window,\n clientX: rect.x + rect.width / 2,\n clientY: rect.y + rect.height / 2,\n button: 0,\n buttons: 1,\n pointerId: 1,\n pointerType: "mouse",\n isPrimary: true\n };\n el.dispatchEvent(new PointerEvent("pointerdown", opts));\n el.dispatchEvent(new MouseEvent("mousedown", opts));\n el.dispatchEvent(new PointerEvent("pointerup", { ...opts, buttons: 0 }));\n el.dispatchEvent(new MouseEvent("mouseup", { ...opts, buttons: 0 }));\n el.dispatchEvent(new MouseEvent("click", { ...opts, buttons: 0 }));\n return { clicked: true, full: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };\n }\n el.click();\n return { clicked: true, target, matches_n, match_level, tag: el.tagName.toLowerCase(), text: getVisibleText(el) };\n }\n function selectElement(sArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n if (el.tagName !== "SELECT") {\n return err(ERROR_CODES.NOT_A_SELECT, "select was called on a non-<select> element.", { tag: el.tagName.toLowerCase() });\n }\n const select = el;\n const want = sArgs.label ?? sArgs.value ?? sArgs.option;\n if (!want) return err("option_required", "Provide label or value.");\n const byLabel = Array.from(select.options).find((o) => (o.textContent || "").trim() === want);\n const byValue = Array.from(select.options).find((o) => o.value === want);\n const opt = byLabel ?? byValue;\n if (!opt) {\n const available = Array.from(select.options).slice(0, 50).map((o) => (o.textContent || "").trim());\n return err(ERROR_CODES.OPTION_NOT_FOUND, "Option not found.", { available });\n }\n select.value = opt.value;\n select.dispatchEvent(new Event("input", { bubbles: true }));\n select.dispatchEvent(new Event("change", { bubbles: true }));\n return { selected: true, value: select.value, target, matches_n, match_level };\n }\n function pressKeys(kArgs) {\n const keyCombo = kArgs.keys ?? kArgs.key ?? "";\n if (!keyCombo) return { error: "keys_required" };\n const comboParts = keyCombo.split("+").map((s) => s.trim());\n const main = comboParts[comboParts.length - 1];\n const ctrl = comboParts.includes("Control") || comboParts.includes("Ctrl") || comboParts.includes("Meta") || comboParts.includes("Cmd");\n const alt = comboParts.includes("Alt");\n const shift = comboParts.includes("Shift");\n const target = document.activeElement ?? document.body;\n const opts = { key: main, bubbles: true, cancelable: true, ctrlKey: ctrl, altKey: alt, shiftKey: shift, metaKey: comboParts.includes("Meta") || comboParts.includes("Cmd") };\n target.dispatchEvent(new KeyboardEvent("keydown", opts));\n target.dispatchEvent(new KeyboardEvent("keypress", opts));\n target.dispatchEvent(new KeyboardEvent("keyup", opts));\n return { pressed: true, key: keyCombo };\n }\n function hoverElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const rect = el.getBoundingClientRect();\n const opts = { bubbles: true, cancelable: true, clientX: rect.x + rect.width / 2, clientY: rect.y + rect.height / 2 };\n el.dispatchEvent(new MouseEvent("mouseover", opts));\n el.dispatchEvent(new MouseEvent("mousemove", opts));\n return { hovered: true, target, matches_n, match_level };\n }\n function focusElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.focus({ preventScroll: true });\n return { focused: true, target, matches_n, match_level };\n }\n function dblclickElement() {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n el.dispatchEvent(new MouseEvent("dblclick", { bubbles: true, cancelable: true }));\n return { dblclicked: true, target, matches_n, match_level };\n }\n function setChecked(want) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n const isAria = input.type !== "checkbox" && input.type !== "radio" && el.getAttribute("aria-checked") != null;\n if (!isAria && input.type !== "checkbox" && input.type !== "radio") {\n return err("invalid_type", "Target is not a checkbox/radio/aria-checked control.", { tag: el.tagName.toLowerCase() });\n }\n if (isAria) {\n el.setAttribute("aria-checked", String(want));\n el.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed: true, target, matches_n, match_level, kind: "aria" };\n }\n if (input.type === "radio" && !want) {\n return err("invalid_type", "Radio buttons cannot be unchecked directly; select another radio in the group.");\n }\n const changed = input.checked !== want;\n input.checked = want;\n input.dispatchEvent(new Event("input", { bubbles: true }));\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return { [want ? "checked" : "unchecked"]: true, changed, target, matches_n, match_level, kind: input.type };\n }\n function scrollElement(sArgs) {\n const dir = sArgs.direction ?? "down";\n const amount = sArgs.amount ?? 500;\n if (dir !== "up" && dir !== "down") return { error: "invalid_direction" };\n const dy = dir === "down" ? amount : -amount;\n window.scrollBy({ top: dy, behavior: "instant" });\n return {\n scrolled: true,\n direction: dir,\n amount,\n scrollY: Math.round(window.scrollY),\n scrollHeight: document.documentElement.scrollHeight\n };\n }\n function backNav() {\n history.back();\n return { back: true };\n }\n function htmlJsonTree(el, depth, maxDepth, childrenMax, textMax, truncated) {\n const children = Array.from(el.children);\n const ownText = (el.textContent || "").replace(/\\s+/g, " ").trim();\n const node = {\n tag: el.tagName.toLowerCase(),\n attrs: getAttrs(el),\n text: ownText.length > textMax ? (ownText.slice(0, textMax), truncated.text_truncated = (truncated.text_truncated || 0) + 1, ownText.slice(0, textMax)) : ownText,\n children: [],\n compound: compoundOf(el) ?? void 0\n };\n if (depth < maxDepth) {\n let kids = children;\n if (children.length > childrenMax) {\n truncated.children_dropped = (truncated.children_dropped || 0) + (children.length - childrenMax);\n kids = children.slice(0, childrenMax);\n }\n node.children = kids.map((c) => htmlJsonTree(c, depth + 1, maxDepth, childrenMax, textMax, truncated));\n } else if (children.length) {\n truncated.depth = (truncated.depth || 0) + 1;\n }\n return node;\n }\n function getElement(gArgs) {\n const what = gArgs.what ?? "text";\n if (what === "title") return { value: document.title, matches_n: 1, match_level: "exact" };\n if (what === "url") return { value: location.href, matches_n: 1, match_level: "exact" };\n if (what === "html") {\n if (gArgs["as"] === "json") {\n const root2 = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root2) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n const truncated = {};\n const tree = htmlJsonTree(root2, 0, gArgs.depth ?? 6, gArgs["children-max"] ?? 20, gArgs["text-max"] ?? 200, truncated);\n return {\n value: tree,\n truncated: Object.keys(truncated).length ? truncated : void 0\n };\n }\n const root = gArgs.css ? document.querySelector(gArgs.css) : document.body;\n if (!root) return err(ERROR_CODES.SELECTOR_NOT_FOUND, "CSS matches 0 elements.", { selector: gArgs.css });\n let html = root.outerHTML;\n if (gArgs.max && html.length > gArgs.max) html = html.slice(0, gArgs.max) + "\\u2026";\n return { value: html };\n }\n const r = resolveTargetRich(gArgs);\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level } = r;\n switch (what) {\n case "text": {\n const t = getVisibleText(el);\n return { value: t.length > 2e3 ? t.slice(0, 2e3) : t, matches_n, match_level };\n }\n case "value":\n return { value: el.value ?? el.innerText ?? null, matches_n, match_level };\n case "attributes":\n return { value: getAttrs(el), matches_n, match_level };\n default:\n return { error: `unknown_what: ${what}` };\n }\n }\n function extractMarkdown(eArgs) {\n const chunkSize = eArgs["chunk-size"] ?? 0;\n const start = eArgs.start ?? 0;\n const scope = eArgs.selector ? document.querySelector(eArgs.selector) : document.querySelector("main") || document.querySelector("article") || document.body;\n if (!scope) return { error: "scope_not_found" };\n const parts = [];\n const walker = document.createTreeWalker(scope, NodeFilter.SHOW_ELEMENT);\n let node;\n while (node = walker.nextNode()) {\n const el = node;\n const tag = el.tagName.toLowerCase();\n const text = getVisibleText(el);\n if (!text) continue;\n if (tag === "h1") parts.push(`# ${text}`);\n else if (tag === "h2") parts.push(`## ${text}`);\n else if (tag === "h3") parts.push(`### ${text}`);\n else if (tag === "p" || tag === "li") parts.push(text);\n else if (tag === "a") parts.push(`[${text}](${el.getAttribute("href") ?? ""})`);\n }\n const full = parts.join("\\n\\n");\n const sliced = chunkSize > 0 ? full.slice(start, start + chunkSize) : full.slice(start);\n return {\n url: location.href,\n title: document.title,\n selector: eArgs.selector ?? "auto",\n total_chars: full.length,\n chunk_size: chunkSize,\n start,\n end: start + sliced.length,\n next_start_char: start + sliced.length < full.length ? start + sliced.length : null,\n content: sliced\n };\n }\n function listFrames() {\n const frames = Array.from(document.querySelectorAll("iframe")).map((f, index) => {\n const src = f.getAttribute("src") ?? "";\n let crossOrigin = false;\n try {\n if (src && /^https?:/.test(src)) crossOrigin = new URL(src, location.href).origin !== location.origin;\n } catch {\n }\n return { index, url: src, name: f.getAttribute("name") ?? "", cross_origin: crossOrigin };\n });\n return { frames };\n }\n function screenshotMeta() {\n return {\n width: window.innerWidth,\n height: window.innerHeight,\n url: location.href,\n title: document.title\n };\n }\n function startTitleMarquee(tArgs) {\n const g = globalThis;\n if (g.__cpMarquee) {\n clearInterval(g.__cpMarquee.interval);\n } else {\n g.__cpMarquee = { original: document.title, interval: 0 };\n }\n const prefix = tArgs.text ?? "\\u{1F916}";\n const base = g.__cpMarquee.original;\n const full = `${prefix} ${base}`;\n const speed = Math.max(100, Math.min(2e3, tArgs.speed ?? 300));\n let pos = 0;\n const tick = () => {\n const display = full.slice(pos) + " " + full.slice(0, pos);\n document.title = display.slice(0, 60);\n pos = (pos + 1) % (full.length + 1 || 1);\n };\n tick();\n g.__cpMarquee.interval = window.setInterval(tick, speed);\n return { started: true, original: base, prefix, speed };\n }\n function stopTitleMarquee() {\n const g = globalThis;\n const state = g.__cpMarquee;\n if (!state) return { restored: false, reason: "no_active_marquee" };\n clearInterval(state.interval);\n document.title = state.original;\n delete g.__cpMarquee;\n return { restored: true, original: state.original };\n }\n function networkEntriesLegacy(nArgs) {\n const all = performance.getEntriesByType("resource").map((e) => ({\n url: e.name,\n type: e.initiatorType ?? "unknown",\n duration: Math.round(e.duration)\n }));\n let entries = all;\n if (nArgs.filter) {\n const keywords = nArgs.filter.split(",").map((s) => s.trim().toLowerCase());\n entries = all.filter((e) => keywords.some((kw) => e.url.toLowerCase().includes(kw)));\n }\n return { entries, count: entries.length, source: "performance-api" };\n }\n async function waitForCondition(wArgs) {\n const timeout = wArgs.timeout ?? 1e4;\n if (wArgs.time != null) {\n await new Promise((res) => setTimeout(res, Math.min(wArgs.time * 1e3, 3e4)));\n return { waited: true, kind: "time", seconds: wArgs.time };\n }\n const deadline = Date.now() + timeout;\n const pollInterval = 200;\n if (wArgs.selector) {\n while (Date.now() < deadline) {\n if (document.querySelector(wArgs.selector)) return { waited: true, kind: "selector", selector: wArgs.selector, elapsedMs: timeout - (deadline - Date.now()) };\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Selector not found within ${timeout}ms.`, { selector: wArgs.selector });\n }\n if (wArgs.text) {\n const lower = wArgs.text.toLowerCase();\n while (Date.now() < deadline) {\n if ((document.body.textContent || "").toLowerCase().includes(lower)) {\n return { waited: true, kind: "text", text: wArgs.text, elapsedMs: timeout - (deadline - Date.now()) };\n }\n await new Promise((res) => setTimeout(res, pollInterval));\n }\n return err("wait_timeout", `Text not found within ${timeout}ms.`, { text: wArgs.text });\n }\n return { error: "no_condition" };\n }\n function uploadFiles(uArgs) {\n const r = resolveTargetRich(sel());\n if ("errorEnvelope" in r) return r.errorEnvelope;\n const { el, matches_n, match_level, target } = r;\n const input = el;\n if (input.type !== "file") return err("invalid_type", "Target is not an input[type=file].", { tag: el.tagName.toLowerCase() });\n try {\n const dt = new DataTransfer();\n for (const name of uArgs.files ?? []) {\n dt.items.add(new File([""], name.split("/").pop() || name));\n }\n input.files = dt.files;\n } catch {\n }\n input.dispatchEvent(new Event("change", { bubbles: true }));\n return {\n uploaded: true,\n files: uArgs.files ?? [],\n file_names: (uArgs.files ?? []).map((f) => f.split("/").pop()),\n target,\n matches_n,\n match_level,\n multiple: input.multiple,\n accept: input.accept || null\n };\n }\n function dragElements(dArgs) {\n const r1 = resolveTargetRich(sel());\n if ("errorEnvelope" in r1) return r1.errorEnvelope;\n const r2 = resolveTargetRich({ ref: dArgs.ref2, css: dArgs.css2 });\n if ("errorEnvelope" in r2) return r2.errorEnvelope;\n const src = r1.el;\n const tgt = r2.el;\n const make = (type) => {\n const r1rect = src.getBoundingClientRect();\n return new DragEvent(type, { bubbles: true, cancelable: true, clientX: r1rect.x + r1rect.width / 2, clientY: r1rect.y + r1rect.height / 2 });\n };\n src.dispatchEvent(make("dragstart"));\n tgt.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));\n tgt.dispatchEvent(new DragEvent("dragover", { bubbles: true, cancelable: true }));\n tgt.dispatchEvent(new DragEvent("drop", { bubbles: true, cancelable: true }));\n src.dispatchEvent(new DragEvent("dragend", { bubbles: true }));\n return { dragged: true, source: String(sel().ref ?? sel().css ?? ""), target: String(dArgs.ref2 ?? dArgs.css2 ?? ""), source_matches_n: r1.matches_n, target_matches_n: r2.matches_n };\n }\n switch (command) {\n case "cp.state":\n return a.tree === false ? pageState() : buildStateTree();\n case "cp.snapshot":\n return buildStateTree();\n case "cp.find":\n return findElements({\n css: a.css,\n text: a.text,\n limit: a.limit,\n role: a.role,\n name: a.name,\n label: a.label,\n testid: a.testid,\n "text-max": a["text-max"]\n });\n case "cp.click":\n return clickElement();\n case "cp.type":\n return typeElement({ text: a.text });\n case "cp.goto":\n return goto({ url: a.url });\n case "cp.eval":\n return evalScript({ script: a.script });\n case "cp.fill":\n return fillElement({ text: a.text });\n case "cp.select":\n return selectElement({ label: a.label, value: a.value, option: a.option });\n case "cp.keys":\n return pressKeys({ keys: a.keys, key: a.key });\n case "cp.hover":\n return hoverElement();\n case "cp.focus":\n return focusElement();\n case "cp.dblclick":\n return dblclickElement();\n case "cp.check":\n return setChecked(true);\n case "cp.uncheck":\n return setChecked(false);\n case "cp.scroll":\n return scrollElement({ direction: a.direction, amount: a.amount });\n case "cp.back":\n return backNav();\n case "cp.get":\n return getElement({\n what: a.what,\n css: a.css,\n ref: a.ref,\n nth: a.nth,\n "as": a["as"],\n depth: a.depth,\n "children-max": a["children-max"],\n "text-max": a["text-max"],\n max: a.max,\n role: a.role,\n name: a.name,\n label: a.label,\n text: a.text,\n testid: a.testid\n });\n case "cp.extract":\n return extractMarkdown({\n "chunk-size": a["chunk-size"],\n start: a.start,\n selector: a.selector\n });\n case "cp.frames":\n return listFrames();\n case "cp.screenshot":\n return screenshotMeta();\n case "cp.network":\n return networkEntriesLegacy({ filter: a.filter });\n case "cp.wait":\n return waitForCondition({\n selector: a.selector,\n text: a.text,\n time: a.time,\n timeout: a.timeout\n });\n case "cp.upload":\n return uploadFiles({ files: a.files });\n case "cp.drag":\n return dragElements({ ref2: a.ref2, css2: a.css2 });\n case "cp.title.marquee":\n return startTitleMarquee({ text: a.text, speed: a.speed });\n case "cp.title.restore":\n return stopTitleMarquee();\n default:\n return { error: `unknown_command: ${command}` };\n }\n }\n var RUNTIME_VERSION = "0.3.0-opencli-parity";\n globalThis.__cpRunControl = runControl;\n globalThis.__cpRuntimeVersion = RUNTIME_VERSION;\n})();\n';
1003
1346
 
1004
1347
  // src/background/cdp.ts
1005
1348
  var attached = /* @__PURE__ */ new Set();
1349
+ function debuggerAttachCount() {
1350
+ return attached.size;
1351
+ }
1006
1352
  var CDP_COMMAND_TIMEOUT_MS = 15e3;
1007
1353
  function send(target, method, params, timeoutMs = CDP_COMMAND_TIMEOUT_MS) {
1008
1354
  return new Promise((resolve, reject) => {
@@ -1619,157 +1965,389 @@ ${script}
1619
1965
  })()`;
1620
1966
  }
1621
1967
 
1622
- // src/background/session-groups.ts
1623
- var STORAGE_KEY = "hmSessionGroups";
1624
- var cache = /* @__PURE__ */ new Map();
1625
- var hydrated = false;
1626
- async function hydrate() {
1627
- if (hydrated) return;
1628
- try {
1629
- const stored = await chrome.storage.session.get(STORAGE_KEY);
1630
- const state2 = stored[STORAGE_KEY] ?? {};
1631
- cache.clear();
1632
- for (const [sessionId, entries] of Object.entries(state2)) {
1633
- cache.set(sessionId, entries.map((e) => ({ ...e })));
1968
+ // ../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/session-groups.js
1969
+ function createSessionGroups(deps) {
1970
+ const titleMaxLength = deps.titleMaxLength ?? 20;
1971
+ const state2 = {};
1972
+ let hydrated = false;
1973
+ let listenerInstalled = false;
1974
+ let queue = Promise.resolve();
1975
+ async function hydrate() {
1976
+ if (hydrated)
1977
+ return;
1978
+ try {
1979
+ const stored = await chrome.storage.session.get(deps.storageKey);
1980
+ const incoming = stored[deps.storageKey] ?? {};
1981
+ for (const key of Object.keys(state2))
1982
+ delete state2[key];
1983
+ for (const [sessionId, entries] of Object.entries(incoming)) {
1984
+ state2[sessionId] = entries.map((e) => ({ ...e, tabIds: [...e.tabIds] }));
1985
+ }
1986
+ } catch (err3) {
1987
+ console.warn(`${deps.logPrefix} failed to hydrate session groups`, err3);
1988
+ } finally {
1989
+ hydrated = true;
1634
1990
  }
1635
- } catch (err3) {
1636
- console.warn("[HunterMate] failed to hydrate session groups", err3);
1637
- } finally {
1638
- hydrated = true;
1639
1991
  }
1640
- }
1641
- async function persist() {
1642
- const state2 = {};
1643
- for (const [sessionId, entries] of cache.entries()) {
1644
- state2[sessionId] = entries.map((e) => ({ ...e }));
1992
+ async function persist() {
1993
+ try {
1994
+ await chrome.storage.session.set({ [deps.storageKey]: state2 });
1995
+ } catch (err3) {
1996
+ console.warn(`${deps.logPrefix} failed to persist session groups`, err3);
1997
+ }
1645
1998
  }
1646
- try {
1647
- await chrome.storage.session.set({ [STORAGE_KEY]: state2 });
1648
- } catch (err3) {
1649
- console.warn("[HunterMate] failed to persist session groups", err3);
1999
+ function getEntries(sessionId) {
2000
+ return state2[sessionId] ?? [];
1650
2001
  }
1651
- }
1652
- function getSessionEntries(sessionId) {
1653
- return cache.get(sessionId) ?? [];
1654
- }
1655
- async function setSessionEntries(sessionId, entries) {
1656
- if (entries.length === 0) {
1657
- cache.delete(sessionId);
1658
- } else {
1659
- cache.set(sessionId, entries);
2002
+ function setEntries(sessionId, entries) {
2003
+ if (entries.length === 0) {
2004
+ delete state2[sessionId];
2005
+ } else {
2006
+ state2[sessionId] = entries;
2007
+ }
1660
2008
  }
1661
- await persist();
1662
- }
1663
- function hasExplicitTabTarget(args) {
1664
- return ["tab", "tabId", "page", "targetId"].some((k) => args[k] !== void 0);
1665
- }
1666
- function buildGroupTitle(args, sessionId) {
1667
- const raw = String(args.title ?? args.summary ?? args.taskTitle ?? args.taskName ?? "").trim();
1668
- const compact = raw ? raw.replace(/\s+/g, " ").slice(0, 12) : sessionId.slice(0, 8);
1669
- return `\u{1F916}${compact}`;
1670
- }
1671
- async function ensureSessionGroup(sessionId, title, tab) {
1672
- await hydrate();
1673
- if (!tab.id || tab.windowId == null) {
1674
- throw new Error("ensureSessionGroup_requires_tab_id_and_window_id");
1675
- }
1676
- const entries = getSessionEntries(sessionId);
1677
- const existing = entries.find((e) => e.windowId === tab.windowId);
1678
- if (existing) {
2009
+ function installTabCloseListener() {
2010
+ if (listenerInstalled)
2011
+ return;
2012
+ listenerInstalled = true;
2013
+ chrome.tabs.onRemoved.addListener((tabId) => {
2014
+ void (async () => {
2015
+ await hydrate();
2016
+ let dirty = false;
2017
+ for (const sessionId of Object.keys(state2)) {
2018
+ const entries = state2[sessionId];
2019
+ for (const entry of entries) {
2020
+ const before = entry.tabIds.length;
2021
+ entry.tabIds = entry.tabIds.filter((id) => id !== tabId);
2022
+ if (entry.tabIds.length !== before)
2023
+ dirty = true;
2024
+ }
2025
+ const cleaned = entries.filter((e) => e.tabIds.length > 0);
2026
+ if (cleaned.length !== entries.length)
2027
+ dirty = true;
2028
+ setEntries(sessionId, cleaned);
2029
+ }
2030
+ if (dirty)
2031
+ await persist();
2032
+ })().catch((err3) => console.warn(`${deps.logPrefix} evictClosedTab failed`, err3));
2033
+ });
2034
+ }
2035
+ function hasExplicitTabTarget2(args) {
2036
+ return ["tab", "tabId", "page", "targetId"].some((k) => args[k] !== void 0);
2037
+ }
2038
+ function buildGroupTitle2(args, sessionId) {
2039
+ const raw = String(args.label ?? args.title ?? args.summary ?? args.taskTitle ?? args.taskName ?? "").trim();
2040
+ const compact = raw ? raw.replace(/\s+/g, " ").slice(0, titleMaxLength) : sessionId.slice(0, 8);
2041
+ return `${deps.titlePrefix ?? ""}${compact}`;
2042
+ }
2043
+ function normalizeGroupTitle2(title) {
2044
+ return title.trim().replace(/\s+/g, " ").toLowerCase();
2045
+ }
2046
+ async function findGroupByTitle(windowId, title) {
2047
+ const wanted = normalizeGroupTitle2(title);
2048
+ if (!wanted)
2049
+ return void 0;
2050
+ let groups2;
1679
2051
  try {
1680
- await chrome.tabGroups.get(existing.groupId);
1681
- } catch {
1682
- const cleaned = entries.filter((e) => e.groupId !== existing.groupId);
1683
- await setSessionEntries(sessionId, cleaned);
1684
- return ensureSessionGroup(sessionId, title, tab);
2052
+ groups2 = await chrome.tabGroups.query({ windowId });
2053
+ } catch (err3) {
2054
+ console.warn(`${deps.logPrefix} failed to query tab groups for adoption`, err3);
2055
+ return void 0;
2056
+ }
2057
+ return groups2.find((g) => normalizeGroupTitle2(g.title ?? "") === wanted);
2058
+ }
2059
+ async function ensureSessionGroupUnqueued(sessionId, title, tab) {
2060
+ await hydrate();
2061
+ installTabCloseListener();
2062
+ if (!tab.id || tab.windowId == null) {
2063
+ throw new Error("ensureSessionGroup_requires_tab_id_and_window_id");
1685
2064
  }
1686
- if (tab.groupId !== existing.groupId) {
2065
+ const tabId = tab.id;
2066
+ const windowId = tab.windowId;
2067
+ const color = deps.colorForSession ? deps.colorForSession(sessionId) : "grey";
2068
+ const entries = getEntries(sessionId);
2069
+ const existing = entries.find((e) => e.windowId === windowId);
2070
+ if (existing) {
1687
2071
  try {
1688
- await chrome.tabs.group({ groupId: existing.groupId, tabIds: [tab.id] });
1689
- } catch (err3) {
1690
- console.warn("[HunterMate] failed to add tab to existing session group", { sessionId, groupId: existing.groupId, tabId: tab.id, err: err3 });
2072
+ await chrome.tabGroups.get(existing.groupId);
2073
+ } catch {
2074
+ setEntries(sessionId, entries.filter((e) => e.groupId !== existing.groupId));
2075
+ await persist();
2076
+ return ensureSessionGroupUnqueued(sessionId, title, tab);
1691
2077
  }
2078
+ if (tab.groupId !== existing.groupId) {
2079
+ try {
2080
+ await chrome.tabs.group({ groupId: existing.groupId, tabIds: [tabId] });
2081
+ } catch (err3) {
2082
+ console.warn(`${deps.logPrefix} failed to add tab to existing session group`, {
2083
+ sessionId,
2084
+ groupId: existing.groupId,
2085
+ tabId,
2086
+ err: err3
2087
+ });
2088
+ }
2089
+ }
2090
+ if (title && title !== existing.title) {
2091
+ try {
2092
+ await chrome.tabGroups.update(existing.groupId, deps.colorForSession ? { title, color } : { title });
2093
+ existing.title = title;
2094
+ await persist();
2095
+ } catch (err3) {
2096
+ console.warn(`${deps.logPrefix} failed to update session group title`, {
2097
+ sessionId,
2098
+ groupId: existing.groupId,
2099
+ err: err3
2100
+ });
2101
+ }
2102
+ }
2103
+ if (!existing.tabIds.includes(tabId)) {
2104
+ existing.tabIds.push(tabId);
2105
+ await persist();
2106
+ }
2107
+ deps.onGroupInfo?.(tabId, {
2108
+ groupId: existing.groupId,
2109
+ groupColor: color,
2110
+ groupTitle: existing.title
2111
+ });
2112
+ return { groupId: existing.groupId, color, created: false };
1692
2113
  }
1693
- if (title && title !== existing.title) {
1694
- try {
1695
- await chrome.tabGroups.update(existing.groupId, { title });
1696
- existing.title = title;
2114
+ if (title) {
2115
+ const adoptable = await findGroupByTitle(windowId, title);
2116
+ if (adoptable) {
2117
+ if (tab.groupId !== adoptable.id) {
2118
+ try {
2119
+ await chrome.tabs.group({ groupId: adoptable.id, tabIds: [tabId] });
2120
+ } catch (err3) {
2121
+ console.warn(`${deps.logPrefix} failed to adopt tab into existing group`, {
2122
+ sessionId,
2123
+ groupId: adoptable.id,
2124
+ tabId,
2125
+ err: err3
2126
+ });
2127
+ }
2128
+ }
2129
+ const entry2 = {
2130
+ groupId: adoptable.id,
2131
+ windowId,
2132
+ title: adoptable.title || title,
2133
+ tabIds: [tabId],
2134
+ createdAt: Date.now()
2135
+ };
2136
+ entries.push(entry2);
2137
+ setEntries(sessionId, entries);
1697
2138
  await persist();
1698
- } catch (err3) {
1699
- console.warn("[HunterMate] failed to update session group title", { sessionId, groupId: existing.groupId, err: err3 });
2139
+ console.info(`${deps.logPrefix} session group adopted by title`, {
2140
+ sessionId,
2141
+ groupId: adoptable.id,
2142
+ title: entry2.title
2143
+ });
2144
+ deps.onGroupInfo?.(tabId, {
2145
+ groupId: adoptable.id,
2146
+ groupColor: adoptable.color,
2147
+ groupTitle: entry2.title
2148
+ });
2149
+ return { groupId: adoptable.id, color: adoptable.color, created: false, adopted: true };
1700
2150
  }
1701
2151
  }
1702
- if (!existing.tabIds.includes(tab.id)) {
1703
- existing.tabIds.push(tab.id);
1704
- await persist();
2152
+ let groupId;
2153
+ try {
2154
+ groupId = await chrome.tabs.group({ tabIds: [tabId] });
2155
+ } catch (err3) {
2156
+ console.warn(`${deps.logPrefix} failed to create session group`, { sessionId, tabId, err: err3 });
2157
+ throw err3;
2158
+ }
2159
+ try {
2160
+ await chrome.tabGroups.update(groupId, {
2161
+ title,
2162
+ color,
2163
+ collapsed: deps.collapsedOnCreate ?? false
2164
+ });
2165
+ } catch (err3) {
2166
+ console.warn(`${deps.logPrefix} failed to update newly created session group`, {
2167
+ sessionId,
2168
+ groupId,
2169
+ err: err3
2170
+ });
1705
2171
  }
1706
- return { groupId: existing.groupId, created: false };
2172
+ const entry = {
2173
+ groupId,
2174
+ windowId,
2175
+ title,
2176
+ tabIds: [tabId],
2177
+ createdAt: Date.now()
2178
+ };
2179
+ entries.push(entry);
2180
+ setEntries(sessionId, entries);
2181
+ await persist();
2182
+ deps.onGroupInfo?.(tabId, { groupId, groupColor: color, groupTitle: title });
2183
+ return { groupId, color, created: true };
1707
2184
  }
1708
- let groupId;
1709
- try {
1710
- groupId = await chrome.tabs.group({ tabIds: [tab.id] });
1711
- } catch (err3) {
1712
- console.warn("[HunterMate] failed to create session group", { sessionId, tabId: tab.id, err: err3 });
1713
- throw err3;
2185
+ function ensureSessionGroup2(sessionId, title, tab) {
2186
+ const result = queue.then(() => ensureSessionGroupUnqueued(sessionId, title, tab), () => ensureSessionGroupUnqueued(sessionId, title, tab));
2187
+ queue = result.catch(() => void 0);
2188
+ return result;
1714
2189
  }
1715
- try {
1716
- await chrome.tabGroups.update(groupId, {
1717
- title,
1718
- color: "grey",
1719
- collapsed: false
1720
- });
1721
- } catch (err3) {
1722
- console.warn("[HunterMate] failed to update newly created session group", { sessionId, groupId, err: err3 });
2190
+ async function addTabToSessionGroup2(sessionId, title, tab) {
2191
+ if (!sessionId || !tab.id || tab.windowId == null) {
2192
+ return { created: false };
2193
+ }
2194
+ try {
2195
+ const result = await ensureSessionGroup2(sessionId, title, tab);
2196
+ return result;
2197
+ } catch (err3) {
2198
+ console.warn(`${deps.logPrefix} addTabToSessionGroup failed`, {
2199
+ sessionId,
2200
+ tabId: tab.id,
2201
+ err: err3
2202
+ });
2203
+ return { created: false };
2204
+ }
1723
2205
  }
1724
- const entry = {
1725
- groupId,
1726
- windowId: tab.windowId,
1727
- title,
1728
- tabIds: [tab.id],
1729
- createdAt: Date.now()
2206
+ return {
2207
+ hasExplicitTabTarget: hasExplicitTabTarget2,
2208
+ buildGroupTitle: buildGroupTitle2,
2209
+ ensureSessionGroup: ensureSessionGroup2,
2210
+ addTabToSessionGroup: addTabToSessionGroup2,
2211
+ normalizeGroupTitle: normalizeGroupTitle2
1730
2212
  };
1731
- entries.push(entry);
1732
- await setSessionEntries(sessionId, entries);
1733
- return { groupId, created: true };
1734
2213
  }
1735
- async function addTabToSessionGroup(sessionId, title, tab) {
1736
- if (!sessionId || !tab.id || tab.windowId == null) {
1737
- return { created: false };
1738
- }
1739
- try {
1740
- const result = await ensureSessionGroup(sessionId, title, tab);
1741
- return result;
1742
- } catch (err3) {
1743
- console.warn("[HunterMate] addTabToSessionGroup failed", { sessionId, tabId: tab.id, err: err3 });
1744
- return { created: false };
2214
+
2215
+ // src/background/session-groups.ts
2216
+ var groups = createSessionGroups({
2217
+ storageKey: "hmSessionGroups",
2218
+ logPrefix: "[HunterMate]",
2219
+ titlePrefix: "\u{1F916}",
2220
+ titleMaxLength: 20,
2221
+ collapsedOnCreate: false
2222
+ });
2223
+ var {
2224
+ hasExplicitTabTarget,
2225
+ buildGroupTitle,
2226
+ ensureSessionGroup,
2227
+ addTabToSessionGroup,
2228
+ normalizeGroupTitle
2229
+ } = groups;
2230
+
2231
+ // ../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/tab-recycle.js
2232
+ function createTabRecycler(deps) {
2233
+ const recycleGroupTitle = deps.groupTitle ?? "\u{1F4E6}\u5DF2\u7528\u5B8C";
2234
+ const now = deps.now ?? Date.now;
2235
+ const ALARM_NAME = "tab-recycle";
2236
+ let alarmInstalled = false;
2237
+ function matchPattern(url) {
2238
+ if (!url)
2239
+ return false;
2240
+ return deps.urlPatterns.some((p) => p.test(url));
1745
2241
  }
1746
- }
1747
- async function evictClosedTab(tabId) {
1748
- await hydrate();
1749
- let dirty = false;
1750
- for (const [sessionId, entries] of cache.entries()) {
1751
- for (const entry of entries) {
1752
- const before = entry.tabIds.length;
1753
- entry.tabIds = entry.tabIds.filter((id) => id !== tabId);
1754
- if (entry.tabIds.length !== before) {
1755
- dirty = true;
1756
- }
2242
+ function isBusy(tab) {
2243
+ if (tab.active || tab.audible)
2244
+ return true;
2245
+ if (deps.isTabBusy)
2246
+ return deps.isTabBusy(tab, now());
2247
+ return false;
2248
+ }
2249
+ async function sweep() {
2250
+ const tabs = await chrome.tabs.query({});
2251
+ const matched = tabs.filter((t) => t.id != null && matchPattern(t.url));
2252
+ const candidates = matched.filter((t) => !isBusy(t));
2253
+ const result = {
2254
+ moved: 0,
2255
+ busy: matched.length - candidates.length
2256
+ };
2257
+ if (candidates.length === 0) {
2258
+ deps.onSweepResult?.(result);
2259
+ return result;
1757
2260
  }
1758
- const cleaned = entries.filter((e) => e.tabIds.length > 0);
1759
- if (cleaned.length !== entries.length) {
1760
- dirty = true;
2261
+ const byWindow = /* @__PURE__ */ new Map();
2262
+ const tabById = /* @__PURE__ */ new Map();
2263
+ for (const t of candidates) {
2264
+ const w = t.windowId ?? -1;
2265
+ if (!byWindow.has(w))
2266
+ byWindow.set(w, []);
2267
+ byWindow.get(w).push(t.id);
2268
+ tabById.set(t.id, t);
1761
2269
  }
1762
- cache.set(sessionId, cleaned);
2270
+ for (const [windowId, tabIds] of byWindow) {
2271
+ try {
2272
+ const existing = (await chrome.tabGroups.query({ windowId })).find((g) => g.title === recycleGroupTitle);
2273
+ const freshIds = existing ? tabIds.filter((id) => tabById.get(id)?.groupId !== existing.id) : tabIds;
2274
+ if (freshIds.length === 0)
2275
+ continue;
2276
+ let groupId;
2277
+ if (existing) {
2278
+ groupId = existing.id;
2279
+ await chrome.tabs.group({ groupId, tabIds: freshIds });
2280
+ } else {
2281
+ groupId = await chrome.tabs.group({ tabIds: [freshIds[0]] });
2282
+ await chrome.tabGroups.update(groupId, {
2283
+ title: recycleGroupTitle,
2284
+ color: "grey",
2285
+ collapsed: true
2286
+ });
2287
+ const rest = freshIds.slice(1);
2288
+ if (rest.length)
2289
+ await chrome.tabs.group({ groupId, tabIds: rest });
2290
+ }
2291
+ result.moved += freshIds.length;
2292
+ result.groupId = groupId;
2293
+ console.info(`${deps.logPrefix} tab-recycle swept`, { windowId, count: freshIds.length, groupId });
2294
+ } catch (err3) {
2295
+ console.warn(`${deps.logPrefix} tab-recycle sweep failed`, { windowId, err: err3 });
2296
+ }
2297
+ }
2298
+ deps.onSweepResult?.(result);
2299
+ return result;
1763
2300
  }
1764
- if (dirty) {
1765
- await persist();
2301
+ function installAlarm(periodMinutes = 10) {
2302
+ if (alarmInstalled)
2303
+ return;
2304
+ alarmInstalled = true;
2305
+ chrome.alarms.create(ALARM_NAME, { periodInMinutes: periodMinutes });
2306
+ chrome.alarms.onAlarm.addListener((alarm) => {
2307
+ if (alarm.name !== ALARM_NAME)
2308
+ return;
2309
+ void sweep().catch((err3) => console.warn(`${deps.logPrefix} tab-recycle alarm sweep failed`, err3));
2310
+ });
2311
+ }
2312
+ async function findReusableTab(url) {
2313
+ if (!/^https?:/.test(url))
2314
+ return void 0;
2315
+ const tabs = await chrome.tabs.query({ url });
2316
+ const idle = tabs.find((t) => t.id != null && !t.active && !t.audible && !isBusy(t));
2317
+ return idle?.id;
1766
2318
  }
2319
+ return { sweep, installAlarm, findReusableTab, recycleGroupTitle };
1767
2320
  }
1768
- chrome.tabs.onRemoved.addListener((tabId) => {
1769
- evictClosedTab(tabId).catch((err3) => console.warn("[HunterMate] evictClosedTab failed", err3));
2321
+
2322
+ // src/background/tab-recycle.ts
2323
+ var DETAIL_PATHS = [
2324
+ "https://h.liepin.com/resume/showresumedetail/",
2325
+ "https://h.liepin.com/job/gethjobdetail"
2326
+ ];
2327
+ var tabRecycler = createTabRecycler({
2328
+ logPrefix: "[HunterMate]",
2329
+ urlPatterns: DETAIL_PATHS.map((p) => new RegExp("^" + p.replace(/[/.]/g, (c) => c === "/" ? "\\/" : "\\.")))
1770
2330
  });
2331
+ function isDetailUrl(url) {
2332
+ return DETAIL_PATHS.some((p) => url.startsWith(p));
2333
+ }
2334
+ async function findDetailTabToReuse(url) {
2335
+ if (!isDetailUrl(url)) return void 0;
2336
+ const prefix = DETAIL_PATHS.find((p) => url.startsWith(p));
2337
+ const candidates = await chrome.tabs.query({ url: prefix + "*" }).catch(() => []);
2338
+ const idle = candidates.find((t) => t.id != null && !t.active && !t.audible);
2339
+ return idle?.id;
2340
+ }
1771
2341
 
1772
2342
  // src/background/control.ts
2343
+ var RUNTIME_BRIDGE_FILE = "content/control-runtime.js";
2344
+ async function invokeRuntimeBridge(command, args) {
2345
+ const rt = globalThis.__hmControlRuntime;
2346
+ if (!rt || typeof rt.runControl !== "function") {
2347
+ return { error: "runtime_bridge_missing: content/control-runtime.js \u672A\u6CE8\u5165\u6210\u529F" };
2348
+ }
2349
+ return await Promise.resolve(rt.runControl(command, args));
2350
+ }
1773
2351
  var boundTabId = null;
1774
2352
  async function getActiveTab() {
1775
2353
  const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
@@ -1810,10 +2388,35 @@ async function frameListCdp(tabId) {
1810
2388
  walk(tree?.frameTree);
1811
2389
  return { frames: out };
1812
2390
  }
1813
- function buildRuntimeExpression(command, args) {
1814
- return '(() => { try { if (typeof globalThis.__cpRunControl !== "function" || globalThis.__cpRuntimeVersion !== ' + JSON.stringify(RUNTIME_VERSION) + ") {\n" + control_runtime_source_default + "\n} const r = globalThis.__cpRunControl(" + JSON.stringify(command) + ", " + JSON.stringify(args) + '); if (r && typeof r.then === "function") { return r; } return r; } catch (e) { return { __cpError: String((e && e.message) || e) }; } })()';
1815
- }
1816
2391
  var marqueeCounters = /* @__PURE__ */ new Map();
2392
+ var MARQUEE_START_PAGE = (text) => {
2393
+ const g = globalThis;
2394
+ if (g.__cpMarquee) {
2395
+ clearInterval(g.__cpMarquee.interval);
2396
+ } else {
2397
+ g.__cpMarquee = { original: document.title, interval: 0 };
2398
+ }
2399
+ const prefix = text ?? "\u{1F916}";
2400
+ const base = g.__cpMarquee.original;
2401
+ const full = `${prefix} ${base}`;
2402
+ const speed = 300;
2403
+ let pos = 0;
2404
+ const tick = () => {
2405
+ const display = full.slice(pos) + " " + full.slice(0, pos);
2406
+ document.title = display.slice(0, 60);
2407
+ pos = (pos + 1) % (full.length + 1 || 1);
2408
+ };
2409
+ tick();
2410
+ g.__cpMarquee.interval = window.setInterval(tick, speed);
2411
+ };
2412
+ var MARQUEE_STOP_PAGE = () => {
2413
+ const g = globalThis;
2414
+ const state2 = g.__cpMarquee;
2415
+ if (!state2) return;
2416
+ clearInterval(state2.interval);
2417
+ document.title = state2.original;
2418
+ delete g.__cpMarquee;
2419
+ };
1817
2420
  async function startTitleMarquee(tabId, sessionId, text) {
1818
2421
  if (!isAttachableUrl((await chrome.tabs.get(tabId).catch(() => null))?.url)) {
1819
2422
  return;
@@ -1825,8 +2428,11 @@ async function startTitleMarquee(tabId, sessionId, text) {
1825
2428
  }
1826
2429
  marqueeCounters.set(tabId, { count: 1, sessionId });
1827
2430
  try {
1828
- const expression = buildRuntimeExpression("cp.title.marquee", { text: text ?? "\u{1F916}" });
1829
- await cdpEvaluateTransient(tabId, expression);
2431
+ await chrome.scripting.executeScript({
2432
+ target: { tabId },
2433
+ func: MARQUEE_START_PAGE,
2434
+ args: [text ?? "\u{1F916}"]
2435
+ });
1830
2436
  } catch (err3) {
1831
2437
  console.warn("[HunterMate] startTitleMarquee failed", { tabId, error: err3.message });
1832
2438
  }
@@ -1838,8 +2444,7 @@ async function stopTitleMarquee(tabId, _sessionId) {
1838
2444
  if (entry.count > 0) return;
1839
2445
  marqueeCounters.delete(tabId);
1840
2446
  try {
1841
- const expression = buildRuntimeExpression("cp.title.restore", {});
1842
- await cdpEvaluateTransient(tabId, expression);
2447
+ await chrome.scripting.executeScript({ target: { tabId }, func: MARQUEE_STOP_PAGE, args: [] });
1843
2448
  } catch (err3) {
1844
2449
  console.warn("[HunterMate] stopTitleMarquee failed", { tabId, error: err3.message });
1845
2450
  }
@@ -2069,7 +2674,7 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2069
2674
  return err2(e instanceof Error ? e.message : String(e));
2070
2675
  }
2071
2676
  }
2072
- if (command === "cp.eval" && typeof controlArgs.script === "string") {
2677
+ if (command === "cp.eval" && typeof controlArgs.script === "string" && controlArgs.debuggerFree !== true) {
2073
2678
  const script = controlArgs.script;
2074
2679
  const frame = controlArgs.frame;
2075
2680
  const tabId = tab.id;
@@ -2129,7 +2734,7 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2129
2734
  if (command.startsWith("hm.")) {
2130
2735
  return { status: "error", code: "not_implemented", message: `${command} is not implemented yet` };
2131
2736
  }
2132
- if (isAttachableUrl(tab.url)) {
2737
+ if (isAttachableUrl(tab.url) && controlArgs.debuggerFree !== true) {
2133
2738
  log("debug", "control running via CDP runtime", { command, tabId: tab.id });
2134
2739
  try {
2135
2740
  const result = await cdpEvaluate(tab.id, buildInjectionExpression(command, controlArgs));
@@ -2147,9 +2752,13 @@ async function executeControlCommand(rawCommand, rawArgs, options = {}) {
2147
2752
  `Cannot access a ${tab.url.split("/")[0]}// page for control commands. Open the target page in a normal tab first.`
2148
2753
  );
2149
2754
  }
2755
+ await chrome.scripting.executeScript({
2756
+ target: { tabId: tab.id },
2757
+ files: [RUNTIME_BRIDGE_FILE]
2758
+ });
2150
2759
  const [injection] = await chrome.scripting.executeScript({
2151
2760
  target: { tabId: tab.id },
2152
- func: runControl,
2761
+ func: invokeRuntimeBridge,
2153
2762
  args: [command, controlArgs]
2154
2763
  });
2155
2764
  const inj = injection;
@@ -3443,10 +4052,16 @@ async function handleTabCommand(command, args, log) {
3443
4052
  case "cp.tab.new": {
3444
4053
  const url = typeof args.url === "string" && args.url ? args.url : "about:blank";
3445
4054
  const active = args.active === true;
4055
+ const reusable = await findDetailTabToReuse(url).catch(() => void 0);
4056
+ if (reusable != null) {
4057
+ await chrome.tabs.update(reusable, { url, active });
4058
+ log("info", "cp.tab.new reused detail tab", { page: String(reusable), url, active });
4059
+ return { page: String(reusable), url, reused: true };
4060
+ }
3446
4061
  log("debug", "cp.tab.new creating tab", { url, active });
3447
4062
  const tab = await chrome.tabs.create({ url, active });
3448
4063
  log("info", "cp.tab.new created", { page: String(tab.id), url: tab.url ?? url, active });
3449
- return { page: String(tab.id), url: tab.url ?? url };
4064
+ return { page: String(tab.id), url: tab.url ?? url, reused: false };
3450
4065
  }
3451
4066
  case "cp.tab.open": {
3452
4067
  const url = typeof args.url === "string" && args.url.trim() ? args.url.trim() : "";
@@ -3522,6 +4137,7 @@ function etldPlusOne(hostname) {
3522
4137
  }
3523
4138
 
3524
4139
  // src/background/sites.ts
4140
+ var import_shared = __toESM(require_dist());
3525
4141
  var DYNAMIC_SCRIPT_ID = "hm-auto-capture";
3526
4142
  var RECRUITING_HINTS = [
3527
4143
  "jobs",
@@ -3556,6 +4172,7 @@ var PATH_CLASS_HINTS = [
3556
4172
  ];
3557
4173
  var sitePolicyMap = /* @__PURE__ */ new Map();
3558
4174
  function getSitePolicy(hostname) {
4175
+ if ((0, import_shared.isHarnessInternalHostname)(hostname)) return { record: false, parse: false };
3559
4176
  return sitePolicyMap.get(etldPlusOne(hostname)) ?? { record: false, parse: false };
3560
4177
  }
3561
4178
  async function syncSites() {
@@ -3828,13 +4445,13 @@ async function createHttpTransport(opts) {
3828
4445
  }
3829
4446
 
3830
4447
  // src/shared/queue.ts
3831
- var STORAGE_KEY2 = "hunter-mate:logs-pending";
4448
+ var STORAGE_KEY = "hunter-mate:logs-pending";
3832
4449
  var ChromeStorageLogQueue = class {
3833
4450
  buffer = [];
3834
4451
  nextId = 1;
3835
4452
  maxBytes = 2 * 1024 * 1024;
3836
4453
  async load() {
3837
- const stored = await chromeStorageLocalGet(STORAGE_KEY2);
4454
+ const stored = await chromeStorageLocalGet(STORAGE_KEY);
3838
4455
  if (!stored) return;
3839
4456
  try {
3840
4457
  const parsed = JSON.parse(stored);
@@ -3859,10 +4476,10 @@ var ChromeStorageLogQueue = class {
3859
4476
  }
3860
4477
  async persist() {
3861
4478
  try {
3862
- await chromeStorageLocalSet(STORAGE_KEY2, JSON.stringify(this.buffer));
4479
+ await chromeStorageLocalSet(STORAGE_KEY, JSON.stringify(this.buffer));
3863
4480
  } catch {
3864
4481
  this.buffer.splice(0, Math.ceil(this.buffer.length * 0.5));
3865
- await chromeStorageLocalSet(STORAGE_KEY2, JSON.stringify(this.buffer));
4482
+ await chromeStorageLocalSet(STORAGE_KEY, JSON.stringify(this.buffer));
3866
4483
  }
3867
4484
  }
3868
4485
  size() {
@@ -3887,7 +4504,7 @@ var ChromeStorageLogQueue = class {
3887
4504
  async clear() {
3888
4505
  this.buffer = [];
3889
4506
  this.nextId = 1;
3890
- await chromeStorageLocalRemove(STORAGE_KEY2);
4507
+ await chromeStorageLocalRemove(STORAGE_KEY);
3891
4508
  }
3892
4509
  };
3893
4510
  function chromeStorageLocalGet(key) {
@@ -4008,7 +4625,7 @@ async function readDiagRing() {
4008
4625
  }
4009
4626
  }
4010
4627
 
4011
- // ../../node_modules/.pnpm/@chromepilot+kit@0.1.8_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/offscreen.js
4628
+ // ../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/offscreen.js
4012
4629
  var OFFSCREEN_MSG = {
4013
4630
  /** SW → offscreen:注入身份(getManifest 在 offscreen 不可用) */
4014
4631
  INIT: "offscreen:init",
@@ -4221,6 +4838,95 @@ function createSwOffscreenRelay(opts) {
4221
4838
  };
4222
4839
  }
4223
4840
 
4841
+ // ../../node_modules/.pnpm/@chromepilot+kit@0.1.13_@sentry+node@10.70.0_@opentelemetry+core@2.10.0_@opentelemetry+api@1.9.1__/node_modules/@chromepilot/kit/dist/hard-reload.js
4842
+ var OFFSCREEN_CONTEXT_TYPE = "OFFSCREEN_DOCUMENT";
4843
+ function createHardReloader(deps) {
4844
+ const minUptimeMs = deps.minUptimeMs ?? 3e3;
4845
+ const settleMs = deps.settleMs ?? 500;
4846
+ const now = deps.now ?? Date.now;
4847
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4848
+ const reloadFn = deps.reload ?? (() => chrome.runtime.reload());
4849
+ const getContexts = deps.getContexts ?? ((filter) => chrome.runtime.getContexts(filter));
4850
+ const closeDocument = deps.closeDocument ?? (() => chrome.offscreen.closeDocument());
4851
+ function canHardReload() {
4852
+ if (now() - deps.startedAt < minUptimeMs)
4853
+ return { ok: false, reason: "boot_window" };
4854
+ if (deps.hasDebuggerAttached())
4855
+ return { ok: false, reason: "debugger_attached" };
4856
+ return { ok: true };
4857
+ }
4858
+ async function teardownOffscreen() {
4859
+ try {
4860
+ await closeDocument();
4861
+ } catch {
4862
+ }
4863
+ for (let i = 0; i < 6; i++) {
4864
+ const contexts = await getContexts({
4865
+ contextTypes: [OFFSCREEN_CONTEXT_TYPE]
4866
+ }).catch(() => []);
4867
+ if (contexts.length === 0)
4868
+ return true;
4869
+ await sleep2(250);
4870
+ }
4871
+ return false;
4872
+ }
4873
+ async function hardReload() {
4874
+ const verdict = canHardReload();
4875
+ if (!verdict.ok)
4876
+ return verdict;
4877
+ if (!await teardownOffscreen()) {
4878
+ return { ok: false, reason: "offscreen_teardown_failed" };
4879
+ }
4880
+ await sleep2(settleMs);
4881
+ console.info(`${deps.logPrefix} hard reload: checks passed, calling runtime.reload()`);
4882
+ reloadFn();
4883
+ return { ok: true };
4884
+ }
4885
+ return { canHardReload, hardReload };
4886
+ }
4887
+ async function togglePeerExtension(management, namePattern, sleep2 = (ms) => new Promise((r) => setTimeout(r, ms)), sendWake) {
4888
+ const all = await management.getAll();
4889
+ const selfId = chrome.runtime.id;
4890
+ const peer = all.find((e) => e.id !== selfId && e.installType === "development" && (namePattern.test(e.name) || namePattern.test(e.shortName ?? "")));
4891
+ if (!peer)
4892
+ return { ok: false, reason: "peer_not_found" };
4893
+ try {
4894
+ await management.setEnabled(peer.id, false);
4895
+ await sleep2(400);
4896
+ await management.setEnabled(peer.id, true);
4897
+ const wake = sendWake ?? ((id) => chrome.runtime.sendMessage(id, { type: "__cp_peer_wake__" }));
4898
+ const wakeWithTimeout = async (id, ms) => {
4899
+ await Promise.race([
4900
+ wake(id),
4901
+ sleep2(ms).then(() => {
4902
+ throw new Error("wake timeout");
4903
+ })
4904
+ ]);
4905
+ };
4906
+ try {
4907
+ await wakeWithTimeout(peer.id, 5e3);
4908
+ await sleep2(300);
4909
+ void wakeWithTimeout(peer.id, 5e3).catch(() => {
4910
+ });
4911
+ } catch {
4912
+ }
4913
+ return { ok: true, peerId: peer.id };
4914
+ } catch {
4915
+ return { ok: false, reason: "toggle_failed", peerId: peer.id };
4916
+ }
4917
+ }
4918
+
4919
+ // src/background/extension-reload.ts
4920
+ var SW_STARTED_AT = Date.now();
4921
+ var hardReloader = createHardReloader({
4922
+ logPrefix: "[HunterMate]",
4923
+ hasDebuggerAttached: () => debuggerAttachCount() > 0,
4924
+ startedAt: SW_STARTED_AT
4925
+ });
4926
+ function reloadChromepilot() {
4927
+ return togglePeerExtension(chrome.management, /ChromePilot/);
4928
+ }
4929
+
4224
4930
  // src/background/service-worker.ts
4225
4931
  diag("sw", "sw-start", { reason: "top-level-eval" });
4226
4932
  self.addEventListener("error", (e) => diag("sw", "sw-error", { message: e.message, src: (e.filename || "").split("/").pop() }));
@@ -4405,7 +5111,22 @@ async function handleServerMessage(data) {
4405
5111
  const startedAt = Date.now();
4406
5112
  sendLog("info", "extension handling control command", { command: data.command, requestId: data.requestId, params: data.params });
4407
5113
  let outcome;
4408
- if (data.command === "hm.status") {
5114
+ let startSelfHardReloadAfterAck = false;
5115
+ if (data.command === "hm.system.reloadExtension") {
5116
+ const target = data.params?.target === "cp" ? "cp" : "self";
5117
+ if (target === "cp") {
5118
+ const toggleResult = await reloadChromepilot();
5119
+ outcome = toggleResult.ok ? { status: "ok", result: { ok: true, via: "cp", name: toggleResult.name } } : { status: "error", code: "peer_reload_failed", message: toggleResult.reason ?? "toggle ChromePilot failed" };
5120
+ } else {
5121
+ const verdict = hardReloader.canHardReload();
5122
+ if (!verdict.ok) {
5123
+ outcome = { status: "error", code: "reload_refused", message: `hard reload refused: ${verdict.reason}` };
5124
+ } else {
5125
+ outcome = { status: "ok", result: { ok: true, via: "self", reloading: true } };
5126
+ startSelfHardReloadAfterAck = true;
5127
+ }
5128
+ }
5129
+ } else if (data.command === "hm.status") {
4409
5130
  const manifest = chrome.runtime.getManifest();
4410
5131
  outcome = {
4411
5132
  status: "ok",
@@ -4451,6 +5172,9 @@ async function handleServerMessage(data) {
4451
5172
  };
4452
5173
  sendToOffscreen(JSON.stringify(resp));
4453
5174
  sendLog("debug", "cmd_response sent", { requestId: data.requestId, status: resp.status, durationMs: resp.durationMs });
5175
+ if (startSelfHardReloadAfterAck) {
5176
+ void hardReloader.hardReload();
5177
+ }
4454
5178
  }
4455
5179
  }
4456
5180
  var RECONNECT_ALARM_NAME = "hm:reconnect";
@@ -4493,6 +5217,8 @@ chrome.runtime.onStartup.addListener(() => {
4493
5217
  setInterval(() => {
4494
5218
  checkHttpHealth();
4495
5219
  void ensureOffscreen();
5220
+ const TAB_RECYCLE_SWEEP_ENABLED = false;
5221
+ if (TAB_RECYCLE_SWEEP_ENABLED) tabRecycler.installAlarm(10);
4496
5222
  broadcastState();
4497
5223
  }, 2e4);
4498
5224
  setInterval(() => {