whipdesk 0.0.9 → 0.1.2

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.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # WhipDesk
2
+
3
+ > Control your desktop — and the AI coding agents running on it — from any phone browser.
4
+
5
+ [![CI](https://github.com/BinaryBananaLLC/WhipDesk/actions/workflows/ci.yml/badge.svg)](https://github.com/BinaryBananaLLC/WhipDesk/actions/workflows/ci.yml)
6
+ [![Release](https://img.shields.io/github/v/release/BinaryBananaLLC/WhipDesk)](https://github.com/BinaryBananaLLC/WhipDesk/releases/latest)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/BinaryBananaLLC/WhipDesk/badge)](https://scorecard.dev/viewer/?uri=github.com/BinaryBananaLLC/WhipDesk)
8
+ [![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/LICENSE)
9
+ ![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)
10
+
11
+ WhipDesk turns any mobile browser into a remote control for your dev machine. See the screen,
12
+ move the mouse, type, paste prompts into your AI tools, and get a push notification the moment a
13
+ long-running build or agent job finishes — no app store install, no kernel extension, no agent
14
+ running on your phone.
15
+
16
+ Because it's **screen-level**, it works with *every* AI agent — Claude Code and Codex in a
17
+ terminal, Copilot Chat inside VS Code, Cursor, a browser tab running tests — with **no wrappers
18
+ and no hooks required**: you never change how you launch your tools. CLI-wrapper apps only see
19
+ the one agent they wrap; WhipDesk sees your actual desktop.
20
+
21
+ It's peer-to-peer: an encrypted WebRTC connection that talks **directly** between phone and
22
+ desktop, on your LAN or across the internet. The cloud only brokers the initial handshake — your
23
+ screen and keystrokes never flow through anyone else's server.
24
+
25
+ ## Features
26
+
27
+ - **Live screen, tuned for mobile data** — direct H.264, hardware-encoded. When you zoom, the
28
+ host **re-crops the encode to just your phone's viewport**, so a magnified terminal costs a
29
+ fraction of the bandwidth of streaming the whole desktop — full-desktop streamers (RustDesk,
30
+ Parsec, Chrome Remote Desktop) always ship every pixel. A quality ladder steps the bitrate
31
+ down automatically on a lossy cellular link, and encoding pauses entirely while your phone's
32
+ screen is off.
33
+ - **AI-agent monitoring (auto-whips)** — the host detects running agents (Claude Code, Codex,
34
+ Gemini CLI, Aider, Copilot — including Copilot Chat inside VS Code — opencode, Cursor, Amp) by
35
+ observing processes and transcripts, and pings your phone the moment one stops working: waiting
36
+ on you, finished, or crashed. Zero config; optional [agent-native hooks](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/docs/HOOKS.md) make the
37
+ alert instant.
38
+ - **Full input** — mouse, touch, and keyboard injected into the real OS; built for poking at
39
+ AI agents and CLIs from your couch.
40
+ - **Job-done notifications** — a token-authenticated webhook (`POST /api/notify`) or an opt-in
41
+ file watcher fires a notification when a slow task completes; optional background push to a
42
+ closed PWA via FCM.
43
+ - **Private by design** — DTLS-encrypted P2P media, a PIN challenge on **every** connection (the
44
+ PIN itself never crosses the wire), a pairing token underneath, and persistent brute-force
45
+ lockout. Secrets stay on your machine. See [SECURITY.md](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/SECURITY.md) for the threat model.
46
+ - **No account needed on LAN** — cloud (remote access, device dashboard) is strictly opt-in.
47
+
48
+ ## Quick start (macOS)
49
+
50
+ ```bash
51
+ npm install
52
+ npm run dev # builds the web controller, then starts the desktop agent
53
+ ```
54
+
55
+ The agent prints a `http://<lan-ip>:8787/#t=<token>` URL and a QR code. Open it on your phone
56
+ (same Wi-Fi), then grant the launching terminal app **Screen Recording** and **Accessibility**
57
+ in System Settings → Privacy & Security, and restart it. Without those, frames show only the
58
+ wallpaper and input does nothing.
59
+
60
+ Fire a test notification:
61
+
62
+ ```bash
63
+ npm run notify -- "Build done" "tsc finished with 0 errors"
64
+ ```
65
+
66
+ > Tested on macOS (Apple Silicon). The capture/input stack is cross-platform, but Windows and
67
+ > Linux hosts aren't verified yet.
68
+
69
+ ## Install a prebuilt agent
70
+
71
+ You don't have to build from source. Every [release](https://github.com/BinaryBananaLLC/WhipDesk/releases/latest)
72
+ ships two ways to run the agent — both built by GitHub Actions straight from the tagged source,
73
+ with [verifiable build provenance](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/docs/VERIFYING-DOWNLOADS.md):
74
+
75
+ **Homebrew (macOS).**
76
+
77
+ ```bash
78
+ brew install --cask BinaryBananaLLC/whipdesk/whipdesk
79
+ whipdesk
80
+ ```
81
+
82
+ **Self-contained download (no Node needed).** Grab the package for your OS:
83
+
84
+ | OS | Asset | Notes |
85
+ | --- | --- | --- |
86
+ | macOS | `whipdesk-<ver>-macos-arm64.pkg` / `-x64.pkg` | Signed with a Developer ID & **notarized** — installs `whipdesk` to your PATH. |
87
+ | Windows | `whipdesk-<ver>-windows-x64.zip` | Unzip and run `whipdesk.exe`. SmartScreen: **More info → Run anyway** (see verification below). |
88
+ | Linux | `whipdesk-<ver>-linux-x64.tar.gz` | Extract and run `./whipdesk` (needs X11). |
89
+
90
+ **npm (if you already have Node ≥ 20):**
91
+
92
+ ```bash
93
+ npm install -g whipdesk
94
+ whipdesk
95
+ ```
96
+
97
+ The npm package is published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements)
98
+ (the verified badge on npmjs.com links back to the exact build).
99
+
100
+ Prebuilt agents keep their pairing/PIN state in `~/.whipdesk`, so updates don't re-pair you.
101
+ **Always verify a download before running it** — see [docs/VERIFYING-DOWNLOADS.md](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/docs/VERIFYING-DOWNLOADS.md).
102
+
103
+ ## Setup & permissions (troubleshooting)
104
+
105
+ The agent prints a short reminder for your OS at startup. If the screen shows only your wallpaper,
106
+ or the mouse/keyboard don't respond, it's almost always an OS permission — here's the fix per
107
+ platform:
108
+
109
+ **macOS** — grant the app that *launched* the agent (Terminal, iTerm, or VS Code), not "node":
110
+
111
+ 1. **Screen Recording** — System Settings → Privacy & Security → Screen Recording → enable the app.
112
+ Without it, frames show only the wallpaper.
113
+ 2. **Accessibility** — System Settings → Privacy & Security → Accessibility → enable the app.
114
+ Without it, mouse and keyboard input do nothing.
115
+ 3. **Fully quit and reopen** that app (a plain window-close isn't enough), then run it again.
116
+
117
+ Shortcut to the right pane: `open "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture"`
118
+
119
+ **Windows** — capture and input work out of the box. If the screen is black or clicks are ignored
120
+ on an elevated window (a UAC prompt or an app "Run as administrator"), relaunch your terminal via
121
+ **Run as administrator** so the agent can see and drive those windows.
122
+
123
+ **Linux** — X11 sessions work out of the box. On **Wayland**, screen capture is gated behind the
124
+ desktop's screen-share portal: install/enable `xdg-desktop-portal` (plus your compositor's backend,
125
+ e.g. `xdg-desktop-portal-wlr` or `-gnome`), or log in under an **X11/Xorg** session instead of
126
+ Wayland. Input injection also needs access to `/dev/uinput` on some setups.
127
+
128
+ If capture is genuinely blocked, the agent also logs step-by-step help and pushes a
129
+ "Screen capture blocked" alert to your phone.
130
+
131
+ ## How it works
132
+
133
+ A **desktop agent** (Node) captures the screen, injects input, and serves the **web controller**
134
+ (a framework-light vanilla-TS PWA). They speak one message contract
135
+ ([`packages/protocol`](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/packages/protocol)) over WebRTC — one DataChannel for control plus H.264
136
+ video tracks, all DTLS-encrypted — with two ways to broker the handshake:
137
+
138
+ - **LAN** — the agent's own WebSocket swaps the SDP offer/answer; the media flows host-to-host
139
+ on your network, touching nothing else.
140
+ - **Remote** — Firebase Realtime Database swaps the SDP; STUN connects directly, with
141
+ ephemeral-credential TURN as a last-resort relay.
142
+
143
+ Every session goes through the same gate: pairing token → PIN challenge → only then does the
144
+ screen start. Pointer coordinates travel normalized to `[0,1]`, so control is resolution- and
145
+ Retina-independent. See [docs/ARCHITECTURE.md](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/docs/ARCHITECTURE.md) for the design seams.
146
+
147
+ ## Cloud (optional)
148
+
149
+ LAN needs nothing. Remote access and the device dashboard run on the **hosted WhipDesk.com
150
+ backend, which is baked into this repo** (`apps/desktop-agent/src/cloud/config.ts`) — so it just
151
+ works out of the box. On startup the agent asks whether to enable secure cloud discovery; answer
152
+ **No** to stay strictly LAN-only, and nothing ever touches Firebase.
153
+
154
+ Those baked-in values are a Firebase **web** config — public by design, the same ones whipdesk.com
155
+ serves in its browser bundle. They are **not** secrets: there's no service-account key, cloud is
156
+ opt-in, and every read/write is locked to your own account by the Firestore/RTDB rules. The agent
157
+ signs in as the real user via passwordless email-link — the same account as the website.
158
+
159
+ **Prefer your own backend?** Point the agent at your own Firebase project + TURN by dropping a web
160
+ config in `.whipdesk/firebase.json` (gitignored) — that file is the only override, and it
161
+ replaces the baked-in default.
162
+
163
+ ## Project layout
164
+
165
+ ```
166
+ packages/protocol/ Types-only wire contract shared by both apps
167
+ apps/desktop-agent/ Node host: capture, input, server, watchers, cloud
168
+ apps/mobile-web/ Vite PWA controller, served by the agent
169
+ scripts/ notify.mjs + smoke-test harnesses
170
+ docs/ARCHITECTURE.md Design rationale
171
+ docs/HOOKS.md Optional agent-native hooks for instant monitoring alerts
172
+ ```
173
+
174
+ ## Contributing
175
+
176
+ PRs welcome. Read [AGENTS.md](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/AGENTS.md) first — it's the operating contract for both humans and
177
+ AI coding agents (where each change goes, the wire-contract-first rule, how to verify). In short:
178
+
179
+ ```bash
180
+ npm run typecheck # tsc --noEmit across workspaces
181
+ npm run test # node --test (auth handshake, pin, monitor states, crypto, protocol contract)
182
+ ```
183
+
184
+ ## Security
185
+
186
+ Found a vulnerability? Please open a [security advisory](https://github.com/BinaryBananaLLC/WhipDesk/security/advisories/new)
187
+ rather than a public issue. Good-faith security research is welcome.
188
+
189
+ ## License
190
+
191
+ [GNU AGPL-3.0](https://github.com/BinaryBananaLLC/WhipDesk/blob/main/LICENSE). You're free to run, study, modify, and share WhipDesk. If you offer a
192
+ modified version as a network service, the AGPL requires you to publish your source under the
193
+ same license. For commercial licensing, contact BinaryBanana LLC.
package/dist/agent.cjs CHANGED
@@ -18909,7 +18909,7 @@ var require_view = __commonJS({
18909
18909
  var dirname3 = path.dirname;
18910
18910
  var basename2 = path.basename;
18911
18911
  var extname = path.extname;
18912
- var join10 = path.join;
18912
+ var join11 = path.join;
18913
18913
  var resolve = path.resolve;
18914
18914
  module2.exports = View;
18915
18915
  function View(name, options) {
@@ -18971,12 +18971,12 @@ var require_view = __commonJS({
18971
18971
  };
18972
18972
  View.prototype.resolve = function resolve2(dir, file) {
18973
18973
  var ext = this.ext;
18974
- var path2 = join10(dir, file);
18974
+ var path2 = join11(dir, file);
18975
18975
  var stat2 = tryStat(path2);
18976
18976
  if (stat2 && stat2.isFile()) {
18977
18977
  return path2;
18978
18978
  }
18979
- path2 = join10(dir, basename2(file, ext), "index" + ext);
18979
+ path2 = join11(dir, basename2(file, ext), "index" + ext);
18980
18980
  stat2 = tryStat(path2);
18981
18981
  if (stat2 && stat2.isFile()) {
18982
18982
  return path2;
@@ -22802,7 +22802,7 @@ var require_send = __commonJS({
22802
22802
  var Stream = require("stream");
22803
22803
  var util = require("util");
22804
22804
  var extname = path.extname;
22805
- var join10 = path.join;
22805
+ var join11 = path.join;
22806
22806
  var normalize = path.normalize;
22807
22807
  var resolve = path.resolve;
22808
22808
  var sep2 = path.sep;
@@ -22974,7 +22974,7 @@ var require_send = __commonJS({
22974
22974
  return res;
22975
22975
  }
22976
22976
  parts = path2.split(sep2);
22977
- path2 = normalize(join10(root, path2));
22977
+ path2 = normalize(join11(root, path2));
22978
22978
  } else {
22979
22979
  if (UP_PATH_REGEXP.test(path2)) {
22980
22980
  debug('malicious path "%s"', path2);
@@ -23107,7 +23107,7 @@ var require_send = __commonJS({
23107
23107
  if (err) return self.onStatError(err);
23108
23108
  return self.error(404);
23109
23109
  }
23110
- var p = join10(path2, self._index[i]);
23110
+ var p = join11(path2, self._index[i]);
23111
23111
  debug('stat "%s"', p);
23112
23112
  fs.stat(p, function(err2, stat2) {
23113
23113
  if (err2) return next(err2);
@@ -27693,7 +27693,7 @@ function isPackaged() {
27693
27693
  }
27694
27694
 
27695
27695
  // src/version.ts
27696
- var AGENT_VERSION = "0.0.9";
27696
+ var AGENT_VERSION = "0.1.2";
27697
27697
 
27698
27698
  // src/config.ts
27699
27699
  var here = (0, import_node_path2.dirname)((0, import_node_url2.fileURLToPath)(__whipdesk_meta_url));
@@ -28650,6 +28650,13 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28650
28650
  spawnAt = 0;
28651
28651
  lastPacketAt = 0;
28652
28652
  everGotPacket = false;
28653
+ /** Whether the CURRENT spawn has delivered any packet — distinguishes a capture that "went
28654
+ * silent" from one that never started (the avfoundation zero-frame deadlock) in stall logs. */
28655
+ spawnGotPacket = false;
28656
+ /** Consecutive stall-restarts whose spawn never produced a frame (resets on any packet). */
28657
+ framelessRestarts = 0;
28658
+ /** Resolves the restart loop's bounded wait as soon as the current spawn's first packet lands. */
28659
+ firstPacketWaiter = null;
28653
28660
  deadRestarts = 0;
28654
28661
  erroredOut = false;
28655
28662
  stopped = false;
@@ -28674,7 +28681,7 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28674
28681
  this.activeListener = cb;
28675
28682
  }
28676
28683
  async start() {
28677
- await this.spawn();
28684
+ await this.restart();
28678
28685
  }
28679
28686
  /** Change the zoom crop or target display. A no-op when nothing changed, so a redundant viewport
28680
28687
  * echo never restarts ffmpeg. If a restart is already running (the controller settled a new zoom
@@ -28702,16 +28709,28 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28702
28709
  return false;
28703
28710
  }
28704
28711
  const ov = !isFull(cfg.crop) && cfg.overview ? cfg.overview : null;
28705
- let sock;
28712
+ let sock = null;
28706
28713
  let sockOv = null;
28707
28714
  try {
28708
28715
  sock = await bindUdp();
28709
- if (ov) sockOv = await bindUdp();
28716
+ if (ov) {
28717
+ sockOv = await bindUdp();
28718
+ for (let tries = 0; tries < 4; tries++) {
28719
+ const p = sock.address().port;
28720
+ const q = sockOv.address().port;
28721
+ if (Math.abs(p - q) > 1) break;
28722
+ const again = await bindUdp();
28723
+ closeQuietly(sockOv);
28724
+ sockOv = again;
28725
+ }
28726
+ }
28710
28727
  } catch {
28728
+ if (sock) closeQuietly(sock);
28711
28729
  if (sockOv) closeQuietly(sockOv);
28712
28730
  this.fail();
28713
28731
  return false;
28714
28732
  }
28733
+ if (!sock) return false;
28715
28734
  if (this.stopped) {
28716
28735
  closeQuietly(sock);
28717
28736
  if (sockOv) closeQuietly(sockOv);
@@ -28731,6 +28750,12 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28731
28750
  "yuv420p",
28732
28751
  "-g",
28733
28752
  String(gop),
28753
+ // Also key on WALL-CLOCK time, not just frame count: with VFR a static screen yields few output
28754
+ // frames, so a frames-based GOP can leave a lost IDR unrepaired "until something moves". Forcing
28755
+ // a keyframe ~1s (by PTS, which advances via -use_wallclock_as_timestamps) guarantees the client
28756
+ // can always re-sync within ~1s even with no PLI and a frozen desktop.
28757
+ "-force_key_frames",
28758
+ "expr:gte(t,n_forced*1)",
28734
28759
  "-b:v",
28735
28760
  kbpsArg(kbps),
28736
28761
  "-maxrate",
@@ -28774,14 +28799,18 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28774
28799
  this.appliedCfg = cfg;
28775
28800
  this.spawnAt = Date.now();
28776
28801
  this.lastPacketAt = Date.now();
28802
+ this.spawnGotPacket = false;
28777
28803
  const spawnCrop = cfg.crop;
28778
28804
  let firstPacket = true;
28779
28805
  sock.on("message", (msg) => {
28780
28806
  this.lastPacketAt = Date.now();
28781
28807
  this.everGotPacket = true;
28808
+ this.spawnGotPacket = true;
28782
28809
  this.deadRestarts = 0;
28783
28810
  if (firstPacket) {
28784
28811
  firstPacket = false;
28812
+ this.framelessRestarts = 0;
28813
+ this.firstPacketWaiter?.();
28785
28814
  this.activeListener?.(spawnCrop);
28786
28815
  }
28787
28816
  this.listener?.(msg);
@@ -28801,9 +28830,13 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28801
28830
  }
28802
28831
  });
28803
28832
  proc.on("error", (e) => log.warn("video encoder process error:", e.message));
28833
+ proc.on("exit", (code, sig) => {
28834
+ if (this.proc === proc && !this.stopped) log.debug(`ffmpeg exited unexpectedly (${code ?? sig ?? "?"})`);
28835
+ });
28804
28836
  this.startHealthMonitor();
28837
+ const cropDesc = isFull(cfg.crop) ? "full desktop" : `zoomed ${cfg.crop.x.toFixed(2)},${cfg.crop.y.toFixed(2)} ${cfg.crop.w.toFixed(2)}x${cfg.crop.h.toFixed(2)}`;
28805
28838
  log.debug(
28806
- `video: H.264 capture started (${enc}, ${kbpsArg(cfg.kbps)}@${cfg.fps}fps, ${cfg.crop ? "zoomed" : "full desktop"}${ov ? " + overview" : ""})`
28839
+ `video: H.264 capture started (${enc}, ${kbpsArg(cfg.kbps)}@${cfg.fps}fps, ${cropDesc}${ov ? " + overview" : ""})`
28807
28840
  );
28808
28841
  return true;
28809
28842
  }
@@ -28817,8 +28850,12 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28817
28850
  if (this.stopped || this.restarting || !this.proc) return;
28818
28851
  const now = Date.now();
28819
28852
  if (this.everGotPacket) {
28820
- if (now - this.lastPacketAt > _ScreenCaptureSession.STALL_MS) {
28821
- log.debug("screen capture stalled \u2014 restarting");
28853
+ const stallMs = this.spawnGotPacket ? _ScreenCaptureSession.STALL_MS : Math.min(4e3 + this.framelessRestarts * 1500, 1e4);
28854
+ if (now - this.lastPacketAt > stallMs) {
28855
+ if (!this.spawnGotPacket) this.framelessRestarts += 1;
28856
+ log.debug(
28857
+ `screen capture stalled \u2014 restarting (${this.spawnGotPacket ? "went silent" : `spawn never produced a frame after ${Math.round(stallMs / 1e3)}s`})`
28858
+ );
28822
28859
  void this.restart();
28823
28860
  }
28824
28861
  return;
@@ -28838,13 +28875,58 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28838
28875
  this.restarting = true;
28839
28876
  try {
28840
28877
  do {
28841
- this.kill();
28878
+ await this.killAndWait();
28842
28879
  if (!await this.spawn()) break;
28880
+ await this.waitForFirstPacket(3e3);
28843
28881
  } while (!this.stopped && this.appliedCfg !== null && !sameConfig(this.cfg, this.appliedCfg));
28844
28882
  } finally {
28845
28883
  this.restarting = false;
28846
28884
  }
28847
28885
  }
28886
+ /**
28887
+ * Kill the current ffmpeg and WAIT until it has actually EXITED — plus a short macOS beat for
28888
+ * WindowServer to release its screen-capture session — before the caller spawns the next one.
28889
+ *
28890
+ * kill() alone is fire-and-forget: signal delivery and the OS-side capture teardown are both
28891
+ * asynchronous, so under re-crop churn (pan swipe after swipe) the next ffmpeg starts while the
28892
+ * old capture session is still registered — the documented avfoundation deadlock where BOTH
28893
+ * captures yield zero frames. Worse, the health monitor's recovery restart repeats the same
28894
+ * unserialized kill→spawn every 5s, re-triggering the overlap each time: the observed
28895
+ * "stalled — restarting" loop with no "crop live" ever following, dead for tens of seconds.
28896
+ * Serializing exit→settle→spawn removes the overlap entirely, at ~150ms per re-crop.
28897
+ */
28898
+ async killAndWait() {
28899
+ const proc = this.proc;
28900
+ const exited = proc && proc.exitCode === null && proc.signalCode === null ? new Promise((resolve) => {
28901
+ const cap = setTimeout(resolve, 2500);
28902
+ cap.unref?.();
28903
+ proc.once("exit", () => {
28904
+ clearTimeout(cap);
28905
+ resolve();
28906
+ });
28907
+ }) : null;
28908
+ this.kill();
28909
+ if (exited) {
28910
+ await exited;
28911
+ if (process.platform === "darwin") await new Promise((r) => setTimeout(r, 150));
28912
+ }
28913
+ }
28914
+ /** Bounded wait for the current spawn's first RTP packet (resolves immediately if it already
28915
+ * produced, on stop, or at the cap). See the restart loop for why replacing a capture that
28916
+ * hasn't produced yet is dangerous on macOS. */
28917
+ waitForFirstPacket(capMs) {
28918
+ if (this.spawnGotPacket || this.stopped) return Promise.resolve();
28919
+ return new Promise((resolve) => {
28920
+ const done = () => {
28921
+ clearTimeout(cap);
28922
+ if (this.firstPacketWaiter === done) this.firstPacketWaiter = null;
28923
+ resolve();
28924
+ };
28925
+ const cap = setTimeout(done, capMs);
28926
+ cap.unref?.();
28927
+ this.firstPacketWaiter = done;
28928
+ });
28929
+ }
28848
28930
  fail() {
28849
28931
  if (this.erroredOut) return;
28850
28932
  this.erroredOut = true;
@@ -28858,11 +28940,22 @@ var ScreenCaptureSession = class _ScreenCaptureSession {
28858
28940
  }
28859
28941
  const proc = this.proc;
28860
28942
  this.proc = null;
28861
- if (proc) {
28943
+ if (proc && proc.exitCode === null && proc.signalCode === null) {
28862
28944
  try {
28863
- proc.kill("SIGKILL");
28945
+ proc.kill("SIGTERM");
28864
28946
  } catch {
28865
28947
  }
28948
+ const hardKill = setTimeout(
28949
+ () => {
28950
+ try {
28951
+ proc.kill("SIGKILL");
28952
+ } catch {
28953
+ }
28954
+ },
28955
+ this.spawnGotPacket ? 1500 : 700
28956
+ );
28957
+ hardKill.unref?.();
28958
+ proc.once("exit", () => clearTimeout(hardKill));
28866
28959
  }
28867
28960
  if (this.sock) {
28868
28961
  closeQuietly(this.sock);
@@ -28989,7 +29082,15 @@ var VideoHub = class {
28989
29082
  if (this.opts.onCropActive) session.onActive((crop) => this.opts.onCropActive(crop));
28990
29083
  await session.start();
28991
29084
  if (this.paused || this.sinks.size === 0 && this.overviewSinks.size === 0) session.stop();
28992
- else this.session = session;
29085
+ else {
29086
+ this.session = session;
29087
+ void session.reconfigure({
29088
+ displayIndex: this.opts.displayIndex(),
29089
+ crop: this.crop,
29090
+ kbps: this.kbps,
29091
+ fps: this.fps
29092
+ });
29093
+ }
28993
29094
  })().finally(() => this.starting = null);
28994
29095
  return this.starting;
28995
29096
  }
@@ -29125,6 +29226,9 @@ async function answerWebRtcOffer(ctx, offerSdp, onClosed, opts = {}) {
29125
29226
  pc.connectionStateChange.subscribe((s) => {
29126
29227
  if (s === "failed" || s === "closed" || s === "disconnected") teardown();
29127
29228
  });
29229
+ pc.iceConnectionStateChange.subscribe((s) => {
29230
+ if (s === "failed" || s === "closed" || s === "disconnected") teardown();
29231
+ });
29128
29232
  await pc.setRemoteDescription({ type: "offer", sdp: offerSdp });
29129
29233
  if (offerVideoCount > 0 && canSendVideo && ctx.video) {
29130
29234
  for (let i = 0; i < offerVideoCount; i++) {
@@ -29345,8 +29449,8 @@ function printSetupReminder() {
29345
29449
 
29346
29450
  // src/server.ts
29347
29451
  var import_node_http = require("node:http");
29348
- var import_node_fs9 = require("node:fs");
29349
- var import_node_path11 = require("node:path");
29452
+ var import_node_fs10 = require("node:fs");
29453
+ var import_node_path12 = require("node:path");
29350
29454
  var import_node_url3 = require("node:url");
29351
29455
  var import_express = __toESM(require_express2(), 1);
29352
29456
 
@@ -30073,9 +30177,9 @@ var PinGuard = class _PinGuard {
30073
30177
  get salt() {
30074
30178
  return this.record?.salt ?? "";
30075
30179
  }
30076
- /** Persist a new PIN (>= 4 chars). Stores only the stretched key. */
30180
+ /** Persist a new PIN (>= 6 chars). Stores only the stretched key. */
30077
30181
  setPin(pin) {
30078
- if (pin.length < 4) throw new Error("PIN must be at least 4 characters");
30182
+ if (pin.length < 6) throw new Error("PIN must be at least 6 characters");
30079
30183
  const salt = (0, import_node_crypto5.randomBytes)(16).toString("hex");
30080
30184
  const key = stretch(pin, salt, ITERATIONS);
30081
30185
  this.record = { salt, iterations: ITERATIONS, key };
@@ -31036,6 +31140,30 @@ function saveAlwaysAgents(stateDir2, agents) {
31036
31140
  }
31037
31141
  }
31038
31142
 
31143
+ // src/timer-store.ts
31144
+ var import_node_fs9 = require("node:fs");
31145
+ var import_node_path11 = require("node:path");
31146
+ var FILE2 = "timers.json";
31147
+ function loadTimers(stateDir2) {
31148
+ try {
31149
+ const raw = (0, import_node_fs9.readFileSync)((0, import_node_path11.join)(stateDir2, FILE2), "utf8");
31150
+ const parsed = JSON.parse(raw);
31151
+ if (!Array.isArray(parsed.timers)) return [];
31152
+ return parsed.timers.filter(
31153
+ (t) => !!t && typeof t.id === "string" && typeof t.fireAtMs === "number"
31154
+ );
31155
+ } catch {
31156
+ return [];
31157
+ }
31158
+ }
31159
+ function saveTimers(stateDir2, timers) {
31160
+ try {
31161
+ (0, import_node_fs9.mkdirSync)(stateDir2, { recursive: true });
31162
+ (0, import_node_fs9.writeFileSync)((0, import_node_path11.join)(stateDir2, FILE2), JSON.stringify({ timers }), { mode: 384 });
31163
+ } catch {
31164
+ }
31165
+ }
31166
+
31039
31167
  // src/util/update-check.ts
31040
31168
  async function checkForUpdate(current) {
31041
31169
  try {
@@ -31078,8 +31206,8 @@ function announceUpdate(latest, current, notify) {
31078
31206
  }
31079
31207
 
31080
31208
  // src/server.ts
31081
- var here2 = (0, import_node_path11.dirname)((0, import_node_url3.fileURLToPath)(__whipdesk_meta_url));
31082
- var webDist = isPackaged() ? (0, import_node_path11.join)(here2, "mobile-web") : (0, import_node_path11.join)(here2, "..", "..", "mobile-web", "dist");
31209
+ var here2 = (0, import_node_path12.dirname)((0, import_node_url3.fileURLToPath)(__whipdesk_meta_url));
31210
+ var webDist = isPackaged() ? (0, import_node_path12.join)(here2, "mobile-web") : (0, import_node_path12.join)(here2, "..", "..", "mobile-web", "dist");
31083
31211
  async function startAgent() {
31084
31212
  const config = loadConfig();
31085
31213
  const capturer = new ScreenCapturer({ quality: config.quality, maxWidth: config.maxWidth });
@@ -31169,6 +31297,7 @@ async function startAgent() {
31169
31297
  let captureFailing = false;
31170
31298
  let viewport = { x: 0, y: 0, w: 1, h: 1 };
31171
31299
  const timers = /* @__PURE__ */ new Map();
31300
+ const persistTimers = () => saveTimers(config.stateDir, [...timers.values()]);
31172
31301
  const listTimers = () => [...timers.values()].map((t) => ({ id: t.id, label: t.label, fireAtMs: t.fireAtMs, hasAction: !!t.action }));
31173
31302
  const broadcastTimers = () => {
31174
31303
  const msg = { type: "timers", timers: listTimers() };
@@ -31190,13 +31319,38 @@ async function startAgent() {
31190
31319
  const t = timers.get(id);
31191
31320
  if (!t) return;
31192
31321
  timers.delete(id);
31322
+ persistTimers();
31193
31323
  broadcastTimers();
31194
31324
  const a = t.action;
31195
31325
  if (a) {
31196
31326
  try {
31197
31327
  log.info(`timer "${t.label || id}" firing${a.kind ? ` (${a.kind})` : ""}`);
31198
31328
  if (typeof a.x === "number" && typeof a.y === "number") {
31199
- await input.click(a.button ?? "left", false, a.x, a.y);
31329
+ const pinnedId = t.displayId;
31330
+ let restoreInput = null;
31331
+ if (pinnedId !== void 0 && pinnedId !== activeDisplay) {
31332
+ const pinned = displays.find((d) => d.id === pinnedId);
31333
+ if (!pinned || pinned.width <= 0 || pinned.height <= 0) {
31334
+ throw new Error(`the display it was scheduled on ([${pinnedId}]) is no longer connected`);
31335
+ }
31336
+ input.setActiveDisplay({
31337
+ originX: pinned.originX,
31338
+ originY: pinned.originY,
31339
+ width: pinned.width,
31340
+ height: pinned.height
31341
+ });
31342
+ restoreInput = () => {
31343
+ const cur = displays.find((d) => d.id === activeDisplay);
31344
+ input.setActiveDisplay(
31345
+ cur && cur.width > 0 && cur.height > 0 ? { originX: cur.originX, originY: cur.originY, width: cur.width, height: cur.height } : null
31346
+ );
31347
+ };
31348
+ }
31349
+ try {
31350
+ await input.click(a.button ?? "left", false, a.x, a.y);
31351
+ } finally {
31352
+ restoreInput?.();
31353
+ }
31200
31354
  if (a.kind === "key" || a.kind === "text") await delay(250);
31201
31355
  }
31202
31356
  if (a.kind === "key" && a.key) await input.keyTap(a.key);
@@ -31220,6 +31374,23 @@ async function startAgent() {
31220
31374
  source: "timer"
31221
31375
  });
31222
31376
  };
31377
+ for (const t of loadTimers(config.stateDir)) {
31378
+ if (t.fireAtMs <= Date.now()) {
31379
+ hub.emit({
31380
+ title: t.label || "WhipDesk timer",
31381
+ body: "This timer came due while WhipDesk was not running \u2014 its action was NOT performed.",
31382
+ level: "error",
31383
+ source: "timer"
31384
+ });
31385
+ } else {
31386
+ timers.set(t.id, t);
31387
+ }
31388
+ }
31389
+ persistTimers();
31390
+ if (timers.size > 0) {
31391
+ ensureTimerTicker();
31392
+ log.info(`restored ${timers.size} pending timer${timers.size === 1 ? "" : "s"} from disk`);
31393
+ }
31223
31394
  const captureOnce = async () => {
31224
31395
  if (regionWatcher.count === 0) return "idle";
31225
31396
  try {
@@ -31384,13 +31555,15 @@ async function startAgent() {
31384
31555
  addTimer(msg) {
31385
31556
  const fireInMs = Math.max(1e3, Math.min(msg.fireInMs, 7 * 24 * 36e5));
31386
31557
  const fireAtMs = Date.now() + fireInMs;
31387
- timers.set(msg.id, { id: msg.id, label: msg.label, fireAtMs, action: msg.action });
31558
+ timers.set(msg.id, { id: msg.id, label: msg.label, fireAtMs, action: msg.action, displayId: activeDisplay });
31559
+ persistTimers();
31388
31560
  ensureTimerTicker();
31389
31561
  broadcastTimers();
31390
31562
  log.info(`timer "${msg.label}" in ${Math.round(fireInMs / 1e3)}s${msg.action ? ` (+${msg.action.kind})` : ""}`);
31391
31563
  },
31392
31564
  removeTimer(id) {
31393
31565
  if (!timers.delete(id)) return;
31566
+ persistTimers();
31394
31567
  broadcastTimers();
31395
31568
  },
31396
31569
  listTimers() {
@@ -31474,9 +31647,9 @@ async function startAgent() {
31474
31647
  }
31475
31648
  res.json({ ok: true });
31476
31649
  });
31477
- if ((0, import_node_fs9.existsSync)(webDist)) {
31650
+ if ((0, import_node_fs10.existsSync)(webDist)) {
31478
31651
  app.use(import_express.default.static(webDist));
31479
- app.get("/*splat", (_req, res) => res.sendFile((0, import_node_path11.join)(webDist, "index.html")));
31652
+ app.get("/*splat", (_req, res) => res.sendFile((0, import_node_path12.join)(webDist, "index.html")));
31480
31653
  } else {
31481
31654
  log.warn(`mobile-web build not found at ${webDist} \u2014 run "npm run build:web"`);
31482
31655
  app.get(
@@ -0,0 +1 @@
1
+ :root{--bg:#f4f6f9;--panel:#fff;--panel-2:#eef1f5;--border:#d6dce4;--text:#1a2330;--muted:#5f6b7a;--accent:#2f6fed;--accent-2:#1aa66b;--warn:#c77700;--error:#d83b3b;--shadow:#141e3229;--screen-bg:#1b2027}@media (prefers-color-scheme:dark){:root{--bg:#0b0e14;--panel:#151a23;--panel-2:#1d2430;--border:#2a3342;--text:#e6edf3;--muted:#8b98a9;--accent:#4ea1ff;--accent-2:#36d399;--warn:#f5a623;--error:#ff5c5c;--shadow:#00000080;--screen-bg:#000}}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}html,body{background:var(--bg);height:100%;color:var(--text);overscroll-behavior:none;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,system-ui,sans-serif;overflow:hidden}#app{-webkit-user-select:none;user-select:none;position:fixed;inset:0}input,textarea{-webkit-user-select:text;user-select:text}.wd-screen{background:var(--screen-bg);touch-action:none;width:100%;height:100%;display:block;position:absolute;inset:0}.wd-statusbar{top:calc(env(safe-area-inset-top,0px) + 8px);background:var(--panel);border:1px solid var(--border);max-width:calc(100% - 64px);min-height:38px;box-shadow:0 2px 10px var(--shadow);z-index:5;cursor:pointer;border-radius:10px;align-items:center;gap:8px;padding:6px 10px;font-size:12px;transition:max-width .25s,padding .25s;display:flex;position:absolute;left:8px}.wd-statusbar.collapsed{max-width:124px;padding:6px 8px;overflow:hidden}.wd-statusbar.collapsed .wd-status-text,.wd-statusbar.collapsed .wd-watchers{display:none}.wd-dot{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.wd-dot[data-status=connected]{background:var(--accent-2)}.wd-dot[data-status=connecting]{background:var(--warn)}.wd-dot[data-status=disconnected]{background:var(--error)}.wd-status-text{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;font-weight:600;overflow:hidden}.wd-watchers{color:var(--warn);white-space:nowrap;flex:none;font-weight:700}.wd-transport{letter-spacing:.4px;color:#fff;background:var(--accent-2);text-transform:uppercase;border-radius:6px;flex:none;padding:2px 6px;font-size:11px;font-weight:800}.wd-transport.hidden{display:none}.wd-transport[data-kind=turn]{color:#111;background:#e0a800}.wd-transport[data-kind=stun],.wd-transport[data-kind=direct]{background:var(--accent-2)}.wd-transport[data-kind=lan]{background:#3b82f6}.wd-bell{top:calc(env(safe-area-inset-top,0px) + 8px);z-index:6;border:1px solid var(--border);background:var(--panel);min-width:38px;min-height:38px;color:var(--text);box-shadow:0 2px 10px var(--shadow);cursor:pointer;border-radius:10px;place-items:center;padding:6px;display:grid;position:absolute;right:8px}.wd-badge{background:var(--accent);color:#fff;text-align:center;min-width:18px;height:18px;box-shadow:0 1px 4px var(--shadow);border-radius:9px;padding:0 5px;font-size:11px;font-weight:700;line-height:18px;position:absolute;top:-6px;right:-6px}.wd-ribbon{z-index:6;padding-bottom:env(safe-area-inset-bottom,0px);background:var(--panel);border-top:1px solid var(--border);box-shadow:0 -4px 16px var(--shadow);position:absolute;bottom:0;left:0;right:0}.wd-panel{flex-direction:column;display:flex}.wd-options{border-bottom:1px solid var(--border);padding:6px}.wd-pane{flex-wrap:wrap;justify-content:center;align-items:center;gap:5px;display:flex}.wd-pane-col{flex-direction:column;align-items:stretch;gap:8px}.wd-pane-single-row{scrollbar-width:none;flex-wrap:nowrap;justify-content:center;align-items:stretch;gap:4px;overflow-x:auto}.wd-pane-single-row::-webkit-scrollbar{display:none}.wd-pane-single-row .wd-group{flex:0 auto;gap:2px;min-width:0;padding:3px 4px}.wd-pane-single-row .wd-group-items{flex-wrap:nowrap;gap:3px}.wd-pane-single-row .wd-btn{min-width:0}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:30px;padding:4px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:18px;height:18px}.wd-pane-single-row .wd-btn.wd-go{gap:4px;padding:6px 9px;font-size:12px}@media (min-width:390px){.wd-pane-single-row{gap:5px}.wd-pane-single-row .wd-group{padding:3px 6px}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:36px;padding:5px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:20px;height:20px}.wd-pane-single-row .wd-btn.wd-go{padding:7px 12px;font-size:13px}}@media (min-width:560px){.wd-pane-single-row .wd-btn.wd-icon-only{min-width:42px;padding:6px}.wd-pane-single-row .wd-btn.wd-go{padding:8px 16px}}@media (min-width:820px){.wd-ribbon{border:1px solid var(--border);width:min(880px,94vw);box-shadow:0 10px 34px var(--shadow);border-radius:16px;padding-bottom:0;bottom:16px;left:50%;right:auto;transform:translate(-50%)}.wd-options{padding:5px 6px}.wd-btn{min-height:32px;font-size:12px}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:32px;padding:4px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:17px;height:17px}.wd-pane-single-row .wd-btn.wd-go{padding:6px 12px;font-size:12px}.wd-group{padding:3px 6px}.wd-bell{min-width:32px;min-height:32px}.wd-statusbar{min-height:32px}}.wd-group{border:1px solid var(--border);background:var(--panel-2);border-radius:12px;flex-direction:column;align-items:stretch;gap:3px;padding:4px 6px;display:inline-flex}.wd-group-label{letter-spacing:.03em;text-transform:uppercase;color:var(--muted);white-space:nowrap;text-align:center;font-size:9px;font-weight:700}.wd-group-head{justify-content:space-between;align-items:center;gap:4px;display:flex}.wd-mode-toggle{border:1px solid var(--border);background:var(--panel);border-radius:999px;display:inline-flex;overflow:hidden}.wd-mode-btn{color:var(--muted);cursor:pointer;background:0 0;border:0;padding:4px 7px;font-size:10px;font-weight:700;line-height:1}.wd-mode-btn.on{background:var(--accent);color:#fff}.wd-group-items{flex-wrap:wrap;justify-content:center;align-items:center;gap:3px;display:flex}.wd-wrap{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-btn-label{line-height:1}.wd-row{flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;display:flex}.wd-tabs{align-items:center;gap:4px;padding:5px 6px;display:flex}.wd-tab{min-height:48px;color:var(--muted);cursor:pointer;touch-action:manipulation;background:0 0;border:1px solid #0000;border-radius:10px;flex-direction:column;flex:1;align-items:center;gap:2px;padding:6px 4px;font-size:11px;font-weight:600;display:flex}.wd-tab .wd-tab-label{line-height:1}.wd-tab.on{background:var(--panel-2);color:var(--accent);border-color:var(--border)}.wd-collapse{border:1px solid var(--border);background:var(--panel);min-width:44px;min-height:48px;color:var(--text);cursor:pointer;border-radius:10px;flex:none;place-items:center;display:grid}.wd-btn{border:1px solid var(--border);background:var(--panel);min-width:40px;min-height:40px;color:var(--text);cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:manipulation;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:7px;padding:6px 10px;font-size:13px;font-weight:600;line-height:1;display:inline-flex}.wd-btn svg{flex:none}.wd-btn.wd-icon-only{min-width:40px;padding:6px}.wd-btn:active{background:var(--panel-2);transform:translateY(1px)}.wd-btn.on{border-color:var(--accent);color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.wd-btn.wd-primary{background:var(--accent);border-color:var(--accent);color:#fff}.wd-btn.wd-go{background:var(--accent-2);border-color:var(--accent-2);color:#fff;padding:8px 14px}.wd-btn.wd-go:active{filter:brightness(.92);transform:translateY(1px)}.wd-seg{border:1px solid var(--border);border-radius:10px;display:inline-flex;overflow:hidden}.wd-seg-btn{background:var(--panel);min-height:44px;color:var(--muted);cursor:pointer;touch-action:manipulation;border:none;align-items:center;gap:6px;padding:8px 18px;font-size:15px;font-weight:600;display:inline-flex}.wd-seg-btn.on{background:var(--accent);color:#fff}.wd-zoom{text-align:center;font-variant-numeric:tabular-nums;min-width:52px;color:var(--text);font-weight:600}.wd-hint{text-align:center;color:var(--muted);flex-basis:100%;font-size:12px}.wd-type-input{resize:vertical;border:1px solid var(--border);background:var(--panel-2);width:100%;min-height:46px;color:var(--text);border-radius:10px;padding:10px;font-family:inherit;font-size:16px}.wd-keys-row{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-type-actions{gap:6px;display:flex}.wd-type-actions .wd-btn{flex:1}.wd-monitor-list{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-toasts{top:calc(env(safe-area-inset-top,0px) + 36px);z-index:9;pointer-events:none;flex-direction:column;gap:8px;display:flex;position:absolute;left:8px;right:8px}.wd-toast{background:var(--panel);border:1px solid var(--border);border-left-width:4px;border-radius:10px;flex-direction:column;gap:2px;padding:10px 12px;transition:opacity .3s,transform .3s;display:flex;box-shadow:0 6px 20px #0006}.wd-toast strong{font-size:14px}.wd-toast span{color:var(--muted);font-size:13px}.wd-toast.wd-success{border-left-color:var(--accent-2)}.wd-toast.wd-warning{border-left-color:var(--warn)}.wd-toast.wd-error{border-left-color:var(--error)}.wd-toast.wd-info{border-left-color:var(--accent)}.wd-toast.wd-hide{opacity:0;transform:translateY(-8px)}.hidden{display:none!important}.wd-sharpen{pointer-events:none;z-index:15;border:2.5px solid #ebf0f840;border-top-color:#ebf0f8e6;border-radius:50%;width:22px;height:22px;margin:-11px 0 0 -11px;animation:.9s linear infinite wd-sharpen-spin;position:fixed;top:50%;left:50%;box-shadow:0 0 0 4px #06090e59}@keyframes wd-sharpen-spin{to{transform:rotate(360deg)}}.wd-pin-overlay{z-index:20;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#06090eeb;place-items:center;padding:20px;display:grid;position:absolute;inset:0}.wd-pin-card{background:var(--panel);border:1px solid var(--border);text-align:center;border-radius:14px;width:100%;max-width:320px;padding:24px}.wd-pin-whip{object-fit:contain;width:64px;height:64px;margin:0 auto 12px;display:block}.wd-pin-card h2{margin:0 0 8px;font-size:20px}.wd-pin-msg{color:var(--muted);margin:0 0 16px;font-size:14px}.wd-pin-msg.err{color:var(--error)}.wd-pin-input{border:1px solid var(--border);background:var(--panel-2);width:100%;color:var(--text);text-align:center;letter-spacing:6px;border-radius:10px;margin-bottom:14px;padding:14px;font-size:22px}.wd-pin-submit{background:var(--accent);color:#fff;cursor:pointer;touch-action:manipulation;border:none;border-radius:12px;width:100%;min-height:50px;font-size:17px;font-weight:700}.wd-pin-submit:active{filter:brightness(.92);transform:translateY(1px)}.wd-pin-back{color:var(--muted);cursor:pointer;background:0 0;border:none;margin:14px auto 0;padding:6px 10px;font-size:13px;font-weight:600;display:block}.wd-pin-back:active{color:var(--text)}.wd-connecting{z-index:18;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#000000f5;place-items:center;padding:20px;display:grid;position:absolute;inset:0}.wd-connecting-card{text-align:center;width:100%;max-width:320px}.wd-connecting-anim{background:#000;border-radius:10px;width:180px;height:auto;margin:0 auto 16px;display:block}.wd-connecting-msg{color:#eef2f7;text-shadow:0 1px 2px #00000080;margin:0;font-size:15px;font-weight:600}.wd-dialog-overlay{z-index:18;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#06090e99;place-items:center;padding:16px;display:grid;position:absolute;inset:0}.wd-dialog{background:var(--panel);border:1px solid var(--border);border-radius:14px;width:100%;max-width:380px;padding:16px}.wd-dialog-head{justify-content:space-between;align-items:center;display:flex}.wd-dialog-head h2{margin:0;font-size:18px}.wd-dialog-x{border:1px solid var(--border);background:var(--panel-2);width:36px;height:36px;color:var(--text);cursor:pointer;border-radius:8px;place-items:center;display:grid}.wd-dialog-help{color:var(--muted);margin:8px 0 12px;font-size:13px}.wd-help-intro{margin:0 0 6px}.wd-help-list{flex-direction:column;gap:5px;margin:0 0 8px;padding-left:18px;display:flex}.wd-help-list strong{color:var(--text)}.wd-help-note{margin:0;font-style:italic}.wd-watch-list{flex-direction:column;gap:8px;margin-bottom:12px;display:flex}.wd-watch-row{border:1px solid var(--border);background:var(--panel-2);border-radius:10px;align-items:center;gap:8px;padding:8px 10px;display:flex}.wd-watch-name{text-align:left;color:var(--text);cursor:pointer;background:0 0;border:none;flex:1;padding:4px 0;font-family:inherit;font-size:14px;font-weight:600}.wd-watch-name:active{color:var(--accent)}.wd-timer-info{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.wd-timer-name{color:var(--text);align-items:center;gap:6px;font-size:14px;font-weight:600;display:flex}.wd-timer-name svg{color:var(--accent);flex:none}.wd-timer-remain{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.wd-timer-tag{letter-spacing:.5px;text-transform:uppercase;color:#fff;background:var(--accent);border-radius:6px;flex:none;padding:2px 6px;font-size:10px;font-weight:800}.wd-dialog-actions{gap:8px;margin-top:4px;display:flex}.wd-dialog-actions .wd-btn{flex:1;justify-content:center}.wd-dialog-actions.wd-actions-stack{flex-direction:column}.wd-actions-stack .wd-btn{width:100%}.wd-mon-state{letter-spacing:.4px;text-transform:uppercase;color:#fff;background:var(--muted);border-radius:6px;flex:none;padding:2px 6px;font-size:10px;font-weight:800}.wd-mon-state[data-state=working]{background:var(--accent-2)}.wd-mon-state[data-state=blocked]{background:var(--warn)}.wd-mon-state[data-state=finished]{background:var(--accent)}.wd-mon-state[data-state=crashed]{background:var(--error)}.wd-mon-pick-head{justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px;display:flex}.wd-mon-pick{flex-direction:column;gap:6px;max-height:40vh;margin-bottom:12px;display:flex;overflow-y:auto}.wd-mon-item{border:1px solid var(--border);background:var(--panel);width:100%;color:var(--text);cursor:pointer;text-align:left;border-radius:10px;justify-content:space-between;align-items:center;gap:10px;padding:9px 11px;display:flex}.wd-mon-item.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.wd-mon-item-main{align-items:center;gap:8px;min-width:0;display:flex}.wd-mon-item-main svg{color:var(--accent);flex:none}.wd-mon-item-title{white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;overflow:hidden}.wd-mon-always{flex-direction:column;gap:8px;display:flex}.wd-check{color:var(--text);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}.wd-check input{width:16px;height:16px;accent-color:var(--accent);flex:none;margin-top:1px}.wd-check-text{flex-direction:column;gap:2px;display:flex}.wd-check-sub{color:var(--muted);font-size:11px}.wd-form-row{flex-direction:column;gap:5px;margin-bottom:12px;display:flex}.wd-form-label{color:var(--muted);font-size:12px;font-weight:700}.wd-input{border:1px solid var(--border);background:var(--panel-2);width:100%;color:var(--text);border-radius:10px;padding:10px 12px;font-family:inherit;font-size:15px}.wd-input-area{resize:vertical;min-height:64px}.wd-form-duration{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.wd-input-num{text-align:center;width:56px}.wd-form-unit{color:var(--muted);font-size:14px;font-weight:600}.wd-preset-row{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}.wd-preset{border:1px solid var(--border);background:var(--panel-2);min-width:48px;color:var(--text);cursor:pointer;border-radius:999px;flex:auto;padding:9px 10px;font-family:inherit;font-size:13px;font-weight:700}.wd-preset:active{background:var(--accent);color:#fff;border-color:#0000}.wd-stepper{align-items:center;gap:4px;display:flex}.wd-step-btn{border:1px solid var(--border);background:var(--panel-2);width:38px;height:38px;color:var(--text);cursor:pointer;touch-action:manipulation;border-radius:10px;flex:none;font-family:inherit;font-size:22px;font-weight:700;line-height:1}.wd-step-btn:active{background:var(--accent);color:#fff;border-color:#0000}.wd-form-target{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.wd-form-target-status{color:var(--muted);font-size:13px}.wd-form-target-status.set{color:var(--accent-2);font-weight:700}.wd-pick-banner{top:calc(env(safe-area-inset-top,0px) + 56px);z-index:30;background:var(--accent);color:#fff;box-shadow:0 6px 20px var(--shadow);pointer-events:none;text-align:center;border-radius:999px;max-width:90%;padding:10px 16px;font-size:13px;font-weight:700;position:absolute;left:50%;transform:translate(-50%)}.wd-conn-row{border-bottom:1px solid var(--border);align-items:flex-start;gap:10px;padding:10px 0;display:flex}.wd-conn-label{width:92px;color:var(--muted);flex:none;padding-top:2px;font-size:13px;font-weight:700}.wd-conn-value{color:var(--text);word-break:break-word;flex:1;font-size:15px;font-weight:600}.wd-conn-route{flex-direction:column;flex:1;gap:4px;display:flex}.wd-conn-route .wd-transport{align-self:flex-start;position:static}.wd-conn-desc{color:var(--muted);font-size:12.5px}.wd-conn-status{flex:1;align-items:center;gap:8px;display:flex}.wd-conn-error{border:1px solid var(--error);color:var(--error);word-break:break-word;background:#dc35451f;border-radius:10px;margin-top:12px;padding:9px 11px;font-size:12.5px;font-weight:600}.wd-disconnect{background:var(--error);color:#fff;border-color:#0000;justify-content:center;width:100%;min-height:46px;margin-top:14px;font-size:15px;font-weight:700}.wd-support-link{border:1px solid var(--border);color:var(--muted);cursor:pointer;background:0 0;border-radius:999px;align-self:flex-start;align-items:center;gap:6px;margin-top:8px;padding:5px 12px;font-size:12.5px;font-weight:600;transition:color .15s,border-color .15s;display:inline-flex}.wd-support-link:hover,.wd-support-link:active{color:var(--accent);border-color:var(--accent)}.wd-support-link svg{flex:none;width:14px;height:14px}.wd-conn-feedback{border-top:1px solid var(--border);text-align:center;margin-top:14px;padding-top:12px}.wd-conn-feedback-text{color:var(--muted);margin:0;font-size:12.5px}.wd-conn-feedback-links{justify-content:center;gap:10px;margin-top:9px;display:flex}.wd-conn-feedback-link{border:1px solid var(--border);color:var(--text);background:0 0;border-radius:999px;align-items:center;gap:7px;padding:7px 16px;font-size:13px;font-weight:600;text-decoration:none;transition:color .15s,border-color .15s;display:inline-flex}.wd-conn-feedback-link:hover,.wd-conn-feedback-link:active{color:var(--accent);border-color:var(--accent)}.wd-conn-feedback-link svg{flex:none;width:16px;height:16px}.wd-pin-support{margin-top:16px}.wd-placing .wd-ribbon,.wd-placing .wd-statusbar,.wd-placing .wd-bell{display:none}.wd-place-layer{z-index:40;touch-action:none;background:0 0;position:absolute;inset:0}.wd-place-marker{z-index:41;pointer-events:none;border:2px solid #4ea1ff;border-radius:50%;width:46px;height:46px;position:absolute;transform:translate(-50%,-50%);box-shadow:0 0 0 2px #00000080,0 0 14px #4ea1ffb3}.wd-place-marker:before,.wd-place-marker:after{content:"";background:#4ea1ff;position:absolute}.wd-place-marker:before{width:2px;top:-8px;bottom:-8px;left:50%;transform:translate(-50%)}.wd-place-marker:after{height:2px;top:50%;left:-8px;right:-8px;transform:translateY(-50%)}.wd-place-bar{z-index:42;padding:12px 14px calc(env(safe-area-inset-bottom,0px) + 12px);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border-top:1px solid var(--border);background:#0a0e14f0;flex-direction:column;gap:10px;display:flex;position:absolute;bottom:0;left:0;right:0}.wd-place-hint{color:#eef2f7;text-align:center;margin:0;font-size:13px;font-weight:600}.wd-place-text{width:100%}.wd-place-buttons{gap:8px;display:flex}.wd-place-buttons .wd-btn{flex:1;justify-content:center}.wd-perm-row{border:1px solid var(--border);background:var(--panel-2);border-radius:10px;align-items:center;gap:8px;margin-bottom:12px;padding:8px 10px;display:flex}.wd-perm-dot{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.wd-perm-dot[data-state=on]{background:var(--accent-2)}.wd-perm-dot[data-state=warn]{background:var(--warn)}.wd-perm-dot[data-state=off]{background:var(--error)}.wd-perm-text{color:var(--muted);flex:1;font-size:12px}.wd-perm-enable{flex:none;min-width:auto;min-height:32px;padding:4px 12px;font-size:12px}.wd-dialog .wd-go{justify-content:center;width:100%}.wd-selector{z-index:19;border:2px solid var(--accent);touch-action:none;background:#2f6fed24;border-radius:6px;position:absolute;box-shadow:0 0 0 9999px #06090e59}.wd-selector-move{background:var(--accent);color:#fff;cursor:move;touch-action:none;border-radius:8px 0;place-items:center;width:38px;height:38px;display:grid;position:absolute;top:-1px;left:-1px}.wd-selector-handle{background:var(--accent);border:3px solid var(--panel);cursor:nwse-resize;touch-action:none;border-radius:50%;width:28px;height:28px;position:absolute;bottom:-12px;right:-12px}.wd-selector-bar{white-space:nowrap;gap:8px;display:flex;position:absolute;bottom:-56px;left:50%;transform:translate(-50%)}.wd-selector-info{top:calc(env(safe-area-inset-top,0px) + 52px);z-index:20;background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--accent);max-width:430px;box-shadow:0 6px 20px var(--shadow);color:var(--text);text-align:center;pointer-events:none;border-radius:10px;margin:0 auto;padding:8px 12px;font-size:12.5px;line-height:1.35;position:absolute;left:12px;right:12px}
@@ -0,0 +1,2 @@
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index.esm-mXKu2C3o.js","./index.esm-B3ZTr43m.js","./index.esm-CmSagqpw.js","./index.esm-wogdVWd2.js","./index.esm-BgjPHsdM.js","./index.esm-Di8jSGaG.js"])))=>i.map(i=>d[i]);
2
+ (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){return typeof e==`object`&&!!e&&typeof e.type==`string`}var t=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function n(e){return new TextEncoder().encode(e)}function r(e){let n=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),r=e.length*8,i=new Uint8Array((e.length+8>>6)+1<<6);i.set(e),i[e.length]=128;let a=new DataView(i.buffer);a.setUint32(i.length-4,r>>>0,!1),a.setUint32(i.length-8,Math.floor(r/4294967296),!1);let o=new Uint32Array(64);for(let e=0;e<i.length;e+=64){for(let t=0;t<16;t++)o[t]=a.getUint32(e+t*4,!1);for(let e=16;e<64;e++){let t=o[e-15],n=o[e-2],r=(t>>>7|t<<25)^(t>>>18|t<<14)^t>>>3,i=(n>>>17|n<<15)^(n>>>19|n<<13)^n>>>10;o[e]=o[e-16]+r+o[e-7]+i>>>0}let[r,i,s,c,l,u,d,f]=n;for(let e=0;e<64;e++){let n=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7),a=l&u^~l&d,p=f+n+a+t[e]+o[e]>>>0,m=((r>>>2|r<<30)^(r>>>13|r<<19)^(r>>>22|r<<10))+(r&i^r&s^i&s)>>>0;f=d,d=u,u=l,l=c+p>>>0,c=s,s=i,i=r,r=p+m>>>0}n[0]=n[0]+r>>>0,n[1]=n[1]+i>>>0,n[2]=n[2]+s>>>0,n[3]=n[3]+c>>>0,n[4]=n[4]+l>>>0,n[5]=n[5]+u>>>0,n[6]=n[6]+d>>>0,n[7]=n[7]+f>>>0}let s=new Uint8Array(32);new DataView(s.buffer).setUint32(0,n[0],!1);for(let e=0;e<8;e++)new DataView(s.buffer).setUint32(e*4,n[e],!1);return s}var i=`0123456789abcdef`;function a(e){let t=``;for(let n of e)t+=i[n>>4]+i[n&15];return t}function o(e){return a(r(n(e)))}function s(e,t,n){let r=o(`${t}:${e}`);for(let e=1;e<n;e++)r=o(r);return r}function c(e,t){return o(`${e}:${t}`)}var l=class{token;handlers={status:new Set,welcome:new Set,screenMeta:new Set,screenRegion:new Set,videoTrack:new Set,overviewTrack:new Set,transport:new Set,notification:new Set,presence:new Set,pinRequired:new Set,watchers:new Set,timers:new Set,monitors:new Set,monitorAlways:new Set,monitorSessions:new Set,netStats:new Set,versionMismatch:new Set,error:new Set};sender=()=>{};challenge=null;wrongPin=!1;rememberedPin=null;constructor(e){this.token=e}on(e,t){this.handlers[e].add(t)}emit(e,t){for(let n of this.handlers[e])n(t)}setSender(e){this.sender=e}send(e){try{this.sender(JSON.stringify(e))}catch{}}sendHello(){this.send({type:`hello`,protocol:1,token:this.token,role:`controller`,client:{userAgent:navigator.userAgent}})}submitPin(e){if(this.rememberedPin=e,!this.challenge)return;let{salt:t,iterations:n,nonce:r}=this.challenge,i=s(e,t,n);this.send({type:`auth`,response:c(i,r)})}setVisible(e){this.send({type:`visibility`,visible:e})}handleText(t){let n;try{n=JSON.parse(t)}catch{return}if(!e(n))return;let r=n;switch(r.type){case`welcome`:this.challenge=null,r.protocol!==1&&this.emit(`versionMismatch`,{agentProtocol:r.protocol,clientProtocol:1,agentVersion:r.agent?.version}),this.emit(`welcome`,r),this.emit(`timers`,r.timers??[]),this.emit(`monitors`,r.monitors??[]),this.emit(`monitorAlways`,r.alwaysAgents??[]);break;case`auth-required`:this.challenge={salt:r.salt,iterations:r.iterations,nonce:r.nonce},this.rememberedPin&&!this.wrongPin?this.submitPin(this.rememberedPin):this.emit(`pinRequired`,{attemptsLeft:r.attemptsLeft,retry:this.wrongPin}),this.wrongPin=!1;break;case`screen-meta`:this.emit(`screenMeta`,{screen:r.screen,activeDisplay:r.activeDisplay});break;case`screen-region`:this.emit(`screenRegion`,{x:r.x,y:r.y,w:r.w,h:r.h,active:r.active});break;case`notification`:this.emit(`notification`,r);break;case`presence`:this.emit(`presence`,r.watchers);break;case`watchers`:this.emit(`watchers`,r.regions);break;case`timers`:this.emit(`timers`,r.timers);break;case`monitors`:this.emit(`monitors`,r.monitors);break;case`monitor-always-agents`:this.emit(`monitorAlways`,r.agents);break;case`monitor-sessions`:this.emit(`monitorSessions`,r.sessions);break;case`error`:r.code===`pin`?(this.wrongPin=!0,this.rememberedPin=null):this.emit(`error`,r.message);break;case`pong`:break}}},u=class{url;core;ws=null;pc=null;dc=null;mainXcv=null;overviewXcv=null;reconnectTimer=0;statsTimer=0;closedByUser=!1;constructor(e,t){this.url=e,this.core=new l(t),this.core.setSender(e=>{if(this.dc&&this.dc.readyState===`open`)try{this.dc.send(e)}catch{}})}on(e,t){this.core.on(e,t)}send(e){this.core.send(e)}submitPin(e){this.core.submitPin(e)}setVisible(e){this.core.setVisible(e)}connect(){this.closedByUser=!1,this.open()}close(){this.closedByUser=!0,window.clearTimeout(this.reconnectTimer),this.teardown()}isHealthy(){return!this.closedByUser&&this.dc?.readyState===`open`&&this.pc?.connectionState===`connected`}wake(){this.closedByUser||this.isHealthy()||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.open())}teardown(){if(window.clearInterval(this.statsTimer),this.statsTimer=0,this.core.emit(`videoTrack`,null),this.core.emit(`overviewTrack`,null),this.dc){this.dc.onopen=this.dc.onclose=this.dc.onmessage=null;try{this.dc.close()}catch{}this.dc=null}if(this.pc){this.pc.onconnectionstatechange=null,this.pc.ontrack=null,this.pc.onicecandidate=null;try{this.pc.close()}catch{}this.pc=null}if(this.ws){this.ws.onopen=this.ws.onclose=this.ws.onmessage=this.ws.onerror=null;try{this.ws.close()}catch{}this.ws=null}this.mainXcv=null,this.overviewXcv=null}scheduleReconnect(){this.closedByUser||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=window.setTimeout(()=>this.open(),1500))}open(){this.core.emit(`status`,`connecting`),this.teardown();let e=new WebSocket(this.url);this.ws=e;let t=t=>{if(e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch{}},n=new RTCPeerConnection({iceServers:[]});this.pc=n;let r=n.createDataChannel(`whipdesk`);r.binaryType=`arraybuffer`,this.dc=r,r.onopen=()=>{this.core.sendHello(),this.core.emit(`status`,`connected`),this.core.emit(`transport`,`LAN`),this.startStatsPoll(n)},r.onclose=()=>this.core.emit(`status`,`disconnected`),r.onmessage=e=>{typeof e.data==`string`&&this.core.handleText(e.data)};try{this.mainXcv=n.addTransceiver(`video`,{direction:`recvonly`}),this.overviewXcv=n.addTransceiver(`video`,{direction:`recvonly`})}catch{}n.ontrack=e=>{let t=e.streams[0]??new MediaStream([e.track]);this.overviewXcv&&e.transceiver===this.overviewXcv?this.core.emit(`overviewTrack`,t):this.core.emit(`videoTrack`,t)},n.onicecandidate=e=>{e.candidate&&t({kind:`candidate`,candidate:e.candidate.toJSON()})},n.onconnectionstatechange=()=>{let e=n.connectionState;(e===`failed`||e===`disconnected`||e===`closed`)&&(this.core.emit(`status`,`disconnected`),this.scheduleReconnect())};let i=!1;e.onopen=async()=>{try{let e=await n.createOffer();await n.setLocalDescription(e),t({kind:`offer`,sdp:n.localDescription?.sdp??``})}catch{}},e.onmessage=e=>{let t;try{t=JSON.parse(typeof e.data==`string`?e.data:``)}catch{return}if(t.kind===`answer`&&t.sdp&&!i)i=!0,n.setRemoteDescription({type:`answer`,sdp:t.sdp});else if(t.kind===`candidate`&&t.candidate)try{n.addIceCandidate(t.candidate)}catch{}else t.kind===`error`&&this.core.emit(`error`,String(t.message??`host error`))},e.onclose=()=>{this.core.emit(`status`,`disconnected`),this.scheduleReconnect()},e.onerror=()=>{}}startStatsPoll(e){window.clearInterval(this.statsTimer),this.statsTimer=window.setInterval(async()=>{let t=null,n=0;try{(await e.getStats()).forEach(e=>{e.type===`inbound-rtp`&&e.kind===`video`?n=Math.max(n,Number(e.framesPerSecond??0)):e.type===`candidate-pair`&&e.nominated&&typeof e.currentRoundTripTime==`number`&&(t=e.currentRoundTripTime)})}catch{return}this.core.emit(`netStats`,{fps:Math.round(n),rtt:t==null?null:Math.round(t*1e3)})},1e3)}},d=``+new URL(`loading-whip-anim-DmlnYIJ-.gif`,import.meta.url).href,f=[`Rounding up your AI agents…`,`Connecting to your agent farm…`,`Uncoiling the whip…`,`Negotiating the fastest route…`,`Tightening the leash…`,`Almost in the saddle…`],p=class{overlay;msg;rotateTimer=0;msgIndex=0;constructor(e){this.overlay=document.createElement(`div`),this.overlay.className=`wd-connecting hidden`;let t=document.createElement(`div`);t.className=`wd-connecting-card`;let n=document.createElement(`img`);n.className=`wd-connecting-anim`,n.src=d,n.alt=`WhipDesk`,n.decoding=`async`,this.msg=document.createElement(`p`),this.msg.className=`wd-connecting-msg`,this.msg.textContent=f[0],t.append(n,this.msg),this.overlay.appendChild(t),e.appendChild(this.overlay)}show(e){this.msg.textContent=e??f[this.msgIndex],this.overlay.classList.contains(`hidden`)&&(this.overlay.classList.remove(`hidden`),!e&&(this.rotateTimer=window.setInterval(()=>{this.msgIndex=(this.msgIndex+1)%f.length,this.msg.textContent=f[this.msgIndex]},2200)))}hide(){this.overlay.classList.add(`hidden`),window.clearInterval(this.rotateTimer),this.rotateTimer=0}},m={eye:`<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>`,mouse:`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 7v4"/>`,keyboard:`<rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/>`,monitor:`<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/>`,hand:`<path d="M7 11V6a1.5 1.5 0 0 1 3 0v4m0-1V4.5a1.5 1.5 0 0 1 3 0V10m0-1.5a1.5 1.5 0 0 1 3 0V12c0 4-2.5 8-6.5 8S6 17 5 15l-1.5-3a1.4 1.4 0 0 1 2.3-1.5L7 12"/>`,bell:`<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z"/><path d="M10 19a2 2 0 0 0 4 0"/>`,plus:`<path d="M12 5v14M5 12h14"/>`,minus:`<path d="M5 12h14"/>`,"chevron-down":`<path d="m6 9 6 6 6-6"/>`,"chevron-up":`<path d="m6 15 6-6 6 6"/>`,pointer:`<path d="m4 4 6 16 2.5-6.5L19 11Z"/>`,"scroll-up":`<path d="m6 14 6-6 6 6"/><path d="M12 8v11"/>`,"scroll-down":`<path d="m6 10 6 6 6-6"/><path d="M12 16V5"/>`,"mouse-left":`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 3v8H6V8"/>`,"mouse-right":`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 3v8h6V8"/>`,"double-click":`<path d="m4 4 6 16 2.5-6.5L19 11Z"/><path d="M18 4v3M21 6h-3"/>`,drag:`<path d="M12 2v20M2 12h20" /><path d="m8 6 4-4 4 4M8 18l4 4 4-4M6 8l-4 4 4 4M18 8l4 4-4 4"/>`,send:`<path d="M22 2 11 13M22 2l-7 20-4-9-9-4Z"/>`,insert:`<path d="M12 5v14M5 12h7"/><rect x="16" y="4" width="4" height="16" rx="1"/>`,lock:`<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>`,clock:`<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>`,power:`<path d="M12 3v9"/><path d="M6.4 7.4a8 8 0 1 0 11.2 0"/>`,activity:`<path d="M3 12h4l2-7 4 14 2-7h6"/>`,x:`<path d="M6 6 18 18M18 6 6 18"/>`,heart:`<path d="M12 20s-7-4.4-9.3-8.5a4.5 4.5 0 0 1 8.1-3.9l1.2 1.6 1.2-1.6a4.5 4.5 0 0 1 8.1 3.9C19 15.6 12 20 12 20Z"/>`,trash:`<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/>`,github:`<path fill="currentColor" stroke="none" d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.25 2.88.12 3.18.77.84 1.23 1.91 1.23 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58A12 12 0 0 0 24 12.5C24 5.87 18.63.5 12 .5Z"/>`,reddit:`<path fill="currentColor" stroke="none" d="M24 11.78a2.6 2.6 0 0 0-4.4-1.86 12.74 12.74 0 0 0-6.86-2.16l1.17-3.68 3.16.74a1.83 1.83 0 1 0 .2-1.18l-3.6-.85a.6.6 0 0 0-.72.43l-1.3 4.08a12.8 12.8 0 0 0-7 2.18 2.6 2.6 0 1 0-2.86 4.28 5.1 5.1 0 0 0-.06.79c0 4 4.66 7.24 10.42 7.24S20.42 17.83 20.42 12.95c0-.26-.02-.52-.06-.78A2.6 2.6 0 0 0 24 11.78ZM6.13 13.5a1.83 1.83 0 1 1 3.66 0 1.83 1.83 0 0 1-3.66 0Zm10.2 4.83c-1.25 1.25-3.64 1.34-4.33 1.34-.7 0-3.09-.09-4.33-1.34a.47.47 0 0 1 .67-.67c.79.79 2.47.99 3.66.99 1.2 0 2.88-.2 3.67-.99a.47.47 0 1 1 .66.67h.03Zm-.27-3a1.83 1.83 0 1 1 0-3.66 1.83 1.83 0 0 1 0 3.66Z"/>`},h=`http://www.w3.org/2000/svg`;function g(e,t=20){let n=document.createElementNS(h,`svg`);return n.setAttribute(`viewBox`,`0 0 24 24`),n.setAttribute(`width`,String(t)),n.setAttribute(`height`,String(t)),n.setAttribute(`fill`,`none`),n.setAttribute(`stroke`,`currentColor`),n.setAttribute(`stroke-width`,`2`),n.setAttribute(`stroke-linecap`,`round`),n.setAttribute(`stroke-linejoin`,`round`),n.setAttribute(`aria-hidden`,`true`),n.innerHTML=m[e],n}var _=[`en`],v=`en`;function y(){try{let e=localStorage.getItem(`wd-locale`);if(e&&_.includes(e))return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=(t||``).toLowerCase().split(`-`)[0]??``;if(_.includes(e))return e}return v}function b(){return location.hostname.endsWith(`whipdesk.com`)?``:`https://whipdesk.com`}function x(){return`${b()}/${y()}/dashboard/`}function S(e){let t=e?`?next=${encodeURIComponent(e)}`:``;return`${b()}/${y()}/sign-in/${t}`}var C=`https://donate.stripe.com/6oU5kE19N5v35652n88so01`,w=`https://github.com/BinaryBananaLLC/WhipDesk/`,T=`https://www.reddit.com/r/WhipDesk/`,E=[[`Esc`,`Escape`],[`Tab`,`Tab`],[`⌫`,`Backspace`],[`⏎`,`Enter`],[`←`,`ArrowLeft`],[`↑`,`ArrowUp`],[`↓`,`ArrowDown`],[`→`,`ArrowRight`]];function D(e,t,n){let r=document.createElement(e);return t&&(r.className=t),n!==void 0&&(r.textContent=n),r}function O(e,...t){let n=D(`div`,`wd-group`);n.appendChild(D(`span`,`wd-group-label`,e));let r=D(`div`,`wd-group-items`);return r.append(...t),n.appendChild(r),n}function k(e,t){let n=0,r=0,i=()=>{window.clearTimeout(n),window.clearInterval(r)};e.addEventListener(`pointerdown`,e=>{e.preventDefault(),t(),n=window.setTimeout(()=>{r=window.setInterval(t,80)},350)});for(let t of[`pointerup`,`pointercancel`,`pointerleave`])e.addEventListener(t,i);return e}function A(e,t=`wd-btn`){return D(`button`,t,e)}function j(e,t=``,n=`wd-btn`){let r=D(`button`,n);if(r.appendChild(g(e)),t){let e=D(`span`,`wd-btn-label`,t);r.appendChild(e)}else r.classList.add(`wd-icon-only`),r.setAttribute(`aria-label`,e);return r}function ee(e,t,n){let r=D(`a`,`wd-conn-feedback-link`);return r.href=n,r.target=`_blank`,r.rel=`noopener noreferrer`,r.append(g(e,16),D(`span`,void 0,t)),r}function te(e){switch(e.toUpperCase()){case`LAN`:return`Direct on your local network — fastest, no relay.`;case`STUN`:return`Direct peer-to-peer across networks.`;case`TURN`:return`Your network blocked a direct P2P connection, so we're bouncing your stream through our encrypted relay. Yes, this costs us actual money to run. Consider supporting us.`;default:return``}}function M(e){return e.toUpperCase()===`TURN`?`$ ${e}`:e}var ne=class{root;deps;statusDot=D(`span`,`wd-dot`);statusText=D(`span`,`wd-status-text`,`Connecting…`);transportBadge=D(`span`,`wd-transport hidden`);watchersText=D(`span`,`wd-watchers`,``);alertBadge=D(`span`,`wd-badge hidden`);statusbar;statusCollapseTimer=0;connectionOverlay;connName;connRoute;connPresence;connSpeed;connStatusDot;connStatusText;connError;lastError=``;netFps=0;netRtt=null;panel;optionsArea;tabButtons=new Map;tabPanes=new Map;collapseBtn;interactHost;monitorList;promptInput;activeTab=null;interactMode=`mouse`;collapsed=!1;deviceName=``;transport=``;presenceCount=1;status=`connecting`;displays=[];activeDisplay=0;constructor(e,t){this.root=e,this.deps=t,this.build()}setStatus(e){this.status=e,this.statusDot.dataset.status=e,e===`connected`&&(this.lastError=``),this.renderStatusText(),this.updateStatusCollapse(),this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection()}setAlertCount(e){this.alertBadge.textContent=e>0?String(e):``,this.alertBadge.classList.toggle(`hidden`,e<=0)}setTransport(e){this.transport=e,this.transportBadge.textContent=M(e),this.transportBadge.dataset.kind=e.toLowerCase(),this.transportBadge.classList.toggle(`hidden`,!e),this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection(),this.peekStatus()}updateStatusCollapse(){window.clearTimeout(this.statusCollapseTimer),this.status===`connected`?this.scheduleStatusCollapse():this.statusbar?.classList.remove(`collapsed`)}scheduleStatusCollapse(){window.clearTimeout(this.statusCollapseTimer),this.statusCollapseTimer=window.setTimeout(()=>{this.status===`connected`&&this.statusbar?.classList.add(`collapsed`)},4e3)}peekStatus(){this.statusbar?.classList.remove(`collapsed`),this.scheduleStatusCollapse()}renderStatusText(){this.statusText.textContent=this.status===`connected`?this.deviceName?`Connected to ${this.deviceName}`:`Connected`:this.status===`connecting`?`Connecting…`:`Disconnected`}setWelcome(e){this.deviceName=e.agent.hostname,this.renderStatusText(),this.displays=e.displays??[],this.activeDisplay=e.activeDisplay??0,this.renderMonitors(),e.capabilities.mouse||this.deps.notifications.show({type:`notification`,id:`cap-${Date.now()}`,title:`View-only`,body:`Host mouse/keyboard unavailable — grant Accessibility on the host.`,level:`warning`,source:`client`,t:Date.now()})}setActiveDisplay(e){this.activeDisplay=e,this.renderMonitors()}setPresence(e){this.presenceCount=e,this.watchersText.textContent=e>1?`● ${e} watching`:``,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection()}buildConnectionDialog(){let e=D(`div`,`wd-dialog-overlay hidden`);e.addEventListener(`pointerdown`,t=>{t.target===e&&e.classList.add(`hidden`)});let t=D(`div`,`wd-dialog`),n=D(`div`,`wd-dialog-head`);n.append(D(`h2`,``,`Connection`));let r=D(`button`,`wd-dialog-x`);r.appendChild(g(`x`)),r.onclick=()=>e.classList.add(`hidden`),n.appendChild(r);let i=D(`div`,`wd-conn-row`);i.append(D(`span`,`wd-conn-label`,`Status`));let a=D(`div`,`wd-conn-status`);this.connStatusDot=D(`span`,`wd-dot`),this.connStatusText=D(`span`,`wd-conn-value`,`—`),a.append(this.connStatusDot,this.connStatusText),i.appendChild(a);let o=D(`div`,`wd-conn-row`);o.append(D(`span`,`wd-conn-label`,`Machine`)),this.connName=D(`span`,`wd-conn-value`,`—`),o.appendChild(this.connName);let s=D(`div`,`wd-conn-row`);s.append(D(`span`,`wd-conn-label`,`Connection`)),this.connRoute=D(`div`,`wd-conn-route`),s.appendChild(this.connRoute);let c=D(`div`,`wd-conn-row`);c.append(D(`span`,`wd-conn-label`,`Viewers`)),this.connPresence=D(`span`,`wd-conn-value`,`1`),c.appendChild(this.connPresence);let l=D(`div`,`wd-conn-row`);l.append(D(`span`,`wd-conn-label`,`Speed (FPS/latency)`)),this.connSpeed=D(`span`,`wd-conn-value`,`—`),l.appendChild(this.connSpeed),this.connError=D(`div`,`wd-conn-error hidden`);let u=D(`button`,`wd-btn wd-disconnect`);u.append(g(`power`),D(`span`,`wd-btn-label`,`Disconnect`)),u.onclick=()=>this.disconnect();let d=D(`div`,`wd-conn-feedback`);d.append(D(`p`,`wd-conn-feedback-text`,`Noticed an issue or have an idea? Reach out:`));let f=D(`div`,`wd-conn-feedback-links`);f.append(ee(`reddit`,`Reddit`,T),ee(`github`,`GitHub`,w)),d.appendChild(f),t.append(n,i,s,l,o,c,this.connError,u,d),e.appendChild(t),this.root.appendChild(e),this.connectionOverlay=e}renderConnection(){if(this.connStatusDot.dataset.status=this.status,this.connStatusText.textContent=this.status===`connected`?`Connected`:this.status===`connecting`?`Connecting…`:`Disconnected`,this.lastError?(this.connError.textContent=this.lastError,this.connError.classList.remove(`hidden`)):this.connError.classList.add(`hidden`),this.connName.textContent=this.deviceName||`Connected device`,this.connPresence.textContent=String(Math.max(1,this.presenceCount)),this.connRoute.replaceChildren(),this.transport){let e=D(`span`,`wd-transport`);if(e.textContent=M(this.transport),e.dataset.kind=this.transport.toLowerCase(),this.connRoute.append(e,D(`span`,`wd-conn-desc`,te(this.transport))),this.transport.toLowerCase()===`turn`){let e=D(`button`,`wd-support-link`);e.append(g(`heart`,14),D(`span`,void 0,`Support WhipDesk`)),e.onclick=()=>window.open(C,`_blank`,`noopener`),this.connRoute.append(e)}}else this.connRoute.append(D(`span`,`wd-conn-desc`,this.status===`connected`?`Detecting route…`:`Connecting…`));this.renderSpeed()}renderSpeed(){let e=`${this.netFps} FPS`;this.connSpeed.textContent=this.netRtt==null?e:`${e} / ${this.netRtt} ms`}setNetStats(e,t){this.netFps=e,this.netRtt=t,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderSpeed()}openConnection(){this.renderConnection(),this.connectionOverlay.classList.remove(`hidden`)}disconnect(){this.deps.conn.close(),window.location.href=x()}flashError(e){this.lastError=e,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection(),this.deps.notifications.show({type:`notification`,id:`err-${Date.now()}`,title:`WhipDesk`,body:e,level:`warning`,source:`client`,t:Date.now()})}build(){let e=D(`div`,`wd-statusbar`);e.append(this.statusDot,this.statusText,this.transportBadge,this.watchersText),e.onclick=()=>this.openConnection(),this.statusbar=e,this.buildConnectionDialog();let t=j(`bell`,``,`wd-bell`);t.setAttribute(`aria-label`,`Auto-Whips`),t.title=`Auto-Whips`,t.appendChild(this.alertBadge),t.onclick=()=>this.deps.watchers.open(),this.root.append(e,t);let n=D(`div`,`wd-ribbon`);this.panel=D(`div`,`wd-panel`),this.optionsArea=D(`div`,`wd-options`),this.tabPanes.set(`viewer`,this.buildViewerPane()),this.tabPanes.set(`interact`,this.buildInteractPane()),this.tabPanes.set(`type`,this.buildTypePane()),this.tabPanes.set(`monitor`,this.buildMonitorPane());for(let e of this.tabPanes.values())this.optionsArea.appendChild(e);let r=D(`div`,`wd-tabs`),i=(e,t,n)=>{let i=D(`button`,`wd-tab`);i.appendChild(g(t,18)),i.appendChild(D(`span`,`wd-tab-label`,n)),i.onclick=()=>this.selectTab(e),this.tabButtons.set(e,i),r.appendChild(i)};i(`viewer`,`eye`,`Viewer`),i(`interact`,`mouse`,`Interact`),i(`type`,`keyboard`,`Type`),i(`monitor`,`monitor`,`Monitor`),this.collapseBtn=j(`chevron-down`,``,`wd-collapse`),this.collapseBtn.onclick=()=>this.setCollapsed(!this.collapsed),r.appendChild(this.collapseBtn),this.panel.append(this.optionsArea,r),n.appendChild(this.panel),this.root.appendChild(n),this.selectTab(`viewer`)}buildViewerPane(){let{view:e,input:t}=this.deps,n=D(`div`,`wd-pane wd-pane-single-row`),r=k(j(`minus`,``,`wd-btn wd-icon-only`),()=>e.zoomBy(.9)),i=k(j(`plus`,``,`wd-btn wd-icon-only`),()=>e.zoomBy(1.11)),a=k(j(`scroll-up`,``,`wd-btn wd-icon-only`),()=>t.scrollStep(-6)),o=k(j(`scroll-down`,``,`wd-btn wd-icon-only`),()=>t.scrollStep(6)),s=j(`hand`,``,`wd-btn wd-icon-only`);s.setAttribute(`aria-label`,`Drag to scroll`),s.title=`Drag to scroll`;let c=j(`drag`,``,`wd-btn wd-icon-only`);c.setAttribute(`aria-label`,`Pan the zoomed screen with one finger`),c.title=`Pan the zoomed screen with one finger`,s.onclick=()=>{let e=!t.getDragScroll();t.setDragScroll(e),s.classList.toggle(`on`,e),c.classList.toggle(`on`,t.getPan())},c.onclick=()=>{let e=!t.getPan();t.setPan(e),c.classList.toggle(`on`,e),s.classList.toggle(`on`,t.getDragScroll())};let l=j(`pointer`,`Click`,`wd-btn wd-go`);return l.onclick=()=>t.click(`left`),n.append(O(`Zoom`,r,i),O(`Pan`,c),O(`Scroll`,a,o,s),O(`Pointer`,l)),n}buildInteractPane(){let e=D(`div`,`wd-pane`);return this.interactHost=D(`div`,`wd-pane`),e.appendChild(this.interactHost),this.renderInteract(),e}renderInteract(){let{input:e}=this.deps;this.interactHost.replaceChildren();let t=D(`div`,`wd-group`),n=D(`div`,`wd-group-head`),r=D(`span`,`wd-group-label`,`Mode`),i=D(`div`,`wd-mode-toggle`),a=D(`button`,`wd-mode-btn`,`Mouse`),o=D(`button`,`wd-mode-btn`,`Touch`);a.classList.toggle(`on`,this.interactMode===`mouse`),o.classList.toggle(`on`,this.interactMode===`touch`),a.onclick=()=>{this.interactMode=`mouse`,this.activeTab===`interact`&&this.deps.input.setInteraction(`mouse`),this.renderInteract()},o.onclick=()=>{this.interactMode=`touch`,this.activeTab===`interact`&&this.deps.input.setInteraction(`touch`),this.renderInteract()},i.append(a,o),n.append(r,i);let s=D(`div`,`wd-group-items`);if(this.interactMode===`mouse`){let t=j(`mouse-left`,`Left`);t.onclick=()=>e.click(`left`);let n=j(`mouse-right`,`Right`);n.onclick=()=>e.click(`right`);let r=j(`double-click`,`Double`);r.onclick=()=>e.multiClick(2);let i=j(`drag`,`Drag`);i.onclick=()=>{let t=!e.getDragLock();e.setDragLock(t),i.classList.toggle(`on`,t)},s.append(t,n,r,i)}else{let t=j(`pointer`,`Tap`);t.onclick=()=>e.click(`left`);let n=j(`hand`,`Hold`);n.onclick=()=>e.longPress();let r=k(j(`scroll-up`,`Up`,`wd-btn`),()=>e.swipe(0,-.25)),i=k(j(`scroll-down`,`Down`,`wd-btn`),()=>e.swipe(0,.25)),a=A(`←`);a.onclick=()=>e.swipe(-.25,0);let o=A(`→`);o.onclick=()=>e.swipe(.25,0);let c=A(`2 fingers`);c.onclick=()=>e.click(`right`),s.append(t,n,r,i,a,o,c)}t.append(n,s),this.interactHost.append(t)}buildTypePane(){let{conn:e,input:t}=this.deps,n=D(`div`,`wd-pane wd-pane-col`);this.promptInput=D(`textarea`,`wd-type-input`),this.promptInput.placeholder=`Type to send to the focused app (URL, command, message…)`,this.promptInput.rows=2;let r=D(`div`,`wd-wrap`);for(let[t,n]of E){let i=A(t);i.onclick=()=>e.send({type:`key`,key:n}),r.appendChild(i)}let i=j(`pointer`,`1x click`);i.onclick=()=>t.click(`left`);let a=j(`double-click`,`2x click`);a.onclick=()=>t.multiClick(2);let o=j(`double-click`,`3x click`);o.onclick=()=>t.multiClick(3);let s=j(`insert`,`Insert`);s.onclick=()=>this.sendText(!1);let c=j(`send`,`Send`,`wd-btn wd-go`);return c.onclick=()=>this.sendText(!0),r.append(i,a,o,s,c),n.append(this.promptInput,r),n}buildMonitorPane(){let e=D(`div`,`wd-pane`);return this.monitorList=D(`div`,`wd-monitor-list`),e.appendChild(this.monitorList),this.renderMonitors(),e}renderMonitors(){if(this.monitorList){if(this.monitorList.replaceChildren(),this.displays.length===0){this.monitorList.appendChild(D(`span`,`wd-hint`,`Single display`));return}this.displays.forEach((e,t)=>{let n=A(`${t+1}. ${e.name}${e.primary?` ★`:``}`);n.classList.toggle(`on`,e.id===this.activeDisplay),n.onclick=()=>{this.deps.conn.send({type:`select-display`,id:e.id}),this.activeDisplay=e.id,this.renderMonitors()},this.monitorList.appendChild(n)})}}selectTab(e){if(this.activeTab!==null&&e===this.activeTab){this.setCollapsed(!this.collapsed);return}this.activeTab=e,this.collapsed&&this.setCollapsed(!1);for(let[t,n]of this.tabButtons)n.classList.toggle(`on`,t===e);for(let[t,n]of this.tabPanes)n.classList.toggle(`hidden`,t!==e);this.deps.input.setInteraction(e===`interact`?this.interactMode:`viewer`),e===`type`&&window.setTimeout(()=>this.promptInput.focus(),50)}setCollapsed(e){this.collapsed=e,this.optionsArea.classList.toggle(`hidden`,e),this.collapseBtn.replaceChildren(g(e?`chevron-up`:`chevron-down`))}sendText(e){let t=this.promptInput.value;t&&(this.deps.conn.send({type:`type`,text:t,submit:e}),this.promptInput.value=``)}};function N(e){return e<0?0:e>1?1:e}var P=2.5,F=250,re=500,ie=8,ae=class{canvas;view;conn;cb;interaction=`viewer`;dragLock=!1;dragScroll=!1;pan=!1;holdingLeft=!1;cursor={nx:.5,ny:.5};pointers=new Map;longPressTimer=0;twoFinger=null;suppressTap=!1;constructor(e,t,n,r={}){this.canvas=e,this.view=t,this.conn=n,this.cb=r,e.style.touchAction=`none`,e.addEventListener(`pointerdown`,e=>this.onDown(e)),e.addEventListener(`pointermove`,e=>this.onMove(e)),e.addEventListener(`pointerup`,e=>this.onUp(e)),e.addEventListener(`pointercancel`,e=>this.onUp(e)),e.addEventListener(`contextmenu`,e=>e.preventDefault()),this.view.setCursor(this.cursor.nx,this.cursor.ny)}setInteraction(e){this.interaction=e,e===`viewer`&&(this.dragLock=!1)}getInteraction(){return this.interaction}setCallbacks(e){this.cb=e}setDragLock(e){this.dragLock=e}getDragLock(){return this.dragLock}setDragScroll(e){this.dragScroll=e,e&&(this.pan=!1)}getDragScroll(){return this.dragScroll}setPan(e){this.pan=e,e&&(this.dragScroll=!1)}getPan(){return this.pan}isPanning(){return this.pan&&this.interaction===`viewer`}click(e,t=!1){this.send({type:`pointer`,action:`click`,button:e,double:t,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(15)}multiClick(e){for(let t=0;t<e;t++)this.send({type:`pointer`,action:`click`,button:`left`,x:this.cursor.nx,y:this.cursor.ny});navigator.vibrate?.(15)}longPress(e=650){this.send({type:`pointer`,action:`down`,button:`left`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(25),window.setTimeout(()=>this.send({type:`pointer`,action:`up`,button:`left`}),e)}swipe(e,t){let n=N(this.cursor.nx),r=N(this.cursor.ny),i=N(this.cursor.nx+e),a=N(this.cursor.ny+t);this.send({type:`pointer`,action:`down`,button:`left`,x:n,y:r});for(let e=1;e<=6;e++){let t=e/6;window.setTimeout(()=>{this.send({type:`pointer`,action:`move`,x:n+(i-n)*t,y:r+(a-r)*t}),e===6&&(this.moveCursor(i,a),this.send({type:`pointer`,action:`up`,button:`left`}))},e*16)}navigator.vibrate?.(15)}scrollStep(e){this.send({type:`scroll`,dx:0,dy:e})}send(e){this.conn.send(e)}positionOf(e){let t=this.canvas.getBoundingClientRect();return{x:e.clientX-t.left,y:e.clientY-t.top}}moveCursor(e,t){this.cursor.nx=N(e),this.cursor.ny=N(t),this.view.setCursor(this.cursor.nx,this.cursor.ny),this.cb.onCursor?.(this.cursor.nx,this.cursor.ny)}onDown(e){this.canvas.setPointerCapture?.(e.pointerId);let t=this.positionOf(e);if(this.pointers.set(e.pointerId,{x:t.x,y:t.y,startX:t.x,startY:t.y,startT:performance.now(),moved:!1,consumed:!1}),this.pointers.size===2){window.clearTimeout(this.longPressTimer),this.beginTwoFinger();return}if(this.pointers.size===1&&(this.view.beginViewGesture(),window.clearTimeout(this.longPressTimer),this.interaction===`mouse`&&!this.dragScroll&&(this.longPressTimer=window.setTimeout(()=>this.onLongPress(),re)),!this.dragScroll&&!this.isPanning()&&(this.interaction===`mouse`||this.interaction===`viewer`))){let e=this.view.canvasToNorm(t.x,t.y);this.moveCursor(e.nx,e.ny)}}onLongPress(){let e=[...this.pointers.values()][0];!e||e.moved||e.consumed||(e.consumed=!0,this.send({type:`pointer`,action:`click`,button:`right`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(20))}beginTwoFinger(){let e=[...this.pointers.values()];if(e.length<2)return;let t=e[0],n=e[1];this.twoFinger={dist:Math.hypot(t.x-n.x,t.y-n.y),mx:(t.x+n.x)/2,my:(t.y+n.y)/2,start:performance.now(),moved:!1}}onMove(e){let t=this.pointers.get(e.pointerId);if(!t)return;let n=this.positionOf(e),r=t.x,i=t.y;if(t.x=n.x,t.y=n.y,Math.hypot(n.x-t.startX,n.y-t.startY)>ie&&(t.moved=!0),this.pointers.size>=2&&this.twoFinger){this.onTwoFingerMove();return}if(this.pointers.size!==1||t.consumed||!t.moved)return;window.clearTimeout(this.longPressTimer);let a=n.x-r,o=n.y-i;if(this.isPanning()){this.view.panByCanvasPixels(a,o);return}if(this.interaction===`touch`||this.dragScroll){this.send({type:`scroll`,dx:Math.round(-a/P),dy:Math.round(-o/P)});return}let s=this.view.canvasToNorm(n.x,n.y);this.moveCursor(s.nx,s.ny),this.interaction===`mouse`&&this.dragLock&&!this.holdingLeft&&(this.holdingLeft=!0,this.send({type:`pointer`,action:`down`,button:`left`,x:this.cursor.nx,y:this.cursor.ny})),this.send({type:`pointer`,action:`move`,x:this.cursor.nx,y:this.cursor.ny})}onTwoFingerMove(){let e=[...this.pointers.values()];if(e.length<2||!this.twoFinger)return;let t=e[0],n=e[1],r=Math.hypot(t.x-n.x,t.y-n.y),i=(t.x+n.x)/2,a=(t.y+n.y)/2,o=r-this.twoFinger.dist,s=i-this.twoFinger.mx,c=a-this.twoFinger.my;if(Math.abs(o)>6){this.twoFinger.moved=!0,this.view.zoomAround(1+o/200,i,a),this.twoFinger.dist=r;return}(Math.abs(c)>2||Math.abs(s)>2)&&(this.twoFinger.moved=!0,this.view.getZoom()>1?this.view.panByCanvasPixels(s,c):this.send({type:`scroll`,dx:Math.round(-s/P),dy:Math.round(-c/P)}),this.twoFinger.mx=i,this.twoFinger.my=a)}onUp(e){let t=this.pointers.size,n=this.pointers.get(e.pointerId);if(this.pointers.delete(e.pointerId),window.clearTimeout(this.longPressTimer),this.pointers.size===0&&this.view.endViewGesture(),t===2){let e=this.twoFinger;e&&!e.moved&&this.interaction===`mouse`&&performance.now()-e.start<F&&(this.send({type:`pointer`,action:`click`,button:`right`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(20)),this.twoFinger=null,this.suppressTap=!0;for(let e of this.pointers.values())e.consumed=!0;return}if(this.holdingLeft&&this.pointers.size===0&&(this.holdingLeft=!1,this.send({type:`pointer`,action:`up`,button:`left`})),this.pointers.size<2&&(this.twoFinger=null),!n||n.consumed)return;if(this.suppressTap){this.pointers.size===0&&(this.suppressTap=!1);return}let r=performance.now()-n.startT;if(!(!(!n.moved&&r<F&&this.pointers.size===0)||this.dragScroll)&&(this.interaction===`mouse`||this.interaction===`touch`)){let e=this.view.canvasToNorm(n.startX,n.startY);this.moveCursor(e.nx,e.ny),this.send({type:`pointer`,action:`click`,button:`left`,x:e.nx,y:e.ny}),navigator.vibrate?.(12)}}},oe=class{container;constructor(e){this.container=e}async requestPermission(){try{`Notification`in window&&Notification.permission==="default"&&await Notification.requestPermission()}catch{}}get permission(){return`Notification`in window?Notification.permission:`unsupported`}flash(e,t,n=`info`){this.toast({type:`notification`,id:`flash-${Date.now()}`,title:e,body:t,level:n,source:`client`,t:Date.now()})}show(e){this.toast(e);try{`Notification`in window&&Notification.permission===`granted`&&new Notification(e.title,{body:e.body,tag:e.id})}catch{}navigator.vibrate?.(e.level===`error`?[60,40,60]:40)}toast(e){let t=document.createElement(`div`);t.className=`wd-toast wd-${e.level}`;let n=document.createElement(`strong`);if(n.textContent=e.title,t.appendChild(n),e.body){let n=document.createElement(`span`);n.textContent=e.body,t.appendChild(n)}this.container.appendChild(t),window.setTimeout(()=>{t.classList.add(`wd-hide`),window.setTimeout(()=>t.remove(),300)},5e3)}},se=``+new URL(`whip-CTqIatiK.png`,import.meta.url).href,ce=class{overlay;input;message;onSubmit=null;constructor(e){this.overlay=document.createElement(`div`),this.overlay.className=`wd-pin-overlay hidden`;let t=document.createElement(`div`);t.className=`wd-pin-card`;let n=document.createElement(`img`);n.className=`wd-pin-whip`,n.src=se,n.alt=`WhipDesk`,n.decoding=`async`;let r=document.createElement(`h2`);r.textContent=`Enter device PIN`,this.message=document.createElement(`p`),this.message.className=`wd-pin-msg`,this.message.textContent=`You're connected. Enter the PIN to unlock this device and start whipping.`,this.input=document.createElement(`input`),this.input.className=`wd-pin-input`,this.input.type=`password`,this.input.inputMode=`numeric`,this.input.autocomplete=`one-time-code`,this.input.name=`wd-device-pin`,this.input.setAttribute(`data-1p-ignore`,`true`),this.input.setAttribute(`data-lpignore`,`true`),this.input.setAttribute(`aria-label`,`Device PIN`),this.input.addEventListener(`keydown`,e=>{e.key===`Enter`&&this.submit()});let i=document.createElement(`button`);i.className=`wd-pin-submit`,i.textContent=`Unlock`,i.onclick=()=>this.submit();let a=document.createElement(`button`);a.type=`button`,a.className=`wd-support-link wd-pin-support`,a.append(g(`heart`,14));let o=document.createElement(`span`);o.textContent=`Support WhipDesk`,a.append(o),a.onclick=()=>window.open(C,`_blank`,`noopener`);let s=document.createElement(`button`);s.className=`wd-pin-back`,s.textContent=`← Back to dashboard`,s.onclick=()=>{window.location.href=x()},t.append(n,r,this.message,this.input,i,a,s),this.overlay.appendChild(t),e.appendChild(this.overlay)}show(e,t){this.onSubmit=t,this.input.value=``,e.retry?(this.message.textContent=e.attemptsLeft>0?`Wrong PIN — ${e.attemptsLeft} attempt(s) left.`:`Wrong PIN. Try again.`,this.message.classList.add(`err`),navigator.vibrate?.([40,40,40])):(this.message.textContent=`You're connected. Enter the PIN to unlock this device and start whipping.`,this.message.classList.remove(`err`)),this.overlay.classList.remove(`hidden`),window.setTimeout(()=>this.input.focus(),50)}hide(){this.overlay.classList.add(`hidden`)}submit(){let e=this.input.value.trim();if(e.length<4){this.message.textContent=`PIN is at least 4 characters.`,this.message.classList.add(`err`);return}this.onSubmit?.(e),this.message.textContent=`Checking…`,this.message.classList.remove(`err`)}},le=`modulepreload`,ue=function(e,t){return new URL(e,t).href},de={},I=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,new URL(`../../../src/node/plugins/importAnalysisBuild.ts`,import.meta.url)).href}r=o(t.map(t=>{if(t=ue(t,n),t=s(t),t in de)return;de[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:le,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};async function fe(e,t){if(e.vapidKey&&!(!(`serviceWorker`in navigator)||!(`Notification`in window)))try{let{initializeApp:n,getApps:r}=await I(async()=>{let{initializeApp:e,getApps:t}=await import(`./index.esm-mXKu2C3o.js`);return{initializeApp:e,getApps:t}},__vite__mapDeps([0,1]),import.meta.url),{getAuth:i}=await I(async()=>{let{getAuth:e}=await import(`./index.esm-CmSagqpw.js`);return{getAuth:e}},__vite__mapDeps([2,1]),import.meta.url),{getFirestore:a,doc:o,setDoc:s,serverTimestamp:c}=await I(async()=>{let{getFirestore:e,doc:t,setDoc:n,serverTimestamp:r}=await import(`./index.esm-wogdVWd2.js`);return{getFirestore:e,doc:t,setDoc:n,serverTimestamp:r}},__vite__mapDeps([3,1]),import.meta.url),{getMessaging:l,getToken:u,onMessage:d,isSupported:f}=await I(async()=>{let{getMessaging:e,getToken:t,onMessage:n,isSupported:r}=await import(`./index.esm-BgjPHsdM.js`);return{getMessaging:e,getToken:t,onMessage:n,isSupported:r}},__vite__mapDeps([4,1]),import.meta.url);if(!await f().catch(()=>!1))return;let p=r()[0]??n(e),m=i(p).currentUser;if(!m||Notification.permission==="default"&&await Notification.requestPermission()!==`granted`||Notification.permission!==`granted`)return;let h=await navigator.serviceWorker.register(`./firebase-messaging-sw.js`),g=l(p),_=await u(g,{vapidKey:e.vapidKey,serviceWorkerRegistration:h});if(!_)return;await s(o(a(p),`users`,m.uid,`fcmTokens`,_),{token:_,ua:navigator.userAgent,updatedAt:c()},{merge:!0}),d(g,e=>{let n=e.notification;t.show({type:`notification`,id:`push-${Date.now()}`,title:n?.title??`WhipDesk`,body:n?.body,level:`info`,source:`push`,t:Date.now()})})}catch{}}var pe=[{urls:`stun:turn-us1.whipdesk.com:3478`}];async function me(e){try{let t=await e.getStats(),n=``,r=``;t.forEach(e=>{e.type===`transport`&&e.selectedCandidatePairId&&(n=e.selectedCandidatePairId)}),t.forEach(e=>{e.type===`candidate-pair`&&!r&&(e.id===n||e.nominated&&e.state===`succeeded`)&&(r=e.localCandidateId)});let i=``;if(t.forEach(e=>{e.id===r&&e.type===`local-candidate`&&(i=e.candidateType)}),i===`relay`)return`TURN`;if(i===`srflx`||i===`prflx`)return`STUN`;if(i===`host`)return`LAN`}catch{}return null}var he=class{deviceId;config;core;pc=null;dc=null;mainXcv=null;overviewXcv=null;cleanupSignal=null;deleteSessionDoc=null;pushCandidate=null;appliedAnswer=!1;appliedCandidates=new Set;closed=!1;reconnectTimer=0;statsTimer=0;constructor(e,t,n){this.deviceId=e,this.config=n,this.core=new l(t),this.core.setSender(e=>{if(this.dc&&this.dc.readyState===`open`)try{this.dc.send(e)}catch{}})}on(e,t){this.core.on(e,t)}send(e){this.core.send(e)}submitPin(e){this.core.submitPin(e)}setVisible(e){this.core.setVisible(e)}connect(){this.closed=!1,this.start()}close(){this.closed=!0,window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.teardown()}isHealthy(){return!this.closed&&this.pc?.connectionState===`connected`&&this.dc?.readyState===`open`}wake(){this.closed||this.isHealthy()||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.teardown(),this.start())}start(){this.open().catch(e=>{this.core.emit(`error`,`Remote connect failed: ${e.message}`),this.core.emit(`status`,`disconnected`),this.scheduleReconnect()})}teardown(){if(window.clearInterval(this.statsTimer),this.statsTimer=0,this.cleanupSignal?.(),this.cleanupSignal=null,this.deleteSessionDoc?.(),this.deleteSessionDoc=null,this.pushCandidate=null,this.appliedAnswer=!1,this.appliedCandidates.clear(),this.core.emit(`videoTrack`,null),this.core.emit(`overviewTrack`,null),this.dc){this.dc.onopen=this.dc.onclose=this.dc.onmessage=null;try{this.dc.close()}catch{}this.dc=null}if(this.pc){this.pc.onconnectionstatechange=null,this.pc.ontrack=null;try{this.pc.close()}catch{}this.pc=null}this.mainXcv=null,this.overviewXcv=null}scheduleReconnect(){this.closed||this.reconnectTimer||(this.reconnectTimer=window.setTimeout(()=>{this.reconnectTimer=0,!this.closed&&(this.teardown(),this.start())},2e3))}async open(){this.core.emit(`status`,`connecting`);let{initializeApp:e,getApps:t}=await I(async()=>{let{initializeApp:e,getApps:t}=await import(`./index.esm-mXKu2C3o.js`);return{initializeApp:e,getApps:t}},__vite__mapDeps([0,1]),import.meta.url),{getAuth:n}=await I(async()=>{let{getAuth:e}=await import(`./index.esm-CmSagqpw.js`);return{getAuth:e}},__vite__mapDeps([2,1]),import.meta.url),{getDatabase:r,ref:i,child:a,push:o,set:s,onValue:c,remove:l,onDisconnect:u}=await I(async()=>{let{getDatabase:e,ref:t,child:n,push:r,set:i,onValue:a,remove:o,onDisconnect:s}=await import(`./index.esm-Di8jSGaG.js`);return{getDatabase:e,ref:t,child:n,push:r,set:i,onValue:a,remove:o,onDisconnect:s}},__vite__mapDeps([5,1]),import.meta.url),d=t()[0]??e(this.config),f=n(d);if(await new Promise(e=>{if(f.currentUser)return e();let t=f.onAuthStateChanged(()=>{t(),e()})}),!f.currentUser){window.location.assign(S(`/app/${window.location.hash}`));return}let p=f.currentUser.uid,m=r(d),h=await this.fetchIceServers(f.currentUser),g=new RTCPeerConnection({iceServers:h,iceTransportPolicy:`all`});this.pc=g;let _=g.createDataChannel(`whipdesk`);_.binaryType=`arraybuffer`,this.dc=_,this.wireDataChannel(_);try{this.mainXcv=g.addTransceiver(`video`,{direction:`recvonly`}),this.overviewXcv=g.addTransceiver(`video`,{direction:`recvonly`})}catch{}g.ontrack=e=>{let t=e.streams[0]??new MediaStream([e.track]);this.overviewXcv&&e.transceiver===this.overviewXcv?this.core.emit(`overviewTrack`,t):this.core.emit(`videoTrack`,t)};let v=!1,y=async()=>{if(v)return;let e=await me(g);if(!e)return;v=!0,this.core.emit(`transport`,e);let t=e===`TURN`?`turn`:e===`STUN`?`stun`:`lan`;try{let{getFirestore:e,doc:n,setDoc:r,increment:i}=await I(async()=>{let{getFirestore:e,doc:t,setDoc:n,increment:r}=await import(`./index.esm-wogdVWd2.js`);return{getFirestore:e,doc:t,setDoc:n,increment:r}},__vite__mapDeps([3,1]),import.meta.url);r(n(e(d),`users`,p),{stats:{[t]:i(1)}},{merge:!0}).catch(()=>{})}catch{}};g.onconnectionstatechange=()=>{let e=g.connectionState;e===`connected`?(this.core.emit(`status`,`connected`),y(),this.startStatsPoll(g)):(e===`failed`||e===`disconnected`||e===`closed`)&&(this.core.emit(`status`,`disconnected`),this.scheduleReconnect())};let b=o(i(m,`signaling/${p}/${this.deviceId}`));this.pushCandidate=e=>{try{s(o(a(b,`offerCandidates`)),e)}catch{}},g.onicecandidate=e=>{e.candidate&&this.pushCandidate?.(e.candidate.toJSON())};let x=await g.createOffer();await g.setLocalDescription(x),await s(b,{offer:{sdp:g.localDescription?.sdp??``},controllerUid:p,createdAtMs:Date.now()}),u(b).remove(),this.deleteSessionDoc=()=>void l(b).catch(()=>{}),this.cleanupSignal=c(b,e=>{let t=e.val();if(!(!t||this.closed)&&(t.answer?.sdp&&!this.appliedAnswer&&(this.appliedAnswer=!0,g.setRemoteDescription({type:`answer`,sdp:t.answer.sdp})),t.answerCandidates&&this.appliedAnswer)){for(let[e,n]of Object.entries(t.answerCandidates))if(!this.appliedCandidates.has(e)){this.appliedCandidates.add(e);try{g.addIceCandidate(n)}catch{}}}})}startStatsPoll(e){window.clearInterval(this.statsTimer),this.lossBase=null,this.statsPolls=0,this.statsTimer=window.setInterval(()=>void this.pollStats(e),1e3)}lossBase=null;statsPolls=0;async pollStats(e){let t=null,n=0,r=0,i=0;try{(await e.getStats()).forEach(e=>{e.type===`inbound-rtp`&&e.kind===`video`?(n=Math.max(n,Number(e.framesPerSecond??0)),r+=Number(e.packetsLost??0),i+=Number(e.packetsReceived??0)):e.type===`candidate-pair`&&e.nominated&&typeof e.currentRoundTripTime==`number`&&(t=e.currentRoundTripTime)})}catch{return}if(this.core.emit(`netStats`,{fps:Math.round(n),rtt:t==null?null:Math.round(t*1e3)}),this.statsPolls+=1,!this.lossBase)this.lossBase={lost:r,received:i};else if(this.statsPolls%5==0){let e=Math.max(0,r-this.lossBase.lost),n=Math.max(0,i-this.lossBase.received);this.lossBase={lost:r,received:i};let a=e+n;if(a>0){let n=e/a*100;this.core.send({type:`video-stats`,lossPct:n,rttMs:t==null?void 0:Math.round(t*1e3)})}}}wireDataChannel(e){e.onopen=()=>this.core.sendHello(),e.onclose=()=>{this.core.emit(`status`,`disconnected`),this.scheduleReconnect()},e.onmessage=e=>{typeof e.data==`string`&&this.core.handleText(e.data)}}async fetchIceServers(e){let t=[...pe];try{let e=sessionStorage.getItem(`wd-ice`);if(e){let{servers:t,exp:n}=JSON.parse(e);if(Array.isArray(t)&&t.length&&n>Date.now())return t}}catch{}try{let t=this.config.iceUrl||`https://us-central1-${this.config.projectId}.cloudfunctions.net/iceServers`,n=await e.getIdToken(),r=await fetch(t,{headers:{authorization:`Bearer ${n}`}});if(r.ok){let e=await r.json();if(Array.isArray(e.iceServers)&&e.iceServers.length){try{sessionStorage.setItem(`wd-ice`,JSON.stringify({servers:e.iceServers,exp:Date.now()+8*6e4}))}catch{}return e.iceServers}}}catch{}return t}};function L(e,t,n){return e<t?t:e>n?n:e}var ge=480,_e=600,R=-.6,z=1.6,ve=1500,ye=120,be=3e3;function B(e){return!e||e.x<=.001&&e.y<=.001&&e.w>=.999&&e.h>=.999}function V(e,t){if(B(e)&&B(t))return!0;if(!e||!t)return!1;let n=.005;return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n&&Math.abs(e.w-t.w)<n&&Math.abs(e.h-t.h)<n}var xe=class{canvas;ctx;mainEl=null;region=null;shownRegion=null;regionBridge=0;regionActiveHold=0;hasActiveEcho=!1;screen={width:0,height:0};viewMode=`fit`;zoom=1;center={nx:.5,ny:.5};cursor=null;onZoomCb;onViewCb;onGestureCb;gestureActive=!1;viewDirty=!1;videoActive=!1;videoRaf=0;cssW=1;cssH=1;dpr=1;layout={S:1,tx:0,ty:0};rafPending=!1;overlayBox=null;overlayView=null;overlayCanvas=null;overlayCtx=null;lastOverlayKey=``;overlayCanvasKey=``;minimapActiveAt=0;minimapShown=!0;overview=null;overviewCtx=null;overviewReady=!1;overviewDirty=!1;lastSnapAt=0;overviewEl=null;constructor(e){this.canvas=e;let t=e.getContext(`2d`);if(!t)throw Error(`2D canvas context unavailable`);this.ctx=t,this.createOverlay(),this.startOverlayLoop(),this.resize(),new ResizeObserver(()=>this.resize()).observe(e),window.addEventListener(`orientationchange`,()=>window.setTimeout(()=>this.resize(),200))}createOverlay(){let e=document.createElement(`div`);e.style.cssText=`position:fixed;top:calc(env(safe-area-inset-top, 0px) + 56px);right:10px;box-sizing:border-box;border:1.5px solid rgba(255,255,255,0.8);border-radius:6px;background:rgba(0,0,0,0.55);z-index:2147483600;pointer-events:none;display:none;box-shadow:0 1px 8px rgba(0,0,0,0.6);`;let t=document.createElement(`canvas`);t.style.cssText=`position:absolute;inset:0;width:100%;height:100%;border-radius:5px;display:block;`;let n=document.createElement(`div`);n.style.cssText=`position:absolute;box-sizing:border-box;border:2px solid #4ea1ff;background:rgba(78,161,255,0.18);border-radius:2px;`,e.appendChild(t),e.appendChild(n),document.body.appendChild(e),this.overlayBox=e,this.overlayView=n,this.overlayCanvas=t,this.overlayCtx=t.getContext(`2d`)}startOverlayLoop(){let e=()=>{this.renderOverlay(),requestAnimationFrame(e)};requestAnimationFrame(e)}renderOverlay(){let e=this.overlayBox,t=this.overlayView;if(!e||!t)return;let n=this.getVisibleRegion(),r=this.region,i=this.dispW(),a=this.dispH();if(!(r||n.w<.985||n.h<.985)){e.style.display!==`none`&&(e.style.display=`none`),this.lastOverlayKey=``,this.overlayCanvasKey=``;return}e.style.display===`none`&&(e.style.display=`block`,this.minimapActiveAt=performance.now(),this.minimapShown=!1);let o=Math.round(Math.min(132,Math.max(76,this.cssW*.32))),s=Math.round(o*(a/i||.625)),c=`${o}x${s}`;(c!==this.overlayCanvasKey||this.overviewDirty)&&(this.overlayCanvasKey=c,this.overviewDirty=!1,e.style.width=`${o}px`,e.style.height=`${s}px`,this.paintMinimap(o,s));let l=n,u=performance.now(),d=`${l.x.toFixed(3)},${l.y.toFixed(3)},${l.w.toFixed(3)},${l.h.toFixed(3)}`;d!==this.lastOverlayKey&&(this.lastOverlayKey=d,this.minimapActiveAt=u,t.style.left=`${(L(l.x,0,1)*100).toFixed(2)}%`,t.style.top=`${(L(l.y,0,1)*100).toFixed(2)}%`,t.style.width=`${(L(Math.max(.04,l.w),0,1)*100).toFixed(2)}%`,t.style.height=`${(L(Math.max(.04,l.h),0,1)*100).toFixed(2)}%`);let f=u-this.minimapActiveAt<be;f!==this.minimapShown&&(this.minimapShown=f,e.style.transition=`opacity ${f?.18:.5}s ease`,e.style.opacity=f?`1`:`0`)}paintMinimap(e,t){let n=this.overlayCanvas,r=this.overlayCtx;if(!n||!r)return;let i=Math.max(1,Math.round(e*this.dpr)),a=Math.max(1,Math.round(t*this.dpr));n.width!==i&&(n.width=i),n.height!==a&&(n.height=a),r.clearRect(0,0,i,a);let o=this.overviewImg();o&&(r.imageSmoothingEnabled=!0,r.imageSmoothingQuality=`low`,r.drawImage(o,0,0,i,a))}overviewImg(){return this.overviewReady?this.overview:null}maybeSnapshot(e,t,n){let r=performance.now();if(r-this.lastSnapAt<_e)return;this.lastSnapAt=r,this.overview||(this.overview=document.createElement(`canvas`),this.overviewCtx=this.overview.getContext(`2d`));let i=this.overview,a=this.overviewCtx;if(!i||!a)return;let o=ge,s=Math.max(1,Math.round(o*n/t));i.width!==o&&(i.width=o),i.height!==s&&(i.height=s),a.imageSmoothingEnabled=!0,a.imageSmoothingQuality=`medium`,a.drawImage(e,0,0,t,n,0,0,o,s),this.overviewReady=!0,this.overviewDirty=!0}getCanvas(){return this.canvas}resize(){let e=this.canvas.getBoundingClientRect();this.cssW=Math.max(1,e.width),this.cssH=Math.max(1,e.height),this.dpr=Math.min(window.devicePixelRatio||1,2),this.canvas.width=Math.round(this.cssW*this.dpr),this.canvas.height=Math.round(this.cssH*this.dpr),this.requestDraw(),this.emitView()}setScreen(e){let t=e.width!==this.screen.width||e.height!==this.screen.height;this.screen=e,t&&(this.overviewReady=!1,this.lastSnapAt=0,this.overviewDirty=!0),this.requestDraw()}getScreen(){return this.screen}dispW(){return this.screen.width||this.mainEl?.videoWidth||1}dispH(){return this.screen.height||this.mainEl?.videoHeight||1}setFrameRegion(e){let t=B(e)?null:e;this.region=t,window.clearTimeout(this.regionActiveHold),window.clearTimeout(this.regionBridge),this.hasActiveEcho||(this.regionBridge=window.setTimeout(()=>{this.shownRegion=t,this.videoActive||this.requestDraw()},ve)),this.videoActive||this.requestDraw()}setFrameRegionActive(e){this.hasActiveEcho=!0;let t=B(e)?null:e;if(!V(t,this.region)||V(t,this.shownRegion))return;window.clearTimeout(this.regionActiveHold);let n=()=>{V(t,this.region)&&(this.shownRegion=t,window.clearTimeout(this.regionBridge),this.videoActive||this.requestDraw())};this.regionActiveHold=window.setTimeout(()=>{if(!V(t,this.region))return;let e=this.mainEl;e&&typeof e.requestVideoFrameCallback==`function`?e.requestVideoFrameCallback(n):n()},ye)}setVideoSource(e){this.mainEl=e,this.videoActive=!!e,e?this.startVideoLoop():this.stopVideoLoop()}isVideoActive(){return this.videoActive}setOverviewSource(e){this.overviewEl=e}startVideoLoop(){if(this.videoRaf)return;let e=()=>{if(!this.videoActive){this.videoRaf=0;return}this.draw(),this.videoRaf=requestAnimationFrame(e)};this.videoRaf=requestAnimationFrame(e)}stopVideoLoop(){this.videoRaf&&cancelAnimationFrame(this.videoRaf),this.videoRaf=0,this.requestDraw()}setViewMode(e){this.viewMode=e,e===`fit`?(this.zoom=1,this.center={nx:.5,ny:.5}):this.zoom===1&&(this.zoom=2),this.emitView(),this.requestDraw()}getViewMode(){return this.viewMode}toggleViewMode(){return this.setViewMode(this.viewMode===`fit`?`magnify`:`fit`),this.viewMode}setZoom(e){this.zoom=L(e,1,8),this.viewMode=this.zoom===1?`fit`:`magnify`,this.zoom===1&&(this.center={nx:.5,ny:.5}),this.onZoomCb?.(this.zoom),this.emitView(),this.requestDraw()}zoomBy(e){this.setZoom(this.zoom*e)}getZoom(){return this.zoom}setOnZoom(e){this.onZoomCb=e}setOnView(e){this.onViewCb=e}setOnGesture(e){this.onGestureCb=e}beginViewGesture(){this.gestureActive=!0,this.onGestureCb?.(!0)}endViewGesture(){this.gestureActive&&(this.gestureActive=!1,this.viewDirty&&(this.viewDirty=!1,this.onViewCb?.(this.getVisibleRegion())),this.onGestureCb?.(!1))}emitView(){if(this.gestureActive){this.viewDirty=!0;return}this.onViewCb?.(this.getVisibleRegion())}zoomAround(e,t,n){let r=this.canvasToNorm(t,n),i=L(this.zoom*e,1,8);if(i===1){this.setZoom(1);return}this.zoom=i,this.viewMode=`magnify`;let a=this.dispW(),o=this.dispH(),s=Math.min(this.cssW/a,this.cssH/o)*this.zoom;this.center.nx=L(r.nx-(t-this.cssW/2)/(a*s||1),R,z),this.center.ny=L(r.ny-(n-this.cssH/2)/(o*s||1),R,z),this.onZoomCb?.(this.zoom),this.emitView(),this.requestDraw()}panByNorm(e,t){this.center.nx=L(this.center.nx+e,R,z),this.center.ny=L(this.center.ny+t,R,z),this.emitView(),this.requestDraw()}panByCanvasPixels(e,t){let{S:n}=this.computeLayout(),r=this.dispW(),i=this.dispH();!r||!i||!n||(this.center.nx=L(this.center.nx-e/(r*n),R,z),this.center.ny=L(this.center.ny-t/(i*n),R,z),this.emitView(),this.requestDraw())}setCursor(e,t=0){this.cursor=e===null?null:{nx:e,ny:t},this.videoActive||this.requestDraw()}computeLayout(){let{cssW:e,cssH:t}=this,n=this.dispW(),r=this.dispH();if(!n||!r)return this.layout={S:1,tx:0,ty:0},this.layout;let i=Math.min(e/n,t/r)*this.zoom,a,o;return this.viewMode===`fit`||this.zoom===1?(a=(e-n*i)/2,o=(t-r*i)/2):(a=e/2-this.center.nx*n*i,o=t/2-this.center.ny*r*i),this.layout={S:i,tx:a,ty:o},this.layout}canvasToNorm(e,t){let{S:n,tx:r,ty:i}=this.computeLayout();return{nx:L((e-r)/(this.dispW()*n||1),0,1),ny:L((t-i)/(this.dispH()*n||1),0,1)}}normToCanvas(e,t){let{S:n,tx:r,ty:i}=this.computeLayout();return{cx:r+e*this.dispW()*n,cy:i+t*this.dispH()*n}}getVisibleRegion(){let{S:e,tx:t,ty:n}=this.computeLayout(),r=this.dispW()*e,i=this.dispH()*e;if(!r||!i)return{x:0,y:0,w:1,h:1};let a=L(-t/r,0,1),o=L(-n/i,0,1),s=L((this.cssW-t)/r,0,1),c=L((this.cssH-n)/i,0,1);return{x:a,y:o,w:Math.max(.05,s-a),h:Math.max(.05,c-o)}}requestDraw(){this.videoActive||this.rafPending||(this.rafPending=!0,requestAnimationFrame(()=>{this.rafPending=!1,this.draw()}))}draw(){let e=this.ctx;e.setTransform(this.dpr,0,0,this.dpr,0,0),e.clearRect(0,0,this.cssW,this.cssH);let t=this.mainEl&&this.mainEl.videoWidth>0?this.mainEl:null;if(!t)return;let{S:n,tx:r,ty:i}=this.computeLayout(),a=this.dispW(),o=this.dispH(),s=a*n,c=o*n,l=t.videoWidth,u=t.videoHeight,d=l/u,f=e=>e?e.w*a/(e.h*o):a/o;if(this.region!==this.shownRegion){let e=Math.abs(d/f(this.region)-1)<.06,t=Math.abs(d/f(this.shownRegion)-1)<.06;e&&!t&&(this.shownRegion=this.region,window.clearTimeout(this.regionBridge))}let p=this.shownRegion,m=!!p&&Math.abs(d/f(p)-1)<.08,h=m?r+p.x*s:r,g=m?i+p.y*c:i,_=m?p.w*s:s,v=m?p.h*c:c,y=this.overviewImg();y&&(this.region||this.shownRegion)&&(e.imageSmoothingEnabled=!0,e.imageSmoothingQuality=`low`,e.drawImage(y,r,i,s,c));let b=Math.min(_/l,v/u),x=l*b,S=u*b;if(e.imageSmoothingEnabled=!0,e.imageSmoothingQuality=`high`,e.drawImage(t,0,0,l,u,h+(_-x)/2,g+(v-S)/2,x,S),this.cursor){let{cx:e,cy:t}=this.normToCanvas(this.cursor.nx,this.cursor.ny);this.drawCursor(e,t)}let C=this.overviewEl;if(this.region&&C&&C.videoWidth>0)this.maybeSnapshot(C,C.videoWidth,C.videoHeight);else if(!this.region&&!this.shownRegion&&o>0){let e=a/o;e>0&&Math.abs(d/e-1)<.06&&this.maybeSnapshot(t,l,u)}}drawCursor(e,t){let n=this.ctx;n.save(),n.beginPath(),n.arc(e,t,9,0,Math.PI*2),n.strokeStyle=`rgba(78,161,255,0.95)`,n.lineWidth=2,n.stroke(),n.beginPath(),n.arc(e,t,2.5,0,Math.PI*2),n.fillStyle=`rgba(78,161,255,0.95)`,n.fill(),n.restore()}};function Se(e,t,n,r){let i=document.createElement(`div`);i.className=`wd-place-layer`;let a=document.createElement(`div`);a.className=`wd-place-marker`;let o=document.createElement(`div`);o.className=`wd-place-bar`;let s=document.createElement(`p`);s.className=`wd-place-hint`,s.textContent=n.hint,o.appendChild(s);let c=null;n.withText&&(c=document.createElement(`textarea`),c.className=`wd-input wd-input-area wd-place-text`,c.placeholder=`Type the prompt to send…`,o.appendChild(c));let l=document.createElement(`div`);l.className=`wd-place-buttons`;let u=document.createElement(`button`);u.className=`wd-btn`,u.textContent=`Cancel`;let d=document.createElement(`button`);d.className=`wd-btn wd-go`,d.textContent=n.confirmLabel,l.append(u,d),o.appendChild(l),t.append(i,a,o),t.classList.add(`wd-placing`);let f={nx:.5,ny:.5},p=0,m=()=>{let{cx:t,cy:n}=e.normToCanvas(f.nx,f.ny);a.style.left=`${t}px`,a.style.top=`${n}px`,p=requestAnimationFrame(m)};p=requestAnimationFrame(m);let h=new Map,g=`idle`,_=null,v={x:0,y:0},y=!1,b=e=>{let t=i.getBoundingClientRect();return{x:e.clientX-t.left,y:e.clientY-t.top}},x=t=>{let{cx:n,cy:r}=e.normToCanvas(f.nx,f.ny);return Math.hypot(t.x-n,t.y-r)<38};i.addEventListener(`pointerdown`,t=>{i.setPointerCapture?.(t.pointerId);let n=b(t);if(h.set(t.pointerId,n),h.size===2){let e=[...h.values()],t=e[0],n=e[1];_={dist:Math.hypot(t.x-n.x,t.y-n.y),mx:(t.x+n.x)/2,my:(t.y+n.y)/2},g=`idle`}else h.size===1&&(v=n,y=!1,g=x(n)?`marker`:`pan`,g===`marker`&&(f=e.canvasToNorm(n.x,n.y)))}),i.addEventListener(`pointermove`,t=>{let n=h.get(t.pointerId);if(!n)return;let r=b(t);if(h.set(t.pointerId,r),h.size>=2&&_){let t=[...h.values()],n=t[0],r=t[1],i=Math.hypot(n.x-r.x,n.y-r.y),a=(n.x+r.x)/2,o=(n.y+r.y)/2;Math.abs(i-_.dist)>4&&(e.zoomAround(1+(i-_.dist)/200,a,o),_.dist=i),e.panByCanvasPixels(a-_.mx,o-_.my),_.mx=a,_.my=o;return}Math.hypot(r.x-v.x,r.y-v.y)>6&&(y=!0);let i=r.x-n.x,a=r.y-n.y;g===`marker`?f=e.canvasToNorm(r.x,r.y):g===`pan`&&e.panByCanvasPixels(i,a)});let S=t=>{let n=h.get(t.pointerId);h.delete(t.pointerId),n&&g===`pan`&&!y&&(f=e.canvasToNorm(n.x,n.y)),h.size<2&&(_=null),h.size===0&&(g=`idle`)};i.addEventListener(`pointerup`,S),i.addEventListener(`pointercancel`,S);let C=e=>{cancelAnimationFrame(p),t.classList.remove(`wd-placing`),i.remove(),a.remove(),o.remove(),r(e)};u.onclick=()=>C(null),d.onclick=()=>{let e=c?.value.trim()??``;if(n.withText&&!e){c?.focus();return}C({nx:f.nx,ny:f.ny,text:n.withText?e:void 0})}}var Ce=0;function H(){return`w${Date.now().toString(36)}${(Ce++).toString(36)}`}function we(){return new Date().toLocaleString(void 0,{month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`})}function Te(e){let t=Math.max(0,Math.round((e-Date.now())/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${String(i).padStart(2,`0`)}s`:`${i}s`}function U(e,t,n){let r=document.createElement(e);return t&&(r.className=t),n!==void 0&&(r.textContent=n),r}var Ee={claude:`Claude Code`,codex:`Codex CLI`,gemini:`Gemini CLI`,aider:`Aider`,copilot:`Copilot CLI`,opencode:`opencode`,cursor:`Cursor Agent`,amp:`Amp`,unknown:`AI agent`};function W(e){return Ee[e]??`AI agent`}function De(e){switch(e){case`working`:return`working`;case`blocked`:return`needs you`;case`idle`:return`idle`;case`finished`:return`finished`;case`crashed`:return`crashed`;default:return`…`}}var Oe=class{root;conn;view;notifications;requestNotifications;regions=[];timers=[];monitors=[];monitorSessions=[];alwaysAgents=new Set;renderPicker=null;renderAlways=null;countdownTimer=0;overlay;list;permissionRow;selector=null;constructor(e,t,n,r,i=()=>{}){this.root=e,this.conn=t,this.view=n,this.notifications=r,this.requestNotifications=i,this.overlay=U(`div`,`wd-dialog-overlay hidden`),this.overlay.addEventListener(`pointerdown`,e=>{e.target===this.overlay&&this.close()});let a=U(`div`,`wd-dialog`),o=U(`div`,`wd-dialog-head`);o.append(U(`h2`,``,`Auto-Whips`));let s=U(`button`,`wd-dialog-x`);s.appendChild(g(`x`)),s.onclick=()=>this.close(),o.appendChild(s);let c=U(`div`,`wd-dialog-help`),l=U(`p`,`wd-help-intro`,`Auto-Whips are a set of features that can whip and monitor agents for you automatically:`),u=U(`ul`,`wd-help-list`),d=U(`li`);d.append(U(`strong`,void 0,`Alerts`),document.createTextNode(` — watch part of the screen and ping you when it changes (e.g. your agent finishes).`));let f=U(`li`);f.append(U(`strong`,void 0,`Timers`),document.createTextNode(` — ping you after a set time, and can auto-click or send a prompt when they fire.`));let p=U(`li`);p.append(U(`strong`,void 0,`Session Monitoring`),document.createTextNode(` — pick a running AI session and get pinged the moment the agent stops working (it's waiting on you or has gone idle).`)),u.append(d,f,p);let m=U(`p`,`wd-help-note`,`Enable browser notifications to be reminded even when the browser is closed.`);c.append(l,u,m),this.permissionRow=U(`div`,`wd-perm-row`),this.list=U(`div`,`wd-watch-list`);let h=U(`button`,`wd-btn wd-go`);h.append(g(`plus`),U(`span`,`wd-btn-label`,`Add alert`)),h.onclick=()=>this.beginSelection();let _=U(`button`,`wd-btn wd-go`);_.append(g(`clock`),U(`span`,`wd-btn-label`,`Add timer`)),_.onclick=()=>this.beginTimer();let v=U(`button`,`wd-btn wd-go`);v.append(g(`activity`),U(`span`,`wd-btn-label`,`Add Session Monitoring`)),v.onclick=()=>this.beginMonitor();let y=U(`div`,`wd-dialog-actions wd-actions-stack`);y.append(h,_,v),a.append(o,c,this.permissionRow,this.list,y),this.overlay.appendChild(a),this.root.appendChild(this.overlay),this.renderPermission(),this.conn.on(`monitorSessions`,e=>{this.monitorSessions=e,this.renderPicker?.()})}setRegions(e){this.regions=e,this.renderList()}setTimers(e){this.timers=e,this.renderList()}setMonitors(e){this.monitors=e,this.renderList()}setAlwaysAgents(e){this.alwaysAgents=new Set(e),this.renderAlways?.()}open(){this.renderPermission(),this.renderList(),this.overlay.classList.remove(`hidden`),window.clearInterval(this.countdownTimer),this.countdownTimer=window.setInterval(()=>{this.timers.length&&this.renderList()},1e3)}close(){this.overlay.classList.add(`hidden`),window.clearInterval(this.countdownTimer),this.countdownTimer=0}renderPermission(){this.permissionRow.replaceChildren();let e=U(`span`,`wd-perm-dot`),t=U(`span`,`wd-perm-text`),n=this.notifications.permission;if(n===`granted`)e.dataset.state=`on`,t.textContent=`Browser notifications are on.`,this.permissionRow.append(e,t);else if(n===`denied`)e.dataset.state=`off`,t.textContent=`Notifications are blocked — enable them in your browser settings.`,this.permissionRow.append(e,t);else if(n===`unsupported`)e.dataset.state=`off`,t.textContent=`This browser can't show notifications. Keep this page open for in-app alerts.`,this.permissionRow.append(e,t);else{e.dataset.state=`warn`,t.textContent=`Allow notifications to be alerted while this page is in the background.`;let n=U(`button`,`wd-btn wd-perm-enable`);n.append(U(`span`,`wd-btn-label`,`Enable`)),n.onclick=async()=>{await this.requestNotifications(),this.renderPermission()},this.permissionRow.append(e,t,n)}}renderList(){if(this.list.replaceChildren(),this.regions.length===0&&this.timers.length===0&&this.monitors.length===0){this.list.appendChild(U(`p`,`wd-dialog-help`,`No alerts, timers, or session monitors yet.`));return}for(let e of this.regions){let t=U(`div`,`wd-watch-row`),n=U(`button`,`wd-watch-name`,e.label);n.title=`Edit this alert`,n.onclick=()=>this.beginSelection(e),t.appendChild(n);let r=U(`button`,`wd-btn wd-icon-only`);r.appendChild(g(`trash`)),r.setAttribute(`aria-label`,`Remove ${e.label}`),r.onclick=()=>{this.conn.send({type:`watch-remove`,id:e.id}),this.regions=this.regions.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(r),this.list.appendChild(t)}for(let e of this.timers){let t=U(`div`,`wd-watch-row`),n=U(`div`,`wd-timer-info`),r=U(`span`,`wd-timer-name`);r.append(g(`clock`,15),document.createTextNode(e.label));let i=U(`span`,`wd-timer-remain`,Te(e.fireAtMs));n.append(r,i),t.appendChild(n),e.hasAction&&t.appendChild(U(`span`,`wd-timer-tag`,`auto`));let a=U(`button`,`wd-btn wd-icon-only`);a.appendChild(g(`trash`)),a.setAttribute(`aria-label`,`Cancel ${e.label}`),a.onclick=()=>{this.conn.send({type:`timer-remove`,id:e.id}),this.timers=this.timers.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(a),this.list.appendChild(t)}for(let e of this.monitors){let t=U(`div`,`wd-watch-row`),n=U(`div`,`wd-timer-info`),r=U(`span`,`wd-timer-name`);r.append(g(`activity`,15),document.createTextNode(`${W(e.agent)} · ${e.label}`));let i=U(`span`,`wd-mon-state`);i.dataset.state=e.live?e.state:`finished`,i.textContent=e.live?De(e.state):`ended`,n.append(r,i),t.appendChild(n);let a=U(`button`,`wd-btn wd-icon-only`);a.appendChild(g(`trash`)),a.setAttribute(`aria-label`,`Stop monitoring ${e.label}`),a.onclick=()=>{this.conn.send({type:`monitor-remove`,id:e.id}),this.monitors=this.monitors.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(a),this.list.appendChild(t)}}beginSelection(e){this.close(),this.selector&&this.selector.remove();let t=this.root.getBoundingClientRect(),n=e?this.boxFromRegion(e,t):{x:t.width*.3,y:t.height*.3,w:t.width*.4,h:t.height*.25},r=U(`div`,`wd-selector-info`,e?`Move or resize the area to watch, then Save — you'll be alerted when its pixels change.`:`You'll get a browser notification when the pixels inside this box change. The agent watches this part of the screen.`),i=U(`div`,`wd-selector`),a=U(`div`,`wd-selector-move`);a.appendChild(g(`drag`));let o=U(`div`,`wd-selector-handle`),s=U(`div`,`wd-selector-bar`),c=U(`button`,`wd-btn wd-go`);c.append(U(`span`,`wd-btn-label`,e?`Save`:`Create`));let l=U(`button`,`wd-btn`);l.append(U(`span`,`wd-btn-label`,`Cancel`)),s.append(l,c),i.append(a,o,s),this.root.append(r,i),this.selector=i;let u=()=>{i.remove(),r.remove(),this.selector=null,this.open()},d=()=>{i.style.left=`${n.x}px`,i.style.top=`${n.y}px`,i.style.width=`${n.w}px`,i.style.height=`${n.h}px`};d();let f=(e,t)=>{e.addEventListener(`pointerdown`,n=>{n.preventDefault(),n.stopPropagation(),e.setPointerCapture(n.pointerId);let r=n.clientX,i=n.clientY,a=e=>{t(e.clientX-r,e.clientY-i),r=e.clientX,i=e.clientY,d()},o=()=>{e.removeEventListener(`pointermove`,a),e.removeEventListener(`pointerup`,o)};e.addEventListener(`pointermove`,a),e.addEventListener(`pointerup`,o)})};f(a,(e,r)=>{n.x=Math.max(0,Math.min(t.width-n.w,n.x+e)),n.y=Math.max(0,Math.min(t.height-n.h,n.y+r))}),f(o,(e,r)=>{n.w=Math.max(40,Math.min(t.width-n.x,n.w+e)),n.h=Math.max(40,Math.min(t.height-n.y,n.h+r))}),l.onclick=u,c.onclick=()=>{let t=this.toScreenRegion(n,e);t&&(this.conn.send({type:`watch-add`,region:t}),this.regions=e?this.regions.map(e=>e.id===t.id?t:e):[...this.regions,t],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(e?`Alert updated`:`Alert created`,e?`"${t.label}" updated.`:`Monitoring "${t.label}". We'll notify you when it changes.`,`success`)),u()}}boxFromRegion(e,t){let n=this.view.normToCanvas(e.x,e.y),r=this.view.normToCanvas(e.x+e.w,e.y+e.h),i=Math.max(40,r.cx-n.cx),a=Math.max(40,r.cy-n.cy);return{x:Math.max(0,Math.min(t.width-i,n.cx)),y:Math.max(0,Math.min(t.height-a,n.cy)),w:i,h:a}}toScreenRegion(e,t){let n=this.view.canvasToNorm(e.x,e.y),r=this.view.canvasToNorm(e.x+e.w,e.y+e.h),i=Math.min(n.nx,r.nx),a=Math.min(n.ny,r.ny),o=Math.abs(r.nx-n.nx),s=Math.abs(r.ny-n.ny);return o<.005||s<.005?null:{id:t?.id??H(),label:t?.label??we(),x:i,y:a,w:o,h:s}}beginTimer(){this.close();let e=U(`div`,`wd-dialog-overlay`),t=()=>{e.remove(),this.open()};e.addEventListener(`pointerdown`,n=>{n.target===e&&t()});let n=U(`div`,`wd-dialog`),r=U(`div`,`wd-dialog-head`);r.append(U(`h2`,``,`Set timer`));let i=U(`button`,`wd-dialog-x`);i.appendChild(g(`x`)),i.onclick=t,r.appendChild(i);let a=U(`p`,`wd-dialog-help`,`Waiting for a session limit to reset? Get notified when the time's up. You can also schedule an action for the moment it hits zero — click Retry, or enter a new prompt.`),o=U(`div`,`wd-form-row`);o.appendChild(U(`label`,`wd-form-label`,`Remind me in`));let s=U(`input`,`wd-input wd-input-num`);s.type=`number`,s.min=`0`,s.max=`168`,s.value=`0`,s.inputMode=`numeric`;let c=U(`input`,`wd-input wd-input-num`);c.type=`number`,c.min=`0`,c.max=`59`,c.value=`30`,c.inputMode=`numeric`;let l=U(`div`,`wd-preset-row`);for(let[e,t,n]of[[`15m`,0,15],[`30m`,0,30],[`1h`,1,0],[`2h`,2,0],[`5h`,5,0]]){let r=U(`button`,`wd-preset`,e);r.type=`button`,r.onclick=()=>{s.value=String(t),c.value=String(n)},l.appendChild(r)}let u=(e,t,n,r)=>{let i=U(`div`,`wd-stepper`),a=U(`button`,`wd-step-btn`,`−`);a.type=`button`;let o=U(`button`,`wd-step-btn`,`+`);o.type=`button`;let s=e=>Math.max(0,Math.min(n,e));return a.onclick=()=>e.value=String(s((Number(e.value)||0)-t)),o.onclick=()=>e.value=String(s((Number(e.value)||0)+t)),i.append(a,e,U(`span`,`wd-form-unit`,r),o),i},d=U(`div`,`wd-form-duration`);d.append(u(s,1,168,`h`),u(c,5,59,`m`)),o.append(l,d);let f=U(`div`,`wd-form-row`);f.appendChild(U(`label`,`wd-form-label`,`Label`));let p=U(`input`,`wd-input`);p.placeholder=`e.g. Claude is back`,f.appendChild(p);let m=U(`div`,`wd-form-row`);m.appendChild(U(`label`,`wd-form-label`,`When it fires`));let h=U(`select`,`wd-input`);for(let[e,t]of[[`none`,`Just remind me`],[`click`,`Click a button`],[`key`,`Click & press Enter`],[`text`,`Click, type & send a prompt`]]){let n=document.createElement(`option`);n.value=e,n.textContent=t,h.appendChild(n)}m.appendChild(h);let _=U(`div`,`wd-dialog-actions`),v=U(`button`,`wd-btn`);v.append(U(`span`,`wd-btn-label`,`Cancel`)),v.onclick=t;let y=U(`button`,`wd-btn wd-go`),b=U(`span`,`wd-btn-label`,`Start timer`);y.append(g(`clock`),b);let x=()=>{b.textContent=h.value===`none`?`Start timer`:`Next: place target`};h.onchange=x,x();let S=()=>{let e=Math.max(0,Math.min(168,Math.round(Number(s.value)||0))),t=Math.max(0,Math.min(59,Math.round(Number(c.value)||0))),n=(e*60+t)*6e4;return n>=6e4?n:null},C=()=>{let e=Math.max(0,Math.round(Number(s.value)||0)),t=Math.max(0,Math.round(Number(c.value)||0));return`${e?`${e}h `:``}${t}m`},w=e=>{let n=H(),r=S(),i=p.value.trim()||`Timer (${C()})`;this.conn.send({type:`timer-add`,id:n,fireInMs:r,label:i,action:e}),this.timers=[...this.timers,{id:n,label:i,fireAtMs:Date.now()+r,hasAction:!!e}],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(`Timer started`,`"${i}" — I'll ping you in ${C()}.`,`success`),t()};y.onclick=()=>{if(S()==null){this.notifications.flash(`Set a time`,`Choose at least 1 minute.`,`warning`);return}let t=h.value;if(t===`none`){w(void 0);return}e.style.display=`none`;let n=t===`text`?`Drag the crosshair onto the target input and type your prompt. The timer will click it to focus, insert the text and press Enter when time is up.`:t===`key`?`Drag the crosshair onto the field. The timer will click it to focus and press Enter when time is up.`:`Drag the crosshair onto the button or UI. The timer will click it when time is up.`;Se(this.view,this.root,{withText:t===`text`,hint:n,confirmLabel:`Confirm target`},n=>{if(!n){e.style.display=``;return}let r;r=t===`click`?{kind:`click`,x:n.nx,y:n.ny,button:`left`}:t===`key`?{kind:`key`,key:`Enter`,x:n.nx,y:n.ny}:{kind:`text`,text:n.text??``,x:n.nx,y:n.ny},w(r)})},_.append(v,y),n.append(r,a,o,f,m,_),e.appendChild(n),this.root.appendChild(e)}beginMonitor(){this.close();let e=U(`div`,`wd-dialog-overlay`),t=()=>{this.renderPicker=null,this.renderAlways=null,e.remove(),this.open()};e.addEventListener(`pointerdown`,n=>{n.target===e&&t()});let n=U(`div`,`wd-dialog`),r=U(`div`,`wd-dialog-head`);r.append(U(`h2`,``,`Monitor a session`));let i=U(`button`,`wd-dialog-x`);i.appendChild(g(`x`)),i.onclick=t,r.appendChild(i);let a=U(`p`,`wd-dialog-help`,`Pick a running AI session to watch. WhipDesk finds them automatically — no setup, no wrappers. You'll get one ping the moment the agent stops working (it's waiting on you or has gone idle).`),o=U(`div`,`wd-mon-pick-head`),s=U(`button`,`wd-btn`);s.append(U(`span`,`wd-btn-label`,`Rescan`)),s.onclick=()=>this.conn.send({type:`monitor-scan`}),o.append(U(`span`,`wd-form-label`,`Running sessions`),s);let c=U(`div`,`wd-mon-pick`),l=``,u=()=>{if(c.replaceChildren(),this.monitorSessions.length===0){l=``,v(),p(),c.appendChild(U(`p`,`wd-dialog-help`,`No AI sessions detected yet. Start Claude Code, Codex, Gemini, or Aider, then Rescan.`));return}(!l||!this.monitorSessions.some(e=>e.key===l))&&(l=this.monitorSessions[0].key);for(let e of this.monitorSessions){let t=U(`button`,`wd-mon-item`);t.type=`button`,e.key===l&&t.classList.add(`on`);let n=U(`div`,`wd-mon-item-main`);n.append(g(`activity`,15),U(`span`,`wd-mon-item-title`,`${W(e.agent)} · ${e.title}`));let r=U(`span`,`wd-mon-state`);r.dataset.state=e.state,r.textContent=De(e.state),t.append(n,r),t.onclick=()=>{l=e.key,u()},c.appendChild(t)}v(),p()};this.renderPicker=u;let d=U(`div`,`wd-form-row`);d.appendChild(U(`label`,`wd-form-label`,`Always alert me`));let f=U(`div`,`wd-mon-always`);d.appendChild(f);let p=()=>{f.replaceChildren();let e=this.monitorSessions.find(e=>e.key===l);if(!e){f.appendChild(U(`p`,`wd-dialog-help`,`Pick a session above to always alert for its agent.`));return}let t=e.agent,n=U(`label`,`wd-check`),r=U(`input`);r.type=`checkbox`,r.checked=this.alwaysAgents.has(t),r.onchange=()=>{r.checked?this.alwaysAgents.add(t):this.alwaysAgents.delete(t),this.conn.send({type:`monitor-always`,agent:t,enabled:r.checked}),r.checked&&this.notifications.permission==="default"&&this.requestNotifications(),p()};let i=U(`span`,`wd-check-text`);i.append(document.createTextNode(`Always monitor every ${W(t)} session`),U(`span`,`wd-check-sub`,`Keeps alerting across agent and WhipDesk restarts — no need to re-add it.`)),n.append(r,i),f.appendChild(n)};this.renderAlways=p;let m=U(`div`,`wd-dialog-actions`),h=U(`button`,`wd-btn`);h.append(U(`span`,`wd-btn-label`,`Cancel`)),h.onclick=t;let _=U(`button`,`wd-btn wd-go`);_.append(g(`activity`),U(`span`,`wd-btn-label`,`Add monitor`));let v=()=>_.disabled=!l;v(),_.onclick=()=>{let e=this.monitorSessions.find(e=>e.key===l);if(!e)return;let n=H(),r=e.title;this.conn.send({type:`monitor-add`,id:n,key:e.key,agent:e.agent,label:r}),this.monitors=[...this.monitors,{id:n,key:e.key,agent:e.agent,label:r,state:e.state,live:!0}],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(`Monitoring started`,`Watching ${W(e.agent)} · ${r}.`,`success`),t()},m.append(h,_),n.append(r,a,o,c,d,m),e.appendChild(n),this.root.appendChild(e),u(),this.conn.send({type:`monitor-scan`})}};function ke(){let e=new URLSearchParams(location.hash.replace(/^#/,``));return{token:e.get(`t`)??e.get(`token`)??``,remote:e.get(`remote`)===`1`||e.has(`device`),device:e.get(`device`)??e.get(`d`)??``}}function Ae(){return`${location.protocol===`https:`?`wss`:`ws`}://${location.host}/ws`}async function je(){let e=window.__WHIPDESK_FB__;if(e?.apiKey)return e;try{let e=await fetch(`firebase.json`,{cache:`no-store`});if(e.ok)return await e.json()}catch{}return null}var G=document.getElementById(`app`);if(!G)throw Error(`#app not found`);var K=$(`canvas`,`wd-screen`);K.id=`wd-screen`;var Me=$(`div`,`wd-toasts`);G.append(K,Me);var{token:q,remote:J,device:Y}=ke(),X=new xe(K),Z=new oe(Me),Ne=new ce(G),Q=new p(G);async function Pe(e){if(J&&Y){if(e)return new he(Y,q,e);Z.show({type:`notification`,id:`no-fb`,title:`Remote unavailable`,body:`Firebase config not found for remote mode.`,level:`error`,source:`client`,t:Date.now()})}return new u(Ae(),q)}async function Fe(){let e=J&&Y?await je():null,t=await Pe(e),n=new ae(K,X,t),r=new Oe(G,t,X,Z,()=>e?fe(e,Z):Z.requestPermission()),i=new ne(G,{conn:t,view:X,input:n,notifications:Z,watchers:r}),a=document.createElement(`video`);a.muted=!0,a.autoplay=!0,a.playsInline=!0,a.setAttribute(`playsinline`,``),a.style.display=`none`,G.append(a);let o=document.createElement(`video`);o.muted=!0,o.autoplay=!0,o.playsInline=!0,o.setAttribute(`playsinline`,``),o.style.display=`none`,G.append(o);let s=!1,c=!1,l=null,u=0,d=e=>e.w>=.999&&e.h>=.999,f=null,p=0,m=0,h=!1,g=0,_=document.createElement(`div`);_.className=`wd-sharpen hidden`,_.setAttribute(`aria-label`,`updating view`),G.append(_);let v=()=>{h=!1,_.classList.add(`hidden`)},y=e=>{let n=e.w>=.92&&e.h>=.92?{x:0,y:0,w:1,h:1}:e,r=d(n);if(l){if(r&&d(l))return;if(!r){let e=.08;if(Math.abs(n.x-l.x)<n.w*e&&Math.abs(n.y-l.y)<n.h*e&&Math.abs(n.w-l.w)<n.w*e&&Math.abs(n.h-l.h)<n.h*e)return}}l=n,p=Date.now(),m=0,h=!f||!b(n,f),h&&(g=Date.now()),t.send({type:`set-viewport`,x:n.x,y:n.y,w:n.w,h:n.h})},b=(e,t)=>Math.abs(e.x-t.x)<.005&&Math.abs(e.y-t.y)<.005&&Math.abs(e.w-t.w)<.005&&Math.abs(e.h-t.h)<.005;window.setInterval(()=>{let e=Date.now()-g;if(_.classList.toggle(`hidden`,!(h&&e>1200&&e<3e4)),!(!l||!f)){if(b(f,l)){m=0;return}Date.now()-p<1200||m>=3||(m+=1,p=Date.now(),t.send({type:`set-viewport`,x:l.x,y:l.y,w:l.w,h:l.h}))}},1e3);let x=!1,S=!1,C=()=>{let e=X.getZoom()>1.01;window.clearTimeout(u),u=window.setTimeout(()=>{u=0,y(X.getZoom()>1.01?X.getVisibleRegion():{x:0,y:0,w:1,h:1})},e?400:0)};X.setOnView(()=>{x=!0,C()}),X.setOnGesture(e=>{e?(S=u!==0,S&&(window.clearTimeout(u),u=0),x=!1):!x&&S&&(S=!1,C())}),t.on(`status`,e=>{i.setStatus(e),e===`connected`?(c=!1,Q.hide()):(!s||c)&&Q.show(s?`Reconnecting…`:void 0),e===`disconnected`&&(l=null,f=null,v())}),t.on(`transport`,e=>i.setTransport(e)),t.on(`welcome`,e=>{let t=!s;s=!0,Q.hide(),Ne.hide(),X.setScreen(e.screen),i.setWelcome(e),t?(X.setZoom(1),l=null,y({x:0,y:0,w:1,h:1})):(l=null,window.clearTimeout(u),u=0,y(X.getZoom()>1.01?X.getVisibleRegion():{x:0,y:0,w:1,h:1}))}),t.on(`versionMismatch`,e=>Ie(e.agentVersion)),t.on(`pinRequired`,e=>{Q.hide(),Ne.show(e,e=>t.submitPin(e))}),t.on(`presence`,e=>i.setPresence(e));let w=0,T=0,E=0,D=()=>i.setAlertCount(w+T+E);if(t.on(`watchers`,e=>{w=e.length,r.setRegions(e),D()}),t.on(`timers`,e=>{T=e.length,r.setTimers(e),D()}),t.on(`monitors`,e=>{E=e.length,r.setMonitors(e),D()}),t.on(`monitorAlways`,e=>{r.setAlwaysAgents(e)}),t.on(`screenRegion`,e=>{f={x:e.x,y:e.y,w:e.w,h:e.h},e.active&&v();let t=d(e)?null:e;e.active?X.setFrameRegionActive(t):X.setFrameRegion(t)}),t.on(`videoTrack`,e=>{if(!e){a.srcObject=null,X.setVideoSource(null);return}a.srcObject=e,a.play().catch(()=>{}),X.setVideoSource(a)}),t.on(`overviewTrack`,e=>{if(!e){o.srcObject=null,X.setOverviewSource(null);return}o.srcObject=e,o.play().catch(()=>{}),X.setOverviewSource(o)}),t.on(`netStats`,({fps:e,rtt:t})=>i.setNetStats(e,t)),t.on(`screenMeta`,({screen:e,activeDisplay:t})=>{X.setScreen(e),t!==void 0&&i.setActiveDisplay(t)}),t.on(`notification`,e=>Z.show(e)),t.on(`error`,e=>i.flashError(e)),e){let n=!1;t.on(`welcome`,()=>{n||Z.permission!==`granted`||(n=!0,fe(e,Z))})}let O=()=>{document.hidden||t.isHealthy()||(c=!0,Q.show(`Reconnecting…`),t.wake())};document.addEventListener(`visibilitychange`,()=>{t.setVisible(!document.hidden),document.hidden||O()}),window.addEventListener(`pageshow`,O),window.addEventListener(`pagehide`,e=>{e.persisted||t.close()}),q||i.flashError(`No pairing token in the URL (#t=…). Re-open the link/QR from the agent.`),t.connect()}Fe();function $(e,t){let n=document.createElement(e);return n.className=t,n}function Ie(e){let t=`wd-outdated-banner`;if(document.getElementById(t))return;let n=document.createElement(`div`);n.id=t,n.style.cssText=`position:fixed;top:0;left:0;right:0;z-index:9999;background:#7a2e00;color:#fff;font:14px/1.4 system-ui,sans-serif;padding:10px 40px 10px 14px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.35)`,n.innerHTML=`⚠️ This WhipDesk agent${e?` (agent ${e})`:``} is out of date. Update from <a style="color:#ffd9a6" href="https://github.com/BinaryBananaLLC/WhipDesk/releases/latest" target="_blank" rel="noreferrer noopener">the latest release</a> or run <code>npm i -g whipdesk@latest</code>.`;let r=document.createElement(`button`);r.textContent=`✕`,r.setAttribute(`aria-label`,`Dismiss`),r.style.cssText=`position:absolute;right:8px;top:6px;background:none;border:none;color:#fff;font-size:16px;cursor:pointer`,r.onclick=()=>n.remove(),n.appendChild(r),document.body.appendChild(n)}
@@ -2,13 +2,41 @@
2
2
  /**
3
3
  * WhipDesk FCM background handler (service worker).
4
4
  *
5
- * Receives Firebase Cloud Messaging web pushes and shows a notification even when the
6
- * controller PWA is closed. Uses the compat SDK via importScripts (the modular SDK can't run
7
- * in a classic service worker) and reads the public-safe web config emitted next to this file
8
- * by scripts/sync-controller.cjs (firebase.json). See apps/mobile-web/src/push.ts.
5
+ * Shows a notification for every incoming web push even when the controller PWA is closed.
6
+ *
7
+ * IMPORTANT: the `push` listener is registered SYNCHRONOUSLY at script evaluation and displays
8
+ * the notification itself — no Firebase SDK. The previous version initialized the FCM compat SDK
9
+ * asynchronously (after fetching ./firebase.json), but a push that WAKES a cold service worker
10
+ * dispatches before any late-registered handler exists, so nothing called showNotification and
11
+ * Chrome displayed its generic "This site has been updated in the background" fallback instead
12
+ * of the real alert. Token registration lives page-side (src/push.ts); the worker only needs to
13
+ * parse the FCM webpush payload ({ notification: { title, body, ... }, data: {...} }) and render
14
+ * it, which needs no SDK at all.
9
15
  */
10
- importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js");
11
- importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js");
16
+ self.addEventListener("push", (event) => {
17
+ let payload = {};
18
+ try {
19
+ payload = event.data ? event.data.json() : {};
20
+ } catch (e) {
21
+ /* non-JSON push — fall through to the generic title below */
22
+ }
23
+ const n = payload.notification || {};
24
+ const d = payload.data || {};
25
+ const title = n.title || d.title || "WhipDesk";
26
+ const body = n.body || d.body || "";
27
+ event.waitUntil(
28
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((wins) => {
29
+ // App open and visible => its own in-app alerts cover this; skip the OS notification
30
+ // (same policy the FCM SDK applied).
31
+ if (wins.some((w) => w.visibilityState === "visible")) return;
32
+ return self.registration.showNotification(title, {
33
+ body,
34
+ tag: n.tag || d.tag || "whipdesk-alert",
35
+ renotify: true,
36
+ });
37
+ }),
38
+ );
39
+ });
12
40
 
13
41
  self.addEventListener("notificationclick", (event) => {
14
42
  event.notification.close();
@@ -21,23 +49,3 @@ self.addEventListener("notificationclick", (event) => {
21
49
  }),
22
50
  );
23
51
  });
24
-
25
- (async () => {
26
- try {
27
- const res = await fetch("./firebase.json", { cache: "no-store" });
28
- if (!res.ok) return;
29
- const config = await res.json();
30
- firebase.initializeApp(config);
31
- const messaging = firebase.messaging();
32
- messaging.onBackgroundMessage((payload) => {
33
- const n = (payload && payload.notification) || {};
34
- self.registration.showNotification(n.title || "WhipDesk", {
35
- body: n.body || "",
36
- tag: (payload && payload.data && payload.data.tag) || "whipdesk-alert",
37
- renotify: true,
38
- });
39
- });
40
- } catch (e) {
41
- /* no config / unsupported browser — background push stays disabled */
42
- }
43
- })();
@@ -18,8 +18,8 @@
18
18
  <meta name="apple-mobile-web-app-title" content="WhipDesk" />
19
19
  <meta name="mobile-web-app-capable" content="yes" />
20
20
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
21
- <script type="module" crossorigin src="./assets/index-YSgNjLfG.js"></script>
22
- <link rel="stylesheet" crossorigin href="./assets/index-C1gDHBib.css">
21
+ <script type="module" crossorigin src="./assets/index-_PAhuG6R.js"></script>
22
+ <link rel="stylesheet" crossorigin href="./assets/index-C_O8Rc7r.css">
23
23
  </head>
24
24
  <body>
25
25
  <div id="app"></div>
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "whipdesk",
3
- "version": "0.0.9",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "WhipDesk desktop host: screen capture, input injection, notification hub, and the HTTP/WebSocket server that serves the mobile client.",
6
6
  "license": "AGPL-3.0",
7
- "homepage": "https://github.com/BinaryBananaLLC/WhipDesk#readme",
7
+ "homepage": "https://whipdesk.com",
8
8
  "repository": {
9
9
  "type": "git",
10
10
  "url": "git+https://github.com/BinaryBananaLLC/WhipDesk.git",
@@ -15,7 +15,8 @@
15
15
  },
16
16
  "files": [
17
17
  "dist/agent.cjs",
18
- "dist/mobile-web"
18
+ "dist/mobile-web",
19
+ "README.md"
19
20
  ],
20
21
  "engines": {
21
22
  "node": ">=20"
@@ -1 +0,0 @@
1
- :root{--bg:#f4f6f9;--panel:#fff;--panel-2:#eef1f5;--border:#d6dce4;--text:#1a2330;--muted:#5f6b7a;--accent:#2f6fed;--accent-2:#1aa66b;--warn:#c77700;--error:#d83b3b;--shadow:#141e3229;--screen-bg:#1b2027}@media (prefers-color-scheme:dark){:root{--bg:#0b0e14;--panel:#151a23;--panel-2:#1d2430;--border:#2a3342;--text:#e6edf3;--muted:#8b98a9;--accent:#4ea1ff;--accent-2:#36d399;--warn:#f5a623;--error:#ff5c5c;--shadow:#00000080;--screen-bg:#000}}*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}html,body{background:var(--bg);height:100%;color:var(--text);overscroll-behavior:none;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,system-ui,sans-serif;overflow:hidden}#app{-webkit-user-select:none;user-select:none;position:fixed;inset:0}input,textarea{-webkit-user-select:text;user-select:text}.wd-screen{background:var(--screen-bg);touch-action:none;width:100%;height:100%;display:block;position:absolute;inset:0}.wd-statusbar{top:calc(env(safe-area-inset-top,0px) + 8px);background:var(--panel);border:1px solid var(--border);max-width:calc(100% - 64px);min-height:38px;box-shadow:0 2px 10px var(--shadow);z-index:5;cursor:pointer;border-radius:10px;align-items:center;gap:8px;padding:6px 10px;font-size:12px;transition:max-width .25s,padding .25s;display:flex;position:absolute;left:8px}.wd-statusbar.collapsed{max-width:124px;padding:6px 8px;overflow:hidden}.wd-statusbar.collapsed .wd-status-text,.wd-statusbar.collapsed .wd-watchers{display:none}.wd-dot{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.wd-dot[data-status=connected]{background:var(--accent-2)}.wd-dot[data-status=connecting]{background:var(--warn)}.wd-dot[data-status=disconnected]{background:var(--error)}.wd-status-text{color:var(--text);white-space:nowrap;text-overflow:ellipsis;flex:auto;min-width:0;font-weight:600;overflow:hidden}.wd-watchers{color:var(--warn);white-space:nowrap;flex:none;font-weight:700}.wd-transport{letter-spacing:.4px;color:#fff;background:var(--accent-2);text-transform:uppercase;border-radius:6px;flex:none;padding:2px 6px;font-size:11px;font-weight:800}.wd-transport.hidden{display:none}.wd-transport[data-kind=turn]{color:#111;background:#e0a800}.wd-transport[data-kind=stun],.wd-transport[data-kind=direct]{background:var(--accent-2)}.wd-transport[data-kind=lan]{background:#3b82f6}.wd-bell{top:calc(env(safe-area-inset-top,0px) + 8px);z-index:6;border:1px solid var(--border);background:var(--panel);min-width:38px;min-height:38px;color:var(--text);box-shadow:0 2px 10px var(--shadow);cursor:pointer;border-radius:10px;place-items:center;padding:6px;display:grid;position:absolute;right:8px}.wd-badge{background:var(--accent);color:#fff;text-align:center;min-width:18px;height:18px;box-shadow:0 1px 4px var(--shadow);border-radius:9px;padding:0 5px;font-size:11px;font-weight:700;line-height:18px;position:absolute;top:-6px;right:-6px}.wd-ribbon{z-index:6;padding-bottom:env(safe-area-inset-bottom,0px);background:var(--panel);border-top:1px solid var(--border);box-shadow:0 -4px 16px var(--shadow);position:absolute;bottom:0;left:0;right:0}.wd-panel{flex-direction:column;display:flex}.wd-options{border-bottom:1px solid var(--border);padding:6px}.wd-pane{flex-wrap:wrap;justify-content:center;align-items:center;gap:5px;display:flex}.wd-pane-col{flex-direction:column;align-items:stretch;gap:8px}.wd-pane-single-row{scrollbar-width:none;flex-wrap:nowrap;justify-content:center;align-items:stretch;gap:4px;overflow-x:auto}.wd-pane-single-row::-webkit-scrollbar{display:none}.wd-pane-single-row .wd-group{flex:0 auto;gap:2px;min-width:0;padding:3px 4px}.wd-pane-single-row .wd-group-items{flex-wrap:nowrap;gap:3px}.wd-pane-single-row .wd-btn{min-width:0}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:30px;padding:4px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:18px;height:18px}.wd-pane-single-row .wd-btn.wd-go{gap:4px;padding:6px 9px;font-size:12px}@media (min-width:390px){.wd-pane-single-row{gap:5px}.wd-pane-single-row .wd-group{padding:3px 6px}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:36px;padding:5px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:20px;height:20px}.wd-pane-single-row .wd-btn.wd-go{padding:7px 12px;font-size:13px}}@media (min-width:560px){.wd-pane-single-row .wd-btn.wd-icon-only{min-width:42px;padding:6px}.wd-pane-single-row .wd-btn.wd-go{padding:8px 16px}}@media (min-width:820px){.wd-ribbon{border:1px solid var(--border);width:min(880px,94vw);box-shadow:0 10px 34px var(--shadow);border-radius:16px;padding-bottom:0;bottom:16px;left:50%;right:auto;transform:translate(-50%)}.wd-options{padding:5px 6px}.wd-btn{min-height:32px;font-size:12px}.wd-pane-single-row .wd-btn.wd-icon-only{min-width:32px;padding:4px}.wd-pane-single-row .wd-btn.wd-icon-only svg{width:17px;height:17px}.wd-pane-single-row .wd-btn.wd-go{padding:6px 12px;font-size:12px}.wd-group{padding:3px 6px}.wd-bell{min-width:32px;min-height:32px}.wd-statusbar{min-height:32px}}.wd-group{border:1px solid var(--border);background:var(--panel-2);border-radius:12px;flex-direction:column;align-items:stretch;gap:3px;padding:4px 6px;display:inline-flex}.wd-group-label{letter-spacing:.03em;text-transform:uppercase;color:var(--muted);white-space:nowrap;text-align:center;font-size:9px;font-weight:700}.wd-group-head{justify-content:space-between;align-items:center;gap:4px;display:flex}.wd-mode-toggle{border:1px solid var(--border);background:var(--panel);border-radius:999px;display:inline-flex;overflow:hidden}.wd-mode-btn{color:var(--muted);cursor:pointer;background:0 0;border:0;padding:4px 7px;font-size:10px;font-weight:700;line-height:1}.wd-mode-btn.on{background:var(--accent);color:#fff}.wd-group-items{flex-wrap:wrap;justify-content:center;align-items:center;gap:3px;display:flex}.wd-wrap{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-btn-label{line-height:1}.wd-row{flex-wrap:wrap;justify-content:center;align-items:center;gap:6px;display:flex}.wd-tabs{align-items:center;gap:4px;padding:5px 6px;display:flex}.wd-tab{min-height:48px;color:var(--muted);cursor:pointer;touch-action:manipulation;background:0 0;border:1px solid #0000;border-radius:10px;flex-direction:column;flex:1;align-items:center;gap:2px;padding:6px 4px;font-size:11px;font-weight:600;display:flex}.wd-tab .wd-tab-label{line-height:1}.wd-tab.on{background:var(--panel-2);color:var(--accent);border-color:var(--border)}.wd-collapse{border:1px solid var(--border);background:var(--panel);min-width:44px;min-height:48px;color:var(--text);cursor:pointer;border-radius:10px;flex:none;place-items:center;display:grid}.wd-btn{border:1px solid var(--border);background:var(--panel);min-width:40px;min-height:40px;color:var(--text);cursor:pointer;-webkit-user-select:none;user-select:none;touch-action:manipulation;white-space:nowrap;border-radius:10px;justify-content:center;align-items:center;gap:7px;padding:6px 10px;font-size:13px;font-weight:600;line-height:1;display:inline-flex}.wd-btn svg{flex:none}.wd-btn.wd-icon-only{min-width:40px;padding:6px}.wd-btn:active{background:var(--panel-2);transform:translateY(1px)}.wd-btn.on{border-color:var(--accent);color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.wd-btn.wd-primary{background:var(--accent);border-color:var(--accent);color:#fff}.wd-btn.wd-go{background:var(--accent-2);border-color:var(--accent-2);color:#fff;padding:8px 14px}.wd-btn.wd-go:active{filter:brightness(.92);transform:translateY(1px)}.wd-seg{border:1px solid var(--border);border-radius:10px;display:inline-flex;overflow:hidden}.wd-seg-btn{background:var(--panel);min-height:44px;color:var(--muted);cursor:pointer;touch-action:manipulation;border:none;align-items:center;gap:6px;padding:8px 18px;font-size:15px;font-weight:600;display:inline-flex}.wd-seg-btn.on{background:var(--accent);color:#fff}.wd-zoom{text-align:center;font-variant-numeric:tabular-nums;min-width:52px;color:var(--text);font-weight:600}.wd-hint{text-align:center;color:var(--muted);flex-basis:100%;font-size:12px}.wd-type-input{resize:vertical;border:1px solid var(--border);background:var(--panel-2);width:100%;min-height:46px;color:var(--text);border-radius:10px;padding:10px;font-family:inherit;font-size:16px}.wd-keys-row{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-type-actions{gap:6px;display:flex}.wd-type-actions .wd-btn{flex:1}.wd-monitor-list{flex-wrap:wrap;justify-content:center;gap:6px;display:flex}.wd-toasts{top:calc(env(safe-area-inset-top,0px) + 36px);z-index:9;pointer-events:none;flex-direction:column;gap:8px;display:flex;position:absolute;left:8px;right:8px}.wd-toast{background:var(--panel);border:1px solid var(--border);border-left-width:4px;border-radius:10px;flex-direction:column;gap:2px;padding:10px 12px;transition:opacity .3s,transform .3s;display:flex;box-shadow:0 6px 20px #0006}.wd-toast strong{font-size:14px}.wd-toast span{color:var(--muted);font-size:13px}.wd-toast.wd-success{border-left-color:var(--accent-2)}.wd-toast.wd-warning{border-left-color:var(--warn)}.wd-toast.wd-error{border-left-color:var(--error)}.wd-toast.wd-info{border-left-color:var(--accent)}.wd-toast.wd-hide{opacity:0;transform:translateY(-8px)}.hidden{display:none!important}.wd-pin-overlay{z-index:20;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#06090eeb;place-items:center;padding:20px;display:grid;position:absolute;inset:0}.wd-pin-card{background:var(--panel);border:1px solid var(--border);text-align:center;border-radius:14px;width:100%;max-width:320px;padding:24px}.wd-pin-whip{object-fit:contain;width:64px;height:64px;margin:0 auto 12px;display:block}.wd-pin-card h2{margin:0 0 8px;font-size:20px}.wd-pin-msg{color:var(--muted);margin:0 0 16px;font-size:14px}.wd-pin-msg.err{color:var(--error)}.wd-pin-input{border:1px solid var(--border);background:var(--panel-2);width:100%;color:var(--text);text-align:center;letter-spacing:6px;border-radius:10px;margin-bottom:14px;padding:14px;font-size:22px}.wd-pin-submit{background:var(--accent);color:#fff;cursor:pointer;touch-action:manipulation;border:none;border-radius:12px;width:100%;min-height:50px;font-size:17px;font-weight:700}.wd-pin-submit:active{filter:brightness(.92);transform:translateY(1px)}.wd-pin-back{color:var(--muted);cursor:pointer;background:0 0;border:none;margin:14px auto 0;padding:6px 10px;font-size:13px;font-weight:600;display:block}.wd-pin-back:active{color:var(--text)}.wd-connecting{z-index:18;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);background:#06090eeb;place-items:center;padding:20px;display:grid;position:absolute;inset:0}.wd-connecting-card{text-align:center;width:100%;max-width:320px}.wd-connecting-whip{object-fit:contain;transform-origin:0 100%;width:56px;height:56px;margin:0 auto 18px;animation:1.4s ease-in-out infinite wd-whip-crack;display:block}.wd-connecting-spinner{border:3px solid #ffffff38;border-top-color:#4ea1ff;border-radius:50%;width:34px;height:34px;margin:0 auto 16px;animation:.8s linear infinite wd-spin}.wd-connecting-msg{color:#eef2f7;text-shadow:0 1px 2px #00000080;margin:0;font-size:15px;font-weight:600}@keyframes wd-spin{to{transform:rotate(360deg)}}@keyframes wd-whip-crack{0%,to{transform:rotate(-16deg)}40%{transform:rotate(14deg)}55%{transform:rotate(5deg)}70%{transform:rotate(11deg)}}.wd-dialog-overlay{z-index:18;-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);background:#06090e99;place-items:center;padding:16px;display:grid;position:absolute;inset:0}.wd-dialog{background:var(--panel);border:1px solid var(--border);border-radius:14px;width:100%;max-width:380px;padding:16px}.wd-dialog-head{justify-content:space-between;align-items:center;display:flex}.wd-dialog-head h2{margin:0;font-size:18px}.wd-dialog-x{border:1px solid var(--border);background:var(--panel-2);width:36px;height:36px;color:var(--text);cursor:pointer;border-radius:8px;place-items:center;display:grid}.wd-dialog-help{color:var(--muted);margin:8px 0 12px;font-size:13px}.wd-help-intro{margin:0 0 6px}.wd-help-list{flex-direction:column;gap:5px;margin:0 0 8px;padding-left:18px;display:flex}.wd-help-list strong{color:var(--text)}.wd-help-note{margin:0;font-style:italic}.wd-watch-list{flex-direction:column;gap:8px;margin-bottom:12px;display:flex}.wd-watch-row{border:1px solid var(--border);background:var(--panel-2);border-radius:10px;align-items:center;gap:8px;padding:8px 10px;display:flex}.wd-watch-name{text-align:left;color:var(--text);cursor:pointer;background:0 0;border:none;flex:1;padding:4px 0;font-family:inherit;font-size:14px;font-weight:600}.wd-watch-name:active{color:var(--accent)}.wd-timer-info{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.wd-timer-name{color:var(--text);align-items:center;gap:6px;font-size:14px;font-weight:600;display:flex}.wd-timer-name svg{color:var(--accent);flex:none}.wd-timer-remain{color:var(--muted);font-variant-numeric:tabular-nums;font-size:12px}.wd-timer-tag{letter-spacing:.5px;text-transform:uppercase;color:#fff;background:var(--accent);border-radius:6px;flex:none;padding:2px 6px;font-size:10px;font-weight:800}.wd-dialog-actions{gap:8px;margin-top:4px;display:flex}.wd-dialog-actions .wd-btn{flex:1;justify-content:center}.wd-dialog-actions.wd-actions-stack{flex-direction:column}.wd-actions-stack .wd-btn{width:100%}.wd-mon-state{letter-spacing:.4px;text-transform:uppercase;color:#fff;background:var(--muted);border-radius:6px;flex:none;padding:2px 6px;font-size:10px;font-weight:800}.wd-mon-state[data-state=working]{background:var(--accent-2)}.wd-mon-state[data-state=blocked]{background:var(--warn)}.wd-mon-state[data-state=finished]{background:var(--accent)}.wd-mon-state[data-state=crashed]{background:var(--error)}.wd-mon-pick-head{justify-content:space-between;align-items:center;gap:8px;margin-bottom:6px;display:flex}.wd-mon-pick{flex-direction:column;gap:6px;max-height:40vh;margin-bottom:12px;display:flex;overflow-y:auto}.wd-mon-item{border:1px solid var(--border);background:var(--panel);width:100%;color:var(--text);cursor:pointer;text-align:left;border-radius:10px;justify-content:space-between;align-items:center;gap:10px;padding:9px 11px;display:flex}.wd-mon-item.on{border-color:var(--accent);box-shadow:inset 0 0 0 1px var(--accent)}.wd-mon-item-main{align-items:center;gap:8px;min-width:0;display:flex}.wd-mon-item-main svg{color:var(--accent);flex:none}.wd-mon-item-title{white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:600;overflow:hidden}.wd-mon-always{flex-direction:column;gap:8px;display:flex}.wd-check{color:var(--text);cursor:pointer;align-items:flex-start;gap:8px;font-size:13px;display:flex}.wd-check input{width:16px;height:16px;accent-color:var(--accent);flex:none;margin-top:1px}.wd-check-text{flex-direction:column;gap:2px;display:flex}.wd-check-sub{color:var(--muted);font-size:11px}.wd-form-row{flex-direction:column;gap:5px;margin-bottom:12px;display:flex}.wd-form-label{color:var(--muted);font-size:12px;font-weight:700}.wd-input{border:1px solid var(--border);background:var(--panel-2);width:100%;color:var(--text);border-radius:10px;padding:10px 12px;font-family:inherit;font-size:15px}.wd-input-area{resize:vertical;min-height:64px}.wd-form-duration{flex-wrap:wrap;align-items:center;gap:12px;display:flex}.wd-input-num{text-align:center;width:56px}.wd-form-unit{color:var(--muted);font-size:14px;font-weight:600}.wd-preset-row{flex-wrap:wrap;gap:6px;margin-bottom:10px;display:flex}.wd-preset{border:1px solid var(--border);background:var(--panel-2);min-width:48px;color:var(--text);cursor:pointer;border-radius:999px;flex:auto;padding:9px 10px;font-family:inherit;font-size:13px;font-weight:700}.wd-preset:active{background:var(--accent);color:#fff;border-color:#0000}.wd-stepper{align-items:center;gap:4px;display:flex}.wd-step-btn{border:1px solid var(--border);background:var(--panel-2);width:38px;height:38px;color:var(--text);cursor:pointer;touch-action:manipulation;border-radius:10px;flex:none;font-family:inherit;font-size:22px;font-weight:700;line-height:1}.wd-step-btn:active{background:var(--accent);color:#fff;border-color:#0000}.wd-form-target{flex-wrap:wrap;align-items:center;gap:10px;display:flex}.wd-form-target-status{color:var(--muted);font-size:13px}.wd-form-target-status.set{color:var(--accent-2);font-weight:700}.wd-pick-banner{top:calc(env(safe-area-inset-top,0px) + 56px);z-index:30;background:var(--accent);color:#fff;box-shadow:0 6px 20px var(--shadow);pointer-events:none;text-align:center;border-radius:999px;max-width:90%;padding:10px 16px;font-size:13px;font-weight:700;position:absolute;left:50%;transform:translate(-50%)}.wd-conn-row{border-bottom:1px solid var(--border);align-items:flex-start;gap:10px;padding:10px 0;display:flex}.wd-conn-label{width:92px;color:var(--muted);flex:none;padding-top:2px;font-size:13px;font-weight:700}.wd-conn-value{color:var(--text);word-break:break-word;flex:1;font-size:15px;font-weight:600}.wd-conn-route{flex-direction:column;flex:1;gap:4px;display:flex}.wd-conn-route .wd-transport{align-self:flex-start;position:static}.wd-conn-desc{color:var(--muted);font-size:12.5px}.wd-conn-status{flex:1;align-items:center;gap:8px;display:flex}.wd-conn-error{border:1px solid var(--error);color:var(--error);word-break:break-word;background:#dc35451f;border-radius:10px;margin-top:12px;padding:9px 11px;font-size:12.5px;font-weight:600}.wd-disconnect{background:var(--error);color:#fff;border-color:#0000;justify-content:center;width:100%;min-height:46px;margin-top:14px;font-size:15px;font-weight:700}.wd-support-link{border:1px solid var(--border);color:var(--muted);cursor:pointer;background:0 0;border-radius:999px;align-self:flex-start;align-items:center;gap:6px;margin-top:8px;padding:5px 12px;font-size:12.5px;font-weight:600;transition:color .15s,border-color .15s;display:inline-flex}.wd-support-link:hover,.wd-support-link:active{color:var(--accent);border-color:var(--accent)}.wd-support-link svg{flex:none;width:14px;height:14px}.wd-conn-feedback{border-top:1px solid var(--border);text-align:center;margin-top:14px;padding-top:12px}.wd-conn-feedback-text{color:var(--muted);margin:0;font-size:12.5px}.wd-conn-feedback-links{justify-content:center;gap:10px;margin-top:9px;display:flex}.wd-conn-feedback-link{border:1px solid var(--border);color:var(--text);background:0 0;border-radius:999px;align-items:center;gap:7px;padding:7px 16px;font-size:13px;font-weight:600;text-decoration:none;transition:color .15s,border-color .15s;display:inline-flex}.wd-conn-feedback-link:hover,.wd-conn-feedback-link:active{color:var(--accent);border-color:var(--accent)}.wd-conn-feedback-link svg{flex:none;width:16px;height:16px}.wd-pin-support{margin-top:16px}.wd-placing .wd-ribbon,.wd-placing .wd-statusbar,.wd-placing .wd-bell{display:none}.wd-place-layer{z-index:40;touch-action:none;background:0 0;position:absolute;inset:0}.wd-place-marker{z-index:41;pointer-events:none;border:2px solid #4ea1ff;border-radius:50%;width:46px;height:46px;position:absolute;transform:translate(-50%,-50%);box-shadow:0 0 0 2px #00000080,0 0 14px #4ea1ffb3}.wd-place-marker:before,.wd-place-marker:after{content:"";background:#4ea1ff;position:absolute}.wd-place-marker:before{width:2px;top:-8px;bottom:-8px;left:50%;transform:translate(-50%)}.wd-place-marker:after{height:2px;top:50%;left:-8px;right:-8px;transform:translateY(-50%)}.wd-place-bar{z-index:42;padding:12px 14px calc(env(safe-area-inset-bottom,0px) + 12px);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);border-top:1px solid var(--border);background:#0a0e14f0;flex-direction:column;gap:10px;display:flex;position:absolute;bottom:0;left:0;right:0}.wd-place-hint{color:#eef2f7;text-align:center;margin:0;font-size:13px;font-weight:600}.wd-place-text{width:100%}.wd-place-buttons{gap:8px;display:flex}.wd-place-buttons .wd-btn{flex:1;justify-content:center}.wd-perm-row{border:1px solid var(--border);background:var(--panel-2);border-radius:10px;align-items:center;gap:8px;margin-bottom:12px;padding:8px 10px;display:flex}.wd-perm-dot{background:var(--muted);border-radius:50%;flex:none;width:9px;height:9px}.wd-perm-dot[data-state=on]{background:var(--accent-2)}.wd-perm-dot[data-state=warn]{background:var(--warn)}.wd-perm-dot[data-state=off]{background:var(--error)}.wd-perm-text{color:var(--muted);flex:1;font-size:12px}.wd-perm-enable{flex:none;min-width:auto;min-height:32px;padding:4px 12px;font-size:12px}.wd-dialog .wd-go{justify-content:center;width:100%}.wd-selector{z-index:19;border:2px solid var(--accent);touch-action:none;background:#2f6fed24;border-radius:6px;position:absolute;box-shadow:0 0 0 9999px #06090e59}.wd-selector-move{background:var(--accent);color:#fff;cursor:move;touch-action:none;border-radius:8px 0;place-items:center;width:38px;height:38px;display:grid;position:absolute;top:-1px;left:-1px}.wd-selector-handle{background:var(--accent);border:3px solid var(--panel);cursor:nwse-resize;touch-action:none;border-radius:50%;width:28px;height:28px;position:absolute;bottom:-12px;right:-12px}.wd-selector-bar{white-space:nowrap;gap:8px;display:flex;position:absolute;bottom:-56px;left:50%;transform:translate(-50%)}.wd-selector-info{top:calc(env(safe-area-inset-top,0px) + 52px);z-index:20;background:var(--panel);border:1px solid var(--border);border-left:4px solid var(--accent);max-width:430px;box-shadow:0 6px 20px var(--shadow);color:var(--text);text-align:center;pointer-events:none;border-radius:10px;margin:0 auto;padding:8px 12px;font-size:12.5px;line-height:1.35;position:absolute;left:12px;right:12px}
@@ -1,2 +0,0 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./index.esm-mXKu2C3o.js","./index.esm-B3ZTr43m.js","./index.esm-CmSagqpw.js","./index.esm-wogdVWd2.js","./index.esm-BgjPHsdM.js","./index.esm-Di8jSGaG.js"])))=>i.map(i=>d[i]);
2
- (function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();function e(e){return typeof e==`object`&&!!e&&typeof e.type==`string`}var t=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]);function n(e){return new TextEncoder().encode(e)}function r(e){let n=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),r=e.length*8,i=new Uint8Array((e.length+8>>6)+1<<6);i.set(e),i[e.length]=128;let a=new DataView(i.buffer);a.setUint32(i.length-4,r>>>0,!1),a.setUint32(i.length-8,Math.floor(r/4294967296),!1);let o=new Uint32Array(64);for(let e=0;e<i.length;e+=64){for(let t=0;t<16;t++)o[t]=a.getUint32(e+t*4,!1);for(let e=16;e<64;e++){let t=o[e-15],n=o[e-2],r=(t>>>7|t<<25)^(t>>>18|t<<14)^t>>>3,i=(n>>>17|n<<15)^(n>>>19|n<<13)^n>>>10;o[e]=o[e-16]+r+o[e-7]+i>>>0}let[r,i,s,c,l,u,d,f]=n;for(let e=0;e<64;e++){let n=(l>>>6|l<<26)^(l>>>11|l<<21)^(l>>>25|l<<7),a=l&u^~l&d,p=f+n+a+t[e]+o[e]>>>0,m=((r>>>2|r<<30)^(r>>>13|r<<19)^(r>>>22|r<<10))+(r&i^r&s^i&s)>>>0;f=d,d=u,u=l,l=c+p>>>0,c=s,s=i,i=r,r=p+m>>>0}n[0]=n[0]+r>>>0,n[1]=n[1]+i>>>0,n[2]=n[2]+s>>>0,n[3]=n[3]+c>>>0,n[4]=n[4]+l>>>0,n[5]=n[5]+u>>>0,n[6]=n[6]+d>>>0,n[7]=n[7]+f>>>0}let s=new Uint8Array(32);new DataView(s.buffer).setUint32(0,n[0],!1);for(let e=0;e<8;e++)new DataView(s.buffer).setUint32(e*4,n[e],!1);return s}var i=`0123456789abcdef`;function a(e){let t=``;for(let n of e)t+=i[n>>4]+i[n&15];return t}function o(e){return a(r(n(e)))}function s(e,t,n){let r=o(`${t}:${e}`);for(let e=1;e<n;e++)r=o(r);return r}function c(e,t){return o(`${e}:${t}`)}var l=class{token;handlers={status:new Set,welcome:new Set,screenMeta:new Set,screenRegion:new Set,videoTrack:new Set,overviewTrack:new Set,transport:new Set,notification:new Set,presence:new Set,pinRequired:new Set,watchers:new Set,timers:new Set,monitors:new Set,monitorAlways:new Set,monitorSessions:new Set,netStats:new Set,versionMismatch:new Set,error:new Set};sender=()=>{};challenge=null;wrongPin=!1;rememberedPin=null;constructor(e){this.token=e}on(e,t){this.handlers[e].add(t)}emit(e,t){for(let n of this.handlers[e])n(t)}setSender(e){this.sender=e}send(e){try{this.sender(JSON.stringify(e))}catch{}}sendHello(){this.send({type:`hello`,protocol:1,token:this.token,role:`controller`,client:{userAgent:navigator.userAgent}})}submitPin(e){if(this.rememberedPin=e,!this.challenge)return;let{salt:t,iterations:n,nonce:r}=this.challenge,i=s(e,t,n);this.send({type:`auth`,response:c(i,r)})}setVisible(e){this.send({type:`visibility`,visible:e})}handleText(t){let n;try{n=JSON.parse(t)}catch{return}if(!e(n))return;let r=n;switch(r.type){case`welcome`:this.challenge=null,r.protocol!==1&&this.emit(`versionMismatch`,{agentProtocol:r.protocol,clientProtocol:1,agentVersion:r.agent?.version}),this.emit(`welcome`,r),this.emit(`timers`,r.timers??[]),this.emit(`monitors`,r.monitors??[]),this.emit(`monitorAlways`,r.alwaysAgents??[]);break;case`auth-required`:this.challenge={salt:r.salt,iterations:r.iterations,nonce:r.nonce},this.rememberedPin&&!this.wrongPin?this.submitPin(this.rememberedPin):this.emit(`pinRequired`,{attemptsLeft:r.attemptsLeft,retry:this.wrongPin}),this.wrongPin=!1;break;case`screen-meta`:this.emit(`screenMeta`,{screen:r.screen,activeDisplay:r.activeDisplay});break;case`screen-region`:this.emit(`screenRegion`,{x:r.x,y:r.y,w:r.w,h:r.h,active:r.active});break;case`notification`:this.emit(`notification`,r);break;case`presence`:this.emit(`presence`,r.watchers);break;case`watchers`:this.emit(`watchers`,r.regions);break;case`timers`:this.emit(`timers`,r.timers);break;case`monitors`:this.emit(`monitors`,r.monitors);break;case`monitor-always-agents`:this.emit(`monitorAlways`,r.agents);break;case`monitor-sessions`:this.emit(`monitorSessions`,r.sessions);break;case`error`:r.code===`pin`?(this.wrongPin=!0,this.rememberedPin=null):this.emit(`error`,r.message);break;case`pong`:break}}},u=class{url;core;ws=null;pc=null;dc=null;mainXcv=null;overviewXcv=null;reconnectTimer=0;statsTimer=0;closedByUser=!1;constructor(e,t){this.url=e,this.core=new l(t),this.core.setSender(e=>{if(this.dc&&this.dc.readyState===`open`)try{this.dc.send(e)}catch{}})}on(e,t){this.core.on(e,t)}send(e){this.core.send(e)}submitPin(e){this.core.submitPin(e)}setVisible(e){this.core.setVisible(e)}connect(){this.closedByUser=!1,this.open()}close(){this.closedByUser=!0,window.clearTimeout(this.reconnectTimer),this.teardown()}isHealthy(){return!this.closedByUser&&this.dc?.readyState===`open`&&this.pc?.connectionState===`connected`}wake(){this.closedByUser||this.isHealthy()||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.open())}teardown(){if(window.clearInterval(this.statsTimer),this.statsTimer=0,this.core.emit(`videoTrack`,null),this.core.emit(`overviewTrack`,null),this.dc){this.dc.onopen=this.dc.onclose=this.dc.onmessage=null;try{this.dc.close()}catch{}this.dc=null}if(this.pc){this.pc.onconnectionstatechange=null,this.pc.ontrack=null,this.pc.onicecandidate=null;try{this.pc.close()}catch{}this.pc=null}if(this.ws){this.ws.onopen=this.ws.onclose=this.ws.onmessage=this.ws.onerror=null;try{this.ws.close()}catch{}this.ws=null}this.mainXcv=null,this.overviewXcv=null}scheduleReconnect(){this.closedByUser||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=window.setTimeout(()=>this.open(),1500))}open(){this.core.emit(`status`,`connecting`),this.teardown();let e=new WebSocket(this.url);this.ws=e;let t=t=>{if(e.readyState===WebSocket.OPEN)try{e.send(JSON.stringify(t))}catch{}},n=new RTCPeerConnection({iceServers:[]});this.pc=n;let r=n.createDataChannel(`whipdesk`);r.binaryType=`arraybuffer`,this.dc=r,r.onopen=()=>{this.core.sendHello(),this.core.emit(`status`,`connected`),this.core.emit(`transport`,`LAN`),this.startStatsPoll(n)},r.onclose=()=>this.core.emit(`status`,`disconnected`),r.onmessage=e=>{typeof e.data==`string`&&this.core.handleText(e.data)};try{this.mainXcv=n.addTransceiver(`video`,{direction:`recvonly`}),this.overviewXcv=n.addTransceiver(`video`,{direction:`recvonly`})}catch{}n.ontrack=e=>{let t=e.streams[0]??new MediaStream([e.track]);this.overviewXcv&&e.transceiver===this.overviewXcv?this.core.emit(`overviewTrack`,t):this.core.emit(`videoTrack`,t)},n.onicecandidate=e=>{e.candidate&&t({kind:`candidate`,candidate:e.candidate.toJSON()})},n.onconnectionstatechange=()=>{let e=n.connectionState;(e===`failed`||e===`disconnected`||e===`closed`)&&(this.core.emit(`status`,`disconnected`),this.scheduleReconnect())};let i=!1;e.onopen=async()=>{try{let e=await n.createOffer();await n.setLocalDescription(e),t({kind:`offer`,sdp:n.localDescription?.sdp??``})}catch{}},e.onmessage=e=>{let t;try{t=JSON.parse(typeof e.data==`string`?e.data:``)}catch{return}if(t.kind===`answer`&&t.sdp&&!i)i=!0,n.setRemoteDescription({type:`answer`,sdp:t.sdp});else if(t.kind===`candidate`&&t.candidate)try{n.addIceCandidate(t.candidate)}catch{}else t.kind===`error`&&this.core.emit(`error`,String(t.message??`host error`))},e.onclose=()=>{this.core.emit(`status`,`disconnected`),this.scheduleReconnect()},e.onerror=()=>{}}startStatsPoll(e){window.clearInterval(this.statsTimer),this.statsTimer=window.setInterval(async()=>{let t=null,n=0;try{(await e.getStats()).forEach(e=>{e.type===`inbound-rtp`&&e.kind===`video`?n=Math.max(n,Number(e.framesPerSecond??0)):e.type===`candidate-pair`&&e.nominated&&typeof e.currentRoundTripTime==`number`&&(t=e.currentRoundTripTime)})}catch{return}this.core.emit(`netStats`,{fps:Math.round(n),rtt:t==null?null:Math.round(t*1e3)})},1e3)}},d=``+new URL(`whip-CTqIatiK.png`,import.meta.url).href,f=[`Rounding up your AI agents…`,`Connecting to your agent farm…`,`Uncoiling the whip…`,`Negotiating the fastest route…`,`Tightening the leash…`,`Almost in the saddle…`],p=class{overlay;msg;rotateTimer=0;msgIndex=0;constructor(e){this.overlay=document.createElement(`div`),this.overlay.className=`wd-connecting hidden`;let t=document.createElement(`div`);t.className=`wd-connecting-card`;let n=document.createElement(`img`);n.className=`wd-connecting-whip`,n.src=d,n.alt=`WhipDesk`,n.decoding=`async`;let r=document.createElement(`div`);r.className=`wd-connecting-spinner`,this.msg=document.createElement(`p`),this.msg.className=`wd-connecting-msg`,this.msg.textContent=f[0],t.append(n,r,this.msg),this.overlay.appendChild(t),e.appendChild(this.overlay)}show(e){this.msg.textContent=e??f[this.msgIndex],this.overlay.classList.contains(`hidden`)&&(this.overlay.classList.remove(`hidden`),!e&&(this.rotateTimer=window.setInterval(()=>{this.msgIndex=(this.msgIndex+1)%f.length,this.msg.textContent=f[this.msgIndex]},2200)))}hide(){this.overlay.classList.add(`hidden`),window.clearInterval(this.rotateTimer),this.rotateTimer=0}},m={eye:`<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>`,mouse:`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 7v4"/>`,keyboard:`<rect x="2" y="6" width="20" height="12" rx="2"/><path d="M6 10h.01M10 10h.01M14 10h.01M18 10h.01M7 14h10"/>`,monitor:`<rect x="3" y="4" width="18" height="12" rx="2"/><path d="M8 20h8M12 16v4"/>`,hand:`<path d="M7 11V6a1.5 1.5 0 0 1 3 0v4m0-1V4.5a1.5 1.5 0 0 1 3 0V10m0-1.5a1.5 1.5 0 0 1 3 0V12c0 4-2.5 8-6.5 8S6 17 5 15l-1.5-3a1.4 1.4 0 0 1 2.3-1.5L7 12"/>`,bell:`<path d="M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6Z"/><path d="M10 19a2 2 0 0 0 4 0"/>`,plus:`<path d="M12 5v14M5 12h14"/>`,minus:`<path d="M5 12h14"/>`,"chevron-down":`<path d="m6 9 6 6 6-6"/>`,"chevron-up":`<path d="m6 15 6-6 6 6"/>`,pointer:`<path d="m4 4 6 16 2.5-6.5L19 11Z"/>`,"scroll-up":`<path d="m6 14 6-6 6 6"/><path d="M12 8v11"/>`,"scroll-down":`<path d="m6 10 6 6 6-6"/><path d="M12 16V5"/>`,"mouse-left":`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 3v8H6V8"/>`,"mouse-right":`<rect x="6" y="3" width="12" height="18" rx="6"/><path d="M12 3v8h6V8"/>`,"double-click":`<path d="m4 4 6 16 2.5-6.5L19 11Z"/><path d="M18 4v3M21 6h-3"/>`,drag:`<path d="M12 2v20M2 12h20" /><path d="m8 6 4-4 4 4M8 18l4 4 4-4M6 8l-4 4 4 4M18 8l4 4-4 4"/>`,send:`<path d="M22 2 11 13M22 2l-7 20-4-9-9-4Z"/>`,insert:`<path d="M12 5v14M5 12h7"/><rect x="16" y="4" width="4" height="16" rx="1"/>`,lock:`<rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/>`,clock:`<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>`,power:`<path d="M12 3v9"/><path d="M6.4 7.4a8 8 0 1 0 11.2 0"/>`,activity:`<path d="M3 12h4l2-7 4 14 2-7h6"/>`,x:`<path d="M6 6 18 18M18 6 6 18"/>`,heart:`<path d="M12 20s-7-4.4-9.3-8.5a4.5 4.5 0 0 1 8.1-3.9l1.2 1.6 1.2-1.6a4.5 4.5 0 0 1 8.1 3.9C19 15.6 12 20 12 20Z"/>`,trash:`<path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/>`,github:`<path fill="currentColor" stroke="none" d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.74.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.13-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 0 1 6 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.25 2.88.12 3.18.77.84 1.23 1.91 1.23 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58A12 12 0 0 0 24 12.5C24 5.87 18.63.5 12 .5Z"/>`,reddit:`<path fill="currentColor" stroke="none" d="M24 11.78a2.6 2.6 0 0 0-4.4-1.86 12.74 12.74 0 0 0-6.86-2.16l1.17-3.68 3.16.74a1.83 1.83 0 1 0 .2-1.18l-3.6-.85a.6.6 0 0 0-.72.43l-1.3 4.08a12.8 12.8 0 0 0-7 2.18 2.6 2.6 0 1 0-2.86 4.28 5.1 5.1 0 0 0-.06.79c0 4 4.66 7.24 10.42 7.24S20.42 17.83 20.42 12.95c0-.26-.02-.52-.06-.78A2.6 2.6 0 0 0 24 11.78ZM6.13 13.5a1.83 1.83 0 1 1 3.66 0 1.83 1.83 0 0 1-3.66 0Zm10.2 4.83c-1.25 1.25-3.64 1.34-4.33 1.34-.7 0-3.09-.09-4.33-1.34a.47.47 0 0 1 .67-.67c.79.79 2.47.99 3.66.99 1.2 0 2.88-.2 3.67-.99a.47.47 0 1 1 .66.67h.03Zm-.27-3a1.83 1.83 0 1 1 0-3.66 1.83 1.83 0 0 1 0 3.66Z"/>`},h=`http://www.w3.org/2000/svg`;function g(e,t=20){let n=document.createElementNS(h,`svg`);return n.setAttribute(`viewBox`,`0 0 24 24`),n.setAttribute(`width`,String(t)),n.setAttribute(`height`,String(t)),n.setAttribute(`fill`,`none`),n.setAttribute(`stroke`,`currentColor`),n.setAttribute(`stroke-width`,`2`),n.setAttribute(`stroke-linecap`,`round`),n.setAttribute(`stroke-linejoin`,`round`),n.setAttribute(`aria-hidden`,`true`),n.innerHTML=m[e],n}var _=[`en`],v=`en`;function y(){try{let e=localStorage.getItem(`wd-locale`);if(e&&_.includes(e))return e}catch{}let e=navigator.languages?.length?navigator.languages:[navigator.language];for(let t of e){let e=(t||``).toLowerCase().split(`-`)[0]??``;if(_.includes(e))return e}return v}function b(){return location.hostname.endsWith(`whipdesk.com`)?``:`https://whipdesk.com`}function x(){return`${b()}/${y()}/dashboard/`}function S(e){let t=e?`?next=${encodeURIComponent(e)}`:``;return`${b()}/${y()}/sign-in/${t}`}var C=`https://donate.stripe.com/6oU5kE19N5v35652n88so01`,w=`https://github.com/BinaryBananaLLC/WhipDesk/`,ee=`https://www.reddit.com/r/WhipDesk/`,te=[[`Esc`,`Escape`],[`Tab`,`Tab`],[`⌫`,`Backspace`],[`⏎`,`Enter`],[`←`,`ArrowLeft`],[`↑`,`ArrowUp`],[`↓`,`ArrowDown`],[`→`,`ArrowRight`]];function T(e,t,n){let r=document.createElement(e);return t&&(r.className=t),n!==void 0&&(r.textContent=n),r}function E(e,...t){let n=T(`div`,`wd-group`);n.appendChild(T(`span`,`wd-group-label`,e));let r=T(`div`,`wd-group-items`);return r.append(...t),n.appendChild(r),n}function D(e,t){let n=0,r=0,i=()=>{window.clearTimeout(n),window.clearInterval(r)};e.addEventListener(`pointerdown`,e=>{e.preventDefault(),t(),n=window.setTimeout(()=>{r=window.setInterval(t,80)},350)});for(let t of[`pointerup`,`pointercancel`,`pointerleave`])e.addEventListener(t,i);return e}function O(e,t=`wd-btn`){return T(`button`,t,e)}function k(e,t=``,n=`wd-btn`){let r=T(`button`,n);if(r.appendChild(g(e)),t){let e=T(`span`,`wd-btn-label`,t);r.appendChild(e)}else r.classList.add(`wd-icon-only`),r.setAttribute(`aria-label`,e);return r}function A(e,t,n){let r=T(`a`,`wd-conn-feedback-link`);return r.href=n,r.target=`_blank`,r.rel=`noopener noreferrer`,r.append(g(e,16),T(`span`,void 0,t)),r}function ne(e){switch(e.toUpperCase()){case`LAN`:return`Direct on your local network — fastest, no relay.`;case`STUN`:return`Direct peer-to-peer across networks.`;case`TURN`:return`Your network blocked a direct P2P connection, so we're bouncing your stream through our encrypted relay. Yes, this costs us actual money to run. Consider supporting us.`;default:return``}}function j(e){return e.toUpperCase()===`TURN`?`$ ${e}`:e}var re=class{root;deps;statusDot=T(`span`,`wd-dot`);statusText=T(`span`,`wd-status-text`,`Connecting…`);transportBadge=T(`span`,`wd-transport hidden`);watchersText=T(`span`,`wd-watchers`,``);alertBadge=T(`span`,`wd-badge hidden`);statusbar;statusCollapseTimer=0;connectionOverlay;connName;connRoute;connPresence;connSpeed;connStatusDot;connStatusText;connError;lastError=``;netFps=0;netRtt=null;panel;optionsArea;tabButtons=new Map;tabPanes=new Map;collapseBtn;interactHost;monitorList;promptInput;activeTab=null;interactMode=`mouse`;collapsed=!1;deviceName=``;transport=``;presenceCount=1;status=`connecting`;displays=[];activeDisplay=0;constructor(e,t){this.root=e,this.deps=t,this.build()}setStatus(e){this.status=e,this.statusDot.dataset.status=e,e===`connected`&&(this.lastError=``),this.renderStatusText(),this.updateStatusCollapse(),this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection()}setAlertCount(e){this.alertBadge.textContent=e>0?String(e):``,this.alertBadge.classList.toggle(`hidden`,e<=0)}setTransport(e){this.transport=e,this.transportBadge.textContent=j(e),this.transportBadge.dataset.kind=e.toLowerCase(),this.transportBadge.classList.toggle(`hidden`,!e),this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection(),this.peekStatus()}updateStatusCollapse(){window.clearTimeout(this.statusCollapseTimer),this.status===`connected`?this.scheduleStatusCollapse():this.statusbar?.classList.remove(`collapsed`)}scheduleStatusCollapse(){window.clearTimeout(this.statusCollapseTimer),this.statusCollapseTimer=window.setTimeout(()=>{this.status===`connected`&&this.statusbar?.classList.add(`collapsed`)},4e3)}peekStatus(){this.statusbar?.classList.remove(`collapsed`),this.scheduleStatusCollapse()}renderStatusText(){this.statusText.textContent=this.status===`connected`?this.deviceName?`Connected to ${this.deviceName}`:`Connected`:this.status===`connecting`?`Connecting…`:`Disconnected`}setWelcome(e){this.deviceName=e.agent.hostname,this.renderStatusText(),this.displays=e.displays??[],this.activeDisplay=e.activeDisplay??0,this.renderMonitors(),e.capabilities.mouse||this.deps.notifications.show({type:`notification`,id:`cap-${Date.now()}`,title:`View-only`,body:`Host mouse/keyboard unavailable — grant Accessibility on the host.`,level:`warning`,source:`client`,t:Date.now()})}setActiveDisplay(e){this.activeDisplay=e,this.renderMonitors()}setPresence(e){this.presenceCount=e,this.watchersText.textContent=e>1?`● ${e} watching`:``,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection()}buildConnectionDialog(){let e=T(`div`,`wd-dialog-overlay hidden`);e.addEventListener(`pointerdown`,t=>{t.target===e&&e.classList.add(`hidden`)});let t=T(`div`,`wd-dialog`),n=T(`div`,`wd-dialog-head`);n.append(T(`h2`,``,`Connection`));let r=T(`button`,`wd-dialog-x`);r.appendChild(g(`x`)),r.onclick=()=>e.classList.add(`hidden`),n.appendChild(r);let i=T(`div`,`wd-conn-row`);i.append(T(`span`,`wd-conn-label`,`Status`));let a=T(`div`,`wd-conn-status`);this.connStatusDot=T(`span`,`wd-dot`),this.connStatusText=T(`span`,`wd-conn-value`,`—`),a.append(this.connStatusDot,this.connStatusText),i.appendChild(a);let o=T(`div`,`wd-conn-row`);o.append(T(`span`,`wd-conn-label`,`Machine`)),this.connName=T(`span`,`wd-conn-value`,`—`),o.appendChild(this.connName);let s=T(`div`,`wd-conn-row`);s.append(T(`span`,`wd-conn-label`,`Connection`)),this.connRoute=T(`div`,`wd-conn-route`),s.appendChild(this.connRoute);let c=T(`div`,`wd-conn-row`);c.append(T(`span`,`wd-conn-label`,`Viewers`)),this.connPresence=T(`span`,`wd-conn-value`,`1`),c.appendChild(this.connPresence);let l=T(`div`,`wd-conn-row`);l.append(T(`span`,`wd-conn-label`,`Speed (FPS/latency)`)),this.connSpeed=T(`span`,`wd-conn-value`,`—`),l.appendChild(this.connSpeed),this.connError=T(`div`,`wd-conn-error hidden`);let u=T(`button`,`wd-btn wd-disconnect`);u.append(g(`power`),T(`span`,`wd-btn-label`,`Disconnect`)),u.onclick=()=>this.disconnect();let d=T(`div`,`wd-conn-feedback`);d.append(T(`p`,`wd-conn-feedback-text`,`Noticed an issue or have an idea? Reach out:`));let f=T(`div`,`wd-conn-feedback-links`);f.append(A(`reddit`,`Reddit`,ee),A(`github`,`GitHub`,w)),d.appendChild(f),t.append(n,i,s,l,o,c,this.connError,u,d),e.appendChild(t),this.root.appendChild(e),this.connectionOverlay=e}renderConnection(){if(this.connStatusDot.dataset.status=this.status,this.connStatusText.textContent=this.status===`connected`?`Connected`:this.status===`connecting`?`Connecting…`:`Disconnected`,this.lastError?(this.connError.textContent=this.lastError,this.connError.classList.remove(`hidden`)):this.connError.classList.add(`hidden`),this.connName.textContent=this.deviceName||`Connected device`,this.connPresence.textContent=String(Math.max(1,this.presenceCount)),this.connRoute.replaceChildren(),this.transport){let e=T(`span`,`wd-transport`);if(e.textContent=j(this.transport),e.dataset.kind=this.transport.toLowerCase(),this.connRoute.append(e,T(`span`,`wd-conn-desc`,ne(this.transport))),this.transport.toLowerCase()===`turn`){let e=T(`button`,`wd-support-link`);e.append(g(`heart`,14),T(`span`,void 0,`Support WhipDesk`)),e.onclick=()=>window.open(C,`_blank`,`noopener`),this.connRoute.append(e)}}else this.connRoute.append(T(`span`,`wd-conn-desc`,this.status===`connected`?`Detecting route…`:`Connecting…`));this.renderSpeed()}renderSpeed(){let e=`${this.netFps} FPS`;this.connSpeed.textContent=this.netRtt==null?e:`${e} / ${this.netRtt} ms`}setNetStats(e,t){this.netFps=e,this.netRtt=t,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderSpeed()}openConnection(){this.renderConnection(),this.connectionOverlay.classList.remove(`hidden`)}disconnect(){this.deps.conn.close(),window.location.href=x()}flashError(e){this.lastError=e,this.connectionOverlay&&!this.connectionOverlay.classList.contains(`hidden`)&&this.renderConnection(),this.deps.notifications.show({type:`notification`,id:`err-${Date.now()}`,title:`WhipDesk`,body:e,level:`warning`,source:`client`,t:Date.now()})}build(){let e=T(`div`,`wd-statusbar`);e.append(this.statusDot,this.statusText,this.transportBadge,this.watchersText),e.onclick=()=>this.openConnection(),this.statusbar=e,this.buildConnectionDialog();let t=k(`bell`,``,`wd-bell`);t.setAttribute(`aria-label`,`Auto-Whips`),t.title=`Auto-Whips`,t.appendChild(this.alertBadge),t.onclick=()=>this.deps.watchers.open(),this.root.append(e,t);let n=T(`div`,`wd-ribbon`);this.panel=T(`div`,`wd-panel`),this.optionsArea=T(`div`,`wd-options`),this.tabPanes.set(`viewer`,this.buildViewerPane()),this.tabPanes.set(`interact`,this.buildInteractPane()),this.tabPanes.set(`type`,this.buildTypePane()),this.tabPanes.set(`monitor`,this.buildMonitorPane());for(let e of this.tabPanes.values())this.optionsArea.appendChild(e);let r=T(`div`,`wd-tabs`),i=(e,t,n)=>{let i=T(`button`,`wd-tab`);i.appendChild(g(t,18)),i.appendChild(T(`span`,`wd-tab-label`,n)),i.onclick=()=>this.selectTab(e),this.tabButtons.set(e,i),r.appendChild(i)};i(`viewer`,`eye`,`Viewer`),i(`interact`,`mouse`,`Interact`),i(`type`,`keyboard`,`Type`),i(`monitor`,`monitor`,`Monitor`),this.collapseBtn=k(`chevron-down`,``,`wd-collapse`),this.collapseBtn.onclick=()=>this.setCollapsed(!this.collapsed),r.appendChild(this.collapseBtn),this.panel.append(this.optionsArea,r),n.appendChild(this.panel),this.root.appendChild(n),this.selectTab(`viewer`)}buildViewerPane(){let{view:e,input:t}=this.deps,n=T(`div`,`wd-pane wd-pane-single-row`),r=D(k(`minus`,``,`wd-btn wd-icon-only`),()=>e.zoomBy(.9)),i=D(k(`plus`,``,`wd-btn wd-icon-only`),()=>e.zoomBy(1.11)),a=D(k(`scroll-up`,``,`wd-btn wd-icon-only`),()=>t.scrollStep(-6)),o=D(k(`scroll-down`,``,`wd-btn wd-icon-only`),()=>t.scrollStep(6)),s=k(`hand`,``,`wd-btn wd-icon-only`);s.setAttribute(`aria-label`,`Drag to scroll`),s.title=`Drag to scroll`;let c=k(`drag`,``,`wd-btn wd-icon-only`);c.setAttribute(`aria-label`,`Pan the zoomed screen with one finger`),c.title=`Pan the zoomed screen with one finger`,s.onclick=()=>{let e=!t.getDragScroll();t.setDragScroll(e),s.classList.toggle(`on`,e),c.classList.toggle(`on`,t.getPan())},c.onclick=()=>{let e=!t.getPan();t.setPan(e),c.classList.toggle(`on`,e),s.classList.toggle(`on`,t.getDragScroll())};let l=k(`pointer`,`Click`,`wd-btn wd-go`);return l.onclick=()=>t.click(`left`),n.append(E(`Zoom`,r,i),E(`Pan`,c),E(`Scroll`,a,o,s),E(`Pointer`,l)),n}buildInteractPane(){let e=T(`div`,`wd-pane`);return this.interactHost=T(`div`,`wd-pane`),e.appendChild(this.interactHost),this.renderInteract(),e}renderInteract(){let{input:e}=this.deps;this.interactHost.replaceChildren();let t=T(`div`,`wd-group`),n=T(`div`,`wd-group-head`),r=T(`span`,`wd-group-label`,`Mode`),i=T(`div`,`wd-mode-toggle`),a=T(`button`,`wd-mode-btn`,`Mouse`),o=T(`button`,`wd-mode-btn`,`Touch`);a.classList.toggle(`on`,this.interactMode===`mouse`),o.classList.toggle(`on`,this.interactMode===`touch`),a.onclick=()=>{this.interactMode=`mouse`,this.activeTab===`interact`&&this.deps.input.setInteraction(`mouse`),this.renderInteract()},o.onclick=()=>{this.interactMode=`touch`,this.activeTab===`interact`&&this.deps.input.setInteraction(`touch`),this.renderInteract()},i.append(a,o),n.append(r,i);let s=T(`div`,`wd-group-items`);if(this.interactMode===`mouse`){let t=k(`mouse-left`,`Left`);t.onclick=()=>e.click(`left`);let n=k(`mouse-right`,`Right`);n.onclick=()=>e.click(`right`);let r=k(`double-click`,`Double`);r.onclick=()=>e.multiClick(2);let i=k(`drag`,`Drag`);i.onclick=()=>{let t=!e.getDragLock();e.setDragLock(t),i.classList.toggle(`on`,t)},s.append(t,n,r,i)}else{let t=k(`pointer`,`Tap`);t.onclick=()=>e.click(`left`);let n=k(`hand`,`Hold`);n.onclick=()=>e.longPress();let r=D(k(`scroll-up`,`Up`,`wd-btn`),()=>e.swipe(0,-.25)),i=D(k(`scroll-down`,`Down`,`wd-btn`),()=>e.swipe(0,.25)),a=O(`←`);a.onclick=()=>e.swipe(-.25,0);let o=O(`→`);o.onclick=()=>e.swipe(.25,0);let c=O(`2 fingers`);c.onclick=()=>e.click(`right`),s.append(t,n,r,i,a,o,c)}t.append(n,s),this.interactHost.append(t)}buildTypePane(){let{conn:e,input:t}=this.deps,n=T(`div`,`wd-pane wd-pane-col`);this.promptInput=T(`textarea`,`wd-type-input`),this.promptInput.placeholder=`Type to send to the focused app (URL, command, message…)`,this.promptInput.rows=2;let r=T(`div`,`wd-wrap`);for(let[t,n]of te){let i=O(t);i.onclick=()=>e.send({type:`key`,key:n}),r.appendChild(i)}let i=O(`Double-click`);i.onclick=()=>t.multiClick(2);let a=O(`Triple-click`);a.onclick=()=>t.multiClick(3);let o=k(`insert`,`Insert`);o.onclick=()=>this.sendText(!1);let s=k(`send`,`Send`,`wd-btn wd-go`);return s.onclick=()=>this.sendText(!0),r.append(i,a,o,s),n.append(this.promptInput,r),n}buildMonitorPane(){let e=T(`div`,`wd-pane`);return this.monitorList=T(`div`,`wd-monitor-list`),e.appendChild(this.monitorList),this.renderMonitors(),e}renderMonitors(){if(this.monitorList){if(this.monitorList.replaceChildren(),this.displays.length===0){this.monitorList.appendChild(T(`span`,`wd-hint`,`Single display`));return}this.displays.forEach((e,t)=>{let n=O(`${t+1}. ${e.name}${e.primary?` ★`:``}`);n.classList.toggle(`on`,e.id===this.activeDisplay),n.onclick=()=>{this.deps.conn.send({type:`select-display`,id:e.id}),this.activeDisplay=e.id,this.renderMonitors()},this.monitorList.appendChild(n)})}}selectTab(e){if(this.activeTab!==null&&e===this.activeTab){this.setCollapsed(!this.collapsed);return}this.activeTab=e,this.collapsed&&this.setCollapsed(!1);for(let[t,n]of this.tabButtons)n.classList.toggle(`on`,t===e);for(let[t,n]of this.tabPanes)n.classList.toggle(`hidden`,t!==e);this.deps.input.setInteraction(e===`interact`?this.interactMode:`viewer`),e===`type`&&window.setTimeout(()=>this.promptInput.focus(),50)}setCollapsed(e){this.collapsed=e,this.optionsArea.classList.toggle(`hidden`,e),this.collapseBtn.replaceChildren(g(e?`chevron-up`:`chevron-down`))}sendText(e){let t=this.promptInput.value;t&&(this.deps.conn.send({type:`type`,text:t,submit:e}),this.promptInput.value=``)}};function M(e){return e<0?0:e>1?1:e}var N=2.5,ie=250,ae=500,oe=8,se=class{canvas;view;conn;cb;interaction=`viewer`;dragLock=!1;dragScroll=!1;pan=!1;holdingLeft=!1;cursor={nx:.5,ny:.5};pointers=new Map;longPressTimer=0;twoFinger=null;suppressTap=!1;constructor(e,t,n,r={}){this.canvas=e,this.view=t,this.conn=n,this.cb=r,e.style.touchAction=`none`,e.addEventListener(`pointerdown`,e=>this.onDown(e)),e.addEventListener(`pointermove`,e=>this.onMove(e)),e.addEventListener(`pointerup`,e=>this.onUp(e)),e.addEventListener(`pointercancel`,e=>this.onUp(e)),e.addEventListener(`contextmenu`,e=>e.preventDefault()),this.view.setCursor(this.cursor.nx,this.cursor.ny)}setInteraction(e){this.interaction=e,e===`viewer`&&(this.dragLock=!1)}getInteraction(){return this.interaction}setCallbacks(e){this.cb=e}setDragLock(e){this.dragLock=e}getDragLock(){return this.dragLock}setDragScroll(e){this.dragScroll=e,e&&(this.pan=!1)}getDragScroll(){return this.dragScroll}setPan(e){this.pan=e,e&&(this.dragScroll=!1)}getPan(){return this.pan}isPanning(){return this.pan&&this.interaction===`viewer`}click(e,t=!1){this.send({type:`pointer`,action:`click`,button:e,double:t,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(15)}multiClick(e){for(let t=0;t<e;t++)this.send({type:`pointer`,action:`click`,button:`left`,x:this.cursor.nx,y:this.cursor.ny});navigator.vibrate?.(15)}longPress(e=650){this.send({type:`pointer`,action:`down`,button:`left`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(25),window.setTimeout(()=>this.send({type:`pointer`,action:`up`,button:`left`}),e)}swipe(e,t){let n=M(this.cursor.nx),r=M(this.cursor.ny),i=M(this.cursor.nx+e),a=M(this.cursor.ny+t);this.send({type:`pointer`,action:`down`,button:`left`,x:n,y:r});for(let e=1;e<=6;e++){let t=e/6;window.setTimeout(()=>{this.send({type:`pointer`,action:`move`,x:n+(i-n)*t,y:r+(a-r)*t}),e===6&&(this.moveCursor(i,a),this.send({type:`pointer`,action:`up`,button:`left`}))},e*16)}navigator.vibrate?.(15)}scrollStep(e){this.send({type:`scroll`,dx:0,dy:e})}send(e){this.conn.send(e)}positionOf(e){let t=this.canvas.getBoundingClientRect();return{x:e.clientX-t.left,y:e.clientY-t.top}}moveCursor(e,t){this.cursor.nx=M(e),this.cursor.ny=M(t),this.view.setCursor(this.cursor.nx,this.cursor.ny),this.cb.onCursor?.(this.cursor.nx,this.cursor.ny)}onDown(e){this.canvas.setPointerCapture?.(e.pointerId);let t=this.positionOf(e);if(this.pointers.set(e.pointerId,{x:t.x,y:t.y,startX:t.x,startY:t.y,startT:performance.now(),moved:!1,consumed:!1}),this.pointers.size===2){window.clearTimeout(this.longPressTimer),this.beginTwoFinger();return}if(this.pointers.size===1&&(this.view.beginViewGesture(),window.clearTimeout(this.longPressTimer),this.interaction===`mouse`&&!this.dragScroll&&(this.longPressTimer=window.setTimeout(()=>this.onLongPress(),ae)),!this.dragScroll&&!this.isPanning()&&(this.interaction===`mouse`||this.interaction===`viewer`))){let e=this.view.canvasToNorm(t.x,t.y);this.moveCursor(e.nx,e.ny)}}onLongPress(){let e=[...this.pointers.values()][0];!e||e.moved||e.consumed||(e.consumed=!0,this.send({type:`pointer`,action:`click`,button:`right`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(20))}beginTwoFinger(){let e=[...this.pointers.values()];if(e.length<2)return;let t=e[0],n=e[1];this.twoFinger={dist:Math.hypot(t.x-n.x,t.y-n.y),mx:(t.x+n.x)/2,my:(t.y+n.y)/2,start:performance.now(),moved:!1}}onMove(e){let t=this.pointers.get(e.pointerId);if(!t)return;let n=this.positionOf(e),r=t.x,i=t.y;if(t.x=n.x,t.y=n.y,Math.hypot(n.x-t.startX,n.y-t.startY)>oe&&(t.moved=!0),this.pointers.size>=2&&this.twoFinger){this.onTwoFingerMove();return}if(this.pointers.size!==1||t.consumed||!t.moved)return;window.clearTimeout(this.longPressTimer);let a=n.x-r,o=n.y-i;if(this.isPanning()){this.view.panByCanvasPixels(a,o);return}if(this.interaction===`touch`||this.dragScroll){this.send({type:`scroll`,dx:Math.round(-a/N),dy:Math.round(-o/N)});return}let s=this.view.canvasToNorm(n.x,n.y);this.moveCursor(s.nx,s.ny),this.interaction===`mouse`&&this.dragLock&&!this.holdingLeft&&(this.holdingLeft=!0,this.send({type:`pointer`,action:`down`,button:`left`,x:this.cursor.nx,y:this.cursor.ny})),this.send({type:`pointer`,action:`move`,x:this.cursor.nx,y:this.cursor.ny})}onTwoFingerMove(){let e=[...this.pointers.values()];if(e.length<2||!this.twoFinger)return;let t=e[0],n=e[1],r=Math.hypot(t.x-n.x,t.y-n.y),i=(t.x+n.x)/2,a=(t.y+n.y)/2,o=r-this.twoFinger.dist,s=i-this.twoFinger.mx,c=a-this.twoFinger.my;if(Math.abs(o)>6){this.twoFinger.moved=!0,this.view.zoomAround(1+o/200,i,a),this.twoFinger.dist=r;return}(Math.abs(c)>2||Math.abs(s)>2)&&(this.twoFinger.moved=!0,this.view.getZoom()>1?this.view.panByCanvasPixels(s,c):this.send({type:`scroll`,dx:Math.round(-s/N),dy:Math.round(-c/N)}),this.twoFinger.mx=i,this.twoFinger.my=a)}onUp(e){let t=this.pointers.size,n=this.pointers.get(e.pointerId);if(this.pointers.delete(e.pointerId),window.clearTimeout(this.longPressTimer),this.pointers.size===0&&this.view.endViewGesture(),t===2){let e=this.twoFinger;e&&!e.moved&&this.interaction===`mouse`&&performance.now()-e.start<ie&&(this.send({type:`pointer`,action:`click`,button:`right`,x:this.cursor.nx,y:this.cursor.ny}),navigator.vibrate?.(20)),this.twoFinger=null,this.suppressTap=!0;for(let e of this.pointers.values())e.consumed=!0;return}if(this.holdingLeft&&this.pointers.size===0&&(this.holdingLeft=!1,this.send({type:`pointer`,action:`up`,button:`left`})),this.pointers.size<2&&(this.twoFinger=null),!n||n.consumed)return;if(this.suppressTap){this.pointers.size===0&&(this.suppressTap=!1);return}let r=performance.now()-n.startT;if(!(!(!n.moved&&r<ie&&this.pointers.size===0)||this.dragScroll)&&(this.interaction===`mouse`||this.interaction===`touch`)){let e=this.view.canvasToNorm(n.startX,n.startY);this.moveCursor(e.nx,e.ny),this.send({type:`pointer`,action:`click`,button:`left`,x:e.nx,y:e.ny}),navigator.vibrate?.(12)}}},ce=class{container;constructor(e){this.container=e}async requestPermission(){try{`Notification`in window&&Notification.permission==="default"&&await Notification.requestPermission()}catch{}}get permission(){return`Notification`in window?Notification.permission:`unsupported`}flash(e,t,n=`info`){this.toast({type:`notification`,id:`flash-${Date.now()}`,title:e,body:t,level:n,source:`client`,t:Date.now()})}show(e){this.toast(e);try{`Notification`in window&&Notification.permission===`granted`&&new Notification(e.title,{body:e.body,tag:e.id})}catch{}navigator.vibrate?.(e.level===`error`?[60,40,60]:40)}toast(e){let t=document.createElement(`div`);t.className=`wd-toast wd-${e.level}`;let n=document.createElement(`strong`);if(n.textContent=e.title,t.appendChild(n),e.body){let n=document.createElement(`span`);n.textContent=e.body,t.appendChild(n)}this.container.appendChild(t),window.setTimeout(()=>{t.classList.add(`wd-hide`),window.setTimeout(()=>t.remove(),300)},5e3)}},le=class{overlay;input;message;onSubmit=null;constructor(e){this.overlay=document.createElement(`div`),this.overlay.className=`wd-pin-overlay hidden`;let t=document.createElement(`div`);t.className=`wd-pin-card`;let n=document.createElement(`img`);n.className=`wd-pin-whip`,n.src=d,n.alt=`WhipDesk`,n.decoding=`async`;let r=document.createElement(`h2`);r.textContent=`Enter device PIN`,this.message=document.createElement(`p`),this.message.className=`wd-pin-msg`,this.message.textContent=`You're connected. Enter the PIN to unlock this device and start whipping.`,this.input=document.createElement(`input`),this.input.className=`wd-pin-input`,this.input.type=`password`,this.input.inputMode=`numeric`,this.input.autocomplete=`one-time-code`,this.input.name=`wd-device-pin`,this.input.setAttribute(`data-1p-ignore`,`true`),this.input.setAttribute(`data-lpignore`,`true`),this.input.setAttribute(`aria-label`,`Device PIN`),this.input.addEventListener(`keydown`,e=>{e.key===`Enter`&&this.submit()});let i=document.createElement(`button`);i.className=`wd-pin-submit`,i.textContent=`Unlock`,i.onclick=()=>this.submit();let a=document.createElement(`button`);a.type=`button`,a.className=`wd-support-link wd-pin-support`,a.append(g(`heart`,14));let o=document.createElement(`span`);o.textContent=`Support WhipDesk`,a.append(o),a.onclick=()=>window.open(C,`_blank`,`noopener`);let s=document.createElement(`button`);s.className=`wd-pin-back`,s.textContent=`← Back to dashboard`,s.onclick=()=>{window.location.href=x()},t.append(n,r,this.message,this.input,i,a,s),this.overlay.appendChild(t),e.appendChild(this.overlay)}show(e,t){this.onSubmit=t,this.input.value=``,e.retry?(this.message.textContent=e.attemptsLeft>0?`Wrong PIN — ${e.attemptsLeft} attempt(s) left.`:`Wrong PIN. Try again.`,this.message.classList.add(`err`),navigator.vibrate?.([40,40,40])):(this.message.textContent=`You're connected. Enter the PIN to unlock this device and start whipping.`,this.message.classList.remove(`err`)),this.overlay.classList.remove(`hidden`),window.setTimeout(()=>this.input.focus(),50)}hide(){this.overlay.classList.add(`hidden`)}submit(){let e=this.input.value.trim();if(e.length<4){this.message.textContent=`PIN is at least 4 characters.`,this.message.classList.add(`err`);return}this.onSubmit?.(e),this.message.textContent=`Checking…`,this.message.classList.remove(`err`)}},ue=`modulepreload`,de=function(e,t){return new URL(e,t).href},P={},F=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,new URL(`../../../src/node/plugins/importAnalysisBuild.ts`,import.meta.url)).href}r=o(t.map(t=>{if(t=de(t,n),t=s(t),t in P)return;P[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:ue,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})};async function I(e,t){if(e.vapidKey&&!(!(`serviceWorker`in navigator)||!(`Notification`in window)))try{let{initializeApp:n,getApps:r}=await F(async()=>{let{initializeApp:e,getApps:t}=await import(`./index.esm-mXKu2C3o.js`);return{initializeApp:e,getApps:t}},__vite__mapDeps([0,1]),import.meta.url),{getAuth:i}=await F(async()=>{let{getAuth:e}=await import(`./index.esm-CmSagqpw.js`);return{getAuth:e}},__vite__mapDeps([2,1]),import.meta.url),{getFirestore:a,doc:o,setDoc:s,serverTimestamp:c}=await F(async()=>{let{getFirestore:e,doc:t,setDoc:n,serverTimestamp:r}=await import(`./index.esm-wogdVWd2.js`);return{getFirestore:e,doc:t,setDoc:n,serverTimestamp:r}},__vite__mapDeps([3,1]),import.meta.url),{getMessaging:l,getToken:u,onMessage:d,isSupported:f}=await F(async()=>{let{getMessaging:e,getToken:t,onMessage:n,isSupported:r}=await import(`./index.esm-BgjPHsdM.js`);return{getMessaging:e,getToken:t,onMessage:n,isSupported:r}},__vite__mapDeps([4,1]),import.meta.url);if(!await f().catch(()=>!1))return;let p=r()[0]??n(e),m=i(p).currentUser;if(!m||Notification.permission==="default"&&await Notification.requestPermission()!==`granted`||Notification.permission!==`granted`)return;let h=await navigator.serviceWorker.register(`./firebase-messaging-sw.js`),g=l(p),_=await u(g,{vapidKey:e.vapidKey,serviceWorkerRegistration:h});if(!_)return;await s(o(a(p),`users`,m.uid,`fcmTokens`,_),{token:_,ua:navigator.userAgent,updatedAt:c()},{merge:!0}),d(g,e=>{let n=e.notification;t.show({type:`notification`,id:`push-${Date.now()}`,title:n?.title??`WhipDesk`,body:n?.body,level:`info`,source:`push`,t:Date.now()})})}catch{}}var fe=[{urls:`stun:turn-us1.whipdesk.com:3478`}];async function pe(e){try{let t=await e.getStats(),n=``,r=``;t.forEach(e=>{e.type===`transport`&&e.selectedCandidatePairId&&(n=e.selectedCandidatePairId)}),t.forEach(e=>{e.type===`candidate-pair`&&!r&&(e.id===n||e.nominated&&e.state===`succeeded`)&&(r=e.localCandidateId)});let i=``;if(t.forEach(e=>{e.id===r&&e.type===`local-candidate`&&(i=e.candidateType)}),i===`relay`)return`TURN`;if(i===`srflx`||i===`prflx`)return`STUN`;if(i===`host`)return`LAN`}catch{}return null}var me=class{deviceId;config;core;pc=null;dc=null;mainXcv=null;overviewXcv=null;cleanupSignal=null;deleteSessionDoc=null;pushCandidate=null;appliedAnswer=!1;appliedCandidates=new Set;closed=!1;reconnectTimer=0;statsTimer=0;constructor(e,t,n){this.deviceId=e,this.config=n,this.core=new l(t),this.core.setSender(e=>{if(this.dc&&this.dc.readyState===`open`)try{this.dc.send(e)}catch{}})}on(e,t){this.core.on(e,t)}send(e){this.core.send(e)}submitPin(e){this.core.submitPin(e)}setVisible(e){this.core.setVisible(e)}connect(){this.closed=!1,this.start()}close(){this.closed=!0,window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.teardown()}isHealthy(){return!this.closed&&this.pc?.connectionState===`connected`&&this.dc?.readyState===`open`}wake(){this.closed||this.isHealthy()||(window.clearTimeout(this.reconnectTimer),this.reconnectTimer=0,this.teardown(),this.start())}start(){this.open().catch(e=>{this.core.emit(`error`,`Remote connect failed: ${e.message}`),this.core.emit(`status`,`disconnected`),this.scheduleReconnect()})}teardown(){if(window.clearInterval(this.statsTimer),this.statsTimer=0,this.cleanupSignal?.(),this.cleanupSignal=null,this.deleteSessionDoc?.(),this.deleteSessionDoc=null,this.pushCandidate=null,this.appliedAnswer=!1,this.appliedCandidates.clear(),this.core.emit(`videoTrack`,null),this.core.emit(`overviewTrack`,null),this.dc){this.dc.onopen=this.dc.onclose=this.dc.onmessage=null;try{this.dc.close()}catch{}this.dc=null}if(this.pc){this.pc.onconnectionstatechange=null,this.pc.ontrack=null;try{this.pc.close()}catch{}this.pc=null}this.mainXcv=null,this.overviewXcv=null}scheduleReconnect(){this.closed||this.reconnectTimer||(this.reconnectTimer=window.setTimeout(()=>{this.reconnectTimer=0,!this.closed&&(this.teardown(),this.start())},2e3))}async open(){this.core.emit(`status`,`connecting`);let{initializeApp:e,getApps:t}=await F(async()=>{let{initializeApp:e,getApps:t}=await import(`./index.esm-mXKu2C3o.js`);return{initializeApp:e,getApps:t}},__vite__mapDeps([0,1]),import.meta.url),{getAuth:n}=await F(async()=>{let{getAuth:e}=await import(`./index.esm-CmSagqpw.js`);return{getAuth:e}},__vite__mapDeps([2,1]),import.meta.url),{getDatabase:r,ref:i,child:a,push:o,set:s,onValue:c,remove:l,onDisconnect:u}=await F(async()=>{let{getDatabase:e,ref:t,child:n,push:r,set:i,onValue:a,remove:o,onDisconnect:s}=await import(`./index.esm-Di8jSGaG.js`);return{getDatabase:e,ref:t,child:n,push:r,set:i,onValue:a,remove:o,onDisconnect:s}},__vite__mapDeps([5,1]),import.meta.url),d=t()[0]??e(this.config),f=n(d);if(await new Promise(e=>{if(f.currentUser)return e();let t=f.onAuthStateChanged(()=>{t(),e()})}),!f.currentUser){window.location.assign(S(`/app/${window.location.hash}`));return}let p=f.currentUser.uid,m=r(d),h=await this.fetchIceServers(f.currentUser),g=new RTCPeerConnection({iceServers:h,iceTransportPolicy:`all`});this.pc=g;let _=g.createDataChannel(`whipdesk`);_.binaryType=`arraybuffer`,this.dc=_,this.wireDataChannel(_);try{this.mainXcv=g.addTransceiver(`video`,{direction:`recvonly`}),this.overviewXcv=g.addTransceiver(`video`,{direction:`recvonly`})}catch{}g.ontrack=e=>{let t=e.streams[0]??new MediaStream([e.track]);this.overviewXcv&&e.transceiver===this.overviewXcv?this.core.emit(`overviewTrack`,t):this.core.emit(`videoTrack`,t)};let v=!1,y=async()=>{if(v)return;let e=await pe(g);if(!e)return;v=!0,this.core.emit(`transport`,e);let t=e===`TURN`?`turn`:e===`STUN`?`stun`:`lan`;try{let{getFirestore:e,doc:n,setDoc:r,increment:i}=await F(async()=>{let{getFirestore:e,doc:t,setDoc:n,increment:r}=await import(`./index.esm-wogdVWd2.js`);return{getFirestore:e,doc:t,setDoc:n,increment:r}},__vite__mapDeps([3,1]),import.meta.url);r(n(e(d),`users`,p),{stats:{[t]:i(1)}},{merge:!0}).catch(()=>{})}catch{}};g.onconnectionstatechange=()=>{let e=g.connectionState;e===`connected`?(this.core.emit(`status`,`connected`),y(),this.startStatsPoll(g)):(e===`failed`||e===`disconnected`||e===`closed`)&&(this.core.emit(`status`,`disconnected`),this.scheduleReconnect())};let b=o(i(m,`signaling/${p}/${this.deviceId}`));this.pushCandidate=e=>{try{s(o(a(b,`offerCandidates`)),e)}catch{}},g.onicecandidate=e=>{e.candidate&&this.pushCandidate?.(e.candidate.toJSON())};let x=await g.createOffer();await g.setLocalDescription(x),await s(b,{offer:{sdp:g.localDescription?.sdp??``},controllerUid:p,createdAtMs:Date.now()}),u(b).remove(),this.deleteSessionDoc=()=>void l(b).catch(()=>{}),this.cleanupSignal=c(b,e=>{let t=e.val();if(!(!t||this.closed)&&(t.answer?.sdp&&!this.appliedAnswer&&(this.appliedAnswer=!0,g.setRemoteDescription({type:`answer`,sdp:t.answer.sdp})),t.answerCandidates&&this.appliedAnswer)){for(let[e,n]of Object.entries(t.answerCandidates))if(!this.appliedCandidates.has(e)){this.appliedCandidates.add(e);try{g.addIceCandidate(n)}catch{}}}})}startStatsPoll(e){window.clearInterval(this.statsTimer),this.lossBase=null,this.statsPolls=0,this.statsTimer=window.setInterval(()=>void this.pollStats(e),1e3)}lossBase=null;statsPolls=0;async pollStats(e){let t=null,n=0,r=0,i=0;try{(await e.getStats()).forEach(e=>{e.type===`inbound-rtp`&&e.kind===`video`?(n=Math.max(n,Number(e.framesPerSecond??0)),r+=Number(e.packetsLost??0),i+=Number(e.packetsReceived??0)):e.type===`candidate-pair`&&e.nominated&&typeof e.currentRoundTripTime==`number`&&(t=e.currentRoundTripTime)})}catch{return}if(this.core.emit(`netStats`,{fps:Math.round(n),rtt:t==null?null:Math.round(t*1e3)}),this.statsPolls+=1,!this.lossBase)this.lossBase={lost:r,received:i};else if(this.statsPolls%5==0){let e=Math.max(0,r-this.lossBase.lost),n=Math.max(0,i-this.lossBase.received);this.lossBase={lost:r,received:i};let a=e+n;if(a>0){let n=e/a*100;this.core.send({type:`video-stats`,lossPct:n,rttMs:t==null?void 0:Math.round(t*1e3)})}}}wireDataChannel(e){e.onopen=()=>this.core.sendHello(),e.onclose=()=>{this.core.emit(`status`,`disconnected`),this.scheduleReconnect()},e.onmessage=e=>{typeof e.data==`string`&&this.core.handleText(e.data)}}async fetchIceServers(e){let t=[...fe];try{let e=sessionStorage.getItem(`wd-ice`);if(e){let{servers:t,exp:n}=JSON.parse(e);if(Array.isArray(t)&&t.length&&n>Date.now())return t}}catch{}try{let t=this.config.iceUrl||`https://us-central1-${this.config.projectId}.cloudfunctions.net/iceServers`,n=await e.getIdToken(),r=await fetch(t,{headers:{authorization:`Bearer ${n}`}});if(r.ok){let e=await r.json();if(Array.isArray(e.iceServers)&&e.iceServers.length){try{sessionStorage.setItem(`wd-ice`,JSON.stringify({servers:e.iceServers,exp:Date.now()+8*6e4}))}catch{}return e.iceServers}}}catch{}return t}};function L(e,t,n){return e<t?t:e>n?n:e}var he=480,ge=600,R=-.6,z=1.6,_e=1500,ve=120,ye=3e3;function B(e){return!e||e.x<=.001&&e.y<=.001&&e.w>=.999&&e.h>=.999}function V(e,t){if(B(e)&&B(t))return!0;if(!e||!t)return!1;let n=.005;return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n&&Math.abs(e.w-t.w)<n&&Math.abs(e.h-t.h)<n}var be=class{canvas;ctx;mainEl=null;region=null;shownRegion=null;regionBridge=0;regionActiveHold=0;screen={width:0,height:0};viewMode=`fit`;zoom=1;center={nx:.5,ny:.5};cursor=null;onZoomCb;onViewCb;gestureActive=!1;viewDirty=!1;videoActive=!1;videoRaf=0;cssW=1;cssH=1;dpr=1;layout={S:1,tx:0,ty:0};rafPending=!1;overlayBox=null;overlayView=null;overlayCanvas=null;overlayCtx=null;lastOverlayKey=``;overlayCanvasKey=``;minimapActiveAt=0;minimapShown=!0;overview=null;overviewCtx=null;overviewReady=!1;overviewDirty=!1;lastSnapAt=0;overviewEl=null;constructor(e){this.canvas=e;let t=e.getContext(`2d`);if(!t)throw Error(`2D canvas context unavailable`);this.ctx=t,this.createOverlay(),this.startOverlayLoop(),this.resize(),new ResizeObserver(()=>this.resize()).observe(e),window.addEventListener(`orientationchange`,()=>window.setTimeout(()=>this.resize(),200))}createOverlay(){let e=document.createElement(`div`);e.style.cssText=`position:fixed;top:calc(env(safe-area-inset-top, 0px) + 56px);right:10px;box-sizing:border-box;border:1.5px solid rgba(255,255,255,0.8);border-radius:6px;background:rgba(0,0,0,0.55);z-index:2147483600;pointer-events:none;display:none;box-shadow:0 1px 8px rgba(0,0,0,0.6);`;let t=document.createElement(`canvas`);t.style.cssText=`position:absolute;inset:0;width:100%;height:100%;border-radius:5px;display:block;`;let n=document.createElement(`div`);n.style.cssText=`position:absolute;box-sizing:border-box;border:2px solid #4ea1ff;background:rgba(78,161,255,0.18);border-radius:2px;`,e.appendChild(t),e.appendChild(n),document.body.appendChild(e),this.overlayBox=e,this.overlayView=n,this.overlayCanvas=t,this.overlayCtx=t.getContext(`2d`)}startOverlayLoop(){let e=()=>{this.renderOverlay(),requestAnimationFrame(e)};requestAnimationFrame(e)}renderOverlay(){let e=this.overlayBox,t=this.overlayView;if(!e||!t)return;let n=this.getVisibleRegion(),r=this.region,i=this.dispW(),a=this.dispH();if(!(r||n.w<.985||n.h<.985)){e.style.display!==`none`&&(e.style.display=`none`),this.lastOverlayKey=``,this.overlayCanvasKey=``;return}e.style.display===`none`&&(e.style.display=`block`,this.minimapActiveAt=performance.now(),this.minimapShown=!1);let o=Math.round(Math.min(132,Math.max(76,this.cssW*.32))),s=Math.round(o*(a/i||.625)),c=`${o}x${s}`;(c!==this.overlayCanvasKey||this.overviewDirty)&&(this.overlayCanvasKey=c,this.overviewDirty=!1,e.style.width=`${o}px`,e.style.height=`${s}px`,this.paintMinimap(o,s));let l=n,u=performance.now(),d=`${l.x.toFixed(3)},${l.y.toFixed(3)},${l.w.toFixed(3)},${l.h.toFixed(3)}`;d!==this.lastOverlayKey&&(this.lastOverlayKey=d,this.minimapActiveAt=u,t.style.left=`${(L(l.x,0,1)*100).toFixed(2)}%`,t.style.top=`${(L(l.y,0,1)*100).toFixed(2)}%`,t.style.width=`${(L(Math.max(.04,l.w),0,1)*100).toFixed(2)}%`,t.style.height=`${(L(Math.max(.04,l.h),0,1)*100).toFixed(2)}%`);let f=u-this.minimapActiveAt<ye;f!==this.minimapShown&&(this.minimapShown=f,e.style.transition=`opacity ${f?.18:.5}s ease`,e.style.opacity=f?`1`:`0`)}paintMinimap(e,t){let n=this.overlayCanvas,r=this.overlayCtx;if(!n||!r)return;let i=Math.max(1,Math.round(e*this.dpr)),a=Math.max(1,Math.round(t*this.dpr));n.width!==i&&(n.width=i),n.height!==a&&(n.height=a),r.clearRect(0,0,i,a);let o=this.overviewImg();o&&(r.imageSmoothingEnabled=!0,r.imageSmoothingQuality=`low`,r.drawImage(o,0,0,i,a))}overviewImg(){return this.overviewReady?this.overview:null}maybeSnapshot(e,t,n){let r=performance.now();if(r-this.lastSnapAt<ge)return;this.lastSnapAt=r,this.overview||(this.overview=document.createElement(`canvas`),this.overviewCtx=this.overview.getContext(`2d`));let i=this.overview,a=this.overviewCtx;if(!i||!a)return;let o=he,s=Math.max(1,Math.round(o*n/t));i.width!==o&&(i.width=o),i.height!==s&&(i.height=s),a.imageSmoothingEnabled=!0,a.imageSmoothingQuality=`medium`,a.drawImage(e,0,0,t,n,0,0,o,s),this.overviewReady=!0,this.overviewDirty=!0}getCanvas(){return this.canvas}resize(){let e=this.canvas.getBoundingClientRect();this.cssW=Math.max(1,e.width),this.cssH=Math.max(1,e.height),this.dpr=Math.min(window.devicePixelRatio||1,2),this.canvas.width=Math.round(this.cssW*this.dpr),this.canvas.height=Math.round(this.cssH*this.dpr),this.requestDraw(),this.emitView()}setScreen(e){let t=e.width!==this.screen.width||e.height!==this.screen.height;this.screen=e,t&&(this.overviewReady=!1,this.lastSnapAt=0,this.overviewDirty=!0),this.requestDraw()}getScreen(){return this.screen}dispW(){return this.screen.width||this.mainEl?.videoWidth||1}dispH(){return this.screen.height||this.mainEl?.videoHeight||1}setFrameRegion(e){let t=B(e)?null:e;this.region=t,window.clearTimeout(this.regionActiveHold),window.clearTimeout(this.regionBridge),this.regionBridge=window.setTimeout(()=>{this.shownRegion=t,this.videoActive||this.requestDraw()},_e),this.videoActive||this.requestDraw()}setFrameRegionActive(e){let t=B(e)?null:e;V(t,this.region)&&(V(t,this.shownRegion)||(window.clearTimeout(this.regionActiveHold),this.regionActiveHold=window.setTimeout(()=>{V(t,this.region)&&(this.shownRegion=t,window.clearTimeout(this.regionBridge),this.videoActive||this.requestDraw())},ve)))}setVideoSource(e){this.mainEl=e,this.videoActive=!!e,e?this.startVideoLoop():this.stopVideoLoop()}isVideoActive(){return this.videoActive}setOverviewSource(e){this.overviewEl=e}startVideoLoop(){if(this.videoRaf)return;let e=()=>{if(!this.videoActive){this.videoRaf=0;return}this.draw(),this.videoRaf=requestAnimationFrame(e)};this.videoRaf=requestAnimationFrame(e)}stopVideoLoop(){this.videoRaf&&cancelAnimationFrame(this.videoRaf),this.videoRaf=0,this.requestDraw()}setViewMode(e){this.viewMode=e,e===`fit`?(this.zoom=1,this.center={nx:.5,ny:.5}):this.zoom===1&&(this.zoom=2),this.emitView(),this.requestDraw()}getViewMode(){return this.viewMode}toggleViewMode(){return this.setViewMode(this.viewMode===`fit`?`magnify`:`fit`),this.viewMode}setZoom(e){this.zoom=L(e,1,8),this.viewMode=this.zoom===1?`fit`:`magnify`,this.zoom===1&&(this.center={nx:.5,ny:.5}),this.onZoomCb?.(this.zoom),this.emitView(),this.requestDraw()}zoomBy(e){this.setZoom(this.zoom*e)}getZoom(){return this.zoom}setOnZoom(e){this.onZoomCb=e}setOnView(e){this.onViewCb=e}beginViewGesture(){this.gestureActive=!0}endViewGesture(){this.gestureActive&&(this.gestureActive=!1,this.viewDirty&&(this.viewDirty=!1,this.onViewCb?.(this.getVisibleRegion())))}emitView(){if(this.gestureActive){this.viewDirty=!0;return}this.onViewCb?.(this.getVisibleRegion())}zoomAround(e,t,n){let r=this.canvasToNorm(t,n),i=L(this.zoom*e,1,8);if(i===1){this.setZoom(1);return}this.zoom=i,this.viewMode=`magnify`;let a=this.dispW(),o=this.dispH(),s=Math.min(this.cssW/a,this.cssH/o)*this.zoom;this.center.nx=L(r.nx-(t-this.cssW/2)/(a*s||1),R,z),this.center.ny=L(r.ny-(n-this.cssH/2)/(o*s||1),R,z),this.onZoomCb?.(this.zoom),this.emitView(),this.requestDraw()}panByNorm(e,t){this.center.nx=L(this.center.nx+e,R,z),this.center.ny=L(this.center.ny+t,R,z),this.emitView(),this.requestDraw()}panByCanvasPixels(e,t){let{S:n}=this.computeLayout(),r=this.dispW(),i=this.dispH();!r||!i||!n||(this.center.nx=L(this.center.nx-e/(r*n),R,z),this.center.ny=L(this.center.ny-t/(i*n),R,z),this.emitView(),this.requestDraw())}setCursor(e,t=0){this.cursor=e===null?null:{nx:e,ny:t},this.videoActive||this.requestDraw()}computeLayout(){let{cssW:e,cssH:t}=this,n=this.dispW(),r=this.dispH();if(!n||!r)return this.layout={S:1,tx:0,ty:0},this.layout;let i=Math.min(e/n,t/r)*this.zoom,a,o;return this.viewMode===`fit`||this.zoom===1?(a=(e-n*i)/2,o=(t-r*i)/2):(a=e/2-this.center.nx*n*i,o=t/2-this.center.ny*r*i),this.layout={S:i,tx:a,ty:o},this.layout}canvasToNorm(e,t){let{S:n,tx:r,ty:i}=this.computeLayout();return{nx:L((e-r)/(this.dispW()*n||1),0,1),ny:L((t-i)/(this.dispH()*n||1),0,1)}}normToCanvas(e,t){let{S:n,tx:r,ty:i}=this.computeLayout();return{cx:r+e*this.dispW()*n,cy:i+t*this.dispH()*n}}getVisibleRegion(){let{S:e,tx:t,ty:n}=this.computeLayout(),r=this.dispW()*e,i=this.dispH()*e;if(!r||!i)return{x:0,y:0,w:1,h:1};let a=L(-t/r,0,1),o=L(-n/i,0,1),s=L((this.cssW-t)/r,0,1),c=L((this.cssH-n)/i,0,1);return{x:a,y:o,w:Math.max(.05,s-a),h:Math.max(.05,c-o)}}requestDraw(){this.videoActive||this.rafPending||(this.rafPending=!0,requestAnimationFrame(()=>{this.rafPending=!1,this.draw()}))}draw(){let e=this.ctx;e.setTransform(this.dpr,0,0,this.dpr,0,0),e.clearRect(0,0,this.cssW,this.cssH);let t=this.mainEl&&this.mainEl.videoWidth>0?this.mainEl:null;if(!t)return;let{S:n,tx:r,ty:i}=this.computeLayout(),a=this.dispW(),o=this.dispH(),s=a*n,c=o*n,l=t.videoWidth,u=t.videoHeight,d=l/u,f=e=>e?e.w*a/(e.h*o):a/o;if(this.region!==this.shownRegion){let e=Math.abs(d/f(this.region)-1)<.06,t=Math.abs(d/f(this.shownRegion)-1)<.06;e&&!t&&(this.shownRegion=this.region,window.clearTimeout(this.regionBridge))}let p=this.shownRegion,m=!!p&&Math.abs(d/f(p)-1)<.08,h=m?r+p.x*s:r,g=m?i+p.y*c:i,_=m?p.w*s:s,v=m?p.h*c:c,y=this.overviewImg();y&&(this.region||this.shownRegion)&&(e.imageSmoothingEnabled=!0,e.imageSmoothingQuality=`low`,e.drawImage(y,r,i,s,c));let b=Math.min(_/l,v/u),x=l*b,S=u*b;if(e.imageSmoothingEnabled=!0,e.imageSmoothingQuality=`high`,e.drawImage(t,0,0,l,u,h+(_-x)/2,g+(v-S)/2,x,S),this.cursor){let{cx:e,cy:t}=this.normToCanvas(this.cursor.nx,this.cursor.ny);this.drawCursor(e,t)}let C=this.overviewEl;if(this.region&&C&&C.videoWidth>0)this.maybeSnapshot(C,C.videoWidth,C.videoHeight);else if(!this.region&&!this.shownRegion&&o>0){let e=a/o;e>0&&Math.abs(d/e-1)<.06&&this.maybeSnapshot(t,l,u)}}drawCursor(e,t){let n=this.ctx;n.save(),n.beginPath(),n.arc(e,t,9,0,Math.PI*2),n.strokeStyle=`rgba(78,161,255,0.95)`,n.lineWidth=2,n.stroke(),n.beginPath(),n.arc(e,t,2.5,0,Math.PI*2),n.fillStyle=`rgba(78,161,255,0.95)`,n.fill(),n.restore()}};function xe(e,t,n,r){let i=document.createElement(`div`);i.className=`wd-place-layer`;let a=document.createElement(`div`);a.className=`wd-place-marker`;let o=document.createElement(`div`);o.className=`wd-place-bar`;let s=document.createElement(`p`);s.className=`wd-place-hint`,s.textContent=n.hint,o.appendChild(s);let c=null;n.withText&&(c=document.createElement(`textarea`),c.className=`wd-input wd-input-area wd-place-text`,c.placeholder=`Type the prompt to send…`,o.appendChild(c));let l=document.createElement(`div`);l.className=`wd-place-buttons`;let u=document.createElement(`button`);u.className=`wd-btn`,u.textContent=`Cancel`;let d=document.createElement(`button`);d.className=`wd-btn wd-go`,d.textContent=n.confirmLabel,l.append(u,d),o.appendChild(l),t.append(i,a,o),t.classList.add(`wd-placing`);let f={nx:.5,ny:.5},p=0,m=()=>{let{cx:t,cy:n}=e.normToCanvas(f.nx,f.ny);a.style.left=`${t}px`,a.style.top=`${n}px`,p=requestAnimationFrame(m)};p=requestAnimationFrame(m);let h=new Map,g=`idle`,_=null,v={x:0,y:0},y=!1,b=e=>{let t=i.getBoundingClientRect();return{x:e.clientX-t.left,y:e.clientY-t.top}},x=t=>{let{cx:n,cy:r}=e.normToCanvas(f.nx,f.ny);return Math.hypot(t.x-n,t.y-r)<38};i.addEventListener(`pointerdown`,t=>{i.setPointerCapture?.(t.pointerId);let n=b(t);if(h.set(t.pointerId,n),h.size===2){let e=[...h.values()],t=e[0],n=e[1];_={dist:Math.hypot(t.x-n.x,t.y-n.y),mx:(t.x+n.x)/2,my:(t.y+n.y)/2},g=`idle`}else h.size===1&&(v=n,y=!1,g=x(n)?`marker`:`pan`,g===`marker`&&(f=e.canvasToNorm(n.x,n.y)))}),i.addEventListener(`pointermove`,t=>{let n=h.get(t.pointerId);if(!n)return;let r=b(t);if(h.set(t.pointerId,r),h.size>=2&&_){let t=[...h.values()],n=t[0],r=t[1],i=Math.hypot(n.x-r.x,n.y-r.y),a=(n.x+r.x)/2,o=(n.y+r.y)/2;Math.abs(i-_.dist)>4&&(e.zoomAround(1+(i-_.dist)/200,a,o),_.dist=i),e.panByCanvasPixels(a-_.mx,o-_.my),_.mx=a,_.my=o;return}Math.hypot(r.x-v.x,r.y-v.y)>6&&(y=!0);let i=r.x-n.x,a=r.y-n.y;g===`marker`?f=e.canvasToNorm(r.x,r.y):g===`pan`&&e.panByCanvasPixels(i,a)});let S=t=>{let n=h.get(t.pointerId);h.delete(t.pointerId),n&&g===`pan`&&!y&&(f=e.canvasToNorm(n.x,n.y)),h.size<2&&(_=null),h.size===0&&(g=`idle`)};i.addEventListener(`pointerup`,S),i.addEventListener(`pointercancel`,S);let C=e=>{cancelAnimationFrame(p),t.classList.remove(`wd-placing`),i.remove(),a.remove(),o.remove(),r(e)};u.onclick=()=>C(null),d.onclick=()=>{let e=c?.value.trim()??``;if(n.withText&&!e){c?.focus();return}C({nx:f.nx,ny:f.ny,text:n.withText?e:void 0})}}var Se=0;function H(){return`w${Date.now().toString(36)}${(Se++).toString(36)}`}function Ce(){return new Date().toLocaleString(void 0,{month:`short`,day:`numeric`,hour:`2-digit`,minute:`2-digit`})}function we(e){let t=Math.max(0,Math.round((e-Date.now())/1e3)),n=Math.floor(t/3600),r=Math.floor(t%3600/60),i=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${String(i).padStart(2,`0`)}s`:`${i}s`}function U(e,t,n){let r=document.createElement(e);return t&&(r.className=t),n!==void 0&&(r.textContent=n),r}var Te={claude:`Claude Code`,codex:`Codex CLI`,gemini:`Gemini CLI`,aider:`Aider`,copilot:`Copilot CLI`,opencode:`opencode`,cursor:`Cursor Agent`,amp:`Amp`,unknown:`AI agent`};function W(e){return Te[e]??`AI agent`}function G(e){switch(e){case`working`:return`working`;case`blocked`:return`needs you`;case`idle`:return`idle`;case`finished`:return`finished`;case`crashed`:return`crashed`;default:return`…`}}var Ee=class{root;conn;view;notifications;requestNotifications;regions=[];timers=[];monitors=[];monitorSessions=[];alwaysAgents=new Set;renderPicker=null;renderAlways=null;countdownTimer=0;overlay;list;permissionRow;selector=null;constructor(e,t,n,r,i=()=>{}){this.root=e,this.conn=t,this.view=n,this.notifications=r,this.requestNotifications=i,this.overlay=U(`div`,`wd-dialog-overlay hidden`),this.overlay.addEventListener(`pointerdown`,e=>{e.target===this.overlay&&this.close()});let a=U(`div`,`wd-dialog`),o=U(`div`,`wd-dialog-head`);o.append(U(`h2`,``,`Auto-Whips`));let s=U(`button`,`wd-dialog-x`);s.appendChild(g(`x`)),s.onclick=()=>this.close(),o.appendChild(s);let c=U(`div`,`wd-dialog-help`),l=U(`p`,`wd-help-intro`,`Auto-Whips are a set of features that can whip and monitor agents for you automatically:`),u=U(`ul`,`wd-help-list`),d=U(`li`);d.append(U(`strong`,void 0,`Alerts`),document.createTextNode(` — watch part of the screen and ping you when it changes (e.g. your agent finishes).`));let f=U(`li`);f.append(U(`strong`,void 0,`Timers`),document.createTextNode(` — ping you after a set time, and can auto-click or send a prompt when they fire.`));let p=U(`li`);p.append(U(`strong`,void 0,`Session Monitoring`),document.createTextNode(` — pick a running AI session and get pinged the moment the agent stops working (it's waiting on you or has gone idle).`)),u.append(d,f,p);let m=U(`p`,`wd-help-note`,`Enable browser notifications to be reminded even when the browser is closed.`);c.append(l,u,m),this.permissionRow=U(`div`,`wd-perm-row`),this.list=U(`div`,`wd-watch-list`);let h=U(`button`,`wd-btn wd-go`);h.append(g(`plus`),U(`span`,`wd-btn-label`,`Add alert`)),h.onclick=()=>this.beginSelection();let _=U(`button`,`wd-btn wd-go`);_.append(g(`clock`),U(`span`,`wd-btn-label`,`Add timer`)),_.onclick=()=>this.beginTimer();let v=U(`button`,`wd-btn wd-go`);v.append(g(`activity`),U(`span`,`wd-btn-label`,`Add Session Monitoring`)),v.onclick=()=>this.beginMonitor();let y=U(`div`,`wd-dialog-actions wd-actions-stack`);y.append(h,_,v),a.append(o,c,this.permissionRow,this.list,y),this.overlay.appendChild(a),this.root.appendChild(this.overlay),this.renderPermission(),this.conn.on(`monitorSessions`,e=>{this.monitorSessions=e,this.renderPicker?.()})}setRegions(e){this.regions=e,this.renderList()}setTimers(e){this.timers=e,this.renderList()}setMonitors(e){this.monitors=e,this.renderList()}setAlwaysAgents(e){this.alwaysAgents=new Set(e),this.renderAlways?.()}open(){this.renderPermission(),this.renderList(),this.overlay.classList.remove(`hidden`),window.clearInterval(this.countdownTimer),this.countdownTimer=window.setInterval(()=>{this.timers.length&&this.renderList()},1e3)}close(){this.overlay.classList.add(`hidden`),window.clearInterval(this.countdownTimer),this.countdownTimer=0}renderPermission(){this.permissionRow.replaceChildren();let e=U(`span`,`wd-perm-dot`),t=U(`span`,`wd-perm-text`),n=this.notifications.permission;if(n===`granted`)e.dataset.state=`on`,t.textContent=`Browser notifications are on.`,this.permissionRow.append(e,t);else if(n===`denied`)e.dataset.state=`off`,t.textContent=`Notifications are blocked — enable them in your browser settings.`,this.permissionRow.append(e,t);else if(n===`unsupported`)e.dataset.state=`off`,t.textContent=`This browser can't show notifications. Keep this page open for in-app alerts.`,this.permissionRow.append(e,t);else{e.dataset.state=`warn`,t.textContent=`Allow notifications to be alerted while this page is in the background.`;let n=U(`button`,`wd-btn wd-perm-enable`);n.append(U(`span`,`wd-btn-label`,`Enable`)),n.onclick=async()=>{await this.requestNotifications(),this.renderPermission()},this.permissionRow.append(e,t,n)}}renderList(){if(this.list.replaceChildren(),this.regions.length===0&&this.timers.length===0&&this.monitors.length===0){this.list.appendChild(U(`p`,`wd-dialog-help`,`No alerts, timers, or session monitors yet.`));return}for(let e of this.regions){let t=U(`div`,`wd-watch-row`),n=U(`button`,`wd-watch-name`,e.label);n.title=`Edit this alert`,n.onclick=()=>this.beginSelection(e),t.appendChild(n);let r=U(`button`,`wd-btn wd-icon-only`);r.appendChild(g(`trash`)),r.setAttribute(`aria-label`,`Remove ${e.label}`),r.onclick=()=>{this.conn.send({type:`watch-remove`,id:e.id}),this.regions=this.regions.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(r),this.list.appendChild(t)}for(let e of this.timers){let t=U(`div`,`wd-watch-row`),n=U(`div`,`wd-timer-info`),r=U(`span`,`wd-timer-name`);r.append(g(`clock`,15),document.createTextNode(e.label));let i=U(`span`,`wd-timer-remain`,we(e.fireAtMs));n.append(r,i),t.appendChild(n),e.hasAction&&t.appendChild(U(`span`,`wd-timer-tag`,`auto`));let a=U(`button`,`wd-btn wd-icon-only`);a.appendChild(g(`trash`)),a.setAttribute(`aria-label`,`Cancel ${e.label}`),a.onclick=()=>{this.conn.send({type:`timer-remove`,id:e.id}),this.timers=this.timers.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(a),this.list.appendChild(t)}for(let e of this.monitors){let t=U(`div`,`wd-watch-row`),n=U(`div`,`wd-timer-info`),r=U(`span`,`wd-timer-name`);r.append(g(`activity`,15),document.createTextNode(`${W(e.agent)} · ${e.label}`));let i=U(`span`,`wd-mon-state`);i.dataset.state=e.live?e.state:`finished`,i.textContent=e.live?G(e.state):`ended`,n.append(r,i),t.appendChild(n);let a=U(`button`,`wd-btn wd-icon-only`);a.appendChild(g(`trash`)),a.setAttribute(`aria-label`,`Stop monitoring ${e.label}`),a.onclick=()=>{this.conn.send({type:`monitor-remove`,id:e.id}),this.monitors=this.monitors.filter(t=>t.id!==e.id),this.renderList()},t.appendChild(a),this.list.appendChild(t)}}beginSelection(e){this.close(),this.selector&&this.selector.remove();let t=this.root.getBoundingClientRect(),n=e?this.boxFromRegion(e,t):{x:t.width*.3,y:t.height*.3,w:t.width*.4,h:t.height*.25},r=U(`div`,`wd-selector-info`,e?`Move or resize the area to watch, then Save — you'll be alerted when its pixels change.`:`You'll get a browser notification when the pixels inside this box change. The agent watches this part of the screen.`),i=U(`div`,`wd-selector`),a=U(`div`,`wd-selector-move`);a.appendChild(g(`drag`));let o=U(`div`,`wd-selector-handle`),s=U(`div`,`wd-selector-bar`),c=U(`button`,`wd-btn wd-go`);c.append(U(`span`,`wd-btn-label`,e?`Save`:`Create`));let l=U(`button`,`wd-btn`);l.append(U(`span`,`wd-btn-label`,`Cancel`)),s.append(l,c),i.append(a,o,s),this.root.append(r,i),this.selector=i;let u=()=>{i.remove(),r.remove(),this.selector=null,this.open()},d=()=>{i.style.left=`${n.x}px`,i.style.top=`${n.y}px`,i.style.width=`${n.w}px`,i.style.height=`${n.h}px`};d();let f=(e,t)=>{e.addEventListener(`pointerdown`,n=>{n.preventDefault(),n.stopPropagation(),e.setPointerCapture(n.pointerId);let r=n.clientX,i=n.clientY,a=e=>{t(e.clientX-r,e.clientY-i),r=e.clientX,i=e.clientY,d()},o=()=>{e.removeEventListener(`pointermove`,a),e.removeEventListener(`pointerup`,o)};e.addEventListener(`pointermove`,a),e.addEventListener(`pointerup`,o)})};f(a,(e,r)=>{n.x=Math.max(0,Math.min(t.width-n.w,n.x+e)),n.y=Math.max(0,Math.min(t.height-n.h,n.y+r))}),f(o,(e,r)=>{n.w=Math.max(40,Math.min(t.width-n.x,n.w+e)),n.h=Math.max(40,Math.min(t.height-n.y,n.h+r))}),l.onclick=u,c.onclick=()=>{let t=this.toScreenRegion(n,e);t&&(this.conn.send({type:`watch-add`,region:t}),this.regions=e?this.regions.map(e=>e.id===t.id?t:e):[...this.regions,t],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(e?`Alert updated`:`Alert created`,e?`"${t.label}" updated.`:`Monitoring "${t.label}". We'll notify you when it changes.`,`success`)),u()}}boxFromRegion(e,t){let n=this.view.normToCanvas(e.x,e.y),r=this.view.normToCanvas(e.x+e.w,e.y+e.h),i=Math.max(40,r.cx-n.cx),a=Math.max(40,r.cy-n.cy);return{x:Math.max(0,Math.min(t.width-i,n.cx)),y:Math.max(0,Math.min(t.height-a,n.cy)),w:i,h:a}}toScreenRegion(e,t){let n=this.view.canvasToNorm(e.x,e.y),r=this.view.canvasToNorm(e.x+e.w,e.y+e.h),i=Math.min(n.nx,r.nx),a=Math.min(n.ny,r.ny),o=Math.abs(r.nx-n.nx),s=Math.abs(r.ny-n.ny);return o<.005||s<.005?null:{id:t?.id??H(),label:t?.label??Ce(),x:i,y:a,w:o,h:s}}beginTimer(){this.close();let e=U(`div`,`wd-dialog-overlay`),t=()=>{e.remove(),this.open()};e.addEventListener(`pointerdown`,n=>{n.target===e&&t()});let n=U(`div`,`wd-dialog`),r=U(`div`,`wd-dialog-head`);r.append(U(`h2`,``,`Set timer`));let i=U(`button`,`wd-dialog-x`);i.appendChild(g(`x`)),i.onclick=t,r.appendChild(i);let a=U(`p`,`wd-dialog-help`,`Waiting for a session limit to reset? Get notified when the time's up. You can also schedule an action for the moment it hits zero — click Retry, or enter a new prompt.`),o=U(`div`,`wd-form-row`);o.appendChild(U(`label`,`wd-form-label`,`Remind me in`));let s=U(`input`,`wd-input wd-input-num`);s.type=`number`,s.min=`0`,s.max=`168`,s.value=`0`,s.inputMode=`numeric`;let c=U(`input`,`wd-input wd-input-num`);c.type=`number`,c.min=`0`,c.max=`59`,c.value=`30`,c.inputMode=`numeric`;let l=U(`div`,`wd-preset-row`);for(let[e,t,n]of[[`15m`,0,15],[`30m`,0,30],[`1h`,1,0],[`2h`,2,0],[`5h`,5,0]]){let r=U(`button`,`wd-preset`,e);r.type=`button`,r.onclick=()=>{s.value=String(t),c.value=String(n)},l.appendChild(r)}let u=(e,t,n,r)=>{let i=U(`div`,`wd-stepper`),a=U(`button`,`wd-step-btn`,`−`);a.type=`button`;let o=U(`button`,`wd-step-btn`,`+`);o.type=`button`;let s=e=>Math.max(0,Math.min(n,e));return a.onclick=()=>e.value=String(s((Number(e.value)||0)-t)),o.onclick=()=>e.value=String(s((Number(e.value)||0)+t)),i.append(a,e,U(`span`,`wd-form-unit`,r),o),i},d=U(`div`,`wd-form-duration`);d.append(u(s,1,168,`h`),u(c,5,59,`m`)),o.append(l,d);let f=U(`div`,`wd-form-row`);f.appendChild(U(`label`,`wd-form-label`,`Label`));let p=U(`input`,`wd-input`);p.placeholder=`e.g. Claude is back`,f.appendChild(p);let m=U(`div`,`wd-form-row`);m.appendChild(U(`label`,`wd-form-label`,`When it fires`));let h=U(`select`,`wd-input`);for(let[e,t]of[[`none`,`Just remind me`],[`click`,`Click a button`],[`key`,`Click & press Enter`],[`text`,`Click, type & send a prompt`]]){let n=document.createElement(`option`);n.value=e,n.textContent=t,h.appendChild(n)}m.appendChild(h);let _=U(`div`,`wd-dialog-actions`),v=U(`button`,`wd-btn`);v.append(U(`span`,`wd-btn-label`,`Cancel`)),v.onclick=t;let y=U(`button`,`wd-btn wd-go`),b=U(`span`,`wd-btn-label`,`Start timer`);y.append(g(`clock`),b);let x=()=>{b.textContent=h.value===`none`?`Start timer`:`Next: place target`};h.onchange=x,x();let S=()=>{let e=Math.max(0,Math.min(168,Math.round(Number(s.value)||0))),t=Math.max(0,Math.min(59,Math.round(Number(c.value)||0))),n=(e*60+t)*6e4;return n>=6e4?n:null},C=()=>{let e=Math.max(0,Math.round(Number(s.value)||0)),t=Math.max(0,Math.round(Number(c.value)||0));return`${e?`${e}h `:``}${t}m`},w=e=>{let n=H(),r=S(),i=p.value.trim()||`Timer (${C()})`;this.conn.send({type:`timer-add`,id:n,fireInMs:r,label:i,action:e}),this.timers=[...this.timers,{id:n,label:i,fireAtMs:Date.now()+r,hasAction:!!e}],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(`Timer started`,`"${i}" — I'll ping you in ${C()}.`,`success`),t()};y.onclick=()=>{if(S()==null){this.notifications.flash(`Set a time`,`Choose at least 1 minute.`,`warning`);return}let t=h.value;if(t===`none`){w(void 0);return}e.style.display=`none`;let n=t===`text`?`Drag the crosshair onto the target input and type your prompt. The timer will click it to focus, insert the text and press Enter when time is up.`:t===`key`?`Drag the crosshair onto the field. The timer will click it to focus and press Enter when time is up.`:`Drag the crosshair onto the button or UI. The timer will click it when time is up.`;xe(this.view,this.root,{withText:t===`text`,hint:n,confirmLabel:`Confirm target`},n=>{if(!n){e.style.display=``;return}let r;r=t===`click`?{kind:`click`,x:n.nx,y:n.ny,button:`left`}:t===`key`?{kind:`key`,key:`Enter`,x:n.nx,y:n.ny}:{kind:`text`,text:n.text??``,x:n.nx,y:n.ny},w(r)})},_.append(v,y),n.append(r,a,o,f,m,_),e.appendChild(n),this.root.appendChild(e)}beginMonitor(){this.close();let e=U(`div`,`wd-dialog-overlay`),t=()=>{this.renderPicker=null,this.renderAlways=null,e.remove(),this.open()};e.addEventListener(`pointerdown`,n=>{n.target===e&&t()});let n=U(`div`,`wd-dialog`),r=U(`div`,`wd-dialog-head`);r.append(U(`h2`,``,`Monitor a session`));let i=U(`button`,`wd-dialog-x`);i.appendChild(g(`x`)),i.onclick=t,r.appendChild(i);let a=U(`p`,`wd-dialog-help`,`Pick a running AI session to watch. WhipDesk finds them automatically — no setup, no wrappers. You'll get one ping the moment the agent stops working (it's waiting on you or has gone idle).`),o=U(`div`,`wd-mon-pick-head`),s=U(`button`,`wd-btn`);s.append(U(`span`,`wd-btn-label`,`Rescan`)),s.onclick=()=>this.conn.send({type:`monitor-scan`}),o.append(U(`span`,`wd-form-label`,`Running sessions`),s);let c=U(`div`,`wd-mon-pick`),l=``,u=()=>{if(c.replaceChildren(),this.monitorSessions.length===0){l=``,v(),p(),c.appendChild(U(`p`,`wd-dialog-help`,`No AI sessions detected yet. Start Claude Code, Codex, Gemini, or Aider, then Rescan.`));return}(!l||!this.monitorSessions.some(e=>e.key===l))&&(l=this.monitorSessions[0].key);for(let e of this.monitorSessions){let t=U(`button`,`wd-mon-item`);t.type=`button`,e.key===l&&t.classList.add(`on`);let n=U(`div`,`wd-mon-item-main`);n.append(g(`activity`,15),U(`span`,`wd-mon-item-title`,`${W(e.agent)} · ${e.title}`));let r=U(`span`,`wd-mon-state`);r.dataset.state=e.state,r.textContent=G(e.state),t.append(n,r),t.onclick=()=>{l=e.key,u()},c.appendChild(t)}v(),p()};this.renderPicker=u;let d=U(`div`,`wd-form-row`);d.appendChild(U(`label`,`wd-form-label`,`Always alert me`));let f=U(`div`,`wd-mon-always`);d.appendChild(f);let p=()=>{f.replaceChildren();let e=this.monitorSessions.find(e=>e.key===l);if(!e){f.appendChild(U(`p`,`wd-dialog-help`,`Pick a session above to always alert for its agent.`));return}let t=e.agent,n=U(`label`,`wd-check`),r=U(`input`);r.type=`checkbox`,r.checked=this.alwaysAgents.has(t),r.onchange=()=>{r.checked?this.alwaysAgents.add(t):this.alwaysAgents.delete(t),this.conn.send({type:`monitor-always`,agent:t,enabled:r.checked}),r.checked&&this.notifications.permission==="default"&&this.requestNotifications(),p()};let i=U(`span`,`wd-check-text`);i.append(document.createTextNode(`Always monitor every ${W(t)} session`),U(`span`,`wd-check-sub`,`Keeps alerting across agent and WhipDesk restarts — no need to re-add it.`)),n.append(r,i),f.appendChild(n)};this.renderAlways=p;let m=U(`div`,`wd-dialog-actions`),h=U(`button`,`wd-btn`);h.append(U(`span`,`wd-btn-label`,`Cancel`)),h.onclick=t;let _=U(`button`,`wd-btn wd-go`);_.append(g(`activity`),U(`span`,`wd-btn-label`,`Add monitor`));let v=()=>_.disabled=!l;v(),_.onclick=()=>{let e=this.monitorSessions.find(e=>e.key===l);if(!e)return;let n=H(),r=e.title;this.conn.send({type:`monitor-add`,id:n,key:e.key,agent:e.agent,label:r}),this.monitors=[...this.monitors,{id:n,key:e.key,agent:e.agent,label:r,state:e.state,live:!0}],this.notifications.permission==="default"&&this.requestNotifications(),this.notifications.flash(`Monitoring started`,`Watching ${W(e.agent)} · ${r}.`,`success`),t()},m.append(h,_),n.append(r,a,o,c,d,m),e.appendChild(n),this.root.appendChild(e),u(),this.conn.send({type:`monitor-scan`})}};function De(){let e=new URLSearchParams(location.hash.replace(/^#/,``));return{token:e.get(`t`)??e.get(`token`)??``,remote:e.get(`remote`)===`1`||e.has(`device`),device:e.get(`device`)??e.get(`d`)??``}}function Oe(){return`${location.protocol===`https:`?`wss`:`ws`}://${location.host}/ws`}async function ke(){let e=window.__WHIPDESK_FB__;if(e?.apiKey)return e;try{let e=await fetch(`firebase.json`,{cache:`no-store`});if(e.ok)return await e.json()}catch{}return null}var K=document.getElementById(`app`);if(!K)throw Error(`#app not found`);var q=$(`canvas`,`wd-screen`);q.id=`wd-screen`;var Ae=$(`div`,`wd-toasts`);K.append(q,Ae);var{token:J,remote:je,device:Y}=De(),X=new be(q),Z=new ce(Ae),Me=new le(K),Q=new p(K);async function Ne(e){if(je&&Y){if(e)return new me(Y,J,e);Z.show({type:`notification`,id:`no-fb`,title:`Remote unavailable`,body:`Firebase config not found for remote mode.`,level:`error`,source:`client`,t:Date.now()})}return new u(Oe(),J)}async function Pe(){let e=je&&Y?await ke():null,t=await Ne(e),n=new se(q,X,t),r=new Ee(K,t,X,Z,()=>e?I(e,Z):Z.requestPermission()),i=new re(K,{conn:t,view:X,input:n,notifications:Z,watchers:r}),a=document.createElement(`video`);a.muted=!0,a.autoplay=!0,a.playsInline=!0,a.setAttribute(`playsinline`,``),a.style.display=`none`,K.append(a);let o=document.createElement(`video`);o.muted=!0,o.autoplay=!0,o.playsInline=!0,o.setAttribute(`playsinline`,``),o.style.display=`none`,K.append(o);let s=!1,c=!1,l=``,u=0,d=e=>e.w>=.999&&e.h>=.999,f=e=>{let n=e.w>=.92&&e.h>=.92?{x:0,y:0,w:1,h:1}:e,r=`${n.x.toFixed(2)},${n.y.toFixed(2)},${n.w.toFixed(2)},${n.h.toFixed(2)}`;r!==l&&(l=r,t.send({type:`set-viewport`,x:n.x,y:n.y,w:n.w,h:n.h}))};X.setOnView(e=>{let t=X.getZoom()>1.01;window.clearTimeout(u),u=window.setTimeout(()=>f(t?e:{x:0,y:0,w:1,h:1}),t?120:0)}),t.on(`status`,e=>{i.setStatus(e),e===`connected`?(c=!1,Q.hide()):(!s||c)&&Q.show(s?`Reconnecting…`:void 0),e===`disconnected`&&(l=``)}),t.on(`transport`,e=>i.setTransport(e)),t.on(`welcome`,e=>{let n=!s;s=!0,Q.hide(),Me.hide(),X.setScreen(e.screen),i.setWelcome(e),n&&(l=`0.000,0.000,1.000,1.000`,X.setZoom(1),t.send({type:`set-viewport`,x:0,y:0,w:1,h:1}))}),t.on(`versionMismatch`,e=>Fe(e.agentVersion)),t.on(`pinRequired`,e=>{Q.hide(),Me.show(e,e=>t.submitPin(e))}),t.on(`presence`,e=>i.setPresence(e));let p=0,m=0,h=0,g=()=>i.setAlertCount(p+m+h);if(t.on(`watchers`,e=>{p=e.length,r.setRegions(e),g()}),t.on(`timers`,e=>{m=e.length,r.setTimers(e),g()}),t.on(`monitors`,e=>{h=e.length,r.setMonitors(e),g()}),t.on(`monitorAlways`,e=>{r.setAlwaysAgents(e)}),t.on(`screenRegion`,e=>{let t=d(e)?null:e;e.active?X.setFrameRegionActive(t):X.setFrameRegion(t)}),t.on(`videoTrack`,e=>{if(!e){a.srcObject=null,X.setVideoSource(null);return}a.srcObject=e,a.play().catch(()=>{}),X.setVideoSource(a)}),t.on(`overviewTrack`,e=>{if(!e){o.srcObject=null,X.setOverviewSource(null);return}o.srcObject=e,o.play().catch(()=>{}),X.setOverviewSource(o)}),t.on(`netStats`,({fps:e,rtt:t})=>i.setNetStats(e,t)),t.on(`screenMeta`,({screen:e,activeDisplay:t})=>{X.setScreen(e),t!==void 0&&i.setActiveDisplay(t)}),t.on(`notification`,e=>Z.show(e)),t.on(`error`,e=>i.flashError(e)),e){let n=!1;t.on(`welcome`,()=>{n||Z.permission!==`granted`||(n=!0,I(e,Z))})}let _=()=>{document.hidden||t.isHealthy()||(c=!0,Q.show(`Reconnecting…`),t.wake())};document.addEventListener(`visibilitychange`,()=>{t.setVisible(!document.hidden),document.hidden||_()}),window.addEventListener(`pageshow`,_),J||i.flashError(`No pairing token in the URL (#t=…). Re-open the link/QR from the agent.`),t.connect()}Pe();function $(e,t){let n=document.createElement(e);return n.className=t,n}function Fe(e){let t=`wd-outdated-banner`;if(document.getElementById(t))return;let n=document.createElement(`div`);n.id=t,n.style.cssText=`position:fixed;top:0;left:0;right:0;z-index:9999;background:#7a2e00;color:#fff;font:14px/1.4 system-ui,sans-serif;padding:10px 40px 10px 14px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,.35)`,n.innerHTML=`⚠️ This WhipDesk agent${e?` (agent ${e})`:``} is out of date. Update from <a style="color:#ffd9a6" href="https://github.com/BinaryBananaLLC/WhipDesk/releases/latest" target="_blank" rel="noreferrer noopener">the latest release</a> or run <code>npm i -g whipdesk@latest</code>.`;let r=document.createElement(`button`);r.textContent=`✕`,r.setAttribute(`aria-label`,`Dismiss`),r.style.cssText=`position:absolute;right:8px;top:6px;background:none;border:none;color:#fff;font-size:16px;cursor:pointer`,r.onclick=()=>n.remove(),n.appendChild(r),document.body.appendChild(n)}