Repository: dxos/dxos
Pull request #8898: refactor: use queue for channel messages
Author: login: wittjosiah
Created: 2025-04-03T23:19:50Z | Merged: 2025-04-04T02:00:29Z



Diff:
diff --git a/packages/apps/composer-app/src/migrations/migrations.node.test.ts b/packages/apps/composer-app/src/migrations/migrations.node.test.ts
index c92278063ec..c9b2646ad99 100644
--- a/packages/apps/composer-app/src/migrations/migrations.node.test.ts
+++ b/packages/apps/composer-app/src/migrations/migrations.node.test.ts
@@ -172,24 +172,6 @@ describe('Composer migrations', () => {
         ],
       }),
     );
-    const thread2 = space.db.add(
-      create(LegacyTypes.ThreadType, {
-        title: 'My Thread',
-        messages: [
-          makeRef(
-            create(LegacyTypes.MessageType, {
-              from: { identityKey: PublicKey.random().toHex() },
-              blocks: [
-                {
-                  timestamp: new Date().toISOString(),
-                  content: makeRef(create(LegacyTypes.TextType, { content: 'hello world' })),
-                },
-              ],
-            }),
-          ),
-        ],
-      }),
-    );
     const cursor = toCursorRange(createDocAccessor(doc1.content!.target!, ['content']), 0, 3);
     doc1.comments?.push({ cursor, thread: makeRef(thread1) });
     expect(doc1.comments![0].thread?.target instanceof LegacyTypes.ThreadType).to.be.true;
@@ -220,32 +202,16 @@ describe('Composer migrations', () => {
     await __COMPOSER_MIGRATIONS__[1].next({ space, builder });
     await (builder as any)._commit();
 
-    {
-      const migratedDoc1 = space.db.getObjectById<DocumentType>(doc1.id);
-      expect(isInstanceOf(DocumentType, migratedDoc1)).to.be.true;
-      expect(migratedDoc1?.threads?.[0].target instanceof ThreadType).to.be.true;
-      expect(migratedDoc1?.threads?.[0].target?.id).to.equal(thread1.id);
-      expect(migratedDoc1?.threads?.[0].target?.anchor).to.equal(cursor);
-      expect(migratedDoc1?.threads?.[0].target?.messages?.[0]?.target instanceof MessageType).to.be.true;
-      const block = migratedDoc1?.threads?.[0].target?.messages?.[0]?.target?.blocks?.[0];
-      expect(block?.type).to.equal('text');
-      invariant(block?.type === 'text', 'Block is a text block');
-      expect(block?.text).to.equal('comment1');
-    }
-
-    {
-      const { objects: channels } = await space.db.query(Filter.schema(ChannelType)).run();
-      expect(channels).to.have.lengthOf(1);
-      const migratedThread2 = channels[0]?.threads[0].target;
-      expect(migratedThread2 instanceof ThreadType).to.be.true;
-      expect(migratedThread2?.id).to.equal(thread2.id);
-      expect(migratedThread2?.name).to.equal('My Thread');
-      expect(migratedThread2?.messages?.[0].target instanceof MessageType).to.be.true;
-      const block = migratedThread2?.messages?.[0]?.target?.blocks?.[0];
-      expect(block?.type).to.equal('text');
-      invariant(block?.type === 'text', 'Block is a text block');
-      expect(block?.text).to.equal('hello world');
-    }
+    const migratedDoc1 = space.db.getObjectById<DocumentType>(doc1.id);
+    expect(isInstanceOf(DocumentType, migratedDoc1)).to.be.true;
+    expect(migratedDoc1?.threads?.[0].target instanceof ThreadType).to.be.true;
+    expect(migratedDoc1?.threads?.[0].target?.id).to.equal(thread1.id);
+    expect(migratedDoc1?.threads?.[0].target?.anchor).to.equal(cursor);
+    expect(migratedDoc1?.threads?.[0].target?.messages?.[0]?.target instanceof MessageType).to.be.true;
+    const block = migratedDoc1?.threads?.[0].target?.messages?.[0]?.target?.blocks?.[0];
+    expect(block?.type).to.equal('text');
+    invariant(block?.type === 'text', 'Block is a text block');
+    expect(block?.text).to.equal('comment1');
 
     const migratedSketch1 = space.db.getObjectById<DiagramType>(sketch1.id);
     expect(migratedSketch1 instanceof DiagramType).to.be.true;
diff --git a/packages/apps/composer-app/src/migrations/migrations.ts b/packages/apps/composer-app/src/migrations/migrations.ts
index 6d7a7692943..99aad92a9b4 100644
--- a/packages/apps/composer-app/src/migrations/migrations.ts
+++ b/packages/apps/composer-app/src/migrations/migrations.ts
@@ -11,7 +11,7 @@ import { type Migration, type MigrationBuilder, type ObjectStructure } from '@dx
 import { DocumentType } from '@dxos/plugin-markdown/types';
 import { type MigrateCanvas } from '@dxos/plugin-sketch/sdk';
 import { DiagramType, CanvasType, TLDRAW_SCHEMA } from '@dxos/plugin-sketch/types';
-import { CollectionType, ChannelType, ThreadType } from '@dxos/plugin-space/types';
+import { CollectionType, ThreadType } from '@dxos/plugin-space/types';
 import { TableType } from '@dxos/react-ui-table/types';
 import { TextType, MessageType } from '@dxos/schema';
 import { getDeep, isNode, isNonNullable } from '@dxos/util';
@@ -200,46 +200,6 @@ export const __COMPOSER_MIGRATIONS__: Migration[] = [
           },
         }));
       }
-
-      //
-      // Threads
-      //
-
-      const { objects: threads } = await space.db.query(Filter.schema(LegacyTypes.ThreadType)).run();
-      const documentThreads = docs
-        .flatMap((doc) => doc.comments?.map((comment) => comment.thread?.target))
-        .filter(isNonNullable);
-      const standaloneThreads = threads.filter((thread) => !documentThreads.includes(thread));
-
-      for (const thread of standaloneThreads) {
-        const messages = await RefArray.loadAll(thread.messages);
-        for (const message of messages) {
-          // NOTE: Catching errors because some messages may not have block content.
-          const { content } = (await message.blocks[0].content?.load().catch(() => ({ content: '' }))) ?? {
-            content: '',
-          };
-          await builder.migrateObject(message.id, ({ data }) => ({
-            schema: MessageType,
-            props: {
-              sender: data.from ?? space.members.get()[0].identity.identityKey.toHex(),
-              created: data.blocks[0].timestamp,
-              blocks: [{ type: 'text', text: content }],
-            },
-          }));
-        }
-
-        await builder.migrateObject(thread.id, ({ data }) => ({
-          schema: ThreadType,
-          props: {
-            name: data.title,
-            anchor: undefined,
-            messages: data.messages,
-          },
-        }));
-
-        const threadRef = builder.createReference(thread.id);
-        await builder.addObject(ChannelType, { name: thread.title, threads: [threadRef] });
-      }
     },
   },
 ];
diff --git a/packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx b/packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx
index 5e63029db88..98086217f70 100644
--- a/packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx
+++ b/packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx
@@ -107,7 +107,8 @@ const PlankImpl = memo(
     }, [id, scrollIntoView, layoutMode]);
 
     const isSolo = layoutMode === 'solo' && part === 'solo';
-    const isAttendable = isSolo || (layoutMode === 'deck' && part === 'deck');
+    const isAttendable =
+      (layoutMode === 'solo' && part.startsWith('solo')) || (layoutMode === 'deck' && part === 'deck');
 
     const sizeAttrs = useMainSize();
 
@@ -129,7 +130,8 @@ const PlankImpl = memo(
       'attention-surface relative',
       isSolo && mainIntrinsicSize,
       isSolo && railGridHorizontal,
-      isSolo && 'grid absolute inset-0',
+      isSolo && 'absolute inset-0',
+      part.startsWith('solo') && 'grid',
       part === 'deck' && (companioned === 'companion' ? '!border-separator border-ie' : '!border-separator border-li'),
       part.startsWith('solo-') && 'row-span-2 min-is-0',
       part === 'solo-companion' && '!border-separator border-is',
diff --git a/packages/plugins/plugin-space/src/types/thread.ts b/packages/plugins/plugin-space/src/types/thread.ts
index 91d1e97992e..59f6db99839 100644
--- a/packages/plugins/plugin-space/src/types/thread.ts
+++ b/packages/plugins/plugin-space/src/types/thread.ts
@@ -2,7 +2,7 @@
 // Copyright 2024 DXOS.org
 //
 
-import { Ref, S, TypedObject } from '@dxos/echo-schema';
+import { Expando, Ref, S, TypedObject } from '@dxos/echo-schema';
 import { MessageType } from '@dxos/schema';
 
 // TODO(wittjosiah): These types were placed here rather than in @dxos/plugin-thread
@@ -20,5 +20,5 @@ export class ThreadType extends TypedObject({ typename: 'dxos.org/type/Thread',
 
 export class ChannelType extends TypedObject({ typename: 'dxos.org/type/Channel', version: '0.1.0' })({
   name: S.optional(S.String),
-  threads: S.mutable(S.Array(Ref(ThreadType))),
+  queue: Ref(Expando),
 }) {}
diff --git a/packages/plugins/plugin-thread/package.json b/packages/plugins/plugin-thread/package.json
index f3fd30365c4..1c8e0dc38ae 100644
--- a/packages/plugins/plugin-thread/package.json
+++ b/packages/plugins/plugin-thread/package.json
@@ -41,6 +41,7 @@
     "@dxos/display-name": "workspace:*",
     "@dxos/echo-schema": "workspace:*",
     "@dxos/invariant": "workspace:*",
+    "@dxos/keys": "workspace:*",
     "@dxos/live-object": "workspace:*",
     "@dxos/local-storage": "workspace:*",
     "@dxos/log": "workspace:*",
diff --git a/packages/plugins/plugin-thread/src/ThreadPlugin.tsx b/packages/plugins/plugin-thread/src/ThreadPlugin.tsx
index 2696f23e468..97604f02df7 100644
--- a/packages/plugins/plugin-thread/src/ThreadPlugin.tsx
+++ b/packages/plugins/plugin-thread/src/ThreadPlugin.tsx
@@ -49,8 +49,6 @@ export const ThreadPlugin = () =>
           id: ChannelType.typename,
           metadata: {
             icon: 'ph--chat--regular',
-            // TODO(wittjosiah): Move out of metadata.
-            loadReferences: async (channel: ChannelType) => await RefArray.loadAll(channel.threads ?? []),
           },
         }),
         contributes(Capabilities.Metadata, {
@@ -92,7 +90,7 @@ export const ThreadPlugin = () =>
           SpaceCapabilities.ObjectForm,
           defineObjectForm({
             objectSchema: ChannelType,
-            getIntent: () => createIntent(ThreadAction.Create),
+            getIntent: (_, options) => createIntent(ThreadAction.CreateChannel, { spaceId: options.space.id }),
           }),
         ),
     }),
diff --git a/packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts b/packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts
index 16263a34aed..b2785f31737 100644
--- a/packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts
+++ b/packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts
@@ -10,7 +10,9 @@ import {
   LayoutAction,
   type PluginsContext,
 } from '@dxos/app-framework';
-import { Ref } from '@dxos/echo-schema';
+import { ObjectId, Ref } from '@dxos/echo-schema';
+import { DXN, QueueSubspaceTags } from '@dxos/keys';
+import { refFromDXN } from '@dxos/live-object';
 import { log } from '@dxos/log';
 import { ObservabilityAction } from '@dxos/plugin-observability/types';
 import { ChannelType, ThreadType } from '@dxos/plugin-space/types';
@@ -22,44 +24,48 @@ import { ThreadAction } from '../types';
 
 export default (context: PluginsContext) =>
   contributes(Capabilities.IntentResolver, [
+    createResolver({
+      intent: ThreadAction.CreateChannel,
+      resolve: ({ spaceId, name }) => ({
+        data: {
+          object: create(ChannelType, {
+            name,
+            queue: refFromDXN(new DXN(DXN.kind.QUEUE, [QueueSubspaceTags.DATA, spaceId, ObjectId.random()])),
+          }),
+        },
+      }),
+    }),
     createResolver({
       intent: ThreadAction.Create,
       resolve: ({ name, cursor, subject }) => {
-        if (cursor && subject) {
-          // Seed the threads array if it does not exist.
-          if (subject?.threads === undefined) {
-            try {
-              // Static schema will throw an error if subject does not support threads array property.
-              subject.threads = [];
-            } catch (err) {
-              log.error('Subject does not support threads array', { typename: subject?.typename });
-              return;
-            }
-          }
-
-          const { state } = context.requestCapability(ThreadCapabilities.MutableState);
-          const subjectId = fullyQualifiedId(subject);
-          const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
-          const draft = state.drafts[subjectId];
-          if (draft) {
-            draft.push(thread);
-          } else {
-            state.drafts[subjectId] = [thread];
+        // Seed the threads array if it does not exist.
+        if (subject?.threads === undefined) {
+          try {
+            // Static schema will throw an error if subject does not support threads array property.
+            subject.threads = [];
+          } catch (err) {
+            log.error('Subject does not support threads array', { typename: subject?.typename });
+            return;
           }
+        }
 
-          return {
-            data: { object: thread },
-            intents: [
-              createIntent(ThreadAction.Select, { current: fullyQualifiedId(thread) }),
-              createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: 'comments' }),
-            ],
-          };
+        const { state } = context.requestCapability(ThreadCapabilities.MutableState);
+        const subjectId = fullyQualifiedId(subject);
+        const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
+        const draft = state.drafts[subjectId];
+        if (draft) {
+          draft.push(thread);
         } else {
-          // TODO(wittjosiah): Create separate intent for channel creation.
-          return {
-            data: { object: create(ChannelType, { threads: [makeRef(create(ThreadType, { messages: [] }))] }) },
-          };
+          state.drafts[subjectId] = [thread];
         }
+
+        return {
+          data: { object: thread },
+          intents: [
+            createIntent(ThreadAction.Select, { current: fullyQualifiedId(thread) }),
+            createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: 'comments' }),
+          ],
+        };
       },
     }),
     createResolver({
diff --git a/packages/plugins/plugin-thread/src/capabilities/react-surface.tsx b/packages/plugins/plugin-thread/src/capabilities/react-surface.tsx
index d7717d08a61..9e13c36f073 100644
--- a/packages/plugins/plugin-thread/src/capabilities/react-surface.tsx
+++ b/packages/plugins/plugin-thread/src/capabilities/react-surface.tsx
@@ -19,8 +19,7 @@ export default () =>
     createSurface({
       id: `${THREAD_PLUGIN}/channel`,
       role: 'article',
-      filter: (data): data is { subject: ChannelType } =>
-        data.subject instanceof ChannelType && !!data.subject.threads[0],
+      filter: (data): data is { subject: ChannelType } => data.subject instanceof ChannelType,
       component: ({ data: { subject: channel }, role }) => {
         const layout = useLayout();
         const currentPosition = layout.active.findIndex((id) => id === channel.id);
diff --git a/packages/plugins/plugin-thread/src/components/ChannelContainer.tsx b/packages/plugins/plugin-thread/src/components/ChannelContainer.tsx
index baffd4f18cf..8aee0269b59 100644
--- a/packages/plugins/plugin-thread/src/components/ChannelContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/ChannelContainer.tsx
@@ -7,7 +7,7 @@ import React, { useCallback, type ComponentProps } from 'react';
 import { Surface, useCapabilities, useCapability } from '@dxos/app-framework';
 import { type NodeArg } from '@dxos/plugin-graph';
 import { type ChannelType } from '@dxos/plugin-space/types';
-import { fullyQualifiedId } from '@dxos/react-client/echo';
+import { fullyQualifiedId, getSpace } from '@dxos/react-client/echo';
 import { createEditorActionGroup } from '@dxos/react-ui-editor';
 import {
   createMenuAction,
@@ -58,8 +58,8 @@ const ChannelContainer = ({
     [currentActivity],
   );
 
-  const thread = channel.threads[0].target;
-  if (!thread) {
+  const space = getSpace(channel);
+  if (!space) {
     return null;
   }
 
@@ -71,7 +71,7 @@ const ChannelContainer = ({
       {currentActivity ? (
         <Surface role={`channel-activity--${currentActivity}`} data={{ subject: channel }} />
       ) : (
-        <ChatContainer thread={thread} context={context} />
+        <ChatContainer space={space} dxn={channel.queue.dxn} context={context} />
       )}
     </StackItem.Content>
   );
diff --git a/packages/plugins/plugin-thread/src/components/Chat.stories.tsx b/packages/plugins/plugin-thread/src/components/Chat.stories.tsx
index a9c3439176a..3b6eb12a5dc 100644
--- a/packages/plugins/plugin-thread/src/components/Chat.stories.tsx
+++ b/packages/plugins/plugin-thread/src/components/Chat.stories.tsx
@@ -9,10 +9,13 @@ import React, { useEffect, useState } from 'react';
 
 import { Capabilities, contributes, createSurface, IntentPlugin } from '@dxos/app-framework';
 import { withPluginManager } from '@dxos/app-framework/testing';
-import { ThreadType } from '@dxos/plugin-space/types';
+import { ObjectId } from '@dxos/echo-schema';
+import { DXN, QueueSubspaceTags } from '@dxos/keys';
+import { refFromDXN } from '@dxos/live-object';
+import { ChannelType, ThreadType } from '@dxos/plugin-space/types';
 import { faker } from '@dxos/random';
 import { useClient } from '@dxos/react-client';
-import { type Space } from '@dxos/react-client/echo';
+import { create, type Space } from '@dxos/react-client/echo';
 import { useIdentity } from '@dxos/react-client/halo';
 import { withClientProvider } from '@dxos/react-client/testing';
 import { Thread } from '@dxos/react-ui-thread';
@@ -20,7 +23,6 @@ import { MessageType } from '@dxos/schema';
 import { withLayout, withTheme } from '@dxos/storybook-utils';
 
 import { ChatContainer } from './ChatContainer';
-import { createChatThread } from './testing';
 import translations from '../translations';
 
 faker.seed(1);
@@ -29,25 +31,31 @@ const Story = () => {
   const client = useClient();
   const identity = useIdentity();
   const [space, setSpace] = useState<Space>();
-  const [thread, setThread] = useState<ThreadType | null>();
+  const [channel, setChannel] = useState<ChannelType | null>();
 
   useEffect(() => {
     if (identity) {
       setTimeout(async () => {
         const space = await client.spaces.create();
-        const thread = space.db.add(createChatThread(identity));
+        const channel = space.db.add(
+          create(ChannelType, {
+            queue: refFromDXN(new DXN(DXN.kind.QUEUE, [QueueSubspaceTags.DATA, space.id, ObjectId.random()])),
+          }),
+        );
         setSpace(space);
-        setThread(thread);
+        setChannel(channel);
       });
     }
   }, [identity]);
 
-  if (!identity || !thread) {
+  if (!identity || !channel || !space) {
     return null;
   }
 
   return (
-    <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>{space && <ChatContainer thread={thread} />}</main>
+    <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>
+      <ChatContainer space={space} dxn={channel.queue.dxn} />
+    </main>
   );
 };
 
diff --git a/packages/plugins/plugin-thread/src/components/ChatContainer.tsx b/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
index a299bfaab32..feb506c51a6 100644
--- a/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
@@ -4,9 +4,10 @@
 
 import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
 
-import { Ref } from '@dxos/echo-schema';
-import { create, makeRef, RefArray } from '@dxos/live-object';
-import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
+import { createStatic } from '@dxos/echo-schema';
+import { type DXN } from '@dxos/keys';
+import { makeRef } from '@dxos/live-object';
+import { type Space, useMembers, useQueue } from '@dxos/react-client/echo';
 import { useIdentity } from '@dxos/react-client/halo';
 import { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
 import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
@@ -34,11 +35,17 @@ export const ChatHeading = ({ attendableId }: { attendableId?: string }) => {
   );
 };
 
-export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: ThreadContainerProps) => {
+export type ChatContainerProps = Omit<ThreadContainerProps, 'thread'> & {
+  space: Space;
+  dxn: DXN;
+};
+
+export const ChatContainer = ({ space, dxn, context, current, autoFocusTextbox }: ChatContainerProps) => {
+  const id = dxn.toString();
   const identity = useIdentity()!;
-  const space = getSpace(thread);
   const members = useMembers(space?.key);
-  const activity = useStatus(space, fullyQualifiedId(thread));
+  const queue = useQueue<MessageType>(dxn, { pollInterval: 1_000 });
+  const activity = useStatus(space, id);
   const { t } = useTranslation(THREAD_PLUGIN);
   // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
   const [_count, _setCount] = useState(0);
@@ -47,7 +54,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
   const threadScrollRef = useRef<HTMLDivElement | null>(null);
   const { themeMode } = useThemeContext();
 
-  const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
+  const textboxMetadata = getMessageMetadata(id, identity);
   const messageRef = useRef('');
   const extensions = useMemo(
     () => [
@@ -72,20 +79,18 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
 
   // TODO(burdon): Change to model.
   const handleCreate: MessageTextboxProps['onSend'] = () => {
-    if (!messageRef.current?.length) {
+    if (!messageRef.current?.length || !queue) {
       return false;
     }
 
-    thread.messages.push(
-      makeRef(
-        create(MessageType, {
-          sender: { identityKey: identity.identityKey.toHex() },
-          created: new Date().toISOString(),
-          blocks: [{ type: 'text', text: messageRef.current }],
-          properties: context ? { context: makeRef(context) } : undefined,
-        }),
-      ),
-    );
+    queue.append([
+      createStatic(MessageType, {
+        sender: { identityKey: identity.identityKey.toHex() },
+        created: new Date().toISOString(),
+        blocks: [{ type: 'text', text: messageRef.current }],
+        properties: context ? { context: makeRef(context) } : undefined,
+      }),
+    ]);
 
     messageRef.current = '';
     setAutoFocus(true);
@@ -94,24 +99,17 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
     return true;
   };
 
-  const handleDelete = (id: string) => {
-    const messageIndex = thread.messages.findIndex(Ref.hasObjectId(id));
-    if (messageIndex !== -1) {
-      thread.messages.splice(messageIndex, 1);
-    }
-  };
-
   return (
     <Thread
       current={current}
-      id={fullyQualifiedId(thread)}
+      id={id}
       classNames='bs-full grid-rows-[1fr_min-content_min-content] overflow-hidden transition-[padding-block-end] [[data-sidebar-inline-start-state=open]_&]:lg:pbe-0'
     >
       <ScrollArea.Root classNames='col-span-2'>
         <ScrollArea.Viewport classNames='overflow-anchored after:overflow-anchor after:block after:bs-px after:-mbs-px [&>div]:min-bs-full [&>div]:!grid [&>div]:grid-rows-[1fr_0]'>
           <div role='none' className={mx(threadLayout, 'place-self-end')}>
-            {RefArray.targets(thread.messages ?? []).map((message) => (
-              <MessageContainer key={message.id} message={message} members={members} onDelete={handleDelete} />
+            {(queue?.items ?? []).map((message) => (
+              <MessageContainer key={message.id} message={message} members={members} />
             ))}
           </div>
           {/* NOTE(thure): This can’t also be the `overflow-anchor` because `ScrollArea` injects an interceding node that contains this necessary ref’d element. */}
diff --git a/packages/plugins/plugin-thread/src/components/CommentContainer.tsx b/packages/plugins/plugin-thread/src/components/CommentContainer.tsx
index 284df8dbfd9..a3957d0df8a 100644
--- a/packages/plugins/plugin-thread/src/components/CommentContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/CommentContainer.tsx
@@ -180,7 +180,13 @@ export const CommentContainer = ({
       </div>
       {/** TODO(dmaretskyi): How's `thread.messages` undefined? */}
       {RefArray.targets(thread.messages?.filter(isNonNullable) ?? []).map((message) => (
-        <MessageContainer key={message.id} message={message} members={members} onDelete={handleMessageDelete} />
+        <MessageContainer
+          key={message.id}
+          editable
+          message={message}
+          members={members}
+          onDelete={handleMessageDelete}
+        />
       ))}
       {/*
         TODO(wittjosiah): Can't autofocus this generally.
diff --git a/packages/plugins/plugin-thread/src/components/MessageContainer.tsx b/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
index d0c4c3dd664..dfd9688bb51 100644
--- a/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
@@ -34,10 +34,12 @@ const messageControlClassNames = ['!p-1 !min-bs-0 transition-opacity', hoverable
 export const MessageContainer = ({
   message,
   members,
+  editable = false,
   onDelete,
 }: {
   message: MessageType;
   members: SpaceMember[];
+  editable?: boolean;
   onDelete?: (id: string) => void;
 }) => {
   const senderIdentity = members.find(
@@ -59,7 +61,7 @@ export const MessageContainer = ({
     <MessageRoot {...messageMetadata} classNames={[hoverableControls, hoverableFocusedWithinControls]}>
       <MessageHeading authorName={messageMetadata.authorName} timestamp={messageMetadata.timestamp}>
         <ButtonGroup classNames='mie-1'>
-          {userIsAuthor && (
+          {userIsAuthor && editable && (
             <Tooltip.Root>
               <Tooltip.Trigger asChild>
                 <Button
diff --git a/packages/plugins/plugin-thread/src/components/testing.ts b/packages/plugins/plugin-thread/src/components/testing.ts
index da869b934ee..f3f74915b6c 100644
--- a/packages/plugins/plugin-thread/src/components/testing.ts
+++ b/packages/plugins/plugin-thread/src/components/testing.ts
@@ -2,7 +2,6 @@
 // Copyright 2024 DXOS.org
 //
 
-import { Expando } from '@dxos/echo-schema';
 import { PublicKey } from '@dxos/keys';
 import { create, makeRef } from '@dxos/live-object';
 import { ThreadType } from '@dxos/plugin-space/types';
@@ -10,31 +9,6 @@ import { faker } from '@dxos/random';
 import { type Identity } from '@dxos/react-client/halo';
 import { MessageType } from '@dxos/schema';
 
-export const createChatThread = (identity: Identity) => {
-  return create(ThreadType, {
-    messages: Array.from({ length: 8 }).map(() =>
-      makeRef(
-        create(MessageType, {
-          sender: {
-            identityKey: faker.datatype.boolean() ? identity.identityKey.toHex() : PublicKey.random().toHex(),
-          },
-          created: faker.date.recent().toISOString(),
-          blocks: [
-            { type: 'text' as const, text: faker.lorem.sentences(3) },
-            ...faker.helpers.multiple(
-              () => {
-                const reference = makeRef(create(Expando, { name: faker.lorem.sentence() })) as any; // TODO(dmaretskyi): Fix types in schema.
-                return { type: 'reference' as const, reference };
-              },
-              { count: { min: 0, max: 3 } },
-            ),
-          ],
-        }),
-      ),
-    ),
-  });
-};
-
 export const createCommentThread = (identity: Identity) => {
   return create(ThreadType, {
     messages: faker.helpers.multiple(
diff --git a/packages/plugins/plugin-thread/src/types.ts b/packages/plugins/plugin-thread/src/types.ts
index cb5c5570a55..372d4b3bb4a 100644
--- a/packages/plugins/plugin-thread/src/types.ts
+++ b/packages/plugins/plugin-thread/src/types.ts
@@ -12,14 +12,25 @@ import { THREAD_PLUGIN } from './meta';
 export namespace ThreadAction {
   const THREAD_ACTION = `${THREAD_PLUGIN}/action`;
 
+  export class CreateChannel extends S.TaggedClass<CreateChannel>()(`${THREAD_ACTION}/create-channel`, {
+    input: S.Struct({
+      // TODO(wittjosiah): Should be SpaceId.
+      spaceId: S.String,
+      name: S.optional(S.String),
+    }),
+    output: S.Struct({
+      object: ChannelType,
+    }),
+  }) {}
+
   export class Create extends S.TaggedClass<Create>()(`${THREAD_ACTION}/create`, {
     input: S.Struct({
       name: S.optional(S.String),
-      cursor: S.optional(S.String),
-      subject: S.optional(EchoObjectSchema),
+      cursor: S.String,
+      subject: EchoObjectSchema,
     }),
     output: S.Struct({
-      object: S.Union(ChannelType, ThreadType),
+      object: ThreadType,
     }),
   }) {}
 
diff --git a/packages/plugins/plugin-thread/tsconfig.json b/packages/plugins/plugin-thread/tsconfig.json
index ab0814fe536..17c6e5f028c 100644
--- a/packages/plugins/plugin-thread/tsconfig.json
+++ b/packages/plugins/plugin-thread/tsconfig.json
@@ -26,6 +26,9 @@
     {
       "path": "../../common/invariant"
     },
+    {
+      "path": "../../common/keys"
+    },
     {
       "path": "../../common/local-storage"
     },
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f2fbea9c14d..749af2806c7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -52,7 +52,7 @@ importers:
     devDependencies:
       '@cloudflare/vitest-pool-workers':
         specifier: ^0.5.17
-        version: 0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1)
+        version: 0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@cloudflare/workers-types':
         specifier: ^4.20241018.0
         version: 4.20241022.0
@@ -106,7 +106,7 @@ importers:
         version: link:packages/common/typings
       '@effect/vitest':
         specifier: ~0.17.3
-        version: 0.17.8(effect@3.13.3)(vitest@3.1.1)
+        version: 0.17.8(effect@3.13.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@mdx-js/react':
         specifier: ^2.1.3
         version: 2.1.5(react@18.2.0)
@@ -127,13 +127,13 @@ importers:
         version: 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))
       '@nx/playwright':
         specifier: 19.7.2
-        version: 19.7.2(@babel/traverse@7.25.9)(@playwright/test@1.46.1)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.23.1)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)
+        version: 19.7.2(@babel/traverse@7.25.9)(@playwright/test@1.46.1)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.23.1)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@nx/storybook':
         specifier: 19.7.2
         version: 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)
       '@nx/vite':
         specifier: 19.7.2
-        version: 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)
+        version: 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@nx/web':
         specifier: 19.7.2
         version: 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)
@@ -229,7 +229,7 @@ importers:
         version: 3.1.1(playwright@1.46.1)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)(webdriverio@8.33.1(encoding@0.1.13)(typescript@5.7.3))
       '@vitest/coverage-v8':
         specifier: ^3.1.1
-        version: 3.1.1(@vitest/browser@3.1.1)(vitest@3.1.1)
+        version: 3.1.1(@vitest/browser@3.1.1(playwright@1.46.1)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)(webdriverio@8.33.1(encoding@0.1.13)(typescript@5.7.3)))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@vitest/expect':
         specifier: ^3.1.1
         version: 3.1.1
@@ -4825,7 +4825,7 @@ importers:
     devDependencies:
       '@cloudflare/vitest-pool-workers':
         specifier: ^0.5.17
-        version: 0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1)
+        version: 0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@cloudflare/workers-types':
         specifier: ^4.20241018.0
         version: 4.20241022.0
@@ -9368,6 +9368,9 @@ importers:
       '@dxos/invariant':
         specifier: workspace:*
         version: link:../../common/invariant
+      '@dxos/keys':
+        specifier: workspace:*
+        version: link:../../common/keys
       '@dxos/live-object':
         specifier: workspace:*
         version: link:../../core/echo/live-object
@@ -37575,7 +37578,7 @@ snapshots:
     dependencies:
       mime: 3.0.0
 
-  '@cloudflare/vitest-pool-workers@0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1)':
+  '@cloudflare/vitest-pool-workers@0.5.17(@cloudflare/workers-types@4.20241022.0)(@vitest/runner@3.1.1)(@vitest/snapshot@3.1.1)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
       '@vitest/runner': 3.1.1
       '@vitest/snapshot': 3.1.1
@@ -38057,7 +38060,7 @@ snapshots:
       effect: 3.13.3
       fast-check: 3.23.1
 
-  '@effect/vitest@0.17.8(effect@3.13.3)(vitest@3.1.1)':
+  '@effect/vitest@0.17.8(effect@3.13.3)(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
       effect: 3.13.3
       vitest: 3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2)
@@ -39787,9 +39790,9 @@ snapshots:
       - '@swc/core'
       - debug
 
-  '@nrwl/vite@19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)':
+  '@nrwl/vite@19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
-      '@nx/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)
+      '@nx/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
     transitivePeerDependencies:
       - '@babel/traverse'
       - '@swc-node/register'
@@ -40097,12 +40100,12 @@ snapshots:
   '@nx/nx-win32-x64-msvc@19.7.2':
     optional: true
 
-  '@nx/playwright@19.7.2(@babel/traverse@7.25.9)(@playwright/test@1.46.1)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.23.1)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)':
+  '@nx/playwright@19.7.2(@babel/traverse@7.25.9)(@playwright/test@1.46.1)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(esbuild@0.23.1)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
       '@nx/devkit': 19.7.2(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))
       '@nx/eslint': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(@zkochan/js-yaml@0.0.7)(eslint@8.57.0)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))
       '@nx/js': 19.7.2(patch_hash=mavp33mumclr54vvfyyf557nvq)(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)
-      '@nx/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)
+      '@nx/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@nx/webpack': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(esbuild@0.23.1)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.7.3)
       '@phenomnomnominal/tsquery': 5.0.1(typescript@5.7.3)
       minimatch: 9.0.3
@@ -40169,9 +40172,9 @@ snapshots:
       - typescript
       - verdaccio
 
-  '@nx/vite@19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)':
+  '@nx/vite@19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
-      '@nrwl/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)
+      '@nrwl/vite': 19.7.2(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))
       '@nx/devkit': 19.7.2(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))
       '@nx/js': 19.7.2(patch_hash=mavp33mumclr54vvfyyf557nvq)(@babel/traverse@7.25.9)(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1))(@types/node@22.10.2)(nx@19.7.2(@swc-node/register@1.9.1(@swc/core@1.5.7(@swc/helpers@0.5.1))(@swc/types@0.1.5)(typescript@5.7.3))(@swc/core@1.5.7(@swc/helpers@0.5.1)))(typescript@5.7.3)
       '@phenomnomnominal/tsquery': 5.0.1(typescript@5.7.3)
@@ -45603,7 +45606,7 @@ snapshots:
       - utf-8-validate
       - vite
 
-  '@vitest/coverage-v8@3.1.1(@vitest/browser@3.1.1)(vitest@3.1.1)':
+  '@vitest/coverage-v8@3.1.1(@vitest/browser@3.1.1(playwright@1.46.1)(vite@5.4.7(patch_hash=i2cyw7vpoo7fbe7qgy5pwsznyq)(@types/node@22.10.2)(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))(vitest@3.1.1)(webdriverio@8.33.1(encoding@0.1.13)(typescript@5.7.3)))(vitest@3.1.1(@types/debug@4.1.12)(@types/node@22.10.2)(@vitest/browser@3.1.1)(@vitest/ui@3.1.1)(happy-dom@13.3.4)(jsdom@24.0.0(canvas@2.11.2(encoding@0.1.13)))(less@4.1.3)(sass@1.70.0)(stylus@0.59.0)(terser@5.19.2))':
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 1.0.2


Reviews:
user: login: thure
body: fantastic ✨
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T23:39:08Z
user: login: wittjosiah
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-03T23:49:41Z

Review comments:
diff_hunk: @@ -189,7 +191,7 @@ const SplitFrame = ({ children }: PropsWithChildren<{}>) => {
   return (
     <div
       role='none'
-      className={mx('grid grid-cols-[1fr_1fr] absolute inset-0', railGridHorizontal, mainIntrinsicSize)}
+      className={mx('grid grid-cols-[1fr_1fr] absolute inset-0 min-bs-0', railGridHorizontal, mainIntrinsicSize)}
path: packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx
user: login: thure
body: Did this not do anything? We can remove it if it had no effect, but it'll do no harm to leave in-place.
created_at: 2025-04-03T23:38:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -189,7 +191,7 @@ const SplitFrame = ({ children }: PropsWithChildren<{}>) => {
   return (
     <div
       role='none'
-      className={mx('grid grid-cols-[1fr_1fr] absolute inset-0', railGridHorizontal, mainIntrinsicSize)}
+      className={mx('grid grid-cols-[1fr_1fr] absolute inset-0 min-bs-0', railGridHorizontal, mainIntrinsicSize)}
path: packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx
user: login: wittjosiah
body: oh right, yeah I forgot about this, will revert
created_at: 2025-04-03T23:49:41Z
author_association: MEMBER

PR comments:
user: login: semanticdiff-com[bot]
created_at: 2025-04-03T23:19:53Z
body: Review changes with &nbsp;<a href="https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes"><picture><source media="(prefers-color-scheme: dark)" srcset="https://app.semanticdiff.com/images/github_button.svg?theme=dark&smaller=40"><source media="(prefers-color-scheme: light)" srcset="https://app.semanticdiff.com/images/github_button.svg?theme=light&smaller=40"><img src="https://app.semanticdiff.com/images/github_button.svg?theme=light&smaller=40" align="center" alt="SemanticDiff"></picture></a>
<details>
<summary>Changed Files</summary>
<br/>

| File | Status |
| :--- | :---   |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/CommentContainer.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/CommentContainer\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/CommentContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=88">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/CommentContainer.tsx) &nbsp;88% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/react-surface.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/capabilities/react\-surface\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/react-surface.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=88">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/react-surface.tsx) &nbsp;88% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-deck/src/components/DeckLayout/Plank\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=72">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-deck/src/components/DeckLayout/Plank.tsx) &nbsp;72% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/typescript.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/capabilities/intent\-resolver\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=72">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/capabilities/intent-resolver.ts) &nbsp;72% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChatContainer.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/ChatContainer\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChatContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=51">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChatContainer.tsx) &nbsp;51% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/storybook.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/Chat.stories.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/Chat\.stories\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/Chat.stories.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=49">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/Chat.stories.tsx) &nbsp;49% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChannelContainer.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/ChannelContainer\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChannelContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=48">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/ChannelContainer.tsx) &nbsp;48% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/test-ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.node.test.ts)&nbsp;&nbsp;[packages/apps/composer\-app/src/migrations/migrations\.node\.test\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.node.test.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=38">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.node.test.ts) &nbsp;38% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/typescript.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-space/src/types/thread.ts)&nbsp;&nbsp;[packages/plugins/plugin\-space/src/types/thread\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-space/src/types/thread.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=32">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-space/src/types/thread.ts) &nbsp;32% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/typescript.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/types.ts)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/types\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/types.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=4">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/types.ts) &nbsp;4% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/MessageContainer.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/MessageContainer\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/MessageContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=2">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/MessageContainer.tsx) &nbsp;2% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/typescript.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.ts)&nbsp;&nbsp;[packages/apps/composer\-app/src/migrations/migrations\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=1">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/apps/composer-app/src/migrations/migrations.ts) &nbsp;1% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/json.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/package.json)&nbsp;&nbsp;[packages/plugins/plugin\-thread/package\.json](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/package.json) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/package.json) &nbsp;0% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/react_ts.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/ThreadPlugin.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/ThreadPlugin\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/ThreadPlugin.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/ThreadPlugin.tsx) &nbsp;0% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/typescript.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/testing.ts)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/testing\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/testing.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/src/components/testing.ts) &nbsp;0% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/json.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/tsconfig.json)&nbsp;&nbsp;[packages/plugins/plugin\-thread/tsconfig\.json](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/tsconfig.json) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#packages/plugins/plugin-thread/tsconfig.json) &nbsp;0% smaller |
| [<img src="https://app.semanticdiff.com/static/file-icons/pnpm.svg" width="20" height="20" align="center">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#pnpm-lock.yaml)&nbsp;&nbsp;[pnpm\-lock\.yaml](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#pnpm-lock.yaml) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8898/changes#pnpm-lock.yaml) &nbsp;0% smaller |

</details>
author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-04-03T23:53:16Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67ef3c8110b1822ec0bceac4?utm_source=pull-request&utm_medium=comment) for commit ce8b09d1a703e386747eebd15f84257afeede5de.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected --base=5166fc0362eba0f0a3c14ae37d3a...` | ✅ Succeeded | 32s | [View ↗](https://cloud.nx.app/runs/gusOiLm2KV?utm_source=pull-request&utm_medium=comment) |
| `nx affected --base=5166fc0362eba0f0a3c14ae37d3a...` | ✅ Succeeded | 2m 10s | [View ↗](https://cloud.nx.app/runs/x49tzuBMJ6?utm_source=pull-request&utm_medium=comment) |
| `nx affected --base=5166fc0362eba0f0a3c14ae37d3a...` | ✅ Succeeded | 12s | [View ↗](https://cloud.nx.app/runs/kqzXc5I8ty?utm_source=pull-request&utm_medium=comment) |

---

☁️ [Nx Cloud](https://cloud.nx.app?utm_source=pull-request&utm_medium=comment) last updated this comment at `2025-04-04 02:00:28` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE