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.
- package/.github/workflows/ci.yml +25 -0
- package/.github/workflows/publish.yml +130 -0
- package/DESIGN-input-acks.md +349 -0
- package/MULTI-AGENT-CHECKLIST.md +5 -5
- package/README-MCP.md +84 -5
- package/README.md +55 -4
- package/demo/README.md +52 -0
- package/demo/capture/capture-env.mjs +108 -0
- package/demo/capture/capture.mjs +634 -0
- package/demo/capture/dispatch-task.mjs +44 -0
- package/demo/capture/mcp-client.mjs +61 -0
- package/demo/capture/native-css.mjs +49 -0
- package/demo/capture/setup-04.mjs +67 -0
- package/demo/capture/setup-sessions.mjs +40 -0
- package/demo/capture/spawn-aurora.mjs +53 -0
- package/demo/capture/tools/agent.sh +30 -0
- package/demo/capture/tools/build.sh +18 -0
- package/demo/capture/tools/metrics.sh +17 -0
- package/docs/hero-overview.svg +119 -0
- package/docs/hero.svg +118 -0
- package/docs/restore-reliability.md +139 -0
- package/docs/session-identity.md +76 -0
- package/docs/switcher-first-ux.md +103 -0
- package/hay/CHANGELOG.md +59 -1
- package/hay/apps/cli/package.json +3 -2
- package/hay/apps/cli/src/index.ts +705 -28
- package/hay/apps/server/src/pty.ts +36 -2
- package/hay/apps/server/src/rooms.ts +525 -34
- package/hay/apps/server/test/rooms.test.ts +411 -1
- package/hay/apps/web/index.html +2 -0
- package/hay/apps/web/package.json +14 -12
- package/hay/apps/web/scripts/run-e2e.mjs +42 -5
- package/hay/apps/web/src/App.tsx +2492 -281
- package/hay/apps/web/src/components/ContextMenu.tsx +109 -0
- package/hay/apps/web/src/components/MobileKeyboard.tsx +135 -14
- package/hay/apps/web/src/components/SecondaryPane.tsx +218 -0
- package/hay/apps/web/src/components/SessionSwitcher.tsx +2525 -0
- package/hay/apps/web/src/setupTests.ts +23 -0
- package/hay/apps/web/src/styles.css +2460 -180
- package/hay/apps/web/src/utils/fileDrop.ts +15 -0
- package/hay/apps/web/src/utils/keyboardProtocol.ts +22 -0
- package/hay/apps/web/src/utils/optimisticEcho.ts +83 -7
- package/hay/apps/web/src/utils/scrollFlywheel.ts +145 -0
- package/hay/apps/web/src/utils/switcherModel.ts +314 -0
- package/hay/apps/web/src/utils/terminalSearch.ts +54 -0
- package/hay/apps/web/src/vite-env.d.ts +3 -0
- package/hay/apps/web/test/SessionSwitcher.test.tsx +324 -0
- package/hay/apps/web/test/fileDrop.test.ts +22 -0
- package/hay/apps/web/test/optimisticEcho.test.ts +17 -0
- package/hay/apps/web/test/scrollFlywheel.test.ts +113 -0
- package/hay/apps/web/test/switcherModel.test.ts +377 -0
- package/hay/apps/web/test/terminalSearch.test.ts +37 -0
- package/hay/apps/web/vite.config.ts +20 -0
- package/hay/package-lock.json +1696 -1171
- package/hay/packages/shared/src/protocol.ts +25 -2
- package/hay-web/assets/index-BohAWpcL.css +1 -0
- package/hay-web/assets/index-DfGT3DuO.js +130 -0
- package/hay-web/assets/index-DfGT3DuO.js.map +1 -0
- package/hay-web/index.html +4 -2
- package/hop +4348 -302
- package/hopa +465 -0
- package/mcp/hop-mcp.js +2878 -122
- package/package.json +7 -4
- package/scripts/claude-session-hook.js +152 -0
- package/scripts/hay-host.js +227 -4
- package/scripts/hop-math.js +316 -0
- package/scripts/salvage-and-restart-host.js +217 -0
- package/skills/hop-manager/SKILL.md +62 -0
- package/skills/hop-session/SKILL.md +42 -0
- package/skills/hopa/SKILL.md +75 -0
- package/tests/agent-skill.unit.test.cjs +60 -0
- package/tests/composer-state.unit.test.cjs +102 -0
- package/tests/hop-mcp.integration.test.cjs +476 -94
- package/tests/hopa-parity.unit.test.cjs +188 -0
- package/tests/readable-echo.unit.test.cjs +94 -0
- package/tests/readable-flush.unit.test.cjs +93 -0
- package/tests/submit-verification.unit.test.cjs +331 -0
- package/tests/terminal-api.integration.test.cjs +301 -13
- package/hay-web/assets/index-BzJB9r_I.js +0 -64
- package/hay-web/assets/index-BzJB9r_I.js.map +0 -1
- package/hay-web/assets/index-DG8Ntznc.css +0 -32
- package/sessions.html +0 -726
package/README-MCP.md
CHANGED
|
@@ -43,7 +43,16 @@ Supported auto-detected clients:
|
|
|
43
43
|
|
|
44
44
|
4. Restart your MCP client.
|
|
45
45
|
|
|
46
|
+
Install the bundled Hop workflow instructions for Claude Code and Codex:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
hop agent-skill install
|
|
50
|
+
```
|
|
51
|
+
|
|
46
52
|
The server auto-connects to the local Hop daemon via `~/.hop2/.tunnel-state`.
|
|
53
|
+
Long-lived MCP clients refresh that file automatically when the local daemon
|
|
54
|
+
restarts on a new port. Explicit `connect_server` and `HOP_API_URL` connections
|
|
55
|
+
remain pinned to the configured endpoint.
|
|
47
56
|
|
|
48
57
|
## Recommended Flow: One Agent Terminal
|
|
49
58
|
|
|
@@ -85,6 +94,50 @@ Safety notes:
|
|
|
85
94
|
- Keep one subagent per terminal (no multiplexing multiple agent CLIs in one PTY).
|
|
86
95
|
- For existing user sessions, verify `agentPermitted` (or set with `hop_set_agent_permission`) before writing input.
|
|
87
96
|
|
|
97
|
+
## Manager Agents: Orchestrating a Fleet
|
|
98
|
+
|
|
99
|
+
A manager agent (any MCP client — Claude Code, Codex, ...) can run a whole
|
|
100
|
+
fleet of subagent terminals through hop. The loop:
|
|
101
|
+
|
|
102
|
+
1. **Spawn** — one call per subagent:
|
|
103
|
+
`hopx_spawn_agent(name="worker-1", cwd="/repo", agent="claude", initial_task="<task>", async=true)`
|
|
104
|
+
→ `{ terminal_id, sessionName, wait_id, completion_contract }`. Hop gives
|
|
105
|
+
each initial task a unique completion contract automatically.
|
|
106
|
+
2. **Wait for anyone** — block on the whole fleet with one call:
|
|
107
|
+
`hopx_wait_any(wait_ids=[...], max_wait_ms=60000)` returns the first
|
|
108
|
+
completed turn(s) and the still-pending set. No round-robin polling.
|
|
109
|
+
A settled result is successful only when `ok=true` and its completion
|
|
110
|
+
contract matched; a mismatch is `contract_failed`. A watch whose window
|
|
111
|
+
merely expired is re-armed under a new wait_id and stays in `pending` (carry
|
|
112
|
+
those forward to the next call). Two distinct timers: each background job's own
|
|
113
|
+
wait window defaults to 15 minutes; `max_wait_ms` on the call is only how
|
|
114
|
+
long that one `hopx_wait_any` invocation blocks (default 30s).
|
|
115
|
+
3. **Read the result** — `hop_read_trajectory(name=<sessionName>)` for the
|
|
116
|
+
full turn (digest mode is context-safe), or use the slimmed wait result
|
|
117
|
+
returned by `hopx_wait_any` directly. Follow-up turns should pass
|
|
118
|
+
`until_reply_regex`; only `reply_matched=true` counts as completion.
|
|
119
|
+
4. **Redispatch** — next task via
|
|
120
|
+
`hopx_agent_turn(terminal_id=..., data="<task>", async=true)`; collect the
|
|
121
|
+
new `wait_id` and go back to step 2.
|
|
122
|
+
5. **Check the room** — `hopx_agents_overview()` any time you lose track:
|
|
123
|
+
who is busy (a live wait job), who is idle, turn counts, and bell counters.
|
|
124
|
+
Diff `bellSeq`/`turnCount` against your previous call to spot a subagent
|
|
125
|
+
asking for attention or finishing turns you didn't drive.
|
|
126
|
+
6. **Interrupt / tear down** — `hopx_agent_turn(wait_id=..., control="interrupt")`
|
|
127
|
+
for a stuck turn; `hop_close_terminal(terminal_id=..., killSession=true)`
|
|
128
|
+
when a worker is done. Never leave orphaned sessions.
|
|
129
|
+
7. **Survive your own restart** — every async dispatch is recorded in the
|
|
130
|
+
durable task ledger. If the manager process dies, start fresh and call
|
|
131
|
+
`hopx_task_ledger`: turns that finished unwatched are reconciled (contract
|
|
132
|
+
verdicts included), still-pending ones can be re-armed with
|
|
133
|
+
`hopx_wait_any(terminal_ids=[...])`. Workers commit to their own fleet/
|
|
134
|
+
branch when using worktree isolation, so no work is ever trapped in a dead
|
|
135
|
+
manager's memory.
|
|
136
|
+
|
|
137
|
+
Guardrails (see MULTI-AGENT-CHECKLIST.md): one subagent per terminal, explicit
|
|
138
|
+
deadlines on every wait, never write into a terminal whose overview state is
|
|
139
|
+
`busy`, and gate destructive commands behind user confirmation.
|
|
140
|
+
|
|
88
141
|
## Shell Commands: `hopx_exec`
|
|
89
142
|
|
|
90
143
|
For plain shell work (not interactive TUIs), `hopx_exec` behaves like a Bash
|
|
@@ -130,8 +183,15 @@ connect_server(base_url="https://hop2.example.com", token="<token>", verify=true
|
|
|
130
183
|
|
|
131
184
|
## Tools
|
|
132
185
|
|
|
133
|
-
|
|
186
|
+
Naming convention: `hop_` tools are atomic operations — one stable primitive
|
|
187
|
+
each. `hopx_` tools are the convenience/specialized layer composed from those
|
|
188
|
+
atomic operations; anything that orchestrates, joins, or wraps multiple
|
|
189
|
+
primitives lives under `hopx_`.
|
|
190
|
+
|
|
191
|
+
Connection (the one un-prefixed tool):
|
|
134
192
|
- `connect_server` — connect to a Hop API base_url (optional token); use for remote hop instances
|
|
193
|
+
|
|
194
|
+
Core tools (`hop_`): stable atomic operations.
|
|
135
195
|
- `hop_server_info` — hop-mcp runtime diagnostics (version, script path, read-mode capabilities)
|
|
136
196
|
- `hop_list_sessions` — list Hop sessions and metadata
|
|
137
197
|
- `hop_list_terminals` — list terminal API sessions (created via `hop_create_terminal` / `hop_attach_terminal`)
|
|
@@ -139,11 +199,11 @@ Core tools (`hop_`): stable atomic operations.
|
|
|
139
199
|
- `hop_attach_terminal` — attach to an existing terminal session by name or internalName
|
|
140
200
|
- `hop_write_terminal` — write raw input to a terminal session
|
|
141
201
|
- `hop_send_key` — send a named keypress (enter, esc, tab, shift_tab, arrows, f1-f12, ctrl+[a-z], ...)
|
|
142
|
-
- `hop_wait_terminal` — wait for output conditions (regex, prompt, idle, agent_done); `async=true` returns a `wait_id`
|
|
143
|
-
- `hop_wait_start` — deprecated legacy alias for `hop_wait_terminal(async=true)`
|
|
202
|
+
- `hop_wait_terminal` — wait for output conditions (regex, prompt, idle, agent_done); `async=true` returns a `wait_id` (`hop_wait_start` was removed; use `hop_wait_terminal(async=true)`)
|
|
144
203
|
- `hop_wait_poll` — poll or await completion of a background wait job
|
|
145
204
|
- `hop_resize_terminal` — resize the terminal PTY
|
|
146
205
|
- `hop_read_terminal` — read terminal output events (default `mode="readable_raw"`); returns a cursor for incremental reads
|
|
206
|
+
- `hop_read_trajectory` — read a Claude Code session's real conversation history from its on-disk transcript (resolved from the hop session name via the SessionStart hook record); context-safe reduced `digest` mode by default, plus `summary`/`list`/`get`/`tail` modes; gated by the per-session agent permission (`hop_set_agent_permission`) and requires the transcript on the same host as the MCP
|
|
147
207
|
- `hop_close_terminal` — detach the terminal API session; optionally kill the underlying hop session
|
|
148
208
|
- `hop_set_agent_permission` — allow or block agent access for a session
|
|
149
209
|
- `hop_list_workspaces` — list available workspaces
|
|
@@ -156,7 +216,21 @@ Core tools (`hop_`): stable atomic operations.
|
|
|
156
216
|
Helper tools (`hopx_`): convenience wrappers built on top of core tools.
|
|
157
217
|
- `hopx_send_and_wait` — single-call send + wait wrapper
|
|
158
218
|
- `hopx_exec` — Bash-tool-style shell execution: command in, clean stdout + `exit_code` out (see section above)
|
|
159
|
-
- `hopx_agent_turn` — single-turn send + wait + mode-aware output, default `mode="auto"`
|
|
219
|
+
- `hopx_agent_turn` — single-turn send + wait + mode-aware output, default `mode="auto"`; optional `until_reply_regex` is a completion contract, and a mismatch returns `contract_failed`
|
|
220
|
+
- `hopx_capture_scrollback` — capture an alternate-screen TUI's scrollback the way a user would: scroll up page-by-page, snapshot each frame, and stitch newly-revealed rows together (live view restored afterward by default); best-effort and lossy for wrapped/redrawn content; requires agent permission on the session
|
|
221
|
+
- `hopx_agents_overview` — fleet status in one call: every agent-created/agent-permitted session with state (running/busy/idle), cwd, foreground program, Claude turn count, bell counters, last activity, and pending wait jobs; `include_user_sessions=false` narrows to agent-created only, `include_ports=true` adds proxy sessions
|
|
222
|
+
- `hopx_wait_any` — race several background waits (wait_ids and/or terminal_ids, auto-starting `until_agent_done` waits); returns the first real completion(s) plus the still-pending set; expired watches are re-armed automatically (new wait_id in `pending`) rather than reported as completions
|
|
223
|
+
- `hopx_spawn_agent` — one-call subagent bring-up: create terminal + launch agent CLI (claude/codex/gemini/custom) + wait until ready + optionally run a first task with an automatic completion contract; `isolation="worktree"` gives the worker its own git worktree + fleet/ branch for overlapping-file fleets
|
|
224
|
+
- the Claude preset uses the `sonnet` model alias and `bypassPermissions` mode by default, and removes inherited Claude Code process credentials before launch, so a delegated worker uses the host's durable `claude auth login` without parking on approval prompts; pass `permission_mode="manual"` for a restricted worker, `args="--model ..."` to choose another model, or an explicit `command` when inherited credentials are intentional
|
|
225
|
+
- the Codex preset runs autonomously and bypasses workspace-trust, approval, and self-update startup gates; Hop rejects detected login, trust, or update screens instead of dispatching a task into them
|
|
226
|
+
- MCP and `hopa spawn --task` wait by default and return the transcript-backed `assistant_reply`; `async=true` is for fleet dispatch through a long-lived MCP connection, while the short-lived `hopa` CLI rejects async waits
|
|
227
|
+
- `hopx_task_ledger` — the durable orchestration ledger (~/.hop2/orchestration): async dispatches with contracts survive manager/MCP restarts; pending entries reconcile lazily from turn counters + transcripts; acknowledge to consume
|
|
228
|
+
|
|
229
|
+
### Reading terminal history
|
|
230
|
+
|
|
231
|
+
- `hop_read_terminal` — recent and incremental output; the day-to-day read primitive (cursor-based deltas).
|
|
232
|
+
- `hop_read_trajectory` — a Claude Code session's full conversation history from its transcript; prefer it when a transcript is available (the default `digest` mode is context-safe and shows far more than the terminal frame).
|
|
233
|
+
- `hopx_capture_scrollback` — scrollback of non-Claude TUIs, or when no transcript file is reachable; best-effort and lossy for wrapped/redrawn content.
|
|
160
234
|
|
|
161
235
|
## Resources
|
|
162
236
|
|
|
@@ -227,11 +301,15 @@ Helper tools (`hopx_`): convenience wrappers built on top of core tools.
|
|
|
227
301
|
|
|
228
302
|
- For long-running conditions, start a background wait with `hop_wait_terminal(terminal_id=<id>, async=true, ...)` (returns `wait_id` immediately) and poll with `hop_wait_poll(wait_id=..., wait=true)`
|
|
229
303
|
- use `consume=true` on poll to remove completed jobs
|
|
230
|
-
- `
|
|
304
|
+
- use `cancel=true` on poll to abort a still-running wait and reclaim it
|
|
305
|
+
- async jobs default to a 15-minute wait window when `max_wait_ms` is not given (synchronous waits default to 30s)
|
|
306
|
+
- `hop_wait_start` was removed; use `hop_wait_terminal(async=true)`
|
|
231
307
|
|
|
232
308
|
### `hopx_send_and_wait`
|
|
233
309
|
|
|
234
310
|
- `hopx_send_and_wait` combines input send + optional enter/key + wait, and defaults to cursor-based delta capture from just before send.
|
|
311
|
+
- gotcha: `press_enter` defaults to **false** here but **true** on `hopx_agent_turn` (when data is provided) — sending text without `press_enter=true` types it but never submits it
|
|
312
|
+
- `clean_text=true` strips ANSI escape codes from captured output (also available on `hopx_agent_turn`)
|
|
235
313
|
- helper defaults are token-thrifty for interactive loops: `capture_max_events=60`, `control_level=none`, `noise_filter=balanced`, `coalesce_ms=350` (unless overridden)
|
|
236
314
|
- for `capture="readable_raw"`, `text_only` defaults to `true`; this condenses wait payloads by returning joined `wait.text`, setting `wait.events=[]`, `wait.eventCount=0`, and preserving prior count in `wait.originalEventCount` (set `text_only=false` to keep full events)
|
|
237
315
|
- `hop_read_terminal` supports deterministic delta reads:
|
|
@@ -245,6 +323,7 @@ Helper tools (`hopx_`): convenience wrappers built on top of core tools.
|
|
|
245
323
|
- in `mode="auto"`, if alternate-screen starts during the same turn, it auto-promotes to `ui` for that response
|
|
246
324
|
- wait-only continuation is supported: omit `data`/`message` and use `control="wait"` to keep waiting on a terminal without sending new input
|
|
247
325
|
- `async=true` starts the turn wait in the background and returns `wait_id` immediately; continue polling or controlling that turn with `hopx_agent_turn(wait_id=..., wait=true|false, control=...)`
|
|
326
|
+
- `until_reply_regex` is a completion contract; a finished turn that does not match returns `status="contract_failed"`, `ok=false`, and a tool error
|
|
248
327
|
- `control="interrupt"` / `control="terminate"` send an explicit interrupt key (default `esc`); `terminate_message` can send a final follow-up after the interrupt
|
|
249
328
|
- `mode="ui"` returns a UI snapshot payload after wait
|
|
250
329
|
- for readable modes, `text_only` defaults to `true` and condenses waits the same way as `hopx_send_and_wait` (set `text_only=false` to keep full wait events)
|
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ Hop gives you secure browser access to local terminals and a built-in MCP server
|
|
|
12
12
|
o_(")(")
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
<p align="center">
|
|
16
|
+
<img src="docs/hero-overview.svg" width="980" alt="One hop runtime, three surfaces: a browser window, an MCP agent panel, and a CLI window all attached to the same live terminal session. Web sharing with presence and take/release control; MCP agents that create, drive, and read sessions; and a CLI that attaches from any terminal." />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
15
19
|
## Why Hop
|
|
16
20
|
|
|
17
21
|
- **Browser terminal from anywhere** — Tunnel through Cloudflare with no port forwarding
|
|
@@ -31,6 +35,14 @@ Most users only need the npm install. Source install is mainly for development o
|
|
|
31
35
|
npm install -g hop2
|
|
32
36
|
```
|
|
33
37
|
|
|
38
|
+
That installs the **nightly** — a build from the latest green `main`, which
|
|
39
|
+
is usually what you want here since almost every commit is a fix. To hold
|
|
40
|
+
still on a version that was deliberately cut and used:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g hop2@stable
|
|
44
|
+
```
|
|
45
|
+
|
|
34
46
|
This installs:
|
|
35
47
|
- `hop` — main CLI / daemon / browser terminal entrypoint
|
|
36
48
|
- `hop-mcp` — MCP server exposing Hop terminals and sessions
|
|
@@ -80,6 +92,10 @@ source ~/.zshrc
|
|
|
80
92
|
hop
|
|
81
93
|
```
|
|
82
94
|
|
|
95
|
+
`hop` is safe to put at the end of your shell rc (`~/.zshrc`): it only launches
|
|
96
|
+
for interactive terminals — non-interactive shells (`ssh host <cmd>`, scp/rsync,
|
|
97
|
+
scripts, cron) and shells already inside a hop session are silently unaffected.
|
|
98
|
+
|
|
83
99
|
First run:
|
|
84
100
|
1. Optionally set a password with `hop password set`
|
|
85
101
|
2. Scan the authenticator QR code
|
|
@@ -93,9 +109,10 @@ Then open the URL from another device, sign in, and pick or create a session.
|
|
|
93
109
|
```bash
|
|
94
110
|
hop
|
|
95
111
|
hop-mcp-setup
|
|
112
|
+
hop agent-skill install
|
|
96
113
|
```
|
|
97
114
|
|
|
98
|
-
Then restart your MCP client. `hop-mcp-setup` auto-detects and configures supported clients such as Claude Code, Claude Desktop, Cursor, Gemini CLI, Codex CLI, VS Code / Copilot, and Antigravity.
|
|
115
|
+
Then restart your MCP client. `hop-mcp-setup` auto-detects and configures supported clients such as Claude Code, Claude Desktop, Cursor, Gemini CLI, Codex CLI, VS Code / Copilot, and Antigravity. `hop agent-skill install` installs Hop's bundled orchestration instructions for Claude Code and Codex.
|
|
99
116
|
|
|
100
117
|
Typical use cases:
|
|
101
118
|
- Launch a dedicated Claude Code / Codex / Gemini terminal and drive it over multiple turns
|
|
@@ -140,7 +157,7 @@ Use one dedicated terminal per subagent.
|
|
|
140
157
|
2. Start the agent CLI with `hop_write_terminal`.
|
|
141
158
|
3. Wait for readiness with `hop_wait_terminal(until_prompt=true)`.
|
|
142
159
|
4. Prefer `hopx_agent_turn(mode="auto")` or `hopx_send_and_wait(...)` for one turn at a time.
|
|
143
|
-
5. For long waits, use `hopx_agent_turn(async=true, ...)` or the lower-level `hop_wait_terminal(async=true, ...)` + `hop_wait_poll(...)` (`hop_wait_start`
|
|
160
|
+
5. For long waits, use `hopx_agent_turn(async=true, ...)` or the lower-level `hop_wait_terminal(async=true, ...)` + `hop_wait_poll(...)` (`hop_wait_start` was removed; use `hop_wait_terminal(async=true)`).
|
|
144
161
|
6. Interrupt with `hop_send_key(key="ctrl_c")` and close with `hop_close_terminal`.
|
|
145
162
|
|
|
146
163
|
Safety tips:
|
|
@@ -172,7 +189,13 @@ hop attach <session>
|
|
|
172
189
|
```
|
|
173
190
|
|
|
174
191
|
This is useful when you want a browser, an MCP client, and a local shell to converge on the same named session.
|
|
175
|
-
Local terminal sessions are persistent by default: they keep running in the Hay host if the local CLI detaches or the Hop daemon restarts.
|
|
192
|
+
Local terminal sessions are persistent by default: they keep running in the Hay host if the local CLI detaches or the Hop daemon restarts. Press `Ctrl+Q` twice inside the local Hop view to kill a session for everyone; `Ctrl+G` detaches and leaves it running.
|
|
193
|
+
|
|
194
|
+
The attached-terminal UI adapts light/dark themes to your terminal and puts the session state at a glance:
|
|
195
|
+
|
|
196
|
+
<p align="center">
|
|
197
|
+
<img src="docs/hero.svg" width="980" alt="The hop terminal UI, annotated: a session chip pinned flush-left, a live state dot (green connected, amber reconnecting, red dropped, purple locked), a scrollback position indicator, keycap-style hints, adaptive light/dark themes, and scrollback search with toast notices." />
|
|
198
|
+
</p>
|
|
176
199
|
|
|
177
200
|
### Use Hop on Mobile
|
|
178
201
|
|
|
@@ -180,9 +203,37 @@ Hop’s mobile UI includes:
|
|
|
180
203
|
|
|
181
204
|
- a terminal-oriented accessory row with `Esc`, `Tab`, `Ctrl`, `Alt`, and arrows
|
|
182
205
|
- a native keyboard button for dictation, spellcheck, and autocomplete
|
|
183
|
-
- a floating menu for keyboard toggle and session switching
|
|
206
|
+
- a floating menu for keyboard toggle and session switching; the switcher starts with user sessions and can toggle to agent sessions or all sessions
|
|
184
207
|
- draggable controls designed for one-handed use
|
|
185
208
|
|
|
209
|
+
### Restore Sessions (incl. Claude Code)
|
|
210
|
+
|
|
211
|
+
A restart kills the underlying processes, but hop can rebuild your sessions and — for Claude Code — resume each conversation where it left off.
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
hop claude-hook install # one-time: lets hop track each session's Claude conversation
|
|
215
|
+
hop restore # recreate saved sessions; claude ones resume their own conversation
|
|
216
|
+
hop restore --dry-run # preview what would be restored
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
How it works: every hop terminal gets a `HOP_SESSION` env var; a Claude Code `SessionStart` hop hook records which conversation is running in each session. On `hop restore`, each session is reopened in its original directory and claude sessions relaunch with `claude --resume <id>` — so even multiple Claude sessions in the *same* directory each come back to their own conversation. Plain-shell sessions reopen a shell in their last directory and replay their last screen: on a graceful shutdown hop saves each session's recent output and, on restore, paints it back above a dim `──── session restored ────` separator before the fresh prompt — so you keep the context even though the old process is gone. `hop status` shows how many sessions can be restored.
|
|
220
|
+
|
|
221
|
+
`hop claude-hook install` adds a `SessionStart` entry to `~/.claude/settings.json` (backed up first); `hop claude-hook remove` reverts it. The hook is a no-op outside hop terminals.
|
|
222
|
+
|
|
223
|
+
### Render LaTeX math (`hop math`)
|
|
224
|
+
|
|
225
|
+
Glance at a formula without leaving the terminal:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
hop math 'e^{i\pi} + 1 = 0'
|
|
229
|
+
hop math '\frac{-b \pm \sqrt{b^2 - 4ac}}{2a}'
|
|
230
|
+
echo '\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}' | hop math
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Agents working inside hop terminals can render formulas for the humans watching by invoking the command themselves — install the bundled skills with `hop agent-skill install` so Claude and Codex sessions know to do this (plus ring the bell when blocked, and keep output phone-friendly).
|
|
234
|
+
|
|
235
|
+
`hop math` renders a **2D Unicode layout** — fractions, roots, sums/limits, sub/superscripts, Greek + operators, and accents — with no dependencies. Inline-image rendering (Kitty/iTerm) is deferred until graphics support lands in the web client (xterm.js): the real payoff is math that renders *inside* a shared hop session, which the re-rendering clients can't show yet, so until then it's Unicode everywhere.
|
|
236
|
+
|
|
186
237
|
## Operations
|
|
187
238
|
|
|
188
239
|
### Logging and History
|
package/demo/README.md
CHANGED
|
@@ -10,6 +10,7 @@ Small reusable pieces for recording real Hop browser footage and driving real Ho
|
|
|
10
10
|
- `static-preview-server.mjs` serves the sanitized demo workspace for preview clips
|
|
11
11
|
- `stitch-demo-video.mjs` concatenates recorded WebM clips into one rough MP4
|
|
12
12
|
- `hop-demo-lib.mjs` holds the shared state, auth, browser, and API helpers
|
|
13
|
+
- `capture/` holds the sanitized product-video capture rig (see [Capture Rig](#capture-rig-democapture) below)
|
|
13
14
|
|
|
14
15
|
The harness uses the local Hop daemon port from `~/.hop2/.tunnel-state` and injects the Hop session cookie directly into Playwright. That avoids interactive login during capture while still exercising the real Hop UI.
|
|
15
16
|
|
|
@@ -97,6 +98,57 @@ PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/stitch-demo-video.mjs \
|
|
|
97
98
|
5. Record a mobile view of the same session
|
|
98
99
|
6. Stitch the clips with `ffmpeg`
|
|
99
100
|
|
|
101
|
+
## Capture Rig (`demo/capture/`)
|
|
102
|
+
|
|
103
|
+
The capture rig records the sanitized, dark-theme live-footage clips used in the Hop product video. It drives the real Hop UI in headless Chrome with two sanitization layers installed before first paint: `/api/sessions` responses are filtered down to the demo session cast, and a document-start MutationObserver rewrites the internal session names (`Aurora2` → `Aurora`, `Lyra2` → `Lyra`, …) in every text node and attribute. Before the kept portion of each clip it asserts a genuinely dark rendered frame (ffmpeg signalstats) and that no identity strings (account email banner, home directory, username) are visible; it aborts rather than save a bad take. Recording starts at page open and the setup head is trimmed off with ffmpeg on save.
|
|
104
|
+
|
|
105
|
+
### Files
|
|
106
|
+
|
|
107
|
+
- `capture/capture.mjs` — the clip driver. `node demo/capture/capture.mjs <clip>` with `<clip>` one of `01-sessions`, `02-agent-live`, `02b-desktop-terminal`, `03-phone-live`, `04-phone-switcher`, `05-presence`, `06-math`, `07-theme`
|
|
108
|
+
- `capture/setup-sessions.mjs` — creates the sanitized shell cast (`Lyra2`, `Nebula2`, `Polaris2`) with a clean `env -i` bash, wiped scrollback, and live ticker output, and seeds the demo workspace tree
|
|
109
|
+
- `capture/setup-04.mjs` — recreates all four demo sessions phone-fit (46x40) for the `04-phone-switcher` clip, with a scripted agent-lookalike ticker in `Aurora2` instead of a real agent
|
|
110
|
+
- `capture/spawn-aurora.mjs` — spawns a real coding agent (default `claude`, override with `HOP_CAPTURE_AGENT`) in session `Aurora2` via the Hop MCP server
|
|
111
|
+
- `capture/dispatch-task.mjs` — sends a narration task to the `Aurora2` agent and waits until the identity banner has scrolled off the visible screen
|
|
112
|
+
- `capture/capture-env.mjs` — shared configuration: paths, env overrides, the demo cast, forbidden identity strings, and `terminals.json` helpers
|
|
113
|
+
- `capture/mcp-client.mjs` — minimal stdio MCP client for `mcp/hop-mcp.js`
|
|
114
|
+
- `capture/native-css.mjs` — document-start CSS that hides all web chrome for the `02b-desktop-terminal` "native terminal" clip
|
|
115
|
+
- `capture/tools/*.sh` — endless sanitized ticker scripts (build, metrics, agent-lookalike) that keep switcher preview cards alive
|
|
116
|
+
|
|
117
|
+
### Prerequisites
|
|
118
|
+
|
|
119
|
+
Same as above (running Hop daemon, Google Chrome, vendored Playwright, Homebrew Node), plus `ffmpeg` and — for the live-agent clips — a `claude`-style agent CLI on `PATH`.
|
|
120
|
+
|
|
121
|
+
### Configuration
|
|
122
|
+
|
|
123
|
+
Everything machine-specific is an env override with a macOS default (see `capture/capture-env.mjs`):
|
|
124
|
+
|
|
125
|
+
- `HOP_HOME` — hop state directory (default `~/.hop2`)
|
|
126
|
+
- `HOP_CAPTURE_ROOT` — sanitized demo tree: workspace, ticker scripts, fake `$HOME`, raw recordings, `terminals.json` (default `/tmp/hop-demo`)
|
|
127
|
+
- `HOP_CAPTURE_OUT` — where finished clips land (default `demo-output/footage/live` in the repo)
|
|
128
|
+
- `HOP_CAPTURE_CHROME`, `HOP_CAPTURE_FFMPEG` — binary paths
|
|
129
|
+
- `HOP_CAPTURE_AGENT` — command launched in `Aurora2` by `spawn-aurora.mjs` (default `claude`)
|
|
130
|
+
- `HOP_CAPTURE_FORBIDDEN` — extra comma-separated strings that must never appear in a kept frame, on top of the built-in home-directory/username/welcome-banner checks
|
|
131
|
+
|
|
132
|
+
### Typical run
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/setup-sessions.mjs
|
|
136
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/spawn-aurora.mjs
|
|
137
|
+
|
|
138
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 01-sessions
|
|
139
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 02-agent-live
|
|
140
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 03-phone-live
|
|
141
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 05-presence
|
|
142
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 06-math
|
|
143
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 07-theme
|
|
144
|
+
|
|
145
|
+
# Clip 04 uses its own phone-fit session setup:
|
|
146
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/setup-04.mjs
|
|
147
|
+
PATH=/opt/homebrew/bin:$PATH /opt/homebrew/bin/node demo/capture/capture.mjs 04-phone-switcher
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Notes on ordering: `02b-desktop-terminal` must run against an agent session whose welcome banner has already scrolled off — dispatch a first narration turn with `dispatch-task.mjs` (or run `02-agent-live` first). A clip that would show a leak, a light frame, or a finished/static screen aborts with `ABORT`/`RESHOOT` instead of saving; rerun it. Rerunning `setup-sessions.mjs` after `setup-04.mjs` restores the desktop-width cast, but `Aurora2` needs `spawn-aurora.mjs` again since `setup-04.mjs` replaces the real agent with a ticker.
|
|
151
|
+
|
|
100
152
|
## Notes
|
|
101
153
|
|
|
102
154
|
- The current scripts are intentionally small. The new `director.mjs` is still a thin orchestrator, not a large demo framework.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Shared configuration for the capture rig (demo/capture/*). Every
|
|
2
|
+
// machine-specific value lives here as an env override with a macOS default,
|
|
3
|
+
// so the individual scripts stay free of hardcoded paths and identities.
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { prepareSanitizedWorkspace } from "../hop-demo-lib.mjs";
|
|
8
|
+
|
|
9
|
+
const HERE = path.dirname(new URL(import.meta.url).pathname);
|
|
10
|
+
export const REPO_ROOT = path.resolve(HERE, "../..");
|
|
11
|
+
|
|
12
|
+
export const CHROME =
|
|
13
|
+
process.env.HOP_CAPTURE_CHROME ||
|
|
14
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
15
|
+
export const FFMPEG = process.env.HOP_CAPTURE_FFMPEG || "/opt/homebrew/bin/ffmpeg";
|
|
16
|
+
|
|
17
|
+
// Root of the sanitized demo tree: workspace/ (session cwd), tools/ (ticker
|
|
18
|
+
// scripts), home/ (fake $HOME for the sanitized shells), video/ (raw
|
|
19
|
+
// Playwright recordings before trim), terminals.json (session registry).
|
|
20
|
+
export const DEMO_ROOT = process.env.HOP_CAPTURE_ROOT || "/tmp/hop-demo";
|
|
21
|
+
export const WORKSPACE = path.join(DEMO_ROOT, "workspace");
|
|
22
|
+
export const TOOLS_DIR = path.join(DEMO_ROOT, "tools");
|
|
23
|
+
export const DEMO_HOME = path.join(DEMO_ROOT, "home");
|
|
24
|
+
export const VID_TMP = path.join(DEMO_ROOT, "video");
|
|
25
|
+
export const TERMINALS_PATH = path.join(DEMO_ROOT, "terminals.json");
|
|
26
|
+
|
|
27
|
+
// Where finished clips land.
|
|
28
|
+
export const OUT =
|
|
29
|
+
process.env.HOP_CAPTURE_OUT || path.join(REPO_ROOT, "demo-output", "footage", "live");
|
|
30
|
+
|
|
31
|
+
// Command launched inside Aurora2 by spawn-aurora.mjs.
|
|
32
|
+
export const AGENT_CMD = process.env.HOP_CAPTURE_AGENT || "claude";
|
|
33
|
+
|
|
34
|
+
// Demo cast, by INTERNAL session name. The capture driver's DOM rewriter
|
|
35
|
+
// shows the display names (Aurora/Lyra/Nebula/Polaris); Vega appears via the
|
|
36
|
+
// 01-sessions rename clip.
|
|
37
|
+
export const ALLOWED = ["Aurora2", "Lyra2", "Nebula2", "Polaris2", "Vega"];
|
|
38
|
+
|
|
39
|
+
// Strings that must NEVER be visible in a kept frame (identity leaks). The
|
|
40
|
+
// defaults cover the account-email welcome banner plus the real home
|
|
41
|
+
// directory and username; add more with HOP_CAPTURE_FORBIDDEN="a,b,c".
|
|
42
|
+
export const SCREEN_FORBIDDEN = [
|
|
43
|
+
"Welcome back",
|
|
44
|
+
os.homedir(),
|
|
45
|
+
os.userInfo().username,
|
|
46
|
+
...(process.env.HOP_CAPTURE_FORBIDDEN || "")
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((s) => s.trim())
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
// Deterministic clean shell for demo sessions: env -i wipes the real
|
|
53
|
+
// environment, --norc/--noprofile skip dotfiles, PS1 is the demo prompt.
|
|
54
|
+
export const sanitizedShell = (prompt = "demo@hop workspace $ ") =>
|
|
55
|
+
`exec env -i HOME=${DEMO_HOME} USER=demo LOGNAME=demo TERM=xterm-256color ` +
|
|
56
|
+
"LANG=en_US.UTF-8 PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin " +
|
|
57
|
+
`PS1='${prompt}' /bin/bash --norc --noprofile`;
|
|
58
|
+
|
|
59
|
+
// Wipe screen AND scrollback (removes the real zsh prompt from history).
|
|
60
|
+
export const WIPE = "printf '\\033[2J\\033[3J\\033[H'";
|
|
61
|
+
|
|
62
|
+
export function loadTerminals({ required = true } = {}) {
|
|
63
|
+
if (!fs.existsSync(TERMINALS_PATH)) {
|
|
64
|
+
if (!required) return {};
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${TERMINALS_PATH} not found — run setup-sessions.mjs (and spawn-aurora.mjs) first`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return JSON.parse(fs.readFileSync(TERMINALS_PATH, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function saveTerminals(map) {
|
|
73
|
+
fs.mkdirSync(DEMO_ROOT, { recursive: true });
|
|
74
|
+
fs.writeFileSync(TERMINALS_PATH, JSON.stringify(map, null, 2));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Seed the sanitized demo tree: the workspace files the agent tours, the
|
|
78
|
+
// ticker scripts the shell sessions run, the fake HOME, and the `hop math`
|
|
79
|
+
// wrapper used by clip 06. Idempotent; never overwrites existing workspace
|
|
80
|
+
// content except the rig-owned tools and wrapper.
|
|
81
|
+
export function seedDemoTree() {
|
|
82
|
+
fs.mkdirSync(DEMO_HOME, { recursive: true });
|
|
83
|
+
fs.mkdirSync(TOOLS_DIR, { recursive: true });
|
|
84
|
+
prepareSanitizedWorkspace(WORKSPACE, { reset: false });
|
|
85
|
+
for (const f of fs.readdirSync(path.join(HERE, "tools"))) {
|
|
86
|
+
fs.copyFileSync(path.join(HERE, "tools", f), path.join(TOOLS_DIR, f));
|
|
87
|
+
}
|
|
88
|
+
const wsScripts = path.join(WORKSPACE, "scripts");
|
|
89
|
+
fs.mkdirSync(wsScripts, { recursive: true });
|
|
90
|
+
fs.copyFileSync(
|
|
91
|
+
path.join(REPO_ROOT, "scripts", "hop-math.js"),
|
|
92
|
+
path.join(wsScripts, "hop-math.js")
|
|
93
|
+
);
|
|
94
|
+
const hopWrapper = path.join(WORKSPACE, "hop");
|
|
95
|
+
fs.writeFileSync(
|
|
96
|
+
hopWrapper,
|
|
97
|
+
`#!/usr/bin/env node
|
|
98
|
+
// hop (demo workspace copy) — \`hop math\` entry point.
|
|
99
|
+
if (process.argv[2] === 'math') {
|
|
100
|
+
const { runMathCli } = require('./scripts/hop-math.js');
|
|
101
|
+
process.exit(runMathCli(process.argv.slice(3)));
|
|
102
|
+
}
|
|
103
|
+
console.error('hop (demo): only \`hop math\` is available in this workspace');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
`
|
|
106
|
+
);
|
|
107
|
+
fs.chmodSync(hopWrapper, 0o755);
|
|
108
|
+
}
|