openui-rails 0.5.0 → 0.9.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a8e938067e723b80e9efc974e2a2bd19878499bdc459c6d675452fa919584fb
4
- data.tar.gz: 1a0d9ffaa09afef3c185f21154c56bf78ef8141f15bf4bef8fe1562aa47eb68b
3
+ metadata.gz: 1d89e42577f933fcb68ce1740bbc51496998c91f4f2f529880566e568f66543d
4
+ data.tar.gz: 8ee1ecfbef87fb51c07d5a85dc461555901f8dfc20c45da27093eabab82b5416
5
5
  SHA512:
6
- metadata.gz: 0161351030bb99e07eb7fa9c7fa968c4921ea436e76da253c6b1827ac3a55650e0dfc6903d737cd1ae94a71b9ae978ae20642e9ea9aec5dd5aa0bd550ff5eb2c
7
- data.tar.gz: 6b583705522c3871e981289572701a5c0c62bb6c9a8cb7679c26a6af9cac185937d4401b960c4a653442f08ef08b8b552468e42deeaf7ba45bcca5d78e491005
6
+ metadata.gz: 36050c0b40b6bf4afac05329c4cf5e393156519922592a1486b0c95d76d9b995f2174317db863349795f6b1d7274267cc9040dfb74a39282683b4d7b1e8c336c
7
+ data.tar.gz: c4c5833187751750c71e999d6e6098139fb4e33bd12a56d146dcacd18df5ac06bc360f0b2b424ec03569123f2caa91a567440ca3c4caef28e2485362fac9c0f6
data/CHANGELOG.md CHANGED
@@ -6,6 +6,59 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.1] - 2026-10-28
10
+
11
+ ### Added
12
+
13
+ - `bin/demo` prepares a disposable Support Desk host, binds its foreground
14
+ server to loopback, and removes the host when the server stops.
15
+ - Independent public-API host checks, an optional old-gem upgrade and rollback
16
+ rehearsal, and an explicitly authorized single-request live qualification.
17
+ Live failure and retry limits are tested offline and never run in CI.
18
+ - Browser assertions for the reference's enforced script CSP and complete
19
+ keyboard navigation through its generated form and mutation.
20
+
21
+ ### Changed
22
+
23
+ - The opt-in Support Desk example uses nonce-authorized scripts and a
24
+ self-hosted content policy. Inline styles remain necessary for the pinned
25
+ renderer. No gem API, runtime pin, database migration, or dependency changes.
26
+
27
+ ## [0.9.0] - 2026-10-21
28
+
29
+ ### Added
30
+
31
+ - A Supported API reference page that names the Ruby methods, settings, tasks,
32
+ events, wire contracts, and runtime pin policy covered by 1.x compatibility.
33
+
34
+ ### Deprecated
35
+
36
+ - `config.library_name` and `config.surface_id_param` have no effect and report
37
+ a deprecation through the new `OpenUI::Rails.deprecator`, registered in
38
+ `Rails.application.deprecators`. They will be removed in 1.0.
39
+
40
+ ### Changed
41
+
42
+ - The action endpoint returns HTTP 500 with code `error` and the fixed message
43
+ `action failed` for server-side gem errors, such as an unserializable result.
44
+ It previously returned 422 with the internal message. Details go to the log.
45
+ - `OpenUI::Rails::RuntimeAsset` is marked internal. Configure runtime assets
46
+ through `Configuration`.
47
+
48
+ ### Fixed
49
+
50
+ - A response that exceeds `stream_byte_limit` no longer drains the provider.
51
+ `Bridge#envelopes`, `Broadcaster.broadcast`, and the RubyLLM adapter stop
52
+ reading after the `stream_too_large` frame. `Writer#terminated?` lets
53
+ callback-based providers do the same.
54
+ - A surface that reconnects while paused for a snapshot, including a Turbo
55
+ cache restoration, emits `openui:snapshot-required` again. It previously
56
+ dropped every later delta without telling the host.
57
+ - An exception raised by the `on_error` hook no longer suppresses the stream's
58
+ error frame or replaces the provider error, which left the surface busy.
59
+ - A surface without a stored `library_version` now checks stream frames against
60
+ the configured library fingerprint instead of accepting any library.
61
+
9
62
  ## [0.5.0] - 2026-10-21
10
63
 
11
64
  ### Added
@@ -153,7 +206,9 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
153
206
  - Gem-scoped RubyGems Trusted Publishing through GitHub OIDC, with a protected
154
207
  release environment and verified artifact handoff.
155
208
 
156
- [Unreleased]: https://github.com/yorzi/openui-rails/compare/v0.5.0...main
209
+ [Unreleased]: https://github.com/yorzi/openui-rails/compare/v0.9.1...main
210
+ [0.9.1]: https://github.com/yorzi/openui-rails/releases/tag/v0.9.1
211
+ [0.9.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.9.0
157
212
  [0.5.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.5.0
158
213
  [0.4.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.4.0
159
214
  [0.3.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.3.0
data/CONTRIBUTING.md CHANGED
@@ -25,7 +25,7 @@ bundle exec bin/rails test # unit + integration tests
25
25
  bundle exec bin/rails test:system # browser (system) tests, needs Chrome
26
26
  bundle exec rubocop # style (rubocop-rails-omakase)
27
27
  bundle exec gem build openui-rails.gemspec
28
- bundle exec ruby bin/smoke-package openui-rails-0.5.0.gem
28
+ bundle exec ruby bin/smoke-package openui-rails-0.9.0.gem
29
29
  bundle exec bundler-audit check --update
30
30
  bundle exec ruby -Itest test/compatibility/ruby_llm_smoke.rb
31
31
  ```
data/DEVELOPMENT.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Development and verification
2
2
 
3
+ Select a Ruby version covered by the CI matrix before running `bundle install`.
4
+ The qualification commands reuse that Ruby and its resolved Rails dependencies.
5
+
3
6
  Use `test/dummy` as the maintained local host. Temporary generated hosts verify
4
7
  the built gem. Keep examples synthetic and provider-free unless a task explicitly
5
8
  authorizes a live call. Follow this loop for each feature:
@@ -14,10 +17,23 @@ authorizes a live call. Follow this loop for each feature:
14
17
 
15
18
  ## Reference workflow
16
19
 
20
+ For an interactive, provider-free demo:
21
+
22
+ ```sh
23
+ bundle exec rake "app:openui:runtime:fetch[$PWD/tmp/real-runtime]"
24
+ bin/demo --runtime tmp/real-runtime --port 3000
25
+ ```
26
+
27
+ Open `http://127.0.0.1:3000/support`. The server runs in the foreground and binds
28
+ loopback only. Ctrl-C stops it and removes the newly generated host and database.
29
+ No browser suite or provider runs during startup. Use `bin/check-reference`
30
+ for automated browser verification. Add `--prepare-only` with `--gem --host`
31
+ when you want a retained review host without running the browser suite.
32
+
17
33
  ```sh
18
34
  bin/check-reference
19
35
  bin/check-reference --runtime tmp/real-runtime
20
- bin/check-reference --gem tmp/package/openui-rails-0.5.0.gem --runtime tmp/real-runtime
36
+ bin/check-reference --gem tmp/package/openui-rails-0.9.0.gem --runtime tmp/real-runtime
21
37
  # Focus one browser scenario:
22
38
  bin/check-reference --runtime tmp/real-runtime --name test_a_lost_turbo_envelope_recovers_from_the_scoped_snapshot
23
39
  ```
@@ -60,6 +76,45 @@ a browser network sandbox. The stub suite exercises bridge failure cases; the
60
76
  real-runtime suite verifies the pinned renderer, library identity, actions,
61
77
  tools, and keyboard behavior. Neither proves live-provider compatibility.
62
78
 
79
+ ## Independent host and release qualification
80
+
81
+ The independent host uses public gem APIs and its own SQLite record, view,
82
+ authorization, and action. It loads no Support Desk or dummy application code.
83
+ The default path runs in `bin/check --runtime` and CI. To rehearse an upgrade,
84
+ provide a checksum-verified historical gem explicitly:
85
+
86
+ ```sh
87
+ bin/check-host --gem tmp/package/openui-rails-0.9.1.gem --runtime tmp/real-runtime
88
+ bin/check-host --gem tmp/package/openui-rails-0.9.1.gem --runtime tmp/real-runtime \
89
+ --upgrade-from tmp/qualification/openui-rails-0.1.0.gem
90
+ ```
91
+
92
+ The optional rehearsal boots the old artifact, the candidate, and the old
93
+ artifact again against the same database. It checks saved source, a draft,
94
+ authorized effects, reload, library mismatch fallback, and rollback. Its
95
+ synthetic fixed actor is a test boundary, not production authentication.
96
+
97
+ Live qualification always requires explicit owner authorization and uses
98
+ `OPENROUTER_API_KEY` from the existing environment. Never put a credential in
99
+ a command argument. After authorization:
100
+
101
+ The `drafts/` paths below are local, optional materials. The gem, `bin/check`,
102
+ and CI do not depend on them and never commit them.
103
+
104
+ ```sh
105
+ bin/check-live --authorized --output drafts/live-qualification
106
+ bin/check-host --gem tmp/package/openui-rails-0.9.1.gem --runtime tmp/real-runtime \
107
+ --source drafts/live-qualification/source.openui
108
+ ```
109
+
110
+ The live command uses one `openai/gpt-4.1-mini` request, a 1024-output-token
111
+ limit, and a conservative US$0.25 token-cost preflight. SDK retries and provider
112
+ fallbacks are disabled. An existing output directory is refused, including a
113
+ failed attempt; do not automatically repeat it. Usage-based cost estimates do
114
+ not prove the billed charge. Generated output becomes browser evidence only
115
+ after `bin/check-host --source` passes. CI tests this command with mocked HTTP;
116
+ it never reads a live key or calls a paid provider.
117
+
63
118
  ## Refresh the default library exports
64
119
 
65
120
  When changing the pinned runtime, regenerate from its checksum-verified bundle:
data/README.md CHANGED
@@ -32,7 +32,7 @@ The runtime-fetch task also needs npm. RubyLLM is optional.
32
32
 
33
33
  ```ruby
34
34
  # Gemfile
35
- gem "openui-rails", "~> 0.5.0"
35
+ gem "openui-rails", "~> 0.9.1"
36
36
  ```
37
37
 
38
38
  ```sh
@@ -65,15 +65,19 @@ The documentation site contains the full guides and API reference:
65
65
  - [RubyLLM](https://openui-rails.wangyaodi.com/ruby-llm/) and [production setup](https://openui-rails.wangyaodi.com/production/)
66
66
  - [Upgrading](https://openui-rails.wangyaodi.com/upgrading/) and [API reference](https://openui-rails.wangyaodi.com/api-reference/)
67
67
 
68
- Version `0.5.0` adds outcome diagnostics and host-owned recovery examples.
69
- See [What's New](https://openui-rails.wangyaodi.com/whats-new-in-0-5/) for the upgrade.
68
+ Version `0.9.1` improves the demo, CSP, keyboard checks, and host qualification
69
+ without changing the public gem API or pinned runtime.
70
+ See [What's New](https://openui-rails.wangyaodi.com/whats-new-in-0-9/) for the upgrade and
71
+ [Supported API](https://openui-rails.wangyaodi.com/supported-api/) for the compatibility boundary.
70
72
  The API may change during `0.x`; see the
71
73
  [changelog](CHANGELOG.md) and the proposed [path to 1.0](ROADMAP.md).
72
74
  The site's stable channel follows the latest release; [/next/](https://openui-rails.wangyaodi.com/next/)
73
75
  follows `main`.
74
76
 
75
77
  For repository work, use [Contributing](CONTRIBUTING.md),
76
- [Releasing](RELEASING.md), and the [0.4.0 release record](RELEASE_0_4.md).
78
+ [Releasing](RELEASING.md), and the [0.9.0 release record](RELEASE_0_9.md).
79
+ Run the disposable demo with `bin/demo --runtime tmp/real-runtime` after the
80
+ runtime setup in [Development](DEVELOPMENT.md).
77
81
  Report vulnerabilities through the [security policy](SECURITY.md).
78
82
 
79
83
  ## License
data/RELEASING.md CHANGED
@@ -32,6 +32,12 @@ The live smoke completed on 2026-10-18 through OpenRouter with RubyLLM 2.1.1
32
32
  and `openai/gpt-4.1-mini`. It used a synthetic prompt, `:complete` streaming,
33
33
  and a 16-token output cap. See [AUDIT.md](AUDIT.md) for the result and limits.
34
34
 
35
+ The fuller, explicitly authorized qualification on 2026-10-28 generated complete
36
+ UI through RubyLLM, rendered its persisted output, and executed an authorized
37
+ action in an independent host. See [QUALIFICATION_1_0.md](QUALIFICATION_1_0.md)
38
+ for its budget, upgrade rehearsal, and limits. Release CI remains provider-free;
39
+ publishing does not require repeating a paid call.
40
+
35
41
  ## Prepare the candidate
36
42
 
37
43
  During development, keep `released: false`, document delivered work under
@@ -10,17 +10,17 @@ The Gem itself is licensed under the MIT License (see `LICENSE`).
10
10
 
11
11
  ## Referenced upstream packages
12
12
 
13
- The following `@openuidev/*` packages are published by Thesys under the MIT
14
- License and are loaded by the browser at runtime (self-hosted by the host app,
15
- or referenced at an exact pinned version):
13
+ The integration references these official MIT-licensed packages. The browser
14
+ bundle and the maintainer CLI have exact pins. The other rows describe the
15
+ browser bundle's declared dependency ranges, not proven compiled versions.
16
+ The CLI is a development tool and is not loaded in the browser.
16
17
 
17
- | Package | Pinned version | License |
18
+ | Package | Version reference | License |
18
19
  | --- | --- | --- |
19
- | `@openuidev/browser-bundle` | 0.2.1 | MIT |
20
- | `@openuidev/react-lang` | 1.0.1 | MIT |
21
- | `@openuidev/lang-core` | 1.0.1 | MIT |
22
- | `@openuidev/react-ui` | 0.18.1 | MIT |
23
- | `@openuidev/cli` | 0.6.0 | MIT |
20
+ | `@openuidev/browser-bundle` | 0.2.1 exact runtime pin | MIT |
21
+ | `@openuidev/react-lang` | ^1.0.1 declared range | MIT |
22
+ | `@openuidev/react-ui` | ^0.18.1 declared range | MIT |
23
+ | `@openuidev/cli` | 0.6.0 exact guide pin | MIT |
24
24
 
25
25
  The OpenUI repository is licensed MIT: "Copyright (c) 2011-2024 Thesys Inc."
26
26
 
@@ -36,6 +36,25 @@ prompt instructions, not the browser renderer implementation.
36
36
 
37
37
  ## Transitive browser dependencies
38
38
 
39
+ The 2026-10-28 review checks the exact checksum-locked 0.2.1 JavaScript, CSS,
40
+ and upstream license used by the demo. Its JavaScript's final bundled-notices
41
+ block names these package groups:
42
+
43
+ | Embedded package group | Notice |
44
+ | --- | --- |
45
+ | `react`, `react-dom`, `scheduler`, `use-sync-external-store` | Meta copyright and MIT references. |
46
+ | `lucide-react` | Version 0.562.0 and ISC references across its bundled icon modules. |
47
+ | `@tanstack/table-core`, `@tanstack/react-table` | TanStack copyright and MIT references. |
48
+
49
+ These are the groups named by the embedded notices, not a reconstructed lockfile
50
+ or complete dependency inventory. The npm browser-bundle manifest declares
51
+ ranges for React, React DOM, react-ui, and react-lang. Those ranges do not
52
+ identify every resolved transitive version. Preserve the fetched files intact;
53
+ do not strip their legal comments. A host that redistributes the bundle must
54
+ still establish its complete dependency and license inventory.
55
+
56
+ The reviewed manifest is the [immutable npm version record](https://registry.npmjs.org/@openuidev/browser-bundle/0.2.1).
57
+
39
58
  The official browser bundle inlines dependencies, including React 19. They do
40
59
  not all share one license. The pinned bundle's embedded notices identify
41
60
  `lucide-react` 0.562.0 as ISC-licensed. React and TanStack identify MIT licenses.
@@ -48,9 +48,17 @@ module OpenUI
48
48
  status: :unprocessable_content
49
49
  end
50
50
 
51
+ # Request errors return their own message. Any other gem error is a
52
+ # server-side fault, possibly after the handler ran: log it and return a
53
+ # generic message so configuration details never reach the browser.
51
54
  def render_domain_error(error)
52
- render json: { error: { code: error_code(error), message: error.message } },
53
- status: error_status(error)
55
+ code = error_code(error)
56
+ if code == "error"
57
+ logger&.error("[openui-rails] action failed: #{error.class}: #{error.message}")
58
+ render json: { error: { code: code, message: "action failed" } }, status: :internal_server_error
59
+ else
60
+ render json: { error: { code: code, message: error.message } }, status: error_status(error)
61
+ end
54
62
  end
55
63
 
56
64
  def error_code(error)
@@ -62,7 +70,8 @@ module OpenUI
62
70
  when UnknownActionError then :not_found
63
71
  when UnauthorizedActionError then :forbidden
64
72
  when StaleSurfaceError, ReplayError then :conflict
65
- else :unprocessable_content
73
+ when InvalidActionError then :unprocessable_content
74
+ else :internal_server_error
66
75
  end
67
76
  end
68
77
  end
@@ -73,6 +73,12 @@ export default class extends Controller {
73
73
 
74
74
  this.syncBusy()
75
75
  this.renderRuntime(connection)
76
+ // A surface restored from the Turbo cache or reattached while paused for a
77
+ // snapshot would otherwise wait forever: the host abandoned the earlier
78
+ // request on disconnect. Announce it again once sibling controllers connect.
79
+ if (this.state.needsSnapshot) {
80
+ queueMicrotask(() => { if (this.isCurrent(connection)) this.announceSnapshotRequired() })
81
+ }
76
82
  }
77
83
 
78
84
  disconnect() {
@@ -159,7 +165,10 @@ export default class extends Controller {
159
165
  (envelope.type === "openui.snapshot" && typeof envelope.response !== "string")) {
160
166
  return this.handleError(Object.assign(new Error("Malformed stream envelope"), { code: "invalid_envelope" }))
161
167
  }
162
- if (envelope.library_version && this.libraryVersion && envelope.library_version !== this.libraryVersion) {
168
+ // Without a stored library version, the verified runtime fingerprint is
169
+ // still the only library this mount can render.
170
+ const library = this.libraryVersion || this.element.dataset.openuiLibraryFingerprint
171
+ if (envelope.library_version && library && envelope.library_version !== library) {
163
172
  return this.handleError(Object.assign(new Error("OpenUI library version mismatch"), { code: "library_version_mismatch" }))
164
173
  }
165
174
  if (envelope.type !== "openui.snapshot") {
@@ -215,6 +224,10 @@ export default class extends Controller {
215
224
  requestSnapshot() {
216
225
  if (this.state.needsSnapshot) return
217
226
  this.state.needsSnapshot = true
227
+ this.announceSnapshotRequired()
228
+ }
229
+
230
+ announceSnapshotRequired() {
218
231
  this.dispatch("snapshot-required", { surfaceId: this.surfaceId, sequence: this.state.nextSequence })
219
232
  }
220
233
 
@@ -15,10 +15,15 @@ From the repository:
15
15
 
16
16
  ```sh
17
17
  mkdir -p tmp/package
18
- bundle exec gem build openui-rails.gemspec --output tmp/package/openui-rails-0.5.0.gem
19
- bin/check-reference --gem tmp/package/openui-rails-0.5.0.gem --runtime tmp/real-runtime
18
+ bundle exec gem build openui-rails.gemspec --output tmp/package/openui-rails-0.9.1.gem
19
+ bin/check-reference --gem tmp/package/openui-rails-0.9.1.gem --runtime tmp/real-runtime
20
20
  ```
21
21
 
22
+ For an interactive disposable demo, use `bin/demo --runtime tmp/real-runtime`.
23
+ It starts a foreground server on `127.0.0.1:3000` and removes the new host and
24
+ database when you stop it with Ctrl-C. No provider call is made. Its script CSP
25
+ uses importmap nonces; inline styles remain enabled for the pinned renderer.
26
+
22
27
  Fetch the runtime first. See the [complete guide](https://openui-rails.wangyaodi.com/reference-workflow/)
23
28
  for setup, retained-host review, failure checks, and evidence limits. The
24
29
  reference is not enabled by requiring the gem. Keep the demo loopback-only;
@@ -3,6 +3,17 @@
3
3
  module SupportDesk # :nodoc:
4
4
  class BaseController < ::ApplicationController
5
5
  protect_from_forgery with: :exception
6
+ content_security_policy do |policy|
7
+ policy.default_src :self
8
+ policy.script_src :self
9
+ # The pinned renderer inserts styles and uses style attributes.
10
+ policy.style_src :self, :unsafe_inline
11
+ policy.img_src :self, :data
12
+ policy.object_src :none
13
+ policy.base_uri :self
14
+ policy.form_action :self
15
+ policy.frame_ancestors :none
16
+ end
6
17
  layout "support_desk/application"
7
18
  before_action :local_demo_only
8
19
  before_action :authenticate
@@ -4,6 +4,7 @@
4
4
  <title>Support Desk · OpenUI Rails reference</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
7
8
  <%= openui_stylesheet_tag %>
8
9
  <%= javascript_importmap_tags "support_desk" %>
9
10
  <style>
@@ -1,6 +1,13 @@
1
1
  <h1>Support Desk reference host</h1>
2
2
  <p class="notice">Synthetic accounts and tickets. Local demo only. No model provider is called.</p>
3
3
  <p>Sign in to stream a ticket, save a private draft, continue the conversation, and close the ticket.</p>
4
+ <h2>Try the workflow</h2>
5
+ <ol>
6
+ <li>Choose Alice, enter a private note, and wait for “Draft saved”. Reload to check the saved note.</li>
7
+ <li>Continue the conversation, then close the ticket. Reload to review its persisted status.</li>
8
+ <li>Try Reader for read-only access, or Bob for a separate tenant.</li>
9
+ </ol>
10
+ <p>Alice and Reader belong to the north tenant. Bob belongs to the south tenant. These are local synthetic accounts.</p>
4
11
  <% %w[Alice Bob Reader].each do |name| %>
5
12
  <%= button_to "Sign in as #{name}", session_path, params: { name: name } %>
6
13
  <% end %>
@@ -11,6 +11,8 @@ module SupportDesk # :nodoc:
11
11
  # form tags and the shared action endpoint protected in that environment too.
12
12
  initializer "support_desk.csrf", before: "action_controller.set_configs" do |app|
13
13
  app.config.action_controller.allow_forgery_protection = true
14
+ app.config.content_security_policy_nonce_generator ||= ->(_request) { SecureRandom.base64(16) }
15
+ app.config.content_security_policy_nonce_directives ||= %w[script-src]
14
16
  end
15
17
 
16
18
  initializer "support_desk.assets", before: "importmap" do |app|
@@ -17,6 +17,10 @@ module OpenUI
17
17
  module RubyLLM
18
18
  STRATEGIES = %i[complete ask].freeze
19
19
 
20
+ # Internal signal that aborts the provider stream after a terminal frame.
21
+ Halt = Class.new(StandardError)
22
+ private_constant :Halt
23
+
20
24
  class << self
21
25
  # Stream a RubyLLM completion, yielding ordered {Stream::Envelope}s.
22
26
  #
@@ -47,6 +51,7 @@ module OpenUI
47
51
 
48
52
  begin
49
53
  consumer_failed = false
54
+ halted = false
50
55
  deliver = lambda do |envelope|
51
56
  begin
52
57
  block.call(envelope)
@@ -60,10 +65,17 @@ module OpenUI
60
65
  next if text.nil?
61
66
 
62
67
  writer.push(text).each(&deliver)
68
+ # A terminal frame (the byte limit) has already been delivered.
69
+ # Raise out of the provider callback so it stops generating.
70
+ if writer.terminated?
71
+ halted = true
72
+ raise Halt
73
+ end
63
74
  end
64
75
  writer.finish.each(&deliver)
65
76
  rescue StandardError => e
66
77
  raise if consumer_failed
78
+ return writer if halted
67
79
 
68
80
  envelope = writer.error_for(e, code: error_code_for(e))
69
81
  block.call(envelope) if envelope
@@ -29,8 +29,8 @@ module OpenUI
29
29
  DEFAULT_ACTIONS_ENDPOINT = "/openui-rails/actions"
30
30
 
31
31
  attr_reader :transport, :runtime_assets
32
- attr_accessor :library_spec_path, :prompt_path, :library_name,
33
- :actions_endpoint, :surface_id_param,
32
+ attr_accessor :library_spec_path, :prompt_path,
33
+ :actions_endpoint,
34
34
  :stream_byte_limit, :pending_byte_limit, :idempotency_ttl,
35
35
  :authorize_action, :on_error, :action_registry, :surface_resolver, :actor_resolver,
36
36
  :runtime_version, :runtime_url, :runtime_integrity, :local_runtime_path,
@@ -62,6 +62,32 @@ module OpenUI
62
62
  @local_runtime_path = "/openui"
63
63
  end
64
64
 
65
+ # @deprecated Has no effect: the runtime library comes from the loaded
66
+ # bundle and its manifest. Removed in 1.0.
67
+ def library_name
68
+ deprecated_setting(:library_name)
69
+ @library_name
70
+ end
71
+
72
+ # @deprecated Has no effect. Removed in 1.0.
73
+ def library_name=(value)
74
+ deprecated_setting(:library_name)
75
+ @library_name = value
76
+ end
77
+
78
+ # @deprecated Has no effect: action requests always use the `surface_id`
79
+ # key. Removed in 1.0.
80
+ def surface_id_param
81
+ deprecated_setting(:surface_id_param)
82
+ @surface_id_param
83
+ end
84
+
85
+ # @deprecated Has no effect. Removed in 1.0.
86
+ def surface_id_param=(value)
87
+ deprecated_setting(:surface_id_param)
88
+ @surface_id_param = value
89
+ end
90
+
65
91
  # Set the preferred host stream transport.
66
92
  #
67
93
  # @param value [Symbol] `:turbo`, `:action_cable`, or `:none`
@@ -243,6 +269,12 @@ module OpenUI
243
269
  PromptBuilder.load(self) if prompt_path
244
270
  true
245
271
  end
272
+
273
+ private
274
+
275
+ def deprecated_setting(name)
276
+ OpenUI::Rails.deprecator.warn("config.#{name} has no effect and will be removed; delete it from your initializer")
277
+ end
246
278
  end
247
279
 
248
280
  class << self
@@ -27,6 +27,10 @@ module OpenUI
27
27
  ::Rails.autoloaders.main.inflector.inflect("openui" => "OpenUI")
28
28
  end
29
29
 
30
+ initializer "openui_rails.deprecator" do |app|
31
+ app.deprecators[:openui_rails] = OpenUI::Rails.deprecator
32
+ end
33
+
30
34
  # Expose the surface helper to every host view.
31
35
  initializer "openui_rails.view_helper" do
32
36
  ActiveSupport.on_load(:action_view) do
@@ -2,14 +2,15 @@
2
2
 
3
3
  module OpenUI
4
4
  module Rails
5
- # Resolves the pinned OpenUI browser runtime and the integrity metadata the
6
- # mount element advertises.
5
+ # Internal: resolves the pinned OpenUI browser runtime and the integrity
6
+ # metadata the mount element advertises. Configure assets through
7
+ # {Configuration}; this module is not part of the supported API.
7
8
  #
8
9
  # The runtime is version-pinned. Self-hosted (the default) points at a file
9
10
  # the install generator vendored into the host app; the external strategy
10
11
  # requires an explicit, exact-version URL so no floating CDN range is used
11
12
  # in production.
12
- module RuntimeAsset
13
+ module RuntimeAsset # :nodoc:
13
14
  PINNED_VERSION = "0.2.1"
14
15
  FILE_PREFIX = "openui-browser-bundle"
15
16
 
@@ -20,8 +20,12 @@ module OpenUI
20
20
  @needs_snapshot = false
21
21
  end
22
22
 
23
- # @return [Symbol] one of :applied, :duplicate, :snapshot, :completed,
24
- # :error, :ignored
23
+ # Apply one envelope for this message.
24
+ #
25
+ # @param envelope [Envelope]
26
+ #
27
+ # @return [Symbol] one of :applied, :duplicate, :gap, :snapshot,
28
+ # :completed, :error, :ignored. After :gap, only a snapshot applies.
25
29
  def apply(envelope)
26
30
  return :ignored unless envelope.message_id == message_id
27
31
  unless envelope.snapshot?
@@ -39,7 +39,8 @@ module OpenUI
39
39
 
40
40
  # Yield ordered envelopes for every chunk from a pull-based source.
41
41
  # Provider errors become a structured error envelope. Exceptions raised
42
- # by the consumer block propagate to the caller.
42
+ # by the consumer block propagate to the caller. Pulling stops once the
43
+ # writer terminates, so an oversized response does not drain the source.
43
44
  #
44
45
  # @param source [#each] yields String chunks
45
46
  # @yieldparam envelope [Envelope]
@@ -60,6 +61,7 @@ module OpenUI
60
61
  # Transport/consumer failures are not provider failures. Let the
61
62
  # host retry or report them without recursively broadcasting errors.
62
63
  envelopes.each { |envelope| yield envelope }
64
+ return if writer.terminated?
63
65
  end
64
66
  writer.finish.each { |envelope| yield envelope }
65
67
  end
@@ -112,6 +114,12 @@ module OpenUI
112
114
  @started = Instrumentation.now
113
115
  end
114
116
 
117
+ # Whether a completion or error envelope has ended this stream. Stop
118
+ # reading the provider once it is true; later pushes are ignored.
119
+ #
120
+ # @return [Boolean]
121
+ def terminated? = @terminated
122
+
115
123
  # Append a chunk and return a delta once the pending threshold is reached.
116
124
  # A chunk that exceeds the total stream limit returns an error envelope.
117
125
  #
@@ -153,6 +161,7 @@ module OpenUI
153
161
  end
154
162
 
155
163
  # Structured, PII-free error envelope for a provider/consumer failure.
164
+ # Calls `on_error` first; an exception raised by that hook is ignored.
156
165
  #
157
166
  # @param error [Exception]
158
167
  #
@@ -162,7 +171,7 @@ module OpenUI
162
171
  def error_for(error, code: "stream_failed")
163
172
  return nil if @terminated
164
173
 
165
- @on_error&.call(error, message_id: @message_id)
174
+ notify_error(error)
166
175
 
167
176
  message = "stream failed (#{error.class})"
168
177
  message = "stream cancelled" if code == "stream_cancelled"
@@ -193,6 +202,14 @@ module OpenUI
193
202
  envelope
194
203
  end
195
204
 
205
+ # A failing host hook must not suppress the terminal frame or replace
206
+ # the provider error, matching notification subscriber isolation.
207
+ def notify_error(error)
208
+ @on_error&.call(error, message_id: @message_id)
209
+ rescue StandardError
210
+ nil
211
+ end
212
+
196
213
  def notify_termination(outcome, code = nil)
197
214
  Instrumentation.publish("stream", started: @started, outcome: outcome,
198
215
  code: code, sequence: @sequence - 1, bytes: @total_bytes)
@@ -24,6 +24,7 @@ module OpenUI
24
24
  attr_reader :transport, :message_id, :library_version
25
25
 
26
26
  # Broadcast every envelope produced by an enumerable source of chunks.
27
+ # Iteration stops after a terminal envelope, such as a byte-limit error.
27
28
  #
28
29
  # @param source [#each] yields String chunks
29
30
  # @param options [Hash] arguments forwarded to the constructor
@@ -31,7 +32,10 @@ module OpenUI
31
32
  # @return [Broadcaster]
32
33
  def self.broadcast(source, **options)
33
34
  broadcaster = new(**options)
34
- source.each { |chunk| broadcaster.push(chunk) }
35
+ source.each do |chunk|
36
+ broadcaster.push(chunk)
37
+ break if broadcaster.writer.terminated?
38
+ end
35
39
  broadcaster.finish
36
40
  end
37
41
 
@@ -1,5 +1,5 @@
1
1
  module OpenUI
2
2
  module Rails
3
- VERSION = "0.5.0"
3
+ VERSION = "0.9.1"
4
4
  end
5
5
  end
data/lib/openui/rails.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/deprecation"
3
4
  require "openui/rails/version"
4
5
 
5
6
  module OpenUI
@@ -10,6 +11,13 @@ module OpenUI
10
11
  # into a Rails engine and keeps Rails authoritative for auth, tenancy and
11
12
  # persistence.
12
13
  module Rails
14
+ # Deprecator for APIs removed in openui-rails 1.0. The engine registers it
15
+ # in `Rails.application.deprecators`, so host deprecation settings apply.
16
+ #
17
+ # @return [ActiveSupport::Deprecation]
18
+ def self.deprecator
19
+ @deprecator ||= ActiveSupport::Deprecation.new("1.0", "openui-rails")
20
+ end
13
21
  end
14
22
  end
15
23
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openui-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-21 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails