Repository: dxos/dxos
Pull request #8916: feat: connect gmail sync to mailbox
Author: login: wittjosiah
Created: 2025-04-07T22:40:34Z | Merged: 2025-04-08T17:56:58Z



Diff:
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 8dba87888ff..e22531db040 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -64,6 +64,11 @@
       },
       "group": {
         "kind": "test"
+      },
+      "options": {
+        "env": {
+          "DX_LABS": "true"
+        }
       }
     }
   ]
diff --git a/packages/common/log/src/config.ts b/packages/common/log/src/config.ts
index f6656256bad..84b9edc7d33 100644
--- a/packages/common/log/src/config.ts
+++ b/packages/common/log/src/config.ts
@@ -21,6 +21,7 @@ export enum LogLevel {
 export const levels: { [index: string]: LogLevel } = {
   trace: LogLevel.TRACE,
   debug: LogLevel.DEBUG,
+  verbose: LogLevel.VERBOSE,
   info: LogLevel.INFO,
   warn: LogLevel.WARN,
   error: LogLevel.ERROR,
diff --git a/packages/core/functions/src/types/schema.ts b/packages/core/functions/src/types/schema.ts
index 5ddfa384852..6944bcbd5d7 100644
--- a/packages/core/functions/src/types/schema.ts
+++ b/packages/core/functions/src/types/schema.ts
@@ -25,7 +25,6 @@ export type ScriptType = S.Schema.Type<typeof ScriptType>;
 /**
  * Function deployment.
  */
-// TODO(burdon): Move to core/functions.
 export class FunctionType extends TypedObject({
   typename: 'dxos.org/type/Function',
   version: '0.1.0',
diff --git a/packages/core/functions/src/types/types.ts b/packages/core/functions/src/types/types.ts
index 2947e0ab522..a9e5042aa7b 100644
--- a/packages/core/functions/src/types/types.ts
+++ b/packages/core/functions/src/types/types.ts
@@ -119,7 +119,8 @@ export const FunctionTriggerSchema = S.Struct({
   // TODO(burdon): Flatten entire schema.
   spec: S.optional(TriggerSchema),
 
-  // TODO(burdon): Get meta from function.
+  // TODO(burdon): Get schema as partial from function.
+  // TODO(wittjosiah): Rename to payload.
   // The `meta` property is merged into the event data passed to the function.
   meta: S.optional(S.mutable(S.Record({ key: S.String, value: S.Any }))),
 });
diff --git a/packages/plugins/experimental/plugin-assistant/src/hooks/email.ts b/packages/plugins/experimental/plugin-assistant/src/hooks/email.ts
deleted file mode 100644
index 3e27c573ea1..00000000000
--- a/packages/plugins/experimental/plugin-assistant/src/hooks/email.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-//
-// Copyright 2023 DXOS.org
-//
-
-import { findObjectWithForeignKey } from '@dxos/echo-db';
-import { foreignKey } from '@dxos/echo-schema';
-import { log } from '@dxos/log';
-import { MailboxType } from '@dxos/plugin-inbox/types';
-import { type Space, Filter, create, makeRef } from '@dxos/react-client/echo';
-import { MessageType } from '@dxos/schema';
-
-export const SOURCE_ID = 'hub.dxos.network/api/mailbox';
-
-export const handleEmail = async (space: Space, data: any) => {
-  const { messages } = data;
-
-  // Create mailbox if doesn't exist.
-  const { objects: mailboxes } = await space.db.query(Filter.schema(MailboxType)).run();
-  const mailbox = mailboxes[0] ?? space.db.add(create(MailboxType, { messages: [] }));
-
-  log.info('messages', { count: messages.length, existingMailbox: mailboxes.length > 0 });
-
-  const { objects } = await space.db.query(Filter.schema(MessageType)).run();
-  for (const message of messages) {
-    let object = findObjectWithForeignKey(objects, { source: SOURCE_ID, id: String(message.id) });
-    if (!object) {
-      object = space.db.add(
-        create(
-          MessageType,
-          {
-            sender: { email: message.from },
-            created: new Date(message.created).toISOString(),
-            blocks: [{ type: 'text', text: message.body }],
-            properties: {
-              subject: message.subject,
-              to: [{ email: message.to }],
-            },
-          },
-          {
-            keys: [foreignKey(SOURCE_ID, String(message.id))],
-          },
-        ),
-      );
-      mailbox.messages?.push(makeRef(object));
-    }
-  }
-
-  return 200;
-};
diff --git a/packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx b/packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx
index 8bb6a8ce4d7..7e30c06bdd1 100644
--- a/packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx
+++ b/packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx
@@ -47,7 +47,7 @@ export const AutomationPanel = ({ space, object }: AutomationPanelProps) => {
   };
 
   const handleAdd = () => {
-    setTrigger(create(FunctionTriggerSchema, { meta: {} }));
+    setTrigger(create(FunctionTriggerSchema, {}));
     setSelected(undefined);
   };
 
diff --git a/packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx b/packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
index 7d61ecefd66..df0d1d43a2b 100644
--- a/packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
+++ b/packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
@@ -2,7 +2,7 @@
 // Copyright 2024 DXOS.org
 //
 
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 
 import { ComputeGraph } from '@dxos/conductor';
 import {
@@ -15,7 +15,7 @@ import {
 } from '@dxos/functions/types';
 import { Filter, useQuery, type Space } from '@dxos/react-client/echo';
 import { IconButton, Input, useTranslation } from '@dxos/react-ui';
-import { Form, SelectInput, TextInput } from '@dxos/react-ui-form';
+import { type CustomInputMap, Form, type InputProps, SelectInput, TextInput, useInputProps } from '@dxos/react-ui-form';
 
 import { AUTOMATION_PLUGIN } from '../../meta';
 
@@ -26,6 +26,30 @@ export type TriggerEditorProps = {
   onCancel?: () => void;
 };
 
+const PayloadInput = (props: InputProps & { property: string }) => {
+  const { t } = useTranslation(AUTOMATION_PLUGIN);
+  // TODO(dmaretskyi): Prop name (`meta`) should be passed in.
+  const inputProps = useInputProps(['meta', props.property]);
+  return (
+    <div role='none' className='flex items-center mt-2 gap-1'>
+      <div role='none' className='flex-1'>
+        <TextInput {...inputProps} type='string' label={props.property} />
+      </div>
+      <IconButton
+        icon='ph--trash--regular'
+        iconOnly
+        classNames={'mt-6'}
+        label={t('trigger meta remove')}
+        onClick={() => {
+          const newValues: any = { ...props.getValue() };
+          delete newValues[props.property];
+          props.onValueChange('object', newValues);
+        }}
+      />
+    </div>
+  );
+};
+
 export const TriggerEditor = ({ space, trigger, onSave, onCancel }: TriggerEditorProps) => {
   const { t } = useTranslation(AUTOMATION_PLUGIN);
 
@@ -37,90 +61,75 @@ export const TriggerEditor = ({ space, trigger, onSave, onCancel }: TriggerEdito
     onSave?.(values);
   };
 
+  const Custom = useMemo(
+    (): CustomInputMap => ({
+      ['function' satisfies keyof FunctionTriggerType]: (props) => (
+        <SelectInput
+          {...props}
+          options={getWorkflowOptions(workflows).concat(getFunctionOptions(scripts, functions))}
+        />
+      ),
+      ['spec.type' as const]: (props) => (
+        <SelectInput
+          {...props}
+          options={Object.values(TriggerKind).map((kind) => ({
+            value: kind,
+            label: t(`trigger type ${kind}`),
+          }))}
+        />
+      ),
+      // TODO(wittjosiah): Form should be able to handle arbitrary records by default.
+      ['meta' as const]: (props) => {
+        const payload = props.getValue() ?? {};
+        useEffect(() => props.onValueChange('object', { ...payload }), []);
+        const [newPayloadFieldName, setNewPayloadFieldName] = useState('');
+
+        const handleAddPayload = useCallback(() => {
+          if (newPayloadFieldName.length) {
+            const payload = props.getValue() ?? {};
+            const payloadWithNewProp = { ...payload, [newPayloadFieldName]: '' };
+            setNewPayloadFieldName('');
+            props.onValueChange('object', payloadWithNewProp);
+          }
+        }, [newPayloadFieldName, props.getValue, props.onValueChange]);
+
+        return (
+          <>
+            <div>{/* TODO(wittjosiah): props.label */ 'Payload'}</div>
+            {[...Object.keys(payload)].map((key) => (
+              <PayloadInput key={key} property={key} {...props} />
+            ))}
+            <div role='none' className='flex items-center mt-2 gap-1 plb-1'>
+              <div role='none' className='flex-1'>
+                <Input.Root>
+                  <Input.TextInput
+                    placeholder={t('trigger payload prop name placeholder')}
+                    value={newPayloadFieldName}
+                    onChange={(event) => setNewPayloadFieldName(event.target.value)}
+                  />
+                </Input.Root>
+              </div>
+              <IconButton
+                icon='ph--plus--regular'
+                iconOnly
+                label={t('trigger payload add')}
+                onClick={handleAddPayload}
+              />
+            </div>
+          </>
+        );
+      },
+    }),
+    [workflows, scripts, functions, t],
+  );
+
   return (
     <Form<FunctionTriggerType>
       schema={FunctionTriggerSchema}
       values={trigger}
       onSave={handleSave}
       onCancel={onCancel}
-      Custom={{
-        ['function' satisfies keyof FunctionTriggerType]: (props) => (
-          <SelectInput
-            {...props}
-            options={getWorkflowOptions(workflows).concat(getFunctionOptions(scripts, functions))}
-          />
-        ),
-        ['spec.type' as const]: (props) => (
-          <SelectInput
-            {...props}
-            options={Object.values(TriggerKind).map((kind) => ({
-              value: kind,
-              label: t(`trigger type ${kind}`),
-            }))}
-          />
-        ),
-        ['meta' as const]: (props) => {
-          const meta = props.getValue()!;
-          useEffect(() => props.onValueChange('object', { ...meta }), []);
-          const [newMetaFieldName, setNewMetaFieldName] = useState('');
-
-          return (
-            <>
-              <div>{props.label}</div>
-              {[...Object.keys(meta)].map((key) => {
-                const compositeKey: any = `meta.${key}`;
-                return (
-                  <div key={compositeKey} role='none' className='flex items-center mt-2 gap-1'>
-                    <div role='none' className='flex-1'>
-                      <TextInput
-                        {...props}
-                        getValue={() => (props.getValue() as any)[key]}
-                        type={'string'}
-                        label={key}
-                      />
-                    </div>
-                    <IconButton
-                      icon='ph--trash--regular'
-                      iconOnly
-                      classNames={'mt-6'}
-                      label={t('trigger meta remove')}
-                      onClick={() => {
-                        const newValues: any = { ...props.getValue() };
-                        delete newValues[key];
-                        props.onValueChange('object', newValues);
-                      }}
-                    />
-                  </div>
-                );
-              })}
-              <div role='none' className='flex items-center mt-2 gap-1 plb-1'>
-                <div role='none' className='flex-1'>
-                  <Input.Root>
-                    <Input.TextInput
-                      placeholder={t('trigger meta prop name placeholder')}
-                      value={newMetaFieldName}
-                      onChange={(event) => setNewMetaFieldName(event.target.value)}
-                    />
-                  </Input.Root>
-                </div>
-                <IconButton
-                  icon='ph--plus--regular'
-                  iconOnly
-                  label={t('trigger meta add')}
-                  onClick={() => {
-                    if (newMetaFieldName.length) {
-                      const meta = props.getValue() ?? {};
-                      const metaWithNewProp = { ...meta, [newMetaFieldName]: '' };
-                      setNewMetaFieldName('');
-                      props.onValueChange('object', metaWithNewProp);
-                    }
-                  }}
-                />
-              </div>
-            </>
-          );
-        },
-      }}
+      Custom={Custom}
     />
   );
 };
diff --git a/packages/plugins/experimental/plugin-automation/src/translations.ts b/packages/plugins/experimental/plugin-automation/src/translations.ts
index b5c8ecd8fc2..f91665717a0 100644
--- a/packages/plugins/experimental/plugin-automation/src/translations.ts
+++ b/packages/plugins/experimental/plugin-automation/src/translations.ts
@@ -22,9 +22,9 @@ export default [
         'trigger copy url': 'Copy URL',
         'trigger copy email': 'Copy Email',
 
-        'trigger meta add': 'Add',
-        'trigger meta remove': 'Remove',
-        'trigger meta prop name placeholder': 'New meta property name',
+        'trigger payload add': 'Add',
+        'trigger payload remove': 'Remove',
+        'trigger payload prop name placeholder': 'New payload property name',
       },
     },
   },
diff --git a/packages/plugins/experimental/plugin-inbox/package.json b/packages/plugins/experimental/plugin-inbox/package.json
index a78d08dc3e7..1b81eabe86e 100644
--- a/packages/plugins/experimental/plugin-inbox/package.json
+++ b/packages/plugins/experimental/plugin-inbox/package.json
@@ -38,6 +38,7 @@
     "@dxos/async": "workspace:*",
     "@dxos/display-name": "workspace:*",
     "@dxos/echo-schema": "workspace:*",
+    "@dxos/keys": "workspace:*",
     "@dxos/live-object": "workspace:*",
     "@dxos/log": "workspace:*",
     "@dxos/plugin-client": "workspace:*",
diff --git a/packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx b/packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx
index 921ae39b98e..26b30ef6a56 100644
--- a/packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx
+++ b/packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx
@@ -59,7 +59,7 @@ export const InboxPlugin = () =>
           SpaceCapabilities.ObjectForm,
           defineObjectForm({
             objectSchema: MailboxType,
-            getIntent: () => createIntent(InboxAction.CreateMailbox),
+            getIntent: (_, options) => createIntent(InboxAction.CreateMailbox, { spaceId: options.space.id }),
           }),
         ),
         contributes(
diff --git a/packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts b/packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts
index 33e41c12ac0..cff614f350d 100644
--- a/packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts
+++ b/packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts
@@ -3,7 +3,9 @@
 //
 
 import { contributes, Capabilities, createResolver } from '@dxos/app-framework';
-import { create } from '@dxos/live-object';
+import { ObjectId } from '@dxos/echo-schema';
+import { QueueSubspaceTags, DXN } from '@dxos/keys';
+import { create, refFromDXN } from '@dxos/live-object';
 
 import { CalendarType, ContactsType, InboxAction, MailboxType } from '../types';
 
@@ -11,8 +13,13 @@ export default () =>
   contributes(Capabilities.IntentResolver, [
     createResolver({
       intent: InboxAction.CreateMailbox,
-      resolve: () => ({
-        data: { object: create(MailboxType, { messages: [] }) },
+      resolve: ({ spaceId, name }) => ({
+        data: {
+          object: create(MailboxType, {
+            name,
+            queue: refFromDXN(new DXN(DXN.kind.QUEUE, [QueueSubspaceTags.DATA, spaceId, ObjectId.random()])),
+          }),
+        },
       }),
     }),
     createResolver({
diff --git a/packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx b/packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx
index 9897607a5cb..8db2ffd6a60 100644
--- a/packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx
+++ b/packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx
@@ -4,8 +4,8 @@
 
 import React, { useEffect, useRef, useState } from 'react';
 
+import { useQueue } from '@dxos/react-client/echo';
 import { type MessageType } from '@dxos/schema';
-import { isNonNullable } from '@dxos/util';
 
 import { type ActionType, MessageList } from './MessageList';
 import { type MailboxType, MessageState } from '../../types';
@@ -37,14 +37,15 @@ export type MailboxProps = { mailbox: MailboxType; options?: MailboxOptions };
 
 export const Mailbox = ({ mailbox, options = {} }: MailboxProps) => {
   const [selected, setSelected] = useState<string>();
+  const queue = useQueue<MessageType>(mailbox.queue.dxn, { pollInterval: 1_000 });
   const tRef = useRef<ReturnType<typeof setTimeout>>();
   useEffect(() => {
     clearTimeout(tRef.current);
     if (selected) {
       tRef.current = setTimeout(() => {
-        const object = mailbox?.messages?.find((message) => message.target?.id === selected);
-        if (object?.target?.properties) {
-          setMessageProperty(object.target, 'read', true);
+        const object = queue?.items?.find((message) => message.id === selected);
+        if (object?.properties) {
+          setMessageProperty(object, 'read', true);
         }
       }, options?.readTimout ?? DEFAULT_READ_TIMEOUT);
     }
@@ -52,9 +53,7 @@ export const Mailbox = ({ mailbox, options = {} }: MailboxProps) => {
     return () => clearTimeout(tRef.current);
   }, [selected]);
 
-  const messages = [...(mailbox.messages ?? [])]
-    .map((message) => message.target)
-    .filter(isNonNullable) // TODO(burdon): [SDK] Why is filter needed?
+  const messages = [...(queue?.items ?? [])]
     .filter(
       (message) =>
         message.properties?.state !== MessageState.ARCHIVED && message.properties?.state !== MessageState.DELETED,
diff --git a/packages/plugins/experimental/plugin-inbox/src/testing/testing.ts b/packages/plugins/experimental/plugin-inbox/src/testing/testing.ts
index 96dd840cff2..c39a66ff7e0 100644
--- a/packages/plugins/experimental/plugin-inbox/src/testing/testing.ts
+++ b/packages/plugins/experimental/plugin-inbox/src/testing/testing.ts
@@ -2,10 +2,10 @@
 // Copyright 2023 DXOS.org
 //
 
+import { IdentityDid } from '@dxos/keys';
 import { create, makeRef } from '@dxos/live-object';
 import { ThreadType } from '@dxos/plugin-space/types';
 import { faker } from '@dxos/random';
-import { PublicKey } from '@dxos/react-client';
 import { MessageType } from '@dxos/schema';
 
 faker.seed(1);
@@ -20,7 +20,7 @@ export const createInbox = (count = 10) => {
         makeRef(
           create(MessageType, {
             sender: {
-              identityKey: PublicKey.random().toHex(),
+              identityDid: IdentityDid.random(),
               name: faker.person.fullName(),
             },
             created: faker.date.recent().toISOString(),
diff --git a/packages/plugins/experimental/plugin-inbox/src/types/mail.ts b/packages/plugins/experimental/plugin-inbox/src/types/mail.ts
index 2ebb5c8ff0a..8b08a810770 100644
--- a/packages/plugins/experimental/plugin-inbox/src/types/mail.ts
+++ b/packages/plugins/experimental/plugin-inbox/src/types/mail.ts
@@ -2,8 +2,7 @@
 // Copyright 2024 DXOS.org
 //
 
-import { Ref, S, TypedObject } from '@dxos/echo-schema';
-import { MessageType } from '@dxos/schema';
+import { Expando, Ref, S, TypedObject } from '@dxos/echo-schema';
 
 export enum MessageState {
   NONE = 0,
@@ -12,7 +11,7 @@ export enum MessageState {
   SPAM = 3,
 }
 
-export class MailboxType extends TypedObject({ typename: 'dxos.org/type/MailboxType', version: '0.1.0' })({
+export class MailboxType extends TypedObject({ typename: 'dxos.org/type/Mailbox', version: '0.1.0' })({
   name: S.optional(S.String),
-  messages: S.mutable(S.Array(Ref(MessageType))),
+  queue: Ref(Expando),
 }) {}
diff --git a/packages/plugins/experimental/plugin-inbox/src/types/types.ts b/packages/plugins/experimental/plugin-inbox/src/types/types.ts
index 34f75d0a44e..ee42400c256 100644
--- a/packages/plugins/experimental/plugin-inbox/src/types/types.ts
+++ b/packages/plugins/experimental/plugin-inbox/src/types/types.ts
@@ -14,6 +14,7 @@ export namespace InboxAction {
 
   export class CreateMailbox extends S.TaggedClass<CreateMailbox>()(`${INBOX_ACTION}/create-mailbox`, {
     input: S.Struct({
+      spaceId: S.String,
       name: S.optional(S.String),
     }),
     output: S.Struct({
diff --git a/packages/plugins/experimental/plugin-inbox/tsconfig.json b/packages/plugins/experimental/plugin-inbox/tsconfig.json
index 35652f68307..b99fdcd867f 100644
--- a/packages/plugins/experimental/plugin-inbox/tsconfig.json
+++ b/packages/plugins/experimental/plugin-inbox/tsconfig.json
@@ -23,6 +23,9 @@
     {
       "path": "../../../common/display-name"
     },
+    {
+      "path": "../../../common/keys"
+    },
     {
       "path": "../../../common/log"
     },
diff --git a/packages/plugins/experimental/plugin-script/src/templates/gmail.ts b/packages/plugins/experimental/plugin-script/src/templates/gmail.ts
index f5b48f513ac..cadbe8baa20 100644
--- a/packages/plugins/experimental/plugin-script/src/templates/gmail.ts
+++ b/packages/plugins/experimental/plugin-script/src/templates/gmail.ts
@@ -3,7 +3,7 @@
 //
 
 // @ts-ignore
-import { createStatic, defineFunction, DXN, EchoObject, Filter, ObjectId, S } from 'dxos:functions';
+import { createStatic, defineFunction, EchoObject, Filter, ObjectId, S } from 'dxos:functions';
 import {
   HttpClient,
   HttpClientRequest,
@@ -22,7 +22,7 @@ export default defineFunction({
     userId: S.optional(S.String),
     after: S.optional(S.Union(S.Number, S.String)),
     pageSize: S.optional(S.Number),
-    queueId: S.String,
+    mailboxId: S.String,
   }),
 
   outputSchema: S.Struct({
@@ -31,7 +31,7 @@ export default defineFunction({
 
   handler: ({
     event: {
-      data: { queueId, userId = 'me', after = DEFAULT_AFTER, pageSize = 100 },
+      data: { mailboxId, userId = 'me', after = DEFAULT_AFTER, pageSize = 100 },
     },
     context: { space },
   }: any) =>
@@ -61,8 +61,12 @@ export default defineFunction({
       const getMessage = (userId: string, id: string) =>
         makeRequest(`https://gmail.googleapis.com/gmail/v1/users/${userId}/messages/${id}`);
 
+      const mailbox = yield* Effect.tryPromise({
+        try: () => space.db.query({ id: mailboxId }).first(),
+        catch: (e: any) => e,
+      });
       const { objects } = yield* Effect.tryPromise({
-        try: () => space.queues.queryQueue(DXN.parse(queueId)),
+        try: () => space.queues.queryQueue(mailbox.queue.dxn),
         catch: (e: any) => e,
       });
       const newMessages = yield* Ref.make([]);
@@ -120,7 +124,7 @@ export default defineFunction({
           Stream.grouped(10),
           Stream.flatMap((batch: any) =>
             Effect.tryPromise({
-              try: () => space.queues.insertIntoQueue(DXN.parse(queueId), Chunk.toReadonlyArray(batch)),
+              try: () => space.queues.insertIntoQueue(mailbox.queue.dxn, Chunk.toReadonlyArray(batch)),
               catch: (e: any) => e,
             }),
           ),
diff --git a/packages/plugins/plugin-thread/src/components/ChatContainer.tsx b/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
index feb506c51a6..b3c6a191fa1 100644
--- a/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/ChatContainer.tsx
@@ -85,7 +85,7 @@ export const ChatContainer = ({ space, dxn, context, current, autoFocusTextbox }
 
     queue.append([
       createStatic(MessageType, {
-        sender: { identityKey: identity.identityKey.toHex() },
+        sender: { identityDid: identity.did },
         created: new Date().toISOString(),
         blocks: [{ type: 'text', text: messageRef.current }],
         properties: context ? { context: makeRef(context) } : undefined,
diff --git a/packages/plugins/plugin-thread/src/components/MessageContainer.tsx b/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
index 895c8efa506..595dcc3ec96 100644
--- a/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
+++ b/packages/plugins/plugin-thread/src/components/MessageContainer.tsx
@@ -43,10 +43,12 @@ export const MessageContainer = ({
   onDelete?: (id: string) => void;
 }) => {
   const senderIdentity = members.find(
-    (member) => message.sender.identityKey && PublicKey.equals(member.identity.identityKey, message.sender.identityKey),
+    (member) =>
+      (message.sender.identityDid && member.identity.did === message.sender.identityDid) ||
+      (message.sender.identityKey && PublicKey.equals(member.identity.identityKey, message.sender.identityKey)),
   )?.identity;
   const messageMetadata = getMessageMetadata(message.id, senderIdentity);
-  const userIsAuthor = useIdentity()?.identityKey.toHex() === messageMetadata.authorId;
+  const userIsAuthor = useIdentity()?.did === messageMetadata.authorId;
   const [editing, setEditing] = useState(false);
   const handleDelete = useCallback(() => onDelete?.(message.id), [message, onDelete]);
   const { t } = useTranslation(THREAD_PLUGIN);
diff --git a/packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx b/packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx
index 71d7e6141e7..090c0ba93f1 100644
--- a/packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx
+++ b/packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx
@@ -92,7 +92,7 @@ export const ThreadComplementary = ({ subject }: { subject: any }) => {
       thread.messages.push(
         makeRef(
           create(MessageType, {
-            sender: { identityKey: identity?.identityKey.toHex() },
+            sender: { identityDid: identity?.did },
             created: new Date().toISOString(),
             blocks: [{ type: 'text', text: message }],
             // TODO(wittjosiah): Context based on attention.
diff --git a/packages/plugins/plugin-thread/src/components/testing.ts b/packages/plugins/plugin-thread/src/components/testing.ts
index f3f74915b6c..cfea321facb 100644
--- a/packages/plugins/plugin-thread/src/components/testing.ts
+++ b/packages/plugins/plugin-thread/src/components/testing.ts
@@ -2,7 +2,7 @@
 // Copyright 2024 DXOS.org
 //
 
-import { PublicKey } from '@dxos/keys';
+import { IdentityDid } from '@dxos/keys';
 import { create, makeRef } from '@dxos/live-object';
 import { ThreadType } from '@dxos/plugin-space/types';
 import { faker } from '@dxos/random';
@@ -16,7 +16,7 @@ export const createCommentThread = (identity: Identity) => {
         makeRef(
           create(MessageType, {
             sender: {
-              identityKey: faker.datatype.boolean() ? identity.identityKey.toHex() : PublicKey.random().toHex(),
+              identityDid: faker.datatype.boolean() ? identity.did : IdentityDid.random(),
             },
             created: new Date().toISOString(),
             blocks: [{ type: 'text', text: faker.lorem.sentences(3) }],
diff --git a/packages/sdk/schema/src/common/actor.ts b/packages/sdk/schema/src/common/actor.ts
index a0c976c120a..5dc54e523b5 100644
--- a/packages/sdk/schema/src/common/actor.ts
+++ b/packages/sdk/schema/src/common/actor.ts
@@ -26,6 +26,8 @@ export const ActorSchema = S.Struct({
   contact: S.optional(Ref(ContactType)),
   // TODO(wittjosiah): Should the below fields just be the contact schema?
   //  i.e. it should either be a reference to an existing contact or an inline contact schema.
+  identityDid: S.optional(S.String),
+  /** @deprecated */
   identityKey: S.optional(S.String),
   // TODO(burdon): Generalize to handle/identifier?
   email: S.optional(S.String),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 55f78acc2be..42ab311ecc3 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
@@ -4828,7 +4828,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
@@ -6050,6 +6050,9 @@ importers:
       '@dxos/echo-schema':
         specifier: workspace:*
         version: link:../../../core/echo/echo-schema
+      '@dxos/keys':
+        specifier: workspace:*
+        version: link:../../../common/keys
       '@dxos/live-object':
         specifier: workspace:*
         version: link:../../../core/echo/live-object
@@ -37606,7 +37609,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
@@ -38088,7 +38091,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)
@@ -39818,9 +39821,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'
@@ -40128,12 +40131,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
@@ -40200,9 +40203,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)
@@ -45634,7 +45637,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: richburdon
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-07T22:57:51Z
user: login: wittjosiah
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-07T23:02:34Z

Review comments:
diff_hunk: @@ -26,6 +26,29 @@ export type TriggerEditorProps = {
   onCancel?: () => void;
 };
 
+const PayloadInput = (props: InputProps & { property: string }) => {
+  const { t } = useTranslation(AUTOMATION_PLUGIN);
+  const inputProps = useInputProps(['payload', props.property]);
+  return (
+    <div role='none' className='flex items-center mt-2 gap-1'>
+      <div role='none' className='flex-1'>
path: packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
user: login: richburdon
body: do you need a wrapping div here?
created_at: 2025-04-07T22:55:51Z
author_association: MEMBER
diff_hunk: @@ -26,6 +26,29 @@ export type TriggerEditorProps = {
   onCancel?: () => void;
 };
 
+const PayloadInput = (props: InputProps & { property: string }) => {
+  const { t } = useTranslation(AUTOMATION_PLUGIN);
+  const inputProps = useInputProps(['payload', props.property]);
+  return (
+    <div role='none' className='flex items-center mt-2 gap-1'>
+      <div role='none' className='flex-1'>
+        <TextInput {...inputProps} type='string' label={props.property} />
+      </div>
+      <IconButton
+        icon='ph--trash--regular'
+        iconOnly
+        classNames={'mt-6'}
+        label={t('trigger meta remove')}
+        onClick={() => {
+          const newValues: any = { ...props.getValue() };
path: packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
user: login: richburdon
body: i prefer to just call this value.
created_at: 2025-04-07T22:56:05Z
author_association: MEMBER
diff_hunk: @@ -37,90 +60,75 @@ export const TriggerEditor = ({ space, trigger, onSave, onCancel }: TriggerEdito
     onSave?.(values);
   };
 
+  const Custom = useMemo(
+    (): CustomInputMap => ({
+      ['function' satisfies keyof FunctionTriggerType]: (props) => (
+        <SelectInput
+          {...props}
+          options={getWorkflowOptions(workflows).concat(getFunctionOptions(scripts, functions))}
+        />
+      ),
+      ['spec.type' as const]: (props) => (
+        <SelectInput
+          {...props}
+          options={Object.values(TriggerKind).map((kind) => ({
+            value: kind,
+            label: t(`trigger type ${kind}`),
+          }))}
+        />
+      ),
+      // TODO(wittjosiah): Form should be able to handle arbitrary records by default.
+      ['meta' as const]: (props) => {
+        const payload = props.getValue() ?? {};
+        useEffect(() => props.onValueChange('object', { ...payload }), []);
+        const [newPayloadFieldName, setNewPayloadFieldName] = useState('');
+
+        const handleAddPayload = useCallback(() => {
+          if (newPayloadFieldName.length) {
+            const payload = props.getValue() ?? {};
+            const payloadWithNewProp = { ...payload, [newPayloadFieldName]: '' };
+            setNewPayloadFieldName('');
+            props.onValueChange('object', payloadWithNewProp);
+          }
+        }, [newPayloadFieldName, props.getValue, props.onValueChange]);
+
+        return (
+          <>
+            <div>{/* TODO(wittjosiah): props.label */ 'Payload'}</div>
+            {[...Object.keys(payload)].map((key) => (
+              <PayloadInput key={key} property={key} {...props} />
+            ))}
+            <div role='none' className='flex items-center mt-2 gap-1 plb-1'>
+              <div role='none' className='flex-1'>
path: packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
user: login: richburdon
body: again, is the div required?
created_at: 2025-04-07T22:56:40Z
author_association: MEMBER
diff_hunk: @@ -26,6 +26,29 @@ export type TriggerEditorProps = {
   onCancel?: () => void;
 };
 
+const PayloadInput = (props: InputProps & { property: string }) => {
+  const { t } = useTranslation(AUTOMATION_PLUGIN);
+  const inputProps = useInputProps(['payload', props.property]);
+  return (
+    <div role='none' className='flex items-center mt-2 gap-1'>
+      <div role='none' className='flex-1'>
path: packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx
user: login: wittjosiah
body: I just extracted this from below. It's a mess and the Form should really be handling this type of input directly, which is what the todo below is about.
created_at: 2025-04-07T23:02:34Z
author_association: MEMBER

PR comments:
user: login: semanticdiff-com[bot]
created_at: 2025-04-07T22:40:37Z
body: Review changes with &nbsp;<a href="https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes"><picture><source media="(prefers-color-scheme: dark)" srcset="https://app.semanticdiff.com/images/github_button.svg?theme=dark&smaller=47"><source media="(prefers-color-scheme: light)" srcset="https://app.semanticdiff.com/images/github_button.svg?theme=light&smaller=47"><img src="https://app.semanticdiff.com/images/github_button.svg?theme=light&smaller=47" 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/8916/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/8916/changes#packages/plugins/plugin-thread/src/components/MessageContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=70">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/plugin-thread/src/components/MessageContainer.tsx) &nbsp;70% 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/8916/changes#packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-automation/src/components/TriggerEditor/TriggerEditor\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=70">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/components/TriggerEditor/TriggerEditor.tsx) &nbsp;70% 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/8916/changes#packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/capabilities/intent\-resolver\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=63">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/capabilities/intent-resolver.ts) &nbsp;63% 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/8916/changes#packages/core/functions/src/types/types.ts)&nbsp;&nbsp;[packages/core/functions/src/types/types\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/core/functions/src/types/types.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=54">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/core/functions/src/types/types.ts) &nbsp;54% 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/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/mail.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/types/mail\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/mail.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=51">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/mail.ts) &nbsp;51% 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/8916/changes#packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx)&nbsp;&nbsp;[packages/plugins/plugin\-thread/src/components/ThreadComplementary\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/plugin-thread/src/components/ThreadComplementary.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=49">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/plugin-thread/src/components/ThreadComplementary.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/8916/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/8916/changes#packages/plugins/plugin-thread/src/components/ChatContainer.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=47">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/plugin-thread/src/components/ChatContainer.tsx) &nbsp;47% 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/8916/changes#packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-automation/src/components/AutomationPanel/AutomationPanel\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=20">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/components/AutomationPanel/AutomationPanel.tsx) &nbsp;20% 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/8916/changes#packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/components/Mailbox/Mailbox\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=18">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/components/Mailbox/Mailbox.tsx) &nbsp;18% 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/8916/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/8916/changes#packages/plugins/plugin-thread/src/components/testing.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=10">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/plugin-thread/src/components/testing.ts) &nbsp;10% 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/8916/changes#packages/plugins/experimental/plugin-inbox/src/testing/testing.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/testing/testing\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/testing/testing.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=7">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/testing/testing.ts) &nbsp;7% 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/8916/changes#packages/plugins/experimental/plugin-automation/src/translations.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-automation/src/translations\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/translations.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=1">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-automation/src/translations.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/8916/changes#.vscode/tasks.json)&nbsp;&nbsp;[\.vscode/tasks\.json](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#.vscode/tasks.json) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#.vscode/tasks.json) &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/8916/changes#packages/common/log/src/config.ts)&nbsp;&nbsp;[packages/common/log/src/config\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/common/log/src/config.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/common/log/src/config.ts) &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/8916/changes#packages/core/functions/src/types/schema.ts)&nbsp;&nbsp;[packages/core/functions/src/types/schema\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/core/functions/src/types/schema.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/core/functions/src/types/schema.ts) &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/8916/changes#packages/plugins/experimental/plugin-assistant/src/hooks/email.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-assistant/src/hooks/email\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-assistant/src/hooks/email.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-assistant/src/hooks/email.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/8916/changes#packages/plugins/experimental/plugin-inbox/package.json)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/package\.json](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/package.json) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/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/8916/changes#packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/InboxPlugin\.tsx](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/InboxPlugin.tsx) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/InboxPlugin.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/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/types.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/src/types/types\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/types.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/src/types/types.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/8916/changes#packages/plugins/experimental/plugin-inbox/tsconfig.json)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-inbox/tsconfig\.json](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/tsconfig.json) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-inbox/tsconfig.json) &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/8916/changes#packages/plugins/experimental/plugin-script/src/templates/gmail.ts)&nbsp;&nbsp;[packages/plugins/experimental/plugin\-script/src/templates/gmail\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-script/src/templates/gmail.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/plugins/experimental/plugin-script/src/templates/gmail.ts) &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/8916/changes#packages/sdk/schema/src/common/actor.ts)&nbsp;&nbsp;[packages/sdk/schema/src/common/actor\.ts](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/sdk/schema/src/common/actor.ts) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#packages/sdk/schema/src/common/actor.ts) &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/8916/changes#pnpm-lock.yaml)&nbsp;&nbsp;[pnpm\-lock\.yaml](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#pnpm-lock.yaml) | [<img src="https://app.semanticdiff.com/images/progressbar.svg?progress=0">](https://app.semanticdiff.com/gh/dxos/dxos/pull/8916/changes#pnpm-lock.yaml) &nbsp;0% smaller |

</details>
author_association: NONE
user: login: nx-cloud[bot]
created_at: 2025-04-07T22:42:31Z
body: 
View your [CI Pipeline Execution ↗](https://cloud.nx.app/cipes/67f56104b5563e3270ae300c?utm_source=pull-request&utm_medium=comment) for commit 70a6853609d19f464d10bc408c53b5e7f912499b.

| Command | Status | Duration | Result |
|---------|--------|----------:|--------|
| `nx affected --base=95da62412cdf57cb33681d32a95c...` | ✅ Succeeded | 5s | [View ↗](https://cloud.nx.app/runs/ZXlSuaVncS?utm_source=pull-request&utm_medium=comment) |
| `nx affected --base=95da62412cdf57cb33681d32a95c...` | ✅ Succeeded | 3m 16s | [View ↗](https://cloud.nx.app/runs/pkxl76964k?utm_source=pull-request&utm_medium=comment) |
| `nx affected --base=95da62412cdf57cb33681d32a95c...` | ✅ Succeeded | 22s | [View ↗](https://cloud.nx.app/runs/PDz4J74NRh?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-08 17:52:40` UTC

<!-- NX_CLOUD_APP_COMMENT_END -->
author_association: NONE