maquina_stream 0.1.0

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/.rdoc_options +30 -0
  3. data/CHANGELOG.md +38 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +163 -0
  6. data/app/assets/stylesheets/maquina_stream/components/attachment.css +35 -0
  7. data/app/assets/stylesheets/maquina_stream/components/code_block.css +30 -0
  8. data/app/assets/stylesheets/maquina_stream/components/shimmer.css +31 -0
  9. data/app/assets/stylesheets/maquina_stream/components/snippet.css +22 -0
  10. data/app/assets/stylesheets/maquina_stream/components/source_citation.css +14 -0
  11. data/app/assets/stylesheets/maquina_stream/components/suggestion.css +32 -0
  12. data/app/assets/stylesheets/maquina_stream/reveal.css +48 -0
  13. data/app/assets/stylesheets/maquina_stream/themes/dark.css +235 -0
  14. data/app/assets/stylesheets/maquina_stream/themes/light.css +117 -0
  15. data/app/controllers/maquina_stream/application_controller.rb +21 -0
  16. data/app/controllers/maquina_stream/blocks_controller.rb +41 -0
  17. data/app/controllers/maquina_stream/manifests_controller.rb +15 -0
  18. data/app/helpers/maquina_stream/components_helper.rb +80 -0
  19. data/app/javascript/maquina_stream/controllers/application_controller.js +169 -0
  20. data/app/javascript/maquina_stream/controllers/ms_autoscroll_controller.js +110 -0
  21. data/app/javascript/maquina_stream/controllers/ms_code_controller.js +96 -0
  22. data/app/javascript/maquina_stream/controllers/ms_deferred_controller.js +223 -0
  23. data/app/javascript/maquina_stream/controllers/ms_diagram_controller.js +40 -0
  24. data/app/javascript/maquina_stream/controllers/ms_link_safety_controller.js +196 -0
  25. data/app/javascript/maquina_stream/controllers/ms_math_controller.js +32 -0
  26. data/app/javascript/maquina_stream/controllers/ms_repair_controller.js +167 -0
  27. data/app/javascript/maquina_stream/controllers/ms_reveal_controller.js +320 -0
  28. data/app/javascript/maquina_stream/controllers/ms_table_controller.js +183 -0
  29. data/app/javascript/maquina_stream/index.js +59 -0
  30. data/app/views/maquina_stream/components/_attachment.html.erb +139 -0
  31. data/app/views/maquina_stream/components/_code_block.html.erb +74 -0
  32. data/app/views/maquina_stream/components/_shimmer.html.erb +36 -0
  33. data/app/views/maquina_stream/components/_snippet.html.erb +50 -0
  34. data/app/views/maquina_stream/components/_source_citation.html.erb +42 -0
  35. data/app/views/maquina_stream/components/_suggestion.html.erb +73 -0
  36. data/config/importmap.rb +10 -0
  37. data/config/locales/en.yml +79 -0
  38. data/config/locales/es.yml +82 -0
  39. data/config/routes.rb +11 -0
  40. data/docs/configuration.md +219 -0
  41. data/docs/deferred-renderers.md +184 -0
  42. data/docs/getting-started.md +356 -0
  43. data/docs/javascript.md +298 -0
  44. data/docs/registries.md +283 -0
  45. data/docs/repair.md +162 -0
  46. data/docs/security.md +247 -0
  47. data/docs/streaming.md +308 -0
  48. data/lib/generators/maquina_stream/install/USAGE +26 -0
  49. data/lib/generators/maquina_stream/install/install_generator.rb +199 -0
  50. data/lib/generators/maquina_stream/install/templates/initializer.rb.tt +121 -0
  51. data/lib/generators/maquina_stream/streamable/USAGE +28 -0
  52. data/lib/generators/maquina_stream/streamable/streamable_generator.rb +187 -0
  53. data/lib/generators/maquina_stream/streamable/templates/migration.rb.tt +21 -0
  54. data/lib/generators/maquina_stream/streamable/templates/model.rb.tt +4 -0
  55. data/lib/maquina_stream/block.rb +99 -0
  56. data/lib/maquina_stream/broadcaster.rb +233 -0
  57. data/lib/maquina_stream/component_cache.rb +0 -0
  58. data/lib/maquina_stream/components/contract.rb +184 -0
  59. data/lib/maquina_stream/components.rb +135 -0
  60. data/lib/maquina_stream/configuration.rb +240 -0
  61. data/lib/maquina_stream/document.rb +296 -0
  62. data/lib/maquina_stream/engine.rb +46 -0
  63. data/lib/maquina_stream/errors.rb +17 -0
  64. data/lib/maquina_stream/export.rb +66 -0
  65. data/lib/maquina_stream/frame.rb +73 -0
  66. data/lib/maquina_stream/manifest.rb +137 -0
  67. data/lib/maquina_stream/registries.rb +116 -0
  68. data/lib/maquina_stream/renderer/fence.rb +115 -0
  69. data/lib/maquina_stream/renderer/post_pass.rb +363 -0
  70. data/lib/maquina_stream/renderer/tag_blocks.rb +276 -0
  71. data/lib/maquina_stream/renderer/view_context.rb +72 -0
  72. data/lib/maquina_stream/renderer.rb +128 -0
  73. data/lib/maquina_stream/sanitizer.rb +392 -0
  74. data/lib/maquina_stream/streamable.rb +281 -0
  75. data/lib/maquina_stream/text_direction.rb +56 -0
  76. data/lib/maquina_stream/themes.rb +84 -0
  77. data/lib/maquina_stream/version.rb +5 -0
  78. data/lib/maquina_stream.rb +175 -0
  79. metadata +204 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b102e4961a1ab8c838ac27577f9358003a64c31491569600600e5782f1d0b9e0
4
+ data.tar.gz: 0c4c875bd048b17c0533fa86dd0c739d32344a2c376d26034b3c45896d056ff5
5
+ SHA512:
6
+ metadata.gz: ab81d7757223c030d6748fc87abe5124f528497a1562994947c16a1e2ec4a4992e504144beacb83d951b7ebfd36d8d33d43bb6ef542d34e825963c9133f96c5c
7
+ data.tar.gz: 81e39ef8e44f0437cbcd6d83cd8c2fd78d61aa3a945cca8294b663dda8c6869b30f6534f491ae8d57104696f00e7cb4546dbb2ca8b851eb1cae8b64da407c57b
data/.rdoc_options ADDED
@@ -0,0 +1,30 @@
1
+ ---
2
+ # RDoc reads this file from the gem root. Comments in lib/ and app/ are written
3
+ # in Markdown, not RDoc markup — see the `markup` key below.
4
+ markup: markdown
5
+ title: maquina_stream — server-rendered streaming markdown for Rails
6
+ main_page: README.md
7
+ # Everything that documents how the gem was BUILT rather than how it is USED.
8
+ # sdd/ holds the phase specs, test/ includes the whole dummy host application,
9
+ # and docs/{plan,design,remend-patterns,spike-sourcepos}.md are the same four
10
+ # files the gemspec does not package: none of them documents the gem to
11
+ # somebody who installs it.
12
+ #
13
+ # app/javascript is excluded because the JavaScript is documented as a DOM and
14
+ # controller contract in docs/javascript.md, which is a better description of
15
+ # it than RDoc's JavaScript parser produces.
16
+ #
17
+ # These are matched against ABSOLUTE paths, so each pattern is anchored to a
18
+ # path boundary rather than to the start of the string.
19
+ exclude:
20
+ - "(?:\\A|/)test/"
21
+ - "(?:\\A|/)sdd/"
22
+ - "(?:\\A|/)bin/"
23
+ - "(?:\\A|/)tmp/"
24
+ - "(?:\\A|/)pkg/"
25
+ - "(?:\\A|/)doc/"
26
+ - "(?:\\A|/)\\.github/"
27
+ - "(?:\\A|/)app/javascript/"
28
+ - "(?:\\A|/)docs/(?:plan|design|remend-patterns|spike-sourcepos)\\.md\\z"
29
+ - "(?:\\A|/)CLAUDE\\.md\\z"
30
+ - "(?:\\A|/)Gemfile(?:\\.lock)?\\z"
data/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ ## Unreleased — 0.1.0
4
+
5
+ First working version. The engine renders a streaming markdown buffer to HTML on
6
+ the server and broadcasts it over Turbo Streams; only rendered HTML reaches the
7
+ browser.
8
+
9
+ - **Render pipeline** — `maquina_remend` → commonmarker with sourcepos →
10
+ a single Nokogiri post-pass → sanitizer. Pure: it runs outside a Rails request
11
+ with no stubbing, asserted in a subprocess with no application booted.
12
+ - **Sealing and broadcast** — blocks seal behind a configurable lag, ids are
13
+ index-derived, and a sealed block is never re-broadcast. The seal pointer
14
+ stops at an unresolved link reference definition, because a definition
15
+ rewrites links arbitrarily far above it.
16
+ - **Repair** — a windowed manifest of block digests, four triggers, and a silent
17
+ morph of only the blocks that differ. Deltas are an optimization; correctness
18
+ lives here.
19
+ - **Interaction** — copy, download, table export, fullscreen, link safety and
20
+ autoscroll, every control individually disableable and inert while streaming.
21
+ - **Client-deferred renderers** — diagrams and math render in the browser from
22
+ one JSON payload per leaf node, with split ownership across morphs.
23
+ - **Registries** — elements, custom tags and fences, the last with server,
24
+ passthrough and client strategies.
25
+ - **Text direction per block**, decided by the first strong character. Fences
26
+ carrying bidi control characters are pinned, against Trojan Source.
27
+ - **History** — sealed messages are frozen and fragment-cached by buffer digest;
28
+ 1942x on a warm cache.
29
+ - **Generators** — `maquina_stream:install` wires the engine into a host (the
30
+ initializer, the mount, the Turbo and engine importmap pins, the Stimulus
31
+ registration, the stylesheets) and `maquina_stream:streamable Message` makes
32
+ one model streamable (the migration, generated from the `Streamable` contract
33
+ itself, and the macro). Idempotent, non-destructive, and loud about anything
34
+ they cannot do. Both seams stay the host's: `authorize` is generated as a stub
35
+ that denies.
36
+ - Spanish default locale, English secondary. No Node build step anywhere.
37
+
38
+ Not yet released: `maquina_remend` is still a path reference.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mario Alberto Chávez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # maquina_stream
2
+
3
+ Server-rendered streaming markdown for Rails, over Turbo, with a repair path.
4
+
5
+ A model writes markdown a token at a time. `maquina_stream` renders it to HTML
6
+ on the server, broadcasts small patches as it grows, and reconciles the browser
7
+ with the truth when frames go missing — which they do, because Action Cable
8
+ offers no delivery guarantee.
9
+
10
+ **Only rendered HTML reaches the browser.** The client never parses markdown.
11
+
12
+ ## Install
13
+
14
+ ```ruby
15
+ gem "maquina_stream"
16
+ ```
17
+
18
+ Rails 8, Ruby 3.3+. Depends on `maquina_remend`, `commonmarker`, `nokogiri` and
19
+ `rouge`. `maquina_components` is optional — without it, components render plain
20
+ Tailwind fallbacks. Turbo is required: repair applies Turbo Stream morphs, and
21
+ with `window.Turbo` undefined every repair fails silently.
22
+
23
+ ```sh
24
+ bin/rails generate maquina_stream:install
25
+ bin/rails generate maquina_stream:streamable Message
26
+ bin/rails db:migrate
27
+ ```
28
+
29
+ The first wires the engine into the app — the initializer, the mount, the
30
+ importmap pins, the Stimulus registration, the stylesheets. The second is per
31
+ model, because a host may have several: the migration carrying the contract
32
+ columns, and the `include` plus macro in the model. Both are idempotent and
33
+ neither overwrites a file; a host missing an ingredient is told what to paste
34
+ rather than left with a silent no-op.
35
+
36
+ ## Two seams the generators will not guess
37
+
38
+ **`authorize`**, in `config/initializers/maquina_stream.rb`. It is generated as
39
+ a stub that denies everything, which is the safe failure and a broken feature
40
+ both — the browser can never repair a message until you replace it. Guessing a
41
+ host's authorization is how an engine leaks other people's messages:
42
+
43
+ ```ruby
44
+ c.authorize = ->(record, request) { record.conversation.member?(request.session[:user_id]) }
45
+ ```
46
+
47
+ **`stream_for:`**, in the model. Who may subscribe to a stream is your question,
48
+ not the engine's:
49
+
50
+ ```ruby
51
+ class Message < ApplicationRecord
52
+ include MaquinaStream::Streamable
53
+
54
+ maquina_stream buffer: :content,
55
+ stream_for: ->(record) { [:conversation, record.conversation_id, :messages] }
56
+ end
57
+ ```
58
+
59
+ ## Streaming a message
60
+
61
+ Feed the broadcaster and seal exactly once:
62
+
63
+ ```ruby
64
+ broadcaster = MaquinaStream::Broadcaster.new(message)
65
+ chat.ask(prompt) { |chunk| broadcaster.append(chunk.content.to_s) }
66
+ broadcaster.seal!
67
+ ```
68
+
69
+ Render it, live or from history. The engine appends block HTML into
70
+ `#ms-msg-<sid>` and never creates that element — the wrapper is yours:
71
+
72
+ ```erb
73
+ <div id="ms-msg-<%= message.maquina_stream_id %>"
74
+ data-controller="ms-repair ms-reveal"
75
+ data-ms-repair-manifest-url-value="<%= maquina_stream.manifest_path(sid: message.maquina_stream_id) %>"
76
+ data-ms-repair-blocks-url-value="<%= maquina_stream.blocks_path(sid: message.maquina_stream_id) %>"
77
+ <%= "data-ms-streaming" if message.maquina_stream_open? %>><%= MaquinaStream.render(message) %></div>
78
+ ```
79
+
80
+ That is the whole integration. [Getting started](docs/getting-started.md) walks
81
+ the same path with a working end-to-end run, and lists every step the
82
+ generators take for hosts that would rather do them by hand.
83
+
84
+ ## What you get
85
+
86
+ Markdown in:
87
+
88
+ ````markdown
89
+ ## Status
90
+
91
+ All **good**. See [docs](https://example.com).
92
+
93
+ ```ruby
94
+ puts 1
95
+ ```
96
+ ````
97
+
98
+ HTML out — sanitized, block-addressed, and highlighted by CSS class so a theme
99
+ switch needs no re-render (attributes elided for width):
100
+
101
+ ```html
102
+ <h2 id="ms-42-b0" data-ms-element="h2" data-ms-block data-ms-block-digest="fa59d158d6fc403e">Status…</h2>
103
+ <p id="ms-42-b1" data-ms-element="p" data-ms-block data-ms-block-digest="c39b9098233d53d5">All <strong>good</strong>. See <a href="https://example.com" rel="noopener noreferrer">docs</a>.</p>
104
+ <div id="ms-42-b2" data-ms-code data-ms-code-lang="ruby" data-controller="ms-code" data-ms-block …>
105
+ <pre><code><span class="nb">puts</span> <span class="mi">1</span></code></pre>
106
+ <pre hidden data-ms-code-source>puts 1</pre>
107
+ </div>
108
+ ```
109
+
110
+ Plus: copy and download on code blocks and tables, a link-safety dialog, a
111
+ word-level reveal animation, autoscroll that gets out of the reader's way,
112
+ client-rendered diagrams and math, and markdown export.
113
+
114
+ ## Agents
115
+
116
+ This engine is built for agent output, and a tool call is its own `Streamable`
117
+ record rather than a block inside the assistant's message — so each step of a
118
+ run seals, repairs and exports on its own, and two steps can be open at once,
119
+ which is what a parallel tool call is.
120
+
121
+ [Nexo](https://maquina.app/documentation/nexo/) is the agent harness it is built
122
+ alongside: `Agent#prompt` reports tool activity through a block, and each
123
+ `:tool_call` opens a record. `Message.stream_agent_run` in
124
+ `test/dummy/app/models/message.rb` is the whole bridge, and `/harness/agent`
125
+ runs it against a real model. See [streaming.md](docs/streaming.md#streaming-an-agent-run).
126
+
127
+ Nothing here depends on Nexo. Bare `ruby_llm`, or any client that hands you text
128
+ as it arrives, works the same way — `/harness/chat` is that version.
129
+
130
+ ## Documentation
131
+
132
+ | Document | What it covers |
133
+ |---|---|
134
+ | [getting-started.md](docs/getting-started.md) | Install, the model, the migration, the view, the JavaScript, one message end to end |
135
+ | [configuration.md](docs/configuration.md) | Every option, its default, and how to choose |
136
+ | [streaming.md](docs/streaming.md) | The `Streamable` contract, the broadcaster, sealing, agent runs, history |
137
+ | [repair.md](docs/repair.md) | Why repair exists, the two routes, `find_stream` and `authorize` |
138
+ | [registries.md](docs/registries.md) | Fences, custom tags, element overrides |
139
+ | [javascript.md](docs/javascript.md) | The Stimulus controllers, importmap setup, controls, events |
140
+ | [security.md](docs/security.md) | The sanitizer's guarantees and limits, URL hardening |
141
+ | [deferred-renderers.md](docs/deferred-renderers.md) | Diagrams, math, writing your own |
142
+
143
+ `test/dummy` is a working host: the `Streamable` model, both seams, all three
144
+ fence strategies, the tag registry, and live pages at `/harness/chat` and
145
+ `/harness/agent` that talk to a real model.
146
+
147
+ ## What it deliberately does not do
148
+
149
+ - **Deltas do not converge on their own.** Only the open tail is patched; a
150
+ block that changes after it stops being the tail is fixed by the repair path.
151
+ - **Sealed blocks are never re-broadcast.** That is what keeps bandwidth
152
+ tracking drift instead of message length.
153
+ - **An open fence is never highlighted**, and a client-deferred fence emits no
154
+ payload until it closes. Expect no syntax colours and no diagram until the
155
+ closing ``` arrives.
156
+
157
+ ## maquina
158
+
159
+ Part of [maquina](https://maquina.app) — open source for Ruby and Ruby AI.
160
+
161
+ ## License
162
+
163
+ MIT, © Mario Alberto Chávez. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,35 @@
1
+ /* Vendored fallback for the `attachment` component. Loaded only while the
2
+ * vendored partial is the one rendering.
3
+ *
4
+ * Layout is Tailwind, in the partial. What lives here is what a utility class
5
+ * cannot express: the variant shapes, truncation of a long filename, and the
6
+ * hidden state the download control takes when its image fails to load.
7
+ */
8
+ [data-component="attachment"] {
9
+ --attachment-radius: var(--surface-radius, 0.5rem);
10
+
11
+ border-radius: var(--attachment-radius);
12
+ }
13
+
14
+ [data-component="attachment"][data-variant="inline"] {
15
+ --attachment-radius: 9999px;
16
+ }
17
+
18
+ [data-component="attachment"] [data-attachment-part="filename"],
19
+ [data-component="attachment"] [data-attachment-part="details"] {
20
+ overflow: hidden;
21
+ text-overflow: ellipsis;
22
+ white-space: nowrap;
23
+ }
24
+
25
+ [data-component="attachment"] [data-attachment-part="thumbnail"] {
26
+ object-fit: cover;
27
+ background-color: var(--attachment-thumbnail-bg, rgb(0 0 0 / 0.04));
28
+ }
29
+
30
+ /* `ms-attachment` sets this when the thumbnail errors: a blob that will not
31
+ * load must not offer a download that cannot work.
32
+ */
33
+ [data-component="attachment"] [data-attachment-part="download"][hidden] {
34
+ display: none;
35
+ }
@@ -0,0 +1,30 @@
1
+ /* Vendored fallback for the `code-block` component.
2
+ *
3
+ * Loaded ONLY while maquina_stream renders the vendored partial. The day
4
+ * maquina_components ships `code-block`, the resolver stops returning our
5
+ * partial, MaquinaStream::Components.stylesheets stops listing this file, and
6
+ * the same selectors are not defined twice.
7
+ *
8
+ * Colour lives in the two Rouge theme stylesheets, never inline, so a dark
9
+ * mode switch is a stylesheet swap and not a re-render.
10
+ */
11
+ [data-component="code-block"] {
12
+ --code-block-radius: var(--surface-radius, 0.5rem);
13
+
14
+ position: relative;
15
+ border-radius: var(--code-block-radius);
16
+ }
17
+
18
+ [data-component="code-block"] [data-code-block-part="pre"] {
19
+ margin: 0;
20
+ overflow-x: auto;
21
+ }
22
+
23
+ [data-component="code-block"] [data-code-block-part="code"] {
24
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
25
+ tab-size: 2;
26
+ }
27
+
28
+ [data-component="code-block"] script[data-ms-code-source] {
29
+ display: none;
30
+ }
@@ -0,0 +1,31 @@
1
+ /* Engine-owned and permanent: the one skeleton for open blocks and
2
+ * unrendered deferred payloads. Pure CSS, zero JS.
3
+ */
4
+ [data-component="shimmer"] [data-shimmer-part="line"] {
5
+ position: relative;
6
+ overflow: hidden;
7
+ }
8
+
9
+ [data-component="shimmer"] [data-shimmer-part="line"]::after {
10
+ content: "";
11
+ position: absolute;
12
+ inset: 0;
13
+ transform: translateX(-100%);
14
+ background-image: linear-gradient(
15
+ 90deg,
16
+ rgb(255 255 255 / 0) 0,
17
+ rgb(255 255 255 / 0.35) 50%,
18
+ rgb(255 255 255 / 0) 100%
19
+ );
20
+ animation: ms-shimmer 1.4s infinite;
21
+ }
22
+
23
+ @keyframes ms-shimmer {
24
+ 100% { transform: translateX(100%); }
25
+ }
26
+
27
+ @media (prefers-reduced-motion: reduce) {
28
+ [data-component="shimmer"] [data-shimmer-part="line"]::after {
29
+ animation: none;
30
+ }
31
+ }
@@ -0,0 +1,22 @@
1
+ /* Vendored fallback for the `snippet` component. Loaded only while the
2
+ * vendored partial is the one rendering.
3
+ */
4
+ [data-component="snippet"] {
5
+ --snippet-radius: var(--surface-radius, 0.375rem);
6
+
7
+ border-radius: var(--snippet-radius);
8
+ }
9
+
10
+ [data-component="snippet"] [data-snippet-part="command"] {
11
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
12
+ white-space: pre;
13
+ overflow-x: auto;
14
+ }
15
+
16
+ [data-component="snippet"] [data-snippet-part="prompt"] {
17
+ user-select: none;
18
+ }
19
+
20
+ [data-component="snippet"] script[data-ms-code-source] {
21
+ display: none;
22
+ }
@@ -0,0 +1,14 @@
1
+ /* Engine-owned and permanent: the reference implementation of
2
+ * `register_tag :source`.
3
+ */
4
+ [data-component="source-citation"] {
5
+ --source-citation-radius: var(--surface-radius, 0.25rem);
6
+
7
+ border-radius: var(--source-citation-radius);
8
+ line-height: 1.2;
9
+ vertical-align: baseline;
10
+ }
11
+
12
+ [data-component="source-citation"] [data-source-citation-part="index"] {
13
+ font-variant-numeric: tabular-nums;
14
+ }
@@ -0,0 +1,32 @@
1
+ /* Vendored fallback for the `suggestion` component. Loaded only while the
2
+ * vendored partial is the one rendering.
3
+ *
4
+ * No JavaScript and no client state: a chip is a link or a form submission, so
5
+ * everything here is shape and focus.
6
+ */
7
+ [data-component="suggestion"] {
8
+ display: flex;
9
+ flex-wrap: wrap;
10
+ gap: var(--suggestion-gap, 0.5rem);
11
+ }
12
+
13
+ [data-component="suggestion"] [data-suggestion-part="chip"] {
14
+ --suggestion-radius: 9999px;
15
+
16
+ border-radius: var(--suggestion-radius);
17
+ cursor: pointer;
18
+ white-space: nowrap;
19
+ max-width: 100%;
20
+ overflow: hidden;
21
+ text-overflow: ellipsis;
22
+ }
23
+
24
+ /* button_to wraps its button in a form; the chip must still sit in the row. */
25
+ [data-component="suggestion"] form {
26
+ display: contents;
27
+ }
28
+
29
+ [data-component="suggestion"] [data-suggestion-part="chip"]:focus-visible {
30
+ outline: var(--focus-ring-width, 2px) solid var(--focus-ring-color, currentColor);
31
+ outline-offset: 2px;
32
+ }
@@ -0,0 +1,48 @@
1
+ /* maquina_stream — streaming reveal.
2
+ *
3
+ * Load once in the host layout, next to the theme:
4
+ *
5
+ * <%= stylesheet_link_tag "maquina_stream/reveal" %>
6
+ *
7
+ * `ms-reveal` wraps the text that just arrived in one
8
+ * `<span data-ms-revealing>` and unwraps it again on `animationend`. This file
9
+ * is therefore one keyframe and one rule: the controller decides *what*
10
+ * animates, the stylesheet decides only how it looks.
11
+ *
12
+ * There is no mask here on purpose. A gradient mask placed from a character
13
+ * fraction hides a horizontal band, and on a block wrapped over several lines
14
+ * that band crosses lines the reader has already read — they flash out and back
15
+ * in on every frame. Animating the new text itself needs no mapping from
16
+ * characters to pixels at all. See app/javascript/maquina_stream/controllers/
17
+ * ms_reveal_controller.js.
18
+ */
19
+
20
+ @keyframes ms-reveal-in {
21
+ from {
22
+ opacity: 0;
23
+ filter: blur(4px);
24
+ }
25
+
26
+ to {
27
+ opacity: 1;
28
+ filter: blur(0);
29
+ }
30
+ }
31
+
32
+ [data-ms-revealing] {
33
+ animation: ms-reveal-in var(--ms-reveal-duration, 320ms) ease-out both;
34
+ }
35
+
36
+ /* No animation, and the text is fully present the moment it arrives. The
37
+ * controller answers the same query and stops wrapping entirely — with
38
+ * `animation: none` no `animationend` fires, so a span wrapped anyway would
39
+ * never be unwrapped. This rule is the second half of that: it also covers a
40
+ * span left mid-flight by the preference changing while a message streams.
41
+ */
42
+ @media (prefers-reduced-motion: reduce) {
43
+ [data-ms-revealing] {
44
+ animation: none !important;
45
+ opacity: 1 !important;
46
+ filter: none !important;
47
+ }
48
+ }