openui-rails 0.5.0 → 0.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a8e938067e723b80e9efc974e2a2bd19878499bdc459c6d675452fa919584fb
4
- data.tar.gz: 1a0d9ffaa09afef3c185f21154c56bf78ef8141f15bf4bef8fe1562aa47eb68b
3
+ metadata.gz: 348e7ebd421344dd62f6f5449c09fa477de3374dc1803c51aa8bc1415ad0fcfc
4
+ data.tar.gz: 6b82e02f137a9bff26b5b8c9dfd3e6a6053593e5a75f66b87825486e2d667c67
5
5
  SHA512:
6
- metadata.gz: 0161351030bb99e07eb7fa9c7fa968c4921ea436e76da253c6b1827ac3a55650e0dfc6903d737cd1ae94a71b9ae978ae20642e9ea9aec5dd5aa0bd550ff5eb2c
7
- data.tar.gz: 6b583705522c3871e981289572701a5c0c62bb6c9a8cb7679c26a6af9cac185937d4401b960c4a653442f08ef08b8b552468e42deeaf7ba45bcca5d78e491005
6
+ metadata.gz: 6f82e650f49c1b59912959c2e7d3d59d7b0c17088b0fb9dbcc9c97b235910645a5b026834cf509aa4b1915e55e9a2fafe20db57850b3a038dda144f2ce8572f2
7
+ data.tar.gz: 59e46f83487ce218dcd0780d89fc1bbe5a86dd6483d239cffd572effa327b17f130375761cea02ab472865ebd4fa5f8cc6c8587f58dad0e0266bc2a467cec25a
data/CHANGELOG.md CHANGED
@@ -6,6 +6,41 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.0] - 2026-10-21
10
+
11
+ ### Added
12
+
13
+ - A Supported API reference page that names the Ruby methods, settings, tasks,
14
+ events, wire contracts, and runtime pin policy covered by 1.x compatibility.
15
+
16
+ ### Deprecated
17
+
18
+ - `config.library_name` and `config.surface_id_param` have no effect and report
19
+ a deprecation through the new `OpenUI::Rails.deprecator`, registered in
20
+ `Rails.application.deprecators`. They will be removed in 1.0.
21
+
22
+ ### Changed
23
+
24
+ - The action endpoint returns HTTP 500 with code `error` and the fixed message
25
+ `action failed` for server-side gem errors, such as an unserializable result.
26
+ It previously returned 422 with the internal message. Details go to the log.
27
+ - `OpenUI::Rails::RuntimeAsset` is marked internal. Configure runtime assets
28
+ through `Configuration`.
29
+
30
+ ### Fixed
31
+
32
+ - A response that exceeds `stream_byte_limit` no longer drains the provider.
33
+ `Bridge#envelopes`, `Broadcaster.broadcast`, and the RubyLLM adapter stop
34
+ reading after the `stream_too_large` frame. `Writer#terminated?` lets
35
+ callback-based providers do the same.
36
+ - A surface that reconnects while paused for a snapshot, including a Turbo
37
+ cache restoration, emits `openui:snapshot-required` again. It previously
38
+ dropped every later delta without telling the host.
39
+ - An exception raised by the `on_error` hook no longer suppresses the stream's
40
+ error frame or replaces the provider error, which left the surface busy.
41
+ - A surface without a stored `library_version` now checks stream frames against
42
+ the configured library fingerprint instead of accepting any library.
43
+
9
44
  ## [0.5.0] - 2026-10-21
10
45
 
11
46
  ### Added
@@ -153,7 +188,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
153
188
  - Gem-scoped RubyGems Trusted Publishing through GitHub OIDC, with a protected
154
189
  release environment and verified artifact handoff.
155
190
 
156
- [Unreleased]: https://github.com/yorzi/openui-rails/compare/v0.5.0...main
191
+ [Unreleased]: https://github.com/yorzi/openui-rails/compare/v0.9.0...main
192
+ [0.9.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.9.0
157
193
  [0.5.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.5.0
158
194
  [0.4.0]: https://github.com/yorzi/openui-rails/releases/tag/v0.4.0
159
195
  [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
@@ -17,7 +17,7 @@ authorizes a live call. Follow this loop for each feature:
17
17
  ```sh
18
18
  bin/check-reference
19
19
  bin/check-reference --runtime tmp/real-runtime
20
- bin/check-reference --gem tmp/package/openui-rails-0.5.0.gem --runtime tmp/real-runtime
20
+ bin/check-reference --gem tmp/package/openui-rails-0.9.0.gem --runtime tmp/real-runtime
21
21
  # Focus one browser scenario:
22
22
  bin/check-reference --runtime tmp/real-runtime --name test_a_lost_turbo_envelope_recovers_from_the_scoped_snapshot
23
23
  ```
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.0"
36
36
  ```
37
37
 
38
38
  ```sh
@@ -65,15 +65,16 @@ 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.0` freezes the candidate API for 1.0 and fixes stream and recovery defects.
69
+ See [What's New](https://openui-rails.wangyaodi.com/whats-new-in-0-9/) for the upgrade and
70
+ [Supported API](https://openui-rails.wangyaodi.com/supported-api/) for the compatibility boundary.
70
71
  The API may change during `0.x`; see the
71
72
  [changelog](CHANGELOG.md) and the proposed [path to 1.0](ROADMAP.md).
72
73
  The site's stable channel follows the latest release; [/next/](https://openui-rails.wangyaodi.com/next/)
73
74
  follows `main`.
74
75
 
75
76
  For repository work, use [Contributing](CONTRIBUTING.md),
76
- [Releasing](RELEASING.md), and the [0.4.0 release record](RELEASE_0_4.md).
77
+ [Releasing](RELEASING.md), and the [0.5.0 release record](RELEASE_0_5.md).
77
78
  Report vulnerabilities through the [security policy](SECURITY.md).
78
79
 
79
80
  ## License
@@ -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,8 +15,8 @@ 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.0.gem
19
+ bin/check-reference --gem tmp/package/openui-rails-0.9.0.gem --runtime tmp/real-runtime
20
20
  ```
21
21
 
22
22
  Fetch the runtime first. See the [complete guide](https://openui-rails.wangyaodi.com/reference-workflow/)
@@ -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.0"
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,7 +1,7 @@
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andy Wang