@withbuddi/buddi 0.1.0-pre.38 → 0.1.0-pre.39

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 (253) hide show
  1. package/extension/background.js +635 -21
  2. package/extension/manifest.json +2 -2
  3. package/node_modules/@buddi/cli/dist/.tsbuildinfo +1 -1
  4. package/node_modules/@buddi/cli/dist/access-cmd.d.ts +1 -1
  5. package/node_modules/@buddi/cli/dist/access-cmd.d.ts.map +1 -1
  6. package/node_modules/@buddi/cli/dist/access-cmd.js +23 -3
  7. package/node_modules/@buddi/cli/dist/access-cmd.js.map +1 -1
  8. package/node_modules/@buddi/cli/dist/args.d.ts +2 -2
  9. package/node_modules/@buddi/cli/dist/args.d.ts.map +1 -1
  10. package/node_modules/@buddi/cli/dist/args.js +7 -2
  11. package/node_modules/@buddi/cli/dist/args.js.map +1 -1
  12. package/node_modules/@buddi/cli/dist/commands.d.ts.map +1 -1
  13. package/node_modules/@buddi/cli/dist/commands.js +11 -0
  14. package/node_modules/@buddi/cli/dist/commands.js.map +1 -1
  15. package/node_modules/@buddi/cli/dist/main.d.ts.map +1 -1
  16. package/node_modules/@buddi/cli/dist/main.js +2 -1
  17. package/node_modules/@buddi/cli/dist/main.js.map +1 -1
  18. package/node_modules/@buddi/gateway/dist/.tsbuildinfo +1 -1
  19. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts +26 -0
  20. package/node_modules/@buddi/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  21. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js +83 -0
  22. package/node_modules/@buddi/gateway/dist/missions/ask-cap.js.map +1 -0
  23. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts +12 -1
  24. package/node_modules/@buddi/gateway/dist/missions/execute.d.ts.map +1 -1
  25. package/node_modules/@buddi/gateway/dist/missions/execute.js +73 -11
  26. package/node_modules/@buddi/gateway/dist/missions/execute.js.map +1 -1
  27. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts +6 -0
  28. package/node_modules/@buddi/gateway/dist/missions/parked.d.ts.map +1 -1
  29. package/node_modules/@buddi/gateway/dist/missions/parked.js +7 -1
  30. package/node_modules/@buddi/gateway/dist/missions/parked.js.map +1 -1
  31. package/node_modules/@buddi/gateway/dist/serve.js +1 -1
  32. package/node_modules/@buddi/gateway/dist/serve.js.map +1 -1
  33. package/node_modules/@buddi/gateway/dist/system-context.d.ts +7 -0
  34. package/node_modules/@buddi/gateway/dist/system-context.d.ts.map +1 -1
  35. package/node_modules/@buddi/gateway/dist/system-context.js +8 -0
  36. package/node_modules/@buddi/gateway/dist/system-context.js.map +1 -1
  37. package/node_modules/@buddi/gateway/dist/web/api-routes.d.ts.map +1 -1
  38. package/node_modules/@buddi/gateway/dist/web/api-routes.js +11 -3
  39. package/node_modules/@buddi/gateway/dist/web/api-routes.js.map +1 -1
  40. package/node_modules/@buddi/gateway/dist/web/extension.d.ts +13 -1
  41. package/node_modules/@buddi/gateway/dist/web/extension.d.ts.map +1 -1
  42. package/node_modules/@buddi/gateway/dist/web/extension.js +125 -5
  43. package/node_modules/@buddi/gateway/dist/web/extension.js.map +1 -1
  44. package/node_modules/@buddi/gateway/dist/web/remote-hand.d.ts.map +1 -1
  45. package/node_modules/@buddi/gateway/dist/web/remote-hand.js +45 -2
  46. package/node_modules/@buddi/gateway/dist/web/remote-hand.js.map +1 -1
  47. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts +26 -1
  48. package/node_modules/@buddi/gateway/dist/web/secrets.d.ts.map +1 -1
  49. package/node_modules/@buddi/gateway/dist/web/secrets.js +66 -3
  50. package/node_modules/@buddi/gateway/dist/web/secrets.js.map +1 -1
  51. package/node_modules/@buddi/gateway/dist/web/server.d.ts.map +1 -1
  52. package/node_modules/@buddi/gateway/dist/web/server.js +63 -7
  53. package/node_modules/@buddi/gateway/dist/web/server.js.map +1 -1
  54. package/node_modules/@buddi/gateway/dist/web/version.d.ts +2 -0
  55. package/node_modules/@buddi/gateway/dist/web/version.d.ts.map +1 -1
  56. package/node_modules/@buddi/gateway/dist/web/version.js +5 -1
  57. package/node_modules/@buddi/gateway/dist/web/version.js.map +1 -1
  58. package/node_modules/@buddi/install/dist/.tsbuildinfo +1 -1
  59. package/node_modules/@buddi/install/dist/app-layout.d.ts +127 -0
  60. package/node_modules/@buddi/install/dist/app-layout.d.ts.map +1 -0
  61. package/node_modules/@buddi/install/dist/app-layout.js +323 -0
  62. package/node_modules/@buddi/install/dist/app-layout.js.map +1 -0
  63. package/node_modules/@buddi/install/dist/index.d.ts +1 -0
  64. package/node_modules/@buddi/install/dist/index.d.ts.map +1 -1
  65. package/node_modules/@buddi/install/dist/index.js +1 -0
  66. package/node_modules/@buddi/install/dist/index.js.map +1 -1
  67. package/node_modules/@buddi/install/dist/launcher.d.ts.map +1 -1
  68. package/node_modules/@buddi/install/dist/launcher.js +18 -4
  69. package/node_modules/@buddi/install/dist/launcher.js.map +1 -1
  70. package/node_modules/@buddi/install/dist/provenance.d.ts +34 -0
  71. package/node_modules/@buddi/install/dist/provenance.d.ts.map +1 -0
  72. package/node_modules/@buddi/install/dist/provenance.js +200 -0
  73. package/node_modules/@buddi/install/dist/provenance.js.map +1 -0
  74. package/node_modules/@buddi/install/dist/supervisor.d.ts.map +1 -1
  75. package/node_modules/@buddi/install/dist/supervisor.js +9 -4
  76. package/node_modules/@buddi/install/dist/supervisor.js.map +1 -1
  77. package/node_modules/@buddi/install/dist/upgrade.d.ts +77 -3
  78. package/node_modules/@buddi/install/dist/upgrade.d.ts.map +1 -1
  79. package/node_modules/@buddi/install/dist/upgrade.js +325 -43
  80. package/node_modules/@buddi/install/dist/upgrade.js.map +1 -1
  81. package/node_modules/@buddi/tool-browser/dist/.tsbuildinfo +1 -1
  82. package/node_modules/@buddi/tool-browser/dist/controller.d.ts +6 -0
  83. package/node_modules/@buddi/tool-browser/dist/controller.d.ts.map +1 -1
  84. package/node_modules/@buddi/tool-browser/dist/controller.js +80 -6
  85. package/node_modules/@buddi/tool-browser/dist/controller.js.map +1 -1
  86. package/node_modules/@buddi/tool-browser/dist/driver.d.ts +3 -1
  87. package/node_modules/@buddi/tool-browser/dist/driver.d.ts.map +1 -1
  88. package/node_modules/@buddi/tool-browser/dist/driver.js +82 -1
  89. package/node_modules/@buddi/tool-browser/dist/driver.js.map +1 -1
  90. package/node_modules/@buddi/tool-browser/dist/extension.d.ts +60 -7
  91. package/node_modules/@buddi/tool-browser/dist/extension.d.ts.map +1 -1
  92. package/node_modules/@buddi/tool-browser/dist/extension.js +90 -13
  93. package/node_modules/@buddi/tool-browser/dist/extension.js.map +1 -1
  94. package/node_modules/@buddi/tool-browser/dist/index.d.ts +6 -4
  95. package/node_modules/@buddi/tool-browser/dist/index.d.ts.map +1 -1
  96. package/node_modules/@buddi/tool-browser/dist/index.js +7 -4
  97. package/node_modules/@buddi/tool-browser/dist/index.js.map +1 -1
  98. package/node_modules/@buddi/tool-browser/dist/logins.d.ts +150 -0
  99. package/node_modules/@buddi/tool-browser/dist/logins.d.ts.map +1 -0
  100. package/node_modules/@buddi/tool-browser/dist/logins.js +476 -0
  101. package/node_modules/@buddi/tool-browser/dist/logins.js.map +1 -0
  102. package/node_modules/@buddi/tool-browser/dist/routes.d.ts +22 -1
  103. package/node_modules/@buddi/tool-browser/dist/routes.d.ts.map +1 -1
  104. package/node_modules/@buddi/tool-browser/dist/routes.js +51 -3
  105. package/node_modules/@buddi/tool-browser/dist/routes.js.map +1 -1
  106. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts +3 -1
  107. package/node_modules/@buddi/tool-browser/dist/secrets.d.ts.map +1 -1
  108. package/node_modules/@buddi/tool-browser/dist/secrets.js +10 -6
  109. package/node_modules/@buddi/tool-browser/dist/secrets.js.map +1 -1
  110. package/node_modules/@buddi/tool-browser/dist/service.d.ts +26 -2
  111. package/node_modules/@buddi/tool-browser/dist/service.d.ts.map +1 -1
  112. package/node_modules/@buddi/tool-browser/dist/service.js +117 -3
  113. package/node_modules/@buddi/tool-browser/dist/service.js.map +1 -1
  114. package/node_modules/@buddi/tool-browser/dist/types.d.ts +88 -0
  115. package/node_modules/@buddi/tool-browser/dist/types.d.ts.map +1 -1
  116. package/node_modules/@buddi/tool-browser/dist/types.js +18 -0
  117. package/node_modules/@buddi/tool-browser/dist/types.js.map +1 -1
  118. package/node_modules/hono/dist/cjs/hono-base.js +2 -0
  119. package/node_modules/hono/dist/cjs/middleware/language/language.js +1 -1
  120. package/node_modules/hono/dist/cjs/middleware/mount/index.js +71 -0
  121. package/node_modules/hono/dist/cjs/request.js +4 -4
  122. package/node_modules/hono/dist/hono-base.js +2 -0
  123. package/node_modules/hono/dist/middleware/mount/index.js +70 -0
  124. package/node_modules/hono/dist/request.js +4 -4
  125. package/node_modules/hono/dist/types/hono-base.d.ts +2 -0
  126. package/node_modules/hono/dist/types/jsx/base.d.ts +1 -1
  127. package/node_modules/hono/dist/types/middleware/mount/index.d.ts +46 -0
  128. package/node_modules/hono/dist/types/request.d.ts +4 -4
  129. package/node_modules/hono/package.json +9 -1
  130. package/package.json +2 -2
  131. package/packages/cli/dist/.tsbuildinfo +1 -1
  132. package/packages/cli/dist/access-cmd.d.ts +1 -1
  133. package/packages/cli/dist/access-cmd.d.ts.map +1 -1
  134. package/packages/cli/dist/access-cmd.js +23 -3
  135. package/packages/cli/dist/access-cmd.js.map +1 -1
  136. package/packages/cli/dist/args.d.ts +2 -2
  137. package/packages/cli/dist/args.d.ts.map +1 -1
  138. package/packages/cli/dist/args.js +7 -2
  139. package/packages/cli/dist/args.js.map +1 -1
  140. package/packages/cli/dist/commands.d.ts.map +1 -1
  141. package/packages/cli/dist/commands.js +11 -0
  142. package/packages/cli/dist/commands.js.map +1 -1
  143. package/packages/cli/dist/main.d.ts.map +1 -1
  144. package/packages/cli/dist/main.js +2 -1
  145. package/packages/cli/dist/main.js.map +1 -1
  146. package/packages/gateway/dist/.tsbuildinfo +1 -1
  147. package/packages/gateway/dist/missions/ask-cap.d.ts +26 -0
  148. package/packages/gateway/dist/missions/ask-cap.d.ts.map +1 -0
  149. package/packages/gateway/dist/missions/ask-cap.js +83 -0
  150. package/packages/gateway/dist/missions/ask-cap.js.map +1 -0
  151. package/packages/gateway/dist/missions/execute.d.ts +12 -1
  152. package/packages/gateway/dist/missions/execute.d.ts.map +1 -1
  153. package/packages/gateway/dist/missions/execute.js +73 -11
  154. package/packages/gateway/dist/missions/execute.js.map +1 -1
  155. package/packages/gateway/dist/missions/parked.d.ts +6 -0
  156. package/packages/gateway/dist/missions/parked.d.ts.map +1 -1
  157. package/packages/gateway/dist/missions/parked.js +7 -1
  158. package/packages/gateway/dist/missions/parked.js.map +1 -1
  159. package/packages/gateway/dist/serve.js +1 -1
  160. package/packages/gateway/dist/serve.js.map +1 -1
  161. package/packages/gateway/dist/system-context.d.ts +7 -0
  162. package/packages/gateway/dist/system-context.d.ts.map +1 -1
  163. package/packages/gateway/dist/system-context.js +8 -0
  164. package/packages/gateway/dist/system-context.js.map +1 -1
  165. package/packages/gateway/dist/web/api-routes.d.ts.map +1 -1
  166. package/packages/gateway/dist/web/api-routes.js +11 -3
  167. package/packages/gateway/dist/web/api-routes.js.map +1 -1
  168. package/packages/gateway/dist/web/extension.d.ts +13 -1
  169. package/packages/gateway/dist/web/extension.d.ts.map +1 -1
  170. package/packages/gateway/dist/web/extension.js +125 -5
  171. package/packages/gateway/dist/web/extension.js.map +1 -1
  172. package/packages/gateway/dist/web/remote-hand.d.ts.map +1 -1
  173. package/packages/gateway/dist/web/remote-hand.js +45 -2
  174. package/packages/gateway/dist/web/remote-hand.js.map +1 -1
  175. package/packages/gateway/dist/web/secrets.d.ts +26 -1
  176. package/packages/gateway/dist/web/secrets.d.ts.map +1 -1
  177. package/packages/gateway/dist/web/secrets.js +66 -3
  178. package/packages/gateway/dist/web/secrets.js.map +1 -1
  179. package/packages/gateway/dist/web/server.d.ts.map +1 -1
  180. package/packages/gateway/dist/web/server.js +63 -7
  181. package/packages/gateway/dist/web/server.js.map +1 -1
  182. package/packages/gateway/dist/web/version.d.ts +2 -0
  183. package/packages/gateway/dist/web/version.d.ts.map +1 -1
  184. package/packages/gateway/dist/web/version.js +5 -1
  185. package/packages/gateway/dist/web/version.js.map +1 -1
  186. package/packages/install/dist/.tsbuildinfo +1 -1
  187. package/packages/install/dist/app-layout.d.ts +127 -0
  188. package/packages/install/dist/app-layout.d.ts.map +1 -0
  189. package/packages/install/dist/app-layout.js +323 -0
  190. package/packages/install/dist/app-layout.js.map +1 -0
  191. package/packages/install/dist/index.d.ts +1 -0
  192. package/packages/install/dist/index.d.ts.map +1 -1
  193. package/packages/install/dist/index.js +1 -0
  194. package/packages/install/dist/index.js.map +1 -1
  195. package/packages/install/dist/launcher.d.ts.map +1 -1
  196. package/packages/install/dist/launcher.js +18 -4
  197. package/packages/install/dist/launcher.js.map +1 -1
  198. package/packages/install/dist/provenance.d.ts +34 -0
  199. package/packages/install/dist/provenance.d.ts.map +1 -0
  200. package/packages/install/dist/provenance.js +200 -0
  201. package/packages/install/dist/provenance.js.map +1 -0
  202. package/packages/install/dist/supervisor.d.ts.map +1 -1
  203. package/packages/install/dist/supervisor.js +9 -4
  204. package/packages/install/dist/supervisor.js.map +1 -1
  205. package/packages/install/dist/upgrade.d.ts +77 -3
  206. package/packages/install/dist/upgrade.d.ts.map +1 -1
  207. package/packages/install/dist/upgrade.js +325 -43
  208. package/packages/install/dist/upgrade.js.map +1 -1
  209. package/packages/tools/browser/dist/.tsbuildinfo +1 -1
  210. package/packages/tools/browser/dist/controller.d.ts +6 -0
  211. package/packages/tools/browser/dist/controller.d.ts.map +1 -1
  212. package/packages/tools/browser/dist/controller.js +80 -6
  213. package/packages/tools/browser/dist/controller.js.map +1 -1
  214. package/packages/tools/browser/dist/driver.d.ts +3 -1
  215. package/packages/tools/browser/dist/driver.d.ts.map +1 -1
  216. package/packages/tools/browser/dist/driver.js +82 -1
  217. package/packages/tools/browser/dist/driver.js.map +1 -1
  218. package/packages/tools/browser/dist/extension.d.ts +60 -7
  219. package/packages/tools/browser/dist/extension.d.ts.map +1 -1
  220. package/packages/tools/browser/dist/extension.js +90 -13
  221. package/packages/tools/browser/dist/extension.js.map +1 -1
  222. package/packages/tools/browser/dist/index.d.ts +6 -4
  223. package/packages/tools/browser/dist/index.d.ts.map +1 -1
  224. package/packages/tools/browser/dist/index.js +7 -4
  225. package/packages/tools/browser/dist/index.js.map +1 -1
  226. package/packages/tools/browser/dist/logins.d.ts +150 -0
  227. package/packages/tools/browser/dist/logins.d.ts.map +1 -0
  228. package/packages/tools/browser/dist/logins.js +476 -0
  229. package/packages/tools/browser/dist/logins.js.map +1 -0
  230. package/packages/tools/browser/dist/routes.d.ts +22 -1
  231. package/packages/tools/browser/dist/routes.d.ts.map +1 -1
  232. package/packages/tools/browser/dist/routes.js +51 -3
  233. package/packages/tools/browser/dist/routes.js.map +1 -1
  234. package/packages/tools/browser/dist/secrets.d.ts +3 -1
  235. package/packages/tools/browser/dist/secrets.d.ts.map +1 -1
  236. package/packages/tools/browser/dist/secrets.js +10 -6
  237. package/packages/tools/browser/dist/secrets.js.map +1 -1
  238. package/packages/tools/browser/dist/service.d.ts +26 -2
  239. package/packages/tools/browser/dist/service.d.ts.map +1 -1
  240. package/packages/tools/browser/dist/service.js +117 -3
  241. package/packages/tools/browser/dist/service.js.map +1 -1
  242. package/packages/tools/browser/dist/types.d.ts +88 -0
  243. package/packages/tools/browser/dist/types.d.ts.map +1 -1
  244. package/packages/tools/browser/dist/types.js +18 -0
  245. package/packages/tools/browser/dist/types.js.map +1 -1
  246. package/packages/web/dist/assets/index-DYfeUW-0.css +1 -0
  247. package/packages/web/dist/assets/index-vpc904_b.js +146 -0
  248. package/packages/web/dist/assets/{lottie_light-CuiSMqnl.js → lottie_light-BFAMYgLA.js} +1 -1
  249. package/packages/web/dist/build.json +1 -1
  250. package/packages/web/dist/index.html +2 -2
  251. package/packages/web/dist/precache.json +4 -4
  252. package/packages/web/dist/assets/index-Bac-CdD-.css +0 -1
  253. package/packages/web/dist/assets/index-DI8bp_D7.js +0 -146
@@ -1,8 +1,14 @@
1
1
  // src/protocol.ts
2
2
  var COMMAND_NAMES = ["navigate", "observe", "click", "fill", "select", "press", "scroll", "tab", "close", "fieldInfo", "secretFill"];
3
- var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input"];
3
+ var HAND_COMMANDS = ["screencast.start", "screencast.stop", "input", "hold", "unhold"];
4
4
  var PreconditionError = class extends Error {
5
5
  };
6
+ var OpenedError = class extends Error {
7
+ constructor(message, page) {
8
+ super(message);
9
+ this.page = page;
10
+ }
11
+ };
6
12
  var CancelledError = class extends Error {
7
13
  };
8
14
  var Cancellation = class {
@@ -148,6 +154,18 @@ var Protocol = class {
148
154
  case "command":
149
155
  await this.#command(message);
150
156
  return;
157
+ case "loginAck": {
158
+ const id = message["id"];
159
+ if (!this.#authenticated || typeof id !== "string" || id.length > 64) return;
160
+ const ask = message["ask"];
161
+ if (ask === "save" || ask === "update" || ask === "none") {
162
+ this.#options.onLoginAck?.(id, { ask });
163
+ return;
164
+ }
165
+ const reason = typeof message["reason"] === "string" ? message["reason"].slice(0, 300) : "";
166
+ this.#options.onLoginAck?.(id, message["saved"] === true ? { saved: true } : { saved: false, reason: reason || "buddi could not keep that login." });
167
+ return;
168
+ }
151
169
  default:
152
170
  return;
153
171
  }
@@ -212,7 +230,14 @@ var Protocol = class {
212
230
  if (error instanceof CancelledError) {
213
231
  this.#options.send({ type: "result", id, ok: false, error: "cancelled", precondition: !cancel.dispatched });
214
232
  } else {
215
- this.#options.send({ type: "result", id, ok: false, error: sentence(error), precondition: error instanceof PreconditionError });
233
+ this.#options.send({
234
+ type: "result",
235
+ id,
236
+ ok: false,
237
+ error: sentence(error),
238
+ precondition: error instanceof PreconditionError,
239
+ ...error instanceof OpenedError ? { page: error.page } : {}
240
+ });
216
241
  }
217
242
  } finally {
218
243
  this.#running.delete(id);
@@ -300,6 +325,397 @@ function hideBar() {
300
325
  const holder = globalThis;
301
326
  delete holder["__buddiBar"];
302
327
  }
328
+ var GIVE_BACK_MESSAGE = "buddi-give-back";
329
+ function showHeldBar(session) {
330
+ const doc = globalThis.document;
331
+ if (!doc || doc.getElementById("buddi-held-bar")) return;
332
+ const host = doc.createElement("div");
333
+ host.id = "buddi-held-bar";
334
+ host.setAttribute("style", "position:fixed;top:0;left:0;right:0;z-index:2147483647;all:initial;");
335
+ const root = host.attachShadow({ mode: "closed" });
336
+ const bar = doc.createElement("div");
337
+ bar.setAttribute("role", "status");
338
+ bar.setAttribute("style", "position:fixed;top:0;left:0;right:0;display:flex;gap:12px;align-items:center;justify-content:flex-end;padding:6px 16px;font:13px/1.4 system-ui,-apple-system,sans-serif;background:#1f2a44;color:#fff;box-shadow:0 1px 4px rgba(0,0,0,.25);");
339
+ const label = doc.createElement("span");
340
+ label.textContent = "buddi is waiting";
341
+ label.setAttribute("style", "margin-right:auto;");
342
+ const give = doc.createElement("button");
343
+ give.type = "button";
344
+ give.textContent = "Give it back";
345
+ give.setAttribute("style", "font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;background:#fff;color:#1f2a44;");
346
+ give.addEventListener("click", () => {
347
+ const runtime = globalThis.chrome?.runtime;
348
+ void runtime?.sendMessage({ type: "buddi-give-back", session })?.catch?.(() => void 0);
349
+ host.remove();
350
+ });
351
+ bar.append(label, give);
352
+ root.append(bar);
353
+ (doc.documentElement ?? doc.body).append(host);
354
+ const asking = [];
355
+ const restore = () => {
356
+ for (const button of asking.splice(0)) button.remove();
357
+ label.textContent = "buddi is waiting";
358
+ give.style.display = "";
359
+ };
360
+ globalThis["__buddiHeldAsk"] = (question, choices) => {
361
+ restore();
362
+ label.textContent = question;
363
+ give.style.display = "none";
364
+ for (const choice of choices) {
365
+ const b = doc.createElement("button");
366
+ b.type = "button";
367
+ b.textContent = choice.label;
368
+ b.setAttribute("style", `font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;border:1px solid #fff;${choice.primary ? "background:#fff;color:#1f2a44;" : "background:transparent;color:#fff;"}`);
369
+ b.addEventListener("click", () => {
370
+ restore();
371
+ choice.pick();
372
+ });
373
+ asking.push(b);
374
+ bar.insertBefore(b, give);
375
+ }
376
+ return restore;
377
+ };
378
+ }
379
+ function hideHeldBar() {
380
+ const doc = globalThis.document;
381
+ doc?.getElementById("buddi-held-bar")?.remove();
382
+ delete globalThis["__buddiHeldAsk"];
383
+ }
384
+ var LOGIN_MESSAGE = "buddi-login";
385
+ var LOGIN_PROMPT_MS = 2 * 6e4;
386
+ function watchHeldLogins(session, facts) {
387
+ const holder = globalThis;
388
+ const known = { never: [...facts?.never ?? []], saved: [...facts?.saved ?? []] };
389
+ const existing = holder["__buddiHeldLogins"];
390
+ if (existing) {
391
+ existing.session = session;
392
+ existing.facts = known;
393
+ return;
394
+ }
395
+ const doc = globalThis.document;
396
+ if (!doc) return;
397
+ const state = { session, facts: known, last: "", lastAt: 0, typedAt: 0, cached: void 0, timer: void 0, drop: void 0 };
398
+ holder["__buddiHeldLogins"] = state;
399
+ const runtime = () => globalThis.chrome?.runtime;
400
+ const call = async (message) => {
401
+ try {
402
+ return await runtime()?.sendMessage(message);
403
+ } catch {
404
+ return void 0;
405
+ }
406
+ };
407
+ const site = () => (globalThis.location?.hostname ?? "").toLowerCase().replace(/^www\./, "");
408
+ const listed = (where) => state.facts.never.some((entry) => {
409
+ const bare = entry.toLowerCase().replace(/^www\./, "");
410
+ return where === bare || where.endsWith(`.${bare}`);
411
+ });
412
+ const bar = () => {
413
+ const question = holder["__buddiHeldAsk"];
414
+ return typeof question === "function" ? question : void 0;
415
+ };
416
+ const prompt = (where, update) => {
417
+ const question = bar();
418
+ if (!question) return;
419
+ if (state.drop) clearTimeout(state.drop);
420
+ let answered = false;
421
+ const save = () => {
422
+ if (answered) return;
423
+ answered = true;
424
+ const asking = bar();
425
+ asking?.(update ? "Updating\u2026" : "Saving\u2026", []);
426
+ void call({ type: "buddi-login", session: state.session, decision: "save" }).then((reply) => {
427
+ const answer = reply;
428
+ const now = bar();
429
+ if (!now) return;
430
+ if (answer?.saved === true) {
431
+ const restore2 = now(update ? `Updated the login for ${where}` : `Saved the login for ${where}`, []);
432
+ if (state.drop) clearTimeout(state.drop);
433
+ state.drop = setTimeout(restore2, 2e3);
434
+ return;
435
+ }
436
+ const reason = typeof answer?.reason === "string" && answer.reason !== "" ? answer.reason : "buddi did not answer. Try again.";
437
+ now(reason, [
438
+ { label: "Try again", primary: true, pick: () => {
439
+ answered = false;
440
+ save();
441
+ } },
442
+ { label: "Not now", pick: () => {
443
+ void call({ type: "buddi-login", session: state.session, decision: "later" });
444
+ } }
445
+ ]);
446
+ });
447
+ };
448
+ const restore = question(update ? `Update the login for ${where}?` : `Save this login for ${where}?`, [
449
+ { label: update ? "Update" : "Save", primary: true, pick: save },
450
+ { label: "Not now", pick: () => {
451
+ answered = true;
452
+ void call({ type: "buddi-login", session: state.session, decision: "later" });
453
+ } },
454
+ { label: "Never for this site", pick: () => {
455
+ answered = true;
456
+ state.facts.never.push(where);
457
+ void call({ type: "buddi-login", session: state.session, decision: "never" });
458
+ } }
459
+ ]);
460
+ state.drop = setTimeout(() => {
461
+ if (!answered) restore();
462
+ }, 2 * 6e4);
463
+ };
464
+ const ask = (found) => {
465
+ const where = site();
466
+ if (where === "" || listed(where) || !bar()) return;
467
+ const update = state.facts.saved.some((login) => login.site === where && login.username === found.username);
468
+ void call({ type: "buddi-login-seen", session: state.session, username: found.username, password: found.password, update }).then((reply) => {
469
+ const asked = reply;
470
+ if (holder["__buddiHeldLogins"] !== state || !asked || typeof asked !== "object") return;
471
+ prompt(where, asked.update === true);
472
+ });
473
+ };
474
+ void call({ type: "buddi-login-pending", session: state.session }).then((reply) => {
475
+ const waiting = reply;
476
+ if (holder["__buddiHeldLogins"] !== state || !waiting || typeof waiting.site !== "string") return;
477
+ prompt(waiting.site, waiting.update === true);
478
+ });
479
+ const NOT_SUBMIT = /\b(show|hide|reveal|toggle|eye|forgot|reset|cancel|back|close|sign ?up|register|create)\b/i;
480
+ const USER = /user|email|login|account|ident|phone|mail/i;
481
+ const passwords = (root) => Array.from(root.querySelectorAll('input[type="password" i]'));
482
+ const usernameFor = (password) => {
483
+ const scope = password.form ?? doc;
484
+ const inputs = Array.from(scope.querySelectorAll("input")).filter((input) => ["text", "email", "tel", ""].includes((input.getAttribute("type") ?? "").toLowerCase()) && input.value.trim() !== "");
485
+ const marked = inputs.find((input) => (input.getAttribute("autocomplete") ?? "").toLowerCase().includes("username") || (input.getAttribute("type") ?? "").toLowerCase() === "email");
486
+ if (marked) return marked.value.trim();
487
+ const before = inputs.filter((input) => (input.compareDocumentPosition(password) & 4) !== 0);
488
+ const pick = before[before.length - 1] ?? inputs.find((input) => USER.test(`${input.name} ${input.id} ${input.getAttribute("autocomplete") ?? ""}`));
489
+ return pick ? pick.value.trim() : "";
490
+ };
491
+ const read = (scope) => {
492
+ const field = passwords(scope ?? doc).find((input) => input.value !== "");
493
+ return field ? { username: usernameFor(field), password: field.value } : void 0;
494
+ };
495
+ const send2 = (found) => {
496
+ if (holder["__buddiHeldLogins"] !== state) return;
497
+ if (!found || found.password === "") return;
498
+ const key = `${found.username}\0${found.password}`;
499
+ const now = Date.now();
500
+ if (key === state.last && now - state.lastAt < 5e3) return;
501
+ state.last = key;
502
+ state.lastAt = now;
503
+ state.cached = void 0;
504
+ ask(found);
505
+ };
506
+ doc.addEventListener("submit", (event) => {
507
+ send2(read(event.target) ?? state.cached);
508
+ }, true);
509
+ doc.addEventListener("keydown", (event) => {
510
+ if (event.key !== "Enter") return;
511
+ const target = event.target;
512
+ if (!target || target.tagName !== "INPUT") return;
513
+ send2(read(target.form ?? doc));
514
+ }, true);
515
+ doc.addEventListener("click", (event) => {
516
+ const target = event.target;
517
+ const button = target?.closest?.('button, input[type="submit" i], input[type="image" i], input[type="button" i], [role="button"]');
518
+ if (!button) return;
519
+ const words = `${button.textContent ?? ""} ${button.getAttribute("aria-label") ?? ""} ${button.getAttribute("value") ?? ""} ${button.getAttribute("title") ?? ""}`;
520
+ if (NOT_SUBMIT.test(words)) return;
521
+ send2(read(button.form ?? doc));
522
+ }, true);
523
+ doc.addEventListener("input", (event) => {
524
+ const target = event.target;
525
+ if ((target?.getAttribute?.("type") ?? "").toLowerCase() !== "password") return;
526
+ state.typedAt = Date.now();
527
+ state.cached = read(target.form ?? doc);
528
+ }, true);
529
+ const Observer = globalThis.PerformanceObserver;
530
+ if (typeof Observer === "function") {
531
+ try {
532
+ new Observer((list) => {
533
+ if (!state.cached || Date.now() - state.typedAt > 1e4) return;
534
+ const fetched = list.getEntries().some((entry) => ["fetch", "xmlhttprequest"].includes(entry.initiatorType));
535
+ if (!fetched) return;
536
+ if (state.timer) clearTimeout(state.timer);
537
+ state.timer = setTimeout(() => {
538
+ const still = passwords(doc).some((input) => input.isConnected && input.value !== "" && input.getClientRects().length > 0);
539
+ if (!still) send2(state.cached);
540
+ }, 1500);
541
+ }).observe({ type: "resource", buffered: false });
542
+ } catch {
543
+ }
544
+ }
545
+ }
546
+ var LOGIN_SEEN_MESSAGE = "buddi-login-seen";
547
+ var LOGIN_PENDING_MESSAGE = "buddi-login-pending";
548
+ function heldLoginRequest(message, sender, heldTab) {
549
+ const request = message;
550
+ if (!request || typeof request.session !== "string" || request.session === "") return void 0;
551
+ if (heldTab === void 0 || sender?.tab?.id !== heldTab) return void 0;
552
+ const session = request.session;
553
+ const url = sender.url ?? "";
554
+ if (request.type === LOGIN_SEEN_MESSAGE) {
555
+ if (typeof request.password !== "string" || request.password === "" || request.password.length > 1024) return void 0;
556
+ const username = typeof request.username === "string" ? request.username.slice(0, 200) : "";
557
+ return { kind: "seen", session, tabId: heldTab, url, username, password: request.password, update: request.update === true };
558
+ }
559
+ if (request.type === LOGIN_PENDING_MESSAGE) return { kind: "pending", session, tabId: heldTab, url };
560
+ if (request.type === LOGIN_MESSAGE && (request.decision === "save" || request.decision === "later" || request.decision === "never")) {
561
+ return { kind: "answer", session, tabId: heldTab, decision: request.decision };
562
+ }
563
+ return void 0;
564
+ }
565
+
566
+ // src/held-logins.ts
567
+ var HELD_LOGIN_MS = 2 * 6e4;
568
+ var LOGIN_ACK_MS = 2e4;
569
+ var LOGIN_CHECK_MS = 5e3;
570
+ var NO_ANSWER = "buddi did not answer. Try again.";
571
+ var GONE = "That question is gone; add the login in Settings \u2192 Keys and secrets.";
572
+ function siteName(host) {
573
+ return host.toLowerCase().replace(/^www\./, "");
574
+ }
575
+ function siteKey(host) {
576
+ const labels = siteName(host).split(".").filter(Boolean);
577
+ if (labels.length <= 2) return labels.join(".");
578
+ const [second, top] = labels.slice(-2);
579
+ const keep = top.length === 2 && second.length <= 3 ? 3 : 2;
580
+ return labels.slice(-keep).join(".");
581
+ }
582
+ function webOrigin(url) {
583
+ try {
584
+ const parsed = new URL(url ?? "");
585
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed : void 0;
586
+ } catch {
587
+ return void 0;
588
+ }
589
+ }
590
+ var HeldLogins = class {
591
+ constructor(send2, options = {}) {
592
+ this.send = send2;
593
+ this.options = options;
594
+ }
595
+ #byTab = /* @__PURE__ */ new Map();
596
+ /** Frames waiting for buddi's answer, by id. */
597
+ #waiting = /* @__PURE__ */ new Map();
598
+ #id() {
599
+ return (this.options.uuid ?? (() => crypto.randomUUID()))();
600
+ }
601
+ /** Send a frame and wait for buddi's answer to it; undefined when none comes in time or the tab's pair goes. */
602
+ #ask(tabId, frame, ms) {
603
+ const id = this.#id();
604
+ return new Promise((resolve) => {
605
+ const timer2 = setTimeout(() => {
606
+ this.#waiting.delete(id);
607
+ resolve(void 0);
608
+ }, ms);
609
+ this.#waiting.set(id, { tabId, resolve, timer: timer2 });
610
+ this.send({ ...frame, id });
611
+ });
612
+ }
613
+ /**
614
+ * A sign-in went out in a held tab: keep the pair, with the origin Chrome
615
+ * reports for the page (never the page's claim), and ask buddi whether it
616
+ * is worth a question. Replaces whatever this tab was waiting on. Resolves
617
+ * to the question to draw, or undefined: a page not on the web, or nothing
618
+ * to ask (the pair is dropped then). With no word from buddi in time, the
619
+ * tab asks as the hold's facts said (`pair.update`).
620
+ */
621
+ async capture(tabId, session, url, pair) {
622
+ const where = webOrigin(url);
623
+ if (!where || pair.password === "" || pair.password.length > 1024) return void 0;
624
+ this.clearTab(tabId);
625
+ let settle = () => {
626
+ };
627
+ const entry = {
628
+ tabId,
629
+ session,
630
+ origin: where.origin,
631
+ site: siteName(where.hostname),
632
+ username: pair.username.slice(0, 200),
633
+ password: pair.password,
634
+ update: pair.update === true,
635
+ timer: setTimeout(() => this.clearTab(tabId), this.options.holdMs ?? HELD_LOGIN_MS),
636
+ checked: new Promise((resolve) => {
637
+ settle = resolve;
638
+ })
639
+ };
640
+ this.#byTab.set(tabId, entry);
641
+ const word = await this.#ask(tabId, { type: "login", session, decision: "check", origin: entry.origin, username: entry.username, password: entry.password }, this.options.checkMs ?? LOGIN_CHECK_MS);
642
+ if (this.#byTab.get(tabId) !== entry) {
643
+ settle(false);
644
+ return void 0;
645
+ }
646
+ if (word && "ask" in word) {
647
+ if (word.ask === "none") {
648
+ this.clearTab(tabId);
649
+ settle(false);
650
+ return void 0;
651
+ }
652
+ entry.update = word.ask === "update";
653
+ }
654
+ settle(true);
655
+ return { site: entry.site, username: entry.username, update: entry.update };
656
+ }
657
+ /** The question still waiting in this tab, for a page of the same site (once buddi said it is worth asking). */
658
+ async pending(tabId, session, url) {
659
+ const entry = this.#byTab.get(tabId);
660
+ const where = webOrigin(url);
661
+ if (!entry || entry.session !== session || !where || siteKey(where.hostname) !== siteKey(entry.site)) return void 0;
662
+ if (!await entry.checked || this.#byTab.get(tabId) !== entry) return void 0;
663
+ return { site: entry.site, username: entry.username, update: entry.update };
664
+ }
665
+ /** Save: the pair goes to buddi with the original origin; the answer is what buddi says became of it. */
666
+ async save(tabId, session) {
667
+ const entry = this.#byTab.get(tabId);
668
+ if (!entry || entry.session !== session) return { saved: false, reason: GONE };
669
+ const answer = await this.#ask(tabId, { type: "login", session: entry.session, decision: "save", origin: entry.origin, username: entry.username, password: entry.password }, this.options.ackMs ?? LOGIN_ACK_MS);
670
+ if (!answer || !("saved" in answer)) return { saved: false, reason: NO_ANSWER };
671
+ if (answer.saved && this.#byTab.get(tabId) === entry) this.clearTab(tabId);
672
+ return answer;
673
+ }
674
+ /** Never for this site: the site alone goes to buddi, and the pair is dropped. */
675
+ never(tabId, session) {
676
+ const entry = this.#byTab.get(tabId);
677
+ if (!entry || entry.session !== session) return false;
678
+ this.clearTab(tabId);
679
+ this.send({ type: "login", session: entry.session, decision: "never", origin: entry.origin, username: entry.username });
680
+ return true;
681
+ }
682
+ /** Not now: nothing goes anywhere. */
683
+ dismiss(tabId, session) {
684
+ const entry = this.#byTab.get(tabId);
685
+ if (entry && entry.session === session) this.clearTab(tabId);
686
+ }
687
+ /** buddi's answer to a frame this worker sent. */
688
+ ack(id, answer) {
689
+ const waiting = this.#waiting.get(id);
690
+ if (!waiting) return;
691
+ this.#waiting.delete(id);
692
+ clearTimeout(waiting.timer);
693
+ waiting.resolve(answer);
694
+ }
695
+ clearTab(tabId) {
696
+ const entry = this.#byTab.get(tabId);
697
+ if (entry) {
698
+ this.#byTab.delete(tabId);
699
+ clearTimeout(entry.timer);
700
+ entry.password = "";
701
+ }
702
+ for (const [id, waiting] of this.#waiting) {
703
+ if (waiting.tabId !== tabId) continue;
704
+ this.#waiting.delete(id);
705
+ clearTimeout(waiting.timer);
706
+ waiting.resolve(void 0);
707
+ }
708
+ }
709
+ clearSession(session) {
710
+ for (const entry of [...this.#byTab.values()]) if (entry.session === session) this.clearTab(entry.tabId);
711
+ }
712
+ clear() {
713
+ for (const tabId of [...this.#byTab.keys()]) this.clearTab(tabId);
714
+ }
715
+ get size() {
716
+ return this.#byTab.size;
717
+ }
718
+ };
303
719
 
304
720
  // src/commands.ts
305
721
  var TAKEN = "The owner took this tab; observe again to continue in a new one.";
@@ -307,6 +723,8 @@ var MOVED = "This page has changed since that observation. Observe again.";
307
723
  var WATCHED = "You are looking at this tab. buddi waited and asked in the tab; nothing was done.";
308
724
  var TAKEN_IN_TAB = "The owner took over this tab from the bar in it.";
309
725
  var GROUP_TITLE = "buddi";
726
+ var NEW_WINDOW_NOTE = "Your Chrome couldn't open a tab in that window; opened a new window instead.";
727
+ var NO_WINDOW_NOTE = "Your Chrome had no ordinary window open, so the page opened in a new one, in the background.";
310
728
  var MAX_FRAMES = 11;
311
729
  var MAX_TREE = 32e3;
312
730
  var MAX_TARGETS = 240;
@@ -503,6 +921,12 @@ var BrowserCommands = class {
503
921
  #onEvent;
504
922
  /** Sessions the owner let carry on in a tab they are looking at, by tab id. */
505
923
  #inView = /* @__PURE__ */ new Map();
924
+ /** Sessions whose tab the owner holds (taken over in place), by tab id. */
925
+ #held = /* @__PURE__ */ new Map();
926
+ /** What a held tab need not ask about before offering to save a sign-in, as buddi said at the hold. */
927
+ #heldLogins = /* @__PURE__ */ new Map();
928
+ /** Sign-ins seen in held tabs, waiting for the owner's answer: one per tab, two minutes, gone on unhold or close. */
929
+ logins;
506
930
  #now;
507
931
  #wait;
508
932
  /** How many things want the debugger on this tab. A screencast is one of them, and it outlives a command. */
@@ -512,6 +936,8 @@ var BrowserCommands = class {
512
936
  #ownerWaitMs;
513
937
  constructor(chrome2, options = {}) {
514
938
  this.#chrome = chrome2;
939
+ this.logins = new HeldLogins(options.onLogin ?? (() => void 0));
940
+ chrome2.tabs.onRemoved?.addListener((tabId) => this.logins.clearTab(tabId));
515
941
  this.#onEvent = options.onEvent ?? (() => void 0);
516
942
  this.#ownerWaitMs = options.ownerWaitMs;
517
943
  this.#wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -525,6 +951,11 @@ var BrowserCommands = class {
525
951
  const cast = this.#castsByTab.get(tabId);
526
952
  if (cast) this.#forgetCast(cast);
527
953
  });
954
+ chrome2.tabs.onUpdated?.addListener((tabId, change) => {
955
+ if (change.status !== "complete" || ![...this.#held.values()].includes(tabId)) return;
956
+ const session = [...this.#held].find(([, held]) => held === tabId)?.[0];
957
+ if (session) void this.#drawHeld(tabId, session);
958
+ });
528
959
  this.#uuid = options.uuid ?? (() => crypto.randomUUID());
529
960
  this.#contentFile = options.contentFile ?? "content.js";
530
961
  }
@@ -561,6 +992,10 @@ var BrowserCommands = class {
561
992
  }
562
993
  case "input":
563
994
  return this.#input(command, cancel);
995
+ case "hold":
996
+ return this.#hold(command, cancel);
997
+ case "unhold":
998
+ return this.#unhold(command.session);
564
999
  case "close":
565
1000
  return this.#close(command, cancel);
566
1001
  default:
@@ -579,12 +1014,95 @@ var BrowserCommands = class {
579
1014
  this.#sessions.clear();
580
1015
  this.#refs.clear();
581
1016
  this.#inView.clear();
1017
+ this.#held.clear();
1018
+ this.#heldLogins.clear();
1019
+ this.logins.clear();
1020
+ }
1021
+ /* ---- take-over in place ---- */
1022
+ /**
1023
+ * The owner takes the page where it is: the session's tab becomes the
1024
+ * active one, its window comes to the front, and the bar in it says buddi
1025
+ * is waiting, with Give it back. Nothing is streamed; it is their Chrome.
1026
+ */
1027
+ async #hold(command, cancel) {
1028
+ const session = await this.#session(command.session);
1029
+ const tab = await this.#ownTab(session, this.#activeKey(session));
1030
+ const tabId = tab.id;
1031
+ cancel.check();
1032
+ cancel.dispatch();
1033
+ await this.#chrome.tabs.update(tabId, { active: true });
1034
+ if (tab.windowId !== void 0) await this.#chrome.windows.update?.(tab.windowId, { focused: true });
1035
+ this.#held.set(command.session, tabId);
1036
+ const logins = heldLoginFacts(command.args.logins);
1037
+ if (logins) this.#heldLogins.set(command.session, logins);
1038
+ else this.#heldLogins.delete(command.session);
1039
+ await this.#drawHeld(tabId, command.session);
1040
+ return {};
1041
+ }
1042
+ /**
1043
+ * The waiting bar in a held tab, and the watch that offers to save a sign-in
1044
+ * the owner makes there (docs/browser.md, "Saving a sign-in"). Drawn again on
1045
+ * every page the tab loads while it is held.
1046
+ */
1047
+ async #drawHeld(tabId, session) {
1048
+ await this.#chrome.scripting.executeScript({ target: { tabId }, func: showHeldBar, args: [session] }).catch(() => void 0);
1049
+ const facts = this.#heldLogins.get(session) ?? null;
1050
+ await this.#chrome.scripting.executeScript({ target: { tabId }, func: watchHeldLogins, args: [session, facts] }).catch(() => void 0);
1051
+ }
1052
+ /** The tab a session holds, when the owner holds one: where a save-prompt answer may come from. */
1053
+ heldTab(session) {
1054
+ return this.#held.get(session);
1055
+ }
1056
+ /**
1057
+ * A sign-in message from a held tab's page (`heldLoginRequest`): a pair
1058
+ * seen, a page asking whether a question still waits for it, or the
1059
+ * owner's answer. The reply goes back to the page — a Save's is what buddi
1060
+ * said became of it. Undefined for a message this worker does not believe.
1061
+ */
1062
+ loginMessage(message, sender) {
1063
+ const session = message?.session;
1064
+ const request = heldLoginRequest(message, sender, typeof session === "string" ? this.#held.get(session) : void 0);
1065
+ if (!request) return void 0;
1066
+ if (request.kind === "seen") return this.logins.capture(request.tabId, request.session, request.url, request).then((asked) => asked ?? null);
1067
+ if (request.kind === "pending") return this.logins.pending(request.tabId, request.session, request.url).then((asked) => asked ?? null);
1068
+ if (request.decision === "save") return this.logins.save(request.tabId, request.session);
1069
+ if (request.decision === "never") return Promise.resolve({ ok: this.logins.never(request.tabId, request.session) });
1070
+ this.logins.dismiss(request.tabId, request.session);
1071
+ return Promise.resolve({ ok: true });
1072
+ }
1073
+ /**
1074
+ * Given back (from the Canvas, or the bar): the bar goes, and the agent may
1075
+ * act in that tab although the owner is looking at it — they just handed it
1076
+ * over there, so waiting for them to leave it would be asking twice.
1077
+ */
1078
+ async #unhold(session) {
1079
+ const tabId = this.#held.get(session);
1080
+ this.#held.delete(session);
1081
+ this.#heldLogins.delete(session);
1082
+ this.logins.clearSession(session);
1083
+ if (tabId === void 0) return {};
1084
+ this.#inView.set(session, tabId);
1085
+ await this.#chrome.scripting.executeScript({ target: { tabId }, func: hideHeldBar }).catch(() => void 0);
1086
+ return {};
1087
+ }
1088
+ /**
1089
+ * Give it back pressed in a held tab's bar. Refused from any other tab than
1090
+ * the one the session holds; a worker that was restarted since (and so
1091
+ * forgot what it held) passes it on, and the gateway resumes only a page
1092
+ * that is in the owner's hands. It hears it as the Canvas's Give it back.
1093
+ */
1094
+ async giveBack(session, tabId) {
1095
+ const held = this.#held.get(session);
1096
+ if (tabId === void 0 || held !== void 0 && held !== tabId) return false;
1097
+ await this.#unhold(session);
1098
+ this.#onEvent({ type: "event", name: "giveback", session });
1099
+ return true;
582
1100
  }
583
1101
  /* ---- sessions and tabs ---- */
584
1102
  async #session(session) {
585
1103
  const existing = this.#sessions.get(session);
586
1104
  if (existing) return existing;
587
- const created = { groupId: -1, tabs: /* @__PURE__ */ new Map(), active: null, generation: 0 };
1105
+ const created = { groupId: -1, ungrouped: /* @__PURE__ */ new Set(), tabs: /* @__PURE__ */ new Map(), active: null, generation: 0 };
588
1106
  this.#sessions.set(session, created);
589
1107
  return created;
590
1108
  }
@@ -608,12 +1126,17 @@ var BrowserCommands = class {
608
1126
  this.#forget(session, key);
609
1127
  throw new PreconditionError("That tab is gone. Observe again to continue in a new one.");
610
1128
  }
611
- if (session.groupId >= 0 && tab.groupId !== session.groupId) {
1129
+ if (this.#taken(session, tab)) {
612
1130
  this.#forget(session, key);
613
1131
  throw new PreconditionError(TAKEN);
614
1132
  }
615
1133
  return tab;
616
1134
  }
1135
+ /** The owner dragged this tab out of the session's group. A tab that never made it into the group cannot have been. */
1136
+ #taken(session, tab) {
1137
+ if (tab.id !== void 0 && session.ungrouped.has(tab.id)) return false;
1138
+ return session.groupId >= 0 && tab.groupId !== session.groupId;
1139
+ }
617
1140
  #activeKey(session) {
618
1141
  if (!session.active) throw new PreconditionError("This conversation has no browser tab yet. Navigate to open one.");
619
1142
  return session.active;
@@ -671,19 +1194,82 @@ var BrowserCommands = class {
671
1194
  #liveUrl(tab) {
672
1195
  return checkUrl(tab.url ?? "");
673
1196
  }
1197
+ /**
1198
+ * An ordinary window to open the tab in: the group's own when it has one,
1199
+ * else the one the owner used last, else any. `null` when Chrome has none
1200
+ * (only popups or installed apps' windows), `undefined` when this Chrome
1201
+ * cannot say, in which case Chrome picks as it always did.
1202
+ */
1203
+ async #normalWindow(session) {
1204
+ const windows = this.#chrome.windows;
1205
+ if (!windows.getLastFocused && !windows.getAll) return void 0;
1206
+ const normal = (window) => window?.id !== void 0 && (window.type === void 0 || window.type === "normal");
1207
+ if (session.groupId >= 0) {
1208
+ const group = await this.#chrome.tabGroups.get(session.groupId).catch(() => void 0);
1209
+ if (group?.windowId !== void 0) {
1210
+ const found = await windows.get(group.windowId).catch(() => void 0);
1211
+ if (normal(found)) return found.id;
1212
+ }
1213
+ }
1214
+ const last2 = await windows.getLastFocused?.({ windowTypes: ["normal"] }).catch(() => void 0);
1215
+ if (normal(last2)) return last2.id;
1216
+ const all = await windows.getAll?.({ windowTypes: ["normal"] }).catch(() => void 0);
1217
+ return all?.find(normal)?.id ?? null;
1218
+ }
1219
+ /**
1220
+ * One background tab, in an ordinary window, in the session's group.
1221
+ *
1222
+ * The group is cosmetic: Chrome refuses it in a window without a tab strip,
1223
+ * and that refusal must not undo a page that has already opened. No
1224
+ * ordinary window at all, or one that would not take the tab, and the page
1225
+ * opens in a new window that does not take the focus. Nothing here
1226
+ * activates a tab or focuses a window: the owner keeps working.
1227
+ */
674
1228
  async #openTab(session, url, cancel) {
1229
+ const windowId = await this.#normalWindow(session);
675
1230
  cancel.dispatch();
676
- const tab = await this.#chrome.tabs.create({ url, active: false });
677
- if (tab.id === void 0) throw new Error("Chrome opened a tab without an id.");
678
- const groupId = await this.#chrome.tabs.group(session.groupId >= 0 ? { tabIds: [tab.id], groupId: session.groupId } : { tabIds: [tab.id] });
679
- if (session.groupId !== groupId) {
680
- session.groupId = groupId;
681
- await this.#chrome.tabGroups.update(groupId, { title: GROUP_TITLE, collapsed: false }).catch(() => void 0);
1231
+ let tab;
1232
+ let note;
1233
+ let groupable = true;
1234
+ if (windowId === void 0) {
1235
+ tab = await this.#chrome.tabs.create({ url, active: false });
1236
+ } else if (windowId !== null) {
1237
+ tab = await this.#chrome.tabs.create({ url, active: false, windowId }).catch(() => void 0);
1238
+ if (!tab) note = NEW_WINDOW_NOTE;
1239
+ } else {
1240
+ note = NO_WINDOW_NOTE;
1241
+ }
1242
+ if (!tab) {
1243
+ if (!this.#chrome.windows.create) throw new Error("Your Chrome has no ordinary window to open a tab in. Open a Chrome window and try again.");
1244
+ const created = await this.#chrome.windows.create({ url, focused: false, state: "normal" });
1245
+ tab = created.tabs?.[0];
1246
+ groupable = false;
1247
+ }
1248
+ if (tab?.id === void 0) throw new Error("Chrome opened a tab without an id.");
1249
+ const tabId = tab.id;
1250
+ if (groupable) {
1251
+ try {
1252
+ const groupId = await this.#chrome.tabs.group(session.groupId >= 0 ? { tabIds: [tabId], groupId: session.groupId } : { tabIds: [tabId] });
1253
+ if (session.groupId !== groupId) {
1254
+ session.groupId = groupId;
1255
+ await this.#chrome.tabGroups.update(groupId, { title: GROUP_TITLE, collapsed: false }).catch(() => void 0);
1256
+ }
1257
+ } catch {
1258
+ session.ungrouped.add(tabId);
1259
+ }
1260
+ } else {
1261
+ session.ungrouped.add(tabId);
682
1262
  }
683
1263
  const key = `tab-${this.#uuid().slice(0, 12)}`;
684
- session.tabs.set(key, tab.id);
1264
+ session.tabs.set(key, tabId);
685
1265
  session.active = key;
686
- return tab.id;
1266
+ return { tabId, key, ...note ? { note } : {} };
1267
+ }
1268
+ /** What a failure after the tab opened carries back: the page is there. */
1269
+ async #openedError(error, tabId, key, url) {
1270
+ const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
1271
+ const message = error instanceof Error ? error.message : String(error);
1272
+ return new OpenedError(message, { tabId: key, url: tab?.url ?? url, title: tab?.title ?? "" });
687
1273
  }
688
1274
  /** The key of this session's current tab, opening one when there is none to reuse. */
689
1275
  async #reuseOrOpen(session, url, cancel) {
@@ -694,11 +1280,17 @@ var BrowserCommands = class {
694
1280
  cancel.dispatch();
695
1281
  await this.#chrome.tabs.update(tab.id, { url });
696
1282
  await this.#waitForLoad(tab.id);
697
- return;
1283
+ return void 0;
698
1284
  }
699
1285
  }
700
1286
  const opened = await this.#openTab(session, url, cancel);
701
- await this.#waitForLoad(opened);
1287
+ try {
1288
+ await this.#waitForLoad(opened.tabId);
1289
+ } catch (error) {
1290
+ if (error instanceof CancelledError) throw error;
1291
+ throw await this.#openedError(error, opened.tabId, opened.key, url);
1292
+ }
1293
+ return opened.note;
702
1294
  }
703
1295
  /**
704
1296
  * Wait for what the input may have started, not for what was already there.
@@ -738,8 +1330,8 @@ var BrowserCommands = class {
738
1330
  session.observed = void 0;
739
1331
  this.#refs.delete(command.session);
740
1332
  cancel.check();
741
- await this.#reuseOrOpen(session, url, cancel);
742
- return {};
1333
+ const note = await this.#reuseOrOpen(session, url, cancel);
1334
+ return note ? { observation: { note } } : {};
743
1335
  }
744
1336
  async #tab(command) {
745
1337
  const session = await this.#session(command.session);
@@ -760,7 +1352,7 @@ var BrowserCommands = class {
760
1352
  for (const [key, tabId] of [...session.tabs]) {
761
1353
  const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
762
1354
  if (!tab) continue;
763
- if (session.groupId >= 0 && tab.groupId !== session.groupId) {
1355
+ if (this.#taken(session, tab)) {
764
1356
  this.#forget(session, key);
765
1357
  continue;
766
1358
  }
@@ -780,7 +1372,7 @@ var BrowserCommands = class {
780
1372
  const live = [];
781
1373
  for (const [key, tabId] of [...session.tabs]) {
782
1374
  const tab = await this.#chrome.tabs.get(tabId).catch(() => void 0);
783
- if (!tab || session.groupId >= 0 && tab.groupId !== session.groupId) {
1375
+ if (!tab || this.#taken(session, tab)) {
784
1376
  this.#forget(session, key);
785
1377
  continue;
786
1378
  }
@@ -1120,7 +1712,7 @@ Scroll: ${scroll.x}, ${scroll.y}`.slice(0, MAX_TREE);
1120
1712
  await this.#stopScreencast(command.session);
1121
1713
  throw new PreconditionError("That tab is gone. Observe again to continue in a new one.");
1122
1714
  }
1123
- if (session.groupId >= 0 && tab.groupId !== session.groupId) {
1715
+ if (this.#taken(session, tab)) {
1124
1716
  await this.#stopScreencast(command.session);
1125
1717
  throw new PreconditionError(TAKEN);
1126
1718
  }
@@ -1353,6 +1945,13 @@ function pressRef(ref, key, code, keyCode) {
1353
1945
  element.dispatchEvent(new KeyboardEvent(type, { key, code, keyCode, bubbles: true, cancelable: true }));
1354
1946
  }
1355
1947
  }
1948
+ function heldLoginFacts(raw) {
1949
+ if (!raw || typeof raw !== "object") return void 0;
1950
+ const { never, saved } = raw;
1951
+ const sites = Array.isArray(never) ? never.filter((site) => typeof site === "string" && site.length <= 253).slice(0, 500) : [];
1952
+ const kept = Array.isArray(saved) ? saved.filter((login) => typeof login?.site === "string" && typeof login?.username === "string").slice(0, 500).map(({ site, username }) => ({ site, username })) : [];
1953
+ return { never: sites, saved: kept };
1954
+ }
1356
1955
 
1357
1956
  // src/external.ts
1358
1957
  function isLoopbackOrigin(origin) {
@@ -1428,7 +2027,7 @@ function flush(open) {
1428
2027
  }
1429
2028
  }
1430
2029
  }
1431
- var commands = new BrowserCommands(chrome, { onFrame: send, onEvent: send });
2030
+ var commands = new BrowserCommands(chrome, { onFrame: send, onEvent: send, onLogin: send });
1432
2031
  var attempt = 0;
1433
2032
  var timer;
1434
2033
  var silence;
@@ -1441,6 +2040,7 @@ var protocol = new Protocol({
1441
2040
  execute: (command, cancel) => commands.run(command, cancel),
1442
2041
  // The socket ended: sessions and refs go with it. The tabs do not.
1443
2042
  onReset: () => commands.reset(),
2043
+ onLoginAck: (id, answer) => commands.logins.ack(id, answer),
1444
2044
  onState: (state) => {
1445
2045
  last = state;
1446
2046
  safely(() => chrome.runtime.sendMessage({ type: "buddi-state", state }).catch(() => void 0));
@@ -1534,9 +2134,23 @@ function schedule() {
1534
2134
  safely(() => connect());
1535
2135
  }, wait);
1536
2136
  }
1537
- chrome.runtime.onMessage.addListener((message, _sender, respond) => {
2137
+ chrome.runtime.onMessage.addListener((message, sender, respond) => {
1538
2138
  const request = message;
1539
2139
  if (!request || typeof request.type !== "string") return;
2140
+ if (request.type === GIVE_BACK_MESSAGE) {
2141
+ const tabId = sender?.tab?.id;
2142
+ if (typeof request.session === "string") safely(() => commands.giveBack(request.session, tabId).then(() => void 0));
2143
+ return;
2144
+ }
2145
+ if (request.type === LOGIN_MESSAGE || request.type === LOGIN_SEEN_MESSAGE || request.type === LOGIN_PENDING_MESSAGE) {
2146
+ const reply = commands.loginMessage(message, sender);
2147
+ if (!reply) {
2148
+ respond(null);
2149
+ return;
2150
+ }
2151
+ safely(() => reply.then((answer) => respond(answer), () => respond(null)));
2152
+ return true;
2153
+ }
1540
2154
  if (request.type === "buddi-get-state") {
1541
2155
  respond({ state: last });
1542
2156
  return;