Skip to content

bug(lang-core): streaming $binding defaults stick at truncated value (store.initialize skips existing keys) #767

Description

@AlexQuidditch

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: createStoreinitialize(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

  1. Render a <Renderer> (Vue or React) with streaming isStreaming / incremental response text.
  2. 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)])
  1. Observe the first paint while the string literal is still incomplete (lexer recovers by closing the quote early), e.g. $title becomes "issue".
  2. Let the stream finish so stateDeclarations.$title is the full string.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions