remo-cli 2.2.0rc12__tar.gz → 2.2.0rc13__tar.gz
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.
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/PKG-INFO +1 -1
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/web-session-interface.md +10 -7
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppShell.tsx +3 -0
- remo_cli-2.2.0rc13/frontend/src/components/PairToSync.css +71 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/PairToSync.tsx +16 -6
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SettingsPage.tsx +38 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TerminalCard.tsx +11 -4
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TopBar.tsx +25 -6
- remo_cli-2.2.0rc13/frontend/src/state/latency.test.ts +35 -0
- remo_cli-2.2.0rc13/frontend/src/state/latency.ts +69 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/settings.ts +15 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/GhosttyRenderer.ts +12 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/TerminalConnection.ts +37 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/XtermRenderer.ts +17 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/keymap.test.ts +7 -4
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/keymap.ts +6 -7
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/pyproject.toml +1 -1
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/uv.lock +1 -1
- remo_cli-2.2.0rc12/frontend/src/components/PairToSync.css +0 -53
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-analyze/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-checklist/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-clarify/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-constitution/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-converge/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-implement/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-plan/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-specify/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-tasks/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.devcontainer/devcontainer.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.dockerignore +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/dependabot.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/ci.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/provision.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/release.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/smoke-test-cleanup.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/smoke-test.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/teardown.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.gitignore +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/validation_complete.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/.registry +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/README.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/config-template.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/extension.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/git-config.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/feature.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/init-options.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integration.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integrations/claude.manifest.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integrations/speckit.manifest.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/memory/constitution.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/check-prerequisites.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/common.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/create-new-feature.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/setup-plan.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/setup-tasks.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/update-agent-context.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/agent-file-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/checklist-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/constitution-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/plan-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/spec-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/tasks-template.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/workflows/speckit/workflow.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/workflows/workflow-registry.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/AGENTS.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/CLAUDE.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/CONTRIBUTING.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/Dockerfile +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/LICENSE +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/README.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/.ansible-lint +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/README.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/ansible.cfg +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_configure.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_resize.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_site.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_teardown.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/all.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/incus_containers.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/proxmox_containers.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_configure.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_provision.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_resize.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_site.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_teardown.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_bootstrap.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_configure.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_provision.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_resize.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_site.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_teardown.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/inventory/hosts.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/inventory/incus_containers.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_bootstrap.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_configure.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_provision.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_resize.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_site.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_teardown.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/requirements.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/deacon/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/deacon/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/devcontainers/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/docker/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/docker/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/fzf/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/github_cli/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/meta/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/nodejs/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/nodejs/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/meta/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/defaults/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/tasks/main.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/tasks/configure_dev_tools.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/Dockerfile +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/compose.example.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/entrypoint.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/aws.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/hetzner.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/incus.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/install.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/proxmox.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/remo-web-speckit-input.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/remo-web.html +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/index.html +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/package-lock.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/package.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/README.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/dist__ghostty-vt.wasm +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/ghostty-vt.wasm +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/api/client.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppRoot.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppShell.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AwaitingAdoption.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AwaitingAdoption.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/OfflineOverlay.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/OfflineOverlay.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SessionRail.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SessionRail.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SettingsPage.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/ShortcutsModal.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/ShortcutsModal.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TerminalCard.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TopBar.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/WorkspacePane.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/WorkspacePane.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/providerMeta.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/railModel.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/lib/clipboard.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/lib/fullscreen.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/main.tsx +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/discovery.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/fonts.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/health.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/useConsoleKeyboard.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/workspace.test.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/workspace.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/.gitkeep +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/RendererAdapter.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/TerminalConnection.test.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/defaultRenderer.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/test/setup.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/theme/fonts.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/theme/tokens.css +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/tsconfig.json +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/vite.config.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/install.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/maverick.yaml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/playwright.config.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/project-menu.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/remo +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/run.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/scripts/setup-aws-oidc.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/004-proxmox-container-support/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/checklists/requirements.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/contracts/setup-api.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/quickstart-results.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/cli-web-adopt.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/pairing-api.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/setup-api.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/data-model.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/plan.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/quickstart.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/research.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/spec.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/tasks.md +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/__main__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/cp.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/main.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/aws.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/hetzner.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/incus.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/proxmox.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/shell.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/web.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/ansible_runner.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/completion.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/config.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/known_hosts.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/output.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/picker.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/remo_host_client.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/rsync.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/ssh.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/validation.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/version.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/web_adopt.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/capability.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/discovery.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/host.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/session_target.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/aws.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/hetzner.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/incus.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/proxmox.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/hosts.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/pairing.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/setup.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/terminals.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/app.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/check.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/config.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/discovery.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/health.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/logging_config.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/models.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/operator_auth.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/pairing.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/ssh_master.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/state.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/terminal.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/terminal_registry.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/tokens.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/ansible/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/ansible/test_remo_host_idempotency.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/conftest.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/fixtures.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/keyboard-routing.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/mobile.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/reconnect.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/workspace-layout.spec.ts +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/image/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/image/test_docker_image.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/_nine_terminal_fixture.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/orbstack-cloud-init.yml +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/orbstack.sh +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_nine_terminals.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_nine_terminals_soak.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_remo_host_e2e.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_security_rejections.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_terminal_attach.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_web_adopt_e2e.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_web_cli_parity.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/perf/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/perf/test_latency.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_cp.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_main.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_shell.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_web_adopt_cmd.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ansible_runner.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_config.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_known_hosts.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_output.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_picker.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_remo_host_client.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_rsync.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh_controlpath.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh_identity_opts.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_validation.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_version.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_authorize.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_code.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_payload.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_trust.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_push.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_capability.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_discovery.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_session_target.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_aws_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_incus_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/test_ansible_templates.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/test_host_model.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/__init__.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/_pairing_support.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/conftest.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_backpressure.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_check.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_csp.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_discovery.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_health_states.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_hosts_mapping.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_lazy_import.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_log_redaction.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_mint_gating.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_operator_auth.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_pairing.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_pairing_origin.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_api.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_auth.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_dormancy.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_state.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_terminal_resize.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_terminals_api.py +0 -0
- {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_tokens.py +0 -0
|
@@ -137,13 +137,16 @@ they can be stale until something else fetches. Git glyphs only appear on instan
|
|
|
137
137
|
`remo-host` new enough to report git status; see [Upgrade compatibility](#upgrade-compatibility).
|
|
138
138
|
|
|
139
139
|
**Settings** (⚙, top bar; stored in this browser only, FR-034): accent color, terminal font, font
|
|
140
|
-
size, program ligatures, grid display mode (actual-size vs scale-to-fit),
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
140
|
+
size, program ligatures, grid display mode (actual-size vs scale-to-fit), **focus dwell** (how long the
|
|
141
|
+
pointer rests before focus-follows-mouse fires), terminal engine, a **Nerd Font uploader**, and **Pair
|
|
142
|
+
CLI to sync** (mint a re-sync pairing code). Because a browser can't read fonts installed on the
|
|
143
|
+
instance, uploading a patched Nerd Font once registers it via the `FontFace` API (persisted in
|
|
144
|
+
IndexedDB) and offers it as a terminal font — that's how Powerline/Git/devicon glyphs in a prompt or
|
|
145
|
+
Zellij status bar render. Font changes apply live to every open terminal. The top bar shows the
|
|
146
|
+
**WebSocket round-trip latency** (median across open terminals, with the dot green/yellow/red by
|
|
147
|
+
latency), falling back to the service health status when no terminal is connected; an offline overlay
|
|
148
|
+
appears if the service becomes unreachable (terminals reattach automatically when it returns). Press
|
|
149
|
+
**?** for the keyboard-shortcut reference.
|
|
147
150
|
|
|
148
151
|
All fonts are self-hosted (bundled `@fontsource` assets), never fetched from a CDN, so the restrictive
|
|
149
152
|
same-origin CSP (`default-src 'self'`) is satisfied.
|
|
@@ -8,6 +8,7 @@ import type { SessionTarget } from "../api/client";
|
|
|
8
8
|
import { exitBrowserFullscreen } from "../lib/fullscreen";
|
|
9
9
|
import { useDiscovery } from "../state/discovery";
|
|
10
10
|
import { useHealth } from "../state/health";
|
|
11
|
+
import { useLatency } from "../state/latency";
|
|
11
12
|
import { settingsActions, useSettings } from "../state/settings";
|
|
12
13
|
import { useConsoleKeyboard } from "../state/useConsoleKeyboard";
|
|
13
14
|
import { useWorkspace } from "../state/workspace";
|
|
@@ -25,6 +26,7 @@ const NARROW_BREAKPOINT = 820;
|
|
|
25
26
|
export function AppShell(): JSX.Element {
|
|
26
27
|
const discovery = useDiscovery();
|
|
27
28
|
const health = useHealth();
|
|
29
|
+
const latency = useLatency();
|
|
28
30
|
const settings = useSettings();
|
|
29
31
|
const workspace = useWorkspace();
|
|
30
32
|
|
|
@@ -192,6 +194,7 @@ export function AppShell(): JSX.Element {
|
|
|
192
194
|
openCount={workspace.attached.length}
|
|
193
195
|
health={health.status}
|
|
194
196
|
healthDetail={health.detail}
|
|
197
|
+
latencyMs={latency.rttMs}
|
|
195
198
|
refreshing={discovery.isRefreshing}
|
|
196
199
|
onRefresh={() => void discovery.refresh()}
|
|
197
200
|
onSettings={() => setSettingsOpen(true)}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* "Pair CLI to sync" — rendered inline inside a Settings section (no longer a
|
|
2
|
+
* top-bar popover), so the minted-code panel expands in place below the button. */
|
|
3
|
+
.pairsync {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
align-items: flex-start;
|
|
7
|
+
gap: 10px;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.pairsync-btn {
|
|
11
|
+
padding: 7px 12px;
|
|
12
|
+
border-radius: 7px;
|
|
13
|
+
border: 1px solid var(--border-strong, #444);
|
|
14
|
+
background: var(--bg-btn, #24262c);
|
|
15
|
+
color: var(--text-1, #e6e8ec);
|
|
16
|
+
font-size: 13px;
|
|
17
|
+
font-family: inherit;
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
}
|
|
20
|
+
.pairsync-btn:hover {
|
|
21
|
+
background: var(--bg-btn-hover, #2c2f36);
|
|
22
|
+
}
|
|
23
|
+
.pairsync-btn:disabled {
|
|
24
|
+
opacity: 0.6;
|
|
25
|
+
cursor: default;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.pairsync-popover {
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: 380px;
|
|
31
|
+
padding: 12px;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 10px;
|
|
35
|
+
background: var(--bg-header, #1b1d22);
|
|
36
|
+
border: 1px solid var(--border-soft, #333);
|
|
37
|
+
border-radius: 8px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.pairsync-body {
|
|
41
|
+
margin: 0;
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
line-height: 1.5;
|
|
44
|
+
color: var(--text-dim, #b8bcc4);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.pairsync-body code {
|
|
48
|
+
font-family: var(--font-mono, monospace);
|
|
49
|
+
color: var(--text, #e6e8ec);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.pairsync-error {
|
|
53
|
+
margin: 0;
|
|
54
|
+
font-size: 12px;
|
|
55
|
+
line-height: 1.5;
|
|
56
|
+
color: var(--danger, #e06c6c);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.pairsync-close {
|
|
60
|
+
align-self: flex-end;
|
|
61
|
+
background: none;
|
|
62
|
+
border: none;
|
|
63
|
+
color: var(--text-dim, #b8bcc4);
|
|
64
|
+
font-size: 12px;
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
padding: 2px 4px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.pairsync-close:hover {
|
|
70
|
+
color: var(--text, #e6e8ec);
|
|
71
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// "Pair CLI to sync" affordance (012-web-adopt-pairing, US4 / FR-017), shown in
|
|
2
|
+
// Settings once the service has been adopted.
|
|
2
3
|
//
|
|
3
4
|
// Mints a fresh pairing code (origin="resync") through the same lifecycle and
|
|
4
5
|
// operator-auth gate as the awaiting-adoption page, and offers a Copy button.
|
|
5
6
|
// The code value is held only in a ref and is NEVER rendered into the DOM
|
|
6
|
-
// (FR-015/FR-016); opening
|
|
7
|
-
// best-effort (the idle TTL is the backstop).
|
|
7
|
+
// (FR-015/FR-016); opening mints, closing (or unmounting, e.g. leaving Settings)
|
|
8
|
+
// ends the session best-effort (the idle TTL is the backstop).
|
|
8
9
|
|
|
9
10
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
10
11
|
import { ApiError, endPairing, mintPairingCode } from "../api/client";
|
|
@@ -19,6 +20,9 @@ export function PairToSync(): JSX.Element {
|
|
|
19
20
|
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle");
|
|
20
21
|
const codeRef = useRef<string | null>(null);
|
|
21
22
|
const copyResetHandle = useRef<ReturnType<typeof setTimeout>>();
|
|
23
|
+
// Track "open" for the unmount cleanup below without stale-closure issues.
|
|
24
|
+
const openRef = useRef(false);
|
|
25
|
+
openRef.current = open;
|
|
22
26
|
|
|
23
27
|
const close = useCallback(() => {
|
|
24
28
|
setOpen(false);
|
|
@@ -46,6 +50,12 @@ export function PairToSync(): JSX.Element {
|
|
|
46
50
|
}, []);
|
|
47
51
|
|
|
48
52
|
useEffect(() => () => clearTimeout(copyResetHandle.current), []);
|
|
53
|
+
// End the pairing session if Settings is closed with a code still live.
|
|
54
|
+
useEffect(() => () => {
|
|
55
|
+
if (openRef.current) {
|
|
56
|
+
endPairing();
|
|
57
|
+
}
|
|
58
|
+
}, []);
|
|
49
59
|
|
|
50
60
|
const onCopy = useCallback(() => {
|
|
51
61
|
const code = codeRef.current;
|
|
@@ -64,12 +74,12 @@ export function PairToSync(): JSX.Element {
|
|
|
64
74
|
<div className="pairsync">
|
|
65
75
|
<button
|
|
66
76
|
type="button"
|
|
67
|
-
className="
|
|
77
|
+
className="pairsync-btn"
|
|
68
78
|
onClick={open ? close : openAndMint}
|
|
69
79
|
data-testid="pair-to-sync"
|
|
70
80
|
title="Mint a pairing code to run `remo web push` from your workstation"
|
|
71
81
|
>
|
|
72
|
-
|
|
82
|
+
{open ? "Cancel" : "Mint pairing code"}
|
|
73
83
|
</button>
|
|
74
84
|
|
|
75
85
|
{open && (
|
|
@@ -88,7 +98,7 @@ export function PairToSync(): JSX.Element {
|
|
|
88
98
|
) : (
|
|
89
99
|
<button
|
|
90
100
|
type="button"
|
|
91
|
-
className="
|
|
101
|
+
className="pairsync-btn"
|
|
92
102
|
onClick={onCopy}
|
|
93
103
|
disabled={mintState !== "ready"}
|
|
94
104
|
data-testid="pairsync-copy"
|
|
@@ -7,13 +7,16 @@ import { listUploadedFonts, registerUploadedFont } from "../state/fonts";
|
|
|
7
7
|
import {
|
|
8
8
|
ACCENT_OPTIONS,
|
|
9
9
|
FONT_OPTIONS,
|
|
10
|
+
MAX_FOCUS_DWELL_MS,
|
|
10
11
|
MAX_TERM_SIZE,
|
|
12
|
+
MIN_FOCUS_DWELL_MS,
|
|
11
13
|
MIN_TERM_SIZE,
|
|
12
14
|
RENDERER_OPTIONS,
|
|
13
15
|
settingsActions,
|
|
14
16
|
useSettings,
|
|
15
17
|
type FontOption,
|
|
16
18
|
} from "../state/settings";
|
|
19
|
+
import { PairToSync } from "./PairToSync";
|
|
17
20
|
import "./SettingsPage.css";
|
|
18
21
|
|
|
19
22
|
interface SettingsPageProps {
|
|
@@ -156,6 +159,30 @@ export function SettingsPage({ onClose }: SettingsPageProps): JSX.Element {
|
|
|
156
159
|
</div>
|
|
157
160
|
</section>
|
|
158
161
|
|
|
162
|
+
{/* Focus-follows-mouse dwell */}
|
|
163
|
+
<section>
|
|
164
|
+
<div className="settings-row2-head">
|
|
165
|
+
<span className="settings-heading">Focus dwell</span>
|
|
166
|
+
<span className="settings-value">
|
|
167
|
+
{settings.focusDwellMs === 0 ? "instant" : `${settings.focusDwellMs} ms`}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
<p className="settings-sub">
|
|
171
|
+
In a grid, how long the pointer must rest on a tile before it takes focus
|
|
172
|
+
(focus-follows-mouse). Lower is snappier; higher is calmer. 0 = instant.
|
|
173
|
+
</p>
|
|
174
|
+
<input
|
|
175
|
+
type="range"
|
|
176
|
+
min={MIN_FOCUS_DWELL_MS}
|
|
177
|
+
max={MAX_FOCUS_DWELL_MS}
|
|
178
|
+
step={20}
|
|
179
|
+
value={settings.focusDwellMs}
|
|
180
|
+
onChange={(e) => settingsActions.setFocusDwell(Number(e.target.value))}
|
|
181
|
+
className="settings-range"
|
|
182
|
+
data-testid="focus-dwell-range"
|
|
183
|
+
/>
|
|
184
|
+
</section>
|
|
185
|
+
|
|
159
186
|
{/* Size + ligatures */}
|
|
160
187
|
<section className="settings-row2">
|
|
161
188
|
<div className="settings-col">
|
|
@@ -244,6 +271,17 @@ export function SettingsPage({ onClose }: SettingsPageProps): JSX.Element {
|
|
|
244
271
|
})}
|
|
245
272
|
</div>
|
|
246
273
|
</section>
|
|
274
|
+
|
|
275
|
+
{/* Pair CLI to sync (post-adoption re-sync) */}
|
|
276
|
+
<section>
|
|
277
|
+
<div className="settings-heading">Pair CLI to sync</div>
|
|
278
|
+
<p className="settings-sub">
|
|
279
|
+
Mint a one-time code to run <code>remo web push <url></code> from your
|
|
280
|
+
workstation and push registry / host-key updates to this service. The code is copied
|
|
281
|
+
to your clipboard — it is never shown.
|
|
282
|
+
</p>
|
|
283
|
+
<PairToSync />
|
|
284
|
+
</section>
|
|
247
285
|
</div>
|
|
248
286
|
</div>
|
|
249
287
|
</div>
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type SettingsState,
|
|
20
20
|
type TerminalFontOptions,
|
|
21
21
|
} from "../state/settings";
|
|
22
|
+
import { removeLatency, reportLatency } from "../state/latency";
|
|
22
23
|
import type { RendererAdapter } from "../terminal/RendererAdapter";
|
|
23
24
|
import { createDefaultRenderer } from "../terminal/defaultRenderer";
|
|
24
25
|
import { TerminalConnection, type TerminalConnectionState } from "../terminal/TerminalConnection";
|
|
@@ -28,9 +29,6 @@ const DEFAULT_COLS = 80;
|
|
|
28
29
|
const DEFAULT_ROWS = 24;
|
|
29
30
|
/** How much to shrink the terminal font in a grid tile when "scale to fit". */
|
|
30
31
|
const GRID_FIT_SCALE = 0.8;
|
|
31
|
-
/** Focus-follows-mouse dwell: the pointer must REST on a tile this long before
|
|
32
|
-
* it takes focus, so passing through tiles (or a small drift) doesn't steal it. */
|
|
33
|
-
const HOVER_FOCUS_DELAY_MS = 220;
|
|
34
32
|
|
|
35
33
|
const STATE_LABELS: Record<TerminalConnectionState, string> = {
|
|
36
34
|
connecting: "Connecting…",
|
|
@@ -124,6 +122,9 @@ export function TerminalCard({
|
|
|
124
122
|
const lastSentDimsRef = useRef<{ cols: number; rows: number } | null>(null);
|
|
125
123
|
// Pending focus-follows-mouse dwell timer (cleared if the pointer leaves first).
|
|
126
124
|
const hoverTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
125
|
+
// Latest focus-dwell setting, read inside the timer without re-creating the handler.
|
|
126
|
+
const dwellMsRef = useRef(settings.focusDwellMs);
|
|
127
|
+
dwellMsRef.current = settings.focusDwellMs;
|
|
127
128
|
|
|
128
129
|
const [connectionState, setConnectionState] = useState<TerminalConnectionState>("connecting");
|
|
129
130
|
const [needsManualReconnect, setNeedsManualReconnect] = useState(false);
|
|
@@ -244,7 +245,12 @@ export function TerminalCard({
|
|
|
244
245
|
onStateChange: (state) => {
|
|
245
246
|
setConnectionState(state);
|
|
246
247
|
setNeedsManualReconnect(connectionRef.current?.needsManualReconnect ?? false);
|
|
248
|
+
// Only a connected terminal contributes to the header latency median.
|
|
249
|
+
if (state !== "ready") {
|
|
250
|
+
removeLatency(target.id);
|
|
251
|
+
}
|
|
247
252
|
},
|
|
253
|
+
onLatency: (rttMs) => reportLatency(target.id, rttMs),
|
|
248
254
|
});
|
|
249
255
|
connectionRef.current = connection;
|
|
250
256
|
|
|
@@ -272,6 +278,7 @@ export function TerminalCard({
|
|
|
272
278
|
fitRafRef.current = null;
|
|
273
279
|
}
|
|
274
280
|
lastSentDimsRef.current = null;
|
|
281
|
+
removeLatency(target.id);
|
|
275
282
|
unsubscribeInput();
|
|
276
283
|
unsubscribeSelection();
|
|
277
284
|
resizeObserver.disconnect();
|
|
@@ -343,7 +350,7 @@ export function TerminalCard({
|
|
|
343
350
|
hoverTimerRef.current = setTimeout(() => {
|
|
344
351
|
hoverTimerRef.current = null;
|
|
345
352
|
onHoverFocus();
|
|
346
|
-
},
|
|
353
|
+
}, dwellMsRef.current);
|
|
347
354
|
}, [onHoverFocus, clearHoverTimer]);
|
|
348
355
|
|
|
349
356
|
// Cancel a pending dwell when the pointer leaves or the card unmounts.
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// health indicator, refresh, settings, and shortcuts.
|
|
3
3
|
|
|
4
4
|
import type { HealthStatus } from "../state/health";
|
|
5
|
-
import { PairToSync } from "./PairToSync";
|
|
6
5
|
import "./TopBar.css";
|
|
7
6
|
|
|
8
7
|
// "unconfigured" is included for Record exhaustiveness, but in practice the
|
|
@@ -32,11 +31,24 @@ interface TopBarProps {
|
|
|
32
31
|
health: HealthStatus;
|
|
33
32
|
healthDetail: string | null;
|
|
34
33
|
refreshing: boolean;
|
|
34
|
+
/** Median WS round-trip latency (ms) across open terminals, or null if none. */
|
|
35
|
+
latencyMs: number | null;
|
|
35
36
|
onRefresh: () => void;
|
|
36
37
|
onSettings: () => void;
|
|
37
38
|
onShortcuts: () => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
/** Latency → dot color: good (green) / so-so (yellow) / poor (red). */
|
|
42
|
+
function latencyColor(ms: number): string {
|
|
43
|
+
if (ms < 100) {
|
|
44
|
+
return "var(--ok)";
|
|
45
|
+
}
|
|
46
|
+
if (ms < 300) {
|
|
47
|
+
return "var(--warn)";
|
|
48
|
+
}
|
|
49
|
+
return "var(--danger)";
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
export function TopBar({
|
|
41
53
|
showRailToggle,
|
|
42
54
|
railCollapsed,
|
|
@@ -45,10 +57,19 @@ export function TopBar({
|
|
|
45
57
|
health,
|
|
46
58
|
healthDetail,
|
|
47
59
|
refreshing,
|
|
60
|
+
latencyMs,
|
|
48
61
|
onRefresh,
|
|
49
62
|
onSettings,
|
|
50
63
|
onShortcuts,
|
|
51
64
|
}: TopBarProps): JSX.Element {
|
|
65
|
+
// Prefer live WS latency (the real data-path measure); fall back to the
|
|
66
|
+
// health status when no terminal is connected to measure.
|
|
67
|
+
const showLatency = latencyMs != null;
|
|
68
|
+
const dotColor = showLatency ? latencyColor(latencyMs) : HEALTH_COLOR[health];
|
|
69
|
+
const statusLabel = showLatency ? `${Math.round(latencyMs)} ms` : HEALTH_LABEL[health];
|
|
70
|
+
const statusTitle = showLatency
|
|
71
|
+
? "WebSocket round-trip latency (median across open terminals)"
|
|
72
|
+
: (healthDetail ?? HEALTH_LABEL[health]);
|
|
52
73
|
return (
|
|
53
74
|
<header className="topbar">
|
|
54
75
|
{showRailToggle && (
|
|
@@ -82,17 +103,15 @@ export function TopBar({
|
|
|
82
103
|
{openCount} open
|
|
83
104
|
</span>
|
|
84
105
|
|
|
85
|
-
<span className="topbar-health" title={
|
|
86
|
-
<span className="topbar-health-dot" style={{ background:
|
|
87
|
-
<span>{
|
|
106
|
+
<span className="topbar-health" title={statusTitle} data-testid="topbar-status">
|
|
107
|
+
<span className="topbar-health-dot" style={{ background: dotColor }} />
|
|
108
|
+
<span>{statusLabel}</span>
|
|
88
109
|
</span>
|
|
89
110
|
|
|
90
111
|
<button type="button" className="topbar-btn" onClick={onRefresh} data-testid="refresh-button">
|
|
91
112
|
<span className={refreshing ? "rail-spin" : undefined}>⟳</span> Refresh
|
|
92
113
|
</button>
|
|
93
114
|
|
|
94
|
-
<PairToSync />
|
|
95
|
-
|
|
96
115
|
<button
|
|
97
116
|
type="button"
|
|
98
117
|
className="topbar-icon-btn"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { getLatencySnapshot, removeLatency, reportLatency } from "./latency";
|
|
3
|
+
|
|
4
|
+
describe("latency store", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Clear any samples left by a prior test (module singleton).
|
|
7
|
+
for (const id of ["a", "b", "c", "d"]) {
|
|
8
|
+
removeLatency(id);
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("reports the median across connected terminals", () => {
|
|
13
|
+
reportLatency("a", 40);
|
|
14
|
+
reportLatency("b", 80);
|
|
15
|
+
reportLatency("c", 120);
|
|
16
|
+
const snap = getLatencySnapshot();
|
|
17
|
+
expect(snap.rttMs).toBe(80);
|
|
18
|
+
expect(snap.count).toBe(3);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("updates a terminal's latest sample in place", () => {
|
|
22
|
+
reportLatency("a", 40);
|
|
23
|
+
reportLatency("a", 300);
|
|
24
|
+
expect(getLatencySnapshot()).toEqual({ rttMs: 300, count: 1 });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("drops a terminal's sample on removal; null when none remain", () => {
|
|
28
|
+
reportLatency("a", 40);
|
|
29
|
+
reportLatency("b", 80);
|
|
30
|
+
removeLatency("a");
|
|
31
|
+
expect(getLatencySnapshot()).toEqual({ rttMs: 80, count: 1 });
|
|
32
|
+
removeLatency("b");
|
|
33
|
+
expect(getLatencySnapshot()).toEqual({ rttMs: null, count: 0 });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// WebSocket latency store (header indicator).
|
|
2
|
+
//
|
|
3
|
+
// Each connected terminal measures its WS round-trip (ping→pong in
|
|
4
|
+
// TerminalConnection) and reports it here keyed by session-target id. The header
|
|
5
|
+
// shows the MEDIAN across open terminals — since every terminal's WebSocket
|
|
6
|
+
// terminates at the same remo-web service, they all measure the same
|
|
7
|
+
// browser↔service path, so the median is a robust single number. Terminals
|
|
8
|
+
// remove their sample when they disconnect/unmount, so the store only ever holds
|
|
9
|
+
// currently-healthy connections (no freshness timer needed).
|
|
10
|
+
//
|
|
11
|
+
// The cached-snapshot pattern keeps `getSnapshot()` referentially stable between
|
|
12
|
+
// mutations (returning a fresh object each call would loop useSyncExternalStore).
|
|
13
|
+
|
|
14
|
+
import { useSyncExternalStore } from "react";
|
|
15
|
+
|
|
16
|
+
const samples = new Map<string, number>();
|
|
17
|
+
const listeners = new Set<() => void>();
|
|
18
|
+
|
|
19
|
+
export interface LatencySnapshot {
|
|
20
|
+
/** Median WS round-trip in ms across open terminals, or null if none. */
|
|
21
|
+
rttMs: number | null;
|
|
22
|
+
/** How many terminals are currently reporting. */
|
|
23
|
+
count: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let snapshot: LatencySnapshot = { rttMs: null, count: 0 };
|
|
27
|
+
|
|
28
|
+
function recompute(): void {
|
|
29
|
+
if (samples.size === 0) {
|
|
30
|
+
snapshot = { rttMs: null, count: 0 };
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const sorted = [...samples.values()].sort((a, b) => a - b);
|
|
34
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
35
|
+
snapshot = { rttMs: median, count: sorted.length };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function emit(): void {
|
|
39
|
+
for (const listener of listeners) {
|
|
40
|
+
listener();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reportLatency(id: string, rttMs: number): void {
|
|
45
|
+
samples.set(id, rttMs);
|
|
46
|
+
recompute();
|
|
47
|
+
emit();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function removeLatency(id: string): void {
|
|
51
|
+
if (samples.delete(id)) {
|
|
52
|
+
recompute();
|
|
53
|
+
emit();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Test/inspection accessor for the current aggregate. */
|
|
58
|
+
export function getLatencySnapshot(): LatencySnapshot {
|
|
59
|
+
return snapshot;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function subscribe(listener: () => void): () => void {
|
|
63
|
+
listeners.add(listener);
|
|
64
|
+
return () => listeners.delete(listener);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useLatency(): LatencySnapshot {
|
|
68
|
+
return useSyncExternalStore(subscribe, getLatencySnapshot, getLatencySnapshot);
|
|
69
|
+
}
|
|
@@ -69,6 +69,10 @@ export const MAX_TERM_SIZE = 18;
|
|
|
69
69
|
export const MIN_RAIL_WIDTH = 262;
|
|
70
70
|
export const MAX_RAIL_WIDTH = 520;
|
|
71
71
|
const DEFAULT_RAIL_WIDTH = 320;
|
|
72
|
+
/** Focus-follows-mouse dwell (ms): 0 = instant, higher = calmer. */
|
|
73
|
+
export const MIN_FOCUS_DWELL_MS = 0;
|
|
74
|
+
export const MAX_FOCUS_DWELL_MS = 1000;
|
|
75
|
+
const DEFAULT_FOCUS_DWELL_MS = 220;
|
|
72
76
|
|
|
73
77
|
export interface SettingsState {
|
|
74
78
|
accent: string;
|
|
@@ -83,6 +87,8 @@ export interface SettingsState {
|
|
|
83
87
|
nerdFontName: string | null;
|
|
84
88
|
/** Browser terminal engine to back each terminal. */
|
|
85
89
|
renderer: RendererChoice;
|
|
90
|
+
/** Focus-follows-mouse dwell in ms (how long the pointer rests before focus). */
|
|
91
|
+
focusDwellMs: number;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
export interface TerminalFontOptions {
|
|
@@ -101,6 +107,7 @@ const DEFAULTS: SettingsState = {
|
|
|
101
107
|
railCollapsed: false,
|
|
102
108
|
nerdFontName: null,
|
|
103
109
|
renderer: "xterm",
|
|
110
|
+
focusDwellMs: DEFAULT_FOCUS_DWELL_MS,
|
|
104
111
|
};
|
|
105
112
|
|
|
106
113
|
function clamp(n: number, lo: number, hi: number): number {
|
|
@@ -137,6 +144,10 @@ function loadPersisted(): SettingsState {
|
|
|
137
144
|
railCollapsed: typeof c.railCollapsed === "boolean" ? c.railCollapsed : DEFAULTS.railCollapsed,
|
|
138
145
|
nerdFontName: typeof c.nerdFontName === "string" ? c.nerdFontName : null,
|
|
139
146
|
renderer: c.renderer === "ghostty" || c.renderer === "xterm" ? c.renderer : DEFAULTS.renderer,
|
|
147
|
+
focusDwellMs:
|
|
148
|
+
typeof c.focusDwellMs === "number"
|
|
149
|
+
? clamp(Math.round(c.focusDwellMs), MIN_FOCUS_DWELL_MS, MAX_FOCUS_DWELL_MS)
|
|
150
|
+
: DEFAULTS.focusDwellMs,
|
|
140
151
|
};
|
|
141
152
|
} catch (error) {
|
|
142
153
|
console.error("settings: failed to restore from localStorage", error);
|
|
@@ -214,6 +225,10 @@ export const settingsActions = {
|
|
|
214
225
|
toggleRailCollapsed: () => setState({ railCollapsed: !state.railCollapsed }),
|
|
215
226
|
setNerdFontName: (nerdFontName: string | null) => setState({ nerdFontName }),
|
|
216
227
|
setRenderer: (renderer: RendererChoice) => setState({ renderer }),
|
|
228
|
+
setFocusDwell: (focusDwellMs: number) =>
|
|
229
|
+
setState({
|
|
230
|
+
focusDwellMs: clamp(Math.round(focusDwellMs), MIN_FOCUS_DWELL_MS, MAX_FOCUS_DWELL_MS),
|
|
231
|
+
}),
|
|
217
232
|
};
|
|
218
233
|
|
|
219
234
|
export function useSettings(): SettingsState {
|
|
@@ -78,9 +78,20 @@ export class GhosttyRenderer implements RendererAdapter {
|
|
|
78
78
|
return true;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Native `copy` event (⌘C, Edit▸Copy, right-click▸Copy): write the selection
|
|
82
|
+
// synchronously via clipboardData — reliable in Safari (see XtermRenderer).
|
|
83
|
+
private readonly handleCopyEvent = (e: ClipboardEvent): void => {
|
|
84
|
+
const selection = this.getSelection();
|
|
85
|
+
if (selection && e.clipboardData) {
|
|
86
|
+
e.clipboardData.setData("text/plain", selection);
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
81
91
|
open(container: HTMLElement): void {
|
|
82
92
|
this.container = container;
|
|
83
93
|
this.terminal.open(container);
|
|
94
|
+
container.addEventListener("copy", this.handleCopyEvent);
|
|
84
95
|
}
|
|
85
96
|
|
|
86
97
|
write(data: Uint8Array | string): void {
|
|
@@ -149,6 +160,7 @@ export class GhosttyRenderer implements RendererAdapter {
|
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
dispose(): void {
|
|
163
|
+
this.container?.removeEventListener("copy", this.handleCopyEvent);
|
|
152
164
|
this.terminal.dispose();
|
|
153
165
|
}
|
|
154
166
|
}
|
|
@@ -38,6 +38,8 @@ interface ControlMessage {
|
|
|
38
38
|
|
|
39
39
|
const MAX_AUTO_RECONNECT_ATTEMPTS = 3;
|
|
40
40
|
const RECONNECT_BACKOFF_MS = [500, 1500, 3500];
|
|
41
|
+
/** How often to ping the WS to measure round-trip latency (also a keepalive). */
|
|
42
|
+
const PING_INTERVAL_MS = 4000;
|
|
41
43
|
|
|
42
44
|
export interface TerminalConnectionCallbacks {
|
|
43
45
|
onData?: (data: Uint8Array) => void;
|
|
@@ -45,6 +47,8 @@ export interface TerminalConnectionCallbacks {
|
|
|
45
47
|
onExit?: (code: number) => void;
|
|
46
48
|
onError?: (error: TypedError) => void;
|
|
47
49
|
onStateChange?: (state: TerminalConnectionState) => void;
|
|
50
|
+
/** WS round-trip time in ms, from each ping→pong while connected. */
|
|
51
|
+
onLatency?: (rttMs: number) => void;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -72,6 +76,9 @@ export class TerminalConnection {
|
|
|
72
76
|
/** Bumped each attach() so a slow, superseded attach can't assign a socket. */
|
|
73
77
|
private attachGen = 0;
|
|
74
78
|
private wakeListenersBound = false;
|
|
79
|
+
private pingTimer: ReturnType<typeof setInterval> | undefined;
|
|
80
|
+
/** performance.now() when the outstanding ping was sent (null if none). */
|
|
81
|
+
private pingSentAt: number | null = null;
|
|
75
82
|
|
|
76
83
|
constructor(
|
|
77
84
|
sessionTargetId: string,
|
|
@@ -130,6 +137,7 @@ export class TerminalConnection {
|
|
|
130
137
|
async close(): Promise<void> {
|
|
131
138
|
this.clientInitiatedClose = true;
|
|
132
139
|
this.removeWakeListeners();
|
|
140
|
+
this.stopPinging();
|
|
133
141
|
this.clearReconnectTimer();
|
|
134
142
|
const socket = this.socket;
|
|
135
143
|
const terminalId = this.terminalId;
|
|
@@ -278,6 +286,7 @@ export class TerminalConnection {
|
|
|
278
286
|
|
|
279
287
|
socket.onclose = (event: CloseEvent) => {
|
|
280
288
|
this.socket = null;
|
|
289
|
+
this.stopPinging();
|
|
281
290
|
if (this.clientInitiatedClose || event.code === 1000) {
|
|
282
291
|
this.setState("closed");
|
|
283
292
|
return;
|
|
@@ -286,6 +295,28 @@ export class TerminalConnection {
|
|
|
286
295
|
};
|
|
287
296
|
}
|
|
288
297
|
|
|
298
|
+
/** Begin (or restart) the latency ping loop; call once per fresh connection. */
|
|
299
|
+
private startPinging(): void {
|
|
300
|
+
this.stopPinging();
|
|
301
|
+
const sendMeasuredPing = (): void => {
|
|
302
|
+
if (this.socket?.readyState !== WebSocket.OPEN) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.pingSentAt = performance.now();
|
|
306
|
+
this.sendPing();
|
|
307
|
+
};
|
|
308
|
+
sendMeasuredPing(); // one immediately, for a fast first sample
|
|
309
|
+
this.pingTimer = setInterval(sendMeasuredPing, PING_INTERVAL_MS);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private stopPinging(): void {
|
|
313
|
+
if (this.pingTimer !== undefined) {
|
|
314
|
+
clearInterval(this.pingTimer);
|
|
315
|
+
this.pingTimer = undefined;
|
|
316
|
+
}
|
|
317
|
+
this.pingSentAt = null;
|
|
318
|
+
}
|
|
319
|
+
|
|
289
320
|
private handleControlMessage(raw: string): void {
|
|
290
321
|
let message: ControlMessage;
|
|
291
322
|
try {
|
|
@@ -306,6 +337,7 @@ export class TerminalConnection {
|
|
|
306
337
|
// so re-send them now to size the remote terminal to the real surface.
|
|
307
338
|
this.sendControl({ v: 1, type: "resize", cols: this.cols, rows: this.rows });
|
|
308
339
|
this.callbacks.onReady?.();
|
|
340
|
+
this.startPinging();
|
|
309
341
|
break;
|
|
310
342
|
case "exit":
|
|
311
343
|
this.callbacks.onExit?.(message.code ?? 0);
|
|
@@ -320,6 +352,11 @@ export class TerminalConnection {
|
|
|
320
352
|
this.setState("error");
|
|
321
353
|
break;
|
|
322
354
|
case "pong":
|
|
355
|
+
if (this.pingSentAt !== null) {
|
|
356
|
+
const rtt = performance.now() - this.pingSentAt;
|
|
357
|
+
this.pingSentAt = null;
|
|
358
|
+
this.callbacks.onLatency?.(rtt);
|
|
359
|
+
}
|
|
323
360
|
break;
|
|
324
361
|
}
|
|
325
362
|
}
|