diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a9bac..c8113fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,50 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [semantic versioning](https://semver.org/) (minor for new functionality, patch for fixes and internal changes). +## [1.3.0] - 2026-08-18 + +### Added + +- Assistant replies in chat are now formatted. Bold, italics, bulleted and + numbered lists, inline code and multi-line code blocks render as intended, + instead of showing the raw `**`, `###` and backtick markers the model has + always written. The rendered set is deliberately small: headings, block + quotes, tables and strikethrough flatten to plain text, and links and images + are never rendered as such — a markdown link keeps its label, so the agent is + asked to write URLs plainly and leave them readable. Text still streams in as + plain text and formats once the reply finishes. Nothing is stored formatted, + so older conversations pick up the change too. +- Claude Sonnet 5 and Claude Opus 5 can be selected for any of the six + generation stages, both as account defaults and per project. Stage defaults + are unchanged, so existing projects keep the models they were created with. + +### Changed + +- Preview subdomains now use the pre-issued wildcard certificate that 1.2.0 + added as opt-in. A first visit no longer waits for a certificate to be + issued, and the certificate-transparency warning that a slightly fast clock + could trigger is gone. + +### Fixed + +- Selecting Claude Sonnet 4.6 or Claude Opus 4.6 did not actually work: chat + answered with "The configured model is unavailable", and a project using one + for its template stage failed mid-build. Only the default Haiku model + resolved. Both environments are corrected, and a new check fails loudly if a + model offered in the picker cannot be resolved, so the picker and what the + system can actually run cannot drift apart again silently. + +### Security + +- No model-authored HTML is ever rendered in chat. The new formatting passes + every reply through a CommonMark parser that omits raw HTML outright and then + an allowlist sanitizer, because the app's content-security policy permits + scripts from any HTTPS origin and so cannot be relied on as a second line of + defense. +- Twelve gems updated for newly published advisories — among them nokogiri, + rails-html-sanitizer, loofah, sqlite3 and websocket-driver — together with + the Rails 8.1.3.1 patch releases. + ## [1.2.0] - 2026-06-16 ### Added diff --git a/Gemfile b/Gemfile index 89ed084..d5ad8a6 100644 --- a/Gemfile +++ b/Gemfile @@ -56,6 +56,10 @@ gem "omniauth-rails_csrf_protection" # GitHub REST API client (used by ExportToGithubJob to create repos) gem "octokit", "~> 10.0" +# CommonMark renderer for assistant chat replies. `unsafe: false` (the default) +# omits raw HTML rather than passing it through — see lib/markdown.rb. +gem "commonmarker" + group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri windows ], require: "debug/prelude" diff --git a/Gemfile.lock b/Gemfile.lock index 4497f21..26cc050 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,29 +3,29 @@ GEM specs: action_text-trix (2.1.18) railties - actioncable (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) + actioncable (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + actionmailbox (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) - actionmailer (8.1.3) - actionpack (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activesupport (= 8.1.3) + actionmailer (8.1.3.1) + actionpack (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activesupport (= 8.1.3.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.1.3) - actionview (= 8.1.3) - activesupport (= 8.1.3) + actionpack (8.1.3.1) + actionview (= 8.1.3.1) + activesupport (= 8.1.3.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -33,36 +33,36 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.1.3) + actiontext (8.1.3.1) action_text-trix (~> 2.1.15) - actionpack (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + actionpack (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.1.3) - activesupport (= 8.1.3) + actionview (8.1.3.1) + activesupport (= 8.1.3.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.1.3) - activesupport (= 8.1.3) + activejob (8.1.3.1) + activesupport (= 8.1.3.1) globalid (>= 0.3.6) - activemodel (8.1.3) - activesupport (= 8.1.3) - activerecord (8.1.3) - activemodel (= 8.1.3) - activesupport (= 8.1.3) + activemodel (8.1.3.1) + activesupport (= 8.1.3.1) + activerecord (8.1.3.1) + activemodel (= 8.1.3.1) + activesupport (= 8.1.3.1) timeout (>= 0.4.0) - activestorage (8.1.3) - actionpack (= 8.1.3) - activejob (= 8.1.3) - activerecord (= 8.1.3) - activesupport (= 8.1.3) + activestorage (8.1.3.1) + actionpack (= 8.1.3.1) + activejob (= 8.1.3.1) + activerecord (= 8.1.3.1) + activesupport (= 8.1.3.1) marcel (~> 1.0) - activesupport (8.1.3) + activesupport (8.1.3.1) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.3.1) @@ -93,7 +93,7 @@ GEM bindex (0.8.1) bootsnap (1.24.5) msgpack (~> 1.2) - brakeman (8.0.5) + brakeman (8.0.6) racc builder (3.3.0) bundler-audit (0.9.3) @@ -108,13 +108,19 @@ GEM rack-test (>= 0.6.3) regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) - concurrent-ruby (1.3.6) + commonmarker (2.9.0-aarch64-linux) + commonmarker (2.9.0-aarch64-linux-musl) + commonmarker (2.9.0-arm-linux) + commonmarker (2.9.0-arm64-darwin) + commonmarker (2.9.0-x86_64-linux) + commonmarker (2.9.0-x86_64-linux-musl) + concurrent-ruby (1.3.8) connection_pool (3.0.2) console (1.34.3) fiber-annotation fiber-local (~> 1.1) json - crass (1.0.6) + crass (1.0.7) date (3.5.1) debug (1.11.1) irb (~> 1.10) @@ -133,7 +139,7 @@ GEM et-orbi (1.4.0) tzinfo event_stream_parser (1.0.0) - faraday (2.14.2) + faraday (2.14.3) faraday-net_http (>= 2.0, < 3.5) json logger @@ -177,7 +183,7 @@ GEM prism (>= 1.3.0) rdoc (>= 4.0.0) reline (>= 0.4.2) - json (2.19.5) + json (2.21.2) jwt (3.2.0) base64 kamal (2.11.0) @@ -194,10 +200,10 @@ GEM language_server-protocol (3.17.0.5) lint_roller (1.1.0) logger (1.7.0) - loofah (2.25.1) + loofah (2.25.2) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.9.0) + mail (2.9.1) logger mini_mime (>= 0.1.1) net-imap @@ -212,7 +218,7 @@ GEM minitest (6.0.6) drb (~> 2.0) prism (~> 1.5) - msgpack (1.8.0) + msgpack (1.8.4) multi_xml (0.9.1) bigdecimal (>= 3.1, < 5) multipart-post (2.4.1) @@ -233,19 +239,19 @@ GEM net-protocol net-ssh (7.3.2) nio4r (2.7.5) - nokogiri (1.19.3-aarch64-linux-gnu) + nokogiri (1.19.4-aarch64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.3-aarch64-linux-musl) + nokogiri (1.19.4-aarch64-linux-musl) racc (~> 1.4) - nokogiri (1.19.3-arm-linux-gnu) + nokogiri (1.19.4-arm-linux-gnu) racc (~> 1.4) - nokogiri (1.19.3-arm-linux-musl) + nokogiri (1.19.4-arm-linux-musl) racc (~> 1.4) - nokogiri (1.19.3-arm64-darwin) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) - nokogiri (1.19.3-x86_64-linux-musl) + nokogiri (1.19.4-x86_64-linux-musl) racc (~> 1.4) oauth2 (2.0.22) auth-sanitizer (~> 0.2, >= 0.2.1) @@ -307,30 +313,30 @@ GEM rack (>= 1.3) rackup (2.3.1) rack (>= 3) - rails (8.1.3) - actioncable (= 8.1.3) - actionmailbox (= 8.1.3) - actionmailer (= 8.1.3) - actionpack (= 8.1.3) - actiontext (= 8.1.3) - actionview (= 8.1.3) - activejob (= 8.1.3) - activemodel (= 8.1.3) - activerecord (= 8.1.3) - activestorage (= 8.1.3) - activesupport (= 8.1.3) + rails (8.1.3.1) + actioncable (= 8.1.3.1) + actionmailbox (= 8.1.3.1) + actionmailer (= 8.1.3.1) + actionpack (= 8.1.3.1) + actiontext (= 8.1.3.1) + actionview (= 8.1.3.1) + activejob (= 8.1.3.1) + activemodel (= 8.1.3.1) + activerecord (= 8.1.3.1) + activestorage (= 8.1.3.1) + activesupport (= 8.1.3.1) bundler (>= 1.15.0) - railties (= 8.1.3) + railties (= 8.1.3.1) rails-dom-testing (2.3.0) activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.7.0) - loofah (~> 2.25) + rails-html-sanitizer (1.7.1) + loofah (~> 2.25, >= 2.25.2) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.1.3) - actionpack (= 8.1.3) - activesupport (= 8.1.3) + railties (8.1.3.1) + actionpack (= 8.1.3.1) + activesupport (= 8.1.3.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) @@ -430,13 +436,13 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite3 (2.9.4-aarch64-linux-gnu) - sqlite3 (2.9.4-aarch64-linux-musl) - sqlite3 (2.9.4-arm-linux-gnu) - sqlite3 (2.9.4-arm-linux-musl) - sqlite3 (2.9.4-arm64-darwin) - sqlite3 (2.9.4-x86_64-linux-gnu) - sqlite3 (2.9.4-x86_64-linux-musl) + sqlite3 (2.9.6-aarch64-linux-gnu) + sqlite3 (2.9.6-aarch64-linux-musl) + sqlite3 (2.9.6-arm-linux-gnu) + sqlite3 (2.9.6-arm-linux-musl) + sqlite3 (2.9.6-arm64-darwin) + sqlite3 (2.9.6-x86_64-linux-gnu) + sqlite3 (2.9.6-x86_64-linux-musl) sshkit (1.25.0) base64 logger @@ -485,7 +491,7 @@ GEM bindex (>= 0.4.0) railties (>= 8.0.0) websocket (1.2.11) - websocket-driver (0.8.0) + websocket-driver (0.8.2) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -509,6 +515,7 @@ DEPENDENCIES brakeman bundler-audit capybara + commonmarker debug devise image_processing (~> 1.2) @@ -537,17 +544,17 @@ DEPENDENCIES CHECKSUMS action_text-trix (2.1.18) sha256=3fdb83f8bff4145d098be283cdd47ac41caf5110bfa6df4695ed7127d7fb3642 - actioncable (8.1.3) sha256=e5bc7f75e44e6a22de29c4f43176927c3a9ce4824464b74ed18d8226e75a80f0 - actionmailbox (8.1.3) sha256=df7da474eaa0e70df4ed5a6fef66eb3b3b0f2dbf7f14518deee8d77f1b4aae59 - actionmailer (8.1.3) sha256=831f724891bb70d0aaa4d76581a6321124b6a752cb655c9346aae5479318448d - actionpack (8.1.3) sha256=af998cae4d47c5d581a2cc363b5c77eb718b7c4b45748d81b1887b25621c29a3 - actiontext (8.1.3) sha256=d291019c00e1ea9e6463011fa214f6081a56d7b9a1d224e7d3f6384c1dafc7d2 - actionview (8.1.3) sha256=1347c88c7f3edb38100c5ce0e9fb5e62d7755f3edc1b61cce2eb0b2c6ea2fd5d - activejob (8.1.3) sha256=a149b1766aa8204c3c3da7309e4becd40fcd5529c348cffbf6c9b16b565fe8d3 - activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219 - activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab - activestorage (8.1.3) sha256=0564ce9309143951a67615e1bb4e090ee54b8befed417133cae614479b46384d - activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e + actioncable (8.1.3.1) sha256=e318528295c878a3efdfe25f0f2267c80cb7a76eba41bb5f64d44aa380a3d91b + actionmailbox (8.1.3.1) sha256=5f704972097d843ade8e435e93694a1dac732b926df1717aceba1f3840082b1c + actionmailer (8.1.3.1) sha256=88ea441b28ff02a0c6c006468892642a3d9942affce9d294e81a74504aa5c43c + actionpack (8.1.3.1) sha256=974cb7154548e81f470b1b0f247b99cb38e87825899dca58610596e2817723d0 + actiontext (8.1.3.1) sha256=5da729d833d1a29cddb1eee938878e55e503d2613e00e735f5daf58c2ba98af2 + actionview (8.1.3.1) sha256=2da68b8414c47b43bfbed1ce69c5afe1c04f78c267aacb5660a4cab5ca12cfb6 + activejob (8.1.3.1) sha256=1c8dd275df930df40deecffec63d913a550a33fd94bd298f69721dd96939954a + activemodel (8.1.3.1) sha256=99cc02ce2faec371d14440949d85787ebd23a907c9baef0a9d4bcd4d21888f88 + activerecord (8.1.3.1) sha256=0a2fb6c28f4938f6b013a3a549bec0a7e37d535f3dc8990e804bcc3258c0403b + activestorage (8.1.3.1) sha256=f555254f387b1cffa499d2fd3115d12635eadc5b15206a8534316a67036163ef + activesupport (8.1.3.1) sha256=85458765f25ea48b9019c46b6bb3fa5683197bf4280d9f06710a6e8d7a831376 addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 async (2.39.0) sha256=df18730073f2bbb45788077dfa20cb365ecc1b9453969f44de6796b5191a00aa @@ -558,14 +565,20 @@ CHECKSUMS bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e bootsnap (1.24.5) sha256=36b677448524d279b470469aabd5dff4a980e3fa4931a0df68da4a500eb1b6c4 - brakeman (8.0.5) sha256=03735f9690d3fd4b32d66aacbf0a6d15a84266bdd06b32c05c8ecc8f6021d2be + brakeman (8.0.6) sha256=759cc69341115e6c2dcd47b6fd8649a0b9bd540e3585ac8a0a94e31c66fee386 builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 capybara (3.40.0) sha256=42dba720578ea1ca65fd7a41d163dd368502c191804558f6e0f71b391054aeef - concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab + commonmarker (2.9.0-aarch64-linux) sha256=e2b4aadb9a2cfa4e206582641ce3a49465549ac1ed4c289fdd63b78d8f24579c + commonmarker (2.9.0-aarch64-linux-musl) sha256=1cf4d2821f2a7e64945f1ed7c4eeced586fdb27ea7fe7337788802cf8f57cfca + commonmarker (2.9.0-arm-linux) sha256=290a000947cf9d154d0480c33c0a36bcef49ce4c2e74bb75e9d06f5afe93f670 + commonmarker (2.9.0-arm64-darwin) sha256=1748dbfa4f5813b0d2a14bb4bbfa65a4ec293aa1c825016d60029ee0e132b046 + commonmarker (2.9.0-x86_64-linux) sha256=8cfe92970eef585a19ddf6613224b91cab64d6029834661bda801f877c9c7f43 + commonmarker (2.9.0-x86_64-linux-musl) sha256=293921398b839f79ceaf55010e061357e34f053822c3b003cd0be6686176335e + concurrent-ruby (1.3.8) sha256=b2f1be836e968ccc78ccfce277ea79c72a88633f22306782c16ff23fb415d1e1 connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a console (1.34.3) sha256=869fbd74697efc4c606f102d2812b0b008e4e7fd738a91c591e8577140ec0dcc - crass (1.0.6) sha256=dc516022a56e7b3b156099abc81b6d2b08ea1ed12676ac7a5657617f012bd45d + crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295 date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 devise (5.0.4) sha256=d605f2b85854e74e56ee789e2d398702bc2d06e6bcd894717a670a3199c74cc1 @@ -576,7 +589,7 @@ CHECKSUMS erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 et-orbi (1.4.0) sha256=6c7e3c90779821f9e3b324c5e96fda9767f72995d6ae435b96678a4f3e2de8bc event_stream_parser (1.0.0) sha256=a2683bab70126286f8184dc88f7968ffc4028f813161fb073ec90d171f7de3c8 - faraday (2.14.2) sha256=73ccb9994a9e8648f010e32eca2ae82e41c57860aa10932cda29418b9e0223ad + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe @@ -599,21 +612,21 @@ CHECKSUMS io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc io-event (1.15.1) sha256=c644cdcf48254015d63f558bf4492f35471f5bb204a42180ea49752be59b30cc irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 - json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 kamal (2.11.0) sha256=1408864425e0dec7e0a14d712a3b13f614e9f3a425b7661d3f9d287a51d7dd75 language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 - loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 - mail (2.9.0) sha256=6fa6673ecd71c60c2d996260f9ee3dd387d4673b8169b502134659ece6d34941 + loofah (2.25.2) sha256=2007f746959ac65552456e04b433e83deb22759ab38c838b4445c70e43425918 + mail (2.9.1) sha256=06574eca475253d6c18145dd70af80d0eb970182d55053497c5f4d797ea160e8 marcel (1.1.0) sha256=fdcfcfa33cc52e93c4308d40e4090a5d4ea279e160a7f6af988260fa970e0bee matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072 mini_magick (5.3.1) sha256=29395dfd76badcabb6403ee5aff6f681e867074f8f28ce08d78661e9e4a351c4 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 - msgpack (1.8.0) sha256=e64ce0212000d016809f5048b48eb3a65ffb169db22238fb4b72472fecb2d732 + msgpack (1.8.4) sha256=4411c22d350dd1c20250f7eada3cca2695438c2f769cf0782f0cd065d90a3e7b multi_xml (0.9.1) sha256=7ce766b59c17241ed62976caeae1fae9b2431b263398c35396239a68c4a64e57 multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 @@ -625,13 +638,13 @@ CHECKSUMS net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 net-ssh (7.3.2) sha256=65029e213c380e20e5fd92ece663934ab0a0fe888e0cd7cc6a5b664074362dd4 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 - nokogiri (1.19.3-aarch64-linux-musl) sha256=8392dfdcd21be7a94dbbe9ccc138dea01b97b24cb2dc02a114ca98bfb1d9a0b7 - nokogiri (1.19.3-arm-linux-gnu) sha256=3919d5ffc334ad778a4a9eb88fda7dcb8b1fb58c8a52ac640c6dcd2f038e774f - nokogiri (1.19.3-arm-linux-musl) sha256=9ce1cb6346bb9c67b1550eb537aa183ead91e4b6eadb2f36ade02d8dd2a79fb6 - nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42 - nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 - nokogiri (1.19.3-x86_64-linux-musl) sha256=248c906d2166eca5efb56d52fdee5f9a1f51d69a72e2b64fdac647b4ce39ea3f + nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f + nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af + nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca + nokogiri (1.19.4-arm-linux-musl) sha256=588923c101bcfa78869734d247d25b598674323e7f22474fc468f6e5647311eb + nokogiri (1.19.4-arm64-darwin) sha256=a46db9853286e6597b36ebc6953817d15acf3a299583eb3f89fdc6f91dd63527 + nokogiri (1.19.4-x86_64-linux-gnu) sha256=379fae440b28915e3f19d752ce2dcf8465ed2b2fbefd2a7ca0dd497bc981a06a + nokogiri (1.19.4-x86_64-linux-musl) sha256=17dfb7c1fa194ae02fbf7c51a7afc8d278045ab3fdacfd86f91d02d7b274470b oauth2 (2.0.22) sha256=8f4669f0c8de69f6db7ed786bda20996eaf0003966b886f1c519efb1a5682fa6 octokit (10.0.0) sha256=82e99a539b7637b7e905e6d277bb0c1a4bed56735935cc33db6da7eae49a24e8 omniauth (2.1.4) sha256=42a05b0496f0d22e1dd85d42aaf602f064e36bb47a6826a27ab55e5ba608763c @@ -656,10 +669,10 @@ CHECKSUMS rack-session (2.1.2) sha256=595434f8c0c3473ae7d7ac56ecda6cc6dfd9d37c0b2b5255330aa1576967ffe8 rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 rackup (2.3.1) sha256=6c79c26753778e90983761d677a48937ee3192b3ffef6bc963c0950f94688868 - rails (8.1.3) sha256=6d017ba5348c98fc909753a8169b21d44de14d2a0b92d140d1a966834c3c9cd3 + rails (8.1.3.1) sha256=ccd11a36bfc171bf9c66d585d14c0ece91c0c9dde840aae60c0118d6f5c9c52a rails-dom-testing (2.3.0) sha256=8acc7953a7b911ca44588bf08737bc16719f431a1cc3091a292bca7317925c1d - rails-html-sanitizer (1.7.0) sha256=28b145cceaf9cc214a9874feaa183c3acba036c9592b19886e0e45efc62b1e89 - railties (8.1.3) sha256=913eb0e0cb520aac687ffd74916bd726d48fa21f47833c6292576ef6a286de22 + rails-html-sanitizer (1.7.1) sha256=e797a7c9b01e567307e317c576b49ab4168017e63eea4dba9ce3cb587e2f22c2 + railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 @@ -685,13 +698,13 @@ CHECKSUMS solid_cable (3.0.12) sha256=a168a54731a455d5627af48d8441ea3b554b8c1f6e6cd6074109de493e6b0460 solid_cache (1.0.10) sha256=bc05a2fb3ac78a6f43cbb5946679cf9db67dd30d22939ededc385cb93e120d41 solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a - sqlite3 (2.9.4-aarch64-linux-gnu) sha256=ecabed721e6eaad54601d2685f09029d90025efc8d931040dc89cb3f8a2080ec - sqlite3 (2.9.4-aarch64-linux-musl) sha256=ffb4255947fb54c8c3eeca97460c9702b40de91ce390455ef7367ca6a3929a31 - sqlite3 (2.9.4-arm-linux-gnu) sha256=9ee2008b9fbec984c3c165b0d7eedd2bd2a415100b761bfa3a4c6fbec9208bf6 - sqlite3 (2.9.4-arm-linux-musl) sha256=8dc1fe4da6977992cd62decf4a93ccf6cc2e124a5e6a340160d52092f70e837a - sqlite3 (2.9.4-arm64-darwin) sha256=1d5aad413a815d236e96d43f05a1acc600b6cd086800770342a3f9c2877499ff - sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 - sqlite3 (2.9.4-x86_64-linux-musl) sha256=3fc5e865b4be9a85d998203ef8d0c0fdcb92f20acf34a254346ff8a19088efec + sqlite3 (2.9.6-aarch64-linux-gnu) sha256=d8b1f7d23efd7abac285775a9566562fc7debfef79d594e3a20354406fb7907c + sqlite3 (2.9.6-aarch64-linux-musl) sha256=3579e1c98cdc7ff5c3722847bb63ed4e1efb7ff675cb5e1e48ef2d4da5fb3bc9 + sqlite3 (2.9.6-arm-linux-gnu) sha256=33541500e3615da02afe54a9cc38b17a6985d3cf9d8b76d6d0a83002f114e7ec + sqlite3 (2.9.6-arm-linux-musl) sha256=c5490af48bb228fefa54314e9541375c3907e70f8109f3881b5ff97e1c93ae33 + sqlite3 (2.9.6-arm64-darwin) sha256=849b5d7f795e60fe25076d62c72dd722beb45b3850b516ad978d60ee848ec15b + sqlite3 (2.9.6-x86_64-linux-gnu) sha256=613188ce02f614126ddbc38c5e217ccffd6306d0dcd9adca9764547aa890a634 + sqlite3 (2.9.6-x86_64-linux-musl) sha256=d493b11818a3573387a1d56e1ee8fa00da23a683a7a1cc063e7a0feeed843abf sshkit (1.25.0) sha256=c8c6543cdb60f91f1d277306d585dd11b6a064cb44eab0972827e4311ff96744 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 @@ -721,7 +734,7 @@ CHECKSUMS warden (1.2.9) sha256=46684f885d35a69dbb883deabf85a222c8e427a957804719e143005df7a1efd0 web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 - websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-driver (0.8.2) sha256=97c556b019bf3410b4961002ac501621e9322d3f8a7bc02161a09301cc4c4146 websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 xpath (3.2.0) sha256=6dfda79d91bb3b949b947ecc5919f042ef2f399b904013eb3ef6d20dd3a4082e zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 51c7e4e..0375bee 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -861,7 +861,7 @@ body { letter-spacing: var(--tracking-caps); margin-bottom: 4px; } -.msg-body { color: var(--fg); white-space: pre-wrap; line-height: 1.55; } +.msg-body { color: var(--fg); white-space: pre-wrap; line-height: 1.55; overflow-wrap: anywhere; } .msg-pill { font-family: var(--hi-font-mono); font-size: 20px; @@ -880,6 +880,57 @@ body { animation: hi-blink 1.6s ease-in-out infinite; } +/* Assistant replies rendered from markdown (lib/markdown.rb). Replaces .msg-body's + pre-wrap — markdown supplies
/
structure, and pre-wrap on top of that
+ would double every gap. Must stay after .msg-body: equal specificity, so source
+ order decides. */
+.msg-prose { white-space: normal; }
+.msg-prose > :first-child { margin-block-start: 0; }
+.msg-prose > :last-child { margin-block-end: 0; }
+.msg-prose p { margin-block: 0 10px; }
+.msg-prose ul,
+.msg-prose ol {
+ margin-block: 0 10px;
+ padding-inline-start: 1.4em;
+}
+.msg-prose ul { list-style: disc; }
+.msg-prose ol { list-style: decimal; }
+.msg-prose li + li { margin-block-start: 6px; }
+/* The chat model separates numbered items with blank lines, which makes the list
+ loose and wraps each item's content in
. Collapse those. */ +.msg-prose li > p { margin-block: 0; } +.msg-prose li > p + p { margin-block-start: 6px; } +.msg-prose strong { font-weight: 600; } +/* Inline code. A fenced block is
, handled below, so this can never
+ receive a newline and needs no white-space of its own. */
+.msg-prose code {
+ font-family: var(--hi-font-mono);
+ font-size: 0.92em;
+ background: var(--bg-sunken);
+ padding: 1px 6px;
+ border-radius: var(--radius-sm);
+}
+/* Fenced block, at any depth — top level or inside an . The panel (padding +
+ background) lives on rather than on the inner so it stays put
+ while the content scrolls horizontally instead of sliding out from under it. */
+.msg-prose pre {
+ margin-block: 0 10px;
+ padding: 8px 10px;
+ background: var(--bg-sunken);
+ border-radius: var(--radius-sm);
+ /* `pre`, not `pre-wrap`: a code line must not be broken mid-token. It also
+ neutralises the inherited overflow-wrap: anywhere, which would otherwise
+ split a long URL or path inside the block. With wrapping off the line can
+ overflow, which is what overflow-x scrolls. */
+ white-space: pre;
+ overflow-x: auto;
+}
+.msg-prose pre > code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+}
+
/* ============================================================
COMPOSER
============================================================ */
diff --git a/app/helpers/messages_helper.rb b/app/helpers/messages_helper.rb
index 19b552e..5dc16b9 100644
--- a/app/helpers/messages_helper.rb
+++ b/app/helpers/messages_helper.rb
@@ -15,4 +15,23 @@ def tool_call_pill_text(message)
"running: #{message.tool_calls.map(&:name).uniq.join(", ")}"
end
end
+
+ # Assistant replies render as markdown once the stream has finished; user text and
+ # mid-stream text stay plain. Returns an html_safe fragment in the formatted case
+ # and a plain String otherwise, so the view's <%= %> escapes it.
+ def message_body_html(message, streaming: false)
+ return message.content.to_s unless format_message_body?(message, streaming)
+
+ Markdown.render(message.content)
+ end
+
+ def message_body_class(message, streaming: false)
+ format_message_body?(message, streaming) ? "msg-body msg-prose" : "msg-body"
+ end
+
+ private
+
+ def format_message_body?(message, streaming)
+ !streaming && message.role == "assistant"
+ end
end
diff --git a/app/jobs/chat_respond_job.rb b/app/jobs/chat_respond_job.rb
index 9b442c2..1a700d1 100644
--- a/app/jobs/chat_respond_job.rb
+++ b/app/jobs/chat_respond_job.rb
@@ -81,7 +81,10 @@ def broadcast_replace(project, message)
project,
target: ActionView::RecordIdentifier.dom_id(message),
partial: "messages/message",
- locals: { message: message }
+ # Mid-stream: markdown is still incomplete (an unclosed ``` fence would
+ # swallow the rest of the reply), so render plain text and let the final
+ # broadcast from Message#broadcast_replace_message format it.
+ locals: { message: message, streaming: true }
)
end
end
diff --git a/app/models/message.rb b/app/models/message.rb
index e6ae0a0..66a91d2 100644
--- a/app/models/message.rb
+++ b/app/models/message.rb
@@ -25,7 +25,12 @@ def broadcast_append_message
broadcast_append_later_to chat.project,
target: "messages",
partial: "messages/message",
- locals: { message: self }
+ # `_later` renders inside Turbo::Streams::ActionBroadcastJob, ~100-300ms
+ # after this commit, from a GlobalID reload — by which point ChatRespondJob
+ # has already written chunks. An append is only ever a user message or
+ # RubyLLM's empty assistant placeholder, never a finished reply, so it must
+ # never format.
+ locals: { message: self, streaming: true }
end
def broadcast_replace_message
diff --git a/app/prompts/generator_agent/instructions.txt.erb b/app/prompts/generator_agent/instructions.txt.erb
index d773548..53c6f4e 100644
--- a/app/prompts/generator_agent/instructions.txt.erb
+++ b/app/prompts/generator_agent/instructions.txt.erb
@@ -17,6 +17,7 @@ GENERAL RULES:
- Do NOT generate an implementation plan yourself. Do NOT list models, controllers, or files. That's the backend's job.
- After `create_application` or `modify_application` returns, leave your text response empty. Do not narrate that a build started or finished — those events surface in the UI on their own; you don't need to announce them.
- Call each tool AT MOST ONCE per user turn. After a tool returns, write your final reply (or no reply at all, per the rule above) and stop — do not call the same tool again until the user replies. Calling the same tool twice in one turn corrupts the conversation history.
+- Formatting: write short paragraphs. Use **bold** for emphasis, `backticks` for code, file and command names, and `-` or `1.` lists where a list genuinely helps. Do not use headings, tables, images, or raw HTML. Write URLs plainly rather than as markdown links. If you need to show markup, put it in backticks.
Current project state:
<%= current_state %>
diff --git a/app/views/messages/_message.html.erb b/app/views/messages/_message.html.erb
index a8941bd..fc63f16 100644
--- a/app/views/messages/_message.html.erb
+++ b/app/views/messages/_message.html.erb
@@ -1,3 +1,4 @@
+<% streaming = local_assigns.fetch(:streaming, false) %>