Skip to content

feat(state-editor): add live signal and store editing with object tree inspection - #41

Open
Xoppyc wants to merge 1 commit into
solidjs:mainfrom
Xoppyc:feat/live-state-editor
Open

Xoppyc wants to merge 1 commit into
solidjs:mainfrom
Xoppyc:feat/live-state-editor

Conversation

@Xoppyc

@Xoppyc Xoppyc commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds live state and store editing directly from the Start Devtools toolbar. Developers can now inspect and update signals and stores on-the-fly, with immediate reactive propagation across dependent memos, effects, and the DOM.

Both the Ownership Tree (component detail view) and the Reactivity Graph (signal/store node detail view) support editing.


What's Changed

  1. Reactivity Write Bridge (value-editor.ts):

    • Bridges external devtool writes through Solid 2.0's native reactivity scheduler using a signal bridge node (createSignal(0)) and property delegation.
    • Wraps writes inside runWithOwner(null, ...) to respect Solid 2.0 runtime invariants and prevent [REACTIVE_WRITE_IN_OWNED_SCOPE].
    • Supports plain signals, nested paths (updatePath), store slot signals (_host), and store proxies ($TARGET).
  2. Safe Input Serialization & Parsing:

    • formatDraftValue() cleanly formats primitives and uses JSON.stringify() for objects and arrays.
    • parseValueInput() parses numbers, booleans, null, undefined, JSON objects/arrays, and strings.
    • Protects complex object signals (such as selected-entry in demo:explorer) from being inadvertently corrupted into the string "[object Object]".
  3. Ownership Panel Integration:

    • Added OwnershipSignalRow: inline editing with draft commit on Enter / blur and cancel on Escape.
    • 1-click boolean toggle button (true ⇄ false).
    • Signals holding complex objects or arrays feature an expandable chevron rendering an embedded <ValueInspector> tree with inline property editing.
  4. Reactivity Graph Integration:

    • Enabled interactive editing in the Reactivity Graph detail pane (editable={!node().errored && (node().kind === 'signal' || node().kind === 'store')}).
    • Double-click or click the edit icon to modify values or nested paths in <ValueInspector>.

Showcase

2026-10-30.16-58-20.mp4

Verification

  • Unit tests: 16 new unit tests in value-editor.test.ts (all 166 Vitest unit tests pass across 15 test files).
  • E2E tests: Added Playwright test verifying live editing of <Counter>'s count signal in both Ownership Tree and Reactivity Graph, asserting live DOM updates (count 0 -> 42 -> 100). All 15 E2E tests pass.
  • Typecheck: Clean (pnpm typecheck, 0 errors).
  • Build & Format: Clean Rolldown build and oxfmt check.
  • Changeset: Added .changeset/live-state-editor.md.

@lxsmnsyc lxsmnsyc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. Plain signal and store slot edits work: I ran writeReactiveValue against the browser dev build of solid-js and a dependent memo updated for a plain signal, a nested path in an object signal, a top-level store key and a nested store path (todos[0].done). A few things hold it back.

Blocking

  1. A no-op edit rewrites object values. OwnershipSignalRow fills the input with JSON.stringify(value). On blur, parseValueInput returns a new object from JSON.parse, which is never === the old value, so writeReactiveValue runs. Double-clicking an object signal and clicking away replaces it with a JSON copy: Map, Set, Date, class instances, functions and undefined fields are lost, and identity changes. Fix: skip the write when the draft equals the initial draft, and only offer JSON editing for plain objects and arrays.

Should fix

  1. Editing inside a Map turns it into a plain object. ValueInspector passes the containerEntries key down as the path. For a Map, that key is the formatted string ("k" with quotes), and updatePath spreads the Map into {}. Repro: signal { m: new Map([['k', 1]]) }, writeReactiveValue(node, ['m', '"k"'], 2) gives m === { '"k"': 2 }. The same applies to Set and typed arrays. Fix: turn off editable below non-plain containers, or handle them in updatePath.
  2. The unit tests do not reach the bridge. Vitest runs in Node, so solid-js resolves to dist/server.dev.js. The writeReactiveValue tests use hand-made { _value, _subs: null } objects and only check _value. The test named "notifies observers" creates count and double but never connects them to the node. Please test with real createSignal and createStore nodes under the browser + development conditions and assert a dependent memo updates.
  3. Failure is reported as success. When the bridge cannot be set up, write sets target._value directly and returns true. No subscriber is notified and the UI shows the new value while the app does not. Return false here and show it in the UI.
  4. The $TARGET / own-symbol branch looks unreachable and is unsafe. Both callers pass a reactive node (signal.node, node().raw), not a store proxy. Any node with an own symbol key enters this branch, and if it has v, it mutates the raw store object without notifying anyone. Please remove it or show the case that needs it.

Cost

The writer depends on about 20 hard-coded private field names of the Solid 2 runtime (_value, _subs, _pendingValue, _statusFlags, ...) and re-points one shared signal node at each target. A rename in a solid-js release breaks editing without a type error. A write hook in solid-js DEV would remove this. I'd like the maintainer to decide whether to take the bridge for now.

Nits

  • parseValueInput turns any input other than true or 1 into false for a boolean (yes gives false). Return the raw input or keep the old value.
  • Editing a bigint leaf writes a number.
  • EditableValue and OwnershipSignalRow repeat the same draft, commit and cancel logic.

This PR and #42 both change src/dev-toolbar/ownership/index.tsx and conflict there; whichever merges second needs a rebase.

Checks: CI test job passed (build, unit, typecheck, e2e, lint, format).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants