hop2 0.11.60 → 0.11.61

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.
@@ -21,7 +21,7 @@ import { enableMathHover } from "./utils/mathLinkProvider";
21
21
  import { getMathTip } from "./utils/mathTooltip";
22
22
  import { scanKeyboardProtocol } from "./utils/keyboardProtocol";
23
23
  import { originalPathHint, pasteableUploadPaths } from "./utils/fileDrop";
24
- import { claimOnAttach, claimOnClick } from "./utils/sizeClaim";
24
+ import { claimOnAttach, claimOnClick, isPlainClick } from "./utils/sizeClaim";
25
25
  import { MobileKeyboard } from "./components/MobileKeyboard";
26
26
  import { SessionSwitcher } from "./components/SessionSwitcher";
27
27
  import { SecondaryPane } from "./components/SecondaryPane";
@@ -1356,6 +1356,9 @@ const App = () => {
1356
1356
  // switch, closing the wall into the session, a click, the Fit button) —
1357
1357
  // a reconnect is the socket healing itself, not a person.
1358
1358
  const deliberateAttachRef = useRef(true);
1359
+ // Where the last press on the terminal landed, so a click can be told
1360
+ // apart from the release of a drag.
1361
+ const clickDownRef = useRef<{ x: number; y: number } | null>(null);
1359
1362
  // A canvas that was HIDDEN can come back showing a stale frame: the wall
1360
1363
  // covers the terminal with visibility:hidden, browsers throttle or drop
1361
1364
  // WebGL contexts for hidden canvases, and xterm only repaints rows it
@@ -2170,6 +2173,8 @@ const App = () => {
2170
2173
 
2171
2174
  termRef.current = terminal;
2172
2175
  fitRef.current = fitAddon;
2176
+ // Debug handle for probes and the console (selection, buffer, size).
2177
+ (window as any).__hopTerm = terminal;
2173
2178
  fitWhenReady();
2174
2179
 
2175
2180
  // Register OSC handlers for sequences xterm.js doesn't fully support
@@ -4603,15 +4608,20 @@ const App = () => {
4603
4608
  <section inert={switcherOpen} style={switcherOpen ? { visibility: "hidden" } : undefined} className={`terminal${visibleStatus === "disconnected" || visibleStatus === "ended" ? " degraded" : ""}`}>
4604
4609
  <div
4605
4610
  className="terminal-frame"
4606
- onClick={() => {
4611
+ onMouseDown={(e) => { clickDownRef.current = { x: e.clientX, y: e.clientY }; }}
4612
+ onClick={(e) => {
4607
4613
  // On mobile, don't focus terminal to prevent system keyboard
4608
4614
  if (!isMobile) {
4609
4615
  termRef.current?.focus();
4610
4616
  }
4611
4617
  // A click here is a human act on this surface, so it may
4612
4618
  // take a size some other window left behind. No-op when
4613
- // the session already fits this viewport.
4614
- claimSizeHere();
4619
+ // the session already fits this viewport — and never for
4620
+ // the click that ends a drag: that was a text selection,
4621
+ // and the resize a claim brings would wipe it.
4622
+ if (isPlainClick(clickDownRef.current, { x: e.clientX, y: e.clientY }, !!termRef.current?.hasSelection())) {
4623
+ claimSizeHere();
4624
+ }
4615
4625
  }}
4616
4626
  onDragEnter={(e) => {
4617
4627
  if (Array.from(e.dataTransfer?.types || []).includes("Files")) {
@@ -39,3 +39,20 @@ export const claimOnClick = (o: {
39
39
  if (!o.active) return true;
40
40
  return o.active.cols !== o.natural.cols || o.active.rows !== o.natural.rows;
41
41
  };
42
+
43
+ /**
44
+ * A click is a click only when the pointer stayed put between press and
45
+ * release and nothing is selected. A drag that ends over the terminal also
46
+ * fires `click` — and that drag was a text selection, which a size claim
47
+ * (a resize) would wipe the moment it finished.
48
+ */
49
+ export const isPlainClick = (
50
+ down: { x: number; y: number } | null,
51
+ up: { x: number; y: number },
52
+ hasSelection: boolean,
53
+ slopPx = 4
54
+ ): boolean => {
55
+ if (hasSelection) return false;
56
+ if (!down) return true;
57
+ return Math.hypot(up.x - down.x, up.y - down.y) <= slopPx;
58
+ };
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { claimOnAttach, claimOnClick } from "../src/utils/sizeClaim";
2
+ import { claimOnAttach, claimOnClick, isPlainClick } from "../src/utils/sizeClaim";
3
3
 
4
4
  describe("claimOnAttach — only a deliberate open takes the size", () => {
5
5
  const base = { viewMode: "fit" as const, visible: true, deliberate: true };
@@ -29,3 +29,14 @@ describe("claimOnClick — a click takes the size only when it changes something
29
29
  expect(claimOnClick({ viewMode: "fit", natural: null, active: null, owned: false })).toBe(false);
30
30
  });
31
31
  });
32
+
33
+ describe("isPlainClick — the release of a drag is not a click", () => {
34
+ it("a press and release in the same place, nothing selected, is a click", () => {
35
+ expect(isPlainClick({ x: 100, y: 100 }, { x: 102, y: 101 }, false)).toBe(true);
36
+ expect(isPlainClick(null, { x: 5, y: 5 }, false)).toBe(true);
37
+ });
38
+ it("a drag, or any release with a selection standing, is not", () => {
39
+ expect(isPlainClick({ x: 100, y: 100 }, { x: 260, y: 100 }, false)).toBe(false);
40
+ expect(isPlainClick({ x: 100, y: 100 }, { x: 100, y: 100 }, true)).toBe(false);
41
+ });
42
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hop2",
3
- "version": "0.11.60",
3
+ "version": "0.11.61",
4
4
  "description": "🐰 Hop into your terminal from anywhere. Secure remote shell access with 2FA via Cloudflare Tunnel. Next-gen rewrite with native terminal sharing.",
5
5
  "main": "hop",
6
6
  "bin": {