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 +4 -4
- data/CHANGELOG.md +56 -1
- data/CONTRIBUTING.md +1 -1
- data/DEVELOPMENT.md +56 -1
- data/README.md +8 -4
- data/RELEASING.md +6 -0
- data/THIRD_PARTY_NOTICES.md +28 -9
- data/app/controllers/openui/rails/actions_controller.rb +12 -3
- data/app/javascript/openui/rails/openui_controller.js +14 -1
- data/examples/support_desk/README.md +7 -2
- data/examples/support_desk/app/controllers/support_desk/base_controller.rb +11 -0
- data/examples/support_desk/app/views/layouts/support_desk/application.html.erb +1 -0
- data/examples/support_desk/app/views/support_desk/conversations/index.html.erb +7 -0
- data/examples/support_desk/boot.rb +2 -0
- data/lib/openui/rails/adapters/ruby_llm.rb +12 -0
- data/lib/openui/rails/configuration.rb +34 -2
- data/lib/openui/rails/engine.rb +4 -0
- data/lib/openui/rails/runtime_asset.rb +4 -3
- data/lib/openui/rails/stream/accumulator.rb +6 -2
- data/lib/openui/rails/stream/bridge.rb +19 -2
- data/lib/openui/rails/stream/broadcaster.rb +5 -1
- data/lib/openui/rails/version.rb +1 -1
- data/lib/openui/rails.rb +8 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d89e42577f933fcb68ce1740bbc51496998c91f4f2f529880566e568f66543d
|
|
4
|
+
data.tar.gz: 8ee1ecfbef87fb51c07d5a85dc461555901f8dfc20c45da27093eabab82b5416
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
-
|
|
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.
|
|
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
|
data/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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
|
|
14
|
-
|
|
15
|
-
|
|
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 |
|
|
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/
|
|
22
|
-
| `@openuidev/
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
19
|
-
bin/check-reference --gem tmp/package/openui-rails-0.
|
|
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
|
|
@@ -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,
|
|
33
|
-
:actions_endpoint,
|
|
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
|
data/lib/openui/rails/engine.rb
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
24
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
data/lib/openui/rails/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|