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

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions