From 96977b576a31e26661c60af28dcf9fe078992336 Mon Sep 17 00:00:00 2001 From: Rose Date: Mon, 20 Apr 2026 10:43:30 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Initial=20dev=20copy=20from=20li?= =?UTF-8?q?ve?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 7 + .env.example | 31 + .github/workflows/release.yml | 56 + .github/workflows/tests.yml | 30 + .gitignore | 38 + .signing_key | 1 + ARCHITECTURE.md | 1631 +++++++ BUGS.md | 40 + CHANGELOG.md | 1674 +++++++ CONTRIBUTING.md | 171 + Dockerfile | 88 + HERMES.md | 489 ++ LICENSE | 21 + README.md | 629 +++ ROADMAP.md | 354 ++ SPRINTS.md | 1176 +++++ TESTING.md | 1789 ++++++++ THEMES.md | 145 + api/__init__.py | 1 + api/agents.py | 150 + api/auth.py | 204 + api/clarify.py | 128 + api/commands.py | 56 + api/config.py | 1304 ++++++ api/gateway_watcher.py | 229 + api/gateways.py | 265 ++ api/helpers.py | 175 + api/mc.py | 218 + api/models.py | 405 ++ api/onboarding.py | 555 +++ api/profiles.py | 450 ++ api/routes.py | 3160 +++++++++++++ api/session_ops.py | 151 + api/startup.py | 74 + api/state_sync.py | 118 + api/streaming.py | 1465 ++++++ api/updates.py | 257 ++ api/upload.py | 131 + api/workspace.py | 379 ++ bootstrap-8787.log | Bin 0 -> 5600 bytes bootstrap.py | 232 + docker-compose.two-container.yml | 60 + docker-compose.yml | 37 + docker_init.bash | 315 ++ docs/ui-ux/index.html | 862 ++++ docs/ui-ux/two-stage-proposal.html | 742 +++ last_workspace.txt | 1 + requirements.txt | 4 + server.py | 200 + sessions/01853b2c2980.json | 22 + sessions/112af33ff8cd.json | 42 + sessions/15f931a60ad7.json | 484 ++ sessions/1789760bc6bf.json | 22 + sessions/180b872d74dc.json | 22 + sessions/1a4a440c7557.json | 207 + sessions/1e30018b3937.json | 22 + sessions/1f01122f66d1.json | 47 + sessions/20260415_103948_734d2b.json | 50 + sessions/20260415_104423_0c3947.json | 45 + sessions/20260415_113727_acff2a.json | 45 + sessions/20260415_121547_7636f9.json | 35 + sessions/20260415_121636_45bfcb.json | 90 + sessions/20260415_125702_5eb6e3ab.json | 1585 +++++++ sessions/20260415_133049_159703.json | 1215 +++++ sessions/20260415_144029_4d1dee10.json | 320 ++ sessions/20260415_144434_850301.json | 1730 +++++++ sessions/20260415_144819_676403.json | 220 + sessions/20260415_145951_b1f615.json | 410 ++ sessions/20260415_150239_c61ce1.json | 185 + sessions/20260415_150408_b85394.json | 1505 +++++++ sessions/20260415_150841_077493.json | 1630 +++++++ sessions/20260415_153048_ddb735.json | 775 ++++ sessions/20260415_154314_757075.json | 680 +++ sessions/20260415_160947_44c865.json | 560 +++ sessions/20260415_161040_32db95.json | 70 + sessions/20260415_161340_22da25.json | 45 + sessions/20260415_161851_8e3721.json | 100 + sessions/20260416_081900_f5dc6085.json | 40 + sessions/20260416_083750_d0e62e2a.json | 210 + sessions/20260416_090519_2889ea.json | 260 ++ sessions/20260416_093031_7664e5.json | 202 + sessions/20260416_101546_40f03e.json | 100 + sessions/20260416_101717_3a49e4.json | 145 + sessions/20260416_101717_53925a.json | 70 + sessions/20260416_114231_2b3a42.json | 2213 +++++++++ sessions/20260416_121900_91b4fb.json | 95 + sessions/20260416_123157_d3a944.json | 170 + sessions/20260416_123417_dab47fbd.json | 40 + sessions/20260416_131542_c4953a21.json | 40 + sessions/20260416_150850_6e45bf.json | 355 ++ sessions/20260416_160558_6c41e6.json | 3994 +++++++++++++++++ sessions/20260416_161529_4c63e3.json | 1375 ++++++ sessions/20260416_162758_79181e.json | 540 +++ sessions/20260416_165318_1a751b.json | 773 ++++ sessions/20260416_165625_1c564c.json | 510 +++ sessions/20260416_222254_efc663.json | 165 + sessions/20260417_002031_bd11f9.json | 470 ++ sessions/20260417_112536_15d478.json | 855 ++++ sessions/20260417_114728_9b4e9a.json | 805 ++++ sessions/20260420_095944_654a3d.json | 232 + sessions/20260420_100120_66768e.json | 152 + sessions/20260420_100919_149c4d.json | 97 + sessions/21fb94d726f7.json | 22 + sessions/2479449eefd2.json | 35 + sessions/267de3c9430b.json | 139 + sessions/26c8bde448b9.json | 22 + sessions/2ed1c1fc0e0e.json | 3137 +++++++++++++ sessions/319032255cd0.json | 35 + sessions/31d5d395ad4f.json | 353 ++ sessions/35e23bffbba4.json | 543 +++ sessions/38825c9b409d.json | 36 + sessions/3b85e2e5ea50.json | 1353 ++++++ sessions/3cfdb75a15b5.json | 48 + sessions/418e476d5488.json | 642 +++ sessions/43d6fbbd9c8c.json | 22 + sessions/4a8c4bb66ef7.json | 27 + sessions/4f5b77b3ce7e.json | 426 ++ sessions/5b0842a55174.json | 22 + sessions/63f8a09ea53a.json | 22 + sessions/649cb1e9fa24.json | 22 + sessions/68b3c8187c4a.json | 130 + sessions/6ad38a98a9aa.json | 36 + sessions/78962d66520a.json | 27 + sessions/78ef8a1b4dc3.json | 62 + sessions/7aa32f9bba59.json | 31 + sessions/7be1fae59247.json | 58 + sessions/7f2d3130ad72.json | 760 ++++ sessions/8012a1d03f83.json | 22 + sessions/88f01e6986cc.json | 22 + sessions/92fbe2f88b3b.json | 57 + sessions/940e100e2d8c.json | 1296 ++++++ sessions/965ca6b5c69e.json | 653 +++ sessions/9b5637066984.json | 22 + sessions/_index.json | 1978 ++++++++ sessions/a587dec8343b.json | 22 + sessions/abc42a3fc4bf.json | 46 + sessions/ac9d2423e751.json | 58 + sessions/bac18e5a0628.json | 886 ++++ sessions/c3822d5c457b.json | 22 + sessions/c5b2a7b7054b.json | 218 + sessions/ca99d44f3d76.json | 36 + .../cron_56b0be860bf8_20260417_102012.json | 43 + .../cron_9de5e64d03ef_20260417_101717.json | 255 ++ sessions/d1c78db62ddc.json | 22 + sessions/d3cb6f4e9987.json | 22 + sessions/d5d2a8b0753b.json | 22 + sessions/e3df499b3d63.json | 117 + sessions/e65710a2fbf4.json | 96 + sessions/f2779d2b4848.json | 22 + sessions/f3c25f057780.json | 22 + sessions/fa733f35cc18.json | 22 + sessions/fb3fd3a85d0d.json | 372 ++ sessions/fc176d079c9e.json | 36 + sessions/fc7639ee08bf.json | 494 ++ settings.json | 17 + settings.json.backup-20260415-152843 | 17 + start.sh | 25 + static/boot.js | 844 ++++ static/commands.js | 379 ++ static/favicon-32.png | Bin 0 -> 1602 bytes static/favicon.ico | Bin 0 -> 2299 bytes static/favicon.svg | 20 + static/i18n.js | 2409 ++++++++++ static/icons.js | 77 + static/index.html | 697 +++ static/login.js | 55 + static/messages.js | 1096 +++++ static/onboarding.js | 390 ++ static/panels.js | 1911 ++++++++ static/sessions.js | 932 ++++ static/style.css | 1282 ++++++ static/ui.js | 1921 ++++++++ static/workspace.js | 438 ++ tests/__init__.py | 0 tests/_pytest_port.py | 42 + tests/conftest.py | 392 ++ tests/test_approval_queue.py | 188 + tests/test_approval_unblock.py | 288 ++ tests/test_auth_sessions.py | 134 + tests/test_batch_fixes.py | 226 + tests/test_bugbatch_apr2026.py | 140 + tests/test_cancel_interrupt.py | 115 + tests/test_chinese_locale.py | 111 + tests/test_clarify_unblock.py | 165 + tests/test_commands_endpoint.py | 84 + tests/test_custom_provider_display_name.py | 135 + tests/test_default_workspace_fallback.py | 148 + tests/test_gateway_sync.py | 420 ++ tests/test_ime_composition.py | 61 + tests/test_issue336.py | 322 ++ tests/test_issue341.py | 34 + tests/test_issue342.py | 124 + tests/test_issue347.py | 348 ++ tests/test_issue357.py | 199 + tests/test_issue401.py | 114 + tests/test_issue470.py | 313 ++ tests/test_issue477.py | 26 + tests/test_issue486_487.py | 572 +++ tests/test_issue487b.py | 131 + tests/test_issue569_579.py | 157 + tests/test_issue570_permission.py | 56 + tests/test_issue572.py | 205 + tests/test_issue607.py | 98 + tests/test_issue609.py | 107 + tests/test_issue644.py | 125 + tests/test_issue646.py | 54 + tests/test_issue677.py | 135 + tests/test_issue_code_syntax_highlight.py | 25 + tests/test_issues_373_374_375.py | 317 ++ tests/test_language_precedence.py | 262 ++ tests/test_login_locale.py | 86 + tests/test_media_inline.py | 216 + tests/test_minimax_provider.py | 148 + tests/test_mobile_layout.py | 278 ++ tests/test_model_resolver.py | 476 ++ tests/test_onboarding_existing_config.py | 368 ++ tests/test_onboarding_mvp.py | 244 + tests/test_onboarding_network.py | 184 + tests/test_onboarding_static.py | 58 + tests/test_opencode_providers.py | 121 + tests/test_orphaned_tool_messages.py | 175 + tests/test_profile_env_isolation.py | 67 + tests/test_profile_path_security.py | 63 + tests/test_provider_mismatch.py | 325 ++ tests/test_regressions.py | 766 ++++ tests/test_russian_locale.py | 116 + tests/test_security_redaction.py | 310 ++ tests/test_session_ops.py | 251 ++ tests/test_session_sidebar_relative_time.py | 155 + tests/test_session_summary_redaction.py | 66 + tests/test_spanish_locale.py | 45 + tests/test_sprint1.py | 440 ++ tests/test_sprint10.py | 139 + tests/test_sprint11.py | 101 + tests/test_sprint12.py | 179 + tests/test_sprint13.py | 122 + tests/test_sprint14.py | 153 + tests/test_sprint15.py | 234 + tests/test_sprint16.py | 721 +++ tests/test_sprint17.py | 96 + tests/test_sprint19.py | 128 + tests/test_sprint2.py | 106 + tests/test_sprint20.py | 444 ++ tests/test_sprint20b.py | 341 ++ tests/test_sprint23.py | 196 + tests/test_sprint26.py | 175 + tests/test_sprint27.py | 136 + tests/test_sprint28.py | 224 + tests/test_sprint29.py | 731 +++ tests/test_sprint3.py | 199 + tests/test_sprint30.py | 576 +++ tests/test_sprint31.py | 143 + tests/test_sprint32.py | 72 + tests/test_sprint33.py | 59 + tests/test_sprint34.py | 300 ++ tests/test_sprint35.py | 146 + tests/test_sprint36.py | 182 + tests/test_sprint37.py | 103 + tests/test_sprint38.py | 140 + tests/test_sprint39.py | 235 + tests/test_sprint4.py | 158 + tests/test_sprint40.py | 162 + tests/test_sprint40_ui_polish.py | 267 ++ tests/test_sprint41.py | 381 ++ tests/test_sprint42.py | 456 ++ tests/test_sprint43.py | 253 ++ tests/test_sprint44.py | 134 + tests/test_sprint45.py | 157 + tests/test_sprint46.py | 167 + tests/test_sprint47.py | 39 + tests/test_sprint48.py | 209 + tests/test_sprint5.py | 157 + tests/test_sprint6.py | 152 + tests/test_sprint7.py | 130 + tests/test_sprint8.py | 125 + tests/test_sprint9.py | 115 + tests/test_title_sanitization.py | 35 + tests/test_tls_support.py | 214 + tests/test_tool_call_persistence.py | 71 + tests/test_ui_card_animation.py | 47 + tests/test_update_checker.py | 317 ++ tests/test_updates.py | 53 + tests/test_voice_transcribe_endpoint.py | 87 + workspaces.json | 10 + 284 files changed, 95780 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .signing_key create mode 100644 ARCHITECTURE.md create mode 100644 BUGS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 HERMES.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 ROADMAP.md create mode 100644 SPRINTS.md create mode 100644 TESTING.md create mode 100644 THEMES.md create mode 100644 api/__init__.py create mode 100644 api/agents.py create mode 100644 api/auth.py create mode 100644 api/clarify.py create mode 100644 api/commands.py create mode 100644 api/config.py create mode 100644 api/gateway_watcher.py create mode 100644 api/gateways.py create mode 100644 api/helpers.py create mode 100644 api/mc.py create mode 100644 api/models.py create mode 100644 api/onboarding.py create mode 100644 api/profiles.py create mode 100644 api/routes.py create mode 100644 api/session_ops.py create mode 100644 api/startup.py create mode 100644 api/state_sync.py create mode 100644 api/streaming.py create mode 100644 api/updates.py create mode 100644 api/upload.py create mode 100644 api/workspace.py create mode 100644 bootstrap-8787.log create mode 100644 bootstrap.py create mode 100644 docker-compose.two-container.yml create mode 100644 docker-compose.yml create mode 100644 docker_init.bash create mode 100644 docs/ui-ux/index.html create mode 100644 docs/ui-ux/two-stage-proposal.html create mode 100644 last_workspace.txt create mode 100644 requirements.txt create mode 100644 server.py create mode 100644 sessions/01853b2c2980.json create mode 100644 sessions/112af33ff8cd.json create mode 100644 sessions/15f931a60ad7.json create mode 100644 sessions/1789760bc6bf.json create mode 100644 sessions/180b872d74dc.json create mode 100644 sessions/1a4a440c7557.json create mode 100644 sessions/1e30018b3937.json create mode 100644 sessions/1f01122f66d1.json create mode 100644 sessions/20260415_103948_734d2b.json create mode 100644 sessions/20260415_104423_0c3947.json create mode 100644 sessions/20260415_113727_acff2a.json create mode 100644 sessions/20260415_121547_7636f9.json create mode 100644 sessions/20260415_121636_45bfcb.json create mode 100644 sessions/20260415_125702_5eb6e3ab.json create mode 100644 sessions/20260415_133049_159703.json create mode 100644 sessions/20260415_144029_4d1dee10.json create mode 100644 sessions/20260415_144434_850301.json create mode 100644 sessions/20260415_144819_676403.json create mode 100644 sessions/20260415_145951_b1f615.json create mode 100644 sessions/20260415_150239_c61ce1.json create mode 100644 sessions/20260415_150408_b85394.json create mode 100644 sessions/20260415_150841_077493.json create mode 100644 sessions/20260415_153048_ddb735.json create mode 100644 sessions/20260415_154314_757075.json create mode 100644 sessions/20260415_160947_44c865.json create mode 100644 sessions/20260415_161040_32db95.json create mode 100644 sessions/20260415_161340_22da25.json create mode 100644 sessions/20260415_161851_8e3721.json create mode 100644 sessions/20260416_081900_f5dc6085.json create mode 100644 sessions/20260416_083750_d0e62e2a.json create mode 100644 sessions/20260416_090519_2889ea.json create mode 100644 sessions/20260416_093031_7664e5.json create mode 100644 sessions/20260416_101546_40f03e.json create mode 100644 sessions/20260416_101717_3a49e4.json create mode 100644 sessions/20260416_101717_53925a.json create mode 100644 sessions/20260416_114231_2b3a42.json create mode 100644 sessions/20260416_121900_91b4fb.json create mode 100644 sessions/20260416_123157_d3a944.json create mode 100644 sessions/20260416_123417_dab47fbd.json create mode 100644 sessions/20260416_131542_c4953a21.json create mode 100644 sessions/20260416_150850_6e45bf.json create mode 100644 sessions/20260416_160558_6c41e6.json create mode 100644 sessions/20260416_161529_4c63e3.json create mode 100644 sessions/20260416_162758_79181e.json create mode 100644 sessions/20260416_165318_1a751b.json create mode 100644 sessions/20260416_165625_1c564c.json create mode 100644 sessions/20260416_222254_efc663.json create mode 100644 sessions/20260417_002031_bd11f9.json create mode 100644 sessions/20260417_112536_15d478.json create mode 100644 sessions/20260417_114728_9b4e9a.json create mode 100644 sessions/20260420_095944_654a3d.json create mode 100644 sessions/20260420_100120_66768e.json create mode 100644 sessions/20260420_100919_149c4d.json create mode 100644 sessions/21fb94d726f7.json create mode 100644 sessions/2479449eefd2.json create mode 100644 sessions/267de3c9430b.json create mode 100644 sessions/26c8bde448b9.json create mode 100644 sessions/2ed1c1fc0e0e.json create mode 100644 sessions/319032255cd0.json create mode 100644 sessions/31d5d395ad4f.json create mode 100644 sessions/35e23bffbba4.json create mode 100644 sessions/38825c9b409d.json create mode 100644 sessions/3b85e2e5ea50.json create mode 100644 sessions/3cfdb75a15b5.json create mode 100644 sessions/418e476d5488.json create mode 100644 sessions/43d6fbbd9c8c.json create mode 100644 sessions/4a8c4bb66ef7.json create mode 100644 sessions/4f5b77b3ce7e.json create mode 100644 sessions/5b0842a55174.json create mode 100644 sessions/63f8a09ea53a.json create mode 100644 sessions/649cb1e9fa24.json create mode 100644 sessions/68b3c8187c4a.json create mode 100644 sessions/6ad38a98a9aa.json create mode 100644 sessions/78962d66520a.json create mode 100644 sessions/78ef8a1b4dc3.json create mode 100644 sessions/7aa32f9bba59.json create mode 100644 sessions/7be1fae59247.json create mode 100644 sessions/7f2d3130ad72.json create mode 100644 sessions/8012a1d03f83.json create mode 100644 sessions/88f01e6986cc.json create mode 100644 sessions/92fbe2f88b3b.json create mode 100644 sessions/940e100e2d8c.json create mode 100644 sessions/965ca6b5c69e.json create mode 100644 sessions/9b5637066984.json create mode 100644 sessions/_index.json create mode 100644 sessions/a587dec8343b.json create mode 100644 sessions/abc42a3fc4bf.json create mode 100644 sessions/ac9d2423e751.json create mode 100644 sessions/bac18e5a0628.json create mode 100644 sessions/c3822d5c457b.json create mode 100644 sessions/c5b2a7b7054b.json create mode 100644 sessions/ca99d44f3d76.json create mode 100644 sessions/cron_56b0be860bf8_20260417_102012.json create mode 100644 sessions/cron_9de5e64d03ef_20260417_101717.json create mode 100644 sessions/d1c78db62ddc.json create mode 100644 sessions/d3cb6f4e9987.json create mode 100644 sessions/d5d2a8b0753b.json create mode 100644 sessions/e3df499b3d63.json create mode 100644 sessions/e65710a2fbf4.json create mode 100644 sessions/f2779d2b4848.json create mode 100644 sessions/f3c25f057780.json create mode 100644 sessions/fa733f35cc18.json create mode 100644 sessions/fb3fd3a85d0d.json create mode 100644 sessions/fc176d079c9e.json create mode 100644 sessions/fc7639ee08bf.json create mode 100644 settings.json create mode 100644 settings.json.backup-20260415-152843 create mode 100755 start.sh create mode 100644 static/boot.js create mode 100644 static/commands.js create mode 100644 static/favicon-32.png create mode 100644 static/favicon.ico create mode 100644 static/favicon.svg create mode 100644 static/i18n.js create mode 100644 static/icons.js create mode 100644 static/index.html create mode 100644 static/login.js create mode 100644 static/messages.js create mode 100644 static/onboarding.js create mode 100644 static/panels.js create mode 100644 static/sessions.js create mode 100644 static/style.css create mode 100644 static/ui.js create mode 100644 static/workspace.js create mode 100644 tests/__init__.py create mode 100644 tests/_pytest_port.py create mode 100644 tests/conftest.py create mode 100644 tests/test_approval_queue.py create mode 100644 tests/test_approval_unblock.py create mode 100644 tests/test_auth_sessions.py create mode 100644 tests/test_batch_fixes.py create mode 100644 tests/test_bugbatch_apr2026.py create mode 100644 tests/test_cancel_interrupt.py create mode 100644 tests/test_chinese_locale.py create mode 100644 tests/test_clarify_unblock.py create mode 100644 tests/test_commands_endpoint.py create mode 100644 tests/test_custom_provider_display_name.py create mode 100644 tests/test_default_workspace_fallback.py create mode 100644 tests/test_gateway_sync.py create mode 100644 tests/test_ime_composition.py create mode 100644 tests/test_issue336.py create mode 100644 tests/test_issue341.py create mode 100644 tests/test_issue342.py create mode 100644 tests/test_issue347.py create mode 100644 tests/test_issue357.py create mode 100644 tests/test_issue401.py create mode 100644 tests/test_issue470.py create mode 100644 tests/test_issue477.py create mode 100644 tests/test_issue486_487.py create mode 100644 tests/test_issue487b.py create mode 100644 tests/test_issue569_579.py create mode 100644 tests/test_issue570_permission.py create mode 100644 tests/test_issue572.py create mode 100644 tests/test_issue607.py create mode 100644 tests/test_issue609.py create mode 100644 tests/test_issue644.py create mode 100644 tests/test_issue646.py create mode 100644 tests/test_issue677.py create mode 100644 tests/test_issue_code_syntax_highlight.py create mode 100644 tests/test_issues_373_374_375.py create mode 100644 tests/test_language_precedence.py create mode 100644 tests/test_login_locale.py create mode 100644 tests/test_media_inline.py create mode 100644 tests/test_minimax_provider.py create mode 100644 tests/test_mobile_layout.py create mode 100644 tests/test_model_resolver.py create mode 100644 tests/test_onboarding_existing_config.py create mode 100644 tests/test_onboarding_mvp.py create mode 100644 tests/test_onboarding_network.py create mode 100644 tests/test_onboarding_static.py create mode 100644 tests/test_opencode_providers.py create mode 100644 tests/test_orphaned_tool_messages.py create mode 100644 tests/test_profile_env_isolation.py create mode 100644 tests/test_profile_path_security.py create mode 100644 tests/test_provider_mismatch.py create mode 100644 tests/test_regressions.py create mode 100644 tests/test_russian_locale.py create mode 100644 tests/test_security_redaction.py create mode 100644 tests/test_session_ops.py create mode 100644 tests/test_session_sidebar_relative_time.py create mode 100644 tests/test_session_summary_redaction.py create mode 100644 tests/test_spanish_locale.py create mode 100644 tests/test_sprint1.py create mode 100644 tests/test_sprint10.py create mode 100644 tests/test_sprint11.py create mode 100644 tests/test_sprint12.py create mode 100644 tests/test_sprint13.py create mode 100644 tests/test_sprint14.py create mode 100644 tests/test_sprint15.py create mode 100644 tests/test_sprint16.py create mode 100644 tests/test_sprint17.py create mode 100644 tests/test_sprint19.py create mode 100644 tests/test_sprint2.py create mode 100644 tests/test_sprint20.py create mode 100644 tests/test_sprint20b.py create mode 100644 tests/test_sprint23.py create mode 100644 tests/test_sprint26.py create mode 100644 tests/test_sprint27.py create mode 100644 tests/test_sprint28.py create mode 100644 tests/test_sprint29.py create mode 100644 tests/test_sprint3.py create mode 100644 tests/test_sprint30.py create mode 100644 tests/test_sprint31.py create mode 100644 tests/test_sprint32.py create mode 100644 tests/test_sprint33.py create mode 100644 tests/test_sprint34.py create mode 100644 tests/test_sprint35.py create mode 100644 tests/test_sprint36.py create mode 100644 tests/test_sprint37.py create mode 100644 tests/test_sprint38.py create mode 100644 tests/test_sprint39.py create mode 100644 tests/test_sprint4.py create mode 100644 tests/test_sprint40.py create mode 100644 tests/test_sprint40_ui_polish.py create mode 100644 tests/test_sprint41.py create mode 100644 tests/test_sprint42.py create mode 100644 tests/test_sprint43.py create mode 100644 tests/test_sprint44.py create mode 100644 tests/test_sprint45.py create mode 100644 tests/test_sprint46.py create mode 100644 tests/test_sprint47.py create mode 100644 tests/test_sprint48.py create mode 100644 tests/test_sprint5.py create mode 100644 tests/test_sprint6.py create mode 100644 tests/test_sprint7.py create mode 100644 tests/test_sprint8.py create mode 100644 tests/test_sprint9.py create mode 100644 tests/test_title_sanitization.py create mode 100644 tests/test_tls_support.py create mode 100644 tests/test_tool_call_persistence.py create mode 100644 tests/test_ui_card_animation.py create mode 100644 tests/test_update_checker.py create mode 100644 tests/test_updates.py create mode 100644 tests/test_voice_transcribe_endpoint.py create mode 100644 workspaces.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..218f5d2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.pytest_cache +__pycache__ +*.pyc +*.pyo +tests/ +.env* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..938f023 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# Hermes Web UI -- local machine config template +# Copy this to .env and fill in your values. +# start.sh sources .env automatically if present. +# All values are optional -- auto-discovery will fill in anything left blank. + +# Path to your hermes-agent checkout (the repo that contains run_agent.py) +# HERMES_WEBUI_AGENT_DIR=/path/to/hermes-agent + +# Python executable to use (defaults to the agent venv if found) +# HERMES_WEBUI_PYTHON=/path/to/python + +# Bind address (default: 127.0.0.1 -- loopback only, safe default) +# HERMES_WEBUI_HOST=127.0.0.1 + +# Port to listen on (default: 8787) +# HERMES_WEBUI_PORT=8787 + +# Where to store sessions, workspaces, and other state (default: ~/.hermes/webui-mvp) +# HERMES_WEBUI_STATE_DIR=~/.hermes/webui-mvp + +# Default workspace directory shown on first launch +# HERMES_WEBUI_DEFAULT_WORKSPACE=~/workspace + +# Base directory for all Hermes state (affects all paths above if set) +# HERMES_HOME=~/.hermes + +# Path to your Hermes config.yaml (for toolsets and model config) +# HERMES_CONFIG_PATH=~/.hermes/config.yaml + +# Display name for the assistant in the UI (default: Hermes) +# HERMES_WEBUI_BOT_NAME=Hermes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5c31119 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Release & Docker + +on: + push: + tags: + - 'v*' + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write # required: create GitHub Release + packages: write # required: push to ghcr.io + + steps: + - uses: actions/checkout@v4 + + # Create GitHub Release from tag with auto-generated notes + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + + # Set up multi-arch build (QEMU + Buildx) + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + # Log in to GitHub Container Registry + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract tags from the git ref (supports vX.Y and vX.Y.Z formats) + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=match,pattern=v(\d+\.\d+(?:\.\d+)?),group=1 + type=raw,value=latest + + # Build and push multi-arch image (amd64 + arm64) + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..b16b1d5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,30 @@ +name: Tests + +on: + pull_request: + branches: [master] + push: + branches: [master] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.11', '3.12', '3.13'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pyyaml>=6.0 pytest pytest-timeout + + - name: Run tests + run: pytest tests/ -v --timeout=60 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20373fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Python cache +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Backup and temporary files +*.bak +*.swp +*.swo + +# Archive directory (pre-git backups, kept on disk but not tracked) +archive/ + +# Local environment and secrets (but keep the example template) +.env +.env.* +!.env.example +.claude/ +CLAUDE.md +AGENTS.md +.cursorrules +.windsurfrules +.aider* +copilot-instructions.md + +# Generated screenshots and transient artifacts +screenshot-*.png +full-UI.png + +# OS files +.DS_Store +Thumbs.db + +# Local reference clones — never committed (except tracked design/UI-UX reference pages) +docs/* +!docs/ui-ux/ +!docs/ui-ux/** diff --git a/.signing_key b/.signing_key new file mode 100644 index 0000000..52c135b --- /dev/null +++ b/.signing_key @@ -0,0 +1 @@ +)DÈ·:ª§Þ˜û®m§¹ÌgývàˆssuÛEH0Óç \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..45e18bb --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,1631 @@ +# Hermes Web UI: Developer and Architecture Guide + +> This document is the canonical reference for anyone (human or agent) working on the +> Hermes Web UI. It covers the exact current state of the code, every design decision and +> quirk discovered during development, and a phased architecture improvement roadmap that +> runs in parallel with the feature roadmap in ROADMAP.md. +> +> Keep this document updated as architecture changes are made. + +> Current shipped build: `v0.50.36-local.1` (April 16, 2026). +> Baseline: upstream `nesquena/hermes-webui` `v0.50.36`. +> Intentional local delta: first-time password enablement from Settings immediately issues a `hermes_session` cookie so the current browser remains signed in. The previous `Assistant Reply Language` customization has been removed, legacy `assistant_language` settings are filtered out on load/save, the workspace panel closed/open state is preloaded via a `documentElement` dataset marker before `style.css` paints to avoid a first-load desktop flash, transcript disclosure cards now animate caret rotation and body expansion with transitionable `max-height`/`opacity` states instead of `display:none/block`, and thinking cards now share the same rounded bordered card chrome as tool cards while keeping their gold palette. +> Automated coverage: 1353 tests collected (`pytest tests/ --collect-only -q`). + +--- + +## 1. Overview and Purpose + +The Hermes Web UI is a lightweight web application that gives you a browser-based +interface to the Hermes agent that is functionally equivalent to the CLI. It is modeled on +the Claude-style interface: a sidebar for session management, a central chat area, +and a demand-driven right panel used for workspace browsing and preview surfaces. +The right panel is closed by default on desktop and opens only when it is actively +being used for browsing or previewing content. + +To prevent a visible first-paint mismatch on refresh, `static/index.html` preloads the +saved workspace panel state into `document.documentElement.dataset.workspacePanel` +before the main stylesheet loads. Desktop CSS honors that preload marker immediately, +and `static/boot.js` keeps the dataset synchronized with the runtime panel state machine. + +The design philosophy is deliberately minimal. There is no build step, no bundler, no +frontend framework. The Python server is split into a routing shell (server.py) and +business logic modules (api/). The frontend is seven vanilla JS modules loaded from static/. +This makes the code easy to modify from a terminal or by an agent. + +For the current local build, the codebase is intentionally as close to upstream as possible: +the app now tracks upstream `v0.50.36`, keeps the password-session continuity patch in the +settings/onboarding flow, and does not carry forward the prior reply-language preference +feature. + +Hermes-level chrome is intentionally consolidated: the sidebar has no dedicated brand header. +Instead, the footer exposes a single "Hermes WebUI" launch button that opens one tabbed +control-center modal for global preferences, conversation import/export, and clear-conversation +actions. The topbar remains focused on conversation context and the workspace/files toggle. + +--- + +## 2. File Inventory + + / + server.py Thin routing shell + HTTP Handler + auth middleware. ~81 lines. + Delegates all route handling to api/routes.py. + bootstrap.py One-shot launcher: optional agent install, deps, health wait, browser open. + start.sh Thin wrapper around bootstrap.py for shell-based startup. + Dockerfile python:3.12-slim container image (~23 lines) + docker-compose.yml Compose config with named volume and optional auth (~22 lines) + .dockerignore Excludes .git, tests/, .env* from Docker builds + api/ + __init__.py Package marker + auth.py Optional password authentication, signed cookies (~149 lines) + config.py Discovery, globals, model detection, reloadable config (~701 lines) + helpers.py HTTP helpers: j(), bad(), require(), safe_resolve(), security headers (~71 lines) + models.py Session model + CRUD, per-session profile tracking (~137 lines) + profiles.py Profile state management, hermes_cli wrapper (~246 lines) + onboarding.py First-run onboarding status, real provider config writes, and readiness detection. + routes.py All GET + POST route handlers (~1180 lines) + startup.py Startup helpers: auto_install_agent_deps() (~50 lines) + streaming.py SSE engine, run_agent, cancel, HERMES_HOME save/restore (~236 lines) + upload.py Multipart parser, file upload handler (~78 lines) + workspace.py File ops: list_dir, read_file_content, workspace helpers (~77 lines) + static/ + index.html HTML template (~364 lines) + style.css All CSS incl. mobile responsive (~670 lines) + ui.js DOM helpers, renderMd, tool cards, model dropdown, file tree (~977 lines) + workspace.js File preview, file ops, loadDir, clearPreview (~185 lines) + sessions.js Session CRUD, list rendering, search, SVG icons, dropdown actions (~533 lines) + messages.js send(), SSE event handlers, approval, transcript (~297 lines) + panels.js Cron, skills, memory, workspace, profiles, todo, settings (~974 lines) + commands.js Slash command registry, parser, autocomplete dropdown (~156 lines) + onboarding.js First-run wizard overlay, provider setup flow, and settings/workspace orchestration. + boot.js Event wiring, mobile sidebar/workspace nav, voice input, boot IIFE (~338 lines) + tests/ + conftest.py Isolated test server (port 8788, separate HERMES_HOME) (~240 lines) + test_sprint{1-20b}.py Feature tests per sprint (21 files, 415 test functions) + test_regressions.py Permanent regression gate (23 tests) + AGENTS.md Instruction file for agents working in this directory. + ROADMAP.md Feature and product roadmap document. + SPRINTS.md Forward sprint plan with CLI + Claude parity targets. + ARCHITECTURE.md THIS FILE. + TESTING.md Manual browser test plan and automated coverage reference. + CHANGELOG.md Release notes per sprint. + BUGS.md Bug backlog and fixed items tracker. + requirements.txt Python dependencies. + .env.example Sample environment variable overrides. + +State directory (runtime data, separate from source): + + ~/.hermes/webui-mvp/ + sessions/ One JSON file per session: {session_id}.json + workspaces.json Registered workspaces list + last_workspace.txt Last-used workspace path + settings.json User settings (default model, workspace, send key, password hash) + projects.json Session project groups (name, color, id) + +Log file: + + /tmp/webui-mvp.log stdout/stderr from the background server process + +--- + +## 3. Runtime Environment + +- Python interpreter: /venv/bin/python +- The venv has all Hermes agent dependencies (run_agent, tools/*, cron/*) +- Server binds to 127.0.0.1:8787 (localhost only, not public internet) +- Access from Mac: SSH tunnel: ssh -N -L 8787:127.0.0.1:8787 @ +- The server imports Hermes modules via sys.path.insert(0, parent_dir) + +Environment variables controlling behavior: + + HERMES_WEBUI_HOST Bind address (default: 127.0.0.1) + HERMES_WEBUI_PORT Port (default: 8787) + HERMES_WEBUI_DEFAULT_WORKSPACE Default workspace path for new sessions + HERMES_WEBUI_STATE_DIR Where sessions/ folder lives + HERMES_CONFIG_PATH Path to ~/.hermes/config.yaml + HERMES_WEBUI_DEFAULT_MODEL Default LLM model string + HERMES_WEBUI_PASSWORD Optional: enable password auth (off by default) + HERMES_HOME Base directory for Hermes state (~/.hermes by default) + +Test isolation environment variables (set by conftest.py): + + HERMES_WEBUI_PORT=8788 Isolated test port + HERMES_WEBUI_STATE_DIR=~/.hermes/webui-mvp-test Isolated test state + HERMES_WEBUI_DEFAULT_WORKSPACE=.../test-workspace Isolated test workspace + +Tests NEVER talk to the production server (port 8787). +The test state dir is wiped before each test session and deleted after. +See: /tests/conftest.py + +Per-request environment variables (set by chat handler, restored after): + + TERMINAL_CWD Set to session.workspace before running agent. + The terminal tool reads this to default cwd. + HERMES_EXEC_ASK Set to "1" to enable approval gate for dangerous commands. + HERMES_SESSION_KEY Set to session_id. The approval tool keys pending entries + by this value, enabling per-session approval state. + HERMES_HOME Set to the active profile's directory before running agent. + Saved and restored around each agent run. + +WARNING: These env vars are process-global. Two concurrent chat requests will clobber +each other. This is safe only for single-user, single-concurrent-request use. +See Architecture Phase B for the fix. + +--- + +## 4. Server Architecture: Current State + +### 4.1 HTTP Server Layer + +Python stdlib ThreadingHTTPServer (from http.server). Each HTTP request runs in its own +thread. The Handler class subclasses BaseHTTPRequestHandler with two methods: + + do_GET Routes: /, /health, /api/session, /api/sessions, /api/list, + /api/chat/stream, /api/file, /api/approval/pending + do_POST Routes: /api/upload, /api/session/new, /api/session/update, + /api/session/delete, /api/chat/start, /api/chat, + /api/approval/respond + +Routing is a flat if/elif chain inside each method. No routing framework. + +Helper functions used by all handlers: + + j(handler, payload, status=200) Sends JSON response with correct headers + t(handler, payload, status=200, ct) Sends plain text or HTML response + read_body(handler) Reads and JSON-parses the POST body + +CRITICAL ORDERING RULE in do_POST: +The /api/upload check MUST appear BEFORE calling read_body(). read_body() calls +handler.rfile.read() which consumes the HTTP body stream. The upload handler also +needs rfile (to read the multipart payload). If read_body() runs first on a multipart +request, the upload handler receives an empty body and the upload silently fails. + +### 4.2 Session Model + +Session is a plain Python class (not a dataclass, not SQLAlchemy): + + Fields: + session_id hex string, 12 chars (uuid4().hex[:12]) + title string, auto-set from first user message + workspace absolute path string, resolved at creation + model model ID string (e.g. "anthropic/claude-sonnet-4.6") + messages list of OpenAI-format message dicts + created_at float Unix timestamp + updated_at float Unix timestamp, updated on every save() + pinned bool, default False (Sprint 12) + archived bool, default False (Sprint 14) + project_id string or null, FK to projects.json (Sprint 15) + tool_calls list of tool call dicts (Sprint 10) + + Key methods: + path (property) Returns SESSION_DIR/{session_id}.json + save() Writes __dict__ as pretty JSON to path, updates updated_at + load(cls, sid) Class method: reads JSON from disk, returns Session or None + compact() Returns metadata-only dict (no messages) for the session list + + In-memory cache: + SESSIONS = {} dict: session_id -> Session object + LOCK = threading.Lock() defined but NOT currently used around SESSIONS access + + get_session(sid): checks SESSIONS cache, loads from disk on miss, raises KeyError + new_session(workspace, model): creates Session, caches in SESSIONS, saves, returns + all_sessions(): scans SESSION_DIR/*.json + SESSIONS, deduplicates, sorts by updated_at, + returns list of compact() dicts + + all_sessions() does a full directory scan on every call. + With 10 sessions: negligible. With 1000+: will be slow. + See Architecture Phase C for the index file fix. + +title_from(): takes messages list, finds first user message, returns first 64 chars. +Called after run_conversation() completes to set the session title retroactively. + +### 4.3 SSE Streaming Engine + +This is the most architecturally interesting part. Two endpoints cooperate: + + POST /api/chat/start Receives the user message. Creates a queue.Queue, stores it + in STREAMS[stream_id], spawns a daemon thread running + _run_agent_streaming(), returns {stream_id} immediately. + + GET /api/chat/stream Long-lived SSE connection. Reads from STREAMS[stream_id] + and forwards events to the browser until 'done' or 'error'. + +Queue registry: + + STREAMS = {} dict: stream_id -> queue.Queue + STREAMS_LOCK = threading.Lock() + +SSE event types and their data shapes: + + token {"text": "..."} LLM token delta + tool {"name": "...", "preview": "..."} Tool invocation started + approval {"command": "...", "description": "...", "pattern_keys": [...]} + done {"session": {compact_fields + messages}} Agent finished successfully + error {"message": "...", "trace": "..."} Agent threw exception + +The SSE handler loop: + - Blocks on queue.get(timeout=30) + - On timeout (no events in 30s): sends a heartbeat comment (": heartbeat + +") + to keep the connection alive through proxies and firewalls + - On 'done' or 'error' event: breaks the loop and returns + - Catches BrokenPipeError and ConnectionResetError silently (browser disconnected) + +Stream cleanup: _run_agent_streaming() pops its stream_id from STREAMS in a finally +block. If the browser disconnects mid-stream, the daemon thread runs to completion and +then cleans up. The queue fills and the put_nowait() calls fail silently (queue.Full +is caught). + +Fallback sync endpoint: POST /api/chat still exists and holds the connection open until +the agent finishes. The frontend never uses it but it can be useful for debugging. + +### 4.4 Agent Invocation (_run_agent_streaming) + + def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id): + +1. Fetches session from SESSIONS (not from disk -- session was just updated by /api/chat/start) +2. Sets TERMINAL_CWD, HERMES_EXEC_ASK, HERMES_SESSION_KEY env vars +3. Creates AIAgent with: + - model=model, platform='cli', quiet_mode=True + - enabled_toolsets=CLI_TOOLSETS (from config.yaml or hardcoded default) + - session_id=session_id + - stream_delta_callback=on_token (fires per token) + - tool_progress_callback=on_tool (fires per tool invocation) +4. Calls agent.run_conversation(user_message=msg_text, conversation_history=s.messages, + task_id=session_id) + NOTE: keyword is task_id NOT session_id (common mistake, documented in skill) +5. On return: updates s.messages, calls title_from(), saves session +6. Puts ('done', {session: ...}) into queue +7. Finally block: restores env vars, pops stream_id from STREAMS + +on_token callback: + if text is None: return # end-of-stream sentinel from AIAgent + put('token', {'text': text}) + +on_tool callback: + put('tool', {'name': name, 'preview': preview}) + # Also immediately surface any pending approval: + if has_pending(session_id): + with _lock: p = dict(_pending.get(session_id, {})) + if p: put('approval', p) + +The approval surface-on-tool logic means approvals appear immediately after the tool +fires (within the same SSE stream), without waiting for the next poll cycle. + +### 4.5 Approval System Integration + +The approval system uses the existing Hermes gateway module at tools/approval.py. +All state lives in module-level variables in that file: + + _pending = {} dict: session_key -> pending_entry_dict + _lock = Lock() protects _pending + _permanent_approved set of permanently approved pattern keys + +Because server.py imports tools.approval at module load time and everything runs in the +same process, this state IS shared between HTTP threads and agent daemon threads. + +Important: this only works because Python imports are cached (sys.modules). The same +module object is used everywhere. If the approval module were ever imported in a subprocess +or via importlib.reload(), this would break. + +GET /api/approval/pending: + - Peeks at _pending[sid] without removing it + - Returns {pending: entry} or {pending: null} + - Called by the browser every 1500ms while S.busy is true (polling fallback) + +POST /api/approval/respond: + - Pops _pending[sid] (removes it) + - For choice "once" or "session": calls approve_session(sid, pattern_key) for each key + - For choice "always": calls approve_session + approve_permanent + save_permanent_allowlist + - For choice "deny": just pops, does nothing (agent gets denied result) + - Returns {ok: true, choice: choice} + +### 4.6 File Upload Parser + +parse_multipart(rfile, content_type, content_length): + - Reads all content_length bytes from rfile into memory (up to MAX_UPLOAD_BYTES = 20MB) + - Extracts boundary from Content-Type header + - Splits raw bytes on b'--' + boundary + - For each part: parses MIME headers via email.parser.HeaderParser + - Returns (fields, files) where fields is {name: value} and files is {name: (filename, bytes)} + +handle_upload(handler): + - Calls parse_multipart() + - Validates: file field present, filename present, session exists + - Sanitizes filename: replaces non-word chars with _, truncates to 200 chars + - Writes bytes to session.workspace / safe_name + - Returns {filename, path, size} + +Why not cgi.FieldStorage: + - Deprecated in Python 3.11+ + - Broken for binary files (silently corrupts or throws) + - The manual parser handles all file types correctly + +### 4.7 File System Operations + +safe_resolve(root, requested): + - Resolves requested path relative to root + - Calls .relative_to(root) to assert the result is inside root + - Raises ValueError on path traversal (../../etc/passwd) + +list_dir(workspace, rel='.'): + - Calls safe_resolve, then iterdir() + - Sorts: directories first, then files, case-insensitive alpha within each group + - Returns up to 200 entries with {name, path, type, size} + +read_file_content(workspace, rel): + - Calls safe_resolve + - Enforces MAX_FILE_BYTES = 200KB size limit + - Reads as UTF-8 with errors='replace' (binary files show replacement chars) + - Returns {path, content, size, lines} + +--- + +## 5. Frontend Architecture: Current State + +### 5.1 Structure + +The frontend is served from static/ as separate files: one HTML template, one CSS file, +and six JavaScript modules (~2,786 lines total). External dependencies: Prism.js (syntax +highlighting) and Mermaid.js (diagrams) from CDN, both loaded async/deferred with SRI hashes. + +Six JS modules loaded in order at end of : + 1. ui.js (~846 lines) DOM helpers, renderMd, tool card rendering, global state + 2. workspace.js (~169 lines) File tree, preview, file operations + 3. sessions.js (~532 lines) Session CRUD, list rendering, search, SVG icons, dropdown actions, project picker + 4. messages.js (~293 lines) send(), SSE event handlers, approval, transcript + 5. panels.js (~771 lines) Cron, skills, memory, workspace, todo, switchPanel + 6. boot.js (~175 lines) Event wiring + boot IIFE + +sessions.js defines an `ICONS` constant at module level with hardcoded SVG strings for all +session action buttons (pin, unpin, folder, archive, unarchive, duplicate, trash). All icons +inherit `currentColor` for consistent theming. + +Three-panel layout (in static/index.html): + +