hop2 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/.hop-tmux.conf +18 -0
  2. package/INTEGRATION.md +92 -0
  3. package/README.md +260 -0
  4. package/hay/CHANGELOG.md +46 -0
  5. package/hay/DEV_NOTES.md +39 -0
  6. package/hay/README.md +290 -0
  7. package/hay/apps/cli/package.json +25 -0
  8. package/hay/apps/cli/src/index.ts +1250 -0
  9. package/hay/apps/cli/tsconfig.json +8 -0
  10. package/hay/apps/server/package.json +37 -0
  11. package/hay/apps/server/src/index.ts +103 -0
  12. package/hay/apps/server/src/lib.ts +161 -0
  13. package/hay/apps/server/src/pty.ts +68 -0
  14. package/hay/apps/server/src/rooms.ts +421 -0
  15. package/hay/apps/server/src/utils.ts +20 -0
  16. package/hay/apps/server/test/rooms.test.ts +146 -0
  17. package/hay/apps/server/tsconfig.json +9 -0
  18. package/hay/apps/web/index.html +12 -0
  19. package/hay/apps/web/package.json +32 -0
  20. package/hay/apps/web/scripts/run-e2e.mjs +130 -0
  21. package/hay/apps/web/src/App.tsx +1302 -0
  22. package/hay/apps/web/src/components/MobileKeyboard.tsx +701 -0
  23. package/hay/apps/web/src/main.tsx +13 -0
  24. package/hay/apps/web/src/setupTests.ts +1 -0
  25. package/hay/apps/web/src/styles.css +1198 -0
  26. package/hay/apps/web/src/utils/optimisticEcho.ts +115 -0
  27. package/hay/apps/web/src/utils/presence.ts +24 -0
  28. package/hay/apps/web/src/vite-env.d.ts +10 -0
  29. package/hay/apps/web/test/optimisticEcho.test.ts +39 -0
  30. package/hay/apps/web/test/presence.test.ts +18 -0
  31. package/hay/apps/web/tsconfig.json +8 -0
  32. package/hay/apps/web/vite.config.ts +13 -0
  33. package/hay/apps/web/vitest.config.ts +12 -0
  34. package/hay/package-lock.json +5988 -0
  35. package/hay/package.json +18 -0
  36. package/hay/packages/shared/package.json +27 -0
  37. package/hay/packages/shared/src/colors.ts +16 -0
  38. package/hay/packages/shared/src/index.ts +2 -0
  39. package/hay/packages/shared/src/protocol.ts +90 -0
  40. package/hay/packages/shared/test/protocol.test.ts +33 -0
  41. package/hay/packages/shared/tsconfig.json +7 -0
  42. package/hay/tsconfig.base.json +15 -0
  43. package/hay-web/assets/index-CTTzApRp.css +32 -0
  44. package/hay-web/assets/index-FDfdolCs.js +60 -0
  45. package/hay-web/assets/index-FDfdolCs.js.map +1 -0
  46. package/hay-web/index.html +13 -0
  47. package/hop +3717 -0
  48. package/package.json +52 -0
  49. package/sessions.html +243 -0
  50. package/tunnel-shell.js +254 -0
@@ -0,0 +1,115 @@
1
+ export type OptimisticEchoOptions = {
2
+ maxPendingMs?: number;
3
+ now?: () => number;
4
+ };
5
+
6
+ export type OptimisticEcho = {
7
+ onInput: (data: string, enabled: boolean) => string;
8
+ reconcileOutput: (data: string) => string;
9
+ reset: () => void;
10
+ getPending: () => string;
11
+ };
12
+
13
+ const DEFAULT_MAX_PENDING_MS = 800;
14
+
15
+ const isPrintable = (char: string) => {
16
+ const code = char.charCodeAt(0);
17
+ return code >= 0x20 && code <= 0x7e;
18
+ };
19
+
20
+ const filterPrintable = (data: string) => {
21
+ let result = "";
22
+ let index = 0;
23
+ while (index < data.length) {
24
+ const char = data[index];
25
+ if (char === "\u001b") {
26
+ index += 1;
27
+ if (data[index] === "[") {
28
+ index += 1;
29
+ while (index < data.length) {
30
+ const code = data.charCodeAt(index);
31
+ if (code >= 0x40 && code <= 0x7e) {
32
+ index += 1;
33
+ break;
34
+ }
35
+ index += 1;
36
+ }
37
+ } else {
38
+ index += 1;
39
+ }
40
+ continue;
41
+ }
42
+ if (isPrintable(char)) {
43
+ result += char;
44
+ }
45
+ index += 1;
46
+ }
47
+ return result;
48
+ };
49
+
50
+ export const createOptimisticEcho = (options: OptimisticEchoOptions = {}): OptimisticEcho => {
51
+ const maxPendingMs = options.maxPendingMs ?? DEFAULT_MAX_PENDING_MS;
52
+ const now = options.now ?? (() => Date.now());
53
+ let pending = "";
54
+ let lastEchoAt = 0;
55
+ let mismatchCount = 0;
56
+
57
+ const onInput = (data: string, enabled: boolean) => {
58
+ if (!enabled) {
59
+ return "";
60
+ }
61
+ const filtered = filterPrintable(data);
62
+ if (!filtered) {
63
+ return "";
64
+ }
65
+ pending += filtered;
66
+ lastEchoAt = now();
67
+ return filtered;
68
+ };
69
+
70
+ const reconcileOutput = (data: string) => {
71
+ if (!pending) {
72
+ return data;
73
+ }
74
+
75
+ if (now() - lastEchoAt > maxPendingMs) {
76
+ pending = "";
77
+ mismatchCount = 0;
78
+ return data;
79
+ }
80
+
81
+ let matched = 0;
82
+ const limit = Math.min(pending.length, data.length);
83
+ while (matched < limit && data[matched] === pending[matched]) {
84
+ matched += 1;
85
+ }
86
+
87
+ if (matched > 0) {
88
+ pending = pending.slice(matched);
89
+ mismatchCount = 0;
90
+ return data.slice(matched);
91
+ }
92
+
93
+ mismatchCount += 1;
94
+ if (mismatchCount >= 2) {
95
+ pending = "";
96
+ mismatchCount = 0;
97
+ }
98
+
99
+ return data;
100
+ };
101
+
102
+ const reset = () => {
103
+ pending = "";
104
+ mismatchCount = 0;
105
+ };
106
+
107
+ const getPending = () => pending;
108
+
109
+ return {
110
+ onInput,
111
+ reconcileOutput,
112
+ reset,
113
+ getPending
114
+ };
115
+ };
@@ -0,0 +1,24 @@
1
+ import type { PresenceClient } from "hay-shared";
2
+
3
+ export const sortPresence = (clients: PresenceClient[], selfId: string | null) => {
4
+ return [...clients].sort((a, b) => {
5
+ if (selfId && a.id === selfId) {
6
+ return -1;
7
+ }
8
+ if (selfId && b.id === selfId) {
9
+ return 1;
10
+ }
11
+ return a.name.localeCompare(b.name);
12
+ });
13
+ };
14
+
15
+ export const activityLabel = (client: PresenceClient) => {
16
+ if (client.typing) {
17
+ return "typing";
18
+ }
19
+ const seconds = (Date.now() - client.lastActive) / 1000;
20
+ if (seconds < 15) {
21
+ return "active";
22
+ }
23
+ return "idle";
24
+ };
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_WS_URL?: string;
5
+ readonly VITE_E2E?: string;
6
+ }
7
+
8
+ interface ImportMeta {
9
+ readonly env: ImportMetaEnv;
10
+ }
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { createOptimisticEcho } from "../src/utils/optimisticEcho";
3
+
4
+ describe("optimistic echo", () => {
5
+ it("echoes printable input and strips from output", () => {
6
+ const echo = createOptimisticEcho();
7
+ const echoed = echo.onInput("hello", true);
8
+ expect(echoed).toBe("hello");
9
+ const output = echo.reconcileOutput("hello");
10
+ expect(output).toBe("");
11
+ expect(echo.getPending()).toBe("");
12
+ });
13
+
14
+ it("handles partial output chunks", () => {
15
+ const echo = createOptimisticEcho();
16
+ echo.onInput("abc", true);
17
+ const first = echo.reconcileOutput("ab");
18
+ expect(first).toBe("");
19
+ const second = echo.reconcileOutput("cX");
20
+ expect(second).toBe("X");
21
+ });
22
+
23
+ it("filters non-printable input", () => {
24
+ const echo = createOptimisticEcho();
25
+ const echoed = echo.onInput("\u001b[A", true);
26
+ expect(echoed).toBe("");
27
+ expect(echo.getPending()).toBe("");
28
+ });
29
+
30
+ it("clears pending after timeout", () => {
31
+ let time = 0;
32
+ const echo = createOptimisticEcho({ now: () => time, maxPendingMs: 10 });
33
+ echo.onInput("abc", true);
34
+ time = 20;
35
+ const output = echo.reconcileOutput("XYZ");
36
+ expect(output).toBe("XYZ");
37
+ expect(echo.getPending()).toBe("");
38
+ });
39
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { sortPresence } from "../src/utils/presence";
3
+
4
+ const makeClient = (id: string, name: string) => ({
5
+ id,
6
+ name,
7
+ color: "#fff",
8
+ typing: false,
9
+ lastActive: Date.now()
10
+ });
11
+
12
+ describe("sortPresence", () => {
13
+ it("keeps self at the top", () => {
14
+ const clients = [makeClient("b", "Blake"), makeClient("a", "Alex")];
15
+ const sorted = sortPresence(clients, "a");
16
+ expect(sorted[0].id).toBe("a");
17
+ });
18
+ });
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["vite/client", "vitest/globals"],
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"]
6
+ },
7
+ "include": ["src", "test", "scripts"]
8
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig(() => ({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173
8
+ },
9
+ build: {
10
+ outDir: "dist",
11
+ sourcemap: true
12
+ }
13
+ }));
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: "jsdom",
8
+ setupFiles: "./src/setupTests.ts",
9
+ globals: true,
10
+ exclude: ["playwright/**"]
11
+ }
12
+ });