hop2 0.11.0 → 0.11.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.
Files changed (82) hide show
  1. package/.github/workflows/ci.yml +25 -0
  2. package/.github/workflows/publish.yml +130 -0
  3. package/DESIGN-input-acks.md +349 -0
  4. package/MULTI-AGENT-CHECKLIST.md +5 -5
  5. package/README-MCP.md +84 -5
  6. package/README.md +55 -4
  7. package/demo/README.md +52 -0
  8. package/demo/capture/capture-env.mjs +108 -0
  9. package/demo/capture/capture.mjs +634 -0
  10. package/demo/capture/dispatch-task.mjs +44 -0
  11. package/demo/capture/mcp-client.mjs +61 -0
  12. package/demo/capture/native-css.mjs +49 -0
  13. package/demo/capture/setup-04.mjs +67 -0
  14. package/demo/capture/setup-sessions.mjs +40 -0
  15. package/demo/capture/spawn-aurora.mjs +53 -0
  16. package/demo/capture/tools/agent.sh +30 -0
  17. package/demo/capture/tools/build.sh +18 -0
  18. package/demo/capture/tools/metrics.sh +17 -0
  19. package/docs/hero-overview.svg +119 -0
  20. package/docs/hero.svg +118 -0
  21. package/docs/restore-reliability.md +139 -0
  22. package/docs/session-identity.md +76 -0
  23. package/docs/switcher-first-ux.md +103 -0
  24. package/hay/CHANGELOG.md +59 -1
  25. package/hay/apps/cli/package.json +3 -2
  26. package/hay/apps/cli/src/index.ts +705 -28
  27. package/hay/apps/server/src/pty.ts +36 -2
  28. package/hay/apps/server/src/rooms.ts +525 -34
  29. package/hay/apps/server/test/rooms.test.ts +411 -1
  30. package/hay/apps/web/index.html +2 -0
  31. package/hay/apps/web/package.json +14 -12
  32. package/hay/apps/web/scripts/run-e2e.mjs +42 -5
  33. package/hay/apps/web/src/App.tsx +2492 -281
  34. package/hay/apps/web/src/components/ContextMenu.tsx +109 -0
  35. package/hay/apps/web/src/components/MobileKeyboard.tsx +135 -14
  36. package/hay/apps/web/src/components/SecondaryPane.tsx +218 -0
  37. package/hay/apps/web/src/components/SessionSwitcher.tsx +2525 -0
  38. package/hay/apps/web/src/setupTests.ts +23 -0
  39. package/hay/apps/web/src/styles.css +2460 -180
  40. package/hay/apps/web/src/utils/fileDrop.ts +15 -0
  41. package/hay/apps/web/src/utils/keyboardProtocol.ts +22 -0
  42. package/hay/apps/web/src/utils/optimisticEcho.ts +83 -7
  43. package/hay/apps/web/src/utils/scrollFlywheel.ts +145 -0
  44. package/hay/apps/web/src/utils/switcherModel.ts +314 -0
  45. package/hay/apps/web/src/utils/terminalSearch.ts +54 -0
  46. package/hay/apps/web/src/vite-env.d.ts +3 -0
  47. package/hay/apps/web/test/SessionSwitcher.test.tsx +324 -0
  48. package/hay/apps/web/test/fileDrop.test.ts +22 -0
  49. package/hay/apps/web/test/optimisticEcho.test.ts +17 -0
  50. package/hay/apps/web/test/scrollFlywheel.test.ts +113 -0
  51. package/hay/apps/web/test/switcherModel.test.ts +377 -0
  52. package/hay/apps/web/test/terminalSearch.test.ts +37 -0
  53. package/hay/apps/web/vite.config.ts +20 -0
  54. package/hay/package-lock.json +1696 -1171
  55. package/hay/packages/shared/src/protocol.ts +25 -2
  56. package/hay-web/assets/index-BohAWpcL.css +1 -0
  57. package/hay-web/assets/index-DfGT3DuO.js +130 -0
  58. package/hay-web/assets/index-DfGT3DuO.js.map +1 -0
  59. package/hay-web/index.html +4 -2
  60. package/hop +4348 -302
  61. package/hopa +465 -0
  62. package/mcp/hop-mcp.js +2878 -122
  63. package/package.json +7 -4
  64. package/scripts/claude-session-hook.js +152 -0
  65. package/scripts/hay-host.js +227 -4
  66. package/scripts/hop-math.js +316 -0
  67. package/scripts/salvage-and-restart-host.js +217 -0
  68. package/skills/hop-manager/SKILL.md +62 -0
  69. package/skills/hop-session/SKILL.md +42 -0
  70. package/skills/hopa/SKILL.md +75 -0
  71. package/tests/agent-skill.unit.test.cjs +60 -0
  72. package/tests/composer-state.unit.test.cjs +102 -0
  73. package/tests/hop-mcp.integration.test.cjs +476 -94
  74. package/tests/hopa-parity.unit.test.cjs +188 -0
  75. package/tests/readable-echo.unit.test.cjs +94 -0
  76. package/tests/readable-flush.unit.test.cjs +93 -0
  77. package/tests/submit-verification.unit.test.cjs +331 -0
  78. package/tests/terminal-api.integration.test.cjs +301 -13
  79. package/hay-web/assets/index-BzJB9r_I.js +0 -64
  80. package/hay-web/assets/index-BzJB9r_I.js.map +0 -1
  81. package/hay-web/assets/index-DG8Ntznc.css +0 -32
  82. package/sessions.html +0 -726
@@ -0,0 +1,61 @@
1
+ // Minimal stdio MCP client for driving mcp/hop-mcp.js from rig scripts
2
+ // (spawn-aurora.mjs, dispatch-task.mjs).
3
+ import { spawn } from "node:child_process";
4
+ import path from "node:path";
5
+ import { REPO_ROOT } from "./capture-env.mjs";
6
+
7
+ export function startMcp(clientName) {
8
+ const mcp = spawn(process.execPath, [path.join(REPO_ROOT, "mcp", "hop-mcp.js")], {
9
+ stdio: ["pipe", "pipe", "pipe"]
10
+ });
11
+ mcp.stderr.on("data", () => {});
12
+ let nextId = 1;
13
+ const pending = new Map();
14
+ let buf = "";
15
+ mcp.stdout.on("data", (chunk) => {
16
+ buf += chunk.toString();
17
+ let i;
18
+ while ((i = buf.indexOf("\n")) !== -1) {
19
+ const line = buf.slice(0, i).trim();
20
+ buf = buf.slice(i + 1);
21
+ if (!line) continue;
22
+ try {
23
+ const msg = JSON.parse(line);
24
+ if (msg.id && pending.has(msg.id)) {
25
+ pending.get(msg.id)(msg);
26
+ pending.delete(msg.id);
27
+ }
28
+ } catch {}
29
+ }
30
+ });
31
+ const rpc = (method, params, timeoutMs = 240000) =>
32
+ new Promise((resolve, reject) => {
33
+ const id = nextId++;
34
+ pending.set(id, resolve);
35
+ mcp.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
36
+ setTimeout(() => {
37
+ if (pending.has(id)) {
38
+ pending.delete(id);
39
+ reject(new Error(`rpc timeout: ${method}`));
40
+ }
41
+ }, timeoutMs);
42
+ });
43
+ const callTool = async (name, args, timeoutMs = 240000) => {
44
+ const res = await rpc("tools/call", { name, arguments: args }, timeoutMs);
45
+ const text = res.result?.content?.[0]?.text ?? "";
46
+ let parsed = null;
47
+ try {
48
+ parsed = JSON.parse(text);
49
+ } catch {}
50
+ return { isError: res.result?.isError === true, text, parsed };
51
+ };
52
+ const init = async () => {
53
+ await rpc("initialize", {
54
+ protocolVersion: "2024-11-05",
55
+ capabilities: {},
56
+ clientInfo: { name: clientName, version: "0" }
57
+ });
58
+ mcp.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
59
+ };
60
+ return { rpc, callTool, init, kill: () => mcp.kill() };
61
+ }
@@ -0,0 +1,49 @@
1
+ // CSS injected at document-start for the 02b "native local terminal" clip:
2
+ // hide ALL web chrome (topbar, drawer/controls, footer, FAB, banners, toasts)
3
+ // and make the terminal frame fill the whole viewport. The .terminal-scroll
4
+ // padding (16px) is kept so text has a natural terminal margin.
5
+ export const NATIVE_TERMINAL_CSS = `
6
+ .topbar, .controls, .terminal-footer, .drawer-toggle, .drawer-overlay,
7
+ .drawer-close, .connection-banner, .terminal-toast, .terminal-find {
8
+ display: none !important;
9
+ }
10
+ .app { padding: 0 !important; }
11
+ .session {
12
+ display: block !important;
13
+ padding: 0 !important;
14
+ gap: 0 !important;
15
+ }
16
+ .terminal { display: flex !important; }
17
+ .terminal-frame {
18
+ height: 100vh !important;
19
+ min-height: 100vh !important;
20
+ border: none !important;
21
+ border-radius: 0 !important;
22
+ box-shadow: none !important;
23
+ animation: none !important;
24
+ }
25
+ .terminal-scroll { scrollbar-width: none !important; }
26
+ .terminal-scroll::-webkit-scrollbar { display: none !important; }
27
+ `;
28
+
29
+ // Playwright addInitScript source that installs the style at document-start
30
+ // (before first paint), so no chrome ever flashes into a recorded frame.
31
+ export const NATIVE_CSS_INIT = `(() => {
32
+ const CSS = ${JSON.stringify(NATIVE_TERMINAL_CSS)};
33
+ let done = false;
34
+ const add = () => {
35
+ if (done) return;
36
+ const root = document.head || document.documentElement;
37
+ if (!root) return;
38
+ done = true;
39
+ const st = document.createElement("style");
40
+ st.textContent = CSS;
41
+ root.appendChild(st);
42
+ };
43
+ add();
44
+ if (!done) {
45
+ new MutationObserver((m, mo) => { add(); if (done) mo.disconnect(); })
46
+ .observe(document, { childList: true, subtree: true });
47
+ document.addEventListener("DOMContentLoaded", add);
48
+ }
49
+ })();`;
@@ -0,0 +1,67 @@
1
+ // Recreate the four sanitized demo sessions for the 04-phone-switcher clip,
2
+ // PHONE-FIT. Every PTY is created at 46x40 AND re-claimed via REST resize
3
+ // after the last write, so the API client is the latest typer and its size
4
+ // wins the election (same trick the capture driver uses for clip 03). No
5
+ // 140-col content ever exists, so the phone view cannot pan/crop the left
6
+ // text edge.
7
+ //
8
+ // Aurora2 runs a scripted agent-lookalike ticker (tools/agent.sh) — NOT a
9
+ // real agent — because clip 04 only shows preview cards plus a brief
10
+ // terminal shot.
11
+ import {
12
+ getHopState, ensureTerminal, execTerminal, resizeTerminal, sleep
13
+ } from "../hop-demo-lib.mjs";
14
+ import {
15
+ WORKSPACE, sanitizedShell, WIPE, saveTerminals, seedDemoTree
16
+ } from "./capture-env.mjs";
17
+
18
+ const state = getHopState();
19
+ seedDemoTree();
20
+
21
+ const COLS = 46, ROWS = 40;
22
+
23
+ const defs = [
24
+ { name: "Lyra2", after: ["ls -1"] },
25
+ { name: "Aurora2", after: ["sh ../tools/agent.sh"] },
26
+ { name: "Nebula2", after: ["sh ../tools/build.sh"] },
27
+ { name: "Polaris2", after: ["sh ../tools/metrics.sh"] }
28
+ ];
29
+
30
+ const out = {};
31
+ for (const def of defs) {
32
+ const t = await ensureTerminal(state, { name: def.name, cwd: WORKSPACE, cols: COLS, rows: ROWS });
33
+ await resizeTerminal(state, t.id, COLS, ROWS);
34
+ await sleep(1400); // let the real zsh prompt appear
35
+ await execTerminal(state, t.id, sanitizedShell("demo@hop $ "));
36
+ await sleep(900);
37
+ await execTerminal(state, t.id, WIPE); // wipe screen + scrollback (removes real prompt)
38
+ await sleep(500);
39
+ for (const cmd of def.after) {
40
+ await execTerminal(state, t.id, cmd);
41
+ await sleep(700);
42
+ }
43
+ // Re-claim the size election AFTER the last write on this terminal.
44
+ await resizeTerminal(state, t.id, COLS, ROWS);
45
+ out[def.name] = { terminalId: t.id, sessionName: t.sessionName || def.name };
46
+ console.log(`ready: ${def.name} -> ${t.id}`);
47
+ }
48
+ saveTerminals(out);
49
+
50
+ // Sanity: every demo PTY must report phone-fit width, and no preview line may
51
+ // exceed COLS characters.
52
+ await sleep(2500);
53
+ const sess = await fetch(`${state.localUrl}/api/sessions`, {
54
+ headers: { Authorization: `Bearer ${state.sessionSecret}` }
55
+ }).then((r) => r.json());
56
+ for (const def of defs) {
57
+ const s = (sess.sessions || []).find((x) => x.name === def.name);
58
+ console.log(`${def.name}: cols=${s?.cols} rows=${s?.rows} live=${s?.live}`);
59
+ const p = await fetch(
60
+ `${state.localUrl}/api/sessions/preview?name=${encodeURIComponent(def.name)}`,
61
+ { headers: { Authorization: `Bearer ${state.sessionSecret}` } }
62
+ ).then((r) => r.json()).catch(() => ({}));
63
+ const maxw = Math.max(0, ...String(p.text || "").split("\n").map((l) => l.length));
64
+ console.log(`${def.name}: preview max line width = ${maxw}`);
65
+ if (maxw > COLS) throw new Error(`ABORT: ${def.name} preview wider than ${COLS} cols`);
66
+ }
67
+ console.log("done");
@@ -0,0 +1,40 @@
1
+ // Create the sanitized demo shell sessions: Lyra2, Nebula2, Polaris2. The
2
+ // "2"-suffixed internal names keep them clear of any real session or stale
3
+ // alias; the capture driver's DOM rewriter displays them as Lyra/Nebula/
4
+ // Polaris. Each gets a deterministic clean bash (env -i) and a scrollback
5
+ // wipe, then Nebula2/Polaris2 start endless colorful tickers so switcher
6
+ // cards look live. Run spawn-aurora.mjs afterwards for the agent session.
7
+ import {
8
+ getHopState, ensureTerminal, execTerminal, resizeTerminal, sleep
9
+ } from "../hop-demo-lib.mjs";
10
+ import {
11
+ WORKSPACE, sanitizedShell, WIPE, loadTerminals, saveTerminals, seedDemoTree
12
+ } from "./capture-env.mjs";
13
+
14
+ const state = getHopState();
15
+ seedDemoTree();
16
+
17
+ const defs = [
18
+ { name: "Lyra2", after: ["ls -1"] },
19
+ { name: "Nebula2", after: ["sh ../tools/build.sh"] },
20
+ { name: "Polaris2", after: ["sh ../tools/metrics.sh"] }
21
+ ];
22
+
23
+ const out = loadTerminals({ required: false });
24
+ for (const def of defs) {
25
+ const t = await ensureTerminal(state, { name: def.name, cwd: WORKSPACE, cols: 140, rows: 40 });
26
+ await resizeTerminal(state, t.id, 140, 40);
27
+ await sleep(1200); // let the real zsh prompt appear
28
+ await execTerminal(state, t.id, sanitizedShell());
29
+ await sleep(900);
30
+ await execTerminal(state, t.id, WIPE); // wipe screen + scrollback (removes real prompt)
31
+ await sleep(500);
32
+ for (const cmd of def.after) {
33
+ await execTerminal(state, t.id, cmd);
34
+ await sleep(700);
35
+ }
36
+ out[def.name] = { terminalId: t.id, sessionName: t.sessionName || def.name };
37
+ console.log(`ready: ${def.name} -> ${t.id}`);
38
+ }
39
+ saveTerminals(out);
40
+ console.log("done");
@@ -0,0 +1,53 @@
1
+ // Spawn a real coding agent in session Aurora2 (displayed as "Aurora") via
2
+ // the Hop MCP server. Wipes scrollback before launch so no real shell prompt
3
+ // survives, accepts a trust prompt if one appears, checks the agent is on
4
+ // screen, and stores terminal info for later dispatch.
5
+ //
6
+ // The agent command defaults to `claude`; override with HOP_CAPTURE_AGENT.
7
+ import { startMcp } from "./mcp-client.mjs";
8
+ import {
9
+ AGENT_CMD, WORKSPACE, loadTerminals, saveTerminals, seedDemoTree
10
+ } from "./capture-env.mjs";
11
+
12
+ seedDemoTree();
13
+ const mcp = startMcp("video-capture");
14
+
15
+ try {
16
+ await mcp.init();
17
+
18
+ const res = await mcp.callTool("hopx_spawn_agent", {
19
+ name: "Aurora2",
20
+ cwd: WORKSPACE,
21
+ agent: "custom",
22
+ command: `clear; printf '\\033[3J'; ${AGENT_CMD}`,
23
+ ready_timeout_ms: 120000,
24
+ cols: 140, rows: 40
25
+ }, 240000);
26
+ if (res.isError || !res.parsed?.terminal_id) {
27
+ console.error("SPAWN FAIL: " + res.text.slice(0, 500));
28
+ process.exit(1);
29
+ }
30
+ const terminalId = res.parsed.terminal_id;
31
+ console.log(`spawned terminal_id=${terminalId} ready=${res.parsed.ready}`);
32
+
33
+ // Screen check: trust prompt? agent visible?
34
+ const agentToken = AGENT_CMD.split(/\s+/)[0].toLowerCase();
35
+ for (let attempt = 0; attempt < 3; attempt++) {
36
+ const ui = await mcp.callTool("hop_read_terminal", { terminal_id: terminalId, mode: "ui" }, 30000);
37
+ const screen = JSON.stringify(ui.parsed?.ui?.lines || ui.text || "").toLowerCase();
38
+ if (screen.includes("do you trust") || screen.includes("trust the files")) {
39
+ console.log("trust prompt detected -> accepting");
40
+ await mcp.callTool("hop_write_terminal", { terminal_id: terminalId, data: "\r" }, 20000);
41
+ await new Promise((r) => setTimeout(r, 4000));
42
+ continue;
43
+ }
44
+ console.log(`agent-on-screen=${screen.includes(agentToken)}`);
45
+ console.log("screen sample: " + screen.slice(0, 400));
46
+ break;
47
+ }
48
+ const info = loadTerminals({ required: false });
49
+ info.Aurora2 = { terminalId, sessionName: res.parsed.sessionName || "Aurora2" };
50
+ saveTerminals(info);
51
+ } finally {
52
+ mcp.kill();
53
+ }
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ # Demo agent-lookalike ticker — endless, sanitized, phone-width (<=44 cols).
3
+ # NOT a real agent: scripted output that reads like one for preview cards.
4
+ C="\033[36m"; G="\033[32m"; M="\033[35m"; D="\033[2m"; B="\033[1m"; R="\033[0m"
5
+ O="\033[38;5;208m"
6
+ printf "${O}✻${R} ${B}agent${R} ${D}· workspace tour${R}\n\n"
7
+ printf "${D}>${R} walk the repo and draft a refactor plan\n\n"
8
+ i=0
9
+ while true; do
10
+ i=$((i+1))
11
+ case $((i % 16)) in
12
+ 0) printf "${G}●${R} Read ${D}README.md${R}\n";;
13
+ 1) printf " ${D}⎿ 38 lines${R}\n";;
14
+ 2) printf "\n${C}✻ Thinking…${R}\n\n";;
15
+ 3) printf " The server keeps one room per\n";;
16
+ 4) printf " session; presence rides the same\n";;
17
+ 5) printf " socket as terminal frames.\n\n";;
18
+ 6) printf "${G}●${R} Read ${D}server/rooms.ts${R}\n";;
19
+ 7) printf " ${D}⎿ 112 lines${R}\n";;
20
+ 8) printf " Rooms elect a size owner — the\n";;
21
+ 9) printf " latest typer wins, so phones\n";;
22
+ 10) printf " never fight the desktop.\n\n";;
23
+ 11) printf "${G}●${R} Grep ${D}\"presence\" · 9 matches${R}\n";;
24
+ 12) printf " Next: split usePresence into\n";;
25
+ 13) printf " transport + view-model layers,\n";;
26
+ 14) printf " then add reconnect backoff.\n\n";;
27
+ 15) printf "${M}◐${R} ${D}streaming…${R}\n";;
28
+ esac
29
+ sleep 0.8
30
+ done
@@ -0,0 +1,18 @@
1
+ #!/bin/sh
2
+ # Demo build/test ticker — endless, colorful, sanitized.
3
+ G="\033[32m"; Y="\033[33m"; C="\033[36m"; D="\033[2m"; B="\033[1m"; R="\033[0m"
4
+ i=0
5
+ printf "${B}vite build ${D}(watch mode)${R}\n\n"
6
+ while true; do
7
+ i=$((i+1))
8
+ ts=$(date +%H:%M:%S)
9
+ case $((i % 6)) in
10
+ 0) printf "${D}${ts}${R} ${C}transform${R} src/components/RoomView.tsx\n";;
11
+ 1) printf "${D}${ts}${R} ${C}transform${R} src/hooks/usePresence.ts\n";;
12
+ 2) printf "${D}${ts}${R} ${G}✓${R} 128 modules transformed in $((180 + i % 140))ms\n";;
13
+ 3) printf "${D}${ts}${R} ${G}✓${R} dist/assets/index.js ${D}142.5 kB │ gzip: 45.9 kB${R}\n";;
14
+ 4) printf "${D}${ts}${R} ${Y}hmr update${R} /src/styles.css\n";;
15
+ 5) printf "${D}${ts}${R} ${G}✓${R} tests: 47 passed ${D}(bells 12, election 9, switcher 26)${R}\n";;
16
+ esac
17
+ sleep 1
18
+ done
@@ -0,0 +1,17 @@
1
+ #!/bin/sh
2
+ # Demo deploy/metrics ticker — endless, sanitized.
3
+ G="\033[32m"; Y="\033[33m"; M="\033[35m"; D="\033[2m"; B="\033[1m"; R="\033[0m"
4
+ i=0
5
+ printf "${B}edge-deploy ${D}watching production${R}\n\n"
6
+ while true; do
7
+ i=$((i+1))
8
+ ts=$(date +%H:%M:%S)
9
+ case $((i % 5)) in
10
+ 0) printf "${D}${ts}${R} ${M}render${R} p50=$((18 + i % 9))ms p99=$((61 + i % 22))ms\n";;
11
+ 1) printf "${D}${ts}${R} ${G}healthy${R} 12/12 instances ${D}us-east, eu-west, ap-south${R}\n";;
12
+ 2) printf "${D}${ts}${R} ${G}✓${R} cdn cache hit $((92 + i % 6)).$((i % 10))%%\n";;
13
+ 3) printf "${D}${ts}${R} ${Y}queue${R} depth=$((i % 4)) ${D}drained in $((3 + i % 5))s${R}\n";;
14
+ 4) printf "${D}${ts}${R} ${G}✓${R} rollout wave $((1 + i % 4))/4 complete\n";;
15
+ esac
16
+ sleep 1
17
+ done
@@ -0,0 +1,119 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="980" height="640" viewBox="0 0 980 640" role="img" aria-label="One hop runtime, three surfaces: a browser window, an MCP agent panel, and a CLI window all attached to the same live terminal session named claude. A legend explains web sharing, MCP-native agent control, and the CLI client.">
2
+ <defs>
3
+ <linearGradient id="canvas" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#16171c"/>
5
+ <stop offset="1" stop-color="#101116"/>
6
+ </linearGradient>
7
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="160%">
8
+ <feDropShadow dx="0" dy="6" stdDeviation="14" flood-color="#000000" flood-opacity="0.45"/>
9
+ </filter>
10
+ <clipPath id="winBrowser"><rect x="40" y="78" width="460" height="296" rx="10"/></clipPath>
11
+ <clipPath id="winMcp"><rect x="540" y="78" width="400" height="186" rx="10"/></clipPath>
12
+ <clipPath id="winCli"><rect x="540" y="292" width="400" height="182" rx="10"/></clipPath>
13
+ </defs>
14
+
15
+ <!-- canvas -->
16
+ <rect x="0.5" y="0.5" width="979" height="639" rx="18" fill="url(#canvas)" stroke="#272831"/>
17
+
18
+ <!-- kicker -->
19
+ <text x="490" y="44" text-anchor="middle" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="12.5" letter-spacing="0.14em" fill="#8d909b">ONE RUNTIME — THE SAME LIVE SESSION IN THE BROWSER, THE TERMINAL, AND UNDER AGENT CONTROL</text>
20
+
21
+ <!-- ═══════════ ① browser window (web client, light theme) ═══════════ -->
22
+ <g filter="url(#shadow)">
23
+ <rect x="40" y="78" width="460" height="296" rx="10" fill="#ffffff" stroke="#d4d5db"/>
24
+ </g>
25
+ <g clip-path="url(#winBrowser)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="11.5">
26
+ <!-- browser chrome -->
27
+ <rect x="40" y="78" width="460" height="30" fill="#e9eaef"/>
28
+ <circle cx="60" cy="93" r="5" fill="#ff5f57"/>
29
+ <circle cx="76" cy="93" r="5" fill="#febc2e"/>
30
+ <circle cx="92" cy="93" r="5" fill="#28c840"/>
31
+ <rect x="130" y="85" width="300" height="16" rx="8" fill="#ffffff" stroke="#d4d5db"/>
32
+ <text x="280" y="96.5" text-anchor="middle" fill="#6e6e73" font-size="10">https://hop.example.com/s/claude/</text>
33
+ <!-- web topbar -->
34
+ <rect x="40" y="108" width="460" height="32" fill="#f2f2f7"/>
35
+ <rect x="54" y="115" width="18" height="18" rx="5" fill="#7c3aed"/>
36
+ <text x="63" y="128" text-anchor="middle" font-size="10">🐰</text>
37
+ <text x="80" y="127.5" fill="#1d1d1f" font-weight="700" font-size="11">Hay</text>
38
+ <rect x="306" y="113" width="84" height="22" rx="11" fill="#ffffff" stroke="#dcdce3"/>
39
+ <text x="318" y="127.5" font-size="10"><tspan fill="#1a7f37">●</tspan><tspan fill="#1d1d1f" font-weight="600"> alice</tspan></text>
40
+ <rect x="396" y="113" width="92" height="22" rx="11" fill="#ffffff" stroke="#7c3aed"/>
41
+ <text x="406" y="127.5" font-size="10"><tspan fill="#7c3aed">●</tspan><tspan fill="#1d1d1f" font-weight="600"> claude</tspan><tspan fill="#6e6e73"> ai</tspan></text>
42
+ <!-- terminal body -->
43
+ <text x="58" y="166"><tspan fill="#6e6e73">$</tspan><tspan fill="#1d1d1f"> npm test</tspan></text>
44
+ <text x="58" y="188"><tspan fill="#1a7f37">✓</tspan><tspan fill="#1d1d1f"> 20 tests passed</tspan><tspan fill="#6e6e73"> (1.2s)</tspan></text>
45
+ <text x="58" y="218"><tspan fill="#6e6e73">$</tspan><tspan fill="#1d1d1f"> claude</tspan></text>
46
+ <text x="58" y="240"><tspan fill="#7c3aed">✻</tspan><tspan fill="#1d1d1f"> Refactoring rooms.ts…</tspan></text>
47
+ <text x="58" y="262" fill="#6e6e73"> ⎿ Updated packages/shared/protocol.ts</text>
48
+ <text x="58" y="292"><tspan fill="#6e6e73">&gt;</tspan></text>
49
+ <rect x="72" y="281" width="7" height="13" fill="#7c3aed" opacity="0.85"/>
50
+ <!-- web status bar -->
51
+ <rect x="40" y="344" width="460" height="30" fill="#e9e9ed"/>
52
+ <rect x="48" y="349" width="62" height="20" rx="5" fill="#7c3aed"/>
53
+ <text x="79" y="363" text-anchor="middle" fill="#ffffff" font-weight="700" font-size="10.5">claude</text>
54
+ <text x="118" y="363" fill="#6e6e73" font-size="10.5">~/code/hop2</text>
55
+ <text x="492" y="363" text-anchor="end" font-size="10.5"><tspan fill="#6e6e73">⌘F find · 2 viewers </tspan><tspan fill="#1a7f37">●</tspan></text>
56
+ </g>
57
+
58
+ <!-- ═══════════ ② MCP agent panel ═══════════ -->
59
+ <g filter="url(#shadow)">
60
+ <rect x="540" y="78" width="400" height="186" rx="10" fill="#111116" stroke="#2c2c31"/>
61
+ </g>
62
+ <g clip-path="url(#winMcp)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="11">
63
+ <rect x="540" y="78" width="400" height="28" fill="#1a1a1f"/>
64
+ <text x="556" y="96.5" fill="#93939b" font-size="10.5">Claude Code — MCP client</text>
65
+ <rect x="848" y="84" width="78" height="17" rx="8.5" fill="none" stroke="#a78bfa"/>
66
+ <text x="887" y="96" text-anchor="middle" fill="#a78bfa" font-size="9.5">hop-mcp</text>
67
+ <text x="556" y="126"><tspan fill="#e6e6e9">▸ hop_create_terminal</tspan><tspan fill="#93939b"> { name: "claude" }</tspan></text>
68
+ <text x="556" y="144"><tspan fill="#3fb950"> ✓</tspan><tspan fill="#93939b"> { id: "t_4f21", sessionName: "claude" }</tspan></text>
69
+ <text x="556" y="172"><tspan fill="#e6e6e9">▸ hopx_exec</tspan><tspan fill="#93939b"> { command: "npm test" }</tspan></text>
70
+ <text x="556" y="190"><tspan fill="#3fb950"> ✓</tspan><tspan fill="#93939b"> exit_code 0 · "20 tests passed"</tspan></text>
71
+ <text x="556" y="218"><tspan fill="#e6e6e9">▸ hop_wait_terminal</tspan><tspan fill="#93939b"> { until_agent_done: true }</tspan></text>
72
+ <text x="556" y="236"><tspan fill="#d29922"> ⧗</tspan><tspan fill="#93939b"> waiting — agent turn in progress…</tspan></text>
73
+ </g>
74
+
75
+ <!-- ═══════════ ③ CLI window (dark theme) ═══════════ -->
76
+ <g filter="url(#shadow)">
77
+ <rect x="540" y="292" width="400" height="182" rx="10" fill="#17181d" stroke="#2e3038"/>
78
+ </g>
79
+ <g clip-path="url(#winCli)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="11">
80
+ <rect x="540" y="292" width="400" height="26" fill="#1f2026"/>
81
+ <circle cx="558" cy="305" r="4.5" fill="#ff5f57"/>
82
+ <circle cx="573" cy="305" r="4.5" fill="#febc2e"/>
83
+ <circle cx="588" cy="305" r="4.5" fill="#28c840"/>
84
+ <text x="740" y="309" text-anchor="middle" fill="#8d909b" font-size="10" font-family="-apple-system, 'Segoe UI', sans-serif">hop attach claude</text>
85
+ <text x="556" y="342"><tspan fill="#3fb950">●</tspan><tspan fill="#93939b"> Attached to 'claude' (3 participants)</tspan></text>
86
+ <text x="556" y="368"><tspan fill="#93939b">&gt;</tspan></text>
87
+ <rect x="570" y="357" width="7" height="13" fill="#a78bfa" opacity="0.85"/>
88
+ <!-- status bar -->
89
+ <rect x="540" y="408" width="400" height="22" fill="#2a2a2e"/>
90
+ <rect x="540" y="408" width="58" height="22" fill="#a78bfa"/>
91
+ <text x="569" y="423" text-anchor="middle" fill="#1d1d1f" font-weight="700" font-size="10.5">claude</text>
92
+ <text x="606" y="423" fill="#93939b" font-size="10.5">~/code/hop2</text>
93
+ <text x="930" y="423" text-anchor="end" font-size="10.5"><tspan fill="#93939b">peers: alice, claude* · </tspan><tspan fill="#3fb950">●</tspan></text>
94
+ <!-- hint bar -->
95
+ <rect x="540" y="430" width="400" height="22" fill="#232327"/>
96
+ <text x="552" y="445" font-size="10.5"><tspan fill="#e6e6e9">⌃G </tspan><tspan fill="#93939b">detach · </tspan><tspan fill="#e6e6e9">⌥F </tspan><tspan fill="#93939b">find · </tspan><tspan fill="#e6e6e9">⌥C </tspan><tspan fill="#93939b">control · </tspan><tspan fill="#e6e6e9">⌥0 </tspan><tspan fill="#93939b">live</tspan></text>
97
+ </g>
98
+
99
+ <!-- numbered badges -->
100
+ <g font-family="-apple-system, 'Segoe UI', sans-serif" font-size="11" font-weight="bold" text-anchor="middle">
101
+ <circle cx="40" cy="94" r="9" fill="#8b6ef0"/><text x="40" y="98" fill="#ffffff">1</text>
102
+ <circle cx="540" cy="94" r="9" fill="#8b6ef0"/><text x="540" y="98" fill="#ffffff">2</text>
103
+ <circle cx="540" cy="308" r="9" fill="#8b6ef0"/><text x="540" y="312" fill="#ffffff">3</text>
104
+ </g>
105
+
106
+ <!-- legend -->
107
+ <g font-family="-apple-system, 'Segoe UI', sans-serif">
108
+ <g font-size="11" font-weight="bold" text-anchor="middle" fill="#ffffff">
109
+ <circle cx="60" cy="516" r="8" fill="#8b6ef0"/><text x="60" y="520">1</text>
110
+ <circle cx="60" cy="556" r="8" fill="#8b6ef0"/><text x="60" y="560">2</text>
111
+ <circle cx="60" cy="596" r="8" fill="#8b6ef0"/><text x="60" y="600">3</text>
112
+ </g>
113
+ <g font-size="13.5" fill="#e8e9ee">
114
+ <text x="78" y="521"><tspan font-weight="600">Share in the browser</tspan><tspan fill="#9a9da8"> — tunneled anywhere, with presence, typing markers, and take/release control.</tspan></text>
115
+ <text x="78" y="561"><tspan font-weight="600">MCP-native agents</tspan><tspan fill="#9a9da8"> — Claude Code, Codex &amp; friends create, drive, and read the same sessions.</tspan></text>
116
+ <text x="78" y="601"><tspan font-weight="600">Attach from any terminal</tspan><tspan fill="#9a9da8"> — detach freely; sessions persist on the hop runtime.</tspan></text>
117
+ </g>
118
+ </g>
119
+ </svg>
package/docs/hero.svg ADDED
@@ -0,0 +1,118 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="980" height="600" viewBox="0 0 980 600" role="img" aria-label="Annotated screenshot of the hop terminal: a dark-themed terminal window with a session chip, live state dot, scroll indicator and keycap hints, next to a light-themed window showing adaptive themes and scrollback search; a legend explains each numbered feature.">
2
+ <defs>
3
+ <linearGradient id="canvas" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#16171c"/>
5
+ <stop offset="1" stop-color="#101116"/>
6
+ </linearGradient>
7
+ <filter id="shadow" x="-20%" y="-20%" width="140%" height="160%">
8
+ <feDropShadow dx="0" dy="6" stdDeviation="14" flood-color="#000000" flood-opacity="0.45"/>
9
+ </filter>
10
+ <clipPath id="winDark"><rect x="48" y="48" width="580" height="300" rx="10"/></clipPath>
11
+ <clipPath id="winLight"><rect x="664" y="96" width="276" height="136" rx="8"/></clipPath>
12
+ </defs>
13
+
14
+ <!-- canvas -->
15
+ <rect x="0.5" y="0.5" width="979" height="599" rx="18" fill="url(#canvas)" stroke="#272831"/>
16
+
17
+ <!-- ======================= dark terminal window ======================= -->
18
+ <g filter="url(#shadow)">
19
+ <rect x="48" y="48" width="580" height="300" rx="10" fill="#17181d" stroke="#2e3038"/>
20
+ </g>
21
+ <g clip-path="url(#winDark)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="13">
22
+ <!-- chrome -->
23
+ <rect x="48" y="48" width="580" height="32" fill="#1f2026"/>
24
+ <circle cx="70" cy="64" r="5.5" fill="#ff5f57"/>
25
+ <circle cx="88" cy="64" r="5.5" fill="#febc2e"/>
26
+ <circle cx="106" cy="64" r="5.5" fill="#28c840"/>
27
+ <text x="338" y="68.5" text-anchor="middle" fill="#8d909b" font-size="12" font-family="-apple-system, 'Segoe UI', sans-serif">hop attach claude — dark</text>
28
+
29
+ <!-- body -->
30
+ <text x="64" y="106"><tspan fill="#93939b">$</tspan><tspan fill="#e6e6e9"> hop attach claude</tspan></text>
31
+ <text x="64" y="128"><tspan fill="#3fb950">●</tspan><tspan fill="#93939b"> Attached to 'claude' (2 participants)</tspan></text>
32
+ <text x="64" y="172"><tspan fill="#a78bfa">✻</tspan><tspan fill="#e6e6e9"> Refactoring rooms.ts…</tspan></text>
33
+ <text x="64" y="194" fill="#93939b"> ⎿ Updated packages/shared/protocol.ts</text>
34
+ <text x="64" y="216" fill="#93939b"> ⎿ Ran vitest — 20 tests passed</text>
35
+ <text x="64" y="260"><tspan fill="#3fb950">✓</tspan><tspan fill="#e6e6e9"> Done. Anything else?</tspan></text>
36
+ <text x="64" y="282"><tspan fill="#93939b">&gt;</tspan><tspan fill="#e6e6e9"> </tspan></text>
37
+ <rect x="80" y="271" width="8" height="14" fill="#a78bfa" opacity="0.85"/>
38
+
39
+ <!-- status bar -->
40
+ <rect x="48" y="300" width="580" height="24" fill="#2a2a2e"/>
41
+ <rect x="48" y="300" width="66" height="24" fill="#a78bfa"/>
42
+ <text x="81" y="316.5" text-anchor="middle" fill="#1d1d1f" font-weight="bold">claude</text>
43
+ <text x="122" y="316.5" fill="#93939b">~/code/hop2</text>
44
+ <text x="612" y="316.5" text-anchor="end"><tspan fill="#e6e6e9">scroll 142/520 · ⌥0 live</tspan><tspan fill="#93939b"> · peers: alice* · </tspan><tspan fill="#3fb950">●</tspan></text>
45
+
46
+ <!-- hint bar -->
47
+ <rect x="48" y="324" width="580" height="24" fill="#232327"/>
48
+ <text x="60" y="340.5"><tspan fill="#e6e6e9">⌃G </tspan><tspan fill="#93939b">detach · </tspan><tspan fill="#e6e6e9">⌃Q </tspan><tspan fill="#93939b">kill · </tspan><tspan fill="#e6e6e9">⌥←→↑↓ </tspan><tspan fill="#93939b">pan · </tspan><tspan fill="#e6e6e9">⌥0 </tspan><tspan fill="#93939b">live · </tspan><tspan fill="#e6e6e9">⌥F </tspan><tspan fill="#93939b">find · </tspan><tspan fill="#e6e6e9">⌥C </tspan><tspan fill="#93939b">control</tspan></text>
49
+ </g>
50
+
51
+ <!-- ======================= light terminal window ======================= -->
52
+ <g filter="url(#shadow)">
53
+ <rect x="664" y="96" width="276" height="136" rx="8" fill="#ffffff" stroke="#d4d5db"/>
54
+ </g>
55
+ <g clip-path="url(#winLight)" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" font-size="11">
56
+ <rect x="664" y="96" width="276" height="26" fill="#e9eaef"/>
57
+ <circle cx="681" cy="109" r="4.5" fill="#ff5f57"/>
58
+ <circle cx="696" cy="109" r="4.5" fill="#febc2e"/>
59
+ <circle cx="711" cy="109" r="4.5" fill="#28c840"/>
60
+ <text x="812" y="113" text-anchor="middle" fill="#7a7d88" font-size="10.5" font-family="-apple-system, 'Segoe UI', sans-serif">hop — light</text>
61
+
62
+ <text x="676" y="140"><tspan fill="#6e6e73">$</tspan><tspan fill="#1d1d1f"> npm run build</tspan></text>
63
+ <text x="676" y="158"><tspan fill="#1a7f37">✓</tspan><tspan fill="#1d1d1f"> built in 0.6s</tspan></text>
64
+
65
+ <!-- status bar -->
66
+ <rect x="664" y="166" width="276" height="22" fill="#e9e9ed"/>
67
+ <rect x="664" y="166" width="48" height="22" fill="#7c3aed"/>
68
+ <text x="688" y="181" text-anchor="middle" fill="#ffffff" font-weight="bold">build</text>
69
+ <text x="718" y="181" fill="#6e6e73">~/code</text>
70
+ <text x="928" y="181" text-anchor="end"><tspan fill="#b45309">reconnecting · ●</tspan></text>
71
+
72
+ <!-- hint bar : search -->
73
+ <rect x="664" y="188" width="276" height="22" fill="#f2f2f7"/>
74
+ <text x="676" y="203"><tspan fill="#7c3aed">/error</tspan><tspan fill="#1d1d1f"> 3/17</tspan><tspan fill="#6e6e73"> · ↓ next · ↑ prev · Esc</tspan></text>
75
+
76
+ <!-- toast strip -->
77
+ <rect x="664" y="210" width="276" height="22" fill="#f2f2f7"/>
78
+ <text x="676" y="225"><tspan fill="#1a7f37">✓ </tspan><tspan fill="#1d1d1f">Sent 12 line(s) to clipboard</tspan></text>
79
+ </g>
80
+
81
+ <!-- ======================= numbered badges ======================= -->
82
+ <g font-family="-apple-system, 'Segoe UI', sans-serif" font-size="11" font-weight="bold" text-anchor="middle">
83
+ <circle cx="44" cy="312" r="9" fill="#8b6ef0"/><text x="44" y="316" fill="#ffffff">1</text>
84
+ <circle cx="632" cy="312" r="9" fill="#8b6ef0"/><text x="632" y="316" fill="#ffffff">2</text>
85
+ <circle cx="466" cy="296" r="9" fill="#8b6ef0"/><text x="466" y="300" fill="#ffffff">3</text>
86
+ <circle cx="300" cy="352" r="9" fill="#8b6ef0"/><text x="300" y="356" fill="#ffffff">4</text>
87
+ <circle cx="662" cy="109" r="9" fill="#8b6ef0"/><text x="662" y="113" fill="#ffffff">5</text>
88
+ <circle cx="942" cy="199" r="9" fill="#8b6ef0"/><text x="942" y="203" fill="#ffffff">6</text>
89
+ </g>
90
+
91
+ <!-- ======================= legend ======================= -->
92
+ <g font-family="-apple-system, 'Segoe UI', sans-serif">
93
+ <g font-size="11" font-weight="bold" text-anchor="middle" fill="#ffffff">
94
+ <circle cx="60" cy="416" r="8" fill="#8b6ef0"/><text x="60" y="420">1</text>
95
+ <circle cx="60" cy="472" r="8" fill="#8b6ef0"/><text x="60" y="476">2</text>
96
+ <circle cx="60" cy="528" r="8" fill="#8b6ef0"/><text x="60" y="532">3</text>
97
+ <circle cx="524" cy="416" r="8" fill="#8b6ef0"/><text x="524" y="420">4</text>
98
+ <circle cx="524" cy="472" r="8" fill="#8b6ef0"/><text x="524" y="476">5</text>
99
+ <circle cx="524" cy="528" r="8" fill="#8b6ef0"/><text x="524" y="532">6</text>
100
+ </g>
101
+ <g font-size="13.5" font-weight="600" fill="#e8e9ee">
102
+ <text x="78" y="421">Session chip</text>
103
+ <text x="78" y="477">Live state dot</text>
104
+ <text x="78" y="533">Scroll indicator</text>
105
+ <text x="542" y="421">Keycap hints</text>
106
+ <text x="542" y="477">Adaptive themes</text>
107
+ <text x="542" y="533">Search &amp; toasts</text>
108
+ </g>
109
+ <g font-size="12.5" fill="#9a9da8">
110
+ <text x="78" y="440">The room name, pinned flush-left as an accent pill.</text>
111
+ <text x="78" y="496">Green connected · amber reconnecting · red dropped · purple locked.</text>
112
+ <text x="78" y="552">Your place in scrollback — ⌥0 snaps back to live output.</text>
113
+ <text x="542" y="440">Keys bright, labels dim; ⌃G detach / ⌃Q kill never truncate away.</text>
114
+ <text x="542" y="496">Light/dark detected from your terminal (OSC 11), or --theme.</text>
115
+ <text x="542" y="552">⌥F search with live match counts; ✓ / ! toasts for feedback.</text>
116
+ </g>
117
+ </g>
118
+ </svg>