bug(lang-core): streaming $binding defaults stick at truncated value (store.initialize skips existing keys)
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)inpackages/lang-core/src/runtime/store.ts - Observed via: openui-lang
$bindingsused inTextContent/Mutationargs (e.g.$title = "...")
Steps to reproduce
- Render a
<Renderer>(Vue or React) with streamingisStreaming/ 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.
$titlebecomes"issue". - Let the stream finish so
stateDeclarations.$titleis 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:
$titlestays truncated becauseinitializeskips existing keys. - After page reload:
$titleis 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-chatwith Query/Mutation +$titlebindings 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
initializeis used from bothvue-langandreact-langstate hooks whenstateDeclarationschange.
Source: thesysdev/openui