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.
Files changed (443) hide show
  1. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/PKG-INFO +1 -1
  2. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/web-session-interface.md +10 -7
  3. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppShell.tsx +3 -0
  4. remo_cli-2.2.0rc13/frontend/src/components/PairToSync.css +71 -0
  5. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/PairToSync.tsx +16 -6
  6. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SettingsPage.tsx +38 -0
  7. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TerminalCard.tsx +11 -4
  8. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TopBar.tsx +25 -6
  9. remo_cli-2.2.0rc13/frontend/src/state/latency.test.ts +35 -0
  10. remo_cli-2.2.0rc13/frontend/src/state/latency.ts +69 -0
  11. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/settings.ts +15 -0
  12. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/GhosttyRenderer.ts +12 -0
  13. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/TerminalConnection.ts +37 -0
  14. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/XtermRenderer.ts +17 -0
  15. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/keymap.test.ts +7 -4
  16. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/keymap.ts +6 -7
  17. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/pyproject.toml +1 -1
  18. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/uv.lock +1 -1
  19. remo_cli-2.2.0rc12/frontend/src/components/PairToSync.css +0 -53
  20. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-analyze/SKILL.md +0 -0
  21. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-checklist/SKILL.md +0 -0
  22. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-clarify/SKILL.md +0 -0
  23. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-constitution/SKILL.md +0 -0
  24. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-converge/SKILL.md +0 -0
  25. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-commit/SKILL.md +0 -0
  26. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-feature/SKILL.md +0 -0
  27. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-initialize/SKILL.md +0 -0
  28. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-remote/SKILL.md +0 -0
  29. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-git-validate/SKILL.md +0 -0
  30. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-implement/SKILL.md +0 -0
  31. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-plan/SKILL.md +0 -0
  32. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-specify/SKILL.md +0 -0
  33. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-tasks/SKILL.md +0 -0
  34. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.claude/skills/speckit-taskstoissues/SKILL.md +0 -0
  35. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.devcontainer/devcontainer.json +0 -0
  36. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.dockerignore +0 -0
  37. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/dependabot.yml +0 -0
  38. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/ci.yml +0 -0
  39. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/provision.yml +0 -0
  40. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/release.yml +0 -0
  41. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/smoke-test-cleanup.yml +0 -0
  42. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/smoke-test.yml +0 -0
  43. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.github/workflows/teardown.yml +0 -0
  44. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.gitignore +0 -0
  45. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/implementation_complete.json +0 -0
  46. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/phase_${{ index }}_complete.json +0 -0
  47. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.maverick/checkpoints/feature/validation_complete.json +0 -0
  48. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/.registry +0 -0
  49. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/README.md +0 -0
  50. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.commit.md +0 -0
  51. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.feature.md +0 -0
  52. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.initialize.md +0 -0
  53. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.remote.md +0 -0
  54. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/commands/speckit.git.validate.md +0 -0
  55. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/config-template.yml +0 -0
  56. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/extension.yml +0 -0
  57. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/git-config.yml +0 -0
  58. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/auto-commit.sh +0 -0
  59. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/create-new-feature.sh +0 -0
  60. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/git-common.sh +0 -0
  61. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/bash/initialize-repo.sh +0 -0
  62. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/auto-commit.ps1 +0 -0
  63. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/create-new-feature.ps1 +0 -0
  64. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/git-common.ps1 +0 -0
  65. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions/git/scripts/powershell/initialize-repo.ps1 +0 -0
  66. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/extensions.yml +0 -0
  67. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/feature.json +0 -0
  68. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/init-options.json +0 -0
  69. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integration.json +0 -0
  70. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integrations/claude.manifest.json +0 -0
  71. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/integrations/speckit.manifest.json +0 -0
  72. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/memory/constitution.md +0 -0
  73. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/check-prerequisites.sh +0 -0
  74. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/common.sh +0 -0
  75. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/create-new-feature.sh +0 -0
  76. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/setup-plan.sh +0 -0
  77. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/setup-tasks.sh +0 -0
  78. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/scripts/bash/update-agent-context.sh +0 -0
  79. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/agent-file-template.md +0 -0
  80. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/checklist-template.md +0 -0
  81. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/constitution-template.md +0 -0
  82. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/plan-template.md +0 -0
  83. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/spec-template.md +0 -0
  84. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/templates/tasks-template.md +0 -0
  85. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/workflows/speckit/workflow.yml +0 -0
  86. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/.specify/workflows/workflow-registry.json +0 -0
  87. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/AGENTS.md +0 -0
  88. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/CLAUDE.md +0 -0
  89. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/CONTRIBUTING.md +0 -0
  90. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/Dockerfile +0 -0
  91. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/LICENSE +0 -0
  92. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/README.md +0 -0
  93. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/.ansible-lint +0 -0
  94. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/README.md +0 -0
  95. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/ansible.cfg +0 -0
  96. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_configure.yml +0 -0
  97. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_resize.yml +0 -0
  98. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_site.yml +0 -0
  99. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/aws_teardown.yml +0 -0
  100. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/all.yml +0 -0
  101. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/incus_containers.yml +0 -0
  102. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/group_vars/proxmox_containers.yml +0 -0
  103. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_configure.yml +0 -0
  104. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_provision.yml +0 -0
  105. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_resize.yml +0 -0
  106. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_site.yml +0 -0
  107. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/hetzner_teardown.yml +0 -0
  108. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_bootstrap.yml +0 -0
  109. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_configure.yml +0 -0
  110. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_provision.yml +0 -0
  111. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_resize.yml +0 -0
  112. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_site.yml +0 -0
  113. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/incus_teardown.yml +0 -0
  114. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/inventory/hosts.yml +0 -0
  115. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/inventory/incus_containers.yml +0 -0
  116. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_bootstrap.yml +0 -0
  117. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_configure.yml +0 -0
  118. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_provision.yml +0 -0
  119. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_resize.yml +0 -0
  120. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_site.yml +0 -0
  121. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/proxmox_teardown.yml +0 -0
  122. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/requirements.yml +0 -0
  123. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/defaults/main.yml +0 -0
  124. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/ebs.yml +0 -0
  125. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/ec2.yml +0 -0
  126. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/main.yml +0 -0
  127. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/patch_manager.yml +0 -0
  128. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/preflight.yml +0 -0
  129. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/aws_server/tasks/security_group.yml +0 -0
  130. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/deacon/defaults/main.yml +0 -0
  131. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/deacon/tasks/main.yml +0 -0
  132. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/devcontainers/tasks/main.yml +0 -0
  133. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/docker/handlers/main.yml +0 -0
  134. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/docker/tasks/main.yml +0 -0
  135. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/fzf/tasks/main.yml +0 -0
  136. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/github_cli/tasks/main.yml +0 -0
  137. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/hetzner_server/defaults/main.yml +0 -0
  138. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/hetzner_server/tasks/main.yml +0 -0
  139. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/defaults/main.yml +0 -0
  140. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/handlers/main.yml +0 -0
  141. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_bootstrap/tasks/main.yml +0 -0
  142. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/defaults/main.yml +0 -0
  143. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/handlers/main.yml +0 -0
  144. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/meta/main.yml +0 -0
  145. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/tasks/main.yml +0 -0
  146. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container/tasks/preflight.yml +0 -0
  147. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/defaults/main.yml +0 -0
  148. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/meta/main.yml +0 -0
  149. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/incus_container_teardown/tasks/main.yml +0 -0
  150. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/nodejs/defaults/main.yml +0 -0
  151. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/nodejs/tasks/main.yml +0 -0
  152. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/defaults/main.yml +0 -0
  153. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/meta/main.yml +0 -0
  154. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_bootstrap/tasks/main.yml +0 -0
  155. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/defaults/main.yml +0 -0
  156. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/handlers/main.yml +0 -0
  157. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/meta/main.yml +0 -0
  158. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/tasks/main.yml +0 -0
  159. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container/tasks/preflight.yml +0 -0
  160. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/defaults/main.yml +0 -0
  161. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/meta/main.yml +0 -0
  162. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/proxmox_container_teardown/tasks/main.yml +0 -0
  163. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/defaults/main.yml +0 -0
  164. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/tasks/main.yml +0 -0
  165. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/devshell.sh.j2 +0 -0
  166. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/project-launch.sh.j2 +0 -0
  167. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/project-menu.sh.j2 +0 -0
  168. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/user_setup/templates/remo-host.sh.j2 +0 -0
  169. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/defaults/main.yml +0 -0
  170. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/tasks/main.yml +0 -0
  171. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/templates/config.kdl.j2 +0 -0
  172. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/roles/zellij/templates/layouts/minimal.kdl.j2 +0 -0
  173. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/ansible/tasks/configure_dev_tools.yml +0 -0
  174. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/Dockerfile +0 -0
  175. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/compose.example.yml +0 -0
  176. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docker/entrypoint.sh +0 -0
  177. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/aws.md +0 -0
  178. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/hetzner.md +0 -0
  179. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/incus.md +0 -0
  180. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/install.sh +0 -0
  181. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/proxmox.md +0 -0
  182. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/remo-web-speckit-input.md +0 -0
  183. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/docs/remo-web.html +0 -0
  184. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/index.html +0 -0
  185. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/package-lock.json +0 -0
  186. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/package.json +0 -0
  187. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/README.md +0 -0
  188. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/dist__ghostty-vt.wasm +0 -0
  189. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/public/ghostty-vt.wasm +0 -0
  190. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/scripts/copy-ghostty-wasm.mjs +0 -0
  191. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/api/client.ts +0 -0
  192. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppRoot.tsx +0 -0
  193. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AppShell.css +0 -0
  194. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AwaitingAdoption.css +0 -0
  195. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/AwaitingAdoption.tsx +0 -0
  196. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/OfflineOverlay.css +0 -0
  197. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/OfflineOverlay.tsx +0 -0
  198. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SessionRail.css +0 -0
  199. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SessionRail.tsx +0 -0
  200. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/SettingsPage.css +0 -0
  201. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/ShortcutsModal.css +0 -0
  202. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/ShortcutsModal.tsx +0 -0
  203. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TerminalCard.css +0 -0
  204. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/TopBar.css +0 -0
  205. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/WorkspacePane.css +0 -0
  206. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/WorkspacePane.tsx +0 -0
  207. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/providerMeta.ts +0 -0
  208. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/components/railModel.ts +0 -0
  209. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/lib/clipboard.ts +0 -0
  210. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/lib/fullscreen.ts +0 -0
  211. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/main.tsx +0 -0
  212. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/discovery.ts +0 -0
  213. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/fonts.ts +0 -0
  214. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/health.ts +0 -0
  215. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/useConsoleKeyboard.ts +0 -0
  216. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/workspace.test.ts +0 -0
  217. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/state/workspace.ts +0 -0
  218. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/.gitkeep +0 -0
  219. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/RendererAdapter.ts +0 -0
  220. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/TerminalConnection.test.ts +0 -0
  221. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/terminal/defaultRenderer.ts +0 -0
  222. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/test/setup.ts +0 -0
  223. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/theme/fonts.ts +0 -0
  224. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/src/theme/tokens.css +0 -0
  225. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/tsconfig.json +0 -0
  226. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/frontend/vite.config.ts +0 -0
  227. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/install.sh +0 -0
  228. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/maverick.yaml +0 -0
  229. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/playwright.config.ts +0 -0
  230. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/project-menu.sh +0 -0
  231. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/remo +0 -0
  232. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/run.sh +0 -0
  233. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/scripts/setup-aws-oidc.sh +0 -0
  234. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/checklists/requirements.md +0 -0
  235. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/checklists/validation.md +0 -0
  236. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/contracts/playbook-interface.yaml +0 -0
  237. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/contracts/role-interface.yaml +0 -0
  238. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/data-model.md +0 -0
  239. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/plan.md +0 -0
  240. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/quickstart.md +0 -0
  241. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/research.md +0 -0
  242. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/spec.md +0 -0
  243. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/001-bootstrap-incus-host/tasks.md +0 -0
  244. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/checklists/requirements.md +0 -0
  245. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/contracts/playbook-interface.yaml +0 -0
  246. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/contracts/role-interface.yaml +0 -0
  247. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/data-model.md +0 -0
  248. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/plan.md +0 -0
  249. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/quickstart.md +0 -0
  250. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/research.md +0 -0
  251. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/spec.md +0 -0
  252. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/002-incus-container-support/tasks.md +0 -0
  253. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/checklists/requirements.md +0 -0
  254. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/data-model.md +0 -0
  255. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/plan.md +0 -0
  256. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/quickstart.md +0 -0
  257. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/research.md +0 -0
  258. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/spec.md +0 -0
  259. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/003-python-cli-rewrite/tasks.md +0 -0
  260. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/004-proxmox-container-support/spec.md +0 -0
  261. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/checklists/requirements.md +0 -0
  262. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/contracts/cli-surface.md +0 -0
  263. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/data-model.md +0 -0
  264. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/plan.md +0 -0
  265. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/quickstart.md +0 -0
  266. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/research.md +0 -0
  267. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/spec.md +0 -0
  268. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/005-provider-snapshots/tasks.md +0 -0
  269. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/checklists/requirements.md +0 -0
  270. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/remo-host-protocol.md +0 -0
  271. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/rest-api.md +0 -0
  272. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/contracts/terminal-websocket.md +0 -0
  273. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/data-model.md +0 -0
  274. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/plan.md +0 -0
  275. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/quickstart.md +0 -0
  276. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/research.md +0 -0
  277. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/spec.md +0 -0
  278. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/010-web-session-interface/tasks.md +0 -0
  279. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/checklists/requirements.md +0 -0
  280. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/contracts/cli-web-adopt.md +0 -0
  281. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/contracts/setup-api.md +0 -0
  282. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/data-model.md +0 -0
  283. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/plan.md +0 -0
  284. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/quickstart-results.md +0 -0
  285. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/quickstart.md +0 -0
  286. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/research.md +0 -0
  287. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/spec.md +0 -0
  288. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/011-web-adopt/tasks.md +0 -0
  289. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/cli-web-adopt.md +0 -0
  290. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/pairing-api.md +0 -0
  291. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/contracts/setup-api.md +0 -0
  292. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/data-model.md +0 -0
  293. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/plan.md +0 -0
  294. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/quickstart.md +0 -0
  295. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/research.md +0 -0
  296. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/spec.md +0 -0
  297. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/specs/012-web-adopt-pairing/tasks.md +0 -0
  298. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/__init__.py +0 -0
  299. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/__main__.py +0 -0
  300. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/__init__.py +0 -0
  301. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/cp.py +0 -0
  302. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/main.py +0 -0
  303. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/__init__.py +0 -0
  304. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/aws.py +0 -0
  305. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/hetzner.py +0 -0
  306. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/incus.py +0 -0
  307. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/providers/proxmox.py +0 -0
  308. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/shell.py +0 -0
  309. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/cli/web.py +0 -0
  310. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/__init__.py +0 -0
  311. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/ansible_runner.py +0 -0
  312. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/completion.py +0 -0
  313. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/config.py +0 -0
  314. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/known_hosts.py +0 -0
  315. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/output.py +0 -0
  316. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/picker.py +0 -0
  317. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/remo_host_client.py +0 -0
  318. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/rsync.py +0 -0
  319. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/snapshot.py +0 -0
  320. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/ssh.py +0 -0
  321. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/validation.py +0 -0
  322. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/version.py +0 -0
  323. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/core/web_adopt.py +0 -0
  324. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/__init__.py +0 -0
  325. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/capability.py +0 -0
  326. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/discovery.py +0 -0
  327. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/host.py +0 -0
  328. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/session_target.py +0 -0
  329. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/models/snapshot.py +0 -0
  330. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/__init__.py +0 -0
  331. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/aws.py +0 -0
  332. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/hetzner.py +0 -0
  333. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/incus.py +0 -0
  334. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/providers/proxmox.py +0 -0
  335. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/__init__.py +0 -0
  336. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/__init__.py +0 -0
  337. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/hosts.py +0 -0
  338. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/pairing.py +0 -0
  339. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/setup.py +0 -0
  340. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/api/terminals.py +0 -0
  341. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/app.py +0 -0
  342. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/check.py +0 -0
  343. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/config.py +0 -0
  344. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/discovery.py +0 -0
  345. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/health.py +0 -0
  346. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/logging_config.py +0 -0
  347. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/models.py +0 -0
  348. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/operator_auth.py +0 -0
  349. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/pairing.py +0 -0
  350. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/ssh_master.py +0 -0
  351. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/state.py +0 -0
  352. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/terminal.py +0 -0
  353. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/terminal_registry.py +0 -0
  354. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/src/remo_cli/web/tokens.py +0 -0
  355. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/__init__.py +0 -0
  356. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/ansible/__init__.py +0 -0
  357. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/ansible/test_remo_host_idempotency.py +0 -0
  358. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/conftest.py +0 -0
  359. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/fixtures.ts +0 -0
  360. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/ghostty-shell-io.spec.ts +0 -0
  361. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/ghostty-tui-compatibility.spec.ts +0 -0
  362. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/keyboard-routing.spec.ts +0 -0
  363. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/mobile.spec.ts +0 -0
  364. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/reconnect.spec.ts +0 -0
  365. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/e2e/workspace-layout.spec.ts +0 -0
  366. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/image/__init__.py +0 -0
  367. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/image/test_docker_image.py +0 -0
  368. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/__init__.py +0 -0
  369. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/_nine_terminal_fixture.py +0 -0
  370. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/orbstack-cloud-init.yml +0 -0
  371. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/orbstack.sh +0 -0
  372. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_nine_terminals.py +0 -0
  373. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_nine_terminals_soak.py +0 -0
  374. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_remo_host_e2e.py +0 -0
  375. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_security_rejections.py +0 -0
  376. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_terminal_attach.py +0 -0
  377. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_web_adopt_e2e.py +0 -0
  378. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/integration/test_web_cli_parity.py +0 -0
  379. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/perf/__init__.py +0 -0
  380. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/perf/test_latency.py +0 -0
  381. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/__init__.py +0 -0
  382. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/__init__.py +0 -0
  383. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_aws_snapshot.py +0 -0
  384. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_hetzner_snapshot.py +0 -0
  385. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_incus_snapshot.py +0 -0
  386. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/providers/test_proxmox_snapshot.py +0 -0
  387. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_cp.py +0 -0
  388. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_main.py +0 -0
  389. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_shell.py +0 -0
  390. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/cli/test_web_adopt_cmd.py +0 -0
  391. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/__init__.py +0 -0
  392. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ansible_runner.py +0 -0
  393. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_config.py +0 -0
  394. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_known_hosts.py +0 -0
  395. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_output.py +0 -0
  396. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_picker.py +0 -0
  397. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_remo_host_client.py +0 -0
  398. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_rsync.py +0 -0
  399. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_snapshot.py +0 -0
  400. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh.py +0 -0
  401. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh_controlpath.py +0 -0
  402. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_ssh_identity_opts.py +0 -0
  403. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_validation.py +0 -0
  404. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_version.py +0 -0
  405. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_authorize.py +0 -0
  406. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_code.py +0 -0
  407. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_payload.py +0 -0
  408. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_adopt_trust.py +0 -0
  409. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/core/test_web_push.py +0 -0
  410. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/__init__.py +0 -0
  411. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_capability.py +0 -0
  412. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_discovery.py +0 -0
  413. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/models/test_session_target.py +0 -0
  414. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/__init__.py +0 -0
  415. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_aws_snapshot.py +0 -0
  416. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_hetzner_snapshot.py +0 -0
  417. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_incus_snapshot.py +0 -0
  418. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_proxmox_devcontainer_runtime.py +0 -0
  419. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/providers/test_proxmox_snapshot.py +0 -0
  420. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/test_ansible_templates.py +0 -0
  421. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/test_host_model.py +0 -0
  422. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/__init__.py +0 -0
  423. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/_pairing_support.py +0 -0
  424. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/conftest.py +0 -0
  425. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_backpressure.py +0 -0
  426. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_check.py +0 -0
  427. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_csp.py +0 -0
  428. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_discovery.py +0 -0
  429. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_health_states.py +0 -0
  430. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_hosts_mapping.py +0 -0
  431. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_lazy_import.py +0 -0
  432. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_log_redaction.py +0 -0
  433. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_mint_gating.py +0 -0
  434. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_operator_auth.py +0 -0
  435. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_pairing.py +0 -0
  436. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_pairing_origin.py +0 -0
  437. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_api.py +0 -0
  438. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_auth.py +0 -0
  439. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_setup_dormancy.py +0 -0
  440. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_state.py +0 -0
  441. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_terminal_resize.py +0 -0
  442. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_terminals_api.py +0 -0
  443. {remo_cli-2.2.0rc12 → remo_cli-2.2.0rc13}/tests/unit/web/test_tokens.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remo-cli
3
- Version: 2.2.0rc12
3
+ Version: 2.2.0rc13
4
4
  Summary: Remote development environment CLI
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -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), and a **Nerd Font uploader**.
141
- Because a browser can't read fonts installed on the instance, uploading a patched Nerd Font once
142
- registers it via the `FontFace` API (persisted in IndexedDB) and offers it as a terminal font that's
143
- how Powerline/Git/devicon glyphs in a prompt or Zellij status bar render. Font changes apply live to
144
- every open terminal. The top bar also shows a health indicator (from `GET /api/v1/ready`) and an
145
- offline overlay if the service becomes unreachable (terminals reattach automatically when it returns).
146
- Press **?** for the keyboard-shortcut reference.
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
- // Dashboard "Pair CLI to sync" affordance (012-web-adopt-pairing, US4 / FR-017).
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 the popover mints, closing it ends the session
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="topbar-btn"
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
- Pair CLI to sync
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="topbar-btn"
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 &lt;url&gt;</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
- }, HOVER_FOCUS_DELAY_MS);
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={healthDetail ?? HEALTH_LABEL[health]}>
86
- <span className="topbar-health-dot" style={{ background: HEALTH_COLOR[health] }} />
87
- <span>{HEALTH_LABEL[health]}</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
  }