Repository: cashapp/redwood
Pull request #600 (2022-12-06 20:22:05)

Migrate widget tag to value class
This also removes usage of 'type' and 'kind' to describe this value.

Refs #418 


Patch:
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
index f5607f8f4..755ad8f2c 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffConsumingGeneration.kt
@@ -24,7 +24,6 @@ import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.ClassName
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.INT
 import com.squareup.kotlinpoet.KModifier.INTERNAL
 import com.squareup.kotlinpoet.KModifier.OVERRIDE
 import com.squareup.kotlinpoet.KModifier.PRIVATE
@@ -47,15 +46,15 @@ public class DiffConsumingSunspotWidgetFactory<W : Any>(
   override fun create(
     parentId: Id,
     parentChildren: Widget.Children<W>,
-    kind: Int,
-  ): DiffConsumingWidget<W>? = when (kind) {
+    tag: WidgetTag,
+  ): DiffConsumingWidget<W>? = when (tag.value) {
     1 -> SunspotBox(parentId, parentChildren)
     2 -> SunspotText(parentId, parentChildren)
     3 -> SunspotButton(parentId, parentChildren)
     1_000_001 -> RedwoodLayout.Row(parentId, parentChildren)
     1_000_002 -> RedwoodLayout.Column(parentId, parentChildren)
     else -> {
-      mismatchHandler.onUnknownWidget(kind)
+      mismatchHandler.onUnknownWidget(tag)
       null
     }
   }
@@ -119,12 +118,12 @@ internal fun generateDiffConsumingWidgetFactory(schema: Schema, host: Schema = s
                 .addModifiers(OVERRIDE)
                 .addParameter("parentId", Protocol.Id)
                 .addParameter("parentChildren", RedwoodWidget.WidgetChildrenOfW)
-                .addParameter("kind", INT)
+                .addParameter("tag", Protocol.WidgetTag)
                 .returns(
                   WidgetProtocol.DiffConsumingNode.parameterizedBy(typeVariableW)
                     .copy(nullable = true),
                 )
-                .beginControlFlow("return when (kind)")
+                .beginControlFlow("return when (tag.value)")
                 .apply {
                   for (widget in schema.widgets.sortedBy { it.tag }) {
                     addStatement("%L -> %N(parentId, parentChildren)", widget.tag, widget.type.flatName)
@@ -136,7 +135,7 @@ internal fun generateDiffConsumingWidgetFactory(schema: Schema, host: Schema = s
                   }
                 }
                 .beginControlFlow("else ->")
-                .addStatement("mismatchHandler.onUnknownWidget(kind)")
+                .addStatement("mismatchHandler.onUnknownWidget(tag)")
                 .addStatement("null")
                 .endControlFlow()
                 .endControlFlow()
@@ -207,7 +206,7 @@ internal class DiffConsumingSunspotButton<W : Any>(
         }
         delegate.onClick(onClick)
       }
-      else -> mismatchHandler.onUnknownProperty(12, diff.tag)
+      else -> mismatchHandler.onUnknownProperty(WidgetTag(12), diff.tag)
     }
   }
 }
@@ -328,7 +327,11 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                   )
                 }
               }
-              .addStatement("else -> mismatchHandler.onUnknownProperty(%L, diff.tag)", widget.tag)
+              .addStatement(
+                "else -> mismatchHandler.onUnknownProperty(%T(%L), diff.tag)",
+                Protocol.WidgetTag,
+                widget.tag,
+              )
               .endControlFlow()
               .build(),
           )
@@ -345,12 +348,20 @@ internal fun generateDiffConsumingWidget(schema: Schema, widget: Widget, host: S
                     addStatement("%L -> widget.%N", children.tag, children.name)
                   }
                   beginControlFlow("else ->")
-                  addStatement("mismatchHandler.onUnknownChildren(%L, tag)", widget.tag)
+                  addStatement(
+                    "mismatchHandler.onUnknownChildren(%T(%L), tag)",
+                    Protocol.WidgetTag,
+                    widget.tag,
+                  )
                   addStatement("null")
                   endControlFlow()
                   endControlFlow()
                 } else {
-                  addStatement("mismatchHandler.onUnknownChildren(%L, tag)", widget.tag)
+                  addStatement(
+                    "mismatchHandler.onUnknownChildren(%T(%L), tag)",
+                    Protocol.WidgetTag,
+                    widget.tag,
+                  )
                   addStatement("return null")
                 }
               }
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
index 45c6ad859..1fece39d6 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/diffProducingGeneration.kt
@@ -24,7 +24,6 @@ import com.squareup.kotlinpoet.AnnotationSpec
 import com.squareup.kotlinpoet.CodeBlock
 import com.squareup.kotlinpoet.FileSpec
 import com.squareup.kotlinpoet.FunSpec
-import com.squareup.kotlinpoet.INT
 import com.squareup.kotlinpoet.KModifier.INTERNAL
 import com.squareup.kotlinpoet.KModifier.OVERRIDE
 import com.squareup.kotlinpoet.KModifier.PRIVATE
@@ -124,7 +123,7 @@ internal class DiffProducingSunspotButton(
   private val mismatchHandler: ProtocolMismatchHandler,
 ) : DiffProducingWidget, SunspotButton<Nothing> {
   public override val id: Id = bridge.nextId()
-  public override val type: Int get() = 3
+  public override val tag: WidgetTag get() = WidgetTag(3)
 
   private var onClick: (() -> Unit)? = null
 
@@ -197,10 +196,10 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
             .build(),
         )
         .addProperty(
-          PropertySpec.builder("type", INT, PUBLIC, OVERRIDE)
+          PropertySpec.builder("tag", Protocol.WidgetTag, PUBLIC, OVERRIDE)
             .getter(
               FunSpec.getterBuilder()
-                .addStatement("return %L", widget.tag)
+                .addStatement("return %T(%L)", Protocol.WidgetTag, widget.tag)
                 .build(),
             )
             .build(),
@@ -300,7 +299,7 @@ internal fun generateDiffProducingWidget(schema: Schema, widget: Widget, host: S
                   }
                 }
               }
-              .addStatement("else -> mismatchHandler.onUnknownEvent(%L, event.tag)", widget.tag)
+              .addStatement("else -> mismatchHandler.onUnknownEvent(tag, event.tag)")
               .endControlFlow()
               .build(),
           )
diff --git a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
index 9e72b33d6..ad34cc293 100644
--- a/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
+++ b/redwood-cli/src/main/kotlin/app/cash/redwood/generator/types.kt
@@ -31,9 +31,10 @@ internal object Protocol {
   val EventTag = ClassName("app.cash.redwood.protocol", "EventTag")
   val EventSink = ClassName("app.cash.redwood.protocol", "EventSink")
   val Id = ClassName("app.cash.redwood.protocol", "Id")
+  val LayoutModifiers = ClassName("app.cash.redwood.protocol", "LayoutModifiers")
   val PropertyDiff = ClassName("app.cash.redwood.protocol", "PropertyDiff")
   val PropertyTag = ClassName("app.cash.redwood.protocol", "PropertyTag")
-  val LayoutModifiers = ClassName("app.cash.redwood.protocol", "LayoutModifiers")
+  val WidgetTag = ClassName("app.cash.redwood.protocol", "WidgetTag")
 }
 
 internal object ComposeProtocol {
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidget.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidget.kt
index 0307a131e..0f14cb74e 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidget.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidget.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.Event
 import app.cash.redwood.protocol.Id
+import app.cash.redwood.protocol.WidgetTag
 import app.cash.redwood.widget.Widget
 
 /**
@@ -25,7 +26,7 @@ import app.cash.redwood.widget.Widget
  */
 public interface DiffProducingWidget : Widget<Nothing> {
   public val id: Id
-  public val type: Int
+  public val tag: WidgetTag
 
   override val value: Nothing
     get() = throw AssertionError()
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
index 9f91cbb25..b6de731ac 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetChildren.kt
@@ -31,7 +31,7 @@ internal class DiffProducingWidgetChildren(
     widget as DiffProducingWidget
     ids.add(index, widget.id)
     bridge.addWidget(widget)
-    bridge.append(ChildrenDiff.Insert(id, tag, widget.id, widget.type, index))
+    bridge.append(ChildrenDiff.Insert(id, tag, widget.id, widget.tag, index))
   }
 
   override fun remove(index: Int, count: Int) {
diff --git a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
index e0f635faa..2869cf0a4 100644
--- a/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
+++ b/redwood-protocol-compose/src/commonMain/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandler.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.EventTag
+import app.cash.redwood.protocol.WidgetTag
 import kotlin.jvm.JvmField
 
 /**
@@ -24,15 +25,17 @@ import kotlin.jvm.JvmField
  * a newer schema than the other, or if their schemas were changed in an incompatible way.
  */
 public interface ProtocolMismatchHandler {
-  /** Handle a request to process an unknown event [tag] for the specified widget [kind]. */
-  public fun onUnknownEvent(kind: Int, tag: EventTag)
+  /** Handle a request to process an unknown event [tag] for the specified widget [widgetTag]. */
+  public fun onUnknownEvent(widgetTag: WidgetTag, tag: EventTag)
 
   public companion object {
     /** A [ProtocolMismatchHandler] which throws [IllegalArgumentException] for all callbacks. */
     @JvmField
     public val Throwing: ProtocolMismatchHandler = object : ProtocolMismatchHandler {
-      override fun onUnknownEvent(kind: Int, tag: EventTag) {
-        throw IllegalArgumentException("Unknown event tag ${tag.value} for widget kind $kind")
+      override fun onUnknownEvent(widgetTag: WidgetTag, tag: EventTag) {
+        throw IllegalArgumentException(
+          "Unknown event tag ${tag.value} for widget tag ${widgetTag.value}",
+        )
       }
     }
   }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
index 48c8f0e82..6d336dbf1 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/DiffConsumingNode.kt
@@ -19,6 +19,7 @@ import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.EventSink
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.PropertyDiff
+import app.cash.redwood.protocol.WidgetTag
 import app.cash.redwood.widget.Widget
 import kotlinx.serialization.json.JsonArray
 
@@ -52,16 +53,16 @@ public abstract class DiffConsumingNode<W : Any>(
 
   public interface Factory<W : Any> {
     /**
-     * Create a new protocol-consuming widget of the specified [kind].
+     * Create a new protocol-consuming widget of the specified [tag].
      *
-     * Invalid [kind] values can either produce an exception or result in `null` being returned.
+     * Invalid [tag] values can either produce an exception or result in `null` being returned.
      * If `null` is returned, the caller should make every effort to ignore this widget and
      * continue executing.
      */
     public fun create(
       parentId: Id,
       parentChildren: Widget.Children<W>,
-      kind: Int,
+      tag: WidgetTag,
     ): DiffConsumingNode<W>?
   }
 }
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
index b4ca388a8..4771f80e4 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolBridge.kt
@@ -50,7 +50,7 @@ public class ProtocolBridge<W : Any>(
 
       when (childrenDiff) {
         is ChildrenDiff.Insert -> {
-          val childWidget = factory.create(id, children, childrenDiff.kind) ?: continue
+          val childWidget = factory.create(id, children, childrenDiff.widgetTag) ?: continue
           children.insert(childrenDiff.index, childWidget.widget)
           val old = nodes.put(childrenDiff.childId, childWidget)
           require(old == null) {
diff --git a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
index 28a98e948..e447e77f1 100644
--- a/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonMain/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandler.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import kotlin.jvm.JvmField
 
 /**
@@ -25,36 +26,40 @@ import kotlin.jvm.JvmField
  * a newer schema than the other, or if their schemas were changed in an incompatible way.
  */
 public interface ProtocolMismatchHandler {
-  /** Handle a request to create an unknown widget [kind]. */
-  public fun onUnknownWidget(kind: Int)
+  /** Handle a request to create an unknown widget [tag]. */
+  public fun onUnknownWidget(tag: WidgetTag)
 
   /** Handle a request to create an unknown layout modifier [tag]. */
   public fun onUnknownLayoutModifier(tag: Int)
 
-  /** Handle a request to manipulate unknown children [tag] for the specified widget [kind]. */
-  public fun onUnknownChildren(kind: Int, tag: ChildrenTag)
+  /** Handle a request to manipulate unknown children [tag] for the specified [widgetTag]. */
+  public fun onUnknownChildren(widgetTag: WidgetTag, tag: ChildrenTag)
 
-  /** Handle a request to set an unknown property [tag] for the specified widget [kind]. */
-  public fun onUnknownProperty(kind: Int, tag: PropertyTag)
+  /** Handle a request to set an unknown property [tag] for the specified [widgetTag]. */
+  public fun onUnknownProperty(widgetTag: WidgetTag, tag: PropertyTag)
 
   public companion object {
     /** A [ProtocolMismatchHandler] which throws [IllegalArgumentException] for all callbacks. */
     @JvmField
     public val Throwing: ProtocolMismatchHandler = object : ProtocolMismatchHandler {
-      override fun onUnknownWidget(kind: Int) {
-        throw IllegalArgumentException("Unknown widget kind $kind")
+      override fun onUnknownWidget(tag: WidgetTag) {
+        throw IllegalArgumentException("Unknown widget tag ${tag.value}")
       }
 
       override fun onUnknownLayoutModifier(tag: Int) {
         throw IllegalArgumentException("Unknown layout modifier tag $tag")
       }
 
-      override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
-        throw IllegalArgumentException("Unknown children tag ${tag.value} for widget kind $kind")
+      override fun onUnknownChildren(widgetTag: WidgetTag, tag: ChildrenTag) {
+        throw IllegalArgumentException(
+          "Unknown children tag ${tag.value} for widget tag ${widgetTag.value}",
+        )
       }
 
-      override fun onUnknownProperty(kind: Int, tag: PropertyTag) {
-        throw IllegalArgumentException("Unknown property tag ${tag.value} for widget kind $kind")
+      override fun onUnknownProperty(widgetTag: WidgetTag, tag: PropertyTag) {
+        throw IllegalArgumentException(
+          "Unknown property tag ${tag.value} for widget tag ${widgetTag.value}",
+        )
       }
     }
   }
diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
index 3506d5485..f6bca5136 100644
--- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
+++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/protocol.kt
@@ -71,7 +71,7 @@ public sealed class ChildrenDiff {
     override val id: Id,
     override val tag: ChildrenTag,
     val childId: Id,
-    val kind: Int,
+    val widgetTag: WidgetTag,
     val index: Int,
   ) : ChildrenDiff()
 
diff --git a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
index 302b08859..58a9ac10b 100644
--- a/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
+++ b/redwood-protocol/src/commonMain/kotlin/app/cash/redwood/protocol/values.kt
@@ -31,6 +31,10 @@ public value class Id(public val value: Long) {
 @Serializable
 public value class EventTag(public val value: Int)
 
+@JvmInline
+@Serializable
+public value class WidgetTag(public val value: Int)
+
 @JvmInline
 @Serializable
 public value class PropertyTag(public val value: Int)
diff --git a/redwood-treehouse/build.gradle b/redwood-treehouse/build.gradle
index bbef3781a..93fba5d67 100644
--- a/redwood-treehouse/build.gradle
+++ b/redwood-treehouse/build.gradle
@@ -42,12 +42,18 @@ kotlin {
     nativeMain {
       dependsOn(hostMain)
     }
-    androidMain {
+    commonJvmMain {
       dependsOn(hostMain)
       dependencies {
         api libs.okHttp
       }
     }
+    androidMain {
+      dependsOn(commonJvmMain)
+    }
+    jvmMain {
+      dependsOn(commonJvmMain)
+    }
 
     hostTest {
       dependencies {
@@ -59,12 +65,18 @@ kotlin {
     nativeTest {
       dependsOn(hostTest)
     }
-    androidTest {
+    commonJvmTest {
       dependsOn(hostTest)
+    }
+    androidTest {
+      dependsOn(commonJvmTest)
       dependencies {
         implementation libs.robolectric
       }
     }
+    jvmTest {
+      dependsOn(commonJvmTest)
+    }
   }
 }
 
diff --git a/redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/hotReloadAndroid.kt b/redwood-treehouse/src/commonJvmMain/kotlin/app/cash/redwood/treehouse/hotReloadOkHttp.kt
similarity index 100%
rename from redwood-treehouse/src/androidMain/kotlin/app/cash/redwood/treehouse/hotReloadAndroid.kt
rename to redwood-treehouse/src/commonJvmMain/kotlin/app/cash/redwood/treehouse/hotReloadOkHttp.kt
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
index 2915f9c4e..946465c17 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventListener.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.treehouse
 
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import app.cash.zipline.Call
 import app.cash.zipline.CallResult
 import app.cash.zipline.Zipline
@@ -111,7 +112,7 @@ public abstract class EventListener {
    */
   public open fun onUnknownWidget(
     app: TreehouseApp<*>,
-    kind: Int,
+    tag: WidgetTag,
   ) {
   }
 
@@ -129,7 +130,7 @@ public abstract class EventListener {
    */
   public open fun onUnknownChildren(
     app: TreehouseApp<*>,
-    kind: Int,
+    widgetTag: WidgetTag,
     tag: ChildrenTag,
   ) {
   }
@@ -139,7 +140,7 @@ public abstract class EventListener {
    */
   public open fun onUnknownProperty(
     app: TreehouseApp<*>,
-    kind: Int,
+    widgetTag: WidgetTag,
     tag: PropertyTag,
   ) {
   }
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
index 7245e16af..fe7d3ab23 100644
--- a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
+++ b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/EventPublisher.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.treehouse
 
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import app.cash.redwood.protocol.widget.ProtocolMismatchHandler
 import app.cash.zipline.Call
 import app.cash.zipline.CallResult
@@ -117,20 +118,20 @@ internal class EventPublisher(
   }
 
   fun protocolMismatchHandler(app: TreehouseApp<*>) = object : ProtocolMismatchHandler {
-    override fun onUnknownWidget(kind: Int) {
-      listener.onUnknownWidget(app, kind)
+    override fun onUnknownWidget(tag: WidgetTag) {
+      listener.onUnknownWidget(app, tag)
     }
 
     override fun onUnknownLayoutModifier(tag: Int) {
       listener.onUnknownLayoutModifier(app, tag)
     }
 
-    override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
-      listener.onUnknownChildren(app, kind, tag)
+    override fun onUnknownChildren(widgetTag: WidgetTag, tag: ChildrenTag) {
+      listener.onUnknownChildren(app, widgetTag, tag)
     }
 
-    override fun onUnknownProperty(kind: Int, tag: PropertyTag) {
-      listener.onUnknownProperty(app, kind, tag)
+    override fun onUnknownProperty(widgetTag: WidgetTag, tag: PropertyTag) {
+      listener.onUnknownProperty(app, widgetTag, tag)
     }
   }
 }
diff --git a/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceHost.kt b/redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockService.kt
similarity index 100%
rename from redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceHost.kt
rename to redwood-treehouse/src/hostMain/kotlin/app/cash/redwood/treehouse/FrameClockService.kt
diff --git a/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt b/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt
deleted file mode 100644
index a893e52f4..000000000
--- a/redwood-treehouse/src/jvmMain/kotlin/app/cash/redwood/treehouse/FrameClockServiceJvm.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * Copyright (C) 2022 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package app.cash.redwood.treehouse
-
-// TODO(jwilson): deduplicate with identical symbol in hostMain
-public actual val StandardFrameClockService: FrameClockService = object : FrameClockService {
-  override fun sendFrame(timeNanos: Long) {
-    error("unexpected call to sendFrame() on the host")
-  }
-}


Test patch:
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
index 0aa04a80d..93f1d004a 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/DiffProducingWidgetFactoryTest.kt
@@ -159,7 +159,7 @@ class DiffProducingWidgetFactoryTest {
       button.sendEvent(event)
     }
 
-    assertEquals("Unknown event tag 3456543 for widget kind 4", t.message)
+    assertEquals("Unknown event tag 3456543 for widget tag 4", t.message)
   }
 
   @Test fun unknownEventCallsHandler() {
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandlerTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandlerTest.kt
index 62d39253a..151626245 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandlerTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolMismatchHandlerTest.kt
@@ -16,6 +16,7 @@
 package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.EventTag
+import app.cash.redwood.protocol.WidgetTag
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -23,8 +24,8 @@ import kotlin.test.assertFailsWith
 class ProtocolMismatchHandlerTest {
   @Test fun throwingUnknownEvent() {
     val t = assertFailsWith<IllegalArgumentException> {
-      ProtocolMismatchHandler.Throwing.onUnknownEvent(1, EventTag(2))
+      ProtocolMismatchHandler.Throwing.onUnknownEvent(WidgetTag(1), EventTag(2))
     }
-    assertEquals("Unknown event tag 2 for widget kind 1", t.message)
+    assertEquals("Unknown event tag 2 for widget tag 1", t.message)
   }
 }
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
index 87e796ce4..dcea18a65 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/ProtocolTest.kt
@@ -29,6 +29,7 @@ import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.LayoutModifiers
 import app.cash.redwood.protocol.PropertyDiff
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import example.redwood.compose.Button
 import example.redwood.compose.DiffProducingExampleSchemaWidgetFactory
 import example.redwood.compose.Row
@@ -86,10 +87,10 @@ class ProtocolTest {
     assertEquals(
       Diff(
         childrenDiffs = listOf(
-          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), 1 /* row */, 0),
-          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(2), 3 /* text */, 0),
-          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(3), 1 /* row */, 1),
-          ChildrenDiff.Insert(Id(3), ChildrenTag(1), Id(4), 3 /* text */, 0),
+          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), WidgetTag(1) /* row */, 0),
+          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(2), WidgetTag(3) /* text */, 0),
+          ChildrenDiff.Insert(Id(1), ChildrenTag(1), Id(3), WidgetTag(1) /* row */, 1),
+          ChildrenDiff.Insert(Id(3), ChildrenTag(1), Id(4), WidgetTag(3) /* text */, 0),
         ),
         layoutModifiers = listOf(
           LayoutModifiers(Id(1), JsonArray(listOf())),
@@ -137,7 +138,7 @@ class ProtocolTest {
     assertEquals(
       Diff(
         childrenDiffs = listOf(
-          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), 4 /* button */, 0),
+          ChildrenDiff.Insert(Id.Root, ChildrenTag.Root, Id(1), WidgetTag(4) /* button */, 0),
         ),
         layoutModifiers = listOf(
           LayoutModifiers(Id(1), JsonArray(listOf())),
diff --git a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
index a5eaef5ab..09b1383f0 100644
--- a/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
+++ b/redwood-protocol-compose/src/commonTest/kotlin/app/cash/redwood/protocol/compose/RecordingProtocolMismatchHandler.kt
@@ -16,11 +16,12 @@
 package app.cash.redwood.protocol.compose
 
 import app.cash.redwood.protocol.EventTag
+import app.cash.redwood.protocol.WidgetTag
 
 class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
   val events = mutableListOf<String>()
 
-  override fun onUnknownEvent(kind: Int, tag: EventTag) {
-    events += "Unknown event ${tag.value} for $kind"
+  override fun onUnknownEvent(widgetTag: WidgetTag, tag: EventTag) {
+    events += "Unknown event ${tag.value} for ${widgetTag.value}"
   }
 }
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
index a4340f503..d101277e6 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/DiffConsumingWidgetFactoryTest.kt
@@ -23,6 +23,7 @@ import app.cash.redwood.protocol.EventTag
 import app.cash.redwood.protocol.Id
 import app.cash.redwood.protocol.PropertyDiff
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import example.redwood.compose.TestScope
 import example.redwood.widget.DiffConsumingExampleSchemaWidgetFactory
 import example.redwood.widget.TextInput
@@ -45,9 +46,9 @@ class DiffConsumingWidgetFactoryTest {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
 
     val t = assertFailsWith<IllegalArgumentException> {
-      factory.create(Id.Root, ThrowingWidgetChildren(), 345432)
+      factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(345432))
     }
-    assertEquals("Unknown widget kind 345432", t.message)
+    assertEquals("Unknown widget tag 345432", t.message)
   }
 
   @Test fun unknownWidgetCallsHandler() {
@@ -57,7 +58,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    assertNull(factory.create(Id.Root, ThrowingWidgetChildren(), 345432))
+    assertNull(factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(345432)))
 
     assertEquals("Unknown widget 345432", handler.events.single())
   }
@@ -75,7 +76,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(5))!!
 
     textInput.updateLayoutModifier(
       buildJsonArray {
@@ -110,7 +111,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(5))!!
 
     textInput.updateLayoutModifier(
       buildJsonArray {
@@ -134,7 +135,7 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownLayoutModifierThrowsDefault() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val t = assertFailsWith<IllegalArgumentException> {
       button.updateLayoutModifier(
@@ -167,7 +168,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(5))!!
     textInput.updateLayoutModifier(
       buildJsonArray {
         add(
@@ -195,12 +196,12 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownChildrenThrowsDefault() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val t = assertFailsWith<IllegalArgumentException> {
       button.children(ChildrenTag(345432))
     }
-    assertEquals("Unknown children tag 345432 for widget kind 4", t.message)
+    assertEquals("Unknown children tag 345432 for widget tag 4", t.message)
   }
 
   @Test fun unknownChildrenCallsHandler() {
@@ -210,7 +211,7 @@ class DiffConsumingWidgetFactoryTest {
       mismatchHandler = handler,
     )
 
-    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
     assertNull(button.children(ChildrenTag(345432)))
 
     assertEquals("Unknown children 345432 for 4", handler.events.single())
@@ -229,7 +230,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(5))!!
 
     val throwingEventSink = EventSink { error(it) }
     textInput.apply(PropertyDiff(Id(1), PropertyTag(2), JsonPrimitive("PT10S")), throwingEventSink)
@@ -239,14 +240,14 @@ class DiffConsumingWidgetFactoryTest {
 
   @Test fun unknownPropertyThrowsDefaults() {
     val factory = DiffConsumingExampleSchemaWidgetFactory(EmptyExampleSchemaWidgetFactory())
-    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     val diff = PropertyDiff(Id(1), PropertyTag(345432))
     val eventSink = EventSink { throw UnsupportedOperationException() }
     val t = assertFailsWith<IllegalArgumentException> {
       button.apply(diff, eventSink)
     }
-    assertEquals("Unknown property tag 345432 for widget kind 4", t.message)
+    assertEquals("Unknown property tag 345432 for widget tag 4", t.message)
   }
 
   @Test fun unknownPropertyCallsHandler() {
@@ -255,7 +256,7 @@ class DiffConsumingWidgetFactoryTest {
       delegate = EmptyExampleSchemaWidgetFactory(),
       mismatchHandler = handler,
     )
-    val button = factory.create(Id.Root, ThrowingWidgetChildren(), 4)!!
+    val button = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(4))!!
 
     button.apply(PropertyDiff(Id(1), PropertyTag(345432))) { throw UnsupportedOperationException() }
 
@@ -275,7 +276,7 @@ class DiffConsumingWidgetFactoryTest {
       },
       json = json,
     )
-    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), 5)!!
+    val textInput = factory.create(Id.Root, ThrowingWidgetChildren(), WidgetTag(5))!!
 
     val eventSink = RecordingEventSink()
     textInput.apply(PropertyDiff(Id(1), PropertyTag(4), JsonPrimitive(true)), eventSink)
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
index ec634940f..6ee248a24 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolBridgeTest.kt
@@ -19,6 +19,7 @@ import app.cash.redwood.protocol.ChildrenDiff
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.Diff
 import app.cash.redwood.protocol.Id
+import app.cash.redwood.protocol.WidgetTag
 import app.cash.redwood.widget.MutableListChildren
 import example.redwood.widget.DiffConsumingExampleSchemaWidgetFactory
 import kotlin.test.Test
@@ -38,7 +39,7 @@ class ProtocolBridgeTest {
           id = Id.Root,
           tag = ChildrenTag.Root,
           childId = Id.Root,
-          kind = 4 /* button */,
+          widgetTag = WidgetTag(4) /* button */,
           index = 0,
         ),
       ),
@@ -61,7 +62,7 @@ class ProtocolBridgeTest {
           id = Id.Root,
           tag = ChildrenTag.Root,
           childId = Id(1),
-          kind = 4 /* button */,
+          widgetTag = WidgetTag(4) /* button */,
           index = 0,
         ),
       ),
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
index 0b8dff49e..fb2b83b3b 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/ProtocolMismatchHandlerTest.kt
@@ -17,6 +17,7 @@ package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -24,9 +25,9 @@ import kotlin.test.assertFailsWith
 class ProtocolMismatchHandlerTest {
   @Test fun throwingUnknownWidget() {
     val t = assertFailsWith<IllegalArgumentException> {
-      ProtocolMismatchHandler.Throwing.onUnknownWidget(1)
+      ProtocolMismatchHandler.Throwing.onUnknownWidget(WidgetTag(1))
     }
-    assertEquals("Unknown widget kind 1", t.message)
+    assertEquals("Unknown widget tag 1", t.message)
   }
 
   @Test fun throwingUnknownLayoutModifier() {
@@ -38,15 +39,15 @@ class ProtocolMismatchHandlerTest {
 
   @Test fun throwingUnknownChildren() {
     val t = assertFailsWith<IllegalArgumentException> {
-      ProtocolMismatchHandler.Throwing.onUnknownChildren(1, ChildrenTag(2))
+      ProtocolMismatchHandler.Throwing.onUnknownChildren(WidgetTag(1), ChildrenTag(2))
     }
-    assertEquals("Unknown children tag 2 for widget kind 1", t.message)
+    assertEquals("Unknown children tag 2 for widget tag 1", t.message)
   }
 
   @Test fun throwingUnknownProperty() {
     val t = assertFailsWith<IllegalArgumentException> {
-      ProtocolMismatchHandler.Throwing.onUnknownProperty(1, PropertyTag(2))
+      ProtocolMismatchHandler.Throwing.onUnknownProperty(WidgetTag(1), PropertyTag(2))
     }
-    assertEquals("Unknown property tag 2 for widget kind 1", t.message)
+    assertEquals("Unknown property tag 2 for widget tag 1", t.message)
   }
 }
diff --git a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
index 3e2d6c1a0..6fb14853c 100644
--- a/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
+++ b/redwood-protocol-widget/src/commonTest/kotlin/app/cash/redwood/protocol/widget/RecordingProtocolMismatchHandler.kt
@@ -17,23 +17,24 @@ package app.cash.redwood.protocol.widget
 
 import app.cash.redwood.protocol.ChildrenTag
 import app.cash.redwood.protocol.PropertyTag
+import app.cash.redwood.protocol.WidgetTag
 
 class RecordingProtocolMismatchHandler : ProtocolMismatchHandler {
   val events = mutableListOf<String>()
 
-  override fun onUnknownWidget(kind: Int) {
-    events += "Unknown widget $kind"
+  override fun onUnknownWidget(tag: WidgetTag) {
+    events += "Unknown widget ${tag.value}"
   }
 
   override fun onUnknownLayoutModifier(tag: Int) {
     events += "Unknown layout modifier $tag"
   }
 
-  override fun onUnknownChildren(kind: Int, tag: ChildrenTag) {
-    events += "Unknown children ${tag.value} for $kind"
+  override fun onUnknownChildren(widgetTag: WidgetTag, tag: ChildrenTag) {
+    events += "Unknown children ${tag.value} for ${widgetTag.value}"
   }
 
-  override fun onUnknownProperty(kind: Int, tag: PropertyTag) {
-    events += "Unknown property ${tag.value} for $kind"
+  override fun onUnknownProperty(widgetTag: WidgetTag, tag: PropertyTag) {
+    events += "Unknown property ${tag.value} for ${widgetTag.value}"
   }
 }
diff --git a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
index 3eedbcc42..b8660fb50 100644
--- a/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
+++ b/redwood-protocol/src/commonTest/kotlin/app/cash/redwood/protocol/ProtocolTest.kt
@@ -44,7 +44,7 @@ class ProtocolTest {
   @Test fun diff() {
     val model = Diff(
       childrenDiffs = listOf(
-        ChildrenDiff.Insert(Id(1), ChildrenTag(2), Id(3), 4, 5),
+        ChildrenDiff.Insert(Id(1), ChildrenTag(2), Id(3), WidgetTag(4), 5),
         ChildrenDiff.Move(Id(1), ChildrenTag(2), 3, 4, 5),
         ChildrenDiff.Remove(Id(1), ChildrenTag(2), 3, 4),
       ),
@@ -68,7 +68,7 @@ class ProtocolTest {
     )
     val json = "" +
       """{"childrenDiffs":[""" +
-      """["insert",{"id":1,"tag":2,"childId":3,"kind":4,"index":5}],""" +
+      """["insert",{"id":1,"tag":2,"childId":3,"widgetTag":4,"index":5}],""" +
       """["move",{"id":1,"tag":2,"fromIndex":3,"toIndex":4,"count":5}],""" +
       """["remove",{"id":1,"tag":2,"index":3,"count":4}]""" +
       """],"layoutModifiers":[""" +