Description
In @openuidev/lang-core, createStore().initialize() only applies declaration defaults for new keys. During streaming parse recovery, a $binding can first materialize with a truncated string default (lexer auto-closes an incomplete "..."). That truncated value is written into the store and then never updated when the full declaration arrives, because the key already exists.
After a full page reload the store is fresh, so the complete default is applied once and the UI looks correct — which makes the bug easy to miss.
This affects any consumer of the store (@openuidev/vue-lang, @openuidev/react-lang) that re-calls initialize as stateDeclarations change during streaming.
Package / API
- Package:
@openuidev/lang-core
- API:
createStore → initialize(defaults, persisted) in packages/lang-core/src/runtime/store.ts
- Observed via: openui-lang
$bindings used in TextContent / Mutation args (e.g. $title = "...")
Steps to reproduce
- Render a
<Renderer> (Vue or React) with streaming isStreaming / incremental response text.
- Stream openui-lang that declares a long string binding, for example:
$title = "issue with OpenUI, missing Vue 3 headless package"
root = Stack([TextContent("Support ticket prepared: " + $title)])
- Observe the first paint while the string literal is still incomplete (lexer recovers by closing the quote early), e.g.
$title becomes "issue".
- Let the stream finish so
stateDeclarations.$title is the full string.
- Note that the UI still shows the truncated title until a full remount / page reload.
Expected behavior
Once streaming recovers the complete declaration default for a $binding that the user has not edited, the store should update to the new default so the UI shows the full value without reload.
Actual behavior
- Mid-stream:
$title === "issue" (or another truncated recovery).
- After stream completes:
$title stays truncated because initialize skips existing keys.
- After page reload:
$title is correct (fresh store).
Relevant current logic in packages/lang-core/src/runtime/store.ts L84–L88:
for (const key of Object.keys(defaults)) {
if (!state.has(key)) {
state.set(key, defaults[key]);
}
}
The comment intends to preserve user-edited bindings and avoid deleting keys when declarations briefly disappear during streaming — but it also blocks updating pristine declaration defaults as they grow from partial → complete.
Proposed direction (for maintainer approval)
Track pristine keys (set only via initialize, cleared on set() / persisted restore). On re-initialize, update values for keys that are still pristine when the declaration default changes; keep dirty / user-edited values untouched.
Happy to open a PR once this issue is approved (per CONTRIBUTING).
Environment
- Monorepo:
thesysdev/openui
- Package:
@openuidev/lang-core (workspace)
- Reproduced in
examples/vue-chat with Query/Mutation + $title bindings while streaming assistant messages
- Node / pnpm workspace setup as in repo docs
Additional context
- Relates to streaming lexer recovery that closes incomplete string tokens (
packages/lang-core/src/parser/lexer.ts).
- Same
initialize is used from both vue-lang and react-lang state hooks when stateDeclarations change.
Description
In
@openuidev/lang-core,createStore().initialize()only applies declaration defaults for new keys. During streaming parse recovery, a$bindingcan first materialize with a truncated string default (lexer auto-closes an incomplete"..."). That truncated value is written into the store and then never updated when the full declaration arrives, because the key already exists.After a full page reload the store is fresh, so the complete default is applied once and the UI looks correct — which makes the bug easy to miss.
This affects any consumer of the store (
@openuidev/vue-lang,@openuidev/react-lang) that re-callsinitializeasstateDeclarationschange during streaming.Package / API
@openuidev/lang-corecreateStore→initialize(defaults, persisted)inpackages/lang-core/src/runtime/store.ts$bindingsused inTextContent/Mutationargs (e.g.$title = "...")Steps to reproduce
<Renderer>(Vue or React) with streamingisStreaming/ incremental response text.$titlebecomes"issue".stateDeclarations.$titleis the full string.Expected behavior
Once streaming recovers the complete declaration default for a
$bindingthat the user has not edited, the store should update to the new default so the UI shows the full value without reload.Actual behavior
$title === "issue"(or another truncated recovery).$titlestays truncated becauseinitializeskips existing keys.$titleis correct (fresh store).Relevant current logic in
packages/lang-core/src/runtime/store.tsL84–L88:The comment intends to preserve user-edited bindings and avoid deleting keys when declarations briefly disappear during streaming — but it also blocks updating pristine declaration defaults as they grow from partial → complete.
Proposed direction (for maintainer approval)
Track pristine keys (set only via
initialize, cleared onset()/ persisted restore). On re-initialize, update values for keys that are still pristine when the declaration default changes; keep dirty / user-edited values untouched.Happy to open a PR once this issue is approved (per CONTRIBUTING).
Environment
thesysdev/openui@openuidev/lang-core(workspace)examples/vue-chatwith Query/Mutation +$titlebindings while streaming assistant messagesAdditional context
packages/lang-core/src/parser/lexer.ts).initializeis used from bothvue-langandreact-langstate hooks whenstateDeclarationschange.