@oharness/server 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/browser-pane.d.ts +48 -0
  2. package/dist/browser-pane.js +185 -0
  3. package/dist/browser-pane.js.map +1 -0
  4. package/dist/i18n.js +132 -0
  5. package/dist/i18n.js.map +1 -1
  6. package/dist/main.js +20 -0
  7. package/dist/main.js.map +1 -1
  8. package/dist/page/client/account.d.ts +8 -0
  9. package/dist/page/client/account.js +1895 -0
  10. package/dist/page/client/account.js.map +1 -0
  11. package/dist/page/client/artifacts.d.ts +7 -0
  12. package/dist/page/client/artifacts.js +767 -0
  13. package/dist/page/client/artifacts.js.map +1 -0
  14. package/dist/page/client/base.d.ts +8 -0
  15. package/dist/page/client/base.js +400 -0
  16. package/dist/page/client/base.js.map +1 -0
  17. package/dist/page/client/browser.d.ts +21 -0
  18. package/dist/page/client/browser.js +904 -0
  19. package/dist/page/client/browser.js.map +1 -0
  20. package/dist/page/client/composer.d.ts +8 -0
  21. package/dist/page/client/composer.js +1694 -0
  22. package/dist/page/client/composer.js.map +1 -0
  23. package/dist/page/client/dialogs.d.ts +7 -0
  24. package/dist/page/client/dialogs.js +724 -0
  25. package/dist/page/client/dialogs.js.map +1 -0
  26. package/dist/page/client/events.d.ts +7 -0
  27. package/dist/page/client/events.js +288 -0
  28. package/dist/page/client/events.js.map +1 -0
  29. package/dist/page/client/index.d.ts +16 -0
  30. package/dist/page/client/index.js +46 -0
  31. package/dist/page/client/index.js.map +1 -0
  32. package/dist/page/client/panels.d.ts +7 -0
  33. package/dist/page/client/panels.js +659 -0
  34. package/dist/page/client/panels.js.map +1 -0
  35. package/dist/page/client/pickers.d.ts +7 -0
  36. package/dist/page/client/pickers.js +624 -0
  37. package/dist/page/client/pickers.js.map +1 -0
  38. package/dist/page/client/routines.d.ts +6 -0
  39. package/dist/page/client/routines.js +910 -0
  40. package/dist/page/client/routines.js.map +1 -0
  41. package/dist/page/client/shell.d.ts +6 -0
  42. package/dist/page/client/shell.js +690 -0
  43. package/dist/page/client/shell.js.map +1 -0
  44. package/dist/page/client/state.d.ts +7 -0
  45. package/dist/page/client/state.js +1275 -0
  46. package/dist/page/client/state.js.map +1 -0
  47. package/dist/page/client/transcript.d.ts +8 -0
  48. package/dist/page/client/transcript.js +699 -0
  49. package/dist/page/client/transcript.js.map +1 -0
  50. package/dist/page/client/usage.d.ts +7 -0
  51. package/dist/page/client/usage.js +954 -0
  52. package/dist/page/client/usage.js.map +1 -0
  53. package/dist/page/style.d.ts +1 -0
  54. package/dist/page/style.js +2793 -0
  55. package/dist/page/style.js.map +1 -0
  56. package/dist/page.js +62 -13871
  57. package/dist/page.js.map +1 -1
  58. package/dist/server.d.ts +24 -0
  59. package/dist/server.js +349 -22
  60. package/dist/server.js.map +1 -1
  61. package/package.json +2 -2
package/dist/page.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AAEH,0EAA0E;AAC1E,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAA;EACtB,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAw9EV,CAAC;AAEF,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAglW5B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG;;;;;yCAKyB,CAAC;AAE1C,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;4CAOwB,kBAAkB,CAAC,OAAO,CAAC;SAC9D,KAAK;EACZ,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBA8SK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;qBACvB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;EAC1C,aAAa;EACb,UAAU;EACV,gBAAgB;;;QAGV,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../src/page.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;;;;;;;;GAcG;AAGH;;;;;;;GAOG;AACH,MAAM,OAAO,GAAG;;;;;yCAKyB,CAAC;AAE1C,MAAM,CAAC,MAAM,IAAI,GAAG;;;;;;;4CAOwB,kBAAkB,CAAC,OAAO,CAAC;SAC9D,KAAK;EACZ,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAwWK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;qBACvB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;EAC1C,aAAa;EACb,UAAU;EACV,gBAAgB;;;QAGV,CAAC"}
package/dist/server.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { PermissionRequest } from "@oharness/engine/agent/permissions.ts";
5
5
  import type { QuestionRequest, QuestionResult } from "@oharness/engine/tools/questions.ts";
6
6
  import type { SettingsScope } from "@oharness/engine/agent/settings.ts";
7
7
  import type { ScheduleRun } from "@oharness/engine/schedule/scheduler.ts";
8
+ import type { BrowserTargetFactory, BrowserUsePlugin } from "@oharness/engine";
8
9
  /**
9
10
  * A browser frontend for the same agent the terminal drives.
10
11
  *
@@ -33,6 +34,15 @@ export interface WebServerOptions {
33
34
  * have to pay for.
34
35
  */
35
36
  openProject?: (cwd: string) => Promise<Harness>;
37
+ /**
38
+ * Where browser tools get a browser, for the harnesses this server builds.
39
+ *
40
+ * The desktop shell passes its own so a packaged app drives the Chromium it
41
+ * already ships; left out, the engine's own launcher finds a Chrome on the
42
+ * machine. Either way nothing starts until a browser tool is called.
43
+ */
44
+ browserTarget?: BrowserTargetFactory;
45
+ browserUsePlugin?: BrowserUsePlugin;
36
46
  /**
37
47
  * Told when a turn starts waiting on a person — once, not again on a replay.
38
48
  * The desktop shell uses it to notify while its window is in the background;
@@ -67,6 +77,20 @@ export interface WebServerOptions {
67
77
  * and whenever it changes.
68
78
  */
69
79
  onKeepAwake?: (on: boolean) => void;
80
+ /**
81
+ * The browser extension's files on this machine, when the app ships them.
82
+ * Until the extension is in the Chrome Web Store, loading this folder is how
83
+ * it gets installed, so the page offers to show it.
84
+ */
85
+ extensionDir?: string;
86
+ /**
87
+ * Tell the browser extension where this page is, so its side panel can show
88
+ * it. On by default as a web server; the desktop app says it is the desktop
89
+ * app, which the side panel prefers. `false` for a server nobody should find.
90
+ */
91
+ presence?: {
92
+ kind: "desktop" | "web";
93
+ } | false;
70
94
  }
71
95
  export interface WebServer {
72
96
  readonly url: string;
package/dist/server.js CHANGED
@@ -17,6 +17,9 @@ import { visibleText } from "@oharness/engine/core/types.ts";
17
17
  import { errorMessage } from "@oharness/engine/core/errors.ts";
18
18
  import { officialBaseUrl, officialTeam } from "@oharness/engine/auth/providers/official.ts";
19
19
  import { loadUserConfig, updateUserConfig } from "@oharness/engine/config.ts";
20
+ import { EXTENSION_ID, EXTENSION_INSTALL_URL } from "@oharness/engine/browser/host-manifest.ts";
21
+ import { announcePresence } from "@oharness/engine/browser/presence.ts";
22
+ import { extensionHostReachable } from "@oharness/engine/browser/socket.ts";
20
23
  import { RoutineRunner } from "@oharness/engine/schedule/runner.ts";
21
24
  import { accountsConfigured, browserKeyUrl, browserSignInUrl, exchangeHandoff, siteUrl, } from "@oharness/engine/auth/providers/account.ts";
22
25
  import { ChangeLedger } from "./changes.js";
@@ -28,6 +31,7 @@ import { listDirectories, pickDirectory, pickFiles, revealDirectory } from "./pi
28
31
  import { browseFiles, searchFiles, within } from "./files.js";
29
32
  import { analyticsState } from "./analytics.js";
30
33
  import { PAGE } from "./page.js";
34
+ import { browserState, parseInputEvent, streamFrames } from "./browser-pane.js";
31
35
  import { artifactDocument, sendArtifact } from "./artifacts.js";
32
36
  import { sendProjectExport } from "./project-export.js";
33
37
  import { ArtifactStore, getArtifactStore, isCanvas, isValidArtifactId, restoreArtifacts, } from "@oharness/engine/artifacts/store.ts";
@@ -204,12 +208,23 @@ export async function startWebServer(opts) {
204
208
  const makeRunner = (session, harness) => {
205
209
  const ledger = new ChangeLedger(session.cwd);
206
210
  ledger.attach(harness.hooks);
207
- return { session, agent: harness.agent(session), harness, ledger, queue: [] };
211
+ return {
212
+ session,
213
+ agent: harness.agent(session),
214
+ harness,
215
+ browser: harness.browserFor(session.id),
216
+ ledger,
217
+ queue: [],
218
+ };
208
219
  };
209
220
  /** The runner a request names, or the most recently focused one. */
210
221
  let focused = opts.session.id;
211
222
  const runnerFor = (id) => runners.get(typeof id === "string" && id ? id : focused);
212
- runners.set(opts.session.id, makeRunner(opts.session, opts.harness));
223
+ const initial = makeRunner(opts.session, opts.harness);
224
+ // Preserve caller-supplied agent overrides for the initial session. Its
225
+ // browser is still the same one bound by Harness.agent(session).
226
+ initial.agent = opts.agent;
227
+ runners.set(opts.session.id, initial);
213
228
  let current = { session: opts.session, agent: opts.agent, harness: opts.harness };
214
229
  /**
215
230
  * Sign-ins waiting for their emailed code.
@@ -430,7 +445,12 @@ export async function startWebServer(opts) {
430
445
  if (url.pathname === "/" && req.method === "GET") {
431
446
  if (!authorized(url, req))
432
447
  return send(res, 401, { error: "missing or bad token" });
433
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
448
+ res.writeHead(200, {
449
+ "content-type": "text/html; charset=utf-8",
450
+ // Framed by itself and by the OHarness extension's side panel, and by
451
+ // nothing else: a page that drives a shell is not one to embed.
452
+ "content-security-policy": `frame-ancestors 'self' chrome-extension://${EXTENSION_ID}`,
453
+ });
434
454
  res.end(PAGE);
435
455
  return;
436
456
  }
@@ -458,6 +478,201 @@ export async function startWebServer(opts) {
458
478
  const runner = runnerFor(url.searchParams.get("session"));
459
479
  return send(res, 200, { pages: runner?.harness.extensions.listPages() ?? [] });
460
480
  }
481
+ /*
482
+ * The browser pane: what state it is in, its picture, and the input of
483
+ * somebody who has taken it over.
484
+ *
485
+ * State is polled by every open page, so it is answered from what the
486
+ * session already knows and never starts a browser to find out — a person
487
+ * who opened the pane has not asked for Chrome.
488
+ */
489
+ case "GET /browser/state": {
490
+ res.setHeader("cache-control", "no-store");
491
+ const runner = runnerFor(url.searchParams.get("session"));
492
+ return send(res, 200, await browserState(runner?.browser));
493
+ }
494
+ case "GET /browser/stream": {
495
+ const runner = runnerFor(url.searchParams.get("session"));
496
+ const quality = Number(url.searchParams.get("quality") ?? "85");
497
+ return streamFrames(runner?.browser, res, Number.isFinite(quality) ? Math.min(90, Math.max(20, quality)) : 85);
498
+ }
499
+ case "POST /browser/input": {
500
+ res.setHeader("cache-control", "no-store");
501
+ const body = await readJson(req);
502
+ const runner = runnerFor(typeof body.session === "string" ? body.session : undefined);
503
+ const session = runner?.browser;
504
+ if (!session || !session.state().running)
505
+ return send(res, 409, { error: "no browser is running" });
506
+ const event = parseInputEvent(body.event);
507
+ if (!event)
508
+ return send(res, 400, { error: "unrecognised input event" });
509
+ await session.forward(event);
510
+ return send(res, 200, { ok: true });
511
+ }
512
+ case "POST /browser/navigate": {
513
+ res.setHeader("cache-control", "no-store");
514
+ const body = await readJson(req);
515
+ const runner = runnerFor(typeof body.session === "string" ? body.session : undefined);
516
+ const session = runner?.browser;
517
+ if (!session)
518
+ return send(res, 409, { error: "browser is disabled" });
519
+ if (body.action === "reload") {
520
+ if (!session.running)
521
+ return send(res, 409, { error: "no browser is running" });
522
+ try {
523
+ return send(res, 200, await session.reload());
524
+ }
525
+ catch (error) {
526
+ return send(res, 400, { error: errorMessage(error) });
527
+ }
528
+ }
529
+ const raw = typeof body.url === "string" ? body.url.trim() : "";
530
+ if (!raw)
531
+ return send(res, 400, { error: "url is required" });
532
+ const local = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])(?::|\/|$)/i.test(raw);
533
+ const value = /^[a-z][a-z0-9+.-]*:/i.test(raw) ? raw : `${local ? "http" : "https"}://${raw}`;
534
+ let parsed;
535
+ try {
536
+ parsed = new URL(value);
537
+ }
538
+ catch {
539
+ return send(res, 400, { error: "invalid url" });
540
+ }
541
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
542
+ return send(res, 400, { error: "only http and https URLs are supported" });
543
+ }
544
+ try {
545
+ return send(res, 200, await session.navigate(parsed.href));
546
+ }
547
+ catch (error) {
548
+ return send(res, 400, { error: errorMessage(error) });
549
+ }
550
+ }
551
+ case "POST /browser/tab": {
552
+ res.setHeader("cache-control", "no-store");
553
+ const body = await readJson(req);
554
+ const runner = runnerFor(typeof body.session === "string" ? body.session : undefined);
555
+ const session = runner?.browser;
556
+ if (!session || !session.running)
557
+ return send(res, 409, { error: "no browser is running" });
558
+ if (typeof body.id !== "string" || !body.id)
559
+ return send(res, 400, { error: "tab id is required" });
560
+ if (body.action === "close") {
561
+ if (runner?.running)
562
+ return send(res, 409, { error: "stop the running turn before closing its browser tab" });
563
+ if (!session.closePaneTab)
564
+ return send(res, 501, { error: "this browser provider cannot close tabs" });
565
+ try {
566
+ return send(res, 200, await session.closePaneTab(body.id));
567
+ }
568
+ catch (error) {
569
+ return send(res, 400, { error: errorMessage(error) });
570
+ }
571
+ }
572
+ if (!session.selectPaneTab)
573
+ return send(res, 501, { error: "this browser provider cannot switch tabs" });
574
+ try {
575
+ return send(res, 200, await session.selectPaneTab(body.id));
576
+ }
577
+ catch (error) {
578
+ return send(res, 400, { error: errorMessage(error) });
579
+ }
580
+ }
581
+ case "POST /browser/close": {
582
+ res.setHeader("cache-control", "no-store");
583
+ const body = await readJson(req);
584
+ const runner = runnerFor(typeof body.session === "string" ? body.session : undefined);
585
+ const session = runner?.browser;
586
+ if (!runner || !session)
587
+ return send(res, 404, { error: "no such browser session" });
588
+ if (runner.running)
589
+ return send(res, 409, { error: "stop the running turn before stopping its browser" });
590
+ if (!session.running)
591
+ return send(res, 200, { closed: false, running: false });
592
+ await session.close();
593
+ return send(res, 200, { closed: true, running: false });
594
+ }
595
+ /*
596
+ * Importing a site's sign-in from the person's own browser.
597
+ *
598
+ * GET lists what could be imported and names the browser; it lists tabs
599
+ * and attaches to none. POST is the dialog's confirmation — the only way
600
+ * anything is read out of that browser — or its "don't ask again".
601
+ */
602
+ /**
603
+ * Hand the person's current tab to a conversation. Asked for by the
604
+ * extension's side panel, from the page the person is looking at — a
605
+ * click of theirs, never something the agent can call.
606
+ */
607
+ case "POST /browser/adopt": {
608
+ res.setHeader("cache-control", "no-store");
609
+ const body = await readJson(req);
610
+ const session = runnerFor(typeof body.session === "string" ? body.session : null)?.browser;
611
+ if (!session)
612
+ return send(res, 404, { error: "browser is disabled" });
613
+ if (!session.adoptYourTab)
614
+ return send(res, 501, { error: "this browser cannot take one of your tabs" });
615
+ const tabId = typeof body.tabId === "number" || typeof body.tabId === "string" ? String(body.tabId) : "";
616
+ if (!tabId)
617
+ return send(res, 400, { error: "tabId is required" });
618
+ try {
619
+ return send(res, 200, await session.adoptYourTab(tabId));
620
+ }
621
+ catch (error) {
622
+ return send(res, 409, { error: errorMessage(error) });
623
+ }
624
+ }
625
+ /** Whether the extension is connected, and how to get it; see extensionInfo. */
626
+ case "GET /browser/extension":
627
+ res.setHeader("cache-control", "no-store");
628
+ return send(res, 200, await extensionInfo());
629
+ /** Open the extension's folder, for loading it into Chrome by hand. */
630
+ case "POST /browser/extension/reveal": {
631
+ if (!opts.extensionDir || !(await isDirectory(opts.extensionDir)))
632
+ return send(res, 404, { error: "no extension folder here" });
633
+ const result = await revealDirectory(opts.extensionDir);
634
+ return send(res, "error" in result ? 500 : 200, result);
635
+ }
636
+ case "GET /browser/import": {
637
+ res.setHeader("cache-control", "no-store");
638
+ const session = runnerFor(url.searchParams.get("session"))?.browser;
639
+ if (!session)
640
+ return send(res, 404, { error: "browser is disabled" });
641
+ if (!session.loginImportSources)
642
+ return send(res, 501, { error: "this browser provider cannot import sign-ins" });
643
+ try {
644
+ return send(res, 200, await session.loginImportSources());
645
+ }
646
+ catch (error) {
647
+ return send(res, 409, { error: errorMessage(error) });
648
+ }
649
+ }
650
+ case "POST /browser/import": {
651
+ res.setHeader("cache-control", "no-store");
652
+ const body = await readJson(req);
653
+ const runner = runnerFor(typeof body.session === "string" ? body.session : undefined);
654
+ const session = runner?.browser;
655
+ if (!runner || !session)
656
+ return send(res, 404, { error: "no such browser session" });
657
+ if (body.action === "dismiss") {
658
+ if (typeof body.site !== "string")
659
+ return send(res, 400, { error: "site is required" });
660
+ await session.dismissLoginImport?.(body.site);
661
+ return send(res, 200, { ok: true });
662
+ }
663
+ // The import navigates the page the agent is using.
664
+ if (runner.running)
665
+ return send(res, 409, { error: "wait for the running turn to finish before importing a sign-in" });
666
+ if (!session.importLogin)
667
+ return send(res, 501, { error: "this browser provider cannot import sign-ins" });
668
+ const sites = Array.isArray(body.sites) ? body.sites.filter((site) => typeof site === "string").slice(0, 20) : [];
669
+ try {
670
+ return send(res, 200, await session.importLogin(sites));
671
+ }
672
+ catch (error) {
673
+ return send(res, 400, { error: errorMessage(error) });
674
+ }
675
+ }
461
676
  case "GET /plugin-page": {
462
677
  res.setHeader("cache-control", "no-store");
463
678
  const runner = runnerFor(url.searchParams.get("session"));
@@ -1370,6 +1585,10 @@ export async function startWebServer(opts) {
1370
1585
  // The whole status, not just the path: the page needs to know
1371
1586
  // whether the switch it draws is the user's to flip.
1372
1587
  audit: await current.harness.auditStatus(),
1588
+ browserMode: current.harness.browserMode(),
1589
+ // For the browser row: whether the person's browser can be reached
1590
+ // now, and where to get the extension when it cannot.
1591
+ browserExtension: await extensionInfo(),
1373
1592
  settingsPaths: {
1374
1593
  user: current.harness.settings.pathFor("user"),
1375
1594
  project: current.harness.settings.pathFor("project"),
@@ -1392,6 +1611,23 @@ export async function startWebServer(opts) {
1392
1611
  const saved = await current.harness.setWritableRoots(body.roots);
1393
1612
  return send(res, 200, { roots: saved });
1394
1613
  }
1614
+ /**
1615
+ * The built-in browser or the person's own, through the extension.
1616
+ *
1617
+ * A machine preference, so every project's harness moves with it — a
1618
+ * switch that changed only the project on screen would look ignored the
1619
+ * moment someone opened another one.
1620
+ */
1621
+ case "POST /settings/browser-mode": {
1622
+ const body = await readJson(req);
1623
+ if (body.mode !== "auto" && body.mode !== "builtin" && body.mode !== "extension") {
1624
+ return send(res, 400, { error: "mode must be auto, builtin or extension" });
1625
+ }
1626
+ for (const harness of new Set([current.harness, ...harnesses.values()])) {
1627
+ await harness.setBrowserMode(body.mode);
1628
+ }
1629
+ return send(res, 200, { mode: body.mode });
1630
+ }
1395
1631
  /**
1396
1632
  * Who is signed in, if anyone.
1397
1633
  *
@@ -2253,6 +2489,7 @@ export async function startWebServer(opts) {
2253
2489
  const runner = runners.get(id);
2254
2490
  runner?.running?.abort();
2255
2491
  runners.delete(id);
2492
+ await runner?.harness.closeBrowserFor(id);
2256
2493
  // Its artifacts move from memory to the log half of the catalogue,
2257
2494
  // and a fold taken before this session existed does not have them.
2258
2495
  cachedLogs = undefined;
@@ -2282,7 +2519,9 @@ export async function startWebServer(opts) {
2282
2519
  // knowing about it at all, not the absence of a transcript.
2283
2520
  if (!deleted && !runners.has(id))
2284
2521
  return send(res, 404, { error: "no such session" });
2522
+ const removed = runners.get(id);
2285
2523
  runners.delete(id);
2524
+ await removed?.harness.closeBrowserFor(id);
2286
2525
  // Its artifact log went with it, and a fold taken a moment ago
2287
2526
  // still names it.
2288
2527
  cachedLogs = undefined;
@@ -2344,6 +2583,7 @@ export async function startWebServer(opts) {
2344
2583
  const runner = runnerFor(id);
2345
2584
  if (runner)
2346
2585
  runner.session.title = title;
2586
+ broadcast({ type: "title", session: id, title });
2347
2587
  return send(res, 200, { ok: true, id, title });
2348
2588
  }
2349
2589
  case "POST /upload": {
@@ -2557,14 +2797,14 @@ export async function startWebServer(opts) {
2557
2797
  // attempt could be replayed by anything that saw it in the address bar.
2558
2798
  pendingBrowserSignIns.delete(state);
2559
2799
  if (!pending) {
2560
- return closeTab(res, 400, "That sign-in has expired. Start it again from the app.");
2800
+ return closeTab(res, 400, "That sign-in has expired.", "These links are only good for a few minutes. Start the sign-in again from OHarness.");
2561
2801
  }
2562
2802
  const failure = url.searchParams.get("error");
2563
2803
  if (failure)
2564
- return closeTab(res, 400, failure);
2804
+ return closeTab(res, 400, "That sign-in did not go through.", failure);
2565
2805
  const code = url.searchParams.get("code") ?? "";
2566
2806
  if (!code)
2567
- return closeTab(res, 400, "The site sent no sign-in back.");
2807
+ return closeTab(res, 400, "That sign-in did not go through.", "The site sent no sign-in back. Start it again from OHarness.");
2568
2808
  const result = await exchangeHandoff(code, state);
2569
2809
  if (result.type !== "success") {
2570
2810
  const message = result.error ?? "that sign-in was not accepted";
@@ -2572,19 +2812,19 @@ export async function startWebServer(opts) {
2572
2812
  type: "notice",
2573
2813
  message: `${pending.intent === "key" ? "getting a key" : "sign-in"} failed: ${message}`,
2574
2814
  });
2575
- return closeTab(res, 400, message);
2815
+ return closeTab(res, 400, "That sign-in was not accepted.", message);
2576
2816
  }
2577
2817
  try {
2578
2818
  await current.harness.auth.complete(pending.provider, result);
2579
2819
  }
2580
2820
  catch (err) {
2581
- return closeTab(res, 500, errorMessage(err));
2821
+ return closeTab(res, 500, "That sign-in could not be saved.", errorMessage(err));
2582
2822
  }
2583
2823
  // What happened, in the words of the errand that was run. "Signed in" on
2584
2824
  // a tab where somebody just created an API key is a small lie that makes
2585
2825
  // them wonder which of the two actually took.
2586
2826
  closeTab(res, 200, pending.intent === "key"
2587
- ? `That key is now in use for ${pending.provider}. You can close this tab.`
2827
+ ? `That key is now in use for ${pending.provider}.`
2588
2828
  : `Signed in to ${pending.provider}.`);
2589
2829
  // Same as a saved key: the credential is stored, now go ask the gateway
2590
2830
  // what the account can actually call.
@@ -2851,6 +3091,8 @@ export async function startWebServer(opts) {
2851
3091
  cwd: project.path,
2852
3092
  permissionPrompt: (request, signal) => permissionPrompt(request, signal),
2853
3093
  questionPrompt: (request, signal) => questionPrompt(request, signal),
3094
+ ...(opts.browserTarget ? { browserTarget: opts.browserTarget } : {}),
3095
+ ...(opts.browserUsePlugin ? { browserUsePlugin: opts.browserUsePlugin } : {}),
2854
3096
  }));
2855
3097
  harnesses.set(project.path, harness);
2856
3098
  watchConnections(harness);
@@ -2871,6 +3113,8 @@ export async function startWebServer(opts) {
2871
3113
  cwd,
2872
3114
  permissionPrompt: async () => false,
2873
3115
  questionPrompt: async () => ({ outcome: "unavailable" }),
3116
+ ...(opts.browserTarget ? { browserTarget: opts.browserTarget } : {}),
3117
+ ...(opts.browserUsePlugin ? { browserUsePlugin: opts.browserUsePlugin } : {}),
2874
3118
  }),
2875
3119
  onStart: (routine, meta) => {
2876
3120
  if (meta.catchUp)
@@ -3039,8 +3283,16 @@ export async function startWebServer(opts) {
3039
3283
  const text = note ? `${note}\n\n${prompt}` : prompt;
3040
3284
  const input = staged.length ? buildMessage(text, staged) : text;
3041
3285
  let failed = false;
3286
+ // The session names itself from its first prompt once the agent takes it
3287
+ // in. Said the moment that happens: waiting for the idle state left a new
3288
+ // tab reading "Untitled" for the whole of its first turn.
3289
+ let untitled = !runner.session.title;
3042
3290
  try {
3043
3291
  for await (const event of runner.agent.run(input, { signal: controller.signal })) {
3292
+ if (untitled && runner.session.title) {
3293
+ untitled = false;
3294
+ broadcast({ type: "title", session: id, title: runner.session.title });
3295
+ }
3044
3296
  // The agent's own question_request is not forwarded: the page draws
3045
3297
  // the card from questionPrompt's announce, which carries the id the
3046
3298
  // answer is posted back to. Forwarding both drew the card twice, and
@@ -3084,6 +3336,12 @@ export async function startWebServer(opts) {
3084
3336
  ...(typeof err?.provider === "string"
3085
3337
  ? { provider: err.provider }
3086
3338
  : {}),
3339
+ // And the code, for the failure that carries no status: nothing was
3340
+ // sent, so there is no HTTP answer to read — but "not signed in" is
3341
+ // just as much a dialog to open as a refused key is.
3342
+ ...(typeof err?.code === "string"
3343
+ ? { code: err.code }
3344
+ : {}),
3087
3345
  fatal: true,
3088
3346
  });
3089
3347
  }
@@ -3275,6 +3533,18 @@ export async function startWebServer(opts) {
3275
3533
  return send(res, 404, { error: "no such artifact" });
3276
3534
  sendArtifact(res, found.artifact, found.revision);
3277
3535
  }
3536
+ /**
3537
+ * What the page needs to help someone get the browser extension: whether it
3538
+ * is connected now, where to get it, and — where this app ships its files —
3539
+ * that there is a folder to load it from.
3540
+ */
3541
+ async function extensionInfo() {
3542
+ return {
3543
+ connected: await extensionHostReachable(),
3544
+ installUrl: EXTENSION_INSTALL_URL,
3545
+ folder: Boolean(opts.extensionDir) && (await isDirectory(opts.extensionDir)),
3546
+ };
3547
+ }
3278
3548
  function constantTimeEqual(supplied, expected) {
3279
3549
  if (!supplied)
3280
3550
  return false;
@@ -3317,6 +3587,9 @@ export async function startWebServer(opts) {
3317
3587
  }
3318
3588
  const address = server.address();
3319
3589
  const port = typeof address === "object" && address ? address.port : opts.port;
3590
+ const presence = opts.presence === false
3591
+ ? undefined
3592
+ : announcePresence({ url: `http://${host}:${port}`, token, kind: opts.presence?.kind ?? "web" });
3320
3593
  return {
3321
3594
  url: `http://${host}:${port}/?token=${token}`,
3322
3595
  token,
@@ -3324,6 +3597,7 @@ export async function startWebServer(opts) {
3324
3597
  questionPrompt,
3325
3598
  async close() {
3326
3599
  clearInterval(staleTimer);
3600
+ presence?.close();
3327
3601
  await routines.close();
3328
3602
  for (const unsubscribe of connectionSubscriptions)
3329
3603
  unsubscribe();
@@ -3535,35 +3809,88 @@ function isLoopbackHost(host) {
3535
3809
  }
3536
3810
  return hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "localhost";
3537
3811
  }
3538
- function closeTab(res, status, message) {
3539
- const escaped = message.replace(/[&<>]/g, (char) => char === "&" ? "&amp;" : char === "<" ? "&lt;" : "&gt;");
3812
+ /**
3813
+ * The last thing a sign-in shows anybody.
3814
+ *
3815
+ * A tab the app opened, on a loopback port, that exists to say one sentence
3816
+ * and be closed. It is still the product's own page, though — for a lot of
3817
+ * people it is the first screen after "Continue with Google" — so it gets the
3818
+ * mark, a state that is legible at a glance, and a line saying what to do
3819
+ * next. `message` is the sentence; `hint` is what follows it, and defaults to
3820
+ * the only useful instruction a successful sign-in has.
3821
+ *
3822
+ * Failure is carried by the badge and by one tinted rule, not by a heading set
3823
+ * in red: a red sentence reads as the page shouting, and the sentence is
3824
+ * usually the calmest thing on it ("that sign-in has expired").
3825
+ */
3826
+ function closeTab(res, status, message, hint) {
3540
3827
  const ok = status < 400;
3828
+ const escape = (text) => text.replace(/[&<>]/g, (char) => (char === "&" ? "&amp;" : char === "<" ? "&lt;" : "&gt;"));
3829
+ const heading = escape(message);
3830
+ const body = escape(hint ?? (ok ? "You can close this tab and go back to OHarness." : "Start it again from OHarness."));
3541
3831
  const page = `<!doctype html><meta charset="utf-8"><title>OHarness</title>` +
3542
3832
  `<meta name="viewport" content="width=device-width,initial-scale=1">` +
3543
3833
  `<style>` +
3544
3834
  `:root{color-scheme:light dark;` +
3545
- `--bg:#fff;--card:#fff;--fg:#1a1a1a;--dim:#6b7280;--line:#00000012;--mark:#111827;--bad:#b91c1c}` +
3835
+ `--bg:#fafafa;--card:#fff;--fg:#141618;--dim:#6b7280;--line:#0000001a;--mark:#111827;` +
3836
+ `--good:#047857;--good-bg:#0478571f;--bad:#b91c1c;--bad-bg:#b91c1c1a;--glow:#f9731640}` +
3546
3837
  `@media(prefers-color-scheme:dark){:root{` +
3547
- `--bg:#0e1013;--card:#15171c;--fg:#e6e6e6;--dim:#8b93a1;--line:#ffffff14;--mark:#e5e7eb;--bad:#f87171}}` +
3838
+ `--bg:#0a0b0d;--card:#141619;--fg:#e8e8e8;--dim:#8b93a1;--line:#ffffff14;--mark:#e5e7eb;` +
3839
+ `--good:#6ee7b7;--good-bg:#6ee7b71f;--bad:#f87171;--bad-bg:#f871711f;--glow:#f9731636}}` +
3548
3840
  `*{box-sizing:border-box}` +
3549
- `body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;` +
3841
+ // The two washes are the site's own lighting, reduced to what a page this
3842
+ // small can carry: a warm pool above the card and a cooler one below it.
3843
+ // Fixed, so a short page does not scroll them into view as a seam.
3844
+ `body{margin:0;min-height:100svh;display:grid;place-items:center;padding:24px;` +
3550
3845
  `background:var(--bg);color:var(--fg);` +
3846
+ `background-image:radial-gradient(60% 46% at 50% 4%,var(--glow) 0%,transparent 68%),` +
3847
+ `radial-gradient(48% 38% at 50% 100%,#6366f11f 0%,transparent 70%);` +
3848
+ `background-attachment:fixed;` +
3551
3849
  `font:15px/1.6 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Inter,sans-serif;` +
3552
3850
  `-webkit-font-smoothing:antialiased}` +
3553
- `main{width:100%;max-width:26rem;padding:28px;border-radius:16px;background:var(--card);` +
3554
- `border:1px solid var(--line);box-shadow:0 1px 2px #0000000f,0 20px 60px #00000014;text-align:center}` +
3555
- `svg{width:34px;height:34px;color:var(--mark)}` +
3556
- `h1{margin:14px 0 0;font-size:16px;font-weight:600;letter-spacing:-.01em}` +
3557
- `h1.bad{color:var(--bad)}` +
3558
- `p{margin:8px 0 0;font-size:13px;color:var(--dim)}` +
3851
+ `main{width:100%;max-width:25rem;padding:32px 28px 26px;border-radius:18px;background:var(--card);` +
3852
+ `border:1px solid var(--line);box-shadow:0 1px 2px #0000000f,0 24px 60px #00000026;text-align:center;` +
3853
+ `animation:rise .34s cubic-bezier(.2,.8,.3,1) both}` +
3854
+ `@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}` +
3855
+ `@media(prefers-reduced-motion:reduce){main{animation:none}}` +
3856
+ // The mark alone, in the state's colour. A disc behind it made a third
3857
+ // bordered shape on a card that only has one thing to say, and a filled
3858
+ // circle reads as a notification — nothing here needs acknowledging.
3859
+ `.badge{display:grid;place-items:center;color:var(--tone)}` +
3860
+ `.badge svg{width:30px;height:30px}` +
3861
+ `.ok{--tone:var(--good);--tone-bg:var(--good-bg)}` +
3862
+ `.bad{--tone:var(--bad);--tone-bg:var(--bad-bg)}` +
3863
+ `h1{margin:18px 0 0;font-size:17px;font-weight:600;letter-spacing:-.012em;line-height:1.4}` +
3864
+ `p{margin:9px 0 0;font-size:13.5px;color:var(--dim)}` +
3865
+ // The mark, at a size that says whose page this is without competing with
3866
+ // the one sentence it exists to show. No rule above it: the gap already
3867
+ // separates it, and a divider on a card this short is a second border
3868
+ // around almost nothing.
3869
+ `footer{display:flex;align-items:center;justify-content:center;gap:7px;` +
3870
+ `margin-top:26px;opacity:.75;` +
3871
+ `font-size:12px;font-weight:550;letter-spacing:.01em;color:var(--dim)}` +
3872
+ `footer svg{width:15px;height:15px;color:var(--mark)}` +
3559
3873
  `</style>` +
3560
3874
  `<body><main>` +
3875
+ `<div class="badge ${ok ? "ok" : "bad"}">` +
3876
+ `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" ` +
3877
+ `stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">` +
3878
+ (ok
3879
+ ? `<path d="M4 12.5 9.5 18 20 6.5"/>`
3880
+ : // The ring belongs to the glyph, not behind it: a bare exclamation
3881
+ // stroke, with nothing around it, reads as a piece of punctuation
3882
+ // that lost its sentence.
3883
+ `<circle cx="12" cy="12" r="9.2" stroke-width="1.9"/><path d="M12 7.4v5.4"/>` +
3884
+ `<circle cx="12" cy="16.4" r="1.15" fill="currentColor" stroke="none"/>`) +
3885
+ `</svg></div>` +
3886
+ `<h1>${heading}</h1>` +
3887
+ `<p>${body}</p>` +
3888
+ `<footer>` +
3561
3889
  `<svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.2" ` +
3562
3890
  `stroke-linecap="round" aria-hidden="true">` +
3563
3891
  `<path d="M20 6h-8a6 6 0 0 0-6 6v8"/><path d="M12 26h8a6 6 0 0 0 6-6v-8"/>` +
3564
3892
  `<circle cx="16" cy="16" r="3.5" fill="currentColor" stroke="none"/></svg>` +
3565
- `<h1${ok ? "" : ' class="bad"'}>${escaped}</h1>` +
3566
- (ok ? `<p>You can close this tab and go back to OHarness.</p>` : "") +
3893
+ `OHarness</footer>` +
3567
3894
  `</main>`;
3568
3895
  res.writeHead(status, {
3569
3896
  "content-type": "text/html; charset=utf-8",